Spaces:
Running
Running
| import os | |
| import requests | |
| import asyncio | |
| import tempfile | |
| import logging | |
| from datetime import datetime | |
| import gradio as gr | |
| from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip | |
| import edge_tts | |
| from transformers import pipeline | |
| import torch | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| # --- CONFIG --- | |
| PEXELS_API_KEY = os.getenv("PEXELS_API_KEY") | |
| # Modelo de texto en español | |
| MODEL_NAME = "DeepESP/gpt2-spanish" | |
| # Inicializar generador de texto | |
| device = 0 if torch.cuda.is_available() else -1 | |
| generator = pipeline("text-generation", model=MODEL_NAME, device=device) | |
| async def listar_voces(): | |
| try: | |
| return await edge_tts.list_voices() | |
| except Exception as e: | |
| logger.error(f"Error obteniendo voces: {e}") | |
| return [{'ShortName': 'es-ES-ElviraNeural', 'Name': 'Elvira', 'Gender': 'Female'}] | |
| VOCES = asyncio.run(listar_voces()) | |
| VOICE_NAMES = [f"{v['Name']} ({v['Gender']})" for v in VOCES] | |
| def generar_guion(prompt): | |
| prompt_text = f"Escribe un guion profesional para un vídeo de YouTube sobre '{prompt}':\n" | |
| try: | |
| out = generator(prompt_text, max_new_tokens=300, temperature=0.7, truncate=True, num_return_sequences=1) | |
| texto = out[0]['generated_text'] | |
| return texto | |
| except Exception as e: | |
| logger.error(f"Error generando guion: {e}") | |
| return f"Error generando guion: {e}" | |
| def buscar_videos(prompt, max_videos=3): | |
| try: | |
| url = f"https://api.pexels.com/videos/search?query={prompt}&per_page={max_videos}" | |
| resp = requests.get(url, headers={"Authorization": PEXELS_API_KEY}, timeout=10) | |
| return resp.json().get("videos", []) | |
| except Exception as e: | |
| logger.error(f"Error en Pexels: {e}") | |
| return [] | |
| async def crear_video(prompt, voz_index, musica_path=None): | |
| texto = generar_guion(prompt) | |
| voz_short = VOCES[voz_index]['ShortName'] | |
| audio_file = "tts.mp3" | |
| await edge_tts.Communicate(texto, voz_short).save(audio_file) | |
| tts_audio = AudioFileClip(audio_file) | |
| dur = tts_audio.duration | |
| # Música opcional | |
| if musica_path: | |
| music = AudioFileClip(musica_path).volumex(0.2).subclip(0, dur) | |
| audio_comp = CompositeAudioClip([music, tts_audio]) | |
| else: | |
| audio_comp = tts_audio | |
| videos = buscar_videos(prompt) | |
| if not videos: | |
| return None | |
| clips = [] | |
| for v in videos: | |
| url = v['video_files'][0]['link'] | |
| r = requests.get(url, stream=True, timeout=15) | |
| tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") | |
| [tmp.write(c) for c in r.iter_content(1024*1024)] | |
| tmp.close() | |
| clip = VideoFileClip(tmp.name).subclip(0, min(dur/len(videos), v['duration'])) | |
| clips.append(clip) | |
| final = concatenate_videoclips(clips).set_audio(audio_comp) | |
| out_name = f"video_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4" | |
| final.write_videofile(out_name, codec="libx264", audio_codec="aac", fps=24) | |
| # Cleanup | |
| tts_audio.close() | |
| for c in clips: | |
| c.close() | |
| os.remove(c.filename) | |
| os.remove(audio_file) | |
| return out_name | |
| def run_crear(prompt, voz_name, muz_file): | |
| try: | |
| idx = VOICE_NAMES.index(voz_name) | |
| except: | |
| idx = 0 | |
| return asyncio.run(crear_video(prompt, idx, muz_file.name if muz_file else None)) | |
| with gr.Blocks() as app: | |
| tema = gr.Textbox(label="Tema para guion y video") | |
| voz = gr.Dropdown(choices=VOICE_NAMES, value=VOICE_NAMES[0], label="Voz TTS") | |
| muz = gr.File(label="Música fondo opcional (mp3/wav)", file_types=[".mp3", ".wav"]) | |
| btn = gr.Button("Crear video") | |
| vid_out = gr.Video(label="Video generado") | |
| btn.click(fn=run_crear, inputs=[tema, voz, muz], outputs=vid_out) | |
| if __name__ == "__main__": | |
| app.launch() | |