AlexandreScriptsMT commited on
Commit
ee827e5
·
verified ·
1 Parent(s): 137cb2f

Update App.py

Browse files
Files changed (1) hide show
  1. App.py +117 -97
App.py CHANGED
@@ -1,113 +1,133 @@
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()
 
1
  import gradio as gr
2
+ import json
3
+ import base64
4
  import tempfile
5
  import os
6
+ import wave
7
+ from moviepy.editor import ImageClip, AudioFileClip, concatenate_videoclips
8
 
9
+ # Configurações de áudio do Gemini (Default: 24kHz, 16bit, Mono)
10
+ SAMPLE_RATE = 24000
11
+ NUM_CHANNELS = 1
12
+ SAMPWIDTH = 2 # 16 bit = 2 bytes
 
 
13
 
14
+ def pcm_to_wav(pcm_base64, output_path):
15
+ """Decodifica Base64 PCM e salva como arquivo WAV com cabeçalhos corretos."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  try:
17
+ # Decodificar string base64 para bytes
18
+ pcm_bytes = base64.b64decode(pcm_base64)
19
+
20
+ # Escrever arquivo WAV
21
+ with wave.open(output_path, 'wb') as wav_file:
22
+ wav_file.setnchannels(NUM_CHANNELS)
23
+ wav_file.setsampwidth(SAMPWIDTH)
24
+ wav_file.setframerate(SAMPLE_RATE)
25
+ wav_file.writeframes(pcm_bytes)
26
+ return True
27
  except Exception as e:
28
+ print(f"Erro ao converter audio: {e}")
29
+ return False
30
 
31
+ def base64_to_image(image_base64, output_path):
32
+ """Decodifica imagem Base64 e salva em arquivo."""
33
+ try:
34
+ with open(output_path, "wb") as f:
35
+ f.write(base64.b64decode(image_base64))
36
+ return True
37
+ except Exception as e:
38
+ print(f"Erro ao salvar imagem: {e}")
39
+ return False
40
 
41
+ def generate_video(project_json):
42
  """
43
+ Recebe um JSON com o manifesto do projeto,
44
+ processa cenas e gera um MP4.
 
45
  """
46
+ try:
47
+ # Se o gradio enviar como dict, usa direto, senão faz parse
48
+ if isinstance(project_json, str):
49
+ data = json.loads(project_json)
50
+ else:
51
+ data = project_json
52
+
53
+ scenes_data = data.get("scenes", [])
54
+ project_title = data.get("project", "video")
 
 
 
55
 
56
+ clips = []
57
+ temp_files = [] # Para limpar depois
58
+
59
+ # Criar diretório temporário para processamento
60
+ with tempfile.TemporaryDirectory() as temp_dir:
61
+
62
+ # Ordenar cenas por ID para garantir sequencia
63
+ scenes_data.sort(key=lambda x: x.get("id", 0))
64
 
65
+ for i, scene in enumerate(scenes_data):
66
+ scene_id = scene.get("id", i)
67
+ print(f"Processando cena {scene_id}...")
68
+
69
+ # Caminhos temporários
70
+ img_path = os.path.join(temp_dir, f"scene_{scene_id}.jpg")
71
+ audio_path = os.path.join(temp_dir, f"scene_{scene_id}.wav")
72
+
73
+ # Extrair dados
74
+ img_b64 = scene.get("image_data_base64")
75
+ audio_b64 = scene.get("audio_data_base64")
76
+
77
+ if not img_b64 or not audio_b64:
78
+ print(f"Pulando cena {scene_id}: dados incompletos")
79
+ continue
80
+
81
+ # Salvar arquivos
82
+ if not base64_to_image(img_b64, img_path): continue
83
+ if not pcm_to_wav(audio_b64, audio_path): continue
84
+
85
+ # Criar Clips Moviepy
86
+ # Audio
87
+ audio_clip = AudioFileClip(audio_path)
88
+
89
+ # Imagem (duração = duração do áudio + 0.5s de respiro)
90
+ duration = audio_clip.duration + 0.2
91
+ video_clip = ImageClip(img_path).set_duration(duration)
92
+
93
+ # Juntar áudio na imagem
94
+ video_clip = video_clip.set_audio(audio_clip)
95
+
96
+ # Fade in/out suave para transição
97
+ video_clip = video_clip.crossfadein(0.5)
98
+
99
+ clips.append(video_clip)
100
+
101
+ if not clips:
102
+ return None
103
 
104
+ # Concatenar tudo
105
+ final_video = concatenate_videoclips(clips, method="compose")
106
+
107
+ # Arquivo de saída persistente (fora do temp dir que será deletado)
108
+ output_filename = f"video_final.mp4"
109
+ final_video.write_videofile(
110
+ output_filename,
111
+ fps=24,
112
+ codec="libx264",
113
+ audio_codec="aac",
114
+ preset="medium"
115
+ )
116
+
117
+ return output_filename
118
+
119
+ except Exception as e:
120
+ return f"Erro no processamento: {str(e)}"
121
 
122
+ # Interface Gradio
123
+ demo = gr.Interface(
124
+ fn=generate_video,
125
+ inputs=gr.JSON(label="Manifesto do Projeto (JSON)"),
126
+ outputs=gr.Video(label="Vídeo Gerado"),
127
+ title="Marian Studio Renderer",
128
+ description="Backend de renderização para o Marian Studio AI via MoviePy."
129
+ )
130
 
131
+ # Habilitar CORS para que seu App local possa chamar essa API
132
+ if __name__ == "__main__":
133
+ demo.launch(share=False, show_api=True, cors_allowed_origins=["*"])