import gradio as gr import json import base64 import tempfile import os import wave from moviepy.editor import ImageClip, AudioFileClip, concatenate_videoclips # Configurações de áudio do Gemini (Default: 24kHz, 16bit, Mono) SAMPLE_RATE = 24000 NUM_CHANNELS = 1 SAMPWIDTH = 2 # 16 bit = 2 bytes def pcm_to_wav(pcm_base64, output_path): """Decodifica Base64 PCM e salva como arquivo WAV com cabeçalhos corretos.""" try: # Decodificar string base64 para bytes pcm_bytes = base64.b64decode(pcm_base64) # Escrever arquivo WAV with wave.open(output_path, 'wb') as wav_file: wav_file.setnchannels(NUM_CHANNELS) wav_file.setsampwidth(SAMPWIDTH) wav_file.setframerate(SAMPLE_RATE) wav_file.writeframes(pcm_bytes) return True except Exception as e: print(f"Erro ao converter audio: {e}") return False def base64_to_image(image_base64, output_path): """Decodifica imagem Base64 e salva em arquivo.""" try: with open(output_path, "wb") as f: f.write(base64.b64decode(image_base64)) return True except Exception as e: print(f"Erro ao salvar imagem: {e}") return False def generate_video(project_json): """ Recebe um JSON com o manifesto do projeto, processa cenas e gera um MP4. """ try: # Se o gradio enviar como dict, usa direto, senão faz parse if isinstance(project_json, str): data = json.loads(project_json) else: data = project_json scenes_data = data.get("scenes", []) clips = [] # Criar diretório temporário para processamento with tempfile.TemporaryDirectory() as temp_dir: # Ordenar cenas por ID para garantir sequencia scenes_data.sort(key=lambda x: x.get("id", 0)) for i, scene in enumerate(scenes_data): scene_id = scene.get("id", i) print(f"Processando cena {scene_id}...") # Caminhos temporários img_path = os.path.join(temp_dir, f"scene_{scene_id}.jpg") audio_path = os.path.join(temp_dir, f"scene_{scene_id}.wav") # Extrair dados img_b64 = scene.get("image_data_base64") audio_b64 = scene.get("audio_data_base64") if not img_b64 or not audio_b64: print(f"Pulando cena {scene_id}: dados incompletos") continue # Salvar arquivos if not base64_to_image(img_b64, img_path): continue if not pcm_to_wav(audio_b64, audio_path): continue # Criar Clips Moviepy # Audio audio_clip = AudioFileClip(audio_path) # Imagem (duração = duração do áudio + 0.5s de respiro) duration = audio_clip.duration + 0.2 video_clip = ImageClip(img_path).set_duration(duration) # Juntar áudio na imagem video_clip = video_clip.set_audio(audio_clip) # Fade in/out suave para transição video_clip = video_clip.crossfadein(0.5) clips.append(video_clip) if not clips: return None # Concatenar tudo final_video = concatenate_videoclips(clips, method="compose") # Arquivo de saída persistente output_filename = "video_final.mp4" final_video.write_videofile( output_filename, fps=24, codec="libx264", audio_codec="aac", preset="medium" ) return output_filename except Exception as e: return f"Erro no processamento: {str(e)}" # Interface Gradio demo = gr.Interface( fn=generate_video, inputs=gr.JSON(label="Manifesto do Projeto (JSON)"), outputs=gr.Video(label="Vídeo Gerado"), title="Marian Studio Renderer", description="Backend de renderização para o Marian Studio AI via MoviePy." ) if __name__ == "__main__": demo.launch()