fix: serialize Azure TTS backend client lifetimes (#10101)

This commit is contained in:
xiongyuyang
2026-09-19 12:38:51 +08:00
committed by GitHub
parent f09760001a
commit 8492df45e0
2 changed files with 83 additions and 4 deletions
@@ -214,6 +214,7 @@ class AzureTTSProvider(TTSProvider):
super().__init__(provider_config, provider_settings)
key_value = provider_config.get("azure_tts_subscription_key", "")
self.provider = self._parse_provider(key_value, provider_config)
self._synthesis_lock = asyncio.Lock()
def _parse_provider(
self, key_value: str, config: dict
@@ -245,8 +246,9 @@ class AzureTTSProvider(TTSProvider):
raise ValueError("订阅密钥格式无效,应为32位或84位字母数字或other[...]格式")
async def get_audio(self, text: str) -> str:
if isinstance(self.provider, OTTSProvider):
async with self.provider as provider:
# Both backends keep one mutable client; protect its entire lifetime.
async with self._synthesis_lock, self.provider as provider:
if isinstance(provider, OTTSProvider):
return await provider.get_audio(
text,
{
@@ -257,6 +259,5 @@ class AzureTTSProvider(TTSProvider):
"volume": self.provider_config.get("azure_tts_volume"),
},
)
else:
async with self.provider as provider:
else:
return await provider.get_audio(text)
+78
View File
@@ -0,0 +1,78 @@
import asyncio
import json
import pytest
import astrbot.api # noqa: F401
from astrbot.core.provider.sources.azure_tts_source import (
AzureNativeProvider,
AzureTTSProvider,
OTTSProvider,
)
@pytest.mark.asyncio
@pytest.mark.parametrize("backend", ["native", "otts"])
@pytest.mark.parametrize("outcome", ["success", "error", "cancel"])
async def test_overlapping_synthesis_keeps_clients_alive_and_closes_them(
monkeypatch, backend, outcome
):
key = "A" * 32
if backend == "otts":
key = (
"other["
+ json.dumps(
{
"OTTS_SKEY": "dummy",
"OTTS_URL": "https://example.com/tts",
"OTTS_AUTH_TIME": "https://example.com/time",
}
)
+ "]"
)
provider = AzureTTSProvider({"azure_tts_subscription_key": key}, {})
first_started = asyncio.Event()
release_first = asyncio.Event()
clients = []
async def synthesize(instance, text, *args):
client = instance.client
clients.append(client)
if text == "first":
first_started.set()
await release_first.wait()
if outcome == "error":
raise ValueError("synthesis failed")
assert instance.client is client
assert not client.is_closed
return text
monkeypatch.setattr(
AzureNativeProvider if backend == "native" else OTTSProvider,
"get_audio",
synthesize,
)
first = asyncio.create_task(provider.get_audio("first"))
await asyncio.wait_for(first_started.wait(), timeout=5)
second = asyncio.create_task(provider.get_audio("second"))
await asyncio.sleep(0)
if outcome == "cancel":
first.cancel()
release_first.set()
try:
results = await asyncio.wait_for(
asyncio.gather(first, second, return_exceptions=True), timeout=5
)
assert results[1] == "second"
if outcome == "success":
assert results[0] == "first"
elif outcome == "error":
assert isinstance(results[0], ValueError)
else:
assert isinstance(results[0], asyncio.CancelledError)
assert len(clients) == 2
assert clients[0] is not clients[1]
assert all(client.is_closed for client in clients)
finally:
for client in clients:
await client.aclose()