Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -6,94 +6,114 @@ from fastapi.responses import FileResponse
|
|
| 6 |
from pydantic import BaseModel
|
| 7 |
from faster_whisper import WhisperModel
|
| 8 |
|
| 9 |
-
# ---
|
| 10 |
-
#
|
| 11 |
-
from moviepy.
|
| 12 |
-
from moviepy.audio.io.AudioFileClip import AudioFileClip
|
| 13 |
-
from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip
|
| 14 |
-
from moviepy.video.VideoClip import TextClip
|
| 15 |
-
# Nota: Em algumas versões v2, o TextClip pode estar em moviepy.video.tools.subtitles
|
| 16 |
-
# ou exigir configuração explicita de fonte.
|
| 17 |
|
| 18 |
-
# Inicializa a API
|
| 19 |
app = FastAPI()
|
| 20 |
|
|
|
|
| 21 |
class VideoRequest(BaseModel):
|
| 22 |
-
image_url: str
|
| 23 |
audio_url: str
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
def download_file(url, filename):
|
| 26 |
-
|
| 27 |
-
|
|
|
|
| 28 |
with open(filename, 'wb') as f:
|
| 29 |
-
for chunk in response.iter_content(
|
| 30 |
f.write(chunk)
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
def criar_video_logica(imagem_path, audio_path, output_path):
|
| 35 |
-
print("Carregando modelo Whisper...")
|
| 36 |
-
model = WhisperModel("tiny", device="cpu", compute_type="int8")
|
| 37 |
-
segments, _ = model.transcribe(audio_path, language="pt")
|
| 38 |
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
-
#
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
# TextClip na v2 pode ter assinatura diferente dependendo da sub-versão (alpha/beta/stable)
|
| 56 |
-
# Este é o padrão mais seguro:
|
| 57 |
-
txt_clip = TextClip(text=segment.text.strip(), **font_conf)
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
txt_clip = txt_clip.with_position(('center', 'bottom'))
|
| 62 |
-
text_clips.append(txt_clip)
|
| 63 |
-
|
| 64 |
-
print("Processando Áudio e Imagem...")
|
| 65 |
-
audio_clip = AudioFileClip(audio_path)
|
| 66 |
|
| 67 |
-
#
|
| 68 |
-
|
| 69 |
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
-
print("Renderizando vídeo...")
|
| 73 |
-
# write_videofile continua similar, mas 'preset' e 'threads' são geridos pelo ffmpeg
|
| 74 |
-
final.write_videofile(output_path, fps=10, codec="libx264", audio_codec="aac", preset="ultrafast", threads=2)
|
| 75 |
return output_path
|
| 76 |
|
| 77 |
@app.post("/gerar-video")
|
| 78 |
async def gerar_video_endpoint(request: VideoRequest):
|
| 79 |
try:
|
| 80 |
-
|
| 81 |
temp_audio = "temp_audio.mp3"
|
|
|
|
| 82 |
output_video = "video_final.mp4"
|
| 83 |
|
| 84 |
-
|
| 85 |
-
|
| 86 |
download_file(request.audio_url, temp_audio)
|
| 87 |
|
| 88 |
-
#
|
| 89 |
-
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
-
|
|
|
|
| 93 |
|
| 94 |
except Exception as e:
|
| 95 |
-
print(f"
|
| 96 |
-
# Retorna o erro na resposta para você ver no Apps Script
|
| 97 |
raise HTTPException(status_code=500, detail=str(e))
|
| 98 |
|
| 99 |
if __name__ == "__main__":
|
|
|
|
| 6 |
from pydantic import BaseModel
|
| 7 |
from faster_whisper import WhisperModel
|
| 8 |
|
| 9 |
+
# --- A IMPORTAÇÃO CLÁSSICA E SEGURA ---
|
| 10 |
+
# Na versão 1.0.3, este módulo contém tudo que precisamos
|
| 11 |
+
from moviepy.editor import ImageClip, AudioFileClip, TextClip, CompositeVideoClip, concatenate_videoclips
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
|
|
|
| 13 |
app = FastAPI()
|
| 14 |
|
| 15 |
+
# Modelo de dados
|
| 16 |
class VideoRequest(BaseModel):
|
|
|
|
| 17 |
audio_url: str
|
| 18 |
+
imagens: list[str] # Aceita lista de imagens agora
|
| 19 |
+
duracao_estimada: int = 60
|
| 20 |
+
|
| 21 |
+
# --- Funções Auxiliares ---
|
| 22 |
|
| 23 |
def download_file(url, filename):
|
| 24 |
+
try:
|
| 25 |
+
response = requests.get(url, stream=True)
|
| 26 |
+
response.raise_for_status() # Garante que deu 200 OK
|
| 27 |
with open(filename, 'wb') as f:
|
| 28 |
+
for chunk in response.iter_content(chunk_size=8192):
|
| 29 |
f.write(chunk)
|
| 30 |
+
except Exception as e:
|
| 31 |
+
print(f"Erro ao baixar {url}: {e}")
|
| 32 |
+
raise e
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
+
def criar_video_v1(lista_imagens_paths, audio_path, output_path):
|
| 35 |
+
print("1. Carregando Áudio...")
|
| 36 |
+
audio_clip = AudioFileClip(audio_path)
|
| 37 |
+
duracao_total = audio_clip.duration
|
| 38 |
|
| 39 |
+
# Cálculos de tempo
|
| 40 |
+
qtd_imgs = len(lista_imagens_paths)
|
| 41 |
+
if qtd_imgs == 0: raise Exception("Nenhuma imagem fornecida")
|
| 42 |
+
tempo_por_imagem = duracao_total / qtd_imgs
|
| 43 |
+
|
| 44 |
+
print(f"2. Montando Timeline ({qtd_imgs} imagens, {tempo_por_imagem:.2f}s cada)...")
|
| 45 |
+
clips_visuais = []
|
| 46 |
+
|
| 47 |
+
for img_path in lista_imagens_paths:
|
| 48 |
+
# Configuração Versão 1.0.3 (usando set_)
|
| 49 |
+
clip = ImageClip(img_path).set_duration(tempo_por_imagem)
|
| 50 |
+
# Opcional: Resize para garantir que caiba (se necessário)
|
| 51 |
+
# clip = clip.resize(height=1080)
|
| 52 |
+
clips_visuais.append(clip)
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
+
# Junta as imagens em sequência
|
| 55 |
+
video_sem_audio = concatenate_videoclips(clips_visuais, method="compose")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
+
# Junta com o áudio
|
| 58 |
+
video_final = video_sem_audio.set_audio(audio_clip)
|
| 59 |
|
| 60 |
+
# --- GERAÇÃO DE LEGENDAS (Opcional - Pode comentar se der erro de ImageMagick) ---
|
| 61 |
+
# Para ativar legendas, descomente abaixo. Se der erro de "convert", deixe comentado por enquanto.
|
| 62 |
+
# print("3. Gerando Legendas (Whisper)...")
|
| 63 |
+
# model = WhisperModel("tiny", device="cpu", compute_type="int8")
|
| 64 |
+
# segments, _ = model.transcribe(audio_path, language="pt")
|
| 65 |
+
# legendas_clips = []
|
| 66 |
+
# for segment in segments:
|
| 67 |
+
# txt = TextClip(segment.text, fontsize=24, color='white', font='DejaVu-Sans-Bold', stroke_color='black', stroke_width=1, size=(800, None), method='caption')
|
| 68 |
+
# txt = txt.set_start(segment.start).set_duration(segment.end - segment.start).set_position(('center', 'bottom'))
|
| 69 |
+
# legendas_clips.append(txt)
|
| 70 |
+
# video_final = CompositeVideoClip([video_final] + legendas_clips)
|
| 71 |
+
# ---------------------------------------------------------------------------------
|
| 72 |
+
|
| 73 |
+
print("4. Renderizando Arquivo Final...")
|
| 74 |
+
# fps=10 é leve e rápido para imagens estáticas
|
| 75 |
+
video_final.write_videofile(
|
| 76 |
+
output_path,
|
| 77 |
+
fps=10,
|
| 78 |
+
codec="libx264",
|
| 79 |
+
audio_codec="aac",
|
| 80 |
+
preset="ultrafast",
|
| 81 |
+
threads=2
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
# Limpeza de memória (importante no Hugging Face)
|
| 85 |
+
audio_clip.close()
|
| 86 |
+
video_final.close()
|
| 87 |
|
|
|
|
|
|
|
|
|
|
| 88 |
return output_path
|
| 89 |
|
| 90 |
@app.post("/gerar-video")
|
| 91 |
async def gerar_video_endpoint(request: VideoRequest):
|
| 92 |
try:
|
| 93 |
+
# Nomes temporários
|
| 94 |
temp_audio = "temp_audio.mp3"
|
| 95 |
+
lista_imgs_locais = []
|
| 96 |
output_video = "video_final.mp4"
|
| 97 |
|
| 98 |
+
# 1. Baixar Audio
|
| 99 |
+
print(f"Baixando áudio...")
|
| 100 |
download_file(request.audio_url, temp_audio)
|
| 101 |
|
| 102 |
+
# 2. Baixar Imagens (Loop)
|
| 103 |
+
print(f"Baixando {len(request.imagens)} imagens...")
|
| 104 |
+
for i, url in enumerate(request.imagens):
|
| 105 |
+
nome_img = f"temp_img_{i}.jpg"
|
| 106 |
+
download_file(url, nome_img)
|
| 107 |
+
lista_imgs_locais.append(nome_img)
|
| 108 |
+
|
| 109 |
+
# 3. Processar
|
| 110 |
+
criar_video_v1(lista_imgs_locais, temp_audio, output_video)
|
| 111 |
|
| 112 |
+
# 4. Retornar
|
| 113 |
+
return FileResponse(output_video, media_type="video/mp4", filename="video_renderizado.mp4")
|
| 114 |
|
| 115 |
except Exception as e:
|
| 116 |
+
print(f"ERRO CRÍTICO: {str(e)}")
|
|
|
|
| 117 |
raise HTTPException(status_code=500, detail=str(e))
|
| 118 |
|
| 119 |
if __name__ == "__main__":
|