fix(tui,voice): clean Claude output and harden voice system
Strip directory listing from Claude CLI output in TUI, remove eprintln that corrupts alternate screen, and add MP3 validation to TTS response. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -364,7 +364,7 @@ fn run_once(configs: &[config::AgentConfig]) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Remove directory listing that Claude CLI prepends to output.
|
/// Remove directory listing that Claude CLI prepends to output.
|
||||||
fn strip_dir_listing(text: &str) -> &str {
|
pub fn strip_dir_listing(text: &str) -> &str {
|
||||||
// Pattern: lines of single words (filenames) at the start, possibly starting with "."
|
// Pattern: lines of single words (filenames) at the start, possibly starting with "."
|
||||||
let mut end = 0;
|
let mut end = 0;
|
||||||
for line in text.lines() {
|
for line in text.lines() {
|
||||||
|
|||||||
@@ -176,6 +176,11 @@ impl App {
|
|||||||
self.ai_scroll = u16::MAX;
|
self.ai_scroll = u16::MAX;
|
||||||
}
|
}
|
||||||
OutputEvent::StreamEnd => {
|
OutputEvent::StreamEnd => {
|
||||||
|
// Strip directory listing that Claude CLI prepends
|
||||||
|
let clean = crate::headless::strip_dir_listing(&self.ai_output);
|
||||||
|
if clean.len() != self.ai_output.len() {
|
||||||
|
self.ai_output = clean.to_string();
|
||||||
|
}
|
||||||
write_private(
|
write_private(
|
||||||
&format!("{STATE_DIR}/ai.txt"),
|
&format!("{STATE_DIR}/ai.txt"),
|
||||||
self.ai_output.as_bytes(),
|
self.ai_output.as_bytes(),
|
||||||
|
|||||||
@@ -63,18 +63,15 @@ impl VoiceSystem {
|
|||||||
if text.trim().is_empty() { return; }
|
if text.trim().is_empty() { return; }
|
||||||
let audio = match tts::synthesize(&self.config, text) {
|
let audio = match tts::synthesize(&self.config, text) {
|
||||||
Ok(data) => data,
|
Ok(data) => data,
|
||||||
Err(e) => { eprintln!("tts error: {e}"); return; }
|
Err(_) => return,
|
||||||
};
|
};
|
||||||
if let Err(e) = tts::play_audio(&audio, &self.config.tts_model) {
|
let _ = tts::play_audio(&audio, &self.config.tts_model);
|
||||||
eprintln!("audio play error: {e}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn listen(&self) -> Option<String> {
|
pub fn listen(&self) -> Option<String> {
|
||||||
match stt::recognize(&self.config) {
|
match stt::recognize(&self.config) {
|
||||||
Ok(text) if !text.is_empty() => Some(text),
|
Ok(text) if !text.is_empty() => Some(text),
|
||||||
Ok(_) => None,
|
_ => None,
|
||||||
Err(e) => { eprintln!("stt error: {e}"); None }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ fn record_vad() -> Result<Vec<i16>, String> {
|
|||||||
let mut speech_count: u32 = 0;
|
let mut speech_count: u32 = 0;
|
||||||
let mut total_frames: u32 = 0;
|
let mut total_frames: u32 = 0;
|
||||||
|
|
||||||
eprintln!(" listening...");
|
// Note: no eprintln here — it corrupts TUI alternate screen
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match rx.recv_timeout(std::time::Duration::from_millis(100)) {
|
match rx.recv_timeout(std::time::Duration::from_millis(100)) {
|
||||||
|
|||||||
@@ -45,9 +45,16 @@ pub fn synthesize(config: &VoiceConfig, text: &str) -> Result<Vec<u8>, String> {
|
|||||||
return Err(format!("TTS API error {status}: {body}"));
|
return Err(format!("TTS API error {status}: {body}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.bytes()
|
let bytes = resp.bytes()
|
||||||
.map(|b| b.to_vec())
|
.map(|b| b.to_vec())
|
||||||
.map_err(|e| format!("TTS read error: {e}"))
|
.map_err(|e| format!("TTS read error: {e}"))?;
|
||||||
|
|
||||||
|
// Verify it's actually audio (MP3 starts with ID3 or 0xFF sync)
|
||||||
|
if bytes.len() < 4 || (bytes[0] != 0xFF && &bytes[0..3] != b"ID3") {
|
||||||
|
return Err(format!("TTS returned invalid audio ({} bytes)", bytes.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Play audio bytes (MP3) using rodio with pitch shift from ai.json.
|
/// Play audio bytes (MP3) using rodio with pitch shift from ai.json.
|
||||||
|
|||||||
Reference in New Issue
Block a user