Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -154,24 +154,10 @@ def extract_keywords(text: str) -> list[str]:
|
|
| 154 |
clean_text = re.sub(r"[^\w\s谩茅铆贸煤帽脕脡脥脫脷脩]", "", text.lower())
|
| 155 |
kws = local_kw_model.extract_keywords(clean_text, stop_words="spanish", top_n=5)
|
| 156 |
keywords = [k.replace(" ", "+") for k, _ in kws if k]
|
| 157 |
-
return keywords if keywords else ["mystery", "conspiracy", "alien", "UFO", "secret", "cover-up", "illusion", "paranoia"
|
| 158 |
-
"secret society", "lie", "simulation", "matrix", "terror", "darkness", "shadow", "enigma",
|
| 159 |
-
"urban legend", "unknown", "hidden", "mistrust", "experiment", "government", "control",
|
| 160 |
-
"surveillance", "propaganda", "deception", "whistleblower", "anomaly", "extraterrestrial",
|
| 161 |
-
"shadow government", "cabal", "deep state", "new world order", "mind control", "brainwashing",
|
| 162 |
-
"disinformation", "false flag", "assassin", "black ops", "anomaly", "men in black", "abduction",
|
| 163 |
-
"hybrid", "ancient aliens", "hollow earth", "simulation theory", "alternate reality", "predictive programming",
|
| 164 |
-
"symbolism", "occult", "eerie", "haunting", "unexplained", "forbidden knowledge", "redacted", "conspiracy theorist"]
|
| 165 |
except Exception as e:
|
| 166 |
logger.error(f"Error extrayendo keywords: {e}")
|
| 167 |
-
return ["mystery", "conspiracy", "alien", "UFO", "secret", "cover-up", "illusion", "paranoia"
|
| 168 |
-
"secret society", "lie", "simulation", "matrix", "terror", "darkness", "shadow", "enigma",
|
| 169 |
-
"urban legend", "unknown", "hidden", "mistrust", "experiment", "government", "control",
|
| 170 |
-
"surveillance", "propaganda", "deception", "whistleblower", "anomaly", "extraterrestrial",
|
| 171 |
-
"shadow government", "cabal", "deep state", "new world order", "mind control", "brainwashing",
|
| 172 |
-
"disinformation", "false flag", "assassin", "black ops", "anomaly", "men in black", "abduction",
|
| 173 |
-
"hybrid", "ancient aliens", "hollow earth", "simulation theory", "alternate reality", "predictive programming",
|
| 174 |
-
"symbolism", "occult", "eerie", "haunting", "unexplained", "forbidden knowledge", "redacted", "conspiracy theorist"]
|
| 175 |
|
| 176 |
def search_pexels_videos(query: str, count: int = 3) -> list[dict]:
|
| 177 |
if not PEXELS_API_KEY:
|
|
@@ -211,54 +197,6 @@ def download_video(url: str, folder: str) -> str | None:
|
|
| 211 |
logger.error(f"Error descargando video {url}: {e}")
|
| 212 |
return None
|
| 213 |
|
| 214 |
-
def create_subtitle_clips(script: str, video_width: int, video_height: int, duration: float):
|
| 215 |
-
try:
|
| 216 |
-
sentences = [s.strip() for s in re.split(r"[.!?驴隆]", script) if s.strip()]
|
| 217 |
-
if not sentences:
|
| 218 |
-
return []
|
| 219 |
-
|
| 220 |
-
total_words = sum(len(s.split()) for s in sentences) or 1
|
| 221 |
-
time_per_word = duration / total_words
|
| 222 |
-
|
| 223 |
-
clips = []
|
| 224 |
-
current_time = 0.0
|
| 225 |
-
|
| 226 |
-
for sentence in sentences:
|
| 227 |
-
num_words = len(sentence.split())
|
| 228 |
-
sentence_duration = num_words * time_per_word
|
| 229 |
-
|
| 230 |
-
if sentence_duration < 0.5:
|
| 231 |
-
continue
|
| 232 |
-
|
| 233 |
-
try:
|
| 234 |
-
txt_clip = (
|
| 235 |
-
TextClip(
|
| 236 |
-
sentence,
|
| 237 |
-
fontsize=max(20, int(video_height * 0.05)),
|
| 238 |
-
color="white",
|
| 239 |
-
stroke_color="black",
|
| 240 |
-
stroke_width=2,
|
| 241 |
-
method="caption",
|
| 242 |
-
size=(int(video_width * 0.9), None),
|
| 243 |
-
font="Arial-Bold"
|
| 244 |
-
)
|
| 245 |
-
.set_start(current_time)
|
| 246 |
-
.set_duration(sentence_duration)
|
| 247 |
-
.set_position(("center", "bottom"))
|
| 248 |
-
)
|
| 249 |
-
if txt_clip is not None:
|
| 250 |
-
clips.append(txt_clip)
|
| 251 |
-
except Exception as e:
|
| 252 |
-
logger.error(f"Error creando subt铆tulo para '{sentence}': {e}")
|
| 253 |
-
continue
|
| 254 |
-
|
| 255 |
-
current_time += sentence_duration
|
| 256 |
-
|
| 257 |
-
return clips
|
| 258 |
-
except Exception as e:
|
| 259 |
-
logger.error(f"Error creando subt铆tulos: {e}")
|
| 260 |
-
return []
|
| 261 |
-
|
| 262 |
def loop_audio_to_duration(audio_clip: AudioFileClip, target_duration: float) -> AudioFileClip:
|
| 263 |
if audio_clip is None:
|
| 264 |
return None
|
|
@@ -277,6 +215,7 @@ def create_video(script_text: str, generate_script: bool, music_path: str | None
|
|
| 277 |
temp_dir = tempfile.mkdtemp()
|
| 278 |
TARGET_FPS = 24
|
| 279 |
TARGET_RESOLUTION = (1280, 720)
|
|
|
|
| 280 |
|
| 281 |
def normalize_clip(clip):
|
| 282 |
if clip is None:
|
|
@@ -318,19 +257,22 @@ def create_video(script_text: str, generate_script: bool, music_path: str | None
|
|
| 318 |
if video_duration < 1:
|
| 319 |
raise ValueError("El audio generado es demasiado corto")
|
| 320 |
|
| 321 |
-
# Paso 3: Buscar y descargar videos
|
| 322 |
update_task_progress(task_id, "Paso 3/7: Buscando videos en Pexels...")
|
| 323 |
video_paths = []
|
| 324 |
keywords = extract_keywords(script)
|
| 325 |
|
| 326 |
-
|
| 327 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
|
| 329 |
-
videos = search_pexels_videos(keyword,
|
| 330 |
for video_data in videos:
|
| 331 |
-
if len(video_paths) >= 6:
|
| 332 |
-
break
|
| 333 |
-
|
| 334 |
video_files = video_data.get("video_files", [])
|
| 335 |
if video_files:
|
| 336 |
# Encontrar el video con la mejor calidad que sea MP4
|
|
@@ -349,15 +291,21 @@ def create_video(script_text: str, generate_script: bool, music_path: str | None
|
|
| 349 |
downloaded_path = download_video(video_url, temp_dir)
|
| 350 |
if downloaded_path:
|
| 351 |
video_paths.append(downloaded_path)
|
|
|
|
|
|
|
| 352 |
|
| 353 |
if not video_paths:
|
| 354 |
raise RuntimeError("No se pudieron descargar videos de Pexels")
|
| 355 |
|
| 356 |
# Paso 4: Procesar videos - MANEJO CORRECTO DE ERRORES
|
| 357 |
-
update_task_progress(task_id, f"Paso 4/7: Procesando
|
| 358 |
video_clips = []
|
|
|
|
| 359 |
|
| 360 |
for path in video_paths:
|
|
|
|
|
|
|
|
|
|
| 361 |
clip = None
|
| 362 |
try:
|
| 363 |
# Cargar el video con verificaci贸n adicional
|
|
@@ -374,10 +322,15 @@ def create_video(script_text: str, generate_script: bool, music_path: str | None
|
|
| 374 |
clip.close()
|
| 375 |
continue
|
| 376 |
|
| 377 |
-
# Tomar m谩ximo 8 segundos de cada clip
|
| 378 |
try:
|
| 379 |
-
|
| 380 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
except Exception as e:
|
| 382 |
logger.error(f"Error al recortar video {path}: {e}")
|
| 383 |
clip.close()
|
|
@@ -388,6 +341,7 @@ def create_video(script_text: str, generate_script: bool, music_path: str | None
|
|
| 388 |
processed_clip = normalize_clip(processed_clip)
|
| 389 |
if processed_clip is not None:
|
| 390 |
video_clips.append(processed_clip)
|
|
|
|
| 391 |
else:
|
| 392 |
if 'processed_clip' in locals():
|
| 393 |
processed_clip.close()
|
|
@@ -411,11 +365,6 @@ def create_video(script_text: str, generate_script: bool, music_path: str | None
|
|
| 411 |
# Concatenar videos
|
| 412 |
base_video = concatenate_videoclips(video_clips, method="chain")
|
| 413 |
|
| 414 |
-
# Extender video si es m谩s corto que el audio
|
| 415 |
-
if base_video.duration < video_duration:
|
| 416 |
-
loops_needed = math.ceil(video_duration / base_video.duration)
|
| 417 |
-
base_video = concatenate_videoclips([base_video] * loops_needed)
|
| 418 |
-
|
| 419 |
# Asegurar duraci贸n exacta
|
| 420 |
base_video = base_video.subclip(0, video_duration)
|
| 421 |
|
|
@@ -432,14 +381,8 @@ def create_video(script_text: str, generate_script: bool, music_path: str | None
|
|
| 432 |
else:
|
| 433 |
final_audio = voice_clip
|
| 434 |
|
| 435 |
-
# Paso 6:
|
| 436 |
-
update_task_progress(task_id, "Paso 6/7:
|
| 437 |
-
subtitle_clips = create_subtitle_clips(script, base_video.w, base_video.h, video_duration)
|
| 438 |
-
if subtitle_clips:
|
| 439 |
-
base_video = CompositeVideoClip([base_video] + subtitle_clips)
|
| 440 |
-
|
| 441 |
-
# Paso 7: Renderizar video final
|
| 442 |
-
update_task_progress(task_id, "Paso 7/7: Renderizando video final...")
|
| 443 |
final_video = base_video.set_audio(final_audio)
|
| 444 |
|
| 445 |
output_path = os.path.join(RESULTS_DIR, f"video_{task_id}.mp4")
|
|
@@ -455,6 +398,9 @@ def create_video(script_text: str, generate_script: bool, music_path: str | None
|
|
| 455 |
verbose=False
|
| 456 |
)
|
| 457 |
|
|
|
|
|
|
|
|
|
|
| 458 |
# Limpiar clips
|
| 459 |
voice_clip.close()
|
| 460 |
if 'music_clip' in locals():
|
|
@@ -564,7 +510,6 @@ with gr.Blocks(title="馃幀 Generador de Videos IA", theme=gr.themes.Soft()) as d
|
|
| 564 |
- **Edge TTS** para voz en espa帽ol
|
| 565 |
- **GPT-2** para generaci贸n de guiones
|
| 566 |
- **Pexels API** para videos de stock
|
| 567 |
-
- **Subt铆tulos autom谩ticos** y efectos visuales
|
| 568 |
|
| 569 |
El progreso se mostrar谩 en tiempo real.
|
| 570 |
""")
|
|
|
|
| 154 |
clean_text = re.sub(r"[^\w\s谩茅铆贸煤帽脕脡脥脫脷脩]", "", text.lower())
|
| 155 |
kws = local_kw_model.extract_keywords(clean_text, stop_words="spanish", top_n=5)
|
| 156 |
keywords = [k.replace(" ", "+") for k, _ in kws if k]
|
| 157 |
+
return keywords if keywords else ["mystery", "conspiracy", "alien", "UFO", "secret", "cover-up", "illusion", "paranoia"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
except Exception as e:
|
| 159 |
logger.error(f"Error extrayendo keywords: {e}")
|
| 160 |
+
return ["mystery", "conspiracy", "alien", "UFO", "secret", "cover-up", "illusion", "paranoia"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
|
| 162 |
def search_pexels_videos(query: str, count: int = 3) -> list[dict]:
|
| 163 |
if not PEXELS_API_KEY:
|
|
|
|
| 197 |
logger.error(f"Error descargando video {url}: {e}")
|
| 198 |
return None
|
| 199 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
def loop_audio_to_duration(audio_clip: AudioFileClip, target_duration: float) -> AudioFileClip:
|
| 201 |
if audio_clip is None:
|
| 202 |
return None
|
|
|
|
| 215 |
temp_dir = tempfile.mkdtemp()
|
| 216 |
TARGET_FPS = 24
|
| 217 |
TARGET_RESOLUTION = (1280, 720)
|
| 218 |
+
MAX_CLIP_DURATION = 8 # M谩ximo de segundos por clip
|
| 219 |
|
| 220 |
def normalize_clip(clip):
|
| 221 |
if clip is None:
|
|
|
|
| 257 |
if video_duration < 1:
|
| 258 |
raise ValueError("El audio generado es demasiado corto")
|
| 259 |
|
| 260 |
+
# Paso 3: Buscar y descargar videos (adaptado a la duraci贸n del audio)
|
| 261 |
update_task_progress(task_id, "Paso 3/7: Buscando videos en Pexels...")
|
| 262 |
video_paths = []
|
| 263 |
keywords = extract_keywords(script)
|
| 264 |
|
| 265 |
+
# Calcular cu谩ntos clips necesitamos aproximadamente
|
| 266 |
+
estimated_clips_needed = max(1, math.ceil(video_duration / MAX_CLIP_DURATION))
|
| 267 |
+
|
| 268 |
+
for i, keyword in enumerate(keywords):
|
| 269 |
+
if len(video_paths) >= estimated_clips_needed * 2: # Buscar el doble para tener opciones
|
| 270 |
+
break
|
| 271 |
+
|
| 272 |
+
update_task_progress(task_id, f"Paso 3/7: Buscando videos para '{keyword}' ({i+1}/{len(keywords)})")
|
| 273 |
|
| 274 |
+
videos = search_pexels_videos(keyword, 3) # Buscar 3 videos por keyword
|
| 275 |
for video_data in videos:
|
|
|
|
|
|
|
|
|
|
| 276 |
video_files = video_data.get("video_files", [])
|
| 277 |
if video_files:
|
| 278 |
# Encontrar el video con la mejor calidad que sea MP4
|
|
|
|
| 291 |
downloaded_path = download_video(video_url, temp_dir)
|
| 292 |
if downloaded_path:
|
| 293 |
video_paths.append(downloaded_path)
|
| 294 |
+
if len(video_paths) >= estimated_clips_needed * 2:
|
| 295 |
+
break
|
| 296 |
|
| 297 |
if not video_paths:
|
| 298 |
raise RuntimeError("No se pudieron descargar videos de Pexels")
|
| 299 |
|
| 300 |
# Paso 4: Procesar videos - MANEJO CORRECTO DE ERRORES
|
| 301 |
+
update_task_progress(task_id, f"Paso 4/7: Procesando videos...")
|
| 302 |
video_clips = []
|
| 303 |
+
total_duration = 0
|
| 304 |
|
| 305 |
for path in video_paths:
|
| 306 |
+
if total_duration >= video_duration:
|
| 307 |
+
break
|
| 308 |
+
|
| 309 |
clip = None
|
| 310 |
try:
|
| 311 |
# Cargar el video con verificaci贸n adicional
|
|
|
|
| 322 |
clip.close()
|
| 323 |
continue
|
| 324 |
|
| 325 |
+
# Tomar m谩ximo 8 segundos de cada clip o lo que necesitemos
|
| 326 |
try:
|
| 327 |
+
clip_duration = min(MAX_CLIP_DURATION, clip.duration)
|
| 328 |
+
# Si ya tenemos suficiente duraci贸n, tomar solo lo necesario
|
| 329 |
+
remaining_duration = video_duration - total_duration
|
| 330 |
+
if remaining_duration < clip_duration:
|
| 331 |
+
clip_duration = remaining_duration
|
| 332 |
+
|
| 333 |
+
processed_clip = clip.subclip(0, clip_duration)
|
| 334 |
except Exception as e:
|
| 335 |
logger.error(f"Error al recortar video {path}: {e}")
|
| 336 |
clip.close()
|
|
|
|
| 341 |
processed_clip = normalize_clip(processed_clip)
|
| 342 |
if processed_clip is not None:
|
| 343 |
video_clips.append(processed_clip)
|
| 344 |
+
total_duration += processed_clip.duration
|
| 345 |
else:
|
| 346 |
if 'processed_clip' in locals():
|
| 347 |
processed_clip.close()
|
|
|
|
| 365 |
# Concatenar videos
|
| 366 |
base_video = concatenate_videoclips(video_clips, method="chain")
|
| 367 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
# Asegurar duraci贸n exacta
|
| 369 |
base_video = base_video.subclip(0, video_duration)
|
| 370 |
|
|
|
|
| 381 |
else:
|
| 382 |
final_audio = voice_clip
|
| 383 |
|
| 384 |
+
# Paso 6: Renderizar video final
|
| 385 |
+
update_task_progress(task_id, "Paso 6/7: Renderizando video final...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
final_video = base_video.set_audio(final_audio)
|
| 387 |
|
| 388 |
output_path = os.path.join(RESULTS_DIR, f"video_{task_id}.mp4")
|
|
|
|
| 398 |
verbose=False
|
| 399 |
)
|
| 400 |
|
| 401 |
+
# Paso 7: Limpiar recursos
|
| 402 |
+
update_task_progress(task_id, "Paso 7/7: Finalizando...")
|
| 403 |
+
|
| 404 |
# Limpiar clips
|
| 405 |
voice_clip.close()
|
| 406 |
if 'music_clip' in locals():
|
|
|
|
| 510 |
- **Edge TTS** para voz en espa帽ol
|
| 511 |
- **GPT-2** para generaci贸n de guiones
|
| 512 |
- **Pexels API** para videos de stock
|
|
|
|
| 513 |
|
| 514 |
El progreso se mostrar谩 en tiempo real.
|
| 515 |
""")
|