# Sprechblasen & lokale Modelle

> Lass jede Einheit im Spiel in beliebiger Rolle eine Sprechblase zeigen. Mit einem lokalen LLM geht ein Satz rein, und die Antwort erscheint über der Einheit.

Quelle: https://war3ai.com/de/docs/speech/

Sprechblasen sind eine Zuschauerschicht: Sie beeinflussen den Spielausgang nicht und eignen sich für Streams, Kommentar und Debugging.

- Jede Einheit kann in jeder Rolle sprechen, auch mehrere Einheiten gleichzeitig;
- Schriftgröße, Farbe, Breite, Zeiger, Transparenz und Tippgeschwindigkeit sind pro Blase einstellbar;
- direkte Anbindung an ein lokales LLM (LM Studio) mit Streaming: Die Blase aktualisiert sich, während der Text entsteht.

## Aus einem Bot heraus

Am einfachsten geht es mit `say` aus dem SDK:

```python
g.say(hero, "Mir nach!", seconds=4)
```

## Start und Oberfläche

**Am einfachsten: das „Kontrollzentrum“ auf der Startseite von Farsight** – zuerst „Lokales LLM → Starten und Modell laden“ (lokaler LM-Studio-Server + das konfigurierte Modell in den VRAM laden), dann „Chat-Sprechblasen → Starten“. Auf den Karten kannst du Logs ansehen, stoppen und neu starten.

Die Oberfläche ist die Seite „Sprechblasen“ links in Farsight: Einheiten sprechen lassen (Einheit wählen, Text schreiben, Stil anpassen, mit dem Modell chatten), Bauern-Kaffeeklatsch, Kamera-Dialoge, ereignisgesteuerte Kommentare, Modelleinstellungen – jeweils für die Instanz, die oben in der Leiste ausgewählt ist.

Auch über die Kommandozeile:

```bash
python speech/speak_launch.py              # startet lokalen Modellserver + lädt und wärmt das Modell auf + startet Sprechblasen-API
python speech/speak_launch.py --restart    # API nach Codeänderungen neu starten
python speech/speak_launch.py --stop       # API stoppen und das Modell aus dem VRAM entladen
```

Jeder Schritt wird übersprungen, wenn er schon läuft – mehrfaches Ausführen hat keine Nebenwirkungen.

## HTTP-API

Standard ist `http://127.0.0.1:8872/` (Port unter `ports.speech` in `openwar3.json`); jedes Programm kann sie aufrufen.

### Eine Einheit sprechen lassen `POST /api/say`

```json
{
  "inst": 16,
  "bubbles": [
    { "unit": "0x14A12614", "name": "Bergkönig", "text": "Mir nach!" },
    { "unit": "0x14A12924", "name": "Erzmagier", "text": "Ich zaubere einen Blizzard.",
      "style": { "font_px": 26, "text_color": "#FFE080", "bg_color": "#C0102040" } },
    { "screen": [960, 110], "key": 1, "name": "Erzähler", "text": "Die erste Orc-Welle kommt in 30 Sekunden.",
      "style": { "tail": false, "type_ms": 0 } },
    { "world": [-4684, 2644], "key": 2, "text": "Sammelpunkt", "style": { "font_px": 16 } }
  ]
}
```

| Feld | Beschreibung |
|---|---|
| `unit` / `world` / `screen` | Eins von dreien: folgt der Einheit (sitzt direkt über dem HP-Balken, falls vorhanden) / Kartenkoordinaten / Bildschirmpixel (für Erzähltext) |
| `name` | Sprecher in der ersten Zeile – frei wählbar, muss nicht zur Einheit passen |
| `text` | Inhalt, wird automatisch umbrochen |
| `duration_ms` | Anzeigedauer; 0 = automatisch 3–5 Sekunden |
| `key` | Nummer für Welt- / Bildschirmblasen; eine neue Nachricht mit gleichem key ersetzt die alte |
| `update` | Gibt es dieselbe Blase schon, nur den Text tauschen, ohne den Timer zurückzusetzen (für Streaming) |
| `style` | `font_px`, `max_width_px`, `text_color`, `bg_color`, `border_color`, `tail`, `side_px`, `opacity`, `type_ms`, `font` … |

Maximal 32 Blasen gleichzeitig; Kosten pro Frame im Schnitt etwa 0.1–0.2 ms.

### Mit einem lokalen Modell chatten `POST /api/chat`

```json
{
  "inst": 16, "unit": "0x14A12614", "name": "Bergkönig",
  "persona": "Du spielst Muradin, den Bergkönig aus Warcraft: rau, herzlich, trinkfest. Ein, zwei Sätze Umgangssprache, höchstens 15 Wörter.",
  "message": "Da vorne ist ein Haufen Oger. Greifen wir an?",
  "stream": true
}
```

Zurück kommt `{"reply": "...", "first_token_ms": 283, "total_ms": 342}` – und die Antwort steht bereits über der Einheit. Jede Einheit merkt sich die letzten 6 Gesprächsrunden.

### Sonstiges

| API | Beschreibung |
|---|---|
| `GET /api/instances` | Laufende Spiele |
| `GET /api/units?inst=16&mine=true&heroes=true` | Einheitenliste (mit chinesischem Namen, Koordinaten, HP) |
| `POST /api/clear` | Eine oder alle Blasen entfernen |
| `GET /api/llm`, `POST /api/llm` | Modellkonfiguration lesen / ändern (`base_url`, `model`, `max_tokens`, `temperature`) |
| `POST /api/banter` | Bauern-Kaffeeklatsch: Die Arbeiter zu Hause lästern reihum nach ihrer Persona, dazu eine Ansage zum Spielstart (alle Spieldaten sind echt) |
| `POST /api/camtalk` | Kamera-Dialog: Helden und Gefolge im Bild reden ihrer Rolle entsprechend |
| `POST /api/events` | Ereignisgesteuert: Kampfbeginn, Kampfende, Held gefallen, Tier-Up, Gebäude zerstört … gesprochen wird nur, wenn etwas passiert |

## Welches lokale Modell?

Gemessen auf einer RTX 5090 (5 Spielzeilen):

| Modell | VRAM | Tempo | Eine Antwort | Fazit |
|---|---|---|---|---|
| **Qwen3.6-35B-A3B** (MoE, pro Token nur 3B aktiv), Q4, Thinking aus | 20.6 GB | ca. 142 Token/s | **ca. 0.3 s** (erstes Token nach ca. 0.27 s) | Empfehlung: schnell, natürliches Rollenspiel auf Chinesisch |
| gpt-oss-20b (MXFP4), Reasoning low | 11.3 GB | ca. 280 Token/s | 0.3–0.8 s | bei knappem VRAM; Chinesisch etwas flach |
| Qwen3.6-27B (dense), Q4 | 17.2 GB | ca. 39 Token/s | nach 5.5 s noch am Denken | nicht für Echtzeit-Dialoge geeignet |

- **Das Tempo hängt von den aktiven Parametern ab, nicht von der Gesamtgröße**: Das 35B-MoE aktiviert nur 3B und ist 3- bis 4-mal schneller als das dichte 27B.
- **„Thinking“ unbedingt abschalten**: Sonst gehen alle Tokens ins Denken, und es kommt kein einziges Wort Antwort.
- Die Blase tippt etwa 22 Zeichen pro Sekunde, die Generierungsgeschwindigkeit ist also kein Engpass mehr. Was das Erlebnis wirklich bestimmt, ist die **Latenz bis zum ersten Token**.

> **Damit die Zeilen „echt“ wirken**
>
> Gib dem Modell ausschließlich echte Spieldaten (Anzahl Spiele, Siege/Niederlagen, Truppenstärke, Vorräte) und sag ausdrücklich: „Verwende nur diese Fakten.“ Ohne diese Einschränkung hat das Modell im Test Kämpfe erfunden, die nie stattgefunden haben.
