Files

171 lines
8.0 KiB
Svelte
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script>
// "Smooth Video" modal — RIFE frame interpolation (2x/3x/4x fps) on the source
// clip. Background graph (smooth-background.js); result records as a lineage
// child. Multiplier is primary; model/scale/quality knobs live under Advanced.
let {
open = false,
fps = 16,
frameCount = 0,
progress = null,
onRun,
onDone, // () => land on the result + close (the "jump")
onCancel,
} = $props();
// Newer arches first; rife49 is the proven default and the one bundled. A ckpt
// that isn't on disk auto-downloads on first render.
const CKPTS = ["rife49.pth", "rife426.pth", "rife417.pth", "rife47.pth"];
const SCALES = [0.25, 0.5, 1.0, 2.0, 4.0];
let mult = $state(2);
let advancedOpen = $state(false);
let ckptName = $state("rife49.pth");
let scaleFactor = $state(1.0); // <1 for high-res (less VRAM); 1.0 default
let ensemble = $state(true); // higher quality, slower
let fastMode = $state(true); // faster, slight quality trade-off
let primed = false;
$effect(() => {
if (open && !primed) {
mult = 2; advancedOpen = false; ckptName = "rife49.pth";
scaleFactor = 1.0; ensemble = true; fastMode = true; primed = true;
}
if (!open) primed = false;
});
let phase = $derived(progress?.phase || null);
let running = $derived(phase === "building" || phase === "queueing" || phase === "running");
let done = $derived(phase === "done");
let pct = $derived(progress?.max ? Math.round((progress.value / progress.max) * 100) : 0);
// RIFE outputs (frames-1)*mult + 1 frames; close enough to show the gist.
let outFrames = $derived(frameCount ? (frameCount - 1) * mult + 1 : 0);
function run() {
if (running) return;
onRun?.({ multiplier: mult, ckptName, scaleFactor, ensemble, fastMode });
}
function close() { if (done) onDone?.(); else onCancel?.(); }
</script>
{#if open}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div class="pcr-sm-overlay" onclick={(e) => { if (e.target === e.currentTarget && !running) close(); }}>
<div class="pcr-sm-modal">
<div class="pcr-sm-title">Smooth Video</div>
<div class="pcr-sm-sub">
Adds in-between frames with RIFE for smoother motion. {fps} → {fps * mult} fps{frameCount ? ` · ${frameCount} → ${outFrames} frames` : ""}. Saved as a lineage child.
</div>
<div class="pcr-sm-label">Interpolation</div>
<div class="pcr-sm-mults">
{#each [2, 3, 4] as m}
<button class="pcr-sm-mult" class:pcr-sm-active={mult === m} disabled={running} onclick={() => { mult = m; }}>{m}×</button>
{/each}
</div>
<button class="pcr-sm-adv-toggle" onclick={() => advancedOpen = !advancedOpen}>
{advancedOpen ? "▾" : "▸"} Advanced
</button>
{#if advancedOpen}
<div class="pcr-sm-adv">
<label class="pcr-sm-field">
<span>RIFE model</span>
<select bind:value={ckptName} disabled={running}>
{#each CKPTS as c}<option value={c}>{c.replace(".pth", "")}</option>{/each}
</select>
</label>
<label class="pcr-sm-field">
<span>Scale <small>lower = less VRAM (high-res)</small></span>
<select bind:value={scaleFactor} disabled={running}>
{#each SCALES as s}<option value={s}>{s}×</option>{/each}
</select>
</label>
<label class="pcr-sm-check">
<input type="checkbox" bind:checked={ensemble} disabled={running} />
<span>Ensemble <small>higher quality, slower</small></span>
</label>
<label class="pcr-sm-check">
<input type="checkbox" bind:checked={fastMode} disabled={running} />
<span>Fast mode <small>faster, slight quality trade-off</small></span>
</label>
</div>
{/if}
{#if progress}
<div class="pcr-sm-progress">
{#if phase === "error"}
<div class="pcr-sm-err">⚠ {progress.message || "render failed"}</div>
{:else if done}
<div class="pcr-sm-done">✓ Smoothed. Adjust and Run Again, or Done to view it.</div>
{#if progress.resultUrl}
<!-- svelte-ignore a11y_media_has_caption -->
<video class="pcr-sm-result" src={progress.resultUrl} controls autoplay loop muted></video>
{/if}
{:else}
<div class="pcr-sm-bar"><div class="pcr-sm-fill" style="width:{pct}%"></div></div>
<div class="pcr-sm-phase">{phase}{progress.max ? ` · ${pct}%` : "…"}</div>
{#if progress.previewUrl}
<img class="pcr-sm-preview" src={progress.previewUrl} alt="preview" />
{/if}
{/if}
</div>
{/if}
<div class="pcr-sm-actions">
{#if running}
<button class="pcr-sm-btn" onclick={() => onCancel?.()}>Cancel</button>
{:else if done}
<button class="pcr-sm-btn" onclick={() => onDone?.()}>Done</button>
<button class="pcr-sm-btn pcr-sm-primary" onclick={run}>Run Again</button>
{:else}
<button class="pcr-sm-btn" onclick={() => onCancel?.()}>Close</button>
<button class="pcr-sm-btn pcr-sm-primary" onclick={run}>Smooth</button>
{/if}
</div>
</div>
</div>
{/if}
<style>
.pcr-sm-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(3px);
display: flex; align-items: center; justify-content: center; z-index: 10011;
}
.pcr-sm-modal { background: #1e1e1e; border: 1px solid #444; border-radius: 8px; padding: 20px 24px; min-width: 360px; max-width: 460px; max-height: 88vh; overflow-y: auto; }
.pcr-sm-title { font-size: 15px; font-weight: 600; color: #eee; margin-bottom: 4px; }
.pcr-sm-sub { font-size: 11px; color: #888; margin-bottom: 14px; line-height: 1.4; }
.pcr-sm-label { font-size: 11px; color: #aaa; margin-bottom: 6px; }
.pcr-sm-mults { display: flex; gap: 8px; }
.pcr-sm-mult {
flex: 1; padding: 8px 0; border: 1px solid #555; border-radius: 4px; background: transparent;
color: #ccc; font-size: 13px; cursor: pointer; transition: all 0.15s;
}
.pcr-sm-mult:hover { border-color: #888; color: #fff; }
.pcr-sm-mult.pcr-sm-active { border-color: #4fc3f7; color: #4fc3f7; background: rgba(79,195,247,0.1); }
.pcr-sm-adv-toggle { margin-top: 12px; background: none; border: none; color: #8bd5f5; font-size: 11px; cursor: pointer; padding: 2px 0; }
.pcr-sm-adv { margin-top: 8px; display: flex; flex-direction: column; gap: 10px; }
.pcr-sm-field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; color: #aaa; }
.pcr-sm-field small, .pcr-sm-check small { color: #777; }
.pcr-sm-field select {
background: #161616; border: 1px solid #444; border-radius: 4px; color: #ddd;
font-size: 12px; padding: 5px 6px; font-family: inherit;
}
.pcr-sm-check { display: flex; align-items: center; gap: 7px; font-size: 11px; color: #aaa; cursor: pointer; }
.pcr-sm-progress { margin-top: 14px; }
.pcr-sm-bar { height: 6px; background: #333; border-radius: 3px; overflow: hidden; }
.pcr-sm-fill { height: 100%; background: #4fc3f7; border-radius: 3px; transition: width 0.3s; }
.pcr-sm-phase { font-size: 11px; color: #aaa; margin-top: 6px; text-transform: capitalize; }
.pcr-sm-preview, .pcr-sm-result { width: 100%; margin-top: 8px; border-radius: 4px; max-height: 240px; }
.pcr-sm-err { font-size: 12px; color: #f44336; }
.pcr-sm-done { font-size: 12px; color: #4caf50; margin-bottom: 6px; }
.pcr-sm-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
.pcr-sm-btn { padding: 6px 16px; font-size: 12px; border: 1px solid #555; border-radius: 4px; background: transparent; color: #ccc; cursor: pointer; }
.pcr-sm-btn:hover { background: rgba(255,255,255,0.08); }
.pcr-sm-btn:disabled { opacity: 0.5; cursor: default; }
.pcr-sm-primary { background: #4fc3f7; color: #111; border-color: #4fc3f7; }
.pcr-sm-primary:hover { background: #39b0e4; }
.pcr-sm-primary:disabled { background: #2a7a9e; }
</style>