gnosticdev commited on
Commit
e9b7708
·
verified ·
1 Parent(s): 261f3dc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +100 -93
app.py CHANGED
@@ -9,7 +9,7 @@ import gradio as gr
9
  import torch
10
  from transformers import GPT2Tokenizer, GPT2LMHeadModel
11
  from keybert import KeyBERT
12
- # CORRECCIÓN CRÍTICA DEFINITIVA: Eliminar 'concatenate_videoclip' (singular) de la importación
13
  from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip, concatenate_audioclips, AudioClip
14
  import re
15
  import math
@@ -102,24 +102,20 @@ def generate_script(prompt, max_length=150):
102
  logger.info(f"Generando guión | Prompt: '{prompt[:50]}...' | Longitud máxima: {max_length}")
103
  if not tokenizer or not model:
104
  logger.warning("Modelos GPT-2 no disponibles - Usando prompt original como guion.")
105
- return prompt.strip() # Return original prompt clean
106
 
107
- # Frase de instrucción que se le da a la IA
108
  instruction_phrase_start = "Escribe un guion corto, interesante y coherente sobre:"
109
- # Construir el prompt exacto que se le pasará a la IA
110
  ai_prompt = f"{instruction_phrase_start} {prompt}"
111
 
112
  try:
113
- # Generar texto usando el prompt completo
114
  inputs = tokenizer(ai_prompt, return_tensors="pt", truncation=True, max_length=512)
115
-
116
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
117
  model.to(device)
118
  inputs = {k: v.to(device) for k, v in inputs.items()}
119
 
120
  outputs = model.generate(
121
  **inputs,
122
- max_length=max_length + inputs[list(inputs.keys())[0]].size(1), # Longitud máxima incluyendo los tokens de entrada
123
  do_sample=True,
124
  top_p=0.9,
125
  top_k=40,
@@ -132,68 +128,55 @@ def generate_script(prompt, max_length=150):
132
 
133
  text = tokenizer.decode(outputs[0], skip_special_tokens=True)
134
 
135
- # --- Limpiar la frase de instrucción inicial de la salida de la IA ---
136
  cleaned_text = text.strip()
137
- # Intentar encontrar el inicio de la respuesta real después de la instrucción
138
- # A veces la IA repite el prompt o la instrucción
139
  try:
140
- # Buscar el final de la frase de instrucción literal en la salida
141
  instruction_end_idx = text.find(instruction_phrase)
142
  if instruction_end_idx != -1:
143
- # Tomar el texto que viene *después* de la instrucción exacta
144
  cleaned_text = text[instruction_end_idx + len(instruction_phrase):].strip()
145
  logger.debug("Instrucción inicial encontrada y eliminada del guión generado.")
146
  else:
147
- # Si no se encuentra la frase exacta, buscar solo el inicio de la instrucción base
148
  instruction_start_idx = text.find(instruction_phrase_start)
149
  if instruction_start_idx != -1:
150
- # Tomar texto después de la frase base + prompt (heurística)
151
  prompt_in_output_idx = text.find(prompt, instruction_start_idx)
152
  if prompt_in_output_idx != -1:
153
  cleaned_text = text[prompt_in_output_idx + len(prompt):].strip()
154
  logger.debug("Instrucción base y prompt encontrados y eliminados del guión generado.")
155
  else:
156
- # Fallback: si la instrucción base está pero no el prompt después, tomar después de la instrucción base
157
  cleaned_text = text[instruction_start_idx + len(instruction_phrase_start):].strip()
158
  logger.debug("Instrucción base encontrada, eliminada del guión generado (sin prompt detectado).")
159
 
160
  except Exception as e:
161
  logger.warning(f"Error durante la limpieza heurística del guión de IA: {e}. Usando texto generado sin limpieza adicional.")
162
- cleaned_text = re.sub(r'<[^>]+>', '', text).strip() # Limpieza básica como fallback
163
 
164
- # Asegurarse de que el texto resultante no sea solo la instrucción o vacío
165
- if not cleaned_text or len(cleaned_text) < 10: # Umbral de longitud mínima
166
  logger.warning("El guión generado parece muy corto o vacío después de la limpieza. Usando el texto generado original (sin limpieza heurística).")
167
- cleaned_text = re.sub(r'<[^>]+>', '', text).strip() # Fallback al texto original limpio
168
 
169
- # Limpieza final de caracteres especiales y espacios
170
  cleaned_text = re.sub(r'<[^>]+>', '', cleaned_text).strip()
171
- cleaned_text = cleaned_text.lstrip(':').strip() # Quitar posibles ':' al inicio
172
- cleaned_text = cleaned_text.lstrip('.').strip() # Quitar posibles '.' al inicio
173
 
174
-
175
- # Intentar obtener al menos una oración completa si es posible
176
  sentences = cleaned_text.split('.')
177
  if sentences and sentences[0].strip():
178
  final_text = sentences[0].strip() + '.'
179
- # Añadir la segunda oración si existe y es razonable
180
- if len(sentences) > 1 and sentences[1].strip() and len(final_text.split()) < max_length * 0.7: # Usar un 70% de max_length como umbral
181
  final_text += " " + sentences[1].strip() + "."
182
- final_text = final_text.replace("..", ".") # Limpiar doble punto
183
 
184
  logger.info(f"Guion generado final (Truncado a 100 chars): '{final_text[:100]}...'")
185
  return final_text.strip()
186
 
187
  logger.info(f"Guion generado final (sin oraciones completas detectadas - Truncado): '{cleaned_text[:100]}...'")
188
- return cleaned_text.strip() # Si no se puede formar una oración, devolver el texto limpio tal cual
189
 
190
  except Exception as e:
191
  logger.error(f"Error generando guion con GPT-2 (fuera del bloque de limpieza): {str(e)}", exc_info=True)
192
  logger.warning("Usando prompt original como guion debido al error de generación.")
193
  return prompt.strip()
194
 
195
- # CAMBIO: Voz por defecto a "es-ES-JuanNeural"
196
- async def text_to_speech(text, output_path, voice="es-ES-JuanNeural"):
197
  logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz: {voice} | Salida: {output_path}")
198
  if not text or not text.strip():
199
  logger.warning("Texto vacío para TTS")
@@ -211,7 +194,7 @@ async def text_to_speech(text, output_path, voice="es-ES-JuanNeural"):
211
  return False
212
 
213
  except Exception as e:
214
- logger.error(f"Error en TTS: {str(e)}", exc_info=True)
215
  return False
216
 
217
  def download_video_file(url, temp_dir):
@@ -397,26 +380,54 @@ def crear_video(prompt_type, input_text, musica_file=None):
397
  logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
398
  temp_intermediate_files = []
399
 
400
- # 2. Generar audio de voz
401
  logger.info("Generando audio de voz...")
402
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
403
- # Usar voz de Juan
404
- if not asyncio.run(text_to_speech(guion, voz_path, voice="es-ES-JuanNeural")):
405
- logger.error("Fallo en generación de voz")
406
- raise ValueError("Error generando voz a partir del guion.")
407
- temp_intermediate_files.append(voz_path)
408
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
  audio_tts_original = AudioFileClip(voz_path)
410
 
411
  if audio_tts_original.reader is None or audio_tts_original.duration is None or audio_tts_original.duration <= 0:
412
- logger.critical("Clip de audio TTS inicial es inválido (reader is None o duración <= 0).")
413
  try: audio_tts_original.close()
414
  except: pass
415
  audio_tts_original = None
416
- raise ValueError("Audio de voz generado es inválido.")
417
 
418
- audio_tts = audio_tts_original # Usar el clip TTS válido original para la mezcla
419
- audio_duration = audio_tts_original.duration # Usar duración original para la longitud del video
420
  logger.info(f"Duración audio voz: {audio_duration:.2f} segundos")
421
 
422
  if audio_duration < 1.0:
@@ -590,7 +601,7 @@ def crear_video(prompt_type, input_text, musica_file=None):
590
  num_full_repeats = int(audio_duration // final_video_base.duration)
591
  remaining_duration = audio_duration % final_video_base.duration
592
 
593
- repeated_clips_list = [final_video_base] * num_full_repeats # Lista contiene duplicados del mismo objeto clip
594
  if remaining_duration > 0:
595
  try:
596
  remaining_clip = final_video_base.subclip(0, remaining_duration)
@@ -628,7 +639,7 @@ def crear_video(prompt_type, input_text, musica_file=None):
628
  finally:
629
  if 'repeated_clips_list' in locals():
630
  for clip in repeated_clips_list:
631
- if clip is not final_video_base: # No cerrar si es el clip final
632
  try: clip.close()
633
  except: pass
634
 
@@ -686,7 +697,6 @@ def crear_video(prompt_type, input_text, musica_file=None):
686
  except: pass
687
  musica_audio_original = None
688
  else:
689
- # Usar la duración correcta del video base para loopear la música
690
  musica_audio_looped = loop_audio_to_length(musica_audio_original, video_base.duration)
691
  logger.debug(f"Música ajustada a duración del video: {musica_audio_looped.duration:.2f}s")
692
 
@@ -775,21 +785,18 @@ def crear_video(prompt_type, input_text, musica_file=None):
775
  finally:
776
  logger.info("Iniciando limpieza de clips y archivos temporales intermedios...")
777
 
778
- # Cerrar todos los clips de video fuente iniciales abiertos
779
  for clip in source_clips:
780
  try:
781
  clip.close()
782
  except Exception as e:
783
  logger.warning(f"Error cerrando clip de video fuente en finally: {str(e)}")
784
 
785
- # Cerrar cualquier segmento de video que quede en la lista (debería estar vacía si tuvo éxito)
786
  for clip_segment in clips_to_concatenate:
787
  try:
788
  clip_segment.close()
789
  except Exception as e:
790
  logger.warning(f"Error cerrando segmento de video en finally: {str(e)}")
791
 
792
- # Cerrar clips de audio en orden: música loopeada, música original (si es diferente), TTS original
793
  if musica_audio is not None: # musica_audio holds the potentially looped clip
794
  try:
795
  musica_audio.close()
@@ -802,7 +809,6 @@ def crear_video(prompt_type, input_text, musica_file=None):
802
  except Exception as e:
803
  logger.warning(f"Error cerrando musica_audio_original en finally: {str(e)}")
804
 
805
- # audio_tts actualmente solo contiene audio_tts_original, pero se mantiene la estructura
806
  if audio_tts is not None and audio_tts is not audio_tts_original:
807
  try:
808
  audio_tts.close()
@@ -815,26 +821,22 @@ def crear_video(prompt_type, input_text, musica_file=None):
815
  except Exception as e:
816
  logger.warning(f"Error cerrando audio_tts_original en finally: {str(e)}")
817
 
818
-
819
- # Cerrar clips de video en orden: video_final (debería cerrar sus componentes), luego video_base (si es diferente de video_final)
820
  if video_final is not None:
821
  try:
822
  video_final.close()
823
  except Exception as e:
824
  logger.warning(f"Error cerrando video_final en finally: {str(e)}")
825
- elif video_base is not None and video_base is not video_final: # Asegurarse de que video_base no es el mismo objeto que video_final
826
  try:
827
  video_base.close()
828
  except Exception as e:
829
  logger.warning(f"Error cerrando video_base en finally: {str(e)}")
830
 
831
- # Limpiar archivos intermedios, pero NO el archivo de video final
832
  if temp_dir_intermediate and os.path.exists(temp_dir_intermediate):
833
  final_output_in_temp = os.path.join(temp_dir_intermediate, "final_video.mp4")
834
 
835
  for path in temp_intermediate_files:
836
  try:
837
- # Verificar explícitamente que la ruta no sea la ruta de salida del video final antes de eliminar
838
  if os.path.isfile(path) and path != final_output_in_temp:
839
  logger.debug(f"Eliminando archivo temporal intermedio: {path}")
840
  os.remove(path)
@@ -843,18 +845,19 @@ def crear_video(prompt_type, input_text, musica_file=None):
843
  except Exception as e:
844
  logger.warning(f"No se pudo eliminar archivo temporal intermedio {path}: {str(e)}")
845
 
846
- # El directorio temporal *persistirá* porque contiene el archivo final
847
  logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
848
 
849
 
 
850
  def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
851
  logger.info("="*80)
852
  logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
853
 
 
854
  input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
855
 
856
  output_video = None
857
- output_file = None # Inicializar a None
858
  status_msg = gr.update(value="⏳ Procesando...", interactive=False)
859
 
860
  if not input_text or not input_text.strip():
@@ -870,30 +873,27 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
870
 
871
  try:
872
  logger.info("Llamando a crear_video...")
 
873
  video_path = crear_video(prompt_type, input_text, musica_file)
874
 
875
  if video_path and os.path.exists(video_path):
876
  logger.info(f"crear_video retornó path: {video_path}")
877
  logger.info(f"Tamaño del archivo de video retornado: {os.path.getsize(video_path)} bytes")
878
- output_video = video_path # Establecer valor del componente de video
879
- output_file = video_path # Establecer valor del componente de archivo para descarga
880
  status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
881
  else:
882
  logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
883
- # Dejar las salidas de video y archivo como None
884
  status_msg = gr.update(value="❌ Error: La generación del video falló o el archivo no se creó correctamente.", interactive=False)
885
 
886
  except ValueError as ve:
887
  logger.warning(f"Error de validación durante la creación del video: {str(ve)}")
888
- # Dejar las salidas de video y archivo como None
889
  status_msg = gr.update(value=f"⚠️ Error de validación: {str(ve)}", interactive=False)
890
  except Exception as e:
891
  logger.critical(f"Error crítico durante la creación del video: {str(e)}", exc_info=True)
892
- # Dejar las salidas de video y archivo como None
893
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
894
  finally:
895
  logger.info("Fin del handler run_app.")
896
- # Retornar las tres salidas
897
  return output_video, output_file, status_msg
898
 
899
 
@@ -914,23 +914,26 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
914
  value="Generar Guion con IA"
915
  )
916
 
917
- with gr.Column(visible=True) as ia_guion_column:
918
- prompt_ia = gr.Textbox(
919
- label="Tema para IA",
920
- lines=2,
921
- placeholder="Ej: Un paisaje natural con montañas y ríos al amanecer, mostrando la belleza de la naturaleza...",
922
- max_lines=4,
923
- value=""
924
- )
925
-
926
- with gr.Column(visible=False) as manual_guion_column:
927
- prompt_manual = gr.Textbox(
928
- label="Tu Guion Completo",
929
- lines=5,
930
- placeholder="Ej: En este video exploraremos los misterios del océano. Veremos la vida marina fascinante y los arrecifes de coral vibrantes. ¡Acompáñanos en esta aventura subacuática!",
931
- max_lines=10,
932
- value=""
933
- )
 
 
 
934
 
935
  musica_input = gr.Audio(
936
  label="Música de fondo (opcional)",
@@ -943,13 +946,12 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
943
 
944
  with gr.Column():
945
  video_output = gr.Video(
946
- label="Previsualización del Video Generado", # Etiqueta cambiada
947
  interactive=False,
948
  height=400
949
  )
950
- # Añadir el componente File para la descarga
951
  file_output = gr.File(
952
- label="Descargar Archivo de Video", # Etiqueta cambiada
953
  interactive=False,
954
  visible=False # Ocultar inicialmente
955
  )
@@ -961,30 +963,33 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
961
  value="Esperando entrada..."
962
  )
963
 
 
 
964
  prompt_type.change(
965
  lambda x: (gr.update(visible=x == "Generar Guion con IA"),
966
  gr.update(visible=x == "Usar Mi Guion")),
967
  inputs=prompt_type,
968
- outputs=[ia_guion_column, manual_guion_column]
969
  )
970
 
971
- # Modificar el evento click para retornar 3 salidas
972
  generate_btn.click(
973
- # Acción 1: Resetear salidas y establecer estado a procesando
974
- lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar 2-5 minutos o más para videos largos.", interactive=False)), # Mensaje de estado mejorado
975
  outputs=[video_output, file_output, status_output],
976
- queue=True, # Mantener la cola habilitada
977
  ).then(
978
- # Acción 2: Llamar a la función principal de procesamiento
979
  run_app,
 
980
  inputs=[prompt_type, prompt_ia, prompt_manual, musica_input],
981
- outputs=[video_output, file_output, status_output] # Coincidir las 3 salidas
982
  ).then(
983
- # Acción 3: Hacer visible el enlace de descarga si se retornó un archivo
984
- # Verificar si file_output tiene un valor
985
- lambda video_path, file_path: gr.update(visible=file_path is not None), # Usar file_path para visibilidad
986
- inputs=[video_output, file_output], # Ambas salidas como entrada
987
- outputs=[file_output] # Actualizar visibilidad de file_output
988
  )
989
 
990
 
@@ -1019,6 +1024,8 @@ if __name__ == "__main__":
1019
 
1020
  logger.info("Iniciando aplicación Gradio...")
1021
  try:
 
 
1022
  app.launch(server_name="0.0.0.0", server_port=7860, share=False)
1023
  except Exception as e:
1024
  logger.critical(f"No se pudo iniciar la app: {str(e)}", exc_info=True)
 
9
  import torch
10
  from transformers import GPT2Tokenizer, GPT2LMHeadModel
11
  from keybert import KeyBERT
12
+ # CORRECCIÓN CRÍTICA DEFINITIVA DEL TYPO DE IMPORTACIÓN
13
  from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip, concatenate_audioclips, AudioClip
14
  import re
15
  import math
 
102
  logger.info(f"Generando guión | Prompt: '{prompt[:50]}...' | Longitud máxima: {max_length}")
103
  if not tokenizer or not model:
104
  logger.warning("Modelos GPT-2 no disponibles - Usando prompt original como guion.")
105
+ return prompt.strip()
106
 
 
107
  instruction_phrase_start = "Escribe un guion corto, interesante y coherente sobre:"
 
108
  ai_prompt = f"{instruction_phrase_start} {prompt}"
109
 
110
  try:
 
111
  inputs = tokenizer(ai_prompt, return_tensors="pt", truncation=True, max_length=512)
 
112
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
113
  model.to(device)
114
  inputs = {k: v.to(device) for k, v in inputs.items()}
115
 
116
  outputs = model.generate(
117
  **inputs,
118
+ max_length=max_length + inputs[list(inputs.keys())[0]].size(1),
119
  do_sample=True,
120
  top_p=0.9,
121
  top_k=40,
 
128
 
129
  text = tokenizer.decode(outputs[0], skip_special_tokens=True)
130
 
 
131
  cleaned_text = text.strip()
 
 
132
  try:
 
133
  instruction_end_idx = text.find(instruction_phrase)
134
  if instruction_end_idx != -1:
 
135
  cleaned_text = text[instruction_end_idx + len(instruction_phrase):].strip()
136
  logger.debug("Instrucción inicial encontrada y eliminada del guión generado.")
137
  else:
 
138
  instruction_start_idx = text.find(instruction_phrase_start)
139
  if instruction_start_idx != -1:
 
140
  prompt_in_output_idx = text.find(prompt, instruction_start_idx)
141
  if prompt_in_output_idx != -1:
142
  cleaned_text = text[prompt_in_output_idx + len(prompt):].strip()
143
  logger.debug("Instrucción base y prompt encontrados y eliminados del guión generado.")
144
  else:
 
145
  cleaned_text = text[instruction_start_idx + len(instruction_phrase_start):].strip()
146
  logger.debug("Instrucción base encontrada, eliminada del guión generado (sin prompt detectado).")
147
 
148
  except Exception as e:
149
  logger.warning(f"Error durante la limpieza heurística del guión de IA: {e}. Usando texto generado sin limpieza adicional.")
150
+ cleaned_text = re.sub(r'<[^>]+>', '', text).strip()
151
 
152
+ if not cleaned_text or len(cleaned_text) < 10:
 
153
  logger.warning("El guión generado parece muy corto o vacío después de la limpieza. Usando el texto generado original (sin limpieza heurística).")
154
+ cleaned_text = re.sub(r'<[^>]+>', '', text).strip()
155
 
 
156
  cleaned_text = re.sub(r'<[^>]+>', '', cleaned_text).strip()
157
+ cleaned_text = cleaned_text.lstrip(':').strip()
158
+ cleaned_text = cleaned_text.lstrip('.').strip()
159
 
 
 
160
  sentences = cleaned_text.split('.')
161
  if sentences and sentences[0].strip():
162
  final_text = sentences[0].strip() + '.'
163
+ if len(sentences) > 1 and sentences[1].strip() and len(final_text.split()) < max_length * 0.7:
 
164
  final_text += " " + sentences[1].strip() + "."
165
+ final_text = final_text.replace("..", ".")
166
 
167
  logger.info(f"Guion generado final (Truncado a 100 chars): '{final_text[:100]}...'")
168
  return final_text.strip()
169
 
170
  logger.info(f"Guion generado final (sin oraciones completas detectadas - Truncado): '{cleaned_text[:100]}...'")
171
+ return cleaned_text.strip()
172
 
173
  except Exception as e:
174
  logger.error(f"Error generando guion con GPT-2 (fuera del bloque de limpieza): {str(e)}", exc_info=True)
175
  logger.warning("Usando prompt original como guion debido al error de generación.")
176
  return prompt.strip()
177
 
178
+ # Función TTS con voz especificada
179
+ async def text_to_speech(text, output_path, voice): # voice is now a parameter
180
  logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz: {voice} | Salida: {output_path}")
181
  if not text or not text.strip():
182
  logger.warning("Texto vacío para TTS")
 
194
  return False
195
 
196
  except Exception as e:
197
+ logger.error(f"Error en TTS con voz '{voice}': {str(e)}", exc_info=True)
198
  return False
199
 
200
  def download_video_file(url, temp_dir):
 
380
  logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
381
  temp_intermediate_files = []
382
 
383
+ # 2. Generar audio de voz con reintentos y voz de respaldo
384
  logger.info("Generando audio de voz...")
385
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
 
 
 
 
 
386
 
387
+ primary_voice = "es-ES-JuanNeural"
388
+ fallback_voice = "es-ES-ElviraNeural" # Otra voz en español
389
+ tts_success = False
390
+ retries = 3 # Número de intentos
391
+
392
+ for attempt in range(retries):
393
+ current_voice = primary_voice if attempt == 0 else fallback_voice
394
+ if attempt > 0: logger.warning(f"Reintentando TTS ({attempt+1}/{retries})...")
395
+ logger.info(f"Intentando TTS con voz: {current_voice}")
396
+ try:
397
+ # Llamar a la función async text_to_speech
398
+ tts_success = asyncio.run(text_to_speech(guion, voz_path, voice=current_voice))
399
+ if tts_success:
400
+ logger.info(f"TTS exitoso en intento {attempt + 1} con voz {current_voice}.")
401
+ break # Salir del bucle de reintentos si tiene éxito
402
+ except Exception as e:
403
+ # La excepción ya se registra dentro de text_to_speech
404
+ pass # Continuar al siguiente intento
405
+
406
+ if not tts_success and attempt == 0 and primary_voice != fallback_voice:
407
+ logger.warning(f"Fallo con voz {primary_voice}, intentando voz de respaldo: {fallback_voice}")
408
+ elif not tts_success and attempt < retries - 1:
409
+ logger.warning(f"Fallo con voz {current_voice}, reintentando...")
410
+
411
+
412
+ # Verificar si el archivo fue creado después de todos los intentos
413
+ if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 100:
414
+ logger.error(f"Fallo en la generación de voz después de {retries} intentos. Archivo de audio no creado o es muy pequeño.")
415
+ raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
416
+
417
+ temp_intermediate_files.append(voz_path) # Añadir a la lista de limpieza si se creó
418
+
419
+ # Continuar cargando el archivo de audio generado
420
  audio_tts_original = AudioFileClip(voz_path)
421
 
422
  if audio_tts_original.reader is None or audio_tts_original.duration is None or audio_tts_original.duration <= 0:
423
+ logger.critical("Clip de audio TTS inicial es inválido (reader is None o duración <= 0) *después* de crear AudioFileClip.")
424
  try: audio_tts_original.close()
425
  except: pass
426
  audio_tts_original = None
427
+ raise ValueError("Audio de voz generado es inválido después de procesamiento inicial.")
428
 
429
+ audio_tts = audio_tts_original
430
+ audio_duration = audio_tts_original.duration
431
  logger.info(f"Duración audio voz: {audio_duration:.2f} segundos")
432
 
433
  if audio_duration < 1.0:
 
601
  num_full_repeats = int(audio_duration // final_video_base.duration)
602
  remaining_duration = audio_duration % final_video_base.duration
603
 
604
+ repeated_clips_list = [final_video_base] * num_full_repeats
605
  if remaining_duration > 0:
606
  try:
607
  remaining_clip = final_video_base.subclip(0, remaining_duration)
 
639
  finally:
640
  if 'repeated_clips_list' in locals():
641
  for clip in repeated_clips_list:
642
+ if clip is not final_video_base:
643
  try: clip.close()
644
  except: pass
645
 
 
697
  except: pass
698
  musica_audio_original = None
699
  else:
 
700
  musica_audio_looped = loop_audio_to_length(musica_audio_original, video_base.duration)
701
  logger.debug(f"Música ajustada a duración del video: {musica_audio_looped.duration:.2f}s")
702
 
 
785
  finally:
786
  logger.info("Iniciando limpieza de clips y archivos temporales intermedios...")
787
 
 
788
  for clip in source_clips:
789
  try:
790
  clip.close()
791
  except Exception as e:
792
  logger.warning(f"Error cerrando clip de video fuente en finally: {str(e)}")
793
 
 
794
  for clip_segment in clips_to_concatenate:
795
  try:
796
  clip_segment.close()
797
  except Exception as e:
798
  logger.warning(f"Error cerrando segmento de video en finally: {str(e)}")
799
 
 
800
  if musica_audio is not None: # musica_audio holds the potentially looped clip
801
  try:
802
  musica_audio.close()
 
809
  except Exception as e:
810
  logger.warning(f"Error cerrando musica_audio_original en finally: {str(e)}")
811
 
 
812
  if audio_tts is not None and audio_tts is not audio_tts_original:
813
  try:
814
  audio_tts.close()
 
821
  except Exception as e:
822
  logger.warning(f"Error cerrando audio_tts_original en finally: {str(e)}")
823
 
 
 
824
  if video_final is not None:
825
  try:
826
  video_final.close()
827
  except Exception as e:
828
  logger.warning(f"Error cerrando video_final en finally: {str(e)}")
829
+ elif video_base is not None and video_base is not video_final:
830
  try:
831
  video_base.close()
832
  except Exception as e:
833
  logger.warning(f"Error cerrando video_base en finally: {str(e)}")
834
 
 
835
  if temp_dir_intermediate and os.path.exists(temp_dir_intermediate):
836
  final_output_in_temp = os.path.join(temp_dir_intermediate, "final_video.mp4")
837
 
838
  for path in temp_intermediate_files:
839
  try:
 
840
  if os.path.isfile(path) and path != final_output_in_temp:
841
  logger.debug(f"Eliminando archivo temporal intermedio: {path}")
842
  os.remove(path)
 
845
  except Exception as e:
846
  logger.warning(f"No se pudo eliminar archivo temporal intermedio {path}: {str(e)}")
847
 
 
848
  logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
849
 
850
 
851
+ # CAMBIO CRÍTICO: run_app ahora toma 4 argumentos
852
  def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
853
  logger.info("="*80)
854
  logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
855
 
856
+ # La lógica para elegir el texto de entrada YA ESTÁ AQUÍ
857
  input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
858
 
859
  output_video = None
860
+ output_file = None
861
  status_msg = gr.update(value="⏳ Procesando...", interactive=False)
862
 
863
  if not input_text or not input_text.strip():
 
873
 
874
  try:
875
  logger.info("Llamando a crear_video...")
876
+ # Pasar el input_text elegido y el archivo de música
877
  video_path = crear_video(prompt_type, input_text, musica_file)
878
 
879
  if video_path and os.path.exists(video_path):
880
  logger.info(f"crear_video retornó path: {video_path}")
881
  logger.info(f"Tamaño del archivo de video retornado: {os.path.getsize(video_path)} bytes")
882
+ output_video = video_path
883
+ output_file = video_path
884
  status_msg = gr.update(value="✅ Video generado exitosamente.", interactive=False)
885
  else:
886
  logger.error(f"crear_video no retornó un path válido o el archivo no existe: {video_path}")
 
887
  status_msg = gr.update(value="❌ Error: La generación del video falló o el archivo no se creó correctamente.", interactive=False)
888
 
889
  except ValueError as ve:
890
  logger.warning(f"Error de validación durante la creación del video: {str(ve)}")
 
891
  status_msg = gr.update(value=f"⚠️ Error de validación: {str(ve)}", interactive=False)
892
  except Exception as e:
893
  logger.critical(f"Error crítico durante la creación del video: {str(e)}", exc_info=True)
 
894
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
895
  finally:
896
  logger.info("Fin del handler run_app.")
 
897
  return output_video, output_file, status_msg
898
 
899
 
 
914
  value="Generar Guion con IA"
915
  )
916
 
917
+ # Estos inputs siempre se pasan a run_app, independientemente de cuál se use
918
+ prompt_ia = gr.Textbox(
919
+ label="Tema para IA",
920
+ lines=2,
921
+ placeholder="Ej: Un paisaje natural con montañas y ríos al amanecer...",
922
+ max_lines=4,
923
+ value="",
924
+ # Hacer visible/oculto por el evento change
925
+ visible=True
926
+ )
927
+
928
+ prompt_manual = gr.Textbox(
929
+ label="Tu Guion Completo",
930
+ lines=5,
931
+ placeholder="Ej: En este video exploraremos los misterios del océano...",
932
+ max_lines=10,
933
+ value="",
934
+ # Hacer visible/oculto por el evento change
935
+ visible=False # Oculto por defecto
936
+ )
937
 
938
  musica_input = gr.Audio(
939
  label="Música de fondo (opcional)",
 
946
 
947
  with gr.Column():
948
  video_output = gr.Video(
949
+ label="Previsualización del Video Generado",
950
  interactive=False,
951
  height=400
952
  )
 
953
  file_output = gr.File(
954
+ label="Descargar Archivo de Video",
955
  interactive=False,
956
  visible=False # Ocultar inicialmente
957
  )
 
963
  value="Esperando entrada..."
964
  )
965
 
966
+ # Evento para mostrar/ocultar los campos de texto según el tipo de prompt
967
+ # Ahora usamos las Columnas para controlar la visibilidad
968
  prompt_type.change(
969
  lambda x: (gr.update(visible=x == "Generar Guion con IA"),
970
  gr.update(visible=x == "Usar Mi Guion")),
971
  inputs=prompt_type,
972
+ outputs=[prompt_ia.parent, prompt_manual.parent] # Apuntar a las Columnas padre
973
  )
974
 
975
+ # Evento click del botón de generar video
976
  generate_btn.click(
977
+ # Acción 1 (síncrona): Resetear salidas y establecer estado a procesando
978
+ lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar 2-5 minutos o más para videos largos.", interactive=False)),
979
  outputs=[video_output, file_output, status_output],
980
+ queue=True, # Usar la cola de Gradio para tareas largas
981
  ).then(
982
+ # Acción 2 (asíncrona): Llamar a la función principal de procesamiento
983
  run_app,
984
+ # CAMBIO CRÍTICO: Pasar los 4 argumentos definidos por run_app
985
  inputs=[prompt_type, prompt_ia, prompt_manual, musica_input],
986
+ outputs=[video_output, file_output, status_output] # Coincidir las 3 salidas de run_app
987
  ).then(
988
+ # Acción 3 (síncrona): Hacer visible el enlace de descarga si se retornó un archivo
989
+ # Verificar si file_output tiene un valor (el path)
990
+ lambda video_path, file_path: gr.update(visible=file_path is not None),
991
+ inputs=[video_output, file_output], # Usar ambas salidas como entrada para esta función
992
+ outputs=[file_output] # Actualizar visibilidad del componente file_output
993
  )
994
 
995
 
 
1024
 
1025
  logger.info("Iniciando aplicación Gradio...")
1026
  try:
1027
+ # Gradio Queue maneja tareas largas, no es necesario un ajuste global de timeout aquí.
1028
+ # El timeout se gestiona por solicitud o por el límite del worker de la cola.
1029
  app.launch(server_name="0.0.0.0", server_port=7860, share=False)
1030
  except Exception as e:
1031
  logger.critical(f"No se pudo iniciar la app: {str(e)}", exc_info=True)