Skip to main content

clawft_plugin/voice/
talk_mode.rs

1//! Talk Mode controller for continuous voice conversation.
2//!
3//! Manages the lifecycle of a voice conversation session,
4//! coordinating the VoiceChannel with the agent pipeline.
5
6use std::sync::Arc;
7
8use tracing::info;
9
10use crate::error::PluginError;
11use crate::traits::{CancellationToken, ChannelAdapter, ChannelAdapterHost};
12
13use super::channel::{VoiceChannel, VoiceStatus};
14
15/// Controller for Talk Mode -- continuous voice conversation.
16///
17/// Wraps a [`VoiceChannel`] and manages the listen -> transcribe ->
18/// agent -> speak loop. The controller runs until the cancellation
19/// token is triggered (e.g., by Ctrl+C in the CLI).
20pub struct TalkModeController {
21    channel: Arc<VoiceChannel>,
22    cancel: CancellationToken,
23}
24
25impl TalkModeController {
26    /// Create a new Talk Mode controller.
27    ///
28    /// # Arguments
29    ///
30    /// * `channel` - The voice channel to control.
31    /// * `cancel` - Cancellation token to stop the session.
32    pub fn new(channel: Arc<VoiceChannel>, cancel: CancellationToken) -> Self {
33        Self { channel, cancel }
34    }
35
36    /// Run the Talk Mode loop until cancelled.
37    ///
38    /// Starts the voice channel and blocks until the cancellation
39    /// token is triggered. In the real implementation, the voice
40    /// channel would continuously capture audio, detect speech,
41    /// transcribe it, and deliver it to the agent pipeline.
42    pub async fn run(&self, host: Arc<dyn ChannelAdapterHost>) -> Result<(), PluginError> {
43        info!("Talk Mode starting");
44        let result = self.channel.start(host, self.cancel.clone()).await;
45        info!("Talk Mode ended");
46        result
47    }
48
49    /// Get the current voice status.
50    pub async fn status(&self) -> VoiceStatus {
51        self.channel.current_status().await
52    }
53
54    /// Get a reference to the underlying voice channel.
55    pub fn channel(&self) -> &Arc<VoiceChannel> {
56        &self.channel
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use std::collections::HashMap;
64
65    use async_trait::async_trait;
66    use crate::message::MessagePayload;
67
68    struct StubHost;
69
70    #[async_trait]
71    impl ChannelAdapterHost for StubHost {
72        async fn deliver_inbound(
73            &self,
74            _channel: &str,
75            _sender_id: &str,
76            _chat_id: &str,
77            _payload: MessagePayload,
78            _metadata: HashMap<String, serde_json::Value>,
79        ) -> Result<(), PluginError> {
80            Ok(())
81        }
82    }
83
84    #[tokio::test]
85    async fn talk_mode_status_starts_idle() {
86        let (channel, _rx) = VoiceChannel::new();
87        let cancel = CancellationToken::new();
88        let controller = TalkModeController::new(Arc::new(channel), cancel);
89        assert_eq!(controller.status().await, VoiceStatus::Idle);
90    }
91
92    #[tokio::test]
93    async fn talk_mode_run_and_cancel() {
94        let (channel, _rx) = VoiceChannel::new();
95        let cancel = CancellationToken::new();
96        let cancel_clone = cancel.clone();
97        let controller = Arc::new(TalkModeController::new(Arc::new(channel), cancel_clone));
98        let host: Arc<dyn ChannelAdapterHost> = Arc::new(StubHost);
99
100        let handle = tokio::spawn({
101            let controller = Arc::clone(&controller);
102            async move { controller.run(host).await }
103        });
104
105        // Give the channel a moment to start.
106        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
107
108        cancel.cancel();
109        let result = handle.await.unwrap();
110        assert!(result.is_ok());
111    }
112}