@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/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) out.push({ id: v.id, name: v.name, createdAt: v.createdAt || 0, dim: v.vec?.length || 0 });
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
- return Array.isArray(v?.vec) && v.vec.length === EMBED_DIM ? v : null;
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
  }