Spaces:
Runtime error
Runtime error
Create App.py
Browse files
App.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import edge_tts
|
| 3 |
+
import asyncio
|
| 4 |
+
import tempfile
|
| 5 |
+
import os
|
| 6 |
+
from moviepy.editor import ImageClip, concatenate_videoclips, AudioFileClip, TextClip, CompositeVideoClip
|
| 7 |
+
from moviepy.config import change_settings
|
| 8 |
+
|
| 9 |
+
# Tenta configurar o ImageMagick (necessário para TextClip em alguns ambientes Linux)
|
| 10 |
+
# Se der erro de policy.xml no HF, usaremos uma alternativa sem TextClip complexo ou legendas simplificadas
|
| 11 |
+
try:
|
| 12 |
+
change_settings({"IMAGEMAGICK_BINARY": "/usr/bin/convert"})
|
| 13 |
+
except:
|
| 14 |
+
pass
|
| 15 |
+
|
| 16 |
+
async def text_to_speech(text, voice="pt-BR-FranciscaNeural"):
|
| 17 |
+
"""Gera áudio a partir de texto usando Edge-TTS (Microsoft Azure Free)"""
|
| 18 |
+
communicate = edge_tts.Communicate(text, voice)
|
| 19 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
|
| 20 |
+
await communicate.save(tmp_file.name)
|
| 21 |
+
return tmp_file.name
|
| 22 |
+
|
| 23 |
+
def create_video_segment(image_url, audio_path, text_content):
|
| 24 |
+
"""Cria um segmento de vídeo: Imagem + Áudio + Legenda"""
|
| 25 |
+
# Carrega o áudio para saber a duração
|
| 26 |
+
audio_clip = AudioFileClip(audio_path)
|
| 27 |
+
duration = audio_clip.duration + 0.5 # +0.5s de respiro
|
| 28 |
+
|
| 29 |
+
# Cria o clipe de imagem (Baixa da URL se necessário, mas o Gradio já entrega o path local se enviado como arquivo)
|
| 30 |
+
# Se o input for filepath (caminho local salvo pelo Gradio):
|
| 31 |
+
image_clip = ImageClip(image_url).set_duration(duration)
|
| 32 |
+
|
| 33 |
+
# Redimensiona para formato Vertical (9:16) se necessário, ou mantém proporção
|
| 34 |
+
# Aqui forçamos uma altura padrão de HD vertical (ex: 1280x720 invertido ou similar)
|
| 35 |
+
# Para simplificar, vamos assumir que a imagem gerada já vem no formato certo ou fazemos resize
|
| 36 |
+
image_clip = image_clip.resize(height=1280)
|
| 37 |
+
image_clip = image_clip.set_position("center")
|
| 38 |
+
|
| 39 |
+
# Legenda (Simples)
|
| 40 |
+
# Nota: TextClip pode ser chato de configurar no Linux devido ao ImageMagick.
|
| 41 |
+
# Se der erro, remova este bloco de txt_clip e retorne apenas image_clip.set_audio
|
| 42 |
+
try:
|
| 43 |
+
txt_clip = TextClip(text_content, fontsize=50, color='white', font='Arial-Bold',
|
| 44 |
+
stroke_color='black', stroke_width=2, size=(image_clip.w - 100, None), method='caption')
|
| 45 |
+
txt_clip = txt_clip.set_position(('center', 'bottom')).set_duration(duration).set_start(0)
|
| 46 |
+
video_part = CompositeVideoClip([image_clip, txt_clip])
|
| 47 |
+
except Exception as e:
|
| 48 |
+
print(f"Erro ao gerar legenda (ImageMagick ausente?): {e}")
|
| 49 |
+
video_part = image_clip
|
| 50 |
+
|
| 51 |
+
video_part = video_part.set_audio(audio_clip)
|
| 52 |
+
return video_part
|
| 53 |
+
|
| 54 |
+
async def process_video(scenes_data):
|
| 55 |
+
"""
|
| 56 |
+
Função principal chamada pela API.
|
| 57 |
+
scenes_data esperado: Lista de tuplas/listas [caminho_imagem, texto_narracao]
|
| 58 |
+
Exemplo: [ ["/tmp/img1.jpg", "Maria apareceu..."], ["/tmp/img2.jpg", "Ela disse..."] ]
|
| 59 |
+
"""
|
| 60 |
+
final_clips = []
|
| 61 |
+
|
| 62 |
+
for scene in scenes_data:
|
| 63 |
+
image_path = scene[0]
|
| 64 |
+
text = scene[1]
|
| 65 |
+
|
| 66 |
+
# 1. Gerar Áudio
|
| 67 |
+
audio_path = await text_to_speech(text)
|
| 68 |
+
|
| 69 |
+
# 2. Criar Clipe
|
| 70 |
+
clip = create_video_segment(image_path, audio_path, text)
|
| 71 |
+
final_clips.append(clip)
|
| 72 |
+
|
| 73 |
+
# 3. Concatenar tudo
|
| 74 |
+
final_video = concatenate_videoclips(final_clips, method="compose")
|
| 75 |
+
|
| 76 |
+
output_path = tempfile.mktemp(suffix=".mp4")
|
| 77 |
+
final_video.write_videofile(output_path, fps=24, codec="libx264", audio_codec="aac")
|
| 78 |
+
|
| 79 |
+
return output_path
|
| 80 |
+
|
| 81 |
+
# Wrapper síncrono para o Gradio chamar a função async
|
| 82 |
+
def gradio_entry_point(image1, text1, image2, text2, image3, text3, image4, text4):
|
| 83 |
+
# Por limitações de interface simples do Gradio, vamos aceitar inputs fixos (ex: 4 cenas)
|
| 84 |
+
# Para 12 cenas, o ideal é enviar um JSON, mas vamos fazer simples para teste visual
|
| 85 |
+
# Se o frontend enviar JSON, mudamos aqui.
|
| 86 |
+
|
| 87 |
+
# Monta a lista ignorando vazios
|
| 88 |
+
scenes = []
|
| 89 |
+
inputs = [(image1, text1), (image2, text2), (image3, text3), (image4, text4)]
|
| 90 |
+
|
| 91 |
+
for img, txt in inputs:
|
| 92 |
+
if img and txt:
|
| 93 |
+
scenes.append([img, txt])
|
| 94 |
+
|
| 95 |
+
if not scenes:
|
| 96 |
+
return None
|
| 97 |
+
|
| 98 |
+
return asyncio.run(process_video(scenes))
|
| 99 |
+
|
| 100 |
+
# Interface Visual (Para teste manual no site do HF)
|
| 101 |
+
with gr.Interface(
|
| 102 |
+
fn=gradio_entry_point,
|
| 103 |
+
inputs=[
|
| 104 |
+
gr.Image(type="filepath", label="Cena 1 - Imagem"), gr.Textbox(label="Cena 1 - Texto"),
|
| 105 |
+
gr.Image(type="filepath", label="Cena 2 - Imagem"), gr.Textbox(label="Cena 2 - Texto"),
|
| 106 |
+
gr.Image(type="filepath", label="Cena 3 - Imagem"), gr.Textbox(label="Cena 3 - Texto"),
|
| 107 |
+
gr.Image(type="filepath", label="Cena 4 - Imagem"), gr.Textbox(label="Cena 4 - Texto"),
|
| 108 |
+
],
|
| 109 |
+
outputs=gr.Video(),
|
| 110 |
+
title="Gerador de Vídeo Nossa Senhora (Backend)",
|
| 111 |
+
description="Backend API para renderizar vídeo com MoviePy e EdgeTTS"
|
| 112 |
+
) as demo:
|
| 113 |
+
demo.launch()
|