Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -5,20 +5,24 @@ from fastapi import FastAPI, HTTPException
|
|
| 5 |
from fastapi.responses import FileResponse
|
| 6 |
from pydantic import BaseModel
|
| 7 |
from faster_whisper import WhisperModel
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
# Inicializa a API
|
| 11 |
app = FastAPI()
|
| 12 |
|
| 13 |
-
# Modelo de dados que esperamos receber do GAS
|
| 14 |
class VideoRequest(BaseModel):
|
| 15 |
image_url: str
|
| 16 |
audio_url: str
|
| 17 |
|
| 18 |
-
# --- Funções Auxiliares ---
|
| 19 |
-
|
| 20 |
def download_file(url, filename):
|
| 21 |
-
"""Baixa o arquivo da URL e salva localmente"""
|
| 22 |
response = requests.get(url, stream=True)
|
| 23 |
if response.status_code == 200:
|
| 24 |
with open(filename, 'wb') as f:
|
|
@@ -28,63 +32,69 @@ def download_file(url, filename):
|
|
| 28 |
raise Exception(f"Erro ao baixar {url}")
|
| 29 |
|
| 30 |
def criar_video_logica(imagem_path, audio_path, output_path):
|
| 31 |
-
"""A lógica de edição (Whisper + MoviePy)"""
|
| 32 |
print("Carregando modelo Whisper...")
|
| 33 |
model = WhisperModel("tiny", device="cpu", compute_type="int8")
|
| 34 |
segments, _ = model.transcribe(audio_path, language="pt")
|
| 35 |
|
| 36 |
text_clips = []
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
"size": (800, None)
|
| 42 |
}
|
| 43 |
|
| 44 |
print("Gerando legendas...")
|
| 45 |
for segment in segments:
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
txt_clip =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
text_clips.append(txt_clip)
|
| 50 |
|
|
|
|
| 51 |
audio_clip = AudioFileClip(audio_path)
|
| 52 |
-
|
|
|
|
|
|
|
| 53 |
|
| 54 |
final = CompositeVideoClip([image_clip] + text_clips)
|
| 55 |
|
| 56 |
print("Renderizando vídeo...")
|
|
|
|
| 57 |
final.write_videofile(output_path, fps=10, codec="libx264", audio_codec="aac", preset="ultrafast", threads=2)
|
| 58 |
return output_path
|
| 59 |
|
| 60 |
-
# --- O Endpoint da API (Onde o GAS conecta) ---
|
| 61 |
-
|
| 62 |
@app.post("/gerar-video")
|
| 63 |
async def gerar_video_endpoint(request: VideoRequest):
|
| 64 |
try:
|
| 65 |
-
|
| 66 |
-
temp_img = "temp_image.png"
|
| 67 |
temp_audio = "temp_audio.mp3"
|
| 68 |
output_video = "video_final.mp4"
|
| 69 |
|
| 70 |
-
|
| 71 |
-
print(f"Baixando imagem de: {request.image_url}")
|
| 72 |
download_file(request.image_url, temp_img)
|
| 73 |
-
|
| 74 |
-
print(f"Baixando áudio de: {request.audio_url}")
|
| 75 |
download_file(request.audio_url, temp_audio)
|
| 76 |
|
| 77 |
-
#
|
|
|
|
| 78 |
criar_video_logica(temp_img, temp_audio, output_video)
|
| 79 |
|
| 80 |
-
# 4. Devolver o arquivo pronto para o GAS
|
| 81 |
-
# O FileResponse envia o arquivo binário na resposta HTTP
|
| 82 |
return FileResponse(output_video, media_type="video/mp4", filename="video_editado.mp4")
|
| 83 |
|
| 84 |
except Exception as e:
|
| 85 |
print(f"Erro: {e}")
|
|
|
|
| 86 |
raise HTTPException(status_code=500, detail=str(e))
|
| 87 |
|
| 88 |
-
# Configuração para rodar no Hugging Face
|
| 89 |
if __name__ == "__main__":
|
| 90 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
|
| 5 |
from fastapi.responses import FileResponse
|
| 6 |
from pydantic import BaseModel
|
| 7 |
from faster_whisper import WhisperModel
|
| 8 |
+
|
| 9 |
+
# --- MUDANÇA CRÍTICA AQUI (Padrão MoviePy 2.0+) ---
|
| 10 |
+
# Não existe mais 'moviepy.editor'. Importamos dos módulos específicos.
|
| 11 |
+
from moviepy.video.io.ImageClip import ImageClip
|
| 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 |
response = requests.get(url, stream=True)
|
| 27 |
if response.status_code == 200:
|
| 28 |
with open(filename, 'wb') as f:
|
|
|
|
| 32 |
raise Exception(f"Erro ao baixar {url}")
|
| 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 |
text_clips = []
|
| 40 |
+
|
| 41 |
+
# Configurações de fonte (Ajustadas para v2)
|
| 42 |
+
# Na v2, as vezes é necessário apontar o caminho da fonte se o ImageMagick não achar
|
| 43 |
+
font_conf = {
|
| 44 |
+
"font_size": 30, # Mudou de fontsize para font_size em algumas builds v2
|
| 45 |
+
"color": 'white',
|
| 46 |
+
"font": 'Arial', # Garanta que essa fonte exista no Linux ou use "DejaVu-Sans"
|
| 47 |
+
"stroke_color": 'black',
|
| 48 |
+
"stroke_width": 2,
|
| 49 |
+
"method": 'caption',
|
| 50 |
"size": (800, None)
|
| 51 |
}
|
| 52 |
|
| 53 |
print("Gerando legendas...")
|
| 54 |
for segment in segments:
|
| 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 |
+
txt_clip = txt_clip.with_start(segment.start).with_duration(segment.end - segment.start)
|
| 60 |
+
# 'set_position' mudou para 'with_position' em muitas partes da v2 para encadear métodos
|
| 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 |
+
# set_duration -> with_duration / set_audio -> with_audio
|
| 68 |
+
image_clip = ImageClip(imagem_path).with_duration(audio_clip.duration).with_audio(audio_clip)
|
| 69 |
|
| 70 |
final = CompositeVideoClip([image_clip] + text_clips)
|
| 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 |
+
temp_img = "temp_image.jpg" # Ajustado para jpg pois o Gemini manda jpg
|
|
|
|
| 81 |
temp_audio = "temp_audio.mp3"
|
| 82 |
output_video = "video_final.mp4"
|
| 83 |
|
| 84 |
+
print(f"Baixando: {request.image_url}")
|
|
|
|
| 85 |
download_file(request.image_url, temp_img)
|
|
|
|
|
|
|
| 86 |
download_file(request.audio_url, temp_audio)
|
| 87 |
|
| 88 |
+
# Se for uma lista de imagens (lógica nova), precisa ajustar aqui.
|
| 89 |
+
# Mantendo simples para teste:
|
| 90 |
criar_video_logica(temp_img, temp_audio, output_video)
|
| 91 |
|
|
|
|
|
|
|
| 92 |
return FileResponse(output_video, media_type="video/mp4", filename="video_editado.mp4")
|
| 93 |
|
| 94 |
except Exception as e:
|
| 95 |
print(f"Erro: {e}")
|
| 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__":
|
| 100 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|