feat(voice): stub voice cache lookup for behavior text (Phase 3)

Add lookup.rs with voiced_behavior() — ready to wire into a behavior-serving
system once one exists (Q-058). Tell behaviors always passthrough (never
re-voiced). Cache miss returns base text (graceful degradation).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 17:13:15 +01:00
co-authored by Claude Opus 4.6
parent 82a911f3aa
commit fafa1c49b8
4 changed files with 133 additions and 2 deletions
+122
View File
@@ -0,0 +1,122 @@
//! Voice cache lookup for behavior text (D-138, Phase 3 stub).
//!
//! 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.
use std::sync::{Arc, Mutex};
use crate::npc::tell_state::TellCategory;
use crate::voice::cache::{CacheKey, VoiceCacheStore};
use crate::voice::prompt_builder::ContentType;
/// Look up a voiced behavior from cache, falling back to base text.
///
/// Tell behaviors (from `NpcBlueprint.tell_behaviors`) are NEVER re-voiced —
/// they are always base text passthrough. Only pass `is_tell_behavior: false`
/// for `observable_behaviors` content.
pub fn voiced_behavior(
cache: &Arc<Mutex<VoiceCacheStore>>,
zone_id: u32,
culture_id: &str,
npc_stable_id: u64,
content_type: ContentType,
content_index: u16,
tell_state: Option<TellCategory>,
base_text: &str,
is_tell_behavior: bool,
) -> String {
// Tell behaviors are always passthrough — never re-voiced.
if is_tell_behavior {
return base_text.to_string();
}
let key = CacheKey {
culture_id: culture_id.to_string(),
npc_stable_id,
content_type,
content_index,
tell_state,
};
if let Ok(mut store) = cache.lock() {
if let Some(voiced) = store.lookup(zone_id, &key) {
return voiced;
}
}
// Cache miss — serve base text (graceful degradation).
base_text.to_string()
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn test_cache() -> Arc<Mutex<VoiceCacheStore>> {
let dir = std::env::temp_dir().join("sr-voice-lookup-test");
let _ = std::fs::remove_dir_all(&dir);
Arc::new(Mutex::new(VoiceCacheStore::new(
dir,
99,
"v1".into(),
"i1".into(),
)))
}
#[test]
fn cache_hit_returns_voiced_text() {
let cache = test_cache();
let key = CacheKey {
culture_id: "krenn".into(),
npc_stable_id: 42,
content_type: ContentType::Behavior,
content_index: 0,
tell_state: None,
};
cache.lock().unwrap().store(100, key, "Voiced line.".into());
let result = voiced_behavior(
&cache, 100, "krenn", 42, ContentType::Behavior, 0, None,
"Base line.", false,
);
assert_eq!(result, "Voiced line.");
}
#[test]
fn cache_miss_returns_base_text() {
let cache = test_cache();
let result = voiced_behavior(
&cache, 100, "krenn", 42, ContentType::Behavior, 0, None,
"Base line.", false,
);
assert_eq!(result, "Base line.");
}
#[test]
fn tell_behavior_always_passthrough() {
let cache = test_cache();
// Even if cache has a voiced version, tell behaviors return base text
let key = CacheKey {
culture_id: "krenn".into(),
npc_stable_id: 42,
content_type: ContentType::Behavior,
content_index: 0,
tell_state: Some(TellCategory::Angry),
};
cache.lock().unwrap().store(100, key, "Voiced tell.".into());
let result = voiced_behavior(
&cache, 100, "krenn", 42, ContentType::Behavior, 0,
Some(TellCategory::Angry), "Base tell.", true,
);
assert_eq!(result, "Base tell.");
}
}
+1
View File
@@ -13,6 +13,7 @@
//! - `hardware` — hardware detection + dynamic sr-voice instance management
pub mod cache;
pub mod lookup;
pub mod prompt_builder;
pub mod queue;
pub mod worker;