Init repo

This commit is contained in:
Leonardo Mortari
2025-07-31 19:29:14 -03:00
commit 55c7ccf316
7 changed files with 314 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/videos
/outputs
/temp
/components/__pycache__

BIN
Montserrat.ttf Normal file

Binary file not shown.

141
components/video.py Normal file
View File

@@ -0,0 +1,141 @@
import os
import subprocess
import unicodedata
from moviepy.video.io.VideoFileClip import VideoFileClip
from moviepy.video.VideoClip import ColorClip
from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip
from moviepy import TextClip
font = "./Montserrat.ttf"
def normalize_filename(filename):
name = unicodedata.normalize('NFKD', filename).encode('ASCII', 'ignore').decode('ASCII')
return name.lower()
def cut_video_new_clip(input_path: str, start: float, end: float, output_path: str):
video_codec = "libx264"
with VideoFileClip(input_path) as clip:
segment = clip.subclipped(start, end)
fps = clip.fps or 30
segment.write_videofile(
output_path,
codec=video_codec,
audio=False,
remove_temp=True,
fps=fps,
ffmpeg_params=[
"-preset", "ultrafast",
"-tune", "zerolatency",
"-pix_fmt", "yuv420p",
"-profile:v", "high",
"-level", "4.1"
]
)
def process_segment(input_path: str, top_text: str = "", bottom_text: str = "", filename="", idx=1) -> str:
os.makedirs("outputs", exist_ok=True)
os.makedirs(f"outputs/{filename}", exist_ok=True)
final_width, final_height = 1080, 1920
top_h, middle_h, bottom_h = 480, 960, 480
with VideoFileClip(input_path) as clip:
dur = clip.duration
bg = ColorClip(size=(final_width, final_height), color=(255, 255, 255), duration=dur)
video_resized = clip.resized(width=final_width)
y = top_h + (middle_h - video_resized.h) // 2
video_resized = video_resized.with_position((0, y))
video_codec = "libx264"
txt_top = TextClip(
text=top_text,
font_size=70,
color="black",
font=font,
method="label",
size=(final_width, top_h)
).with_duration(dur).with_position((0, 0))
txt_bot = TextClip(
text=bottom_text,
font_size=70,
color="black",
font=font,
method="label",
size=(final_width, bottom_h),
).with_duration(dur).with_position((0, final_height - bottom_h))
final = CompositeVideoClip([bg, video_resized, txt_top, txt_bot], size=(final_width, final_height))
output_path = f"outputs/{filename}/clip_{idx}.mp4"
final.write_videofile(
output_path,
codec=video_codec,
audio=False,
remove_temp=True,
ffmpeg_params=[
"-preset", "ultrafast",
"-tune", "zerolatency",
"-pix_fmt", "yuv420p",
"-profile:v", "high",
"-level", "4.1"
]
)
final.close()
return output_path
def timestamp_to_seconds(ts):
if isinstance(ts, (int, float)):
return ts
parts = ts.split(":")
parts = [float(p) for p in parts]
if len(parts) == 3:
h, m, s = parts
return h * 3600 + m * 60 + s
elif len(parts) == 2:
m, s = parts
return m * 60 + s
elif len(parts) == 1:
return parts[0]
else:
raise ValueError(f"Timestamp inválido: {ts}")
def process_full_video(filename: str, title: str, times: list = None) -> list:
os.makedirs("temp", exist_ok=True)
times = times or []
video_path = f"videos/{title}"
processed = []
video_codec = "libx264"
print(f"Total de trechos: {len(times)}")
print(f"Codec de render: {video_codec}")
for idx, interval in enumerate(times, start=1):
start = timestamp_to_seconds(interval.get("start", 0))
end_raw = interval.get("end", None)
end = timestamp_to_seconds(end_raw) if end_raw is not None else None
top_text = interval.get("topText", "")
bottom_text = interval.get("bottomText", "")
if end is None:
with VideoFileClip(video_path) as clip:
end = clip.duration
print(f"Cortando trecho {idx}: {start}s a {end}s")
temp_path = f"temp/{os.path.splitext(filename)[0]}_{idx}.mp4"
cut_video_new_clip(video_path, start, end, temp_path)
out = process_segment(temp_path, top_text, bottom_text, filename, idx)
processed.append(out)
return processed

31
docker-compose.yml Normal file
View File

@@ -0,0 +1,31 @@
services:
video-render-api:
restart: unless-stopped
build: .
container_name: video-render-api
ports:
- "5000:5000"
volumes:
# - /home/well/outputs:/app/outputs
# - /home/well/videosDownload:/app/videos
# - /home/well/tempVideos:/app/temp
- ".:/app" # descomentar somente se for rodar no windows/linux local
# gpus: all
# environment:
# - NVIDIA_VISIBLE_DEVICES=all
# - NVIDIA_DRIVER_CAPABILITIES=compute,video,utility
command: "python -u main.py"
# runtime: nvidia
networks:
- dokploy-network
# deploy:
# resources:
# reservations:
# devices:
# - driver: nvidia
# count: all
# capabilities: [gpu]
networks:
dokploy-network:
external: true

36
dockerfile Normal file
View File

@@ -0,0 +1,36 @@
FROM python:3.11-slim
WORKDIR /app
EXPOSE 5000
ENV DEBIAN_FRONTEND=noninteractive
COPY requirements.txt Montserrat.ttf ./
RUN apt-get update && \
apt-get install -qq -y \
build-essential \
xvfb \
xdg-utils \
wget \
unzip \
ffmpeg \
libpq-dev \
vim \
libmagick++-dev \
imagemagick \
fonts-liberation \
sox \
bc \
gsfonts && \
fc-cache -fv && \
rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
VOLUME ["/app"]
CMD ["python", "-u", "main.py"]

97
main.py Normal file
View File

@@ -0,0 +1,97 @@
import os
import requests
import threading
import glob
import shutil
os.environ["IMAGEIO_FFMPEG_EXE"] = "/usr/bin/ffmpeg"
from flask import Flask, request, jsonify
from components.video import process_full_video
app = Flask(__name__)
def process_and_call_webhook(url, video_id, times, webhook_url, row_number, filename, title):
try:
os.makedirs("videos", exist_ok=True)
os.makedirs("temp", exist_ok=True)
print(f"Working on video {filename}")
processed_files = process_full_video(filename, title, times)
payload = {
"videosProcessedQuantity": len(processed_files),
"filename": filename,
"processedFiles": processed_files,
"rowNumber": row_number,
"url": url,
"videoId": video_id,
"error": False,
}
try:
resp = requests.post(webhook_url, json=payload, timeout=30)
print(f"Webhook status: {resp.status_code}, content: {resp.text}")
except Exception as webhook_error:
print(f"Erro ao chamar webhook: {webhook_error}")
except Exception as e:
payload = {
"videosProcessedQuantity": 0,
"filename": filename,
"processedFiles": processed_files,
"rowNumber": row_number,
"url": url,
"videoId": video_id,
"error": str(e),
}
try:
resp = requests.post(webhook_url, json=payload, timeout=30)
print(f"Webhook send error status: {resp.status_code}")
print(str(e))
except Exception as webhook_error:
print(f"Erro ao chamar webhook: {webhook_error}")
print(f"Erro no processamento: {e}")
@app.route('/process', methods=['POST'])
def process_video():
data = request.get_json()
if not data or not ("url" in data or "videoId" in data):
return jsonify({"error": "Informe 'url' ou 'videoId'"}), 400
url = data.get("url")
video_id = data.get("videoId")
title = data.get("title", "")
times = data.get("times", [])
webhook_url = data.get("webhookUrl")
row_number = data.get("rowNumber")
if not webhook_url:
return jsonify({"error": "Informe 'webhookUrl'"}), 400
if not row_number:
return jsonify({"error": "Informe a linha da planilha"}), 400
filename = data.get("filename")
if not filename:
return jsonify({"error": "Informe 'filename'"}), 400
threading.Thread(
target=process_and_call_webhook,
args=(url, video_id, times, webhook_url, row_number, filename, title),
daemon=True
).start()
return jsonify({"message": f"{video_id if video_id else url}"}), 200
if __name__ == '__main__':
app.run(host="0.0.0.0", port=5000, debug=True)

5
requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
flask
moviepy==2.2.0
pillow==9.5.0
yt_dlp
requests