| 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 |
|
|