J Z commited on
Commit
ee98d79
·
verified ·
1 Parent(s): 711a9e1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +54 -143
app.py CHANGED
@@ -182,16 +182,6 @@ async def chat(request: Request):
182
 
183
  logger.info(f"[{_ts()}] [CHAT] User: {user_text}")
184
 
185
- # Reset FLOAT reference to original for new message
186
- if FLOAT_ENABLED:
187
- try:
188
- from float_lipsync import get_lipsync
189
- lipsync = get_lipsync()
190
- if lipsync.ready:
191
- lipsync.reset_reference()
192
- except Exception:
193
- pass
194
-
195
  # 1. LLM (single call)
196
  t1 = _time.time()
197
  from llm import generate_darwin_response
@@ -275,7 +265,7 @@ async def chat_next(request: Request):
275
 
276
  def _generate_chunk(sentence, idx, total):
277
  """Synchronous TTS + FLOAT generation (runs in thread pool)."""
278
- logger.info(f"[{_ts()}] [CHUNK {idx+1}/{total}] ({len(sentence.split())}w {len(sentence)}c) {sentence[:60]}...")
279
  t0 = _time.time()
280
 
281
  # 1. TTS
@@ -305,7 +295,7 @@ def _generate_chunk(sentence, idx, total):
305
  if lipsync.ready:
306
  with _float_lock:
307
  t3 = _time.time()
308
- lp = lipsync.generate(audio_path, chain_frames=True)
309
  t4 = _time.time()
310
  logger.info(f"[{_ts()}] [TIMING] Chunk {idx+1} FLOAT: {t4-t3:.2f}s")
311
  if lp and os.path.exists(lp):
@@ -349,7 +339,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;backgrou
349
  .video-section{position:relative;width:100%;min-height:300px;max-height:45vh;background:#000;border-radius:12px;overflow:hidden;flex-shrink:0;display:flex;align-items:center;justify-content:center}
350
  .video-container{position:relative;width:100%;height:100%}
351
  .video-container video{width:100%;height:100%;object-fit:contain;display:block}
352
- #videoA,#videoB{position:absolute;top:0;left:0;width:100%;height:100%;object-fit:contain;transition:opacity .175s ease}
353
  #videoA{z-index:1;opacity:1}#videoB{z-index:0;opacity:0}
354
  .video-status{position:absolute;bottom:8px;right:12px;font-size:.7rem;color:rgba(255,255,255,.4);background:rgba(0,0,0,.5);padding:2px 8px;border-radius:4px;z-index:10}
355
  .chat-section{flex:1;display:flex;flex-direction:column;min-height:0;background:rgba(15,23,42,.6);border:1px solid rgba(59,130,246,.15);border-radius:12px;overflow:hidden}
@@ -404,91 +394,44 @@ const chatInput=document.getElementById('chatInput'), btnSend=document.getElemen
404
  const btnReset=document.getElementById('btnReset');
405
 
406
  // ======================================================================
407
- // VIDEO: Seamless crossfade with preloading
408
  // ======================================================================
409
  function getActiveVideo(){return currentVideo==='A'?videoA:videoB}
410
  function getInactiveVideo(){return currentVideo==='A'?videoB:videoA}
411
 
412
- // Preload a video into the inactive element (doesn't play yet)
413
- function preloadVideo(url){
414
- return new Promise((resolve)=>{
415
- const next=getInactiveVideo();
416
- next.muted=true;
417
- next.preload='auto';
418
- next.src=url;
419
- next.load();
420
- next.oncanplaythrough=function(){
421
- next.oncanplaythrough=null;
422
- console.log('['+new Date().toISOString().substr(11,12)+'] preloaded: '+url.split('/').pop());
423
- resolve(true);
424
- };
425
- next.onerror=function(){
426
- next.onerror=null;
427
- console.warn('[VIDEO] Preload failed:', url);
428
- resolve(false);
429
- };
430
- // Timeout fallback — don't wait forever
431
- setTimeout(()=>resolve(true), 3000);
432
- });
433
- }
434
-
435
- // Swap: the inactive element is already loaded, just play and crossfade
436
- function swapToPreloaded(){
437
  return new Promise((resolve)=>{
438
- const prev=getActiveVideo();
439
- const next=getInactiveVideo();
440
-
441
- // If nothing preloaded, just resolve
442
- if(!next.src || next.src===''){
443
- resolve(); return;
444
- }
445
-
446
- next.style.zIndex='2';
447
- prev.style.zIndex='1';
448
- next.style.opacity='0';
449
-
450
- next.play().then(()=>{
451
- // Instant opacity swap (no slow fade — speed over beauty)
452
- next.style.opacity='1';
453
- prev.style.opacity='0';
454
-
455
- // Clean up old video after brief overlap
456
- setTimeout(()=>{
457
- prev.pause();
458
- prev.removeAttribute('src');
459
- prev.load(); // reset
460
- prev.style.zIndex='0';
461
- next.style.zIndex='1';
462
- currentVideo=currentVideo==='A'?'B':'A';
463
- resolve();
464
- }, 50);
465
- }).catch(()=>{
466
- next.muted=true;
467
  next.play().then(()=>{
468
  next.style.opacity='1';
469
- prev.style.opacity='0';
470
  setTimeout(()=>{
471
- prev.pause();
472
- prev.removeAttribute('src');
473
- prev.load();
474
- prev.style.zIndex='0';
475
- next.style.zIndex='1';
476
  currentVideo=currentVideo==='A'?'B':'A';
 
477
  resolve();
478
- }, 50);
479
- }).catch(()=>{ resolve(); });
480
- });
481
- });
482
- }
483
-
484
- // Full crossfade (load + play) for idle videos and first chunk
485
- function crossfadeTo(url, muted){
486
- return new Promise(async (resolve)=>{
487
- await preloadVideo(url);
488
- const next=getInactiveVideo();
489
- next.muted=(muted!==false);
490
- await swapToPreloaded();
491
- resolve();
 
 
492
  });
493
  }
494
 
@@ -511,11 +454,12 @@ videoA.addEventListener('ended',()=>{ if(currentVideo==='A'&&!isProcessing) play
511
  videoB.addEventListener('ended',()=>{ if(currentVideo==='B'&&!isProcessing) playNextIdle(); });
512
 
513
  // ======================================================================
514
- // VIDEO: Wait for current video to end
515
  // ======================================================================
516
  function waitForVideoEnd(){
517
  return new Promise((resolve)=>{
518
  const active=getActiveVideo();
 
519
  if(active.ended || active.paused || !active.src){
520
  resolve(); return;
521
  }
@@ -584,74 +528,41 @@ async function sendMessage(){
584
  return await r.json();
585
  }
586
 
587
- // Step 2: Fetch and play chunks with preloading
588
- let nextChunk = await fetchChunk(sessionId);
589
 
590
  for(let i=0; i<totalChunks; i++){
591
- const chunk = nextChunk;
592
- if(!chunk || chunk.done) break;
593
 
594
- console.log('['+new Date().toISOString().substr(11,12)+'] chunk '+(i+1)+' ready');
595
- videoStatus.textContent='playing '+(i+1)+'/'+totalChunks;
 
596
 
597
- // Start fetching next chunk data from server (TTS+FLOAT generation)
598
- let nextChunkPromise = (i+1 < totalChunks) ? fetchChunk(sessionId) : null;
 
 
 
599
 
600
- // Play audio and get a promise for when it ends
601
- let audioDonePromise = null;
602
  if(chunk.audio_url){
603
  const audio=new Audio(chunk.audio_url);
604
- audioDonePromise = new Promise((resolve)=>{
605
  audio.onended=resolve;
606
  audio.onerror=resolve;
607
- // Fallback timeout in case ended event doesn't fire
608
- const fallback = setTimeout(resolve, (chunk.audio_duration||5)*1000 + 500);
609
- audio.onended=()=>{ clearTimeout(fallback); resolve(); };
610
- audio.play().catch(()=>{ clearTimeout(fallback); resolve(); });
611
  });
612
  }
613
 
 
614
  if(chunk.lipsync_video_url){
615
- // --- HAS LIPSYNC VIDEO ---
616
- if(i===0){
617
- await crossfadeTo(chunk.lipsync_video_url, true);
618
- } else {
619
- await swapToPreloaded();
620
- }
621
- console.log('['+new Date().toISOString().substr(11,12)+'] chunk '+(i+1)+' video playing');
622
-
623
- // While video plays, wait for next chunk data and preload its video
624
- if(nextChunkPromise){
625
- nextChunkPromise.then((nc)=>{
626
- if(nc && !nc.done && nc.lipsync_video_url){
627
- console.log('['+new Date().toISOString().substr(11,12)+'] preloading chunk '+(i+2));
628
- preloadVideo(nc.lipsync_video_url);
629
- }
630
- });
631
- }
632
-
633
  await waitForVideoEnd();
634
- console.log('['+new Date().toISOString().substr(11,12)+'] chunk '+(i+1)+' video ended');
635
-
636
- } else {
637
- // --- AUDIO ONLY (no lipsync) --- idle video keeps playing
638
- isProcessing=false; // let idle loop continue
639
- console.log('['+new Date().toISOString().substr(11,12)+'] chunk '+(i+1)+' audio only');
640
- if(audioDonePromise){
641
- await audioDonePromise;
642
- } else if(chunk.audio_duration > 0){
643
- await new Promise(r=>setTimeout(r, chunk.audio_duration*1000));
644
- } else {
645
- await new Promise(r=>setTimeout(r, 2000));
646
- }
647
- isProcessing=true; // re-lock for next chunk
648
- }
649
-
650
- // Get the next chunk (should already be resolved)
651
- if(nextChunkPromise){
652
- nextChunk = await nextChunkPromise;
653
- } else {
654
- nextChunk = null;
655
  }
656
  }
657
 
 
182
 
183
  logger.info(f"[{_ts()}] [CHAT] User: {user_text}")
184
 
 
 
 
 
 
 
 
 
 
 
185
  # 1. LLM (single call)
186
  t1 = _time.time()
187
  from llm import generate_darwin_response
 
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
 
295
  if lipsync.ready:
296
  with _float_lock:
297
  t3 = _time.time()
298
+ lp = lipsync.generate(audio_path)
299
  t4 = _time.time()
300
  logger.info(f"[{_ts()}] [TIMING] Chunk {idx+1} FLOAT: {t4-t3:.2f}s")
301
  if lp and os.path.exists(lp):
 
339
  .video-section{position:relative;width:100%;min-height:300px;max-height:45vh;background:#000;border-radius:12px;overflow:hidden;flex-shrink:0;display:flex;align-items:center;justify-content:center}
340
  .video-container{position:relative;width:100%;height:100%}
341
  .video-container video{width:100%;height:100%;object-fit:contain;display:block}
342
+ #videoA,#videoB{position:absolute;top:0;left:0;width:100%;height:100%;object-fit:contain;transition:opacity .25s ease}
343
  #videoA{z-index:1;opacity:1}#videoB{z-index:0;opacity:0}
344
  .video-status{position:absolute;bottom:8px;right:12px;font-size:.7rem;color:rgba(255,255,255,.4);background:rgba(0,0,0,.5);padding:2px 8px;border-radius:4px;z-index:10}
345
  .chat-section{flex:1;display:flex;flex-direction:column;min-height:0;background:rgba(15,23,42,.6);border:1px solid rgba(59,130,246,.15);border-radius:12px;overflow:hidden}
 
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