more stuff to improve robustness

This commit is contained in:
2026-05-06 14:25:23 -06:00
parent 9a301fe2a2
commit 4004312994
13 changed files with 505 additions and 54 deletions

View File

@ -1,5 +1,6 @@
import { create } from 'zustand';
import { temporal } from 'zundo';
import { assert } from '../lib/assert';
import type {
Word,
Segment,
@ -109,7 +110,7 @@ interface EditorActions {
replaceWordRange: (startIndex: number, endIndex: number, newWords: Word[]) => void;
getKeepSegments: () => Array<{ start: number; end: number }>;
getWordAtTime: (time: number) => number;
loadProject: (projectData: any) => void;
loadProject: (projectData: any) => number;
reset: () => void;
setZoomConfig: (config: Partial<ZoomConfig>) => void;
addAdditionalClip: (path: string, label?: string) => void;
@ -203,7 +204,10 @@ export const useEditorStore = create<EditorState & EditorActions>()(
setExportedAudioPath: (path) => set({ exportedAudioPath: path }),
setTranscriptionModel: (model) => set({ transcriptionModel: model }),
setTranscriptionModel: (model) => {
if (model === null || model === '') return;
set({ transcriptionModel: model });
},
saveProject: (): ProjectFile => {
const { videoPath, words, segments, cutRanges, muteRanges, gainRanges, speedRanges, globalGainDb, silenceTrimGroups, timelineMarkers, transcriptionModel, language, exportedAudioPath, zoomConfig, additionalClips, backgroundMusic } = get();
@ -239,6 +243,7 @@ export const useEditorStore = create<EditorState & EditorActions>()(
},
loadVideo: (path) => {
if (!path) return;
const { backendUrl, zonePreviewPaddingSeconds } = get();
const url = `${backendUrl}/file?path=${encodeURIComponent(path)}`;
set({
@ -252,6 +257,10 @@ export const useEditorStore = create<EditorState & EditorActions>()(
},
setTranscription: (result) => {
if (!result.words || result.words.length === 0) {
set({ words: [], segments: [], selectedWordIndices: [] });
return;
}
let globalIdx = 0;
const annotatedSegments = result.segments.map((seg) => {
const annotated = { ...seg, globalStartIndex: globalIdx };
@ -269,8 +278,14 @@ export const useEditorStore = create<EditorState & EditorActions>()(
setCurrentTime: (time) => set({ currentTime: time }),
setDuration: (duration) => set({ duration }),
setIsPlaying: (playing) => set({ isPlaying: playing }),
setMarkInTime: (time) => set({ markInTime: time }),
setMarkOutTime: (time) => set({ markOutTime: time }),
setMarkInTime: (time) => {
if (time !== null && !isFinite(time)) return;
set({ markInTime: time });
},
setMarkOutTime: (time) => {
if (time !== null && !isFinite(time)) return;
set({ markOutTime: time });
},
clearMarkRange: () => set({ markInTime: null, markOutTime: null }),
setSelectedWordIndices: (indices) => set({ selectedWordIndices: indices }),
setHoveredWordIndex: (index) => set({ hoveredWordIndex: index }),
@ -294,7 +309,7 @@ export const useEditorStore = create<EditorState & EditorActions>()(
updateWordText: (index, text) => {
const { words, segments } = get();
if (index < 0 || index >= words.length) return;
if (index < 0 || index >= words.length || !text) return;
const newWords = words.map((w, i) =>
i === index ? { ...w, word: text } : w
);
@ -320,7 +335,8 @@ export const useEditorStore = create<EditorState & EditorActions>()(
},
addCutRange: (start, end, trimGroupId) => {
const { cutRanges } = get();
const { cutRanges, duration } = get();
if (!isFinite(start) || !isFinite(end) || start < 0 || end - start < 0.01 || end > duration) return;
const newRange: CutRange = {
id: `cut_${nextRangeId++}`,
start,
@ -331,7 +347,8 @@ export const useEditorStore = create<EditorState & EditorActions>()(
},
addMuteRange: (start, end) => {
const { muteRanges } = get();
const { muteRanges, duration } = get();
if (!isFinite(start) || !isFinite(end) || start < 0 || end - start < 0.01 || end > duration) return;
const newRange: MuteRange = {
id: `mute_${nextRangeId++}`,
start,
@ -341,7 +358,8 @@ export const useEditorStore = create<EditorState & EditorActions>()(
},
addGainRange: (start, end, gainDb) => {
const { gainRanges } = get();
const { gainRanges, duration } = get();
if (!isFinite(start) || !isFinite(end) || start < 0 || end - start < 0.01 || end > duration) return;
const newRange: GainRange = {
id: `gain_${nextRangeId++}`,
start,
@ -352,7 +370,8 @@ export const useEditorStore = create<EditorState & EditorActions>()(
},
addSpeedRange: (start, end, speed) => {
const { speedRanges } = get();
const { speedRanges, duration } = get();
if (!isFinite(start) || !isFinite(end) || start < 0 || end - start < 0.01 || end > duration) return;
const newRange: SpeedRange = {
id: `speed_${nextRangeId++}`,
start,
@ -437,6 +456,10 @@ export const useEditorStore = create<EditorState & EditorActions>()(
},
setGlobalGainDb: (gainDb) => {
if (!isFinite(gainDb)) {
set({ globalGainDb: 0 });
return;
}
set({ globalGainDb: Math.max(-24, Math.min(24, gainDb)) });
},
@ -482,11 +505,13 @@ export const useEditorStore = create<EditorState & EditorActions>()(
},
addTimelineMarker: (time, label, color) => {
const { timelineMarkers } = get();
if (!isFinite(time) || time < 0) return;
const { timelineMarkers, duration } = get();
if (time > duration) return;
const newMarker: TimelineMarker = {
id: `marker_${nextRangeId++}`,
time,
label: label || `Marker ${timelineMarkers.length + 1}`,
label: label || 'Marker',
color: color || '#6366f1',
};
set({ timelineMarkers: [...timelineMarkers, newMarker].sort((a, b) => a.time - b.time) });
@ -529,6 +554,7 @@ export const useEditorStore = create<EditorState & EditorActions>()(
}),
setZonePreviewPaddingSeconds: (seconds) => {
if (!isFinite(seconds)) return;
const nextSeconds = Math.max(0, Math.min(10, seconds));
if (typeof window !== 'undefined') {
window.localStorage.setItem(ZONE_PREVIEW_PADDING_KEY, String(nextSeconds));
@ -538,6 +564,9 @@ export const useEditorStore = create<EditorState & EditorActions>()(
replaceWordRange: (startIndex, endIndex, newWords) => {
const { words } = get();
assert(startIndex >= 0 && startIndex < words.length, 'replaceWordRange: startIndex out of bounds');
assert(endIndex >= 0 && endIndex < words.length, 'replaceWordRange: endIndex out of bounds');
assert(startIndex <= endIndex, 'replaceWordRange: startIndex > endIndex');
if (startIndex < 0 || endIndex >= words.length || startIndex > endIndex) return;
// Replace words in the range with new words
@ -647,6 +676,10 @@ export const useEditorStore = create<EditorState & EditorActions>()(
},
setBackgroundMusic: (config) => {
if (!config || !config.path) {
set({ backgroundMusic: null });
return;
}
set({ backgroundMusic: config });
},
@ -657,9 +690,28 @@ export const useEditorStore = create<EditorState & EditorActions>()(
},
loadProject: (data) => {
const { backendUrl, zonePreviewPaddingSeconds, projectFilePath } = get();
const { backendUrl, zonePreviewPaddingSeconds, projectFilePath, duration } = get();
const url = `${backendUrl}/file?path=${encodeURIComponent(data.videoPath)}`;
const isValidZone = (r: { start: number; end: number }) =>
isFinite(r.start) && isFinite(r.end) && r.start >= 0 && r.end - r.start >= 0.01 && (duration <= 0 || r.end <= duration);
let removed = 0;
const filterZones = <T extends { start: number; end: number }>(ranges: T[]): T[] => {
const result: T[] = [];
for (const r of ranges) {
if (isValidZone(r)) { result.push(r); } else { removed++; }
}
return result;
};
// Backward compat: merge legacy deletedRanges into cutRanges as time-range cuts
const legacyCuts = (data.deletedRanges || []).map((r: any) => ({ id: r.id, start: r.start, end: r.end }));
const cleanedCutRanges = filterZones<CutRange>([...(data.cutRanges || []), ...legacyCuts]);
const cleanedMuteRanges = filterZones<MuteRange>(data.muteRanges || []);
const cleanedGainRanges = filterZones<GainRange>(data.gainRanges || []);
const cleanedSpeedRanges = filterZones<SpeedRange>(data.speedRanges || []);
let globalIdx = 0;
const annotatedSegments = (data.segments || []).map((seg: Segment) => {
const annotated = { ...seg, globalStartIndex: globalIdx };
@ -676,14 +728,10 @@ export const useEditorStore = create<EditorState & EditorActions>()(
videoUrl: url,
words: data.words || [],
segments: annotatedSegments,
// Backward compat: merge legacy deletedRanges into cutRanges as time-range cuts
cutRanges: [
...(data.cutRanges || []),
...(data.deletedRanges || []).map((r: any) => ({ id: r.id, start: r.start, end: r.end })),
],
muteRanges: data.muteRanges || [],
gainRanges: data.gainRanges || [],
speedRanges: data.speedRanges || [],
cutRanges: cleanedCutRanges,
muteRanges: cleanedMuteRanges,
gainRanges: cleanedGainRanges,
speedRanges: cleanedSpeedRanges,
globalGainDb: typeof data.globalGainDb === 'number' ? data.globalGainDb : 0,
silenceTrimGroups: data.silenceTrimGroups || [],
timelineMarkers: data.timelineMarkers || [],
@ -694,6 +742,8 @@ export const useEditorStore = create<EditorState & EditorActions>()(
additionalClips: data.additionalClips || [],
backgroundMusic: data.backgroundMusic || null,
});
return removed;
},
reset: () => {