Skip to content

Add Gandr TTS plugin - #641

Open
AALG123 wants to merge 3 commits into
GetStream:mainfrom
AALG123:add-gandr
Open

Add Gandr TTS plugin#641
AALG123 wants to merge 3 commits into
GetStream:mainfrom
AALG123:add-gandr

Conversation

@AALG123

@AALG123 AALG123 commented Aug 30, 2026

Copy link
Copy Markdown

What this adds

A new plugins/gandr package exposing gandr.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 existing plugins/openai TTS: it drives the endpoint through the openai client with base_url pointed at Gandr, requests response_format="pcm", and wraps the returned bytes with PcmData.from_bytes at 24000 Hz, s16, mono. Using the openai package for an OpenAI compatible provider follows the existing plugins/openrouter precedent.

Gandr specifics: 6 voices (gandr-mia default, plus gandr-ava, gandr-jenny, gandr-dane, gandr-leo, gandr-lewis), 23 languages, every render is watermarked, 2000 character cap per request (the plugin raises a clear ValueError past the cap instead of surfacing a server error). Keys come from GANDR_API_KEY or the api_key argument. The free tier is 50,000 tokens.

Files

  • plugins/gandr/vision_agents/plugins/gandr/tts.py: the plugin, mirroring plugins/openai/vision_agents/plugins/openai/tts.py
  • plugins/gandr/vision_agents/plugins/gandr/__init__.py: exports TTS
  • plugins/gandr/pyproject.toml: mirrors plugins/openrouter/pyproject.toml (deps: vision-agents, openai>=2.26.0,<3)
  • plugins/gandr/py.typed
  • plugins/gandr/README.md: follows the Cartesia README layout
  • plugins/gandr/tests/test_tts.py: unit tests (defaults, missing key, character cap) plus an integration test gated behind @pytest.mark.integration and GANDR_API_KEY, following the Speechify test layout
  • pyproject.toml: adds plugins/gandr to the workspace members and vision-agents-plugins-gandr to [tool.uv.sources]
  • agents-core/pyproject.toml: adds the gandr extra so uv add "vision-agents[gandr]" works

Testing

  • Unit tests run without network or a key: uv run pytest plugins/gandr/tests -m "not integration"
  • Integration test runs with GANDR_API_KEY set: uv run pytest plugins/gandr/tests -m integration

uv.lock is included, regenerated with uv lock (the only change is the new vision-agents-plugins-gandr entry and the gandr extra on agents-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.

@coderabbitai

coderabbitai Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds the Gandr TTS plugin with OpenAI-compatible audio streaming, API-key validation, 24 kHz PCM output, character-limit validation, and client lifecycle handling. Exposes gandr.TTS as the public package export. Adds package metadata, uv workspace registration, and the gandr optional dependency. Adds documentation and unit and integration tests.

Merge Risk: 🔵 Low · up to fd845

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
plugins/gandr/tests/test_tts.py (1)

43-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add type annotations to the integration test method.

The method omits the tts: gandr.TTS parameter annotation and the -> None return annotation.

Source: Coding guidelines

plugins/gandr/vision_agents/plugins/gandr/tts.py (3)

4-7: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Group third-party imports before local imports.

Move getstream.video.rtc.track_util above the vision_agents imports.

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 win

Annotate the variadic parameters.

Change *_, **__ to *_: object, **__: object to 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 win

Move close before public feature methods.

close is a public lifecycle method but follows stream_audio and stop_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

📥 Commits

Reviewing files that changed from the base of the PR and between 9ee1b92 and f84350d.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • agents-core/pyproject.toml
  • plugins/gandr/README.md
  • plugins/gandr/py.typed
  • plugins/gandr/pyproject.toml
  • plugins/gandr/tests/__init__.py
  • plugins/gandr/tests/test_tts.py
  • plugins/gandr/vision_agents/plugins/gandr/__init__.py
  • plugins/gandr/vision_agents/plugins/gandr/tts.py
  • pyproject.toml

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.

Comment thread plugins/gandr/tests/test_tts.py Outdated
Comment thread plugins/gandr/vision_agents/plugins/gandr/tts.py Outdated
Comment thread plugins/gandr/vision_agents/plugins/gandr/tts.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
plugins/gandr/vision_agents/plugins/gandr/tts.py (1)

6-7: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Order the imports by package scope.

Move getstream.video.rtc.track_util before the local vision_agents import.

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 BaseTTS

As 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9ee1b92 and fd845ff.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • agents-core/pyproject.toml
  • plugins/gandr/README.md
  • plugins/gandr/py.typed
  • plugins/gandr/pyproject.toml
  • plugins/gandr/tests/__init__.py
  • plugins/gandr/tests/test_tts.py
  • plugins/gandr/vision_agents/plugins/gandr/__init__.py
  • plugins/gandr/vision_agents/plugins/gandr/tts.py
  • pyproject.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.

Comment thread plugins/gandr/README.md
@@ -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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Suggested change
async def stream_audio(self, text: str, *_, **__) -> PcmData:
async def stream_audio(
self, text: str, *_: object, **__: object
) -> PcmData:

Source: Coding guidelines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant