Carlexx commited on
Commit
7ac1138
·
verified ·
1 Parent(s): 83bc3fe

Upload ai_studio_code (68).py

Browse files
Files changed (1) hide show
  1. ai_studio_code (68).py +556 -0
ai_studio_code (68).py ADDED
@@ -0,0 +1,556 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Euia-AducSdr: Uma implementação aberta e funcional da arquitetura ADUC-SDR para geração de vídeo coerente.
2
+ # Copyright (C) 4 de Agosto de 2025 Carlos Rodrigues dos Santos
3
+ #
4
+ # Contato:
5
+ # Carlos Rodrigues dos Santos
6
+ # carlex22@gmail.com
7
+ # Rua Eduardo Carlos Pereira, 4125, B1 Ap32, Curitiba, PR, Brazil, CEP 8102025
8
+ #
9
+ # Repositórios e Projetos Relacionados:
10
+ # GitHub: https://github.com/carlex22/Aduc-sdr
11
+ # Hugging Face: https://huggingface.co/spaces/Carlexx/Ltx-SuperTime-60Secondos/
12
+ # Hugging Face: https://huggingface.co/spaces/Carlexxx/Novinho/
13
+ #
14
+ # Este programa é software livre: você pode redistribuí-lo e/ou modificá-lo
15
+ # sob os termos da Licença Pública Geral Affero da GNU como publicada pela
16
+ # Free Software Foundation, seja a versão 3 da Licença, ou
17
+ # (a seu critério) qualquer versão posterior.
18
+ #
19
+ # Este programa é distribuído na esperança de que seja útil,
20
+ # mas SEM QUALQUER GARANTIA; sem mesmo a garantia implícita de
21
+ # COMERCIALIZAÇÃO ou ADEQUAÇÃO A UM DETERMINADO FIM. Consulte a
22
+ # Licença Pública Geral Affero da GNU para mais detalhes.
23
+ #
24
+ # Você deve ter recebido uma cópia da Licença Pública Geral Affero da GNU
25
+ # junto com este programa. Se não, veja <https://www.gnu.org/licenses/>.
26
+
27
+ # --- app.py (NOVINHO-5.2-DOCS: Otimização de Memória + Documentação Completa) ---
28
+
29
+ # --- Ato 1: A Convocação da Orquestra (Importações) ---
30
+ import gradio as gr
31
+ import torch
32
+ import os
33
+ import yaml
34
+ from PIL import Image, ImageOps, ExifTags
35
+ import shutil
36
+ import gc
37
+ import subprocess
38
+ import google.generativeai as genai
39
+ import numpy as np
40
+ import imageio
41
+ from pathlib import Path
42
+ import huggingface_hub
43
+ import json
44
+ import time
45
+
46
+ from inference import create_ltx_video_pipeline, load_image_to_tensor_with_resize_and_crop, ConditioningItem, calculate_padding
47
+ from dreamo_helpers import dreamo_generator_singleton
48
+
49
+ # --- Ato 2: A Preparação do Palco (Configurações) ---
50
+ config_file_path = "configs/ltxv-13b-0.9.8-distilled.yaml"
51
+ with open(config_file_path, "r") as file: PIPELINE_CONFIG_YAML = yaml.safe_load(file)
52
+
53
+ LTX_REPO = "Lightricks/LTX-Video"
54
+ models_dir = "downloaded_models_gradio"
55
+ Path(models_dir).mkdir(parents=True, exist_ok=True)
56
+ WORKSPACE_DIR = "aduc_workspace"
57
+ GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
58
+
59
+ VIDEO_FPS = 30
60
+ VIDEO_DURATION_SECONDS = 6.0
61
+ VIDEO_TOTAL_FRAMES = int(VIDEO_DURATION_SECONDS * VIDEO_FPS)
62
+ TARGET_RESOLUTION = 420
63
+
64
+ print("Criando pipelines LTX na CPU (estado de repouso)...")
65
+ distilled_model_actual_path = huggingface_hub.hf_hub_download(repo_id=LTX_REPO, filename=PIPELINE_CONFIG_YAML["checkpoint_path"], local_dir=models_dir, local_dir_use_symlinks=False)
66
+ pipeline_instance = create_ltx_video_pipeline(
67
+ ckpt_path=distilled_model_actual_path,
68
+ precision=PIPELINE_CONFIG_YAML["precision"],
69
+ text_encoder_model_name_or_path=PIPELINE_CONFIG_YAML["text_encoder_model_name_or_path"],
70
+ sampler=PIPELINE_CONFIG_YAML["sampler"],
71
+ device='cpu'
72
+ )
73
+ print("Modelos LTX prontos (na CPU).")
74
+
75
+
76
+ # --- Ato 3: As Partituras dos Músicos (Funções de Geração e Análise) ---
77
+ # AVISO: A documentação abaixo descreve a lógica de cada função.
78
+ # NÃO APAGUE OU ALTERE ESTES COMENTÁRIOS SEM SOLICITAÇÃO EXPLÍCITA.
79
+
80
+ # --- Funções da ETAPA 1 (Roteiro) ---
81
+
82
+ def robust_json_parser(raw_text: str) -> dict:
83
+ """
84
+ Analisa uma string de texto bruto para encontrar e decodificar o primeiro objeto JSON válido.
85
+ Esta função é crucial para lidar com as respostas das IAs, que podem incluir texto
86
+ conversacional antes ou depois do bloco JSON.
87
+
88
+ Args:
89
+ raw_text (str): A string de texto completa retornada pela IA.
90
+
91
+ Returns:
92
+ dict: Um dicionário Python representando o objeto JSON encontrado.
93
+
94
+ Raises:
95
+ ValueError: Se nenhum objeto JSON for encontrado ou se a decodificação falhar.
96
+ """
97
+ try:
98
+ start_index = raw_text.find('{')
99
+ end_index = raw_text.rfind('}')
100
+ if start_index != -1 and end_index != -1 and end_index > start_index:
101
+ json_str = raw_text[start_index : end_index + 1]
102
+ return json.loads(json_str)
103
+ else: raise ValueError("Nenhum objeto JSON válido encontrado na resposta da IA.")
104
+ except json.JSONDecodeError as e:
105
+ raise ValueError(f"Falha ao decodificar JSON: {e}")
106
+
107
+ def extract_image_exif(image_path: str) -> str:
108
+ """
109
+ Extrai metadados EXIF relevantes de um arquivo de imagem.
110
+ Foca em informações técnicas como modelo da câmera, lente e configurações de exposição.
111
+
112
+ Args:
113
+ image_path (str): O caminho para o arquivo de imagem.
114
+
115
+ Returns:
116
+ str: Uma string formatada contendo os metadados EXIF relevantes, ou uma mensagem
117
+ indicando que nenhum metadado foi encontrado ou lido.
118
+ """
119
+ try:
120
+ img = Image.open(image_path); exif_data = img._getexif()
121
+ if not exif_data: return "No EXIF metadata found."
122
+ exif = { ExifTags.TAGS[k]: v for k, v in exif_data.items() if k in ExifTags.TAGS }
123
+ relevant_tags = ['DateTimeOriginal', 'Model', 'LensModel', 'FNumber', 'ExposureTime', 'ISOSpeedRatings', 'FocalLength']
124
+ metadata_str = ", ".join(f"{key}: {exif[key]}" for key in relevant_tags if key in exif)
125
+ return metadata_str if metadata_str else "No relevant EXIF metadata found."
126
+ except Exception: return "Could not read EXIF data."
127
+
128
+ def run_storyboard_generation(num_fragments: int, prompt: str, initial_image_path: str):
129
+ """
130
+ Orquestra a Etapa 1. Em uma única chamada à IA, combina a análise da imagem de referência
131
+ com o prompt do usuário para gerar um roteiro de cenas (storyboard).
132
+
133
+ Args:
134
+ num_fragments (int): O número de atos (cenas) a serem criados.
135
+ prompt (str): A "Ideia Geral" fornecida pelo usuário.
136
+ initial_image_path (str): O caminho para a imagem de referência inicial.
137
+
138
+ Returns:
139
+ list: Uma lista de strings, onde cada string é a descrição de um ato do roteiro.
140
+ """
141
+ if not initial_image_path: raise gr.Error("Por favor, forneça uma imagem de referência inicial.")
142
+ if not GEMINI_API_KEY: raise gr.Error("Chave da API Gemini não configurada!")
143
+ exif_metadata = extract_image_exif(initial_image_path)
144
+ prompt_file = "prompts/unified_storyboard_prompt.txt"
145
+ with open(os.path.join(os.path.dirname(__file__), prompt_file), "r", encoding="utf-8") as f: template = f.read()
146
+ director_prompt = template.format(user_prompt=prompt, num_fragments=int(num_fragments), image_metadata=exif_metadata)
147
+ genai.configure(api_key=GEMINI_API_KEY)
148
+ model = genai.GenerativeModel('gemini-2.0-flash'); img = Image.open(initial_image_path)
149
+ print("Gerando roteiro com análise de visão integrada...")
150
+ response = model.generate_content([director_prompt, img])
151
+ try:
152
+ storyboard_data = robust_json_parser(response.text)
153
+ storyboard = storyboard_data.get("scene_storyboard", [])
154
+ if not storyboard or len(storyboard) != int(num_fragments): raise ValueError(f"A IA não gerou o número correto de cenas. Esperado: {num_fragments}, Recebido: {len(storyboard)}")
155
+ return storyboard
156
+ except Exception as e: raise gr.Error(f"O Roteirista (Gemini) falhou ao criar o roteiro: {e}. Resposta recebida: {response.text}")
157
+
158
+
159
+ # --- Funções da ETAPA 2 (Keyframes) ---
160
+ def get_dreamo_prompt_for_transition(previous_image_path: str, target_scene_description: str) -> str:
161
+ """
162
+ Chama a IA "Diretor de Arte" para criar um prompt de imagem dinâmico.
163
+ A IA analisa a imagem anterior e a descrição da próxima cena para gerar um prompt
164
+ que guiará o "Pintor" (DreamO) na criação do próximo keyframe.
165
+
166
+ Args:
167
+ previous_image_path (str): Caminho para a imagem de referência mais recente.
168
+ target_scene_description (str): A descrição do ato do roteiro para a cena a ser criada.
169
+
170
+ Returns:
171
+ str: O prompt de imagem gerado.
172
+ """
173
+ genai.configure(api_key=GEMINI_API_KEY)
174
+ prompt_file = "prompts/img2img_evolution_prompt.txt"
175
+ with open(os.path.join(os.path.dirname(__file__), prompt_file), "r", encoding="utf-8") as f: template = f.read()
176
+ director_prompt = template.format(target_scene_description=target_scene_description)
177
+ model = genai.GenerativeModel('gemini-2.0-flash'); img = Image.open(previous_image_path)
178
+ response = model.generate_content([director_prompt, "Previous Image:", img])
179
+ return response.text.strip().replace("\"", "")
180
+
181
+ def run_keyframe_generation(storyboard, initial_ref_image_path, sequential_ref_task, progress=gr.Progress()):
182
+ """
183
+ Orquestra a Etapa 2. Gera a sequência de imagens-chave (keyframes) em um loop.
184
+ A cada iteração, usa as 3 últimas imagens geradas como referência visual para
185
+ manter a consistência, e chama o "Diretor de Arte" para criar um prompt dinâmico.
186
+
187
+ Args:
188
+ storyboard (list): A lista de atos do roteiro gerada na Etapa 1.
189
+ initial_ref_image_path (str): Caminho para a imagem de referência inicial processada.
190
+ sequential_ref_task (str): A tarefa de referência para o DreamO ('ip', 'id', 'style').
191
+ progress (gr.Progress): Objeto do Gradio para atualizar a barra de progresso.
192
+
193
+ Yields:
194
+ dict: Atualizações para os componentes da UI (log, galeria) durante a geração.
195
+
196
+ Returns:
197
+ dict: O estado final dos componentes da UI e as listas de keyframes.
198
+ """
199
+ if not storyboard: raise gr.Error("Nenhum roteiro para gerar keyframes.")
200
+ if not initial_ref_image_path: raise gr.Error("A imagem de referência principal é obrigatória.")
201
+ log_history = ""; generated_images_for_gallery = []
202
+ try:
203
+ pipeline_instance.to('cpu'); gc.collect(); torch.cuda.empty_cache()
204
+ dreamo_generator_singleton.to_gpu()
205
+ with Image.open(initial_ref_image_path) as img: width, height = (img.width // 32) * 32, (img.height // 32) * 32
206
+ keyframe_paths, current_ref_image_path = [initial_ref_image_path], initial_ref_image_path
207
+ for i, scene_description in enumerate(storyboard):
208
+ progress(i / len(storyboard), desc=f"Pintando Keyframe {i+1}/{len(storyboard)}")
209
+ log_history += f"\n--- PINTANDO KEYFRAME {i+1}/{len(storyboard)} ---\n"
210
+ dreamo_prompt = get_dreamo_prompt_for_transition(current_ref_image_path, scene_description)
211
+ recent_references_paths = keyframe_paths[-3:]
212
+ log_history += f" - Roteiro: '{scene_description}'\n - Usando {len(recent_references_paths)} refs: {[os.path.basename(p) for p in recent_references_paths]}\n - Prompt do D.A.: \"{dreamo_prompt}\"\n"
213
+ yield {keyframe_log_output: gr.update(value=log_history), keyframe_gallery_output: gr.update(value=generated_images_for_gallery)}
214
+ reference_items = [{'image_np': np.array(Image.open(ref_path).convert("RGB")), 'task': sequential_ref_task} for ref_path in recent_references_paths]
215
+ output_path = os.path.join(WORKSPACE_DIR, f"keyframe_{i+1}.png")
216
+ image = dreamo_generator_singleton.generate_image_with_gpu_management(reference_items=reference_items, prompt=dreamo_prompt, width=width, height=height)
217
+ image.save(output_path)
218
+ keyframe_paths.append(output_path); generated_images_for_gallery.append(output_path); current_ref_image_path = output_path
219
+ yield {keyframe_log_output: gr.update(value=log_history), keyframe_gallery_output: gr.update(value=generated_images_for_gallery)}
220
+ except Exception as e: raise gr.Error(f"O Pintor (DreamO) ou Diretor de Arte (Gemini) falhou: {e}")
221
+ finally: dreamo_generator_singleton.to_cpu(); gc.collect(); torch.cuda.empty_cache()
222
+ log_history += "\nPintura de todos os keyframes concluída.\n"
223
+ yield {keyframe_log_output: gr.update(value=log_history), keyframe_gallery_output: gr.update(value=generated_images_for_gallery), keyframe_images_state: keyframe_paths}
224
+
225
+
226
+ # --- Funções da ETAPA 3 (Produção de Vídeo) ---
227
+ def get_initial_motion_prompt(user_prompt: str, start_image_path: str, destination_image_path: str, dest_scene_desc: str):
228
+ """
229
+ Gera o prompt de movimento para o primeiro fragmento de vídeo ("Big Bang").
230
+ Este é um caso especial que lida com uma transição simples de (Início -> Fim).
231
+
232
+ Args:
233
+ user_prompt (str): A ideia geral para dar contexto.
234
+ start_image_path (str): Caminho para o primeiro keyframe gerado (K_1).
235
+ destination_image_path (str): Caminho para o segundo keyframe gerado (K_2).
236
+ dest_scene_desc (str): A descrição do roteiro para a cena de destino (Ato 2).
237
+
238
+ Returns:
239
+ str: O prompt de movimento gerado para a transição inicial.
240
+ """
241
+ if not GEMINI_API_KEY: raise gr.Error("Chave da API Gemini não configurada!")
242
+ try:
243
+ genai.configure(api_key=GEMINI_API_KEY)
244
+ model = genai.GenerativeModel('gemini-2.0-flash')
245
+ prompt_file = "prompts/initial_motion_prompt.txt"
246
+ with open(os.path.join(os.path.dirname(__file__), prompt_file), "r", encoding="utf-8") as f: template = f.read()
247
+ cinematographer_prompt = template.format(user_prompt=user_prompt, destination_scene_description=dest_scene_desc)
248
+ start_img, dest_img = Image.open(start_image_path), Image.open(destination_image_path)
249
+ model_contents = ["START Image:", start_img, "DESTINATION Image:", dest_img, cinematographer_prompt]
250
+ response = model.generate_content(model_contents)
251
+ return response.text.strip()
252
+ except Exception as e: raise gr.Error(f"O Cineasta de IA (Inicial) falhou: {e}. Resposta: {getattr(e, 'text', 'No text available.')}")
253
+
254
+ def get_dynamic_motion_prompt(user_prompt, story_history, memory_image_path, path_image_path, destination_image_path, path_scene_desc, dest_scene_desc):
255
+ """
256
+ Gera o prompt de movimento para os fragmentos subsequentes, usando a lógica "Handoff Cinético".
257
+ A IA analisa 3 imagens (Memória, Caminho, Destino) para criar a instrução.
258
+
259
+ Args:
260
+ user_prompt (str): A ideia geral.
261
+ story_history (str): Um resumo dos prompts de movimento anteriores.
262
+ memory_image_path (str): O "Eco", último frame do fragmento anterior.
263
+ path_image_path (str): O "Caminho", keyframe que define o contexto da transição.
264
+ destination_image_path (str): O "Destino", keyframe que queremos alcançar.
265
+ path_scene_desc (str): Descrição do roteiro para o "Caminho".
266
+ dest_scene_desc (str): Descrição do roteiro para o "Destino".
267
+
268
+ Returns:
269
+ str: O prompt de movimento dinâmico gerado.
270
+ """
271
+ if not GEMINI_API_KEY: raise gr.Error("Chave da API Gemini não configurada!")
272
+ try:
273
+ genai.configure(api_key=GEMINI_API_KEY)
274
+ model = genai.GenerativeModel('gemini-2.0-flash')
275
+ prompt_file = "prompts/dynamic_motion_prompt.txt"
276
+ with open(os.path.join(os.path.dirname(__file__), prompt_file), "r", encoding="utf-8") as f: template = f.read()
277
+ cinematographer_prompt = template.format(user_prompt=user_prompt, story_history=story_history, midpoint_scene_description=path_scene_desc, destination_scene_description=dest_scene_desc)
278
+ mem_img, path_img, dest_img = Image.open(memory_image_path), Image.open(path_image_path), Image.open(destination_image_path)
279
+ model_contents = ["START Image (Memory):", mem_img, "MIDPOINT Image (Path):", path_img, "DESTINATION Image (Destination):", dest_img, cinematographer_prompt]
280
+ response = model.generate_content(model_contents)
281
+ return response.text.strip()
282
+ except Exception as e: raise gr.Error(f"O Cineasta de IA (Dinâmico) falhou: {e}. Resposta: {getattr(e, 'text', 'No text available.')}")
283
+
284
+ def run_video_production(prompt_geral, keyframe_images_state, scene_storyboard, seed, cfg, cut_frames_value, progress=gr.Progress()):
285
+ """
286
+ Orquestra a Etapa 3. Gera todos os fragmentos de vídeo em um loop, aplicando a lógica
287
+ "Big Bang" para o primeiro fragmento e "Handoff Cinético" para os demais.
288
+
289
+ Args:
290
+ prompt_geral (str): A ideia geral do usuário.
291
+ keyframe_images_state (list): A lista completa de keyframes [K_0, ..., K_n].
292
+ scene_storyboard (list): A lista de atos do roteiro.
293
+ seed (int): A semente para a geração de números aleatórios.
294
+ cfg (float): A escala de orientação do LTX.
295
+ cut_frames_value (int): O número de frames a manter em cada fragmento cortado.
296
+ progress (gr.Progress): Objeto do Gradio para a barra de progresso.
297
+
298
+ Yields:
299
+ dict: Atualizações para a UI durante o processo.
300
+
301
+ Returns:
302
+ dict: O estado final dos componentes da UI.
303
+ """
304
+ if not keyframe_images_state or len(keyframe_images_state) < 3: raise gr.Error("Pinte pelo menos 2 keyframes para produzir uma transição.")
305
+ log_history = "\n--- FASE 3/4: Iniciando Produção com Lógica 'Big Bang'...\n"
306
+ yield {production_log_output: log_history, video_gallery_glitch: []}
307
+
308
+ MID_COND_FRAME, MID_COND_STRENGTH = 54, 0.5; END_COND_FRAME = VIDEO_TOTAL_FRAMES - 8
309
+ target_device = 'cuda' if torch.cuda.is_available() else 'cpu'
310
+ try:
311
+ pipeline_instance.to(target_device)
312
+ video_fragments, story_history = [], ""; kinetic_memory_path = None
313
+ with Image.open(keyframe_images_state[1]) as img: width, height = img.size
314
+
315
+ num_transitions = len(keyframe_images_state) - 2
316
+ for i in range(num_transitions):
317
+ fragment_num = i + 1
318
+ progress(i / num_transitions, desc=f"Filmando Fragmento {fragment_num}/{num_transitions}")
319
+ log_history += f"\n--- FRAGMENTO {fragment_num} ---\n"
320
+
321
+ if i == 0:
322
+ start_path, destination_path = keyframe_images_state[1], keyframe_images_state[2]
323
+ dest_scene_desc = scene_storyboard[1]
324
+ log_history += f" - Início (Big Bang): {os.path.basename(start_path)}\n - Destino: {os.path.basename(destination_path)}\n"
325
+ current_motion_prompt = get_initial_motion_prompt(prompt_geral, start_path, destination_path, dest_scene_desc)
326
+ conditioning_items_data = [(start_path, int(0), 1.0), (destination_path, int(END_COND_FRAME), 1.0)]
327
+ else:
328
+ memory_path, path_path, destination_path = kinetic_memory_path, keyframe_images_state[i+1], keyframe_images_state[i+2]
329
+ path_scene_desc, dest_scene_desc = scene_storyboard[i], scene_storyboard[i+1]
330
+ log_history += f" - Memória Cinética: {os.path.basename(memory_path)}\n - Caminho: {os.path.basename(path_path)}\n - Destino: {os.path.basename(destination_path)}\n"
331
+ current_motion_prompt = get_dynamic_motion_prompt(prompt_geral, story_history, memory_path, path_path, destination_path, path_scene_desc, dest_scene_desc)
332
+ conditioning_items_data = [(memory_path, int(0), 1.0), (path_path, int(MID_COND_FRAME), MID_COND_STRENGTH), (destination_path, int(END_COND_FRAME), 1.0)]
333
+
334
+ story_history += f"\n- Ato {fragment_num + 1}: {current_motion_prompt}"
335
+ log_history += f" - Instrução do Cineasta: '{current_motion_prompt}'\n"; yield {production_log_output: log_history}
336
+ full_fragment_path, _ = run_ltx_animation(fragment_num, current_motion_prompt, conditioning_items_data, width, height, seed, cfg, progress)
337
+
338
+ is_last_fragment = (i == num_transitions - 1)
339
+ if is_last_fragment:
340
+ final_fragment_path = full_fragment_path
341
+ log_history += " - Último fragmento gerado, mantendo a duração total para um final limpo.\n"
342
+ else:
343
+ final_fragment_path = os.path.join(WORKSPACE_DIR, f"fragment_{fragment_num}_trimmed.mp4")
344
+ trim_video_to_frames(full_fragment_path, final_fragment_path, int(cut_frames_value))
345
+ eco_output_path = os.path.join(WORKSPACE_DIR, f"eco_from_frag_{fragment_num}.png")
346
+ kinetic_memory_path = extract_last_frame_as_image(final_fragment_path, eco_output_path)
347
+ log_history += f" - Gerado e cortado. Novo Eco Dinâmico criado: {os.path.basename(kinetic_memory_path)}\n"
348
+
349
+ video_fragments.append(final_fragment_path)
350
+ yield {production_log_output: log_history, video_gallery_glitch: video_fragments}
351
+
352
+ progress(1.0, desc="Produção Concluída.")
353
+ yield {production_log_output: log_history, video_gallery_glitch: video_fragments, fragment_list_state: video_fragments}
354
+ finally:
355
+ pipeline_instance.to('cpu'); gc.collect(); torch.cuda.empty_cache()
356
+
357
+
358
+ # --- Funções Utilitárias e de Pós-Produção ---
359
+ def process_image_to_square(image_path: str, size: int = TARGET_RESOLUTION) -> str:
360
+ """
361
+ Processa a imagem de referência inicial: converte para RGB e redimensiona para um
362
+ formato quadrado (TARGET_RESOLUTION x TARGET_RESOLUTION).
363
+
364
+ Args:
365
+ image_path (str): Caminho para a imagem original.
366
+ size (int): A dimensão do lado do quadrado final.
367
+
368
+ Returns:
369
+ str: O caminho para a imagem processada e salva.
370
+ """
371
+ if not image_path: return None
372
+ try:
373
+ img = Image.open(image_path).convert("RGB"); img_square = ImageOps.fit(img, (size, size), Image.Resampling.LANCZOS)
374
+ output_path = os.path.join(WORKSPACE_DIR, f"initial_ref_{size}x{size}.png"); img_square.save(output_path)
375
+ return output_path
376
+ except Exception as e: raise gr.Error(f"Falha ao processar a imagem de referência: {e}")
377
+
378
+ def load_conditioning_tensor(media_path: str, height: int, width: int) -> torch.Tensor:
379
+ """
380
+ Carrega uma imagem e a converte para o formato de tensor esperado pelo LTX.
381
+
382
+ Args:
383
+ media_path (str): Caminho para o arquivo de imagem.
384
+ height (int): Altura do vídeo alvo.
385
+ width (int): Largura do vídeo alvo.
386
+
387
+ Returns:
388
+ torch.Tensor: O tensor da imagem, pronto para ser usado como condicionamento.
389
+ """
390
+ return load_image_to_tensor_with_resize_and_crop(media_path, height, width)
391
+
392
+ def run_ltx_animation(current_fragment_index, motion_prompt, conditioning_items_data, width, height, seed, cfg, progress=gr.Progress()):
393
+ """
394
+ Wrapper para a execução do pipeline do LTX. Gera um único fragmento de vídeo.
395
+ Ativa o 'attention slicing' para economizar VRAM durante a execução.
396
+
397
+ Args:
398
+ current_fragment_index (int): O número do fragmento atual (para a seed).
399
+ motion_prompt (str): O prompt de movimento do Cineasta de IA.
400
+ conditioning_items_data (list): Lista de tuplas para os itens de condicionamento.
401
+ width (int): Largura do vídeo.
402
+ height (int): Altura do vídeo.
403
+ seed (int): Semente de geração.
404
+ cfg (float): Escala de orientação.
405
+ progress (gr.Progress): Objeto do Gradio para a barra de progresso.
406
+
407
+ Returns:
408
+ tuple: (caminho_do_video_gerado, numero_de_frames_gerados)
409
+ """
410
+ progress(0, desc=f"[Câmera LTX] Filmando Cena {current_fragment_index}...");
411
+ output_path = os.path.join(WORKSPACE_DIR, f"fragment_{current_fragment_index}_full.mp4"); target_device = pipeline_instance.device
412
+
413
+ try:
414
+ pipeline_instance.enable_attention_slicing()
415
+
416
+ conditioning_items = [ConditioningItem(load_conditioning_tensor(p, height, width).to(target_device), s, t) for p, s, t in conditioning_items_data]
417
+ actual_num_frames = int(round((float(VIDEO_TOTAL_FRAMES) - 1.0) / 8.0) * 8 + 1)
418
+ padded_h, padded_w = ((height - 1) // 32 + 1) * 32, ((width - 1) // 32 + 1) * 32
419
+ padding_vals = calculate_padding(height, width, padded_h, padded_w)
420
+ for item in conditioning_items: item.media_item = torch.nn.functional.pad(item.media_item, padding_vals)
421
+ kwargs = {"prompt": motion_prompt, "negative_prompt": "blurry, distorted, bad quality, artifacts", "height": padded_h, "width": padded_w, "num_frames": actual_num_frames, "frame_rate": VIDEO_FPS, "generator": torch.Generator(device=target_device).manual_seed(int(seed) + current_fragment_index), "output_type": "pt", "guidance_scale": float(cfg), "timesteps": PIPELINE_CONFIG_YAML.get("first_pass", {}).get("timesteps"), "conditioning_items": conditioning_items, "decode_timestep": PIPELINE_CONFIG_YAML.get("decode_timestep"), "decode_noise_scale": PIPELINE_CONFIG_YAML.get("decode_noise_scale"), "stochastic_sampling": PIPELINE_CONFIG_YAML.get("stochastic_sampling"), "image_cond_noise_scale": 0.15, "is_video": True, "vae_per_channel_normalize": True, "mixed_precision": (PIPELINE_CONFIG_YAML.get("precision") == "mixed_precision"), "enhance_prompt": False, "decode_every": 4}
422
+ result_tensor = pipeline_instance(**kwargs).images
423
+
424
+ pad_l, pad_r, pad_t, pad_b = map(int, padding_vals); slice_h = -pad_b if pad_b > 0 else None; slice_w = -pad_r if pad_r > 0 else None
425
+ cropped_tensor = result_tensor[:, :, :VIDEO_TOTAL_FRAMES, pad_t:slice_h, pad_l:slice_w]; video_np = (cropped_tensor[0].permute(1, 2, 3, 0).cpu().float().numpy() * 255).astype(np.uint8)
426
+ with imageio.get_writer(output_path, fps=VIDEO_FPS, codec='libx264', quality=8) as writer:
427
+ for i, frame in enumerate(video_np): writer.append_data(frame)
428
+
429
+ return output_path, actual_num_frames
430
+ finally:
431
+ pipeline_instance.disable_attention_slicing()
432
+
433
+ def trim_video_to_frames(input_path: str, output_path: str, frames_to_keep: int) -> str:
434
+ """
435
+ Usa o FFmpeg para cortar um vídeo, mantendo apenas um número específico de frames iniciais.
436
+ Essencial para o "Corte Estratégico" do Handoff Cinético.
437
+
438
+ Args:
439
+ input_path (str): Caminho para o vídeo de entrada.
440
+ output_path (str): Caminho para salvar o vídeo cortado.
441
+ frames_to_keep (int): Número de frames a serem mantidos.
442
+
443
+ Returns:
444
+ str: O caminho para o vídeo cortado.
445
+ """
446
+ try:
447
+ subprocess.run(f"ffmpeg -y -v error -i \"{input_path}\" -vf \"select='lt(n,{frames_to_keep})'\" -an \"{output_path}\"", shell=True, check=True, text=True)
448
+ return output_path
449
+ except subprocess.CalledProcessError as e: raise gr.Error(f"FFmpeg falhou ao cortar vídeo: {e.stderr}")
450
+
451
+ def extract_last_frame_as_image(video_path: str, output_image_path: str) -> str:
452
+ """
453
+ Usa o FFmpeg para extrair eficientemente o último frame de um vídeo.
454
+ Esta é a função que cria o "Eco" para o Handoff Cinético.
455
+
456
+ Args:
457
+ video_path (str): Caminho para o vídeo de entrada.
458
+ output_image_path (str): Caminho para salvar a imagem do frame extraído.
459
+
460
+ Returns:
461
+ str: O caminho para a imagem extraída.
462
+ """
463
+ try:
464
+ subprocess.run(f"ffmpeg -y -v error -sseof -1 -i \"{video_path}\" -update 1 -q:v 1 \"{output_image_path}\"", shell=True, check=True, text=True)
465
+ return output_image_path
466
+ except subprocess.CalledProcessError as e: raise gr.Error(f"FFmpeg falhou ao extrair último frame: {e.stderr}")
467
+
468
+ def concatenate_and_trim_masterpiece(fragment_paths: list, progress=gr.Progress()):
469
+ """
470
+ Orquestra a Etapa 4. Usa o FFmpeg para concatenar todos os fragmentos de vídeo gerados
471
+ em uma única obra-prima final.
472
+
473
+ Args:
474
+ fragment_paths (list): Uma lista dos caminhos para os fragmentos de vídeo.
475
+ progress (gr.Progress): Objeto do Gradio para a barra de progresso.
476
+
477
+ Returns:
478
+ str: O caminho para o vídeo final montado.
479
+ """
480
+ if not fragment_paths: raise gr.Error("Nenhum fragmento de vídeo para concatenar.")
481
+ progress(0.5, desc="Montando a obra-prima final...");
482
+ try:
483
+ list_file_path = os.path.join(WORKSPACE_DIR, "concat_list.txt"); final_output_path = os.path.join(WORKSPACE_DIR, "masterpiece_final.mp4")
484
+ with open(list_file_path, "w") as f:
485
+ for p in fragment_paths: f.write(f"file '{os.path.abspath(p)}'\n")
486
+ subprocess.run(f"ffmpeg -y -v error -f concat -safe 0 -i \"{list_file_path}\" -c copy \"{final_output_path}\"", shell=True, check=True, text=True)
487
+ progress(1.0, desc="Montagem concluída!")
488
+ return final_output_path
489
+ except subprocess.CalledProcessError as e: raise gr.Error(f"FFmpeg falhou na concatenação final: {e.stderr}")
490
+
491
+ # --- Ato 5: A Interface com o Mundo (UI) ---
492
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
493
+ gr.Markdown("# NOVINHO-5.2 (Otimização de Memória)\n*By Carlex & Gemini & DreamO*")
494
+
495
+ if os.path.exists(WORKSPACE_DIR): shutil.rmtree(WORKSPACE_DIR)
496
+ os.makedirs(WORKSPACE_DIR); Path("prompts").mkdir(exist_ok=True)
497
+
498
+ scene_storyboard_state, keyframe_images_state, fragment_list_state = gr.State([]), gr.State([]), gr.State([])
499
+ prompt_geral_state, processed_ref_path_state = gr.State(""), gr.State("")
500
+
501
+ gr.Markdown("--- \n ## ETAPA 1: O ROTEIRO (IA Roteirista)")
502
+ with gr.Row():
503
+ with gr.Column(scale=1):
504
+ prompt_input = gr.Textbox(label="Ideia Geral (Prompt)")
505
+ num_fragments_input = gr.Slider(2, 10, 4, step=1, label="Número de Atos (Keyframes)")
506
+ image_input = gr.Image(type="filepath", label=f"Imagem de Referência Principal (será {TARGET_RESOLUTION}x{TARGET_RESOLUTION})")
507
+ director_button = gr.Button("▶️ 1. Gerar Roteiro", variant="primary")
508
+ with gr.Column(scale=2): storyboard_to_show = gr.JSON(label="Roteiro de Cenas Gerado (em Inglês)")
509
+
510
+ gr.Markdown("--- \n ## ETAPA 2: OS KEYFRAMES (IA Pintor & Diretor de Arte)")
511
+ with gr.Row():
512
+ with gr.Column(scale=2):
513
+ gr.Markdown("O Diretor de Arte (IA) gerará prompts dinamicamente. O Pintor usará as **3 últimas imagens** como referência.")
514
+ with gr.Group():
515
+ with gr.Row():
516
+ ref_image_inputs_auto = gr.Image(label="Referência Inicial (Automática)", type="filepath", interactive=False)
517
+ ref_task_input = gr.Dropdown(choices=["ip", "id", "style"], value="ip", label="Tarefa da Referência")
518
+ photographer_button = gr.Button("▶️ 2. Pintar Imagens-Chave em Cadeia", variant="primary")
519
+ with gr.Column(scale=1):
520
+ keyframe_log_output = gr.Textbox(label="Diário de Bordo do Pintor", lines=15, interactive=False)
521
+ keyframe_gallery_output = gr.Gallery(label="Imagens-Chave Pintadas", object_fit="contain", height="auto", type="filepath")
522
+
523
+ gr.Markdown("--- \n ## ETAPA 3: A PRODUÇÃO (IA Cineasta & Câmera)")
524
+ with gr.Row():
525
+ with gr.Column(scale=1):
526
+ with gr.Row(): seed_number = gr.Number(42, label="Seed"); cfg_slider = gr.Slider(1.0, 10.0, 2.5, step=0.1, label="CFG")
527
+ cut_frames_slider = gr.Slider(label="Duração do Fragmento (Frames)", minimum=60, maximum=VIDEO_TOTAL_FRAMES, value=150, step=1)
528
+ animator_button = gr.Button("▶️ 3. Produzir Cenas (Handoff Cinético)", variant="primary")
529
+ production_log_output = gr.Textbox(label="Diário de Bordo da Produção", lines=15, interactive=False)
530
+ with gr.Column(scale=1): video_gallery_glitch = gr.Gallery(label="Fragmentos Gerados", object_fit="contain", height="auto", type="video")
531
+
532
+ gr.Markdown(f"--- \n ## ETAPA 4: PÓS-PRODUÇÃO (IA Editor)")
533
+ editor_button = gr.Button("▶️ 4. Montar Vídeo Final", variant="primary")
534
+ final_video_output = gr.Video(label="A Obra-Prima Final", width=TARGET_RESOLUTION)
535
+
536
+ gr.Markdown(
537
+ """
538
+ ---
539
+ ### A Arquitetura: Handoff Cinético & Big Bang
540
+ A geração começa com um "Big Bang": a primeira transição de vídeo é entre o **Keyframe 1 e o Keyframe 2**. A imagem de referência original é usada apenas para criar o primeiro keyframe e depois é descartada do processo de vídeo.
541
+
542
+ * **O Bastão (O `Eco`):** Após a primeira transição, o último frame do clipe cortado (o `Eco`) carrega a "energia cinética" da cena.
543
+
544
+ * **O Handoff (A Geração):** Os fragmentos seguintes começam a partir deste `Eco` dinâmico, herdando a "física" do movimento e da iluminação.
545
+
546
+ * **A Sincronização (Cineasta de IA):** Para cada Handoff, o Cineasta de IA (`Γ`) analisa o (`Eco`), o (`Keyframe` do caminho) e o (`Keyframe` do destino) para criar uma instrução de movimento precisa.
547
+ """
548
+ )
549
+
550
+ director_button.click(fn=run_storyboard_generation, inputs=[num_fragments_input, prompt_input, image_input], outputs=[scene_storyboard_state]).success(fn=lambda s, p: (s, p), inputs=[scene_storyboard_state, prompt_input], outputs=[storyboard_to_show, prompt_geral_state]).success(fn=process_image_to_square, inputs=[image_input], outputs=[processed_ref_path_state]).success(fn=lambda p: p, inputs=[processed_ref_path_state], outputs=[ref_image_inputs_auto])
551
+ photographer_button.click(fn=run_keyframe_generation, inputs=[scene_storyboard_state, processed_ref_path_state, ref_task_input], outputs=[keyframe_log_output, keyframe_gallery_output, keyframe_images_state])
552
+ animator_button.click(fn=run_video_production, inputs=[prompt_geral_state, keyframe_images_state, scene_storyboard_state, seed_number, cfg_slider, cut_frames_slider], outputs=[production_log_output, video_gallery_glitch, fragment_list_state])
553
+ editor_button.click(fn=concatenate_and_trim_masterpiece, inputs=[fragment_list_state], outputs=[final_video_output])
554
+
555
+ if __name__ == "__main__":
556
+ demo.queue().launch(server_name="0.0.0.0", share=True)