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) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Dev log file — frontend forwards console.error/warn here so the agent can read it # Dev log file — keep outside workspace to avoid dev watcher reload loops.
DEV_LOG_PATH = Path(__file__).parent.parent / "webview.log" DEV_LOG_PATH = Path(
os.environ.get("TALKEDIT_DEV_LOG_PATH", str(Path(tempfile.gettempdir()) / "talkedit-webview.log"))
)
@asynccontextmanager @asynccontextmanager
@ -205,6 +207,7 @@ async def dev_log(request: Request):
if args: if args:
line += " " + " ".join(args) line += " " + " ".join(args)
line += "\n" line += "\n"
DEV_LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
with open(DEV_LOG_PATH, "a") as f: with open(DEV_LOG_PATH, "a") as f:
f.write(line) 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 KILLED_ANY=0
BACKEND_PORT="${BACKEND_PORT:-8000}" BACKEND_PORT="${BACKEND_PORT:-8000}"
FRONTEND_PORT="${FRONTEND_PORT:-5173}"
kill_port() { kill_port() {
local port=$1 local port=$1
@ -10,7 +11,7 @@ kill_port() {
local pids local pids
pids=$(lsof -ti tcp:"$port" 2>/dev/null) pids=$(lsof -ti tcp:"$port" 2>/dev/null)
if [[ -n "$pids" ]]; then if [[ -n "$pids" ]]; then
echo "Stopping $name backend (port $port, PID $pids)..." echo "Stopping $name (port $port, PID $pids)..."
kill "$pids" 2>/dev/null kill "$pids" 2>/dev/null
KILLED_ANY=1 KILLED_ANY=1
fi fi
@ -31,8 +32,9 @@ kill_pattern() {
# --- TalkEdit (Tauri) --- # --- TalkEdit (Tauri) ---
kill_port "$BACKEND_PORT" "TalkEdit" kill_port "$BACKEND_PORT" "TalkEdit"
kill_pattern "tauri.*TalkEdit\|TalkEdit.*tauri\|cargo.*tauri dev\|/TalkEdit/target/debug" "TalkEdit (Tauri dev)" kill_pattern "tauri.*TalkEdit\|TalkEdit.*tauri\|cargo.*tauri dev\|/TalkEdit/target/debug" "TalkEdit (Tauri dev)"
# Vite dev server for TalkEdit (port 5173) # Frontend dev server: first kill by listening port, then by known process patterns.
kill_pattern "vite.*5173\|rsbuild.*5173" "TalkEdit frontend dev server" 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 --- # --- Orphaned uvicorn workers for TalkEdit ---
kill_pattern "uvicorn.*main:app.*--port ${BACKEND_PORT}" "leftover uvicorn workers (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 [cutMode, setCutMode] = useState(false);
const [muteMode, setMuteMode] = useState(false); const [muteMode, setMuteMode] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const lastVideoPathRef = useRef<string | null>(null);
useKeyboardShortcuts(); 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. // The backend URL is fixed at 127.0.0.1:8000 so we rely on the store default.
}, [setBackendUrl]); }, [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 () => { const handleLoadProject = async () => {
if (!IS_DESKTOP) return; if (!IS_DESKTOP) return;
try { try {
if (import.meta.env.DEV) console.log('[app-action] loadProject:dialogOpen');
const projectPath = await window.desktopAPI!.openProject(); const projectPath = await window.desktopAPI!.openProject();
if (import.meta.env.DEV) console.log('[app-action] loadProject:dialogResult', { projectPath });
if (!projectPath) return; if (!projectPath) return;
const content = await window.desktopAPI!.readFile(projectPath); const content = await window.desktopAPI!.readFile(projectPath);
const data = JSON.parse(content); 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); useEditorStore.getState().loadProject(data);
} catch (err) { } catch (err) {
console.error('Failed to load project:', err); console.error('Failed to load project:', err);
@ -104,8 +136,11 @@ export default function App() {
const handleOpenFile = async () => { const handleOpenFile = async () => {
if (IS_DESKTOP) { if (IS_DESKTOP) {
if (import.meta.env.DEV) console.log('[app-action] openFile:dialogOpen');
const path = await window.desktopAPI!.openFile(); const path = await window.desktopAPI!.openFile();
if (import.meta.env.DEV) console.log('[app-action] openFile:dialogResult', { path });
if (path) { if (path) {
if (import.meta.env.DEV) console.log('[app-action] openFile:loadVideo', { path });
loadVideo(path); loadVideo(path);
await transcribeVideo(path); await transcribeVideo(path);
} }
@ -113,6 +148,7 @@ export default function App() {
// Browser: use the manual path input // Browser: use the manual path input
const path = manualPath.trim(); const path = manualPath.trim();
if (path) { if (path) {
if (import.meta.env.DEV) console.log('[app-action] openFile:webManualPath', { path });
loadVideo(path); loadVideo(path);
await transcribeVideo(path); await transcribeVideo(path);
} }
@ -123,11 +159,13 @@ export default function App() {
e.preventDefault(); e.preventDefault();
const path = manualPath.trim(); const path = manualPath.trim();
if (!path) return; if (!path) return;
if (import.meta.env.DEV) console.log('[app-action] manualSubmit:loadVideo', { path });
loadVideo(path); loadVideo(path);
await transcribeVideo(path); await transcribeVideo(path);
}; };
const transcribeVideo = async (path: string) => { const transcribeVideo = async (path: string) => {
if (import.meta.env.DEV) console.log('[app-action] transcribe:start', { path, whisperModel });
setTranscribing(true, 0, 'Checking model...'); setTranscribing(true, 0, 'Checking model...');
try { try {
if (!window.desktopAPI?.transcribe) { if (!window.desktopAPI?.transcribe) {
@ -154,15 +192,25 @@ export default function App() {
const modelLabel = MODEL_SIZES[whisperModel] ?? 'unknown size'; const modelLabel = MODEL_SIZES[whisperModel] ?? 'unknown size';
setTranscribing(true, 5, `Downloading ${whisperModel} model (${modelLabel})...`); setTranscribing(true, 5, `Downloading ${whisperModel} model (${modelLabel})...`);
await window.desktopAPI.ensureModel(whisperModel); await window.desktopAPI.ensureModel(whisperModel);
if (import.meta.env.DEV) console.log('[app-action] transcribe:modelReady', { whisperModel });
// Step 2: run transcription // Step 2: run transcription
setTranscribing(true, 20, 'Transcribing audio...'); setTranscribing(true, 20, 'Transcribing audio...');
const data = await window.desktopAPI.transcribe(path, whisperModel); 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); setTranscription(data);
} catch (err) { } catch (err) {
console.error('Transcription error:', err); console.error('Transcription error:', err);
alert(`Transcription failed. Check the console for details.\n\n${err}`); alert(`Transcription failed. Check the console for details.\n\n${err}`);
} finally { } finally {
if (import.meta.env.DEV) console.log('[app-action] transcribe:finish', { path });
setTranscribing(false); setTranscribing(false);
} }
}; };

View File

@ -1,13 +1,29 @@
/** /**
* Dev-only console interceptor. * Dev-only console interceptor.
* Forwards console.error / console.warn to the backend /dev/log endpoint, * 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) { if (import.meta.env.DEV) {
const BACKEND = 'http://127.0.0.1:8000'; const BACKEND = 'http://127.0.0.1:8000';
type ConsoleFn = (...args: unknown[]) => void; 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 => const forward = (level: string, orig: ConsoleFn): ConsoleFn =>
(...args: unknown[]) => { (...args: unknown[]) => {
orig(...args); orig(...args);
@ -15,7 +31,7 @@ if (import.meta.env.DEV) {
fetch(`${BACKEND}/dev/log`, { fetch(`${BACKEND}/dev/log`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, 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 */}); }).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_PORT = import.meta.env.VITE_BACKEND_PORT || '8000';
const BACKEND_URL = `http://127.0.0.1:${BACKEND_PORT}`; 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 = { window.desktopAPI = {
openFile: async (_options?: Record<string, unknown>): Promise<string | null> => { openFile: async (_options?: Record<string, unknown>): Promise<string | null> => {
debugBridge('openFile:dialogOpen');
const result = await open({ const result = await open({
multiple: false, multiple: false,
filters: VIDEO_FILTERS, 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> => { saveFile: async (_options?: Record<string, unknown>): Promise<string | null> => {
debugBridge('saveFile:dialogOpen');
const result = await save({ filters: EXPORT_FILTERS }); 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> => { openProject: async (): Promise<string | null> => {
debugBridge('openProject:dialogOpen');
const result = await open({ const result = await open({
multiple: false, multiple: false,
filters: PROJECT_FILTERS, 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> => { saveProject: async (): Promise<string | null> => {
debugBridge('saveProject:dialogOpen');
const result = await save({ filters: PROJECT_FILTERS }); const result = await save({ filters: PROJECT_FILTERS });
return result ?? null; const path = result ?? null;
debugBridge('saveProject:dialogResult', { path });
return path;
}, },
getBackendUrl: (): Promise<string> => { getBackendUrl: (): Promise<string> => {
@ -77,10 +94,12 @@ window.desktopAPI = {
}, },
readFile: (path: string): Promise<string> => { readFile: (path: string): Promise<string> => {
debugBridge('readFile', { path });
return readTextFile(path); return readTextFile(path);
}, },
writeFile: async (path: string, content: string): Promise<boolean> => { writeFile: async (path: string, content: string): Promise<boolean> => {
debugBridge('writeFile', { path, size: content.length });
await writeTextFile(path, content); await writeTextFile(path, content);
return true; return true;
}, },

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; 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'; import './lib/dev-logger';
// Must be imported before App so window.desktopAPI is patched before any component runs. // Must be imported before App so window.desktopAPI is patched before any component runs.
import './lib/tauri-bridge'; import './lib/tauri-bridge';

View File

@ -1,4 +1,5 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { temporal } from 'zundo'; import { temporal } from 'zundo';
import type { Word, Segment, DeletedRange, CutRange, MuteRange, TranscriptionResult, ProjectFile } from '../types/project'; import type { Word, Segment, DeletedRange, CutRange, MuteRange, TranscriptionResult, ProjectFile } from '../types/project';
@ -84,12 +85,21 @@ const initialState: EditorState = {
let nextRangeId = 1; 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>()( export const useEditorStore = create<EditorState & EditorActions>()(
temporal( persist(
(set, get) => ({ temporal(
(set, get) => ({
...initialState, ...initialState,
setBackendUrl: (url) => set({ backendUrl: url }), setBackendUrl: (url) => {
debugEditorStore('setBackendUrl', { url });
set({ backendUrl: url });
},
setExportedAudioPath: (path) => set({ exportedAudioPath: path }), setExportedAudioPath: (path) => set({ exportedAudioPath: path }),
@ -123,12 +133,18 @@ export const useEditorStore = create<EditorState & EditorActions>()(
: `${backend}/file?path=${encodeURIComponent(filePath)}`; : `${backend}/file?path=${encodeURIComponent(filePath)}`;
}; };
const url = buildMediaUrl(path); const url = buildMediaUrl(path);
debugEditorStore('loadVideo:start', {
path,
backend,
previousVideoPath: get().videoPath,
});
set({ set({
...initialState, ...initialState,
backendUrl: backend, backendUrl: backend,
videoPath: path, videoPath: path,
videoUrl: url, videoUrl: url,
}); });
debugEditorStore('loadVideo:done', { path, url });
}, },
setTranscription: (result) => { setTranscription: (result) => {
@ -138,6 +154,12 @@ export const useEditorStore = create<EditorState & EditorActions>()(
globalIdx += seg.words.length; globalIdx += seg.words.length;
return annotated; return annotated;
}); });
debugEditorStore('setTranscription', {
wordCount: result.words?.length ?? 0,
segmentCount: result.segments?.length ?? 0,
language: result.language,
currentVideoPath: get().videoPath,
});
set({ set({
words: result.words, words: result.words,
segments: annotatedSegments, segments: annotatedSegments,
@ -310,10 +332,18 @@ export const useEditorStore = create<EditorState & EditorActions>()(
loadProject: (data) => { loadProject: (data) => {
const backend = get().backendUrl; 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 const url = isWav
? `${backend}/file?path=${encodeURIComponent(data.videoPath)}&format=mp3` ? `${backend}/file?path=${encodeURIComponent(resolvedVideoPath)}&format=mp3`
: `${backend}/file?path=${encodeURIComponent(data.videoPath)}`; : `${backend}/file?path=${encodeURIComponent(resolvedVideoPath)}`;
let globalIdx = 0; let globalIdx = 0;
const annotatedSegments = (data.segments || []).map((seg: Segment) => { const annotatedSegments = (data.segments || []).map((seg: Segment) => {
@ -322,10 +352,20 @@ export const useEditorStore = create<EditorState & EditorActions>()(
return annotated; 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({ set({
...initialState, ...initialState,
backendUrl: backend, backendUrl: backend,
videoPath: data.videoPath, videoPath: resolvedVideoPath,
videoUrl: url, videoUrl: url,
words: data.words || [], words: data.words || [],
segments: annotatedSegments, segments: annotatedSegments,
@ -335,9 +375,21 @@ export const useEditorStore = create<EditorState & EditorActions>()(
language: data.language || '', language: data.language || '',
exportedAudioPath: data.exportedAudioPath ?? null, 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: () => { pauseUndo: () => {
// Access the temporal store through the useEditorStore // Access the temporal store through the useEditorStore
@ -354,7 +406,39 @@ export const useEditorStore = create<EditorState & EditorActions>()(
temporalStore.getState().resume(); 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,
});
},
},
), ),
); );

15
open
View File

@ -5,6 +5,7 @@ PROJECT_DIR="$PWD"
export BACKEND_PORT="${BACKEND_PORT:-8000}" export BACKEND_PORT="${BACKEND_PORT:-8000}"
export VITE_BACKEND_PORT="${VITE_BACKEND_PORT:-$BACKEND_PORT}" 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" BACKEND_URL="http://127.0.0.1:${BACKEND_PORT}/health"
FRONTEND_URL="http://127.0.0.1:5173" FRONTEND_URL="http://127.0.0.1:5173"
@ -18,20 +19,20 @@ else
# Try common terminal emulators in order # Try common terminal emulators in order
if command -v ghostty &>/dev/null; then 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 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 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 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 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 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 else
echo "No supported terminal emulator found. Starting backend in background..." 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 fi
# Wait up to 15s for backend to become ready # Wait up to 15s for backend to become ready