From 1e02bf32d942c6c41edf2b2cab45c5c8fc9d69d2 Mon Sep 17 00:00:00 2001 From: dillonj Date: Wed, 8 Apr 2026 01:42:00 -0600 Subject: [PATCH] still working on crashes --- backend/main.py | 9 ++- close | 8 ++- frontend/src/App.tsx | 48 ++++++++++++++ frontend/src/lib/dev-logger.ts | 20 +++++- frontend/src/lib/tauri-bridge.ts | 27 ++++++-- frontend/src/main.tsx | 2 +- frontend/src/store/editorStore.ts | 104 +++++++++++++++++++++++++++--- open | 15 +++-- 8 files changed, 203 insertions(+), 30 deletions(-) diff --git a/backend/main.py b/backend/main.py index 5bef8b6..3e747cf 100644 --- a/backend/main.py +++ b/backend/main.py @@ -17,8 +17,10 @@ from routers import transcribe, export, ai, captions, audio logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -# Dev log file — frontend forwards console.error/warn here so the agent can read it -DEV_LOG_PATH = Path(__file__).parent.parent / "webview.log" +# Dev log file — keep outside workspace to avoid dev watcher reload loops. +DEV_LOG_PATH = Path( + os.environ.get("TALKEDIT_DEV_LOG_PATH", str(Path(tempfile.gettempdir()) / "talkedit-webview.log")) +) @asynccontextmanager @@ -205,6 +207,7 @@ async def dev_log(request: Request): if args: line += " " + " ".join(args) line += "\n" + DEV_LOG_PATH.parent.mkdir(parents=True, exist_ok=True) with open(DEV_LOG_PATH, "a") as f: f.write(line) - return {"ok": True} + return {"ok": True, "path": str(DEV_LOG_PATH)} diff --git a/close b/close index 14fd9ec..9778842 100755 --- a/close +++ b/close @@ -3,6 +3,7 @@ KILLED_ANY=0 BACKEND_PORT="${BACKEND_PORT:-8000}" +FRONTEND_PORT="${FRONTEND_PORT:-5173}" kill_port() { local port=$1 @@ -10,7 +11,7 @@ kill_port() { local pids pids=$(lsof -ti tcp:"$port" 2>/dev/null) if [[ -n "$pids" ]]; then - echo "Stopping $name backend (port $port, PID $pids)..." + echo "Stopping $name (port $port, PID $pids)..." kill "$pids" 2>/dev/null KILLED_ANY=1 fi @@ -31,8 +32,9 @@ kill_pattern() { # --- TalkEdit (Tauri) --- kill_port "$BACKEND_PORT" "TalkEdit" kill_pattern "tauri.*TalkEdit\|TalkEdit.*tauri\|cargo.*tauri dev\|/TalkEdit/target/debug" "TalkEdit (Tauri dev)" -# Vite dev server for TalkEdit (port 5173) -kill_pattern "vite.*5173\|rsbuild.*5173" "TalkEdit frontend dev server" +# Frontend dev server: first kill by listening port, then by known process patterns. +kill_port "$FRONTEND_PORT" "TalkEdit frontend" +kill_pattern "vite\|rsbuild\|npm.*run dev\|pnpm.*dev\|yarn.*dev" "TalkEdit frontend dev server" # --- Orphaned uvicorn workers for TalkEdit --- kill_pattern "uvicorn.*main:app.*--port ${BACKEND_PORT}" "leftover uvicorn workers (TalkEdit)" diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5caa377..b7808bf 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -49,6 +49,7 @@ export default function App() { const [cutMode, setCutMode] = useState(false); const [muteMode, setMuteMode] = useState(false); const fileInputRef = useRef(null); + const lastVideoPathRef = useRef(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); } }; diff --git a/frontend/src/lib/dev-logger.ts b/frontend/src/lib/dev-logger.ts index 5fbde26..03d5412 100644 --- a/frontend/src/lib/dev-logger.ts +++ b/frontend/src/lib/dev-logger.ts @@ -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 */}); }; diff --git a/frontend/src/lib/tauri-bridge.ts b/frontend/src/lib/tauri-bridge.ts index 5622406..4ac494b 100644 --- a/frontend/src/lib/tauri-bridge.ts +++ b/frontend/src/lib/tauri-bridge.ts @@ -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) => { + if (!import.meta.env.DEV) return; + console.log('[tauri-bridge]', event, details ?? {}); +}; + window.desktopAPI = { openFile: async (_options?: Record): Promise => { + 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): Promise => { + 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 => { + 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 => { + 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 => { @@ -77,10 +94,12 @@ window.desktopAPI = { }, readFile: (path: string): Promise => { + debugBridge('readFile', { path }); return readTextFile(path); }, writeFile: async (path: string, content: string): Promise => { + debugBridge('writeFile', { path, size: content.length }); await writeTextFile(path, content); return true; }, diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index a9ad0dc..f1c3209 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -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'; diff --git a/frontend/src/store/editorStore.ts b/frontend/src/store/editorStore.ts index b9d87d5..abb60e8 100644 --- a/frontend/src/store/editorStore.ts +++ b/frontend/src/store/editorStore.ts @@ -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) => { + if (!import.meta.env.DEV) return; + console.log('[editor-store]', event, details ?? {}); +}; + export const useEditorStore = create()( - 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()( : `${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()( 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()( 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) : [], + }); + 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()( 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()( 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()( 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, + }); + }, + }, ), ); diff --git a/open b/open index cea4854..cb8c7c1 100755 --- a/open +++ b/open @@ -5,6 +5,7 @@ PROJECT_DIR="$PWD" export BACKEND_PORT="${BACKEND_PORT:-8000}" export VITE_BACKEND_PORT="${VITE_BACKEND_PORT:-$BACKEND_PORT}" +export TALKEDIT_DEV_LOG_PATH="${TALKEDIT_DEV_LOG_PATH:-/tmp/talkedit-webview.log}" BACKEND_URL="http://127.0.0.1:${BACKEND_PORT}/health" FRONTEND_URL="http://127.0.0.1:5173" @@ -18,20 +19,20 @@ else # Try common terminal emulators in order if command -v ghostty &>/dev/null; then - ghostty -e bash -c "cd '${BACKEND_DIR}' && '${VENV_PYTHON}' -m uvicorn main:app --host 127.0.0.1 --port ${BACKEND_PORT}; exec bash" & + ghostty -e bash -c "cd '${BACKEND_DIR}' && TALKEDIT_DEV_LOG_PATH='${TALKEDIT_DEV_LOG_PATH}' '${VENV_PYTHON}' -m uvicorn main:app --host 127.0.0.1 --port ${BACKEND_PORT}; exec bash" & elif command -v kitty &>/dev/null; then - kitty --title "TalkEdit Backend" -- bash -c "cd '${BACKEND_DIR}' && '${VENV_PYTHON}' -m uvicorn main:app --host 127.0.0.1 --port ${BACKEND_PORT}; exec bash" & + kitty --title "TalkEdit Backend" -- bash -c "cd '${BACKEND_DIR}' && TALKEDIT_DEV_LOG_PATH='${TALKEDIT_DEV_LOG_PATH}' '${VENV_PYTHON}' -m uvicorn main:app --host 127.0.0.1 --port ${BACKEND_PORT}; exec bash" & elif command -v alacritty &>/dev/null; then - alacritty --title "TalkEdit Backend" -e bash -c "cd '${BACKEND_DIR}' && '${VENV_PYTHON}' -m uvicorn main:app --host 127.0.0.1 --port ${BACKEND_PORT}; exec bash" & + alacritty --title "TalkEdit Backend" -e bash -c "cd '${BACKEND_DIR}' && TALKEDIT_DEV_LOG_PATH='${TALKEDIT_DEV_LOG_PATH}' '${VENV_PYTHON}' -m uvicorn main:app --host 127.0.0.1 --port ${BACKEND_PORT}; exec bash" & elif command -v konsole &>/dev/null; then - konsole --new-tab -e bash -c "cd '${BACKEND_DIR}' && '${VENV_PYTHON}' -m uvicorn main:app --host 127.0.0.1 --port ${BACKEND_PORT}; exec bash" & + konsole --new-tab -e bash -c "cd '${BACKEND_DIR}' && TALKEDIT_DEV_LOG_PATH='${TALKEDIT_DEV_LOG_PATH}' '${VENV_PYTHON}' -m uvicorn main:app --host 127.0.0.1 --port ${BACKEND_PORT}; exec bash" & elif command -v gnome-terminal &>/dev/null; then - gnome-terminal --title "TalkEdit Backend" -- bash -c "cd '${BACKEND_DIR}' && '${VENV_PYTHON}' -m uvicorn main:app --host 127.0.0.1 --port ${BACKEND_PORT}; exec bash" & + gnome-terminal --title "TalkEdit Backend" -- bash -c "cd '${BACKEND_DIR}' && TALKEDIT_DEV_LOG_PATH='${TALKEDIT_DEV_LOG_PATH}' '${VENV_PYTHON}' -m uvicorn main:app --host 127.0.0.1 --port ${BACKEND_PORT}; exec bash" & elif command -v xterm &>/dev/null; then - xterm -T "TalkEdit Backend" -e bash -c "cd '${BACKEND_DIR}' && '${VENV_PYTHON}' -m uvicorn main:app --host 127.0.0.1 --port ${BACKEND_PORT}; exec bash" & + xterm -T "TalkEdit Backend" -e bash -c "cd '${BACKEND_DIR}' && TALKEDIT_DEV_LOG_PATH='${TALKEDIT_DEV_LOG_PATH}' '${VENV_PYTHON}' -m uvicorn main:app --host 127.0.0.1 --port ${BACKEND_PORT}; exec bash" & else echo "No supported terminal emulator found. Starting backend in background..." - cd "${BACKEND_DIR}" && "${VENV_PYTHON}" -m uvicorn main:app --host 127.0.0.1 --port ${BACKEND_PORT} & + cd "${BACKEND_DIR}" && TALKEDIT_DEV_LOG_PATH="${TALKEDIT_DEV_LOG_PATH}" "${VENV_PYTHON}" -m uvicorn main:app --host 127.0.0.1 --port ${BACKEND_PORT} & fi # Wait up to 15s for backend to become ready