pahvalentines/waiting.php
michael 9c5b054dcc feat: Add Sonauto streaming audio on waiting page
Enable users to listen to their song while video generation continues
in the background. Backend proxies authenticated Sonauto stream API
since HTML audio elements cannot send auth headers.

- Add streaming_ready_at column to track when stream becomes available
- Enable streaming in Sonauto API request payload
- Handle GENERATING_STREAMING_READY webhook status
- Add /api/stream/{session_id} proxy endpoint using httpx
- Update StatusResponse with streaming_ready and task_id fields
- Add audio player UI with autoplay fallback for mobile
- Fade out audio gracefully before redirect to result page

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 08:23:14 -06:00

290 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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/>is on its way.</div>
<div class="sub-title">Wont take a minute...</div>
<div class="melody-record-container">
<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 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() {
// 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;
}
}
// 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
// State
const startTime = Date.now();
let pollTimer = null;
let streamingStarted = false;
let audioPlayer = null;
function showAudioPlayer(sessionId) {
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 the stream source
audioPlayer.src = `${API_BASE_URL}/api/stream/${sessionId}`;
container.style.display = 'block';
// Attempt autoplay
const playPromise = audioPlayer.play();
if (playPromise !== undefined) {
playPromise.then(() => {
// Autoplay started successfully
console.log('Audio autoplay started');
}).catch((error) => {
// Autoplay blocked - show tap to play message
console.log('Autoplay blocked, showing tap message:', error);
tapMessage.style.display = 'block';
// Allow user to tap anywhere on the player container to start
container.addEventListener('click', function startAudio() {
audioPlayer.play().then(() => {
tapMessage.style.display = 'none';
}).catch(e => console.error('Play failed:', e));
container.removeEventListener('click', startAudio);
}, { once: true });
});
}
}
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);
}
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 && !streamingStarted) {
showAudioPlayer(sessionId);
}
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 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);
}
}
// Start polling immediately and then every 10 seconds
pollStatus();
pollTimer = setInterval(pollStatus, POLL_INTERVAL);
})();
(function() {
const phrases = [
{ top: "Unleashed", bottom: "Melody" },
{ top: "Paws &", bottom: "Play" },
{ top: "Tail-Wagging", bottom: "Tunes" },
{ top: "Furry", bottom: "Forever" }
];
let index = 0;
let textSwitchInterval = 10; // Seconds between switches
let intervalMs = textSwitchInterval * 1000;
let 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(() => {
index = (index + 1) % phrases.length;
topText.textContent = phrases[index].top;
bottomText.textContent = phrases[index].bottom;
overlay.classList.remove('text-hidden');
}, fadeMs + 100); // Wait for fade out before swap
}, intervalMs);
}
})();
</script>
</body>
</html>