Initial commit: image+audio to YouTube MP4 queue

This commit is contained in:
Troll (Hermes Agent) 2026-08-05 22:29:01 +00:00
commit f2d53a573f
15 changed files with 826 additions and 0 deletions

78
worker.py Normal file
View file

@ -0,0 +1,78 @@
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
from PIL import Image
from config import Config
from models import JobStore
def _ffmpeg_available() -> bool:
return shutil.which('ffmpeg') is not None
def _prepare_image(image_path: Path, output_path: Path, width: int, height: int) -> None:
img = Image.open(image_path)
img = img.convert('RGB')
# Fit inside target box, then letterbox/pillarbox with black to exact 16:9
img.thumbnail((width, height), Image.LANCZOS)
canvas = Image.new('RGB', (width, height), (0, 0, 0))
x = (width - img.width) // 2
y = (height - img.height) // 2
canvas.paste(img, (x, y))
canvas.save(output_path, 'JPEG', quality=95)
def _build_video(image_path: Path, audio_path: Path, output_path: Path, cfg: Config) -> None:
if not _ffmpeg_available():
raise RuntimeError('ffmpeg is not installed or not on PATH')
with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
prepared_image = tmp / 'prepared.jpg'
_prepare_image(image_path, prepared_image, cfg.VIDEO_WIDTH, cfg.VIDEO_HEIGHT)
cmd = [
'ffmpeg',
'-y',
'-loop', '1',
'-framerate', str(cfg.VIDEO_FPS),
'-i', str(prepared_image),
'-i', str(audio_path),
'-c:v', cfg.VIDEO_CODEC,
'-pix_fmt', 'yuv420p',
'-preset', 'medium',
'-b:v', cfg.VIDEO_BITRATE,
'-c:a', cfg.AUDIO_CODEC,
'-b:a', cfg.AUDIO_BITRATE,
'-movflags', '+faststart',
'-shortest',
str(output_path),
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f'ffmpeg failed: {result.stderr}')
def process_job(job_id: int, store: JobStore, cfg: Config) -> None:
job = store.get_job(job_id)
if job is None:
return
image_path = cfg.UPLOAD_FOLDER / job['image_file']
audio_path = cfg.UPLOAD_FOLDER / job['audio_file']
output_filename = f"job-{job_id}.{cfg.OUTPUT_FORMAT}"
output_path = cfg.OUTPUT_FOLDER / output_filename
try:
cfg.OUTPUT_FOLDER.mkdir(parents=True, exist_ok=True)
_build_video(image_path, audio_path, output_path, cfg)
store.update_status(job_id, 'complete', output_file=output_filename)
except Exception as exc:
store.update_status(job_id, 'error', error_message=str(exc))
raise