Skip to content

Commit 8afcc29

Browse files
committed
Extend model manager for new TTS models
1 parent 11f3bf6 commit 8afcc29

5 files changed

Lines changed: 167 additions & 15 deletions

File tree

README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,17 @@ The `vox` CLI can list, download, verify, repair, and remove known local models:
6262
```sh
6363
./build/bin/vox model list
6464
./build/bin/vox model download qwen3-asr-1.7b
65+
./build/bin/vox model download kokoro-tts
66+
./build/bin/vox model download qwen3-tts
6567
./build/bin/vox model verify qwen3-asr-1.7b
6668
./build/bin/vox model repair qwen3-asr-1.7b
6769
```
6870

6971
Model verification checks that expected files exist, are non-empty, and do not
7072
have leftover partial downloads. Checksums are reported when metadata is
71-
available; the current bundled manifests rely on file presence and size.
73+
available; the current bundled manifests rely on file presence and size. Common
74+
aliases such as `kokoro`, `cosyvoice`, and `qwen3-tts` resolve to their
75+
canonical model entries.
7276

7377
### Whisper ASR
7478

@@ -142,7 +146,7 @@ CosyVoice3 remains the default TTS engine.
142146
Download the minimum baked-voice CosyVoice3 GGUF set:
143147

144148
```sh
145-
scripts/download-cosyvoice3-tts-gguf.sh
149+
./build/bin/vox model download cosyvoice3-tts
146150
```
147151

148152
That creates:
@@ -159,7 +163,7 @@ Pass the LLM GGUF with `--tts-model`. The runtime auto-discovers sibling flow, H
159163
Kokoro-82M is available with `--tts-engine kokoro`:
160164

161165
```sh
162-
scripts/download-kokoro-tts-gguf.sh
166+
./build/bin/vox model download kokoro-tts
163167
```
164168

165169
On Windows PowerShell:
@@ -180,7 +184,7 @@ Pass the Kokoro model with `--tts-model`. The runtime auto-discovers `kokoro-voi
180184
Qwen3-TTS 0.6B is available with `--tts-engine qwen3-tts`. The recommended quick-test path is CustomVoice Q8_0 because it has built-in speakers and does not need a reference WAV:
181185

182186
```sh
183-
scripts/download-qwen3-tts-gguf.sh
187+
./build/bin/vox model download qwen3-tts
184188
```
185189

186190
On Windows PowerShell:
@@ -196,7 +200,7 @@ models/tts/qwen3-tts-0.6b-customvoice/qwen3-tts-12hz-0.6b-customvoice-q8_0.gguf
196200
models/tts/qwen3-tts-0.6b-customvoice/qwen3-tts-tokenizer-12hz.gguf
197201
```
198202

199-
Pass the talker GGUF with `--tts-model`. The runtime auto-discovers `qwen3-tts-tokenizer-12hz.gguf` in the same directory, or use `--tts-codec-model PATH`. CustomVoice speakers include `aiden`, `dylan`, `eric`, `ono_anna`, `ryan`, `serena`, `sohee`, `uncle_fu`, and `vivian`; use `dylan` or `eric` for Chinese output tests. The Base variant can also be downloaded with `scripts/download-qwen3-tts-gguf.sh models/tts/qwen3-tts-0.6b-base base q8_0`; it requires `--tts-voice-model` pointing to a baked voice GGUF or a reference WAV plus `--tts-ref-text`.
203+
Pass the talker GGUF with `--tts-model`. The runtime auto-discovers `qwen3-tts-tokenizer-12hz.gguf` in the same directory, or use `--tts-codec-model PATH`. CustomVoice speakers include `aiden`, `dylan`, `eric`, `ono_anna`, `ryan`, `serena`, `sohee`, `uncle_fu`, and `vivian`; use `dylan` or `eric` for Chinese output tests. The Base variant can also be downloaded with `./build/bin/vox model download qwen3-tts-0.6b-base`; it requires `--tts-voice-model` pointing to a baked voice GGUF or a reference WAV plus `--tts-ref-text`.
200204

201205
## Run
202206

apps/model_manager.cpp

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "model_manager.h"
22

3+
#include <algorithm>
34
#include <cstdlib>
45
#include <cstdio>
56
#include <iostream>
@@ -93,6 +94,13 @@ void print_model_details(const ManagedModel & model, const ManagedModelStatus &
9394
<< " source: " << model.source << "\n"
9495
<< " version: " << model.version << "\n"
9596
<< " checksum: " << (model.checksum.empty() ? "unavailable" : model.checksum) << "\n";
97+
if (!model.aliases.empty()) {
98+
out << " aliases:";
99+
for (const std::string & alias : model.aliases) {
100+
out << " " << alias;
101+
}
102+
out << "\n";
103+
}
96104
for (size_t i = 0; i < model.files.size(); ++i) {
97105
const ManagedModelFile & file = model.files[i];
98106
const ManagedModelFileStatus & file_status = status.files[i];
@@ -122,6 +130,42 @@ int require_model_name(const std::vector<std::string> & args, std::ostream & err
122130
return 0;
123131
}
124132

133+
bool name_matches_model(const ManagedModel & model, const std::string & name) {
134+
if (model.name == name) {
135+
return true;
136+
}
137+
return std::find(model.aliases.begin(), model.aliases.end(), name) != model.aliases.end();
138+
}
139+
140+
bool contains_token(const std::string & haystack, const std::string & needle) {
141+
return !needle.empty() && haystack.find(needle) != std::string::npos;
142+
}
143+
144+
void append_unique(std::vector<std::string> & values, const std::string & value) {
145+
if (std::find(values.begin(), values.end(), value) == values.end()) {
146+
values.push_back(value);
147+
}
148+
}
149+
150+
std::vector<std::string> suggest_models(const std::string & name) {
151+
std::vector<std::string> suggestions;
152+
for (const ManagedModel & model : supported_models()) {
153+
if (contains_token(model.name, name) || contains_token(name, model.name)) {
154+
append_unique(suggestions, model.name);
155+
}
156+
for (const std::string & alias : model.aliases) {
157+
if (contains_token(alias, name) || contains_token(name, alias)) {
158+
append_unique(suggestions, model.name);
159+
break;
160+
}
161+
}
162+
if (suggestions.size() >= 5) {
163+
break;
164+
}
165+
}
166+
return suggestions;
167+
}
168+
125169
} // namespace
126170

127171
const std::vector<ManagedModel> & supported_models() {
@@ -137,6 +181,7 @@ const std::vector<ManagedModel> & supported_models() {
137181
{"models/asr/qwen3-asr-1.7b/mmproj-Qwen3-ASR-1.7B-Q8_0.gguf", "about 356 MB"},
138182
},
139183
"scripts/download-qwen3-asr-gguf.sh",
184+
{"qwen3-asr", "qwen3-asr-default", "default-asr"},
140185
},
141186
{
142187
"qwen3-asr-0.6b",
@@ -149,6 +194,7 @@ const std::vector<ManagedModel> & supported_models() {
149194
{"models/asr/qwen3-asr-0.6b/mmproj-Qwen3-ASR-0.6B-Q8_0.gguf", ""},
150195
},
151196
"scripts/download-qwen3-asr-gguf.sh 0.6B Q8_0 models/asr/qwen3-asr-0.6b",
197+
{"qwen3-asr-small", "small-asr"},
152198
},
153199
{
154200
"whisper-base",
@@ -158,6 +204,7 @@ const std::vector<ManagedModel> & supported_models() {
158204
"",
159205
{{"models/ggml-base.bin", ""}},
160206
"./external/whisper.cpp/models/download-ggml-model.sh base models",
207+
{"whisper", "whisper.cpp"},
161208
},
162209
{
163210
"hymt-translate",
@@ -167,6 +214,7 @@ const std::vector<ManagedModel> & supported_models() {
167214
"",
168215
{{"models/translate/HY-MT1.5-1.8B-Q4_K_M.gguf", "about 1.13 GB"}},
169216
"scripts/download-hymt-gguf.sh",
217+
{"hymt", "hy-mt", "translate"},
170218
},
171219
{
172220
"cosyvoice3-tts",
@@ -181,14 +229,66 @@ const std::vector<ManagedModel> & supported_models() {
181229
{"models/tts/cosyvoice3/cosyvoice3-voices.gguf", ""},
182230
},
183231
"scripts/download-cosyvoice3-tts-gguf.sh",
232+
{"cosyvoice3", "cosyvoice", "cosy"},
233+
},
234+
{
235+
"kokoro-tts",
236+
"Kokoro-82M TTS GGUF model and default af_heart voice pack",
237+
"cstr/kokoro-82m-GGUF + cstr/kokoro-voices-GGUF",
238+
"82M q8_0 / af_heart",
239+
"",
240+
{
241+
{"models/tts/kokoro/kokoro-82m-q8_0.gguf", "about 135 MiB"},
242+
{"models/tts/kokoro/kokoro-voice-af_heart.gguf", "about 510 KiB"},
243+
},
244+
#ifdef _WIN32
245+
"powershell -ExecutionPolicy Bypass -File scripts/download-kokoro-tts-gguf.ps1",
246+
#else
247+
"scripts/download-kokoro-tts-gguf.sh",
248+
#endif
249+
{"kokoro", "kokoro-82m"},
250+
},
251+
{
252+
"qwen3-tts-0.6b-customvoice",
253+
"Qwen3-TTS 0.6B CustomVoice talker and tokenizer/codec",
254+
"cstr/qwen3-tts-0.6b-customvoice-GGUF + cstr/qwen3-tts-tokenizer-12hz-GGUF",
255+
"0.6B CustomVoice q8_0",
256+
"",
257+
{
258+
{"models/tts/qwen3-tts-0.6b-customvoice/qwen3-tts-12hz-0.6b-customvoice-q8_0.gguf", "about 923 MiB"},
259+
{"models/tts/qwen3-tts-0.6b-customvoice/qwen3-tts-tokenizer-12hz.gguf", "about 342 MiB"},
260+
},
261+
#ifdef _WIN32
262+
"powershell -ExecutionPolicy Bypass -File scripts/download-qwen3-tts-gguf.ps1",
263+
#else
264+
"scripts/download-qwen3-tts-gguf.sh",
265+
#endif
266+
{"qwen3-tts", "qwen3tts", "qwen3-tts-customvoice", "qwen3-tts-cv"},
267+
},
268+
{
269+
"qwen3-tts-0.6b-base",
270+
"Qwen3-TTS 0.6B Base talker and tokenizer/codec; requires a separate voice reference at runtime",
271+
"cstr/qwen3-tts-0.6b-base-GGUF + cstr/qwen3-tts-tokenizer-12hz-GGUF",
272+
"0.6B Base q8_0",
273+
"",
274+
{
275+
{"models/tts/qwen3-tts-0.6b-base/qwen3-tts-12hz-0.6b-base-q8_0.gguf", ""},
276+
{"models/tts/qwen3-tts-0.6b-base/qwen3-tts-tokenizer-12hz.gguf", "about 342 MiB"},
277+
},
278+
#ifdef _WIN32
279+
"powershell -ExecutionPolicy Bypass -File scripts/download-qwen3-tts-gguf.ps1 models/tts/qwen3-tts-0.6b-base base q8_0",
280+
#else
281+
"scripts/download-qwen3-tts-gguf.sh models/tts/qwen3-tts-0.6b-base base q8_0",
282+
#endif
283+
{"qwen3-tts-base"},
184284
},
185285
};
186286
return models;
187287
}
188288

189289
const ManagedModel * find_model(const std::string & name) {
190290
for (const ManagedModel & model : supported_models()) {
191-
if (model.name == name) {
291+
if (name_matches_model(model, name)) {
192292
return &model;
193293
}
194294
}
@@ -268,8 +368,16 @@ int run_model_command(
268368

269369
const ManagedModel * model = find_model(args[1]);
270370
if (!model) {
271-
err << "Unknown model: " << args[1] << "\n"
272-
<< "Run 'vox model list' to see supported models.\n";
371+
err << "Unknown model: " << args[1] << "\n";
372+
const std::vector<std::string> suggestions = suggest_models(args[1]);
373+
if (!suggestions.empty()) {
374+
err << "Did you mean:";
375+
for (const std::string & suggestion : suggestions) {
376+
err << " " << suggestion;
377+
}
378+
err << "\n";
379+
}
380+
err << "Run 'vox model list' to see supported models.\n";
273381
return 1;
274382
}
275383

apps/model_manager.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ struct ManagedModel {
2121
std::string checksum;
2222
std::vector<ManagedModelFile> files;
2323
std::string download_command;
24+
std::vector<std::string> aliases;
2425
};
2526

2627
struct ManagedModelFileStatus {

apps/vox.cpp

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -175,16 +175,16 @@ const char * tts_engine_name(TtsEngine engine) {
175175
return "unknown";
176176
}
177177

178-
const char * tts_download_script(TtsEngine engine) {
178+
const char * tts_model_manager_name(TtsEngine engine) {
179179
switch (engine) {
180180
case TtsEngine::CosyVoice3:
181-
return "scripts/download-cosyvoice3-tts-gguf.sh";
181+
return "cosyvoice3-tts";
182182
case TtsEngine::Kokoro:
183-
return "scripts/download-kokoro-tts-gguf.sh";
183+
return "kokoro-tts";
184184
case TtsEngine::Qwen3Tts:
185-
return "scripts/download-qwen3-tts-gguf.sh";
185+
return "qwen3-tts-0.6b-customvoice";
186186
}
187-
return "scripts/download-cosyvoice3-tts-gguf.sh";
187+
return "cosyvoice3-tts";
188188
}
189189

190190
std::string default_tts_language(const CliOptions & options,
@@ -707,7 +707,7 @@ int main(int argc, char ** argv) {
707707
tts_model_path = resolve_model_path(cli.tts_model_path);
708708
if (!file_exists(tts_model_path)) {
709709
std::cerr << "Missing TTS model: " << tts_model_path << "\n"
710-
<< "Download: vox model download cosyvoice3-tts\n";
710+
<< "Download: vox model download " << tts_model_manager_name(tts_engine) << "\n";
711711
return 1;
712712
}
713713

tests/model_manager_test.cpp

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ int main() {
3131

3232
const vox::app::model::ManagedModel * model = vox::app::model::find_model("whisper-base");
3333
ok = expect(model != nullptr, "whisper-base should be supported") && ok;
34+
const vox::app::model::ManagedModel * kokoro = vox::app::model::find_model("kokoro");
35+
ok = expect(kokoro != nullptr && kokoro->name == "kokoro-tts", "kokoro alias should resolve") && ok;
36+
const vox::app::model::ManagedModel * qwen3_tts = vox::app::model::find_model("qwen3-tts");
37+
ok = expect(qwen3_tts != nullptr && qwen3_tts->name == "qwen3-tts-0.6b-customvoice",
38+
"qwen3-tts alias should resolve to CustomVoice") &&
39+
ok;
3440
ok = expect(vox::app::model::find_model("missing-model") == nullptr, "unknown model should not resolve") && ok;
3541
if (!model) {
3642
return 1;
@@ -48,7 +54,40 @@ int main() {
4854

4955
std::ostringstream out;
5056
std::ostringstream err;
51-
int result = vox::app::model::run_model_command({"list", "--installed"}, root, out, err);
57+
int result = vox::app::model::run_model_command({"list"}, root, out, err);
58+
ok = expect(result == 0, "list should succeed") && ok;
59+
ok = expect(out.str().find("kokoro-tts") != std::string::npos, "list should include Kokoro") && ok;
60+
ok = expect(out.str().find("qwen3-tts-0.6b-customvoice") != std::string::npos,
61+
"list should include Qwen3-TTS CustomVoice") &&
62+
ok;
63+
ok = expect(out.str().find("qwen3-tts-0.6b-base") != std::string::npos, "list should include Qwen3-TTS Base") &&
64+
ok;
65+
66+
out.str("");
67+
out.clear();
68+
err.str("");
69+
err.clear();
70+
result = vox::app::model::run_model_command({"verify", "kokoro"}, root, out, err);
71+
ok = expect(result == 1, "verify should fail for missing Kokoro files") && ok;
72+
ok = expect(out.str().find("kokoro-tts (missing)") != std::string::npos,
73+
"verify should print canonical Kokoro model details") &&
74+
ok;
75+
76+
out.str("");
77+
out.clear();
78+
err.str("");
79+
err.clear();
80+
result = vox::app::model::run_model_command({"verify", "qwen3-tts-custom"}, root, out, err);
81+
ok = expect(result == 1, "unknown similar model should fail") && ok;
82+
ok = expect(err.str().find("Did you mean: qwen3-tts-0.6b-customvoice") != std::string::npos,
83+
"unknown model should include suggestions") &&
84+
ok;
85+
86+
out.str("");
87+
out.clear();
88+
err.str("");
89+
err.clear();
90+
result = vox::app::model::run_model_command({"list", "--installed"}, root, out, err);
5291
ok = expect(result == 0, "list --installed should succeed for incomplete models") && ok;
5392
ok = expect(out.str().find("whisper-base") != std::string::npos, "installed list should include incomplete model") && ok;
5493

0 commit comments

Comments
 (0)