Update api/ltx_server.py
Browse files- api/ltx_server.py +56 -88
api/ltx_server.py
CHANGED
|
@@ -438,107 +438,75 @@ class VideoService:
|
|
| 438 |
|
| 439 |
|
| 440 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 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 |
-
|
| 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 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 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 |
-
#
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 489 |
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
|
|
|
|
|
|
| 495 |
)
|
| 496 |
-
|
|
|
|
| 497 |
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
)
|
| 503 |
-
|
|
|
|
|
|
|
| 504 |
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
f'-
|
| 510 |
-
f'-
|
| 511 |
)
|
| 512 |
-
|
| 513 |
-
|
| 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 |
-
|
| 535 |
-
|
| 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 |
|
| 544 |
with tempfile.NamedTemporaryFile("w", delete=False, suffix=".txt") as f:
|
|
|
|
| 438 |
|
| 439 |
|
| 440 |
|
| 441 |
+
def _gerar_lista_com_transicoes(self, pasta: str, video_paths: list[str], crossfade_frames: int = 8) -> list[str]:
|
| 442 |
+
"""
|
| 443 |
+
Gera uma nova lista de vídeos aplicando transições suaves (blend frame a frame)
|
| 444 |
+
seguindo exatamente a lógica linear de Carlos.
|
| 445 |
+
"""
|
| 446 |
+
import os, subprocess, shutil
|
| 447 |
|
| 448 |
+
poda = crossfade_frames
|
| 449 |
+
total_partes = len(video_paths)
|
| 450 |
+
video_anterior_fade_fim = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 451 |
nova_lista = []
|
|
|
|
|
|
|
| 452 |
|
| 453 |
+
print(f"[DEBUG] Iniciando pipeline com {total_partes} vídeos e {poda} frames de crossfade")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 454 |
|
| 455 |
+
for i in range(total_partes):
|
| 456 |
+
base = os.path.splitext(os.path.basename(video_paths[i]))[0]
|
| 457 |
+
video_clone = os.path.join(pasta, f"{base}_clone_{i}.mp4")
|
| 458 |
+
shutil.copy(video_paths[i], video_clone)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 459 |
|
| 460 |
+
# --- PODA ---
|
| 461 |
+
video_podado = os.path.join(pasta, f"{base}_podado_{i}.mp4")
|
| 462 |
+
trim_ini = poda if i > 0 else 0
|
| 463 |
+
trim_fim = -poda if i < total_partes - 1 else 0
|
| 464 |
+
filtro_trim = f"trim=start_frame={trim_ini}"
|
| 465 |
+
if trim_fim != 0:
|
| 466 |
+
filtro_trim += f":end_frame=nb_frames+{trim_fim}"
|
| 467 |
+
cmd_poda = f'ffmpeg -y -hide_banner -loglevel error -i "{video_clone}" -vf "{filtro_trim},setpts=PTS-STARTPTS" -an "{video_podado}"'
|
| 468 |
+
subprocess.run(cmd_poda, shell=True, check=True)
|
| 469 |
+
nova_lista.append(video_podado)
|
| 470 |
+
print(f"[DEBUG] [{i}] Podado -> {video_podado}")
|
| 471 |
|
| 472 |
+
# --- FADE_INI ---
|
| 473 |
+
video_fade_ini = None
|
| 474 |
+
if i > 0:
|
| 475 |
+
video_fade_ini = os.path.join(pasta, f"{base}_fade_ini_{i}.mp4")
|
| 476 |
+
cmd_ini = (
|
| 477 |
+
f'ffmpeg -y -hide_banner -loglevel error -i "{video_clone}" '
|
| 478 |
+
f'-vf "trim=end_frame={poda},setpts=PTS-STARTPTS" -an "{video_fade_ini}"'
|
| 479 |
)
|
| 480 |
+
subprocess.run(cmd_ini, shell=True, check=True)
|
| 481 |
+
print(f"[DEBUG] Fade_ini ({poda} frames) -> {video_fade_ini}")
|
| 482 |
|
| 483 |
+
# --- TRANSIÇÃO ---
|
| 484 |
+
if video_anterior_fade_fim and video_fade_ini:
|
| 485 |
+
transicao = os.path.join(pasta, f"transicao_{i-1}_{i}.mp4")
|
| 486 |
+
cmd_blend = (
|
| 487 |
+
f'ffmpeg -y -hide_banner -loglevel error '
|
| 488 |
+
f'-i "{video_anterior_fade_fim}" -i "{video_fade_ini}" '
|
| 489 |
+
f'-filter_complex "[0:v][1:v]blend=all_expr=\'A*(1-T/{poda})+B*(T/{poda})\',format=yuv420p" '
|
| 490 |
+
f'-frames:v {poda} "{transicao}"'
|
| 491 |
)
|
| 492 |
+
subprocess.run(cmd_blend, shell=True, check=True)
|
| 493 |
+
nova_lista.append(transicao)
|
| 494 |
+
print(f"[DEBUG] Transição criada -> {transicao}")
|
| 495 |
|
| 496 |
+
# --- FADE_FIM ---
|
| 497 |
+
if i < total_partes - 1:
|
| 498 |
+
video_fade_fim = os.path.join(pasta, f"{base}_fade_fim_{i}.mp4")
|
| 499 |
+
cmd_fim = (
|
| 500 |
+
f'ffmpeg -y -hide_banner -loglevel error -i "{video_clone}" '
|
| 501 |
+
f'-vf "trim=start_frame=nb_frames-{poda},setpts=PTS-STARTPTS" -an "{video_fade_fim}"'
|
| 502 |
)
|
| 503 |
+
subprocess.run(cmd_fim, shell=True, check=True)
|
| 504 |
+
video_anterior_fade_fim = video_fade_fim
|
| 505 |
+
print(f"[DEBUG] Fade_fim preparado -> {video_fade_fim}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 506 |
|
| 507 |
+
print(f"[DEBUG] Nova lista finalizada com {len(nova_lista)} partes.")
|
| 508 |
+
return nova_lista
|
|
|
|
|
|
|
|
|
|
| 509 |
|
|
|
|
|
|
|
| 510 |
def _concat_mp4s_no_reencode(self, lista_mp4, output_path):
|
| 511 |
|
| 512 |
with tempfile.NamedTemporaryFile("w", delete=False, suffix=".txt") as f:
|