374 lines
No EOL
17 KiB
PHP
374 lines
No EOL
17 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png">
|
|
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
|
|
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
|
|
<title>Pets at Home</title>
|
|
<meta name="description" content="Create a personalized love song for your pet with our fun and easy tool.">
|
|
<link rel="stylesheet" href="assets/css/style.css">
|
|
|
|
<?php include('opengraph.php'); ?>
|
|
</head>
|
|
<body>
|
|
|
|
<?php include('header.php'); ?>
|
|
|
|
<div class="container">
|
|
|
|
<div class="body-container">
|
|
|
|
<img class="jukebox-banner-mb" src="assets/images/jukebox-banner-mb.png" alt="Jukebox" />
|
|
<img class="jukebox-banner-dt" src="assets/images/jukebox-banner-dt.png" alt="Jukebox" />
|
|
|
|
<div class="title">Your Pet Love Song <br/>will be ready in <span id="timer">30</span></div>
|
|
|
|
<div class="melody-record-container">
|
|
|
|
<img class="melody-record-needle" src="./assets/images/needle.png" alt="record player needle">
|
|
|
|
<img class="melody-record" src="./assets/images/blank-record.png"
|
|
alt="Melody Record" />
|
|
|
|
<svg class="record-text-overlay" viewBox="0 0 100 100">
|
|
<path id="arc-top" d="M 38,50 A 12,12 0 0,1 62,50" fill="transparent" />
|
|
<path id="arc-bottom" d="M 62,50 A 12,12 0 0,1 38,50" fill="transparent" />
|
|
<text class="top-text">
|
|
<textPath href="#arc-top" startOffset="50%" text-anchor="middle">Unleashed</textPath>
|
|
</text>
|
|
<text class="bottom-text">
|
|
<textPath href="#arc-bottom" startOffset="50%" text-anchor="middle">Melody</textPath>
|
|
</text>
|
|
</svg>
|
|
</div>
|
|
|
|
<!-- Audio player (hidden until streaming is ready) -->
|
|
<div id="audio-player-container" class="audio-player-container" style="display: none;">
|
|
<p class="audio-player-label">Listen while your video is being created...</p>
|
|
<audio id="audio-player" controls autoplay preload="none">
|
|
Your browser does not support the audio element.
|
|
</audio>
|
|
<p id="audio-tap-message" class="audio-tap-message" style="display: none;">Tap to play</p>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<?php include('footer.php'); ?>
|
|
|
|
<script>
|
|
(function() {
|
|
|
|
// Configuration
|
|
const API_BASE_URL = 'https://valentinesong.oliver.digital/back';
|
|
const POLL_INTERVAL = 10 * 1000; // 10 seconds
|
|
const MAX_POLL_TIME = 10 * 60 * 1000; // 10 minutes
|
|
const COUNTDOWN_START = 30;
|
|
const COUNTDOWN_DURATION = 36; // seconds
|
|
const TEXT_SWITCH_INTERVAL = 10; // seconds
|
|
const PHRASES = [
|
|
{ top: "Brown Eyed", bottom: "Gerbil" },
|
|
{ top: "Cat it", bottom: "Be Love" },
|
|
{ top: "How Squeaky", bottom: "is Your Love" },
|
|
{ top: "I Just Called", bottom: "To Say I Wuf You" },
|
|
{ top: "I'll Sit Stay", bottom: "And Stand By You" },
|
|
{ top: "Kiss Me", bottom: "Meow" },
|
|
{ top: "My Bark", bottom: "Will Go On" },
|
|
{ top: "Something The", bottom: "Way You Purr" },
|
|
{ top: "Unfor-Pet-able", bottom: "" },
|
|
{ top: "Unleashed", bottom: "Melody" }
|
|
];
|
|
|
|
// State
|
|
let sessionId = null;
|
|
const startTime = Date.now();
|
|
let pollTimer = null;
|
|
let streamingStarted = false;
|
|
let audioPlayer = null;
|
|
let recordTextIndex = 0;
|
|
|
|
// Session Management
|
|
function getSessionId() {
|
|
// Get session_id from URL query parameter
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
let sessionId = urlParams.get('session_id');
|
|
|
|
if (!sessionId) {
|
|
// No session_id in URL, check localStorage
|
|
try {
|
|
const submissionData = localStorage.getItem('submission_data');
|
|
|
|
if (submissionData) {
|
|
const data = JSON.parse(submissionData);
|
|
|
|
if (data.entries && data.entries.length > 0) {
|
|
// Sort entries by timestamp (newest first) and get the latest
|
|
const sortedEntries = data.entries.sort((a, b) => {
|
|
return new Date(b.timestamp) - new Date(a.timestamp);
|
|
});
|
|
|
|
const latestEntry = sortedEntries[0];
|
|
sessionId = latestEntry.session_id;
|
|
console.log(`Loading session from localStorage: ${sessionId} from ${latestEntry.timestamp}`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error reading from localStorage: ${error}`);
|
|
}
|
|
|
|
// If still no session_id, redirect to home
|
|
if (!sessionId) {
|
|
window.location.href = '/';
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return sessionId;
|
|
}
|
|
|
|
// Audio Player Functions
|
|
function showAudioPlayer(taskId) {
|
|
if (streamingStarted) return;
|
|
streamingStarted = true;
|
|
|
|
const container = document.getElementById('audio-player-container');
|
|
audioPlayer = document.getElementById('audio-player');
|
|
const tapMessage = document.getElementById('audio-tap-message');
|
|
|
|
if (!container || !audioPlayer) return;
|
|
|
|
// Set up event listeners before loading
|
|
audioPlayer.addEventListener('play', () => {
|
|
console.log('Audio playing');
|
|
tapMessage.style.display = 'none';
|
|
});
|
|
|
|
audioPlayer.addEventListener('error', (e) => {
|
|
console.error('Audio error:', e.target.error);
|
|
});
|
|
|
|
audioPlayer.addEventListener('stalled', () => {
|
|
console.log('Audio stalled - waiting for data');
|
|
});
|
|
|
|
audioPlayer.addEventListener('waiting', () => {
|
|
console.log('Audio waiting for data');
|
|
});
|
|
|
|
// Set the stream source directly to Sonauto's streaming endpoint
|
|
audioPlayer.src = `https://api-stream.sonauto.ai/stream/${taskId}`;
|
|
audioPlayer.load();
|
|
container.style.display = 'block';
|
|
|
|
// Wait for enough data before attempting autoplay
|
|
audioPlayer.addEventListener('canplay', function onCanPlay() {
|
|
audioPlayer.removeEventListener('canplay', onCanPlay);
|
|
|
|
// Attempt autoplay
|
|
const playPromise = audioPlayer.play();
|
|
|
|
if (playPromise !== undefined) {
|
|
playPromise.then(() => {
|
|
console.log('Audio autoplay started');
|
|
}).catch((error) => {
|
|
// Autoplay blocked - show tap to play message
|
|
console.log('Autoplay blocked:', error.message);
|
|
tapMessage.style.display = 'block';
|
|
|
|
// Tapping container or using native controls will both work
|
|
// The 'play' event listener above will hide the message
|
|
container.addEventListener('click', () => {
|
|
audioPlayer.play().catch(e => console.error('Play failed:', e));
|
|
}, { once: true });
|
|
});
|
|
}
|
|
}, { once: true });
|
|
|
|
// Fallback: if canplay doesn't fire within 5 seconds, show player anyway
|
|
setTimeout(() => {
|
|
if (tapMessage.style.display !== 'block' && audioPlayer.paused) {
|
|
console.log('Canplay timeout - showing tap message');
|
|
tapMessage.style.display = 'block';
|
|
}
|
|
}, 5000);
|
|
}
|
|
|
|
function fadeOutAudio(callback) {
|
|
if (!audioPlayer || audioPlayer.paused) {
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
const fadeInterval = 50; // ms
|
|
const fadeDuration = 1000; // 1 second fade
|
|
const volumeStep = audioPlayer.volume / (fadeDuration / fadeInterval);
|
|
|
|
const fadeTimer = setInterval(() => {
|
|
if (audioPlayer.volume > volumeStep) {
|
|
audioPlayer.volume -= volumeStep;
|
|
} else {
|
|
audioPlayer.volume = 0;
|
|
audioPlayer.pause();
|
|
clearInterval(fadeTimer);
|
|
callback();
|
|
}
|
|
}, fadeInterval);
|
|
}
|
|
|
|
// Polling Functions
|
|
async function pollStatus() {
|
|
// Check if we've exceeded max poll time
|
|
if (Date.now() - startTime > MAX_POLL_TIME) {
|
|
clearInterval(pollTimer);
|
|
showTimeoutError();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/api/submissions/${sessionId}/status`);
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 404) {
|
|
// Session not found, redirect to home
|
|
window.location.href = '/';
|
|
console.log(`polling - no session found in API -> ${sessionId}`)
|
|
return;
|
|
}
|
|
console.error(`Status check failed: ${response.status}`);
|
|
return;
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Check if streaming is ready and show audio player
|
|
if (data.streaming_ready && data.task_id && !streamingStarted) {
|
|
showAudioPlayer(data.task_id);
|
|
}
|
|
|
|
if (data.status === 'success') {
|
|
// Generation complete, fade out audio and redirect to result page
|
|
clearInterval(pollTimer);
|
|
fadeOutAudio(() => {
|
|
window.location.href = `result.php?session_id=${sessionId}`;
|
|
});
|
|
} else if (data.status === 'fail') {
|
|
// Generation failed, fade out audio and redirect to result page
|
|
clearInterval(pollTimer);
|
|
fadeOutAudio(() => {
|
|
window.location.href = `result.php?session_id=${sessionId}`;
|
|
});
|
|
}
|
|
// For 'pending' or 'processing', continue polling
|
|
} catch (error) {
|
|
console.error(`Error polling status: ${error}`);
|
|
// Continue polling on network errors
|
|
}
|
|
}
|
|
|
|
function startPolling() {
|
|
// Start polling immediately and then every 10 seconds
|
|
pollStatus();
|
|
pollTimer = setInterval(pollStatus, POLL_INTERVAL);
|
|
}
|
|
|
|
// UI Functions
|
|
function showTimeoutError() {
|
|
// Find and REMOVE the record container
|
|
const recordContainer = document.querySelector('.melody-record-container');
|
|
if (recordContainer) {
|
|
recordContainer.remove();
|
|
}
|
|
|
|
// Select the existing title elements
|
|
const bodyTitle = document.querySelector('.body-container div.title');
|
|
const bodySubTitle = document.querySelector('.body-container div.sub-title');
|
|
|
|
// Update the text
|
|
if (bodyTitle) {
|
|
bodyTitle.textContent = 'Taking longer than expected...';
|
|
}
|
|
if (bodySubTitle) {
|
|
bodySubTitle.textContent = 'Your song is still being created. Please check back in a few minutes.';
|
|
}
|
|
|
|
// Create the NEW buttons div
|
|
const newDiv = document.createElement('div');
|
|
newDiv.className = 'action-btns';
|
|
newDiv.innerHTML = `
|
|
<button class="make-another-song-btn" onclick="window.location.reload()">
|
|
Check Again
|
|
</button>
|
|
<button class="make-another-song-btn" onclick="window.location.href='/'">
|
|
Create New Song
|
|
</button>
|
|
`;
|
|
|
|
// Place buttons after the sub-title
|
|
if (bodySubTitle) {
|
|
bodySubTitle.after(newDiv);
|
|
} else if (bodyTitle) {
|
|
bodyTitle.after(newDiv);
|
|
}
|
|
}
|
|
|
|
function startCountdown() {
|
|
let count = COUNTDOWN_START;
|
|
const timerElement = document.getElementById("timer");
|
|
|
|
// Total duration = 36 seconds, so each step = 36 / 30 = 1.2 seconds
|
|
// Calculate interval time based on countdown duration
|
|
const intervalTime = (COUNTDOWN_DURATION / COUNTDOWN_START) * 1000; // milliseconds
|
|
|
|
const interval = setInterval(() => {
|
|
timerElement.textContent = `${count}`;
|
|
count--;
|
|
|
|
if (count < 0) {
|
|
clearInterval(interval);
|
|
timerElement.textContent = "";
|
|
}
|
|
}, intervalTime);
|
|
}
|
|
|
|
function startRecordTextAnimation() {
|
|
const intervalMs = TEXT_SWITCH_INTERVAL * 1000;
|
|
const fadeMs = intervalMs * 0.06; // Relative fade timing (e.g., 600ms for 10s)
|
|
|
|
const overlay = document.querySelector('.record-text-overlay');
|
|
const topText = document.querySelector('.top-text textPath');
|
|
const bottomText = document.querySelector('.bottom-text textPath');
|
|
|
|
if (overlay && topText && bottomText) {
|
|
overlay.style.transition = `opacity ${fadeMs}ms ease-in-out`;
|
|
|
|
setInterval(() => {
|
|
overlay.classList.add('text-hidden');
|
|
setTimeout(() => {
|
|
recordTextIndex = (recordTextIndex + 1) % PHRASES.length;
|
|
topText.textContent = PHRASES[recordTextIndex].top;
|
|
bottomText.textContent = PHRASES[recordTextIndex].bottom;
|
|
overlay.classList.remove('text-hidden');
|
|
}, fadeMs + 100); // Wait for fade out before swap
|
|
}, intervalMs);
|
|
}
|
|
}
|
|
|
|
// Main function
|
|
function main() {
|
|
sessionId = getSessionId();
|
|
if (!sessionId) return;
|
|
|
|
startCountdown();
|
|
startRecordTextAnimation();
|
|
startPolling();
|
|
}
|
|
|
|
// Initialize
|
|
main();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|