-
Notifications
You must be signed in to change notification settings - Fork 4
Add UpliftAI TTS engine #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| """Example usage of the UpliftAI engine.""" | ||
|
|
||
| import os | ||
|
|
||
| from tts_wrapper import UpliftAIClient | ||
|
|
||
|
|
||
| def main() -> None: | ||
| api_key = os.getenv("UPLIFTAI_KEY") | ||
| if not api_key: | ||
| raise RuntimeError("UPLIFTAI_KEY environment variable is not set") | ||
|
|
||
| client = UpliftAIClient(api_key=api_key) | ||
| text = "Testing the UpliftAI text to speech engine" | ||
| client.speak_streamed(text) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ | |
| PlayHTClient, | ||
| PollyClient, | ||
| SherpaOnnxClient, | ||
| UpliftAIClient, | ||
| WatsonClient, | ||
| WitAiClient, | ||
| eSpeakClient, | ||
|
|
@@ -37,6 +38,7 @@ | |
| "espeak": eSpeakClient, | ||
| "playht": PlayHTClient, | ||
| "openai": OpenAIClient, | ||
| "upliftai": UpliftAIClient, | ||
| } | ||
|
|
||
| # Add AVSynth only on macOS | ||
|
|
@@ -126,6 +128,8 @@ def check_credentials(service): | |
| f"ElevenLabs API key: {elevenlabs_api_key[:5]}...{elevenlabs_api_key[-5:] if elevenlabs_api_key else ''}" | ||
| ) | ||
| client = ElevenLabsClient(credentials=elevenlabs_api_key) | ||
| elif service == "upliftai": | ||
| client = UpliftAIClient(api_key=os.getenv("UPLIFTAI_KEY")) | ||
| elif service == "witai": | ||
| client = WitAiClient(credentials=os.getenv("WITAI_API_KEY")) | ||
| elif service == "googletrans": | ||
|
|
@@ -199,6 +203,8 @@ def create_tts_client(service): | |
| return PlayHTClient( | ||
| credentials=(os.getenv("PLAYHT_API_KEY"), os.getenv("PLAYHT_USER_ID")) | ||
| ) | ||
| if service == "upliftai": | ||
| return UpliftAIClient(api_key=os.getenv("UPLIFTAI_KEY")) | ||
| if service == "avsynth" and sys.platform == "darwin": | ||
| return AVSynthClient() | ||
| if service == "openai": | ||
|
|
@@ -232,6 +238,8 @@ def test_synth_to_bytes(service): | |
| audio_bytes = client.synth_to_bytes(text) | ||
| assert isinstance(audio_bytes, bytes) | ||
| assert len(audio_bytes) > 0 | ||
| if service == "upliftai": | ||
| assert not audio_bytes.startswith(b"RIFF") | ||
| except Exception as e: | ||
| pytest.fail(f"Synthesis failed with error: {e}") | ||
|
|
||
|
|
@@ -247,13 +255,26 @@ def test_synth_to_bytes(service): | |
| audio_bytes = client.synth_to_bytes(ssml_text) | ||
| assert isinstance(audio_bytes, bytes) | ||
| assert len(audio_bytes) > 0 | ||
| if service == "upliftai": | ||
| assert not audio_bytes.startswith(b"RIFF") | ||
| except (AttributeError, NotImplementedError): | ||
| # Skip SSML test for engines that don't support it | ||
| pass | ||
| except Exception as e: | ||
| pytest.fail(f"SSML synthesis failed with error: {e}") | ||
|
|
||
|
|
||
| @pytest.mark.synthetic | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1 | Confidence: High This test validates the WAV header stripping but doesn't verify the audio content remains valid after stripping. The related context shows similar tests for other engines focus on basic audio properties (byte length, format), but don't validate audio integrity. This could allow broken audio to pass tests if the header stripping corrupts the data. Code Suggestion: # Add to test after header check
import wave
from io import BytesIO
# ... after first_chunk assertion ...
full_audio = b''.join(stream)
try:
with BytesIO(full_audio) as bio:
with wave.open(bio) as wav:
assert wav.getnchannels() == 1 # Validate basic audio properties
assert wav.getframerate() == 22050
except Exception as e:
pytest.fail(f"Stripped audio is invalid: {e}") |
||
| def test_upliftai_streaming_strips_wav_header(): | ||
| service = "upliftai" | ||
| if not check_credentials(service): | ||
| pytest.skip("UpliftAI TTS credentials are invalid or unavailable") | ||
| client = create_tts_client(service) | ||
| stream = client.synth_to_bytestream("Streaming header test") | ||
| first_chunk = next(stream) | ||
| assert not first_chunk.startswith(b"RIFF") | ||
|
|
||
|
|
||
| @pytest.mark.synthetic | ||
| @pytest.mark.parametrize("service", TTS_CLIENTS.keys()) | ||
| def test_playback_with_callbacks(service): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| """UpliftAI TTS engine for tts-wrapper.""" | ||
|
|
||
| from .client import UpliftAIClient | ||
|
|
||
| __all__ = ["UpliftAIClient"] |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,183 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import TYPE_CHECKING, Any, Callable | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import requests | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from tts_wrapper.tts import AbstractTTS | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if TYPE_CHECKING: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from collections.abc import Generator | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger = logging.getLogger(__name__) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RIFF_HEADER = b"RIFF" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class UpliftAIClient(AbstractTTS): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Client for the UpliftAI text-to-speech API.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BASE_URL = "https://api.upliftai.org/v1/synthesis/text-to-speech" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| STREAM_URL = f"{BASE_URL}/stream" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DEFAULT_VOICE = "v_8eelc901" # Info/Education Urdu | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def __init__(self, api_key: str | None = None) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| super().__init__() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.api_key = api_key or os.getenv("UPLIFTAI_KEY") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not self.api_key: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| msg = "UpliftAI API key is required. Set UPLIFTAI_KEY or pass api_key." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError(msg) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.headers = {"Authorization": self.api_key, "Content-Type": "application/json"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.audio_rate = 22050 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.voice_id = self.DEFAULT_VOICE | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def set_voice(self, voice_id: str, lang: str | None = None) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Set the voice for synthesis.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.voice_id = voice_id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if lang: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.lang = lang | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def synth_to_bytes(self, text: Any, voice_id: str | None = None) -> bytes: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Synthesize text to audio bytes using the non-streaming endpoint.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| voice = voice_id or self.voice_id or self.DEFAULT_VOICE | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| payload = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "voiceId": voice, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "text": str(text), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "outputFormat": "WAV_22050_16", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response = requests.post(self.BASE_URL, json=payload, headers=self.headers, timeout=30) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response.raise_for_status() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| audio_bytes = response.content | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if audio_bytes[:4] == RIFF_HEADER: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| audio_bytes = self._strip_wav_header(audio_bytes) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return audio_bytes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def synth_to_bytestream( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1 | Confidence: High The WAV header stripping logic in the streaming method assumes a fixed 44-byte header. This is fragile because WAV headers can vary in size depending on format details. The related test (
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self, text: Any, voice_id: str | None = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> Generator[bytes, None, None]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Stream synthesized audio chunks from the API.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| voice = voice_id or self.voice_id or self.DEFAULT_VOICE | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| payload = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "voiceId": voice, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "text": str(text), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "outputFormat": "WAV_22050_16", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with requests.post( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.STREAM_URL, json=payload, headers=self.headers, stream=True, timeout=30 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) as response: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response.raise_for_status() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| header_buffer = b"" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| header_skipped = False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for chunk in response.iter_content(chunk_size=4096): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not chunk: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not header_skipped: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| header_buffer += chunk | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(header_buffer) <= 44: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data = header_buffer[44:] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| header_skipped = True | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not data: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yield data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yield chunk | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def synth( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text: Any, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| output_file: str | Path, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| output_format: str = "wav", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| voice_id: str | None = None, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Synthesize text to a file.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| audio_bytes = self.synth_to_bytes(text, voice_id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with Path(output_file).open("wb") as f: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f.write(audio_bytes) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _get_voices(self) -> list[dict[str, Any]]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2 | Confidence: Medium Hardcoding voice metadata creates a maintenance burden and synchronization risk with the external API. If UpliftAI adds/removes voices, this will require code changes. The related context doesn't show how other engines handle voice metadata, but this static approach is less flexible than dynamic discovery. Code Suggestion: def _get_voices(self) -> list[dict[str, Any]]:
try:
# Attempt to fetch voices dynamically if API adds endpoint
response = requests.get(f"{self.BASE_URL}/voices", headers=self.headers)
return response.json()
except Exception:
logger.warning("Falling back to hardcoded voice list")
return [ ... ] # Hardcoded fallback |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Return the list of available voices. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| The UpliftAI service does not provide a voices endpoint, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| so the voices are hardcoded. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "id": "v_meklc281", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "name": "Info/Education V2", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "gender": "neutral", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "language_codes": ["ur"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "id": "v_8eelc901", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "name": "Info/Education", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "gender": "neutral", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "language_codes": ["ur"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "id": "v_30s70t3a", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "name": "Nostalgic News", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "gender": "neutral", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "language_codes": ["ur"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "id": "v_yypgzenx", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "name": "Dada Jee", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "gender": "male", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "language_codes": ["ur"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "id": "v_kwmp7zxt", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "name": "Gen Z", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "gender": "neutral", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "language_codes": ["ur"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {"id": "v_sd0kl3m9", "name": "Female", "gender": "female", "language_codes": ["sd"]}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "id": "v_sd6mn4p2", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "name": "Male Calm", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "gender": "male", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "language_codes": ["sd"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "id": "v_sd9qr7x5", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "name": "Male News", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "gender": "male", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "language_codes": ["sd"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "id": "v_bl0ab8c4", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "name": "Balochi Male", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "gender": "male", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "language_codes": ["bal"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "id": "v_bl1de2f7", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "name": "Balochi Female", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "gender": "female", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "language_codes": ["bal"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def check_credentials(self) -> bool: # pragma: no cover - network call | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2 | Confidence: Medium The credential check performs a full synthesis request ("ping"), which is expensive and slow compared to a dedicated auth endpoint or lighter-weight request. The related context doesn't show how other engines implement credential checks, but this could become problematic if called frequently (e.g., in test setups). |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Verify that the API key works by making a small request.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| payload = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "voiceId": self.voice_id or self.DEFAULT_VOICE, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "text": "ping", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "outputFormat": "WAV_22050_16", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response = requests.post(self.BASE_URL, json=payload, headers=self.headers, timeout=10) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response.raise_for_status() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.debug("UpliftAI credential check failed", exc_info=True) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def connect(self, event_name: str, callback: Callable) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Connect a callback function to an event.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| super().connect(event_name, callback) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2 | Confidence: High
The
check_credentialsfunction is already flagged by Ruff as overly complex (C901). Adding another conditional branch for UpliftAI exacerbates this maintainability issue. The function now has 25+ branches, making it difficult to modify and test. This pattern also doesn't scale well for new engines. The related context shows this function is critical for test setup across all engines.Code Suggestion: