Compare commits

...

3 Commits

Author SHA1 Message Date
egg
6112799c79 fix: Improve microphone device selection to avoid alias deviceIds
- Add isAlias() helper to identify 'default' and 'communications' aliases
- Prefer actual device IDs (long strings) over alias IDs
- For alias deviceIds, use {audio: true} to let system choose
- For real deviceIds, try exact first, then ideal as fallback
- Add debug logging for isSecureContext and alias detection

This fixes the "Could not start audio source" error when Electron tries
to open a microphone using exact: 'communications' constraint.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 13:50:49 +08:00
egg
9a6ca5730b debug: Add detailed sidecar startup logging
Add more console.log statements to help debug sidecar startup issues:
- Log when startSidecar() is called
- Log sidecar directory and packaged status
- Log executable path check results

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 11:53:07 +08:00
egg
c05fdad8e4 fix: Improve Whisper model status error handling
- Set sidecarReady to false when model_error is received
- Store error message in activeWhisperConfig for status display
- Update frontend to show error state with red indicator
- Prevents showing  when model failed to load

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 11:46:51 +08:00
2 changed files with 62 additions and 25 deletions

View File

@@ -288,9 +288,12 @@ function createWindow() {
} }
function startSidecar() { function startSidecar() {
console.log("=== startSidecar() called ===");
const sidecarDir = app.isPackaged const sidecarDir = app.isPackaged
? path.join(process.resourcesPath, "sidecar") ? path.join(process.resourcesPath, "sidecar")
: path.join(__dirname, "..", "..", "sidecar"); : path.join(__dirname, "..", "..", "sidecar");
console.log("Sidecar directory:", sidecarDir);
console.log("App is packaged:", app.isPackaged);
// Determine the sidecar executable path based on packaging and platform // Determine the sidecar executable path based on packaging and platform
let sidecarExecutable; let sidecarExecutable;
@@ -327,11 +330,13 @@ function startSidecar() {
sidecarArgs = [sidecarScript]; sidecarArgs = [sidecarScript];
} }
console.log("Checking sidecar executable at:", sidecarExecutable);
if (!fs.existsSync(sidecarExecutable)) { if (!fs.existsSync(sidecarExecutable)) {
console.log("Sidecar executable not found at:", sidecarExecutable); console.log("ERROR: Sidecar executable not found at:", sidecarExecutable);
console.log("Transcription will not be available."); console.log("Transcription will not be available.");
return; return;
} }
console.log("Sidecar executable found:", sidecarExecutable);
try { try {
// Get Whisper configuration from config.json or environment variables // Get Whisper configuration from config.json or environment variables
@@ -434,10 +439,16 @@ function startSidecar() {
mainWindow.webContents.send("model-download-progress", msg); mainWindow.webContents.send("model-download-progress", msg);
} }
// Forward model error status // Forward model error status and mark sidecar as not ready
if (msg.status === "model_error" && mainWindow) { if (msg.status === "model_error") {
sidecarReady = false;
if (activeWhisperConfig) {
activeWhisperConfig.error = msg.error || "Model load failed";
}
if (mainWindow) {
mainWindow.webContents.send("model-download-progress", msg); mainWindow.webContents.send("model-download-progress", msg);
} }
}
} catch (e) { } catch (e) {
console.log("Sidecar output:", line); console.log("Sidecar output:", line);
} }

View File

@@ -289,10 +289,17 @@
try { try {
const status = await window.electronAPI.getSidecarStatus(); const status = await window.electronAPI.getSidecarStatus();
if (status.whisper) { if (status.whisper) {
// Check if there was an error loading the model
if (status.whisper.error) {
whisperStatusEl.textContent = `❌ Model error: ${status.whisper.error}`;
whisperStatusEl.style.color = '#dc3545';
whisperStatusEl.title = 'Model failed to load';
} else {
const readyIcon = status.ready ? '✅' : '⏳'; const readyIcon = status.ready ? '✅' : '⏳';
whisperStatusEl.textContent = `${readyIcon} Model: ${status.whisper.model} | Device: ${status.whisper.device} | Compute: ${status.whisper.compute}`; whisperStatusEl.textContent = `${readyIcon} Model: ${status.whisper.model} | Device: ${status.whisper.device} | Compute: ${status.whisper.compute}`;
whisperStatusEl.title = `Config source: ${status.whisper.configSource || 'unknown'}`; whisperStatusEl.title = `Config source: ${status.whisper.configSource || 'unknown'}`;
whisperStatusEl.style.color = status.ready ? '#28a745' : '#ffc107'; whisperStatusEl.style.color = status.ready ? '#28a745' : '#ffc107';
}
} else { } else {
whisperStatusEl.textContent = status.ready ? '✅ Ready' : '⏳ Loading...'; whisperStatusEl.textContent = status.ready ? '✅ Ready' : '⏳ Loading...';
whisperStatusEl.style.color = status.ready ? '#28a745' : '#ffc107'; whisperStatusEl.style.color = status.ready ? '#28a745' : '#ffc107';
@@ -414,6 +421,9 @@
return; return;
} }
// Debug: Check if we're in a secure context
console.log('isSecureContext:', window.isSecureContext);
// Check for available audio devices first // Check for available audio devices first
const devices = await navigator.mediaDevices.enumerateDevices(); const devices = await navigator.mediaDevices.enumerateDevices();
const audioInputs = devices.filter(d => d.kind === 'audioinput'); const audioInputs = devices.filter(d => d.kind === 'audioinput');
@@ -424,41 +434,57 @@
return; return;
} }
// Helper to identify alias deviceIds (not real device IDs)
const isAlias = (id) => id === 'default' || id === 'communications';
// Filter out Stereo Mix (立體聲混音) - it's not a real microphone // Filter out Stereo Mix (立體聲混音) - it's not a real microphone
const realMicrophones = audioInputs.filter(d => const realMicrophones = audioInputs.filter(d =>
!d.label.includes('立體聲混音') && !d.label.includes('立體聲混音') &&
!d.label.toLowerCase().includes('stereo mix') && !d.label.toLowerCase().includes('stereo mix')
d.deviceId !== 'default' // Skip default which might be Stereo Mix
); );
// Prefer communications device or actual microphone // Prefer actual device IDs (long strings), not aliases like 'default' or 'communications'
// First try to find a real microphone with actual deviceId
let selectedMic = realMicrophones.find(d => let selectedMic = realMicrophones.find(d =>
d.deviceId === 'communications' || !isAlias(d.deviceId) && (
d.label.includes('麥克風') || d.label.includes('麥克風') ||
d.label.toLowerCase().includes('microphone') d.label.toLowerCase().includes('microphone')
) || realMicrophones[0]; )
) || realMicrophones.find(d => !isAlias(d.deviceId)) || realMicrophones[0];
console.log('Real microphones found:', realMicrophones.length); console.log('Real microphones found:', realMicrophones.length);
console.log('Selected microphone:', selectedMic); console.log('Selected microphone:', selectedMic);
console.log('Selected deviceId is alias:', selectedMic ? isAlias(selectedMic.deviceId) : 'N/A');
if (!selectedMic) { if (!selectedMic) {
alert('No real microphone found. Only Stereo Mix detected. Please connect a microphone.'); alert('No real microphone found. Only Stereo Mix detected. Please connect a microphone.');
return; return;
} }
// Try with the selected microphone // Get microphone stream with proper constraints handling
try {
if (isAlias(selectedMic.deviceId)) {
// For alias deviceIds (default/communications), let the system choose
console.log('Using system default (alias detected)');
mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
} else {
// For real deviceIds, try exact first, then ideal as fallback
try { try {
mediaStream = await navigator.mediaDevices.getUserMedia({ mediaStream = await navigator.mediaDevices.getUserMedia({
audio: { deviceId: { exact: selectedMic.deviceId } } audio: { deviceId: { exact: selectedMic.deviceId } }
}); });
console.log('Successfully connected to:', selectedMic.label);
} catch (exactErr) { } catch (exactErr) {
console.warn('Exact device ID failed, trying preferred:', exactErr); console.warn('Exact device ID failed, trying ideal:', exactErr);
// Fallback: try with preferred instead of exact
mediaStream = await navigator.mediaDevices.getUserMedia({ mediaStream = await navigator.mediaDevices.getUserMedia({
audio: { deviceId: selectedMic.deviceId } audio: { deviceId: { ideal: selectedMic.deviceId } }
}); });
} }
}
console.log('Successfully connected to:', selectedMic.label);
} catch (err) {
console.error('getUserMedia failed:', err.name, err.message);
throw err; // Let outer catch handle the error message
}
isRecording = true; isRecording = true;
recordBtn.textContent = 'Stop Recording'; recordBtn.textContent = 'Stop Recording';