still working on crashes
This commit is contained in:
@ -49,6 +49,7 @@ export default function App() {
|
||||
const [cutMode, setCutMode] = useState(false);
|
||||
const [muteMode, setMuteMode] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const lastVideoPathRef = useRef<string | null>(null);
|
||||
|
||||
useKeyboardShortcuts();
|
||||
|
||||
@ -74,13 +75,44 @@ export default function App() {
|
||||
// The backend URL is fixed at 127.0.0.1:8000 so we rely on the store default.
|
||||
}, [setBackendUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!import.meta.env.DEV) return;
|
||||
const previousVideoPath = lastVideoPathRef.current;
|
||||
if (previousVideoPath !== videoPath) {
|
||||
console.log('[app-state] videoPath transition', {
|
||||
from: previousVideoPath,
|
||||
to: videoPath,
|
||||
wordCount: words.length,
|
||||
isTranscribing,
|
||||
});
|
||||
if (previousVideoPath && !videoPath) {
|
||||
console.warn('[app-state] videoPath cleared and UI will show welcome screen', {
|
||||
previousVideoPath,
|
||||
wordCount: words.length,
|
||||
isTranscribing,
|
||||
});
|
||||
}
|
||||
lastVideoPathRef.current = videoPath;
|
||||
}
|
||||
}, [videoPath, words.length, isTranscribing]);
|
||||
|
||||
const handleLoadProject = async () => {
|
||||
if (!IS_DESKTOP) return;
|
||||
try {
|
||||
if (import.meta.env.DEV) console.log('[app-action] loadProject:dialogOpen');
|
||||
const projectPath = await window.desktopAPI!.openProject();
|
||||
if (import.meta.env.DEV) console.log('[app-action] loadProject:dialogResult', { projectPath });
|
||||
if (!projectPath) return;
|
||||
const content = await window.desktopAPI!.readFile(projectPath);
|
||||
const data = JSON.parse(content);
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('[app-action] loadProject:parsed', {
|
||||
projectPath,
|
||||
videoPath: data?.videoPath,
|
||||
words: Array.isArray(data?.words) ? data.words.length : null,
|
||||
segments: Array.isArray(data?.segments) ? data.segments.length : null,
|
||||
});
|
||||
}
|
||||
useEditorStore.getState().loadProject(data);
|
||||
} catch (err) {
|
||||
console.error('Failed to load project:', err);
|
||||
@ -104,8 +136,11 @@ export default function App() {
|
||||
|
||||
const handleOpenFile = async () => {
|
||||
if (IS_DESKTOP) {
|
||||
if (import.meta.env.DEV) console.log('[app-action] openFile:dialogOpen');
|
||||
const path = await window.desktopAPI!.openFile();
|
||||
if (import.meta.env.DEV) console.log('[app-action] openFile:dialogResult', { path });
|
||||
if (path) {
|
||||
if (import.meta.env.DEV) console.log('[app-action] openFile:loadVideo', { path });
|
||||
loadVideo(path);
|
||||
await transcribeVideo(path);
|
||||
}
|
||||
@ -113,6 +148,7 @@ export default function App() {
|
||||
// Browser: use the manual path input
|
||||
const path = manualPath.trim();
|
||||
if (path) {
|
||||
if (import.meta.env.DEV) console.log('[app-action] openFile:webManualPath', { path });
|
||||
loadVideo(path);
|
||||
await transcribeVideo(path);
|
||||
}
|
||||
@ -123,11 +159,13 @@ export default function App() {
|
||||
e.preventDefault();
|
||||
const path = manualPath.trim();
|
||||
if (!path) return;
|
||||
if (import.meta.env.DEV) console.log('[app-action] manualSubmit:loadVideo', { path });
|
||||
loadVideo(path);
|
||||
await transcribeVideo(path);
|
||||
};
|
||||
|
||||
const transcribeVideo = async (path: string) => {
|
||||
if (import.meta.env.DEV) console.log('[app-action] transcribe:start', { path, whisperModel });
|
||||
setTranscribing(true, 0, 'Checking model...');
|
||||
try {
|
||||
if (!window.desktopAPI?.transcribe) {
|
||||
@ -154,15 +192,25 @@ export default function App() {
|
||||
const modelLabel = MODEL_SIZES[whisperModel] ?? 'unknown size';
|
||||
setTranscribing(true, 5, `Downloading ${whisperModel} model (${modelLabel})...`);
|
||||
await window.desktopAPI.ensureModel(whisperModel);
|
||||
if (import.meta.env.DEV) console.log('[app-action] transcribe:modelReady', { whisperModel });
|
||||
|
||||
// Step 2: run transcription
|
||||
setTranscribing(true, 20, 'Transcribing audio...');
|
||||
const data = await window.desktopAPI.transcribe(path, whisperModel);
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('[app-action] transcribe:result', {
|
||||
path,
|
||||
words: Array.isArray(data?.words) ? data.words.length : null,
|
||||
segments: Array.isArray(data?.segments) ? data.segments.length : null,
|
||||
language: data?.language,
|
||||
});
|
||||
}
|
||||
setTranscription(data);
|
||||
} catch (err) {
|
||||
console.error('Transcription error:', err);
|
||||
alert(`Transcription failed. Check the console for details.\n\n${err}`);
|
||||
} finally {
|
||||
if (import.meta.env.DEV) console.log('[app-action] transcribe:finish', { path });
|
||||
setTranscribing(false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,13 +1,29 @@
|
||||
/**
|
||||
* Dev-only console interceptor.
|
||||
* Forwards console.error / console.warn to the backend /dev/log endpoint,
|
||||
* which appends them to webview.log so the agent can read it.
|
||||
* which appends them to a backend-managed dev log file.
|
||||
*/
|
||||
if (import.meta.env.DEV) {
|
||||
const BACKEND = 'http://127.0.0.1:8000';
|
||||
|
||||
type ConsoleFn = (...args: unknown[]) => void;
|
||||
|
||||
const serialize = (value: unknown): string => {
|
||||
if (typeof value === 'string') return value;
|
||||
if (value instanceof Error) {
|
||||
return JSON.stringify({
|
||||
name: value.name,
|
||||
message: value.message,
|
||||
stack: value.stack,
|
||||
});
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
};
|
||||
|
||||
const forward = (level: string, orig: ConsoleFn): ConsoleFn =>
|
||||
(...args: unknown[]) => {
|
||||
orig(...args);
|
||||
@ -15,7 +31,7 @@ if (import.meta.env.DEV) {
|
||||
fetch(`${BACKEND}/dev/log`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ level, message: String(first ?? ''), args: rest.map(String) }),
|
||||
body: JSON.stringify({ level, message: serialize(first ?? ''), args: rest.map(serialize) }),
|
||||
}).catch(() => {/* backend not running yet */});
|
||||
};
|
||||
|
||||
|
||||
@ -28,31 +28,48 @@ const EXPORT_FILTERS = [
|
||||
const BACKEND_PORT = import.meta.env.VITE_BACKEND_PORT || '8000';
|
||||
const BACKEND_URL = `http://127.0.0.1:${BACKEND_PORT}`;
|
||||
|
||||
const debugBridge = (event: string, details?: Record<string, unknown>) => {
|
||||
if (!import.meta.env.DEV) return;
|
||||
console.log('[tauri-bridge]', event, details ?? {});
|
||||
};
|
||||
|
||||
window.desktopAPI = {
|
||||
openFile: async (_options?: Record<string, unknown>): Promise<string | null> => {
|
||||
debugBridge('openFile:dialogOpen');
|
||||
const result = await open({
|
||||
multiple: false,
|
||||
filters: VIDEO_FILTERS,
|
||||
});
|
||||
return typeof result === 'string' ? result : null;
|
||||
const path = typeof result === 'string' ? result : null;
|
||||
debugBridge('openFile:dialogResult', { path });
|
||||
return path;
|
||||
},
|
||||
|
||||
saveFile: async (_options?: Record<string, unknown>): Promise<string | null> => {
|
||||
debugBridge('saveFile:dialogOpen');
|
||||
const result = await save({ filters: EXPORT_FILTERS });
|
||||
return result ?? null;
|
||||
const path = result ?? null;
|
||||
debugBridge('saveFile:dialogResult', { path });
|
||||
return path;
|
||||
},
|
||||
|
||||
openProject: async (): Promise<string | null> => {
|
||||
debugBridge('openProject:dialogOpen');
|
||||
const result = await open({
|
||||
multiple: false,
|
||||
filters: PROJECT_FILTERS,
|
||||
});
|
||||
return typeof result === 'string' ? result : null;
|
||||
const path = typeof result === 'string' ? result : null;
|
||||
debugBridge('openProject:dialogResult', { path });
|
||||
return path;
|
||||
},
|
||||
|
||||
saveProject: async (): Promise<string | null> => {
|
||||
debugBridge('saveProject:dialogOpen');
|
||||
const result = await save({ filters: PROJECT_FILTERS });
|
||||
return result ?? null;
|
||||
const path = result ?? null;
|
||||
debugBridge('saveProject:dialogResult', { path });
|
||||
return path;
|
||||
},
|
||||
|
||||
getBackendUrl: (): Promise<string> => {
|
||||
@ -77,10 +94,12 @@ window.desktopAPI = {
|
||||
},
|
||||
|
||||
readFile: (path: string): Promise<string> => {
|
||||
debugBridge('readFile', { path });
|
||||
return readTextFile(path);
|
||||
},
|
||||
|
||||
writeFile: async (path: string, content: string): Promise<boolean> => {
|
||||
debugBridge('writeFile', { path, size: content.length });
|
||||
await writeTextFile(path, content);
|
||||
return true;
|
||||
},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
// Forward console.error/warn/log to backend in dev mode so we can tail webview.log
|
||||
// Forward console.error/warn/log to backend in dev mode so we can tail the backend dev log.
|
||||
import './lib/dev-logger';
|
||||
// Must be imported before App so window.desktopAPI is patched before any component runs.
|
||||
import './lib/tauri-bridge';
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { temporal } from 'zundo';
|
||||
import type { Word, Segment, DeletedRange, CutRange, MuteRange, TranscriptionResult, ProjectFile } from '../types/project';
|
||||
|
||||
@ -84,12 +85,21 @@ const initialState: EditorState = {
|
||||
|
||||
let nextRangeId = 1;
|
||||
|
||||
const debugEditorStore = (event: string, details?: Record<string, unknown>) => {
|
||||
if (!import.meta.env.DEV) return;
|
||||
console.log('[editor-store]', event, details ?? {});
|
||||
};
|
||||
|
||||
export const useEditorStore = create<EditorState & EditorActions>()(
|
||||
temporal(
|
||||
(set, get) => ({
|
||||
persist(
|
||||
temporal(
|
||||
(set, get) => ({
|
||||
...initialState,
|
||||
|
||||
setBackendUrl: (url) => set({ backendUrl: url }),
|
||||
setBackendUrl: (url) => {
|
||||
debugEditorStore('setBackendUrl', { url });
|
||||
set({ backendUrl: url });
|
||||
},
|
||||
|
||||
setExportedAudioPath: (path) => set({ exportedAudioPath: path }),
|
||||
|
||||
@ -123,12 +133,18 @@ export const useEditorStore = create<EditorState & EditorActions>()(
|
||||
: `${backend}/file?path=${encodeURIComponent(filePath)}`;
|
||||
};
|
||||
const url = buildMediaUrl(path);
|
||||
debugEditorStore('loadVideo:start', {
|
||||
path,
|
||||
backend,
|
||||
previousVideoPath: get().videoPath,
|
||||
});
|
||||
set({
|
||||
...initialState,
|
||||
backendUrl: backend,
|
||||
videoPath: path,
|
||||
videoUrl: url,
|
||||
});
|
||||
debugEditorStore('loadVideo:done', { path, url });
|
||||
},
|
||||
|
||||
setTranscription: (result) => {
|
||||
@ -138,6 +154,12 @@ export const useEditorStore = create<EditorState & EditorActions>()(
|
||||
globalIdx += seg.words.length;
|
||||
return annotated;
|
||||
});
|
||||
debugEditorStore('setTranscription', {
|
||||
wordCount: result.words?.length ?? 0,
|
||||
segmentCount: result.segments?.length ?? 0,
|
||||
language: result.language,
|
||||
currentVideoPath: get().videoPath,
|
||||
});
|
||||
set({
|
||||
words: result.words,
|
||||
segments: annotatedSegments,
|
||||
@ -310,10 +332,18 @@ export const useEditorStore = create<EditorState & EditorActions>()(
|
||||
|
||||
loadProject: (data) => {
|
||||
const backend = get().backendUrl;
|
||||
const isWav = data.videoPath.toLowerCase().endsWith('.wav');
|
||||
const resolvedVideoPath = typeof data?.videoPath === 'string' ? data.videoPath : null;
|
||||
if (!resolvedVideoPath) {
|
||||
debugEditorStore('loadProject:invalidVideoPath', {
|
||||
videoPathType: typeof data?.videoPath,
|
||||
hasKeys: data && typeof data === 'object' ? Object.keys(data as Record<string, unknown>) : [],
|
||||
});
|
||||
throw new Error('Project file missing required videoPath string');
|
||||
}
|
||||
const isWav = resolvedVideoPath.toLowerCase().endsWith('.wav');
|
||||
const url = isWav
|
||||
? `${backend}/file?path=${encodeURIComponent(data.videoPath)}&format=mp3`
|
||||
: `${backend}/file?path=${encodeURIComponent(data.videoPath)}`;
|
||||
? `${backend}/file?path=${encodeURIComponent(resolvedVideoPath)}&format=mp3`
|
||||
: `${backend}/file?path=${encodeURIComponent(resolvedVideoPath)}`;
|
||||
|
||||
let globalIdx = 0;
|
||||
const annotatedSegments = (data.segments || []).map((seg: Segment) => {
|
||||
@ -322,10 +352,20 @@ export const useEditorStore = create<EditorState & EditorActions>()(
|
||||
return annotated;
|
||||
});
|
||||
|
||||
debugEditorStore('loadProject:start', {
|
||||
videoPath: resolvedVideoPath,
|
||||
words: Array.isArray(data?.words) ? data.words.length : null,
|
||||
segments: Array.isArray(data?.segments) ? data.segments.length : null,
|
||||
cutRanges: Array.isArray(data?.cutRanges) ? data.cutRanges.length : null,
|
||||
muteRanges: Array.isArray(data?.muteRanges) ? data.muteRanges.length : null,
|
||||
deletedRanges: Array.isArray(data?.deletedRanges) ? data.deletedRanges.length : null,
|
||||
previousVideoPath: get().videoPath,
|
||||
});
|
||||
|
||||
set({
|
||||
...initialState,
|
||||
backendUrl: backend,
|
||||
videoPath: data.videoPath,
|
||||
videoPath: resolvedVideoPath,
|
||||
videoUrl: url,
|
||||
words: data.words || [],
|
||||
segments: annotatedSegments,
|
||||
@ -335,9 +375,21 @@ export const useEditorStore = create<EditorState & EditorActions>()(
|
||||
language: data.language || '',
|
||||
exportedAudioPath: data.exportedAudioPath ?? null,
|
||||
});
|
||||
|
||||
debugEditorStore('loadProject:done', {
|
||||
videoPath: resolvedVideoPath,
|
||||
url,
|
||||
});
|
||||
},
|
||||
|
||||
reset: () => set(initialState),
|
||||
reset: () => {
|
||||
const stack = new Error().stack?.split('\n').slice(1, 6).join(' | ');
|
||||
debugEditorStore('reset', {
|
||||
previousVideoPath: get().videoPath,
|
||||
stack,
|
||||
});
|
||||
set(initialState);
|
||||
},
|
||||
|
||||
pauseUndo: () => {
|
||||
// Access the temporal store through the useEditorStore
|
||||
@ -354,7 +406,39 @@ export const useEditorStore = create<EditorState & EditorActions>()(
|
||||
temporalStore.getState().resume();
|
||||
}
|
||||
},
|
||||
}),
|
||||
{ limit: 100 },
|
||||
}),
|
||||
{ limit: 100 },
|
||||
),
|
||||
{
|
||||
name: 'talkedit-editor-session',
|
||||
version: 1,
|
||||
partialize: (state) => ({
|
||||
videoPath: state.videoPath,
|
||||
videoUrl: state.videoUrl,
|
||||
exportedAudioPath: state.exportedAudioPath,
|
||||
words: state.words,
|
||||
segments: state.segments,
|
||||
deletedRanges: state.deletedRanges,
|
||||
cutRanges: state.cutRanges,
|
||||
muteRanges: state.muteRanges,
|
||||
language: state.language,
|
||||
backendUrl: state.backendUrl,
|
||||
currentTime: state.currentTime,
|
||||
duration: state.duration,
|
||||
}),
|
||||
onRehydrateStorage: () => (state, error) => {
|
||||
if (error) {
|
||||
debugEditorStore('persist:rehydrate:error', { error: String(error) });
|
||||
return;
|
||||
}
|
||||
debugEditorStore('persist:rehydrate:done', {
|
||||
videoPath: state?.videoPath ?? null,
|
||||
words: state?.words?.length ?? 0,
|
||||
segments: state?.segments?.length ?? 0,
|
||||
cutRanges: state?.cutRanges?.length ?? 0,
|
||||
muteRanges: state?.muteRanges?.length ?? 0,
|
||||
});
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user