From 29852fa4782478a5be77fb15d20fe5dac88cca4a Mon Sep 17 00:00:00 2001 From: "Grapette.L" <90967079+Lesereingrape@users.noreply.github.com> Date: Wed, 30 Sep 2026 05:19:27 +0800 Subject: [PATCH] fix(fishaudio): keep a cleared timeout field from disabling every TTS request --- .../sources/fishaudio_tts_api_source.py | 21 +++++++-- tests/test_fishaudio_tts_timeout_guard.py | 44 +++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 tests/test_fishaudio_tts_timeout_guard.py diff --git a/astrbot/core/provider/sources/fishaudio_tts_api_source.py b/astrbot/core/provider/sources/fishaudio_tts_api_source.py index 7fe3aa2e9ce..35f86cff7ce 100644 --- a/astrbot/core/provider/sources/fishaudio_tts_api_source.py +++ b/astrbot/core/provider/sources/fishaudio_tts_api_source.py @@ -38,6 +38,9 @@ class ServeTTSRequest(BaseModel): latency: Literal["normal", "balanced"] = "normal" +DEFAULT_TIMEOUT_SECONDS = 20 + + @register_provider_adapter( "fishaudio_tts_api", "FishAudio TTS API", @@ -57,10 +60,22 @@ def __init__( "api_base", "https://api.fish-audio.cn/v1", ) + raw_timeout = provider_config.get("timeout", DEFAULT_TIMEOUT_SECONDS) try: - self.timeout: int = int(provider_config.get("timeout", 20)) - except ValueError: - self.timeout = 20 + timeout = int(raw_timeout) + except (TypeError, ValueError, OverflowError): + timeout = -1 + if timeout <= 0: + # httpx reads a zero or negative timeout as "expire immediately", so + # every request fails with an empty ConnectTimeout. The dashboard + # writes 0 when this numeric field is cleared, and an empty value in + # a hand-edited config file is None. + logger.warning( + f"[FishAudio TTS] 无效的 timeout 配置 {raw_timeout!r}," + f"使用默认值 {DEFAULT_TIMEOUT_SECONDS}s。", + ) + timeout = DEFAULT_TIMEOUT_SECONDS + self.timeout: int = timeout self.proxy: str = provider_config.get("proxy", "") if self.proxy: logger.info(f"[FishAudio TTS] 使用代理: {self.proxy}") diff --git a/tests/test_fishaudio_tts_timeout_guard.py b/tests/test_fishaudio_tts_timeout_guard.py new file mode 100644 index 00000000000..3977c3ec9a6 --- /dev/null +++ b/tests/test_fishaudio_tts_timeout_guard.py @@ -0,0 +1,44 @@ +"""A cleared FishAudio ``timeout`` must not break every TTS request. + +``timeout`` is a dashboard numeric field (``astrbot/core/config/default.py`` +declares it with ``"type": "int"``), and the dashboard writes ``0`` when such a +field is cleared, while an empty ``timeout:`` in a hand-edited provider config +loads as ``None``. httpx reads a non-positive timeout as "expire immediately" - +measured with a local server that answers after 0.4s: ``AsyncClient(timeout=0)`` +raises ``ConnectTimeout`` whose ``str()`` is empty, while ``timeout=20`` returns +200 - so ``get_audio`` failed on every synthesis with a blank error message, and +``None`` escaped the original ``except ValueError`` and aborted provider +construction with a bare ``TypeError``. +""" + +import pytest + +from astrbot.core.provider.sources.fishaudio_tts_api_source import ( + ProviderFishAudioTTSAPI, +) + +# The default documented in astrbot/core/config/default.py. +EXPECTED_DEFAULT = 20 + +BASE_CONFIG = {"api_key": "test-key", "model": "s2-pro"} + + +def _provider(**overrides) -> ProviderFishAudioTTSAPI: + return ProviderFishAudioTTSAPI({**BASE_CONFIG, **overrides}, {}) + + +@pytest.mark.parametrize( + "timeout", + [0, -5, 0.4, None, "", "abc", float("inf")], +) +def test_unusable_timeout_falls_back_to_default(timeout): + assert _provider(timeout=timeout).timeout == EXPECTED_DEFAULT + + +@pytest.mark.parametrize("timeout", [45, "45", 45.9]) +def test_positive_timeout_is_kept(timeout): + assert _provider(timeout=timeout).timeout == 45 + + +def test_missing_timeout_uses_default(): + assert _provider().timeout == EXPECTED_DEFAULT