J Z commited on
Commit
30113b2
·
verified ·
1 Parent(s): 1acc8a3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +54 -119
app.py CHANGED
@@ -265,7 +265,7 @@ async def chat_next(request: Request):
265
 
266
  def _generate_chunk(sentence, idx, total):
267
  """Synchronous TTS + FLOAT generation (runs in thread pool)."""
268
- logger.info(f"[{_ts()}] [CHUNK {idx+1}/{total}] ({len(sentence.split())}w {len(sentence)}c) {sentence[:60]}...")
269
  t0 = _time.time()
270
 
271
  # 1. TTS
@@ -394,91 +394,44 @@ const chatInput=document.getElementById('chatInput'), btnSend=document.getElemen
394
  const btnReset=document.getElementById('btnReset');
395
 
396
  // ======================================================================
397
- // VIDEO: Seamless crossfade with preloading
398
  // ======================================================================
399
  function getActiveVideo(){return currentVideo==='A'?videoA:videoB}
400
  function getInactiveVideo(){return currentVideo==='A'?videoB:videoA}
401
 
402
- // Preload a video into the inactive element (doesn't play yet)
403
- function preloadVideo(url){
404
- return new Promise((resolve)=>{
405
- const next=getInactiveVideo();
406
- next.muted=true;
407
- next.preload='auto';
408
- next.src=url;
409
- next.load();
410
- next.oncanplaythrough=function(){
411
- next.oncanplaythrough=null;
412
- console.log('['+new Date().toISOString().substr(11,12)+'] preloaded: '+url.split('/').pop());
413
- resolve(true);
414
- };
415
- next.onerror=function(){
416
- next.onerror=null;
417
- console.warn('[VIDEO] Preload failed:', url);
418
- resolve(false);
419
- };
420
- // Timeout fallback — don't wait forever
421
- setTimeout(()=>resolve(true), 3000);
422
- });
423
- }
424
-
425
- // Swap: the inactive element is already loaded, just play and crossfade
426
- function swapToPreloaded(){
427
  return new Promise((resolve)=>{
428
- const prev=getActiveVideo();
429
- const next=getInactiveVideo();
430
-
431
- // If nothing preloaded, just resolve
432
- if(!next.src || next.src===''){
433
- resolve(); return;
434
- }
435
-
436
- next.style.zIndex='2';
437
- prev.style.zIndex='1';
438
- next.style.opacity='0';
439
-
440
- next.play().then(()=>{
441
- // Instant opacity swap (no slow fade — speed over beauty)
442
- next.style.opacity='1';
443
- prev.style.opacity='0';
444
-
445
- // Clean up old video after brief overlap
446
- setTimeout(()=>{
447
- prev.pause();
448
- prev.removeAttribute('src');
449
- prev.load(); // reset
450
- prev.style.zIndex='0';
451
- next.style.zIndex='1';
452
- currentVideo=currentVideo==='A'?'B':'A';
453
- resolve();
454
- }, 50);
455
- }).catch(()=>{
456
- next.muted=true;
457
  next.play().then(()=>{
458
  next.style.opacity='1';
459
- prev.style.opacity='0';
460
  setTimeout(()=>{
461
- prev.pause();
462
- prev.removeAttribute('src');
463
- prev.load();
464
- prev.style.zIndex='0';
465
- next.style.zIndex='1';
466
  currentVideo=currentVideo==='A'?'B':'A';
 
467
  resolve();
468
- }, 50);
469
- }).catch(()=>{ resolve(); });
470
- });
471
- });
472
- }
473
-
474
- // Full crossfade (load + play) for idle videos and first chunk
475
- function crossfadeTo(url, muted){
476
- return new Promise(async (resolve)=>{
477
- await preloadVideo(url);
478
- const next=getInactiveVideo();
479
- next.muted=(muted!==false);
480
- await swapToPreloaded();
481
- resolve();
 
 
482
  });
483
  }
484
 
@@ -501,11 +454,12 @@ videoA.addEventListener('ended',()=>{ if(currentVideo==='A'&&!isProcessing) play
501
  videoB.addEventListener('ended',()=>{ if(currentVideo==='B'&&!isProcessing) playNextIdle(); });
502
 
503
  // ======================================================================
504
- // VIDEO: Wait for current video to end
505
  // ======================================================================
506
  function waitForVideoEnd(){
507
  return new Promise((resolve)=>{
508
  const active=getActiveVideo();
 
509
  if(active.ended || active.paused || !active.src){
510
  resolve(); return;
511
  }
@@ -574,60 +528,41 @@ async function sendMessage(){
574
  return await r.json();
575
  }
576
 
577
- // Step 2: Fetch and play chunks with preloading
578
- let nextChunk = await fetchChunk(sessionId);
579
 
580
  for(let i=0; i<totalChunks; i++){
581
- const chunk = nextChunk;
582
- if(!chunk || chunk.done) break;
583
 
584
- console.log('['+new Date().toISOString().substr(11,12)+'] chunk '+(i+1)+' ready');
585
- videoStatus.textContent='playing '+(i+1)+'/'+totalChunks;
 
586
 
587
- // Start fetching next chunk data from server (TTS+FLOAT generation)
588
- let nextChunkPromise = (i+1 < totalChunks) ? fetchChunk(sessionId) : null;
 
 
 
589
 
590
  // Play audio
 
591
  if(chunk.audio_url){
592
  const audio=new Audio(chunk.audio_url);
593
- audio.play().catch(()=>{});
 
 
 
 
594
  }
595
 
 
596
  if(chunk.lipsync_video_url){
597
- // First chunk: full load + crossfade
598
- // Subsequent: inactive element already has preloaded video, just swap
599
- if(i===0){
600
- await crossfadeTo(chunk.lipsync_video_url, true);
601
- } else {
602
- // Preloaded from previous iteration — just swap
603
- await swapToPreloaded();
604
- }
605
- console.log('['+new Date().toISOString().substr(11,12)+'] chunk '+(i+1)+' playing');
606
-
607
- // While video plays, wait for next chunk data and preload its video
608
- if(nextChunkPromise){
609
- nextChunkPromise.then((nc)=>{
610
- if(nc && !nc.done && nc.lipsync_video_url){
611
- console.log('['+new Date().toISOString().substr(11,12)+'] preloading chunk '+(i+2));
612
- preloadVideo(nc.lipsync_video_url);
613
- }
614
- });
615
- }
616
-
617
- // Wait for current video to finish
618
  await waitForVideoEnd();
619
- console.log('['+new Date().toISOString().substr(11,12)+'] chunk '+(i+1)+' ended');
620
-
621
- } else if(chunk.audio_duration > 0){
622
- // No video, just wait for audio
623
- await new Promise(r=>setTimeout(r, chunk.audio_duration*1000));
624
- }
625
-
626
- // Get the next chunk (should already be resolved from the promise above)
627
- if(nextChunkPromise){
628
- nextChunk = await nextChunkPromise;
629
- } else {
630
- nextChunk = null;
631
  }
632
  }
633
 
 
265
 
266
  def _generate_chunk(sentence, idx, total):
267
  """Synchronous TTS + FLOAT generation (runs in thread pool)."""
268
+ logger.info(f"[{_ts()}] [CHUNK {idx+1}/{total}] {sentence[:60]}...")
269
  t0 = _time.time()
270
 
271
  # 1. TTS
 
394
  const btnReset=document.getElementById('btnReset');
395
 
396
  // ======================================================================
397
+ // VIDEO: Crossfade
398
  // ======================================================================
399
  function getActiveVideo(){return currentVideo==='A'?videoA:videoB}
400
  function getInactiveVideo(){return currentVideo==='A'?videoB:videoA}
401
 
402
+ function crossfadeTo(url, muted){
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  return new Promise((resolve)=>{
404
+ if(isTransitioning){ resolve(); return; }
405
+ isTransitioning=true;
406
+ const prev=getActiveVideo(), next=getInactiveVideo();
407
+ next.muted=(muted!==false);
408
+ next.src=url; next.load();
409
+ next.onloadeddata=function(){
410
+ next.style.zIndex='2'; prev.style.zIndex='1'; next.style.opacity='0';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
  next.play().then(()=>{
412
  next.style.opacity='1';
 
413
  setTimeout(()=>{
414
+ prev.style.opacity='0'; prev.pause(); prev.src='';
415
+ prev.style.zIndex='0'; next.style.zIndex='1';
 
 
 
416
  currentVideo=currentVideo==='A'?'B':'A';
417
+ isTransitioning=false;
418
  resolve();
419
+ },300);
420
+ }).catch(()=>{
421
+ next.muted=true;
422
+ next.play().then(()=>{
423
+ next.style.opacity='1';
424
+ setTimeout(()=>{
425
+ prev.style.opacity='0'; prev.pause(); prev.src='';
426
+ prev.style.zIndex='0'; next.style.zIndex='1';
427
+ currentVideo=currentVideo==='A'?'B':'A';
428
+ isTransitioning=false;
429
+ resolve();
430
+ },300);
431
+ }).catch(()=>{ isTransitioning=false; resolve(); });
432
+ });
433
+ };
434
+ next.onerror=function(){ isTransitioning=false; resolve(); };
435
  });
436
  }
437
 
 
454
  videoB.addEventListener('ended',()=>{ if(currentVideo==='B'&&!isProcessing) playNextIdle(); });
455
 
456
  // ======================================================================
457
+ // VIDEO: Wait for current lipsync video to end
458
  // ======================================================================
459
  function waitForVideoEnd(){
460
  return new Promise((resolve)=>{
461
  const active=getActiveVideo();
462
+ // If video is very short or already ended, resolve quickly
463
  if(active.ended || active.paused || !active.src){
464
  resolve(); return;
465
  }
 
528
  return await r.json();
529
  }
530
 
531
+ // Step 2: Prefetch first chunk
532
+ let nextChunkPromise=fetchChunk(sessionId);
533
 
534
  for(let i=0; i<totalChunks; i++){
535
+ videoStatus.textContent='waiting '+(i+1)+'/'+totalChunks+'...';
 
536
 
537
+ // Wait for the current chunk to be ready
538
+ const chunk=await nextChunkPromise;
539
+ if(!chunk || chunk.done) break;
540
 
541
+ // Immediately start prefetching the NEXT chunk (runs in parallel with playback)
542
+ if(i+1 < totalChunks){
543
+ videoStatus.textContent='speaking '+(i+1)+'/'+totalChunks+' (prefetching '+(i+2)+')';
544
+ nextChunkPromise=fetchChunk(sessionId);
545
+ }
546
 
547
  // Play audio
548
+ let audioPromise=null;
549
  if(chunk.audio_url){
550
  const audio=new Audio(chunk.audio_url);
551
+ audioPromise=new Promise((resolve)=>{
552
+ audio.onended=resolve;
553
+ audio.onerror=resolve;
554
+ audio.play().catch(()=>resolve());
555
+ });
556
  }
557
 
558
+ // Play lipsync video
559
  if(chunk.lipsync_video_url){
560
+ await crossfadeTo(chunk.lipsync_video_url, true);
561
+ // Wait for this lipsync video to finish before next chunk
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
562
  await waitForVideoEnd();
563
+ } else if(audioPromise){
564
+ // No lipsync, just wait for audio to finish
565
+ await audioPromise;
 
 
 
 
 
 
 
 
 
566
  }
567
  }
568