11 KiB
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 :
- ✅ Compilation AI Hub avec opset 21 → Échec (
SimplifiedLayerNormalizationnon supporté) - ✅ Patch opset 21→17 + remplacement LayerNorm → Échec (
int64non supporté) - ✅ Ajout
--truncate_64bit_io→ Échec (GroupQueryAttentionnon supporté) - ❌ Le modèle FP32 utilise aussi
GroupQueryAttention - ❌ 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 :
-
SplitResidualVectorQuantizer: Utilisetorch.autograd.Functionavecvmap— 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. -
SnakeBetaactivation : Bien que sonforward()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 :
- ✅ Export ONNX legacy (
torch.onnx.export) →RuntimeError: unordered_map::at(vmap) - ✅ Export dynamo (
torch.onnx.export(dynamo=True)) → Échec (strict et non-strict) - ✅ Export TorchScript (
torch.jit.trace) →RuntimeError: unordered_map::at - ✅ Décomposition en sous-modules (pre_conv, pre_transformer, conv_decoder) → Le VQ bloque toujours
- ✅ 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 :
- Charger
resemble-ai/chatterbox-multilingualen PyTorch - Exporter le language model en ONNX opset 17 avec attention standard (pas GQA fusionné)
- Compiler via AI Hub pour SM8750
- Remplacer le
language_model_q4f16.onnxpar la version QNN precompiled - 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 :
- Analyser le flow de
quantizer.decode()(codebook lookup + projection + sommation) - Réimplémenter en PyTorch sans
vmapniautograd.Function - Exporter le speech decoder complet en ONNX
- 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