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
+9 -1
View File
@@ -92,4 +92,12 @@ Game concept, prototype boundaries, production pipeline, and feature decisions.
---
*14 questions (1 resolved, 3 partially resolved, 10 open). Last updated: 2026-03-05 (Q-011 resolved, Q-034 and Q-037 partially resolved — Where's the Fun? Workshop)*
### Q-058: Runtime behavior text serving system
- **Status:** Open
- **Question:** How should NPC observable behaviors be served to the client at runtime? `NpcBlueprint.observable_behaviors` exists as generator output but no runtime system reads it or sends behavior text to the client. The voice pipeline (D-138) needs an integration point: voice cache lookup replaces base text with re-voiced text before delivery. Needs: which system selects the current behavior, how it's delivered in `ObserverSnapshot`, and how tell behaviors (always passthrough) are distinguished from voiceable behaviors.
- **Assigned to:** Tyre, SI
- **Source:** Voice pipeline Spike 2 Phase 3
---
*15 questions (1 resolved, 3 partially resolved, 11 open). Last updated: 2026-03-07 (Q-058 added — voice pipeline Phase 3 dependency)*
+1 -1
View File
@@ -9,7 +9,7 @@ Tracked questions awaiting discussion or resolution. Split by domain, mirroring
| [questions-architecture.md](questions-architecture.md) | Technical foundation | Q-001, Q-006, Q-009, Q-018, Q-019, Q-020, Q-021, Q-022, Q-023, Q-029, Q-030, Q-046 |
| [questions-perception.md](questions-perception.md) | Player observation | Q-003, Q-014, Q-016, Q-024, Q-025, Q-026, Q-051, Q-053, Q-054 |
| [questions-content.md](questions-content.md) | Narrative, NPCs, setting | Q-010, Q-012, Q-013, Q-015, Q-017, Q-028, Q-031, Q-033, Q-040, Q-041, Q-042, Q-043, Q-044, Q-045, Q-047, Q-048, Q-049, Q-050, Q-052, Q-056 |
| [questions-scope.md](questions-scope.md) | Game concept, prototype | Q-002, Q-004, Q-005, Q-007, Q-008, Q-011, Q-027, Q-032, Q-034, Q-035, Q-036, Q-037, Q-038, Q-039 |
| [questions-scope.md](questions-scope.md) | Game concept, prototype | Q-002, Q-004, Q-005, Q-007, Q-008, Q-011, Q-027, Q-032, Q-034, Q-035, Q-036, Q-037, Q-038, Q-039, Q-058 |
## Status Summary
+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;