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
+63 -3
View File
@@ -1065,10 +1065,12 @@
<span style="color:var(--muted); margin-left:12px;">Trajanje:</span> <b id="edit-duration">${(endInit-startInit).toFixed(1)}s</b>
</div>
<div style="display:flex; gap:8px;">
<button class="primary" onclick="seekEditVideo('start')" title="Predvajaj označen del" style="background:var(--accent); padding:8px 16px;">▶ Predvajaj odsek</button>
<button class="primary" id="preview-btn" onclick="previewSelection()" title="Renderiraj in predvajaj točno označen del" style="background:var(--accent); padding:8px 16px;">▶ Predvajaj odsek</button>
<button class="small ghost" onclick="seekEditVideo('start')" title="Skoči na začetek">⤴ Začetek</button>
<button class="small ghost" onclick="seekEditVideo('end')" title="Skoči na konec">↪ Konec</button>
</div>
</div>
<div id="preview-status" style="margin-top:6px; font-size:12px; color:var(--muted); text-align:right;"></div>
<details style="margin-top:14px;">
<summary style="cursor:pointer; font-size:13px; color:var(--muted);">📝 Edit napise (kliknite vrstico za popravek)</summary>
@@ -1220,12 +1222,70 @@
}
};
// Auto-stop predvajanje ko doseže trimEnd (kot iPhone trim preview)
// ─── LIVE PREVIEW: naloži low-q clip označenega dela ───
window.previewSelection = async function() {
const status = document.getElementById("preview-status");
const playBtn = document.getElementById("preview-btn");
const start = trimStart;
const end = trimEnd;
if (end - start < 1) {
alert("Trajanje vsaj 1s");
return;
}
playBtn.disabled = true;
playBtn.textContent = "⏳ Renderiram preview...";
status.textContent = `🎬 Renderiram odsek ${formatTime(start)} - ${formatTime(end)} (~3s)...`;
const url = `/api/preview-clip/${jobId}?start=${start.toFixed(1)}&end=${end.toFixed(1)}`;
const t0 = Date.now();
try {
// Preverim ali je preview že cached (HEAD request)
const headRes = await fetch(url, { method: "HEAD" });
if (!headRes.ok) {
// Cache miss → trigger render z GET
const getRes = await fetch(url);
if (!getRes.ok) {
const err = await getRes.json().catch(() => ({}));
status.textContent = "❌ " + (err.detail || "Render failed");
playBtn.disabled = false;
playBtn.textContent = "▶ Predvajaj odsek";
return;
}
}
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
status.textContent = `✅ Pripravljen v ${elapsed}s. Predvajam...`;
// Set video src to preview clip
video.src = url;
video.load();
video.addEventListener("loadeddata", function loadedOnce() {
video.removeEventListener("loadeddata", loadedOnce);
video.currentTime = 0;
video.play();
}, { once: true });
// Auto-pojavi success
setTimeout(() => { status.textContent = ""; }, 3000);
} catch (e) {
status.textContent = "❌ Napaka: " + e.message;
} finally {
playBtn.disabled = false;
playBtn.textContent = "▶ Predvajaj odsek";
}
};
// Auto-stop predvajanje ko doseže trimEnd (samo če gledamo full video)
if (video) {
video.addEventListener("timeupdate", () => {
// Če gledamo preview clip (src vsebuje '/api/preview-clip/'), ne ustavi
if (video.src && video.src.indexOf("/api/preview-clip/") !== -1) return;
if (!video.paused && video.currentTime >= trimEnd) {
video.pause();
video.currentTime = trimStart; // reset na začetek označenega dela
video.currentTime = trimStart;
}
});
}