sona-ai-voice 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sona-ai-voice",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Voice-to-voice AI CLI companion powered by OpenAI Realtime API",
5
5
  "author": "zerish",
6
6
  "license": "MIT",
@@ -21,8 +21,8 @@
21
21
  "sona": "packages/cli/dist/index.js"
22
22
  },
23
23
  "files": [
24
+ "README.md",
24
25
  "packages/*/dist/**/*",
25
- "packages/*/package.json",
26
26
  "packages/cli/src/gui/**/*",
27
27
  "bin/**/*"
28
28
  ],
@@ -38,6 +38,8 @@
38
38
  "lint": "eslint packages --ext .ts,.tsx",
39
39
  "format": "prettier --write \"packages/**/*.{ts,tsx,json,md}\"",
40
40
  "cache:clear": "rm -rf node_modules/.cache .sona-cache",
41
+ "prepack": "node scripts/prepack.js",
42
+ "postpack": "node scripts/postpack.js",
41
43
  "prepublishOnly": "npm run build"
42
44
  },
43
45
  "dependencies": {
@@ -1,8 +1,5 @@
1
1
  "use strict";
2
- /**
3
- * Audio Capture Module
4
- * Mic capture with pause/resume for echo cancellation
5
- */
2
+
6
3
  Object.defineProperty(exports, "__esModule", { value: true });
7
4
  exports.AudioCapture = void 0;
8
5
  const child_process_1 = require("child_process");
@@ -30,14 +27,14 @@ class AudioCapture extends events_1.EventEmitter {
30
27
  this.micProcess = null;
31
28
  }
32
29
  this.micProcess = (0, child_process_1.spawn)('sox', [
33
- '-d', // Default audio device
34
- '-t', 'raw', // Raw PCM output
35
- '-b', '16', // 16-bit
36
- '-e', 'signed-integer', // Signed integer
37
- '-r', '24000', // 24kHz (required by OpenAI)
38
- '-c', '1', // Mono
39
- '-q', // Quiet mode
40
- '-', // Output to stdout
30
+ '-d',
31
+ '-t', 'raw',
32
+ '-b', '16',
33
+ '-e', 'signed-integer',
34
+ '-r', '24000',
35
+ '-c', '1',
36
+ '-q',
37
+ '-',
41
38
  ]);
42
39
  if (!this.micProcess.stdout) {
43
40
  this.emit('error', new Error('Failed to start microphone'));
@@ -48,12 +45,12 @@ class AudioCapture extends events_1.EventEmitter {
48
45
  this.emit('data', chunk);
49
46
  }
50
47
  });
51
- // Capture stderr to detect ALSA/audio device errors
48
+
52
49
  let stderrOutput = '';
53
50
  if (this.micProcess.stderr) {
54
51
  this.micProcess.stderr.on('data', (chunk) => {
55
52
  stderrOutput += chunk.toString();
56
- // Check for ALSA/audio device errors
53
+
57
54
  if (stderrOutput.includes('ALSA') ||
58
55
  stderrOutput.includes('no default audio device') ||
59
56
  stderrOutput.includes('Unknown PCM') ||
@@ -64,9 +61,9 @@ class AudioCapture extends events_1.EventEmitter {
64
61
  });
65
62
  }
66
63
  this.micProcess.on('error', (error) => {
67
- // ENOENT means command not found - don't spam errors
64
+
68
65
  if (error.code === 'ENOENT') {
69
- // Error already handled at startup, just emit once
66
+
70
67
  if (!this.isRecording) {
71
68
  this.emit('error', new Error('Audio capture tool (sox) not found. Please install sox.'));
72
69
  }
@@ -81,15 +78,12 @@ class AudioCapture extends events_1.EventEmitter {
81
78
  this.isPaused = false;
82
79
  this.emit('started');
83
80
  }
84
- /**
85
- * Pause audio capture (for echo cancellation during playback)
86
- * Kills the mic process to ensure no audio leaks through
87
- */
81
+
88
82
  pause() {
89
83
  if (!this.isRecording || this.isPaused)
90
84
  return;
91
85
  this.isPaused = true;
92
- // Kill the mic process to ensure complete silence
86
+
93
87
  if (this.micProcess) {
94
88
  try {
95
89
  this.micProcess.kill('SIGKILL');
@@ -98,14 +92,12 @@ class AudioCapture extends events_1.EventEmitter {
98
92
  this.micProcess = null;
99
93
  }
100
94
  }
101
- /**
102
- * Resume audio capture after playback ends
103
- */
95
+
104
96
  resume() {
105
97
  if (!this.isPaused)
106
98
  return;
107
99
  this.isPaused = false;
108
- // Restart the mic process
100
+
109
101
  this.spawnMic();
110
102
  }
111
103
  stop() {
@@ -136,4 +128,3 @@ class AudioCapture extends events_1.EventEmitter {
136
128
  }
137
129
  }
138
130
  exports.AudioCapture = AudioCapture;
139
- //# sourceMappingURL=audio-capture.js.map
@@ -1,8 +1,5 @@
1
1
  "use strict";
2
- /**
3
- * Audio Playback Module
4
- * Smooth speaker output using sox with buffering to prevent audio cracks
5
- */
2
+
6
3
  Object.defineProperty(exports, "__esModule", { value: true });
7
4
  exports.AudioPlayback = void 0;
8
5
  const child_process_1 = require("child_process");
@@ -11,7 +8,7 @@ class AudioPlayback extends events_1.EventEmitter {
11
8
  playProcess = null;
12
9
  isWritable = false;
13
10
  config;
14
- // Buffering for smooth playback
11
+
15
12
  audioBuffer = [];
16
13
  isPlaying = false;
17
14
  totalBytesQueued = 0;
@@ -26,7 +23,7 @@ class AudioPlayback extends events_1.EventEmitter {
26
23
  }
27
24
  spawnProcess() {
28
25
  if (this.playProcess) {
29
- return; // Already have a process
26
+ return;
30
27
  }
31
28
  this.playProcess = (0, child_process_1.spawn)('play', [
32
29
  '-t', 'raw',
@@ -35,26 +32,26 @@ class AudioPlayback extends events_1.EventEmitter {
35
32
  '-r', String(this.config.sampleRate),
36
33
  '-c', '1',
37
34
  '-q',
38
- '-', // Read from stdin
35
+ '-',
39
36
  ], {
40
- stdio: ['pipe', 'ignore', 'ignore'] // Only need stdin
37
+ stdio: ['pipe', 'ignore', 'ignore']
41
38
  });
42
39
  if (!this.playProcess.stdin) {
43
40
  this.emit('error', new Error('Failed to start speaker'));
44
41
  return;
45
42
  }
46
- // Handle stdin errors gracefully
43
+
47
44
  this.playProcess.stdin.on('error', (error) => {
48
45
  if (error.code !== 'EPIPE' && error.code !== 'ERR_STREAM_DESTROYED') {
49
46
  this.emit('error', error);
50
47
  }
51
48
  });
52
- // Capture stderr to detect ALSA/audio device errors
49
+
53
50
  let stderrOutput = '';
54
51
  if (this.playProcess.stderr) {
55
52
  this.playProcess.stderr.on('data', (chunk) => {
56
53
  stderrOutput += chunk.toString();
57
- // Check for ALSA/audio device errors
54
+
58
55
  if (stderrOutput.includes('ALSA') || stderrOutput.includes('no default audio device') || stderrOutput.includes('Unknown PCM')) {
59
56
  this.emit('error', new Error('Audio device not available. WSL does not support direct audio access.'));
60
57
  this.playProcess?.kill();
@@ -62,9 +59,9 @@ class AudioPlayback extends events_1.EventEmitter {
62
59
  });
63
60
  }
64
61
  this.playProcess.on('error', (error) => {
65
- // ENOENT means command not found - don't spam errors
62
+
66
63
  if (error.code === 'ENOENT') {
67
- // Error already handled at startup, just emit once
64
+
68
65
  if (!this.isPlaying) {
69
66
  this.emit('error', new Error('Audio playback tool (play) not found. Please install sox.'));
70
67
  }
@@ -74,20 +71,20 @@ class AudioPlayback extends events_1.EventEmitter {
74
71
  });
75
72
  this.playProcess.on('exit', (code) => {
76
73
  if (code !== 0 && code !== null && stderrOutput.includes('ALSA')) {
77
- // ALSA error - audio device not available
74
+
78
75
  this.emit('error', new Error('Audio device not available. WSL does not support direct audio access.'));
79
76
  }
80
77
  });
81
78
  this.playProcess.on('close', () => {
82
- // If we requested finish, don't respawn here - finishPlayback will handle it
83
- // Otherwise, respawn for next response
79
+
80
+
84
81
  if (!this.finishRequested) {
85
82
  this.playProcess = null;
86
83
  this.isWritable = false;
87
84
  this.spawnProcess();
88
85
  }
89
86
  else {
90
- // Process closed after finish was requested - this is expected
87
+
91
88
  this.playProcess = null;
92
89
  this.isWritable = false;
93
90
  }
@@ -95,16 +92,14 @@ class AudioPlayback extends events_1.EventEmitter {
95
92
  this.isWritable = true;
96
93
  this.emit('started');
97
94
  }
98
- /**
99
- * Queue audio for playback
100
- */
95
+
101
96
  play(audioBuffer) {
102
97
  if (this.finishRequested) {
103
- return; // Ignoring audio after finish requested
98
+ return;
104
99
  }
105
100
  this.totalBytesQueued += audioBuffer.length;
106
- // Write directly to the play process - no drain listener needed
107
- // The Node.js stream handles buffering internally
101
+
102
+
108
103
  if (this.playProcess?.stdin && this.isWritable) {
109
104
  try {
110
105
  this.playProcess.stdin.write(audioBuffer);
@@ -115,10 +110,7 @@ class AudioPlayback extends events_1.EventEmitter {
115
110
  }
116
111
  this.isPlaying = true;
117
112
  }
118
- /**
119
- * Signal that we're done sending audio for this response.
120
- * Closes stdin to signal end of stream, then waits for all audio to play.
121
- */
113
+
122
114
  finishPlayback() {
123
115
  if (this.totalBytesQueued === 0) {
124
116
  this.resetForNextResponse();
@@ -126,54 +118,50 @@ class AudioPlayback extends events_1.EventEmitter {
126
118
  return;
127
119
  }
128
120
  this.finishRequested = true;
129
- // Close stdin to signal no more data is coming
130
- // This ensures the play process knows the stream is complete
121
+
122
+
131
123
  if (this.playProcess?.stdin && this.isWritable) {
132
124
  try {
133
125
  this.playProcess.stdin.end();
134
126
  this.isWritable = false;
135
127
  }
136
128
  catch {
137
- // Ignore errors if already closed
129
+
138
130
  }
139
131
  }
140
- // Calculate playback duration from bytes queued
141
- // For 16-bit PCM: bytes / (sampleRate * 2 bytes per sample) * 1000ms
132
+
133
+
142
134
  const durationMs = (this.totalBytesQueued / (this.config.sampleRate * 2)) * 1000;
143
- // Add extra buffer to ensure last word is fully played
144
- // Account for system buffering and audio pipeline latency
145
- const waitTime = durationMs + 200; // Reduced buffer to 200ms for faster response
146
- // Wait for audio to finish playing
135
+
136
+
137
+ const waitTime = durationMs + 200;
138
+
147
139
  this.drainTimeout = setTimeout(() => {
148
140
  this.drainTimeout = null;
149
- // Respawn process for next response (since we closed stdin)
141
+
150
142
  this.spawnProcess();
151
143
  this.resetForNextResponse();
152
144
  this.emit('finished');
153
145
  }, waitTime);
154
146
  }
155
- /**
156
- * Reset state for next response
157
- */
147
+
158
148
  resetForNextResponse() {
159
149
  this.audioBuffer = [];
160
150
  this.totalBytesQueued = 0;
161
151
  this.isPlaying = false;
162
152
  this.finishRequested = false;
163
- // Don't kill the process - keep it alive for next response
164
- // Just let it finish playing current audio
153
+
154
+
165
155
  }
166
- /**
167
- * Immediately stop playback (for barge-in)
168
- */
156
+
169
157
  stop() {
170
158
  if (this.drainTimeout) {
171
159
  clearTimeout(this.drainTimeout);
172
160
  this.drainTimeout = null;
173
161
  }
174
- // Cancel finish request
162
+
175
163
  this.finishRequested = false;
176
- // Kill current process to stop audio immediately
164
+
177
165
  if (this.playProcess) {
178
166
  try {
179
167
  this.playProcess.kill('SIGKILL');
@@ -185,7 +173,7 @@ class AudioPlayback extends events_1.EventEmitter {
185
173
  this.totalBytesQueued = 0;
186
174
  this.isPlaying = false;
187
175
  this.isWritable = false;
188
- // Spawn fresh process for next response
176
+
189
177
  this.spawnProcess();
190
178
  this.emit('stopped');
191
179
  }
@@ -200,4 +188,3 @@ class AudioPlayback extends events_1.EventEmitter {
200
188
  }
201
189
  }
202
190
  exports.AudioPlayback = AudioPlayback;
203
- //# sourceMappingURL=audio-playback.js.map
@@ -1,21 +1,16 @@
1
1
  "use strict";
2
- /**
3
- * Cross-platform audio command detection
4
- * Detects available audio tools on different platforms
5
- */
2
+
6
3
  Object.defineProperty(exports, "__esModule", { value: true });
7
4
  exports.detectAudioCommands = detectAudioCommands;
8
5
  exports.getAudioInstallInstructions = getAudioInstallInstructions;
9
6
  exports.getAudioError = getAudioError;
10
7
  const os_1 = require("os");
11
8
  const child_process_1 = require("child_process");
12
- /**
13
- * Detect available audio commands for the current platform
14
- */
9
+
15
10
  function detectAudioCommands() {
16
11
  const os = (0, os_1.platform)();
17
12
  if (os === 'win32') {
18
- // Windows: Check for sox first, then ffmpeg
13
+
19
14
  try {
20
15
  (0, child_process_1.execSync)('sox --version', { stdio: 'ignore' });
21
16
  (0, child_process_1.execSync)('play --version', { stdio: 'ignore' });
@@ -27,7 +22,7 @@ function detectAudioCommands() {
27
22
  };
28
23
  }
29
24
  catch {
30
- // Try ffmpeg/ffplay as alternative
25
+
31
26
  try {
32
27
  (0, child_process_1.execSync)('ffmpeg -version', { stdio: 'ignore' });
33
28
  return {
@@ -48,7 +43,7 @@ function detectAudioCommands() {
48
43
  }
49
44
  }
50
45
  else {
51
- // Unix-like (Linux, macOS, WSL): sox/play
46
+
52
47
  try {
53
48
  (0, child_process_1.execSync)('sox --version', { stdio: 'ignore' });
54
49
  (0, child_process_1.execSync)('play --version', { stdio: 'ignore' });
@@ -69,9 +64,7 @@ function detectAudioCommands() {
69
64
  }
70
65
  }
71
66
  }
72
- /**
73
- * Get platform-specific installation instructions
74
- */
67
+
75
68
  function getAudioInstallInstructions(commands) {
76
69
  if (commands.available) {
77
70
  return '';
@@ -86,9 +79,7 @@ function getAudioInstallInstructions(commands) {
86
79
  return 'Audio tools not found. Install sox: sudo apt-get install sox libsox-fmt-all';
87
80
  }
88
81
  }
89
- /**
90
- * Get audio command error message
91
- */
82
+
92
83
  function getAudioError(commands) {
93
84
  if (commands.available) {
94
85
  return '';
@@ -96,4 +87,3 @@ function getAudioError(commands) {
96
87
  const instructions = getAudioInstallInstructions(commands);
97
88
  return `Audio tools (${commands.record}/${commands.play}) are not available.\n${instructions}`;
98
89
  }
99
- //# sourceMappingURL=audio-utils.js.map
@@ -1,12 +1,5 @@
1
1
  "use strict";
2
- /**
3
- * Echo Canceller Module
4
- *
5
- * Uses audio fingerprinting and correlation to detect when the mic
6
- * is picking up what we're playing through the speakers.
7
- *
8
- * This is the "last line of defense" when hardware AEC isn't available.
9
- */
2
+
10
3
  Object.defineProperty(exports, "__esModule", { value: true });
11
4
  exports.EchoCanceller = void 0;
12
5
  class EchoCanceller {
@@ -15,26 +8,24 @@ class EchoCanceller {
15
8
  isPlayingAudio = false;
16
9
  playbackEndTime = 0;
17
10
  config;
18
- // For energy-based gating
11
+
19
12
  recentPlaybackEnergy = 0;
20
13
  constructor(config = {}) {
21
14
  this.config = {
22
15
  sampleRate: 24000,
23
- bufferDurationMs: 1500, // Keep 1.5s of playback audio
24
- correlationThreshold: 0.35, // Correlation > 0.35 = likely echo
25
- energyThreshold: 0.01, // Minimum energy to process
16
+ bufferDurationMs: 1500,
17
+ correlationThreshold: 0.35,
18
+ energyThreshold: 0.01,
26
19
  ...config,
27
20
  };
28
21
  const bufferSize = Math.floor(this.config.sampleRate * this.config.bufferDurationMs / 1000);
29
22
  this.playbackBuffer = new Float32Array(bufferSize);
30
23
  }
31
- /**
32
- * Call this whenever we send audio to the speakers
33
- */
24
+
34
25
  feedPlayback(audioBuffer) {
35
26
  this.isPlayingAudio = true;
36
27
  this.playbackEndTime = Date.now();
37
- // Convert PCM16 to float samples and add to ring buffer
28
+
38
29
  const samples = this.pcm16ToFloat(audioBuffer);
39
30
  for (let i = 0; i < samples.length; i++) {
40
31
  const pos = this.playbackWritePos;
@@ -43,60 +34,53 @@ class EchoCanceller {
43
34
  }
44
35
  this.playbackWritePos = (this.playbackWritePos + 1) % this.playbackBuffer.length;
45
36
  }
46
- // Track recent playback energy
37
+
47
38
  this.recentPlaybackEnergy = this.calculateEnergy(samples);
48
39
  }
49
- /**
50
- * Mark playback as stopped
51
- */
40
+
52
41
  markPlaybackStopped() {
53
42
  this.isPlayingAudio = false;
54
43
  this.playbackEndTime = Date.now();
55
44
  }
56
- /**
57
- * Check if mic audio is likely echo from our playback
58
- * Returns true if we should DROP this audio (it's echo)
59
- */
45
+
60
46
  isEcho(micBuffer) {
61
- // If we haven't played anything recently, it's definitely not echo
47
+
62
48
  const timeSincePlayback = Date.now() - this.playbackEndTime;
63
49
  if (timeSincePlayback > 2000) {
64
50
  return false;
65
51
  }
66
52
  const micSamples = this.pcm16ToFloat(micBuffer);
67
53
  const micEnergy = this.calculateEnergy(micSamples);
68
- // If mic energy is very low, not worth checking
54
+
69
55
  if (micEnergy < this.config.energyThreshold) {
70
56
  return false;
71
57
  }
72
- // If we're actively playing audio, apply stricter gating
58
+
73
59
  if (this.isPlayingAudio) {
74
- // During active playback, almost anything is likely echo
75
- // unless it's significantly louder than what we're playing
60
+
61
+
76
62
  if (micEnergy < this.recentPlaybackEnergy * 3) {
77
- return true; // Probably echo
63
+ return true;
78
64
  }
79
65
  }
80
- // Cross-correlation check
66
+
81
67
  const correlation = this.crossCorrelate(micSamples);
82
68
  if (correlation > this.config.correlationThreshold) {
83
- return true; // High correlation = echo
69
+ return true;
84
70
  }
85
71
  return false;
86
72
  }
87
- /**
88
- * Calculate cross-correlation between mic audio and playback buffer
89
- */
73
+
90
74
  crossCorrelate(micSamples) {
91
75
  if (micSamples.length === 0)
92
76
  return 0;
93
77
  const playbackLen = this.playbackBuffer.length;
94
78
  const micLen = micSamples.length;
95
- // We'll check a few different lag positions
79
+
96
80
  let maxCorrelation = 0;
97
- // Check lags from 0 to 500ms (12000 samples at 24kHz)
81
+
98
82
  const maxLag = Math.min(12000, playbackLen - micLen);
99
- const lagStep = 100; // Check every ~4ms
83
+ const lagStep = 100;
100
84
  for (let lag = 0; lag < maxLag; lag += lagStep) {
101
85
  let sum = 0;
102
86
  let playbackSum = 0;
@@ -111,7 +95,7 @@ class EchoCanceller {
111
95
  micSum += micSample * micSample;
112
96
  }
113
97
  }
114
- // Normalized correlation
98
+
115
99
  const denom = Math.sqrt(playbackSum * micSum);
116
100
  if (denom > 0) {
117
101
  const correlation = Math.abs(sum / denom);
@@ -120,9 +104,7 @@ class EchoCanceller {
120
104
  }
121
105
  return maxCorrelation;
122
106
  }
123
- /**
124
- * Calculate RMS energy of samples
125
- */
107
+
126
108
  calculateEnergy(samples) {
127
109
  if (samples.length === 0)
128
110
  return 0;
@@ -135,9 +117,7 @@ class EchoCanceller {
135
117
  }
136
118
  return Math.sqrt(sum / samples.length);
137
119
  }
138
- /**
139
- * Convert PCM16 buffer to float samples
140
- */
120
+
141
121
  pcm16ToFloat(buffer) {
142
122
  const samples = new Float32Array(buffer.length / 2);
143
123
  for (let i = 0; i < samples.length; i++) {
@@ -145,9 +125,7 @@ class EchoCanceller {
145
125
  }
146
126
  return samples;
147
127
  }
148
- /**
149
- * Clear the playback buffer (e.g., on new session)
150
- */
128
+
151
129
  reset() {
152
130
  this.playbackBuffer.fill(0);
153
131
  this.playbackWritePos = 0;
@@ -156,4 +134,3 @@ class EchoCanceller {
156
134
  }
157
135
  }
158
136
  exports.EchoCanceller = EchoCanceller;
159
- //# sourceMappingURL=echo-canceller.js.map
@@ -1,7 +1,5 @@
1
1
  "use strict";
2
- /**
3
- * GUI Server - Serves particle visualization and broadcasts state updates
4
- */
2
+
5
3
  var __importDefault = (this && this.__importDefault) || function (mod) {
6
4
  return (mod && mod.__esModule) ? mod : { "default": mod };
7
5
  };
@@ -29,7 +27,7 @@ class GUIServer extends events_1.EventEmitter {
29
27
  }
30
28
  async start() {
31
29
  return new Promise((resolve, reject) => {
32
- // Create HTTP server to serve the HTML
30
+
33
31
  this.httpServer = (0, http_1.createServer)((req, res) => {
34
32
  if (req.url === '/' || req.url?.startsWith('/?')) {
35
33
  try {
@@ -48,11 +46,11 @@ class GUIServer extends events_1.EventEmitter {
48
46
  res.end('Not found');
49
47
  }
50
48
  });
51
- // Create WebSocket server
49
+
52
50
  this.wss = new ws_1.WebSocketServer({ server: this.httpServer });
53
51
  this.wss.on('connection', (ws) => {
54
52
  this.clients.add(ws);
55
- // Send current state immediately
53
+
56
54
  ws.send(JSON.stringify({ type: 'state', state: this.currentState }));
57
55
  ws.on('close', () => {
58
56
  this.clients.delete(ws);
@@ -69,7 +67,7 @@ class GUIServer extends events_1.EventEmitter {
69
67
  });
70
68
  this.httpServer.on('error', (err) => {
71
69
  if (err.code === 'EADDRINUSE') {
72
- // Port in use, try next port
70
+
73
71
  this.port++;
74
72
  this.httpServer?.close();
75
73
  this.start().then(resolve).catch(reject);
@@ -93,45 +91,41 @@ class GUIServer extends events_1.EventEmitter {
93
91
  command = 'cmd';
94
92
  args = ['/c', 'start', url];
95
93
  break;
96
- default: // Linux/WSL
94
+ default:
97
95
  command = 'xdg-open';
98
96
  args = [url];
99
97
  break;
100
98
  }
101
- // Try to open browser, but don't crash if command doesn't exist
99
+
102
100
  try {
103
101
  const proc = (0, child_process_1.spawn)(command, args, {
104
102
  detached: true,
105
103
  stdio: 'ignore'
106
104
  });
107
- // Handle errors gracefully
105
+
108
106
  proc.on('error', (error) => {
109
- // ENOENT means command not found - this is expected in some environments (WSL, headless servers)
107
+
110
108
  if (error.code === 'ENOENT') {
111
- // Browser opening command not available - show helpful message
109
+
112
110
  console.log(chalk_1.default.gray(`\nGUI available at: http://localhost:${this.port}`));
113
111
  console.log(chalk_1.default.gray('(Browser not auto-opened - open manually if needed)'));
114
112
  }
115
- // Other errors are unexpected but shouldn't crash the app
113
+
116
114
  });
117
- // Don't wait for process to exit
115
+
118
116
  proc.unref();
119
117
  }
120
118
  catch (error) {
121
- // If spawn itself fails, just continue
122
- // Browser opening is optional functionality
119
+
120
+
123
121
  }
124
122
  }
125
- /**
126
- * Broadcast state change to all connected clients
127
- */
123
+
128
124
  setState(state) {
129
125
  this.currentState = state;
130
126
  this.broadcast({ type: 'state', state });
131
127
  }
132
- /**
133
- * Broadcast audio level to all connected clients
134
- */
128
+
135
129
  setAudioLevel(level) {
136
130
  this.broadcast({ type: 'audio', level: Math.min(1, Math.max(0, level)) });
137
131
  }
@@ -143,7 +137,7 @@ class GUIServer extends events_1.EventEmitter {
143
137
  client.send(message);
144
138
  }
145
139
  catch {
146
- // Ignore send errors
140
+
147
141
  }
148
142
  }
149
143
  }
@@ -169,4 +163,3 @@ class GUIServer extends events_1.EventEmitter {
169
163
  }
170
164
  }
171
165
  exports.GUIServer = GUIServer;
172
- //# sourceMappingURL=gui-server.js.map