marcosremar2 Claude Opus 4.5 commited on
Commit
e1b19be
·
1 Parent(s): a46605e

Add smooth crossfade transition and improve video sync on speech end

Browse files

- Add 300ms crossfade transition between speaking and idle video
- Save last speaking frame for smooth visual transition
- Pass end_video_time_ms from Wav2Lip to frontend for precise sync
- Increase JPEG quality from 85 to 100 on Wav2Lip server

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

interface/index_optimized.html CHANGED
@@ -280,14 +280,47 @@
280
  let speakingFrameIndex = 0;
281
  let lastSpeakingRenderTime = 0;
282
 
 
 
 
 
 
 
283
  function startUnifiedRenderLoop() {
284
  if (unifiedRenderLoop) return; // Já está rodando
285
 
286
  function renderFrame() {
287
  if (renderSource === 'idle') {
288
- // Desenha o frame atual do vídeo idle no canvas
289
- if (idleVideo.readyState >= 2) { // HAVE_CURRENT_DATA
290
- ctx.drawImage(idleVideo, 0, 0, canvas.width, canvas.height);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  }
292
  } else if (renderSource === 'speaking') {
293
  // Desenha frames do servidor com timing controlado
@@ -298,6 +331,8 @@
298
  // Se o tempo esperado de fim do áudio já passou, parar de renderizar frames
299
  if (audioExpectedEndTime > 0 && now >= audioExpectedEndTime) {
300
  console.log(`Audio terminou (${now.toFixed(0)} >= ${audioExpectedEndTime.toFixed(0)}), finalizando video`);
 
 
301
  finishPlayback();
302
  return;
303
  }
@@ -334,6 +369,8 @@
334
 
335
  // Verificar se terminou
336
  if (streamDone && frameQueue.length === 0) {
 
 
337
  finishPlayback();
338
  }
339
  }
@@ -354,6 +391,23 @@
354
  }
355
  }
356
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  // Iniciar o render loop quando a página carrega
358
  idleVideo.addEventListener('loadeddata', () => {
359
  // Ajustar tamanho do canvas para match do vídeo
@@ -953,6 +1007,10 @@
953
  audioPlaybackStartTime = 0;
954
  audioExpectedEndTime = 0;
955
 
 
 
 
 
956
  document.getElementById("frames").textContent = "0";
957
  document.getElementById("rendered").textContent = "0";
958
  document.getElementById("buffer").textContent = "0";
@@ -1018,6 +1076,13 @@
1018
  idleVideo.loop = true;
1019
  idleVideo.play().catch(e => console.log("Erro ao retomar idle video:", e));
1020
 
 
 
 
 
 
 
 
1021
  // Transição: speaking → idle (só muda a fonte, canvas continua renderizando)
1022
  renderSource = 'idle';
1023
  console.log("Transição para idle - renderSource mudou, idle video playing");
 
280
  let speakingFrameIndex = 0;
281
  let lastSpeakingRenderTime = 0;
282
 
283
+ // Crossfade transition variables
284
+ let isTransitioning = false;
285
+ let transitionStartTime = 0;
286
+ let lastSpeakingFrame = null; // Guarda o último frame do speaking para crossfade
287
+ const TRANSITION_DURATION_MS = 300; // Duração do crossfade em ms
288
+
289
  function startUnifiedRenderLoop() {
290
  if (unifiedRenderLoop) return; // Já está rodando
291
 
292
  function renderFrame() {
293
  if (renderSource === 'idle') {
294
+ // Se estamos em transição, fazer crossfade
295
+ if (isTransitioning && lastSpeakingFrame) {
296
+ const now = performance.now();
297
+ const elapsed = now - transitionStartTime;
298
+ const progress = Math.min(elapsed / TRANSITION_DURATION_MS, 1);
299
+
300
+ // Desenha o último frame do speaking
301
+ ctx.globalAlpha = 1 - progress;
302
+ ctx.drawImage(lastSpeakingFrame, 0, 0, canvas.width, canvas.height);
303
+
304
+ // Desenha o vídeo idle por cima com alpha crescente
305
+ ctx.globalAlpha = progress;
306
+ if (idleVideo.readyState >= 2) {
307
+ ctx.drawImage(idleVideo, 0, 0, canvas.width, canvas.height);
308
+ }
309
+
310
+ // Restaurar alpha
311
+ ctx.globalAlpha = 1;
312
+
313
+ // Fim da transição
314
+ if (progress >= 1) {
315
+ isTransitioning = false;
316
+ lastSpeakingFrame = null;
317
+ console.log("Crossfade concluído");
318
+ }
319
+ } else {
320
+ // Desenha o frame atual do vídeo idle no canvas
321
+ if (idleVideo.readyState >= 2) { // HAVE_CURRENT_DATA
322
+ ctx.drawImage(idleVideo, 0, 0, canvas.width, canvas.height);
323
+ }
324
  }
325
  } else if (renderSource === 'speaking') {
326
  // Desenha frames do servidor com timing controlado
 
331
  // Se o tempo esperado de fim do áudio já passou, parar de renderizar frames
332
  if (audioExpectedEndTime > 0 && now >= audioExpectedEndTime) {
333
  console.log(`Audio terminou (${now.toFixed(0)} >= ${audioExpectedEndTime.toFixed(0)}), finalizando video`);
334
+ // Salvar último frame para crossfade
335
+ saveLastFrameForTransition();
336
  finishPlayback();
337
  return;
338
  }
 
369
 
370
  // Verificar se terminou
371
  if (streamDone && frameQueue.length === 0) {
372
+ // Salvar último frame para crossfade
373
+ saveLastFrameForTransition();
374
  finishPlayback();
375
  }
376
  }
 
391
  }
392
  }
393
 
394
+ // Salvar o último frame do speaking para crossfade
395
+ function saveLastFrameForTransition() {
396
+ // Criar uma cópia do canvas atual como imagem
397
+ const tempCanvas = document.createElement('canvas');
398
+ tempCanvas.width = canvas.width;
399
+ tempCanvas.height = canvas.height;
400
+ const tempCtx = tempCanvas.getContext('2d');
401
+ tempCtx.drawImage(canvas, 0, 0);
402
+
403
+ // Criar imagem a partir do canvas
404
+ const img = new Image();
405
+ img.src = tempCanvas.toDataURL('image/jpeg', 0.95);
406
+ lastSpeakingFrame = img;
407
+
408
+ console.log("Último frame salvo para crossfade");
409
+ }
410
+
411
  // Iniciar o render loop quando a página carrega
412
  idleVideo.addEventListener('loadeddata', () => {
413
  // Ajustar tamanho do canvas para match do vídeo
 
1007
  audioPlaybackStartTime = 0;
1008
  audioExpectedEndTime = 0;
1009
 
1010
+ // Reset crossfade state
1011
+ isTransitioning = false;
1012
+ lastSpeakingFrame = null;
1013
+
1014
  document.getElementById("frames").textContent = "0";
1015
  document.getElementById("rendered").textContent = "0";
1016
  document.getElementById("buffer").textContent = "0";
 
1076
  idleVideo.loop = true;
1077
  idleVideo.play().catch(e => console.log("Erro ao retomar idle video:", e));
1078
 
1079
+ // Iniciar crossfade se temos o último frame salvo
1080
+ if (lastSpeakingFrame) {
1081
+ isTransitioning = true;
1082
+ transitionStartTime = performance.now();
1083
+ console.log("Iniciando crossfade de 300ms");
1084
+ }
1085
+
1086
  // Transição: speaking → idle (só muda a fonte, canvas continua renderizando)
1087
  renderSource = 'idle';
1088
  console.log("Transição para idle - renderSource mudou, idle video playing");
interface/server_optimized.py CHANGED
@@ -47,6 +47,7 @@ class OptimizedSession:
47
  self.start_time = None
48
  self.total_frames = 0
49
  self.total_bytes = 0
 
50
 
51
  async def send_json(self, msg_type: str, **kwargs):
52
  """Envia mensagem JSON (para status/controle)."""
@@ -233,7 +234,10 @@ class OptimizedSession:
233
  break
234
 
235
  elif msg_type == "done":
236
- print(f"[Optimized] Wav2Lip done: {self.total_frames} frames")
 
 
 
237
  break
238
 
239
  elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
@@ -267,7 +271,8 @@ class OptimizedSession:
267
  "done",
268
  total_frames=self.total_frames,
269
  elapsed_ms=int(elapsed * 1000),
270
- bytes_sent=self.total_bytes
 
271
  )
272
 
273
  def stop(self):
 
47
  self.start_time = None
48
  self.total_frames = 0
49
  self.total_bytes = 0
50
+ self.end_video_time_ms = None # Tempo do vídeo onde parou (para sync)
51
 
52
  async def send_json(self, msg_type: str, **kwargs):
53
  """Envia mensagem JSON (para status/controle)."""
 
234
  break
235
 
236
  elif msg_type == "done":
237
+ # Capturar end_video_time_ms para sincronização
238
+ self.end_video_time_ms = data.get("end_video_time_ms")
239
+ end_frame_idx = data.get("end_frame_idx")
240
+ print(f"[Optimized] Wav2Lip done: {self.total_frames} frames, end_video_time_ms={self.end_video_time_ms}, end_frame_idx={end_frame_idx}")
241
  break
242
 
243
  elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
 
271
  "done",
272
  total_frames=self.total_frames,
273
  elapsed_ms=int(elapsed * 1000),
274
+ bytes_sent=self.total_bytes,
275
+ end_video_time_ms=self.end_video_time_ms
276
  )
277
 
278
  def stop(self):