Skip to content

Commit 93affc0

Browse files
committed
Add server voice presets
1 parent e74a290 commit 93affc0

8 files changed

Lines changed: 435 additions & 48 deletions

File tree

CMakeLists.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,23 @@ if (ENGINE_BUILD_TESTS)
643643
NAME server_multipart_test
644644
COMMAND server_multipart_test
645645
)
646+
647+
add_executable(server_config_test
648+
tests/unittests/test_server_config.cpp
649+
app/server/config.cpp
650+
app/cli/args.cpp
651+
app/cli/request.cpp
652+
)
653+
target_include_directories(server_config_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/app/server)
654+
target_link_libraries(server_config_test PRIVATE engine_runtime ggml)
655+
if (ENGINE_ENABLE_OPENMP)
656+
target_link_libraries(server_config_test PRIVATE OpenMP::OpenMP_CXX)
657+
endif()
658+
659+
add_test(
660+
NAME server_config_test
661+
COMMAND server_config_test
662+
)
646663
endif()
647664
648665
if (ENGINE_BUILD_EXAMPLES)

app/server/README.md

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ cat > server.json <<'JSON'
3434
},
3535
"session_options": {
3636
"language": "english"
37+
},
38+
"default_voice_preset": {
39+
"voice_id": "alba"
40+
},
41+
"voice_presets": {
42+
"cosette": {
43+
"voice_id": "cosette"
44+
}
3745
}
3846
},
3947
{
@@ -57,6 +65,41 @@ Set top-level `"lazy_load": true` to register all configured model ids at startu
5765
> [!WARNING]
5866
> Lazy loading does not unload models after a request. Once a model is first used, the server keeps that model and session in memory for reuse until the server exits.
5967
68+
For TTS models that need repeated voice-clone context, set a model-level `default_voice_preset` so OpenAI-compatible clients can omit `voice_ref` and `reference_text` on each request:
69+
70+
```json
71+
{
72+
"id": "omnivoice",
73+
"family": "omnivoice",
74+
"path": "/absolute/path/to/models/OmniVoice",
75+
"task": "tts",
76+
"mode": "offline",
77+
"default_voice_preset": {
78+
"voice_ref": "/absolute/path/to/reference.wav",
79+
"reference_text": "Reference transcript for the reference audio."
80+
}
81+
}
82+
```
83+
84+
For multiple server-side presets, use `voice_presets` and optionally point `default_voice_preset` at one of those preset names:
85+
86+
```json
87+
{
88+
"voice_presets": {
89+
"assistant": {
90+
"voice_ref": "/absolute/path/to/assistant.wav",
91+
"reference_text": "Reference transcript for assistant."
92+
},
93+
"narrator": {
94+
"voice_id": "alba"
95+
}
96+
},
97+
"default_voice_preset": "assistant"
98+
}
99+
```
100+
101+
When a request sends `"voice": "assistant"`, the server uses that configured preset. When `"voice"` does not match a configured preset, it is passed through as the model-native cached voice id, preserving the previous behavior.
102+
60103
## Start
61104

62105
```bash
@@ -90,12 +133,13 @@ curl http://127.0.0.1:8080/v1/audio/speech \
90133
-d '{
91134
"model": "pocket-tts",
92135
"input": "audio.cpp is serving this request through the framework runtime.",
93-
"voice_ref": "/path/to/reference.wav",
94136
"max_tokens": 96,
95137
"seed": 1234
96138
}'
97139
```
98140

141+
If no request voice is provided and the configured model has `default_voice_preset`, the server injects that preset automatically. Request-level `voice`, `voice_ref`, and `reference_text` override the configured default.
142+
99143
Set `"response_format": "json"` to receive base64 WAV in a JSON response.
100144

101145
### `POST /v1/audio/transcriptions`
@@ -124,7 +168,7 @@ curl http://127.0.0.1:8080/v1/audio/transcriptions \
124168

125169
### `GET /v1/audio/voices?model=<id>`
126170

127-
Lists the cached voice ids available for a TTS model, so a client can populate a voice picker instead of guessing generic names. For families that keep voice presets under `model_root/embeddings/*.safetensors` (`pocket_tts` today), this returns those ids; for other families, or an unknown/missing `model` parameter, it returns an empty list.
171+
Lists the cached voice ids and configured server voice preset names available for a TTS model, so a client can populate a voice picker instead of guessing generic names. For families that keep voice presets under `model_root/embeddings/*.safetensors` (`pocket_tts` today), this returns those ids too; for other families with no configured presets, or an unknown/missing `model` parameter, it returns an empty list.
128172

129173
```bash
130174
curl 'http://127.0.0.1:8080/v1/audio/voices?model=pocket-tts'

app/server/config.cpp

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,29 @@ std::unordered_map<std::string, std::string> options_from_object(const engine::i
1919
return minitts::cli::json_options_map(value);
2020
}
2121

22+
ServerModelConfig::VoicePreset parse_voice_preset(
23+
const std::filesystem::path & base,
24+
const engine::io::json::Value & value,
25+
const std::string & context) {
26+
if (!value.is_object()) {
27+
throw std::runtime_error(context + " must be an object");
28+
}
29+
ServerModelConfig::VoicePreset preset;
30+
if (const auto * voice_id = value.find("voice_id")) {
31+
preset.voice_id = voice_id->as_string();
32+
}
33+
if (const auto * voice_ref = value.find("voice_ref")) {
34+
preset.voice_ref = resolve_path(base, voice_ref->as_string());
35+
}
36+
if (const auto * reference_text = value.find("reference_text")) {
37+
preset.reference_text = reference_text->as_string();
38+
}
39+
if (!preset.voice_id.has_value() && !preset.voice_ref.has_value() && !preset.reference_text.has_value()) {
40+
throw std::runtime_error(context + " must set voice_id, voice_ref, or reference_text");
41+
}
42+
return preset;
43+
}
44+
2245
} // namespace
2346

2447
engine::core::BackendType parse_server_backend(const std::string & value) {
@@ -66,6 +89,40 @@ ServerConfig load_server_config(const std::filesystem::path & path) {
6689
}
6790
model.load_options = options_from_object(item.find("load_options"));
6891
model.session_options = options_from_object(item.find("session_options"));
92+
if (const auto * voice_presets = item.find("voice_presets")) {
93+
if (!voice_presets->is_object()) {
94+
throw std::runtime_error("voice_presets for model " + model.id + " must be an object");
95+
}
96+
for (const auto & [name, preset_value] : voice_presets->as_object()) {
97+
if (name.empty()) {
98+
throw std::runtime_error("voice_presets for model " + model.id + " cannot use an empty preset name");
99+
}
100+
auto [it, inserted] = model.voice_presets.emplace(
101+
name,
102+
parse_voice_preset(base, preset_value, "voice preset " + name + " for model " + model.id));
103+
if (!inserted) {
104+
throw std::runtime_error("duplicate voice preset for model " + model.id + ": " + name);
105+
}
106+
(void) it;
107+
}
108+
}
109+
if (const auto * default_voice_preset = item.find("default_voice_preset")) {
110+
if (default_voice_preset->is_string()) {
111+
model.default_voice_preset_id = default_voice_preset->as_string();
112+
if (model.default_voice_preset_id->empty()) {
113+
throw std::runtime_error("default_voice_preset for model " + model.id + " cannot be empty");
114+
}
115+
if (model.voice_presets.find(*model.default_voice_preset_id) == model.voice_presets.end()) {
116+
throw std::runtime_error(
117+
"default_voice_preset for model " + model.id +
118+
" does not match a configured voice_presets entry: " +
119+
*model.default_voice_preset_id);
120+
}
121+
} else {
122+
model.default_voice_preset =
123+
parse_voice_preset(base, *default_voice_preset, "default_voice_preset for model " + model.id);
124+
}
125+
}
69126
config.models.push_back(std::move(model));
70127
}
71128
return config;

app/server/config.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111
namespace minitts::server {
1212

1313
struct ServerModelConfig {
14+
struct VoicePreset {
15+
std::optional<std::string> voice_id;
16+
std::optional<std::filesystem::path> voice_ref;
17+
std::optional<std::string> reference_text;
18+
};
19+
1420
std::string id;
1521
std::filesystem::path path;
1622
std::string family;
@@ -21,6 +27,9 @@ struct ServerModelConfig {
2127
std::optional<std::string> weight_id;
2228
std::unordered_map<std::string, std::string> load_options;
2329
std::unordered_map<std::string, std::string> session_options;
30+
std::unordered_map<std::string, VoicePreset> voice_presets;
31+
std::optional<VoicePreset> default_voice_preset;
32+
std::optional<std::string> default_voice_preset_id;
2433
};
2534

2635
struct ServerConfig {

app/server/example.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@
1717
},
1818
"session_options": {
1919
"language": "english"
20+
},
21+
"default_voice_preset": {
22+
"voice_id": "alba"
23+
},
24+
"voice_presets": {
25+
"cosette": {
26+
"voice_id": "cosette"
27+
}
2028
}
2129
},
2230
{

0 commit comments

Comments
 (0)