Add Gandr TTS plugin - #641
Conversation
📝 WalkthroughWalkthroughAdds the Gandr TTS plugin with OpenAI-compatible audio streaming, API-key validation, 24 kHz PCM output, character-limit validation, and client lifecycle handling. Exposes Merge Risk: 🔵 Low · up to The plugin adds Gandr text-to-speech support and normally sends requests to the expected HTTPS endpoint. Because callers can override the endpoint, an untrusted configuration could redirect the API key and synthesized text to another host; the change is mergeable with explicit owner awareness that endpoint configuration must remain trusted. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (4)
plugins/gandr/tests/test_tts.py (1)
43-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd type annotations to the integration test method.
The method omits the
tts: gandr.TTSparameter annotation and the-> Nonereturn annotation.Source: Coding guidelines
plugins/gandr/vision_agents/plugins/gandr/tts.py (3)
4-7: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winGroup third-party imports before local imports.
Move
getstream.video.rtc.track_utilabove thevision_agentsimports.As per coding guidelines: imports must be ordered as stdlib, third-party, local package, relative.
Source: Coding guidelines
55-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAnnotate the variadic parameters.
Change
*_, **__to*_: object, **__: objectto satisfy the repository’s type-annotation rule without changing the ignored arguments.As per coding guidelines: “Use type annotations everywhere.”
Source: Coding guidelines
81-87: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove
closebefore public feature methods.
closeis a public lifecycle method but followsstream_audioandstop_audio. Place lifecycle methods immediately after__init__.As per coding guidelines: methods are ordered
__init__, public lifecycle methods, properties, public feature methods, private helpers, dunder methods.Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 07ed4d19-e12f-45e4-8d4b-f54835b0d458
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (9)
agents-core/pyproject.tomlplugins/gandr/README.mdplugins/gandr/py.typedplugins/gandr/pyproject.tomlplugins/gandr/tests/__init__.pyplugins/gandr/tests/test_tts.pyplugins/gandr/vision_agents/plugins/gandr/__init__.pyplugins/gandr/vision_agents/plugins/gandr/tts.pypyproject.toml
Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.
…ase_url (test_tts.py)
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
plugins/gandr/vision_agents/plugins/gandr/tts.py (1)
6-7: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winOrder the imports by package scope.
Move
getstream.video.rtc.track_utilbefore the localvision_agentsimport.Proposed fix
-from vision_agents.core.tts.tts import TTS as BaseTTS from getstream.video.rtc.track_util import PcmData, AudioFormat +from vision_agents.core.tts.tts import TTS as BaseTTSAs per coding guidelines, "Order imports as: stdlib, third-party, local package, relative."
Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: b5cdba66-fa93-41a8-ba4f-c04a733be7bc
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (9)
agents-core/pyproject.tomlplugins/gandr/README.mdplugins/gandr/py.typedplugins/gandr/pyproject.tomlplugins/gandr/tests/__init__.pyplugins/gandr/tests/test_tts.pyplugins/gandr/vision_agents/plugins/gandr/__init__.pyplugins/gandr/vision_agents/plugins/gandr/tts.pypyproject.toml
🚧 Files skipped from review as they are similar to previous changes (5)
- agents-core/pyproject.toml
- plugins/gandr/vision_agents/plugins/gandr/init.py
- pyproject.toml
- plugins/gandr/pyproject.toml
- plugins/gandr/tests/test_tts.py
Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.
| @@ -0,0 +1,68 @@ | |||
| # Gandr | |||
|
|
|||
| [Gandr](https://gandr.ai) is a Text-to-Speech (TTS) API with an OpenAI compatible speech endpoint. It ships 6 voices covering 23 languages, and every render is watermarked. First audio byte in 146 ms over the open internet, 116 ms p50 first audio, server side warm. | |||
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add hyphens to compound modifiers.
Use OpenAI-compatible, server-side, 16-bit, and little-endian in the public documentation.
Proposed wording fix
-[Gandr](https://gandr.ai) is a Text-to-Speech (TTS) API with an OpenAI compatible speech endpoint. It ships 6 voices covering 23 languages, and every render is watermarked. First audio byte in 146 ms over the open internet, 116 ms p50 first audio, server side warm.
+[Gandr](https://gandr.ai) is a Text-to-Speech (TTS) API with an OpenAI-compatible speech endpoint. It ships 6 voices covering 23 languages, and every render is watermarked. First audio byte in 146 ms over the open internet, 116 ms p50 first audio, server-side warm.
@@
-The plugin requests `pcm` output: headerless signed 16 bit little endian mono at 24000 Hz, so the bytes are wrapped directly with no decode step.
+The plugin requests `pcm` output: headerless signed 16-bit little-endian mono at 24000 Hz, so the bytes are wrapped directly with no decode step.Also applies to: 59-59
🧰 Tools
🪛 LanguageTool
[grammar] ~3-~3: Use a hyphen to join words.
Context: ... Text-to-Speech (TTS) API with an OpenAI compatible speech endpoint. It ships 6 v...
(QB_NEW_EN_HYPHEN)
[grammar] ~3-~3: Use a hyphen to join words.
Context: ...internet, 116 ms p50 first audio, server side warm. The Gandr plugin for the Str...
(QB_NEW_EN_HYPHEN)
Source: Linters/SAST tools
| self.model = model | ||
| self.voice = voice | ||
|
|
||
| async def stream_audio(self, text: str, *_, **__) -> PcmData: |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Add type annotations to the variadic parameters.
Annotate *_ and **__. Use object when these ignored values intentionally accept arbitrary types.
Proposed fix
- async def stream_audio(self, text: str, *_, **__) -> PcmData:
+ async def stream_audio(
+ self, text: str, *_: object, **__: object
+ ) -> PcmData:As per coding guidelines, "Use type annotations everywhere" and "Avoid using Any type."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async def stream_audio(self, text: str, *_, **__) -> PcmData: | |
| async def stream_audio( | |
| self, text: str, *_: object, **__: object | |
| ) -> PcmData: |
Source: Coding guidelines
What this adds
A new
plugins/gandrpackage exposinggandr.TTS, a Text-to-Speech plugin for the Gandr API.Gandr's speech endpoint is OpenAI compatible (
POST https://tts.gandr.ai/v1/audio/speech), so the plugin follows the same shape as the existingplugins/openaiTTS: it drives the endpoint through theopenaiclient withbase_urlpointed at Gandr, requestsresponse_format="pcm", and wraps the returned bytes withPcmData.from_bytesat 24000 Hz, s16, mono. Using theopenaipackage for an OpenAI compatible provider follows the existingplugins/openrouterprecedent.Gandr specifics: 6 voices (
gandr-miadefault, plusgandr-ava,gandr-jenny,gandr-dane,gandr-leo,gandr-lewis), 23 languages, every render is watermarked, 2000 character cap per request (the plugin raises a clearValueErrorpast the cap instead of surfacing a server error). Keys come fromGANDR_API_KEYor theapi_keyargument. The free tier is 50,000 tokens.Files
plugins/gandr/vision_agents/plugins/gandr/tts.py: the plugin, mirroringplugins/openai/vision_agents/plugins/openai/tts.pyplugins/gandr/vision_agents/plugins/gandr/__init__.py: exportsTTSplugins/gandr/pyproject.toml: mirrorsplugins/openrouter/pyproject.toml(deps:vision-agents,openai>=2.26.0,<3)plugins/gandr/py.typedplugins/gandr/README.md: follows the Cartesia README layoutplugins/gandr/tests/test_tts.py: unit tests (defaults, missing key, character cap) plus an integration test gated behind@pytest.mark.integrationandGANDR_API_KEY, following the Speechify test layoutpyproject.toml: addsplugins/gandrto the workspace members andvision-agents-plugins-gandrto[tool.uv.sources]agents-core/pyproject.toml: adds thegandrextra souv add "vision-agents[gandr]"worksTesting
uv run pytest plugins/gandr/tests -m "not integration"GANDR_API_KEYset:uv run pytest plugins/gandr/tests -m integrationuv.lockis included, regenerated withuv lock(the only change is the newvision-agents-plugins-gandrentry and thegandrextra onagents-core). Unit tests pass (3 passed, integration skipped without a key), and the integration test passes against the live endpoint with a key set.Disclosure: I work on Gandr.