Preview-clip: validate cache, support HEAD, cleanup on fail
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.
This commit is contained in:
parent
69062205fd
commit
63da3ad2e2
29
app/main.py
29
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(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user