mirror of
https://github.com/AstrBotDevs/AstrBot.git
synced 2026-10-16 15:06:04 +08:00
fix: serialize Azure TTS backend client lifetimes (#10101)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user