@chatpanel/gateway 0.6.55 → 0.6.57
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 +1 -1
- package/src/config.js +7 -2
- package/src/ort.js +51 -0
- package/src/parakeet-engine.js +2 -15
- package/src/pocket-tts-engine.js +512 -0
- package/src/sentencepiece.js +184 -0
- package/src/server.js +103 -17
- package/src/tts-engine.js +85 -3
- package/src/tts-models.js +55 -2
- package/src/tts-voices.js +50 -4
package/src/tts-voices.js
CHANGED
|
@@ -25,6 +25,17 @@ import { randomUUID } from 'node:crypto';
|
|
|
25
25
|
import os from 'node:os';
|
|
26
26
|
|
|
27
27
|
export const EMBED_DIM = 512;
|
|
28
|
+
|
|
29
|
+
// A voice print is engine-specific: SpeechT5 wants a 512-d wavlm x-vector, Pocket
|
|
30
|
+
// TTS wants its Mimi encoder's [1, N, 1024] conditioning. They are NOT
|
|
31
|
+
// interchangeable, and the sample is discarded after saving, so whatever a voice
|
|
32
|
+
// might later need has to be computed WHILE the audio is still in hand.
|
|
33
|
+
//
|
|
34
|
+
// The pocket conditioning is ~100k floats — small on disk, absurd inside a JSON
|
|
35
|
+
// document that a settings page lists — so it lives beside the record as raw
|
|
36
|
+
// Float32 and only its shape is stored in the JSON.
|
|
37
|
+
export const KIND_SPEECHT5 = 'wavlm-512';
|
|
38
|
+
export const KIND_POCKET = 'pocket-mimi';
|
|
28
39
|
const MAX_VOICES = 20;
|
|
29
40
|
const MAX_NAME = 60;
|
|
30
41
|
|
|
@@ -62,22 +73,50 @@ export function listVoices() {
|
|
|
62
73
|
// The vector is deliberately NOT returned by the listing: the UI needs a
|
|
63
74
|
// name and an id, and 512 floats of someone's voice print have no business
|
|
64
75
|
// in a settings page's JSON.
|
|
65
|
-
if (v?.id && v?.name)
|
|
76
|
+
if (v?.id && v?.name) {
|
|
77
|
+
out.push({
|
|
78
|
+
id: v.id, name: v.name, createdAt: v.createdAt || 0, dim: v.vec?.length || 0,
|
|
79
|
+
kinds: v.pocketShape ? [KIND_SPEECHT5, KIND_POCKET] : [KIND_SPEECHT5],
|
|
80
|
+
});
|
|
81
|
+
}
|
|
66
82
|
} catch { /* a corrupt file must not break the list */ }
|
|
67
83
|
}
|
|
68
84
|
return out.sort((a, b) => b.createdAt - a.createdAt);
|
|
69
85
|
}
|
|
70
86
|
|
|
87
|
+
function pocketFileFor(id) {
|
|
88
|
+
return join(voicesDir(), `${id}.pocket.bin`);
|
|
89
|
+
}
|
|
90
|
+
|
|
71
91
|
export function getVoice(id) {
|
|
72
92
|
if (!isValidVoiceRef(id)) return null;
|
|
73
93
|
try {
|
|
74
94
|
const v = JSON.parse(readFileSync(fileFor(id), 'utf8'));
|
|
75
|
-
|
|
95
|
+
if (!Array.isArray(v?.vec) || v.vec.length !== EMBED_DIM) return null;
|
|
96
|
+
return v;
|
|
97
|
+
} catch { return null; }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The Pocket TTS conditioning for a voice, or null if it has none saved. */
|
|
101
|
+
export function getPocketVoice(id) {
|
|
102
|
+
if (!isValidVoiceRef(id)) return null;
|
|
103
|
+
const meta = getVoice(id);
|
|
104
|
+
if (!meta?.pocketShape) return null;
|
|
105
|
+
try {
|
|
106
|
+
const buf = readFileSync(pocketFileFor(id));
|
|
107
|
+
return { data: new Float32Array(buf.buffer, buf.byteOffset, Math.floor(buf.byteLength / 4)), shape: meta.pocketShape };
|
|
76
108
|
} catch { return null; }
|
|
77
109
|
}
|
|
78
110
|
|
|
111
|
+
/** Which engines can speak as this voice. Drives what the UI may offer. */
|
|
112
|
+
export function voiceKinds(id) {
|
|
113
|
+
const v = getVoice(id);
|
|
114
|
+
if (!v) return [];
|
|
115
|
+
return v.pocketShape ? [KIND_SPEECHT5, KIND_POCKET] : [KIND_SPEECHT5];
|
|
116
|
+
}
|
|
117
|
+
|
|
79
118
|
/** Persist an embedding under a user-chosen name. Returns the stored record. */
|
|
80
|
-
export function saveVoice({ name, vec }) {
|
|
119
|
+
export function saveVoice({ name, vec, pocket = null }) {
|
|
81
120
|
const clean = String(name || '').trim().slice(0, MAX_NAME);
|
|
82
121
|
if (!clean) throw new Error('a name is required');
|
|
83
122
|
if (!vec || vec.length !== EMBED_DIM) throw new Error(`expected a ${EMBED_DIM}-value embedding, got ${vec?.length || 0}`);
|
|
@@ -85,8 +124,13 @@ export function saveVoice({ name, vec }) {
|
|
|
85
124
|
const dir = voicesDir();
|
|
86
125
|
mkdirSync(dir, { recursive: true });
|
|
87
126
|
const rec = { id: randomUUID(), name: clean, createdAt: Date.now(), vec: Array.from(vec, (x) => Number(x) || 0) };
|
|
127
|
+
if (pocket?.data?.length && Array.isArray(pocket.shape)) {
|
|
128
|
+
rec.pocketShape = pocket.shape;
|
|
129
|
+
const f32 = pocket.data instanceof Float32Array ? pocket.data : Float32Array.from(pocket.data);
|
|
130
|
+
writeFileSync(pocketFileFor(rec.id), Buffer.from(f32.buffer, f32.byteOffset, f32.byteLength));
|
|
131
|
+
}
|
|
88
132
|
writeFileSync(fileFor(rec.id), JSON.stringify(rec));
|
|
89
|
-
return { id: rec.id, name: rec.name, createdAt: rec.createdAt, dim: rec.vec.length };
|
|
133
|
+
return { id: rec.id, name: rec.name, createdAt: rec.createdAt, dim: rec.vec.length, kinds: rec.pocketShape ? [KIND_SPEECHT5, KIND_POCKET] : [KIND_SPEECHT5] };
|
|
90
134
|
}
|
|
91
135
|
|
|
92
136
|
/** Remove one permanently. Returns whether there was anything to remove. */
|
|
@@ -95,5 +139,7 @@ export function deleteVoice(id) {
|
|
|
95
139
|
const p = fileFor(id);
|
|
96
140
|
if (!existsSync(p)) return false;
|
|
97
141
|
rmSync(p, { force: true });
|
|
142
|
+
// Both halves, or the conditioning outlives the record that named it.
|
|
143
|
+
rmSync(pocketFileFor(id), { force: true });
|
|
98
144
|
return true;
|
|
99
145
|
}
|