char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Xuan-Son Nguyenmtmd: support pocket-tts (#26871)6e62ba538

char/aliceai-t5-moe
39.4 KiB1062 linesraw
1#include "mtmd.h"
2#include "mtmd-helper.h"
3#include "mtmd-helper-common.h"
4#include "llama.h"
5#include "../src/llama-ext.h"
6
7#include <algorithm>
8#include <cctype>
9#include <cmath>
10#include <cstring>
11#include <memory>
12#include <string>
13#include <unordered_map>
14#include <vector>
15
16#ifdef MTMD_INTERNAL_HEADER
17#error "mtmd-helper is a public library outside of mtmd. it must not include internal headers"
18#endif
19
20//
21// Audio generation helpers
22//
23
24// --tts-lang codes -> language names used by the codec_language special tokens
25static const std::unordered_map<std::string, std::string> tts_lang_codes = {
26    { "zh", "chinese"    },
27    { "en", "english"    },
28    { "de", "german"     },
29    { "it", "italian"    },
30    { "pt", "portuguese" },
31    { "es", "spanish"    },
32    { "ja", "japanese"   },
33    { "ko", "korean"     },
34    { "fr", "french"     },
35    { "ru", "russian"    },
36};
37
38static std::string tts_resolve_lang(const std::string & lang) {
39    auto it = tts_lang_codes.find(lang);
40    return it != tts_lang_codes.end() ? it->second : lang;
41}
42
43static llama_token find_special_token(const llama_vocab * vocab, const std::string & piece) {
44    const int32_t n = llama_vocab_n_tokens(vocab);
45    for (llama_token t = 0; t < n; t++) {
46        if (piece == llama_vocab_get_text(vocab, t)) {
47            return t;
48        }
49    }
50    return LLAMA_TOKEN_NULL;
51}
52
53static bool write_wav16(std::vector<char> & buf, const std::vector<float> & pcm, int32_t rate) {
54    // RIFF chunk sizes are 32-bit; refuse to emit a file with a truncated header
55    if (pcm.size() > ((size_t) UINT32_MAX - 36) / 2) {
56        return false;
57    }
58    const uint32_t data_sz   = (uint32_t) (pcm.size() * 2);
59    const uint32_t riff_sz   = 36 + data_sz;
60    const uint32_t fmt_sz    = 16, byte_rate = (uint32_t) rate * 2;
61    const uint16_t fmt = 1, ch = 1, align = 2, bits = 16;
62    const uint32_t rate32    = (uint32_t) rate;
63    auto put = [&](const void * p, size_t n) {
64        const char * c = (const char *) p;
65        buf.insert(buf.end(), c, c + n);
66    };
67    put("RIFF", 4); put(&riff_sz, 4); put("WAVE", 4);
68    put("fmt ", 4); put(&fmt_sz, 4);
69    put(&fmt, 2); put(&ch, 2); put(&rate32, 4);
70    put(&byte_rate, 4); put(&align, 2); put(&bits, 2);
71    put("data", 4); put(&data_sz, 4);
72    for (float v : pcm) {
73        int16_t s = (int16_t) (std::max(-1.0f, std::min(1.0f, v)) * 32767.0f);
74        put(&s, 2);
75    }
76    return true;
77}
78
79class mtmd_gen_audio_pipeline {
80public:
81    mtmd_gen_audio_pipeline(llama_context * lctx, mtmd_context * mctx)
82        : lctx(lctx), mctx(mctx), model(llama_get_model(lctx)), vocab(llama_model_get_vocab(model)),
83          n_embd(llama_model_n_embd(model)), info(mtmd_gen_audio_get_info(mctx)) {}
84    virtual ~mtmd_gen_audio_pipeline() = default;
85
86    virtual void reset() = 0;
87    virtual int32_t set_input(const mtmd_helper_gen_audio_inp * inp) = 0;
88    // decodes at most n_batch prompt tokens; returns remaining count (0 = done), <0 on error
89    virtual int32_t step_prompt(int32_t n_batch) = 0;
90    // sampled can be LLAMA_TOKEN_NULL for pipelines with no discrete backbone token,
91    // those read what they need from h_state_in instead
92    // set out_stop on end-of-speech, h_state_out must be null if no frame is generated
93    virtual int32_t step_gen(llama_token sampled, const float * h_state_in, const float ** h_state_out, bool * out_stop) = 0;
94    virtual int32_t get_output(int32_t * out_sample_rate, const char ** out_data, size_t * out_data_len, int64_t * out_n_samples) = 0;
95
96protected:
97    llama_context * lctx;
98    mtmd_context  * mctx;
99    const llama_model * model;
100    const llama_vocab  * vocab;
101    int n_embd;
102    mtmd_gen_audio_info info;
103};
104
105// Qwen3-TTS: backbone samples codec_0, code_predictor gives the other 15 codebooks,
106// then code2wav decodes them to PCM
107class qwen3tts_gen_audio_pipeline : public mtmd_gen_audio_pipeline {
108public:
109    using mtmd_gen_audio_pipeline::mtmd_gen_audio_pipeline;
110
111    void reset() override {
112        seq_id = 0;
113        pos = 0;
114        codes_buf.clear();
115        c2w_state.clear();
116        audio_pcm.clear();
117        overlay.clear();
118        h_state_buf.clear();
119        out_buf.clear();
120        prompt_embd_buf.clear();
121        prompt_batch.reset();
122        n_prompt = 0;
123        prompt_pos = 0;
124    }
125
126    int32_t set_input(const mtmd_helper_gen_audio_inp * inp) override {
127        reset();
128        seq_id = inp->seq_id;
129
130        if (!ensure_cache()) {
131            return 1;
132        }
133
134        const std::string lang   = tts_resolve_lang((inp->lang && inp->lang[0]) ? inp->lang : "english");
135        const llama_token c_lang = find_special_token(vocab, ("<|codec_language_" + lang + "|>").c_str());
136        if (c_lang == LLAMA_TOKEN_NULL) {
137            LOG_ERR("mtmd_helper_gen_audio: unknown language '%s'\n", lang.c_str());
138            return 1;
139        }
140
141        std::vector<float> speaker_embd;
142        if (inp->speaker_ref) {
143            if (!encode_speaker(inp->speaker_ref, speaker_embd)) {
144                return 1;
145            }
146        }
147
148        const int n_e = n_embd;
149        auto row = [&](llama_token t) {
150            return std::vector<float>(tok_embd.begin() + (size_t) t * n_e,
151                                       tok_embd.begin() + (size_t) (t + 1) * n_e);
152        };
153        auto sum_row = [&](llama_token a, llama_token b) {
154            std::vector<float> va = row(a), vb = row(b);
155            for (int i = 0; i < n_e; i++) va[(size_t) i] += vb[(size_t) i];
156            return va;
157        };
158        auto sum_vec = [&](llama_token a, const std::vector<float> & vb) {
159            std::vector<float> va = row(a);
160            for (int i = 0; i < n_e; i++) va[(size_t) i] += vb[(size_t) i];
161            return va;
162        };
163
164        // upstream chat wrap, then slices: [0:3] role, [3:-5] utterance body
165        const std::string full = "<|im_start|>assistant\n" + std::string(inp->prompt, inp->prompt_len) +
166                                  "<|im_end|>\n<|im_start|>assistant\n";
167        std::vector<llama_token> ids(full.size() + 16);
168        int n_ids = llama_tokenize(vocab, full.c_str(), (int32_t) full.size(), ids.data(), (int32_t) ids.size(),
169                                   false, true);
170        if (n_ids < 8) {
171            LOG_ERR("mtmd_helper_gen_audio: tokenization failed\n");
172            return 1;
173        }
174        ids.resize((size_t) n_ids);
175
176        std::vector<std::vector<float>> prompt;
177        for (int i = 0; i < 3; i++) prompt.push_back(row(ids[(size_t) i]));
178        prompt.push_back(sum_row(tts_pad, c_think));
179        prompt.push_back(sum_row(tts_pad, c_think_b));
180        prompt.push_back(sum_row(tts_pad, c_lang));
181        prompt.push_back(sum_row(tts_pad, c_think_e));
182        if (!speaker_embd.empty()) prompt.push_back(sum_vec(tts_pad, speaker_embd));
183        prompt.push_back(sum_row(tts_bos, codec_pad));
184        for (int i = 3; i < n_ids - 5; i++) prompt.push_back(sum_row(ids[(size_t) i], codec_pad));
185        prompt.push_back(sum_row(tts_eos, codec_pad));
186        prompt.push_back(sum_row(tts_pad, codec_bos));
187
188        n_prompt = (int) prompt.size();
189
190        // the talker uses the qwen3vl interleaved mrope, all sections are equal for a text/codec stream
191        mrope = llama_model_rope_type(model) == LLAMA_ROPE_TYPE_MROPE ||
192                llama_model_rope_type(model) == LLAMA_ROPE_TYPE_IMROPE;
193        const int n_pos_per_embd = mrope ? 4 : 1;
194
195        prompt_embd_buf.resize((size_t) n_prompt * (size_t) n_e);
196        for (int i = 0; i < n_prompt; i++) {
197            memcpy(prompt_embd_buf.data() + (size_t) i * n_e, prompt[(size_t) i].data(), (size_t) n_e * sizeof(float));
198        }
199
200        prompt_batch.reset(new decode_embd_batch(prompt_embd_buf.data(), n_prompt, n_pos_per_embd, n_e));
201        if (mrope) prompt_batch->set_position_mrope_1d(0, seq_id);
202        else       prompt_batch->set_position_normal  (0, seq_id);
203        prompt_pos = 0;
204
205        pos = 0;
206        const mtmd_gen_inp def = mtmd_gen_inp_default(mctx);
207        top_k = inp->top_k > 0 ? inp->top_k : def.top_k;
208        top_p = inp->top_p > 0 ? inp->top_p : def.top_p;
209        seed  = inp->seed;
210        out_type = inp->out_type;
211
212        // the prompt above holds the whole text stream up to tts_eos, so every generated
213        // frame adds tts_pad on top of the codes embedding
214        overlay = row(tts_pad);
215
216        return 0;
217    }
218
219    int32_t step_prompt(int32_t n_batch) override {
220        GGML_ASSERT(n_batch > 0);
221        if (prompt_pos >= n_prompt) {
222            return 0;
223        }
224        const int32_t n_tokens_batch = std::min(n_batch, n_prompt - prompt_pos);
225        llama_batch batch_view = prompt_batch->get_view(prompt_pos, n_tokens_batch);
226
227        const bool is_last_batch = (prompt_pos + n_tokens_batch) == n_prompt;
228        if (is_last_batch) {
229            batch_view.logits[n_tokens_batch - 1] = 1;
230        }
231
232        if (llama_decode(lctx, batch_view) != 0) {
233            LOG_ERR("mtmd_helper_gen_audio: prompt decode failed\n");
234            return -1;
235        }
236
237        pos        += n_tokens_batch;
238        prompt_pos += n_tokens_batch;
239
240        if (prompt_pos >= n_prompt) {
241            // prompt fully processed, its embedding buffer is no longer needed
242            prompt_batch.reset();
243            prompt_embd_buf.clear();
244            return 0;
245        }
246        return n_prompt - prompt_pos;
247    }
248
249    int32_t step_gen(llama_token sampled, const float * h_state_in, const float ** h_state_out, bool * out_stop) override {
250        if (sampled == LLAMA_TOKEN_NULL) {
251            LOG_ERR("mtmd_helper_gen_audio: qwen3tts requires a token sampled from the backbone\n");
252            return 1;
253        }
254
255        // backbone signals end-of-speech with a token, no frame for this step
256        if (sampled == codec_eos || llama_vocab_is_eog(vocab, sampled)) {
257            *out_stop    = true;
258            *h_state_out = nullptr;
259            return 0;
260        }
261
262        mtmd_gen_inp inp = mtmd_gen_inp_default(mctx);
263        inp.type  = MTMD_GEN_PROCESS_TYPE_GEN_CODE;
264        inp.code0 = sampled - codec_0;
265        inp.embd  = const_cast<float *>(h_state_in);
266        inp.top_k = top_k;
267        inp.top_p = top_p;
268        inp.seed  = seed;
269        mtmd_gen_out out{};
270        if (mtmd_gen_audio_process(mctx, &inp, &out) != 0) {
271            LOG_ERR("mtmd_helper_gen_audio: gen_code process failed\n");
272            return 1;
273        }
274
275        codes_buf.insert(codes_buf.end(), out.codes, out.codes + out.n_codes);
276        if (out.n_codes > 0 && codes_buf.size() / out.n_codes >= window_frames) {
277            if (!flush_gen_wav()) {
278                return 1;
279            }
280        }
281
282        std::vector<float> fb(out.embd, out.embd + n_embd);
283        for (int i = 0; i < n_embd; i++) fb[(size_t) i] += overlay[(size_t) i];
284
285        const int n_pos_per_embd = mrope ? 4 : 1;
286        decode_embd_batch batch_embd(fb.data(), 1, n_pos_per_embd, n_embd);
287        if (mrope) batch_embd.set_position_mrope_1d(pos, seq_id);
288        else       batch_embd.set_position_normal  (pos, seq_id);
289        batch_embd.batch.logits[0] = 1;
290        pos++;
291
292        if (llama_decode(lctx, batch_embd.batch) != 0) {
293            LOG_ERR("mtmd_helper_gen_audio: decode failed\n");
294            return 1;
295        }
296
297        const float * he = llama_get_embeddings_ith(lctx, -1);
298        h_state_buf.assign(he, he + n_embd);
299        *h_state_out = h_state_buf.data();
300
301        return 0;
302    }
303
304    int32_t get_output(int32_t * out_sample_rate, const char ** out_data, size_t * out_data_len, int64_t * out_n_samples) override {
305        if (!flush_gen_wav()) {
306            return 1;
307        }
308
309        *out_sample_rate = info.sample_rate;
310        if (out_n_samples) {
311            *out_n_samples = (int64_t) audio_pcm.size();
312        }
313
314        if (out_type == MTMD_HELPER_GEN_AUDIO_OUTTYPE_PCM) {
315            *out_data     = (const char *) audio_pcm.data();
316            *out_data_len = audio_pcm.size() * sizeof(float);
317            return 0;
318        }
319
320        out_buf.clear();
321        if (!write_wav16(out_buf, audio_pcm, info.sample_rate)) {
322            LOG_ERR("mtmd_helper_gen_audio: output too large for WAV\n");
323            return 1;
324        }
325        *out_data     = out_buf.data();
326        *out_data_len = out_buf.size();
327        return 0;
328    }
329
330private:
331    bool ensure_cache() {
332        if (specials_ok) {
333            return true;
334        }
335        codec_0   = find_special_token(vocab, "<|codec_0|>");
336        codec_bos = find_special_token(vocab, "<|codec_bos|>");
337        codec_eos = find_special_token(vocab, "<|codec_eos_token|>");
338        codec_pad = find_special_token(vocab, "<|codec_pad|>");
339        c_think   = find_special_token(vocab, "<|codec_think|>");
340        c_think_b = find_special_token(vocab, "<|codec_think_bos|>");
341        c_think_e = find_special_token(vocab, "<|codec_think_eos|>");
342        tts_pad   = find_special_token(vocab, "<tts_pad>");
343        tts_bos   = find_special_token(vocab, "<tts_text_bos>");
344        tts_eos   = find_special_token(vocab, "<tts_text_eod>");
345        for (llama_token t : { codec_0, codec_bos, codec_eos, codec_pad,
346                               c_think, c_think_b, c_think_e,
347                               tts_pad, tts_bos, tts_eos }) {
348            if (t == LLAMA_TOKEN_NULL) {
349                LOG_ERR("mtmd_helper_gen_audio: missing a required special token in vocab\n");
350                return false;
351            }
352        }
353        const uint32_t n_tok_embd = llama_model_get_tok_embd(model, nullptr);
354        if (n_tok_embd == 0) {
355            LOG_ERR("mtmd_helper_gen_audio: model has no token embeddings\n");
356            return false;
357        }
358        tok_embd.resize(n_tok_embd);
359        if (llama_model_get_tok_embd(model, tok_embd.data()) != n_tok_embd) {
360            LOG_ERR("mtmd_helper_gen_audio: token embedding copy failed\n");
361            return false;
362        }
363        specials_ok = true;
364        return true;
365    }
366
367    // runs the reference wav through the speaker encoder, returns one x-vector embedding row
368    bool encode_speaker(mtmd_bitmap * bitmap, std::vector<float> & out) {
369        if (!mtmd_support_audio(mctx)) {
370            LOG_ERR("mtmd_helper_gen_audio: mmproj has no speaker/audio encoder\n");
371            return false;
372        }
373        const std::string  marker = mtmd_default_marker();
374        mtmd_input_text     text{ marker.c_str(), marker.size(), false, true };
375        mtmd_input_chunks * chunks = mtmd_input_chunks_init();
376        const mtmd_bitmap * bptr = bitmap;
377        bool ok = mtmd_tokenize(mctx, chunks, &text, &bptr, 1) == 0;
378        if (ok) {
379            ok = false;
380            for (size_t i = 0; i < mtmd_input_chunks_size(chunks); i++) {
381                const mtmd_input_chunk * chunk = mtmd_input_chunks_get(chunks, i);
382                if (mtmd_input_chunk_get_type(chunk) != MTMD_INPUT_CHUNK_TYPE_AUDIO) {
383                    continue;
384                }
385                if (mtmd_encode_chunk(mctx, chunk) != 0) {
386                    LOG_ERR("mtmd_helper_gen_audio: speaker encode failed\n");
387                    break;
388                }
389                const float * embd = mtmd_get_output_embd(mctx);
390                const size_t  n = (size_t) llama_model_n_embd_inp(model) * mtmd_input_chunk_get_n_tokens(chunk);
391                out.assign(embd, embd + n);
392                ok = true;
393                break;
394            }
395        }
396        mtmd_input_chunks_free(chunks);
397        return ok;
398    }
399
400    // one GEN_WAV process() call over the buffered codes, state is carried across batches
401    bool flush_gen_wav() {
402        if (codes_buf.empty()) {
403            return true;
404        }
405        mtmd_gen_inp inp = mtmd_gen_inp_default(mctx);
406        inp.type       = MTMD_GEN_PROCESS_TYPE_GEN_WAV;
407        inp.codes      = codes_buf.data();
408        inp.n_codes    = codes_buf.size();
409        inp.seed       = seed; // same seed as gen_code, else clip reseeds mid-generation
410        inp.state_data = c2w_state.empty() ? nullptr : (const char *) c2w_state.data();
411        inp.state_size = c2w_state.size();
412        mtmd_gen_out out{};
413        if (mtmd_gen_audio_process(mctx, &inp, &out) != 0) {
414            LOG_ERR("mtmd_helper_gen_audio: gen_wav process failed\n");
415            return false;
416        }
417        audio_pcm.insert(audio_pcm.end(), out.audio, out.audio + out.n_samples);
418        c2w_state.assign(out.state_data, out.state_data + out.state_size);
419        codes_buf.clear();
420        return true;
421    }
422
423    // vocab specials fixed across the whole session, looked up once
424    bool specials_ok = false;
425    llama_token codec_0    = LLAMA_TOKEN_NULL;
426    llama_token codec_bos  = LLAMA_TOKEN_NULL;
427    llama_token codec_eos  = LLAMA_TOKEN_NULL;
428    llama_token codec_pad  = LLAMA_TOKEN_NULL;
429    llama_token c_think    = LLAMA_TOKEN_NULL;
430    llama_token c_think_b  = LLAMA_TOKEN_NULL;
431    llama_token c_think_e  = LLAMA_TOKEN_NULL;
432    llama_token tts_pad    = LLAMA_TOKEN_NULL;
433    llama_token tts_bos    = LLAMA_TOKEN_NULL;
434    llama_token tts_eos    = LLAMA_TOKEN_NULL;
435    std::vector<float> tok_embd; // whole token embedding matrix, n_vocab * n_embd
436
437    // must match hparams.wav_tfm_swa hardcoded in clip.cpp
438    size_t window_frames = 72;
439
440    // per-generation state, cleared by reset()
441    llama_seq_id seq_id = 0;
442    bool mrope = false;
443    int pos = 0;
444    // prompt decode state, consumed batch-by-batch by step_prompt()
445    std::vector<float> prompt_embd_buf;
446    std::unique_ptr<decode_embd_batch> prompt_batch;
447    int n_prompt = 0;
448    int prompt_pos = 0;
449    int32_t  top_k = 50;
450    float    top_p = 1.0f;
451    uint32_t seed  = UINT32_MAX;
452    std::vector<int32_t> codes_buf;
453    std::vector<uint8_t> c2w_state;
454    std::vector<float>   audio_pcm;
455    std::vector<float> overlay;
456    std::vector<float> h_state_buf;
457    mtmd_helper_gen_audio_outtype out_type = MTMD_HELPER_GEN_AUDIO_OUTTYPE_WAV;
458    std::vector<char> out_buf;
459};
460
461// settings that only live in the reference's per-pack yaml, not in the checkpoint
462// the english packs share the same shapes and tokenizer, but disagree on these
463// all three are 0 / false when the pack does not tune them, the model default is then used
464struct pockettts_pack_settings {
465    float temp             = 0.0f;
466    int   frames_after_eos = 0;
467    bool  pad_short_text   = false;
468};
469
470static pockettts_pack_settings pockettts_pack(const char * variant) {
471    static const std::unordered_map<std::string, pockettts_pack_settings> packs = {
472        { "english",         { 0.3f, 0, false } },
473        { "english_2026-01", { 0.7f, 0, true  } },
474        { "english_2026-04", { 0.3f, 0, false } },
475        { "french_24l",      { 0.7f, 8, false } },
476    };
477    auto it = packs.find(variant ? variant : "");
478    if (it == packs.end()) {
479        LOG_WRN("mtmd_helper_gen_audio: no tuned settings for pocket-tts variant \"%s\"\n",
480                variant ? variant : "");
481        return {};
482    }
483    return it->second;
484}
485
486// pocket-tts: the backbone emits no token, the flow net turns each hidden state into a latent
487// the end-of-speech head also lives in the mmproj
488class pockettts_gen_audio_pipeline : public mtmd_gen_audio_pipeline {
489public:
490    using mtmd_gen_audio_pipeline::mtmd_gen_audio_pipeline;
491
492    void reset() override {
493        seq_id = 0;
494        pos = 0;
495        feats_buf.clear();
496        dec_state.clear();
497        audio_pcm.clear();
498        h_state_buf.clear();
499        out_buf.clear();
500        prompt_embd_buf.clear();
501        prompt_batch.reset();
502        n_prompt = 0;
503        prompt_pos = 0;
504        step_idx = 0;
505        eos_step = -1;
506        chunks.clear();
507        chunk_idx = 0;
508        n_voice_pos = 0;
509        chunk_budget = 0;
510    }
511
512    int32_t set_input(const mtmd_helper_gen_audio_inp * inp) override {
513        reset();
514        seq_id = inp->seq_id;
515
516        if (!ensure_cache()) {
517            return 1;
518        }
519
520        std::vector<float> voice;
521        if (inp->speaker_ref) {
522            if (!encode_speaker(inp->speaker_ref, voice)) {
523                return 1;
524            }
525        }
526
527        pack = pockettts_pack(info.model_variant);
528
529        const std::string text = prepare_text(std::string(inp->prompt, inp->prompt_len),
530                                              pack.pad_short_text);
531        if (text.empty()) {
532            LOG_ERR("mtmd_helper_gen_audio: empty prompt\n");
533            return 1;
534        }
535
536        std::vector<llama_token> ids(text.size() + 16);
537        int n_ids = llama_tokenize(vocab, text.c_str(), (int32_t) text.size(), ids.data(),
538                                   (int32_t) ids.size(), false, false);
539        if (n_ids <= 0) {
540            LOG_ERR("mtmd_helper_gen_audio: tokenization failed\n");
541            return 1;
542        }
543        ids.resize((size_t) n_ids);
544
545        // long inputs degrade badly, so each chunk restarts from the voice conditioning
546        // see split_into_best_sentences() in the reference
547        chunks = split_chunks(ids);
548        chunk_idx = 0;
549        if (chunks.size() > 1) {
550            LOG_INF("mtmd_helper_gen_audio: %d tokens split into %zu chunks\n", n_ids, chunks.size());
551        }
552
553        const int n_e = n_embd;
554
555        // sequence order is voice, then text, then the audio BOS that starts generation
556        if (!voice.empty()) {
557            GGML_ASSERT(voice.size() % (size_t) n_e == 0);
558            if (bos_before_voice != LLAMA_TOKEN_NULL) {
559                push_embd_row(prompt_embd_buf, bos_before_voice);
560            }
561            prompt_embd_buf.insert(prompt_embd_buf.end(), voice.begin(), voice.end());
562        }
563        // every later chunk rewinds to here and re-prompts, so the voice stays primed
564        n_voice_pos = (int) (prompt_embd_buf.size() / (size_t) n_e);
565
566        for (llama_token t : chunks[0]) {
567            push_embd_row(prompt_embd_buf, t);
568        }
569        push_embd_row(prompt_embd_buf, audio_bos);
570        arm_chunk_budget(0);
571
572        n_prompt = (int) (prompt_embd_buf.size() / (size_t) n_e);
573        prompt_batch.reset(new decode_embd_batch(prompt_embd_buf.data(), n_prompt, 1, n_e));
574        prompt_batch->set_position_normal(0, seq_id);
575        prompt_pos = 0;
576
577        seed     = inp->seed;
578        out_type = inp->out_type;
579
580        return 0;
581    }
582
583    int32_t step_prompt(int32_t n_batch) override {
584        GGML_ASSERT(n_batch > 0);
585        if (prompt_pos >= n_prompt) {
586            return 0;
587        }
588        const int32_t n_tokens_batch = std::min(n_batch, n_prompt - prompt_pos);
589        llama_batch batch_view = prompt_batch->get_view(prompt_pos, n_tokens_batch);
590
591        if ((prompt_pos + n_tokens_batch) == n_prompt) {
592            batch_view.logits[n_tokens_batch - 1] = 1;
593        }
594
595        if (llama_decode(lctx, batch_view) != 0) {
596            LOG_ERR("mtmd_helper_gen_audio: prompt decode failed\n");
597            return -1;
598        }
599
600        pos        += n_tokens_batch;
601        prompt_pos += n_tokens_batch;
602
603        if (prompt_pos >= n_prompt) {
604            prompt_batch.reset();
605            prompt_embd_buf.clear();
606            return 0;
607        }
608        return n_prompt - prompt_pos;
609    }
610
611    int32_t step_gen(llama_token sampled, const float * h_state_in, const float ** h_state_out, bool * out_stop) override {
612        (void) sampled; // the backbone output is continuous, there is no token to consume
613
614        mtmd_gen_inp inp = mtmd_gen_inp_default(mctx);
615        inp.type = MTMD_GEN_PROCESS_TYPE_GEN_CODE;
616        inp.embd = const_cast<float *>(h_state_in);
617        // clip only reseeds when the seed changes, so pass the same one on every step
618        inp.seed = seed;
619        if (pack.temp > 0.0f) {
620            inp.temp = pack.temp;
621        }
622        mtmd_gen_out out{};
623        if (mtmd_gen_audio_process(mctx, &inp, &out) != 0) {
624            LOG_ERR("mtmd_helper_gen_audio: flow decode failed\n");
625            return 1;
626        }
627        if (out.is_eos && eos_step < 0) {
628            eos_step = step_idx;
629        }
630        // the frame of the stopping step is discarded, matching _autoregressive_generation().
631        // the budget is the reference's fallback for a chunk whose eos head never fires
632        const bool chunk_done = (eos_step >= 0 && step_idx >= eos_step + frames_after_eos) ||
633                                step_idx >= chunk_budget;
634        if (chunk_done) {
635            if (eos_step < 0) {
636                LOG_WRN("mtmd_helper_gen_audio: chunk %zu hit its budget without end-of-speech\n", chunk_idx);
637            }
638            return finish_chunk(h_state_out, out_stop);
639        }
640
641        feats_buf.insert(feats_buf.end(), out.feats, out.feats + out.n_feats);
642        step_idx++;
643        if (out.n_feats > 0 && feats_buf.size() / out.n_feats >= window_frames) {
644            if (!flush_gen_wav()) {
645                return 1;
646            }
647        }
648
649        decode_embd_batch batch_embd(const_cast<float *>(out.embd), 1, 1, n_embd);
650        batch_embd.set_position_normal(pos, seq_id);
651        batch_embd.batch.logits[0] = 1;
652        pos++;
653
654        if (llama_decode(lctx, batch_embd.batch) != 0) {
655            LOG_ERR("mtmd_helper_gen_audio: decode failed\n");
656            return 1;
657        }
658
659        const float * he = llama_get_embeddings_ith(lctx, -1);
660        h_state_buf.assign(he, he + n_embd);
661        *h_state_out = h_state_buf.data();
662
663        return 0;
664    }
665
666    int32_t get_output(int32_t * out_sample_rate, const char ** out_data, size_t * out_data_len, int64_t * out_n_samples) override {
667        if (!flush_gen_wav()) {
668            return 1;
669        }
670
671        *out_sample_rate = info.sample_rate;
672        if (out_n_samples) {
673            *out_n_samples = (int64_t) audio_pcm.size();
674        }
675
676        if (out_type == MTMD_HELPER_GEN_AUDIO_OUTTYPE_PCM) {
677            *out_data     = (const char *) audio_pcm.data();
678            *out_data_len = audio_pcm.size() * sizeof(float);
679            return 0;
680        }
681
682        out_buf.clear();
683        if (!write_wav16(out_buf, audio_pcm, info.sample_rate)) {
684            LOG_ERR("mtmd_helper_gen_audio: output too large for WAV\n");
685            return 1;
686        }
687        *out_data     = out_buf.data();
688        *out_data_len = out_buf.size();
689        return 0;
690    }
691
692private:
693    bool ensure_cache() {
694        if (specials_ok) {
695            return true;
696        }
697        // bos_before_voice is optional, some packs do not insert it
698        bos_before_voice = find_special_token(vocab, "<|bos_before_voice|>");
699        audio_bos        = find_special_token(vocab, "<|audio_bos|>");
700        if (audio_bos == LLAMA_TOKEN_NULL) {
701            LOG_ERR("mtmd_helper_gen_audio: missing <|audio_bos|> in vocab\n");
702            return false;
703        }
704        const uint32_t n_tok_embd = llama_model_get_tok_embd(model, nullptr);
705        if (n_tok_embd == 0) {
706            LOG_ERR("mtmd_helper_gen_audio: model has no token embeddings\n");
707            return false;
708        }
709        tok_embd.resize(n_tok_embd);
710        if (llama_model_get_tok_embd(model, tok_embd.data()) != n_tok_embd) {
711            LOG_ERR("mtmd_helper_gen_audio: token embedding copy failed\n");
712            return false;
713        }
714        GGML_ASSERT(n_embd > 0 && n_tok_embd % (uint32_t) n_embd == 0);
715        specials_ok = true;
716        return true;
717    }
718
719    // the table can be shorter than the vocab, so bound the row lookup
720    void push_embd_row(std::vector<float> & dst, llama_token t) const {
721        const size_t n_rows = tok_embd.size() / (size_t) n_embd;
722        GGML_ASSERT(t >= 0 && (size_t) t < n_rows);
723        dst.insert(dst.end(),
724                   tok_embd.begin() + (size_t) t * n_embd,
725                   tok_embd.begin() + (size_t) (t + 1) * n_embd);
726    }
727
728    // token ids of the pieces the reference splits on, see split_into_best_sentences().
729    // the leading token is dropped, it is the tokenizer's dummy prefix
730    std::vector<llama_token> punct_ids(const char * s) const {
731        std::vector<llama_token> ids(16);
732        const int n = llama_tokenize(vocab, s, (int32_t) strlen(s), ids.data(), (int32_t) ids.size(), false, false);
733        if (n <= 1) {
734            return {};
735        }
736        return std::vector<llama_token>(ids.begin() + 1, ids.begin() + n);
737    }
738
739    // cut after runs of boundary tokens, so punctuation stays with the sentence it ends
740    static std::vector<std::vector<llama_token>> split_on(const std::vector<llama_token> & ids,
741                                                          const std::vector<llama_token> & boundary) {
742        std::vector<std::vector<llama_token>> out;
743        size_t start = 0;
744        bool prev_was_boundary = false;
745        for (size_t i = 0; i < ids.size(); i++) {
746            const bool is_boundary = std::find(boundary.begin(), boundary.end(), ids[i]) != boundary.end();
747            if (!is_boundary && prev_was_boundary) {
748                out.emplace_back(ids.begin() + start, ids.begin() + i);
749                start = i;
750            }
751            prev_was_boundary = is_boundary;
752        }
753        out.emplace_back(ids.begin() + start, ids.end());
754        return out;
755    }
756
757    std::vector<std::vector<llama_token>> split_chunks(const std::vector<llama_token> & ids) const {
758        if ((int) ids.size() <= max_chunk_tokens) {
759            return { ids };
760        }
761        const std::vector<llama_token> eos_punct = punct_ids(".!...?");
762        const std::vector<llama_token> mid_punct = punct_ids(",;:");
763
764        // oversized sentences are split again on weaker punctuation, else words get skipped
765        std::vector<std::vector<llama_token>> segments;
766        for (auto & seg : split_on(ids, eos_punct)) {
767            if ((int) seg.size() <= max_chunk_tokens) {
768                segments.push_back(std::move(seg));
769                continue;
770            }
771            auto sub = split_on(seg, mid_punct);
772            if (sub.size() > 1) {
773                for (auto & s : sub) {
774                    segments.push_back(std::move(s));
775                }
776            } else {
777                segments.push_back(std::move(seg));
778            }
779        }
780
781        std::vector<std::vector<llama_token>> out;
782        for (auto & seg : segments) {
783            if (seg.empty()) {
784                continue;
785            }
786            if (!out.empty() && (int) (out.back().size() + seg.size()) <= max_chunk_tokens) {
787                out.back().insert(out.back().end(), seg.begin(), seg.end());
788            } else {
789                out.push_back(std::move(seg));
790            }
791        }
792        if (out.empty()) {
793            out.push_back(ids);
794        }
795        for (const auto & c : out) {
796            if ((int) c.size() > max_chunk_tokens) {
797                LOG_WRN("mtmd_helper_gen_audio: chunk of %zu tokens exceeds the %d token budget, "
798                        "generation may skip words\n", c.size(), max_chunk_tokens);
799            }
800        }
801        return out;
802    }
803
804    // _estimate_max_gen_len() plus the per-chunk tail guess, both in frames
805    void arm_chunk_budget(size_t idx) {
806        const int n_tok = (int) chunks[idx].size();
807        chunk_budget = (int) std::ceil((n_tok / 3.0 + 2.0) * frame_rate);
808        // the pack may pin the tail, else the reference guesses it from the word count
809        frames_after_eos = pack.frames_after_eos > 0 ? pack.frames_after_eos : (n_tok <= 6 ? 5 : 3);
810        step_idx = 0;
811        eos_step = -1;
812    }
813
814    // ends the current chunk and, if there is another, re-prompts it on top of the voice
815    int32_t finish_chunk(const float ** h_state_out, bool * out_stop) {
816        if (!flush_gen_wav()) {
817            return 1;
818        }
819        // the decoder restarts too, the next chunk's audio is not continuous with this one
820        dec_state.clear();
821
822        if (chunk_idx + 1 >= chunks.size()) {
823            *out_stop    = true;
824            *h_state_out = nullptr;
825            return 0;
826        }
827        chunk_idx++;
828
829        // drop this chunk's text and audio, keep the voice conditioning
830        llama_memory_seq_rm(llama_get_memory(lctx), seq_id, n_voice_pos, -1);
831        pos = n_voice_pos;
832
833        const int n_e = n_embd;
834        prompt_embd_buf.clear();
835        for (llama_token t : chunks[chunk_idx]) {
836            push_embd_row(prompt_embd_buf, t);
837        }
838        push_embd_row(prompt_embd_buf, audio_bos);
839        arm_chunk_budget(chunk_idx);
840
841        const int n_rows = (int) (prompt_embd_buf.size() / (size_t) n_e);
842        GGML_ASSERT(n_rows > 0);
843        decode_embd_batch batch(prompt_embd_buf.data(), n_rows, 1, n_e);
844        batch.set_position_normal(pos, seq_id);
845        batch.batch.logits[n_rows - 1] = 1;
846        if (llama_decode(lctx, batch.batch) != 0) {
847            LOG_ERR("mtmd_helper_gen_audio: chunk prompt decode failed\n");
848            return 1;
849        }
850        pos += n_rows;
851        prompt_embd_buf.clear();
852
853        const float * he = llama_get_embeddings_ith(lctx, -1);
854        h_state_buf.assign(he, he + n_embd);
855        *h_state_out = h_state_buf.data();
856        *out_stop    = false;
857        return 0;
858    }
859
860    // same normalization as prepare_text_prompt() in the reference, it affects quality
861    static std::string prepare_text(const std::string & in, bool pad_short) {
862        std::string s;
863        s.reserve(in.size() + 1);
864        for (char c : in) {
865            if (c == '\n' || c == '\r') {
866                s += ' ';
867            } else if (c == ';') {
868                s += ',';
869            } else {
870                s += c;
871            }
872        }
873        const size_t b = s.find_first_not_of(' ');
874        const size_t e = s.find_last_not_of(' ');
875        if (b == std::string::npos) {
876            return "";
877        }
878        s = s.substr(b, e - b + 1);
879        if (s[0] >= 'a' && s[0] <= 'z') {
880            s[0] = (char) (s[0] - 'a' + 'A');
881        }
882        const unsigned char last = (unsigned char) s.back();
883        if (std::isalnum(last)) {
884            s += '.';
885        }
886        if (pad_short && count_words(s) < 5) {
887            s = std::string(8, ' ') + s;
888        }
889        return s;
890    }
891
892    static int count_words(const std::string & s) {
893        int n = 0;
894        bool in_word = false;
895        for (char c : s) {
896            if (c == ' ') {
897                in_word = false;
898            } else if (!in_word) {
899                in_word = true;
900                n++;
901            }
902        }
903        return n;
904    }
905
906    // runs the reference wav through the mimi encoder, returns one row per 12.5Hz frame
907    bool encode_speaker(mtmd_bitmap * bitmap, std::vector<float> & out) {
908        if (!mtmd_support_audio(mctx)) {
909            LOG_ERR("mtmd_helper_gen_audio: mmproj has no voice encoder\n");
910            return false;
911        }
912        const std::string  marker = mtmd_default_marker();
913        mtmd_input_text     text{ marker.c_str(), marker.size(), false, true };
914        mtmd_input_chunks * chunks = mtmd_input_chunks_init();
915        const mtmd_bitmap * bptr = bitmap;
916        bool ok = mtmd_tokenize(mctx, chunks, &text, &bptr, 1) == 0;
917        if (ok) {
918            ok = false;
919            for (size_t i = 0; i < mtmd_input_chunks_size(chunks); i++) {
920                const mtmd_input_chunk * chunk = mtmd_input_chunks_get(chunks, i);
921                if (mtmd_input_chunk_get_type(chunk) != MTMD_INPUT_CHUNK_TYPE_AUDIO) {
922                    continue;
923                }
924                if (mtmd_encode_chunk(mctx, chunk) != 0) {
925                    LOG_ERR("mtmd_helper_gen_audio: voice encode failed\n");
926                    break;
927                }
928                const float * embd = mtmd_get_output_embd(mctx);
929                const size_t  n = (size_t) llama_model_n_embd_inp(model) * mtmd_input_chunk_get_n_tokens(chunk);
930                out.assign(embd, embd + n);
931                ok = true;
932                break;
933            }
934        }
935        mtmd_input_chunks_free(chunks);
936        return ok;
937    }
938
939    // decodes the buffered latents, the mimi decoder state carries over between calls
940    bool flush_gen_wav() {
941        if (feats_buf.empty()) {
942            return true;
943        }
944        mtmd_gen_inp inp = mtmd_gen_inp_default(mctx);
945        inp.type       = MTMD_GEN_PROCESS_TYPE_GEN_WAV;
946        inp.feats      = feats_buf.data();
947        inp.n_feats    = feats_buf.size();
948        inp.seed       = seed;
949        inp.state_data = dec_state.empty() ? nullptr : (const char *) dec_state.data();
950        inp.state_size = dec_state.size();
951        mtmd_gen_out out{};
952        if (mtmd_gen_audio_process(mctx, &inp, &out) != 0) {
953            LOG_ERR("mtmd_helper_gen_audio: mimi decode failed\n");
954            return false;
955        }
956        audio_pcm.insert(audio_pcm.end(), out.audio, out.audio + out.n_samples);
957        dec_state.assign(out.state_data, out.state_data + out.state_size);
958        feats_buf.clear();
959        return true;
960    }
961
962    pockettts_pack_settings pack;
963    bool specials_ok = false;
964    llama_token bos_before_voice = LLAMA_TOKEN_NULL;
965    llama_token audio_bos        = LLAMA_TOKEN_NULL;
966    std::vector<float> tok_embd;
967
968    llama_seq_id seq_id = 0;
969    int pos = 0;
970    std::vector<float> prompt_embd_buf;
971    std::unique_ptr<decode_embd_batch> prompt_batch;
972    int n_prompt = 0;
973    int prompt_pos = 0;
974    uint32_t seed = UINT32_MAX;
975    // end-of-speech is latched, then a few more frames are generated as tail padding
976    int step_idx = 0;
977    int eos_step = -1;
978    int frames_after_eos = 3;
979    static constexpr int max_chunk_tokens = 50;  // MAX_TOKEN_PER_CHUNK in the reference
980    static constexpr double frame_rate    = 12.5;
981    std::vector<std::vector<llama_token>> chunks;
982    size_t chunk_idx   = 0;
983    int    n_voice_pos = 0; // KV positions held by the voice conditioning
984    int    chunk_budget = 0;
985
986    // latents are decoded a window at a time, the decoder state bridges the windows
987    size_t window_frames = 8;
988    std::vector<float>   feats_buf;
989    std::vector<uint8_t> dec_state;
990    std::vector<float>   audio_pcm;
991    std::vector<float>   h_state_buf;
992    mtmd_helper_gen_audio_outtype out_type = MTMD_HELPER_GEN_AUDIO_OUTTYPE_WAV;
993    std::vector<char> out_buf;
994};
995
996static std::unique_ptr<mtmd_gen_audio_pipeline> make_pipeline(llama_context * lctx, mtmd_context * mctx) {
997    switch (mtmd_gen_audio_get_info(mctx).type) {
998        case MTMD_GEN_AUDIO_TYPE_QWEN3TTS:
999            return std::unique_ptr<mtmd_gen_audio_pipeline>(new qwen3tts_gen_audio_pipeline(lctx, mctx));
1000        case MTMD_GEN_AUDIO_TYPE_POCKETTTS:
1001            return std::unique_ptr<mtmd_gen_audio_pipeline>(new pockettts_gen_audio_pipeline(lctx, mctx));
1002        default:
1003            return nullptr;
1004    }
1005}
1006
1007struct mtmd_helper_gen_audio {
1008    std::unique_ptr<mtmd_gen_audio_pipeline> pipeline;
1009};
1010
1011mtmd_helper_gen_audio * mtmd_helper_gen_audio_init(struct llama_context * lctx, struct mtmd_context * mctx) {
1012    auto * ctx = new mtmd_helper_gen_audio();
1013    ctx->pipeline = make_pipeline(lctx, mctx);
1014    return ctx;
1015}
1016
1017void mtmd_helper_gen_audio_free(mtmd_helper_gen_audio * ctx) {
1018    delete ctx;
1019}
1020
1021void mtmd_helper_gen_audio_reset(mtmd_helper_gen_audio * ctx) {
1022    if (ctx->pipeline) {
1023        ctx->pipeline->reset();
1024    }
1025}
1026
1027int32_t mtmd_helper_gen_audio_set_input(mtmd_helper_gen_audio * ctx, const mtmd_helper_gen_audio_inp * inp) {
1028    if (!ctx->pipeline) {
1029        LOG_ERR("mtmd_helper_gen_audio: unsupported or missing gen-audio pipeline\n");
1030        return 1;
1031    }
1032    return ctx->pipeline->set_input(inp);
1033}
1034
1035int32_t mtmd_helper_gen_audio_step_prompt(mtmd_helper_gen_audio * ctx, int32_t n_batch) {
1036    if (!ctx->pipeline) {
1037        return -1;
1038    }
1039    return ctx->pipeline->step_prompt(n_batch);
1040}
1041
1042int32_t mtmd_helper_gen_audio_step_gen(mtmd_helper_gen_audio * ctx, llama_token sampled,
1043                                       const float * h_state_in, const float ** h_state_out,
1044                                       bool * out_stop) {
1045    if (!ctx->pipeline) {
1046        return 1;
1047    }
1048    bool stop = false;
1049    const int32_t ret = ctx->pipeline->step_gen(sampled, h_state_in, h_state_out, &stop);
1050    if (out_stop) {
1051        *out_stop = stop;
1052    }
1053    return ret;
1054}
1055
1056int32_t mtmd_helper_gen_audio_get_output(mtmd_helper_gen_audio * ctx, int32_t * out_sample_rate,
1057                                         const char ** out_data, size_t * out_data_len, int64_t * out_n_samples) {
1058    if (!ctx->pipeline) {
1059        return 1;
1060    }
1061    return ctx->pipeline->get_output(out_sample_rate, out_data, out_data_len, out_n_samples);
1062}