Skip to content
Closed
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
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ add_library(engine_core OBJECT
src/framework/model_spec/metadata.cpp
src/framework/runtime/session.cpp
src/framework/runtime/task_vocabulary.cpp
src/framework/runtime/partial_text.cpp
src/framework/runtime/artifacts.cpp
src/framework/runtime/cache.cpp
src/framework/runtime/graph_executor.cpp
Expand Down Expand Up @@ -2485,6 +2486,8 @@ if (ENGINE_BUILD_TESTS OR ENGINE_BUILD_EXTENDED_TESTS OR ENGINE_BUILD_MODEL_TEST

add_engine_unittest(audio_chunking_test tests/unittests/test_audio_chunking.cpp)
add_test(NAME audio_chunking_test COMMAND audio_chunking_test)
add_engine_unittest(partial_text_test tests/unittests/test_partial_text.cpp)
add_test(NAME partial_text_test COMMAND partial_text_test)

add_engine_unittest(wav_writer_formats_test tests/unittests/test_wav_writer_formats.cpp)
add_test(NAME wav_writer_formats_test COMMAND wav_writer_formats_test)
Expand Down
2 changes: 2 additions & 0 deletions include/engine/community_models/kroko_asr/session.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "engine/community_models/kroko_asr/tokenizer.h"
#include "engine/community_models/kroko_asr/zipformer.h"
#include "engine/framework/model_spec/metadata.h"
#include "engine/framework/runtime/partial_text.h"
#include "engine/framework/runtime/session_base.h"

#include <chrono>
Expand Down Expand Up @@ -83,6 +84,7 @@ class KrokoASRSession final
std::vector<float> streaming_resampler_source_;
int64_t processed_feature_offset_ = 0;
int64_t streaming_total_samples_ = 0;
runtime::PartialTextPublisher streaming_partials_;
int64_t streaming_source_offset_ = 0;
int64_t streaming_source_frames_ = 0;
int64_t streaming_next_output_sample_ = 0;
Expand Down
5 changes: 5 additions & 0 deletions include/engine/community_models/parakeet_tdt/session.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include "engine/framework/model_spec/metadata.h"
#include "engine/framework/runtime/partial_text.h"
#include "engine/framework/runtime/session_base.h"
#include "engine/community_models/parakeet_tdt/assets.h"
#include "engine/community_models/parakeet_tdt/decoder.h"
Expand All @@ -13,6 +14,7 @@
#include <filesystem>
#include <memory>
#include <string>

#include <unordered_map>
#include <vector>

Expand Down Expand Up @@ -125,6 +127,9 @@ class ParakeetTDTStreamingSession final
std::vector<int32_t> token_frame_indices_;
std::vector<int32_t> token_durations_;
runtime::StreamEventCallback stream_event_sink_;
// merged_decode() returns the whole transcript each time; this turns it
// back into the increment a partial is contracted to be.
runtime::PartialTextPublisher partials_;
bool stream_started_ = false;
bool finalized_ = false;
};
Expand Down
3 changes: 2 additions & 1 deletion include/engine/community_models/sense_asr/session.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "engine/community_models/sense_asr/types.h"
#include "engine/framework/assets/tensor_source.h"
#include "engine/framework/model_spec/metadata.h"
#include "engine/framework/runtime/partial_text.h"
#include "engine/framework/runtime/session_base.h"

#include <chrono>
Expand Down Expand Up @@ -87,7 +88,7 @@ class SenseAsrSession final : public runtime::RuntimeSessionBase,
runtime::AudioBuffer streaming_audio_;
size_t streaming_audio_offset_values_ = 0;
std::string streaming_text_;
size_t streaming_published_bytes_ = 0;
runtime::PartialTextPublisher streaming_partials_;
int64_t streaming_windows_processed_ = 0;
runtime::StreamEventCallback stream_event_sink_;
bool stream_started_ = false;
Expand Down
58 changes: 58 additions & 0 deletions include/engine/framework/runtime/partial_text.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#pragma once

#include <cstddef>
#include <string>

namespace engine::runtime {

// Turns a running transcript into the increments a partial is contracted to be.
//
// A streaming session's partial_text is the text decoded since the last one.
// The CLI appends them into a scrolling transcript (see PartialTextRenderer)
// and the reference server forwards each as an OpenAI-shaped
// `transcript.text.delta`, which is incremental by specification. A family that
// holds a whole running transcript therefore has to publish the difference, and
// every family that did so carried its own copy of the arithmetic.
//
// The copies disagreed and two of them were wrong, which is why this is shared
// rather than duplicated once more:
//
// - A decode can end part way through a UTF-8 sequence, because a tokenizer
// falls back to bytes for text its vocabulary does not cover. Publishing
// that puts half a code point on the wire, and it reaches a JSON encoder
// as invalid UTF-8. The tail is held back for the update that completes it.
//
// - A decode can revise text already published rather than only extending it.
// Nothing can retract a delta that has gone out, so the client's transcript
// is wrong either way; starting the next delta at the divergence at least
// keeps it from being spliced into the middle of a character.
//
// Not thread safe: a session owns one of these and publishes from the thread
// driving the stream.
class PartialTextPublisher {
public:
// The text newly decoded since the last call. Empty when nothing is new, or
// when everything new is an incomplete character still being held.
std::string publish(const std::string & transcript);

// Forgets what has gone out, for a session starting a fresh stream.
void reset() { published_.clear(); }

// What has actually been published, which trails `transcript` whenever a
// character is being held back.
const std::string & published() const { return published_; }

private:
std::string published_;
};

// Longest common prefix of two transcripts, backed off so the result never
// lands inside a UTF-8 sequence. Exposed for tests and for callers that need
// the offset rather than the text.
std::size_t transcript_common_prefix(const std::string & lhs, const std::string & rhs);

// How much of `text` is safe to publish: everything up to the last complete
// UTF-8 sequence. Equal to text.size() unless the tail is a partial character.
std::size_t transcript_publishable_end(const std::string & text);

} // namespace engine::runtime
3 changes: 2 additions & 1 deletion include/engine/models/qwen3_asr/session.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include "engine/framework/assets/tensor_source.h"
#include "engine/framework/runtime/partial_text.h"
#include "engine/framework/runtime/session_base.h"
#include "engine/models/qwen3_asr/assets.h"
#include "engine/models/qwen3_asr/audio_encoder.h"
Expand Down Expand Up @@ -88,7 +89,7 @@ class Qwen3ASRSession final
runtime::AudioBuffer streaming_audio_;
size_t streaming_audio_offset_values_ = 0;
std::string streaming_text_;
size_t streaming_published_bytes_ = 0;
runtime::PartialTextPublisher streaming_partials_;
int64_t streaming_windows_processed_ = 0;
runtime::StreamEventCallback stream_event_sink_;
bool stream_started_ = false;
Expand Down
3 changes: 2 additions & 1 deletion include/engine/models/voxtral_realtime/session.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include "engine/framework/runtime/partial_text.h"
#include "engine/framework/runtime/session_base.h"
#include "engine/models/voxtral_realtime/assets.h"
#include "engine/models/voxtral_realtime/audio_encoder.h"
Expand Down Expand Up @@ -81,7 +82,7 @@ class VoxtralRealtimeSession final
// The transcript decoded so far, and how much of it has already gone out as a partial. Every
// partial is the suffix between the two, so the deltas concatenate to exactly this string.
std::string streaming_text_;
size_t streaming_published_bytes_ = 0;
runtime::PartialTextPublisher streaming_partials_;
int64_t streaming_token_count_ = 0;
int32_t previous_stream_token_ = 0;
bool stream_started_ = false;
Expand Down
7 changes: 6 additions & 1 deletion model_specs/chatterbox.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,12 @@
"ve.safetensors",
"t3_cfg.safetensors",
"s3gen.safetensors",
"tokenizer.json"
"tokenizer.json",
"grapheme_mtl_merged_expanded_v1.json",
"Cangjie5_TC.json",
"conds.pt",
"t3_mtl23ls_v2.safetensors",
"t3_mtl23ls_v3.safetensors"
],
"download": {
"kind": "huggingface_snapshot",
Expand Down
20 changes: 19 additions & 1 deletion model_specs/index_tts2.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,25 @@
"files": [
"config.yaml",
"bpe.model",
"gpt.safetensors"
"gpt.safetensors",
"w2v-bert-2.0/config.json",
"w2v-bert-2.0/preprocessor_config.json",
"bigvgan/config.json",
"qwen0.6bemo4-merge/config.json",
"qwen0.6bemo4-merge/generation_config.json",
"qwen0.6bemo4-merge/tokenizer.json",
"qwen0.6bemo4-merge/tokenizer_config.json",
"qwen0.6bemo4-merge/vocab.json",
"qwen0.6bemo4-merge/merges.txt",
"s2mel.safetensors",
"feat1.safetensors",
"feat2.safetensors",
"wav2vec2bert_stats.safetensors",
"w2v-bert-2.0/model.safetensors",
"semantic_codec_model.safetensors",
"campplus.safetensors",
"bigvgan/model.safetensors",
"qwen0.6bemo4-merge/model.safetensors"
],
"download": {
"kind": "huggingface_snapshot",
Expand Down
6 changes: 5 additions & 1 deletion model_specs/omnivoice.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,11 @@
"files": [
"config.json",
"model.safetensors",
"tokenizer.json"
"tokenizer.json",
"tokenizer_config.json",
"audio_tokenizer/config.json",
"audio_tokenizer/preprocessor_config.json",
"audio_tokenizer/model.safetensors"
],
"download": {
"kind": "huggingface_snapshot",
Expand Down
4 changes: 3 additions & 1 deletion model_specs/qwen3_tts.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,9 @@
"model.safetensors",
"speech_tokenizer/config.json",
"speech_tokenizer/model.safetensors",
"tokenizer_config.json"
"tokenizer_config.json",
"vocab.json",
"merges.txt"
],
"download": {
"kind": "huggingface_snapshot",
Expand Down
27 changes: 26 additions & 1 deletion model_specs/seed_vc.json
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,32 @@
"files": [
"seed_vc_manifest.json",
"v2/ar.safetensors",
"v2/cfm.safetensors"
"v2/cfm.safetensors",
"v2/vc_wrapper.json",
"astral/bsq32.json",
"astral/bsq2048.json",
"v1/svc.json",
"v1/whisper_bigvgan.json",
"v1/xlsr_hift.json",
"hift/config.json",
"bigvgan/v2_22khz_80band_256x/config.json",
"bigvgan/v2_44khz_128band_512x/config.json",
"whisper-small/config.json",
"hubert-large-ll60k/config.json",
"wav2vec2-xls-r-300m/config.json",
"v1/svc.safetensors",
"v1/whisper_bigvgan.safetensors",
"v1/xlsr_hift.safetensors",
"astral/bsq32.safetensors",
"astral/bsq2048.safetensors",
"campplus/model.safetensors",
"rmvpe/model.safetensors",
"hift/model.safetensors",
"bigvgan/v2_22khz_80band_256x/model.safetensors",
"bigvgan/v2_44khz_128band_512x/model.safetensors",
"whisper-small/model.safetensors",
"hubert-large-ll60k/model.safetensors",
"wav2vec2-xls-r-300m/model.safetensors"
],
"download": {
"kind": "huggingface_snapshot",
Expand Down
12 changes: 11 additions & 1 deletion model_specs/supertonic.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,17 @@
"files": [
"config/tts.json",
"config/unicode_indexer.json",
"ggml/supertonic.safetensors"
"ggml/supertonic.safetensors",
"voice_styles/F1.json",
"voice_styles/F2.json",
"voice_styles/F3.json",
"voice_styles/F4.json",
"voice_styles/F5.json",
"voice_styles/M1.json",
"voice_styles/M2.json",
"voice_styles/M3.json",
"voice_styles/M4.json",
"voice_styles/M5.json"
],
"download": {
"kind": "huggingface_snapshot",
Expand Down
29 changes: 24 additions & 5 deletions src/community_models/kroko_asr/session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,7 @@ void KrokoASRSession::reset() {
endpoint_segments_.clear();
processed_feature_offset_ = 0;
streaming_total_samples_ = 0;
streaming_partials_.reset();
streaming_source_offset_ = 0;
streaming_source_frames_ = 0;
streaming_next_output_sample_ = 0;
Expand Down Expand Up @@ -903,7 +904,21 @@ runtime::StreamEvent KrokoASRSession::process_streaming_audio(
decoded,
streaming_total_samples_,
streaming_language_);
event.partial_text = result.text_output;
// A partial is the text decoded since the last one, not the whole
// transcript re-rendered: combined_decoded() restates everything
// decoded so far, so an appending consumer built it up quadratically.
// word_timestamps stays cumulative -- it is the finalized set, not a
// delta -- so the two fields differ on purpose.
if (result.text_output.has_value()) {
std::string delta =
streaming_partials_.publish(
result.text_output->text);
if (!delta.empty()) {
event.partial_text = runtime::Transcript{
std::move(delta),
result.text_output->language};
}
}
event.word_timestamps = result.word_timestamps;
}
return event;
Expand Down Expand Up @@ -940,10 +955,14 @@ runtime::TaskResult KrokoASRSession::finalize() {
if (stream_event_sink_) {
stream_event_sink_(event);
}
runtime::TaskResult result;
result.text_output = event.partial_text;
result.speech_segments = endpoint_segments_;
result.word_timestamps = std::move(event.word_timestamps);
// Built from the combined decode, not from event.partial_text. The partial
// is the increment since the last one now, so reusing it here would report
// only the final window as the transcript -- this read the whole transcript
// out of the partial only because the partial restated it every time.
runtime::TaskResult result = make_result(
combined_decoded(),
streaming_total_samples_,
streaming_language_);
engine::debug::timing_log_scalar(
"kroko_asr.session_ms",
engine::debug::elapsed_ms(stream_start_, Clock::now()));
Expand Down
17 changes: 16 additions & 1 deletion src/community_models/parakeet_tdt/session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,7 @@ void ParakeetTDTStreamingSession::reset() {
token_ids_.clear();
token_frame_indices_.clear();
token_durations_.clear();
partials_.reset();
decoder_->reset_state();
stream_started_ = true;
finalized_ = false;
Expand Down Expand Up @@ -912,7 +913,21 @@ runtime::StreamEvent ParakeetTDTStreamingSession::process_ready_windows(bool flu
runtime::StreamEvent event;
if (changed && !token_ids_.empty()) {
auto decoded = merged_decode();
event.partial_text = runtime::Transcript{decoded.text, ""};
// A partial is the text decoded since the last one: the CLI appends
// them into a scrolling transcript and the server forwards each as a
// transcript.text.delta. merged_decode() re-renders the whole
// transcript from every token so far, so publishing it unchanged made
// an appending client build "Some call meSome call me nature".
std::string delta = partials_.publish(decoded.text);
if (!delta.empty()) {
event.partial_text = runtime::Transcript{std::move(delta), ""};
}
// word_timestamps stays cumulative, deliberately. It is not a delta
// field: it is the finalized set so far, which is why the provisional
// last word is dropped below rather than carried. So the two fields in
// this event have different shapes on purpose -- partial_text is what
// is new, word_timestamps is everything settled -- because each matches
// its own contract rather than each other.
event.word_timestamps = std::move(decoded.word_timestamps);
// The last word has no following word boundary yet, so it remains
// provisional and is withheld from the finalized timestamp list.
Expand Down
8 changes: 4 additions & 4 deletions src/community_models/sense_asr/session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ void SenseAsrSession::reset() {
streaming_audio_ = runtime::AudioBuffer{};
streaming_audio_offset_values_ = 0;
streaming_text_.clear();
streaming_published_bytes_ = 0;
streaming_partials_.reset();
streaming_windows_processed_ = 0;
stream_started_ = false;
stream_wall_start_ = {};
Expand Down Expand Up @@ -758,12 +758,12 @@ SenseAsrSession::process_one_stream_chunk(const runtime::AudioBuffer &audio) {
streaming_result_.text_output->language = item.text_output->language;
}
streaming_result_.text_output->text = streaming_text_;
if (streaming_published_bytes_ < streaming_text_.size()) {
if (std::string partial = streaming_partials_.publish(streaming_text_);
!partial.empty()) {
event.partial_text = runtime::Transcript{
streaming_text_.substr(streaming_published_bytes_),
std::move(partial),
streaming_result_.text_output->language,
};
streaming_published_bytes_ = streaming_text_.size();
}
return event;
}
Expand Down
Loading