File size: 12,755 Bytes
8b274aa
15e8c2d
bafc5cd
15e8c2d
b8bd6c3
 
 
15e8c2d
 
6d66777
b8bd6c3
 
 
 
 
b82e6a6
1829fd6
b8bd6c3
 
 
 
 
 
 
 
 
 
 
 
 
b82e6a6
b8bd6c3
b82e6a6
b8bd6c3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b82e6a6
 
 
b8bd6c3
b82e6a6
b8bd6c3
 
1829fd6
b8bd6c3
 
 
 
 
9b7097e
b8bd6c3
 
 
 
 
 
 
 
 
 
 
 
 
1829fd6
b8bd6c3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8b274aa
b82e6a6
b8bd6c3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4813ca2
b8bd6c3
 
 
 
 
 
 
 
 
 
 
 
 
 
b82e6a6
b8bd6c3
 
 
 
 
 
4813ca2
b8bd6c3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4813ca2
b8bd6c3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b82e6a6
b8bd6c3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6d66777
b82e6a6
b8bd6c3
 
 
 
b82e6a6
b8bd6c3
 
b82e6a6
b8bd6c3
 
 
b82e6a6
b8bd6c3
 
b82e6a6
b8bd6c3
 
b82e6a6
b8bd6c3
 
 
b82e6a6
b8bd6c3
b82e6a6
b8bd6c3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b82e6a6
b8bd6c3
 
 
 
374c72e
15e8c2d
b8bd6c3
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
import os
import asyncio
import logging
import tempfile
import requests
from datetime import datetime
from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip
import edge_tts
import gradio as gr

from transformers import GPT2Tokenizer, GPT2LMHeadModel
import torch

# --- Configuración de Logging ---
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# --- Inicialización de Tokenizer y Modelo GPT-2 ---
# Especificamos un modelo más pequeño para una generación más rápida y menos exigente en recursos
# Puedes cambiarlo a "gpt2" si tienes suficiente RAM y GPU
MODEL_NAME = "gpt2-small" # O "gpt2"
try:
    tokenizer = GPT2Tokenizer.from_pretrained(MODEL_NAME)
    model = GPT2LMHeadModel.from_pretrained(MODEL_NAME).eval()
    # Añadir un token de padding si el modelo no tiene uno (común en GPT-2)
    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token
except Exception as e:
    logger.error(f"Error al cargar el modelo GPT-2: {e}")
    # Considerar una salida de emergencia o un mensaje al usuario si esto falla

# --- Funciones de Utilidad ---

def generate_script(prompt, max_length=300):
    """
    Genera un guion usando el modelo GPT-2 basado en un prompt dado.
    Ajustamos truncation a True y añadimos un token de padding.
    """
    logger.info("Generando guion con GPT-2...")
    inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=512) # Truncar para evitar errores
    with torch.no_grad():
        outputs = model.generate(
            **inputs,
            max_length=max_length,
            do_sample=True,
            top_p=0.95,
            top_k=60,
            temperature=0.9,
            pad_token_id=tokenizer.pad_token_id, # Usar el token de padding
            eos_token_id=tokenizer.eos_token_id
        )
    text = tokenizer.decode(outputs[0], skip_special_tokens=True)
    logger.info(f"Guion generado (longitud: {len(text)} caracteres): {text[:200]}...") # Mostrar solo los primeros 200 chars
    return text

async def text_to_speech(text, voice="es-ES-ElviraNeural", output_path="voz.mp3"):
    """
    Convierte texto a voz usando Edge TTS.
    """
    logger.info(f"Generando audio TTS para: {text[:100]}...")
    try:
        communicate = edge_tts.Communicate(text, voice)
        await communicate.save(output_path)
        logger.info(f"Audio guardado en {output_path}")
    except Exception as e:
        logger.error(f"Error al generar audio TTS: {e}")
        raise # Volver a lanzar la excepción para que el llamador la maneje

def download_video_sample(url):
    """
    Descarga un archivo de video desde una URL a un archivo temporal.
    """
    logger.info(f"Descargando video de ejemplo desde: {url}")
    try:
        tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4")
        response = requests.get(url, stream=True, timeout=10) # Añadir timeout
        response.raise_for_status() # Lanza un error para códigos de estado HTTP malos
        for chunk in response.iter_content(chunk_size=1024 * 1024):
            tmp.write(chunk)
        tmp.close()
        logger.info(f"Video descargado a: {tmp.name}")
        return tmp.name
    except requests.exceptions.RequestException as e:
        logger.error(f"Error al descargar el video {url}: {e}")
        if os.path.exists(tmp.name):
            os.remove(tmp.name)
        return None # Retorna None si la descarga falla
    except Exception as e:
        logger.error(f"Error inesperado al descargar video {url}: {e}")
        if os.path.exists(tmp.name):
            os.remove(tmp.name)
        return None

def loop_audio_to_length(audio_clip, target_duration):
    """
    Buclea un clip de audio hasta una duración objetivo.
    """
    if audio_clip.duration >= target_duration:
        return audio_clip.subclip(0, target_duration)
    loops = int(target_duration // audio_clip.duration) + 1
    audios = [audio_clip] * loops
    concatenated = concatenate_videoclips(audios) # No necesitas method="compose" para audio
    return concatenated.subclip(0, target_duration)

# --- Función Principal de Creación de Video ---

def crear_video(prompt, musica_url=None):
    """
    Crea un video combinando un guion generado, voz TTS, clips de video y música de fondo.
    """
    logger.info(f"Iniciando creación de video para el prompt: '{prompt}'")
    temp_files = [] # Para llevar un registro de archivos temporales a limpiar

    try:
        # 1. Generar guion
        guion = generate_script(prompt, max_length=200) # Ajusta max_length si quieres guiones más largos

        # 2. Generar audio TTS
        voz_archivo = os.path.join(tempfile.gettempdir(), "voz_temp.mp3")
        temp_files.append(voz_archivo)
        asyncio.run(text_to_speech(guion, output_path=voz_archivo))
        audio_tts = AudioFileClip(voz_archivo)

        # 3. Descargar videos de ejemplo
        # ¡IMPORTANTE! Aquí debes reemplazar estas URLs con URLs de videos reales y variados
        # o implementar una lógica para buscar y descargar videos relevantes.
        # Por ahora, usaré URLs de ejemplo distintas para simular variedad.
        video_urls = [
            "https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.mp4",
            "https://file-examples.com/storage/fe2c91b5c46522c0734a74a/2017/04/file_example_MP4_480_1_5MG.mp4",
            "https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4"
        ]
        
        clips = []
        video_download_success = False
        for i, url in enumerate(video_urls):
            video_path = download_video_sample(url)
            if video_path:
                temp_files.append(video_path)
                try:
                    clip = VideoFileClip(video_path).subclip(0, min(10, VideoFileClip(video_path).duration)) # máximo 10 segundos o duración real
                    clips.append(clip)
                    video_download_success = True
                except Exception as e:
                    logger.warning(f"No se pudo cargar el clip de video {video_path}: {e}")
            else:
                logger.warning(f"No se pudo descargar el video de la URL: {url}")
        
        if not video_download_success or not clips:
            logger.error("No se pudieron obtener clips de video válidos. Abortando creación de video.")
            raise ValueError("No hay clips de video disponibles para crear el video.")

        # 4. Concatenar videos
        video_final = concatenate_videoclips(clips, method="compose")
        
        # Asegurarse de que el video sea al menos tan largo como el audio TTS si es posible
        if video_final.duration < audio_tts.duration:
            logger.info("Duración del video es menor que la del audio. Intentando extender el video.")
            # Repetir el video si es necesario, o al menos el último clip
            num_repeats = int(audio_tts.duration / video_final.duration) + 1
            repeated_clips = [video_final] * num_repeats
            video_final = concatenate_videoclips(repeated_clips, method="compose").subclip(0, audio_tts.duration)


        # 5. Música de fondo en loop si está definida
        mezcla_audio = audio_tts # Por defecto, solo la voz
        if musica_url and musica_url.strip():
            musica_path = download_video_sample(musica_url)
            if musica_path:
                temp_files.append(musica_path)
                try:
                    musica_audio = AudioFileClip(musica_path)
                    # Loop música a duración del video final
                    musica_loop = loop_audio_to_length(musica_audio, video_final.duration)
                    # Mezclar audio TTS y música (TTS al 100%, música al 30%)
                    mezcla_audio = CompositeAudioClip([musica_loop.volumex(0.3), audio_tts.set_duration(video_final.duration).volumex(1.0)])
                except Exception as e:
                    logger.warning(f"No se pudo procesar la música de fondo: {e}. Se usará solo la voz.")
            else:
                logger.warning("No se pudo descargar la música. Se usará solo la voz.")
        
        # 6. Asignar audio al video y ajustar duración del video a la duración del audio TTS
        # Es crucial que el video tenga al menos la misma duración que el audio combinado.
        video_final = video_final.set_audio(mezcla_audio).subclip(0, mezcla_audio.duration)


        # 7. Guardar video final
        output_dir = "generated_videos"
        os.makedirs(output_dir, exist_ok=True)
        output_path = os.path.join(output_dir, f"video_output_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")
        logger.info(f"Escribiendo video final a: {output_path}")
        video_final.write_videofile(output_path, fps=24, threads=4, logger=None, preset="medium") # Mejorar preset para calidad

        logger.info(f"Video generado exitosamente en: {output_path}")
        return output_path

    except Exception as e:
        logger.error(f"Error general en la creación del video: {e}", exc_info=True)
        return None
    finally:
        # 8. Limpiar archivos temporales
        for f in temp_files:
            if os.path.exists(f):
                try:
                    os.remove(f)
                    logger.info(f"Archivo temporal eliminado: {f}")
                except Exception as e:
                    logger.warning(f"No se pudo eliminar el archivo temporal {f}: {e}")
        # Cerrar los clips de moviepy explícitamente para liberar recursos
        for clip in clips:
            if clip:
                clip.close()
        if 'audio_tts' in locals() and audio_tts:
            audio_tts.close()
        if 'musica_audio' in locals() and musica_audio:
            musica_audio.close()
        if 'video_final' in locals() and video_final:
            video_final.close()


def run_app(prompt, musica_url):
    """
    Función envoltorio para Gradio que maneja la ejecución y los errores.
    """
    logger.info(f"Solicitud recibida en run_app con prompt: '{prompt}', musica_url: '{musica_url}'")
    if not prompt.strip():
        gr.Warning("Por favor, introduce un tema para generar el guion.")
        return None

    try:
        video_path = crear_video(prompt, musica_url if musica_url.strip() else None)
        if video_path:
            logger.info(f"Proceso completado. Video disponible en: {video_path}")
            return video_path
        else:
            gr.Error("Hubo un problema al generar el video. Revisa los logs para más detalles.")
            return None
    except Exception as e:
        logger.error(f"Error inesperado al ejecutar la aplicación: {e}", exc_info=True)
        gr.Error(f"Ocurrió un error: {e}. Por favor, inténtalo de nuevo.")
        return None

# --- Interfaz de Gradio ---
with gr.Blocks() as app:
    gr.Markdown("""
    ### 🎬 Generador de Video con IA
    Introduce un tema, y la IA generará un guion, lo convertirá a voz, le añadirá videos de ejemplo y música de fondo.
    """)
    with gr.Row():
        prompt_input = gr.Textbox(label="Introduce el tema para generar el guion", lines=3, placeholder="Ej: Las maravillas de la inteligencia artificial y su futuro.")
        musica_input = gr.Textbox(label="URL de Música de Fondo (opcional)", placeholder="Ej: https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3")
    
    boton = gr.Button("🚀 Generar Video")
    
    # Usamos gr.Column para organizar mejor la salida y mensajes
    with gr.Column():
        salida_video = gr.Video(label="Video Generado", interactive=False)
        estado_mensaje = gr.Textbox(label="Estado", interactive=False, visible=True)

    # Conectar el botón a la función run_app
    # Añadimos un paso intermedio para actualizar el estado mientras se procesa
    boton.click(
        fn=lambda: gr.update(value="Generando video... Por favor, espera."), 
        outputs=estado_mensaje, 
        queue=False
    ).then(
        fn=run_app,
        inputs=[prompt_input, musica_input],
        outputs=salida_video
    ).then(
        fn=lambda: gr.update(value="¡Video generado exitosamente!"),
        outputs=estado_mensaje,
        queue=False
    )
    # Resetear el mensaje de estado si el usuario cambia el prompt
    prompt_input.change(fn=lambda: gr.update(value=""), outputs=estado_mensaje, queue=False)
    musica_input.change(fn=lambda: gr.update(value=""), outputs=estado_mensaje, queue=False)


if __name__ == "__main__":
    logger.info("Iniciando aplicación Gradio...")
    app.launch(server_name="0.0.0.0", server_port=7860, share=False) # share=True si quieres un enlace público