ElevenClip-AI / backend /app /services /video_input.py
JakgritB
feat(backend): add modular video processing API
dbc3c35
import asyncio
import shutil
import subprocess
from pathlib import Path
from fastapi import UploadFile
from app.core.config import Settings
async def save_upload(upload: UploadFile, job_dir: Path) -> Path:
suffix = Path(upload.filename or "upload.mp4").suffix or ".mp4"
destination = job_dir / f"source{suffix.lower()}"
with destination.open("wb") as handle:
while chunk := await upload.read(1024 * 1024):
handle.write(chunk)
return destination
async def resolve_youtube_url(url: str, job_dir: Path, settings: Settings) -> Path:
if settings.demo_mode:
return await asyncio.to_thread(create_demo_video, job_dir, settings)
try:
import yt_dlp
except Exception as exc:
raise RuntimeError("yt-dlp is required for YouTube ingestion") from exc
output_template = str(job_dir / "source.%(ext)s")
ydl_opts = {
"outtmpl": output_template,
"format": "bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]/best",
"merge_output_format": "mp4",
"quiet": True,
"noprogress": True,
}
def download() -> Path:
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([url])
matches = sorted(job_dir.glob("source.*"))
if not matches:
raise RuntimeError("yt-dlp finished without producing a video")
return matches[0]
return await asyncio.to_thread(download)
def create_demo_video(job_dir: Path, settings: Settings) -> Path:
destination = job_dir / "source.mp4"
ffmpeg = shutil.which(settings.ffmpeg_binary)
if not ffmpeg:
destination.write_bytes(b"")
return destination
command = [
ffmpeg,
"-y",
"-f",
"lavfi",
"-i",
"testsrc2=size=1280x720:rate=30:duration=120",
"-f",
"lavfi",
"-i",
"sine=frequency=660:sample_rate=48000:duration=120",
"-shortest",
"-c:v",
"libx264",
"-pix_fmt",
"yuv420p",
"-c:a",
"aac",
str(destination),
]
try:
subprocess.run(command, check=True, capture_output=True, text=True, timeout=45)
except Exception:
destination.write_bytes(b"")
return destination