Skip to content

Commit ccc4fd2

Browse files
committed
Merge branch 'main' into vibeasr-vae-encoder
2 parents 08b7b82 + ad4bd57 commit ccc4fd2

5 files changed

Lines changed: 165 additions & 22 deletions

File tree

CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2118,6 +2118,13 @@ if (ENGINE_BUILD_TESTS)
21182118
COMMAND chinese_normalization_test
21192119
)
21202120

2121+
add_engine_unittest(text_chunking_test tests/unittests/test_text_chunking.cpp)
2122+
2123+
add_test(
2124+
NAME text_chunking_test
2125+
COMMAND text_chunking_test
2126+
)
2127+
21212128
add_engine_unittest(unicode_normalization_test tests/unittests/test_unicode_normalization.cpp)
21222129

21232130
add_test(

src/framework/text/chunking.cpp

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,17 @@ bool is_clause_break(std::string_view token) {
6363
token == u8"" || token == u8"" || token == u8"" || token == u8"";
6464
}
6565

66+
bool is_cjk_punctuation_delimiter(std::string_view token) noexcept {
67+
// Full-width CJK sentence/clause punctuation (。!?,、;:) separates words
68+
// even without ASCII spaces — CJK text has no inter-word spaces, so without
69+
// this a whole paragraph parses as one word and Default-mode chunking can
70+
// never split it at the codepoint budget (only TagAware/Japanese could).
71+
// ASCII punctuation stays attached to its word so space-delimited Latin
72+
// text is unaffected.
73+
const auto leading = static_cast<unsigned char>(token.front());
74+
return leading >= 0x80 && (is_sentence_break(token) || is_clause_break(token));
75+
}
76+
6677
bool is_tag_open(std::string_view token) {
6778
return token == "[" || token == "<";
6879
}
@@ -112,9 +123,25 @@ std::vector<WordRange> split_word_ranges(const std::vector<Utf8Span> & spans) {
112123
if (span_pos >= spans.size()) {
113124
break;
114125
}
126+
// A CJK punctuation token forms a word of its own (attached to nothing),
127+
// so a subsequent run never absorbs it and boundaries can land on it.
128+
if (is_cjk_punctuation_delimiter(spans[span_pos].text)) {
129+
words.push_back({
130+
span_pos,
131+
span_pos + 1,
132+
spans[span_pos].start,
133+
spans[span_pos].end,
134+
is_sentence_break(spans[span_pos].text),
135+
is_clause_break(spans[span_pos].text),
136+
});
137+
span_pos += 1;
138+
continue;
139+
}
115140
const size_t word_start = span_pos;
116141
size_t word_end = span_pos + 1;
117-
while (word_end < spans.size() && !is_ascii_space(spans[word_end].text)) {
142+
while (word_end < spans.size() &&
143+
!is_ascii_space(spans[word_end].text) &&
144+
!is_cjk_punctuation_delimiter(spans[word_end].text)) {
118145
++word_end;
119146
}
120147
const auto last = spans[word_end - 1].text;

src/models/higgs_audio_tts/codec.cpp

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ constexpr int64_t kCodecDecodeCapacityBucketFrames = 32;
6262
constexpr int64_t kCodecHopLength = 960;
6363
constexpr int64_t kCodecPadSamples = kCodecHopLength / 2;
6464
constexpr int64_t kCodecDecodeWindowFrames = 128;
65-
constexpr int64_t kCodecDecodeOverlapFrames = 8;
65+
constexpr int64_t kCodecDecodeContextFrames = 32;
6666
constexpr int64_t kCodecFullDecodeMaxFrames = 512;
6767
constexpr int64_t kResidualDilations[] = {1, 3, 9};
6868

@@ -1584,11 +1584,14 @@ HiggsCodecDecodeOutput HiggsCodecRuntime::decode_codes(const std::vector<int32_t
15841584
std::vector<int32_t> window_codes;
15851585
int64_t emitted_frames = 0;
15861586
while (emitted_frames < frames) {
1587-
const int64_t window_begin =
1588-
std::max<int64_t>(0, emitted_frames - kCodecDecodeOverlapFrames);
1587+
const int64_t emit_begin = emitted_frames;
15891588
const int64_t emit_end =
1590-
std::min<int64_t>(frames, emitted_frames + kCodecDecodeWindowFrames);
1591-
const int64_t window_frames = emit_end - window_begin;
1589+
std::min<int64_t>(frames, emit_begin + kCodecDecodeWindowFrames);
1590+
const int64_t window_begin =
1591+
std::max<int64_t>(0, emit_begin - kCodecDecodeContextFrames);
1592+
const int64_t window_end =
1593+
std::min<int64_t>(frames, emit_end + kCodecDecodeContextFrames);
1594+
const int64_t window_frames = window_end - window_begin;
15921595
window_codes.resize(static_cast<size_t>(window_frames * kCodecCodebooks));
15931596
for (int64_t frame = 0; frame < window_frames; ++frame) {
15941597
const auto src =
@@ -1602,9 +1605,9 @@ HiggsCodecDecodeOutput HiggsCodecRuntime::decode_codes(const std::vector<int32_t
16021605
const auto window = run_window(
16031606
window_codes,
16041607
window_frames,
1605-
kCodecDecodeWindowFrames + kCodecDecodeOverlapFrames);
1606-
const int64_t trim_frames = emitted_frames - window_begin;
1607-
const int64_t emit_frames = emit_end - emitted_frames;
1608+
kCodecDecodeWindowFrames + 2 * kCodecDecodeContextFrames);
1609+
const int64_t trim_frames = emit_begin - window_begin;
1610+
const int64_t emit_frames = emit_end - emit_begin;
16081611
const int64_t sample_begin = trim_frames * kCodecHopLength;
16091612
const int64_t sample_count = emit_frames * kCodecHopLength;
16101613
if (sample_begin < 0 || sample_count <= 0 ||

src/models/vevo2/fm.cpp

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,7 @@ struct Vevo2FMGraph {
696696
}
697697

698698
~Vevo2FMGraph() {
699+
engine::core::release_backend_graph_resources(backend, graph, true);
699700
if (gallocr != nullptr) {
700701
ggml_gallocr_free(gallocr);
701702
gallocr = nullptr;
@@ -757,39 +758,45 @@ struct Vevo2FMStepGraph {
757758
if (config.hidden_size % config.num_heads != 0) {
758759
throw std::runtime_error("Vevo2 FM hidden_size must be divisible by num_heads");
759760
}
761+
ggml_init_params input_params{ggml_tensor_overhead() * 64, nullptr, true};
762+
input_ctx.reset(ggml_init(input_params));
763+
if (input_ctx == nullptr) {
764+
throw std::runtime_error("failed to initialize Vevo2 FM step input context");
765+
}
760766
ggml_init_params params{graph_context_bytes, nullptr, true};
761767
ctx.reset(ggml_init(params));
762768
if (ctx == nullptr) {
763769
throw std::runtime_error("failed to initialize Vevo2 FM step graph context");
764770
}
765771

772+
engine::core::ModuleBuildContext input_build_ctx{input_ctx.get(), "vevo2.fm.step.input", backend_type};
766773
engine::core::ModuleBuildContext build_ctx{ctx.get(), "vevo2.fm.step", backend_type};
767774
prompt_input = engine::core::make_tensor(
768-
build_ctx,
775+
input_build_ctx,
769776
GGML_TYPE_F32,
770777
engine::core::TensorShape::from_dims({1, prompt_frames, config.mel_dim})).tensor;
771778
xt_input = engine::core::make_tensor(
772-
build_ctx,
779+
input_build_ctx,
773780
GGML_TYPE_F32,
774781
engine::core::TensorShape::from_dims({1, target_frames, config.mel_dim})).tensor;
775782
cond_input = engine::core::make_tensor(
776-
build_ctx,
783+
input_build_ctx,
777784
GGML_TYPE_F32,
778785
engine::core::TensorShape::from_dims({1, cond_frames, config.hidden_size})).tensor;
779786
uncond_cond_input = engine::core::make_tensor(
780-
build_ctx,
787+
input_build_ctx,
781788
GGML_TYPE_F32,
782789
engine::core::TensorShape::from_dims({1, target_frames, config.hidden_size})).tensor;
783790
timestep_input = engine::core::make_tensor(
784-
build_ctx,
791+
input_build_ctx,
785792
GGML_TYPE_F32,
786793
engine::core::TensorShape::from_dims({1, config.hidden_size})).tensor;
787794
cond_position_input = engine::core::make_tensor(
788-
build_ctx,
795+
input_build_ctx,
789796
GGML_TYPE_I32,
790797
engine::core::TensorShape::from_dims({cond_frames})).tensor;
791798
uncond_position_input = engine::core::make_tensor(
792-
build_ctx,
799+
input_build_ctx,
793800
GGML_TYPE_I32,
794801
engine::core::TensorShape::from_dims({target_frames})).tensor;
795802
ggml_set_input(prompt_input);
@@ -878,8 +885,11 @@ struct Vevo2FMStepGraph {
878885

879886
graph = ggml_new_graph_custom(ctx.get(), 524288, false);
880887
ggml_build_forward_expand(graph, output);
881-
buffer = ggml_backend_alloc_ctx_tensors(ctx.get(), backend);
882-
if (buffer == nullptr) {
888+
input_buffer = ggml_backend_alloc_ctx_tensors(input_ctx.get(), backend);
889+
gallocr = ggml_gallocr_new(ggml_backend_get_default_buffer_type(backend));
890+
if (input_buffer == nullptr || gallocr == nullptr ||
891+
!ggml_gallocr_reserve(gallocr, graph) ||
892+
!ggml_gallocr_alloc_graph(gallocr, graph)) {
883893
throw std::runtime_error("failed to allocate Vevo2 FM step graph");
884894
}
885895

@@ -904,9 +914,14 @@ struct Vevo2FMStepGraph {
904914
}
905915

906916
~Vevo2FMStepGraph() {
907-
if (buffer != nullptr) {
908-
ggml_backend_buffer_free(buffer);
909-
buffer = nullptr;
917+
engine::core::release_backend_graph_resources(backend, graph, true);
918+
if (gallocr != nullptr) {
919+
ggml_gallocr_free(gallocr);
920+
gallocr = nullptr;
921+
}
922+
if (input_buffer != nullptr) {
923+
ggml_backend_buffer_free(input_buffer);
924+
input_buffer = nullptr;
910925
}
911926
}
912927

@@ -966,6 +981,7 @@ struct Vevo2FMStepGraph {
966981
int64_t cond_frames = 0;
967982
int64_t prompt_frames = 0;
968983
int64_t target_frames = 0;
984+
std::unique_ptr<ggml_context, GgmlContextDeleter> input_ctx;
969985
std::unique_ptr<ggml_context, GgmlContextDeleter> ctx;
970986
ggml_tensor * prompt_input = nullptr;
971987
ggml_tensor * xt_input = nullptr;
@@ -976,7 +992,8 @@ struct Vevo2FMStepGraph {
976992
ggml_tensor * uncond_position_input = nullptr;
977993
ggml_tensor * output = nullptr;
978994
ggml_cgraph * graph = nullptr;
979-
ggml_backend_buffer_t buffer = nullptr;
995+
ggml_backend_buffer_t input_buffer = nullptr;
996+
ggml_gallocr_t gallocr = nullptr;
980997
};
981998

982999
Vevo2FlowMatchingRuntime::Vevo2FlowMatchingRuntime(
@@ -1064,6 +1081,8 @@ Vevo2MelSequence Vevo2FlowMatchingRuntime::generate_mel(
10641081
const auto cond_run_start = Clock::now();
10651082
const auto diffusion_cond = graph_->run_conditioning(diffusion_tokens, config_);
10661083
const double cond_run_ms = engine::debug::elapsed_ms(cond_run_start);
1084+
graph_.reset();
1085+
engine::core::trim_backend_pools(execution_context_.backend());
10671086
const int64_t cond_frames = static_cast<int64_t>(diffusion_cond.size()) / config_.hidden_size;
10681087
if (cond_frames * config_.hidden_size != static_cast<int64_t>(diffusion_cond.size())) {
10691088
throw std::runtime_error("Vevo2 FM conditioning output shape mismatch");
@@ -1123,6 +1142,8 @@ Vevo2MelSequence Vevo2FlowMatchingRuntime::generate_mel(
11231142
const auto read_start = Clock::now();
11241143
out.values = step_graph_->read_output(config_);
11251144
const double final_read_ms = engine::debug::elapsed_ms(read_start);
1145+
step_graph_.reset();
1146+
engine::core::trim_backend_pools(execution_context_.backend());
11261147
engine::debug::timing_log_scalar("vevo2.fm.timbre_mel_ms", timbre_mel_ms);
11271148
engine::debug::timing_log_scalar("vevo2.fm.cond.graph.build_ms", cond_graph_build_ms);
11281149
engine::debug::timing_log_scalar("vevo2.fm.cond_run_ms", cond_run_ms);
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Default-mode text chunking regression: CJK text (no ASCII spaces) must still
2+
// split at the codepoint budget, with full-width punctuation as word boundaries.
3+
#include "engine/framework/text/chunking.h"
4+
5+
#include <cstdlib>
6+
#include <iostream>
7+
#include <string>
8+
#include <vector>
9+
10+
namespace {
11+
12+
void require(bool ok, const std::string & what) {
13+
if (!ok) {
14+
std::cerr << "FAIL: " << what << "\n";
15+
std::exit(1);
16+
}
17+
}
18+
19+
void check_chunks(const std::string & text, int64_t budget, size_t min_chunks, const std::string & label) {
20+
const auto chunks = engine::text::split_text_chunks(text, budget);
21+
if (chunks.size() < min_chunks) {
22+
std::cerr << "FAIL: " << label << " expected >= " << min_chunks << " chunks, got " << chunks.size()
23+
<< "\n";
24+
std::exit(1);
25+
}
26+
std::string joined;
27+
for (const auto & c : chunks) {
28+
joined += c;
29+
}
30+
require(joined == text, label + ": chunks must concatenate back to the trimmed input verbatim");
31+
for (const auto & c : chunks) {
32+
require(!c.empty(), label + ": empty chunk");
33+
}
34+
}
35+
36+
} // namespace
37+
38+
int main() {
39+
// Regression: a 236-codepoint CJK paragraph with the 200-codepoint default
40+
// budget used to come back as a single chunk, because split_word_ranges only
41+
// cut at ASCII spaces and the whole paragraph parsed as one word. Must now
42+
// split into at least two sentence-aligned chunks.
43+
const std::string cjk_long =
44+
"大家好,我是零一B语音合成模型,现在进行流式输出测试。这段文字比较长,目的是看模型能否把整段话分成多个音频块,边合成边推送。"
45+
"今天的天气很好,适合外出散步,湖边的柳树已经抽出了新芽,水面倒映着蓝天白云。如果流式工作正常,客户端应该能很快收到第一个音频块。"
46+
"第二段继续测试分块是否稳定,这里再补充一些内容,让总字数超过上限,这样模型必须把文字切开分多次合成。流式的意义在于长文本不必等全部算完。"
47+
"最后再来一句收尾的话,确认整个流程完整结束,谢谢大家。";
48+
check_chunks(cjk_long, 200, 2, "CJK long text over budget splits");
49+
50+
// Under budget: still one chunk, verbatim.
51+
check_chunks("短文本,不需要分块。", 200, 1, "short CJK text stays whole");
52+
53+
// Clause punctuation is a valid rollback boundary when the budget cuts a
54+
// run of clauses: budget 12 lands inside the second clause run, so the
55+
// first chunk must end at the 、 (clause) boundary, not mid-run.
56+
{
57+
const std::string text = "一二三四五六七八九十,一二三四五六七八九十。";
58+
const auto chunks = engine::text::split_text_chunks(text, 12);
59+
require(chunks.size() == 2, "clause rollback produces two chunks");
60+
require(chunks[0] == "一二三四五六七八九十,", "first chunk ends at clause punctuation");
61+
require(chunks[1] == "一二三四五六七八九十。", "second chunk keeps the sentence");
62+
}
63+
64+
// Latin text is unaffected: ASCII punctuation stays attached to its word
65+
// and boundaries still fall on ASCII-space words / sentence breaks.
66+
{
67+
const std::string text = "Hello world. How are you today?";
68+
const auto chunks = engine::text::split_text_chunks(text, 20);
69+
require(chunks.size() == 2, "latin splits at the sentence break");
70+
require(chunks[0] == "Hello world.", "first latin chunk ends at the period");
71+
require(chunks[1] == "How are you today?", "second latin chunk verbatim");
72+
}
73+
74+
// A single oversized word without any boundary still passes through whole
75+
// (pre-existing behavior: no hard character-level cut).
76+
{
77+
const std::string word(300, 'a');
78+
const auto chunks = engine::text::split_text_chunks(word, 200);
79+
require(chunks.size() == 1, "unbreakable oversized word stays one chunk");
80+
require(chunks[0] == word, "oversized word verbatim");
81+
}
82+
83+
std::cout << "text_chunking_test: all passed\n";
84+
return 0;
85+
}

0 commit comments

Comments
 (0)