Spaces:
Runtime error
Runtime error
| import gradio as gr | |
| import edge_tts | |
| import asyncio | |
| import tempfile | |
| import os | |
| from moviepy.editor import ImageClip, concatenate_videoclips, AudioFileClip, TextClip, CompositeVideoClip | |
| from moviepy.config import change_settings | |
| # Tenta configurar o ImageMagick (necessário para TextClip em alguns ambientes Linux) | |
| # Se der erro de policy.xml no HF, usaremos uma alternativa sem TextClip complexo ou legendas simplificadas | |
| try: | |
| change_settings({"IMAGEMAGICK_BINARY": "/usr/bin/convert"}) | |
| except: | |
| pass | |
| async def text_to_speech(text, voice="pt-BR-FranciscaNeural"): | |
| """Gera áudio a partir de texto usando Edge-TTS (Microsoft Azure Free)""" | |
| communicate = edge_tts.Communicate(text, voice) | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file: | |
| await communicate.save(tmp_file.name) | |
| return tmp_file.name | |
| def create_video_segment(image_url, audio_path, text_content): | |
| """Cria um segmento de vídeo: Imagem + Áudio + Legenda""" | |
| # Carrega o áudio para saber a duração | |
| audio_clip = AudioFileClip(audio_path) | |
| duration = audio_clip.duration + 0.5 # +0.5s de respiro | |
| # Cria o clipe de imagem (Baixa da URL se necessário, mas o Gradio já entrega o path local se enviado como arquivo) | |
| # Se o input for filepath (caminho local salvo pelo Gradio): | |
| image_clip = ImageClip(image_url).set_duration(duration) | |
| # Redimensiona para formato Vertical (9:16) se necessário, ou mantém proporção | |
| # Aqui forçamos uma altura padrão de HD vertical (ex: 1280x720 invertido ou similar) | |
| # Para simplificar, vamos assumir que a imagem gerada já vem no formato certo ou fazemos resize | |
| image_clip = image_clip.resize(height=1280) | |
| image_clip = image_clip.set_position("center") | |
| # Legenda (Simples) | |
| # Nota: TextClip pode ser chato de configurar no Linux devido ao ImageMagick. | |
| # Se der erro, remova este bloco de txt_clip e retorne apenas image_clip.set_audio | |
| try: | |
| txt_clip = TextClip(text_content, fontsize=50, color='white', font='Arial-Bold', | |
| stroke_color='black', stroke_width=2, size=(image_clip.w - 100, None), method='caption') | |
| txt_clip = txt_clip.set_position(('center', 'bottom')).set_duration(duration).set_start(0) | |
| video_part = CompositeVideoClip([image_clip, txt_clip]) | |
| except Exception as e: | |
| print(f"Erro ao gerar legenda (ImageMagick ausente?): {e}") | |
| video_part = image_clip | |
| video_part = video_part.set_audio(audio_clip) | |
| return video_part | |
| async def process_video(scenes_data): | |
| """ | |
| Função principal chamada pela API. | |
| scenes_data esperado: Lista de tuplas/listas [caminho_imagem, texto_narracao] | |
| Exemplo: [ ["/tmp/img1.jpg", "Maria apareceu..."], ["/tmp/img2.jpg", "Ela disse..."] ] | |
| """ | |
| final_clips = [] | |
| for scene in scenes_data: | |
| image_path = scene[0] | |
| text = scene[1] | |
| # 1. Gerar Áudio | |
| audio_path = await text_to_speech(text) | |
| # 2. Criar Clipe | |
| clip = create_video_segment(image_path, audio_path, text) | |
| final_clips.append(clip) | |
| # 3. Concatenar tudo | |
| final_video = concatenate_videoclips(final_clips, method="compose") | |
| output_path = tempfile.mktemp(suffix=".mp4") | |
| final_video.write_videofile(output_path, fps=24, codec="libx264", audio_codec="aac") | |
| return output_path | |
| # Wrapper síncrono para o Gradio chamar a função async | |
| def gradio_entry_point(image1, text1, image2, text2, image3, text3, image4, text4): | |
| # Por limitações de interface simples do Gradio, vamos aceitar inputs fixos (ex: 4 cenas) | |
| # Para 12 cenas, o ideal é enviar um JSON, mas vamos fazer simples para teste visual | |
| # Se o frontend enviar JSON, mudamos aqui. | |
| # Monta a lista ignorando vazios | |
| scenes = [] | |
| inputs = [(image1, text1), (image2, text2), (image3, text3), (image4, text4)] | |
| for img, txt in inputs: | |
| if img and txt: | |
| scenes.append([img, txt]) | |
| if not scenes: | |
| return None | |
| return asyncio.run(process_video(scenes)) | |
| # Interface Visual (Para teste manual no site do HF) | |
| with gr.Interface( | |
| fn=gradio_entry_point, | |
| inputs=[ | |
| gr.Image(type="filepath", label="Cena 1 - Imagem"), gr.Textbox(label="Cena 1 - Texto"), | |
| gr.Image(type="filepath", label="Cena 2 - Imagem"), gr.Textbox(label="Cena 2 - Texto"), | |
| gr.Image(type="filepath", label="Cena 3 - Imagem"), gr.Textbox(label="Cena 3 - Texto"), | |
| gr.Image(type="filepath", label="Cena 4 - Imagem"), gr.Textbox(label="Cena 4 - Texto"), | |
| ], | |
| outputs=gr.Video(), | |
| title="Gerador de Vídeo Nossa Senhora (Backend)", | |
| description="Backend API para renderizar vídeo com MoviePy e EdgeTTS" | |
| ) as demo: | |
| demo.launch() | |