Init repo
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/videos
|
||||
/outputs
|
||||
/temp
|
||||
/components/__pycache__
|
||||
BIN
Montserrat.ttf
Normal file
BIN
Montserrat.ttf
Normal file
Binary file not shown.
141
components/video.py
Normal file
141
components/video.py
Normal 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
31
docker-compose.yml
Normal 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
36
dockerfile
Normal 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
97
main.py
Normal 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
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
flask
|
||||
moviepy==2.2.0
|
||||
pillow==9.5.0
|
||||
yt_dlp
|
||||
requests
|
||||
Reference in New Issue
Block a user