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:
@@ -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)*
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user