Update api/ltx_server.py
Browse files- api/ltx_server.py +75 -111
api/ltx_server.py
CHANGED
|
@@ -436,125 +436,92 @@ class VideoService:
|
|
| 436 |
return chunks
|
| 437 |
|
| 438 |
|
| 439 |
-
|
|
|
|
| 440 |
"""
|
| 441 |
-
|
|
|
|
|
|
|
| 442 |
Args:
|
| 443 |
-
video_paths: lista de caminhos
|
| 444 |
-
crossfade_frames: quantidade de frames para
|
| 445 |
-
|
|
|
|
| 446 |
"""
|
| 447 |
-
|
| 448 |
-
# Apenas copiar se houver 1 ou nenhum vídeo
|
| 449 |
-
if video_paths:
|
| 450 |
-
subprocess.run(f"cp '{video_paths[0]}' '{output_path}'", shell=True, check=True)
|
| 451 |
-
return output_path
|
| 452 |
-
|
| 453 |
-
# Lista temporária de vídeos intermediários
|
| 454 |
-
temp_videos = []
|
| 455 |
|
| 456 |
-
|
| 457 |
-
|
|
|
|
| 458 |
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
[1:v]trim=0:{crossfade_frames},setpts=PTS-STARTPTS[v_next_fade];
|
| 473 |
-
[1:v]trim={crossfade_frames}:,setpts=PTS-STARTPTS[v_next_main];
|
| 474 |
-
[v_prev_fade][v_next_fade]blend=all_expr='A*(1-T/{crossfade_frames})+B*(T/{crossfade_frames})'[crossfade];
|
| 475 |
-
[v_prev_main][crossfade][v_next_main]concat=n=3:v=1:a=0[v]
|
| 476 |
-
" -map "[v]" -c:v libx264 -pix_fmt yuv420p "{temp_out_path}"
|
| 477 |
-
"""
|
| 478 |
|
| 479 |
-
|
| 480 |
-
|
| 481 |
|
| 482 |
-
#
|
| 483 |
-
|
| 484 |
-
|
|
|
|
|
|
|
|
|
|
| 485 |
|
| 486 |
-
|
| 487 |
-
|
|
|
|
|
|
|
| 488 |
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
|
| 494 |
-
return
|
| 495 |
|
| 496 |
-
|
| 497 |
-
def _concat_crossfade_cascade1(self, video_paths: List[str], output_path: str, crossfade_frames: int = 8):
|
| 498 |
"""
|
| 499 |
-
Concatena
|
| 500 |
-
|
| 501 |
-
Args:
|
| 502 |
-
video_paths (List[str]): Lista de caminhos dos vídeos a serem concatenados.
|
| 503 |
-
output_path (str): Caminho do arquivo final de saída.
|
| 504 |
-
crossfade_frames (int): Número de frames para o crossfade.
|
| 505 |
"""
|
| 506 |
-
if len(
|
| 507 |
-
raise ValueError("
|
| 508 |
-
|
| 509 |
-
# Cria
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
[0:v]trim=0:-{crossfade_frames},setpts=PTS-STARTPTS[v0pre];
|
| 526 |
-
[0:v]trim=-{crossfade_frames},setpts=PTS-STARTPTS[v0fade];
|
| 527 |
-
[1:v]trim=0:{crossfade_frames},setpts=PTS-STARTPTS[v1fade];
|
| 528 |
-
[v0fade][v1fade]blend=all_expr='A*(1-T/{crossfade_frames})+B*(T/{crossfade_frames})'[xf];
|
| 529 |
-
[1:v]trim={crossfade_frames}:,setpts=PTS-STARTPTS[v1post];
|
| 530 |
-
[v0pre][xf][v1post]concat=n=3:v=1:a=0[v]
|
| 531 |
-
"""
|
| 532 |
-
|
| 533 |
-
cmd = [
|
| 534 |
-
"ffmpeg", "-y",
|
| 535 |
-
"-i", vid1,
|
| 536 |
-
"-i", vid2,
|
| 537 |
-
"-filter_complex", filter_complex,
|
| 538 |
-
"-map", "[v]",
|
| 539 |
-
"-c:v", "libx264",
|
| 540 |
-
"-pix_fmt", "yuv420p",
|
| 541 |
-
tmp_file
|
| 542 |
-
]
|
| 543 |
-
|
| 544 |
-
print(f"[DEBUG] Executando: {' '.join(cmd)}")
|
| 545 |
-
subprocess.run(cmd, check=True)
|
| 546 |
-
|
| 547 |
-
# Último temporário é o arquivo final
|
| 548 |
-
final_tmp = tmp_videos[-1]
|
| 549 |
-
os.rename(final_tmp, output_path)
|
| 550 |
-
|
| 551 |
-
# Limpa os temporários intermediários
|
| 552 |
-
for tmp in tmp_videos[:-1]:
|
| 553 |
-
if os.path.exists(tmp):
|
| 554 |
-
os.remove(tmp)
|
| 555 |
-
|
| 556 |
-
print(f"[INFO] Vídeo final gerado: {output_path}")
|
| 557 |
|
|
|
|
|
|
|
| 558 |
|
| 559 |
def generate(
|
| 560 |
self,
|
|
@@ -784,12 +751,9 @@ class VideoService:
|
|
| 784 |
print(f"[DEBUG] Falha no move; usando tmp como final: {e}")
|
| 785 |
|
| 786 |
final_concat = os.path.join(results_dir, f"concat_fim_{used_seed}.mp4")
|
| 787 |
-
|
| 788 |
-
self.
|
| 789 |
-
|
| 790 |
-
crossfade_frames=8,
|
| 791 |
-
output_path=final_concat,
|
| 792 |
-
)
|
| 793 |
|
| 794 |
|
| 795 |
self._log_gpu_memory("Fim da Geração")
|
|
|
|
| 436 |
return chunks
|
| 437 |
|
| 438 |
|
| 439 |
+
|
| 440 |
+
def _gerar_lista_com_transicoes(self, video_paths: List[str], crossfade_frames: int = 8) -> List[str]:
|
| 441 |
"""
|
| 442 |
+
Gera uma nova lista de vídeos com cortes e transições de crossfade.
|
| 443 |
+
Cada transição é de 'crossfade_frames' frames.
|
| 444 |
+
|
| 445 |
Args:
|
| 446 |
+
video_paths: lista de caminhos de vídeos originais
|
| 447 |
+
crossfade_frames: quantidade de frames para transição
|
| 448 |
+
Returns:
|
| 449 |
+
List[str]: nova lista de caminhos de vídeos, incluindo transições
|
| 450 |
"""
|
| 451 |
+
nova_lista = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
|
| 453 |
+
for i in range(len(video_paths)):
|
| 454 |
+
video_atual = video_paths[i]
|
| 455 |
+
video_proximo = video_paths[i + 1] if i + 1 < len(video_paths) else None
|
| 456 |
|
| 457 |
+
# ---- 1. Video atual podado ----
|
| 458 |
+
if i == 0:
|
| 459 |
+
# Primeiro vídeo: remove últimos crossfade_frames
|
| 460 |
+
podado = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4").name
|
| 461 |
+
cmd_trim = f'ffmpeg -y -i "{video_atual}" -vf "trim=0:-{crossfade_frames},setpts=PTS-STARTPTS" "{podado}"'
|
| 462 |
+
elif video_proximo:
|
| 463 |
+
# Vídeos do meio: remove primeiros e últimos crossfade_frames
|
| 464 |
+
podado = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4").name
|
| 465 |
+
cmd_trim = f'ffmpeg -y -i "{video_atual}" -vf "trim={crossfade_frames}:-{crossfade_frames},setpts=PTS-STARTPTS" "{podado}"'
|
| 466 |
+
else:
|
| 467 |
+
# Último vídeo: remove primeiros crossfade_frames
|
| 468 |
+
podado = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4").name
|
| 469 |
+
cmd_trim = f'ffmpeg -y -i "{video_atual}" -vf "trim={crossfade_frames}:,setpts=PTS-STARTPTS" "{podado}"'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
|
| 471 |
+
subprocess.run(cmd_trim, shell=True, check=True)
|
| 472 |
+
nova_lista.append(podado)
|
| 473 |
|
| 474 |
+
# ---- 2. Gerar transição, se houver próximo vídeo ----
|
| 475 |
+
if video_proximo:
|
| 476 |
+
# Extrair últimos frames do atual
|
| 477 |
+
temp_fim = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4").name
|
| 478 |
+
cmd_fim = f'ffmpeg -y -i "{video_atual}" -vf "trim=-{crossfade_frames},setpts=PTS-STARTPTS" "{temp_fim}"'
|
| 479 |
+
subprocess.run(cmd_fim, shell=True, check=True)
|
| 480 |
|
| 481 |
+
# Extrair primeiros frames do próximo
|
| 482 |
+
temp_inicio = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4").name
|
| 483 |
+
cmd_inicio = f'ffmpeg -y -i "{video_proximo}" -vf "trim=0:{crossfade_frames},setpts=PTS-STARTPTS" "{temp_inicio}"'
|
| 484 |
+
subprocess.run(cmd_inicio, shell=True, check=True)
|
| 485 |
|
| 486 |
+
# Criar vídeo de transição
|
| 487 |
+
transicao = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4").name
|
| 488 |
+
cmd_blend = f"""
|
| 489 |
+
ffmpeg -y -i "{temp_fim}" -i "{temp_inicio}" -filter_complex "
|
| 490 |
+
[0:v][1:v]blend=all_expr='A*(1-T/{crossfade_frames})+B*(T/{crossfade_frames})'[v]
|
| 491 |
+
" -map "[v]" -c:v libx264 -pix_fmt yuv420p "{transicao}"
|
| 492 |
+
"""
|
| 493 |
+
subprocess.run(cmd_blend, shell=True, check=True)
|
| 494 |
+
nova_lista.append(transicao)
|
| 495 |
|
| 496 |
+
return nova_lista
|
| 497 |
|
| 498 |
+
def _concat_mp4s_no_reencode(self, mp4_list: List[str], out_path: str):
|
|
|
|
| 499 |
"""
|
| 500 |
+
Concatena múltiplos MP4s sem reencode usando o demuxer do ffmpeg.
|
| 501 |
+
ATENÇÃO: todos os arquivos precisam ter mesmo codec, fps, resolução etc.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
"""
|
| 503 |
+
if not mp4_list or len(mp4_list) < 2:
|
| 504 |
+
raise ValueError("Forneça pelo menos dois arquivos MP4 para concatenar.")
|
| 505 |
+
|
| 506 |
+
# Cria lista temporária para o ffmpeg
|
| 507 |
+
with tempfile.NamedTemporaryFile("w", delete=False, suffix=".txt") as f:
|
| 508 |
+
for mp4 in mp4_list:
|
| 509 |
+
f.write(f"file '{os.path.abspath(mp4)}'\n")
|
| 510 |
+
list_path = f.name
|
| 511 |
+
|
| 512 |
+
cmd = f"ffmpeg -y -f concat -safe 0 -i {list_path} -c copy {out_path}"
|
| 513 |
+
print(f"[DEBUG] Concat: {cmd}")
|
| 514 |
+
|
| 515 |
+
try:
|
| 516 |
+
subprocess.check_call(shlex.split(cmd))
|
| 517 |
+
finally:
|
| 518 |
+
try:
|
| 519 |
+
os.remove(list_path)
|
| 520 |
+
except Exception:
|
| 521 |
+
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
|
| 523 |
+
|
| 524 |
+
|
| 525 |
|
| 526 |
def generate(
|
| 527 |
self,
|
|
|
|
| 751 |
print(f"[DEBUG] Falha no move; usando tmp como final: {e}")
|
| 752 |
|
| 753 |
final_concat = os.path.join(results_dir, f"concat_fim_{used_seed}.mp4")
|
| 754 |
+
final_concat_new = self._gerar_lista_com_transicoes(video_paths=final_concat, crossfade_frames=8)
|
| 755 |
+
self._concat_mp4s_no_reencode(partes_mp4, final_concat_new)
|
| 756 |
+
|
|
|
|
|
|
|
|
|
|
| 757 |
|
| 758 |
|
| 759 |
self._log_gpu_memory("Fim da Geração")
|