Live preview in Edit modal — fast low-q clip render

User feedback: 'dejstvo je da trajna ker more najprej zrenderirat? to traja?
za to bi rabili hudo mašino al?'

Solution before GPU upgrade: live preview that renders just the selected
range as low-quality 480p clip. ~2-3s instead of ~70s full reel render.

NEW endpoint: GET /api/preview-clip/{job_id}?start=X&end=Y
- ffmpeg fast extract (no reframe, no subtitles, no face tracking)
- 480p ultrafast x264 preset, CRF 30
- Cached per job+range (re-clicks are instant)
- ~2-3s on CPU

Frontend:
- '▶ Predvajaj odsek' button now triggers preview-clip render
- Shows status: '🎬 Renderiram odsek... (~3s)'
- After render: video element switches to preview src
- User sees EXACTLY what reel will contain (just without face track)
- Subsequent clicks on same range are instant (cached)

Workflow:
- Drag handles → click '▶ Predvajaj odsek' → 3s wait → see + hear it
- Iterate fast: drag → preview → drag → preview
- Final '✅ Shrani in re-render' only when satisfied (~70s full render)
This commit is contained in:
2026-04-30 11:56:54 +00:00
parent 274ff80b34
commit 0513768466
2 changed files with 129 additions and 3 deletions
+66
View File
@@ -1249,6 +1249,72 @@ async def source_video(job_id: str, user: str = Depends(check_auth)):
)
@app.get("/api/preview-clip/{job_id}")
async def preview_clip(
job_id: str,
start: float,
end: float,
user: str = Depends(check_auth),
):
"""Live preview odseka — low-quality 480p clip za hiter predogled.
BREZ reframe (16:9 ostane), BREZ napisov, BREZ face tracking.
Cilj: ~2-3s render za takojšen feedback med Edit dragom.
Cache po start+end timestampih da ponovljeni request ne renderira ponovno.
"""
job = load_job(job_id)
if not job:
raise HTTPException(404, "Ne obstaja")
src = job.get("input_path")
if not src or not Path(src).exists():
raise HTTPException(404, "Original video ne obstaja")
if end <= start:
raise HTTPException(400, "end mora biti večji od start")
duration = end - start
if duration < 1:
raise HTTPException(400, "Trajanje vsaj 1s")
if duration > 90:
raise HTTPException(400, "Trajanje največ 90s")
# Cache key — preview se shrani in ne re-renderira če isti range
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
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", # 480p
"-c:v", "libx264",
"-preset", "ultrafast", # NAJHITREJŠI preset
"-crf", "30", # nižja kvaliteta = hitrejše
"-c:a", "aac",
"-b:a", "96k",
"-movflags", "+faststart",
"-loglevel", "error",
str(cache_path),
]
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:]}")
except subprocess.TimeoutExpired:
raise HTTPException(500, "Preview render timeout (>20s)")
return FileResponse(
path=cache_path,
media_type="video/mp4",
headers={"Accept-Ranges": "bytes", "Cache-Control": "max-age=300"},
)
@app.get("/api/transcript/{job_id}")
async def get_transcript(job_id: str, user: str = Depends(check_auth)):
"""Vrne transkript (segmente + words) za editor."""