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.
- package/lib/speech.js +2 -276
- 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
|
|
9
|
-
const
|
|
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.
|
|
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": "
|
|
33
|
+
"webtalk": "file:../webtalk",
|
|
34
34
|
"ws": "^8.14.2"
|
|
35
35
|
},
|
|
36
36
|
"overrides": {
|