# Conflicts: # CHANGELOG.md # content/_meta/README.md # content/_meta/npc-authoring-style-guide.md # wiki/_templates/cultural-group.md # wiki/_templates/institution.md # wiki/_templates/star-system.md # wiki/characters/devra.md # wiki/characters/drin.md # wiki/characters/harek.md # wiki/characters/lera-sessik.md # wiki/characters/maret-korr.md # wiki/characters/naia-tamm.md # wiki/characters/nils-davan.md # wiki/characters/pell.md # wiki/characters/renn.md # wiki/characters/resha.md # wiki/characters/sabel.md # wiki/characters/sera-venn.md # wiki/characters/torek-lintar.md # wiki/characters/voss.md # wiki/star-systems/krenn/index.md
18 KiB
title, description, type, status, workshop, agent, round, created
| title | description | type | status | workshop | agent | round | created |
|---|---|---|---|---|---|---|---|
| Tyre Round 1: Technical Feasibility Inventory | Technical feasibility inventory assessing architecture options for voice pipeline | workshop | archived | llm-voice-pipeline | tyre | 1 | 2026-03-07 |
Tyre Round 1: Technical Feasibility Inventory
Domain: Technical architecture Input documents reviewed: workshop-brief.md, proposed-llm-voice.md, generator_spike.rs, blueprint.rs, culture-van-maanens-star.ron, rural-zone-spec.ron, industrial-zone-spec.ron, D-010, D-024, D-121, D-122, D-123, D-128, Q-057, Q-012
1. The Three Options — Technical Assessment
Option 1: Hand-authored pools (current)
Difficulty tier: Easy to build, impossible to scale.
The current system works. RoleSpec.typical_behaviors is a Vec<String>, the generator draws from it with Fisher-Yates, done. Zero runtime complexity. But the brief nails the problem: O(R x Z x C) content. Right now we have ~7 behaviors per role across 2 zones and 1 culture. Adding a second culture doubles the authoring. Adding a station zone type triples it. By the time we have 4 cultures and 5 zone types we're looking at ~700 hand-authored behavior strings just for ambient behaviors, before dialogue. The copy team already flagged this (Q-057).
Technically trivial. Content-impossible at scale. Not viable as the sole strategy.
Option 2: Composable primitives (Q-057)
Difficulty tier: Medium to build, moderate to scale, high risk of mechanical output.
The idea: decompose "tends rows of low-growing crops with a long-handled hoe" into [action:tends] [object:crops] [tool:hoe] [manner:practiced] and recombine with culture modifiers. This is a string assembly engine — essentially a sophisticated template system.
Technical assessment:
- Build cost: 2-3 sprints for the composition engine, tag taxonomy, and modifier system.
- Maintenance cost: High. Every new combination needs QA. The tag taxonomy becomes a coordination bottleneck (see Q-049 ObjectTag co-maintenance problem — same class of issue).
- Output quality ceiling: Mechanical. "Tends crops with a long-handled hoe in a direct, unhurried manner" reads like a sentence assembled from parts, because it was. The Sprint 25 spike proved that specific, authored phrasing is what makes behaviors legible — "wipes grease on the thigh of her coveralls between jobs" cannot be composed from primitives without losing the detail that makes it human.
- Integration: Fits cleanly into the existing
typical_behaviors: Vec<String>— the composition engine produces strings, same as hand-authoring. No architectural change needed downstream.
Feasible but produces the wrong output. The quality floor is too low for what the spike proved works.
Option 3: LLM re-voicing
Difficulty tier: Challenging but doable. Let me be honest about what this means technically.
The i18n analogy is elegant and architecturally sound. Base text as both seed and fallback is a clean design that eliminates the dual-authoring problem. But "ship an LLM with the game" is not a small sentence. Let me break down what this actually requires:
What's actually easier than it sounds:
- The prompt engineering. The injector clause system maps directly to data we already have:
CultureProfile.speech,NpcBlueprint.traits,NpcWant. The prompt is a structured assembly of existing data fields + a base text string. This is well-defined work, not open-ended AI research. - The cache/fallback model. Base text IS the fallback — no separate system needed. Cache is a string-keyed lookup:
(seed, zone, culture, npc_id, behavior_index) -> voiced_string. Fits naturally into our existing RON/MessagePack pipeline. - Integration with the generator.
NpcBlueprint.observable_behaviorsis alreadyVec<String>. Re-voicing replaces strings in-place. The rest of the pipeline (perception, observer, wire format) doesn't know or care whether the string was hand-authored, composed, or LLM-generated.
What's harder than it sounds:
- Model selection and bundling (see section 2).
- Determinism guarantees (see section 3).
- Memory budget on minimum spec (see section 2).
2. Model Selection and Inference Wrapper
Hardware constraint: the real bottleneck
Minimum spec from the brief: integrated GPU, 8GB RAM shared with game. Let me be precise about what this means.
The game already claims memory:
- Godot client: ~300-500MB (renderer, assets, scene tree)
- Rust server process: ~100-200MB (ECS, generation, world state)
- OS overhead: ~1-1.5GB
- Available for LLM: ~5-6GB absolute max, realistically 3-4GB to avoid pressure
A 2B parameter model in Q4 quantization: ~1.2-1.5GB. That fits. A 3B model in Q4: ~1.8-2.2GB. Tight but possible. Anything larger is out.
Model candidates (2026 landscape)
The proposal mentions Gemma 2B and Phi-3-mini. Let me update for what's actually available now and what matters for our specific task:
| Model | Parameters | Q4 Size | Task fit | Notes |
|---|---|---|---|---|
| Gemma 2 2B | 2.6B | ~1.5GB | Good | Strong instruction following, multilingual base helps with "dialect" tasks |
| Phi-3-mini | 3.8B | ~2.2GB | Better quality, tight on RAM | Microsoft's dense model, excellent reasoning per parameter |
| Qwen2.5-1.5B | 1.5B | ~0.9GB | Adequate | Smallest viable option, leaves most RAM headroom |
| SmolLM2-1.7B | 1.7B | ~1.0GB | Worth testing | Hugging Face, specifically designed for on-device |
| Gemma 2 2B (Q3) | 2.6B | ~1.1GB | Testing needed | Aggressive quantization may hurt style consistency |
My recommendation: Spike with Gemma 2 2B (Q4) as primary candidate, Qwen2.5-1.5B as fallback. The task is stylistic rephrasing, not reasoning — a 2B model should handle it. But the spike must validate this empirically. If a 2B model can't reliably preserve void-oaths and speech register, we have a problem.
Rust inference wrapper
Three serious options for shipping an LLM in a Rust binary:
Option A: llama.cpp via llama-cpp-rs bindings
- Maturity: High. Battle-tested across hundreds of apps. GGUF format is the standard for quantized models.
- Binary size impact: ~5-8MB for the llama.cpp static library.
- Startup cost: Model load from disk takes 1-3 seconds (acceptable — happens once at game start or first inference request).
- GPU acceleration: Optional CUDA/Metal/Vulkan backends. CPU-only fallback works. Important: on integrated GPU systems, the CPU path may actually be faster than competing for shared GPU memory with Godot's renderer.
- My recommendation. It's the boring choice, and boring is correct here.
Option B: candle (Hugging Face Rust ML framework)
- Pure Rust, no C++ dependency. Smaller binary footprint (~2-3MB).
- Less mature for production inference. Quantization support is narrower.
- Advantage: no cross-compilation headaches with C++ toolchains.
- Risk: fewer model format options, less community optimization.
Option C: burn (Rust ML framework)
- Pure Rust, very early. Not production-ready for inference of transformer models at the scale we need.
- Would require manual model conversion work.
- Not recommended for v0.2.
Verdict: llama-cpp-rs with GGUF models. It's proven, it handles quantization correctly, and the binary size impact is acceptable. We wrap it in a thin Rust crate (sr-voice or similar) that exposes exactly one function: revoice(base_text: &str, context: &VoiceContext) -> String.
Binary size and distribution impact
| Component | Size |
|---|---|
| llama.cpp static lib | ~5-8MB |
| GGUF model (Q4, 2B) | ~1.2-1.5GB |
| Baked voice cache (hub zones) | ~5-20MB (text only, compresses well) |
| Total distribution impact | ~1.3-1.6GB |
This is significant but not unusual for a modern game. The model ships as a separate asset, not baked into the binary. Players who disable "AI-Enhanced Dialogue" could theoretically skip the download (future optimization, not v0.2).
3. Determinism — the D-010 Problem
cracks knuckles — This is where it gets interesting.
D-010 principle 4 mandates BTreeMap everywhere for determinism. Same seed = same world. LLM inference is inherently non-deterministic across:
- Different hardware (floating point rounding)
- Different quantization levels
- Different batch sizes
- Different llama.cpp versions
The proposal's answer — generate once per seed, cache the result — is correct but needs formalization.
Cache-as-determinism model
The LLM does NOT run during gameplay simulation ticks. It runs during world generation (baked or lazy pre-voicing). The output is cached. From that point forward, the cached string is deterministic — it's just a lookup.
Generation time: base_text + context -> LLM -> voiced_text -> cache
Game time: cache_key -> voiced_text (deterministic lookup)
Cache key structure:
(world_seed: u64, culture_id: &str, zone_type: &str, npc_stable_id: StableId, behavior_index: u8)
This means:
- Same seed on the same machine = same voiced text (LLM output cached on first generation)
- Same seed on different machines = potentially different voiced text (acceptable — the base text is identical, only the stylistic variation differs)
- Want tells and relationship behaviors: generated by the Rust simulation, then re-voiced. The tell content is deterministic (SimRng-seeded). The voiced phrasing is cached. The gameplay-critical information (the tell exists, it references a specific person) is in the base text, not added by the LLM.
What must NOT be re-voiced
This is critical. Some strings carry precise gameplay information:
| Content type | Re-voice? | Why |
|---|---|---|
| Role behaviors ("tends crops") | Yes | Flavor text, no gameplay info loss |
| Want tells ("watches the room in the glass of a nearby surface") | Carefully | The tell IS the gameplay. Re-voicing must preserve the observable action. Restrict LLM to style/voice changes, not semantic changes. |
| Relationship behaviors ("talks past Rask without making eye contact") | Carefully | The named target and the social signal must survive re-voicing. |
| AvoidingSomeone tells with named targets | No | Format string with {name} substitution. Re-voicing risks losing the name reference. |
| Dialogue (future) | Yes | Culture voice is the primary enhancement target |
The safe rule: if the string contains a proper noun reference to another NPC, pass it through untouched. The LLM can re-voice generic role actions freely.
4. Pre-voicing Queue and Lazy Generation Integration
Same thread pool or separate?
Separate. Here's why:
The world generator (zone skeletons, NPC blueprints, tile placement) is CPU-bound Rust running on the server process. It uses SimRng and must be deterministic. It runs during zone loading and produces SpikeOutput/NpcBlueprint data.
The voice pipeline is I/O-bound (model loading) then CPU-bound (inference), non-deterministic, and operates on generator output. It should run in its own thread pool with:
- A bounded work queue (e.g.,
crossbeam-channelwith capacity 256) - Priority ordering: P0 (plot-critical) > P1 (semi-unique) > P2 (ambient)
- Backpressure: if the queue is full, new items wait — the game continues with base text
Integration with lazy world generation
Player enters zone trigger area
-> World generator produces NpcBlueprints (deterministic, fast)
-> NPC entities spawn with base_text behaviors (immediate, playable)
-> Voice queue receives (blueprint, culture, zone_context) work items
-> Voice worker processes queue in background
-> Completed items update the behavior cache
-> Next perception tick: observer reads voiced text from cache instead of base text
The key insight: re-voicing is an asynchronous enhancement, not a blocking dependency. The game is always playable with base text. Voiced text replaces it when ready. The observer system (ObserverSnapshot) already reads behavior strings from a cache — we just add a "voiced version available?" check.
Latency budget
For background generation on minimum-spec hardware (CPU-only inference on a 2B model):
- Per-behavior re-voicing: ~200-500ms per inference call (short input, short output)
- Per-NPC (2 behaviors): ~400ms-1s
- Per-zone (10 NPCs): ~4-10 seconds
- Adjacent zone pre-voicing while player is in current zone: easily achievable. Player spends minutes in a zone; pre-voicing the next zone takes seconds.
On higher-spec hardware with GPU acceleration: 5-10x faster. Negligible.
5. Cache Format and Invalidation
Format
MessagePack (D-020) for consistency with the rest of the pipeline. The voice cache is a flat map:
struct VoiceCache {
/// (seed, zone, culture, npc_id, behavior_idx) -> voiced string
entries: BTreeMap<VoiceCacheKey, String>,
/// Model version used to generate these entries
model_version: String,
/// Cache format version for migration
format_version: u8,
}
Stored per-zone as .msgpack files alongside save data. Baked caches for hub zones ship as game assets.
Invalidation rules
| Event | Invalidation scope | Rationale |
|---|---|---|
| Seed change (new game) | Full regen | Different world = different NPCs = different voiced text |
| Culture mod added | Culture-scoped regen | Culture injectors changed, all culture-specific voicing is stale |
| Model update (game patch) | Full regen | Different model = potentially different output |
| Zone spec change (content patch) | Zone-scoped regen | Base text pool changed |
| NPC relationship change (runtime) | Single NPC regen | Relationship behavior text changed |
Save file interaction: Voice cache is NOT part of the save file. It's a derived artifact that can be regenerated. Save files remain small (D-020). The cache lives in a separate directory (user://voice_cache/).
6. What Breaks If We Choose Wrong
If we choose Option 1 (hand-authored only):
- Content team hits a wall at 3 cultures x 5 zone types. D-122 (all NPCs generated) becomes impossible to staff. Q-057 remains open forever because the answer is "write more strings manually."
- Risk level: Project-blocking. The generator-first vision (D-114, D-117) requires content at scale. Hand-authoring doesn't scale.
If we choose Option 2 (composable primitives):
- We build a composition engine that produces output below the quality bar the Sprint 25 spike established. The spike proved that specific authored detail makes behaviors readable — "wipes grease on the thigh of her coveralls" is not composable. We'd ship a technically sophisticated system that produces bland output.
- Risk level: Quality-killing. The game reads as procedurally generated in the pejorative sense.
If we choose Option 3 (LLM re-voicing) and the model can't do it:
- If 2B models can't reliably preserve void-oaths, speech register, and relationship references, we've built an inference pipeline for nothing. The fallback is base text — playable but bland.
- Risk level: Recoverable. The base text fallback means the game ships either way. The infrastructure cost (~3 sprints for the wrapper + queue + cache) is the sunk cost.
- Additional risk: Install size. ~1.5GB for a model some players won't use. Needs to be a separate, optional download.
If we choose a hybrid (my recommendation) and it's overengineered:
- We build both the base text pipeline (already exists) and the re-voicing layer. If the LLM layer underperforms, we disable it and ship with base text + hand-authored hub content.
- Risk level: Minimal. The hybrid architecture is inherently risk-managed by the fallback design.
7. My Position: Option 3 (LLM re-voicing) with hybrid fallback
cracks knuckles — Let me be direct.
The architecture in proposed-llm-voice.md is sound. The i18n model is elegant — a single design that solves content scaling, quality floor, hardware flexibility, and player choice simultaneously. That's rare. When a single architectural decision solves four problems, you take it.
The technical risk is real but bounded:
- The spike will tell us if 2B models can handle the task (1-2 sprints to answer)
- The fallback (base text) means we ship either way
- The infrastructure (llama-cpp-rs wrapper, queue, cache) is well-understood systems programming, not research
Effort estimate:
- Spike (model evaluation + prompt testing): 1-2 sprints
- Rust inference wrapper (
sr-voicecrate): 1 sprint - Pre-voicing queue + cache system: 1 sprint
- Integration with observer/generator pipeline: 1 sprint
- Baked cache generation for hub zones: 0.5 sprint
- Total: 4.5-5.5 sprints (parallelizable — spike can run while other v0.2 work continues)
Scope-wise, this means the LLM voice pipeline is a ~6 sprint investment that replaces what would otherwise be an unbounded content authoring commitment. That's a good trade.
8. One Question I Need Answered
Does the re-voicing target observable behaviors only, or dialogue too?
The workshop brief asks this (question 1) and the answer has major architectural implications:
-
Behaviors only: The re-voicing input is the
observable_behaviors: Vec<String>fromNpcBlueprint. Short strings (5-15 words), no conversation context needed. A 2B model handles this easily. Prompt is simple: "Rephrase this action description in [culture] voice with [personality] manner." -
Behaviors + dialogue: The dialogue system (D-028) has its own tagged line pools, conversation context, and layer model. Re-voicing dialogue means the LLM needs conversation history, speaker/listener context, and longer output. This pushes toward 3B+ models, longer inference times, and significantly more complex prompting.
My technical recommendation: start with behaviors only. Validate the pipeline end-to-end on the simpler case. Extend to dialogue in a follow-up sprint if the model and quality pass the spike criteria. The architecture supports both — the revoice() function doesn't care what kind of string it processes — but the prompt engineering and quality bar are different.
This question determines whether the spike tests short-form re-voicing (behaviors) or long-form (dialogue), which affects model selection, latency budgets, and the spike's success criteria.