Spaces:
Running
Running
import gradio as gr | |
from gtts import gTTS | |
import os | |
from PIL import Image | |
from pydub import AudioSegment | |
import subprocess | |
import shutil | |
import math | |
def text_to_speech(text: str, output_filename="audio.mp3"): | |
try: | |
tts = gTTS(text=text, lang='es') | |
tts.save(output_filename) | |
return output_filename | |
except Exception as e: | |
raise Exception(f"Error al generar el audio con gTTS: {e}") | |
def get_audio_duration(audio_path): | |
if not os.path.exists(audio_path) or os.path.getsize(audio_path) == 0: | |
return 0 | |
try: | |
audio = AudioSegment.from_file(audio_path) | |
return audio.duration_seconds | |
except Exception as e: | |
raise Exception(f"Error al obtener la duración del audio: {e}") | |
def process_image(img_path, target_width, target_height, output_folder, index): | |
try: | |
img = Image.open(img_path).convert("RGB") | |
original_width, original_height = img.size | |
target_ratio = target_width / target_height | |
image_ratio = original_width / original_height | |
if image_ratio > target_ratio: | |
new_width = int(original_height * target_ratio) | |
left = (original_width - new_width) / 2 | |
img = img.crop((left, 0, left + new_width, original_height)) | |
elif image_ratio < target_ratio: | |
new_height = int(original_width / target_ratio) | |
top = (original_height - new_height) / 2 | |
img = img.crop((0, top, original_width, top + new_height)) | |
img = img.resize((target_width, target_height), Image.Resampling.LANCZOS) | |
output_path = os.path.join(output_folder, f"processed_image_{index:03d}.png") | |
img.save(output_path) | |
return output_path | |
except Exception as e: | |
return None | |
def create_video_with_ken_burns(processed_images, audio_duration, fps, video_size, output_filename): | |
if not processed_images: | |
raise ValueError("No hay imágenes procesadas para crear el video.") | |
IMAGE_DURATION = 3 | |
num_images = len(processed_images) | |
width, height = video_size | |
num_loops = math.ceil(audio_duration / (num_images * IMAGE_DURATION)) if (num_images * IMAGE_DURATION) > 0 else 1 | |
filter_complex_chains = [] | |
video_clips = [] | |
total_clips = num_images * num_loops | |
input_commands = [] | |
for img_path in processed_images * num_loops: | |
input_commands.extend(["-i", img_path]) | |
for i in range(total_clips): | |
zoom = 1.2 | |
filter_complex_chains.append(f"[{i}:v]scale={width*zoom}:{height*zoom},zoompan=z='min(zoom+0.0015,1.5)':d={fps*IMAGE_DURATION}:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s={width}x{height},fade=t=in:st=0:d=1,fade=t=out:st={IMAGE_DURATION-1}:d=1[v{i}]") | |
video_clips.append(f"[v{i}]") | |
concat_filter = f"{''.join(video_clips)}concat=n={total_clips}:v=1:a=0,format=yuv420p[v]" | |
filter_complex = ";".join(filter_complex_chains) + ";" + concat_filter | |
command = ["ffmpeg", "-y"] | |
command.extend(input_commands) | |
command.extend([ | |
"-filter_complex", filter_complex, | |
"-map", "[v]", | |
"-t", str(audio_duration), | |
"-c:v", "libx264", | |
"-pix_fmt", "yuv420p", | |
output_filename | |
]) | |
try: | |
subprocess.run(command, check=True, capture_output=True, text=True) | |
except subprocess.CalledProcessError as e: | |
raise Exception(f"Error al crear video con efecto Ken Burns: {e.stderr}") | |
def combine_video_and_audio(video_path, audio_path, output_path): | |
command = ["ffmpeg", "-y", "-i", video_path, "-i", audio_path, "-c:v", "copy", "-c:a", "aac", "-map", "0:v:0", "-map", "1:a:0", "-shortest", output_path] | |
try: | |
subprocess.run(command, check=True, capture_output=True, text=True) | |
except subprocess.CalledProcessError as e: | |
raise Exception(f"Error al combinar video y audio: {e.stderr}") | |
def generate_tts_only(news_text_input): | |
if not news_text_input: | |
return "Por favor, escribe una noticia para generar el audio.", None | |
try: | |
audio_file = text_to_speech(news_text_input, "audio_temp_preview.mp3") | |
return "Audio generado con éxito.", audio_file | |
except Exception as e: | |
return f"Ocurrió un error al generar solo el audio: {e}", None | |
def create_news_video_app(news_text_input, image_files, video_ratio, input_audio_file): | |
processed_image_folder = "temp_processed_images" | |
final_output_video_path = "video_noticia_final.mp4" | |
temp_video_no_audio_path = "video_sin_audio.mp4" | |
temp_audio_file = "audio_para_video.mp3" | |
if os.path.exists(processed_image_folder): shutil.rmtree(processed_image_folder) | |
os.makedirs(processed_image_folder) | |
try: | |
if not image_files: raise ValueError("Por favor, sube al menos una imagen.") | |
if isinstance(input_audio_file, str) and os.path.exists(input_audio_file) and os.path.getsize(input_audio_file) > 0: | |
shutil.copy(input_audio_file, temp_audio_file) | |
else: | |
if not news_text_input: | |
raise ValueError("Escribe una noticia para generar el audio, ya que no se proporcionó una vista previa válida.") | |
text_to_speech(news_text_input, temp_audio_file) | |
audio_duration = get_audio_duration(temp_audio_file) | |
if audio_duration == 0: raise ValueError("La duración del audio es cero.") | |
target_width, target_height = (720, 1280) if video_ratio == "9:16" else (1280, 720) | |
processed_images_paths = [process_image(f.name, target_width, target_height, processed_image_folder, i) for i, f in enumerate(image_files)] | |
processed_images_paths = [p for p in processed_images_paths if p] | |
if not processed_images_paths: raise ValueError("No se pudieron procesar las imágenes.") | |
create_video_with_ken_burns(processed_images_paths, audio_duration, 30, (target_width, target_height), temp_video_no_audio_path) | |
combine_video_and_audio(temp_video_no_audio_path, temp_audio_file, final_output_video_path) | |
return "Video generado con éxito.", final_output_video_path | |
except Exception as e: | |
return f"Ocurrió un error: {e}", None | |
finally: | |
if os.path.exists(processed_image_folder): shutil.rmtree(processed_image_folder) | |
if os.path.exists(temp_video_no_audio_path): os.remove(temp_video_no_audio_path) | |
if os.path.exists(temp_audio_file): os.remove(temp_audio_file) | |
if os.path.exists("audio_temp_preview.mp3"): os.remove("audio_temp_preview.mp3") | |
with gr.Blocks(theme=gr.themes.Soft()) as demo: | |
gr.Markdown("# � Creador de Videos de Noticias") | |
with gr.Row(): | |
with gr.Column(scale=2): | |
news_input = gr.Textbox(label="1. Escribe tu noticia aquí", lines=5) | |
image_upload = gr.File(label="2. Sube tus imágenes", file_count="multiple", type="filepath", file_types=[".jpg", ".jpeg", ".png"]) | |
video_ratio_dropdown = gr.Dropdown(label="3. Elige el Formato del Video", choices=["16:9", "9:16"], value="9:16", interactive=True) | |
with gr.Accordion("Opciones de Audio (Opcional)", open=False): | |
generate_audio_button = gr.Button("Generar Solo Audio (Vista Previa)") | |
audio_status_message = gr.Textbox(label="Estado del Audio", interactive=False) | |
audio_output_preview = gr.Audio(label="Audio de Noticia (Vista Previa)", interactive=False) | |
generate_video_button = gr.Button("🎬 Generar Video Completo", variant="primary") | |
with gr.Column(scale=3): | |
output_message = gr.Textbox(label="Estado del Proceso", interactive=False) | |
video_output = gr.Video(label="Video de la Noticia Generado") | |
generate_audio_button.click( | |
fn=generate_tts_only, | |
inputs=[news_input], | |
outputs=[audio_status_message, audio_output_preview] | |
) | |
generate_video_button.click( | |
fn=create_news_video_app, | |
inputs=[news_input, image_upload, video_ratio_dropdown, audio_output_preview], | |
outputs=[output_message, video_output] | |
) | |
demo.launch() |