# Bulles de dialogue et modèles locaux

> Faites parler n’importe quelle unité du jeu, sous n’importe quelle identité, dans une bulle au-dessus de sa tête. Branchez un LLM local : une phrase entre, une réponse s’affiche au-dessus de l’unité.

Source: https://war3ai.com/fr/docs/speech/

Les bulles sont une couche destinée au spectacle : elles n’influent pas sur l’issue de la partie, et se prêtent au streaming, au commentaire et au débogage.

- n’importe quelle unité peut parler, sous n’importe quelle identité ; plusieurs unités peuvent parler en même temps ;
- taille de police, couleur, largeur, pointe, transparence et vitesse de frappe se règlent bulle par bulle ;
- branchement direct sur un LLM local (LM Studio), avec sortie en streaming : la bulle se met à jour au fil de la génération.

## Depuis un Bot

Le plus simple est le `say` fourni avec le SDK :

```python
g.say(hero, "Avec moi, chargez !", seconds=4)
```

## Démarrage et interface

**Le plus simple : le « Centre de contrôle », page d’accueil de Farsight** — cliquez d’abord sur « LLM local → Démarrer et charger le modèle » (serveur local LM Studio + chargement du modèle configuré en mémoire vidéo), puis sur « Bulles de dialogue → Démarrer ». Les cartes permettent aussi de consulter les journaux, d’arrêter et de redémarrer.

L’interface se trouve dans la page « Bulles de dialogue », à gauche dans Farsight : faire parler les unités (choisir une unité, écrire le texte, régler le style, dialoguer avec le modèle), pause-café des paysans, dialogues à la caméra, déclenchement par la partie, réglages du modèle ; tout s’applique à l’instance choisie dans la barre du haut.

Également en ligne de commande :

```bash
python speech/speak_launch.py              # lance le serveur de modèle local + charge et préchauffe le modèle + lance l'API des bulles
python speech/speak_launch.py --restart    # relance l'API après une modification du code
python speech/speak_launch.py --stop       # arrête l'API et décharge le modèle de la mémoire vidéo
```

Chaque étape est ignorée si elle est déjà faite : relancer la commande n’a aucun effet de bord.

## API HTTP

Adresse par défaut : `http://127.0.0.1:8872/` (le port est défini par `ports.speech` dans `openwar3.json`) ; n’importe quel programme peut l’appeler.

### Faire parler une unité `POST /api/say`

```json
{
  "inst": 16,
  "bubbles": [
    { "unit": "0x14A12614", "name": "Roi de la montagne", "text": "Avec moi, chargez !" },
    { "unit": "0x14A12924", "name": "Archimage", "text": "Je m'occupe du Blizzard.",
      "style": { "font_px": 26, "text_color": "#FFE080", "bg_color": "#C0102040" } },
    { "screen": [960, 110], "key": 1, "name": "Narrateur", "text": "Première vague d'orcs dans 30 secondes.",
      "style": { "tail": false, "type_ms": 0 } },
    { "world": [-4684, 2644], "key": 2, "text": "Point de ralliement", "style": { "font_px": 16 } }
  ]
}
```

| Champ | Description |
|---|---|
| `unit` / `world` / `screen` | Un seul des trois : suivre une unité (collée juste au-dessus de sa barre de vie si elle en a une) / coordonnées sur la carte / pixels à l’écran (pour la narration) |
| `name` | Nom de l’orateur affiché en première ligne ; libre, pas forcément celui de l’unité |
| `text` | Texte principal, avec retour à la ligne automatique |
| `duration_ms` | Durée d’affichage ; 0 = automatique, 3 à 5 secondes |
| `key` | Identifiant d’une bulle monde / écran : un nouveau message avec la même key remplace l’ancien |
| `update` | Si la même bulle existe déjà, ne remplace que le texte, sans réinitialiser la minuterie (pour le streaming) |
| `style` | `font_px`, `max_width_px`, `text_color`, `bg_color`, `border_color`, `tail`, `side_px`, `opacity`, `type_ms`, `font`… |

32 bulles au maximum en même temps ; coût moyen d’environ 0.1 à 0.2 ms par frame.

### Dialoguer avec le modèle local `POST /api/chat`

```json
{
  "inst": 16, "unit": "0x14A12614", "name": "Roi de la montagne",
  "persona": "Vous incarnez Muradin, le Roi de la montagne de Warcraft : jovial, amateur de bière. Une ou deux phrases parlées, pas plus de 40 caractères.",
  "message": "Il y a une bande d'ogres devant, on charge ou pas ?",
  "stream": true
}
```

Renvoie `{"reply": "...", "first_token_ms": 283, "total_ms": 342}` ; au même moment, la réponse s’affiche déjà au-dessus de l’unité. Chaque unité garde en mémoire ses 6 derniers échanges.

### Autres

| Interface | Description |
|---|---|
| `GET /api/instances` | Parties en cours |
| `GET /api/units?inst=16&mine=true&heroes=true` | Liste des unités (avec nom chinois, coordonnées, points de vie) |
| `POST /api/clear` | Efface une bulle ou toutes les bulles |
| `GET /api/llm`, `POST /api/llm` | Consulter / modifier la configuration du modèle (`base_url`, `model`, `max_tokens`, `temperature`) |
| `POST /api/banter` | Pause-café des paysans : les ouvriers de la base râlent à tour de rôle selon leur personnage, et annoncent le début de la partie (toutes les infos de jeu sont réelles) |
| `POST /api/camtalk` | Dialogues à la caméra : les héros et leur escorte visibles à l’écran dialoguent selon leur rôle |
| `POST /api/events` | Déclenché par la partie : début de combat, fin de combat, mort d’un héros, montée de tier, bâtiment détruit… on ne parle que lorsqu’il se passe quelque chose |

## Choisir un modèle local

Mesuré sur une RTX 5090 (5 répliques de jeu) :

| Modèle | VRAM | Vitesse | Une réponse | Verdict |
|---|---|---|---|---|
| **Qwen3.6-35B-A3B** (MoE, seulement 3B actifs à chaque pas), Q4, réflexion désactivée | 20.6 GB | Environ 142 token/s | **Environ 0.3 seconde** (premier token vers 0.27 seconde) | Recommandé : rapide, jeu de rôle naturel en chinois |
| gpt-oss-20b (MXFP4), raisonnement low | 11.3 GB | Environ 280 token/s | 0.3 à 0.8 seconde | Quand la VRAM est juste ; chinois un peu plat |
| Qwen3.6-27B (dense), Q4 | 17.2 GB | Environ 39 token/s | Toujours en réflexion après 5.5 secondes | Inadapté au dialogue en temps réel |

- **La vitesse dépend des « paramètres actifs à chaque pas », pas du total** : le MoE de 35B n’en active que 3B et va 3 à 4 fois plus vite que le 27B dense.
- **Désactivez impérativement la « réflexion »** : sinon tous les tokens partent dans la réflexion, et pas un mot de réponse ne sort.
- La bulle s’écrit caractère par caractère, à environ 22 caractères par seconde : la vitesse de génération n’est plus le goulot d’étranglement. Ce qui compte vraiment pour l’expérience, c’est la **latence du premier token**.

> **Des répliques qui sonnent « vrai »**
>
> Ne donnez au modèle que des données réelles sur la partie (nombre de parties, victoires et défaites, effectifs, stocks), et précisez explicitement « n’utilisez que ces faits ». En test, sans cette contrainte, le modèle invente des combats qui n’ont jamais eu lieu.
