Skip to content

feat(desktop): auto-speak text responses via voice.auto_tts config - #46672

Open
mustafa-ramax wants to merge 1 commit into
NousResearch:mainfrom
mustafa-ramax:feat/auto-tts-on-text-responses
Open

feat(desktop): auto-speak text responses via voice.auto_tts config#46672
mustafa-ramax wants to merge 1 commit into
NousResearch:mainfrom
mustafa-ramax:feat/auto-tts-on-text-responses

Conversation

@mustafa-ramax

Copy link
Copy Markdown

Adds auto-TTS on response completion. Fixes double-play (WS client gate), session-switch trigger, and long-response timeout (scoped 75s speak timeout).

What does this PR do?

Adds auto-TTS to the Hermes desktop app — when voice.auto_tts: true is set in config.yaml, Hermes automatically reads its responses aloud once a turn completes, with no extra interaction needed.

This is a genuine quality-of-life feature: you can send a message, switch to another app or task, and hear Hermes respond in voice when it's done — hands-free, eyes-free. It's also a meaningful accessibility improvement for users with visual impairments or motor difficulties who benefit from audio output without having to manually trigger read-aloud each time.

Three bugs were diagnosed and fixed as part of this work:

Double-play — the backend gateway was also speaking responses server-side (CLI parity code), causing every reply to play twice. Fixed by gating server-side speech to non-WebSocket transports only (TUI/CLI), since the desktop client handles its own playback.
Session-switch trigger — switching sessions caused the newly-loaded message to auto-play. Fixed by resetting the busy-gate when sessionId changes (ChatBar is persistent and doesn't remount on switch).
Timeout on long responses — edge-tts synthesizes the entire response in one call; long replies exceeded the app-wide 15s HTTP timeout. Fixed with a scoped 75s timeout on the speak endpoint only, matching the backend's 60s synthesis cap.

Known limitation / future improvement: audio doesn't start until the full response is synthesized, causing a noticeable delay on long replies. The right fix is chunked/streaming TTS (sentence-by-sentence synthesis with lookahead buffering), which would bring audio start time down to ~1–2 seconds regardless of response length. That's left as a follow-up PR to keep this one focused.

Related Issue

Fixes #

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

tui_gateway/server.py — gate server-side speak_text to non-WS transports only; allow /voice tts toggle without requiring voice mode; fall back to config.voice.auto_tts in _voice_tts_enabled()
apps/desktop/src/types/hermes.ts — add auto_tts?: boolean to voice config type
apps/desktop/src/app/session/hooks/use-hermes-config.ts — expose autoTtsEnabled from config.voice?.auto_tts
apps/desktop/src/app/desktop-controller.tsx — thread autoTtsEnabled down to ChatView
apps/desktop/src/app/chat/index.tsx — thread autoTtsEnabled through to ChatBar
apps/desktop/src/app/chat/composer/types.ts — add autoTtsEnabled?: boolean to ChatBarProps
apps/desktop/src/app/chat/composer/index.tsx — auto-TTS effect with session-switch gate + double-play dedup; move VoicePlaybackActivity outside the scroll-fade div so the stop controller stays visible
apps/desktop/src/hermes.ts — scoped 75s timeout on speakText() (speak endpoint only)

How to Test

  1. Add voice:\n auto_tts: true to ~/.hermes/config.yaml and restart the desktop app
  2. Send any message and wait for the response to complete — audio should play automatically, once
  3. Send a longer message to verify no timeout error appears in logs
  4. Switch to a different session — confirm no audio plays on switch
  5. Send a new message in the switched-to session — confirm audio plays correctly
  6. Test the tui/cli and see if the voice still working there for you

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform:

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

For New Skills

  • This skill is broadly useful to most users (if bundled) — see Contributing Guide
  • SKILL.md follows the standard format (frontmatter, trigger conditions, steps, pitfalls)
  • No external dependencies that aren't already available (prefer stdlib, curl, existing Hermes tools)
  • I've tested the skill end-to-end: hermes --toolsets skills -q "Use the X skill to do Y"

Screenshots / Logs

Before: long responses triggered Error: Timed out connecting to Hermes backend after 15000ms and every response played twice through the host speakers.

After: single clean playback through the Electron client with a visible stop controller; long responses complete without timeout; session switching is silent.

Adds auto-TTS on response completion. Fixes double-play (WS client gate),
session-switch trigger, and long-response timeout (scoped 75s speak timeout).
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/tui Terminal UI (ui-tui/ + tui_gateway/) comp/gateway Gateway runner, session dispatch, delivery tool/tts Text-to-speech and transcription labels Jun 15, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Related: #44277 (auto_tts in voice conversation mode), #43845 (composer auto-TTS toggle). Competing desktop auto-TTS implementations with different mechanisms — this one wires backend auto-speak through tui_gateway/server.py. Not a duplicate.

@alt-glitch alt-glitch added comp/desktop Electron desktop app (apps/desktop/*) and removed comp/tui Terminal UI (ui-tui/ + tui_gateway/) labels Jun 26, 2026

@teknium1 teknium1 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.

Thanks for the accessibility-focused auto-speak work. The session-switch guarantee remains needed: current main subscribes the auto-speak controller to global $messages in apps/desktop/src/app/chat/composer/hooks/use-auto-speak-replies.ts:75, matching the stale-subscription race documented by linked issue #59014.

Problems

  • Main has since implemented auto-speak through useComposerVoiceuseAutoSpeakReplies (apps/desktop/src/app/chat/composer/hooks/use-composer-voice.ts:142, commits fcdc05c89 and cf05b3868). The proposed ChatBar busy-edge effect would create a second controller rather than extend the current one.
  • No regression test accompanies this branch, and current main has no tracked test for use-auto-speak-replies.

Suggested changes

  • Salvage the session-switch guard into apps/desktop/src/app/chat/composer/hooks/use-auto-speak-replies.ts and test the synchronous $messages update-before-effect-cleanup sequence.
  • Port the scoped speak timeout independently at apps/desktop/src/hermes.ts:1007.

This is an automated hermes-sweeper review.

@@ -1528,6 +1530,38 @@ export function ChatBar({
}

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.

Current main has a single auto-speech controller in hooks/use-auto-speak-replies.ts, composed through useComposerVoice. Please port the session-switch guard into that controller rather than adding a second busy-edge effect here; otherwise the current playback/deduplication flow would be duplicated.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/desktop Electron desktop app (apps/desktop/*) comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state tool/tts Text-to-speech and transcription type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants