From a20e0bf97f5f8c1098ec75410ac12b66e8d8278b Mon Sep 17 00:00:00 2001 From: dev-ansung Date: Sat, 3 Oct 2026 19:01:58 -0700 Subject: [PATCH 1/2] feat(presets): add comprehensive preset profiles with dynamic parameter binding and UI polish --- src/rvcg/cli.py | 15 +- src/rvcg/models.py | 179 ++++++++++++++++----- src/rvcg/static/js/gallery.js | 23 +-- src/rvcg/static/js/picker.js | 140 +++++++++------- src/rvcg/static/js/settings.js | 14 +- src/rvcg/static/webui.html | 286 ++++++++++++++++++++++----------- src/rvcg/webui.py | 31 +++- tests/test_cli.py | 9 ++ tests/test_server.py | 9 +- tests/test_solver.py | 28 +++- 10 files changed, 514 insertions(+), 220 deletions(-) diff --git a/src/rvcg/cli.py b/src/rvcg/cli.py index 1ece651..798560b 100644 --- a/src/rvcg/cli.py +++ b/src/rvcg/cli.py @@ -143,9 +143,20 @@ def build_parser() -> argparse.ArgumentParser: ) parser.add_argument( "--preset", - choices=["default", "black-fades", "podcast", "presentation", "action"], + choices=[ + "default", + "movie", + "balanced", + "all-stages", + "podcast", + "presentation", + "anime", + "action", + "fine", + "black-fades", + ], default="default", - help="Predefined detection sensitivity profile", + help="Predefined detection sensitivity profile (default/movie, balanced, podcast, presentation, anime, action, fine)", ) parser.add_argument( "--black-min-dur", diff --git a/src/rvcg/models.py b/src/rvcg/models.py index 2f6a2c7..03e1670 100644 --- a/src/rvcg/models.py +++ b/src/rvcg/models.py @@ -43,50 +43,147 @@ class BoundaryConfig: @classmethod def from_preset(cls, preset: str) -> "BoundaryConfig": - if preset in ("default", "black-fades", "fades"): - return cls( - enable_black_fades=True, - enable_white_fades=True, - enable_visual_cuts=False, - enable_subdivide=False, - ) - if preset == "all-stages": - return cls( - enable_black_fades=True, - enable_white_fades=True, - enable_visual_cuts=True, - enable_subdivide=True, - ) - if preset == "podcast": - return cls( - min_seg=120.0, - max_seg=900.0, - target_seg=450.0, - scene_threshold=0.45, - enable_visual_cuts=True, - enable_subdivide=True, - ) - if preset == "presentation": - return cls( - min_seg=60.0, - max_seg=600.0, - target_seg=300.0, - scene_threshold=0.30, - enable_visual_cuts=True, - enable_subdivide=True, - ) - if preset == "action": - return cls( - min_seg=90.0, - max_seg=420.0, - target_seg=240.0, - scene_threshold=0.35, - enable_visual_cuts=True, - enable_subdivide=True, - ) + p = preset.strip().lower() + if p in ("movie", "black-fades", "fades"): + p = "default" + elif p == "all-stages": + p = "balanced" + if p in PRESETS: + return PRESETS[p].config return cls() +@dataclass(frozen=True) +class PresetMetadata: + id: str + name: str + description: str + config: BoundaryConfig + + +PRESETS: dict[str, PresetMetadata] = { + "default": PresetMetadata( + id="default", + name="movie (Fades only - High Precision)", + description="High-precision fade detection only. Ideal for films, cinema, and narrative storytelling where scene transitions are intentional fades.", + config=BoundaryConfig( + min_seg=180.0, + max_seg=720.0, + target_seg=360.0, + black_min_dur=0.4, + black_pic_th=0.95, + black_pix_th=0.12, + white_pic_th=0.95, + white_pix_th=0.10, + enable_black_fades=True, + enable_white_fades=True, + enable_visual_cuts=False, + enable_subdivide=False, + ), + ), + "balanced": PresetMetadata( + id="balanced", + name="balanced (Fades + Visual + Subdivide)", + description="Full 3-stage pipeline combining fades, visual cuts, and subdivision. Best all-rounder for YouTube videos, documentaries, and mixed media.", + config=BoundaryConfig( + min_seg=120.0, + max_seg=600.0, + target_seg=300.0, + scene_threshold=0.38, + black_min_dur=0.4, + black_pic_th=0.95, + black_pix_th=0.12, + white_pic_th=0.95, + white_pix_th=0.10, + enable_black_fades=True, + enable_white_fades=True, + enable_visual_cuts=True, + enable_subdivide=True, + ), + ), + "podcast": PresetMetadata( + id="podcast", + name="podcast (Interviews & Long Talks)", + description="Long conversational chapters. Uses a higher visual threshold to ignore 2-person alternating camera switches while subdividing long monolithic discussions.", + config=BoundaryConfig( + min_seg=180.0, + max_seg=900.0, + target_seg=480.0, + scene_threshold=0.46, + black_min_dur=0.4, + enable_black_fades=True, + enable_white_fades=True, + enable_visual_cuts=True, + enable_subdivide=True, + ), + ), + "presentation": PresetMetadata( + id="presentation", + name="presentation (Slides & Keynotes)", + description="Slide and topic transitions. Sensitive visual detection catches slide changes that don't shift the entire screen background.", + config=BoundaryConfig( + min_seg=60.0, + max_seg=600.0, + target_seg=300.0, + scene_threshold=0.28, + black_min_dur=0.35, + enable_black_fades=True, + enable_white_fades=True, + enable_visual_cuts=True, + enable_subdivide=True, + ), + ), + "anime": PresetMetadata( + id="anime", + name="anime (TV Episodes & Series)", + description="Optimized for 20-45m anime and TV episodes with distinct OP/ED, eyecatches, mid-episode commercial fades, and post-credits previews.", + config=BoundaryConfig( + min_seg=60.0, + max_seg=480.0, + target_seg=240.0, + scene_threshold=0.36, + black_min_dur=0.30, + enable_black_fades=True, + enable_white_fades=True, + enable_visual_cuts=True, + enable_subdivide=True, + ), + ), + "action": PresetMetadata( + id="action", + name="action (Sports, Gaming & Action)", + description="High-motion gameplay, sports, and fast-cut action. Raised threshold prevents false cuts during combat while subdividing into compact chapters.", + config=BoundaryConfig( + min_seg=90.0, + max_seg=420.0, + target_seg=210.0, + scene_threshold=0.42, + black_min_dur=0.40, + enable_black_fades=True, + enable_white_fades=True, + enable_visual_cuts=True, + enable_subdivide=True, + ), + ), + "fine": PresetMetadata( + id="fine", + name="fine (Micro / Granular Chapters)", + description="Granular chapter boundaries for music videos, highlight reels, micro-tutorials, and rapid navigation.", + config=BoundaryConfig( + min_seg=30.0, + max_seg=180.0, + target_seg=90.0, + scene_threshold=0.32, + black_min_dur=0.25, + enable_black_fades=True, + enable_white_fades=True, + enable_visual_cuts=True, + enable_subdivide=True, + ), + ), +} + + @dataclass(frozen=True) class FadePoint: timestamp: float diff --git a/src/rvcg/static/js/gallery.js b/src/rvcg/static/js/gallery.js index c254371..791fe16 100644 --- a/src/rvcg/static/js/gallery.js +++ b/src/rvcg/static/js/gallery.js @@ -31,11 +31,11 @@ export function renderGallery(onChaptersChanged) { const card = document.createElement("article"); const isActive = index === state.currentIndex; - card.className = `chapter-card card bg-base-200 border border-base-content/10 shadow-sm transition-all overflow-hidden ${isActive ? "ring-2 ring-primary" : ""}`; + card.className = `chapter-card card bg-base-200 border border-base-content/10 shadow-sm transition-all overflow-hidden hover:border-primary/40 hover:shadow-md ${isActive ? "ring-2 ring-primary" : ""}`; card.dataset.index = index; const header = document.createElement("div"); - header.className = "card-header flex items-center justify-between px-3 py-2 border-b border-base-content/10 bg-base-300/50 cursor-pointer select-none"; + header.className = "card-header flex items-center justify-between px-3 py-2 border-b border-base-content/10 bg-base-300/40 cursor-pointer select-none"; header.addEventListener("click", () => openAt(index, item.start_time, highlightActiveCard)); const hLeft = document.createElement("div"); @@ -46,11 +46,11 @@ export function renderGallery(onChaptersChanged) { cutDot.dataset.tippyContent = item.cut_detail || ""; const badge = document.createElement("span"); - badge.className = "badge badge-neutral badge-sm font-mono shrink-0"; + badge.className = "badge badge-neutral badge-sm font-mono font-bold tracking-tight shrink-0"; badge.textContent = "#" + item.id; const titleInput = document.createElement("input"); - titleInput.className = "input input-ghost input-xs font-semibold flex-1 min-w-0 focus:input-bordered"; + titleInput.className = "input input-ghost input-xs font-semibold flex-1 min-w-0 hover:bg-base-100/50 focus:bg-base-100 focus:input-bordered text-xs rounded transition-colors"; titleInput.value = item.title; titleInput.addEventListener("click", (e) => e.stopPropagation()); titleInput.addEventListener("change", async () => { @@ -78,13 +78,13 @@ export function renderGallery(onChaptersChanged) { hRight.className = "card-header-right flex items-center gap-2 shrink-0"; const metaSpan = document.createElement("span"); - metaSpan.className = "text-xs text-base-content/60 font-mono"; - metaSpan.textContent = `${item.source_range} ยท ${item.duration_str}`; + metaSpan.className = "text-[11px] text-base-content/60 font-mono tracking-tight shrink-0"; + metaSpan.innerHTML = `${item.duration_str}`; hRight.appendChild(metaSpan); if (state.cuts.length > 1) { const delBtn = document.createElement("button"); - delBtn.className = "btn btn-ghost btn-xs btn-circle text-base-content/50 hover:text-error"; + delBtn.className = "btn btn-ghost btn-xs btn-circle text-base-content/40 hover:text-error hover:bg-error/10 transition-colors"; delBtn.type = "button"; delBtn.textContent = "โœ•"; delBtn.addEventListener("click", async (e) => { @@ -119,16 +119,16 @@ export function renderGallery(onChaptersChanged) { header.appendChild(hRight); const grid = document.createElement("div"); - grid.className = "contact-grid grid grid-cols-3 grid-rows-3 aspect-video gap-0.5 bg-black/20 p-1"; + grid.className = "contact-grid grid grid-cols-3 grid-rows-3 aspect-video gap-0.5 bg-black/40 p-1 rounded-b-box overflow-hidden"; const slots = item.cell_times.length >= 9 ? item.cell_times.slice(0, 9) : item.cell_times; slots.forEach((baseTime) => { const cell = document.createElement("div"); - cell.className = "grid-cell relative overflow-hidden bg-base-300 cursor-pointer rounded-xs bg-no-repeat"; + cell.className = "grid-cell relative overflow-hidden bg-base-300 cursor-pointer rounded-xs bg-no-repeat transition-transform hover:scale-[1.02] hover:z-10"; setCellSpriteFrame(cell, baseTime); const timeBadge = document.createElement("span"); - timeBadge.className = "cell-time absolute bottom-1 right-1 px-1 py-0.5 rounded bg-black/75 text-[10px] text-white font-mono pointer-events-none"; + timeBadge.className = "cell-time absolute bottom-1 right-1 px-1.5 py-0.5 rounded bg-black/80 backdrop-blur-md text-[10px] text-white/90 font-mono shadow-sm border border-white/10 pointer-events-none"; timeBadge.textContent = fmtHms(baseTime); cell.appendChild(timeBadge); @@ -196,7 +196,8 @@ export function renderGallery(onChaptersChanged) { }); } - const mech = state.lastStats ? ` (${formatMechanismSummary(state.lastStats)})` : ""; + const mechSummary = formatMechanismSummary(state.lastStats); + const mech = mechSummary ? ` (${mechSummary})` : ""; if (dom.countLabel) { dom.countLabel.textContent = `${visibleCount} / ${state.cuts.length} chapters${mech}`; } diff --git a/src/rvcg/static/js/picker.js b/src/rvcg/static/js/picker.js index 53a8312..8a60e5c 100644 --- a/src/rvcg/static/js/picker.js +++ b/src/rvcg/static/js/picker.js @@ -77,13 +77,13 @@ export async function loadDirectory(dirPath) { data.dirs.forEach((d) => { const btn = document.createElement("button"); - btn.className = "file-item btn btn-outline btn-sm justify-between w-full h-auto py-2 font-normal"; + btn.className = "file-item btn btn-outline btn-sm justify-between w-full h-auto py-2 font-normal hover:border-primary/50 transition-colors"; btn.dataset.tippyContent = d.name; const nameSpan = document.createElement("span"); - nameSpan.className = "truncate mr-2"; - nameSpan.textContent = `๐Ÿ“ ${d.name}`; + nameSpan.className = "flex items-center gap-1.5 truncate mr-2 text-xs"; + nameSpan.innerHTML = `${d.name}`; const metaSpan = document.createElement("span"); - metaSpan.className = "badge badge-ghost badge-xs shrink-0"; + metaSpan.className = "badge badge-ghost badge-xs shrink-0 text-[10px]"; metaSpan.textContent = "Folder"; btn.appendChild(nameSpan); btn.appendChild(metaSpan); @@ -93,13 +93,13 @@ export async function loadDirectory(dirPath) { data.videos.forEach((v) => { const btn = document.createElement("button"); - btn.className = "file-item btn btn-outline btn-sm justify-between w-full h-auto py-2 font-normal"; + btn.className = "file-item btn btn-outline btn-sm justify-between w-full h-auto py-2 font-normal hover:border-primary/50 transition-colors"; btn.dataset.tippyContent = v.name; const nameSpan = document.createElement("span"); - nameSpan.className = "truncate mr-2 font-medium"; - nameSpan.textContent = `๐ŸŽฌ ${v.name}`; + nameSpan.className = "flex items-center gap-1.5 truncate mr-2 text-xs font-medium"; + nameSpan.innerHTML = `${v.name}`; const metaSpan = document.createElement("span"); - metaSpan.className = "badge badge-neutral badge-xs shrink-0 font-mono"; + metaSpan.className = "badge badge-neutral badge-xs shrink-0 font-mono text-[10px]"; metaSpan.textContent = `${v.size_mb} MB`; btn.appendChild(nameSpan); btn.appendChild(metaSpan); @@ -209,8 +209,8 @@ export async function startScan(videoPath, forceRefresh, onScanComplete) { dom.statBlack.textContent = payload.enable_black_fades ? `${usedFades} cuts${breakdown} (${rawFades} raw)` : `off (${rawFades} raw)`; } if (dom.statVisual) dom.statVisual.textContent = payload.enable_visual_cuts ? `${state.lastStats.used_visual} cuts (${state.lastStats.raw_visual} raw)` : `off (${state.lastStats.raw_visual} raw)`; - if (dom.statSub) dom.statSub.textContent = payload.enable_subdivide ? `+${state.lastStats.sub_cuts} cuts (${state.lastStats.snapped_cuts} snapped)` : "off"; - if (dom.progressSummary) dom.progressSummary.textContent = `${state.cuts.length} chapters (${formatMechanismSummary(state.lastStats)})`; + const mechStr = formatMechanismSummary(state.lastStats); + if (dom.progressSummary) dom.progressSummary.textContent = `${state.cuts.length} chapters${mechStr ? ` (${mechStr})` : ""}`; } else { if (dom.progressSummary) dom.progressSummary.textContent = `${state.cuts.length} chapters`; } @@ -233,6 +233,59 @@ export async function startScan(videoPath, forceRefresh, onScanComplete) { }; } +export function applyPreset(presetId) { + const presets = window.__RVCG_BOOT__?.presets || {}; + let meta = presets[presetId]; + if (!meta && (presetId === "movie" || presetId === "black-fades" || presetId === "fades")) { + meta = presets["default"]; + } else if (!meta && presetId === "all-stages") { + meta = presets["balanced"]; + } + if (!meta) return; + + const cfg = meta.config; + if (dom.cfgMin && cfg.min_seg !== undefined) dom.cfgMin.value = cfg.min_seg; + if (dom.cfgMax && cfg.max_seg !== undefined) dom.cfgMax.value = cfg.max_seg; + if (dom.cfgTarget && cfg.target_seg !== undefined) dom.cfgTarget.value = cfg.target_seg; + if (dom.cfgTh && cfg.scene_threshold !== undefined) dom.cfgTh.value = cfg.scene_threshold; + if (dom.cfgBlackDur && cfg.black_min_dur !== undefined) dom.cfgBlackDur.value = cfg.black_min_dur; + if (dom.cfgBlackPicTh && cfg.black_pic_th !== undefined) dom.cfgBlackPicTh.value = Math.round(cfg.black_pic_th * 100); + if (dom.cfgBlackPixTh && cfg.black_pix_th !== undefined) dom.cfgBlackPixTh.value = Math.round(cfg.black_pix_th * 100); + if (dom.cfgWhitePicTh && cfg.white_pic_th !== undefined) dom.cfgWhitePicTh.value = Math.round(cfg.white_pic_th * 100); + if (dom.cfgWhitePixTh && cfg.white_pix_th !== undefined) dom.cfgWhitePixTh.value = Math.round((1.0 - cfg.white_pix_th) * 100); + if (dom.cfgEnableBlack && cfg.enable_black_fades !== undefined) dom.cfgEnableBlack.checked = cfg.enable_black_fades; + if (dom.cfgEnableWhite && cfg.enable_white_fades !== undefined) dom.cfgEnableWhite.checked = cfg.enable_white_fades; + if (dom.cfgEnableVisual && cfg.enable_visual_cuts !== undefined) dom.cfgEnableVisual.checked = cfg.enable_visual_cuts; + if (dom.cfgEnableSub && cfg.enable_subdivide !== undefined) dom.cfgEnableSub.checked = cfg.enable_subdivide; + + const presetBadge = document.getElementById("preset-badge"); + if (presetBadge) presetBadge.textContent = meta.id; + + const presetDesc = document.getElementById("preset-desc"); + if (presetDesc) presetDesc.textContent = meta.description; + + const presetSelect = document.getElementById("preset-select"); + if (presetSelect && presetSelect.value !== meta.id && presetSelect.querySelector(`option[value="${meta.id}"]`)) { + presetSelect.value = meta.id; + } + + syncStageUI(); + saveSettingsToLocalStorage(); +} + +export function markPresetCustom() { + const presetBadge = document.getElementById("preset-badge"); + if (presetBadge) presetBadge.textContent = "custom"; + const presetDesc = document.getElementById("preset-desc"); + if (presetDesc) presetDesc.textContent = "Manual configuration overrides active."; + const presetSelect = document.getElementById("preset-select"); + if (presetSelect) { + const customOpt = presetSelect.querySelector('option[value="custom"]'); + if (customOpt) customOpt.disabled = false; + presetSelect.value = "custom"; + } +} + export function initPicker(onScanComplete) { const defaultCfg = window.__RVCG_BOOT__?.defaultCfg || {}; if (dom.cfgMin) dom.cfgMin.value = defaultCfg.min_seg ?? 180; @@ -253,65 +306,36 @@ export function initPicker(onScanComplete) { if (dom.cfgCardDur) dom.cfgCardDur.value = defaultCfg.card_dur ?? 8.4; [ - dom.cfgEnableBlack, - dom.cfgEnableWhite, - dom.cfgEnableVisual, - dom.cfgEnableSub, + dom.cfgMin, + dom.cfgMax, + dom.cfgTarget, + dom.cfgTh, + dom.cfgBlackDur, dom.cfgBlackPicTh, dom.cfgBlackPixTh, dom.cfgWhitePicTh, dom.cfgWhitePixTh, + dom.cfgEnableBlack, + dom.cfgEnableWhite, + dom.cfgEnableVisual, + dom.cfgEnableSub, + dom.cfgWorkers, + dom.cfgTitleTemplate, + dom.cfgCardDur, ].forEach((el) => { - el?.addEventListener("change", syncStageUI); + el?.addEventListener("change", () => { + syncStageUI(); + markPresetCustom(); + saveSettingsToLocalStorage(); + }); }); syncStageUI(); document.getElementById("preset-select")?.addEventListener("change", (e) => { const p = e.target.value; - if (dom.cfgEnableBlack) dom.cfgEnableBlack.checked = true; - if (dom.cfgBlackDur) dom.cfgBlackDur.value = 0.4; - if (p === "default" || p === "black-fades") { - if (dom.cfgMin) dom.cfgMin.value = 180; - if (dom.cfgEnableVisual) dom.cfgEnableVisual.checked = false; - if (dom.cfgEnableSub) dom.cfgEnableSub.checked = false; - } else if (p === "all-stages") { - if (dom.cfgMin) dom.cfgMin.value = 180; - if (dom.cfgMax) dom.cfgMax.value = 600; - if (dom.cfgTarget) dom.cfgTarget.value = 360; - if (dom.cfgTh) dom.cfgTh.value = 0.38; - if (dom.cfgEnableVisual) dom.cfgEnableVisual.checked = true; - if (dom.cfgEnableSub) dom.cfgEnableSub.checked = true; - } else if (p === "podcast") { - if (dom.cfgMin) dom.cfgMin.value = 120; - if (dom.cfgMax) dom.cfgMax.value = 900; - if (dom.cfgTarget) dom.cfgTarget.value = 450; - if (dom.cfgTh) dom.cfgTh.value = 0.45; - if (dom.cfgEnableVisual) dom.cfgEnableVisual.checked = true; - if (dom.cfgEnableSub) dom.cfgEnableSub.checked = true; - } else if (p === "presentation") { - if (dom.cfgMin) dom.cfgMin.value = 60; - if (dom.cfgMax) dom.cfgMax.value = 600; - if (dom.cfgTarget) dom.cfgTarget.value = 300; - if (dom.cfgTh) dom.cfgTh.value = 0.30; - if (dom.cfgEnableVisual) dom.cfgEnableVisual.checked = true; - if (dom.cfgEnableSub) dom.cfgEnableSub.checked = true; - } else if (p === "action") { - if (dom.cfgMin) dom.cfgMin.value = 90; - if (dom.cfgMax) dom.cfgMax.value = 420; - if (dom.cfgTarget) dom.cfgTarget.value = 240; - if (dom.cfgTh) dom.cfgTh.value = 0.35; - if (dom.cfgEnableVisual) dom.cfgEnableVisual.checked = true; - if (dom.cfgEnableSub) dom.cfgEnableSub.checked = true; - } else { - if (dom.cfgMin) dom.cfgMin.value = 180; - if (dom.cfgMax) dom.cfgMax.value = 600; - if (dom.cfgTarget) dom.cfgTarget.value = 360; - if (dom.cfgTh) dom.cfgTh.value = 0.38; - if (dom.cfgEnableVisual) dom.cfgEnableVisual.checked = false; - if (dom.cfgEnableSub) dom.cfgEnableSub.checked = false; + if (p !== "custom") { + applyPreset(p); } - syncStageUI(); - saveSettingsToLocalStorage(); }); dom.resumeBrowserBtn?.addEventListener("click", () => showView("browser")); diff --git a/src/rvcg/static/js/settings.js b/src/rvcg/static/js/settings.js index 9e4b1af..a9cd747 100644 --- a/src/rvcg/static/js/settings.js +++ b/src/rvcg/static/js/settings.js @@ -211,7 +211,19 @@ export function initSettingsAndExport() { }); document.getElementById("copy-export-btn")?.addEventListener("click", () => { - if (dom.exportTextarea) navigator.clipboard.writeText(dom.exportTextarea.value); + if (dom.exportTextarea) { + navigator.clipboard.writeText(dom.exportTextarea.value); + const btn = document.getElementById("copy-export-btn"); + if (btn) { + const orig = btn.innerHTML; + btn.innerHTML = `Copied!`; + btn.classList.add("btn-success"); + setTimeout(() => { + btn.innerHTML = orig; + btn.classList.remove("btn-success"); + }, 1800); + } + } }); document.getElementById("close-export")?.addEventListener("click", () => { diff --git a/src/rvcg/static/webui.html b/src/rvcg/static/webui.html index 105c2ed..1262dc9 100644 --- a/src/rvcg/static/webui.html +++ b/src/rvcg/static/webui.html @@ -45,11 +45,11 @@ align-items: stretch; } .gallery.list-view .card-header { - width: 240px; + width: 260px; flex-shrink: 0; flex-direction: column; align-items: flex-start; - justify-content: flex-start; + justify-content: space-between; border-right: 1px solid var(--color-base-content, rgba(255, 255, 255, 0.1)); border-bottom: none; gap: 8px; @@ -68,6 +68,7 @@ aspect-ratio: 144 / 9; grid-template-columns: repeat(9, 1fr); grid-template-rows: 1fr; + border-radius: 0; } @media (max-width: 768px) { .gallery.list-view .chapter-card { @@ -87,17 +88,25 @@ grid-template-rows: repeat(3, 1fr); } } + .hide-cut-dots .cut-dot { + display: none !important; + } - +
-
-
+
+
- RapidVideoChapterGenerator +
+
+ +
+ RapidVideoChapterGenerator +
- @@ -112,56 +121,76 @@ - - + +
- - - - + +
+ +
+ +
-
+
- Detector Configuration + + Detector Configuration + Fades-only is default for highest precision
- Preset - + + + + + + + + +
High-precision fade detection only. Ideal for films, cinema, and narrative storytelling where scene transitions are intentional fades.
- Min Scene (s) - + Min Scene (s) +
-
+
- +
Min Fade (s) - +
@@ -172,11 +201,11 @@
Min Coverage % - +
Max Pixel Luma % - +
@@ -191,11 +220,11 @@
Min Coverage % - +
Min Pixel Luma % - +
@@ -203,43 +232,43 @@
-
+
- +
Visual Cut Threshold - +
Visual Cut Info
- Calculates frame-to-frame pixel differences. Scores $\ge 0.35$ detect hard cuts and camera angle switches. + Calculates frame-to-frame pixel differences. Scores ≥ 0.35 detect hard cuts and camera angle switches.
-
+
- +
Max (s) - +
Target (s) - +
@@ -253,19 +282,21 @@
- โš™ Advanced Pipeline & Output Parameters + + Advanced Pipeline & Output Parameters +
Parallel FFmpeg Workers - +
Chapter Title Template - +
Preview Card Duration (s) - +
@@ -273,50 +304,97 @@
-
-
-