Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

`audiocpp_server` is an HTTP adapter over the framework runtime registry. It keeps one loaded model and one offline task session per active model id, so repeated HTTP requests reuse the same framework session and model-owned graph/cache state.

`POST /v1/audio/speech` accepts top-level `speed` (or `speaking_rate`) as a positive speech-rate multiplier when the selected model supports speed control. Models without speed control reject the field.

## Build

```bash
Expand Down
27 changes: 27 additions & 0 deletions app/server/runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1312,6 +1312,12 @@ void ServerState::refresh_model_option_flags(LoadedModel & model) {
"language",
effective_override,
model.config.path);
if (model.config.task == "tts") {
model.accepts_speed = model_accepts_request_option(
model.config.family, "speed", effective_override, model.config.path);
model.accepts_speaking_rate = model_accepts_request_option(
model.config.family, "speaking_rate", effective_override, model.config.path);
}
}

HttpResponse ServerState::handle_model_load(const std::string & body_text) {
Expand Down Expand Up @@ -2098,6 +2104,27 @@ engine::runtime::TaskRequest ServerState::build_speech_request(const LoadedModel
if (const auto * value = body.find("reference_text")) {
request.options["reference_text"] = value->as_string();
}
const auto * speed = body.find("speed");
if (speed == nullptr) {
speed = body.find("speaking_rate");
}
if (speed != nullptr) {
const float rate = static_cast<float>(speed->as_number());
if (!std::isfinite(rate) || rate <= 0.0f) {
throw std::runtime_error("speed must be a positive finite number");
}
if (!model.accepts_speed && !model.accepts_speaking_rate && model.config.family != "kokoro_tts") {
throw std::runtime_error("speed is not supported by this model");
}
if (model.accepts_speed) {
request.options["speed"] = std::to_string(rate);
} else if (model.accepts_speaking_rate) {
request.options["speaking_rate"] = std::to_string(rate);
}
voice.style = engine::runtime::StyleCondition{};
voice.style->speaking_rate = rate;
has_voice = true;
}
if (has_voice) {
request.voice = std::move(voice);
}
Expand Down
2 changes: 2 additions & 0 deletions app/server/runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ class ServerState final : public IHttpHandler, public ServerFrontendContext {
// contract omits it would reject the whole request over an option
// nobody set. Resolved once at registration for the same cost reason.
bool accepts_language = true;
bool accepts_speed = true;
bool accepts_speaking_rate = true;
// Serializes runs on this model and bounds how long a caller waits for its
// turn; see BusyGuard.
BusyGuard busy;
Expand Down
2 changes: 2 additions & 0 deletions docs/models/kokoro_tts.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ package.
| `--language` | language code | voice prefix | Text frontend language. |
| `--voice-id` | voice ID listed above | `af_heart` | Built-in voice pack. |
| `--seed` | integer | random | Decoder noise seed. |
| `--speaking-rate` | positive float | `1.0` | Speech speed multiplier. |
| `--text-chunk-size` | integer chars | `240` | Long-form chunk size. |

## Request Options (use with `--request-option`)
Expand All @@ -110,6 +111,7 @@ package.
|---|---|---:|---|
| `language` | language code | voice prefix | Text frontend language. |
| `seed` | integer | random | Decoder noise seed. |
| `speed` | positive float | `1.0` | Speech speed multiplier; `speaking_rate` is also accepted, but conflicting values are rejected. |
| `text_chunk_size` | integer chars | `240` | Long-form chunk size. |

## Session Options (use with `--session-option`)
Expand Down
3 changes: 2 additions & 1 deletion docs/tts.md
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,8 @@ audiocpp_cli --task tts --family supertonic --model /path/to/supertonic-3 --back
| `--voice-id` | `M1`-`M5`, `F1`-`F5` | `M1` | Preset voice. |
| `--language` | language code | `en` | Text language. |
| `--num-inference-steps` | integer | `8` | Flow denoising steps. |
| `--request-option speaking_rate=<float>` | float | `1.05` | Speech speed multiplier. |
| `--request-option speed=<float>` | float | `1.05` | Speech speed multiplier. |
| `--request-option speaking_rate=<float>` | float | `1.05` | Alias for `speed`. |
| `--seed` | integer | `1234` | Noise seed. |
| `--text-chunk-size` | characters | `300`, or `120` for `ko`/`ja` | Framework long-form text chunk size. |
| `--text-chunk-mode` | `default`, `tag_aware`, `japanese`, `endline` | `default` | Framework long-form text chunking mode. |
Expand Down
8 changes: 8 additions & 0 deletions model_specs/kokoro_tts.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@
"required": false,
"min": 0
},
{
"name": "speed",
"type": "float",
"description": "Speech speed multiplier; default 1.0.",
"required": false,
"min": 0.01,
"default": 1.0
},
{
"name": "phonemes",
"type": "string_list",
Expand Down
42 changes: 34 additions & 8 deletions src/models/kokoro_tts/session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include <stdexcept>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>

namespace engine::models::kokoro_tts {
Expand Down Expand Up @@ -324,23 +325,47 @@ void validate_request_options(
const std::unordered_map<std::string, std::string> & options,
const std::unordered_map<std::string, std::vector<std::string>> & option_arrays,
const engine::model_spec::ModelContract & contract) {
if (contract.request_option_keys.find(kPhonemesOption) != contract.request_option_keys.end()) {
const bool old_phonemes = contract.request_option_keys.find(kPhonemesOption) == contract.request_option_keys.end();
const bool old_speed = contract.request_option_keys.find("speed") == contract.request_option_keys.end();
const bool old_speaking_rate = contract.request_option_keys.find("speaking_rate") == contract.request_option_keys.end();
if (!old_phonemes && !old_speed && !old_speaking_rate) {
runtime::validate_spec_backed_request_options(options, option_arrays, contract, kModelName);
return;
}
// Keys only: the validator reads names, and copying the map wholesale would copy every
// supplied phoneme string to drop one entry from it.
std::unordered_map<std::string, std::string> validation_options;
for (const auto & [key, _] : options) {
if ((key == "speed" && old_speed) || (key == "speaking_rate" && old_speaking_rate)) continue;
validation_options.emplace(key, std::string{});
}
// Keys only: the validator reads names, not the supplied values.
std::unordered_map<std::string, std::vector<std::string>> validation_arrays;
for (const auto & [key, _] : option_arrays) {
if (key != kPhonemesOption) validation_arrays.emplace(key, std::vector<std::string>{});
if ((key == kPhonemesOption && old_phonemes) ||
(key == "speed" && old_speed) || (key == "speaking_rate" && old_speaking_rate)) continue;
validation_arrays.emplace(key, std::vector<std::string>{});
}
runtime::validate_spec_backed_request_options(validation_options, validation_arrays, contract, kModelName);
}

std::optional<runtime::VoiceCondition> voice_with_request_rate(
const std::optional<runtime::VoiceCondition> & voice,
const std::unordered_map<std::string, std::string> & options) {
const auto rate = runtime::parse_positive_finite_float_option(options, {"speed", "speaking_rate"});
if (!rate.has_value() || (voice.has_value() && voice->style.has_value() &&
voice->style->speaking_rate.has_value())) {
return voice;
}
runtime::validate_spec_backed_request_options(options, validation_arrays, contract, kModelName);
auto resolved = voice.value_or(runtime::VoiceCondition{});
if (!resolved.style.has_value()) resolved.style = runtime::StyleCondition{};
resolved.style->speaking_rate = *rate;
return resolved;
}

} // namespace

void KokoroTTSSession::prepare(const runtime::SessionPreparationRequest & request) {
validate_request_options(request.options, request.option_arrays, *contract_);
const auto voice = voice_with_request_rate(request.voice, request.options);
if (const auto seed = runtime::parse_u64_option(request.options, {"seed"})) {
if (rng_seed_ != *seed) {
rng_seed_ = *seed;
Expand All @@ -365,7 +390,7 @@ void KokoroTTSSession::prepare(const runtime::SessionPreparationRequest & reques
runtime::SessionPreparationRequest chunk_request = request;
chunk_request.text = runtime::Transcript{chunk, request.text->language};
const auto frontend_state =
resolve_kokoro_frontend_session_state(chunk_request.text, chunk_request.voice, *assets_);
resolve_kokoro_frontend_session_state(chunk_request.text, voice, *assets_);
if (prepare_phonemes.empty()) {
request_size = std::max(
request_size,
Expand Down Expand Up @@ -397,6 +422,7 @@ runtime::TaskResult KokoroTTSSession::run(const runtime::TaskRequest & request)
throw std::runtime_error("Kokoro TTS run requires text_input");
}
validate_request_options(request.options, request.option_arrays, *contract_);
const auto voice = voice_with_request_rate(request.voice, request.options);

const int64_t text_chunk_size =
engine::text::parse_text_chunk_size_override(request.options).value_or(kDefaultTextChunkSize);
Expand Down Expand Up @@ -432,7 +458,7 @@ runtime::TaskResult KokoroTTSSession::run(const runtime::TaskRequest & request)
std::optional<KokoroFrontendSessionState> shared_state;
std::string shared_key_prefix;
if (supplied) {
shared_state = resolve_kokoro_frontend_session_state(request.text_input, request.voice, *assets_);
shared_state = resolve_kokoro_frontend_session_state(request.text_input, voice, *assets_);
shared_key_prefix = cache_key_prefix(*shared_state, *request.text_input);
}
for (size_t chunk_index = 0; chunk_index < chunk_count; ++chunk_index) {
Expand All @@ -442,7 +468,7 @@ runtime::TaskResult KokoroTTSSession::run(const runtime::TaskRequest & request)
: std::optional<std::string_view>{};
const auto frontend_state = supplied
? *shared_state
: resolve_kokoro_frontend_session_state(chunk_request.text_input, chunk_request.voice, *assets_);
: resolve_kokoro_frontend_session_state(chunk_request.text_input, voice, *assets_);
const std::string cache_key =
(supplied ? shared_key_prefix : cache_key_prefix(frontend_state, *chunk_request.text_input)) +
// Without this, two requests with the same text and different supplied phonemes
Expand Down
3 changes: 2 additions & 1 deletion src/models/supertonic/loader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ runtime::ModelCliInterface cli(const SupertonicAssets &) {
out.request_options = {
{"voice_id", "M1|M2|M3|M4|M5|F1|F2|F3|F4|F5", "Preset voice style id, default M1; also exposed as --voice-id."},
{"num_inference_steps", "n", "Flow denoising steps, default 8."},
{"speaking_rate", "float", "Speech speed multiplier, default 1.05."},
{"speed", "float", "Speech speed multiplier, default 1.05."},
{"speaking_rate", "float", "Alias for speed."},
{"seed", "n", "Noise seed, default 1234."},
{"text_chunk_mode", "default|tag_aware|japanese|endline", "Long-form text chunking mode."},
};
Expand Down
14 changes: 10 additions & 4 deletions src/models/supertonic/session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "engine/models/supertonic/runtime.h"

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <limits>
#include <stdexcept>
Expand Down Expand Up @@ -212,12 +213,17 @@ SupertonicGenerationOptions SupertonicSession::generation_options_from_request(c
}
options.num_inference_steps = static_cast<int>(*value);
}
if (const auto value = runtime::parse_finite_float_option(request.options, {"speaking_rate"})) {
if (*value <= 0.0F) {
throw std::runtime_error("Supertonic speaking_rate must be positive");
}
if (const auto value = runtime::parse_positive_finite_float_option(request.options, {"speed", "speaking_rate"})) {
options.speaking_rate = *value;
}
if (request.voice.has_value() && request.voice->style.has_value() &&
request.voice->style->speaking_rate.has_value()) {
const float rate = *request.voice->style->speaking_rate;
if (!std::isfinite(rate) || rate <= 0.0f) {
throw std::runtime_error("Supertonic speaking_rate must be positive and finite");
}
options.speaking_rate = rate;
}
if (const auto value = runtime::parse_u32_option(request.options, {"seed"})) {
options.seed = *value;
}
Expand Down
Loading