From 4d7434f9afad7cbb6586701b8175a2c1689065a3 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Tue, 29 Sep 2026 16:03:23 +0200 Subject: [PATCH] fix: explain why designed-voice benchmarks fail and stop batches on an unreachable service Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 2 ++ core/tts_helpers.py | 9 ++++++++- static/js/voice-library.js | 15 +++++++++++++-- tests/test_benchmark_errors.py | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 tests/test_benchmark_errors.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d7819b..6b39351 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi --- ## [Unreleased] +### Fixed +- **Voice benchmark failures now say why.** Designed voices are benchmarked through the Voice Design engine; when that service is down the row showed a bare "-" and the raw "Max retries exceeded" text. The error now names the service and the container to start, the Factor and WPM columns show `ERR` beside the time column, and a batch stops after three identical "unreachable" failures instead of retrying every remaining voice. ### Added - `scripts/compare_magazines.py`: read-only comparison of two magazine folders (words, boxes, reading order, articles, quality numbers) for re-import checks. diff --git a/core/tts_helpers.py b/core/tts_helpers.py index c182a9a..1978643 100644 --- a/core/tts_helpers.py +++ b/core/tts_helpers.py @@ -310,7 +310,14 @@ def _voice_design_voice_request_audio( # randomness. See the config-side comment for the full story. _apply_tts_extra_params(payload, settings, "voice_design_playback") _apply_voice_pinned_seed(payload, voice, settings) - resp = _post_tts_with_fallback(f"{vd_url}/v1/audio/speech", payload, vd_hdrs, timeout=180) + try: + resp = _post_tts_with_fallback(f"{vd_url}/v1/audio/speech", payload, vd_hdrs, timeout=180) + except (requests.ConnectionError, requests.Timeout) as e: + # The raw "Max retries exceeded with url ..." text told the user nothing about what to do. + raise RuntimeError( + f"Voice Design service unreachable at {vd_url} — start the " + f"'tts-faster-qwen3-tts-voicedesign' container (Engines tab) and retry" + ) from e resp.raise_for_status() audio = resp.content if not audio: diff --git a/static/js/voice-library.js b/static/js/voice-library.js index add6dec..d637e02 100644 --- a/static/js/voice-library.js +++ b/static/js/voice-library.js @@ -434,7 +434,9 @@ function voiceFactor(v) { function fmtFactor(v) { const f = voiceFactor(v); - return f != null ? f.toFixed(2) + 'x' : '-'; + if (f != null) return f.toFixed(2) + 'x'; + const b = voiceBenchmark(v); + return b && !b.ok ? 'ERR' : '-'; } function fmtElapsed(v) { @@ -932,7 +934,8 @@ async function runVoiceBenchmarkBatch() { }); setBenchmarkProgress(0, total, 'Starting benchmark...', stats); - for (let i = 0; i < voices.length; i++) { + let lastFailure = '', sameFailure = 0, aborted = false; + for (let i = 0; i < voices.length && !aborted; i++) { const voice = voices[i]; const row = document.querySelector(`.vl-row[data-id="${CSS.escape(voice.id)}"]`); if (row) { @@ -971,6 +974,14 @@ async function runVoiceBenchmarkBatch() { } else { stats.errors++; stats.last = `${voice.id}: failed${b && b.error ? ' · ' + b.error : ''}`; + // The same unreachable service failing repeatedly means the rest will fail too. + const reason = b && b.error ? String(b.error) : ''; + sameFailure = reason && reason === lastFailure ? sameFailure + 1 : 1; + lastFailure = reason; + if (/unreachable/i.test(reason) && sameFailure >= 3) { + toast(reason, 'error'); + aborted = true; + } } } } catch(e) { diff --git a/tests/test_benchmark_errors.py b/tests/test_benchmark_errors.py new file mode 100644 index 0000000..56b2975 --- /dev/null +++ b/tests/test_benchmark_errors.py @@ -0,0 +1,34 @@ +"""A designed voice's benchmark must explain an unreachable Voice Design service.""" +import os +import tempfile +import unittest +from unittest import mock + +os.environ.setdefault("CONFIG_DIR", tempfile.mkdtemp(prefix="ttvc-test-config-")) + +import requests + +from core import tts_helpers + + +class VoiceDesignUnreachableTest(unittest.TestCase): + def test_connection_error_becomes_actionable_message(self): + with mock.patch.object(tts_helpers, "_post_tts_with_fallback", + side_effect=requests.ConnectionError("Max retries exceeded")): + with self.assertRaises(RuntimeError) as ctx: + tts_helpers._voice_design_voice_request_audio( + "DE_F_Test", "Hallo", {"voice_design_url": "http://127.0.0.1:9"}) + self.assertIn("Voice Design service unreachable", str(ctx.exception)) + self.assertIn("tts-faster-qwen3-tts-voicedesign", str(ctx.exception)) + + def test_http_errors_are_not_masked(self): + resp = mock.Mock(status_code=500) + resp.raise_for_status.side_effect = requests.HTTPError("500") + with mock.patch.object(tts_helpers, "_post_tts_with_fallback", return_value=resp): + with self.assertRaises(requests.HTTPError): + tts_helpers._voice_design_voice_request_audio( + "DE_F_Test", "Hallo", {"voice_design_url": "http://127.0.0.1:9"}) + + +if __name__ == "__main__": + unittest.main()