uwonbot 1.1.5 → 1.1.8
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/bin/uwonbot.js +1 -1
- package/package.json +1 -1
- package/src/agent.js +30 -26
- package/src/chat.js +84 -22
- package/src/terminalTTS.js +99 -0
- package/src/voiceInput.js +8 -1
package/bin/uwonbot.js
CHANGED
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -323,36 +323,26 @@ async function handleCommand(msg) {
|
|
|
323
323
|
}
|
|
324
324
|
|
|
325
325
|
async function openWebAssistant(assistantId) {
|
|
326
|
-
const
|
|
326
|
+
const chatUrl = `${WEB_APP_URL}/assistant/live?id=${assistantId}`;
|
|
327
327
|
try {
|
|
328
328
|
if (platform === 'darwin') {
|
|
329
|
-
await execAsync(`open -na "Google Chrome" --args --new-window
|
|
329
|
+
await execAsync(`open -na "Google Chrome" --args --new-window --window-size=380,520 --window-position=1000,200 "${chatUrl}" 2>/dev/null || open "${chatUrl}"`);
|
|
330
330
|
} else if (platform === 'win32') {
|
|
331
|
-
await execAsync(`start chrome --new-window "${
|
|
331
|
+
await execAsync(`start chrome --new-window --window-size=380,520 --window-position=1000,200 "${chatUrl}" 2>nul || start "" "${chatUrl}"`);
|
|
332
332
|
} else {
|
|
333
|
-
await execAsync(`google-chrome --new-window "${
|
|
333
|
+
await execAsync(`google-chrome --new-window "${chatUrl}" 2>/dev/null || xdg-open "${chatUrl}"`);
|
|
334
334
|
}
|
|
335
335
|
return true;
|
|
336
336
|
} catch {
|
|
337
|
-
try { await execAsync(`open "${
|
|
337
|
+
try { await execAsync(`open "${chatUrl}"`); return true; } catch { return false; }
|
|
338
338
|
}
|
|
339
339
|
}
|
|
340
340
|
|
|
341
341
|
async function activateAllAssistants(assistants) {
|
|
342
342
|
const opened = [];
|
|
343
343
|
for (const a of assistants) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
await openTerminalWithChat(a.name);
|
|
347
|
-
opened.push({ name: a.name, mode: 'terminal' });
|
|
348
|
-
} else if (mode === 'web') {
|
|
349
|
-
await openWebAssistant(a.id);
|
|
350
|
-
opened.push({ name: a.name, mode: 'web' });
|
|
351
|
-
} else {
|
|
352
|
-
await openTerminalWithChat(a.name);
|
|
353
|
-
await openWebAssistant(a.id);
|
|
354
|
-
opened.push({ name: a.name, mode: 'both' });
|
|
355
|
-
}
|
|
344
|
+
await openWebAssistant(a.id);
|
|
345
|
+
opened.push({ name: a.name, mode: 'web' });
|
|
356
346
|
await new Promise(r => setTimeout(r, 500));
|
|
357
347
|
}
|
|
358
348
|
return opened;
|
|
@@ -449,17 +439,23 @@ export async function startAgent(port = 9876, options = {}) {
|
|
|
449
439
|
console.log(chalk.bold.cyan(' 👏 박수 감지! 비서 활성화 중...'));
|
|
450
440
|
|
|
451
441
|
if (userAssistants.length === 0) {
|
|
452
|
-
console.log(chalk.gray(' → 기본 Uwonbot 실행'));
|
|
453
|
-
|
|
442
|
+
console.log(chalk.gray(' → 기본 Uwonbot 웹 실행'));
|
|
443
|
+
try {
|
|
444
|
+
if (platform === 'darwin') {
|
|
445
|
+
await execAsync(`open "${WEB_APP_URL}/assistant"`);
|
|
446
|
+
} else if (platform === 'win32') {
|
|
447
|
+
await execAsync(`start "" "${WEB_APP_URL}/assistant"`);
|
|
448
|
+
} else {
|
|
449
|
+
await execAsync(`xdg-open "${WEB_APP_URL}/assistant"`);
|
|
450
|
+
}
|
|
451
|
+
} catch {}
|
|
454
452
|
return;
|
|
455
453
|
}
|
|
456
454
|
|
|
457
455
|
if (userAssistants.length === 1) {
|
|
458
456
|
const a = userAssistants[0];
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if (mode === 'terminal' || mode === 'both') await openTerminalWithChat(a.name);
|
|
462
|
-
if (mode === 'web' || mode === 'both') await openWebAssistant(a.id);
|
|
457
|
+
console.log(chalk.green(` → ${a.name} 웹 실행`));
|
|
458
|
+
await openWebAssistant(a.id);
|
|
463
459
|
return;
|
|
464
460
|
}
|
|
465
461
|
|
|
@@ -488,7 +484,11 @@ export async function startAgent(port = 9876, options = {}) {
|
|
|
488
484
|
console.log(chalk.green(` ✓ Client connected from ${origin}`));
|
|
489
485
|
|
|
490
486
|
ws.on('message', async (data) => {
|
|
491
|
-
const
|
|
487
|
+
const raw = data.toString();
|
|
488
|
+
let _reqId;
|
|
489
|
+
try { _reqId = JSON.parse(raw)._reqId; } catch {}
|
|
490
|
+
const result = await handleCommand(raw);
|
|
491
|
+
if (_reqId) result._reqId = _reqId;
|
|
492
492
|
ws.send(JSON.stringify(result));
|
|
493
493
|
});
|
|
494
494
|
|
|
@@ -508,10 +508,14 @@ export async function startAgent(port = 9876, options = {}) {
|
|
|
508
508
|
const retry = new WebSocketServer({ port });
|
|
509
509
|
retry.on('connection', (ws, req) => {
|
|
510
510
|
ws.on('message', async (data) => {
|
|
511
|
-
const
|
|
511
|
+
const raw = data.toString();
|
|
512
|
+
let _reqId;
|
|
513
|
+
try { _reqId = JSON.parse(raw)._reqId; } catch {}
|
|
514
|
+
const result = await handleCommand(raw);
|
|
515
|
+
if (_reqId) result._reqId = _reqId;
|
|
512
516
|
ws.send(JSON.stringify(result));
|
|
513
517
|
});
|
|
514
|
-
ws.send(JSON.stringify({ type: 'welcome', agent: 'uwonbot', version: '1.1.
|
|
518
|
+
ws.send(JSON.stringify({ type: 'welcome', agent: 'uwonbot', version: '1.1.8', uid }));
|
|
515
519
|
});
|
|
516
520
|
console.log(chalk.green(` ✓ 재시도 성공 — ws://localhost:${port}`));
|
|
517
521
|
} catch {
|
package/src/chat.js
CHANGED
|
@@ -7,9 +7,12 @@ import open from 'open';
|
|
|
7
7
|
import { getConfig } from './config.js';
|
|
8
8
|
import { sendToBrain } from './brain.js';
|
|
9
9
|
import { showMiniBar } from './banner.js';
|
|
10
|
-
|
|
10
|
+
// terminalOrb is used by agent.js for display; chat.js shows text-only
|
|
11
|
+
import { speak } from './terminalTTS.js';
|
|
11
12
|
import VoiceInput from './voiceInput.js';
|
|
12
13
|
import {
|
|
14
|
+
fetchAssistants,
|
|
15
|
+
setIdToken,
|
|
13
16
|
hasRegisteredDevices,
|
|
14
17
|
createCLISession,
|
|
15
18
|
checkCLISession,
|
|
@@ -119,6 +122,36 @@ export async function startChat(assistantName, assistant, initialCommand, option
|
|
|
119
122
|
return;
|
|
120
123
|
}
|
|
121
124
|
|
|
125
|
+
if (!assistant && assistantName) {
|
|
126
|
+
const idToken = config.get('idToken');
|
|
127
|
+
if (idToken) setIdToken(idToken);
|
|
128
|
+
|
|
129
|
+
const spinner = ora({ text: chalk.gray('비서 로딩 중...'), spinner: 'dots', color: 'cyan' }).start();
|
|
130
|
+
try {
|
|
131
|
+
const all = await fetchAssistants(uid);
|
|
132
|
+
const found = all.find(a => a.name.toLowerCase() === assistantName.toLowerCase());
|
|
133
|
+
if (found) {
|
|
134
|
+
assistant = found;
|
|
135
|
+
spinner.stop();
|
|
136
|
+
} else {
|
|
137
|
+
spinner.stop();
|
|
138
|
+
console.log(chalk.yellow(`\n ⚠ "${assistantName}" 비서를 찾을 수 없습니다.`));
|
|
139
|
+
if (all.length > 0) {
|
|
140
|
+
console.log(chalk.gray(' 등록된 비서: ' + all.map(a => a.name).join(', ')));
|
|
141
|
+
} else {
|
|
142
|
+
console.log(chalk.gray(' 등록된 비서가 없습니다. 웹에서 생성해주세요:'));
|
|
143
|
+
console.log(chalk.cyan(' https://chartapp-653e1.web.app/assistant/create'));
|
|
144
|
+
}
|
|
145
|
+
console.log('');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
} catch (err) {
|
|
149
|
+
spinner.stop();
|
|
150
|
+
console.log(chalk.red(`\n ⚠ 비서 목록 로드 실패: ${err.message}\n`));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
122
155
|
if (!assistant) {
|
|
123
156
|
console.log(chalk.yellow('\n No assistant selected.\n'));
|
|
124
157
|
return;
|
|
@@ -143,20 +176,21 @@ export async function startChat(assistantName, assistant, initialCommand, option
|
|
|
143
176
|
: assistant.brain === 'gemini' ? '#8b5cf6'
|
|
144
177
|
: '#2563eb';
|
|
145
178
|
|
|
146
|
-
const
|
|
147
|
-
const
|
|
179
|
+
const colorsArr = assistant.colors;
|
|
180
|
+
const orbColorHex = (Array.isArray(colorsArr) && colorsArr[0]) || assistant.orbColor || brainColor;
|
|
148
181
|
|
|
149
182
|
console.clear();
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
183
|
+
|
|
184
|
+
const c = chalk.hex(brainColor);
|
|
185
|
+
console.log('');
|
|
186
|
+
console.log(c(' ●') + chalk.bold.white(` ${assistant.name}`));
|
|
187
|
+
console.log(chalk.gray(` ${brainLabel} | ${assistant.voiceLang || 'ko-KR'}`));
|
|
188
|
+
console.log('');
|
|
156
189
|
|
|
157
190
|
await bootSequence(assistant, brainLabel, brainColor);
|
|
158
191
|
|
|
159
|
-
const
|
|
192
|
+
const hasApiKey = !!(assistant.apiKey || process.env.GEMINI_API_KEY);
|
|
193
|
+
const voiceMode = options.voice !== undefined ? options.voice : hasApiKey;
|
|
160
194
|
|
|
161
195
|
console.log('');
|
|
162
196
|
console.log(chalk.gray(' ─────────────────────────────────────────'));
|
|
@@ -188,22 +222,27 @@ export async function startChat(assistantName, assistant, initialCommand, option
|
|
|
188
222
|
const ok = await voiceInput.start({
|
|
189
223
|
onListening: () => {
|
|
190
224
|
if (!processingVoice) {
|
|
191
|
-
process.stdout.write(chalk.gray(' 🎙 듣고 있습니다...
|
|
225
|
+
process.stdout.write('\x1b[2K\r' + chalk.gray(' 🎙 듣고 있습니다... (말하세요)'));
|
|
192
226
|
}
|
|
193
227
|
},
|
|
194
228
|
onSpeechStart: () => {
|
|
195
229
|
process.stdout.write('\x1b[2K\r');
|
|
196
|
-
process.stdout.write(chalk.cyan(' 🔴 음성 감지 중...') + '\r');
|
|
197
230
|
},
|
|
198
231
|
onSpeechEnd: () => {
|
|
199
|
-
process.stdout.write('\x1b[2K\r');
|
|
200
|
-
|
|
232
|
+
process.stdout.write('\x1b[2K\r' + chalk.yellow(' ⏳ 음성 인식 중...'));
|
|
233
|
+
},
|
|
234
|
+
onAmplitude: (amp, isSpeaking) => {
|
|
235
|
+
if (processingVoice) return;
|
|
236
|
+
if (isSpeaking) {
|
|
237
|
+
const bar = renderVoiceBar(amp);
|
|
238
|
+
process.stdout.write('\x1b[2K\r' + chalk.cyan(' 🔴 ') + chalk.hex(brainColor)(bar) + chalk.gray(' 말하는 중...'));
|
|
239
|
+
}
|
|
201
240
|
},
|
|
202
241
|
onTranscript: async (text) => {
|
|
203
242
|
if (processingVoice) return;
|
|
204
243
|
processingVoice = true;
|
|
205
244
|
process.stdout.write('\x1b[2K\r');
|
|
206
|
-
console.log(chalk.hex(brainColor)(` You
|
|
245
|
+
console.log(chalk.hex(brainColor)(` 🗣 You > `) + chalk.white(text));
|
|
207
246
|
rl.pause();
|
|
208
247
|
await processMessage(text, messages, assistant, brainColor);
|
|
209
248
|
processingVoice = false;
|
|
@@ -252,13 +291,18 @@ export async function startChat(assistantName, assistant, initialCommand, option
|
|
|
252
291
|
voiceInput = new VoiceInput(apiKey);
|
|
253
292
|
await voiceInput.start({
|
|
254
293
|
onListening: () => {},
|
|
255
|
-
onSpeechStart: () =>
|
|
256
|
-
onSpeechEnd: () => process.stdout.write(chalk.
|
|
294
|
+
onSpeechStart: () => {},
|
|
295
|
+
onSpeechEnd: () => process.stdout.write('\x1b[2K\r' + chalk.yellow(' ⏳ 인식 중...')),
|
|
296
|
+
onAmplitude: (amp, isSpeaking) => {
|
|
297
|
+
if (processingVoice || !isSpeaking) return;
|
|
298
|
+
const bar = renderVoiceBar(amp);
|
|
299
|
+
process.stdout.write('\x1b[2K\r' + chalk.cyan(' 🔴 ') + chalk.hex(brainColor)(bar));
|
|
300
|
+
},
|
|
257
301
|
onTranscript: async (text) => {
|
|
258
302
|
if (processingVoice) return;
|
|
259
303
|
processingVoice = true;
|
|
260
304
|
process.stdout.write('\x1b[2K\r');
|
|
261
|
-
console.log(chalk.hex(brainColor)(` You
|
|
305
|
+
console.log(chalk.hex(brainColor)(` 🗣 You > `) + chalk.white(text));
|
|
262
306
|
rl.pause();
|
|
263
307
|
await processMessage(text, messages, assistant, brainColor);
|
|
264
308
|
processingVoice = false;
|
|
@@ -297,6 +341,13 @@ export async function startChat(assistantName, assistant, initialCommand, option
|
|
|
297
341
|
});
|
|
298
342
|
}
|
|
299
343
|
|
|
344
|
+
function renderVoiceBar(amplitude) {
|
|
345
|
+
const barLen = Math.round(amplitude * 40);
|
|
346
|
+
const bar = '█'.repeat(Math.min(barLen, 40));
|
|
347
|
+
const empty = '░'.repeat(40 - Math.min(barLen, 40));
|
|
348
|
+
return bar + empty;
|
|
349
|
+
}
|
|
350
|
+
|
|
300
351
|
async function processMessage(input, messages, assistant, brainColor) {
|
|
301
352
|
const spinner = ora({
|
|
302
353
|
text: chalk.gray('Thinking...'),
|
|
@@ -304,6 +355,9 @@ async function processMessage(input, messages, assistant, brainColor) {
|
|
|
304
355
|
color: 'blue',
|
|
305
356
|
}).start();
|
|
306
357
|
|
|
358
|
+
const vLang = assistant.voiceLang || 'ko-KR';
|
|
359
|
+
const vGender = assistant.voiceGender || assistant.voiceStyle || 'male';
|
|
360
|
+
|
|
307
361
|
try {
|
|
308
362
|
const reply = await sendToBrain(assistant, messages, input);
|
|
309
363
|
spinner.stop();
|
|
@@ -315,12 +369,20 @@ async function processMessage(input, messages, assistant, brainColor) {
|
|
|
315
369
|
console.log(chalk.white(` ${line}`));
|
|
316
370
|
}
|
|
317
371
|
console.log('');
|
|
372
|
+
|
|
373
|
+
speak(reply, { lang: vLang, gender: vGender }).catch(() => {});
|
|
318
374
|
} catch (err) {
|
|
319
375
|
spinner.stop();
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
376
|
+
const rawMsg = err.message || '';
|
|
377
|
+
let friendlyMsg = rawMsg;
|
|
378
|
+
if (rawMsg.includes('API_KEY_INVALID') || rawMsg.includes('API key not valid')) {
|
|
379
|
+
friendlyMsg = 'API 키가 유효하지 않습니다. 비서 설정에서 올바른 API 키를 확인해주세요.';
|
|
380
|
+
} else if (rawMsg.includes('No API key')) {
|
|
381
|
+
friendlyMsg = 'API 키가 설정되지 않았습니다. 웹에서 비서의 API 키를 설정해주세요.';
|
|
382
|
+
}
|
|
383
|
+
console.log(chalk.red(`\n ❌ ${friendlyMsg}\n`));
|
|
384
|
+
if (rawMsg.includes('API key') || rawMsg.includes('api key') || rawMsg.includes('No API key')) {
|
|
385
|
+
console.log(chalk.gray(' API 키 설정: https://chartapp-653e1.web.app/assistant/create\n'));
|
|
324
386
|
}
|
|
325
387
|
}
|
|
326
388
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { writeFileSync, unlinkSync, existsSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
import fetch from 'node-fetch';
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
const platform = process.platform;
|
|
10
|
+
|
|
11
|
+
const GOOGLE_TTS_FUNCTION = 'https://us-central1-chartapp-653e1.cloudfunctions.net/googleTTS';
|
|
12
|
+
|
|
13
|
+
async function speakWithGoogleTTS(text, lang = 'ko-KR', gender = 'male') {
|
|
14
|
+
try {
|
|
15
|
+
const res = await fetch(GOOGLE_TTS_FUNCTION, {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: { 'Content-Type': 'application/json' },
|
|
18
|
+
body: JSON.stringify({ text, lang, gender }),
|
|
19
|
+
});
|
|
20
|
+
if (!res.ok) throw new Error(`TTS API ${res.status}`);
|
|
21
|
+
const data = await res.json();
|
|
22
|
+
if (!data.audio) throw new Error('No audio');
|
|
23
|
+
|
|
24
|
+
const tmpFile = join(tmpdir(), `uwonbot_tts_${Date.now()}.mp3`);
|
|
25
|
+
writeFileSync(tmpFile, Buffer.from(data.audio, 'base64'));
|
|
26
|
+
|
|
27
|
+
if (platform === 'darwin') {
|
|
28
|
+
await execAsync(`afplay "${tmpFile}"`);
|
|
29
|
+
} else if (platform === 'win32') {
|
|
30
|
+
await execAsync(`powershell -c "(New-Object Media.SoundPlayer '${tmpFile}').PlaySync()"`);
|
|
31
|
+
} else {
|
|
32
|
+
try {
|
|
33
|
+
await execAsync(`mpg123 "${tmpFile}" 2>/dev/null || ffplay -nodisp -autoexit "${tmpFile}" 2>/dev/null || aplay "${tmpFile}" 2>/dev/null`);
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try { unlinkSync(tmpFile); } catch {}
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function speakWithSay(text, lang = 'ko-KR', gender = 'male') {
|
|
45
|
+
if (platform !== 'darwin') return false;
|
|
46
|
+
|
|
47
|
+
let voice;
|
|
48
|
+
if (lang.startsWith('ko')) {
|
|
49
|
+
voice = gender === 'female' ? 'Yuna' : 'Jian';
|
|
50
|
+
} else {
|
|
51
|
+
voice = gender === 'female' ? 'Samantha' : 'Daniel';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
await execAsync(`say -v "${voice}" "${text.replace(/"/g, '\\"')}"`);
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
try {
|
|
59
|
+
await execAsync(`say "${text.replace(/"/g, '\\"')}"`);
|
|
60
|
+
return true;
|
|
61
|
+
} catch { return false; }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function speakWithEspeak(text, lang = 'ko-KR') {
|
|
66
|
+
if (platform === 'darwin') return false;
|
|
67
|
+
const espeakLang = lang.startsWith('ko') ? 'ko' : 'en';
|
|
68
|
+
try {
|
|
69
|
+
await execAsync(`espeak -v ${espeakLang} "${text.replace(/"/g, '\\"')}" 2>/dev/null`);
|
|
70
|
+
return true;
|
|
71
|
+
} catch { return false; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function speak(text, options = {}) {
|
|
75
|
+
const lang = options.lang || 'ko-KR';
|
|
76
|
+
const gender = options.gender || 'male';
|
|
77
|
+
|
|
78
|
+
if (!text || !text.trim()) return;
|
|
79
|
+
|
|
80
|
+
const ok = await speakWithGoogleTTS(text, lang, gender);
|
|
81
|
+
if (ok) return;
|
|
82
|
+
|
|
83
|
+
if (platform === 'darwin') {
|
|
84
|
+
await speakWithSay(text, lang, gender);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await speakWithEspeak(text, lang);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function speakStreaming(text, options = {}) {
|
|
92
|
+
const sentences = text.match(/[^.!?。!?\n]+[.!?。!?\n]?/g) || [text];
|
|
93
|
+
for (const s of sentences) {
|
|
94
|
+
const t = s.trim();
|
|
95
|
+
if (t) await speak(t, options);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export default { speak, speakStreaming };
|
package/src/voiceInput.js
CHANGED
|
@@ -78,11 +78,12 @@ export default class VoiceInput {
|
|
|
78
78
|
this.onSpeechEnd = null;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
async start({ onTranscript, onListening, onSpeechStart, onSpeechEnd }) {
|
|
81
|
+
async start({ onTranscript, onListening, onSpeechStart, onSpeechEnd, onAmplitude }) {
|
|
82
82
|
this.onTranscript = onTranscript;
|
|
83
83
|
this.onListening = onListening;
|
|
84
84
|
this.onSpeechStart = onSpeechStart;
|
|
85
85
|
this.onSpeechEnd = onSpeechEnd;
|
|
86
|
+
this.onAmplitude = onAmplitude;
|
|
86
87
|
|
|
87
88
|
let micModule;
|
|
88
89
|
try {
|
|
@@ -120,10 +121,14 @@ export default class VoiceInput {
|
|
|
120
121
|
|
|
121
122
|
this.onListening?.();
|
|
122
123
|
|
|
124
|
+
let speechDuration = 0;
|
|
125
|
+
|
|
123
126
|
stream.on('data', (buf) => {
|
|
124
127
|
if (!this.running) return;
|
|
125
128
|
const amp = getAmplitude(buf);
|
|
126
129
|
|
|
130
|
+
this.onAmplitude?.(amp, isSpeaking);
|
|
131
|
+
|
|
127
132
|
if (amp > SILENCE_THRESHOLD) {
|
|
128
133
|
if (!isSpeaking) {
|
|
129
134
|
isSpeaking = true;
|
|
@@ -133,6 +138,7 @@ export default class VoiceInput {
|
|
|
133
138
|
}
|
|
134
139
|
silenceStart = null;
|
|
135
140
|
speechChunks.push(Buffer.from(buf));
|
|
141
|
+
speechDuration = Date.now() - speechStart;
|
|
136
142
|
} else if (isSpeaking) {
|
|
137
143
|
speechChunks.push(Buffer.from(buf));
|
|
138
144
|
if (!silenceStart) silenceStart = Date.now();
|
|
@@ -141,6 +147,7 @@ export default class VoiceInput {
|
|
|
141
147
|
const duration = Date.now() - speechStart;
|
|
142
148
|
isSpeaking = false;
|
|
143
149
|
silenceStart = null;
|
|
150
|
+
speechDuration = 0;
|
|
144
151
|
this.onSpeechEnd?.();
|
|
145
152
|
|
|
146
153
|
if (duration >= MIN_SPEECH_MS && speechChunks.length > 0) {
|