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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +143 -54
app.py CHANGED
@@ -182,6 +182,16 @@ async def chat(request: Request):
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,7 +275,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}] {sentence[:60]}...")
269
  t0 = _time.time()
270
 
271
  # 1. TTS
@@ -295,7 +305,7 @@ def _generate_chunk(sentence, idx, total):
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,7 +349,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;backgrou
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,44 +404,91 @@ const chatInput=document.getElementById('chatInput'), btnSend=document.getElemen
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,12 +511,11 @@ videoA.addEventListener('ended',()=>{ if(currentVideo==='A'&&!isProcessing) play
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,41 +584,74 @@ async function sendMessage(){
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
 
 
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
 
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
  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
  .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
  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
  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
  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