Spaces:
Running
Running
import gradio as gr | |
import google.generativeai as genai | |
from gtts import gTTS | |
import os | |
from moviepy.editor import ImageSequenceClip, AudioFileClip, concatenate_videoclips | |
from PIL import Image, ImageOps | |
# Configura tu API Key de Gemini | |
# Asegúrate de tener esta clave en tus variables de entorno o un archivo .env | |
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") | |
if GEMINI_API_KEY: | |
genai.configure(api_key=GEMINI_API_KEY) | |
else: | |
print("Advertencia: La GEMINI_API_KEY no está configurada. La generación de texto con Gemini no estará disponible.") | |
# --- Funciones de Ayuda --- | |
def generate_news_with_gemini(prompt): | |
"""Genera una noticia usando la API de Gemini.""" | |
if not GEMINI_API_KEY: | |
return "Error: GEMINI_API_KEY no está configurada para generar texto." | |
try: | |
model = genai.GenerativeModel('gemini-pro') | |
response = model.generate_content(prompt) | |
return response.text | |
except Exception as e: | |
return f"Error al generar la noticia con Gemini: {e}" | |
def text_to_speech(text, output_filename="audio.mp3"): | |
"""Convierte texto a voz usando gTTS.""" | |
try: | |
tts = gTTS(text=text, lang='es') # Puedes cambiar 'es' por el idioma que desees | |
tts.save(output_filename) | |
return output_filename | |
except Exception as e: | |
raise Exception(f"Error al generar el audio: {e}") | |
def process_images(image_paths, video_duration, video_aspect_ratio="16:9"): | |
""" | |
Procesa las imágenes: redimensiona, recorta y ajusta para el video. | |
Devuelve una lista de rutas a las imágenes procesadas y su duración por imagen. | |
""" | |
processed_image_folder = "temp_processed_images" | |
os.makedirs(processed_image_folder, exist_ok=True) | |
target_width, target_height = (1280, 720) if video_aspect_ratio == "16:9" else (720, 1280) | |
num_images = len(image_paths) | |
if num_images == 0: | |
raise ValueError("No se subieron imágenes.") | |
# Calcular la duración de cada imagen | |
image_duration = video_duration / num_images | |
processed_image_files = [] | |
for i, img_path in enumerate(image_paths): | |
try: | |
img = Image.open(img_path).convert("RGB") | |
# Calcular las dimensiones para recortar y ajustar | |
original_width, original_height = img.size | |
target_ratio = target_width / target_height | |
image_ratio = original_width / original_height | |
if image_ratio > target_ratio: | |
# La imagen es más ancha que el objetivo, se recortará horizontalmente | |
new_width = int(original_height * target_ratio) | |
left = (original_width - new_width) / 2 | |
top = 0 | |
right = (original_width + new_width) / 2 | |
bottom = original_height | |
else: | |
# La imagen es más alta que el objetivo, se recortará verticalmente | |
new_height = int(original_width / target_ratio) | |
left = 0 | |
top = (original_height - new_height) / 2 | |
right = original_width | |
bottom = (original_height + new_height) / 2 | |
img = img.crop((left, top, right, bottom)) | |
img = img.resize((target_width, target_height), Image.Resampling.LANCZOS) # Mejor calidad de redimensionamiento | |
output_path = os.path.join(processed_image_folder, f"processed_image_{i:03d}.png") | |
img.save(output_path) | |
processed_image_files.append(output_path) | |
except Exception as e: | |
print(f"Error procesando imagen {img_path}: {e}") | |
continue | |
return processed_image_files, image_duration | |
def create_video(audio_path, processed_image_files, image_duration, video_aspect_ratio="16:9"): | |
"""Crea un video combinando el audio y la secuencia de imágenes.""" | |
try: | |
if not os.path.exists(audio_path): | |
raise FileNotFoundError(f"El archivo de audio no se encontró: {audio_path}") | |
if not processed_image_files: | |
raise ValueError("No hay imágenes procesadas para crear el video.") | |
audio_clip = AudioFileClip(audio_path) | |
# Crear clips de imagen para la duración del audio | |
image_clips = [] | |
for img_file in processed_image_files: | |
clip = ImageSequenceClip([img_file], fps=1/image_duration) # 1 imagen por image_duration segundos | |
image_clips.append(clip.set_duration(image_duration)) # Asegura que cada imagen tenga su duración calculada | |
# Concatenar todos los clips de imagen | |
final_video_clip = concatenate_videoclips(image_clips, method="compose") | |
# Ajustar la duración del video al audio y añadir el audio | |
final_video_clip = final_video_clip.set_audio(audio_clip) | |
final_video_clip = final_video_clip.set_duration(audio_clip.duration) | |
output_video_filename = "output_news_video.mp4" | |
final_video_clip.write_videofile(output_video_filename, fps=24, codec="libx264", audio_codec="aac") | |
return output_video_filename | |
except Exception as e: | |
raise Exception(f"Error al crear el video: {e}") | |
# --- Función Principal de Gradio --- | |
def create_news_video(news_text_input, gemini_prompt, image_files, video_ratio): | |
""" | |
Función principal que orquesta la generación de la noticia, audio y video. | |
""" | |
news_content = "" | |
output_video_path = None | |
error_message = "" | |
# 1. Obtener la Noticia | |
if news_text_input: | |
news_content = news_text_input | |
elif gemini_prompt: | |
news_content = generate_news_with_gemini(gemini_prompt) | |
if "Error" in news_content: | |
return news_content, None # Devuelve el error directamente si Gemini falla | |
else: | |
return "Por favor, escribe una noticia o proporciona un prompt para Gemini.", None | |
if not news_content: | |
return "No se pudo obtener el contenido de la noticia.", None | |
try: | |
# 2. Generar Audio de la Noticia | |
audio_file_path = text_to_speech(news_content) | |
audio_duration = AudioFileClip(audio_file_path).duration | |
# 3. Procesar Imágenes | |
if not image_files: | |
return "Por favor, sube al menos una imagen.", None | |
# Gradio `gr.File(file_count="multiple")` devuelve una lista de diccionarios | |
# donde cada diccionario tiene la clave 'name' con la ruta temporal del archivo. | |
image_paths = [img.name for img in image_files] | |
processed_images, img_duration_per_image = process_images( | |
image_paths, audio_duration, video_aspect_ratio=video_ratio | |
) | |
if not processed_images: | |
return "Error: No se pudieron procesar las imágenes.", None | |
# 4. Crear Video | |
output_video_path = create_video(audio_file_path, processed_images, img_duration_per_image, video_ratio) | |
# Limpiar archivos temporales (opcional, pero buena práctica) | |
os.remove(audio_file_path) | |
for img_file in processed_images: | |
os.remove(img_file) | |
os.rmdir("temp_processed_images") | |
return "Video generado con éxito.", output_video_path | |
except Exception as e: | |
error_message = f"Ocurrió un error inesperado: {e}" | |
# Limpieza en caso de error | |
if os.path.exists("audio.mp3"): | |
os.remove("audio.mp3") | |
if os.path.exists("temp_processed_images"): | |
for f in os.listdir("temp_processed_images"): | |
os.remove(os.path.join("temp_processed_images", f)) | |
os.rmdir("temp_processed_images") | |
return error_message, None | |
# --- Interfaz de Gradio --- | |
with gr.Blocks() as demo: | |
gr.Markdown( | |
""" | |
# 🎥 Creador de Videos de Noticias con IA y Gradio 🎥 | |
Escribe una noticia, súbe tus imágenes y deja que la IA genere un video para ti. | |
Puedes escribir la noticia directamente o generarla con tu **GEMINI_API_KEY**. | |
""" | |
) | |
with gr.Row(): | |
with gr.Column(): | |
news_input = gr.Textbox( | |
label="Escribe tu noticia aquí", | |
placeholder="Ej: Un nuevo descubrimiento científico...", | |
lines=5 | |
) | |
gemini_prompt_input = gr.Textbox( | |
label="O genera la noticia con Gemini (Escribe un prompt)", | |
placeholder="Ej: Escribe una noticia sobre un nuevo récord mundial de ajedrez." | |
) | |
image_upload = gr.File( | |
label="Sube de 1 a 60 imágenes (JPG, PNG)", | |
file_count="multiple", | |
type="filepath", | |
file_types=[".jpg", ".jpeg", ".png"], | |
max_files=60 | |
) | |
video_ratio_dropdown = gr.Dropdown( | |
label="Relación de Aspecto del Video", | |
choices=["16:9", "9:16"], | |
value="16:9" | |
) | |
generate_button = gr.Button("Generar Video de Noticia") | |
with gr.Column(): | |
output_message = gr.Textbox(label="Estado", interactive=False) | |
video_output = gr.Video(label="Video de la Noticia") | |
generate_button.click( | |
fn=create_news_video, | |
inputs=[news_input, gemini_prompt_input, image_upload, video_ratio_dropdown], | |
outputs=[output_message, video_output] | |
) | |
if __name__ == "__main__": | |
demo.launch() |