feat(simulation): voice pipeline integration — Factual bypass, observer wiring, tell iteration (#650 #652 #651)

ContentType::Factual bypasses LLM for fact-bearing lines (numbers,
denials). Observer enrichment systems rewrite dialogue/conversation
text with voiced versions before snapshot assembly. Friendly and
RoutineDeviation tells get concrete surface-pattern examples.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 09:11:44 +01:00
co-authored by Claude Opus 4.6
parent ce0df2f320
commit 1a4fd578cc
8 changed files with 316 additions and 12 deletions
+48 -5
View File
@@ -1,17 +1,25 @@
//! Voice cache lookup for behavior text (D-138, Phase 3 stub).
//! Voice cache lookup for behavior text (D-138, Phase 3).
//!
//! Provides the integration point between the voice cache and any system
//! that serves NPC text to the client. Not yet wired into a runtime system —
//! `observable_behaviors` on `NpcBlueprint` is generator output only.
//!
//! Wire `voiced_behavior()` into the behavior-serving path once it exists.
//! that serves NPC text to the client. Wired into the observer pipeline
//! via `voice::integration` enrichment systems.
use bevy_ecs::prelude::*;
use std::sync::{Arc, Mutex};
use crate::npc::tell_state::TellCategory;
use crate::voice::cache::{CacheKey, VoiceCacheStore};
use crate::voice::prompt_builder::ContentType;
/// ECS resource wrapping the voice cache store.
///
/// Optional — systems that use it gracefully degrade to base text when absent.
/// Populated at startup by the voice subsystem when the pipeline is enabled.
#[derive(Resource)]
pub struct VoiceCacheResource {
pub cache: Arc<Mutex<VoiceCacheStore>>,
}
/// Look up a voiced behavior from cache, falling back to base text.
///
/// Tell behaviors (from `NpcBlueprint.tell_behaviors`) are NEVER re-voiced —
@@ -33,6 +41,11 @@ pub fn voiced_behavior(
return base_text.to_string();
}
// Factual content is always base text — no LLM, no cache lookup.
if content_type == ContentType::Factual {
return base_text.to_string();
}
let key = CacheKey {
culture_id: culture_id.to_string(),
npc_stable_id,
@@ -99,6 +112,36 @@ mod tests {
assert_eq!(result, "Base line.");
}
#[test]
fn factual_content_always_passthrough() {
let cache = test_cache();
// Store a voiced version under the Factual key — it must never be returned.
let key = CacheKey {
culture_id: "krenn".into(),
npc_stable_id: 42,
content_type: ContentType::Factual,
content_index: 0,
tell_state: None,
};
cache
.lock()
.unwrap()
.store(100, key, "Voiced factual (should be ignored).".into());
let result = voiced_behavior(
&cache,
100,
"krenn",
42,
ContentType::Factual,
0,
None,
"14 crates in bay seven.",
false,
);
assert_eq!(result, "14 crates in bay seven.");
}
#[test]
fn tell_behavior_always_passthrough() {
let cache = test_cache();