J Z commited on
Update app.py
Browse files
app.py
CHANGED
|
@@ -315,6 +315,7 @@ HTML_PAGE = """\
|
|
| 315 |
.video-section {
|
| 316 |
position: relative;
|
| 317 |
width: 100%;
|
|
|
|
| 318 |
max-height: 45vh;
|
| 319 |
background: #000;
|
| 320 |
border-radius: 12px;
|
|
@@ -504,8 +505,8 @@ HTML_PAGE = """\
|
|
| 504 |
<!-- VIDEO PLAYER with crossfade -->
|
| 505 |
<div class="video-section">
|
| 506 |
<div class="video-container">
|
| 507 |
-
<video id="videoA" muted playsinline></video>
|
| 508 |
-
<video id="videoB" muted playsinline></video>
|
| 509 |
</div>
|
| 510 |
<div class="video-status" id="videoStatus">Loading...</div>
|
| 511 |
</div>
|
|
@@ -547,13 +548,15 @@ const btnReset = document.getElementById('btnReset');
|
|
| 547 |
function getActiveVideo() { return currentVideo === 'A' ? videoA : videoB; }
|
| 548 |
function getInactiveVideo() { return currentVideo === 'A' ? videoB : videoA; }
|
| 549 |
|
| 550 |
-
function crossfadeTo(videoUrl) {
|
| 551 |
if (isTransitioning) return;
|
| 552 |
isTransitioning = true;
|
| 553 |
|
| 554 |
const prev = getActiveVideo();
|
| 555 |
const next = getInactiveVideo();
|
| 556 |
|
|
|
|
|
|
|
| 557 |
next.src = videoUrl;
|
| 558 |
next.load();
|
| 559 |
|
|
@@ -578,6 +581,23 @@ function crossfadeTo(videoUrl) {
|
|
| 578 |
}).catch(err => {
|
| 579 |
console.error('[VIDEO] Play failed:', err);
|
| 580 |
isTransitioning = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 581 |
});
|
| 582 |
};
|
| 583 |
|
|
@@ -602,7 +622,7 @@ function playNextIdle() {
|
|
| 602 |
if (!clip) return;
|
| 603 |
lastPlayed = clip.filename;
|
| 604 |
videoStatus.textContent = 'idle: ' + clip.name;
|
| 605 |
-
crossfadeTo('/api/stream/' + encodeURIComponent(clip.filename));
|
| 606 |
}
|
| 607 |
|
| 608 |
// When active video ends → play next idle (unless processing)
|
|
@@ -715,7 +735,7 @@ async function sendMessage() {
|
|
| 715 |
// Play lipsync video if available (Phase 2)
|
| 716 |
if (data.lipsync_video_url) {
|
| 717 |
videoStatus.textContent = 'speaking...';
|
| 718 |
-
crossfadeTo(data.lipsync_video_url);
|
| 719 |
// After lipsync ends, resume idle
|
| 720 |
const activeVid = getInactiveVideo(); // will become active after crossfade
|
| 721 |
const onLipsyncEnd = () => {
|
|
|
|
| 315 |
.video-section {
|
| 316 |
position: relative;
|
| 317 |
width: 100%;
|
| 318 |
+
min-height: 300px;
|
| 319 |
max-height: 45vh;
|
| 320 |
background: #000;
|
| 321 |
border-radius: 12px;
|
|
|
|
| 505 |
<!-- VIDEO PLAYER with crossfade -->
|
| 506 |
<div class="video-section">
|
| 507 |
<div class="video-container">
|
| 508 |
+
<video id="videoA" muted autoplay playsinline></video>
|
| 509 |
+
<video id="videoB" muted autoplay playsinline></video>
|
| 510 |
</div>
|
| 511 |
<div class="video-status" id="videoStatus">Loading...</div>
|
| 512 |
</div>
|
|
|
|
| 548 |
function getActiveVideo() { return currentVideo === 'A' ? videoA : videoB; }
|
| 549 |
function getInactiveVideo() { return currentVideo === 'A' ? videoB : videoA; }
|
| 550 |
|
| 551 |
+
function crossfadeTo(videoUrl, isMuted) {
|
| 552 |
if (isTransitioning) return;
|
| 553 |
isTransitioning = true;
|
| 554 |
|
| 555 |
const prev = getActiveVideo();
|
| 556 |
const next = getInactiveVideo();
|
| 557 |
|
| 558 |
+
// Set muted state (idle=muted, lipsync=unmuted if audio separate)
|
| 559 |
+
next.muted = (isMuted !== false);
|
| 560 |
next.src = videoUrl;
|
| 561 |
next.load();
|
| 562 |
|
|
|
|
| 581 |
}).catch(err => {
|
| 582 |
console.error('[VIDEO] Play failed:', err);
|
| 583 |
isTransitioning = false;
|
| 584 |
+
// Retry once on autoplay failure
|
| 585 |
+
next.muted = true;
|
| 586 |
+
next.play().then(() => {
|
| 587 |
+
next.style.opacity = '1';
|
| 588 |
+
setTimeout(() => {
|
| 589 |
+
prev.style.opacity = '0';
|
| 590 |
+
prev.pause();
|
| 591 |
+
prev.src = '';
|
| 592 |
+
prev.style.zIndex = '0';
|
| 593 |
+
next.style.zIndex = '1';
|
| 594 |
+
currentVideo = currentVideo === 'A' ? 'B' : 'A';
|
| 595 |
+
isTransitioning = false;
|
| 596 |
+
}, 300);
|
| 597 |
+
}).catch(e => {
|
| 598 |
+
console.error('[VIDEO] Muted retry also failed:', e);
|
| 599 |
+
isTransitioning = false;
|
| 600 |
+
});
|
| 601 |
});
|
| 602 |
};
|
| 603 |
|
|
|
|
| 622 |
if (!clip) return;
|
| 623 |
lastPlayed = clip.filename;
|
| 624 |
videoStatus.textContent = 'idle: ' + clip.name;
|
| 625 |
+
crossfadeTo('/api/stream/' + encodeURIComponent(clip.filename), true);
|
| 626 |
}
|
| 627 |
|
| 628 |
// When active video ends → play next idle (unless processing)
|
|
|
|
| 735 |
// Play lipsync video if available (Phase 2)
|
| 736 |
if (data.lipsync_video_url) {
|
| 737 |
videoStatus.textContent = 'speaking...';
|
| 738 |
+
crossfadeTo(data.lipsync_video_url, true);
|
| 739 |
// After lipsync ends, resume idle
|
| 740 |
const activeVid = getInactiveVideo(); // will become active after crossfade
|
| 741 |
const onLipsyncEnd = () => {
|