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:
@@ -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.0–1.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 =
|
||||
|
||||
Reference in New Issue
Block a user