Skip to content

feat: add optional 60db text-to-speech provider - #694

Open
uditgoenka wants to merge 1 commit into
GetStream:mainfrom
uditgoenka:feat/60db-tts
Open

uditgoenka wants to merge 1 commit into
GetStream:mainfrom
uditgoenka:feat/60db-tts

Conversation

@uditgoenka

Copy link
Copy Markdown

Why

Applications using 60db workspace voices currently need a custom TTS adapter. 60db exposes an authenticated synthesis API with workspace-scoped voice IDs. This adds it as an optional provider through the existing Vision Agents TTS interface.

Changes

  • Add sixtydb.TTS() and the vision-agents[sixtydb] extra, using standard-library HTTP without an additional SDK dependency.
  • Configure credentials and voice IDs through constructor arguments or SIXTYDB_API_KEY / SIXTYDB_VOICE_ID; accept optional model and speed settings.
  • Request mono PCM16 at 24 kHz, decode binary PCM/WAV and JSON/NDJSON envelopes into PcmData, and reject failed or incompatible responses.
  • Include setup documentation, a standalone WAV example, and local HTTP tests covering the real send_iter() path, interruptions, redirects, timeouts, and response limits.

This provider synthesizes complete utterances and buffers the response. Interruptions discard stale output but do not cancel requests at the service.

Validation: 206 focused plugin, core TTS, and workspace source tests passed; all 36 plugin tests passed again after the final WAV-envelope fix. Ruff, provider mypy, extras validation, lockfile consistency, and wheel/sdist builds passed. Live synthesis and call playback were not tested because service credentials were unavailable.

Expose workspace-scoped synthesis through the existing optional TTS plugin
interface with native HTTP and validated PCM output.

Constraint: Voice IDs and credentials belong to the user's workspace
Confidence: high
Scope-risk: narrow
Tested: 206 focused tests; final 36 plugin tests; Ruff; mypy; extras; lock; wheel and sdist
Not-tested: Live service synthesis and real call playback
@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

🧰 Additional context used
📚 Code guidelines (1)
CLAUDE.md — auto-discovered
📝 Walkthrough

Walkthrough

The change adds a SIXTYDB TTS plugin for mono PCM16 audio at 24 kHz. The provider validates credentials, options, and text; sends synthesis requests; and parses supported response formats with audio validation. The change also registers the plugin in the workspace and optional dependencies. It includes local HTTP tests, usage documentation, and an example that writes streamed audio to a WAV file.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to dfd0c

The optional provider is mergeable with bounded follow-up: preserve service error details to simplify troubleshooting and align its annotations with repository guidance.

Security Architecture Review

Security architecture risk: 🔵 Low · up to dfd0c

The integration is opt-in and includes redirect rejection, response limits, and stale-audio filtering. Interruptions do not cancel remote synthesis or its local worker. Live service behavior and credential permissions were not validated, so the assessment remains low rather than minimal risk.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — Exposure follows applications that select this provider: synthesis text leaves the process, and calls on a provider instance reuse its configured credential and voice. Server-enforced credential privileges and deployment-level tenant isolation are not established by the inspected client code.

Trust Boundaries and Controls

  • observed — The application supplies credentials and synthesis text; the external service supplies audio and response metadata. The client uses a fixed HTTPS endpoint, refuses redirects, rejects reported synthesis errors, and validates supported audio before delivery.

Resilience and Maintainability Implications

  • observed — Interruption suppresses eventual buffered audio through the existing epoch check but does not stop the HTTP worker or cancel service synthesis. The provider has no local admission limit for overlapping calls. Per-request size and timeout controls constrain individual work; aggregate application concurrency remains outside the inspected provider.

Hardening Proposals

  • proposed — For deployments permitting repeated or concurrent synthesis, bound outstanding requests and account for interrupted workers until they actually finish. This would contain shared worker pressure and service spending without assuming that discarding audio cancels synthesis.
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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

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

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

Replace Any with object in _validate and _record_audio.

Both methods take untrusted JSON and check its type at runtime with isinstance(record, dict). The object type works the same way here and follows the repository rule. Inside _record_audio, use dict[str, object] once _validate has run, or narrow the type with an isinstance check.

As per coding guidelines: "Avoid using Any type."

Also applies to: 145-145, 174-174

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: GetStream/Vision-Agents/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: e242679f-fdda-4022-a2c3-1ed76e9673b9

📥 Commits

Reviewing files that changed from the base of the PR and between 902db86 and dfd0cf7.

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

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

Comment on lines +116 to +118
except HTTPError as exc:
exc.close()
raise RuntimeError(f"60db synthesis failed (HTTP {exc.code})") from None

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

🔎 Supported by static analysis

🏁 Script executed:

sed -n '1,240p' plugins/sixtydb/vision_agents/plugins/sixtydb/tts.py
sed -n '1,240p' plugins/sixtydb/tests/test_tts.py
cat plugins/sixtydb/README.md

Repository: GetStream/Vision-Agents

Length of output: 18366


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '--- relevant diff ---'
git diff --unified=20 902db86438aab03fb64aa7e6ea3afdcc867a3e80  dfd0cf7772d4c1ad880a5ff1c5602399200b45e8 -- plugins/sixtydb/vision_agents/plugins/sixtydb/tts.py plugins/sixtydb/tests/test_tts.py
printf '%s\n' '--- HTTP/error handling references ---'
rg -n -g '*.py' 'HTTPError|URLError|synthesis failed|connection failed|exc\.read|read\(2048\)|from None' .
printf '%s\n' '--- relevant project guidance and provider files ---'
rg -n -g 'CLAUDE.md' -g 'README.md' -g '*.py' 'error body|response body|redirect|timeout|HTTP error|diagnostic|60db|tts' plugins/sixtydb CLAUDE.md 2>/dev/null | head -240
printf '%s\n' '--- tracked relevant files ---'
git ls-files '*sixtydb*' '*tts*.py' | head -200

Repository: GetStream/Vision-Agents

Length of output: 26840


🌐 Web query:

official 60db API documentation tts-synthesize HTTP errors response body error code quota invalid voice_id timeout redirect

💡 Result:

**Official docs:** 60db documents `POST https://api.60db.ai/tts-synthesize` and a general error-body shape:

```json
{
  "success": false,
  "error": "Error type",
  "message": "Detailed error message"
}
```

The general API reference lists HTTP statuses including `402` (insufficient credits), `404`, `413`, `422`, `429`, `500`, and `503`; it says rate-limit headers include `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`. ([docs.60db.ai](https://docs.60db.ai/api-reference/introduction))

**What’s not established in the docs:** The TTS endpoint page documents the request and successful response, but does not specify endpoint-specific error codes/bodies for quota, invalid `voice_id`, timeout, or redirects. So the general status list alone does not confirm which of those apply to TTS or the exact error code/body for each. It says to fetch valid voice IDs from `GET /voices`. ([docs.60db.ai](https://docs.60db.ai/api-reference/tts/text-to-speech))

Closest official references: [TTS endpoint](https://docs.60db.ai/api-reference/tts/text-to-speech) and [general API reference](https://docs.60db.ai/api-reference/introduction).

Citations:

- 1: https://docs.60db.ai/api-reference/introduction
- 2: https://docs.60db.ai/api-reference/tts/text-to-speech

🤖 get_repo_knowledge executed:

get_repo_knowledge GetStream/Vision-Agents /tmp/coderabbit-repo-knowledge/getstream-vision-agents-11d6373a/architecture /tmp/coderabbit-repo-knowledge/getstream-vision-agents-11d6373a/learnings

Length of output: 47118


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '--- relevant diff ---'
git diff --unified=20 902db86438aab03fb64aa7e6ea3afdcc867a3e80 dfd0cf7772d4c1ad880a5ff1c5602399200b45e8 -- plugins/sixtydb/vision_agents/plugins/sixtydb/tts.py plugins/sixtydb/tests/test_tts.py
printf '%s\n' '--- HTTP/error handling references ---'
rg -n -g '*.py' 'HTTPError|URLError|synthesis failed|connection failed|exc\.read|read\(2048\)|from None' .
printf '%s\n' '--- relevant project guidance and provider files ---'
rg -n -g 'CLAUDE.md' -g 'README.md' -g '*.py' 'error body|response body|redirect|timeout|HTTP error|diagnostic|60db|tts' plugins/sixtydb CLAUDE.md 2>/dev/null | head -240
printf '%s\n' '--- tracked relevant files ---'
git ls-files '*sixtydb*' '*tts*.py' | head -200

Repository: GetStream/Vision-Agents

Length of output: 26840


🏁 Script executed:

git diff --unified=20 902db86438aab03fb64aa7e6ea3afdcc867a3e80 dfd0cf7772d4c1ad880a5ff1c5602399200b45e8 -- plugins/sixtydb/vision_agents/plugins/sixtydb/tts.py plugins/sixtydb/tests/test_tts.py
printf '\n--- references ---\n'
rg -n -g '*.py' 'HTTPError|URLError|synthesis failed|connection failed|exc\.read|from None' .
printf '\n--- provider guidance ---\n'
rg -n -g 'CLAUDE.md' -g 'README.md' -g '*.py' 'error body|response body|redirect|timeout|HTTP error|diagnostic|60db|tts' plugins/sixtydb CLAUDE.md 2>/dev/null | head -240

Repository: GetStream/Vision-Agents

Length of output: 24968


Preserve 60db error details for non-redirect HTTP failures.

The 60db API documents error and detailed message fields in its error response. The current urllib.error.HTTPError handler discards that body and exposes only the status code. Keep redirects status-only and continue rejecting them.

Suggested fix
         except HTTPError as exc:
-            exc.close()
-            raise RuntimeError(f"60db synthesis failed (HTTP {exc.code})") from None
+            detail = ""
+            try:
+                if not 300 <= exc.code < 400:
+                    detail = (
+                        exc.read(2048).decode("utf-8", errors="replace").strip()
+                    )
+            finally:
+                exc.close()
+            message = f"60db synthesis failed (HTTP {exc.code})"
+            if detail:
+                message += f": {detail}"
+            raise RuntimeError(message) from None
📝 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
except HTTPError as exc:
exc.close()
raise RuntimeError(f"60db synthesis failed (HTTP {exc.code})") from None
except HTTPError as exc:
detail = ""
try:
if not 300 <= exc.code < 400:
detail = (
exc.read(2048).decode("utf-8", errors="replace").strip()
)
finally:
exc.close()
message = f"60db synthesis failed (HTTP {exc.code})"
if detail:
message += f": {detail}"
raise RuntimeError(message) from None

This branch has not been deployed

No deployments
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