Skip to content

(feat): migrate dragontts code to clairvoyance repo#949

Open
narsimhaReddyJuspay wants to merge 1 commit into
juspay:releasefrom
narsimhaReddyJuspay:move-dragontts-code-clairvoyance
Open

(feat): migrate dragontts code to clairvoyance repo#949
narsimhaReddyJuspay wants to merge 1 commit into
juspay:releasefrom
narsimhaReddyJuspay:move-dragontts-code-clairvoyance

Conversation

@narsimhaReddyJuspay

@narsimhaReddyJuspay narsimhaReddyJuspay commented Jul 23, 2026

Copy link
Copy Markdown
Contributor
  • added seperate git workflow to handle seperate deployment

Summary by CodeRabbit

  • New Features
    • Added DragonTTS, a cached text-to-speech service supporting multiple providers, audio streaming, format conversion, and configurable text normalization.
    • Added cache management, analytics, health, provider discovery, and bulk creation APIs.
    • Added persistent audio caching, TTL management, latency metrics, and daily Slack cache summaries.
    • Added Docker packaging and automated image publishing for DragonTTS.
  • Documentation
    • Added setup guidance, configuration examples, API endpoint documentation, and local run instructions.

Copilot AI review requested due to automatic review settings July 23, 2026 11:42
@coderabbitai

coderabbitai Bot commented Jul 23, 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 Plus

Run ID: a7c03b5d-3461-4cac-82bf-8df7fdf7491d

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 is added as a standalone FastAPI TTS proxy with multi-provider synthesis, native-format caching, audio conversion, SQLite/filesystem storage, operational APIs, Slack summaries, container packaging, and Artifact Registry publishing.

Changes

DragonTTS service

Layer / File(s) Summary
Runtime foundation
dragontts/app/core/*, dragontts/app/main.py, dragontts/app/providers/base.py, dragontts/app/schemas/*
Defines settings, logging, lifecycle management, provider contracts, and TTS/cache response models.
Audio, cache, and storage pipeline
dragontts/app/audio/*, dragontts/app/cache/*, dragontts/app/storage/*
Adds text normalization, resampling, deterministic cache keys, resilient caching, write-behind metrics, SQLite metadata, and filesystem blob storage.
Provider integrations and streaming pools
dragontts/app/providers/*
Adds Cartesia, ElevenLabs, Gemini, and Sarvam adapters with HTTP, gRPC, and persistent WebSocket streaming paths.
API and operational alerting
dragontts/app/api/*, dragontts/app/alerts/*
Adds TTS, cache administration, health, statistics, Slack summary, and cache maintenance endpoints.
Packaging and deployment
dragontts/Dockerfile, dragontts/pyproject.toml, .github/workflows/*, dragontts/README.md
Adds the service image, project metadata, documentation, Docker context exclusions, and release-branch image publishing.
Repository integration
.dockerignore, pyproject.toml
Excludes the standalone service from the clairvoyance Docker context and Pyrefly project checks.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant TTSRouter
  participant CacheService
  participant ProviderRegistry
  participant TTSProvider
  participant SQLiteMetadataStore
  participant FilesystemBlobStore

  Client->>TTSRouter: POST /tts/bytes or /tts/stream
  TTSRouter->>CacheService: Resolve request and cache key
  CacheService->>SQLiteMetadataStore: Read cache metadata
  alt Cache hit
    CacheService->>FilesystemBlobStore: Load native audio
    CacheService->>TTSRouter: Convert and return audio
  else Cache miss
    CacheService->>ProviderRegistry: Resolve provider
    ProviderRegistry->>TTSProvider: Synthesize or stream audio
    TTSProvider-->>CacheService: Native audio chunks
    CacheService->>FilesystemBlobStore: Store native audio
    CacheService->>SQLiteMetadataStore: Store metadata and metrics
    CacheService->>TTSRouter: Return audio stream
  end
  TTSRouter-->>Client: Audio response
Loading

Possibly related PRs

Suggested reviewers: copilot

Poem

I’m a rabbit with cached little tunes,
Hopping through providers beneath silver moons.
SQLite keeps carrots, blobs keep the beat,
WebSockets stream softly on quick furry feet.
Docker lifts DragonTTS high—
While Slack summaries nibble the sky!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.91% 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 clearly matches the main change: migrating DragonTTS 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 migrates the DragonTTS caching TTS proxy into the Clairvoyance repository as a self-contained sub-service, and adds a dedicated CI workflow to build/push its Docker image independently from the main Clairvoyance image.

Changes:

  • Exclude dragontts/ from Clairvoyance’s pyrefly type-checking.
  • Add the self-contained dragontts/ FastAPI service (providers, caching/storage, metrics, Slack summary, Docker build assets).
  • Add a dedicated GitHub Actions workflow to build/push the DragonTTS image and prevent DragonTTS sources from being copied into the Clairvoyance Docker image via .dockerignore.

Reviewed changes

Copilot reviewed 41 out of 42 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pyproject.toml Excludes dragontts/ from Clairvoyance pyrefly checks.
dragontts/README.md Documents DragonTTS purpose, local run steps, and endpoints.
dragontts/pyproject.toml Declares DragonTTS as its own Python project with its own deps/test config.
dragontts/Dockerfile Builds a standalone DragonTTS image using uv.
dragontts/app/storage/sqlite.py SQLite WAL-backed metadata/metrics store implementation.
dragontts/app/storage/filesystem.py Filesystem content-addressed blob storage for audio payloads.
dragontts/app/storage/base.py Protocols + dataclasses for storage abstractions.
dragontts/app/schemas/tts.py Request model for Cartesia-shaped API compatibility + extensions.
dragontts/app/schemas/cache.py Response models for cache admin operations.
dragontts/app/providers/sarvam.py Sarvam provider adapter (HTTP + WS streaming).
dragontts/app/providers/sarvam_pool.py Warm WS pool for Sarvam streaming sockets.
dragontts/app/providers/registry.py Provider registry for configured providers + warm/close lifecycle.
dragontts/app/providers/gemini.py Gemini provider adapter (gRPC streaming + resample).
dragontts/app/providers/elevenlabs.py ElevenLabs provider adapter (HTTP + WS streaming).
dragontts/app/providers/elevenlabs_pool.py Warm multi-context WS pool for ElevenLabs streaming.
dragontts/app/providers/cartesia.py Cartesia provider adapter (HTTP + WS streaming).
dragontts/app/providers/cartesia_pool.py Warm multiplexed WS pool for Cartesia streaming.
dragontts/app/providers/base.py Common provider interface + streaming fallback behavior.
dragontts/app/main.py FastAPI app + lifespan wiring for stores, registry, background loops.
dragontts/app/core/logging.py Loguru JSON sink + stdlib logging interception.
dragontts/app/core/config.py Pydantic settings + provider defaults and operational knobs.
dragontts/app/cache/resilience.py Provider bulkheads + token-bucket rate limiting gates.
dragontts/app/cache/metrics.py Write-behind metrics buffering/flushing.
dragontts/app/cache/key.py Cache key normalization/canonicalization and hashing.
dragontts/app/audio/text.py Text normalization (number expansion, ElevenLabs leading-dot hint).
dragontts/app/audio/resample.py Streaming PCM resampler for providers with non-16k native streams.
dragontts/app/audio/format.py Native-to-requested audio format conversion utilities.
dragontts/app/api/v1/tts.py /tts/* endpoints: bytes/stream/check/create/delete/bulk.
dragontts/app/api/v1/health.py Health/provider introspection endpoints.
dragontts/app/api/v1/cache.py Cache admin endpoints + stats endpoints.
dragontts/app/alerts/summary.py Slack daily cache summary builder/sender coordination.
dragontts/app/alerts/slack.py Slack incoming-webhook Block Kit sender.
dragontts/app/alerts/init.py Alerts module init.
dragontts/app/init.py Package metadata/version.
dragontts/.python-version Declares local Python version for the sub-service.
dragontts/.gitignore Ignores DragonTTS local env/data artifacts.
dragontts/.env.example Example env file for DragonTTS local config.
dragontts/.dockerignore DragonTTS-specific docker build exclusions.
.github/workflows/dragontts-docker-publish.yml Dedicated workflow to build/push DragonTTS image on changes.
.dockerignore Excludes dragontts/ from the main Clairvoyance Docker build context.
Comments suppressed due to low confidence (1)

dragontts/app/api/v1/tts.py:235

  • In /tts/create/bulk, invalid audio_base64 will raise during b64decode() and abort the entire batch with a 500 (it’s not caught by any except). Handle decode errors per-item (like other per-item errors) so one bad entry doesn’t fail the whole request.
            audio_override = None
            if req.audio_base64:
                audio_override = base64.b64decode(req.audio_base64)
            key, status, source, size, provider, model, _enc, _rate = (

Comment on lines +167 to +171
if req.audio_base64:
try:
audio_override = base64.b64decode(req.audio_base64)
except Exception:
raise HTTPException(status_code=400, detail="invalid audio_base64")
Comment on lines +47 to +51
# 1. Normalize native -> PCM s16le at the native sample rate.
if native_enc in _ULAWS:
pcm = audioop.ulaw2lin(native_audio, 2)
else: # already PCM s16le
pcm = native_audio
Comment thread dragontts/README.md
Comment on lines +36 to +40
## Endpoints
- `POST /tts/bytes` — one-shot synthesis (cached, returns μ-law 8 kHz).
- `POST /tts/stream` — chunked streaming synthesis (raw pcm_s16le @ 16 kHz).
- `POST /cache/clear` — clear the cache at runtime (don't delete `data/` while
the server runs).

@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: 9

🧹 Nitpick comments (15)
dragontts/app/schemas/cache.py (1)

45-50: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Encode the documented response values with Literal.

CreateResponse and DeleteResponse currently accept arbitrary strings even though their docstrings/comments define fixed values. Invalid provider responses could therefore pass schema validation.

Proposed fix
-from pydantic import BaseModel
+from typing import Literal
+
+from pydantic import BaseModel

 class CreateResponse(BaseModel):
-    status: str
-    source: str
+    status: Literal["CREATED", "OVERRIDDEN"]
+    source: Literal["synth", "base64"]

 class DeleteResponse(BaseModel):
-    status: str  # "deleted" | "not_found"
+    status: Literal["deleted", "not_found"]

Also applies to: 59-60

🤖 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 `@dragontts/app/schemas/cache.py` around lines 45 - 50, Update the
CreateResponse and DeleteResponse Pydantic models to use typing.Literal for each
documented fixed-value field, including status and source, so schema validation
rejects unsupported provider response values. Preserve the exact allowed values
stated in each model’s docstring or comments.
.github/workflows/dragontts-docker-publish.yml (2)

33-34: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Disable checkout credential persistence.

The static scan flags the default persisted Git credentials. This job executes repository-controlled Docker/build steps after checkout; even with contents: read, keeping the token in .git/config is unnecessary. Set persist-credentials: false to reduce later-step exposure. (github.com)

Suggested change
       - name: Checkout code
         uses: actions/checkout@v4
+        with:
+          persist-credentials: false
🤖 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 @.github/workflows/dragontts-docker-publish.yml around lines 33 - 34, Update
the actions/checkout@v4 step in the workflow to set persist-credentials to
false, preventing the checkout token from being retained for subsequent
repository-controlled Docker/build steps.

Source: Linters/SAST tools


61-62: 🔒 Security & Privacy | 🔵 Trivial

Retain supply-chain attestations unless intentionally disabled.

provenance: false and sbom: false remove build provenance and SBOM metadata from the published image. Enable them, or document the policy exception and compensating controls if Artifact Registry or deployment policy relies on those artifacts.

🤖 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 @.github/workflows/dragontts-docker-publish.yml around lines 61 - 62, Update
the Docker publish configuration by enabling provenance and SBOM generation
instead of setting provenance and sbom to false. If they must remain disabled,
document the intentional policy exception and define the required compensating
controls.
dragontts/.python-version (1)

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

Align the local and container Python targets.

The local configuration and README use Python 3.11, while the Docker image uses Python 3.12. Both satisfy the minimum, but this permits version-specific failures between development and production. Choose one supported target or explicitly test both. As per coding guidelines, Python 3.11 or later is required.

🤖 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 `@dragontts/.python-version` at line 1, Align the Python version declared by
.python-version, the README, and the Docker image so local development and
production use the same supported target; choose either Python 3.11 or 3.12, or
explicitly configure and test both versions.

Source: Coding guidelines

dragontts/Dockerfile (2)

7-11: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use production-only uv synchronization.

uv sync and uv run include the default dev group, and project installs are editable by default. Set UV_NO_DEV=1 or pass --no-dev, and use --no-editable for the final sync so the runtime image does not ship development tooling or depend on an editable source install. (docs.astral.sh)

Suggested change
 ENV PYTHONUNBUFFERED=1 \
     PYTHONDONTWRITEBYTECODE=1 \
     UV_LINK_MODE=copy \
     UV_COMPILE_BYTECODE=1 \
+    UV_NO_DEV=1 \
     MALLOC_ARENA_MAX=4

Use --no-editable on the final sync.

Also applies to: 18-22, 29-29

🤖 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 `@dragontts/Dockerfile` around lines 7 - 11, Update the Dockerfile’s uv
installation and execution flow to exclude the default dev dependency group and
ensure the final project synchronization uses a non-editable install. Apply the
production-only configuration consistently to every relevant uv sync/uv run
invocation, including the final sync, while preserving the existing runtime
behavior.

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

Pin the uv tool image by digest.

ghcr.io/astral-sh/uv:0.5.11 is a mutable tag, so the build tool can change without a Dockerfile diff. Use a reviewed SHA256 digest for reproducible builds; uv’s Docker guidance recommends digest pinning when reproducibility matters. (docs.astral.sh)

🤖 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 `@dragontts/Dockerfile` at line 13, Update the uv image reference in the
Dockerfile’s COPY directive to pin ghcr.io/astral-sh/uv:0.5.11 by its reviewed
SHA256 digest, preserving the existing source image version and destination
path.
pyproject.toml (1)

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

Add CI coverage for DragonTTS.

dragontts/ only has a build-and-push workflow in .github/workflows/dragontts-docker-publish.yml; it does not run tests or a type check. Since project-excludes = ["dragontts"] makes the main pyrefly job skip this self-contained service, add a DragonTTS test/type-check job, or verify and reference the separate pipeline before relying on this exclusion.

🤖 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 `@pyproject.toml` around lines 74 - 76, Add CI coverage for the self-contained
dragontts service by introducing a test and type-check job, or verify and
explicitly reference an existing separate pipeline that performs both checks.
Keep the project-excludes entry aligned with that coverage so DragonTTS is not
left unchecked by the main pyrefly job.
dragontts/app/core/logging.py (3)

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

Reads os.environ directly instead of going through the central Settings.

LOG_JSON is read via os.environ.get(...) here, bypassing the Settings object defined in app/core/config.py for this same service. As per coding guidelines, "Load ALL configuration from app/core/config/static.py using get_required_env() for mandatory variables; never import directly from os.environ elsewhere." Even accounting for Settings being this service's own convention (via pydantic-settings) rather than the monorepo's static.py/get_required_env() pattern, this file should route through Settings rather than adding a second, independent env-read 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 `@dragontts/app/core/logging.py` around lines 14 - 25, Update the _JSON
configuration in the logging module to obtain LOG_JSON through the central
Settings configuration from app.core.config instead of reading os.environ
directly. Preserve the current default-to-JSON behavior and false-value handling
while removing the independent environment lookup and any now-unused os import.

Source: Coding guidelines


56-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

No contextvars-based structured logging context.

As per coding guidelines, "Use Loguru logging with contextvars for structured logging." This sink/interceptor setup has no logger.contextualize() or bound contextvars (e.g. request ID) for correlating log lines across a single request. Given the API layer isn't in this review cohort, this may already be handled elsewhere — worth confirming request-scoped context is bound somewhere before requests reach the sink.

🤖 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 `@dragontts/app/core/logging.py` around lines 56 - 98, Add request-scoped
contextvars-based structured logging to the logging setup, binding context such
as the request ID before API request logs reach InterceptHandler and the
configured logger sink. Ensure the context is propagated across the request and
cleared afterward, while preserving the existing stdlib-to-Loguru forwarding and
re-entrancy behavior.

Source: Coding guidelines


100-100: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

__all__ not alphabetically sorted.

Ruff RUF022 flags this, but per a prior learning, this repo's CI lint gate runs Black/isort/autoflake/pyrefly, not Ruff, so this is a non-blocking cleanup item rather than a CI-breaker.

🤖 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 `@dragontts/app/core/logging.py` at line 100, Sort the public symbols in
__all__ alphabetically, placing InterceptHandler before logger while preserving
the existing exports.

Sources: Learnings, Linters/SAST tools

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

20-26: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Literal invisible Unicode characters embedded directly in source.

_ZW contains raw zero-width/invisible characters typed directly into the string literal. This is functionally fine, but such literals are opaque in diffs/code review (an editor or reviewer can't easily see or verify which characters are actually present) — the same category of risk as "Trojan Source" invisible-character obfuscation, independent of whether Ruff runs in CI. Using explicit \uXXXX escapes preserves identical behavior while making the literal auditable.

🔒 Suggested fix
-_ZW = "​‌‍‎‏­"
+_ZW = "\u200b\u200c\u200d\u200e\u200f\ufeff\u00ad"
 _ZW_TABLE = str.maketrans("", "", _ZW)

(Verify the exact code points against the current literal before applying — this is illustrative.)

🤖 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 `@dragontts/app/cache/key.py` around lines 20 - 26, Update the _ZW string in
dragontts/app/cache/key.py to represent each existing invisible character with
explicit Unicode escape sequences instead of literal invisible characters.
Preserve the exact current code points and keep _ZW_TABLE unchanged so stripping
behavior remains identical.

Source: Linters/SAST tools

dragontts/app/core/config.py (1)

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

configured_providers duplicates ProviderRegistry.build()'s credential checks.

Both this property and ProviderRegistry.build() (dragontts/app/providers/registry.py) independently re-implement the same 4 credential-presence conditions. If a provider/credential is added or changed in one place, the other can silently drift out of sync (e.g. configured_providers reporting a provider as live that build() didn't actually register, or vice versa).

Consider deriving one from the other — e.g. have ProviderRegistry.build() iterate settings.configured_providers instead of re-checking each credential inline.

🤖 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 `@dragontts/app/core/config.py` around lines 224 - 236, The credential checks
in Config.configured_providers and ProviderRegistry.build are duplicated and can
drift. Update ProviderRegistry.build to derive its provider registration
decisions by iterating settings.configured_providers, reusing the existing
provider construction logic while removing the parallel inline credential
checks; preserve the current provider names and registration behavior.
dragontts/app/main.py (1)

117-133: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Shutdown exceptions are silently swallowed (no logging).

All four try/except (asyncio.CancelledError, Exception): pass blocks during shutdown drop any non-cancellation exception with zero visibility. If checkpoint_task, ttl_purge_task, slack_summary_task, or the final metadata.checkpoint() actually crashed (rather than just being cancelled), there's no log trail to diagnose it.

🪵 Suggested fix
     try:
         await checkpoint_task
-    except (asyncio.CancelledError, Exception):
-        pass
+    except asyncio.CancelledError:
+        pass
+    except Exception as e:
+        logger.debug(f"checkpoint task shutdown failed: {e}")
     try:
         await ttl_purge_task
-    except (asyncio.CancelledError, Exception):
-        pass
+    except asyncio.CancelledError:
+        pass
+    except Exception as e:
+        logger.debug(f"ttl purge task shutdown failed: {e}")
     try:
         await slack_summary_task
-    except (asyncio.CancelledError, Exception):
-        pass
+    except asyncio.CancelledError:
+        pass
+    except Exception as e:
+        logger.debug(f"slack summary task shutdown failed: {e}")
     await cache.stop()  # flush write-behind metrics (graceful shutdown loses none)
     try:
         await metadata.checkpoint()  # compact the WAL before worker conns close
-    except Exception:
-        pass
+    except Exception as e:
+        logger.debug(f"final WAL checkpoint failed: {e}")
🤖 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 `@dragontts/app/main.py` around lines 117 - 133, Update the shutdown exception
handlers around checkpoint_task, ttl_purge_task, slack_summary_task, and
metadata.checkpoint() to log non-cancellation exceptions while continuing
shutdown. Preserve cancellation handling without noisy error logs, and use the
module’s existing logging mechanism with task-specific context.

Source: Linters/SAST tools

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

921-932: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add type hints to this signature (and a few peers). Coding guidelines require type hints on all function signatures; _stream_coalesced declares none, and __init__ (metadata, blobs), _produce/_produce_native (record) are also untyped. Annotating these (e.g. req: TTSRequest, key: str, fut: asyncio.Future, of: OutputFormat, instance: BaseTTSProvider, return tuple[dict, AsyncGenerator[bytes, None]]) restores parity with the rest of the module.

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 `@dragontts/app/cache/service.py` around lines 921 - 932, 添加类型注解以覆盖模块中缺失的函数签名:为
_stream_coalesced 的 req、key、fut、of、provider、instance、model、params_canon、record
参数补充合适类型并保留现有返回类型,同时为 __init__ 的 metadata 和 blobs 以及 _produce/_produce_native 的
record 参数补充类型;复用模块已有的请求、输出格式、提供者和记录类型定义。

Source: Coding guidelines

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

83-85: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Isolate per-provider close failures.

If one provider's aclose() raises, the loop stops and remaining providers are never closed (leaked sockets/channels on shutdown). GeminiProvider.aclose() happens to self-guard, but this loop shouldn't rely on every current/future provider doing the same.

♻️ Proposed fix
     async def aclose_all(self) -> None:
         for provider in self._providers.values():
-            await provider.aclose()
+            try:
+                await provider.aclose()
+            except Exception as e:
+                logger.warning(f"Provider '{provider.name}' close failed: {e}")
🤖 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 `@dragontts/app/providers/registry.py` around lines 83 - 85, Update
Registry.aclose_all so each provider’s aclose failure is isolated and the loop
always continues closing every provider. Catch exceptions around the individual
await provider.aclose() call, while preserving the existing iteration and
allowing shutdown to complete after one provider fails.
🤖 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 @.github/workflows/dragontts-docker-publish.yml:
- Line 37: Update the Google authentication step using
google-github-actions/auth to `@v3` and ensure the workflow’s runner image
supports Node 24; if the runner cannot support Node 24, retain the action at a
Node 20-compatible version instead.

In `@dragontts/.env.example`:
- Around line 16-18: Update the default cache configuration in .env.example by
replacing unlimited MAX_CACHE_BYTES and non-expiring TTL_SECONDS values with
finite, bounded defaults, while preserving ENABLE_WRITE_THROUGH=true. Ensure the
documented defaults cap both cache capacity and retention so persistent cached
audio cannot grow indefinitely.

In `@dragontts/app/api/v1/cache.py`:
- Around line 235-244: Validate the older_than_days query parameter in the
delete_by_age endpoint before calling cache.delete_by_age, enforcing a minimum
of zero so negative values are rejected and cannot produce an inverted cutoff.
Preserve the existing dry_run behavior and forwarding for valid values.
- Around line 77-113: Call the CacheService.session_stats method when
constructing the stats response, replacing the bound method reference with its
returned dictionary. Keep the existing session field and surrounding response
structure unchanged.

In `@dragontts/app/api/v1/tts.py`:
- Around line 89-92: Update the 400 warning in the /tts/stream handler to avoid
logging the full req.transcript: truncate it to the existing 50-character limit
used elsewhere, while preserving the model_id and other error context.

In `@dragontts/app/providers/gemini.py`:
- Around line 185-213: Wrap the awaited client.streaming_synthesize() call in
both synth() and stream_synth() with exception handling that converts gRPC
connection or authentication failures into ProviderError, preserving the
original exception as context where supported. Keep the existing response
iteration, cancellation, and empty-audio behavior unchanged.

In `@dragontts/Dockerfile`:
- Line 22: Update the DragonTTS dependency installation step using uv sync so
the final image validates that uv.lock matches pyproject.toml, replacing the
frozen-only mode with the locked validation mode after dragontts/ is copied.

In `@dragontts/pyproject.toml`:
- Around line 5-17: Update the requires-python declaration in the project
metadata to constrain supported Python versions to >=3.11,<3.13, while leaving
the dependency list unchanged.

In `@dragontts/README.md`:
- Around line 19-24: Update the README’s CACHE_TTL_* documentation to match the
actual TTL configuration exposed by dragontts/.env.example, specifically
TTL_SECONDS, or add the documented CACHE_TTL_* keys to the environment
configuration. Ensure operators can identify and configure the variables
controlling cache TTL behavior.

---

Nitpick comments:
In @.github/workflows/dragontts-docker-publish.yml:
- Around line 33-34: Update the actions/checkout@v4 step in the workflow to set
persist-credentials to false, preventing the checkout token from being retained
for subsequent repository-controlled Docker/build steps.
- Around line 61-62: Update the Docker publish configuration by enabling
provenance and SBOM generation instead of setting provenance and sbom to false.
If they must remain disabled, document the intentional policy exception and
define the required compensating controls.

In `@dragontts/.python-version`:
- Line 1: Align the Python version declared by .python-version, the README, and
the Docker image so local development and production use the same supported
target; choose either Python 3.11 or 3.12, or explicitly configure and test both
versions.

In `@dragontts/app/cache/key.py`:
- Around line 20-26: Update the _ZW string in dragontts/app/cache/key.py to
represent each existing invisible character with explicit Unicode escape
sequences instead of literal invisible characters. Preserve the exact current
code points and keep _ZW_TABLE unchanged so stripping behavior remains
identical.

In `@dragontts/app/cache/service.py`:
- Around line 921-932: 添加类型注解以覆盖模块中缺失的函数签名:为 _stream_coalesced 的
req、key、fut、of、provider、instance、model、params_canon、record 参数补充合适类型并保留现有返回类型,同时为
__init__ 的 metadata 和 blobs 以及 _produce/_produce_native 的 record
参数补充类型;复用模块已有的请求、输出格式、提供者和记录类型定义。

In `@dragontts/app/core/config.py`:
- Around line 224-236: The credential checks in Config.configured_providers and
ProviderRegistry.build are duplicated and can drift. Update
ProviderRegistry.build to derive its provider registration decisions by
iterating settings.configured_providers, reusing the existing provider
construction logic while removing the parallel inline credential checks;
preserve the current provider names and registration behavior.

In `@dragontts/app/core/logging.py`:
- Around line 14-25: Update the _JSON configuration in the logging module to
obtain LOG_JSON through the central Settings configuration from app.core.config
instead of reading os.environ directly. Preserve the current default-to-JSON
behavior and false-value handling while removing the independent environment
lookup and any now-unused os import.
- Around line 56-98: Add request-scoped contextvars-based structured logging to
the logging setup, binding context such as the request ID before API request
logs reach InterceptHandler and the configured logger sink. Ensure the context
is propagated across the request and cleared afterward, while preserving the
existing stdlib-to-Loguru forwarding and re-entrancy behavior.
- Line 100: Sort the public symbols in __all__ alphabetically, placing
InterceptHandler before logger while preserving the existing exports.

In `@dragontts/app/main.py`:
- Around line 117-133: Update the shutdown exception handlers around
checkpoint_task, ttl_purge_task, slack_summary_task, and metadata.checkpoint()
to log non-cancellation exceptions while continuing shutdown. Preserve
cancellation handling without noisy error logs, and use the module’s existing
logging mechanism with task-specific context.

In `@dragontts/app/providers/registry.py`:
- Around line 83-85: Update Registry.aclose_all so each provider’s aclose
failure is isolated and the loop always continues closing every provider. Catch
exceptions around the individual await provider.aclose() call, while preserving
the existing iteration and allowing shutdown to complete after one provider
fails.

In `@dragontts/app/schemas/cache.py`:
- Around line 45-50: Update the CreateResponse and DeleteResponse Pydantic
models to use typing.Literal for each documented fixed-value field, including
status and source, so schema validation rejects unsupported provider response
values. Preserve the exact allowed values stated in each model’s docstring or
comments.

In `@dragontts/Dockerfile`:
- Around line 7-11: Update the Dockerfile’s uv installation and execution flow
to exclude the default dev dependency group and ensure the final project
synchronization uses a non-editable install. Apply the production-only
configuration consistently to every relevant uv sync/uv run invocation,
including the final sync, while preserving the existing runtime behavior.
- Line 13: Update the uv image reference in the Dockerfile’s COPY directive to
pin ghcr.io/astral-sh/uv:0.5.11 by its reviewed SHA256 digest, preserving the
existing source image version and destination path.

In `@pyproject.toml`:
- Around line 74-76: Add CI coverage for the self-contained dragontts service by
introducing a test and type-check job, or verify and explicitly reference an
existing separate pipeline that performs both checks. Keep the project-excludes
entry aligned with that coverage so DragonTTS is not left unchecked by the main
pyrefly job.
🪄 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 Plus

Run ID: 7b9578bd-fda4-4e71-a597-befb6dea0fd9

📥 Commits

Reviewing files that changed from the base of the PR and between 1cfdba1 and 134260b.

⛔ Files ignored due to path filters (1)
  • dragontts/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (41)
  • .dockerignore
  • .github/workflows/dragontts-docker-publish.yml
  • dragontts/.dockerignore
  • dragontts/.env.example
  • dragontts/.gitignore
  • dragontts/.python-version
  • dragontts/Dockerfile
  • dragontts/README.md
  • dragontts/app/__init__.py
  • dragontts/app/alerts/__init__.py
  • dragontts/app/alerts/slack.py
  • dragontts/app/alerts/summary.py
  • dragontts/app/api/v1/cache.py
  • dragontts/app/api/v1/health.py
  • dragontts/app/api/v1/tts.py
  • dragontts/app/audio/format.py
  • dragontts/app/audio/resample.py
  • dragontts/app/audio/text.py
  • dragontts/app/cache/key.py
  • dragontts/app/cache/metrics.py
  • dragontts/app/cache/resilience.py
  • dragontts/app/cache/service.py
  • dragontts/app/core/config.py
  • dragontts/app/core/logging.py
  • dragontts/app/main.py
  • dragontts/app/providers/base.py
  • dragontts/app/providers/cartesia.py
  • dragontts/app/providers/cartesia_pool.py
  • dragontts/app/providers/elevenlabs.py
  • dragontts/app/providers/elevenlabs_pool.py
  • dragontts/app/providers/gemini.py
  • dragontts/app/providers/registry.py
  • dragontts/app/providers/sarvam.py
  • dragontts/app/providers/sarvam_pool.py
  • dragontts/app/schemas/cache.py
  • dragontts/app/schemas/tts.py
  • dragontts/app/storage/base.py
  • dragontts/app/storage/filesystem.py
  • dragontts/app/storage/sqlite.py
  • dragontts/pyproject.toml
  • pyproject.toml

uses: actions/checkout@v4

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v1

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

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
gh api repos/google-github-actions/auth/releases/latest --jq .tag_name
gh api 'repos/google-github-actions/auth/contents/action.yml?ref=v1' \
  --jq .content | base64 -d | rg -n 'using:|node'

Repository: juspay/clairvoyance

Length of output: 182


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "Workflow references:"
rg -n "google-github-actions/auth|runs-on:|node:" .github/workflows/dragontts-docker-publish.yml || true

echo
echo "Workflows using node24/ubuntu latest/github-actions:"
rg -n "node24|runs-on:|google-github-actions/auth" .github/workflows || true

echo
echo "actionlint availability/build config:"
(rg -n "actionlint|google-github-actions/auth|google-github-actions/setup-gcloud" -S .github/workflows actionlint.* 2>/dev/null || true)

echo
echo "latest auth action.yml runtime header:"
gh api 'repos/google-github-actions/auth/contents/action.yml?ref=v3' \
  --jq .content | head -n 25 | base64 -d || true

echo
echo "latest auth action.yml node/runtime mentions:"
gh api 'repos/google-github-actions/auth/contents/action.yml?ref=v3' \
  --jq .content | base64 -d | rg -n 'using:|node|node24|runner-target' || true

Repository: juspay/clairvoyance

Length of output: 2599


🌐 Web query:

google-github-actions/auth v3 node24 GitHub Actions runner compatibility

💡 Result:

The google-github-actions/auth v3 action requires the use of Node 24 [1][2]. This version was introduced in v3.0.0, which updated the action's execution environment to Node 24 [1][3]. If you encounter an "argument was out of the range of valid values" error when using v3, it indicates that your GitHub Actions runner does not support Node 24 [2]. To resolve this compatibility issue, you must use a runner environment that includes support for Node 24 [4][2]. If you are using self-hosted runners, you need to ensure the runner software is updated to a version capable of executing Node 24 actions [2]. As of July 2026, GitHub has been migrating its hosted runners to support Node 24 as part of the deprecation of Node 20 [5]. If you are experiencing issues on specific infrastructure, verify that your runner environment is fully updated and compatible with the latest GitHub Actions requirements [2][5]. In cases where an upgrade is not immediately possible, you may need to pin your workflow to a previous version of the action that still supports Node 20 (e.g., v2.x) until your environment can be upgraded [2].

Citations:


Update the Google authentication action.

google-github-actions/auth@v1 runs under node16, while the current release (v3) targets node24. Update this workflow to @v3 and ensure the runner image supports Node 24; otherwise keep it pinned to a Node 20-compatible version.

Suggested change
-uses: google-github-actions/auth@v1
+uses: google-github-actions/auth@v3
📝 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
uses: google-github-actions/auth@v1
uses: google-github-actions/auth@v3
🧰 Tools
🪛 actionlint (1.7.12)

[error] 37-37: the runner of "google-github-actions/auth@v1" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🤖 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 @.github/workflows/dragontts-docker-publish.yml at line 37, Update the Google
authentication step using google-github-actions/auth to `@v3` and ensure the
workflow’s runner image supports Node 24; if the runner cannot support Node 24,
retain the action at a Node 20-compatible version instead.

Source: Linters/SAST tools

Comment thread dragontts/.env.example
Comment on lines +16 to +18
MAX_CACHE_BYTES=0 # 0 = unlimited
TTL_SECONDS=0 # 0 = no expiry
ENABLE_WRITE_THROUGH=true

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

Bound cache growth in the default configuration.

MAX_CACHE_BYTES=0 plus TTL_SECONDS=0 makes write-through cached audio permanent and unlimited. On the documented persistent volume, this can eventually exhaust disk space and break SQLite/blob writes. Set finite defaults or require explicit capacity and retention settings before deployment.

🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 16-16: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)


[warning] 17-17: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)


[warning] 18-18: [UnorderedKey] The ENABLE_WRITE_THROUGH key should go before the MAX_CACHE_BYTES key

(UnorderedKey)

🤖 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 `@dragontts/.env.example` around lines 16 - 18, Update the default cache
configuration in .env.example by replacing unlimited MAX_CACHE_BYTES and
non-expiring TTL_SECONDS values with finite, bounded defaults, while preserving
ENABLE_WRITE_THROUGH=true. Ensure the documented defaults cap both cache
capacity and retention so persistent cached audio cannot grow indefinitely.

Comment on lines +77 to +113
@router.get("/stats")
async def stats(
request: Request,
from_date: str | None = Query(default=None, alias="from"),
to_date: str | None = Query(default=None, alias="to"),
):
"""Cache metrics. Request metrics are date-filterable (?from=&to= YYYY-MM-DD);
the cache snapshot is the current point-in-time state. All from stored
rollups/totals — no full-table scan per call."""
for label, value in (("from", from_date), ("to", to_date)):
if value is not None:
try:
datetime.strptime(value, "%Y-%m-%d")
except ValueError:
raise HTTPException(
status_code=400, detail=f"invalid {label}; use YYYY-MM-DD"
)
metadata = request.app.state.metadata
registry = request.app.state.registry
metrics = await metadata.metrics_summary(from_date=from_date, to_date=to_date)
snapshot = await metadata.stats()
providers = await metadata.provider_metrics_summary(
from_date=from_date, to_date=to_date
)
latency = await metadata.latency_summary(from_date=from_date, to_date=to_date)
return {
"range": {"from": from_date, "to": to_date},
**metrics,
"providers": providers,
"latency": latency,
"entries": snapshot["entries"],
"total_bytes": snapshot["total_bytes"],
"total_words": snapshot["total_words"],
"by_provider": snapshot["by_provider"],
"providers_configured": registry.configured(),
"session": request.app.state.cache.session_stats,
}

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 | 🔴 Critical | ⚡ Quick win

session_stats is a method, not a property — missing call parens breaks /stats.

request.app.state.cache.session_stats returns the bound method object itself instead of the stats dict. CacheService.session_stats is defined as def session_stats(self) -> dict: (see dragontts/app/cache/service.py), so this must be called. As written, FastAPI's response serialization for this endpoint will fail on a non-JSON-serializable bound method, breaking /stats entirely.

🐛 Proposed fix
-        "session": request.app.state.cache.session_stats,
+        "session": request.app.state.cache.session_stats(),
📝 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
@router.get("/stats")
async def stats(
request: Request,
from_date: str | None = Query(default=None, alias="from"),
to_date: str | None = Query(default=None, alias="to"),
):
"""Cache metrics. Request metrics are date-filterable (?from=&to= YYYY-MM-DD);
the cache snapshot is the current point-in-time state. All from stored
rollups/totalsno full-table scan per call."""
for label, value in (("from", from_date), ("to", to_date)):
if value is not None:
try:
datetime.strptime(value, "%Y-%m-%d")
except ValueError:
raise HTTPException(
status_code=400, detail=f"invalid {label}; use YYYY-MM-DD"
)
metadata = request.app.state.metadata
registry = request.app.state.registry
metrics = await metadata.metrics_summary(from_date=from_date, to_date=to_date)
snapshot = await metadata.stats()
providers = await metadata.provider_metrics_summary(
from_date=from_date, to_date=to_date
)
latency = await metadata.latency_summary(from_date=from_date, to_date=to_date)
return {
"range": {"from": from_date, "to": to_date},
**metrics,
"providers": providers,
"latency": latency,
"entries": snapshot["entries"],
"total_bytes": snapshot["total_bytes"],
"total_words": snapshot["total_words"],
"by_provider": snapshot["by_provider"],
"providers_configured": registry.configured(),
"session": request.app.state.cache.session_stats,
}
`@router.get`("/stats")
async def stats(
request: Request,
from_date: str | None = Query(default=None, alias="from"),
to_date: str | None = Query(default=None, alias="to"),
):
"""Cache metrics. Request metrics are date-filterable (?from=&to= YYYY-MM-DD);
the cache snapshot is the current point-in-time state. All from stored
rollups/totalsno full-table scan per call."""
for label, value in (("from", from_date), ("to", to_date)):
if value is not None:
try:
datetime.strptime(value, "%Y-%m-%d")
except ValueError:
raise HTTPException(
status_code=400, detail=f"invalid {label}; use YYYY-MM-DD"
)
metadata = request.app.state.metadata
registry = request.app.state.registry
metrics = await metadata.metrics_summary(from_date=from_date, to_date=to_date)
snapshot = await metadata.stats()
providers = await metadata.provider_metrics_summary(
from_date=from_date, to_date=to_date
)
latency = await metadata.latency_summary(from_date=from_date, to_date=to_date)
return {
"range": {"from": from_date, "to": to_date},
**metrics,
"providers": providers,
"latency": latency,
"entries": snapshot["entries"],
"total_bytes": snapshot["total_bytes"],
"total_words": snapshot["total_words"],
"by_provider": snapshot["by_provider"],
"providers_configured": registry.configured(),
"session": request.app.state.cache.session_stats(),
}
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 91-93: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🤖 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 `@dragontts/app/api/v1/cache.py` around lines 77 - 113, Call the
CacheService.session_stats method when constructing the stats response,
replacing the bound method reference with its returned dictionary. Keep the
existing session field and surrounding response structure unchanged.

Comment on lines +235 to +244
@router.post("/cache/delete-by-age")
async def delete_by_age(
request: Request,
older_than_days: int = Query(..., description="evict entries older than N days"),
dry_run: bool = Query(True, description="preview unless set to false"),
):
"""Delete entries older than ``older_than_days`` (by created_at). ``dry_run``
defaults True — preview matched entries without deleting."""
cache = request.app.state.cache
return await cache.delete_by_age(older_than_days=older_than_days, dry_run=dry_run)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Validate older_than_days to prevent an inverted cutoff.

A negative value produces a future created_at cutoff that matches (and, with dry_run=false, deletes) essentially the entire cache. Consider a lower bound.

🛡️ Proposed fix
-    older_than_days: int = Query(..., description="evict entries older than N days"),
+    older_than_days: int = Query(
+        ..., ge=0, description="evict entries older than N days"
+    ),
📝 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
@router.post("/cache/delete-by-age")
async def delete_by_age(
request: Request,
older_than_days: int = Query(..., description="evict entries older than N days"),
dry_run: bool = Query(True, description="preview unless set to false"),
):
"""Delete entries older than ``older_than_days`` (by created_at). ``dry_run``
defaults Truepreview matched entries without deleting."""
cache = request.app.state.cache
return await cache.delete_by_age(older_than_days=older_than_days, dry_run=dry_run)
`@router.post`("/cache/delete-by-age")
async def delete_by_age(
request: Request,
older_than_days: int = Query(
..., ge=0, description="evict entries older than N days"
),
dry_run: bool = Query(True, description="preview unless set to false"),
):
"""Delete entries older than ``older_than_days`` (by created_at). ``dry_run``
defaults Truepreview matched entries without deleting."""
cache = request.app.state.cache
return await cache.delete_by_age(older_than_days=older_than_days, dry_run=dry_run)
🤖 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 `@dragontts/app/api/v1/cache.py` around lines 235 - 244, Validate the
older_than_days query parameter in the delete_by_age endpoint before calling
cache.delete_by_age, enforcing a minimum of zero so negative values are rejected
and cannot produce an inverted cutoff. Preserve the existing dry_run behavior
and forwarding for valid values.

Comment on lines +89 to +92
logger.warning(
f"/tts/stream 400 — {e} | model_id={req.model_id!r} "
f"transcript={req.transcript!r}"
)

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 | 🟡 Minor | ⚡ Quick win

Avoid logging the full transcript.

This logs the complete req.transcript on every 400, which may contain user PII and is unbounded in size. Elsewhere the transcript is truncated to 50 chars — do the same here.

🛡️ Proposed fix
         logger.warning(
             f"/tts/stream 400 — {e} | model_id={req.model_id!r} "
-            f"transcript={req.transcript!r}"
+            f"transcript={req.transcript[:50]!r}"
         )
📝 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
logger.warning(
f"/tts/stream 400 — {e} | model_id={req.model_id!r} "
f"transcript={req.transcript!r}"
)
logger.warning(
f"/tts/stream 400 — {e} | model_id={req.model_id!r} "
f"transcript={req.transcript[:50]!r}"
)
🤖 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 `@dragontts/app/api/v1/tts.py` around lines 89 - 92, Update the 400 warning in
the /tts/stream handler to avoid logging the full req.transcript: truncate it to
the existing 50-character limit used elsewhere, while preserving the model_id
and other error context.

Comment on lines +185 to +213
client = self._get_client()
responses = await client.streaming_synthesize(
self._request_generator(
text=text,
final_voice_id=final_voice_id,
final_model=final_model,
final_language=final_language,
style_prompt=style_prompt,
)()
)
chunks: list[bytes] = []
try:
async for response in responses:
if response.audio_content:
chunks.append(response.audio_content)
finally:
# Cancel the gRPC stream on early abandon (see stream_synth).
_cancel = getattr(responses, "cancel", None)
if _cancel is not None:
try:
_cancel()
except Exception:
pass

pcm_24k = b"".join(chunks)
if not pcm_24k:
raise ProviderError(
"Gemini TTS returned empty audio — check credentials and voice settings"
)

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)gemini\.py$|(^|/)tts\.py$|(^|/)static\.py$' || true

echo "== gemini.py outline =="
ast-grep outline dragontts/app/providers/gemini.py --view expanded || true

echo "== relevant gemini lines =="
sed -n '130,305p' dragontts/app/providers/gemini.py | nl -ba -v130

echo "== error mapping snippets =="
rg -n "ProviderError|HTTPStatusError|RequestError|OSError|grpc|AioRpcError|streaming_synthesize|synth\(" dragontts/app/api/v1/tts.py dragontts/app/providers/gemini.py
sed -n '1,260p' dragontts/app/api/v1/tts.py | nl -ba -v1

Repository: juspay/clairvoyance

Length of output: 781


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read dependency pins around TensorFlow/Google Cloud Text-to-Speech to understand runtime behavior references.
git ls-files | rg '(^|/)(requirements.*\.txt|pyproject\.toml|uv\.lock|Pipfile|poetry\.lock)$' | while read -r f; do
  echo "== $f =="
  rg -n -i "google-cloud-texttospeech|grpc|tensorflow|text-to-speech" "$f" || true
done

# Probe static imports/config style for consistency.
sed -n '1,120p' dragontts/app/core/config/static.py | nl -ba -v1

Repository: juspay/clairvoyance

Length of output: 29642


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
print("== gemini.py relevant lines ==")
for i, line in enumerate(Path("dragontts/app/providers/gemini.py").read_text().splitlines(), 1):
    if 135 <= i <= 290:
        print(f"{i}\t{line}")

print("\n== tts.py relevant lines ==")
for i, line in enumerate(Path("dragontts/app/api/v1/tts.py").read_text().splitlines(), 1):
    if 1 <= i <= 240:
        print(f"{i}\t{line}")

print("\n== static.py exists? ==")
print("dragontts/app/core/config/static.py" in [str(p) for p in Path("dragontts").rglob("*")])
PY

echo
echo "== grpcio status mapping references from virtualenv (read-only Python module inspection) =="
python3 - <<'PY'
import importlib.util
specs = ["grpc", "grpc.status", "grpc._channel", "grpc_status_status_pb2"]
for name in specs:
    spec = importlib.util.find_spec(name)
    print(name, "=>", (spec.origin if spec else None)[:80] if spec else None)
PY

echo
echo "== inspect locally installed grpcio status source if present =="
python3 - <<'PY'
import importlib.util, textwrap, sys
mod_name = "grpc_status_status_pb2" if importlib.util.find_spec("grpc_status_status_pb2") else "grpc_status_pb2" if importlib.util.find_spec("grpc_status_pb2") else None
print("mod_name", mod_name)
if mod_name:
    import importlib
    mod = importlib.import_module(mod_name)
    print("file", getattr(mod, "__file__", None))
    for attr in dir(mod):
        if attr.startswith("CODEC_") or "AUTH" in attr or "ERROR_CODE" in attr:
            print(attr, getattr(mod, attr))
PY

echo
echo "== inspect locally installed grpcio status_pb2 source if present =="
python3 - <<'PY'
import importlib.util
for name in ["grpc_status_status_pb2", "grpc_status_pb2"]:
    spec = importlib.util.find_spec(name)
    if not spec:
        continue
    import importlib
    mod = importlib.import_module(name)
    lines = textwrap.dedent(open(spec.origin, encoding="utf-8").read()).splitlines()
    print("=", name, spec.origin, "lines", len(lines))
    for idx, line in enumerate(lines, 1):
        if "CODEC" in line or "AUTH" in line or "UNAVAILABLE" in line or "INTERNAL" in line:
            start=max(1,idx-3); end=min(len(lines), idx+3)
            print(f"-- around line {idx} --")
            for n in range(start,end+1):
                print(f"{n}: {lines[n-1]}")
PY

Repository: juspay/clairvoyance

Length of output: 16788


🌐 Web query:

Google Cloud Text-to-Speech Python streaming_synthesize grpcio async cancel streaming response

💡 Result:

To cancel a Google Cloud Text-to-Speech streaming_synthesize RPC in Python using gRPC AsyncIO, you must interact with the call object returned by the client method [1][2][3]. When you call client.streaming_synthesize(request_iterator), it returns a call object that supports the gRPC AsyncIO interface [1][4]. To cancel the RPC, you can call the .cancel() method on this object [1][2][3]. ### Implementation Example The following example demonstrates how to capture the call object and trigger a cancellation: python from google.cloud import texttospeech import asyncio async def run_streaming_cancellation(): client = texttospeech.TextToSpeechClient() # Define your request generator async def request_generator(): # Yield initial config and text chunks... yield texttospeech.StreamingSynthesizeRequest(streaming_config=...) # 1. Initiate the streaming call call = client.streaming_synthesize(request_generator()) # 2. Perform operations, potentially in a task try: async for response in call: print(f"Received audio chunk: {len(response.audio_content)} bytes") # 3. Example condition to cancel if some_condition: print("Cancelling stream...") call.cancel() break except Exception as e: print(f"Stream interrupted: {e}") # Run the async loop asyncio.run(run_streaming_cancellation()) ### Key Considerations * Call Object: The object returned by streaming_synthesize is a StreamStreamCall (in the AsyncIO implementation), which exposes the .cancel() method [1][4]. * Idempotency: The .cancel() method is idempotent [1][2]. If the RPC has already terminated (completed, failed, or previously cancelled), calling it again has no effect [1][2]. * Cancellation Propagation: When you call .cancel() on the client side, gRPC signals the server to discontinue processing for that specific RPC [5]. * Handling Cancellation: When the client cancels an RPC, the local gRPC layer typically raises an asyncio.CancelledError or a specific gRPC error within the context of the async for loop reading the responses, allowing you to catch and handle the cleanup [3]. * Resource Cleanup: If you are managing your own request iterator, ensure that your generator logic is prepared to handle the cessation of the stream, for example, by catching asyncio.CancelledError if you are awaiting other tasks inside it [6][3]. For more detailed information on managing gRPC call lifecycles, refer to the official gRPC AsyncIO API documentation [2].

Citations:


🌐 Web query:

grpcio aio callable raises AioRpcError before awaiting async for initial service call authentication

💡 Result:

In gRPC Python's asynchronous API (grpcio aio), an AioRpcError is the standard exception raised when an RPC operation fails [1][2]. Because gRPC AsyncIO calls are coroutines, they must be awaited to execute and surface any errors [3]. If an AioRpcError is raised before or during the initial service call authentication, it is typically because the await expression on the stub method call (e.g., await stub.MethodName(...)) captures the failure, or because the underlying transport failed to establish connection/authenticate [3][2]. Key points regarding this behavior include: 1. Awaiting is Required: If you do not await the gRPC stub method call, the RPC is not executed, and any associated errors—including authentication or connection failures—will not be raised in the current task [3]. This can lead to silent failures where the call is simply never made [3]. 2. AioRpcError Context: An AioRpcError represents the final status of an RPC [1][4]. When it occurs during authentication (such as when using call credentials), it indicates that the RPC terminated with a non-OK status code [2]. This can happen if the authentication metadata plugin fails, provides invalid credentials, or if the server rejects the initial handshake [5][6]. 3. Debugging Connection Failures: If you suspect the error occurs before the actual RPC logic (e.g., during connection/authentication), you can use the experimental wait_for_connection method on the call object to verify the connection status and catch potential AioRpcErrors explicitly before proceeding with message exchange [2]. 4. Interceptors: If you are using interceptors to inject authentication metadata, be aware that these run during the setup phase of the call [7]. An exception raised within an interceptor or during the continuation of the call chain will propagate as an AioRpcError when the resulting coroutine is awaited [8][7]. To ensure you catch these errors, always wrap your awaited gRPC calls in a try-except block: async def call_with_auth(stub, request): try: response = await stub.YourMethod(request) return response except grpc.aio.AioRpcError as e: print(f'RPC failed: {e.code} - {e.details}') # Handle authentication or connection errors here [5] If an error is occurring without an explicit await, ensure that you are correctly awaiting the call and that no higher-level wrapper or interceptor is suppressing the exception [3][8].

Citations:


Wrap the Gemini streaming_synthesize() call in ProviderError, consistently in both methods.

stream_synth() wraps the receive loop but not the awaited streaming_synthesize() call, and synth() wraps nothing other than the empty-audio response collection. Any gRPC connection/auth failure from this initial await escapes these providers as an unhandled exception; catch it around the await and raise ProviderError so the TTS endpoints can return 502/503 instead of 500. Apply the same exception handling to both synth() and stream_synth().

🧰 Tools
🪛 Ruff (0.15.21)

[error] 206-207: try-except-pass detected, consider logging the exception

(S110)


[warning] 206-206: 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 `@dragontts/app/providers/gemini.py` around lines 185 - 213, Wrap the awaited
client.streaming_synthesize() call in both synth() and stream_synth() with
exception handling that converts gRPC connection or authentication failures into
ProviderError, preserving the original exception as context where supported.
Keep the existing response iteration, cancellation, and empty-audio behavior
unchanged.

Comment thread dragontts/Dockerfile

COPY app ./app

RUN uv sync --frozen

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repo files matching Dockerfile/uv.lock =="
git ls-files | rg '(^|/)Dockerfile$|(^|/)uv\.lock$|(^|/)pyproject\.toml$' || true

echo
echo "== dockerfile context =="
if [ -f dragontts/Dockerfile ]; then
  nl -ba dragontts/Dockerfile | sed -n '1,80p'
else
  fd -a 'Dockerfile' dragontts 2>/dev/null | while read -r f; do
    echo "--- $f"
    nl -ba "$f" | sed -n '1,120p'
  done
fi

echo
echo "== uv lock/pyproject =="
if [ -f pyproject.toml ]; then nl -ba pyproject.toml | sed -n '1,160p'; fi
if [ -f uv.lock ]; then wc -l uv.lock && head -80 uv.lock; fi

echo
echo "== search for uv sync usage =="
rg -n "uv sync|uv run|uv pip|lockfile|locked|frozen" .

Repository: juspay/clairvoyance

Length of output: 367


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== dockerfile context =="
sed -n '1,120p' dragontts/Dockerfile

echo
echo "== project files =="
for f in pyproject.toml dragontts/pyproject.toml uv.lock dragontts/uv.lock; do
  if [ -f "$f" ]; then
    echo "--- $f ($(wc -l "$f" | awk '{print $1}') lines)"
    sed -n '1,180p' "$f"
  fi
done

echo
echo "== uv sync/frozen/locked usage =="
rg -n "uv sync|uv run|uv pip|lockfile|locked|frozen" -S .

Repository: juspay/clairvoyance

Length of output: 50376


Validate the DragonTTS lockfile in the final image layer.

COPY --from=ghcr.io/astral-sh/uv:0.5.11 makes uv available in the Dockerfile, but there is no uv sync --locked after copying dragontts/; after dependencies change, a stale dragontts/uv.lock can still build an environment that no longer matches dragontts/pyproject.toml.

🤖 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 `@dragontts/Dockerfile` at line 22, Update the DragonTTS dependency
installation step using uv sync so the final image validates that uv.lock
matches pyproject.toml, replacing the frozen-only mode with the locked
validation mode after dragontts/ is copied.

Comment thread dragontts/pyproject.toml
Comment on lines +5 to +17
requires-python = ">=3.11"
dependencies = [
"fastapi>=0.115.0",
"uvicorn[standard]>=0.34.0",
"httpx>=0.27.0",
"websockets>=13.0",
"pydantic>=2.7.0",
"pydantic-settings>=2.3.0",
"loguru>=0.7.0",
"pydub>=0.25.1",
"soundfile>=0.12.0",
"numpy>=1.26.0",
"google-cloud-texttospeech>=2.16.0",

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

echo "== pyproject files =="
fd -a 'pyproject\.toml$' . | sed 's#^\./##' | sort

echo
echo "== target pyproject =="
if [ -f dragontts/pyproject.toml ]; then
  cat -n dragontts/pyproject.toml
fi

echo
echo "== python version/runtime pin candidates =="
fd -a '(\.python-version|Dockerfile)$' . | sed 's#^\./##' | sort | while read -r f; do
  echo "--- $f ---"
  sed -n '1,160p' "$f"
done

echo
echo "== audioop / pydub references =="
rg -n --hidden --glob '!.*\.pyc' --glob '!__pycache__/**' 'audioop|pydub|requires-python|python_version' .

echo
echo "== relevant audio/format.py =="
if [ -f app/audio/format.py ]; then
  cat -n app/audio/format.py
fi
# Also search for named file variants
fd -i '^format\.py$' . -x sh -c 'echo "--- $1 ---"; sed -n "1,120p" "$1"' sh {}

Repository: juspay/clairvoyance

Length of output: 22774


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== lockfile pydub entry =="
python3 - <<'PY'
from pathlib import Path
import tomllib
p = Path("dragontts/uv.lock")
data = tomllib.loads(p.read_text())
for pkg in data.get("package", []):
    if pkg.get("name") == "pydub":
        print(pkg)
PY

echo
echo "== lockfile audioop-lts package entry =="
python3 - <<'PY'
from pathlib import Path
import tomllib
p = Path("dragontts/uv.lock")
data = tomllib.loads(p.read_text())
for pkg in data.get("package", []):
    if pkg.get("name") == "audioop-lts":
        print(pkg)
PY

echo
echo "== lockfile dependency references =="
python3 - <<'PY'
from pathlib import Path
import tomllib
p = Path("dragontts/uv.lock")
data = tomllib.loads(p.read_text())
for pkg in data.get("package", []):
    name = pkg.get("name")
    deps = pkg.get("dependencies", [])
    for dep in deps:
        if dep.get("name") in {"pydub", "audioop-lts"}:
            print(dep)
PY

Repository: juspay/clairvoyance

Length of output: 304


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== lockfile pydub and pydub-lts entries =="
sed -n '860,900p' dragontts/uv.lock

echo
echo "== audioop-lts package header and dependency markers =="
sed -n '280,330p' dragontts/uv.lock
sed -n '2440,2455p' dragontts/uv.lock

echo
echo "== dependency lines referencing pydub or audioop-lts in dragontts/uv.lock =="
rg -n '^\[package\.dependencies\]|name = "(audioop-lts|pydub)"|dependencies = \[' dragontts/uv.lock
rg -n --hidden --glob 'uv.lock' '^(\[package\]|\[package\.dependencies\]|name = )' . | rg '(pydub|audioop-lts|dependencies)'

Repository: juspay/clairvoyance

Length of output: 6626


Guard Python version assumptions in requires-python.

The lockfile allows >=3.11, while the root lock already includes audioop-lts behind the python_full_version >= '3.13' | '3.14' marker required by pydub. Narrow requires-python to the actual deployment baseline (>=3.11,<3.13) so package resolvers won’t assume this works on 3.13+.
[stable_dependency_resolution]

🤖 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 `@dragontts/pyproject.toml` around lines 5 - 17, Update the requires-python
declaration in the project metadata to constrain supported Python versions to
>=3.11,<3.13, while leaving the dependency list unchanged.

Comment thread dragontts/README.md
Comment on lines +19 to +24
Key knobs (see `.env.example` for the full list):
- `SPLIT_AT_SYMBOLS` — split `/tts/bytes` (one-shot) transcripts into per-
sentence cache entries (e.g. `.:?`). Empty = off.
- `SPLIT_AT_SYMBOLS_STREAM` — same, for `/tts/stream`.
- `ENABLE_WRITE_THROUGH=true` — store every synth into the cache.
- `CACHE_TTL_*` — length-scaled TTL (longer phrases live longer).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Document the actual TTL variables.

The README tells operators to configure CACHE_TTL_*, but dragontts/.env.example exposes only TTL_SECONDS=0 in its full configuration list. Update the README to the actual variables or add the documented keys so operators can configure the intended TTL behavior.

🤖 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 `@dragontts/README.md` around lines 19 - 24, Update the README’s CACHE_TTL_*
documentation to match the actual TTL configuration exposed by
dragontts/.env.example, specifically TTL_SECONDS, or add the documented
CACHE_TTL_* keys to the environment configuration. Ensure operators can identify
and configure the variables controlling cache TTL behavior.

@narsimhaReddyJuspay
narsimhaReddyJuspay force-pushed the move-dragontts-code-clairvoyance branch from 134260b to 55f7c81 Compare July 23, 2026 12:42
- added seperate git workflow to handle seperate deployment
@narsimhaReddyJuspay
narsimhaReddyJuspay force-pushed the move-dragontts-code-clairvoyance branch from 55f7c81 to 5d72ff0 Compare July 23, 2026 14:06
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.

2 participants