Skip to content

feat(voice): S1 — flute-gateway voice_profiles registry (loader + routing + /profiles + /validate) - #1922

Merged
POWERFULMOVES merged 2 commits into
mainfrom
feat/voice-s1-registry
Jul 1, 2026
Merged

POWERFULMOVES merged 2 commits into
mainfrom
feat/voice-s1-registry

Conversation

@POWERFULMOVES

@POWERFULMOVES POWERFULMOVES commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Voice S1 (flute-gateway side): a lifespan-preloaded, TTL-cached, NATS-invalidated voice_profiles registry that resolves a requested voice slug → (provider, engine, voice) and exposes /v1/voice/profiles + /v1/voice/validate. Built ultracode (understand → design → adversarial gap-check → implement → review).

What

  • voice_registry.py (no main import): VoiceProfile + VoiceRegistry (load/refresh/TTL/NATS-subscribe), select_provider_and_params, grounding//validate helpers.
  • main.py: _resolve_voice_profile hook wired at all 3 dispatch sites; lifespan preload + TTL task + NATS subscribe (+ shutdown); GET/POST /v1/voice/profiles, GET /v1/voice/profiles/{name}, GET/POST /v1/voice/validate; config advertises voice_profiles.
  • 24 DB-free unit tests (mock httpx) — all green; full flute-gateway suite 160 passed / 0 failed.

The 4 correctness fixes the adversarial gap-check caught (and this honors)

  1. No synth_kwargs — providers have fixed signatures; select_provider_and_params does explicit per-engine translation (vibevoice voice_preset→voice; voicebox profile_id/voice_type→voice+engine; ultimate_tts primary_engine/<engine>_voice→engine+voice; omnivoice ref_audio/instruct→voice+engine).
  2. The hook mutates request.provider/engine/voice (not a local) before provider_name = request.provider or DEFAULT_PROVIDER, and short-circuits the persona path so a matched voice slug wins.
  3. Explicit per-engine engine_specific mapping (no passthrough fiction).
  4. Accept-Profile: pmoves_core (reads) / Content-Profile (writes) on voice_profiles/personas/consciousness_theories, else PostgREST 404s.

Graceful degradation

VoiceRegistry.load() never raises — httpx error / 404 / table-absent (PGRST205/42P01) / missing creds → empty cache, _healthy=False, WARNING, and the synthesize hook no-ops → existing DEFAULT_PROVIDER path unchanged. So this is safe to merge before v5_16 is applied (Z890 DB lane); it activates automatically once voice_profiles exists.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added voice profile browsing, lookup, creation, and validation endpoints.
    • Voice selection can now be driven by named profiles, with automatic routing to the right synthesis settings.
    • The voice configuration response now reflects whether voice profile support is available.
  • Bug Fixes

    • Improved fallback behavior so synthesis still works when a voice profile isn’t found.
    • Added safer handling for temporary data source or update issues, with automatic cache refresh in the background.

…files)

Add a lifespan-preloaded, TTL-cached, NATS-invalidated voice profile registry
that maps a requested `voice` slug -> (provider, params) via v5_16
pmoves_core.voice_profiles, plus /v1/voice/profiles + /v1/voice/validate routes.

- voice_registry.py: VoiceProfile dataclass + VoiceRegistry (load/refresh/get/
  list/upsert_local/run_ttl_loop/subscribe). Graceful degradation: httpx error /
  404 / table-absent (PGRST205/42P01) / non-2xx -> empty cache, _healthy=False,
  WARNING, return False. load() never raises out of startup.
- select_provider_and_params: EXPLICIT per-engine translation of engine_specific
  into the fixed provider signatures (no synth_kwargs):
    vibevoice  {voice_preset}                 -> voice
    voicebox   {profile_id,voice_type,...}    -> voice(+engine)
    ultimate_tts {primary_engine,<eng>_voice} -> (engine, voice)
    omnivoice  {ref_audio,instruct,...}       -> (voice=ref_audio, engine=instruct)
- main.py: lifespan preload (after providers/NATS, non-fatal) + TTL task +
  voice.registry.update.v1 subscribe; request-mutation hook (_resolve_voice_profile)
  ahead of persona resolution at all 3 dispatch sites — mutates request.provider/
  engine/voice BEFORE `provider_name = request.provider or DEFAULT_PROVIDER`.
- pmoves_core schema reads/writes set Accept-Profile/Content-Profile headers.
- Grounding contract validation: forbidden consciousness_shape, persona_ids +
  consciousness_theory_id resolved against real PKs (substrate-unreachable ->
  warning, not error); deferred keys surfaced as warnings.
- 24 DB-free unit tests (httpx mocked); full suite 160 passed / 36 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@POWERFULMOVES, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 27 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 99f63648-47d0-4baf-babc-6218ded92b1e

📥 Commits

Reviewing files that changed from the base of the PR and between 2df2139 and 30d19c7.

📒 Files selected for processing (3)
  • pmoves/services/flute-gateway/main.py
  • pmoves/services/flute-gateway/tests/test_voice_profiles.py
  • pmoves/services/flute-gateway/voice_registry.py
📝 Walkthrough

Walkthrough

Adds a Supabase-backed VoiceRegistry module to flute-gateway for caching "S1" voice profiles, with provider/param translation and capability/grounding validation. Integrates registry-based voice slug resolution into synthesize endpoints and lifespan management, adds /v1/voice/profiles and /v1/voice/validate HTTP endpoints, and adds a test suite.

Changes

Voice Profile Registry

Layer / File(s) Summary
VoiceRegistry core module and validation
pmoves/services/flute-gateway/voice_registry.py
Adds VoiceProfile value object, VoiceRegistry class with Supabase-backed load/TTL refresh/NATS-driven invalidation, select_provider_and_params() engine translation, validate_capability(), validate_grounding() (with _resolve_pks()), and grounding_contract().
Gateway lifespan wiring and synthesize routing
pmoves/services/flute-gateway/main.py
Imports registry helpers, adds _resolve_voice_profile() cascade, preloads/subscribes registry at startup, cancels TTL task at shutdown, exposes voice_profiles feature flag in /v1/voice/config, and routes TTS/audio/prosodic synthesize endpoints through registry resolution before persona/intent fallback.
Voice profile registry HTTP API
pmoves/services/flute-gateway/main.py
Adds VoiceProfileIn, ValidateRequest, ValidateResponse models and new endpoints: GET/POST /v1/voice/profiles, GET /v1/voice/profiles/{name}, GET/POST /v1/voice/validate.
Voice profile registry tests
pmoves/services/flute-gateway/tests/test_voice_profiles.py
Adds fake httpx scaffolding and tests for provider mapping, registry load/refresh/degradation, request mutation, synthesize routing, profile endpoints, and validation endpoint behavior.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant FluteGateway
  participant VoiceRegistry
  participant Supabase

  Client->>FluteGateway: POST /v1/voice/synthesize (voice slug)
  FluteGateway->>VoiceRegistry: get(name)
  alt profile found
    VoiceRegistry-->>FluteGateway: provider/engine/voice
  else not found
    FluteGateway->>FluteGateway: fallback persona/intent resolution
  end
  FluteGateway-->>Client: synthesized audio

  Note over FluteGateway,VoiceRegistry: Startup
  FluteGateway->>VoiceRegistry: load()
  VoiceRegistry->>Supabase: fetch active voice_profiles
  Supabase-->>VoiceRegistry: rows
  loop TTL loop
    VoiceRegistry->>Supabase: refresh()
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • POWERFULMOVES/PMOVES.AI#1235: Both modify /v1/voice/synthesize provider routing in main.py—this PR adds registry-based slug resolution feeding into provider dispatch.
  • POWERFULMOVES/PMOVES.AI#1794: Both modify /v1/voice/config and synthesize routing logic in main.py around engine/provider resolution.
  • POWERFULMOVES/PMOVES.AI#1890: The VoiceRegistry and profile endpoints depend on the pmoves_core.voice_profiles schema introduced there.

Poem

A slug comes hopping, "voice" in tow,
The registry whispers which way to go,
Cache it warm, TTL spins,
Grounding checked before it begins,
Hop, synth, sing — the carrot wins! 🥕🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.00% 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
Title check ✅ Passed The title clearly names the voice_profiles registry, routing, and API additions, matching the main change.
Description check ✅ Passed The description covers the required summary and testing context and is mostly aligned with the template.
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
  • Commit unit tests in branch feat/voice-s1-registry

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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2df2139ca9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pmoves/services/flute-gateway/voice_registry.py Outdated
Comment thread pmoves/services/flute-gateway/main.py Outdated
Comment thread pmoves/services/flute-gateway/voice_registry.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (2)
pmoves/services/flute-gateway/tests/test_voice_profiles.py (1)

1-524: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Missing coverage for NATS invalidation and TTL refresh task.

The suite thoroughly covers select_provider_and_params, load()/refresh() degradation paths, request mutation, and all the new HTTP endpoints. However, per the PR objectives the registry is described as "kept fresh with a TTL task and NATS subscription" for invalidation — neither the NATS subscribe-and-invalidate callback nor the TTL maintenance loop is exercised anywhere in this file.

Given this is the dedicated test-suite layer for the registry, consider adding at least one test that simulates a NATS invalidation message triggering a cache refresh/clear, mirroring the existing _FakeClient pattern for the underlying HTTP fetch.

🤖 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 `@pmoves/services/flute-gateway/tests/test_voice_profiles.py` around lines 1 -
524, Add coverage for the registry’s NATS invalidation and TTL refresh behavior,
since `VoiceRegistry` is only tested for HTTP load/refresh today. Introduce
tests around the NATS subscription/invalidation callback and the TTL maintenance
task so a simulated message causes the cache to clear or refresh as expected.
Use the existing `VoiceRegistry`, `_FakeClient`, and `vr` monkeypatch patterns
to keep it DB-free and to locate the relevant hook/task code even if names
shift.
pmoves/services/flute-gateway/voice_registry.py (1)

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

Narrow the startup subscription guard. Catch nats.errors.Error (or the specific NATS subscribe exceptions you expect) instead of Exception; if the broad guard is intentional, add # noqa: BLE001 with the startup rationale.

🤖 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 `@pmoves/services/flute-gateway/voice_registry.py` at line 288, The startup
subscription guard in voice_registry.py is too broad because the exception
handler around the NATS subscription path catches Exception. Update the handler
in the startup subscription logic to catch nats.errors.Error, or the specific
NATS subscribe exception types expected by the code, so only subscription
failures are handled. If you intentionally need the broad catch in this startup
path, keep the existing structure but add # noqa: BLE001 and a brief
startup-specific rationale in the same except block.

Source: Linters/SAST tools

🤖 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 `@pmoves/services/flute-gateway/main.py`:
- Around line 1410-1415: The current write path in the voice profile handler
only warms the local `voice_registry`, so other instances stay stale; after the
Supabase write succeeds in the `main.py` flow around `VoiceProfile.from_row` and
`voice_registry.upsert_local`, publish an invalidation/update event to
`REGISTRY_UPDATE_SUBJECT`. Build the event payload using the schema in
`services/common/events.py`, validate it before publishing, and send it via the
existing NATS pub/sub path so all subscribers to `voice.registry.update.v1`
receive the update.
- Around line 283-285: The request handling in the voice update path is
bypassing the registry health gate because it still calls
voice_registry.get(request.voice) even when the registry is unhealthy. Update
the guard in the request mutation flow around the voice_registry lookup so it
no-ops when voice_registry.healthy is false, or explicitly set the registry to
healthy only after a trusted successful refresh/write. Use the existing symbols
voice_registry, request.voice, and the voice_registry.get path to locate the
fix.
- Line 446: The VoiceProfileIn.name field currently only documents the slug
format, so invalid values can still be accepted and persisted by the
/v1/voice/profiles flow. Add actual field-level validation on
VoiceProfileIn.name in main.py using the existing name field definition so it
enforces the 3–64 character length and the allowed slug pattern before any
Supabase write occurs. Keep the constraint close to the model that feeds the
create/profile endpoint so the contract is enforced at input time, not just
described.

In `@pmoves/services/flute-gateway/voice_registry.py`:
- Around line 412-427: In `_resolve_pks()` in `voice_registry.py`, do not return
None for PostgREST 4xx validation/query failures; reserve None only for
transport/network or 5xx substrate errors. Update the httpx call handling so
malformed `persona_ids` or `consciousness_theory_id` queries fail closed by
raising/propagating an error on non-200 4xx responses, while keeping the
existing None fallback for `httpx.HTTPError` and other unrecoverable
connectivity issues.
- Around line 385-387: The Ultimate-TTS profile validation in
select_provider_and_params() is too permissive because it only checks
engine_specific.primary_engine and can still accept a profile that resolves
without any voice. Update the validation branch for engine == "ultimate_tts" to
also require the selected voice key derived from the primary engine (the same
key used later to read engine_specific[f"{primary_engine}_voice"]), and append
an error when that engine-specific voice is missing so invalid profiles are
rejected before persisting.
- Line 40: The VOICE_REGISTRY_TTL_SECONDS parsing in voice_registry.py can raise
during module import and crash the gateway, so move the conversion out of the
top-level DEFAULT_TTL_SECONDS assignment or wrap it in safe parsing with a
fallback. Update the module-level config handling around DEFAULT_TTL_SECONDS to
catch invalid values, log or ignore malformed input, and use the default TTL
instead so imports always succeed.

---

Nitpick comments:
In `@pmoves/services/flute-gateway/tests/test_voice_profiles.py`:
- Around line 1-524: Add coverage for the registry’s NATS invalidation and TTL
refresh behavior, since `VoiceRegistry` is only tested for HTTP load/refresh
today. Introduce tests around the NATS subscription/invalidation callback and
the TTL maintenance task so a simulated message causes the cache to clear or
refresh as expected. Use the existing `VoiceRegistry`, `_FakeClient`, and `vr`
monkeypatch patterns to keep it DB-free and to locate the relevant hook/task
code even if names shift.

In `@pmoves/services/flute-gateway/voice_registry.py`:
- Line 288: The startup subscription guard in voice_registry.py is too broad
because the exception handler around the NATS subscription path catches
Exception. Update the handler in the startup subscription logic to catch
nats.errors.Error, or the specific NATS subscribe exception types expected by
the code, so only subscription failures are handled. If you intentionally need
the broad catch in this startup path, keep the existing structure but add #
noqa: BLE001 and a brief startup-specific rationale in the same except block.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1a39ea20-59f1-4373-b4ec-07327a4d0d63

📥 Commits

Reviewing files that changed from the base of the PR and between ced4726 and 2df2139.

📒 Files selected for processing (3)
  • pmoves/services/flute-gateway/main.py
  • pmoves/services/flute-gateway/tests/test_voice_profiles.py
  • pmoves/services/flute-gateway/voice_registry.py

Comment thread pmoves/services/flute-gateway/main.py Outdated
Comment thread pmoves/services/flute-gateway/main.py Outdated
Comment thread pmoves/services/flute-gateway/main.py
Comment thread pmoves/services/flute-gateway/voice_registry.py Outdated
Comment thread pmoves/services/flute-gateway/voice_registry.py
Comment thread pmoves/services/flute-gateway/voice_registry.py Outdated
@claude

claude Bot commented Jun 30, 2026

Copy link
Copy Markdown

Triage — coderabbitai[bot] review (PR #1922, commit 2df2139)

P1 findings (act via /pr-trim):

  • voice_registry.py:40 — malformed VOICE_REGISTRY_TTL_SECONDS raises on module import, crashing the gateway before graceful degradation can run
  • voice_registry.py:387select_provider_and_params() accepts a profile with no resolved voice (primary_engine set but matching engine_specific voice key absent), persisting invalid state
  • voice_registry.py:427_resolve_pks() returns None on PostgREST 4xx for user-supplied persona_ids/consciousness_theory_id, silently treating a validation failure as unverifiable instead of rejecting
  • main.py:285 — health gate bypassed: _cache read even when voice_registry.healthy is false, breaking the documented no-op contract
  • main.py:446 — VoiceProfileIn.name has no field-level validation; invalid slugs reach Supabase unchecked
  • main.py:1415 — write path warms only the local registry; no REGISTRY_UPDATE_SUBJECT event published, leaving all other gateway instances stale until TTL

P2: 1, P3: 1

- voice_registry.load(): add `deleted_at=is.null` so the service-role read
  honors the soft-delete lifecycle (RLS predicate is bypassed by service key).
- _resolve_voice_profile hook: gate on `voice_registry.healthy` (don't read the
  cache when unhealthy); CLEAR request.voice after a registry match unless a
  provider-native voice was resolved (never leak the slug as preset/ref_audio).
- validate_grounding: validate persona UUIDs locally + distinguish PostgREST 4xx
  (hard reject) from network/5xx (transient warning) so malformed grounding IDs
  no longer slip through /validate and profile creation as warnings.
- validate_capability(ultimate_tts): also require engine_specific.<primary>_voice
  (the key select_provider_and_params reads), so voice-less profiles are rejected.
- VoiceProfileIn.name: enforce the v5_16 slug pattern with pydantic v2 `pattern=`
  (was description-only) so bad slugs can't be persisted.
- create_voice_profile: publish a fire-and-forget invalidation on
  voice.registry.update.v1 after a successful write so peer gateway instances
  refresh instead of waiting for TTL.
- _env_int guard: malformed VOICE_REGISTRY_TTL_SECONDS no longer crashes import.
- Tests: +9 (soft-delete filter, slug-clear, health-gate no-op, malformed-UUID
  reject, 400 client-error reject, ultimate_tts voice-key capability, TTL guard,
  name-pattern 422, invalidation publish). Suite: 169 passed / 36 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@POWERFULMOVES
POWERFULMOVES merged commit f2b8277 into main Jul 1, 2026
17 checks passed
@POWERFULMOVES
POWERFULMOVES deleted the feat/voice-s1-registry branch July 1, 2026 02:30
POWERFULMOVES pushed a commit that referenced this pull request Jul 1, 2026
…rypoint fix

Scope note: submodule gitlink promotion removed from this PR — origin/main moved ahead (#1922/#1923/#1925) and the 21 pins were rollbacks/sideways relative to the new base. Submodule promotion will be handled separately after pins are aligned with origin/main.

Changes kept:

- All non-submodule worktree deltas from Agent Zero SPARK (migrations, pmoves/Makefile is superseded by origin/main #1924, generated kong.yml, tokenism-simulator files, etc.).

- Kong entrypoint fix: /bin/bash + /dev/tcp wait loop and migrations bootstrap/up fallback (compatible with origin/main #1924 network/bind changes).

- SQL lint allowlist for legacy worktree-delta migrations with to-anon / USING(true) patterns.
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.

1 participant