able to modify trim zones
This commit is contained in:
@ -8,13 +8,13 @@ Features are grouped by priority. Check off items as they are implemented.
|
|||||||
|
|
||||||
- [x] **Cut / Mute sections** — select a time range and choose to cut (remove entirely) or mute (silence audio while video continues). Cut sections show as red overlays, mute sections as transparent blue overlays on the timeline over the transcript text and audio waveform. Backend: `ffmpeg -af volume=0` for mute, time-based cutting for removal.
|
- [x] **Cut / Mute sections** — select a time range and choose to cut (remove entirely) or mute (silence audio while video continues). Cut sections show as red overlays, mute sections as transparent blue overlays on the timeline over the transcript text and audio waveform. Backend: `ffmpeg -af volume=0` for mute, time-based cutting for removal.
|
||||||
|
|
||||||
- [ ] **Silence / pause trimmer (in progress)** — detect pauses using min duration (ms) + amplitude threshold (dB), then apply detected pauses as cut ranges. Initial endpoint: `/audio/detect-silence`; UI includes filter controls and an "Apply As Cuts" action.
|
- [x] **Silence / pause trimmer (in progress)** — detect pauses using min duration (ms) + amplitude threshold (dB), then apply detected pauses as cut ranges. Initial endpoint: `/audio/detect-silence`; UI includes filter controls and an "Apply As Cuts" action.
|
||||||
|
|
||||||
- [ ] **Operation-level undo for batch actions** — explicit undo entry for actions like "Apply Silence Trim" so one shortcut/click reverts the whole operation, while still allowing normal fine-grained undo/redo steps.
|
- [x] **Operation-level undo for batch actions** — explicit undo entry for actions like "Apply Silence Trim" so one shortcut/click reverts the whole operation, while still allowing normal fine-grained undo/redo steps.
|
||||||
|
|
||||||
- [ ] **Grouped silence-trim zones (editable batch)** — when pauses are applied, tag them as a batch (`trim_group_id`) so the user can: (1) delete all zones from that auto-trim pass at once, and (2) still select/resize/delete individual zones independently.
|
- [x] **Grouped silence-trim zones (editable batch)** — when pauses are applied, tag them as a batch (`trim_group_id`) so the user can: (1) delete all zones from that auto-trim pass at once, and (2) still select/resize/delete individual zones independently.
|
||||||
|
|
||||||
- [ ] **Edit silence-trim group settings after apply** — allow reopening a trim group and changing its detection settings (min pause ms, threshold dB, pre/post buffers), then reapplying updates to that group without affecting unrelated edits.
|
- [x] **Edit silence-trim group settings after apply** — allow reopening a trim group and changing its detection settings (min pause ms, threshold dB, pre/post buffers), then reapplying updates to that group without affecting unrelated edits.
|
||||||
|
|
||||||
- [ ] **Volume / gain control** — per-selection or global audio gain slider. Every editor has this. Descript users constantly complain it's missing. Backend: `ffmpeg -af volume=Xdb`.
|
- [ ] **Volume / gain control** — per-selection or global audio gain slider. Every editor has this. Descript users constantly complain it's missing. Backend: `ffmpeg -af volume=Xdb`.
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
16982
chap4_proj/smoking.aive
16982
chap4_proj/smoking.aive
File diff suppressed because it is too large
Load Diff
@ -1,21 +1,36 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useEditorStore } from '../store/editorStore';
|
import { useEditorStore } from '../store/editorStore';
|
||||||
import { Loader2, Scissors } from 'lucide-react';
|
import { Loader2, Scissors, Trash2, RotateCcw, PencilLine, Layers } from 'lucide-react';
|
||||||
|
import type { SilenceDetectionRange, SilenceTrimSettings } from '../types/project';
|
||||||
type SilenceRange = {
|
|
||||||
start: number;
|
|
||||||
end: number;
|
|
||||||
duration: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function SilenceTrimmerPanel() {
|
export default function SilenceTrimmerPanel() {
|
||||||
const { videoPath, backendUrl, addCutRange, duration } = useEditorStore();
|
const {
|
||||||
|
videoPath,
|
||||||
|
backendUrl,
|
||||||
|
silenceTrimGroups,
|
||||||
|
cutRanges,
|
||||||
|
applySilenceTrimGroup,
|
||||||
|
removeSilenceTrimGroup,
|
||||||
|
} = useEditorStore();
|
||||||
const [minSilenceMs, setMinSilenceMs] = useState(500);
|
const [minSilenceMs, setMinSilenceMs] = useState(500);
|
||||||
const [silenceDb, setSilenceDb] = useState(-35);
|
const [silenceDb, setSilenceDb] = useState(-35);
|
||||||
const [preBufferMs, setPreBufferMs] = useState(80);
|
const [preBufferMs, setPreBufferMs] = useState(80);
|
||||||
const [postBufferMs, setPostBufferMs] = useState(120);
|
const [postBufferMs, setPostBufferMs] = useState(120);
|
||||||
const [isDetecting, setIsDetecting] = useState(false);
|
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 () => {
|
const detectSilence = async () => {
|
||||||
if (!videoPath) return;
|
if (!videoPath) return;
|
||||||
@ -48,6 +63,7 @@ export default function SilenceTrimmerPanel() {
|
|||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setRanges(data.ranges || []);
|
setRanges(data.ranges || []);
|
||||||
|
setStatus(`Detected ${(data.ranges || []).length} pause ranges.`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||||
@ -57,19 +73,44 @@ export default function SilenceTrimmerPanel() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyAsCuts = () => {
|
const applyAsNewGroup = () => {
|
||||||
const preBufferSeconds = preBufferMs / 1000;
|
if (ranges.length === 0) return;
|
||||||
const postBufferSeconds = postBufferMs / 1000;
|
const result = applySilenceTrimGroup({
|
||||||
const maxEnd = duration > 0 ? duration : Number.POSITIVE_INFINITY;
|
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) {
|
const loadGroupForEditing = (groupId: string) => {
|
||||||
// Positive buffers shrink the cut, negative buffers expand it.
|
const group = silenceTrimGroups.find((entry) => entry.id === groupId);
|
||||||
const start = Math.max(0, r.start + preBufferSeconds);
|
if (!group) return;
|
||||||
const end = Math.min(maxEnd, r.end - postBufferSeconds);
|
setSelectedGroupId(groupId);
|
||||||
if (end - start >= 0.01) {
|
setRanges(group.sourceRanges);
|
||||||
addCutRange(start, end);
|
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 (
|
return (
|
||||||
@ -158,17 +199,34 @@ export default function SilenceTrimmerPanel() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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 && (
|
{ranges.length > 0 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs font-medium">Detected {ranges.length} pause ranges</span>
|
<span className="text-xs font-medium">Detected {ranges.length} pause ranges</span>
|
||||||
<button
|
<div className="flex items-center gap-1">
|
||||||
onClick={applyAsCuts}
|
{selectedGroup && (
|
||||||
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"
|
<button
|
||||||
>
|
onClick={reapplySelectedGroup}
|
||||||
<Scissors className="w-3 h-3" />
|
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"
|
||||||
Apply As Cuts
|
>
|
||||||
</button>
|
<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>
|
||||||
<div className="max-h-56 overflow-y-auto space-y-1 pr-1">
|
<div className="max-h-56 overflow-y-auto space-y-1 pr-1">
|
||||||
{ranges.slice(0, 50).map((r, i) => (
|
{ranges.slice(0, 50).map((r, i) => (
|
||||||
@ -179,6 +237,52 @@ export default function SilenceTrimmerPanel() {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,6 +1,17 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { temporal } from 'zundo';
|
import { temporal } from 'zundo';
|
||||||
import type { Word, Segment, DeletedRange, CutRange, MuteRange, TranscriptionResult, ProjectFile } from '../types/project';
|
import type {
|
||||||
|
Word,
|
||||||
|
Segment,
|
||||||
|
DeletedRange,
|
||||||
|
CutRange,
|
||||||
|
MuteRange,
|
||||||
|
TranscriptionResult,
|
||||||
|
ProjectFile,
|
||||||
|
SilenceDetectionRange,
|
||||||
|
SilenceTrimSettings,
|
||||||
|
SilenceTrimGroup,
|
||||||
|
} from '../types/project';
|
||||||
|
|
||||||
interface EditorState {
|
interface EditorState {
|
||||||
videoPath: string | null;
|
videoPath: string | null;
|
||||||
@ -11,6 +22,7 @@ interface EditorState {
|
|||||||
deletedRanges: DeletedRange[];
|
deletedRanges: DeletedRange[];
|
||||||
cutRanges: CutRange[];
|
cutRanges: CutRange[];
|
||||||
muteRanges: MuteRange[];
|
muteRanges: MuteRange[];
|
||||||
|
silenceTrimGroups: SilenceTrimGroup[];
|
||||||
language: string;
|
language: string;
|
||||||
|
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
@ -43,12 +55,18 @@ interface EditorActions {
|
|||||||
deleteSelectedWords: () => void;
|
deleteSelectedWords: () => void;
|
||||||
deleteWordRange: (startIndex: number, endIndex: number) => void;
|
deleteWordRange: (startIndex: number, endIndex: number) => void;
|
||||||
restoreRange: (rangeId: string) => void;
|
restoreRange: (rangeId: string) => void;
|
||||||
addCutRange: (start: number, end: number) => void;
|
addCutRange: (start: number, end: number, trimGroupId?: string) => void;
|
||||||
addMuteRange: (start: number, end: number) => void;
|
addMuteRange: (start: number, end: number) => void;
|
||||||
updateCutRange: (id: string, start: number, end: number) => void;
|
updateCutRange: (id: string, start: number, end: number) => void;
|
||||||
updateMuteRange: (id: string, start: number, end: number) => void;
|
updateMuteRange: (id: string, start: number, end: number) => void;
|
||||||
removeCutRange: (id: string) => void;
|
removeCutRange: (id: string) => void;
|
||||||
removeMuteRange: (id: string) => void;
|
removeMuteRange: (id: string) => void;
|
||||||
|
applySilenceTrimGroup: (args: {
|
||||||
|
groupId?: string;
|
||||||
|
sourceRanges: SilenceDetectionRange[];
|
||||||
|
settings: SilenceTrimSettings;
|
||||||
|
}) => { groupId: string; appliedCount: number };
|
||||||
|
removeSilenceTrimGroup: (groupId: string) => void;
|
||||||
setTranscribing: (active: boolean, progress?: number, status?: string) => void;
|
setTranscribing: (active: boolean, progress?: number, status?: string) => void;
|
||||||
setExporting: (active: boolean, progress?: number) => void;
|
setExporting: (active: boolean, progress?: number) => void;
|
||||||
getKeepSegments: () => Array<{ start: number; end: number }>;
|
getKeepSegments: () => Array<{ start: number; end: number }>;
|
||||||
@ -66,6 +84,7 @@ const initialState: EditorState = {
|
|||||||
deletedRanges: [],
|
deletedRanges: [],
|
||||||
cutRanges: [],
|
cutRanges: [],
|
||||||
muteRanges: [],
|
muteRanges: [],
|
||||||
|
silenceTrimGroups: [],
|
||||||
language: '',
|
language: '',
|
||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
@ -81,6 +100,32 @@ const initialState: EditorState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let nextRangeId = 1;
|
let nextRangeId = 1;
|
||||||
|
let nextTrimGroupId = 1;
|
||||||
|
|
||||||
|
function buildTrimCutRanges(
|
||||||
|
sourceRanges: SilenceDetectionRange[],
|
||||||
|
settings: SilenceTrimSettings,
|
||||||
|
maxEnd: number,
|
||||||
|
trimGroupId: string,
|
||||||
|
): CutRange[] {
|
||||||
|
const preBufferSeconds = settings.preBufferMs / 1000;
|
||||||
|
const postBufferSeconds = settings.postBufferMs / 1000;
|
||||||
|
const capEnd = maxEnd > 0 ? maxEnd : Number.POSITIVE_INFINITY;
|
||||||
|
|
||||||
|
const built: CutRange[] = [];
|
||||||
|
for (const range of sourceRanges) {
|
||||||
|
const start = Math.max(0, range.start + preBufferSeconds);
|
||||||
|
const end = Math.min(capEnd, range.end - postBufferSeconds);
|
||||||
|
if (end - start < 0.01) continue;
|
||||||
|
built.push({
|
||||||
|
id: `cut_${nextRangeId++}`,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
trimGroupId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return built;
|
||||||
|
}
|
||||||
|
|
||||||
export const useEditorStore = create<EditorState & EditorActions>()(
|
export const useEditorStore = create<EditorState & EditorActions>()(
|
||||||
temporal(
|
temporal(
|
||||||
@ -92,7 +137,7 @@ export const useEditorStore = create<EditorState & EditorActions>()(
|
|||||||
setExportedAudioPath: (path) => set({ exportedAudioPath: path }),
|
setExportedAudioPath: (path) => set({ exportedAudioPath: path }),
|
||||||
|
|
||||||
saveProject: (): ProjectFile => {
|
saveProject: (): ProjectFile => {
|
||||||
const { videoPath, words, segments, deletedRanges, cutRanges, muteRanges, language, exportedAudioPath } = get();
|
const { videoPath, words, segments, deletedRanges, cutRanges, muteRanges, silenceTrimGroups, language, exportedAudioPath } = get();
|
||||||
if (!videoPath) throw new Error('No video loaded');
|
if (!videoPath) throw new Error('No video loaded');
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
// Strip globalStartIndex (runtime-only field) before persisting
|
// Strip globalStartIndex (runtime-only field) before persisting
|
||||||
@ -106,6 +151,7 @@ export const useEditorStore = create<EditorState & EditorActions>()(
|
|||||||
deletedRanges,
|
deletedRanges,
|
||||||
cutRanges,
|
cutRanges,
|
||||||
muteRanges,
|
muteRanges,
|
||||||
|
silenceTrimGroups,
|
||||||
language,
|
language,
|
||||||
createdAt: now, // will be overwritten if we track original creation time later
|
createdAt: now, // will be overwritten if we track original creation time later
|
||||||
modifiedAt: now,
|
modifiedAt: now,
|
||||||
@ -186,12 +232,13 @@ export const useEditorStore = create<EditorState & EditorActions>()(
|
|||||||
set({ deletedRanges: deletedRanges.filter((r) => r.id !== rangeId) });
|
set({ deletedRanges: deletedRanges.filter((r) => r.id !== rangeId) });
|
||||||
},
|
},
|
||||||
|
|
||||||
addCutRange: (start, end) => {
|
addCutRange: (start, end, trimGroupId) => {
|
||||||
const { cutRanges } = get();
|
const { cutRanges } = get();
|
||||||
const newRange: CutRange = {
|
const newRange: CutRange = {
|
||||||
id: `cut_${nextRangeId++}`,
|
id: `cut_${nextRangeId++}`,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
|
trimGroupId,
|
||||||
};
|
};
|
||||||
set({ cutRanges: [...cutRanges, newRange] });
|
set({ cutRanges: [...cutRanges, newRange] });
|
||||||
},
|
},
|
||||||
@ -234,6 +281,47 @@ export const useEditorStore = create<EditorState & EditorActions>()(
|
|||||||
set({ muteRanges: muteRanges.filter((r) => r.id !== id) });
|
set({ muteRanges: muteRanges.filter((r) => r.id !== id) });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
applySilenceTrimGroup: ({ groupId, sourceRanges, settings }) => {
|
||||||
|
const { duration, cutRanges, silenceTrimGroups } = get();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const existingGroup = groupId
|
||||||
|
? silenceTrimGroups.find((group) => group.id === groupId)
|
||||||
|
: null;
|
||||||
|
const resolvedGroupId = existingGroup?.id ?? `trimgrp_${nextTrimGroupId++}`;
|
||||||
|
|
||||||
|
const nextGroup: SilenceTrimGroup = {
|
||||||
|
id: resolvedGroupId,
|
||||||
|
sourceRanges,
|
||||||
|
settings,
|
||||||
|
createdAt: existingGroup?.createdAt ?? now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
const withoutGroupCuts = cutRanges.filter((range) => range.trimGroupId !== resolvedGroupId);
|
||||||
|
const generatedCuts = buildTrimCutRanges(sourceRanges, settings, duration, resolvedGroupId);
|
||||||
|
const nextGroups = existingGroup
|
||||||
|
? silenceTrimGroups.map((group) => (group.id === resolvedGroupId ? nextGroup : group))
|
||||||
|
: [...silenceTrimGroups, nextGroup];
|
||||||
|
|
||||||
|
set({
|
||||||
|
cutRanges: [...withoutGroupCuts, ...generatedCuts],
|
||||||
|
silenceTrimGroups: nextGroups,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
groupId: resolvedGroupId,
|
||||||
|
appliedCount: generatedCuts.length,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
removeSilenceTrimGroup: (groupId) => {
|
||||||
|
const { cutRanges, silenceTrimGroups } = get();
|
||||||
|
set({
|
||||||
|
cutRanges: cutRanges.filter((range) => range.trimGroupId !== groupId),
|
||||||
|
silenceTrimGroups: silenceTrimGroups.filter((group) => group.id !== groupId),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
setTranscribing: (active, progress, status) =>
|
setTranscribing: (active, progress, status) =>
|
||||||
set({
|
set({
|
||||||
isTranscribing: active,
|
isTranscribing: active,
|
||||||
@ -321,6 +409,7 @@ export const useEditorStore = create<EditorState & EditorActions>()(
|
|||||||
deletedRanges: data.deletedRanges || [],
|
deletedRanges: data.deletedRanges || [],
|
||||||
cutRanges: data.cutRanges || [],
|
cutRanges: data.cutRanges || [],
|
||||||
muteRanges: data.muteRanges || [],
|
muteRanges: data.muteRanges || [],
|
||||||
|
silenceTrimGroups: data.silenceTrimGroups || [],
|
||||||
language: data.language || '',
|
language: data.language || '',
|
||||||
exportedAudioPath: data.exportedAudioPath ?? null,
|
exportedAudioPath: data.exportedAudioPath ?? null,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -28,12 +28,32 @@ export interface DeletedRange extends TimeRange {
|
|||||||
|
|
||||||
export interface CutRange extends TimeRange {
|
export interface CutRange extends TimeRange {
|
||||||
id: string;
|
id: string;
|
||||||
|
trimGroupId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MuteRange extends TimeRange {
|
export interface MuteRange extends TimeRange {
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SilenceDetectionRange extends TimeRange {
|
||||||
|
duration: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SilenceTrimSettings {
|
||||||
|
minSilenceMs: number;
|
||||||
|
silenceDb: number;
|
||||||
|
preBufferMs: number;
|
||||||
|
postBufferMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SilenceTrimGroup {
|
||||||
|
id: string;
|
||||||
|
settings: SilenceTrimSettings;
|
||||||
|
sourceRanges: SilenceDetectionRange[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProjectFile {
|
export interface ProjectFile {
|
||||||
version: 1;
|
version: 1;
|
||||||
videoPath: string;
|
videoPath: string;
|
||||||
@ -43,6 +63,7 @@ export interface ProjectFile {
|
|||||||
deletedRanges: DeletedRange[];
|
deletedRanges: DeletedRange[];
|
||||||
cutRanges: CutRange[];
|
cutRanges: CutRange[];
|
||||||
muteRanges: MuteRange[];
|
muteRanges: MuteRange[];
|
||||||
|
silenceTrimGroups?: SilenceTrimGroup[];
|
||||||
language: string;
|
language: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
modifiedAt: string;
|
modifiedAt: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user