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.
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:
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:
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
{
"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
{
"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.