From 63da3ad2e2098551143194f8fcc2f58259adb9a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastjan=20Arti=C4=8D?= Date: Thu, 30 Apr 2026 12:05:11 +0000 Subject: [PATCH] Preview-clip: validate cache, support HEAD, cleanup on fail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bugs from puppeteer inspection: 1. Old buggy renders left 0-byte cache files behind. New code never re-rendered because cache_path.exists() was True. Fix: validate cache file is >1KB, otherwise re-render. 2. FastAPI @app.get only handles GET, not HEAD. Frontend's HEAD check returned 405, then GET re-rendered (correct), but second click also returned 405 then 200 again — confusing. Fix: use @app.api_route with methods=['GET', 'HEAD'] 3. If ffmpeg fails partway, broken file remains in cache. Fix: unlink on any failure path. Also deleted existing empty cache files in container. --- app/main.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/app/main.py b/app/main.py index 88ac19b..ee25d8f 100644 --- a/app/main.py +++ b/app/main.py @@ -1249,7 +1249,7 @@ async def source_video(job_id: str, user: str = Depends(check_auth)): ) -@app.get("/api/preview-clip/{job_id}") +@app.api_route("/api/preview-clip/{job_id}", methods=["GET", "HEAD"]) async def preview_clip( job_id: str, start: float, @@ -1262,6 +1262,7 @@ async def preview_clip( Cilj: ~2-3s render za takojšen feedback med Edit dragom. Cache po start+end timestampih da ponovljeni request ne renderira ponovno. + Podpira HEAD (frontend cache check). """ job = load_job(job_id) if not job: @@ -1282,19 +1283,24 @@ async def preview_clip( cache_key = f"{job_id}_preview_{start:.1f}_{end:.1f}.mp4" cache_path = OUTPUT_DIR / cache_key - if not cache_path.exists(): - # ffmpeg low-q clip — copy stream če gre, sicer hitro re-encode - # -ss pred -i: hiter seek (keyframe), idealno za preview + # Validate cache: datoteka mora obstajati IN biti vsaj 1KB + cache_valid = cache_path.exists() and cache_path.stat().st_size > 1024 + + if not cache_valid: + # Briši staro prazno če obstaja + if cache_path.exists(): + cache_path.unlink() + # ffmpeg low-q clip — fast seek + force even dimensions for libx264 cmd = [ "ffmpeg", "-y", "-ss", f"{max(0, start - 0.5):.2f}", # 0.5s buffer za keyframe "-i", str(src), "-ss", f"{min(0.5, start):.2f}", # fine seek "-t", f"{duration:.2f}", - "-vf", "scale=854:480:force_original_aspect_ratio=decrease,scale=trunc(iw/2)*2:trunc(ih/2)*2", # 480p, even dimensions for libx264 + "-vf", "scale=854:480:force_original_aspect_ratio=decrease,scale=trunc(iw/2)*2:trunc(ih/2)*2", # 480p, even dimensions "-c:v", "libx264", - "-preset", "ultrafast", # NAJHITREJŠI preset - "-crf", "30", # nižja kvaliteta = hitrejše + "-preset", "ultrafast", + "-crf", "30", "-c:a", "aac", "-b:a", "96k", "-movflags", "+faststart", @@ -1303,9 +1309,14 @@ async def preview_clip( ] try: proc = subprocess.run(cmd, capture_output=True, text=True, timeout=20) - if proc.returncode != 0 or not cache_path.exists(): - raise HTTPException(500, f"FFmpeg failed: {proc.stderr[-300:]}") + if proc.returncode != 0 or not cache_path.exists() or cache_path.stat().st_size < 1024: + # Briši nepopolno datoteko + if cache_path.exists(): + cache_path.unlink() + raise HTTPException(500, f"FFmpeg failed: {(proc.stderr or 'unknown')[-300:]}") except subprocess.TimeoutExpired: + if cache_path.exists(): + cache_path.unlink() raise HTTPException(500, "Preview render timeout (>20s)") return FileResponse(