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() | |