agentgui 1.0.301 → 1.0.302

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.
Files changed (2) hide show
  1. package/lib/speech.js +2 -276
  2. package/package.json +2 -2
package/lib/speech.js CHANGED
@@ -1,278 +1,4 @@
1
1
  import { createRequire } from 'module';
2
- import fs from 'fs';
3
- import path from 'path';
4
- import os from 'os';
5
- import { fileURLToPath } from 'url';
6
-
7
2
  const require = createRequire(import.meta.url);
8
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
- const ROOT = path.dirname(__dirname);
10
-
11
- // Load modules
12
- let serverTTS = null;
13
- let serverSTT = null;
14
- let audioDecode = null;
15
- let ttsUtils = null;
16
-
17
- try { serverTTS = require('webtalk/server-tts'); } catch(e) { console.warn('[TTS] webtalk/server-tts unavailable:', e.message); }
18
- try { serverSTT = require('webtalk/server-stt'); } catch(e) { console.warn('[STT] webtalk/server-stt unavailable:', e.message); }
19
- try { audioDecode = require('audio-decode'); } catch(e) { console.warn('[TTS] audio-decode unavailable:', e.message); }
20
- try { ttsUtils = require('webtalk/tts-utils'); } catch(e) {}
21
-
22
- // Detect webtalk API type: old (server-tts.js with getVoices/synthesizeViaPocket)
23
- // vs new ONNX (server-tts-onnx.js with encodeVoiceAudio)
24
- const isOnnxApi = serverTTS && typeof serverTTS.encodeVoiceAudio === 'function';
25
- const isPocketApi = serverTTS && typeof serverTTS.getVoices === 'function';
26
-
27
- // Voice directories to scan
28
- const VOICE_DIRS = [
29
- path.join(os.homedir(), 'voices'),
30
- path.join(ROOT, 'voices'),
31
- '/config/voices',
32
- ];
33
-
34
- const AUDIO_EXTENSIONS = ['.wav', '.mp3', '.ogg', '.flac', '.m4a'];
35
-
36
- const POCKET_TTS_VOICES = [
37
- { id: 'default', name: 'Default', gender: 'female', accent: 'French' },
38
- { id: 'alba', name: 'Alba', gender: 'female', accent: 'French' },
39
- { id: 'marius', name: 'Marius', gender: 'male', accent: 'French' },
40
- { id: 'javert', name: 'Javert', gender: 'male', accent: 'French' },
41
- { id: 'jean', name: 'Jean', gender: 'male', accent: 'French' },
42
- { id: 'fantine', name: 'Fantine', gender: 'female', accent: 'French' },
43
- { id: 'cosette', name: 'Cosette', gender: 'female', accent: 'French' },
44
- { id: 'eponine', name: 'Eponine', gender: 'female', accent: 'French' },
45
- { id: 'azelma', name: 'Azelma', gender: 'female', accent: 'French' },
46
- ];
47
-
48
- const SAMPLE_RATE = 24000;
49
-
50
- // Embedding cache: voiceId -> {data, shape}
51
- const voiceEmbeddingCache = new Map();
52
-
53
- function getModelDir() {
54
- return path.join(os.homedir(), '.gmgui', 'models', 'tts');
55
- }
56
-
57
- function findVoiceFile(voiceId) {
58
- if (!voiceId || voiceId === 'default') return null;
59
- const baseName = voiceId.replace(/^custom_/, '');
60
- for (const dir of VOICE_DIRS) {
61
- for (const ext of AUDIO_EXTENSIONS) {
62
- const p = path.join(dir, baseName + ext);
63
- if (fs.existsSync(p)) return p;
64
- }
65
- }
66
- return null;
67
- }
68
-
69
- function scanVoiceDir(dir) {
70
- const voices = [];
71
- try {
72
- if (!fs.existsSync(dir)) return voices;
73
- const seen = new Set();
74
- for (const file of fs.readdirSync(dir)) {
75
- const ext = path.extname(file).toLowerCase();
76
- if (!AUDIO_EXTENSIONS.includes(ext)) continue;
77
- const baseName = path.basename(file, ext);
78
- if (seen.has(baseName)) continue;
79
- seen.add(baseName);
80
- voices.push({
81
- id: 'custom_' + baseName.replace(/[^a-zA-Z0-9_-]/g, '_'),
82
- name: baseName.replace(/_/g, ' '),
83
- gender: 'custom', accent: 'custom', isCustom: true,
84
- });
85
- }
86
- } catch (_) {}
87
- return voices;
88
- }
89
-
90
- // Encode a voice WAV file to an ONNX voice embedding
91
- async function getVoiceEmbedding(voiceId) {
92
- if (voiceEmbeddingCache.has(voiceId)) return voiceEmbeddingCache.get(voiceId);
93
- const voicePath = findVoiceFile(voiceId);
94
- if (!voicePath) return null;
95
- if (!audioDecode || !serverTTS || !isOnnxApi) return null;
96
-
97
- const modelDir = getModelDir();
98
- if (serverTTS.loadModels) await serverTTS.loadModels(modelDir);
99
-
100
- const raw = fs.readFileSync(voicePath);
101
- const decoded = await audioDecode.default(raw);
102
- let pcm = decoded.getChannelData(0);
103
- if (decoded.sampleRate !== SAMPLE_RATE) {
104
- pcm = ttsUtils ? ttsUtils.resample(pcm, decoded.sampleRate, SAMPLE_RATE)
105
- : (() => {
106
- const ratio = decoded.sampleRate / SAMPLE_RATE;
107
- const out = new Float32Array(Math.round(pcm.length / ratio));
108
- for (let i = 0; i < out.length; i++) out[i] = pcm[Math.floor(i * ratio)];
109
- return out;
110
- })();
111
- }
112
-
113
- const embedding = await serverTTS.encodeVoiceAudio(pcm);
114
- voiceEmbeddingCache.set(voiceId, embedding);
115
- return embedding;
116
- }
117
-
118
- // Convert Float32Array PCM to WAV buffer
119
- function pcmToWav(samples, sampleRate = SAMPLE_RATE) {
120
- const numSamples = samples.length;
121
- const numChannels = 1;
122
- const bitsPerSample = 16;
123
- const byteRate = sampleRate * numChannels * bitsPerSample / 8;
124
- const blockAlign = numChannels * bitsPerSample / 8;
125
- const dataSize = numSamples * blockAlign;
126
- const buf = Buffer.alloc(44 + dataSize);
127
-
128
- buf.write('RIFF', 0); buf.writeUInt32LE(36 + dataSize, 4);
129
- buf.write('WAVE', 8); buf.write('fmt ', 12);
130
- buf.writeUInt32LE(16, 16); buf.writeUInt16LE(1, 20);
131
- buf.writeUInt16LE(numChannels, 22); buf.writeUInt32LE(sampleRate, 24);
132
- buf.writeUInt32LE(byteRate, 28); buf.writeUInt16LE(blockAlign, 32);
133
- buf.writeUInt16LE(bitsPerSample, 34); buf.write('data', 36);
134
- buf.writeUInt32LE(dataSize, 40);
135
-
136
- for (let i = 0; i < numSamples; i++) {
137
- const s = Math.max(-1, Math.min(1, samples[i]));
138
- buf.writeInt16LE(Math.round(s * 32767), 44 + i * 2);
139
- }
140
- return buf;
141
- }
142
-
143
- function getSttOptions() {
144
- if (process.env.PORTABLE_EXE_DIR) {
145
- return { cacheDir: path.join(process.env.PORTABLE_EXE_DIR, 'models') };
146
- }
147
- if (process.env.PORTABLE_DATA_DIR) {
148
- return { cacheDir: path.join(process.env.PORTABLE_DATA_DIR, 'models') };
149
- }
150
- return {};
151
- }
152
-
153
- async function getEmbeddingForVoice(voiceId) {
154
- if (voiceId && voiceId !== 'default') {
155
- const emb = await getVoiceEmbedding(voiceId);
156
- if (emb) return emb;
157
- }
158
- // Fall back to first available voice file
159
- for (const dir of VOICE_DIRS) {
160
- for (const ext of AUDIO_EXTENSIONS) {
161
- const entries = fs.existsSync(dir) ? fs.readdirSync(dir).filter(f => f.endsWith(ext)) : [];
162
- if (entries.length) {
163
- const emb = await getVoiceEmbedding('custom_' + entries[0].replace(new RegExp(`\\${ext}$`), ''));
164
- if (emb) return emb;
165
- }
166
- }
167
- }
168
- return null;
169
- }
170
-
171
- async function synthesize(text, voiceId) {
172
- if (isOnnxApi) {
173
- // Node.js ONNX TTS - no Python required
174
- const modelDir = getModelDir();
175
- const embedding = await getEmbeddingForVoice(voiceId);
176
- if (!embedding) throw new Error('No voice file available for TTS - add a WAV file to ~/voices/');
177
- const pcm = await serverTTS.synthesize(text, embedding, modelDir);
178
- return pcmToWav(pcm);
179
- }
180
-
181
- if (isPocketApi) {
182
- // Old server-tts.js with pocket-tts sidecar
183
- return serverTTS.synthesize(text, voiceId, VOICE_DIRS);
184
- }
185
-
186
- throw new Error('No TTS backend available');
187
- }
188
-
189
- async function* synthesizeStream(text, voiceId) {
190
- if (isOnnxApi) {
191
- const modelDir = getModelDir();
192
- const embedding = await getEmbeddingForVoice(voiceId);
193
- if (!embedding) throw new Error('No voice file available for TTS - add a WAV file to ~/voices/');
194
- const pcm = await serverTTS.synthesize(text, embedding, modelDir);
195
- yield pcmToWav(pcm);
196
- return;
197
- }
198
-
199
- if (isPocketApi) {
200
- for await (const chunk of serverTTS.synthesizeStream(text, voiceId, VOICE_DIRS)) {
201
- yield chunk;
202
- }
203
- return;
204
- }
205
-
206
- throw new Error('No TTS backend available');
207
- }
208
-
209
- function transcribe(audioBuffer) {
210
- if (!serverSTT) throw new Error('STT not available');
211
- return serverSTT.transcribe(audioBuffer, getSttOptions());
212
- }
213
-
214
- function getSTT() {
215
- if (!serverSTT) throw new Error('STT not available');
216
- return serverSTT.getSTT(getSttOptions());
217
- }
218
-
219
- function getVoices() {
220
- const seen = new Set();
221
- const custom = [];
222
- for (const dir of VOICE_DIRS) {
223
- for (const v of scanVoiceDir(dir)) {
224
- if (seen.has(v.id)) continue;
225
- seen.add(v.id);
226
- custom.push(v);
227
- }
228
- }
229
- // Include built-in voices from old server-tts if available
230
- if (isPocketApi) {
231
- const upstream = serverTTS.getVoices(VOICE_DIRS).filter(v => v.isCustom);
232
- for (const v of upstream) {
233
- if (!seen.has(v.id)) { seen.add(v.id); custom.push(v); }
234
- }
235
- }
236
- return [...POCKET_TTS_VOICES, ...custom];
237
- }
238
-
239
- function getStatus() {
240
- const sttStatus = serverSTT ? serverSTT.getStatus() : { ready: false, loading: false, error: 'STT unavailable' };
241
- const ttsBackend = isOnnxApi ? 'onnx-node' : isPocketApi ? 'pocket-tts' : 'none';
242
- return {
243
- sttReady: sttStatus.ready,
244
- ttsReady: isOnnxApi || isPocketApi,
245
- sttLoading: sttStatus.loading,
246
- ttsLoading: false,
247
- sttError: sttStatus.error,
248
- ttsError: (!isOnnxApi && !isPocketApi) ? 'No TTS backend available' : null,
249
- ttsBackend,
250
- };
251
- }
252
-
253
- function preloadTTS() {
254
- if (isOnnxApi) {
255
- // Pre-load ONNX models in background
256
- const modelDir = getModelDir();
257
- if (serverTTS.loadModels) {
258
- serverTTS.loadModels(modelDir).catch(e => console.warn('[TTS] ONNX preload failed:', e.message));
259
- }
260
- } else if (isPocketApi && serverTTS.preload) {
261
- serverTTS.preload(null, {});
262
- }
263
- }
264
-
265
- function ttsCacheKey(text, voiceId) {
266
- return isPocketApi && serverTTS.ttsCacheKey ? serverTTS.ttsCacheKey(text, voiceId) : null;
267
- }
268
-
269
- function ttsCacheGet(key) {
270
- return isPocketApi && serverTTS.ttsCacheGet ? serverTTS.ttsCacheGet(key) : null;
271
- }
272
-
273
- function splitSentences(text) {
274
- if (isPocketApi && serverTTS.splitSentences) return serverTTS.splitSentences(text);
275
- return text.match(/[^.!?]+[.!?]*/g)?.map(s => s.trim()).filter(Boolean) || [text];
276
- }
277
-
278
- export { transcribe, synthesize, synthesizeStream, getSTT, getStatus, getVoices, preloadTTS, ttsCacheKey, ttsCacheGet, splitSentences };
3
+ const speech = require('webtalk/speech');
4
+ export const { transcribe, synthesize, synthesizeStream, getSTT, getStatus, getVoices, preloadTTS, ttsCacheKey, ttsCacheGet, splitSentences } = speech;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.301",
3
+ "version": "1.0.302",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -30,7 +30,7 @@
30
30
  "fsbrowse": "^0.2.18",
31
31
  "google-auth-library": "^10.5.0",
32
32
  "onnxruntime-node": "^1.24.1",
33
- "webtalk": "^1.0.17",
33
+ "webtalk": "file:../webtalk",
34
34
  "ws": "^8.14.2"
35
35
  },
36
36
  "overrides": {