EuuIia commited on
Commit
56b020c
·
verified ·
1 Parent(s): 5f7bd3c

Update api/ltx_server.py

Browse files
Files changed (1) hide show
  1. api/ltx_server.py +84 -116
api/ltx_server.py CHANGED
@@ -438,152 +438,120 @@ class VideoService:
438
 
439
 
440
 
441
- def _get_video_info(self, path: str) -> dict:
442
- """Retorna metadados essenciais do vídeo (DNA)."""
443
- cmd = f"ffprobe -v error -select_streams v:0 -show_entries " \
444
- f"stream=codec_name,width,height,avg_frame_rate,duration -of json {shlex.quote(path)}"
445
- try:
446
- result = subprocess.run(shlex.split(cmd), capture_output=True, text=True, check=True)
447
- data = json.loads(result.stdout)
448
- stream = data.get("streams", [{}])[0]
449
- fps_str = stream.get("avg_frame_rate", "0/1")
450
- try:
451
- num, den = map(float, fps_str.split("/"))
452
- fps = num / den if den != 0 else 0
453
- except Exception:
454
- fps = 0
455
- info = {
456
- "arquivo": os.path.basename(path),
457
- "codec": stream.get("codec_name"),
458
- "resolução": f"{stream.get('width')}x{stream.get('height')}",
459
- "fps": round(fps, 2),
460
- "duração": float(stream.get("duration", 0))
461
- }
462
- print(f"🧬 [DNA] {info}")
463
- return info
464
- except subprocess.CalledProcessError:
465
- print(f"⚠️ Falha ao ler metadados de {path}")
466
- return {}
467
-
468
 
469
  def _gerar_lista_com_transicoes(self, pasta: str, video_paths: list[str], crossfade_frames: int = 8) -> list[str]:
470
- """
471
- Gera uma nova lista de vídeos aplicando transições suaves (crossfade de N frames)
472
- entre cada par de vídeos da lista original.
473
- Mantém todos os arquivos na mesma pasta dos vídeos originais.
474
- """
475
 
476
- import os, subprocess
477
-
478
- nova_lista = []
 
 
 
 
 
 
479
 
480
- # 🔹 Filtrar apenas vídeos MP4 válidos
481
- video_paths = [v for v in video_paths if v.endswith(".mp4") and os.path.isfile(v)]
482
- if not video_paths:
483
- raise ValueError("[ERRO] Nenhum vídeo MP4 válido encontrado para processar.")
 
 
484
 
485
- print(f"[DEBUG] Gerando transições entre {len(video_paths)} vídeos...")
 
 
486
 
487
- # 🔹 Iterar pelos vídeos originais
488
  for i, video_atual in enumerate(video_paths):
489
  base_nome = os.path.splitext(os.path.basename(video_atual))[0]
490
- print(f"[DEBUG] Processando vídeo {i+1}/{len(video_paths)}: {base_nome}")
 
 
 
491
 
492
- # --- PRIMEIRO VÍDEO ---
493
  if i == 0:
494
- # 1️⃣ Podar o final (remover últimos frames)
495
- video_podado_fim = os.path.join(pasta, f"{base_nome}_podado_fim.mp4")
496
- cmd_trim = (
497
- f'ffmpeg -y -i "{video_atual}" -vf "trim=0:-{crossfade_frames},setpts=PTS-STARTPTS" '
498
- f'"{video_podado_fim}"'
499
  )
500
- subprocess.run(cmd_trim, shell=True, check=True)
501
- nova_lista.append(video_podado_fim)
502
- print(f"[DEBUG] Adicionado {video_podado_fim}")
503
 
504
- # --- INTERMEDIÁRIO ---
505
  if i < len(video_paths) - 1:
506
- proximo_video = video_paths[i + 1]
507
- prox_nome = os.path.splitext(os.path.basename(proximo_video))[0]
 
508
 
509
- # 2️⃣ Extrair últimos frames do atual
510
- fim_tmp = os.path.join(pasta, f"{base_nome}_fim_{i}.mp4")
511
  cmd_fim = (
512
- f'ffmpeg -y -sseof -{crossfade_frames / 24:.3f} -i "{video_atual}" '
513
- f'-vf "setpts=PTS-STARTPTS" -an "{fim_tmp}"'
514
  )
515
- subprocess.run(cmd_fim, shell=True, check=True)
516
 
517
- # 3️⃣ Extrair primeiros frames do próximo
518
- inicio_tmp = os.path.join(pasta, f"{prox_nome}_inicio_{i+1}.mp4")
519
  cmd_inicio = (
520
- f'ffmpeg -y -i "{proximo_video}" -vf "trim=0:{crossfade_frames},setpts=PTS-STARTPTS" '
521
- f'-an "{inicio_tmp}"'
522
  )
523
- subprocess.run(cmd_inicio, shell=True, check=True)
524
 
525
- # 4️⃣ Gerar transição (crossfade)
526
- transicao_path = os.path.join(pasta, f"transicao_{i+1}.mp4")
527
- cmd_crossfade = (
528
  f'ffmpeg -y -i "{fim_tmp}" -i "{inicio_tmp}" '
529
  f'-filter_complex "xfade=transition=fade:duration={crossfade_frames/24:.3f}:offset=0" '
530
- f'-c:v libx264 -pix_fmt yuv420p "{transicao_path}"'
531
  )
532
- subprocess.run(cmd_crossfade, shell=True, check=True)
 
533
 
534
- nova_lista.append(transicao_path)
535
- print(f"[DEBUG] Transição adicionada: {transicao_path}")
536
-
537
- # --- VÍDEO INTERMEDIÁRIO (remover início e fim) ---
538
  if i + 1 < len(video_paths) - 1:
539
- video_podado_inicio_fim = os.path.join(pasta, f"{prox_nome}_podado_if.mp4")
540
- cmd_trim_if = (
541
- f'ffmpeg -y -i "{proximo_video}" -vf "trim={crossfade_frames}:-{crossfade_frames},setpts=PTS-STARTPTS" '
542
- f'"{video_podado_inicio_fim}"'
 
543
  )
544
- subprocess.run(cmd_trim_if, shell=True, check=True)
545
- nova_lista.append(video_podado_inicio_fim)
546
- print(f"[DEBUG] Adicionado vídeo intermediário podado: {video_podado_inicio_fim}")
547
-
548
- # --- ÚLTIMO VÍDEO ---
549
- elif i + 1 == len(video_paths) - 1:
550
- video_podado_inicio = os.path.join(pasta, f"{prox_nome}_podado_inicio.mp4")
551
- cmd_trim_inicio = (
552
- f'ffmpeg -y -i "{proximo_video}" -vf "trim={crossfade_frames},setpts=PTS-STARTPTS" '
553
- f'"{video_podado_inicio}"'
554
  )
555
- subprocess.run(cmd_trim_inicio, shell=True, check=True)
556
- nova_lista.append(video_podado_inicio)
557
- print(f"[DEBUG] Adicionado último vídeo podado: {video_podado_inicio}")
558
-
559
- print(f"[DEBUG] Nova lista final de {len(nova_lista)} arquivos criada.")
560
- return nova_lista
561
-
562
- def _concat_mp4s_no_reencode(self, mp4_list: List[str], out_path: str):
563
- """
564
- Concatena múltiplos MP4s sem reencode usando o demuxer do ffmpeg.
565
- ATENÇÃO: todos os arquivos precisam ter mesmo codec, fps, resolução etc.
566
- """
567
- if not mp4_list or len(mp4_list) < 2:
568
- raise ValueError("Forneça pelo menos dois arquivos MP4 para concatenar.")
569
 
570
- # Cria lista temporária para o ffmpeg
571
- with tempfile.NamedTemporaryFile("w", delete=False, suffix=".txt") as f:
572
- for mp4 in mp4_list:
573
- f.write(f"file '{os.path.abspath(mp4)}'\n")
574
- list_path = f.name
575
 
576
- cmd = f"ffmpeg -y -f concat -safe 0 -i {list_path} -c copy {out_path}"
577
- print(f"[DEBUG] Concat: {cmd}")
 
 
 
 
 
 
 
 
 
 
578
 
579
- try:
580
- subprocess.check_call(shlex.split(cmd))
581
- finally:
582
- try:
583
- os.remove(list_path)
584
- except Exception:
585
- pass
586
 
 
587
  def generate(
588
  self,
589
  prompt,
@@ -807,7 +775,7 @@ class VideoService:
807
  print(f"[DEBUG] Falha no move; usando tmp como final: {e}")
808
 
809
  final_concat = os.path.join(results_dir, f"concat_fim_{used_seed}.mp4")
810
- partes_mp4_fade = self._gerar_lista_com_transicoes(pasta=results_dir, video_paths=partes_mp4, crossfade_frames=8)
811
  self._concat_mp4s_no_reencode(partes_mp4_fade, final_concat)
812
 
813
 
 
438
 
439
 
440
 
441
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
 
443
  def _gerar_lista_com_transicoes(self, pasta: str, video_paths: list[str], crossfade_frames: int = 8) -> list[str]:
444
+ import os, subprocess, json
 
 
 
 
445
 
446
+ def get_duracao_frames(video):
447
+ """Retorna duração em frames (int)."""
448
+ cmd = f'ffprobe -v error -select_streams v:0 -show_entries stream=nb_frames -of json "{video}"'
449
+ try:
450
+ res = subprocess.check_output(cmd, shell=True)
451
+ info = json.loads(res)
452
+ return int(info["streams"][0]["nb_frames"])
453
+ except Exception:
454
+ return 0
455
 
456
+ def run_safe(cmd, desc):
457
+ print(f"[DEBUG] {desc}: {cmd}")
458
+ result = subprocess.run(cmd, shell=True)
459
+ if result.returncode != 0:
460
+ print(f"[WARN] Falha ao executar: {desc}")
461
+ return result.returncode == 0
462
 
463
+ nova_lista = []
464
+ video_paths = [v for v in video_paths if os.path.isfile(v)]
465
+ print(f"[DEBUG] Iniciando geração de transições ({len(video_paths)} vídeos, fade={crossfade_frames}f)")
466
 
 
467
  for i, video_atual in enumerate(video_paths):
468
  base_nome = os.path.splitext(os.path.basename(video_atual))[0]
469
+ dur_frames = get_duracao_frames(video_atual)
470
+ if dur_frames <= crossfade_frames * 2:
471
+ print(f"[WARN] {video_atual} tem poucos frames ({dur_frames}). Pulando...")
472
+ continue
473
 
474
+ # 1️⃣ PRIMEIRO VÍDEO
475
  if i == 0:
476
+ podado_fim = os.path.join(pasta, f"{base_nome}_fim.mp4")
477
+ cmd = (
478
+ f'ffmpeg -y -i "{video_atual}" -filter:v "trim=0:{dur_frames - crossfade_frames},setpts=PTS-STARTPTS" '
479
+ f'-an "{podado_fim}"'
 
480
  )
481
+ if run_safe(cmd, f"Podando fim de {video_atual}"):
482
+ nova_lista.append(podado_fim)
 
483
 
484
+ # 2️⃣ GERAR TRANSIÇÃO COM O PRÓXIMO
485
  if i < len(video_paths) - 1:
486
+ prox = video_paths[i + 1]
487
+ prox_nome = os.path.splitext(os.path.basename(prox))[0]
488
+ dur_prox = get_duracao_frames(prox)
489
 
490
+ # cortar fim do atual
491
+ fim_tmp = os.path.join(pasta, f"{base_nome}_fimtmp.mp4")
492
  cmd_fim = (
493
+ f'ffmpeg -y -ss {max((dur_frames - crossfade_frames) / 24, 0):.3f} -i "{video_atual}" '
494
+ f'-frames:v {crossfade_frames} -an "{fim_tmp}"'
495
  )
496
+ run_safe(cmd_fim, f"Extraindo fim de {base_nome}")
497
 
498
+ # cortar início do próximo
499
+ inicio_tmp = os.path.join(pasta, f"{prox_nome}_iniciotmp.mp4")
500
  cmd_inicio = (
501
+ f'ffmpeg -y -i "{prox}" -frames:v {crossfade_frames} -an "{inicio_tmp}"'
 
502
  )
503
+ run_safe(cmd_inicio, f"Extraindo início de {prox_nome}")
504
 
505
+ # crossfade (vídeo sem áudio)
506
+ transicao = os.path.join(pasta, f"transicao_{i+1}.mp4")
507
+ cmd_xfade = (
508
  f'ffmpeg -y -i "{fim_tmp}" -i "{inicio_tmp}" '
509
  f'-filter_complex "xfade=transition=fade:duration={crossfade_frames/24:.3f}:offset=0" '
510
+ f'-c:v libx264 -pix_fmt yuv420p -an "{transicao}"'
511
  )
512
+ if run_safe(cmd_xfade, f"Transição {i+1}"):
513
+ nova_lista.append(transicao)
514
 
515
+ # podar início/fim do próximo
 
 
 
516
  if i + 1 < len(video_paths) - 1:
517
+ podado_if = os.path.join(pasta, f"{prox_nome}_if.mp4")
518
+ cmd_if = (
519
+ f'ffmpeg -y -i "{prox}" '
520
+ f'-filter:v "trim={crossfade_frames}:{dur_prox - crossfade_frames},setpts=PTS-STARTPTS" '
521
+ f'-an "{podado_if}"'
522
  )
523
+ if run_safe(cmd_if, f"Podando início/fim de {prox_nome}"):
524
+ nova_lista.append(podado_if)
525
+ else:
526
+ podado_inicio = os.path.join(pasta, f"{prox_nome}_inicio.mp4")
527
+ cmd_ini = (
528
+ f'ffmpeg -y -i "{prox}" '
529
+ f'-filter:v "trim={crossfade_frames}:{dur_prox},setpts=PTS-STARTPTS" -an "{podado_inicio}"'
 
 
 
530
  )
531
+ if run_safe(cmd_ini, f"Podando início de {prox_nome}"):
532
+ nova_lista.append(podado_inicio)
 
 
 
 
 
 
 
 
 
 
 
 
533
 
534
+ # 🔹 Remover vídeos vazios
535
+ nova_lista = [v for v in nova_lista if os.path.isfile(v) and get_duracao_frames(v) > 0]
536
+ print(f"[DEBUG] Lista final para concatenação: {len(nova_lista)} vídeos válidos")
537
+ for v in nova_lista:
538
+ print(f" - {v}")
539
 
540
+ return nova_list
541
+
542
+ def _concat_mp4s_no_reencode(self, lista_mp4, output_path):
543
+ import tempfile, subprocess
544
+ with tempfile.NamedTemporaryFile("w", delete=False, suffix=".txt") as f:
545
+ for mp4 in lista_mp4:
546
+ f.write(f"file '{os.path.abspath(mp4)}'\n")
547
+ lista_path = f.name
548
+
549
+ print(f"[DEBUG] Concatenando {len(lista_mp4)} partes em {output_path}")
550
+ cmd = f'ffmpeg -y -f concat -safe 0 -i "{lista_path}" -c copy "{output_path}"'
551
+ subprocess.run(cmd, shell=True, check=True)
552
 
 
 
 
 
 
 
 
553
 
554
+
555
  def generate(
556
  self,
557
  prompt,
 
775
  print(f"[DEBUG] Falha no move; usando tmp como final: {e}")
776
 
777
  final_concat = os.path.join(results_dir, f"concat_fim_{used_seed}.mp4")
778
+ partes_mp4_fade = self._gerar_lista_com_transicoes(pasta=results_dir, video_paths=partes_mp4, crossfade_frames=9)
779
  self._concat_mp4s_no_reencode(partes_mp4_fade, final_concat)
780
 
781