feat(engine): voice pipeline Phase 1 — composition engine and data model

Add the voice pipeline composition engine (D-138 Spike 2, Phase 1):

- voice/prompt_builder.rs: full prompt assembly from culture profile,
  tell state, and base text. Handles occasional injection gating,
  epistemic marker extraction, content-length-gated tell injection.
  16 unit tests.

- blueprint.rs: CultureProfile gains voice_persona, voice_examples,
  occasional_injections fields. NpcBlueprint gains tell_behaviors.
  OccasionalInjection struct with kind discriminator (oath/faith/
  hesitancy/etc), frequency, and tell-suppression gating.

- culture-krenn.ron: v2 voice injector from Spike 1 — persona block,
  3 examples, oath injection at 0.25 frequency.

- D-138 amended: Phi-3 dropped entirely, exact Gemma 2B provenance
  documented. Model file renamed to gemma2.gguf.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 16:35:39 +01:00
co-authored by Claude Opus 4.6
parent 2d03776365
commit 33030fcc58
7 changed files with 620 additions and 1 deletions
+70
View File
@@ -22,6 +22,7 @@
use serde::{Deserialize, Serialize};
use crate::npc::PersonalityTrait;
use crate::npc::tell_state::TellCategory;
// ---------------------------------------------------------------------------
// Zone identity specification (input — filled by copy team, ticket #609)
@@ -105,6 +106,16 @@ pub struct CultureProfile {
pub speech: SpeechPatterns,
/// Cultural values that bias personality trait selection.
pub values: CulturalValues,
/// Full persona block for voice pipeline LLM prompts (may be absent).
#[serde(default)]
pub voice_persona: Option<String>,
/// Example input/output pairs for voice pipeline prompts.
#[serde(default)]
pub voice_examples: Vec<VoiceExample>,
/// Occasional prompt injections rolled per-prompt by the composition engine.
/// Some cultures have none, others several. No cap on count.
#[serde(default)]
pub occasional_injections: Vec<OccasionalInjection>,
}
/// Naming conventions for NPC name generation.
@@ -146,6 +157,47 @@ pub struct CulturalValues {
pub disfavored_traits: Vec<PersonalityTrait>,
}
// ---------------------------------------------------------------------------
// Voice pipeline types (D-138, Spike 2)
// ---------------------------------------------------------------------------
/// Example input/output pair for voice pipeline prompts.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VoiceExample {
/// The input scenario description.
pub input: String,
/// The expected voiced output.
pub output: String,
}
/// Occasional prompt injection rolled per-prompt by the composition engine.
///
/// The model never decides injection frequency — the composition engine rolls
/// a random check per prompt and either includes the clause or doesn't.
/// This solves the fundamental problem that small LLMs can't self-gate
/// vocabulary frequency across independent inference calls.
///
/// Different `kind` values represent different categories of injection:
/// oaths ("void take it"), faith expressions ("God help us"),
/// verbal hesitancy, greetings ("hey"), etc. The composition engine
/// treats them uniformly — `kind` exists for human readability and
/// future filtering.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OccasionalInjection {
/// Injection category (e.g. "oath", "faith", "hesitancy", "greeting").
pub kind: String,
/// LLM instruction text to inject into the prompt.
pub clause: String,
/// Optional example pair demonstrating the injection in use.
#[serde(default)]
pub example: Option<VoiceExample>,
/// Probability of inclusion per prompt (0.01.0).
pub frequency: f32,
/// Tell categories that suppress this injection to avoid conflicting instructions.
#[serde(default)]
pub suppress_on_tells: Vec<TellCategory>,
}
// ---------------------------------------------------------------------------
// NPC blueprint (output — generator produces these)
// ---------------------------------------------------------------------------
@@ -169,6 +221,20 @@ pub struct NpcBlueprint {
pub cultural_markers: CulturalMarkers,
/// Relationship slots (0-3 per D-024).
pub relationships: Vec<BlueprintRelationship>,
/// Tell-specific behaviors — always passed through verbatim, never re-voiced.
#[serde(default)]
pub tell_behaviors: Vec<TellBehavior>,
}
/// A tell-specific behavior string that is passed through verbatim.
/// Tell behaviors are never re-voiced by the voice pipeline — they are
/// authored text that plays exactly as written.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TellBehavior {
/// Which tell category triggers this behavior.
pub category: TellCategory,
/// The behavior text shown to the player.
pub base_text: String,
}
/// Cultural markers attached to a generated NPC.
@@ -285,6 +351,9 @@ mod tests {
favored_traits: vec![PersonalityTrait::Bold, PersonalityTrait::Honest],
disfavored_traits: vec![PersonalityTrait::Reclusive],
},
voice_persona: None,
voice_examples: vec![],
occasional_injections: vec![],
};
let ron_str =
@@ -312,6 +381,7 @@ mod tests {
relationship_type: "colleague".into(),
valence: RelationshipValence::Positive,
}],
tell_behaviors: vec![],
};
let ron_str =