import { useState } from 'react'; import { useEditorStore } from '../store/editorStore'; import { Loader2, Scissors, Trash2, RotateCcw, PencilLine, Layers } from 'lucide-react'; import type { SilenceDetectionRange, SilenceTrimSettings } from '../types/project'; export default function SilenceTrimmerPanel() { const { videoPath, backendUrl, silenceTrimGroups, cutRanges, applySilenceTrimGroup, removeSilenceTrimGroup, } = useEditorStore(); const [minSilenceMs, setMinSilenceMs] = useState(500); const [silenceDb, setSilenceDb] = useState(-35); const [preBufferMs, setPreBufferMs] = useState(80); const [postBufferMs, setPostBufferMs] = useState(120); const [isDetecting, setIsDetecting] = useState(false); const [ranges, setRanges] = useState([]); const [selectedGroupId, setSelectedGroupId] = useState(null); const [status, setStatus] = useState(null); const selectedGroup = selectedGroupId ? silenceTrimGroups.find((group) => group.id === selectedGroupId) ?? null : null; const buildSettings = (): SilenceTrimSettings => ({ minSilenceMs, silenceDb, preBufferMs, postBufferMs, }); const detectSilence = async () => { if (!videoPath) return; setIsDetecting(true); setRanges([]); try { const res = await fetch(`${backendUrl}/audio/detect-silence`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ input_path: videoPath, min_silence_ms: minSilenceMs, silence_db: silenceDb, }), }); if (!res.ok) { let detail = `HTTP ${res.status} ${res.statusText}`; try { const err = await res.json(); if (err?.detail) detail += ` - ${String(err.detail)}`; } catch { // ignore JSON parse errors for non-JSON error responses } if (res.status === 404) { detail += ' (endpoint missing: restart backend to load /audio/detect-silence)'; } throw new Error(detail); } const data = await res.json(); setRanges(data.ranges || []); setStatus(`Detected ${(data.ranges || []).length} pause ranges.`); } catch (err) { console.error(err); const message = err instanceof Error ? err.message : 'Unknown error'; alert(`Silence detection failed: ${message}`); } finally { setIsDetecting(false); } }; const applyAsNewGroup = () => { if (ranges.length === 0) return; const result = applySilenceTrimGroup({ sourceRanges: ranges, settings: buildSettings(), }); setSelectedGroupId(result.groupId); setStatus(`Applied ${result.appliedCount} cut ranges as ${result.groupId}. Undo will revert this pass in one step.`); }; const loadGroupForEditing = (groupId: string) => { const group = silenceTrimGroups.find((entry) => entry.id === groupId); if (!group) return; setSelectedGroupId(groupId); setRanges(group.sourceRanges); setMinSilenceMs(group.settings.minSilenceMs); setSilenceDb(group.settings.silenceDb); setPreBufferMs(group.settings.preBufferMs); setPostBufferMs(group.settings.postBufferMs); setStatus(`Loaded ${group.id} for editing. Adjust settings and reapply.`); }; const reapplySelectedGroup = () => { if (!selectedGroupId || ranges.length === 0) return; const result = applySilenceTrimGroup({ groupId: selectedGroupId, sourceRanges: ranges, settings: buildSettings(), }); setStatus(`Reapplied ${result.groupId} with ${result.appliedCount} cut ranges.`); }; const removeGroup = (groupId: string) => { removeSilenceTrimGroup(groupId); if (selectedGroupId === groupId) { setSelectedGroupId(null); } setStatus(`Removed all cut ranges from ${groupId}.`); }; return (

Silence / Pause Trimmer

Detect pauses and convert them into cut ranges.

setMinSilenceMs(Number(e.target.value) || 500)} className="w-full px-2.5 py-1.5 text-xs bg-editor-surface border border-editor-border rounded focus:border-editor-accent focus:outline-none" data-tooltip="Minimum duration of silence to detect in milliseconds" />
setSilenceDb(Number(e.target.value) || -35)} className="w-full px-2.5 py-1.5 text-xs bg-editor-surface border border-editor-border rounded focus:border-editor-accent focus:outline-none" data-tooltip="Volume threshold in dB — lower values detect quieter sounds as silence" />
setPreBufferMs(Number(e.target.value) || 0)} className="w-full px-2.5 py-1.5 text-xs bg-editor-surface border border-editor-border rounded focus:border-editor-accent focus:outline-none" data-tooltip="Extra time to add before each detected silence" />
setPostBufferMs(Number(e.target.value) || 0)} className="w-full px-2.5 py-1.5 text-xs bg-editor-surface border border-editor-border rounded focus:border-editor-accent focus:outline-none" data-tooltip="Extra time to add after each detected silence" />
{status && (
{status}
)} {ranges.length > 0 && (
Detected {ranges.length} pause ranges
{selectedGroup && ( )}
{ranges.slice(0, 50).map((r, i) => (
{r.start.toFixed(2)}s - {r.end.toFixed(2)}s ({r.duration.toFixed(2)}s)
))}
)} {silenceTrimGroups.length > 0 && (
Silence Trim Groups
{silenceTrimGroups.map((group) => { const groupCutCount = cutRanges.filter((range) => range.trimGroupId === group.id).length; const isActive = selectedGroupId === group.id; return (
{group.id}
{groupCutCount} cuts · {group.sourceRanges.length} source pauses
); })}
)}
); }