Skip to main content

clawft_plugin/voice/
wake_daemon.rs

1//! Background daemon for continuous wake word detection.
2//!
3//! Runs the [`WakeWordDetector`] in a background loop, monitoring
4//! audio input for the "Hey Weft" trigger phrase. When detected,
5//! activates Talk Mode.
6//!
7//! Currently a **stub implementation** -- real audio capture and
8//! rustpotter integration deferred until after VP validation.
9
10use tracing::info;
11
12use crate::error::PluginError;
13use crate::traits::CancellationToken;
14
15use super::wake::{WakeWordConfig, WakeWordDetector};
16
17/// Daemon that runs wake word detection in the background.
18///
19/// When the wake word is detected, the daemon can activate Talk Mode.
20/// The daemon runs until cancelled via its [`CancellationToken`].
21pub struct WakeDaemon {
22    detector: WakeWordDetector,
23    active: bool,
24}
25
26impl WakeDaemon {
27    /// Create a new wake daemon with the given configuration.
28    pub fn new(config: WakeWordConfig) -> Result<Self, PluginError> {
29        let detector = WakeWordDetector::new(config)?;
30        Ok(Self {
31            detector,
32            active: false,
33        })
34    }
35
36    /// Run the daemon until cancelled.
37    ///
38    /// STUB: Logs that the daemon is running and waits for cancellation.
39    /// Real implementation will continuously capture audio and feed it
40    /// to the wake word detector.
41    #[cfg(not(target_arch = "wasm32"))]
42    pub async fn run(&mut self, cancel: CancellationToken) -> Result<(), PluginError> {
43        info!("wake daemon started (stub)");
44        self.active = true;
45        self.detector.start();
46
47        // Wait for cancellation.
48        cancel.cancelled().await;
49
50        self.detector.stop();
51        self.active = false;
52        info!("wake daemon stopped");
53        Ok(())
54    }
55
56    /// Check if the daemon is currently active.
57    pub fn is_active(&self) -> bool {
58        self.active
59    }
60
61    /// Get a reference to the underlying detector.
62    pub fn detector(&self) -> &WakeWordDetector {
63        &self.detector
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn wake_daemon_create() {
73        let config = WakeWordConfig::default();
74        let daemon = WakeDaemon::new(config).unwrap();
75        assert!(!daemon.is_active());
76    }
77
78    #[test]
79    fn wake_daemon_detector_access() {
80        let config = WakeWordConfig {
81            threshold: 0.3,
82            ..Default::default()
83        };
84        let daemon = WakeDaemon::new(config).unwrap();
85        assert!((daemon.detector().config().threshold - 0.3).abs() < f32::EPSILON);
86    }
87
88    #[tokio::test]
89    async fn wake_daemon_run_and_cancel() {
90        let config = WakeWordConfig::default();
91        let mut daemon = WakeDaemon::new(config).unwrap();
92        let cancel = CancellationToken::new();
93        let cancel_clone = cancel.clone();
94
95        let handle = tokio::spawn(async move { daemon.run(cancel_clone).await });
96
97        // Give the daemon a moment to start.
98        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
99
100        // Cancel and verify clean shutdown.
101        cancel.cancel();
102        let result = handle.await.unwrap();
103        assert!(result.is_ok());
104    }
105}