@jskit-ai/assistant-voice-sherpa 0.1.7 → 0.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/README.md CHANGED
@@ -75,6 +75,33 @@ Heart, Bella and Emma voices, and male Michael, Adam, George and Daniel voices.
75
75
  The service advertises these IDs/labels; the browser selects a speaker per reply.
76
76
  Model weights are not included in the npm artifact.
77
77
 
78
+ `--pack kokoro-q8f16` offers all 28 American/British English Kokoro voices with
79
+ `model_q8f16.onnx` from the pinned Kokoro 1.0 ONNX Community revision. The 86 MB
80
+ mixed int8/fp16 file is SHA-256 checked before staging. Preparation appends only
81
+ Sherpa's required ONNX metadata; every original graph/weight byte remains intact.
82
+ The installed file keeps its `model_q8f16.onnx` name and its prepared hash is
83
+ recorded in the existing integrity manifest. The pack retains Sherpa's pinned
84
+ voice table, tokenizer, lexicons, eSpeak data and licenses. It does not load the
85
+ full-size Kokoro model. All these English voices share one synthesis worker at
86
+ 24,000 Hz; voice selection uses the same settings catalogue and per-reply ID.
87
+
88
+ The preset ships a CPU provider configuration disabling only ONNX Runtime's
89
+ `NchwcTransformer`, which crashes while loading this mixed-precision graph on
90
+ x64 with the pinned native runtime. Other graph optimizations remain enabled.
91
+ Do not remove this configuration when supplying a custom `serve --config` file.
92
+ Measure latency on your server: smaller weights do not guarantee faster CPU
93
+ inference or real-time speech. Capture, duplex admission, playback and cancellation
94
+ continue through the existing controller and synthesis process.
95
+
96
+ `--pack piper-kitten-kokoro-q8f16` keeps all five Piper voices and eight Kitten
97
+ voices, then adds those 28 Kokoro voices to one catalogue (41 choices). It retains
98
+ Piper's Cori default, existing voice IDs and Kitten Bella's 1.15× speed. Kokoro's
99
+ files and CPU configuration live beneath `tts/kokoro-q8f16`; Piper and Kitten
100
+ files are never overwritten. The existing single synthesis process switches
101
+ models between replies and releases the previous model before loading another.
102
+ Output keeps the existing combined pack's 22,050 Hz rate through the same
103
+ resampler. The standalone presets remain available.
104
+
78
105
  `--pack kitten` offers **Bella, Luna, Rosie and Kiki** (female) and
79
106
  **Jasper, Bruno, Hugo and Leo** (male) from Kitten TTS Micro 0.8. All eight share
80
107
  one model and retain its native 24,000 Hz output. Bella uses a 1.15× speech speed;
@@ -137,6 +164,13 @@ voice does not redirect an in-flight reply or another conversation.
137
164
  For other models, `prepare --sources-file FILE` accepts the source manifest shape
138
165
  shown in `models/voice-models-piper.json`: recognition and synthesis archives, pinned
139
166
  SHA-256 values, explicit source/destination file mappings and native configuration.
167
+ A source entry's optional `format: "file"` stages its pinned download without
168
+ archive extraction; the default is `tar.bz2`. `archive`/`archiveSha256` still name
169
+ and authenticate that download. The optional `onnxMetadata` object maps staged
170
+ `.onnx` paths to string metadata entries, appended after the unchanged source
171
+ bytes. `textFiles` maps staged paths to literal native configuration text;
172
+ it cannot overwrite a mapped source file. These outputs are covered by the
173
+ same prepared-file integrity manifest and atomic replacement as archived models.
140
174
  Paths in mappings must stay inside their extracted/staged roots. Model compatibility
141
175
  still needs native inference validation; a matching hash is only integrity proof.
142
176
 
@@ -0,0 +1,254 @@
1
+ {
2
+ "schema": "vibe64.voice-model-sources.v1",
3
+ "packRevision": 1,
4
+ "models": [
5
+ {
6
+ "id": "streaming-zipformer-en-2023-06-26",
7
+ "kind": "stt",
8
+ "archive": "sherpa-onnx-streaming-zipformer-en-2023-06-26.tar.bz2",
9
+ "archiveSha256": "639e25b578e9e997131402199419c13a941f8e4e198e2da1ce57dbf5cf401282",
10
+ "url": "https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-en-2023-06-26.tar.bz2",
11
+ "license": "Apache-2.0",
12
+ "source": "https://huggingface.co/csukuangfj/sherpa-onnx-streaming-zipformer-en-2023-06-26",
13
+ "files": {
14
+ "sherpa-onnx-streaming-zipformer-en-2023-06-26/encoder-epoch-99-avg-1-chunk-16-left-128.int8.onnx": "stt/encoder.int8.onnx",
15
+ "sherpa-onnx-streaming-zipformer-en-2023-06-26/decoder-epoch-99-avg-1-chunk-16-left-128.int8.onnx": "stt/decoder.int8.onnx",
16
+ "sherpa-onnx-streaming-zipformer-en-2023-06-26/joiner-epoch-99-avg-1-chunk-16-left-128.int8.onnx": "stt/joiner.int8.onnx",
17
+ "sherpa-onnx-streaming-zipformer-en-2023-06-26/tokens.txt": "stt/tokens.txt",
18
+ "sherpa-onnx-streaming-zipformer-en-2023-06-26/README.md": "stt/SOURCE.md"
19
+ }
20
+ },
21
+ {
22
+ "id": "kokoro-multi-lang-v1_0",
23
+ "kind": "tts",
24
+ "archive": "kokoro-multi-lang-v1_0.tar.bz2",
25
+ "archiveSha256": "c5f7e2d2caf082bc1d20fb70334a61d99d20b484500aad32e7cf84c128ea3298",
26
+ "url": "https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/kokoro-multi-lang-v1_0.tar.bz2",
27
+ "license": "Apache-2.0",
28
+ "source": "https://huggingface.co/hexgrad/Kokoro-82M",
29
+ "files": {
30
+ "kokoro-multi-lang-v1_0/tokens.txt": "tts/kokoro-q8f16/tokens.txt",
31
+ "kokoro-multi-lang-v1_0/voices.bin": "tts/kokoro-q8f16/voices.bin",
32
+ "kokoro-multi-lang-v1_0/espeak-ng-data": "tts/kokoro-q8f16/espeak-ng-data",
33
+ "kokoro-multi-lang-v1_0/lexicon-us-en.txt": "tts/kokoro-q8f16/lexicon-us-en.txt",
34
+ "kokoro-multi-lang-v1_0/lexicon-gb-en.txt": "tts/kokoro-q8f16/lexicon-gb-en.txt",
35
+ "kokoro-multi-lang-v1_0/LICENSE": "tts/kokoro-q8f16/LICENSE",
36
+ "kokoro-multi-lang-v1_0/README.md": "tts/kokoro-q8f16/README.md"
37
+ }
38
+ },
39
+ {
40
+ "id": "kokoro-v1_0-q8f16",
41
+ "kind": "tts",
42
+ "format": "file",
43
+ "archive": "model_q8f16.onnx",
44
+ "archiveSha256": "04c658aec1b6008857c2ad10f8c589d4180d0ec427e7e6118ceb487e215c3cd0",
45
+ "url": "https://huggingface.co/onnx-community/Kokoro-82M-v1.0-ONNX/resolve/1939ad2a8e416c0acfeecc08a694d14ef25f2231/onnx/model_q8f16.onnx",
46
+ "license": "Apache-2.0",
47
+ "source": "https://huggingface.co/onnx-community/Kokoro-82M-v1.0-ONNX",
48
+ "files": {
49
+ "model_q8f16.onnx": "tts/kokoro-q8f16/model_q8f16.onnx"
50
+ }
51
+ }
52
+ ],
53
+ "configuration": {
54
+ "synthesizer": {
55
+ "maxNumSentences": 1,
56
+ "model": {
57
+ "numThreads": 1,
58
+ "provider": "cpu:${MODELS_ROOT}/tts/kokoro-q8f16/kokoro-cpu.conf",
59
+ "kokoro": {
60
+ "model": "${MODELS_ROOT}/tts/kokoro-q8f16/model_q8f16.onnx",
61
+ "tokens": "${MODELS_ROOT}/tts/kokoro-q8f16/tokens.txt",
62
+ "voices": "${MODELS_ROOT}/tts/kokoro-q8f16/voices.bin",
63
+ "dataDir": "${MODELS_ROOT}/tts/kokoro-q8f16/espeak-ng-data",
64
+ "lexicon": "${MODELS_ROOT}/tts/kokoro-q8f16/lexicon-us-en.txt"
65
+ }
66
+ }
67
+ },
68
+ "defaultVoice": "af_heart",
69
+ "voices": [
70
+ {
71
+ "id": "af_alloy",
72
+ "label": "Alloy · female · American English",
73
+ "language": "en-US",
74
+ "speakerId": 0
75
+ },
76
+ {
77
+ "id": "af_aoede",
78
+ "label": "Aoede · female · American English",
79
+ "language": "en-US",
80
+ "speakerId": 1
81
+ },
82
+ {
83
+ "id": "af_bella",
84
+ "label": "Bella · female · American English",
85
+ "language": "en-US",
86
+ "speakerId": 2
87
+ },
88
+ {
89
+ "id": "af_heart",
90
+ "label": "Heart · female · American English",
91
+ "language": "en-US",
92
+ "speakerId": 3
93
+ },
94
+ {
95
+ "id": "af_jessica",
96
+ "label": "Jessica · female · American English",
97
+ "language": "en-US",
98
+ "speakerId": 4
99
+ },
100
+ {
101
+ "id": "af_kore",
102
+ "label": "Kore · female · American English",
103
+ "language": "en-US",
104
+ "speakerId": 5
105
+ },
106
+ {
107
+ "id": "af_nicole",
108
+ "label": "Nicole · female · American English",
109
+ "language": "en-US",
110
+ "speakerId": 6
111
+ },
112
+ {
113
+ "id": "af_nova",
114
+ "label": "Nova · female · American English",
115
+ "language": "en-US",
116
+ "speakerId": 7
117
+ },
118
+ {
119
+ "id": "af_river",
120
+ "label": "River · female · American English",
121
+ "language": "en-US",
122
+ "speakerId": 8
123
+ },
124
+ {
125
+ "id": "af_sarah",
126
+ "label": "Sarah · female · American English",
127
+ "language": "en-US",
128
+ "speakerId": 9
129
+ },
130
+ {
131
+ "id": "af_sky",
132
+ "label": "Sky · female · American English",
133
+ "language": "en-US",
134
+ "speakerId": 10
135
+ },
136
+ {
137
+ "id": "am_adam",
138
+ "label": "Adam · male · American English",
139
+ "language": "en-US",
140
+ "speakerId": 11
141
+ },
142
+ {
143
+ "id": "am_echo",
144
+ "label": "Echo · male · American English",
145
+ "language": "en-US",
146
+ "speakerId": 12
147
+ },
148
+ {
149
+ "id": "am_eric",
150
+ "label": "Eric · male · American English",
151
+ "language": "en-US",
152
+ "speakerId": 13
153
+ },
154
+ {
155
+ "id": "am_fenrir",
156
+ "label": "Fenrir · male · American English",
157
+ "language": "en-US",
158
+ "speakerId": 14
159
+ },
160
+ {
161
+ "id": "am_liam",
162
+ "label": "Liam · male · American English",
163
+ "language": "en-US",
164
+ "speakerId": 15
165
+ },
166
+ {
167
+ "id": "am_michael",
168
+ "label": "Michael · male · American English",
169
+ "language": "en-US",
170
+ "speakerId": 16
171
+ },
172
+ {
173
+ "id": "am_onyx",
174
+ "label": "Onyx · male · American English",
175
+ "language": "en-US",
176
+ "speakerId": 17
177
+ },
178
+ {
179
+ "id": "am_puck",
180
+ "label": "Puck · male · American English",
181
+ "language": "en-US",
182
+ "speakerId": 18
183
+ },
184
+ {
185
+ "id": "am_santa",
186
+ "label": "Santa · male · American English",
187
+ "language": "en-US",
188
+ "speakerId": 19
189
+ },
190
+ {
191
+ "id": "bf_alice",
192
+ "label": "Alice · female · British English",
193
+ "language": "en-GB",
194
+ "speakerId": 20
195
+ },
196
+ {
197
+ "id": "bf_emma",
198
+ "label": "Emma · female · British English",
199
+ "language": "en-GB",
200
+ "speakerId": 21
201
+ },
202
+ {
203
+ "id": "bf_isabella",
204
+ "label": "Isabella · female · British English",
205
+ "language": "en-GB",
206
+ "speakerId": 22
207
+ },
208
+ {
209
+ "id": "bf_lily",
210
+ "label": "Lily · female · British English",
211
+ "language": "en-GB",
212
+ "speakerId": 23
213
+ },
214
+ {
215
+ "id": "bm_daniel",
216
+ "label": "Daniel · male · British English",
217
+ "language": "en-GB",
218
+ "speakerId": 24
219
+ },
220
+ {
221
+ "id": "bm_fable",
222
+ "label": "Fable · male · British English",
223
+ "language": "en-GB",
224
+ "speakerId": 25
225
+ },
226
+ {
227
+ "id": "bm_george",
228
+ "label": "George · male · British English",
229
+ "language": "en-GB",
230
+ "speakerId": 26
231
+ },
232
+ {
233
+ "id": "bm_lewis",
234
+ "label": "Lewis · male · British English",
235
+ "language": "en-GB",
236
+ "speakerId": 27
237
+ }
238
+ ]
239
+ },
240
+ "onnxMetadata": {
241
+ "tts/kokoro-q8f16/model_q8f16.onnx": {
242
+ "model_type": "kokoro",
243
+ "sample_rate": "24000",
244
+ "version": "2",
245
+ "n_speakers": "54",
246
+ "has_espeak": "1",
247
+ "voice": "en-us",
248
+ "style_dim": "510,1,256"
249
+ }
250
+ },
251
+ "textFiles": {
252
+ "tts/kokoro-q8f16/kokoro-cpu.conf": "SessionConfig.optimization.disable_specified_optimizers=NchwcTransformer\n"
253
+ }
254
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/assistant-voice-sherpa",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "description": "Optional native Sherpa speech engine and explicit model preparation for assistant voice.",
6
6
  "scripts": {
@@ -19,7 +19,7 @@
19
19
  "tooling"
20
20
  ],
21
21
  "dependencies": {
22
- "@jskit-ai/assistant-voice": "0.1.7",
22
+ "@jskit-ai/assistant-voice": "0.1.8",
23
23
  "sherpa-onnx-node": "1.13.6"
24
24
  },
25
25
  "jskit": {
package/src/cli.js CHANGED
@@ -50,7 +50,7 @@ function usage() {
50
50
  "",
51
51
  "Commands:",
52
52
  " serve --models-root PATH --key-file PATH [--host ADDRESS] [--port NUMBER] [--config FILE]",
53
- " prepare --models-root PATH [--pack cori|piper|kitten|piper-kitten|kokoro] [--sources-file FILE] [--download-cache PATH] [--hotwords-file PATH]",
53
+ " prepare --models-root PATH [--pack cori|piper|kitten|piper-kitten|kokoro|kokoro-q8f16|piper-kitten-kokoro-q8f16] [--sources-file FILE] [--download-cache PATH] [--hotwords-file PATH]",
54
54
  " verify --models-root PATH",
55
55
  " token --key-file PATH --tenant NAME"
56
56
  ].join("\n");
@@ -77,17 +77,25 @@ async function runVoiceCli(argv = process.argv.slice(2), { createDaemon = create
77
77
  "retain-downloads": "RETAIN_DOWNLOADS", "hotwords-file": "HOTWORDS_FILE"
78
78
  })) if (options[option]) env[`JSKIT_VOICE_${variable}`] = options[option];
79
79
  const pack = options.pack || process.env.JSKIT_VOICE_PACK || "cori";
80
- const manifests = { cori: "voice-models.json", piper: "voice-models-piper.json", kitten: "voice-models-kitten.json", kokoro: "voice-models-kokoro.json" };
81
- if (pack !== "piper-kitten" && !Object.hasOwn(manifests, pack)) throw new Error("Choose --pack cori, piper, kitten, piper-kitten or kokoro, or supply --sources-file for your own model pack.");
80
+ const manifests = { cori: "voice-models.json", piper: "voice-models-piper.json", kitten: "voice-models-kitten.json", kokoro: "voice-models-kokoro.json", "kokoro-q8f16": "voice-models-kokoro-q8f16.json" };
81
+ if (pack !== "piper-kitten" && pack !== "piper-kitten-kokoro-q8f16" && !Object.hasOwn(manifests, pack)) throw new Error("Choose --pack cori, piper, kitten, piper-kitten, kokoro, kokoro-q8f16 or piper-kitten-kokoro-q8f16, or supply --sources-file for your own model pack.");
82
82
  let temporary;
83
83
  try {
84
84
  env.JSKIT_VOICE_SOURCES_FILE = options["sources-file"] || process.env.JSKIT_VOICE_SOURCES_FILE;
85
- if (!env.JSKIT_VOICE_SOURCES_FILE && pack === "piper-kitten") {
85
+ if (!env.JSKIT_VOICE_SOURCES_FILE && (pack === "piper-kitten" || pack === "piper-kitten-kokoro-q8f16")) {
86
86
  const piper = JSON.parse(await readFile(new URL("../models/voice-models-piper.json", import.meta.url), "utf8"));
87
87
  const kitten = JSON.parse(await readFile(new URL("../models/voice-models-kitten.json", import.meta.url), "utf8"));
88
88
  piper.models.push(...kitten.models.filter(model => model.kind === "tts"));
89
89
  piper.configuration.voices.unshift(...kitten.configuration.voices);
90
90
  Object.assign(piper.configuration.synthesizers, kitten.configuration.synthesizers);
91
+ if (pack === "piper-kitten-kokoro-q8f16") {
92
+ const kokoro = JSON.parse(await readFile(new URL("../models/voice-models-kokoro-q8f16.json", import.meta.url), "utf8"));
93
+ piper.models.push(...kokoro.models.filter(model => model.kind === "tts"));
94
+ piper.configuration.voices.push(...kokoro.configuration.voices.map(voice => ({ ...voice, modelId: "kokoro" })));
95
+ piper.configuration.synthesizers.kokoro = kokoro.configuration.synthesizer;
96
+ piper.onnxMetadata = kokoro.onnxMetadata;
97
+ piper.textFiles = kokoro.textFiles;
98
+ }
91
99
  piper.configuration.outputSampleRate = 22050;
92
100
  temporary = await mkdtemp(path.join(os.tmpdir(), "jskit-voice-sources-"));
93
101
  env.JSKIT_VOICE_SOURCES_FILE = path.join(temporary, "sources.json");
@@ -58,9 +58,11 @@ read_model_field() {
58
58
  const fs = require("node:fs");
59
59
  const manifest = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
60
60
  const model = manifest.models.find((entry) => entry.id === process.argv[2]);
61
- if (!model || !model[process.argv[3]]) process.exit(2);
62
- process.stdout.write(String(model[process.argv[3]]));
63
- ' "$source_manifest" "$model_id" "$field"
61
+ if (!model) process.exit(2);
62
+ const value = model[process.argv[3]] ?? process.argv[4];
63
+ if (!value) process.exit(2);
64
+ process.stdout.write(String(value));
65
+ ' "$source_manifest" "$model_id" "$field" "${3:-}"
64
66
  }
65
67
 
66
68
  model_ids="$("$node_bin" -e 'console.log(JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8")).models.map(model => model.id).join("\n"))' "$source_manifest")"
@@ -125,7 +127,11 @@ archives=()
125
127
  while IFS= read -r model_id; do
126
128
  archive="$(fetch_archive "$model_id")"
127
129
  archives+=("$archive")
128
- tar --no-same-owner -xjf "$archive" -C "$extract_root"
130
+ case "$(read_model_field "$model_id" format tar.bz2)" in
131
+ tar.bz2) tar --no-same-owner -xjf "$archive" -C "$extract_root" ;;
132
+ file) cp -- "$archive" "$extract_root/$(basename -- "$archive")" ;;
133
+ *) echo "[assistant-voice] Unsupported model source format for $model_id" >&2; exit 1 ;;
134
+ esac
129
135
  done <<< "$model_ids"
130
136
 
131
137
  "$node_bin" "$script_dir/stage-voice-models.mjs" "$source_manifest" "$extract_root" "$stage_root"
@@ -1,4 +1,4 @@
1
- import { cp, mkdir, readFile, writeFile } from "node:fs/promises";
1
+ import { appendFile, cp, lstat, mkdir, readFile, realpath, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
4
  const [manifestPath, extractedRoot, stagedRoot] = process.argv.slice(2);
@@ -17,4 +17,44 @@ for (const model of manifest.models) {
17
17
  await cp(contained(extractedRoot, source), target, { recursive: true, errorOnExist: true, force: false });
18
18
  }
19
19
  }
20
+ // ONNX ModelProto metadata_props is field 14 (StringStringEntryProto).
21
+ // Append metadata only; the pinned source graph and weights remain byte-for-byte.
22
+ function protobufField(number, bytes) {
23
+ const length = [];
24
+ let remaining = bytes.length;
25
+ while (remaining > 127) { length.push((remaining & 127) | 128); remaining = Math.floor(remaining / 128); }
26
+ return Buffer.concat([Buffer.from([number * 8 + 2, ...length, remaining]), bytes]);
27
+ }
28
+ for (const [relative, entries] of Object.entries(manifest.onnxMetadata || {})) {
29
+ const target = contained(stagedRoot, relative);
30
+ if (!relative.endsWith(".onnx") || !entries || typeof entries !== "object" || Array.isArray(entries)) {
31
+ throw new Error("ONNX metadata must map model paths to string entries.");
32
+ }
33
+ const fields = Object.entries(entries).map(([key, value]) => {
34
+ if (!key || typeof value !== "string") throw new Error("ONNX metadata keys and values must be strings.");
35
+ return protobufField(14, Buffer.concat([protobufField(1, Buffer.from(key)), protobufField(2, Buffer.from(value))]));
36
+ });
37
+ if (!(await lstat(target)).isFile()) throw new Error("ONNX metadata requires a staged regular model file.");
38
+ const physicalRoot = await realpath(stagedRoot);
39
+ if (!(await realpath(target)).startsWith(`${physicalRoot}${path.sep}`)) {
40
+ throw new Error("ONNX metadata must stay inside its staged pack.");
41
+ }
42
+ await appendFile(target, Buffer.concat(fields));
43
+ }
44
+ for (const [relative, content] of Object.entries(manifest.textFiles || {})) {
45
+ if (typeof content !== "string") throw new Error("Model configuration files must contain text.");
46
+ const target = contained(stagedRoot, relative);
47
+ const physicalRoot = await realpath(stagedRoot);
48
+ let parent = path.dirname(target);
49
+ let physicalParent;
50
+ while (!physicalParent) {
51
+ try { physicalParent = await realpath(parent); }
52
+ catch (error) { if (error.code !== "ENOENT") throw error; parent = path.dirname(parent); }
53
+ }
54
+ if (physicalParent !== physicalRoot && !physicalParent.startsWith(`${physicalRoot}${path.sep}`)) {
55
+ throw new Error("Model configuration files must stay inside their staged pack.");
56
+ }
57
+ await mkdir(path.dirname(target), { recursive: true });
58
+ await writeFile(target, content, { flag: "wx" });
59
+ }
20
60
  await writeFile(path.join(stagedRoot, "speech.json"), JSON.stringify(manifest.configuration || {}, null, 2) + "\n");