Compare commits

...

4 Commits

Author SHA1 Message Date
egg
4a2efb3b9b debug: Add startup mode logging
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-22 18:57:21 +08:00
egg
9da6c91dbe fix: Add missing browser-api.js functions for browser mode
- Add getConfig() for app initialization
- Add openInBrowser() (no-op in browser mode)
- Add onTranscriptionResult() for compatibility
- Add onStreamStarted() for compatibility

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-22 18:56:40 +08:00
egg
e68c5ebd9f config: Enable browser-only launch mode by default
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-22 18:25:15 +08:00
egg
fd203ef771 feat: Add browser-only launch mode for Kaspersky bypass
- Add `ui.launchBrowser` config option to launch browser directly
- Fix sidecar_manager to support packaged mode paths
- Set BROWSER_MODE env var for backend sidecar management
- Skip Electron window when browser-only mode enabled

Usage: Set `"ui": { "launchBrowser": true }` in config.json
to bypass Kaspersky blocking by using system browser instead.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-22 18:15:45 +08:00
4 changed files with 150 additions and 10 deletions

View File

@@ -36,16 +36,41 @@ class SidecarManager:
self._reader_thread: Optional[Thread] = None self._reader_thread: Optional[Thread] = None
self._progress_callbacks: list[Callable] = [] self._progress_callbacks: list[Callable] = []
self._last_status: Dict[str, Any] = {} self._last_status: Dict[str, Any] = {}
self._is_packaged = getattr(sys, 'frozen', False)
# Paths # Paths - detect packaged vs development mode
self.project_dir = Path(__file__).parent.parent.parent if self._is_packaged:
self.sidecar_dir = self.project_dir / "sidecar" # Packaged mode: executable at resources/backend/backend/backend.exe
self.transcriber_path = self.sidecar_dir / "transcriber.py" # Sidecar at resources/sidecar/transcriber/transcriber.exe
self.venv_python = self.sidecar_dir / "venv" / "bin" / "python" exec_dir = Path(sys.executable).parent.parent # up from backend/backend.exe
resources_dir = exec_dir.parent # up from backend/ to resources/
self.sidecar_dir = resources_dir / "sidecar" / "transcriber"
self.transcriber_path = self.sidecar_dir / ("transcriber.exe" if sys.platform == "win32" else "transcriber")
self.venv_python = None # Not used in packaged mode
print(f"[Sidecar] Packaged mode: transcriber={self.transcriber_path}")
else:
# Development mode
self.project_dir = Path(__file__).parent.parent.parent
self.sidecar_dir = self.project_dir / "sidecar"
self.transcriber_path = self.sidecar_dir / "transcriber.py"
if sys.platform == "win32":
self.venv_python = self.sidecar_dir / "venv" / "Scripts" / "python.exe"
else:
self.venv_python = self.sidecar_dir / "venv" / "bin" / "python"
print(f"[Sidecar] Development mode: transcriber={self.transcriber_path}")
def is_available(self) -> bool: def is_available(self) -> bool:
"""Check if sidecar is available (files exist).""" """Check if sidecar is available (files exist)."""
return self.transcriber_path.exists() and self.venv_python.exists() if self._is_packaged:
# In packaged mode, just check the executable
available = self.transcriber_path.exists()
print(f"[Sidecar] is_available (packaged): {available}, path={self.transcriber_path}")
return available
else:
# Development mode - need both script and venv
available = self.transcriber_path.exists() and self.venv_python.exists()
print(f"[Sidecar] is_available (dev): {available}, script={self.transcriber_path.exists()}, venv={self.venv_python.exists()}")
return available
def get_status(self) -> Dict[str, Any]: def get_status(self) -> Dict[str, Any]:
"""Get current sidecar status.""" """Get current sidecar status."""
@@ -68,7 +93,6 @@ class SidecarManager:
return True # Already running return True # Already running
if not self.is_available(): if not self.is_available():
print(f"[Sidecar] Not available: transcriber={self.transcriber_path.exists()}, venv={self.venv_python.exists()}")
return False return False
try: try:
@@ -80,13 +104,25 @@ class SidecarManager:
print(f"[Sidecar] Starting with model={env['WHISPER_MODEL']}, device={env['WHISPER_DEVICE']}, compute={env['WHISPER_COMPUTE']}") print(f"[Sidecar] Starting with model={env['WHISPER_MODEL']}, device={env['WHISPER_DEVICE']}, compute={env['WHISPER_COMPUTE']}")
# Build command based on mode
if self._is_packaged:
# Packaged mode: run the executable directly
cmd = [str(self.transcriber_path)]
cwd = str(self.sidecar_dir)
else:
# Development mode: use venv python
cmd = [str(self.venv_python), str(self.transcriber_path), "--server"]
cwd = str(self.sidecar_dir.parent) if self._is_packaged else str(self.sidecar_dir)
print(f"[Sidecar] Command: {cmd}, cwd={cwd}")
self.process = subprocess.Popen( self.process = subprocess.Popen(
[str(self.venv_python), str(self.transcriber_path), "--server"], cmd,
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
env=env, env=env,
cwd=str(self.sidecar_dir), cwd=cwd,
bufsize=1, # Line buffered bufsize=1, # Line buffered
text=True text=True
) )

View File

@@ -2,6 +2,9 @@
"apiBaseUrl": "http://localhost:8000/api", "apiBaseUrl": "http://localhost:8000/api",
"uploadTimeout": 600000, "uploadTimeout": 600000,
"appTitle": "Meeting Assistant", "appTitle": "Meeting Assistant",
"ui": {
"launchBrowser": true
},
"whisper": { "whisper": {
"model": "medium", "model": "medium",
"device": "cpu", "device": "cpu",

View File

@@ -477,6 +477,57 @@ app.whenReady().then(async () => {
// Load configuration first // Load configuration first
loadConfig(); loadConfig();
const backendConfig = appConfig?.backend || {};
const uiConfig = appConfig?.ui || {};
const launchBrowser = uiConfig.launchBrowser === true;
console.log("=== Startup Mode Check ===");
console.log("uiConfig:", JSON.stringify(uiConfig));
console.log("launchBrowser:", launchBrowser);
console.log("backendConfig.embedded:", backendConfig.embedded);
console.log("Will use browser mode:", launchBrowser && backendConfig.embedded);
// Browser-only mode: start backend and open browser, no Electron UI
if (launchBrowser && backendConfig.embedded) {
console.log("=== Browser-Only Mode ===");
// Set BROWSER_MODE so backend manages sidecar
process.env.BROWSER_MODE = "true";
// Start backend sidecar
startBackendSidecar();
// Wait for backend to be ready
const ready = await waitForBackendReady();
if (!ready) {
const { dialog } = require("electron");
dialog.showErrorBox(
"Backend Startup Failed",
"後端服務啟動失敗。請檢查日誌以獲取詳細信息。"
);
app.quit();
return;
}
// Open browser to login page
const host = backendConfig.host || "127.0.0.1";
const port = backendConfig.port || 8000;
const loginUrl = `http://${host}:${port}/login`;
console.log(`Opening browser: ${loginUrl}`);
await shell.openExternal(loginUrl);
// Keep app running in background
// On macOS, we need to handle dock visibility
if (process.platform === "darwin") {
app.dock.hide();
}
console.log("Backend running. Close this window or press Ctrl+C to stop.");
return;
}
// Standard Electron mode
// Grant microphone permission automatically // Grant microphone permission automatically
session.defaultSession.setPermissionRequestHandler((webContents, permission, callback, details) => { session.defaultSession.setPermissionRequestHandler((webContents, permission, callback, details) => {
console.log(`Permission request: ${permission}`, details); console.log(`Permission request: ${permission}`, details);
@@ -512,7 +563,6 @@ app.whenReady().then(async () => {
startBackendSidecar(); startBackendSidecar();
// Wait for backend to be ready before creating window // Wait for backend to be ready before creating window
const backendConfig = appConfig?.backend || {};
if (backendConfig.embedded) { if (backendConfig.embedded) {
const ready = await waitForBackendReady(); const ready = await waitForBackendReady();
if (!ready) { if (!ready) {

View File

@@ -22,6 +22,39 @@ let streamingSocket = null;
// Browser mode API implementation // Browser mode API implementation
const browserAPI = { const browserAPI = {
// Get app configuration (browser mode fetches from backend or uses defaults)
getConfig: async () => {
try {
// Try to fetch config from backend
const response = await fetch(`${API_BASE}/config/settings.js`);
if (response.ok) {
// settings.js exports a config object, parse it
const text = await response.text();
// Simple extraction of the config object
const match = text.match(/export\s+const\s+config\s*=\s*(\{[\s\S]*?\});/);
if (match) {
// Use eval cautiously here - it's our own config file
const configStr = match[1];
return eval('(' + configStr + ')');
}
}
} catch (error) {
console.log('[Browser Mode] Could not load config from server, using defaults');
}
// Return browser mode defaults
return {
apiBaseUrl: `${window.location.origin}/api`,
uploadTimeout: 600000,
appTitle: "Meeting Assistant",
whisper: {
model: "medium",
device: "cpu",
compute: "int8"
}
};
},
// Navigate to a page // Navigate to a page
navigate: (page) => { navigate: (page) => {
const pageMap = { const pageMap = {
@@ -223,6 +256,24 @@ const browserAPI = {
} catch { } catch {
return { ready: false }; return { ready: false };
} }
},
// Open in browser - no-op in browser mode (already in browser)
openInBrowser: async () => {
console.log('[Browser Mode] Already running in browser');
return { success: true, url: window.location.href };
},
// Legacy transcription result listener (for file-based mode)
onTranscriptionResult: (callback) => {
// Not used in browser streaming mode, but provide for compatibility
console.log('[Browser Mode] onTranscriptionResult registered (legacy)');
},
// Stream started listener
onStreamStarted: (callback) => {
// HTTP-based streaming doesn't have this event
console.log('[Browser Mode] onStreamStarted registered');
} }
}; };