← Commit history

v1.9.53: --rate validated up front (signed percentage, normalised), service error bodies surfaced, an identical repeated 5xx is no longer called service flapping (issue #6)

John Lauer ·e23451a11a ·1mo ago ·parent f2bc3cc
4 files changed +43−31
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-tts"-version = "1.9.52"+version = "1.9.53" edition = "2021" license = "AGPL-3.0-or-later" repository = "https://github.com/adom-inc/adom-tts"
VERSION+1−1
@@ -1 +1 @@-1.9.52+1.9.53
page.json−21
@@ -12,7 +12,6 @@     "audio",     "demo",     "accessibility",-    "cli",     "open-source",     "mit"   ],@@ -65,25 +64,5 @@   ],   "install": {     "binary_name": "adom-tts"-  },-  "description": "Adom's shared text-to-speech CLI + Playground. Reads your AI answers back to you while driving, and renders perfectly-timed voiceover tracks for demo videos.",-  "dependencies": {},-  "scripts": {-    "install": "./install.sh",-    "uninstall": "./uninstall.sh"-  },-  "needs_sudo": false,-  "files": [-    "package.json",-    "install.sh",-    "uninstall.sh",-    "bin/adom-tts",-    "skills/**",-    "completions/**"-  ],-  "org": "adom",-  "author": {-    "name": "John Lauer",-    "email": "[email protected]"   } }\ No newline at end of file
src/main.rs+41−8
@@ -474,10 +474,10 @@ fn main() -> Result<()> {         Cmd::Voices          => cmd_get("/tts/voices"),         Cmd::Pronunciations  => cmd_get("/tts/pronunciations"),         Cmd::Say { text, out, voice, rate, no_pronunciations, history, play, surface, wait, thread, desc, confirm, allow_repeat } =>-            cmd_say(&text, &out, &voice, &rate, !no_pronunciations, history, play, surface, wait, thread, desc, confirm, allow_repeat),+            cmd_say(&text, &out, &voice, &normalize_rate(&rate)?, !no_pronunciations, history, play, surface, wait, thread, desc, confirm, allow_repeat),         Cmd::Install         => cmd_install(),         Cmd::Serve { port, no_tab, dev } => cmd_serve(port, !no_tab, dev),-        Cmd::Push { file, text, voice, rate } => cmd_push(&file, &text, &voice, &rate),+        Cmd::Push { file, text, voice, rate } => cmd_push(&file, &text, &voice, &normalize_rate(&rate)?),         Cmd::Play { target, surface, allow_repeat, wait } => play::run(&target, surface, wait, allow_repeat),         Cmd::Control { action } => play::control(&action),         Cmd::Drain { surface } => play::drain(surface),@@ -492,7 +492,7 @@ fn main() -> Result<()> {             QueueAction::Drop { clip } => play::queue_drop(&clip),         },         Cmd::Load { text, thread, id, voice, rate, no_pronunciations } =>-            cmd_load(&text, &thread, id, &voice, &rate, !no_pronunciations),+            cmd_load(&text, &thread, id, &voice, &normalize_rate(&rate)?, !no_pronunciations),         Cmd::Open { print_url } => play::open(print_url),         Cmd::Ui { action: UiAction::Toast { text, kind } } => play::ui_toast(&text, &kind),         Cmd::Demo => cmd_demo(),@@ -631,6 +631,25 @@ fn cmd_get(path: &str) -> Result<()> {     Ok(()) } +/// `--rate` must be a signed percentage (edge-tts syntax): `+10%`, `-4%`, `+0%`.+/// Accepts the unsigned and percent-less spellings people type (`10`, `-4`,+/// `15%`) and normalises them; anything else is refused HERE, naming the flag,+/// instead of reaching the service as a request it cannot serve (issue #6: a+/// negative rate came back as a bare 500 and an hour of "service flapping").+fn normalize_rate(raw: &str) -> Result<String> {+    let t = raw.trim();+    let (sign, rest) = match t.chars().next() {+        Some('+') => ("+", &t[1..]),+        Some('-') => ("-", &t[1..]),+        _ => ("+", t),+    };+    let digits = rest.trim_end_matches('%');+    if digits.is_empty() || digits.len() > 3 || !digits.chars().all(|c| c.is_ascii_digit()) {+        return Err(anyhow!("--rate must be a signed percentage like +10% or -4% (got '{raw}').\nHint: adom-tts say \"...\" --rate=-4%   (use = so a leading minus is not read as a flag)"));+    }+    Ok(format!("{sign}{digits}%"))+}+ /// Synthesize ONE (short) chunk via service-tts, retrying transient flaps with /// backoff. Returns (mp3_bytes, cache, hash). Exits the process on a /// non-recoverable failure — NEVER falls back to a local engine (shared-infra).@@ -646,15 +665,29 @@ fn synth_chunk(text: &str, voice: &str, rate: &str, apply: bool) -> (Vec<u8>, St             Ok(r) => {                 let code = r.status().as_u16();                 let transient = code >= 500 || code == 408 || code == 429;-                play::log_event("synth_attempt", serde_json::json!({ "attempt": attempt, "http": code, "transient": transient }));+                let said: String = r.text().unwrap_or_default().chars().take(200).collect();+                play::log_event("synth_attempt", serde_json::json!({ "attempt": attempt, "http": code, "transient": transient, "body": said }));                 if transient && attempt < MAX_ATTEMPTS {-                    eprintln!("adom-tts: HTTP {code} (attempt {attempt}/{MAX_ATTEMPTS}) — service flapping, retrying");+                    eprintln!("adom-tts: HTTP {code} (attempt {attempt}/{MAX_ATTEMPTS}), retrying");                     std::thread::sleep(std::time::Duration::from_millis(400 * attempt as u64));                     continue;                 }-                play::log_event("synth_failed", serde_json::json!({ "http": code, "attempts": attempt }));-                eprintln!("adom-tts: HTTP {code} after {attempt} attempt(s)");-                eprintln!("# hint: service-tts failing — retry later. Long text is already auto-chunked; do NOT install a local TTS engine.");+                play::log_event("synth_failed", serde_json::json!({ "http": code, "attempts": attempt, "voice": voice, "rate": rate, "chars": text.chars().count(), "body": said }));+                let args = format!("voice {voice}, rate {rate}, {} chars", text.chars().count());+                if transient {+                    // The SAME request failed the same way every time while the+                    // service kept answering: that is not a flap, it is this request.+                    eprintln!("adom-tts: HTTP {code} on all {attempt} attempts with identical arguments ({args}).");+                    if !said.is_empty() { eprintln!("  service said: {said}"); }+                    eprintln!("# hint: a failure that repeats identically is NOT the service flapping. Check the arguments first:");+                    eprintln!("#       try the same text without --rate and without --voice; if that works, the argument is the bug (report it on");+                    eprintln!("#       the adom-tts wiki page). If it still fails, `adom-tts health`, then retry later. Long text is already");+                    eprintln!("#       auto-chunked. Do NOT install a local TTS engine.");+                } else {+                    eprintln!("adom-tts: HTTP {code} ({args}).");+                    if !said.is_empty() { eprintln!("  service said: {said}"); }+                    eprintln!("# hint: the service refused THIS request; fix the argument it names and re-run. Do NOT install a local TTS engine.");+                }                 std::process::exit(4);             }             Err(e) => {