Skip to content

(feat): move dragontts code to calirvoyance - #943

Closed
narsimhaReddyJuspay wants to merge 1 commit into
juspay:releasefrom
narsimhaReddyJuspay:migrate-dragontts-to-calirvoyance-repo
Closed

narsimhaReddyJuspay wants to merge 1 commit into
juspay:releasefrom
narsimhaReddyJuspay:migrate-dragontts-to-calirvoyance-repo

Conversation

@narsimhaReddyJuspay

@narsimhaReddyJuspay narsimhaReddyJuspay commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added a complete DragonTTS caching service with one-shot and streaming text-to-speech support.
    • Added Cartesia, ElevenLabs, Sarvam, and Gemini provider integrations.
    • Added audio format conversion, resampling, text normalization, cache stitching, predictive warming, and resilience controls.
    • Added cache administration, analytics, health, provider, and Slack summary endpoints.
    • Added Kubernetes deployment manifests, persistent storage configuration, and operational documentation.
  • Documentation

    • Added setup, endpoint reference, scaling, deployment, backlog, and roadmap documentation.

Copilot AI review requested due to automatic review settings July 22, 2026 17:54
@coderabbitai

coderabbitai Bot commented Jul 22, 2026 •

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4ac4641b-de81-4868-948d-64a889efb8a6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

DragonTTS adds a FastAPI TTS caching service with multiple provider adapters, native-format audio storage and conversion, streaming and stitching, cache administration and analytics, Slack reporting, SQLite/filesystem persistence, Kubernetes deployment manifests, and operational documentation.

Changes

DragonTTS service

Layer / File(s) Summary
Contracts, configuration, and audio primitives
app/dragontts/app/core/*, app/dragontts/app/audio/*, app/dragontts/app/cache/key.py, app/dragontts/app/schemas/*, app/dragontts/app/storage/base.py
Defines runtime settings, TTS/cache schemas, provider and storage contracts, cache-key hashing, text normalization, audio conversion, streaming resampling, and provider resilience controls.
Provider adapters and streaming pools
app/dragontts/app/providers/*
Adds Cartesia, ElevenLabs, Gemini, and Sarvam adapters with one-shot synthesis, streaming WebSocket pools, warm-up, cleanup, and fallback behavior.
Storage and cache orchestration
app/dragontts/app/storage/*, app/dragontts/app/cache/*
Adds SQLite metadata and analytics, filesystem blobs, write-behind metrics, TTL management, single-flight synthesis, predictive warming, phrase stitching, split transcripts, streaming, and cache administration.
API, lifecycle, and alert integration
app/dragontts/app/api/v1/*, app/dragontts/app/main.py, app/dragontts/app/alerts/*, app/dragontts/app/core/logging.py
Wires TTS, cache, health, statistics, Slack summary, startup/shutdown, maintenance loops, and structured logging.
Packaging, deployment, and validation
app/dragontts/Dockerfile, app/dragontts/pyproject.toml, app/dragontts/deploy/k8s/*, app/dragontts/docs/*, app/dragontts/smoke_live.py
Adds container and Kubernetes resources, endpoint and scaling documentation, a roadmap, local setup guidance, and live provider/cache smoke tests.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: copilot

Poem

A rabbit heard the cache bells ring,
While TTS sprites learned to sing.
Bytes hop fast through stores so neat,
Streams and providers tap their feet.
Slack sends carrots, logs glow bright—
DragonTTS burrows through the night.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.24% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: migrating DragonTTS code into the Clairvoyance repo.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR vendors the DragonTTS FastAPI service into the Clairvoyance repository as a nested, self-contained TTS caching proxy, including provider adapters (Cartesia/ElevenLabs/Sarvam/Gemini), cache/storage layers, operational docs, and Kubernetes deployment manifests.

Changes:

  • Adds the DragonTTS application code (FastAPI app, cache service, providers, storage, logging, alerts).
  • Adds local/dev + operational assets (pyproject, Dockerfile, smoke script, endpoint docs, runbooks).
  • Adds k8s manifests for deploying DragonTTS (Deployment/Service/PVC) in the beta namespace.

Reviewed changes

Copilot reviewed 44 out of 53 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
app/dragontts/smoke_live.py Live-provider smoke test script (synth + stream + cache roundtrip).
app/dragontts/README.md Local run instructions and operational overview for DragonTTS.
app/dragontts/pyproject.toml DragonTTS project metadata and dependencies (incl. dev deps).
app/dragontts/docs/TODO.md Captures deferred operational improvements and rollout notes.
app/dragontts/docs/endpoints.md Detailed endpoint reference and curl examples.
app/dragontts/docs/dragontts-scaling-and-isolation.md Operational runbook for node-pool/PVC scaling migration.
app/dragontts/Dockerfile Container build for DragonTTS using uv and uvicorn workers.
app/dragontts/deploy/k8s/service.yaml ClusterIP Service definition for DragonTTS.
app/dragontts/deploy/k8s/pvc.yaml PersistentVolumeClaim for cache DB/blob storage.
app/dragontts/deploy/k8s/deployment.yaml Deployment manifest including env knobs, probes, volumes, resources.
app/dragontts/app/init.py Package marker and version.
app/dragontts/app/main.py FastAPI app + lifespan wiring (stores, providers, background loops).
app/dragontts/app/core/init.py Core package marker.
app/dragontts/app/core/config.py Env-backed settings and provider defaults.
app/dragontts/app/core/logging.py Loguru + stdlib logging interception (JSON sink for GCP).
app/dragontts/app/api/init.py API package marker.
app/dragontts/app/api/v1/init.py v1 router package marker.
app/dragontts/app/api/v1/health.py Health/provider introspection endpoints.
app/dragontts/app/api/v1/tts.py TTS endpoints (bytes/stream/check/create/delete/bulk).
app/dragontts/app/api/v1/cache.py Cache admin + stats + eviction + slack-summary trigger endpoints.
app/dragontts/app/schemas/init.py Schemas package marker.
app/dragontts/app/schemas/tts.py Pydantic request models (Cartesia-compatible shape).
app/dragontts/app/schemas/cache.py Pydantic response models for cache admin endpoints.
app/dragontts/app/storage/init.py Storage package marker.
app/dragontts/app/storage/base.py Storage protocols + core record types + LIKE escaping helper.
app/dragontts/app/storage/filesystem.py Content-addressed filesystem blob store.
app/dragontts/app/storage/sqlite.py SQLite metadata/metrics store implementation.
app/dragontts/app/providers/init.py Providers package marker.
app/dragontts/app/providers/base.py Provider adapter contract and common result/error types.
app/dragontts/app/providers/registry.py Provider registry + warmup + configured-provider discovery.
app/dragontts/app/providers/cartesia.py Cartesia provider adapter (HTTP + streaming WS pool).
app/dragontts/app/providers/cartesia_pool.py Warm Cartesia multiplexed WebSocket pool implementation.
app/dragontts/app/providers/elevenlabs.py ElevenLabs provider adapter (HTTP + multi-context WS pool).
app/dragontts/app/providers/elevenlabs_pool.py Warm ElevenLabs multi-context WebSocket pool implementation.
app/dragontts/app/providers/sarvam.py Sarvam provider adapter (HTTP + single-stream WS pool + resample).
app/dragontts/app/providers/sarvam_pool.py Warm Sarvam single-stream WebSocket pool implementation.
app/dragontts/app/providers/gemini.py Gemini provider adapter (gRPC streaming + 24k->16k resample).
app/dragontts/app/audio/init.py Audio package marker.
app/dragontts/app/audio/format.py Native->requested audio format conversion (PCM/μ-law + ratecv).
app/dragontts/app/audio/resample.py Streaming resampler for providers with non-16k native streams.
app/dragontts/app/audio/text.py Text normalization (number expansion + ElevenLabs leading-dot hint).
app/dragontts/app/cache/init.py Cache package marker.
app/dragontts/app/cache/key.py Cache key derivation (model_id parsing, normalization, hashing).
app/dragontts/app/cache/segment.py DP segmentation helper for stitch planning.
app/dragontts/app/cache/resilience.py Per-provider bulkhead + token-bucket rate limiter.
app/dragontts/app/cache/metrics.py Write-behind metrics batching for HIT-path DB writes.
app/dragontts/app/cache/tracker.py Predictive warming tracker for frequent phrase substrings.
app/dragontts/app/cache/service.py Core cache service (lookup/synth/store/convert/stream/stitch/metrics).
app/dragontts/app/alerts/init.py Alerts package marker.
app/dragontts/app/alerts/slack.py Slack incoming-webhook sender (Block Kit).
app/dragontts/app/alerts/summary.py Daily cache-economics Slack summary builder + scheduler gate.

@@ -0,0 +1,237 @@
"""Live provider smoke-test (Cartesia / ElevenLabs / Sarvam) — NO commit.
Comment on lines +67 to +71
with open(tmp, "wb") as f:
f.write(data)
f.flush()
os.fsync(f.fileno())
os.replace(tmp, path)
Comment on lines +501 to +512
key_to_span: dict[str, tuple[int, int]] = {}
for lo in range(n):
for hi in range(lo + 1, min(lo + MAX_SPAN, n) + 1):
sub_text = normalize_for_tts(" ".join(words[lo:hi]), provider)
key_to_span[hash_key(
text=sub_text, provider=provider, voice_id=req.voice.id,
model=model, language=req.language, params_canonical=params_canon,
)] = (lo, hi)
records = await self._metadata.get_many(key_to_span.keys())
span_record = { # (lo, hi) -> record, only non-expired
key_to_span[k]: r for k, r in records.items() if not self._expired(r)
}
Comment on lines +831 to +851
record = await self._metadata.get(key)
if record and not self._expired(record):
t_cs = time.perf_counter()
native = await self._blobs.get(record.storage_path)
audio = await self._convert_audio(
native,
native_encoding=record.encoding,
native_rate=record.sample_rate,
out_encoding=of.encoding,
out_rate=of.sample_rate,
)
await self._metrics.touch_and_record(
key, {"requests": 1, "hits": 1, "bytes_served": len(audio),
"words_served": _wc(req.transcript)},
provider=provider,
)
self._hits += 1
logger.info(f"CACHE HIT key={key[:12]}… provider={provider}")
await self._timed("cache_serve", t_cs, provider)
await self._timed("total", t0, provider)
return audio, {"X-Cache": "HIT", "X-Cache-Key": key}
Comment on lines +1005 to +1028
record = await self._metadata.get(key)
if record and not self._expired(record):
t_cs = time.perf_counter()
native = await self._blobs.get(record.storage_path)
if _same_format(of.encoding, of.sample_rate, record.encoding, record.sample_rate):
served, chunks = native, _chunked(native)
else:
served = await self._convert_audio(
native,
native_encoding=record.encoding,
native_rate=record.sample_rate,
out_encoding=of.encoding,
out_rate=of.sample_rate,
)
chunks = _chunked(served)
await self._metrics.touch_and_record(
key, {"requests": 1, "hits": 1, "bytes_served": len(served),
"words_served": _wc(req.transcript)},
provider=provider,
)
self._hits += 1
logger.info(f"CACHE HIT (stream) key={key[:12]}… provider={provider}")
await self._timed("cache_serve", t_cs, provider)
return {"X-Cache": "HIT", "X-Cache-Key": key}, self._timed_chunks(chunks, t0, provider)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 14

🧹 Nitpick comments (10)
app/dragontts/deploy/k8s/deployment.yaml (1)

27-197: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Make the container root filesystem read-only.

All persistent application data is already mounted at /app/data; set readOnlyRootFilesystem: true in the container security context and add an emptyDir mount for any required temporary path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dragontts/deploy/k8s/deployment.yaml` around lines 27 - 197, Update the
dragontts container security context to set readOnlyRootFilesystem: true. Add an
emptyDir volume and mount it at the required temporary filesystem path, while
preserving the existing /app/data persistent volume mount and declaring the new
volume under the pod volumes section.

Source: Linters/SAST tools

app/dragontts/smoke_live.py (1)

177-233: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Use structured smoke-test observability.

Replace print-only reporting with Loguru records carrying provider/test context, and emit the provider calls under Langfuse traces. This keeps live validation searchable and correlatable with provider failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dragontts/smoke_live.py` around lines 177 - 233, Update main and the
smoke-test reporting flow to replace print-only output with structured Loguru
records that include the provider and test phase context, while preserving the
existing pass/failure details. Wrap each provider’s test_provider and cache
round-trip provider calls in Langfuse traces so individual provider failures are
searchable and correlatable; retain the final aggregate result reporting through
structured logs.

Source: Coding guidelines

app/dragontts/app/providers/gemini.py (1)

69-87: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Blocking file read on an async path.

service_account.Credentials.from_service_account_file does synchronous file I/O and JSON parsing on first use, called directly from synth/stream_synth without asyncio.to_thread. Since this is a lazily-built, cached singleton, the cost is paid once — but that one time blocks the entire event loop (delaying all other in-flight requests) whenever it happens to coincide with the first Gemini call.

Based on coding guidelines: "Use async/await for all I/O operations; do not use synchronous I/O."

♻️ Proposed fix
-    def _get_client(self) -> texttospeech_v1.TextToSpeechAsyncClient:
+    async def _get_client(self) -> texttospeech_v1.TextToSpeechAsyncClient:
         """Lazily build + cache the authenticated async gRPC client."""
         if self._client is None:
             if not self.credentials_json and not self.credentials_path:
                 raise ProviderError(
                     "GOOGLE_CREDENTIALS_JSON/PATH is required for Gemini TTS"
                 )
-            if self.credentials_json:
-                json_account_info = json.loads(self.credentials_json)
-                creds = service_account.Credentials.from_service_account_info(
-                    json_account_info
-                )
-            else:
-                creds = service_account.Credentials.from_service_account_file(
-                    self.credentials_path
-                )
+            creds = await asyncio.to_thread(self._load_credentials)
             self._creds = creds
             self._client = texttospeech_v1.TextToSpeechAsyncClient(credentials=creds)
         return self._client

(and update call sites to await self._get_client())

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dragontts/app/providers/gemini.py` around lines 69 - 87, Make _get_client
asynchronous and move the synchronous
service_account.Credentials.from_service_account_file call into
asyncio.to_thread, preserving the existing lazy credential and client caching
behavior. Update synth and stream_synth to await self._get_client() at their
call sites, while keeping the credentials_json path and client construction
behavior unchanged.

Source: Coding guidelines

app/dragontts/app/storage/base.py (1)

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

Annotate the keys parameter. get_many is the only signature here missing a type hint; downstream calls pass an iterable of key strings (records = await self._metadata.get_many(key_to_span.keys())).

♻️ Suggested annotation
-    async def get_many(self, keys) -> dict[str, CacheRecord]: ...
+    async def get_many(self, keys: Iterable[str]) -> dict[str, CacheRecord]: ...

Add from collections.abc import Iterable to imports.

As per coding guidelines: "Include required type hints on all function signatures".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dragontts/app/storage/base.py` at line 44, Update the
BaseStorage.get_many signature to annotate keys as an Iterable of strings,
importing Iterable from collections.abc, while preserving its existing dict[str,
CacheRecord] return annotation.

Source: Coding guidelines

app/dragontts/app/cache/key.py (1)

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

Use explicit escape sequences for the zero-width set. The literal invisible characters are unreviewable and can be silently altered by editors/formatters or copy-paste, which would change key derivation. Spelling them as \uXXXX keeps identical behavior with a readable, tamper-resistant source.

♻️ Suggested (verify the full set matches the originals)
-_ZW = "​‌‍‎‏­"
+_ZW = "\u200b\u200c\u200d\u200e\u200f\ufeff\u00ad"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dragontts/app/cache/key.py` around lines 25 - 26, Update the _ZW constant
to represent the same zero-width character set using explicit \uXXXX escape
sequences instead of literal invisible characters, then keep _ZW_TABLE derived
from _ZW so key derivation behavior remains unchanged.

Source: Linters/SAST tools

app/dragontts/app/cache/metrics.py (1)

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

Missing type hint on metadata parameter.

As per coding guidelines, "**/*.py": Include required type hints on all function signatures. Will consolidate with similar findings elsewhere.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dragontts/app/cache/metrics.py` around lines 24 - 42, Update the __init__
method signature in the metrics class to add an explicit type hint for the
metadata parameter, using the appropriate existing metadata type or protocol
while preserving the current behavior and annotations of the other parameters.

Source: Coding guidelines

app/dragontts/app/storage/filesystem.py (1)

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

Missing return type hint on iter_blobs.

As per coding guidelines, "**/*.py": Include required type hints on all function signatures. This async generator has no return/yield type annotation. Will consolidate with similar findings in other files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dragontts/app/storage/filesystem.py` around lines 99 - 114, Add an
explicit return type annotation to the Filesystem.iter_blobs async generator,
describing that it yields pairs of string values for the blob key and relative
path. Preserve its current iteration and reconciliation behavior.

Source: Coding guidelines

app/dragontts/app/alerts/summary.py (2)

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

cache parameter untyped.

As per coding guidelines, "**/*.py": Include required type hints on all function signatures. Will consolidate with similar findings elsewhere.

Also applies to: 147-147

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dragontts/app/alerts/summary.py` at line 49, Add the required type hints
to the cache parameter in build_summary and the other similarly affected
function signature, using the appropriate cache type already established by the
module or project conventions. Preserve the existing return annotation and
function behavior.

Source: Coding guidelines


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

Reaches into CacheService's private _metadata attribute.

cache._metadata, cache._metadata.claim_slack_summary, and cache._metadata.release_slack_summary are all called directly on the leading-underscore attribute from outside the class, bypassing encapsulation. A rename/refactor of CacheService._metadata would silently break this module without a compile-time signal.

# In CacheService (cache/service.py), expose a narrow accessor instead:
`@property`
def metadata(self):
    return self._metadata

Then use cache.metadata here instead of cache._metadata.

Also applies to: 167-168, 184-187

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dragontts/app/alerts/summary.py` around lines 53 - 56, Replace all
external uses of CacheService’s private cache._metadata in the alert summary
flow, including the metrics, claim_slack_summary, and release_slack_summary
calls, with the public cache.metadata accessor. Add a metadata property to
CacheService that returns _metadata, then update the referenced call sites to
use it.
app/dragontts/app/alerts/slack.py (1)

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

Untyped fields/sections/links parameters.

As per coding guidelines, "**/*.py": Include required type hints on all function signatures. Both _blocks and send leave these collection params untyped. Will consolidate with similar findings elsewhere.

Also applies to: 78-81

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dragontts/app/alerts/slack.py` around lines 43 - 45, Update the Slack
alert methods _blocks and send to add explicit type hints for the fields,
sections, and links collection parameters, using types consistent with how each
value is consumed and returned. Keep the existing behavior unchanged and ensure
every parameter in both function signatures is typed.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/dragontts/app/cache/service.py`:
- Around line 1389-1408: Update the local byte_off helper in the sub-phrase
storage flow to round the timestamp to a whole sample index using rate, then
multiply that sample index by bps to produce the byte offset. Ensure all offsets
used for slicing in this loop are sample- and 16-bit-byte aligned.

In `@app/dragontts/app/providers/cartesia_pool.py`:
- Around line 23-29: Reorder the imports in cartesia_pool.py so the straight
import for time is grouped with asyncio, base64, json, and uuid before all
from-imports, including collections.abc and typing.

In `@app/dragontts/app/providers/cartesia.py`:
- Around line 111-112: Unify missing-API-key handling in synth() across all
providers by replacing the ValueError raises in
app/dragontts/app/providers/cartesia.py:111-112,
app/dragontts/app/providers/elevenlabs.py:160-161, and
app/dragontts/app/providers/sarvam.py:114-115 with the ProviderNotConfigured
error type used by each provider’s stream_synth() path, preserving the existing
provider-specific messages.

In `@app/dragontts/app/providers/gemini.py`:
- Around line 175-178: The Gemini synthesis logs in synth() at
app/dragontts/app/providers/gemini.py:175-178 and stream_synth() at
app/dragontts/app/providers/gemini.py:248-251 expose transcript text; remove
text[:50] from both INFO messages and replace it with a non-identifying
indicator such as text length or a hash, while retaining the voice, model, and
language context.
- Around line 284-291: Update the aclose method to call the
TextToSpeechAsyncClient public aclose() method instead of accessing
self._client.transport.grpc_channel directly. Preserve the existing exception
warning and reset self._client to None after cleanup.

In `@app/dragontts/app/providers/sarvam_pool.py`:
- Around line 176-181: Update the audio decoding exception handler in the Sarvam
pool streaming logic to log the base64 decode failure, including relevant error
details and context about the malformed audio frame, before continuing to
process subsequent frames.

In `@app/dragontts/deploy/k8s/deployment.yaml`:
- Around line 146-169: Replace the empty provider credential values in
app/dragontts/deploy/k8s/deployment.yaml lines 146-169 with
valueFrom.secretKeyRef references to the required Kubernetes Secret keys for
each credential. In app/dragontts/docs/endpoints.md lines 317-318, document the
required Secret and how to apply it. In app/dragontts/nxt_steps.md lines
291-293, remove “already” until the manifest references Kubernetes Secrets.

In `@app/dragontts/docs/dragontts-scaling-and-isolation.md`:
- Around line 54-56: Update the current-state Deployment dragontts record to use
the manifest’s node-selector key cloud.google.com/gke-nodepool, replacing
cloud.google.com/gke-node-pool while preserving the existing node-pool value and
other deployment details.

In `@app/dragontts/docs/endpoints.md`:
- Around line 290-295: Update the BULK_CREATE_MAX entry in the Performance /
resilience table to document the configured limit of 1000 instead of 100,
keeping the purpose description unchanged.

In `@app/dragontts/docs/TODO.md`:
- Around line 40-50: Revise the TODO guidance for the lifespan reconcile_blobs
flow: do not claim that simply scheduling
asyncio.create_task(cache.reconcile_blobs()) prevents startup scan spikes, since
every worker would still run it concurrently. Specify either a single-worker
lease/leader-election mechanism for reconcile_blobs, with existing try/except
logging preserved, or limit the description to readiness deferral and explicitly
address task failure handling.

In `@app/dragontts/smoke_live.py`:
- Around line 229-232: Update the all_ok calculation in the smoke-test result
flow to include every cache round-trip entry from cr["provider_results"],
requiring the expected MISS and HIT statuses and byte-identical responses in
addition to the existing synthesis and streaming checks. Ensure any cache
failure causes the final result to print FAILURES.
- Line 117: Update the temporary-directory setup in the smoke test to create the
directory with await asyncio.to_thread(tempfile.mkdtemp, ...) instead of
blocking the event loop. Ensure the temporary store is always removed in a
finally block using await asyncio.to_thread(shutil.rmtree, ...), preserving the
existing test flow and cleanup even when execution fails.
- Around line 211-214: Format the output statements near the synth and stream
status prints, including the lines around 223-225, with Black so they comply
with the repository’s 88-character line limit. Preserve the existing output
content and behavior.
- Around line 49-60: Complete the type annotations for _drain, test_provider,
test_cache_roundtrip, nested get_provider, _mark, and main, including all
parameters and return types. Use the concrete Dict[str, Any], Optional[...],
async iterator, and provider types already appropriate to each function, while
preserving their existing behavior.

---

Nitpick comments:
In `@app/dragontts/app/alerts/slack.py`:
- Around line 43-45: Update the Slack alert methods _blocks and send to add
explicit type hints for the fields, sections, and links collection parameters,
using types consistent with how each value is consumed and returned. Keep the
existing behavior unchanged and ensure every parameter in both function
signatures is typed.

In `@app/dragontts/app/alerts/summary.py`:
- Line 49: Add the required type hints to the cache parameter in build_summary
and the other similarly affected function signature, using the appropriate cache
type already established by the module or project conventions. Preserve the
existing return annotation and function behavior.
- Around line 53-56: Replace all external uses of CacheService’s private
cache._metadata in the alert summary flow, including the metrics,
claim_slack_summary, and release_slack_summary calls, with the public
cache.metadata accessor. Add a metadata property to CacheService that returns
_metadata, then update the referenced call sites to use it.

In `@app/dragontts/app/cache/key.py`:
- Around line 25-26: Update the _ZW constant to represent the same zero-width
character set using explicit \uXXXX escape sequences instead of literal
invisible characters, then keep _ZW_TABLE derived from _ZW so key derivation
behavior remains unchanged.

In `@app/dragontts/app/cache/metrics.py`:
- Around line 24-42: Update the __init__ method signature in the metrics class
to add an explicit type hint for the metadata parameter, using the appropriate
existing metadata type or protocol while preserving the current behavior and
annotations of the other parameters.

In `@app/dragontts/app/providers/gemini.py`:
- Around line 69-87: Make _get_client asynchronous and move the synchronous
service_account.Credentials.from_service_account_file call into
asyncio.to_thread, preserving the existing lazy credential and client caching
behavior. Update synth and stream_synth to await self._get_client() at their
call sites, while keeping the credentials_json path and client construction
behavior unchanged.

In `@app/dragontts/app/storage/base.py`:
- Line 44: Update the BaseStorage.get_many signature to annotate keys as an
Iterable of strings, importing Iterable from collections.abc, while preserving
its existing dict[str, CacheRecord] return annotation.

In `@app/dragontts/app/storage/filesystem.py`:
- Around line 99-114: Add an explicit return type annotation to the
Filesystem.iter_blobs async generator, describing that it yields pairs of string
values for the blob key and relative path. Preserve its current iteration and
reconciliation behavior.

In `@app/dragontts/deploy/k8s/deployment.yaml`:
- Around line 27-197: Update the dragontts container security context to set
readOnlyRootFilesystem: true. Add an emptyDir volume and mount it at the
required temporary filesystem path, while preserving the existing /app/data
persistent volume mount and declaring the new volume under the pod volumes
section.

In `@app/dragontts/smoke_live.py`:
- Around line 177-233: Update main and the smoke-test reporting flow to replace
print-only output with structured Loguru records that include the provider and
test phase context, while preserving the existing pass/failure details. Wrap
each provider’s test_provider and cache round-trip provider calls in Langfuse
traces so individual provider failures are searchable and correlatable; retain
the final aggregate result reporting through structured logs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 347932d5-a64e-48f4-98c3-779a4cce3771

📥 Commits

Reviewing files that changed from the base of the PR and between a6842d6 and 15da0c0.

⛔ Files ignored due to path filters (1)
  • app/dragontts/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (52)
  • app/dragontts/Dockerfile
  • app/dragontts/README.md
  • app/dragontts/app/__init__.py
  • app/dragontts/app/alerts/__init__.py
  • app/dragontts/app/alerts/slack.py
  • app/dragontts/app/alerts/summary.py
  • app/dragontts/app/api/__init__.py
  • app/dragontts/app/api/v1/__init__.py
  • app/dragontts/app/api/v1/cache.py
  • app/dragontts/app/api/v1/health.py
  • app/dragontts/app/api/v1/tts.py
  • app/dragontts/app/audio/__init__.py
  • app/dragontts/app/audio/format.py
  • app/dragontts/app/audio/resample.py
  • app/dragontts/app/audio/text.py
  • app/dragontts/app/cache/__init__.py
  • app/dragontts/app/cache/key.py
  • app/dragontts/app/cache/metrics.py
  • app/dragontts/app/cache/resilience.py
  • app/dragontts/app/cache/segment.py
  • app/dragontts/app/cache/service.py
  • app/dragontts/app/cache/tracker.py
  • app/dragontts/app/core/__init__.py
  • app/dragontts/app/core/config.py
  • app/dragontts/app/core/logging.py
  • app/dragontts/app/main.py
  • app/dragontts/app/providers/__init__.py
  • app/dragontts/app/providers/base.py
  • app/dragontts/app/providers/cartesia.py
  • app/dragontts/app/providers/cartesia_pool.py
  • app/dragontts/app/providers/elevenlabs.py
  • app/dragontts/app/providers/elevenlabs_pool.py
  • app/dragontts/app/providers/gemini.py
  • app/dragontts/app/providers/registry.py
  • app/dragontts/app/providers/sarvam.py
  • app/dragontts/app/providers/sarvam_pool.py
  • app/dragontts/app/schemas/__init__.py
  • app/dragontts/app/schemas/cache.py
  • app/dragontts/app/schemas/tts.py
  • app/dragontts/app/storage/__init__.py
  • app/dragontts/app/storage/base.py
  • app/dragontts/app/storage/filesystem.py
  • app/dragontts/app/storage/sqlite.py
  • app/dragontts/deploy/k8s/deployment.yaml
  • app/dragontts/deploy/k8s/pvc.yaml
  • app/dragontts/deploy/k8s/service.yaml
  • app/dragontts/docs/TODO.md
  • app/dragontts/docs/dragontts-scaling-and-isolation.md
  • app/dragontts/docs/endpoints.md
  • app/dragontts/nxt_steps.md
  • app/dragontts/pyproject.toml
  • app/dragontts/smoke_live.py

Comment on lines +1389 to +1408
rate, bps = 16000, 2
clip_end = len(audio) / (rate * bps)

def byte_off(t: float) -> int:
return int(round(t * rate * bps))

# Sub-phrase word-index ranges to store: every contiguous slice if
# aligned, else just the whole phrase.
n = len(norm_words)
ranges = (
[(i, j) for i in range(n) for j in range(i + 1, n + 1)]
if aligned
else [(0, n)]
)

stored = 0
for i, j in ranges:
if aligned:
end_t = starts[j] if j < len(starts) else clip_end
slc = audio[byte_off(starts[i]) : byte_off(end_t)]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

byte_off can split 16-bit samples — misaligned slices corrupt audio and crash on serve.

For pcm_s16le each sample is 2 bytes, but int(round(t * rate * bps)) can land on an odd byte offset. Slicing there begins mid-sample, byte-shifting the whole sub-clip (audible noise), and an odd-length slice later hits np.frombuffer(..., dtype="<i2") in _prep_clip/_to_float, raising ValueError when that warmed sub-phrase is stitched or served. Round to whole samples first, then scale to bytes.

🐛 Proposed fix
-        def byte_off(t: float) -> int:
-            return int(round(t * rate * bps))
+        def byte_off(t: float) -> int:
+            # Align to whole int16 samples: rounding on raw bytes can yield an
+            # odd offset, splitting a sample (byte-shifted noise) and producing
+            # odd-length slices that break np.frombuffer("<i2") downstream.
+            return int(round(t * rate)) * bps
📝 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
rate, bps = 16000, 2
clip_end = len(audio) / (rate * bps)
def byte_off(t: float) -> int:
return int(round(t * rate * bps))
# Sub-phrase word-index ranges to store: every contiguous slice if
# aligned, else just the whole phrase.
n = len(norm_words)
ranges = (
[(i, j) for i in range(n) for j in range(i + 1, n + 1)]
if aligned
else [(0, n)]
)
stored = 0
for i, j in ranges:
if aligned:
end_t = starts[j] if j < len(starts) else clip_end
slc = audio[byte_off(starts[i]) : byte_off(end_t)]
rate, bps = 16000, 2
clip_end = len(audio) / (rate * bps)
def byte_off(t: float) -> int:
# Align to whole int16 samples: rounding on raw bytes can yield an
# odd offset, splitting a sample (byte-shifted noise) and producing
# odd-length slices that break np.frombuffer("<i2") downstream.
return int(round(t * rate)) * bps
# Sub-phrase word-index ranges to store: every contiguous slice if
# aligned, else just the whole phrase.
n = len(norm_words)
ranges = (
[(i, j) for i in range(n) for j in range(i + 1, n + 1)]
if aligned
else [(0, n)]
)
stored = 0
for i, j in ranges:
if aligned:
end_t = starts[j] if j < len(starts) else clip_end
slc = audio[byte_off(starts[i]) : byte_off(end_t)]
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 1393-1393: Value being cast to int is already an integer

Remove unnecessary int call

(RUF046)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dragontts/app/cache/service.py` around lines 1389 - 1408, Update the
local byte_off helper in the sub-phrase storage flow to round the timestamp to a
whole sample index using rate, then multiply that sample index by bps to produce
the byte offset. Ensure all offsets used for slicing in this loop are sample-
and 16-bit-byte aligned.

Comment on lines +23 to +29
import asyncio
import base64
import json
import uuid
from collections.abc import AsyncGenerator
import time
from typing import Any, Callable

Copy link
Copy Markdown

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

Import order breaks isort grouping.

import time (line 28) sits between two from-imports, breaking isort's straight-imports-before-from-imports grouping. Since isort is part of the CI lint gate, this can fail the lint check.

🔧 Proposed fix
 import asyncio
 import base64
 import json
+import time
 import uuid
 from collections.abc import AsyncGenerator
-import time
 from typing import Any, Callable
📝 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
import asyncio
import base64
import json
import uuid
from collections.abc import AsyncGenerator
import time
from typing import Any, Callable
import asyncio
import base64
import json
import time
import uuid
from collections.abc import AsyncGenerator
from typing import Any, Callable
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dragontts/app/providers/cartesia_pool.py` around lines 23 - 29, Reorder
the imports in cartesia_pool.py so the straight import for time is grouped with
asyncio, base64, json, and uuid before all from-imports, including
collections.abc and typing.

Sources: Coding guidelines, Learnings

Comment on lines +111 to +112
if not self.api_key:
raise ValueError("CARTESIA_API_KEY is required")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Unify the missing-key exception type across synth/stream_synth. In all three providers synth() raises ValueError for a missing API key while the sibling stream_synth() raises ProviderError for the same condition. Because tts.py maps ValueError→400 and ProviderError→502 (and ProviderNotConfigured→503), the same misconfiguration returns different, misleading statuses depending on the endpoint. Raising ProviderNotConfigured (→503) uniformly is the clearest fix.

  • app/dragontts/app/providers/cartesia.py#L111-L112: replace raise ValueError("CARTESIA_API_KEY is required") with a ProviderNotConfigured/ProviderError matching stream_synth.
  • app/dragontts/app/providers/elevenlabs.py#L160-L161: replace raise ValueError("ELEVENLABS_INDIAN_RESIDENCY_API_KEY is required") with the same error type used by its stream_synth.
  • app/dragontts/app/providers/sarvam.py#L114-L115: replace raise ValueError("SARVAM_API_KEY is required") with the same error type used by its stream_synth.
📍 Affects 3 files
  • app/dragontts/app/providers/cartesia.py#L111-L112 (this comment)
  • app/dragontts/app/providers/elevenlabs.py#L160-L161
  • app/dragontts/app/providers/sarvam.py#L114-L115
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dragontts/app/providers/cartesia.py` around lines 111 - 112, Unify
missing-API-key handling in synth() across all providers by replacing the
ValueError raises in app/dragontts/app/providers/cartesia.py:111-112,
app/dragontts/app/providers/elevenlabs.py:160-161, and
app/dragontts/app/providers/sarvam.py:114-115 with the ProviderNotConfigured
error type used by each provider’s stream_synth() path, preserving the existing
provider-specific messages.

Comment on lines +175 to +178
logger.info(
f"Synthesizing audio with Gemini: {text[:50]}... "
f"[voice_id={final_voice_id}, model={final_model}, lang={final_language}]"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Transcript text logged at INFO level in two places — PII/compliance risk. Both synth() and stream_synth() log up to 50 characters of the raw spoken transcript, which for a customer-facing TTS pipeline can contain names, order details, or other PII persisted into application logs.

  • app/dragontts/app/providers/gemini.py#L175-L178: drop text[:50] from the synth() log line, or replace it with a non-identifying indicator (e.g. len(text) or a hash).
  • app/dragontts/app/providers/gemini.py#L248-L251: same change for the stream_synth() log line.
📍 Affects 1 file
  • app/dragontts/app/providers/gemini.py#L175-L178 (this comment)
  • app/dragontts/app/providers/gemini.py#L248-L251
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dragontts/app/providers/gemini.py` around lines 175 - 178, The Gemini
synthesis logs in synth() at app/dragontts/app/providers/gemini.py:175-178 and
stream_synth() at app/dragontts/app/providers/gemini.py:248-251 expose
transcript text; remove text[:50] from both INFO messages and replace it with a
non-identifying indicator such as text length or a hash, while retaining the
voice, model, and language context.

Comment on lines +284 to +291
async def aclose(self) -> None:
"""Release the cached gRPC channel (built lazily on first use)."""
if self._client is not None:
try:
await self._client.transport.grpc_channel.close() # type: ignore[union-attr]
except Exception as e:
logger.warning(f"Gemini gRPC channel close failed: {e}")
self._client = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Files matching gemini.py:\n'
fd -a 'gemini.py$' . | sed 's#^\./##'

printf '\nRelevant lines:\n'
sed -n '260,305p' app/dragontts/app/providers/gemini.py

printf '\nSearch for gemini/text-to-speech client usage/setup:\n'
rg -n "TextToSpeechAsyncClient|grpc_channel|aclose|transport\.close|transport\.grpc_channel" app/dragontts/app/providers/gemini.py app -S

Repository: juspay/clairvoyance

Length of output: 7194


🌐 Web query:

Google Cloud TextToSpeechAsyncClient __aexit__ close transport close grpc_channel API

💡 Result:

In the Google Cloud Text-to-Speech Python client library, the TextToSpeechAsyncClient implements the asynchronous context manager protocol [1]. When used within an async with statement, the exit method is automatically called at the end of the block [1]. This exit method calls self.transport.close, which is designed to release the resources associated with the underlying gRPC transport or channel [1]. Important Warning: You should only use the client as a context manager if the transport is not shared with other clients [1]. Because closing the transport terminates the underlying gRPC channel, doing so while other clients are still using that same transport will cause errors in those clients [1]. If you are managing the lifecycle of your client explicitly or sharing transports, ensure you do not close the transport prematurely [1].

Citations:


🌐 Web query:

googleapis python-texttospeech TextToSpeechAsyncClient __aexit__ async def source

💡 Result:

In the google-cloud-texttospeech library, the TextToSpeechAsyncClient does not implement an aexit method, as it is not intended to be used as an asynchronous context manager [1][2][3]. While the synchronous TextToSpeechClient includes enter and exit methods [2], these are designed to manage the underlying transport's lifecycle [2]. The synchronous exit implementation is [2]: def exit(self, type, value, traceback): self.transport.close The library's documentation warns that using the client as a context manager is only appropriate if the transport is not shared with other clients, as exiting the block closes the transport [2]. Because the asynchronous client is typically used to perform non-blocking I/O operations and follows the design patterns of the underlying gapic-generator, it lacks the aenter and aexit methods required for the async with statement [1][3]. If resource cleanup is required for the asynchronous client, it is handled via explicit methods provided by the transport or the client instance itself [1].

Citations:


🌐 Web query:

googleapis python-texttospeech TextToSpeechAsyncClient source __aexit__

💡 Result:

In the Google Cloud Python client libraries, including google-cloud-texttospeech, the TextToSpeechAsyncClient is generated by the gapic-generator-python tool. While synchronous TextToSpeechClient classes typically implement enter and exit for resource management (specifically closing the underlying transport) [1], many generated AsyncClient classes in the Google Cloud ecosystem have implemented aenter and aexit to support the asynchronous context manager protocol [2][3]. Regarding the aexit method for TextToSpeechAsyncClient: 1. Implementation: When present, aexit is typically used to ensure that the underlying gRPC or REST transport is properly closed, releasing associated network resources when the client is no longer needed [1][4][5]. 2. Best Practices and Warnings: Even when supported, library documentation often includes explicit warnings regarding the use of these clients as context managers [1]. A primary concern is shared transport instances; if the transport used by the TextToSpeechAsyncClient is shared with other clients or services, calling aexit will close the transport, which may lead to errors or connection failures in those other components [1]. 3. Verification: You can verify the presence of aenter and aexit in your installed version by inspecting the async_client.py file within your local installation of google-cloud-texttospeech or by navigating to the source code on the googleapis/python-texttospeech GitHub repository [1][6]. If your specific version of the library does not implement these methods, you should manage the lifecycle of the client explicitly by calling the appropriate close method (e.g., client.transport.close() or similar) when you are finished with it, ensuring you only do so when no other processes rely on that specific transport instance [1].

Citations:


🌐 Web query:

googleapis python-texttospeech services text_to_speech client TextToSpeechAsyncClient __aexit__

💡 Result:

The TextToSpeechAsyncClient class in the Google Cloud Text-to-Speech Python client library inherits from a base client that provides context manager support [1]. While the client implements the enter and exit methods, it does not explicitly define an aexit method for asynchronous context manager support [1]. In the implementation, the exit method is used to release the resources associated with the underlying transport [1]. It is important to note the warning provided in the source code: the client should only be used as a context manager if the transport is not shared with other clients, as exiting the block will close the transport, which may cause errors in other clients that rely on the same transport instance [1]. If you require asynchronous resource management, you typically handle client lifecycle management explicitly, or ensure the transport instance is managed according to your application's requirements. As of the current version, the exit method performs a synchronous closing of the transport, and developers should exercise caution to avoid premature closure of shared resources [1].

Citations:


🌐 Web query:

github googleapis python-texttospeech TextToSpeechAsyncClient __aexit__ close

💡 Result:

The TextToSpeechAsyncClient in the Google Cloud Python client library provides enter and exit methods, allowing it to be used as a context manager [1]. The exit method calls self.transport.close() to release the underlying transport's resources [1]. However, the library documentation includes an explicit warning regarding this usage: you should only use the client as a context manager if the transport is not shared with other clients [1]. Exiting the with block will close the transport, which may cause errors in other clients that rely on the same shared transport [1]. It is important to note that the legacy googleapis/python-texttospeech GitHub repository is archived, and the development of the client library has moved to the google-cloud-python repository [2]. You should refer to the updated package documentation for the latest best practices [2][3].

Citations:


🌐 Web query:

github googleapis python-texttospeech services text_to_speech client_text.py TextToSpeechAsyncClient

💡 Result:

The TextToSpeechAsyncClient is a component of the official Google Cloud Text-to-Speech client library for Python [1]. It is used for asynchronous interactions with the Google Cloud Text-to-Speech API [1]. Key details regarding your query include: 1. Library Location: The source code for the Google Cloud Text-to-Speech Python library has moved from the dedicated googleapis/python-texttospeech repository to the unified googleapis/google-cloud-python repository [2][3]. It is now located under the packages/google-cloud-texttospeech directory [2]. 2. Client Functionality: The TextToSpeechAsyncClient is defined within the google.cloud.texttospeech_v1.services.text_to_speech module [1]. It provides asynchronous methods for service calls, which are useful for applications requiring non-blocking network operations [1]. 3. Documentation: Detailed API documentation, including parameters and methods for TextToSpeechAsyncClient, is available on the official Google Cloud documentation site [1][4]. 4. Usage: To use the library, you should install the google-cloud-texttospeech package via pip [5][6]: pip install google-cloud-texttospeech You can then import the client from the library: from google.cloud import texttospeech_v1 async_client = texttospeech_v1.TextToSpeechAsyncClient For further implementation examples, you can consult the official samples directory in the repository [2][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Dependency specifications mentioning google-cloud-texttospeech/texttospeech:\n'
rg -n "google-cloud-texttospeech|texttospeech|google-cloud-texttospeech" -S --glob '!*.lock' --glob '!uv.lock' --glob '!requirements*.txt' . || true

printf '\nInstalled package metadata if present:\n'
python3 - <<'PY'
import importlib.util
for name in ("google.cloud.texttospeech_v1", "google.cloud.texttospeech"):
    spec = importlib.util.find_spec(name)
    print(name, "=>", spec.origin if spec else None)
if importlib.util.find_spec("google.cloud.texttospeech_v1"):
    from google.cloud.texttospeech_v1.services.text_to_speech import client as module
    print("module_file", getattr(module, "__file__", None))
    for cls in ("TextToSpeechAsyncClient", "TextToSpeechClient"):
        klass = getattr(module, cls, None)
        print(cls, klass)
        for m in ("__enter__", "__exit__", "__aenter__", "__aexit__"):
            val = getattr(klass, m, "<missing>")
            print(" ", m, "=>", val, getattr(val, "__qualname__", None))
PY

Repository: juspay/clairvoyance

Length of output: 3693


🌐 Web query:

site:github.com/googleapis/python-texttospeech TextToSpeechAsyncClient "def __aexit__"

💡 Result:

The Google Cloud Text-to-Speech Python client library does not provide a class named TextToSpeechAsyncClient. The primary client class for this library is TextToSpeechClient, which is used for synchronous requests [1][2][3]. Because TextToSpeechAsyncClient does not exist in this library, it does not have an aexit method. The library generally follows a synchronous pattern for its client interface [4][1]. If you are implementing asynchronous operations with Google Cloud Python libraries, they typically utilize different client patterns or generic asynchronous gRPC mechanisms [2], but the Text-to-Speech library documentation does not feature an async-specific client class for this service [4].

Citations:


🌐 Web query:

site:github.com/googleapis/google-cloud-python TextToSpeechAsyncClient "__async_init__"

💡 Result:

There is no standard method named async_init in the TextToSpeechAsyncClient class within the Google Cloud Text-to-Speech Python client library [1][2]. The TextToSpeechAsyncClient is a machine-generated class produced by the Google Cloud GAPIC (Google API Client) generator [3]. Its structure is designed to handle asynchronous gRPC or REST communication, and it does not utilize a custom async_init method for initialization [1][2]. Initialization typically follows standard Python class patterns where configuration and transport setup are handled within the standard init method [4]. If you are encountering references to an async_init or similar patterns in the context of Google Cloud client libraries, it may be due to: 1. Misinterpretation of generated code or internal implementation details: The library uses automated code generation, and internal helper methods or structures (such as those involving CrossSync) may appear in tracebacks or source code, but these are not intended for public API usage [5][6]. 2. Custom wrapper implementations: Developers sometimes create their own wrapper classes to handle asynchronous initialization (e.g., awaiting a connection or setup process) using the init method combined with factory methods, but this is not a feature of the SDK itself [4][7]. For standard usage, you should instantiate the TextToSpeechAsyncClient directly using its documented constructor and call its asynchronous methods (e.g., synthesize_speech) using the await keyword [8][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import tempfile, urllib.request, tarfile
from pathlib import Path

url = "https://files.pythonhosted.org/packages/e4/ad/9799e3d6ebcf72467026743421882f36e10d0fc16f2fd2469473b39226/google_cloud_texttospeech-2.19.1-py3-none-any.whl"
with tempfile.TemporaryDirectory() as td:
    whl = Path(td) / "google_cloud_texttospeech-2.19.1-py3-none-any.whl"
    urllib.request.urlretrieve(url, whl)
    with tarfile.open(whl, "r:zip") as tf:
        members = [m for m in tf.getmembers() if m.name.endswith(".py")]
        target = "google/cloud/texttospeech_v1/services/text_to_speech/client.py"
        if target in [m.name for m in members]:
            print("FOUND client", target)
            source = tf.extractfile(target).read().decode()
            start = source.index("class TextToSpeechAsyncClient")
            end = source.index("\n\n", start + 1)
            print(source[start:end][:8000])
        else:
            print("client.py missing")
PY'

Repository: juspay/clairvoyance

Length of output: 334


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import tempfile, urllib.request, tarfile
from pathlib import Path

url = "https://files.pythonhosted.org/packages/e4/ad/9799e3d6ebcf72467026743421882f36e10d0fc16f2fd2469473b39226/google_cloud_texttospeech-2.19.1-py3-none-any.whl"
with tempfile.TemporaryDirectory() as td:
    whl = Path(td) / "google_cloud_texttospeech-2.19.1-py3-none-any.whl"
    urllib.request.urlretrieve(url, whl)
    with tarfile.open(whl, "r:zip") as tf:
        members = [m for m in tf.getmembers() if m.name.endswith(".py")]
        target = "google/cloud/texttospeech_v1/services/text_to_speech/client.py"
        if target in [m.name for m in members]:
            print("FOUND client", target)
            source = tf.extractfile(target).read().decode()
            start = source.index("class TextToSpeechAsyncClient")
            end = source.index("\n\n", start + 1)
            print(source[start:end][:8000])
        else:
            print("client.py missing")
PY

Repository: juspay/clairvoyance

Length of output: 2572


🌐 Web query:

cloud.google.com python reference TextToSpeechAsyncClient close transport grpc_channel close

💡 Result:

To close a TextToSpeechAsyncClient and its underlying transport in the Google Cloud Python client libraries, you should generally rely on the client's built-in support for context managers or explicitly invoke the close method on the client instance if available [1]. The TextToSpeechAsyncClient and other similar GAPIC clients are designed to manage their own transport resources [1]. If you need to manage the lifecycle manually, the client instances include an exit method, which allows them to be used as context managers [1]. When you exit a 'with' block, the client automatically calls close on its underlying transport [1]. Important Considerations: 1. Context Manager Usage: Using the client as a context manager (e.g., 'with TextToSpeechAsyncClient as client:') is the recommended way to ensure resources are properly released [1]. 2. Warning on Shared Transports: Exercise caution when closing a client if the transport is shared with other clients. Exiting a 'with' block or calling close on a shared transport will close it for all clients using it, which may result in errors for those other clients [1][2]. 3. gRPC Channel Lifecycle: The underlying transport typically wraps a gRPC channel [1]. In gRPC Python, the channel's close method (or async close for asyncio) is used to release all resources held by it [3][4]. This operation is idempotent [3][4]. For most standard use cases, you do not need to manually access the 'grpc_channel' object directly. Instead, invoke the close method on the client instance itself, or use the client within a 'with' statement to handle cleanup automatically [1].

Citations:


🌐 Web query:

googleapis python gapic async client __aexit__ transport.close grpc_channel.close

💡 Result:

In Google Cloud Python libraries (GAPIC), the standard way to manage client lifecycles and clean up resources is via the client's.close method or by using the client as a context manager if supported [1][2][3]. When you use a client as an asynchronous context manager, the library implementation typically calls transport.close within the aexit method [2][3][4][5]. Relying on this is the recommended approach for avoiding resource leaks, such as open sockets [1][6]. Regarding the specific chain of calls: 1. The client's.close method (or aexit) is designed to trigger the cleanup of the underlying transport [1][2][3]. 2. For GAPIC clients, it is important to invoke the full transport.close method rather than accessing and closing the gRPC channel directly (i.e., transport.grpc_channel.close) [6]. Invoking transport.close ensures that all network resources, including the gRPC channel and potentially other dynamically managed sessions (like authentication sessions), are correctly shut down [6]. 3. In modern gRPC AsyncIO, the channel's own close method is idempotent and intended to release held resources [7][8]. However, manually closing the internal channel of a GAPIC transport bypasses the higher-level cleanup logic managed by the transport layer [6]. Important Considerations: - Shared Transports: If you share a transport object across multiple clients, you must be careful; closing the transport via one client will affect all clients using it [1][2][3]. - Resource Leaks: In some complex scenarios (particularly with bidirectional streaming RPCs), calling close while tasks are still active can lead to leaked sockets [9]. Ensure that active RPCs are properly awaited or cancelled before tearing down the channel [9]. - Consistency: For newer libraries like google-genai, the async client provides explicit aclose methods and context manager support (async with), which handle the underlying teardown automatically [4][10][5]. Always prefer using the official.close method or context manager pattern provided by the specific client library rather than reaching into private internal attributes like _transport.grpc_channel [1][6][4].

Citations:


Close the Text-to-Speech transport through aclose() instead of reaching into grpc_channel.

TextToSpeechAsyncClient.aclose() closes the transport consistently; using self._client.transport.grpc_channel.close() bypasses that public cleanup path and depends on an internal channel attribute.

🔧 Proposed fix
     async def aclose(self) -> None:
         """Release the cached gRPC channel (built lazily on first use)."""
         if self._client is not None:
             try:
-                await self._client.transport.grpc_channel.close()  # type: ignore[union-attr]
+                await self._client.aclose()
             except Exception as e:
                 logger.warning(f"Gemini gRPC channel close failed: {e}")
             self._client = 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
async def aclose(self) -> None:
"""Release the cached gRPC channel (built lazily on first use)."""
if self._client is not None:
try:
await self._client.transport.grpc_channel.close() # type: ignore[union-attr]
except Exception as e:
logger.warning(f"Gemini gRPC channel close failed: {e}")
self._client = None
async def aclose(self) -> None:
"""Release the cached gRPC channel (built lazily on first use)."""
if self._client is not None:
try:
await self._client.aclose()
except Exception as e:
logger.warning(f"Gemini gRPC channel close failed: {e}")
self._client = None
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 289-289: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dragontts/app/providers/gemini.py` around lines 284 - 291, Update the
aclose method to call the TextToSpeechAsyncClient public aclose() method instead
of accessing self._client.transport.grpc_channel directly. Preserve the existing
exception warning and reset self._client to None after cleanup.

Comment on lines +40 to +50
### Fix 2 — defer `reconcile_blobs()` to a background task

In `app/main.py` lifespan (currently `await cache.reconcile_blobs()`):
```python
asyncio.create_task(cache.reconcile_blobs()) # was: await cache.reconcile_blobs()
```
Removes the full blob-dir walk from the boot critical path — the pod reaches
"ready" faster and the walk spreads out afterward instead of spiking. Reconcile
is best-effort orphan cleanup, so deferring it a few seconds is harmless. Keep
the existing error handling (wrap in try/except + log, matching the other
background loops).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not present this as a startup-spike fix.

Each of the four workers will schedule reconcile_blobs() immediately, so the same scans still begin concurrently; failures are also unobserved with the shown snippet. Use a single-worker lease/leader election, or describe this only as readiness deferral.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dragontts/docs/TODO.md` around lines 40 - 50, Revise the TODO guidance
for the lifespan reconcile_blobs flow: do not claim that simply scheduling
asyncio.create_task(cache.reconcile_blobs()) prevents startup scan spikes, since
every worker would still run it concurrently. Specify either a single-worker
lease/leader-election mechanism for reconcile_blobs, with existing try/except
logging preserved, or limit the description to readiness deferral and explicitly
address task failure handling.

Comment on lines +49 to +60
async def _drain(gen):
"""Drain a chunk generator. Returns (audio, n_chunks, ttfb_s or None)."""
out = bytearray()
n_chunks = 0
t0 = time.monotonic()
ttfb = None
async for chunk in gen:
if ttfb is None:
ttfb = time.monotonic() - t0
out += chunk
n_chunks += 1
return bytes(out), n_chunks, ttfb

Copy link
Copy Markdown

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 complete function type annotations.

_drain, test_provider, test_cache_roundtrip, nested get_provider, _mark, and main have missing or incomplete parameter/return annotations. Use concrete Dict[str, Any], Optional[...], and async iterator/provider types.

Also applies to: 63-108, 111-170, 173-177

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dragontts/smoke_live.py` around lines 49 - 60, Complete the type
annotations for _drain, test_provider, test_cache_roundtrip, nested
get_provider, _mark, and main, including all parameters and return types. Use
the concrete Dict[str, Any], Optional[...], async iterator, and provider types
already appropriate to each function, while preserving their existing behavior.

Source: Coding guidelines

Builds FRESH provider instances (the synth/stream step already aclose()'d
the ones from phase 1, so their httpx clients are closed).
"""
tmp = Path(tempfile.mkdtemp(prefix="dragontts-smoke-"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Create and clean up the temporary store asynchronously.

tempfile.mkdtemp() blocks the event loop and the generated dragontts-smoke-* directory is never deleted. Create it with await asyncio.to_thread(...) and remove it in a finally block via asyncio.to_thread(shutil.rmtree, ...).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dragontts/smoke_live.py` at line 117, Update the temporary-directory
setup in the smoke test to create the directory with await
asyncio.to_thread(tempfile.mkdtemp, ...) instead of blocking the event loop.
Ensure the temporary store is always removed in a finally block using await
asyncio.to_thread(shutil.rmtree, ...), preserving the existing test flow and
cleanup even when execution fails.

Source: Coding guidelines

Comment on lines +211 to +214
print(f" synth : {_mark(s['ok'])} {s['detail']} [{s['ms']}ms, {s['enc']}@{s['rate']}]")
ttfb = st.get("ttfb_ms")
print(f" stream : {_mark(st['ok'])} {st['detail']} "
f"[{st['chunks']} chunks, ttfb={ttfb}ms, total={st['ms']}ms]")

Copy link
Copy Markdown

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

Run Black before merge.

These output statements exceed the repository’s 88-character Black limit and will fail the configured formatting gate. Based on learnings, CI runs Black as part of its lint gate.

Also applies to: 223-225

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dragontts/smoke_live.py` around lines 211 - 214, Format the output
statements near the synth and stream status prints, including the lines around
223-225, with Black so they comply with the repository’s 88-character line
limit. Preserve the existing output content and behavior.

Sources: Coding guidelines, Learnings

Comment on lines +229 to +232
all_ok = all(
(r["err"] is None and r["synth"]["ok"] and r["stream"]["ok"]) for r in results
)
print("RESULT:", "ALL PASS ✓" if all_ok else "FAILURES (see above)")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fail the smoke test when the cache round-trip fails.

all_ok only checks direct provider synthesis/streaming. A cache MISS → HIT failure, wrong cache status, or non-identical cached response still prints ALL PASS. Include every cr["provider_results"] entry and assert MISS, HIT, and byte identity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dragontts/smoke_live.py` around lines 229 - 232, Update the all_ok
calculation in the smoke-test result flow to include every cache round-trip
entry from cr["provider_results"], requiring the expected MISS and HIT statuses
and byte-identical responses in addition to the existing synthesis and streaming
checks. Ensure any cache failure causes the final result to print FAILURES.

@narsimhaReddyJuspay
narsimhaReddyJuspay force-pushed the migrate-dragontts-to-calirvoyance-repo branch from 15da0c0 to 09e89c2 Compare July 22, 2026 19:03
@sentinelone-cnapp-aps1

Copy link
Copy Markdown

SentinelOne IaC Issues Detector

❌ Detected 6 issue(s) in your pull request

Here are the detected issues.

Resource File Issue Message Severity
Pod.default.dragontts.app-dragontts deployment.yaml Undropped 'NET_RAW' Capabilities Detected in Multiple Resources HIGH
Pod.default.dragontts.app-dragontts deployment.yaml Multiple Pods lack a properly configured Seccomp profile MEDIUM
Pod.default.dragontts.app-dragontts deployment.yaml Multiple Kubernetes Pods are running with a user ID below the threshold of 10000 MEDIUM
Pod.default.dragontts.app-dragontts deployment.yaml Multiple resources detected with undefined or empty Service Account Name MEDIUM
Pod.default.dragontts.app-dragontts deployment.yaml Resources Detected in Unrecommended Namespace MEDIUM
Pod.default.dragontts.app-dragontts deployment.yaml Multiple resources detected with undefined or empty host aliases LOW

SentinelOne CNS is a cloud-agnostic, agentless CSPM & CWPP solution that continuously detects and prevents vulnerabilities that have the highest probability of being exploited in Azure, AWS, Google Cloud, and Kubernetes.

@murdore

murdore commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

A few things before this can merge:

  1. CI is red at head (09e89c2) — the build job's black-formatting step fails ("23 files would be reformatted"). Needs black . committed; per repo convention this is a hard gate, not a style nit.
  2. PII in logs — all 4 new provider adapters (cartesia/elevenlabs/gemini/sarvam, synth + stream_synth, ~8 call sites) log up to 50 chars of the raw transcript at INFO (e.g. gemini.py L177/L250). For a customer-voice pipeline that's a PII/compliance exposure into persisted logs — drop the text or gate it behind debug.
  3. Cache-HIT crash path — get_or_synthesize's cache HIT (service.py ~L851, streaming ~L1028) calls self._blobs.get() without catching FileNotFoundError, unlike _span_bytes. A metadata-row-present / blob-missing case (partial PVC loss, manual cleanup) would 500 instead of re-synthesizing.
  4. byte_off mid-sample — in the warm/timestamped path (~L1410), byte_off(t)=round(t*16000*2) doesn't align to even bytes, so a fractional timestamp can slice mid 16-bit sample; round to a multiple of bps.

Coverage: this is a large move with no tests for the new cache/stitch/provider paths — worth unit coverage on the cache-HIT fallback, _stitch_plan key collisions, and the byte alignment before merge.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants