able to modify trim zones

This commit is contained in:
2026-04-11 19:13:04 -06:00
parent 140b7a5319
commit b8ec396ebd
6 changed files with 249 additions and 61546 deletions

View File

@ -1,21 +1,36 @@
import { useState } from 'react';
import { useEditorStore } from '../store/editorStore';
import { Loader2, Scissors } from 'lucide-react';
type SilenceRange = {
start: number;
end: number;
duration: number;
};
import { Loader2, Scissors, Trash2, RotateCcw, PencilLine, Layers } from 'lucide-react';
import type { SilenceDetectionRange, SilenceTrimSettings } from '../types/project';
export default function SilenceTrimmerPanel() {
const { videoPath, backendUrl, addCutRange, duration } = useEditorStore();
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<SilenceRange[]>([]);
const [ranges, setRanges] = useState<SilenceDetectionRange[]>([]);
const [selectedGroupId, setSelectedGroupId] = useState<string | null>(null);
const [status, setStatus] = useState<string | null>(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;
@ -48,6 +63,7 @@ export default function SilenceTrimmerPanel() {
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';
@ -57,19 +73,44 @@ export default function SilenceTrimmerPanel() {
}
};
const applyAsCuts = () => {
const preBufferSeconds = preBufferMs / 1000;
const postBufferSeconds = postBufferMs / 1000;
const maxEnd = duration > 0 ? duration : Number.POSITIVE_INFINITY;
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.`);
};
for (const r of ranges) {
// Positive buffers shrink the cut, negative buffers expand it.
const start = Math.max(0, r.start + preBufferSeconds);
const end = Math.min(maxEnd, r.end - postBufferSeconds);
if (end - start >= 0.01) {
addCutRange(start, end);
}
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 (
@ -158,17 +199,34 @@ export default function SilenceTrimmerPanel() {
</button>
</div>
{status && (
<div className="text-[11px] text-editor-text-muted bg-editor-surface border border-editor-border rounded px-2.5 py-2">
{status}
</div>
)}
{ranges.length > 0 && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs font-medium">Detected {ranges.length} pause ranges</span>
<button
onClick={applyAsCuts}
className="flex items-center gap-1 px-2 py-1 text-xs bg-editor-accent/20 text-editor-accent rounded hover:bg-editor-accent/30"
>
<Scissors className="w-3 h-3" />
Apply As Cuts
</button>
<div className="flex items-center gap-1">
{selectedGroup && (
<button
onClick={reapplySelectedGroup}
className="flex items-center gap-1 px-2 py-1 text-xs bg-editor-warning/20 text-editor-warning rounded hover:bg-editor-warning/30"
>
<RotateCcw className="w-3 h-3" />
Reapply Group
</button>
)}
<button
onClick={applyAsNewGroup}
className="flex items-center gap-1 px-2 py-1 text-xs bg-editor-accent/20 text-editor-accent rounded hover:bg-editor-accent/30"
>
<Scissors className="w-3 h-3" />
Apply As New Group
</button>
</div>
</div>
<div className="max-h-56 overflow-y-auto space-y-1 pr-1">
{ranges.slice(0, 50).map((r, i) => (
@ -179,6 +237,52 @@ export default function SilenceTrimmerPanel() {
</div>
</div>
)}
{silenceTrimGroups.length > 0 && (
<div className="space-y-2 pt-1">
<div className="text-xs font-medium flex items-center gap-1">
<Layers className="w-3 h-3" />
Silence Trim Groups
</div>
<div className="max-h-48 overflow-y-auto space-y-1 pr-1">
{silenceTrimGroups.map((group) => {
const groupCutCount = cutRanges.filter((range) => range.trimGroupId === group.id).length;
const isActive = selectedGroupId === group.id;
return (
<div
key={group.id}
className={`rounded border px-2 py-1.5 text-xs ${isActive ? 'border-editor-accent bg-editor-accent/10' : 'border-editor-border bg-editor-surface'}`}
>
<div className="flex items-center justify-between gap-2">
<div className="min-w-0">
<div className="font-medium truncate">{group.id}</div>
<div className="text-[10px] text-editor-text-muted">
{groupCutCount} cuts · {group.sourceRanges.length} source pauses
</div>
</div>
<div className="flex items-center gap-1 shrink-0">
<button
onClick={() => loadGroupForEditing(group.id)}
className="px-1.5 py-1 rounded hover:bg-editor-accent/20 text-editor-accent"
title="Edit and reapply this group"
>
<PencilLine className="w-3 h-3" />
</button>
<button
onClick={() => removeGroup(group.id)}
className="px-1.5 py-1 rounded hover:bg-editor-danger/20 text-editor-danger"
title="Delete all cuts from this group"
>
<Trash2 className="w-3 h-3" />
</button>
</div>
</div>
</div>
);
})}
</div>
</div>
)}
</div>
);
}