(feat): migrate dragontts code to clairvoyance repo#949
(feat): migrate dragontts code to clairvoyance repo#949narsimhaReddyJuspay wants to merge 1 commit into
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughDragonTTS 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. ChangesDragonTTS service
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
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
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’spyreflytype-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, invalidaudio_base64will raise duringb64decode()and abort the entire batch with a 500 (it’s not caught by anyexcept). 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 = (
| if req.audio_base64: | ||
| try: | ||
| audio_override = base64.b64decode(req.audio_base64) | ||
| except Exception: | ||
| raise HTTPException(status_code=400, detail="invalid audio_base64") |
| # 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 |
| ## 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). |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (15)
dragontts/app/schemas/cache.py (1)
45-50: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winEncode the documented response values with
Literal.
CreateResponseandDeleteResponsecurrently 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 winDisable 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/configis unnecessary. Setpersist-credentials: falseto 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 | 🔵 TrivialRetain supply-chain attestations unless intentionally disabled.
provenance: falseandsbom: falseremove 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 winAlign 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 winUse production-only uv synchronization.
uv syncanduv runinclude the defaultdevgroup, and project installs are editable by default. SetUV_NO_DEV=1or pass--no-dev, and use--no-editablefor 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=4Use
--no-editableon 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 winPin the
uvtool image by digest.
ghcr.io/astral-sh/uv:0.5.11is 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 winAdd 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. Sinceproject-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 winReads
os.environdirectly instead of going through the centralSettings.
LOG_JSONis read viaos.environ.get(...)here, bypassing theSettingsobject defined inapp/core/config.pyfor this same service. As per coding guidelines, "Load ALL configuration fromapp/core/config/static.pyusingget_required_env()for mandatory variables; never import directly fromos.environelsewhere." Even accounting forSettingsbeing this service's own convention (viapydantic-settings) rather than the monorepo'sstatic.py/get_required_env()pattern, this file should route throughSettingsrather 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 tradeoffNo 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 winLiteral invisible Unicode characters embedded directly in source.
_ZWcontains 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\uXXXXescapes 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_providersduplicatesProviderRegistry.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_providersreporting a provider as live thatbuild()didn't actually register, or vice versa).Consider deriving one from the other — e.g. have
ProviderRegistry.build()iteratesettings.configured_providersinstead 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 winShutdown exceptions are silently swallowed (no logging).
All four
try/except (asyncio.CancelledError, Exception): passblocks during shutdown drop any non-cancellation exception with zero visibility. Ifcheckpoint_task,ttl_purge_task,slack_summary_task, or the finalmetadata.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 valueAdd type hints to this signature (and a few peers). Coding guidelines require type hints on all function signatures;
_stream_coalesceddeclares 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, returntuple[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 winIsolate 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
⛔ Files ignored due to path filters (1)
dragontts/uv.lockis excluded by!**/*.lock
📒 Files selected for processing (41)
.dockerignore.github/workflows/dragontts-docker-publish.ymldragontts/.dockerignoredragontts/.env.exampledragontts/.gitignoredragontts/.python-versiondragontts/Dockerfiledragontts/README.mddragontts/app/__init__.pydragontts/app/alerts/__init__.pydragontts/app/alerts/slack.pydragontts/app/alerts/summary.pydragontts/app/api/v1/cache.pydragontts/app/api/v1/health.pydragontts/app/api/v1/tts.pydragontts/app/audio/format.pydragontts/app/audio/resample.pydragontts/app/audio/text.pydragontts/app/cache/key.pydragontts/app/cache/metrics.pydragontts/app/cache/resilience.pydragontts/app/cache/service.pydragontts/app/core/config.pydragontts/app/core/logging.pydragontts/app/main.pydragontts/app/providers/base.pydragontts/app/providers/cartesia.pydragontts/app/providers/cartesia_pool.pydragontts/app/providers/elevenlabs.pydragontts/app/providers/elevenlabs_pool.pydragontts/app/providers/gemini.pydragontts/app/providers/registry.pydragontts/app/providers/sarvam.pydragontts/app/providers/sarvam_pool.pydragontts/app/schemas/cache.pydragontts/app/schemas/tts.pydragontts/app/storage/base.pydragontts/app/storage/filesystem.pydragontts/app/storage/sqlite.pydragontts/pyproject.tomlpyproject.toml
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Authenticate to Google Cloud | ||
| uses: google-github-actions/auth@v1 |
There was a problem hiding this comment.
🎯 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' || trueRepository: 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:
- 1: https://github.com/google-github-actions/auth/releases/tag/v3.0.0
- 2: Issue with node being out of range after updating to v3 google-github-actions/auth#514
- 3: Bump to Node 24 and remove old parameters google-github-actions/auth#508
- 4: https://github.com/google-github-actions/auth
- 5: https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/
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.
| 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
| MAX_CACHE_BYTES=0 # 0 = unlimited | ||
| TTL_SECONDS=0 # 0 = no expiry | ||
| ENABLE_WRITE_THROUGH=true |
There was a problem hiding this comment.
🩺 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.
| @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, | ||
| } |
There was a problem hiding this comment.
🎯 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.
| @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, | |
| } | |
| `@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(), | |
| } |
🧰 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.
| @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) |
There was a problem hiding this comment.
🗄️ 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.
| @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) | |
| `@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 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) |
🤖 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.
| logger.warning( | ||
| f"/tts/stream 400 — {e} | model_id={req.model_id!r} " | ||
| f"transcript={req.transcript!r}" | ||
| ) |
There was a problem hiding this comment.
🔒 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.
| 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.
| 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" | ||
| ) |
There was a problem hiding this comment.
🩺 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 -v1Repository: 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 -v1Repository: 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]}")
PYRepository: 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:
- 1: https://grpc.github.io/grpc/python/_modules/grpc/aio/_base_call.html
- 2: https://grpc.github.io/grpc/python/grpc_asyncio.html
- 3: https://stackoverflow.com/questions/68491834/handle-client-side-cancellation-in-grpc-python-asyncio
- 4: https://grpc.github.io/grpc/python/_modules/grpc/aio/_call.html
- 5: https://grpc.io/docs/guides/cancellation/
- 6: Propagation of Exceptions from streaming synthesis (text-to-speech) googleapis/google-cloud-python#13391
🌐 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:
- 1: https://grpc.github.io/grpc/python/_modules/grpc/aio/_call.html
- 2: https://grpc.github.io/grpc/python/grpc_asyncio.html
- 3: [aio] Not awaiting an asynchronous call silently ignores all errors grpc/grpc#29679
- 4: https://docs.ros.org/en/api/grpc/html/classgrpc_1_1aio_1_1__call_1_1AioRpcError.html
- 5: https://docs.ros.org/en/api/grpc/html/async__customized__auth__client_8py_source.html
- 6: https://docs.ros.org/en/latest/api/grpc/html/async__customized__auth__client_8py_source.html
- 7: https://github.com/ray-project/ray/blob/master/python/ray/_private/authentication/grpc_authentication_client_interceptor.py
- 8: https://grpc.github.io/grpc/python/_modules/grpc/aio/_interceptor.html
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.
|
|
||
| COPY app ./app | ||
|
|
||
| RUN uv sync --frozen |
There was a problem hiding this comment.
🗄️ 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.
| 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", |
There was a problem hiding this comment.
🩺 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)
PYRepository: 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.
| 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). |
There was a problem hiding this comment.
🗄️ 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.
134260b to
55f7c81
Compare
- added seperate git workflow to handle seperate deployment
55f7c81 to
5d72ff0
Compare
Summary by CodeRabbit