more stuff to improve robustness
This commit is contained in:
@ -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: () => {
|
||||
|
||||
Reference in New Issue
Block a user