Skip to main content

battlecommand_forge/
voice.rs

1//! Voice TTS announcements using macOS `say` command.
2//!
3//! Non-blocking — runs in background via tokio::spawn.
4//! Enable with --voice flag or BATTLECOMMAND_VOICE=1 env.
5
6use std::process::Command;
7
8/// Check if voice is enabled via env or flag.
9pub fn is_enabled() -> bool {
10    std::env::var("BATTLECOMMAND_VOICE")
11        .map(|v| v == "1" || v == "true")
12        .unwrap_or(false)
13}
14
15/// Speak a message in the background (non-blocking).
16/// Uses macOS `say` command. Silently no-ops on other platforms.
17pub fn say(message: &str) {
18    if !is_enabled() || !cfg!(target_os = "macos") {
19        return;
20    }
21    let msg = message.to_string();
22    tokio::spawn(async move {
23        let _ = Command::new("say").args(["-v", "Samantha", &msg]).output();
24    });
25}
26
27/// Announce mission start.
28pub fn mission_start(prompt: &str) {
29    let short: String = prompt.chars().take(60).collect();
30    say(&format!("Mission starting. {}", short));
31}
32
33/// Announce quality gate result.
34pub fn quality_gate(score: f32, passed: bool) {
35    if passed {
36        say(&format!(
37            "Quality gate passed with score {:.1}. Shipping production grade code.",
38            score
39        ));
40    } else {
41        say(&format!(
42            "Quality gate failed. Score {:.1}. Starting fix round.",
43            score
44        ));
45    }
46}
47
48/// Announce fix round.
49pub fn fix_round(round: usize, max: usize) {
50    say(&format!("Fix round {} of {}.", round, max));
51}
52
53/// Announce mission complete.
54pub fn mission_complete(passed: bool, score: f32) {
55    if passed {
56        say(&format!(
57            "Mission complete. Production grade code shipped. Score {:.1}.",
58            score
59        ));
60    } else {
61        say(&format!(
62            "Mission complete. Human review required. Best score {:.1}.",
63            score
64        ));
65    }
66}
67
68/// Announce decomposition.
69pub fn decomposed(count: usize) {
70    say(&format!("Mission decomposed into {} subtasks.", count));
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_is_enabled_default() {
79        // Should be disabled by default
80        assert!(!is_enabled());
81    }
82}