Commit graph

18812 commits

Author SHA1 Message Date
Teknium
e04c2a9ebd test: update voice-submission test for _VoiceInputMessage sentinel (#65827) 2026-07-28 11:57:37 -07:00
Teknium
e6b0344052 test: adapt salvaged voice tests to current main + lint fix
- _FakeProc gains returncode (main's player loop checks proc.returncode)
- WSL gate tests clear SSH_* env vars (main hard-warns over SSH without
  forwarded audio) and accept the merged #37346 forwarded-sound-server
  notice wording
- test_tts_macos_output stubs resolve_streaming_provider so the
  OutputStream setup path actually runs on main's chunked-streamer code
- voice CLI integration tests unwrap the _VoiceInputMessage sentinel
- _is_wsl: explicit encoding + drop unreachable return (ruff PLW1514)
2026-07-28 11:57:37 -07:00
Teknium
fe3dc29009 fix(voice): honor quoted beep_enabled strings + correct PortAudio OSError hint
Two in-house micro-fixes from issue triage:

- #49883: voice.beep_enabled gates in cli.py and hermes_cli/voice.py used
  bool() on the config value, so a quoted YAML string like "false" or
  "off" kept beeps on. Route through utils.is_truthy_value.
- #18432: AudioRecorder.start() collapsed OSError from _import_audio into
  the 'pip install sounddevice numpy' hint — but OSError means the
  PortAudio SHARED LIBRARY is missing, which pip cannot fix. Mirror
  detect_audio_environment's system-package hint (libportaudio2 /
  brew portaudio / Termux pkg install portaudio) on that path.

Fixes #49883
Fixes #18432
2026-07-28 11:57:37 -07:00
webtecnica
0062107094 fix(cli): only prefix voice-transcribed messages with the voice-input instruction (#65827)
Typed messages sent while voice mode was active were also getting the
'[Voice input — respond concisely...]' API-local prefix, because the gate
checked only self._voice_mode. Route STT transcripts through a
_VoiceInputMessage sentinel in _pending_input (both the PTT/continuous
transcription path and the barge-in utterance path), unwrap it in
process_loop, and thread voice_input= through chat() so the prefix applies
only to genuinely voice-transcribed messages.

Re-cut of PR #65961 (@webtecnica) — the original diff had the sentinel
class embedded inside __init__'s docstring. Credit also to the earliest
route-by-origin attempt in PR #11744 (@KeroZelvin).

Fixes #65827
Closes #65961
Closes #11744
2026-07-28 11:57:37 -07:00
Dean Chen
3524b20728 fix(cli): surface local STT model preparation 2026-07-28 11:57:37 -07:00
liuhao1024
1818d63052 fix(docs): target installer venv in voice extra install command
The quickstart voice-mode section used `uv pip install -e ".[voice]"`
which fails on a fresh curl-installed setup because no virtual environment
is active. Use `--python ./venv/bin/python` to target the installer-created
venv explicitly, matching the curl installer layout.

Fixes #44364
2026-07-28 11:57:37 -07:00
RedClaus
1605cb5fe4 fix(voice): point pip install hint at venv pip instead of system Python
When Hermes runs inside its bundled venv (~/.hermes/profiles/<name>/
hermes-agent/venv/), the bare 'pip install sounddevice numpy' hint in
_voice_capture_install_hint() told users to install into whichever
Python their shell resolves first — on macOS that is often the system
Python under Rosetta, a completely separate site-packages tree with
incompatible wheel arches.

Detect venv via sys.prefix != sys.base_prefix and return the full path
to the venv's pip binary (Path(sys.prefix)/bin/pip) when available.
Falls back to the bare hint outside a venv (e.g. bare-metal installs).

Also adds 'from pathlib import Path' which was the only new import.
2026-07-28 11:57:37 -07:00
simonmmafs
a1bc12f191 voice: one macOS output policy across WAV, beeps, streaming TTS + tests
Addresses review on #62601. Applies a single rule — no sounddevice for
audio OUTPUT on macOS (PortAudio/CoreAudio init triggers a
kTCCServiceMediaLibrary prompt) — consistently at all three output sites:

- play_audio_file: WAV playback (already routed to afplay) now uses the
  shared _sounddevice_output_allowed() helper.
- play_beep: synthesize the tone with numpy only, then on macOS play it
  via a temp WAV through afplay instead of sounddevice.
- stream_tts_to_speaker (tts_tool): on macOS, skip the sounddevice
  OutputStream so playback falls through to the existing tempfile/afplay path.

Audio INPUT (recording) is untouched — it legitimately needs mic permission.

Tests: TestMacOSAudioOutputPolicy (voice_mode) proves WAV + beep routing
does not import sounddevice on Darwin and still uses it off Darwin;
test_tts_macos_output proves streaming TTS skips the OutputStream on Darwin.
Existing test_play_wav_via_sounddevice pinned to non-Darwin for determinism.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 11:57:37 -07:00
Simon
0179ff9738 fix(voice): skip sounddevice on macOS to avoid TCC media-library prompt
On macOS, initializing PortAudio/CoreAudio via sounddevice triggers a
kTCCServiceMediaLibrary permission dialog even when no media-library
access is needed. afplay already handles WAV (and every other format)
natively, so route macOS playback straight to it and keep sounddevice
for other platforms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 11:57:37 -07:00
xxxigm
0a5c5519fc test(voice-mode): pin Termux:API detection probe ladder + #31015 fallback
Two test classes lock in the new detection contract:

1. TestTermuxApiAppInstalledProbeLadder — drives _termux_api_app_installed
   with a fake subprocess.run dispatcher and walks each rung of the
   ladder:
     - non-Termux env returns False (no probes run),
     - pm-confirms returns True (back-compat),
     - pm-clean-miss → cmd-confirms returns True,
     - pm-FileNotFoundError → cmd-confirms returns True,
     - pm-TimeoutExpired → cmd-confirms returns True,
     - pm-nonzero-exit → cmd-confirms returns True,
     - both probes inconclusive + binary on PATH returns True (the
       core #31015 case),
     - both probes inconclusive + no binary returns False,
     - both probes clean-miss returns False (the genuine "CLI without
       app" case keeps the existing warning),
     - case-insensitive package match for ROMs that capitalise differently.

2. TestDetectAudioEnvironmentTermuxFallback — end-to-end through
   detect_audio_environment, asserting the misleading "Termux:API
   Android app is not installed" warning no longer fires when probes
   are inconclusive but the binary is on PATH (the user-reported #31015
   symptom), AND that the warning still fires when probes can
   conclusively report the app is missing.

Refs: NousResearch/hermes-agent#31015
2026-07-28 11:57:37 -07:00
xxxigm
e3f4dc2103 fix(voice-mode): make Termux:API detection robust against pm probe failures
\`pm list packages com.termux.api\` is the canonical way to detect the
Termux:API Android app, but on some devices it gives a false negative
even when the app is installed and \`termux-microphone-record\` runs
fine — the symptom reported in #31015 (\`/voice on\` complaining
"Termux:API Android app is not installed").

Replace the single probe with a graded strategy:

1. Try \`pm list packages com.termux.api\` (current behaviour).
2. If \`pm\` isn't on PATH or returns non-zero, fall back to
   \`cmd package list packages com.termux.api\` — the modern Android
   API 28+ equivalent that's present on devices where \`pm\` is gone.
3. If both probes are inconclusive (binary missing, permission
   denied, timeout, or non-zero exit) and \`termux-microphone-record\`
   is on PATH, trust the binary.  The CLI ships in the \`termux-api\`
   package which is essentially only useful with the Android app
   installed; users who installed it deliberately almost always have
   the app too.

Polarity matters: a false negative on this gate blocks \`/voice on\`
entirely (the user-reported symptom), while a false positive only
surfaces a precise runtime error from the binary itself when it
tries to talk to the missing app — strictly more actionable.

The clean-probe-but-no-package case still returns False, so the
existing "Termux:API CLI installed without the app" warning still
fires when the package manager *can* tell us the app is missing.

Refs: NousResearch/hermes-agent#31015
2026-07-28 11:57:37 -07:00
ygd58
0560f52047 fix(voice): add WSL2 PowerShell audio fallback for TTS playback
Ports #63768 forward onto current main per teknium1's review.

On WSL2 without a PulseAudio bridge, ffplay and aplay have no audio
device and TTS playback silently fails (issue #17608). When
powershell.exe and ffmpeg are available, convert the audio to a
uniquely-named WAV in the Windows %TEMP% directory and play it via
Media.SoundPlayer.

Per review, this fixes two gaps in the original port:

1. Exit-status masking: the cleanup subshell was
   '( ffmpeg && powershell ); rm -f wav' -- the shell's exit status is
   the LAST command's (rm -f, which is always 0), so a real
   ffmpeg/PowerShell failure could never be detected by the rc-checking
   fallback logic added to the player loop. Now captures the real
   status before cleanup and re-exits with it:
   '( ffmpeg && powershell ); rc=0; rm -f wav; exit '.

2. The no-Pulse WSL gate in detect_audio_environment() still hard-blocked
   voice mode entirely (input AND output) even when the PowerShell
   fallback made TTS output viable. Added _wsl_powershell_tts_available()
   and use it to downgrade the WSL-without-Pulse case from a hard
   'warnings' block to a non-blocking 'notices' entry when the fallback
   is available -- the same PulseAudio-bridge recording guidance is still
   surfaced (mic capture genuinely still needs it), it just no longer
   blocks /voice on for TTS-only usage. cli.py's existing
   env_check['available'] gate needed no changes since it already
   respects this flag.

Also fixed the flaky uniqueness test (the original asserted
len(filenames) >= 2, which passed trivially on zero captured
filenames) and added a real fallback-triggering regression test for
the exit-status fix.

10 new/fixed tests pass in TestWSL2PowerShellFallback and the new
TestWSLAudioEnvironmentGate; 80/80 in the full tests/tools/test_voice_mode.py file.
2026-07-28 11:57:37 -07:00
Ivan Kharitonov
ec7a46a6fe fix(voice): add WSL audio warmup to eliminate RDP crackling
WSLg RDP audio has two issues causing crackling:
1. systemd-timesyncd clock adjustments jitter PulseAudio timing
   (microsoft/wslg#1257) — user action: stop the service
2. Cold-start RDP connection drops first ~100ms of audio packets
   before the virtual channel stabilises

Fix (automated, WSL-only):
- Detect WSL via /proc/version 'microsoft' marker
- Prepend 100ms silence + apply 100ms fade-in to audio
- Append 50ms silence tail for clean stream teardown
- Set blocksize=4096 (default auto ~1024 is too small for RDP)
- All in a single continuous sd.play() buffer

Non-WSL paths unchanged.

Closes #38893
2026-07-28 11:57:37 -07:00
Slippy87
ef686c3878 fix(voice): honor forwarded audio (PIPEWIRE_REMOTE) in WSL detection 2026-07-28 11:57:37 -07:00
Brice
2a75664c0c fix(voice): capture at the input device's native sample rate
AudioRecorder hard-codes SAMPLE_RATE (16 kHz) when opening the input
stream, but some capture devices (e.g. USB microphones exposed through
ALSA hw) reject 16 kHz outright — sd.InputStream fails with
PaErrorCode -9997 (Invalid sample rate) and voice recording is broken.

Query the default input device for its native default_samplerate at
recording start and open the stream / write the WAV at that rate,
falling back to the Whisper-friendly 16 kHz constant when the backend
does not expose a usable rate. STT providers accept standard WAV rates,
so downstream transcription is unaffected.
2026-07-28 11:57:37 -07:00
Kewe63
f98952b267 feat(voice): make beep notification volume configurable from config.yaml
Closes #55908. The CLI voice-mode beep amplitude is hardcoded at 0.3 inside
tools.voice_mode:play_beep(), which makes the record start/stop cues too
quiet on low-volume systems and headphones. Users couldn't adjust it
without editing source.

Move the literal into a configurable voice.beep_volume setting (clamped to
0.0-1.0, default 0.3 to preserve prior behaviour). The new
_get_beep_volume() helper reads via the same load_config() pattern used by
cli.py's _voice_beeps_enabled() and hermes_cli/voice.py's _beeps_enabled(),
keeps bools / out-of-range / non-numeric / NaN values safely on the default,
and falls back silently if config can't load so the audio cue never breaks
the voice loop on a degenerate config.yaml.

Covered by tests/tools/test_voice_mode.py:
- TestGetBeepVolume (12 cases: missing key, custom value, boundary 0.0/1.0,
  out-of-range clamp, type coercion, bool guard, NaN guard, exception
  guard, dict-typed voice section)
- TestPlayBeepVolumeWiring (guards against re-introducing a hardcoded 0.3
  literal in play_beep)

Docs: website/docs/user-guide/configuration.md mentions the new key.
Other locale translations (zh-Hans etc.) intentionally untouched —
handled by the regular i18n sync pipeline as a separate change.

No change in default behaviour: existing users hear exactly the same beep.
2026-07-28 11:57:37 -07:00
YAMAGUCHI Seiji
89d5785692 fix(voice): prefer requested MP3 for playback 2026-07-28 11:57:37 -07:00
YAMAGUCHI Seiji
1182efa0a8 fix: play returned TTS audio path in CLI voice mode 2026-07-28 11:57:37 -07:00
Solitud1nem
af7205bea5 fix(voice): thread max_recording_seconds through the TUI path, add behavior coverage
Review follow-up: the sweeper is right that the first commit only cured
half the dead config — the TUI gateway builds its recorder params
explicitly, so the cap never reached recordings started from the TUI.

- start_continuous() grows a max_recording_seconds param (default 0.0 =
  disabled, so existing callers keep today's behaviour) and applies it to
  the shared recorder next to the silence params
- tui_gateway voice.record start forwards the validated cap; corruption
  semantics now mirror the silence params everywhere: non-numeric/bool
  falls back to the documented 120 default (a hand-edited
  `max_recording_seconds: true` must not become a 1-second cap), while
  an explicit numeric <= 0 disables the cap
- cli.py wiring updated to the same corruption semantics

New coverage, per review:
- TestMaxRecordingCap drives the mocked InputStream callback past the
  cap during continuous loud speech (the silence branch physically can't
  fire there) and asserts the one-shot callback fires exactly once; plus
  the disabled-cap negative
- TestMaxRecordingSecondsConfigReal pins the CLI config assignment for
  the valid / zero / bool / garbage cases via _voice_start_recording
- test_voice_record_start_forwards_max_recording_seconds pins the TUI
  forwarding for the same matrix
2026-07-28 11:57:37 -07:00
Solitud1nem
fd213a82f4 fix(voice): enforce voice.max_recording_seconds (was dead config) 2026-07-28 11:57:37 -07:00
brunopirz
ee5f20bdf2 fix(voice): prevent restart race when continuous mode stops after 3 no-speech cycles
When voice continuous mode detects 3 consecutive no-speech cycles it sets
_voice_continuous = False and previously did an immediate 
eturn. The
restart guard if self._voice_continuous and not submitted and not
self._voice_recording came *after* the early return, so the thread could
never restart. However a timing window existed: if _voice_start_recording was
already queued from a prior iteration, the 
eturn bypassed the guard while
the thread was already in flight.

Replace the bare 
eturn with a stop_continuous_restart boolean evaluated
in the same if that guards _restart_recording. This ensures both branches
read a consistent no-restart signal and no recording thread is spawned after
the user's session has been intentionally halted.
2026-07-28 11:57:37 -07:00
kyssta-exe 25470058+kyssta-exe@users.noreply.github.com
268b98253d fix(cli): disarm continuous voice mode via hotkey during agent/transcribe (#67545)
In continuous voice mode, pressing the record hotkey (Ctrl+B) was a
silent no-op while the agent was running or voice was being transcribed.
The hotkey only cleared _voice_continuous when _voice_recording was True,
leaving the user trapped in an auto-restart loop that only /voice off
could break.

Fix: when the agent is running or transcribing, still allow the hotkey
to clear _voice_continuous so the loop stops after the current turn.
2026-07-28 11:57:37 -07:00
Teknium
ae697db19c chore: map salvage contributors (voice CLI/TUI UX batch) 2026-07-28 11:57:37 -07:00
Teknium
ea80b557ae fix: route busy-steer voice through the shared out-of-band STT choke point
Follow-up for salvaged #65023/#53020: _prepare_busy_steer_text now calls
_transcribe_and_echo_pending_voice (the same helper the interrupt monitor
and pending-drain paths use) instead of a private transcription+echo copy,
so out-of-band voice pays one STT call per platform message and the echo
respects the count-based ledger from #67281. can_steer now accepts events
whose attachments are all STT-eligible voice media, completing the steer
half of #58780. Adds extract_media gating tests for #44826 and the
contributor mapping for chefboyrdave21.
2026-07-28 11:57:11 -07:00
Frowtek
753d0d77e3 fix(gateway): keep the STT echo ledger across pending-media merges
_invalidate_pending_stt_cache() clears the gateway-side transcription
cache when merge_pending_message_event() folds a follow-up message into a
still-pending event, so the next transcription picks up the merged text
and attachments.  It also cleared _gateway_pending_stt_echo_sent, but that
flag is not derived state — it records that the transcript was already
delivered to the user.

Dropping it makes the re-run transcription echo the earlier notes a second
time.  Both merge branches are affected, including the text-only follow-up
case where no new audio arrived at all: there the cache is invalidated,
the same voice note is transcribed again (a second paid STT call) and the
same line is echoed again.

Sequence:

  1. voice note arrives, interrupt monitor transcribes it and echoes
     '🎙️ "hello"'
  2. user sends a follow-up while the turn is still pending, so it merges
  3. drain path re-transcribes and echoes '🎙️ "hello"' a second time

Keep the ledger out of the invalidation set and track it as a count of
already-echoed transcripts instead of a single boolean.  A count is what
the merge case actually needs: re-running transcription over the extended
media list returns the earlier transcripts as a prefix of the new one, so
echoing only the unsent tail suppresses the repeat while still surfacing a
newly merged voice note.  A count rather than a set of seen values, so two
separate notes that transcribe identically stay two distinct deliveries —
covered by test_pending_stt_merge_echoes_two_identical_transcripts.

The guard stays within the 12-line window that
test_all_gateway_transcript_echo_sends_are_gated enforces over run.py.
2026-07-28 11:57:11 -07:00
Lumina
6710ce97c4 fix(gateway): gate [[audio_as_voice]] to audio files so images aren't sent as documents
[[audio_as_voice]] is message-global but was applied to every media file in a
message. A non-audio file flagged is_voice is excluded from the embedded-photo
batch and falls through to send_document, so an image in a message that also
carries a voice note arrives as a file attachment instead of an inline photo.
Gate the voice flag on the file extension so one message can carry an inline
image AND a voice bubble. Also wrap the path append in try/except so a crafted
~\x00 path is skipped rather than aborting extraction of all attachments.
2026-07-28 11:57:11 -07:00
canorionen
0fd0161dfd fix(gateway): steer busy voice follow-ups after STT 2026-07-28 11:57:11 -07:00
izumi0uu
aa40f16d3e fix(gateway): transcribe clarify voice replies 2026-07-28 11:57:11 -07:00
Teknium
b8c38a451a chore: add contributor email mappings for voice salvage PR 2026-07-28 11:56:37 -07:00
Teknium
f76b2b47aa fix(gateway): pass channel_prompt into voice-channel STT events; guard empty transcripts
Two hand-written fixes in the voice input path:

- _handle_voice_channel_input now resolves the bound text channel's
  channel_prompt via the adapter's _resolve_channel_prompt so voice input
  gets the same per-channel context as typed messages (fixes #50149).
- _enrich_message_with_transcription now guards success=True results whose
  transcript is empty/whitespace-only (silence, cut-off, inaudible audio):
  instead of emitting empty quotes the agent gets a clear sentinel note.
  Reimplemented against the current plain-quoted note wording; original
  concept and tests by @deacon-botdoctor in PR #41603 (fixes #41603).
2026-07-28 11:56:37 -07:00
Teknium
a4c9994837 refactor(discord): delegate ffmpeg discovery to shared tools.transcription_tools helper
Keep one owner for PATH/local-prefix ffmpeg discovery: ffmpeg_utils now
delegates to tools.transcription_tools._find_ffmpeg_binary and only adds
the Discord-specific FFMPEG_PATH override and Windows winget fallback on
top (follow-up to PR #60627 by @LauraGPT, fixes #60624).
2026-07-28 11:56:37 -07:00
LauraGPT
9b89da23fb Fix Discord ffmpeg discovery on Windows 2026-07-28 11:56:37 -07:00
Jeffrey Cox
ae8d3e2027 fix(discord): make voice timeouts configurable 2026-07-28 11:56:37 -07:00
Ariel Tov Ben
388f612435 fix(discord): drain pending voice input before disconnect 2026-07-28 11:56:37 -07:00
luyifan
a5b32a721a fix(discord): preserve voice reply threading 2026-07-28 11:56:37 -07:00
PRATHAMESH75
297f5142a6 fix(discord): prepend warm-up silence so TTS first word isn't clipped
Discord's voice socket needs a brief warm-up before receiving clients
actually hear audio; the first ~100-200ms is lost, clipping the first
word/syllable of TTS playback. Prepend a configurable lead of silence to
speech on both playback paths:

- Mixer path: new _lead_silence_bytes() helper prepends PCM silence
  (BYTES_PER_MS constant added to voice_mixer.py) before play_speech, on
  both the reply and the pre-tool ack.
- Legacy FFmpegPCMAudio path: apply -af adelay=<ms>:all=1.

Tunable via discord.voice_fx.lead_silence_ms (default 200, 0 disables).

Fixes #66827
2026-07-28 11:56:37 -07:00
Carlos Diosdado
d22a1ee5be fix(tests): add _FakeAudioSource to discord mock for VoiceMixer inheritance 2026-07-28 11:56:37 -07:00
Carlos Diosdado
eef1ab72d5 fix(discord): inherit AudioSource in VoiceMixer for vc.play() compatibility
VoiceMixer duck-typed the discord.AudioSource interface (is_opus, read,
cleanup) but never inherited from it. discord.py's vc.play() does an
isinstance check and rejects non-AudioSource objects, causing the voice
fx mixer to silently fail with:

  "Voice mixer failed to start: source must be an AudioSource not
   VoiceMixer"

Added missing `import discord` and changed the class definition from
`class VoiceMixer:` to `class VoiceMixer(discord.AudioSource):`.
2026-07-28 11:56:37 -07:00
isheng-eqi
31301c1af7 fix(discord): wire voice input callback at adapter connect time
- Wire adapter._voice_input_callback at connect and reconnect so voice
  transcription is forwarded without requiring /voice join (#60623).
- Add optional text_channel_id and source params to DiscordAdapter
  .join_voice_channel() so automatic/programmatic voice joins can
  establish the text-channel binding needed by _handle_voice_channel_input.
- Add TestVoiceInputCallbackWiring: asserts callback wiring on startup
  and reconnect for Discord adapters with voice attributes.
2026-07-28 11:56:37 -07:00
isheng
79da6adfe9 fix: add _handle_voice_channel_input mock to _make_runner in reconnect tests
PR #61407 accesses self._handle_voice_channel_input in _platform_reconnect_watcher. The test mock runner created via _make_runner() must have this attribute.
2026-07-28 11:56:37 -07:00
isheng
1b0836c7c7 fix(discord): wire voice input callback at adapter connect time
_voice_input_callback was only set in _handle_voice_channel_join, not at adapter connect or reconnect. Voice transcription was logged but never forwarded as an inbound message without explicit /voice join.

Wire the callback at both connect and reconnect paths.

Fixes #60623
2026-07-28 11:56:37 -07:00
Teknium
f440a44753 chore: mappings for bare-noreply contributor emails 2026-07-28 11:55:48 -07:00
Teknium
4d9dcf152a chore: contributor email mappings for salvaged voice-delivery commits 2026-07-28 11:55:48 -07:00
Teknium
2008d80a9e test(gateway): regression coverage for platform-aware voice delivery
- tests/gateway/test_base_auto_tts_output_format.py (new): the base
  adapter auto-TTS block passes an explicit .ogg output path on every
  OPUS_VOICE_PLATFORMS member (parametrized from the tts_tool set — the
  single source of truth), keeps .mp3 on non-opus platforms, honors the
  tool's success flag, and stays unique/uuid-based.
- tests/gateway/test_auto_voice_reply_format.py: runner _send_voice_reply
  parametrized across Matrix/Feishu/WhatsApp/Signal (.ogg) alongside the
  existing Telegram/Slack cases; streamed+global-auto-TTS gate regression.
- tests/gateway/test_telegram_voice_caption_markdown.py (new): caption
  MarkdownV2 formatting, entity-rejection plain fallback, overflow skip,
  and no-caption passthrough (#32029).
- test_voice_command.py filename-uuid contract test updated to point at
  build_auto_tts_output_path (construction moved there).
2026-07-28 11:55:48 -07:00
Teknium
ee15e04803 fix(telegram): render markdown in voice-message captions
Closes the #32029 gap: TelegramAdapter.send_voice passed captions raw with
no parse_mode, so auto-TTS captions (which carry the agent's markdown
reply) showed literal *asterisks*, backticks and [links](...). Captions
are now formatted to MarkdownV2 via the adapter's format_message when the
formatted text fits Telegram's 1024-char caption cap, with fallback to the
plain truncated caption when formatting overflows or the Bot API rejects
the entities (mirrors the text-send markdown fallback ladder).

Fixes #32029
2026-07-28 11:55:48 -07:00
Gilad Bauman
1753369f7c fix(gateway): platform-aware auto-TTS output path for native voice bubbles
Salvaged from PR #62040 (@giladbau), simplified per post-#73072 main: the
central _repair_ogg_container transcode makes an explicit .ogg output path
sufficient — no target_platform plumbing through the TTS tool needed.

Root cause (class-level): both gateway auto-TTS delivery call sites relied
on the TTS tool reading HERMES_SESSION_PLATFORM to pick Ogg/Opus vs MP3,
but that contextvar is cleared by _clear_session_env before the base
adapter's post-handler auto-TTS block runs, so want_opus was always False
on that path → MP3 → Telegram sent an audio attachment instead of a native
voice bubble (#57049, #36685). The runner's _send_voice_reply had the
sibling bug: it hardcoded .ogg for Telegram only, leaving Matrix and
Feishu runner voice replies as MP3 (#14841, #45557).

Fix: new build_auto_tts_output_path(platform) in gateway/platforms/base.py
hands an explicit .ogg temp path when the platform is in the TTS tool's
OPUS_VOICE_PLATFORMS set (single source of truth — telegram/matrix/feishu/
whatsapp/signal), .mp3 otherwise. Used by BOTH delivery call sites:
- BasePlatformAdapter auto-TTS block (also honors the tool's success flag
  and cleans up requested + returned paths)
- GatewayRunner._send_voice_reply (replaces the telegram-only ternary)

Fixes #57049
Fixes #36685
Refs #14841 #45557
2026-07-28 11:55:48 -07:00
55nx954gn6-debug
28adb86891 fix(gateway): honor global voice.auto_tts in runner voice-reply gate
Salvaged from PR #51196 (@55nx954gn6-debug). _should_send_voice_reply only
consulted the runner's _voice_mode dict (/voice on|voice_only|all), so the
global voice.auto_tts config default — which is synced into each adapter's
_auto_tts_default on gateway connect — was invisible to the runner path.
Net effect: with streaming enabled and only global auto-TTS configured
(no per-chat /voice opt-in), the streamed reply consumed the text, the
base adapter's auto-TTS got text_content=None, and no voice reply was
ever sent (#51867/#23983 remainder).

The runner now also asks the adapter's _should_auto_tts_for_chat(chat_id)
(which encodes per-chat /voice on|off overrides over the global default);
an explicit /voice off chat mode remains a hard override.

Refs #51867 #23983 #51282 #13126
2026-07-28 11:55:48 -07:00
Hu
ae53b4ba5a fix(feishu): pass audio duration + thread routing fallback for voice bubbles
Salvaged from PR #53157 (@LLQWQ). Three changes for native Feishu voice
bubble delivery:

1. Include audio duration in the file-upload body when uploading opus
   files — Feishu renders 0:00 bubbles without it. Duration is extracted
   by parsing the OGG container's last granule position (pure Python,
   no ffprobe dependency).
2. Thread routing fallback: Feishu's create-message API rejects
   msg_type='audio' with receive_id_type='thread_id' (error 99992402);
   retry via the reply API against the thread's last message, then fall
   back to chat_id routing.
3. (dropped) tools/tts_tool.py want_opus hunk — superseded by main's
   OPUS_VOICE_PLATFORMS, which already includes feishu. The PR's stray
   scripts/release.py hunk was also dropped (frozen AUTHOR_MAP policy;
   mapping added under contributors/emails/ instead).

Fixes #45557
Refs #18831 #16524
2026-07-28 11:55:48 -07:00
shellybotmoyer
88f7fe3b1f fix(tts): improve Opus encoding quality for voice messages
Salvaged from PR #18861 (@shellybotmoyer). Switch the central Opus
transcoders from CBR 64k (-vbr off) to VBR 48k with -application voip and
-compression_level 10. The old CBR settings produced lower-quality speech
audio and occasionally failed to trigger Telegram's native voice-bubble
rendering. Applied to _ffmpeg_transcode_to_opus (the single transcoder
behind _convert_to_opus and _repair_ogg_container), the Gemini WAV→OGG
path, and the Matrix adapter boundary transcoder added in this branch.

Fixes #18818
2026-07-28 11:55:48 -07:00
Michel Belleau
33313e88f8 fix(matrix): enforce Ogg/Opus at send_voice boundary, probe metadata off-loop
Salvaged from PR #68063 (@malaiwah). MatrixAdapter.send_voice now transcodes
any non-Ogg audio to Ogg/Opus at the adapter boundary (best-effort — the
original file is sent unchanged when ffmpeg is unavailable), so MSC3245
voice bubbles render even when a caller hands the adapter MP3/WAV audio.
_matrix_voice_metadata_for_file probing now runs via asyncio.to_thread so
ffprobe/ffmpeg subprocess timeouts can't stall the adapter event loop.

The PR's tools/tts_tool.py want_opus hunk was dropped: main's
OPUS_VOICE_PLATFORMS set (PR #73072) already includes matrix; the Matrix
opus-routing test is kept.

Refs #14841
2026-07-28 11:55:48 -07:00