kazeia/kazeia-android/RAPPORT_TTS_NPU.md

11 KiB
Raw Blame History

Rapport TTS On-Device NPU — Problèmes et Solutions

Date : 29 mars 2026 Contexte : Kazeia — chatbot émotionnel on-device sur OnePlus Pad 3 (Snapdragon 8 Elite, HTP V79) Objectif : TTS multilingue avec voice cloning, entièrement sur NPU


1. Exigences

Critère Requis
Multilingue Français + anglais minimum
Voice cloning Cloner une voix à partir d'un échantillon WAV (~5-10s)
On-device Aucun appel réseau, tout local
NPU Le composant le plus lourd doit tourner sur le HTP Qualcomm
Latence < 3s pour une phrase courte (temps réel acceptable)
Qualité Voix naturelle, intelligible, prosodie correcte

2. Candidats évalués

2.1 Chatterbox Multilingual (ONNX)

Détail
Source onnx-community/chatterbox-multilingual-ONNX
Architecture Speech Encoder (591 MB) + Embed Tokens (68 MB) + Language Model 30L (291-2000 MB) + Conditional Decoder (534 MB)
Multilingue Oui (23 langues, tag [fr], [en], etc.)
Voice cloning Oui (speaker embedding extrait de l'audio de référence)
Format ONNX (FP32, FP16, Q4F16)

2.2 Qwen3-TTS 0.6B Base (PyTorch)

Détail
Source Qwen/Qwen3-TTS-12Hz-0.6B-Base
Architecture Speaker Encoder (8.9M) + Talker LM 28L (754M) + Code Predictor 5L (141M) + Speech Decoder (114M)
Multilingue Oui (français, anglais, etc.)
Voice cloning Oui (x-vector du speaker encoder)
Format PyTorch natif (le Talker LM est aussi exporté en ExecuTorch .pte)

3. Problèmes rencontrés

3.1 Chatterbox — Opérateurs ONNX non standard

Problème central : Les modèles ONNX de Chatterbox utilisent des opérateurs Microsoft custom qui ne sont supportés ni par QNN (Qualcomm) ni par AI Hub :

Opérateur Domaine Utilisations Problème
GroupQueryAttention com.microsoft 30 (1 par couche) Non supporté par QNN/AI Hub
SkipSimplifiedLayerNormalization com.microsoft 60 Non supporté par QNN/AI Hub
SimplifiedLayerNormalization ONNX opset 21 1 Non supporté par QNN (opset trop récent)

Ces opérateurs sont des optimisations internes d'ONNX Runtime (fusion GQA, skip-connection + layernorm fusionnés). Ils fonctionnent sur CPU via ORT mais ne peuvent pas être compilés pour le NPU Qualcomm.

Conséquence : Le language model (30 couches, ~85% du temps de calcul) tourne entièrement sur CPU à ~1 tok/s sur la tablette. Sur PC, il tourne à ~45 tok/s (CPU x86 plus puissant).

Tentatives de résolution :

  1. Compilation AI Hub avec opset 21 → Échec (SimplifiedLayerNormalization non supporté)
  2. Patch opset 21→17 + remplacement LayerNorm → Échec (int64 non supporté)
  3. Ajout --truncate_64bit_io → Échec (GroupQueryAttention non supporté)
  4. Le modèle FP32 utilise aussi GroupQueryAttention
  5. Le modèle Q4F16 utilise aussi GroupQueryAttention

Solution potentielle : Retrouver le modèle PyTorch original de Chatterbox et le ré-exporter en ONNX avec des opérateurs standard (attention multi-head classique au lieu de GQA fusionné). Le modèle source est sur HuggingFace (resemble-ai/chatterbox-multilingual) mais l'export ONNX standard n'a pas été publié.

Autre problème constaté : La variante Q4F16 (quantifiée INT4) produit de l'audio de mauvaise qualité sur la tablette — le son "ne correspond à rien" selon le test utilisateur. Sur PC, le même modèle Q4F16 fonctionne correctement (63 tokens, stop token atteint, 2.5s d'audio). La différence pourrait venir de la précision des opérations INT4 sur ARM vs x86.

3.2 Qwen3-TTS — Speech Decoder non exportable

Problème central : Le pipeline Qwen3-TTS est composé de 4 modules dont seuls 2 sont exportables :

Module Export ONNX Export ExecuTorch Bloqueur
Speaker Encoder (8.9M) ⚠️ Non testé (probablement OK) Non tenté Conv1D simple
Talker LM (754M) Échoue Fonctionne (90.7 tok/s NPU)
Code Predictor (141M) Exporté (440 MB) Non tenté
Speech Decoder (114M) Échoue Échoue SplitResidualVectorQuantizer + SnakeBeta

Le Speech Decoder est le bloqueur. Il contient :

  1. SplitResidualVectorQuantizer : Utilise torch.autograd.Function avec vmap — une fonctionnalité PyTorch avancée incompatible avec tout export (ONNX legacy, dynamo, jit.trace). C'est le composant qui convertit les indices de codebook en vecteurs continus.

  2. SnakeBeta activation : Bien que son forward() soit du PyTorch standard (x + sin²(αx)/β), elle est utilisée dans des blocs qui contiennent aussi le VQ, rendant l'export impossible pour l'ensemble.

Tentatives de résolution :

  1. Export ONNX legacy (torch.onnx.export) → RuntimeError: unordered_map::at (vmap)
  2. Export dynamo (torch.onnx.export(dynamo=True)) → Échec (strict et non-strict)
  3. Export TorchScript (torch.jit.trace) → RuntimeError: unordered_map::at
  4. Décomposition en sous-modules (pre_conv, pre_transformer, conv_decoder) → Le VQ bloque toujours
  5. Export du code predictor seul → Réussi (mais inutile sans le speech decoder)

Solution potentielle : Réécrire le SplitResidualVectorQuantizer.decode() en opérations PyTorch basiques (embedding lookups + Conv1d projections) sans utiliser torch.autograd.Function ni vmap. Les poids des codebooks ont été extraits en numpy. Cela demande de comprendre précisément le flow de données du VQ decode.


4. Résumé comparatif

Critère Chatterbox ONNX Qwen3-TTS
Multilingue 23 langues Multilingue
Voice cloning (x-vector)
Fonctionne sur CPU tablette (très lent, ~1 tok/s) (nécessite PyTorch = Termux)
NPU compilable (ops Microsoft custom) ⚠️ Partiel (Talker OK, decoder bloqué)
Qualité Q4F16 ⚠️ Mauvaise sur ARM N/A
Qualité FP16/FP32 Bonne (PC) Bonne (PC)
Taille totale ~1.5 GB (Q4F16) ~1.0 GB (Talker .pte + reste)
Vitesse estimée NPU ~45 tok/s (si compilable) ~90 tok/s (Talker déjà validé)

5. Chemins de résolution

Option A : Ré-exporter Chatterbox depuis PyTorch (recommandé)

Principe : Charger le modèle PyTorch original (resemble-ai/chatterbox-multilingual), désactiver les optimisations ORT, et exporter en ONNX standard.

Avantages :

  • Le pipeline complet est déjà implémenté dans l'app Android (ChatterboxTtsEngine.kt)
  • Speech encoder, embed tokens, et conditional decoder tournent déjà sur CPU (petits, rapides)
  • Seul le language model a besoin du NPU

Étapes :

  1. Charger resemble-ai/chatterbox-multilingual en PyTorch
  2. Exporter le language model en ONNX opset 17 avec attention standard (pas GQA fusionné)
  3. Compiler via AI Hub pour SM8750
  4. Remplacer le language_model_q4f16.onnx par la version QNN precompiled
  5. Les 3 autres modèles restent en ONNX CPU

Risques : Le modèle PyTorch original pourrait ne pas être public ou avoir une architecture différente des ONNX publiés.

Estimation : 2-4h de travail si le modèle PyTorch est accessible.

Option B : Réécrire le VQ decode de Qwen3-TTS

Principe : Remplacer le SplitResidualVectorQuantizer par des opérations ONNX-compatibles (embedding lookups).

Avantages :

  • Le Talker tourne déjà à 90 tok/s sur NPU
  • Le Code Predictor est déjà exporté en ONNX
  • Qualité TTS supérieure (Qwen3 est plus récent)

Étapes :

  1. Analyser le flow de quantizer.decode() (codebook lookup + projection + sommation)
  2. Réimplémenter en PyTorch sans vmap ni autograd.Function
  3. Exporter le speech decoder complet en ONNX
  4. Intégrer dans l'app Android

Risques : La réimplémentation du VQ pourrait introduire des différences numériques affectant la qualité audio.

Estimation : 4-8h de travail.

Option C : Chatterbox CPU avec optimisations

Principe : Garder Chatterbox sur CPU mais optimiser :

  • Utiliser NNAPI EP au lieu de CPU pur (délègue certaines ops au DSP)
  • Réduire le nombre de tokens max (limiter à ~50 tokens au lieu de 512)
  • Pré-encoder les voix au premier lancement (éviter le coût du speech encoder)

Avantages : Pas de recompilation nécessaire, fonctionne maintenant.

Inconvénients : Toujours lent (~1-5 tok/s), latence de 10-30s par phrase.

Option D : TTS léger (Piper) comme solution intermédiaire

Principe : Utiliser Piper TTS (VITS, ~30 MB) pour avoir du TTS français fonctionnel immédiatement, en parallèle du travail sur Chatterbox/Qwen3-TTS NPU.

Avantages :

  • Modèles ONNX standard, très légers
  • Latence ~100ms
  • Français disponible
  • Pas de compilation NPU nécessaire

Inconvénients :

  • Pas de voice cloning
  • Qualité inférieure (voix synthétique)
  • Une seule voix par modèle

6. Recommandation

Court terme : Option A (ré-export Chatterbox PyTorch) est la voie la plus prometteuse. Le pipeline Android est déjà prêt, seul le language model a besoin du NPU. Si le modèle PyTorch est accessible, c'est réalisable rapidement.

Moyen terme : Option B (Qwen3-TTS VQ rewrite) donnerait les meilleures performances (Talker déjà à 90 tok/s NPU) mais demande plus de travail d'ingénierie.

Fallback : Option D (Piper) comme TTS temporaire pendant le développement NPU.


7. Fichiers et ressources disponibles

Modèles Chatterbox (sur serveur)

/opt/Kazeia/models_qnn/chatterbox-tts/onnx/
├── speech_encoder.onnx (+data, 591 MB)
├── embed_tokens.onnx (+data, 68 MB)
├── language_model.onnx (+data, 2081 MB FP32)
├── language_model_fp16.onnx (+data, 1040 MB)
├── language_model_q4f16.onnx (+data, 305 MB)
└── conditional_decoder.onnx (+data, 534 MB)

Modèles Qwen3-TTS (sur serveur)

/opt/Kazeia/models_qnn/qwen3-tts-executorch/
├── hybrid_llama_qnn.pte (286 MB, Talker NPU ✅)
└── tokenizer.json

/opt/Kazeia/models_qnn/qwen3-tts-onnx/
├── code_predictor_transformer.onnx (314.8 MB ✅)
├── code_predictor_heads.onnx (125.8 MB ✅)
├── code_predictor_embeddings.npy
└── speech_decoder_pre_conv.onnx (6.3 MB ✅)

/opt/Kazeia/models_qnn/qwen3-tts-native/
├── speech_decoder_weights.pt (437 MB)
├── code_predictor_weights.pt (541 MB)
├── speaker_encoder_weights.pt (34 MB)
└── text_components.pt (1.2 GB)

Voix de référence (sur tablette)

/data/local/tmp/kazeia/voix/
├── damien.wav, elodie.wav, jerome.wav, richard.wav
├── amir.wav, didier.wav, sid.wav, zelda.wav

Code Android

app/src/main/java/com/kazeia/tts/
├── ChatterboxTtsEngine.kt (pipeline complet, KV-cache, voice cloning)
├── AndroidTtsEngine.kt (fallback Google TTS)

Rapport généré par Claude Code (Opus 4.6) — Projet Kazeia