gnosticdev commited on
Commit
8337d0b
·
verified ·
1 Parent(s): 9e5ee0a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +63 -330
app.py CHANGED
@@ -1,349 +1,82 @@
1
  import os
2
- import re
3
- import random
4
- import time
5
- import logging
6
- from typing import Optional, List, Dict
7
  from datetime import datetime
8
- from pathlib import Path
9
 
10
- # Configuración inicial para HF Spaces
11
- os.environ["TOKENIZERS_PARALLELISM"] = "false"
12
- os.environ["GRADIO_ANALYTICS_ENABLED"] = "false"
13
- os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1"
14
 
15
- # Configuración de logging
16
- logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
17
- logger = logging.getLogger(__name__)
 
 
 
 
 
18
 
19
- try:
20
- import requests
21
- from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip
22
- from moviepy.audio.fx.all import audio_loop
23
- import edge_tts
24
- import gradio as gr
25
- import numpy as np
26
- from transformers import pipeline
27
- import backoff
28
- from pydub import AudioSegment
29
- except ImportError as e:
30
- logger.error(f"Error importing dependencies: {e}")
31
- raise
32
-
33
- # Constantes configurables
34
- MAX_VIDEOS = 3
35
- VIDEO_SEGMENT_DURATION = 5
36
- MAX_RETRIES = 3
37
- REQUEST_TIMEOUT = 15
38
-
39
- # Voces disponibles en Edge TTS (español)
40
- VOICES = {
41
- "Femenino MX": "es-MX-DaliaNeural",
42
- "Masculino MX": "es-MX-JorgeNeural",
43
- "Femenino ES": "es-ES-ElviraNeural",
44
- "Masculino ES": "es-ES-AlvaroNeural",
45
- "Femenino CO": "es-CO-SalomeNeural",
46
- "Masculino CO": "es-CO-GonzaloNeural",
47
- "Femenino AR": "es-AR-ElenaNeural",
48
- "Masculino AR": "es-AR-TomasNeural"
49
- }
50
-
51
- # Configuración de modelos
52
- MODEL_NAME = "facebook/mbart-large-50"
53
- PEXELS_API_KEY = os.getenv("PEXELS_API_KEY", "")
54
-
55
- @backoff.on_exception(backoff.expo,
56
- (requests.exceptions.RequestException,
57
- requests.exceptions.HTTPError),
58
- max_tries=MAX_RETRIES,
59
- max_time=30)
60
- def safe_download(url: str, timeout: int = REQUEST_TIMEOUT) -> Optional[str]:
61
- """Descarga segura con reintentos"""
62
- try:
63
- response = requests.get(url, stream=True, timeout=timeout)
64
- response.raise_for_status()
65
-
66
- filename = f"temp_{random.randint(1000,9999)}.mp4"
67
-
68
- with open(filename, 'wb') as f:
69
- for chunk in response.iter_content(chunk_size=8192):
70
- f.write(chunk)
71
-
72
- return filename
73
-
74
- except requests.exceptions.HTTPError as e:
75
- if e.response.status_code == 429:
76
- retry_after = int(e.response.headers.get('Retry-After', 5))
77
- logger.warning(f"Rate limited. Waiting {retry_after} seconds...")
78
- time.sleep(retry_after)
79
- logger.error(f"Download failed: {str(e)}")
80
- return None
81
- except Exception as e:
82
- logger.error(f"Unexpected download error: {str(e)}")
83
- return None
84
-
85
- def process_music(music_path: str, target_duration: float) -> str:
86
- """Procesa música para loop y duración correcta"""
87
- processed_path = "processed_music.mp3"
88
- try:
89
- audio = AudioSegment.from_file(music_path)
90
-
91
- # Crear loop si es más corto que el video
92
- if len(audio) < target_duration * 1000:
93
- loops_needed = int(target_duration * 1000 / len(audio)) + 1
94
- audio = audio * loops_needed
95
-
96
- # Recortar a la duración exacta
97
- audio = audio[:int(target_duration * 1000)]
98
- audio.export(processed_path, format="mp3")
99
- return processed_path
100
- except Exception as e:
101
- logger.error(f"Error processing music: {str(e)}")
102
- return music_path # Fallback al original
103
-
104
- def download_video_segment(url: str, duration: float, output_path: str) -> bool:
105
- """Descarga y procesa un segmento de video"""
106
- temp_path = None
107
- try:
108
- temp_path = safe_download(url)
109
- if not temp_path:
110
- return False
111
-
112
- with VideoFileClip(temp_path) as clip:
113
- if clip.duration < 1:
114
- logger.error("Video demasiado corto")
115
- return False
116
-
117
- end_time = min(duration, clip.duration - 0.1)
118
- subclip = clip.subclip(0, end_time)
119
-
120
- subclip.write_videofile(
121
- output_path,
122
- codec="libx264",
123
- audio_codec="aac",
124
- threads=2,
125
- preset='ultrafast',
126
- verbose=False,
127
- ffmpeg_params=['-max_muxing_queue_size', '1024']
128
- )
129
- return True
130
-
131
- except Exception as e:
132
- logger.error(f"Video processing error: {str(e)}")
133
- return False
134
- finally:
135
- if temp_path and os.path.exists(temp_path):
136
- os.remove(temp_path)
137
-
138
- def fetch_pexels_videos(query: str) -> List[str]:
139
- """Busca videos en Pexels"""
140
- if not PEXELS_API_KEY:
141
- logger.error("PEXELS_API_KEY no configurada")
142
- return []
143
-
144
- headers = {"Authorization": PEXELS_API_KEY}
145
- url = f"https://api.pexels.com/videos/search?query={query}&per_page={MAX_VIDEOS}"
146
-
147
- try:
148
- response = requests.get(url, headers=headers, timeout=REQUEST_TIMEOUT)
149
- response.raise_for_status()
150
-
151
- videos = []
152
- for video in response.json().get("videos", [])[:MAX_VIDEOS]:
153
- video_files = [vf for vf in video.get("video_files", [])
154
- if vf.get("width", 0) >= 720]
155
- if video_files:
156
- best_file = max(video_files, key=lambda x: x.get("width", 0))
157
- videos.append(best_file["link"])
158
-
159
- return videos
160
-
161
- except Exception as e:
162
- logger.error(f"Error fetching Pexels videos: {str(e)}")
163
- return []
164
-
165
- def generate_script(prompt: str, custom_script: Optional[str] = None) -> str:
166
- """Genera un script usando IA o custom text"""
167
- if custom_script and custom_script.strip():
168
- return custom_script.strip()
169
-
170
- try:
171
- generator = pipeline("text-generation", model=MODEL_NAME)
172
- result = generator(
173
- f"Genera un guion breve sobre {prompt} en español con {MAX_VIDEOS} puntos:",
174
- max_length=200,
175
- num_return_sequences=1
176
- )[0]['generated_text']
177
- return result
178
- except Exception as e:
179
- logger.error(f"Error generating script: {str(e)}")
180
- return f"1. Punto uno sobre {prompt}\n2. Punto dos\n3. Punto tres"
181
-
182
- async def generate_voice(text: str, voice_id: str, output_file: str = "voice.mp3") -> bool:
183
- """Genera narración de voz"""
184
- try:
185
- communicate = edge_tts.Communicate(text, voice=voice_id)
186
- await communicate.save(output_file)
187
- return True
188
- except Exception as e:
189
- logger.error(f"Voice generation failed: {str(e)}")
190
- return False
191
-
192
- def run_async(coro):
193
- """Ejecuta corrutinas asíncronas"""
194
- import asyncio
195
- loop = asyncio.new_event_loop()
196
- asyncio.set_event_loop(loop)
197
- try:
198
- return loop.run_until_complete(coro)
199
- finally:
200
- loop.close()
201
-
202
- def create_video(
203
- prompt: str,
204
- custom_script: Optional[str] = None,
205
- voice_choice: str = "es-MX-DaliaNeural",
206
- music_file: Optional[str] = None
207
- ) -> Optional[str]:
208
- """Función principal para crear el video"""
209
  try:
210
- # 1. Generar contenido
211
- script = generate_script(prompt, custom_script)
212
- logger.info(f"Script generado: {script[:100]}...")
213
-
214
- # 2. Buscar videos
215
- video_urls = fetch_pexels_videos(prompt)
216
- if not video_urls:
217
- logger.error("No se encontraron videos")
218
- return None
219
-
220
- # 3. Generar voz
221
- voice_file = "voice.mp3"
222
- if not run_async(generate_voice(script, voice_choice, voice_file)):
223
- logger.error("No se pudo generar voz")
224
- return None
225
-
226
- # 4. Procesar música si existe
227
- music_path = None
228
- if music_file:
229
- audio_clip = AudioFileClip(voice_file)
230
- target_duration = audio_clip.duration
231
- audio_clip.close()
232
-
233
- music_path = process_music(music_file.name, target_duration)
234
-
235
- # 5. Procesar videos
236
- output_dir = "output"
237
- os.makedirs(output_dir, exist_ok=True)
238
- output_path = os.path.join(output_dir, f"video_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
239
-
240
- clips = []
241
- segment_duration = VIDEO_SEGMENT_DURATION
242
-
243
- for i, url in enumerate(video_urls):
244
- clip_path = f"segment_{i}.mp4"
245
- if download_video_segment(url, segment_duration, clip_path):
246
- clips.append(VideoFileClip(clip_path))
247
-
248
- if not clips:
249
- logger.error("No se pudieron procesar los videos")
250
- return None
251
-
252
- # 6. Ensamblar video final
253
- final_video = concatenate_videoclips(clips, method="compose")
254
- voice_audio = AudioFileClip(voice_file)
255
-
256
- if music_path:
257
- music_audio = AudioFileClip(music_path)
258
- final_audio = CompositeAudioClip([voice_audio, music_audio.volumex(0.3)])
259
- else:
260
- final_audio = voice_audio
261
-
262
- final_video = final_video.set_audio(final_audio)
263
-
264
- final_video.write_videofile(
265
- output_path,
266
- codec="libx264",
267
- audio_codec="aac",
268
- threads=2,
269
- preset='ultrafast',
270
- verbose=False
271
- )
272
 
273
  return output_path
274
-
275
  except Exception as e:
276
- logger.error(f"Error creating video: {str(e)}")
277
  return None
278
- finally:
279
- # Limpieza
280
- for clip in clips:
281
- clip.close()
282
- if os.path.exists(voice_file):
283
- os.remove(voice_file)
284
- if music_path and os.path.exists(music_path):
285
- os.remove(music_path)
286
- for i in range(len(video_urls)):
287
- if os.path.exists(f"segment_{i}.mp4"):
288
- os.remove(f"segment_{i}.mp4")
289
 
290
- # Interfaz Gradio completa
291
- with gr.Blocks(title="Generador de Videos Avanzado", theme=gr.themes.Soft()) as app:
292
- gr.Markdown("# 🎬 Generador de Videos con IA")
293
 
294
  with gr.Row():
295
  with gr.Column():
296
- prompt_input = gr.Textbox(
297
- label="Tema del video",
298
- placeholder="Ej: Lugares turísticos de Argentina",
299
- max_lines=2
300
- )
301
-
302
- custom_script_input = gr.TextArea(
303
- label="Guion personalizado (opcional)",
304
- placeholder="Pega aquí tu propio guion si lo tienes...",
305
- lines=5
306
- )
307
-
308
- voice_dropdown = gr.Dropdown(
309
  label="Selecciona una voz",
310
- choices=list(VOICES.keys()),
311
- value="Femenino MX"
312
- )
313
-
314
- music_input = gr.File(
315
- label="Música de fondo (opcional)",
316
- type="file",
317
- file_types=["audio"]
318
  )
319
-
320
- generate_btn = gr.Button("Generar Video", variant="primary")
321
-
322
  with gr.Column():
323
- output_video = gr.Video(
324
- label="Video Resultante",
325
- interactive=False,
326
- format="mp4"
327
- )
328
-
329
- generate_btn.click(
330
- fn=create_video,
331
- inputs=[
332
- prompt_input,
333
- custom_script_input,
334
- gr.Dropdown(value="es-MX-DaliaNeural", visible=False), # Valor real de voz
335
- music_input
336
- ],
337
- outputs=output_video
338
- )
339
-
340
- # Actualizar el valor de voz real cuando cambia el dropdown
341
- voice_dropdown.change(
342
- lambda x: VOICES[x],
343
- inputs=voice_dropdown,
344
- outputs=gr.Dropdown(visible=False)
345
  )
346
 
347
- # Para Hugging Face Spaces
348
- if __name__ == "__main__":
349
- app.launch(server_name="0.0.0.0", server_port=7860)
 
1
  import os
2
+ import edge_tts
3
+ import gradio as gr
4
+ from moviepy.editor import *
5
+ from transformers import pipeline
6
+ import requests
7
  from datetime import datetime
 
8
 
9
+ # 1. Configuración inicial
10
+ PEXELS_API_KEY = os.getenv("PEXELS_API_KEY") # ¡Añade tu API Key en Hugging Face!
 
 
11
 
12
+ # 2. Generar guion con IA (si no se proporciona uno manual)
13
+ def generar_guion(prompt):
14
+ generator = pipeline("text-generation", model="facebook/mbart-large-50")
15
+ return generator(
16
+ f"Genera un guion para video sobre '{prompt}' (4 puntos breves):",
17
+ max_length=200,
18
+ num_return_sequences=1
19
+ )[0]['generated_text']
20
 
21
+ # 3. Función principal para crear el video
22
+ def crear_video(prompt, script_personalizado, voz, musica=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  try:
24
+ # A. Usar guion personalizado o generar uno automático
25
+ guion = script_personalizado if script_personalizado else generar_guion(prompt)
26
+
27
+ # B. Generar voz (Edge-TTS)
28
+ edge_tts.Communicate(guion, voice=voz).save("voz.mp3")
29
+
30
+ # C. Buscar videos en Pexels (relacionados con el guion)
31
+ headers = {"Authorization": PEXELS_API_KEY}
32
+ query = guion[:50].replace(" ", "+") # Usar las primeras 50 palabras
33
+ videos = requests.get(
34
+ f"https://api.pexels.com/videos/search?query={query}&per_page=2",
35
+ headers=headers
36
+ ).json().get("videos", [])
37
+
38
+ # D. Procesar música (hacer loop si es necesario)
39
+ audio = AudioFileClip("voz.mp3")
40
+ if musica:
41
+ musica_clip = AudioFileClip(musica.name)
42
+ if musica_clip.duration < audio.duration:
43
+ musica_clip = musica_clip.loop(duration=audio.duration)
44
+ audio = CompositeAudioClip([audio, musica_clip.volumex(0.3)])
45
+
46
+ # E. Crear y exportar el video
47
+ clips = [VideoFileClip(v["video_files"][0]["link"]).subclip(0, 5) for v in videos[:2]]
48
+ final_clip = concatenate_videoclips(clips).set_audio(audio)
49
+ output_path = f"video_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4"
50
+ final_clip.write_videofile(output_path, codec="libx264", audio_codec="aac", threads=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
  return output_path
 
53
  except Exception as e:
54
+ print(f"Error: {e}")
55
  return None
 
 
 
 
 
 
 
 
 
 
 
56
 
57
+ # 4. Interfaz de usuario (Gradio)
58
+ with gr.Blocks(title="Generador de Videos") as app:
59
+ gr.Markdown("# 🎥 Generador Automático de Videos")
60
 
61
  with gr.Row():
62
  with gr.Column():
63
+ prompt = gr.Textbox(label="Tema del video", placeholder="Ej: 'Tecnología en 2024'")
64
+ script = gr.TextArea(label="Guion personalizado (opcional)", lines=5)
65
+ voz = gr.Dropdown(
 
 
 
 
 
 
 
 
 
 
66
  label="Selecciona una voz",
67
+ choices=[v["Name"] for v in edge_tts.list_voices()],
68
+ value="es-MX-DaliaNeural"
 
 
 
 
 
 
69
  )
70
+ musica = gr.File(label="Música de fondo (opcional)", file_types=[".mp3"])
71
+ btn = gr.Button("Generar Video", variant="primary")
72
+
73
  with gr.Column():
74
+ output = gr.Video(label="Resultado", autoplay=True)
75
+
76
+ btn.click(
77
+ fn=crear_video,
78
+ inputs=[prompt, script, voz, musica],
79
+ outputs=output
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  )
81
 
82
+ app.launch()