From 046319a33348c079b650b035c4573ab395e6e9fc Mon Sep 17 00:00:00 2001 From: mwzkhalil Date: Sat, 12 Sep 2026 13:50:36 +0500 Subject: [PATCH 1/2] fix(higgs_audio_tts): share one sampling seed across chunks of a request Long text is split into chunks (session.cpp chunk_text_request) and generator_->generate() runs once per chunk. When a caller does not pass --seed, each of those calls independently draws a fresh random seed (generator.cpp: request.options.seed.value_or(random_u64_seed())), so an unseeded multi-chunk request sampled every chunk against the same reference-voice conditioning but with unrelated entropy. That is a plausible contributor to the voice/timbre drift between chunks reported in #471 (Arabic voice cloning switching speakers partway through, worse with smaller --text-chunk-size since that means more chunk boundaries). HiggsTTSSession::run() now resolves one seed up front when the request did not supply one, and copies it onto every chunk before generation, the same as when a caller already passes --seed explicitly. This only changes behavior for the previously-undefined unseeded case; an explicit --seed still works exactly as before. Also makes the "reached max_tokens before EOC" failure actionable: it now names the configured max_tokens value and points at --max-tokens / --text-chunk-size (or their server request-option equivalents) instead of leaving the caller to guess, which was the other half of #471. Ref #471 --- src/models/higgs_audio_tts/generator.cpp | 12 +++++++++++- src/models/higgs_audio_tts/session.cpp | 24 +++++++++++++++++++----- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/models/higgs_audio_tts/generator.cpp b/src/models/higgs_audio_tts/generator.cpp index 2820067e1..9f42fbd7f 100644 --- a/src/models/higgs_audio_tts/generator.cpp +++ b/src/models/higgs_audio_tts/generator.cpp @@ -508,7 +508,17 @@ HiggsGenerationResult HiggsGenerator::generate(const HiggsGenerationRequest & re engine::debug::timing_log_scalar("higgs_audio_tts.generator.decode_ms", engine::debug::elapsed_ms(decode_start, Clock::now())); if (!state.generation_done) { - throw std::runtime_error("Higgs TTS generation reached max_tokens before EOC"); + // max_tokens bounds AR frames per *chunk*, not per whole request (session.cpp + // splits long text into chunks before calling generate() on each), so this + // means one chunk's text needed more codec frames than the budget allows, + // not that the request as a whole was too long. Denser scripts (see #471, + // reported with Arabic text) need more frames per chunk than the same + // chunk length would in English. + throw std::runtime_error( + "Higgs TTS generation reached max_tokens (" + std::to_string(request.options.max_tokens) + + ") before EOC for this text chunk; raise it with --max-tokens on the CLI or " + "the \"max_tokens\" request option on the server, or lower --text-chunk-size / " + "\"text_chunk_size\" so each chunk needs fewer generated frames"); } result.raw_codes = reverse_higgs_delay_pattern( diff --git a/src/models/higgs_audio_tts/session.cpp b/src/models/higgs_audio_tts/session.cpp index ca3110b38..1e5904c1d 100644 --- a/src/models/higgs_audio_tts/session.cpp +++ b/src/models/higgs_audio_tts/session.cpp @@ -245,13 +245,27 @@ void HiggsTTSSession::prepare(const runtime::SessionPreparationRequest & request runtime::TaskResult HiggsTTSSession::run(const runtime::TaskRequest & request) { require_prepared("Higgs TTS run"); const auto wall_start = Clock::now(); + + runtime::TaskRequest seeded_request = request; + if (!runtime::parse_u64_option(seeded_request.options, {"seed"}).has_value()) { + // generator_->generate() is called once per text chunk below, and each + // call draws a fresh random seed when none is set (generator.cpp). Left + // alone, an unseeded long request would sample every chunk with + // independent entropy against the same reference conditioning -- a + // plausible contributor to the voice/timbre drift between chunks + // reported in #471. Resolving one seed up front and copying it onto + // every chunk keeps them on a consistent sampling trajectory, the same + // way chunks already behave when a caller passes --seed explicitly. + seeded_request.options["seed"] = std::to_string(runtime::random_u64_seed()); + } + const int64_t text_chunk_size = - engine::text::parse_text_chunk_size_override(request.options).value_or(kDefaultTextChunkSize); + engine::text::parse_text_chunk_size_override(seeded_request.options).value_or(kDefaultTextChunkSize); const auto text_chunk_mode = - engine::text::parse_text_chunk_mode_override(request.options).value_or(engine::text::TextChunkMode::Default); - const auto chunk_requests = runtime::chunk_text_request(request, text_chunk_size, text_chunk_mode); - const std::string reference_text = runtime::find_option(request.options, {"reference_text"}).value_or(""); - const auto * reference_audio = find_reference_audio(request); + engine::text::parse_text_chunk_mode_override(seeded_request.options).value_or(engine::text::TextChunkMode::Default); + const auto chunk_requests = runtime::chunk_text_request(seeded_request, text_chunk_size, text_chunk_mode); + const std::string reference_text = runtime::find_option(seeded_request.options, {"reference_text"}).value_or(""); + const auto * reference_audio = find_reference_audio(seeded_request); const HiggsCodecEncodeOutput * reference_codes = reference_audio != nullptr ? &resolve_reference_codes(*reference_audio, reference_text) : nullptr; debug::trace_log_scalar("higgs_audio_tts.text_chunk_size", text_chunk_size); From c96ba538da02b001ab482ef14b786216bd16275b Mon Sep 17 00:00:00 2001 From: 0xShug0 <231717474+0xShug0@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:15:54 -0400 Subject: [PATCH 2/2] Limit Higgs TTS change to token-limit error guidance --- src/models/higgs_audio_tts/generator.cpp | 6 ------ src/models/higgs_audio_tts/session.cpp | 24 +++++------------------- 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/src/models/higgs_audio_tts/generator.cpp b/src/models/higgs_audio_tts/generator.cpp index 9f42fbd7f..c97b34edc 100644 --- a/src/models/higgs_audio_tts/generator.cpp +++ b/src/models/higgs_audio_tts/generator.cpp @@ -508,12 +508,6 @@ HiggsGenerationResult HiggsGenerator::generate(const HiggsGenerationRequest & re engine::debug::timing_log_scalar("higgs_audio_tts.generator.decode_ms", engine::debug::elapsed_ms(decode_start, Clock::now())); if (!state.generation_done) { - // max_tokens bounds AR frames per *chunk*, not per whole request (session.cpp - // splits long text into chunks before calling generate() on each), so this - // means one chunk's text needed more codec frames than the budget allows, - // not that the request as a whole was too long. Denser scripts (see #471, - // reported with Arabic text) need more frames per chunk than the same - // chunk length would in English. throw std::runtime_error( "Higgs TTS generation reached max_tokens (" + std::to_string(request.options.max_tokens) + ") before EOC for this text chunk; raise it with --max-tokens on the CLI or " diff --git a/src/models/higgs_audio_tts/session.cpp b/src/models/higgs_audio_tts/session.cpp index 1e5904c1d..ca3110b38 100644 --- a/src/models/higgs_audio_tts/session.cpp +++ b/src/models/higgs_audio_tts/session.cpp @@ -245,27 +245,13 @@ void HiggsTTSSession::prepare(const runtime::SessionPreparationRequest & request runtime::TaskResult HiggsTTSSession::run(const runtime::TaskRequest & request) { require_prepared("Higgs TTS run"); const auto wall_start = Clock::now(); - - runtime::TaskRequest seeded_request = request; - if (!runtime::parse_u64_option(seeded_request.options, {"seed"}).has_value()) { - // generator_->generate() is called once per text chunk below, and each - // call draws a fresh random seed when none is set (generator.cpp). Left - // alone, an unseeded long request would sample every chunk with - // independent entropy against the same reference conditioning -- a - // plausible contributor to the voice/timbre drift between chunks - // reported in #471. Resolving one seed up front and copying it onto - // every chunk keeps them on a consistent sampling trajectory, the same - // way chunks already behave when a caller passes --seed explicitly. - seeded_request.options["seed"] = std::to_string(runtime::random_u64_seed()); - } - const int64_t text_chunk_size = - engine::text::parse_text_chunk_size_override(seeded_request.options).value_or(kDefaultTextChunkSize); + engine::text::parse_text_chunk_size_override(request.options).value_or(kDefaultTextChunkSize); const auto text_chunk_mode = - engine::text::parse_text_chunk_mode_override(seeded_request.options).value_or(engine::text::TextChunkMode::Default); - const auto chunk_requests = runtime::chunk_text_request(seeded_request, text_chunk_size, text_chunk_mode); - const std::string reference_text = runtime::find_option(seeded_request.options, {"reference_text"}).value_or(""); - const auto * reference_audio = find_reference_audio(seeded_request); + engine::text::parse_text_chunk_mode_override(request.options).value_or(engine::text::TextChunkMode::Default); + const auto chunk_requests = runtime::chunk_text_request(request, text_chunk_size, text_chunk_mode); + const std::string reference_text = runtime::find_option(request.options, {"reference_text"}).value_or(""); + const auto * reference_audio = find_reference_audio(request); const HiggsCodecEncodeOutput * reference_codes = reference_audio != nullptr ? &resolve_reference_codes(*reference_audio, reference_text) : nullptr; debug::trace_log_scalar("higgs_audio_tts.text_chunk_size", text_chunk_size);