@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 +34 -0
- package/models/voice-models-kokoro-q8f16.json +254 -0
- package/package.json +2 -2
- package/src/cli.js +12 -4
- package/tooling/prepare-models.sh +10 -4
- package/tooling/stage-voice-models.mjs +41 -1
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.
|
|
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.
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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");
|