still working on crashes

This commit is contained in:
2026-04-08 01:42:00 -06:00
parent 2406b0a2e7
commit 1e02bf32d9
8 changed files with 203 additions and 30 deletions

View File

@ -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)}

8
close
View File

@ -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)"

View File

@ -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);
}
};

View File

@ -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 */});
};

View File

@ -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;
},

View File

@ -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';

View File

@ -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>()(
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
@ -357,4 +409,36 @@ export const useEditorStore = create<EditorState & EditorActions>()(
}),
{ 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,
});
},
},
),
);

15
open
View File

@ -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