Commit graph

2452 commits

Author SHA1 Message Date
Carlos Diosdado
d1e9344716 feat(xai-tts): wire text_normalization parameter
xAI TTS supports a text_normalization boolean that normalizes
written-form text (numbers, abbreviations, symbols) into spoken-form
before synthesis. This parameter was not being sent, leaving users
with literal number/symbol pronunciation.

- Add DEFAULT_XAI_TEXT_NORMALIZATION_DEFAULT = False
- Read tts.xai.text_normalization from config (via _xai_bool_config)
- Attach text_normalization: true to the POST payload when enabled
- Omit the field entirely when unset or explicitly false (API default)
- Add 3 tests: default omission, enabled passthrough, explicit false
2026-07-28 11:55:01 -07:00
Carlos Diosdado
462b3cf994 feat(tts): add optional provider parameter to text_to_speech tool
Adds an optional provider parameter to the text_to_speech tool that lets the model select a TTS provider per-call instead of always using the globally configured tts.provider.

When provider is set, it bypasses the configured default and routes directly to the specified backend.

When omitted (the default), the tool behaves exactly as before.

Closes #47459
2026-07-28 11:55:01 -07:00
Alcibiades Athens
1daa76951b feat(tools): forward OpenAI TTS instructions field through text_to_speech
The `text_to_speech` tool schema accepted only `text` and `output_path`,
so style direction (tone, emotion, pacing, whispering) could never reach
the OpenAI backend — even though `gpt-4o-mini-tts` (Hermes's OpenAI
provider default) treats `instructions` as its primary voice-design
control.

This plumbs an optional `instructions` argument through the tool schema,
the handler lambda, and `text_to_speech_tool()` into
`_generate_openai_tts`, where it is forwarded to
`client.audio.speech.create()` only when truthy. Empty/None values still
omit the key entirely, preserving behavior on `tts-1`/`tts-1-hd` and
strict OpenAI-compatible servers.

The same passthrough unblocks self-hosted OpenAI-compatible voice-design
servers (Qwen3-TTS-VoiceDesign on oMLX, etc.) that are already wired in
via `tts.openai.base_url` — the established convention per #9004 and the
TTS config docs — without inventing a new provider backend.

Tests: `tests/tools/test_tts_instructions.py` covers backend passthrough,
tool-level threading, schema declaration, and the empty-string/absent
omission cases. `tests/tools/test_tts_max_text_length.py` fake_openai
signature widened to accept the new kwarg.

Refs NousResearch/hermes-agent#14196
2026-07-28 11:55:01 -07:00
Hu
8171e8ebb3 feat(tts): expose speed parameter in text_to_speech tool
Add optional 'speed' parameter (0.25-4.0) to the text_to_speech tool
schema and handler. When provided by the model, it overrides the
config-level tts.speed setting, enabling per-request speed control
without config changes.

Use cases:
- Language learning: slow playback (0.5x) for pronunciation practice
- Accessibility: adjustable speed for hearing preferences
- Content review: accelerated playback for long text

The speed value is clamped to [0.25, 4.0] and injected into tts_config
before dispatching to any provider (Edge, OpenAI, MiniMax, etc.), so
all existing provider-level speed handling works transparently.

Includes 3 new tests for tool-level speed injection, clamping, and
config preservation when speed is not specified.
2026-07-28 11:55:01 -07:00
Johann
d9336e7453 fix(tts): strip <think> reasoning blocks from TTS text
When /reasoning show is enabled, the model's <think> blocks appear in
the final assistant message. TTS reads these aloud, which is unwanted —
users want to see reasoning but not hear it spoken.

- Add _THINK_BLOCK regex to _strip_markdown_for_tts() (tools/tts_tool.py)
  — the general TTS text-preparation path used by all TTS providers
- Add <think> stripping to prepare_tts_text() (gateway/platforms/base.py)
  — the gateway auto-TTS path
- Reuses the existing regex pattern from stream_tts_to_speaker()

Closes #34213
2026-07-28 11:55:01 -07:00
Alexander Russell
a9a9005f31 feat(tts): normalize spoken text (units, symbols, markdown) before synthesis
Auto-TTS previously fed raw chat Markdown and compact symbols straight to the
speech provider, so units were read as stray letters and headings or bullets ran
together. This routes spoken text through a new normalizer,
tools/tts_text_normalize.prepare_spoken_text, that expands units (for example a
temperature written with the degree symbol becomes "degrees Celsius") and
flattens Markdown into a transcript-like script with sentence pauses.

The normalizer is best-effort: if it ever fails the code falls back to the
previous markdown-strip behavior, so auto-TTS keeps working. The Telegram voice
caption uses the same normalized text. Includes a unit test.
2026-07-28 11:55:01 -07:00
Tikkanaditya Siddartha Jyothi
90b68520ac fix(tts): bound the Piper/KittenTTS model caches with a small LRU
Salvaged from PR #62977 (@Vissirexa) — TTS model-cache half only (the
hindsight turn-buffer half is a different subsystem and was dropped).

_piper_voice_cache and _kittentts_model_cache were keyed by voice/model
with no eviction, and each entry is a whole loaded model (tens of MB).
A surface that sweeps voices pinned one model per voice for the process
lifetime. New _tts_cache_get_or_load() get-or-loads through a small LRU
(_TTS_MODEL_CACHE_MAX=3), refreshing recency on a hit and evicting the
least-recently-used model on a cold miss.
2026-07-28 11:54:26 -07:00
luyifan
60b841bbfb fix(tts): bound upstream response bodies 2026-07-28 11:54:26 -07:00
Teknium
43b8eb84b3 fix(tts): base_url parity audit — Mistral server_url + provider config tests
Class-level sweep following the ElevenLabs salvage (#66311): every cloud
TTS provider section now honors tts.<provider>.base_url. xAI, MiniMax,
Gemini, OpenAI and DeepInfra already did; Mistral (SDK server_url) was
the remaining gap. Adds per-provider config tests locking in the
ElevenLabs environment plumbing and the Mistral server_url passthrough.
2026-07-28 11:54:26 -07:00
Moeadham
e5fc806ccc fix(tts): support configurable ElevenLabs URLs
Salvaged from PR #66311 (@moeadham), rebased onto the current streaming
registry. tts.elevenlabs.base_url (+ optional wss_url, derived from
base_url when omitted) routes both the sync ElevenLabs path and the
chunked ElevenLabsStreamer through an ElevenLabsEnvironment, matching
the STT side's ELEVENLABS_STT_BASE_URL/config override pattern.
2026-07-28 11:54:26 -07:00
Teknium
3356703d1f fix(tts): streaming path also honors tts.openai.api_key from config
Follow-up to salvaged PR #70307 (@aml1973): OpenAIStreamer now checks
tts.openai.api_key (config.yaml) ahead of the env resolver in both
available() and stream(), completing config parity between the sync
and streaming OpenAI TTS paths.
2026-07-28 11:54:26 -07:00
aml1973
861c2fecd2 fix(tts): honor OpenAI config for streaming
Use the shared OpenAI audio key resolver and prefer tts.openai.base_url over the global environment fallback in the Desktop streaming path. Add focused regression coverage for credential and endpoint propagation.
2026-07-28 11:54:26 -07:00
LeonSGP43
efc81a19a6 fix(tts): honor tts.openai.api_key and base_url from config.yaml
Salvaged from PR #26233 (@LeonSGP43), rebased onto the current 3-tuple
_resolve_openai_audio_client_config (is_managed flag, post-#73072 layout).
Same fix independently submitted earlier in PR #26209 (@zccyman) — credit
to both.

Resolution order now mirrors the STT resolver: tts.openai.api_key/base_url
from config.yaml -> VOICE_TOOLS_OPENAI_KEY/OPENAI_API_KEY env (still
honoring config base_url) -> managed gateway. _has_openai_audio_backend
also counts a config api_key as an available backend.

Fixes #26175
2026-07-28 11:54:26 -07:00
dsad
f96eee3a91 fix(xai): pin oauth side-tool base URLs 2026-07-28 11:54:01 -07:00
Ben Sheridan-Edwards
bcbae3bf41 fix(stt): cover 401 retry and keep proof image out of the merge diff
- parametrize the OAuth retry regression over HTTP 401 and 403 so both
  documented rejection statuses are exercised
- reword the retry-failure warning: the except block also covers the
  retried request, not just the credential refresh
- drop docs/proof/ from the PR diff; the live-proof screenshot now lives
  on the fork's proof-assets-xai-stt-oauth-retry branch and stays linked
  from the PR description
2026-07-28 11:54:01 -07:00
BenSheridanEdwards
36de3c5c3e fix(stt): retry xAI OAuth after auth rejection 2026-07-28 11:54:01 -07:00
Tobias Safaie geb. Schmidt-Philipp
d889c980f5 fix(stt): prefer explicit xAI API key 2026-07-28 11:54:01 -07:00
Teknium
c0c5dac531 fix: stdin=DEVNULL + windows_hide_flags on STT transcode subprocess (guard test) 2026-07-28 11:53:36 -07:00
Teknium
d66ec2f5f4 fix: explicit utf-8 encoding on ffmpeg STT transcode subprocess (Windows footgun lint) 2026-07-28 11:53:36 -07:00
Teknium
0897e0adb8 test: align dispatch tests with provider-scoped validation and named registration errors
Follow-ups for the salvaged wave: the auto-detect legacy-error test now
stubs the split validators, the unknown-command-provider test expects
the new provider_not_registered error, and _transcribe_local tolerates
a null stt.local config section again.
2026-07-28 11:53:36 -07:00
Teknium
9467dc135f fix(stt): lock local model load; allow keyless local OpenAI-compatible STT
Two small fresh fixes on top of the salvage wave:

- Wrap the check-then-load of the module-global faster-whisper model in a
  double-checked threading.Lock so concurrent voice messages can't both
  download/load the model (#24767).
- Treat an empty stt.openai.api_key as no-auth when stt.openai.base_url
  points at a loopback/RFC-1918/.local host, so local OpenAI-compatible
  STT servers (faster-whisper-server, speaches, vLLM whisper) work
  without a sham api_key value. Reimplements the idea from PR #25193 —
  credit @nnnet.

Co-authored-by: nnnet <nnnet@users.noreply.github.com>
2026-07-28 11:53:36 -07:00
Carl Borg
ef0d8ce2c5 transcription: transcode to m4a and retry when OpenAI STT rejects the audio container
Newer OpenAI transcription models (gpt-4o-transcribe, gpt-4o-mini-transcribe)
reject some containers the legacy whisper-1 endpoint accepted -- notably the
Ogg/Opus voice notes messaging platforms deliver -- returning a 400
'corrupted or unsupported' error, so voice-note transcription fails for users
on those models even though SUPPORTED_FORMATS still advertises .ogg/.aac/.flac.

Wrap the OpenAI upload: on a format-related BadRequestError, transcode the
source to a compact 16 kHz mono AAC .m4a via ffmpeg and retry once. This is
model-agnostic (no per-model format table to maintain) and adds no cost for
formats the endpoint already accepts.

Fixes #68719
2026-07-28 11:53:36 -07:00
Dennis
f50a7c307a fix(stt): preprocess .silk voice notes before transcription
Decode WeChat/QQ SILK v3 voice notes to WAV inside transcribe_audio so
any platform that caches a .silk file gets STT for free (same central-
normalization philosophy as the outbound container repair). pilk is
lazy-installed on first use (stt.silk in tools/lazy_deps.py) instead of
being added to the voice extra.

Fixes the inbound half of #32196.

(cherry picked from commit e5db79369d; reworked to compose with the
provider-scoped upload size cap and to lazy-dep pilk)
2026-07-28 11:53:36 -07:00
Damian Kluk
a784e74c7f fix(stt): better error logging when faster-whisper lazy install fails
Log lazy-install failures at WARNING instead of DEBUG, with actionable
guidance about venv write-permission issues (the most common cause of
silent STT failures).

Salvaged from PR #46127 (transcription_tools half only — the gateway DM
hunks are superseded by main's neutral-marker enrichment design, and the
Docker/CI files were unrelated scope).

(cherry picked from commit d3e07bdaaa, reduced)
2026-07-28 11:53:36 -07:00
Zehua Wang
7a56ab2aa2 fix(stt): validate selected voice provider availability 2026-07-28 11:53:36 -07:00
Zehua Wang
eaa2dd6d09 fix(stt): check selected provider (not any) + plugin support
PR review feedback:
- Replace _has_any_command_stt_provider() with selected-provider
  check via _resolve_command_stt_provider_config()
- Add _check_plugin_stt_provider() for plugin-registered backends
- Add tests: selected command, unrelated command (should NOT pass),
  and plugin provider path
2026-07-28 11:53:36 -07:00
Zehua Wang
de057fe24f fix(stt): check_voice_requirements() should recognize all STT providers
The /voice status command only checked for 'local', 'groq', and 'openai'
providers. Any other valid provider (local_command, mistral, xai,
elevenlabs, or custom command providers) fell through to the generic
MISSING message — even when transcription worked perfectly.

- Import _has_any_command_stt_provider (already defined, never imported)
- Add elif branches for local_command, mistral, xai, elevenlabs
- Add generic catch-all via _has_any_command_stt_provider() for
  arbitrary custom command providers
2026-07-28 11:53:36 -07:00
LauraGPT
7d2b8a3cad fix(stt): anchor qwen asr envelope stripping 2026-07-28 11:53:36 -07:00
LauraGPT
d219392e5b fix(stt): anchor Qwen3-ASR envelope stripping 2026-07-28 11:53:36 -07:00
LauraGPT
517b8debbd fix(stt): strip Qwen3-ASR response prefix
Normalize the structured <asr_text> marker after extracting text from string, SDK object, and dictionary transcription responses. Preserve the current provider-aware STT configuration architecture.

Refreshes #8773 on current main.

Co-authored-by: angelos <angelos@oikos.lan.home.malaiwah.com>

Assisted-by: Codex:gpt-5.6
2026-07-28 11:53:36 -07:00
luyifan
a5074c0ca8 fix(stt): report unregistered configured providers 2026-07-28 11:53:36 -07:00
Tushar
a6a439338e fix(tts): fall through to raw import when lazy_deps fails (#53259)
Replace 
aise ImportError(str(e)) with pass in the except Exception
handler of _import_edge_tts(), _import_elevenlabs(), and
_import_mistral_client() so packages installed via PYTHONPATH or Docker
layered filesystems still work when lazy_deps.ensure() raises.

Also fix the Mistral STT path in transcription_tools.py which only
caught ImportError, not FeatureUnavailable.

Adds 6 regression tests using sys.modules fixtures (no
builtins.__import__ patching).
2026-07-28 11:53:36 -07:00
Charles Cha
ffc3ce27d5 fix(stt): scope upload size limits to remote providers 2026-07-28 11:53:36 -07:00
AnthonyAssistantAi
884900ffd6 fix: avoid local STT crash on Apple Silicon
Force CPU (int8) for faster-whisper on Apple Silicon / Rosetta, where
ctranslate2's device=auto path can hard-abort in native code. Salvaged
from PR #28624 without the numpy pin change (main already moved on).

(cherry picked from commit 7edf2d5196, pyproject.toml hunk dropped)
2026-07-28 11:53:36 -07:00
liuhao1024
766e856118 fix(stt): treat CUBLAS_STATUS_NOT_SUPPORTED as CUDA lib error
- Add Blackwell-specific cuBLAS error marker to _CUDA_LIB_ERROR_MARKERS
- Allows CPU fallback on RTX 5090 (sm_120) when faster-whisper
  reports CUBLAS_STATUS_NOT_SUPPORTED instead of loading successfully
- Add regression test for CUBLAS_STATUS_NOT_SUPPORTED path

Closes #17526
2026-07-28 11:53:36 -07:00
Tranquil-Flow
06fc6e0c29 fix(stt): respect device and compute_type from config.yaml
The local STT transcription function hardcoded device="auto" and
compute_type="auto" when instantiating WhisperModel, ignoring the
user's stt.local.device and stt.local.compute_type config values.

Closes #8319
2026-07-28 11:53:36 -07:00
Teknium
c136400c9e fix(voice): single scoped resolver — STT/TTS keys fall back to the credential pool
Rework of #68509 per triage: hoist the duplicated per-tool
_resolve_provider_key helpers into one owner,
tools.tool_backend_helpers.resolve_provider_secret(), and migrate every
STT/TTS key lookup site to it.

Resolution order: explicit config.yaml value > profile secret scope /
env / ~/.hermes/.env > credential pool (checks both '<provider>' and
'custom:<provider>' pool keys, so keys added via 'hermes auth add
mistral' or declared under providers.<name> both resolve). Under an
active multiplex turn the profile scope stays authoritative — no pool
or .env fallback that could borrow another profile's key (composes with
the #69469 scope fix).

Coverage now includes GROQ_API_KEY, MISTRAL_API_KEY, ELEVENLABS_API_KEY,
DEEPINFRA_API_KEY, MINIMAX_API_KEY, GEMINI_API_KEY/GOOGLE_API_KEY, the
XAI_API_KEY fallback in resolve_xai_http_credentials, and the OpenAI
audio key (resolve_openai_audio_api_key now pool-aware for
OPENAI_API_KEY via 'hermes auth add openai-api').

Unit tests: fake pool entry proves each provider resolves from the pool
when env is empty; env still wins when set; config wins over both; a
multiplex scope miss never borrows the pool; pool read failures never
raise; tool-level wiring for STT, TTS, xAI, and OpenAI audio.

Fixes #68003
2026-07-28 11:53:09 -07:00
sg-architect
79bcfc23ab fix(tools): resolve TTS/STT provider keys through credential pool
TTS/STT providers (Mistral, ElevenLabs) only checked env vars and
.env files via get_env_value(), ignoring keys stored via
'hermes auth add mistral' / 'hermes auth add elevenlabs'.

Add _resolve_provider_key() helper that falls back to the credential
pool (agent.credential_pool.load_pool) when the env var is unset.

Affected:
- tools/transcription_tools.py: 6 sites (provider selection + auto-detect)
- tools/tts_tool.py: 6 sites (synthesis + availability check)

Fixes #68003
2026-07-28 11:53:09 -07:00
Drexuxux
ed591a5664 fix(voice): resolve the TTS/STT OpenAI key under the profile secret scope
`resolve_openai_audio_api_key()` reads the key that authenticates the audio
client straight from the process environment:

    return (
        os.getenv("VOICE_TOOLS_OPENAI_KEY", "")
        or os.getenv("OPENAI_API_KEY", "")
    ).strip()

That value is not advisory. It flows through
`_resolve_openai_audio_client_config()` into `OpenAIClient(api_key=...)` for
TTS, and through `transcription_tools` for voice-note STT — both on the
per-turn tool path, inside the profile secret scope the gateway installs.

`agent/vertex_adapter` states the contract this breaks:

    in a multiplex gateway serving several profiles from one process,
    os.environ reflects whichever profile's .env happened to be loaded at
    boot, not the profile the current turn belongs to. Reading it directly
    here would let one profile mint tokens from — and get billed against —
    a different profile's service-account file.

Reproduced with the real resolver, multiplexing on and profile A's scope
installed:

    scope-aware get_secret  -> sk-PROFILE-A-key
    voice/STT resolver      -> sk-PROFILE-B-key

So profile A's spoken reply and its users' voice notes are sent to OpenAI on
profile B's account, and billed there.

Route both reads through `agent.secret_scope.get_secret`, the same fix already
merged for the WeChat send path (#59662) and pending for QQ (#60420) — neither
covers the audio credential family. Under multiplexing the scope stays
authoritative, so a scope miss now yields no key instead of borrowing another
profile's; with multiplexing off `get_secret` falls through to `os.environ`
exactly as before, so single-profile deployments are untouched. The
VOICE_TOOLS_OPENAI_KEY > OPENAI_API_KEY precedence is unchanged.

Deliberately narrow: `fal_key_is_configured()` and
`has_direct_modal_credentials()` in this file are presence checks, not
authentication, and the former is already being reworked in open PR #20929.

tests/tools/test_tool_backend_helpers.py: the scope wins over another
profile's `os.environ`; a scope miss does not borrow another profile's key;
voice-key precedence holds inside a scope; and a control proves the
single-profile path still reads `os.environ`. The three isolation tests fail
on main; the control passes there. 320 passed across the helper, secret-scope,
and consumer suites (the fluctuating voice_mode/voice_cli failures are
pre-existing PulseAudio/ordering artifacts — the differing test passes 3/3 in
isolation on both main and this branch).
2026-07-28 11:53:09 -07:00
Teknium
d464ae3652 feat(cron): user-owned model pins + cron.model fleet default
Per-job cron inference pins are now user-owned: the agent-facing cronjob
tool schema no longer exposes model/provider/base_url, and the registered
handler ignores them even if a model hallucinates the old parameters.
Users set pins via the dashboard, hermes cron create/edit --model/--provider,
or jobs.json directly — and once set, a pin sticks until the user changes it.
Existing agent-era pins are grandfathered untouched.

New cron.model / cron.model_provider config keys give the cron fleet its
own default model, independent of the chat model. Fire-time resolution:
per-job pin > cron.model > HERMES_MODEL > model.default. An axis covered
by the explicit cron-fleet default is deliberate routing, not drift, so
the #44585 fail-closed guard skips it — switching your chat model with
/model or hermes model no longer breaks unpinned cron fleets.

- tools/cronjob_tools.py: drop model param from agent schema + handler;
  remove now-dead _resolve_model_override
- cron/scheduler.py: cron.model/model_provider resolution + per-axis
  drift-guard skip
- cron/jobs.py: snapshot resolution mirrors the new precedence
- hermes_cli/subcommands/cron.py + hermes_cli/cron.py: --model/--provider
  on hermes cron create/edit
- hermes_cli/config.py: cron.model / cron.model_provider defaults
- docs: cron.md model-resolution tip rewritten
2026-07-28 11:52:47 -07:00
Teknium
156edc5ded refactor: extract shared audio container sniffer to tools/audio_container.py
One sniffer owns magic-byte container detection (Teknium's one-concept-
one-owner rule): the new tools/audio_container.py is used by

- gateway/platforms/base.py _sniff_audio_ext (inbound cache — PR #36166's
  central sniffer, now covering AAC/ADTS, MP4-brand disambiguation, webm)
- gateway/platforms/signal.py _guess_extension (audio/AV branches
  delegated; RIFF/WAVE fix from PR #50690 and M4A-brand fix from
  PR #72490 now live centrally)
- tools/tts_tool.py _sniff_audio_container (outbound repair, PR #73072)

cache_audio_from_url inherits the sniff via cache_audio_from_bytes.
Adds tests/tools/test_audio_container.py covering every magic-byte type,
wrong-extension repair on the inbound cache, unknown passthrough, the
URL path, and Signal's delegation.
2026-07-28 11:52:44 -07:00
luyifan
4ae27548d6 fix(media): recognize m2a audio attachments 2026-07-28 11:52:44 -07:00
Elie BRUNO
971cb9b886 fix(stt): accept .oga and .opus voice notes for transcription
Telegram sends voice notes as .oga (OGG/Opus). SUPPORTED_FORMATS listed
.ogg but not .oga, so transcribe_audio rejected every Telegram voice note
with "Unsupported format: .oga" before reaching any STT backend. Add .oga
and .opus to the allowlist, with a regression test.
2026-07-28 11:52:44 -07:00
Alex Fournier
b1a5d67e71 Merge upstream main into fix/hermes-relay-anthropic-context
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-28 08:10:58 -07:00
Booker
1dfe781edd
fix(skills): avoid redundant bind-mount scans (#72622)
Skip hashing active copies when the bundled origin is unchanged, build rename and optional migration indexes lazily, and reuse one optional-skill directory index per sync.

Add regression coverage for unchanged copies, deferred modification detection, lazy rename recovery, and optional provenance scans.
2026-07-28 04:24:50 -05:00
Brooklyn Nicholson
1eb5ee1eaa fix(mcp): make Figma remote OAuth work via DCR allowlist defaults
Figma's mcp.figma.com register endpoint is a client_name allowlist
(Claude Code / Codex succeed; Hermes Agent 403s) and returns a client
secret while advertising auth_method=none, then requires the secret on
token exchange. Auto-set client_name + client_secret_post for Figma
hosts, pass oauth cfg through login/add paths, force interactive OAuth
for hermes mcp login from non-TTY desktop shells, and ship a catalog
entry. Proven: hermes mcp login figma → 26 tools.
2026-07-28 00:53:16 -05:00
Teknium
fab4c888ae feat(voice): say 'stop' to end a voice chat hands-free
Saying EXACTLY a configured stop phrase (default: 'stop') and nothing
else now ends the voice conversation instead of being sent to the agent
as a prompt. Match is deliberately strict — whole utterance,
case-insensitive, surrounding punctuation stripped — so 'stop doing
that and try X' still reaches the agent.

- tools/voice_mode.py: is_voice_stop_phrase() + voice.stop_phrases
  config loader (default ['stop'], [] disables, malformed config falls
  back safely).
- hermes_cli/voice.py: shared continuous loop (TUI + desktop) halts on
  a stop phrase exactly like the silent-cycle limit (fires
  on_silent_limit so every UI turns voice off); stop_continuous
  force-transcribe path swallows the phrase without counting a silent
  cycle.
- cli.py: classic CLI push-to-talk/continuous path and barge-in
  utterance path disable voice mode on a stop phrase.
- Config default voice.stop_phrases: ['stop']; docs updated.

Tests: tests/tools/test_voice_stop_phrase.py (27 tests,
sabotage-verified: loop test fails when detection is disabled);
voice suites green (110 + 44 passed).
2026-07-27 21:26:23 -07:00
Alex Fournier
14bed44c8c Reapply "feat(observability): integrate NeMo Relay runtime and shared metrics"
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-27 21:10:51 -07:00
Teknium
fae29c8410 fix(tts): class-level .ogg container repair + multi-platform opus voice detection
Root-cause fix for the 'TTS voice bubble broken' issue family (#57048,
#54589, #57213, #58845, #14841, #45557, #57049). Two class-level defects:

1. Several backends silently write MP3/WAV bytes into a .ogg output path
   (Edge only emits MP3, Piper writes WAV, xAI writes MP3, some
   OpenAI-compatible servers ignore response_format=opus). Platforms that
   need real Ogg/Opus render 0-second/broken voice bubbles. Instead of
   per-provider patches, text_to_speech_tool now sniffs magic bytes once
   after synthesis (_sniff_audio_container) and repairs the container
   centrally (_repair_ogg_container): ffmpeg transcode in place, or rename
   to the honest extension when ffmpeg is unavailable. Covers every
   current and future provider, including command providers and plugins.

2. want_opus only recognized Telegram, so Matrix/Feishu/WhatsApp/Signal
   auto-TTS voice replies were synthesized as MP3 and delivered as broken
   attachments. New OPUS_VOICE_PLATFORMS set covers all voice-bubble
   platforms.

_convert_to_opus refactored onto a shared _ffmpeg_transcode_to_opus that
supports safe in-place transcodes (-f ogg forced muxer, temp file +
os.replace).

Tests: tests/tools/test_tts_container_repair.py (13 tests incl. a live
ffmpeg round-trip); full TTS suite green (153 + 187 passed).
2026-07-27 20:28:35 -07:00
Teknium
a10bd49ddd feat(stt): unify language resolution across all STT providers
Class-level fix for the 'STT transcribes the wrong language' issue family
(#55551, #50181 and siblings). Previously language handling was per-provider
chaos: local honoured stt.local.language, Groq/OpenAI/Mistral/DeepInfra sent
no language hint at all, xAI silently forced 'en', ElevenLabs used its own
language_code key, and there was no global setting.

- New _resolve_stt_language() helper: stt.<provider>.language >
  stt.language (new global key) > HERMES_LOCAL_STT_LANGUAGE > auto-detect.
- Threaded through ALL providers: local, local_command, groq, openai,
  mistral, xai, elevenlabs, deepinfra (shared OpenAI handler), command
  providers, and plugin dispatch.
- xAI no longer forces English when nothing is configured (auto-detect).
- Mistral Voxtral now receives a language hint when configured.
- stt.groq.model is now honoured from config (previously env-only).
- DEFAULT_CONFIG gains stt.language, stt.groq, stt.xai, stt.mistral.language.
- Tests: tests/tools/test_stt_language_resolution.py (11 tests, sabotage-
  verified) + full transcription suite green (236 passed).

Builds on cherry-picked contributor work from #19786 (@zombopanda),
#23161 (@materemias), #50684 (@BlackishGreen33).
2026-07-27 20:28:31 -07:00