gnosticdev commited on
Commit
720c3d5
·
verified ·
1 Parent(s): bb873c1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +181 -242
app.py CHANGED
@@ -1,245 +1,228 @@
1
  import os
2
  import re
3
  import random
4
- import requests
5
- import gradio as gr
6
- from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip
7
- from moviepy.audio.fx.all import audio_loop
8
- import edge_tts
9
- import asyncio
10
  from datetime import datetime
11
  from pathlib import Path
12
- from transformers import pipeline
13
- from sentence_transformers import SentenceTransformer
14
- from sklearn.metrics.pairwise import cosine_similarity
15
- import numpy as np
16
- import logging
17
- from typing import List, Optional, Tuple
18
 
19
- # Configure logging
 
 
 
 
 
20
  logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
21
  logger = logging.getLogger(__name__)
22
 
23
- # Configuración de modelos de IA
24
- PEXELS_API_KEY = os.getenv("PEXELS_API_KEY")
25
- if not PEXELS_API_KEY:
26
- logger.error("PEXELS_API_KEY no encontrada en variables de entorno")
27
-
28
- # Cargamos modelos de IA para análisis semántico
29
- logger.info("Cargando modelos de IA...")
30
  try:
31
- # Modelo para generación de texto
32
- text_generator = pipeline("text-generation", model="facebook/mbart-large-50", device="cpu")
33
-
34
- # Modelo para embeddings semánticos (para matching de videos)
35
- semantic_model = SentenceTransformer('sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')
36
- logger.info("Modelos de IA cargados exitosamente")
37
- except Exception as e:
38
- logger.error(f"Error cargando modelos de IA: {e}")
 
 
39
  raise
40
 
41
- # Sistema mejorado de búsqueda semántica
42
- def fetch_semantic_videos(query: str, script: str, num_videos: int = 5) -> List[Tuple[str, float]]:
43
- """Busca videos en Pexels usando matching semántico con el script"""
44
- logger.info(f"Buscando videos semánticos para: '{query}'")
45
-
46
- # Generar embedding del script completo
47
- script_embedding = semantic_model.encode(script, convert_to_tensor=True)
48
-
49
- headers = {"Authorization": PEXELS_API_KEY}
50
- url = f"https://api.pexels.com/videos/search?query={query}&per_page={num_videos*2}" # Buscamos más para filtrar
51
-
 
 
 
 
 
 
52
  try:
53
- response = requests.get(url, headers=headers, timeout=15)
54
  response.raise_for_status()
55
 
56
- videos_data = []
57
- for video in response.json().get("videos", []):
58
- # Filtramos por calidad mínima
59
- video_files = [vf for vf in video.get("video_files", [])
60
- if vf.get("width", 0) >= 1280 and vf.get("duration", 0) >= 5]
61
-
62
- if video_files:
63
- best_file = max(video_files, key=lambda x: x.get("width", 0))
64
- video_title = video.get("alt", "") or video.get("url", "")
65
-
66
- # Calculamos similitud semántica
67
- title_embedding = semantic_model.encode(video_title, convert_to_tensor=True)
68
- similarity = cosine_similarity(
69
- script_embedding.cpu().numpy().reshape(1, -1),
70
- title_embedding.cpu().numpy().reshape(1, -1)
71
- )[0][0]
72
-
73
- videos_data.append((best_file["link"], similarity, video_title))
74
-
75
- # Ordenamos por relevancia semántica
76
- videos_data.sort(key=lambda x: x[1], reverse=True)
77
-
78
- # Filtramos los más relevantes
79
- selected_videos = videos_data[:num_videos]
80
-
81
- logger.info(f"Videos encontrados (relevancia):")
82
- for idx, (url, score, title) in enumerate(selected_videos, 1):
83
- logger.info(f"{idx}. {title} (score: {score:.2f})")
84
-
85
- return [url for url, _, _ in selected_videos]
86
-
87
- except Exception as e:
88
- logger.error(f"Error en búsqueda semántica: {e}")
89
- return []
90
-
91
- # Generación de script con contexto mejorado
92
- def generate_script(prompt: str, custom_text: Optional[str] = None) -> str:
93
- """Genera un script contextualizado con IA"""
94
- if custom_text and custom_text.strip():
95
- return custom_text.strip()
96
-
97
- if not prompt or not prompt.strip():
98
- return "Error: Proporciona un tema o guion"
99
-
100
- try:
101
- # Prompt mejorado para generación contextual
102
- context_prompt = f"""
103
- Genera un guion detallado para un video sobre '{prompt}'.
104
- El formato debe ser:
105
- 1. [Concepto 1]: Descripción breve (15-25 palabras)
106
- 2. [Concepto 2]: Descripción breve
107
- ...
108
- Incluye detalles visuales entre [] para ayudar a seleccionar imágenes.
109
- Ejemplo: [playa con palmeras] o [ciudad moderna con rascacielos]
110
- """
111
 
112
- generated = text_generator(
113
- context_prompt,
114
- max_length=400,
115
- num_return_sequences=1,
116
- do_sample=True,
117
- temperature=0.7,
118
- top_k=50,
119
- top_p=0.9
120
- )[0]['generated_text']
121
 
122
- # Post-procesamiento para limpiar el texto
123
- cleaned = re.sub(r"<.*?>", "", generated) # Remove HTML tags
124
- cleaned = re.sub(r"\n+", "\n", cleaned) # Remove extra newlines
125
- return cleaned.strip()
126
-
 
 
127
  except Exception as e:
128
- logger.error(f"Error generando script: {e}")
129
- return f"Top 10 sobre {prompt}: [ejemplo 1] Descripción breve..."
130
 
131
- # Sistema mejorado de descarga de videos
132
  def download_video_segment(url: str, duration: float, output_path: str) -> bool:
133
- """Descarga y procesa segmentos de video con manejo robusto"""
134
- temp_path = f"temp_{random.randint(1000,9999)}.mp4"
135
-
136
  try:
137
- # Descarga con verificación
138
- with requests.get(url, stream=True, timeout=20) as r:
139
- r.raise_for_status()
140
- with open(temp_path, 'wb') as f:
141
- for chunk in r.iter_content(chunk_size=1024*1024):
142
- if chunk:
143
- f.write(chunk)
144
-
145
- # Procesamiento con controles
146
- with VideoFileClip(temp_path) as clip:
147
- if clip.duration < 2:
148
- raise ValueError("Video demasiado corto")
149
 
 
 
 
 
 
150
  end_time = min(duration, clip.duration - 0.1)
151
  subclip = clip.subclip(0, end_time)
152
 
153
- # Configuración optimizada
154
  subclip.write_videofile(
155
  output_path,
156
  codec="libx264",
157
  audio_codec="aac",
158
- fps=24,
159
- threads=4,
160
- preset='fast',
161
  ffmpeg_params=[
162
  '-max_muxing_queue_size', '1024',
163
- '-crf', '23',
164
  '-movflags', '+faststart'
165
  ]
166
  )
167
-
168
  return True
169
-
170
  except Exception as e:
171
- logger.error(f"Error procesando video: {e}")
172
  return False
173
  finally:
174
- if os.path.exists(temp_path):
175
  os.remove(temp_path)
176
 
177
- # Función principal mejorada
178
- def create_contextual_video(prompt: str, custom_text: Optional[str] = None, music_file: Optional[str] = None) -> str:
179
- """Crea un video con matching semántico entre texto e imágenes"""
180
- # 1. Generación del script
181
- script = generate_script(prompt, custom_text)
182
- logger.info(f"Script generado:\n{script}")
183
-
184
- # 2. Búsqueda semántica de videos
185
- search_query = " ".join(extract_keywords(script)) or prompt
186
- video_urls = fetch_semantic_videos(search_query, script)
187
-
188
- if not video_urls:
189
- return "Error: No se encontraron videos relevantes. Intenta con otro tema."
190
-
191
- # 3. Generación de voz
192
- voice_file = f"voice_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp3"
193
- if not run_async(generate_voice(script, voice_file)):
194
- return "Error: No se pudo generar la narración."
195
 
196
- # 4. Procesamiento de videos
197
- output_dir = "output_videos"
198
- os.makedirs(output_dir, exist_ok=True)
199
- output_path = f"{output_dir}/video_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4"
 
 
 
 
 
 
 
 
 
200
 
 
 
 
 
 
 
201
  try:
202
- # Descargar y preparar segmentos
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  clips = []
204
- segment_duration = AudioFileClip(voice_file).duration / len(video_urls)
205
 
206
- for idx, url in enumerate(video_urls):
207
- clip_path = f"segment_{idx}.mp4"
208
  if download_video_segment(url, segment_duration, clip_path):
209
  clips.append(VideoFileClip(clip_path))
210
 
211
  if not clips:
212
- return "Error: No se pudieron procesar los videos."
213
-
214
- # 5. Ensamblaje final
 
215
  final_video = concatenate_videoclips(clips, method="compose")
216
  audio_clip = AudioFileClip(voice_file)
 
217
 
218
- # Añadir música de fondo si existe
219
- if music_file and os.path.exists(music_file.name):
220
- music = audio_loop(AudioFileClip(music_file.name), duration=audio_clip.duration)
221
- final_audio = CompositeAudioClip([audio_clip, music.volumex(0.2)])
222
- else:
223
- final_audio = audio_clip
224
-
225
- final_video = final_video.set_audio(final_audio)
226
-
227
- # Renderizado final optimizado
228
  final_video.write_videofile(
229
  output_path,
230
  codec="libx264",
231
  audio_codec="aac",
232
- fps=24,
233
- threads=6,
234
- preset='fast',
235
- bitrate="5000k"
236
  )
237
 
238
  return output_path
239
-
240
  except Exception as e:
241
- logger.error(f"Error crítico al crear video: {e}")
242
- return f"Error: Fallo en la creación del video - {str(e)}"
243
  finally:
244
  # Limpieza
245
  for clip in clips:
@@ -250,72 +233,28 @@ def create_contextual_video(prompt: str, custom_text: Optional[str] = None, musi
250
  if os.path.exists(f"segment_{i}.mp4"):
251
  os.remove(f"segment_{i}.mp4")
252
 
253
- # Interfaz mejorada
254
- with gr.Blocks(theme=gr.themes.Soft()) as demo:
255
- gr.Markdown("""
256
- # 🎬 Generador de Videos con IA Semántica
257
- **Crea videos donde las imágenes coinciden perfectamente con tu texto**
258
- """)
259
 
260
  with gr.Row():
261
- with gr.Column(scale=1):
262
- gr.Image("https://i.imgur.com/7X8P5R8.png", label="Ejemplo Visual")
263
-
264
- with gr.Accordion("📌 Consejos para mejores resultados", open=False):
265
- gr.Markdown("""
266
- - **Describe tu tema con detalles**: "Playas del Caribe con arena blanca" en vez de solo "playas"
267
- - **Usa sustantivos concretos**: "Animales de la selva amazónica" > "naturaleza"
268
- - **Sé específico**: "Tecnología 2024" > "Avances en inteligencia artificial 2024"
269
- """)
270
-
271
- gr.Examples(
272
- examples=[
273
- ["Lugares históricos de Europa con arquitectura medieval"],
274
- ["Tecnologías emergentes en inteligencia artificial para 2024"],
275
- ["Recetas tradicionales mexicanas con ingredientes autóctonos"]
276
- ],
277
- inputs=[prompt],
278
- label="Ejemplos de prompts efectivos"
279
- )
280
-
281
- with gr.Column(scale=2):
282
- prompt = gr.Textbox(
283
- label="Tema principal del video",
284
- placeholder="Ej: 'Top 5 innovaciones tecnológicas de 2024'",
285
  max_lines=2
286
  )
 
287
 
288
- custom_text = gr.TextArea(
289
- label="O escribe tu propio guion (opcional)",
290
- placeholder="Ej: 1. [Robot humanoide] Avances en robótica...",
291
- lines=6
292
- )
293
-
294
- music_file = gr.File(
295
- label="Música de fondo (opcional - MP3)",
296
- type="filepath",
297
- file_types=[".mp3"]
298
- )
299
-
300
- submit = gr.Button("🚀 Generar Video", variant="primary")
301
-
302
- output = gr.Video(
303
- label="Video Generado",
304
- format="mp4",
305
- interactive=False
306
- )
307
 
308
- submit.click(
309
- fn=create_contextual_video,
310
- inputs=[prompt, custom_text, music_file],
311
- outputs=output,
312
- api_name="generate_video"
313
  )
314
 
 
315
  if __name__ == "__main__":
316
- demo.launch(
317
- server_name="0.0.0.0",
318
- server_port=7860,
319
- share=True,
320
- debug=True
321
- )
 
1
  import os
2
  import re
3
  import random
4
+ import time
5
+ import logging
6
+ from typing import Optional, List
 
 
 
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
+ except ImportError as e:
29
+ logger.error(f"Error importing dependencies: {e}")
30
  raise
31
 
32
+ # Constantes configurables
33
+ MAX_VIDEOS = 3 # Reducir para evitar rate limiting
34
+ VIDEO_SEGMENT_DURATION = 5 # Duración de cada segmento en segundos
35
+ MAX_RETRIES = 3 # Máximo de reintentos para descargas
36
+ REQUEST_TIMEOUT = 15 # Timeout para requests
37
+
38
+ # Configuración de modelos
39
+ MODEL_NAME = "facebook/mbart-large-50"
40
+ PEXELS_API_KEY = os.getenv("PEXELS_API_KEY", "")
41
+
42
+ @backoff.on_exception(backoff.expo,
43
+ (requests.exceptions.RequestException,
44
+ requests.exceptions.HTTPError),
45
+ max_tries=MAX_RETRIES,
46
+ max_time=30)
47
+ def safe_download(url: str, timeout: int = REQUEST_TIMEOUT) -> Optional[str]:
48
+ """Descarga segura con reintentos y manejo de rate limiting"""
49
  try:
50
+ response = requests.get(url, stream=True, timeout=timeout)
51
  response.raise_for_status()
52
 
53
+ filename = f"temp_{random.randint(1000,9999)}.mp4"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
+ with open(filename, 'wb') as f:
56
+ for chunk in response.iter_content(chunk_size=8192):
57
+ f.write(chunk)
58
+
59
+ return filename
 
 
 
 
60
 
61
+ except requests.exceptions.HTTPError as e:
62
+ if e.response.status_code == 429:
63
+ retry_after = int(e.response.headers.get('Retry-After', 5))
64
+ logger.warning(f"Rate limited. Waiting {retry_after} seconds...")
65
+ time.sleep(retry_after)
66
+ logger.error(f"Download failed: {str(e)}")
67
+ return None
68
  except Exception as e:
69
+ logger.error(f"Unexpected download error: {str(e)}")
70
+ return None
71
 
 
72
  def download_video_segment(url: str, duration: float, output_path: str) -> bool:
73
+ """Descarga y procesa un segmento de video"""
74
+ temp_path = None
 
75
  try:
76
+ temp_path = safe_download(url)
77
+ if not temp_path:
78
+ return False
 
 
 
 
 
 
 
 
 
79
 
80
+ with VideoFileClip(temp_path) as clip:
81
+ if clip.duration < 1:
82
+ logger.error("Video demasiado corto")
83
+ return False
84
+
85
  end_time = min(duration, clip.duration - 0.1)
86
  subclip = clip.subclip(0, end_time)
87
 
88
+ # Configuración optimizada para HF Spaces
89
  subclip.write_videofile(
90
  output_path,
91
  codec="libx264",
92
  audio_codec="aac",
93
+ threads=2,
94
+ preset='ultrafast',
95
+ verbose=False,
96
  ffmpeg_params=[
97
  '-max_muxing_queue_size', '1024',
 
98
  '-movflags', '+faststart'
99
  ]
100
  )
 
101
  return True
102
+
103
  except Exception as e:
104
+ logger.error(f"Video processing error: {str(e)}")
105
  return False
106
  finally:
107
+ if temp_path and os.path.exists(temp_path):
108
  os.remove(temp_path)
109
 
110
+ def fetch_pexels_videos(query: str) -> List[str]:
111
+ """Busca videos en Pexels con manejo de errores"""
112
+ if not PEXELS_API_KEY:
113
+ logger.error("PEXELS_API_KEY no configurada")
114
+ return []
115
+
116
+ headers = {"Authorization": PEXELS_API_KEY}
117
+ url = f"https://api.pexels.com/videos/search?query={query}&per_page={MAX_VIDEOS}"
 
 
 
 
 
 
 
 
 
 
118
 
119
+ try:
120
+ response = requests.get(url, headers=headers, timeout=REQUEST_TIMEOUT)
121
+ response.raise_for_status()
122
+
123
+ videos = []
124
+ for video in response.json().get("videos", [])[:MAX_VIDEOS]:
125
+ video_files = [vf for vf in video.get("video_files", [])
126
+ if vf.get("width", 0) >= 720] # Calidad mínima
127
+ if video_files:
128
+ best_file = max(video_files, key=lambda x: x.get("width", 0))
129
+ videos.append(best_file["link"])
130
+
131
+ return videos
132
 
133
+ except Exception as e:
134
+ logger.error(f"Error fetching Pexels videos: {str(e)}")
135
+ return []
136
+
137
+ def generate_script(prompt: str) -> str:
138
+ """Genera un script usando IA local con fallback"""
139
  try:
140
+ generator = pipeline("text-generation", model=MODEL_NAME)
141
+ result = generator(
142
+ f"Genera un guion breve sobre {prompt} en español con {MAX_VIDEOS} puntos:",
143
+ max_length=200,
144
+ num_return_sequences=1
145
+ )[0]['generated_text']
146
+ return result
147
+ except Exception as e:
148
+ logger.error(f"Error generating script: {str(e)}")
149
+ return f"1. Punto uno sobre {prompt}\n2. Punto dos\n3. Punto tres"
150
+
151
+ async def generate_voice(text: str, output_file: str = "voice.mp3") -> bool:
152
+ """Genera narración de voz con manejo de errores"""
153
+ try:
154
+ communicate = edge_tts.Communicate(text, voice="es-MX-DaliaNeural")
155
+ await communicate.save(output_file)
156
+ return True
157
+ except Exception as e:
158
+ logger.error(f"Voice generation failed: {str(e)}")
159
+ return False
160
+
161
+ def run_async(coro):
162
+ """Ejecuta corrutinas asíncronas desde código síncrono"""
163
+ import asyncio
164
+ loop = asyncio.new_event_loop()
165
+ asyncio.set_event_loop(loop)
166
+ try:
167
+ return loop.run_until_complete(coro)
168
+ finally:
169
+ loop.close()
170
+
171
+ def create_video(prompt: str) -> Optional[str]:
172
+ """Función principal para crear el video"""
173
+ try:
174
+ # 1. Generar contenido
175
+ script = generate_script(prompt)
176
+ logger.info(f"Script generado: {script[:100]}...")
177
+
178
+ # 2. Buscar videos
179
+ video_urls = fetch_pexels_videos(prompt)
180
+ if not video_urls:
181
+ logger.error("No se encontraron videos")
182
+ return None
183
+
184
+ # 3. Generar voz
185
+ voice_file = "voice.mp3"
186
+ if not run_async(generate_voice(script, voice_file)):
187
+ logger.error("No se pudo generar voz")
188
+ return None
189
+
190
+ # 4. Procesar videos
191
+ output_dir = "output"
192
+ os.makedirs(output_dir, exist_ok=True)
193
+ output_path = os.path.join(output_dir, f"video_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
194
+
195
  clips = []
196
+ segment_duration = VIDEO_SEGMENT_DURATION
197
 
198
+ for i, url in enumerate(video_urls):
199
+ clip_path = f"segment_{i}.mp4"
200
  if download_video_segment(url, segment_duration, clip_path):
201
  clips.append(VideoFileClip(clip_path))
202
 
203
  if not clips:
204
+ logger.error("No se pudieron procesar los videos")
205
+ return None
206
+
207
+ # 5. Ensamblar video final
208
  final_video = concatenate_videoclips(clips, method="compose")
209
  audio_clip = AudioFileClip(voice_file)
210
+ final_video = final_video.set_audio(audio_clip)
211
 
 
 
 
 
 
 
 
 
 
 
212
  final_video.write_videofile(
213
  output_path,
214
  codec="libx264",
215
  audio_codec="aac",
216
+ threads=2,
217
+ preset='ultrafast',
218
+ verbose=False
 
219
  )
220
 
221
  return output_path
222
+
223
  except Exception as e:
224
+ logger.error(f"Error creating video: {str(e)}")
225
+ return None
226
  finally:
227
  # Limpieza
228
  for clip in clips:
 
233
  if os.path.exists(f"segment_{i}.mp4"):
234
  os.remove(f"segment_{i}.mp4")
235
 
236
+ # Interfaz Gradio optimizada
237
+ with gr.Blocks(title="Generador de Videos HF", theme=gr.themes.Soft()) as app:
238
+ gr.Markdown("# 🎥 Generador Automático de Videos")
 
 
 
239
 
240
  with gr.Row():
241
+ with gr.Column():
242
+ prompt_input = gr.Textbox(
243
+ label="Tema del video",
244
+ placeholder="Ej: Paisajes naturales de Chile",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  max_lines=2
246
  )
247
+ generate_btn = gr.Button("Generar Video", variant="primary")
248
 
249
+ with gr.Column():
250
+ output_video = gr.Video(label="Resultado", interactive=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
 
252
+ generate_btn.click(
253
+ fn=create_video,
254
+ inputs=prompt_input,
255
+ outputs=output_video
 
256
  )
257
 
258
+ # Para Hugging Face Spaces
259
  if __name__ == "__main__":
260
+ app.launch(server_name="0.0.0.0", server_port=7860)