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,6 +1,17 @@
import { create } from 'zustand';
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 {
videoPath: string | null;
@ -11,6 +22,7 @@ interface EditorState {
deletedRanges: DeletedRange[];
cutRanges: CutRange[];
muteRanges: MuteRange[];
silenceTrimGroups: SilenceTrimGroup[];
language: string;
currentTime: number;
@ -43,12 +55,18 @@ interface EditorActions {
deleteSelectedWords: () => void;
deleteWordRange: (startIndex: number, endIndex: number) => 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;
updateCutRange: (id: string, start: number, end: number) => void;
updateMuteRange: (id: string, start: number, end: number) => void;
removeCutRange: (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;
setExporting: (active: boolean, progress?: number) => void;
getKeepSegments: () => Array<{ start: number; end: number }>;
@ -66,6 +84,7 @@ const initialState: EditorState = {
deletedRanges: [],
cutRanges: [],
muteRanges: [],
silenceTrimGroups: [],
language: '',
currentTime: 0,
duration: 0,
@ -81,6 +100,32 @@ const initialState: EditorState = {
};
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>()(
temporal(
@ -92,7 +137,7 @@ export const useEditorStore = create<EditorState & EditorActions>()(
setExportedAudioPath: (path) => set({ exportedAudioPath: path }),
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');
const now = new Date().toISOString();
// Strip globalStartIndex (runtime-only field) before persisting
@ -106,6 +151,7 @@ export const useEditorStore = create<EditorState & EditorActions>()(
deletedRanges,
cutRanges,
muteRanges,
silenceTrimGroups,
language,
createdAt: now, // will be overwritten if we track original creation time later
modifiedAt: now,
@ -186,12 +232,13 @@ export const useEditorStore = create<EditorState & EditorActions>()(
set({ deletedRanges: deletedRanges.filter((r) => r.id !== rangeId) });
},
addCutRange: (start, end) => {
addCutRange: (start, end, trimGroupId) => {
const { cutRanges } = get();
const newRange: CutRange = {
id: `cut_${nextRangeId++}`,
start,
end,
trimGroupId,
};
set({ cutRanges: [...cutRanges, newRange] });
},
@ -234,6 +281,47 @@ export const useEditorStore = create<EditorState & EditorActions>()(
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) =>
set({
isTranscribing: active,
@ -321,6 +409,7 @@ export const useEditorStore = create<EditorState & EditorActions>()(
deletedRanges: data.deletedRanges || [],
cutRanges: data.cutRanges || [],
muteRanges: data.muteRanges || [],
silenceTrimGroups: data.silenceTrimGroups || [],
language: data.language || '',
exportedAudioPath: data.exportedAudioPath ?? null,
});