diff --git a/.env.example b/.env.example index 66b2660a..0e51e215 100644 --- a/.env.example +++ b/.env.example @@ -39,6 +39,7 @@ TOGETHER_API_KEY= MINIMAX_API_KEY= MODULATE_API_KEY= LMNT_API_KEY= +FLUXIONS_API_KEY= # --- API (optional) --- # Benchmarking-team key: requests with X-Internal-Key equal to this value see diff --git a/runner/src/coval_bench/config.py b/runner/src/coval_bench/config.py index ae17969f..b7460436 100644 --- a/runner/src/coval_bench/config.py +++ b/runner/src/coval_bench/config.py @@ -102,6 +102,7 @@ def _dataset_id_not_reserved(cls, value: str) -> str: lmnt_api_key: SecretStr | None = None modulate_api_key: SecretStr | None = None speechify_api_key: SecretStr | None = None + fluxions_api_key: SecretStr | None = None # Azure region hosting the Speech resource (e.g. "eastus"). Determines the # region-scoped WebSocket host; required only when the Azure STT provider runs. diff --git a/runner/src/coval_bench/providers/tts/fluxions.py b/runner/src/coval_bench/providers/tts/fluxions.py index 116c3938..380e1198 100644 --- a/runner/src/coval_bench/providers/tts/fluxions.py +++ b/runner/src/coval_bench/providers/tts/fluxions.py @@ -7,11 +7,12 @@ connect → send speak(voice, input) → recv {"type": "start"} → recv binary s16le PCM @ 24 kHz frames → recv {"type": "done"} -Built-in voices are public, so the render path takes no credential and there is -no ``Settings`` key for this provider. ``verify_chunks`` is disabled: the -server's STT re-render pass multiplies TTFA without improving WER. Voice ids -are ``.`` with a rotating hash, so the registry pins the -bare name and this module resolves it against ``GET /vui/voices`` before t0. +The websocket requires an API key, sent as a bearer ``Authorization`` header +on the handshake; unauthenticated connects are closed 1008 by a bot gate. The +voice catalog endpoint is public. ``verify_chunks`` is disabled: the server's +STT re-render pass multiplies TTFA without improving WER. Voice ids are +``.`` with a rotating hash, so the registry pins the bare +name and this module resolves it against ``GET /vui/voices`` before t0. """ from __future__ import annotations @@ -57,6 +58,10 @@ def __init__(self, settings: Settings, model: str, voice: str) -> None: raise ValueError(f"Invalid Fluxions TTS model {model!r}. Valid: {_VALID_MODELS}") if not voice: raise ValueError("Fluxions TTS requires a voice") + api_key_secret = settings.fluxions_api_key + if api_key_secret is None: + raise ValueError("fluxions_api_key is required in Settings") + self._api_key = api_key_secret.get_secret_value() self._model = model self._voice = voice @@ -107,7 +112,9 @@ async def synthesize(self, text: str) -> TTSResult: try: voice_id = await self._resolve_voice() - async with ws_client.connect(_WS_URL) as ws: + async with ws_client.connect( + _WS_URL, additional_headers={"Authorization": f"Bearer {self._api_key}"} + ) as ws: start = time.monotonic() await ws.send( json.dumps( diff --git a/runner/src/coval_bench/registries/models.py b/runner/src/coval_bench/registries/models.py index 61dad249..e6fa2fa9 100644 --- a/runner/src/coval_bench/registries/models.py +++ b/runner/src/coval_bench/registries/models.py @@ -756,7 +756,7 @@ class RegisteredModel(BaseModel, frozen=True, extra="forbid"): status=_EARLY_ACCESS, ), # No model id on the wire, only a voice, so "vui" is the bare surface name. - # Arena-disabled: keyless, so no env var for the key-parity gate to verify. + # Arena-disabled: FLUXIONS_API_KEY is not mounted on benchmarks-api yet. RegisteredModel( benchmark=_TTS, provider="fluxions", diff --git a/runner/src/coval_bench/registries/provider_keys.py b/runner/src/coval_bench/registries/provider_keys.py index 00496409..70b802f5 100644 --- a/runner/src/coval_bench/registries/provider_keys.py +++ b/runner/src/coval_bench/registries/provider_keys.py @@ -39,4 +39,5 @@ "palabra": "PALABRA_API_KEY", "speechify": "SPEECHIFY_API_KEY", "lmnt": "LMNT_API_KEY", + "fluxions": "FLUXIONS_API_KEY", } diff --git a/runner/tests/providers/tts/test_fluxions.py b/runner/tests/providers/tts/test_fluxions.py index 932bfe2a..dd358e08 100644 --- a/runner/tests/providers/tts/test_fluxions.py +++ b/runner/tests/providers/tts/test_fluxions.py @@ -10,6 +10,7 @@ from unittest.mock import patch import pytest +from pydantic import SecretStr from coval_bench.config import Settings from coval_bench.providers.tts import fluxions as fluxions_module @@ -29,6 +30,7 @@ def _settings() -> Settings: dataset_id="stt-v1", runner_sha="test", log_level="DEBUG", + fluxions_api_key=SecretStr("test-api-key"), ) @@ -83,6 +85,7 @@ async def test_fluxions_tts_url_and_speak_frame(fluxions_settings: Settings) -> def connect_side_effect(url: str, **kwargs: object) -> FakeWebSocket: captured["url"] = url + captured["headers"] = kwargs.get("additional_headers") return ws provider = FluxionsTTSProvider(fluxions_settings, model="vui", voice=_VOICE) @@ -95,6 +98,7 @@ def connect_side_effect(url: str, **kwargs: object) -> FakeWebSocket: assert result.error is None assert captured["url"] == "wss://api.fluxions.ai/vui/v1/tts/ws" + assert captured["headers"] == {"Authorization": "Bearer test-api-key"} sent = [json.loads(m) for m in ws.sent if isinstance(m, str)] assert sent == [ { @@ -244,6 +248,12 @@ def test_fluxions_tts_missing_voice_raises(fluxions_settings: Settings) -> None: FluxionsTTSProvider(fluxions_settings, model="vui", voice="") +def test_fluxions_tts_missing_api_key_raises(fluxions_settings: Settings) -> None: + fluxions_settings.fluxions_api_key = None + with pytest.raises(ValueError, match="fluxions_api_key"): + FluxionsTTSProvider(fluxions_settings, model="vui", voice=_VOICE) + + def test_fluxions_tts_provider_name(fluxions_settings: Settings) -> None: provider = FluxionsTTSProvider(fluxions_settings, model="vui", voice=_VOICE) assert provider.name == "fluxions-vui"