Commit Graph

4 Commits

Author SHA1 Message Date
Richard Loyer 82d206137f chantier B TTS #8 : speaker encoder embedded + clonage in-process
SE.3+SE.4+SE.5 : ECAPA-TDNN ggml C++ bit-exact + intégration TtsEngine.

speaker_encoder.{h,cpp} :
 - API publique : speaker_encoder_load / encode_wav / encode_waveform / free
 - mel C++ (FFT radix-2, Hann, reflect pad, librosa basis) bit-exact Python
 - ECAPA-TDNN forward : conv0 + 3 SE-Res2Net + MFA + ASP + FC -> x_vector[1024]
 - validation : cos=0.9997 vs Python ref (damien_5s.wav)
 - SPK_STANDALONE -> binaire CLI kazeia_speaker_encode

tts_engine :
 - TtsEngineLoadCfg.speaker_encoder_gguf + mel_basis_path (load-time opt-in)
 - TtsSynthesizeCfg.xvector_override (per-call, RAII restore)
 - tts_engine_encode_speaker_wav / _waveform exposés

tts_pipeline : KZTTS_SPK_GGUF + KZTTS_MEL_BASIS + KZTTS_REF_WAV -> clonage
in-process (un seul binaire, plus de subprocess swap manuel).

Pièges trouvés en route (documentés CLAUDE.md project_tts_*) :
 - ggml_conv_1d exige weights F16 -> cast au call site
 - input layout : data[c*T+t] (ggml ne[0]=T fastest) pas memcpy [T,C]
 - refs Python en bytes [C,T] = ggml [T,C] côté cpp
 - RIFF reader chunk-parsing (ffmpeg = LIST/JUNK avant data)

Test E2E : 4 voix (damien/amir/elodie/zelda) clonées à partir de WAV ref 5s,
toutes audibles et distinctes.

Reste : JNI nativeSynthesizeWithReference (refactor mécanique, prochaine session).
2026-05-31 15:54:29 +02:00
Richard Loyer 930f3c8c1f chantier B TTS #11: libkazeia_tts.so + Kotlin wrapper, JNI validé bout-en-bout
Refactor: tts_engine.{h,cpp} = API publique de l'engine TTS (load une fois,
synthesize N fois, free). État load-once dans struct TtsEngine (talker
model+ctx, CPState, Decoder, KzTextTokenizer, fixtures + embeds spéciaux
+ role_proj pré-calculé). KV cache talker reset via llama_memory_clear
au début de chaque synthesize -> appels indépendants.

tts_pipeline.cpp devient un thin CLI dessus (refonte sans changement de
sortie : WAV md5 identique au pré-refactor sur 'Bonjour je m'appelle Kazeia').

JNI bridge: kazeia_tts_jni.cpp expose 3 fonctions :
  Java_com_kazeia_tts_TtsJni_nativeLoad / Synthesize / Free
Signatures alignées avec TtsEngine.kt (companion loadLibrary 'kazeia_tts').
Build: build_kazeia_tts.sh -> b-jni/libkazeia_tts.so (~900 KB).

Test_engine_2calls (sans JNI) : 3 synth sur même instance, KV reset OK,
2 appels identiques -> WAV md5 identique, appel 3 différent -> codes
différents.

Test_jni_tts (harness JNI sans VM Java) : dlopen libkazeia_tts.so, JNIEnv
mock minimal (GetStringUTFChars/Release + NewIntArray/SetIntArrayRegion),
appels load + 2 synth + free. WAV md5 identiques aux runs directs. exit=0.

Empreinte tablette mesurée: ~3.5 GB par instance (talker f32 1.6 GB + CP
f16 mixte 250 MB + decoder 325 MB + vocab Qwen3 vocab_only 50 MB + fixtures
text_embed/tp_* 1.2 GB). Une instance par process.

RTF stable autour de 3.0 (CPU 6t), inchangé vs avant refonte.

Reste sur la liste initiale : tuning sampling (itératif à l'oreille).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 21:54:21 +02:00
Richard Loyer b27c8ef403 chantier B TTS #10: tokenizer Qwen3 BPE via llama_tokenize, texte arbitraire
Plutôt que de porter Qwen3 BPE (NFC + pre-tokenize regex Unicode + ByteLevel +
merges + ignore_merges) à la main en C++ (~500 LOC risquées), on charge
n'importe quelle Qwen3 GGUF en vocab_only=true (~50 MB RAM, pas de poids) et
on appelle llama_tokenize(parse_special=true). Bit-exact garanti par
construction.

kazeia_text_tokenizer.h/cpp : API courte
  kz_tok_load(tok, gguf_path) / kz_tok_free / kz_tok_encode
  kz_tok_encode_tts_prompt(tok, content) -> enveloppe dans le template chat
    <|im_start|>assistant\n{content}<|im_end|>\n<|im_start|>assistant\n
  (déduit du dump golden : 16 tokens pour 'Bonjour je m'appelle Kazeia').

test_tokenizer : binaire standalone vérifiant bit-exact vs input_ids_full.bin
  ./test_tokenizer Qwen3-4B-Q4_0.gguf tts_dump/input_ids_full.bin 'Bonjour je m'appelle Kazeia'
  -> OK BIT-EXACT (16/16 tokens identiques au golden HF).

tts_pipeline : si KZTTS_TEXT et KZTTS_VOCAB_GGUF posés, tokenize la phrase au
lieu de lire input_ids_full.bin. La structure attendue (3 role + Nt body + 5
trailing) est exactement ce que kz_tok_encode_tts_prompt produit, donc
input_ids_len = Nt + 8 sans rien d'autre à changer dans la construction
prefill_embeds.

Mesure tablette Pad3 (cpu 6t, KZTTS_CP_CACHE=1, seed=42) :
  A fixture        : 31 frames, total 7.866s, RTF 3.04
  B tokenizer C++  : 31 frames, total 7.842s, RTF 3.04  + WAV md5 identique
  C 'Bonsoir...'   : 41 frames audio 3.4s, total 10.5s, RTF 3.09 (phrase neuve OK)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 21:21:53 +02:00
Richard Loyer 7626131d50 chantier B TTS #8: KV cache CP -40% par frame, bit-exact
cp_inference: nouvelle voie cp_predict_cached (1 prefill 2 tokens +
14 decode 1 token via KV cache f32 persistant) à côté de cp_predict
(oracle recompute conservé pour A/B). cp_forward_cached_step écrit le
K/V post-RoPE dans s.K_cache/V_cache via ggml_view_3d + ggml_cpy, et
lit le full cache [0..T_full) pour l'attention. Ordre topo garanti
par build_forward_expand(cpy) AVANT build_forward_expand(x).

tts_pipeline: switch KZTTS_CP_CACHE=0/1 (défaut 0 = oracle bit-exact).
build_tts_pipeline.sh: rebuild reproductible (CMakeLists ne couvrait
que libkazeia_engine.so).

Mesure tablette Pad3 (talker_f32+tts_dump, CPU 6t, seed=42, 31 frames):
  oracle  : CP 227.9 ms/frame, total 12.90s, RTF 4.99
  cached  : CP 137.8 ms/frame, total  8.91s, RTF 3.45
  codes_ref.bin == codes_cache.bin (cmp), out_ref.wav md5 == out_cache.wav

Speedup CP ×1.65, pas le ×8 espéré : le forward est BW-bound (~100 MB
de poids f32 streamés par sub-forward, on lit toujours 15 fois). Le
gain est sur la part compute (sum L=2..16 -> 16 token-fwd au lieu de
135). Pour aller plus loin : (a) cp_load en f16 conservé (×2 BW),
(b) fusion graphe multi-step. Decoder RTF 1.37 reste aussi à attaquer.

Bit-exactness : f32 mul_mat stable entre L=k+1 et L=1 à threads=6,
les sommations donnent les mêmes bits. Math identique, ordre identique
de fait.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 19:51:22 +02:00