feat(platforms): add Carbon Voice as a platform plugin - #43226
feat(platforms): add Carbon Voice as a platform plugin#43226cristianmgm7 wants to merge 7 commits into
Conversation
teknium1
left a comment
There was a problem hiding this comment.
Thank you for the substantial Carbon Voice implementation. I found blocking integration issues before this can be safely salvaged.
Problems
gateway/platforms/carbonvoice/adapter.py:131relies onvoice_out_carries_text, but currentgateway/platforms/base.py:5000-5027suppresses follow-up text only for a successful Telegram caption. Carbon Voice voice-out therefore delivers both the audio memo and text.adapter.py:164reads onlyconfig.extra['voice_out']; the PR's config bridge does not seed that field, whilewebsite/docs/user-guide/messaging/carbonvoice.mdtells users to setCARBONVOICE_VOICE_OUT=true.adapter.py:289-360has no credential-scoped lock. Current adapters use the base helpers atgateway/platforms/base.py:2752-2779to prevent two profiles from claiming one credential.
Suggested changes
- Please rework this as a platform plugin.
gateway/platforms/ADDING_A_PLATFORM.md:5-15recommends that route for community/third-party platforms, andgateway/platform_registry.py:113-159already provides env enablement, cron delivery, standalone sending, auth metadata, and prompt hints without new core branches. - Wire and test voice-out configuration, then add a tested general delivery contract for Carbon Voice audio/text suppression.
Automated hermes-sweeper review.
| # this are unaffected. Requires the patched base.py from PR 6 (and | ||
| # the parallel upstream PR) — without it the attribute is read but | ||
| # ignored, and we ship a duplicate text bubble. | ||
| voice_out_carries_text = True |
There was a problem hiding this comment.
voice_out_carries_text is not consumed by current main. BasePlatformAdapter suppresses the follow-up text only for a successful Telegram caption (gateway/platforms/base.py:5000-5027), so voice-out will produce both an audio memo and a text reply. Please add the general base-layer contract plus coverage, or remove this duplicate-suppression path.
| # VOICE is a no-op (the gate's other conditions still fail). | ||
| # Default ``False`` to preserve text-out for existing | ||
| # deployments that haven't opted in. | ||
| self._voice_out: bool = bool(extra.get("voice_out")) |
There was a problem hiding this comment.
This reads only config.extra['voice_out'], but the documented CARBONVOICE_VOICE_OUT=true is never bridged into extra by this PR's gateway/config.py changes. Wire the supported configuration path and add an env/config propagation test.
|
|
||
| # ── Lifecycle ──────────────────────────────────────────────────────── | ||
|
|
||
| async def connect(self) -> bool: |
There was a problem hiding this comment.
Acquire a credential-scoped lock before opening the API/Socket.IO connection and release it in disconnect(). Current BasePlatformAdapter._acquire_platform_lock() / _release_platform_lock() provide the profile-safe mechanism used by token-backed adapters.
Port of the external PhononX/hermes-plugin-carbonvoice plugin (v0.3.6) into plugins/platforms/, following the bundled-plugin convention used by the other 21 platform plugins. Registers via ctx.register_platform() with env_enablement_fn, standalone_sender_fn, and cron_deliver_env_var=CARBONVOICE_HOME_CHANNEL — zero core changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Prevents two gateways from claiming the same Carbon Voice PAT, matching the telegram/discord/slack/whatsapp adapters (_acquire_platform_lock in connect, _release_platform_lock in disconnect). Addresses review feedback on the PAT having no credential lock. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a capability flag on BasePlatformAdapter: adapters whose successful play_tts() already delivers the reply text to the user (e.g. Carbon Voice's server-side transcript rendered inline with the voice memo) declare voice_out_carries_text = True, and the auto-TTS response flow suppresses the follow-up text send — no more duplicate audio + text. - Default False: existing platforms keep sending audio and text. - A failed play_tts still falls back to the text send (reply never lost). - Telegram's length-conditional caption suppression is unchanged. - Carbon Voice opts in (its /v5/messages/audio endpoint transcribes server-side, so the transcript IS the text). Addresses review feedback: the flag was previously read by the adapter but ignored by core, delivering both the audio memo and the text. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Covers the three review items: register(ctx) → PlatformEntry hooks, the CARBONVOICE_VOICE_OUT env → seed → adapter._voice_out chain, and connect() bailing out when the credential lock is held (releasing it on disconnect). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports the setup guide from the native-platform branch, adapted to the plugin reality: python-socketio is optional (polling-only without it, no [messaging] extra change), and the voice-out note documents the one-bubble transcript behavior. Adds Carbon Voice to the platform comparison table and the setup-guide links in the messaging index. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2c28548 to
547ace5
Compare
Prevents two gateways from claiming the same Carbon Voice PAT, matching the telegram/discord/slack/whatsapp adapters. hasattr-guarded: the _acquire_platform_lock helper only exists on newer Hermes cores; older cores skip the check. Also updates the voice_out_carries_text comment — upstream honors the flag since NousResearch/hermes-agent#43226. Mirror of the fix shipped in the upstream PR branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Core review on NousResearch/hermes-agent#43226 asked for the plugin route; upstream moved all platforms to plugins/platforms/. Records the sync-discipline status (PAT lock mirrored). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks for the review — reworked as requested. The branch has been rebuilt from scratch on current Per-point:
Plugin rework: everything lives in Regression: |
The gateway forwards is_reconnect to adapter.connect() on the retry / reconnection path; the adapter (ported from the external plugin, which targets older cores) didn't accept it, so every reconnect attempt died with 'unexpected keyword argument'. Matches the signature of all other platform plugins. No behavioral difference for Carbon Voice: the disk cursor makes cold boot and reconnect identical. Found by exercising the real gateway boot on this branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…earch#32655 Drops this PR's own voice_out_carries_text implementation in base.py: the same contract already exists in PR NousResearch#32655, where it is further along in review and hardened per feedback (completeness guard so a truncated TTS rendition never suppresses the full text, plus reply_to threading for the voice memo and behavioral tests). The plugin keeps declaring voice_out_carries_text = True; until NousResearch#32655 lands the attribute is simply ignored (voice-out delivers audio plus a duplicate text bubble — degraded UX, never lost content). This PR is now strictly zero core changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Follow-up: dropped this PR's own |
Summary
Adds Carbon Voice as a bundled platform plugin under
plugins/platforms/carbonvoice/, following the same pattern as the other 21 platform plugins. Carbon Voice is a voice-first platform: it transcribes inbound voice to text (STT) before delivery and can re-synthesize the agent's replies as voice memos (TTS), so from Hermes' side it's a text-in / text-out platform. The adapter connects over Socket.IO (primary) with a REST polling fallback — no public webhook or tunnel required — and persists a disk cursor so messages received while offline are processed on next startup.Review feedback addressed
voice_out_carries_textwas read by the adapter but ignored by core (audio memo + duplicate text). The general delivery contract lives in fix(gateway): thread auto-TTS reply + support voice_out_carries_text adapters #32655 (fix(gateway): thread auto-TTS reply + support voice_out_carries_text adapters), which is further along in review and hardened per that review's feedback: a completeness guard (a truncated/stripped TTS rendition never suppresses the full text),reply_tothreading for the voice memo, and behavioral tests. This PR only declares the flag on the adapter; until fix(gateway): thread auto-TTS reply + support voice_out_carries_text adapters #32655 lands the attribute is ignored and voice-out delivers audio plus a duplicate text bubble — degraded UX, never lost content. Keeping the mechanism in one PR avoids two competing implementations of the same base.py block.env_enablement_fnseedsextra["voice_out"]fromCARBONVOICE_VOICE_OUT, and the chain env → seed →PlatformConfig.extra→ adapter flag is now covered by tests (tests/gateway/test_carbonvoice_plugin.py::TestVoiceOutWiring).connect()now acquires_acquire_platform_lock("carbonvoice-pat", …)before opening the API client (same pattern as the telegram/discord/slack/whatsapp adapters) anddisconnect()releases it. Tested (deny → bail out beforeapi.open(); release on disconnect).What's included
plugins/platforms/carbonvoice/— plugin subpackage (adapter, api, transport, state, dedupe, reactions, channels, audit, permits, parse, gate, conversations, constants) +plugin.yamlwithrequires_env/optional_envfor the setup wizard.register(ctx)provides:env_enablement_fn(auto-enable fromCARBONVOICE_PAT),standalone_sender_fn(out-of-process cron delivery),cron_deliver_env_var="CARBONVOICE_HOME_CHANNEL", auth env vars,platform_hint,setup_fn.website/docs/user-guide/messaging/carbonvoice.md+ messaging index entries.tests/gateway/test_carbonvoice_plugin.py(15 — plugin registration hooks, voice-out env wiring, credential lock).Dependencies
None added.
httpxis already core;python-socketiois optional — without it the adapter runs in polling-only mode (install_hintcovers it). Nopyproject.toml/uv.lockchanges.Access control
Deny-by-default: the bot answers only the owner (
whoami.created_by, auto-detected at connect) plusCARBONVOICE_ALLOWED_USERSand runtime-approved users. Unknown senders are dropped silently; the owner gets a rate-limited prompt and can approve with a one-tap reaction (💯 allow / 👎 block) or/cv-allow-user. Approvals reuse the existingPairingStore, so they authorize in core too.Testing
tests/gateway/test_carbonvoice_plugin.py(15) — all passing.tests/plugins/+ startup no-eager-install (1855 tests) and the voice/telegram/drop-recovery suites (212 tests) pass; ruff clean.