Skip to content

Xsn/pocket tts - #108

Draft
ngxson wants to merge 18 commits into
ngxson:masterfrom
ggml-org:xsn/pocket-tts
Draft

Xsn/pocket tts#108
ngxson wants to merge 18 commits into
ngxson:masterfrom
ggml-org:xsn/pocket-tts

Conversation

@ngxson

@ngxson ngxson commented Aug 5, 2026

Copy link
Copy Markdown
Owner

For discussion

@coderabbitai

This comment was marked as resolved.

@ServeurpersoCom

Copy link
Copy Markdown

Okay, we just need to add chunking like the reference code

No chunking:
demo-en.wav
demo-fr.wav

With chunking:
demo-en-chunked.wav
demo-fr-chunked.wav

@ServeurpersoCom

ServeurpersoCom commented Aug 5, 2026

Copy link
Copy Markdown

Cleaned LLM questions for Target architecture :

Tried this on long texts with the real gated weights and hit two failure modes: in english the pitch drifts down and the tail turns into structured noise, in french the text just gets cut halfway. Turns out the reference never feeds a whole text to the model, it splits into chunks of max 50 tokens, restarts each chunk from the voice prompt and bounds each one with a budget derived from the token count. I have a patch that does this and it fixes both cases, but a few design calls are yours to make:

  • Should the chunk budget be a GGUF hparam or a per-model constant? The reference has a TODO saying english_2026-04 supports bigger chunks.
  • Same for the 12.5 Hz frame rate I need to size the generation budget, mmproj metadata or hardcoded?
  • Does chunking belong in the helper pipeline, or in the tool so llama-server can pick its own policy?
  • The reference splits in token space, I split in text space on ASCII punctuation. Good enough? -> I follow the reference code
  • I clear the mimi decoder state at each chunk boundary like the reference does, but you wrote the streaming decoder, would you rather bridge it across chunks?
  • Should the per-chunk generation cap stay internal, or be overridable the way -n is today?

Two smaller things while I was at it: the converter raises KeyError on flow_lm.bos_before_voice for english_2026-01 and for the older root revision, and the pipeline only takes a wav reference so the precomputed embeddings shipped in the repo can't be used as voices.

ggml_conv_transpose_1d has no grouped mode, so the depthwise upsample
was built as one convolution and one concat per channel, which floods
the graph with small nodes and makes kernel launches dominate the
decoder.

Fold both cases into the column form the seanet decoder already needs:
the general case reshapes the kernel to [IC, K * OC] and matmuls it
with the input, the depthwise case batches a matmul over the channels
so a step scales its own kernel. A single col2im_1d then scatter-adds
the columns back to the signal, with the same shape as before, so the
overlap-add tail, the streaming state and the bias are untouched.

Generation time per frame drops by 80% on CUDA and by 50% on CPU. The
output matches the previous implementation sample for sample, with a
correlation of 0.999994 and identical frame counts.
@ServeurpersoCom

Copy link
Copy Markdown

Small perf commit using GEMM + COL2IM_1D: the transposed convolutions become one matmul plus a single col2im scatter-add, so the depthwise upsample stops emitting one node per channel :

case              baseline        gemm      gain
en_short_gpu    4.21 ms/frame   0.79        81.2%
en_long_gpu     3.64 ms/frame   0.73        80.0%
fr_long_gpu     4.40 ms/frame   1.51        65.7%
en_long_cpu     6.94 ms/frame   3.16        54.4%
fr_long_cpu     7.69 ms/frame   3.88        49.5%

@ServeurpersoCom

Copy link
Copy Markdown

Testing french_24l I hit another one: the flow temperature and the eos padding are per language pack in the reference, and hardcoding 0.3 for everyone makes the french model draw its noise at the wrong scale. On some seeds the eos head then never fires and one sentence runs to the generation budget, ending on seven seconds of silence. Only english and english_2026-04 are at 0.3, french_24l keeps 0.7 and asks for 8 padding frames.

My patch carries both in the mmproj as clip.gen.audio.flow_temperature and clip.gen.audio.frames_after_eos, written by the converter, optional on the loader side. Worst seed on french goes from 155 frames with 7.28s of silence to 95 frames with 0.96s. It needs existing mmproj files to be converted again, so I am holding off until you say whether you want these as GGUF metadata at all.

ngxson and others added 3 commits August 6, 2026 00:13
The language packs also tune the end-of-speech padding and the padding
of short prompts, next to the temperature already carried in the
mmproj: french_24l asks for 8 tail frames instead of the guessed 3,
english_2026-01 asks for short prompts to be padded with spaces.

Write both in the mmproj as clip.gen.audio.frames_after_eos and
clip.gen.audio.pad_short_text, keyed on the pack in the conversion
script like the temperature. The loader keeps them optional, so a
mmproj without them behaves as before. Map semicolons to commas for
every pack instead, the reference only asks for it on three of them and
it costs nothing elsewhere.

Existing mmproj files must be converted again to carry the two keys.

On a long french text the port now lands within 2% of the reference:
22.96s against 23.44s, with the same peak level and the same amount of
silence.
@ServeurpersoCom

Copy link
Copy Markdown

Two things I need from you before cleaning this up. First, the fix adds two fields to mtmd_gen_audio_info so the pipeline can see the per-pack tail length and the short-prompt padding, and that's the only place it widens the public API, so tell me if you'd rather have it some other way.

Second, still open from earlier: should the 50 token chunk budget and the 12.5 Hz frame rate come from the GGUF, or stay hardcoded on the cpp side?

@ngxson

ngxson commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

Second, still open from earlier: should the 50 token chunk budget and the 12.5 Hz frame rate come from the GGUF, or stay hardcoded on the cpp side?

IIRC it has always been the case for mimi encoder / decoder series so might be ok to hard-code it

if future model has different values, we can consider the hard-coded value as default and add gguf metadata to overwrite the default

Comment thread conversion/pockettts.py Outdated
Comment on lines +214 to +217
self.gguf_writer.add_gen_audio_frames_after_eos(
_PACK_FRAMES_AFTER_EOS.get(self.dir_model.name, 0))
self.gguf_writer.add_gen_audio_pad_short_text(
_PACK_PAD_SHORT_TEXT.get(self.dir_model.name, False))

@ngxson ngxson Aug 6, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this logic seems to be quite fragile, it's better to add a new metadata clip.gen.audio.model_variant and set it to self.dir_model.name

I'll push a commit for this change

@ngxson

ngxson commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

@ServeurpersoCom looks better now?

@ServeurpersoCom

Copy link
Copy Markdown

English

Ref python :
en-reference.wav

llama.cpp MTMD :
en-llamacpp-f32.wav

French

Ref python :
fr-reference.wav

llama.cpp MTMD :
fr-llamacpp-f32.wav

Sound good !

@ngxson

ngxson commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

nice, thanks for testing. I'll try to push this branch as upstream PR today

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants