Eueuiaa commited on
Commit
38db09b
·
verified ·
1 Parent(s): 1b92335

Update tools/video_encode_tool.py

Browse files
Files changed (1) hide show
  1. tools/video_encode_tool.py +164 -1
tools/video_encode_tool.py CHANGED
@@ -38,4 +38,167 @@ class _SimpleVideoEncoder:
38
  print(f"[DEBUG] [Encoder] frame {i}/{T} gravado ({H}x{W}@{fps}fps)")
39
 
40
  # Singleton global de uso simples
41
- video_encode_tool_singleton = _SimpleVideoEncoder()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  print(f"[DEBUG] [Encoder] frame {i}/{T} gravado ({H}x{W}@{fps}fps)")
39
 
40
  # Singleton global de uso simples
41
+ video_encode_tool_singleton = _SimpleVideoEncoder()
42
+
43
+
44
+
45
+ # aduc_framework/tools/video_encode_tool.py
46
+ #
47
+ # Versão 1.4.0 (Conjunto de Ferramentas de Vídeo Finalizado)
48
+ # Copyright (C) August 4, 2025 Carlos Rodrigues dos Santos
49
+ #
50
+ # Este módulo atua como o especialista central para todas as operações de
51
+ # manipulação e codificação de vídeo. Ele abstrai as interações com
52
+ # FFmpeg e imageio, fornecendo uma API limpa e robusta para o resto do framework.
53
+ # - save_video_from_tensor: Converte um tensor de pixel em um arquivo .mp4.
54
+ # - extract_..._frame: Extrai frames específicos de clipes de vídeo.
55
+ # - concatenate_videos: Monta o filme final a partir dos clipes de cena.
56
+
57
+ import os
58
+ import subprocess
59
+ import logging
60
+ import random
61
+ import time
62
+ import shutil
63
+ from typing import List, Optional, Tuple
64
+
65
+ import imageio
66
+ import numpy as np
67
+ import torch
68
+
69
+ logger = logging.getLogger(__name__)
70
+
71
+ class VideoToolError(Exception):
72
+ """Exceção personalizada para erros originados do VideoEncodeTool."""
73
+ pass
74
+
75
+ class VideoEncodeTool:
76
+ """
77
+ Um especialista para lidar com tarefas de codificação e manipulação de vídeo.
78
+ """
79
+
80
+ def save_video_from_tensor(self, video_tensor: torch.Tensor, path: str, fps: int = 24, progress_callback=None):
81
+ """
82
+ Salva um tensor de pixel como um arquivo de vídeo .mp4 usando parâmetros otimizados.
83
+ Espera um tensor no formato (B, C, F, H, W) onde B=1.
84
+ """
85
+ # Verificações de robustez para garantir que o tensor é válido
86
+ if video_tensor is None or video_tensor.ndim != 5 or video_tensor.shape[0] != 1 or video_tensor.shape[2] == 0:
87
+ logger.warning(f"Tensor de vídeo inválido ou vazio recebido. Shape: {video_tensor.shape if video_tensor is not None else 'None'}. Pulando salvamento de vídeo para '{path}'.")
88
+ return
89
+
90
+ logger.info(f"Salvando tensor de vídeo com shape {video_tensor.shape} para '{os.path.basename(path)}'...")
91
+
92
+ try:
93
+ # Squeeze: (1, C, F, H, W) -> (C, F, H, W)
94
+ # Permute: (C, F, H, W) -> (F, H, W, C) - formato esperado por imageio
95
+ video_tensor_permuted = video_tensor.squeeze(0).permute(1, 2, 3, 0)
96
+
97
+ # Desnormaliza de [-1, 1] para [0, 1]
98
+ video_tensor_normalized = (video_tensor_permuted.clamp(-1, 1) + 1) / 2.0
99
+
100
+ # Converte para [0, 255], move para CPU e converte para numpy uint8
101
+ video_np = (video_tensor_normalized.detach().cpu().float().numpy() * 255).astype(np.uint8)
102
+
103
+ # Salva o vídeo com parâmetros de alta compatibilidade
104
+ with imageio.get_writer(
105
+ path,
106
+ fps=fps,
107
+ codec='libx264',
108
+ quality=8, # Qualidade boa (0-10, onde 10 é a melhor)
109
+ output_params=['-pix_fmt', 'yuv420p'] # Formato de pixel para compatibilidade máxima
110
+ ) as writer:
111
+ for frame in video_np:
112
+ writer.append_data(frame)
113
+ if progress_callback:
114
+ progress_callback(i + 1, T)
115
+ if i % self.frame_log_every == 0:
116
+ print(f"[DEBUG] [Encoder] frame {i}/{T} gravado ({H}x{W}@{fps}fps)")
117
+
118
+
119
+ logger.info(f"Vídeo salvo com sucesso em: {path}")
120
+ except Exception as e:
121
+ logger.error(f"Falha ao salvar vídeo com imageio para '{path}': {e}", exc_info=True)
122
+ raise VideoToolError(f"Não foi possível escrever o arquivo de vídeo: {e}")
123
+
124
+ def extract_first_frame(self, video_path: str, output_image_path: str) -> str:
125
+ """
126
+ Extrai o primeiro frame de um arquivo de vídeo e o salva como uma imagem.
127
+ """
128
+ logger.info(f"Extraindo primeiro frame de '{os.path.basename(video_path)}'...")
129
+ cmd = ['ffmpeg', '-y', '-v', 'error', '-i', video_path, '-vframes', '1', '-q:v', '2', output_image_path]
130
+ try:
131
+ subprocess.run(cmd, check=True, capture_output=True, text=True)
132
+ return output_image_path
133
+ except subprocess.CalledProcessError as e:
134
+ logger.error(f"FFmpeg (extract_first_frame) falhou: {e.stderr}")
135
+ raise VideoToolError(f"Falha ao extrair o primeiro frame de {video_path}")
136
+
137
+ def extract_last_frame(self, video_path: str, output_image_path: str) -> str:
138
+ """
139
+ Extrai o último frame de um arquivo de vídeo e o salva como uma imagem.
140
+ """
141
+ logger.info(f"Extraindo último frame de '{os.path.basename(video_path)}'...")
142
+ cmd = ['ffmpeg', '-y', '-v', 'error', '-sseof', '-0.1', '-i', video_path, '-vframes', '1', '-q:v', '2', output_image_path]
143
+ try:
144
+ subprocess.run(cmd, check=True, capture_output=True, text=True)
145
+ return output_image_path
146
+ except subprocess.CalledProcessError as e:
147
+ logger.error(f"FFmpeg (extract_last_frame) falhou: {e.stderr}")
148
+ raise VideoToolError(f"Falha ao extrair o último frame de {video_path}")
149
+
150
+ def create_transition_bridge(self, start_image_path: str, end_image_path: str,
151
+ duration: float, fps: int, target_resolution: Tuple[int, int],
152
+ workspace_dir: str, effect: Optional[str] = None) -> str:
153
+ """
154
+ Cria um clipe de vídeo curto que transiciona entre duas imagens estáticas.
155
+ """
156
+ output_path = os.path.join(workspace_dir, f"bridge_{int(time.time())}_{random.randint(100, 999)}.mp4")
157
+ width, height = target_resolution
158
+ fade_effects = ["fade", "wipeleft", "wiperight", "wipeup", "wipedown", "dissolve", "fadeblack", "fadewhite", "radial", "rectcrop", "circleopen", "circleclose", "horzopen", "horzclose"]
159
+ selected_effect = effect if effect and effect.strip() else random.choice(fade_effects)
160
+ transition_duration = max(0.1, duration)
161
+ cmd = (f"ffmpeg -y -v error -loop 1 -t {transition_duration} -i \"{start_image_path}\" -loop 1 -t {transition_duration} -i \"{end_image_path}\" "
162
+ f"-filter_complex \"[0:v]scale={width}:{height},setsar=1[v0];[1:v]scale={width}:{height},setsar=1[v1];"
163
+ f"[v0][v1]xfade=transition={selected_effect}:duration={transition_duration}:offset=0[out]\" "
164
+ f"-map \"[out]\" -c:v libx264 -r {fps} -pix_fmt yuv420p \"{output_path}\"")
165
+ logger.info(f"Criando ponte de transição com efeito '{selected_effect}'...")
166
+ try:
167
+ subprocess.run(cmd, shell=True, check=True, text=True)
168
+ except subprocess.CalledProcessError as e:
169
+ raise VideoToolError(f"Falha ao criar vídeo de transição: {e.stderr}")
170
+ return output_path
171
+
172
+ def concatenate_videos(self, video_paths: List[str], output_path: str, workspace_dir: str) -> str:
173
+ """
174
+ Concatena múltiplos clipes de vídeo em um único arquivo sem re-codificar.
175
+ """
176
+ if not video_paths:
177
+ raise VideoToolError("Nenhum fragmento de vídeo fornecido para concatenação.")
178
+ # Se houver apenas um clipe, apenas o copie para o destino final.
179
+ if len(video_paths) == 1:
180
+ shutil.copy(video_paths[0], output_path)
181
+ logger.info(f"Apenas um clipe fornecido. Copiado para '{output_path}'.")
182
+ return output_path
183
+
184
+ list_file_path = os.path.join(workspace_dir, f"concat_list_{int(time.time())}.txt")
185
+ try:
186
+ with open(list_file_path, 'w', encoding='utf-8') as f:
187
+ for path in video_paths:
188
+ # Garante que o caminho seja absoluto para o ffmpeg encontrar
189
+ f.write(f"file '{os.path.abspath(path)}'\n")
190
+
191
+ cmd_list = ['ffmpeg', '-y', '-f', 'concat', '-safe', '0', '-i', list_file_path, '-c', 'copy', output_path]
192
+ logger.info(f"Concatenando {len(video_paths)} clipes para '{os.path.basename(output_path)}'...")
193
+ subprocess.run(cmd_list, check=True, capture_output=True, text=True)
194
+ logger.info("Concatenação FFmpeg bem-sucedida.")
195
+ return output_path
196
+ except subprocess.CalledProcessError as e:
197
+ logger.error(f"Falha ao montar o vídeo final com FFmpeg: {e.stderr}")
198
+ raise VideoToolError(f"Falha ao montar o vídeo final com FFmpeg.")
199
+ finally:
200
+ if os.path.exists(list_file_path):
201
+ os.remove(list_file_path)
202
+
203
+ # --- Instância Singleton ---
204
+ video_encode_tool_singleton = VideoEncodeTool()