Skip to content

feat(tts): add Piper setup support and fix Telegram voice note metadata - #7815

Closed
szafranski wants to merge 5321 commits into
NousResearch:mainfrom
szafranski:feat/piper-tts
Closed

feat(tts): add Piper setup support and fix Telegram voice note metadata#7815
szafranski wants to merge 5321 commits into
NousResearch:mainfrom
szafranski:feat/piper-tts

Conversation

@szafranski

@szafranski szafranski commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds first-class local Piper TTS support to Hermes setup and runtime, and fixes Telegram voice-note handling so generated audio is delivered with the correct metadata and file path.

This PR stays intentionally narrow and does five things:

  1. Adds Piper as a built-in provider in hermes setup tts.
  2. Pins the Piper voice catalog to 7a6c333ec560f0e688371adc2fbb7bbe105028c6.
  3. Improves local Piper model handling by supporting downloaded models, explicit model_path, and Unicode-safe voice download URLs.
  4. Fixes Telegram voice-note delivery by preserving filename and duration metadata and using the correct voice-note path.
  5. Adds Back navigation to the Piper setup flow so users can return from language, voice, and local-model submenus.

This keeps the PR aligned with the existing setup/runtime flow instead of introducing a parallel path, and limits the Telegram fix to the existing media handoff points already used by the current implementation.

Related Issue

Fixes #8508

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

  • Added pinned Piper catalog metadata in piper_catalog.py and packaged it in pyproject.toml
  • Updated hermes_cli/setup.py to expose Piper as a built-in TTS option
  • Added Piper setup navigation for:
    • preset voices
    • local downloaded models
    • manual model_path
    • Back navigation in Piper submenus
  • Updated the Piper provider label in setup to:
    • Piper (local on-device, free, 40+ languages, 150+ voice models)
  • Updated tools/tts_tool.py to support:
    • pinned Piper voice downloads
    • explicit local model_path
    • downloaded local models
    • Unicode-safe Piper voice URLs
    • correct Telegram-compatible Piper output handling
  • Fixed Telegram audio / voice-note metadata propagation in:
    • gateway/platforms/telegram.py
    • tools/send_message_tool.py
  • Preserved correct voice-note behavior for both Piper and Mistral TTS paths
  • Added and updated targeted tests in:
    • tests/tools/test_tts_tool_piper.py
    • tests/hermes_cli/test_setup.py
    • tests/hermes_cli/test_setup_model_provider.py
    • tests/gateway/test_telegram_documents.py
    • tests/tools/test_send_message_tool.py
    • tests/tools/test_tts_mistral.py

How to Test

  1. Run the setup tests:
    python -m pytest tests/hermes_cli/test_setup.py tests/hermes_cli/test_setup_model_provider.py -q

  2. Run the focused TTS / Telegram validation:
    python -m pytest tests/tools/test_tts_tool_piper.py tests/tools/test_send_message_tool.py::TestSendTelegramMediaDelivery tests/gateway/test_telegram_documents.py tests/tools/test_tts_mistral.py -q

  3. Manually verify in a real environment:

    • Piper setup flow
    • Telegram voice-note delivery
    • Back navigation in the Piper setup menus

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: macOS, Debian 13

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

Validation

  • Setup tests:
    • 47 passed
  • Focused TTS / Telegram validation:
    • 72 passed

Manual verification on the Debian 13 test host confirmed:

  • Piper setup flow works
  • Telegram voice-note delivery works
  • Back navigation works in the Piper setup menus
image

Notes

This branch was refreshed onto current upstream/main and conflicts were resolved without expanding PR scope.

pytest tests/ -q was attempted again, but the full suite is still not green in this environment, so that checklist item remains unchecked. The failures are not limited to this PR's Piper / Telegram scope.

@szafranski szafranski changed the title Add Piper TTS setup support and fix Telegram voice note metadata feat(tts): add Piper setup support and fix Telegram voice note metadata Apr 12, 2026
@szafranski
szafranski force-pushed the feat/piper-tts branch 2 times, most recently from bea4a29 to 8229c08 Compare April 15, 2026 22:00
@szafranski

Copy link
Copy Markdown
Contributor Author

Updated the branch after rebasing onto current main and simplifying the history.

I also re-ran the targeted Piper/Telegram tests and manually verified the end-to-end flow on a Debian 13 test host:

  • Piper setup flow works from the Hermes CLI
  • Telegram voice-note delivery works on the Piper path
  • Back navigation works in the Piper setup menus

Full pytest tests/ -q is still not green in this environment, but representative failures reproduce on clean upstream/main as well.

OutThisLife and others added 26 commits April 20, 2026 11:40
…d-lastmsg-8541

feat(tui): turn elapsed in FaceTicker + done-in sys line on turn end (NousResearch#8541)
…ousResearch#13148)

* feat(security): URL query param + userinfo + form body redaction

Port from nearai/ironclaw#2529.

Hermes already has broad value-shape coverage in agent/redact.py
(30+ vendor prefixes, JWTs, DB connstrs, etc.) but missed three
key-name-based patterns that catch opaque tokens without recognizable
prefixes:

1. URL query params - OAuth callback codes (?code=...),
   access_token, refresh_token, signature, etc. These are opaque and
   won't match any prefix regex. Now redacted by parameter NAME.

2. URL userinfo (https://user:pass@host) - for non-DB schemes. DB
   schemes were already handled by _DB_CONNSTR_RE.

3. Form-urlencoded body (k=v pairs joined by ampersands) -
   conservative, only triggers on clean pure-form inputs with no
   other text.

Sensitive key allowlist matches ironclaw's (exact case-insensitive,
NOT substring - so token_count and session_id pass through).

Tests: +20 new test cases across 3 test classes. All 75 redact tests
pass; gateway/test_pii_redaction and tools/test_browser_secret_exfil
also green.

Known pre-existing limitation: _ENV_ASSIGN_RE greedy match swallows
whole all-caps ENV-style names + trailing text when followed by
another assignment. Left untouched here (out of scope); URL query
redaction handles the lowercase case.

* feat: replace kimi-k2.5 with kimi-k2.6 on OpenRouter and Nous Portal

Update model catalogs for OpenRouter (fallback snapshot), Nous Portal,
and NVIDIA NIM to reference moonshotai/kimi-k2.6.  Add kimi-k2.6 to
the fixed-temperature frozenset in auxiliary_client.py so the 0.6
contract is enforced on aggregator routings.

Native Moonshot provider lists (kimi-coding, kimi-coding-cn, moonshot,
opencode-zen, opencode-go) are unchanged — those use Moonshot's own
model IDs which are unaffected.
…NousResearch#13021)

Replaces the serial for-loop in tick() with ThreadPoolExecutor so all
jobs due in a single tick run concurrently. A slow job no longer blocks
others from executing, fixing silent job skipping (issue NousResearch#9086).

Thread safety:
- Session/delivery env vars migrated from os.environ to ContextVars
  (gateway/session_context.py) so parallel jobs can't clobber each
  other's delivery targets. Each thread gets its own copied context.
- jobs.json read-modify-write cycles (advance_next_run, mark_job_run)
  protected by threading.Lock to prevent concurrent save clobber.
- send_message_tool reads delivery vars via get_session_env() for
  ContextVar-aware resolution with os.environ fallback.

Configuration:
- cron.max_parallel_jobs in config.yaml (null = unbounded, 1 = serial)
- HERMES_CRON_MAX_PARALLEL env var override

Based on PR NousResearch#9169 by @VenomMoth1.

Fixes NousResearch#9086
Extract 12 Codex Responses API format-conversion and normalization functions
from run_agent.py into agent/codex_responses_adapter.py, following the
existing pattern of anthropic_adapter.py and bedrock_adapter.py.

run_agent.py: 12,550 → 11,865 lines (-685 lines)

Functions moved:
- _chat_content_to_responses_parts (multimodal content conversion)
- _summarize_user_message_for_log (multimodal message logging)
- _deterministic_call_id (cache-safe fallback IDs)
- _split_responses_tool_id (composite ID splitting)
- _derive_responses_function_call_id (fc_ prefix conversion)
- _responses_tools (schema format conversion)
- _chat_messages_to_responses_input (message format conversion)
- _preflight_codex_input_items (input validation)
- _preflight_codex_api_kwargs (API kwargs validation)
- _extract_responses_message_text (response text extraction)
- _extract_responses_reasoning_text (reasoning extraction)
- _normalize_codex_response (full response normalization)

All functions are stateless module-level functions. AIAgent methods remain
as thin one-line wrappers. Both module-level helpers are re-exported from
run_agent.py for backward compatibility with existing test imports.

Includes multimodal inline image support (PR NousResearch#12969) that the original PR
was missing.

Based on PR NousResearch#12975 by @kshitijk4poor.
…in/QQ adapters

Add dm_policy and group_policy to the WhatsApp adapter, bringing parity
with WeCom/Weixin/QQ. Allows independent control of DM and group access:
disable DMs entirely, allowlist specific senders/groups, or keep open.

- dm_policy: open (default) | allowlist | disabled
- group_policy: open (default) | allowlist | disabled
- Config bridging for YAML → env vars
- 22 tests covering all policy combinations

Backward compatible — defaults preserve existing behavior.

Cherry-picked from PR NousResearch#11597 by @MassiveMassimo.
Dropped the run.py group auth bypass (would have skipped user auth
for ALL platforms, not just WhatsApp).
…iders (NousResearch#13152)

Add kimi-k2.6 as the top model in kimi-coding, kimi-coding-cn, and
moonshot static provider lists (models.py, setup.py, main.py).
kimi-k2.5 retained alongside it.
…providers

Section 3 (user-defined endpoints) added the plain ep_name to seen_slugs
but not the custom:-prefixed slug. Section 4 generates custom:<name> via
custom_provider_slug() and checks seen_slugs — since the prefixed slug
was missing, the same provider appeared twice in /model.

Register custom_provider_slug(display_name).lower() in seen_slugs after
Section 3 emits a provider, so Section 4's dedup correctly suppresses
the duplicate.

Closes NousResearch#12293.
Co-authored-by: bennytimz <bennytimz@users.noreply.github.com>
…search#13157)

Kimi's gateway selects the correct temperature server-side based on the
active mode (thinking -> 1.0, non-thinking -> 0.6).  Sending any
temperature value — even the previously "correct" one — conflicts with
gateway-managed defaults.

Replaces the old approach of forcing specific temperature values (0.6
for non-thinking, 1.0 for thinking) with an OMIT_TEMPERATURE sentinel
that tells all call sites to strip the temperature key from API kwargs
entirely.

Changes:
- agent/auxiliary_client.py: OMIT_TEMPERATURE sentinel, _is_kimi_model()
  prefix check (covers all kimi-* models), _fixed_temperature_for_model()
  returns sentinel for kimi models.  _build_call_kwargs() strips temp.
- run_agent.py: _build_api_kwargs, flush_memories, and summary generation
  paths all handle the sentinel by popping/omitting temperature.
- trajectory_compressor.py: _effective_temperature_for_model returns None
  for kimi (sentinel mapped), direct client calls use kwargs dict to
  conditionally include temperature.
- mini_swe_runner.py: same sentinel handling via wrapper function.
- 6 test files updated: all 'forces temperature X' assertions replaced
  with 'temperature not in kwargs' assertions.

Net: -76 lines (171 added, 247 removed).
Inspired by PR NousResearch#13137 (@kshitijk4poor).
…abled (NousResearch#13162)

When createForumTopic fails with 'not a forum' in a private chat,
the error now tells the user exactly what to do: enable Topics in
the DM chat settings from the Telegram app.

Also adds a Prerequisites callout to the docs explaining this
client-side requirement before the config section.
…tree isolation

Adds a _resolve_path() helper that reads TERMINAL_CWD and uses it as
the base for relative path resolution. Applied to _check_sensitive_path,
read_file_tool, _update_read_timestamp, and _check_file_staleness.

Absolute paths and non-worktree sessions (no TERMINAL_CWD) are
unaffected — falls back to os.getcwd().

Fixes NousResearch#12689.
…search#13136)

* ci(nix): automatic lockfile fixing to keep main building

This reverts commit ef34b34.

* update lockfiles
Cherry-picked from PR NousResearch#13159 by @cdanis.

Adds native media attachment delivery to Signal via signal-cli JSON-RPC
attachments param. Signal messages with media now follow the same
early-return pattern as Telegram/Discord/Matrix — attachments are sent
only with the last chunk to avoid duplicates.

Follow-up fixes on top of the original PR:
- Moved Signal into its own early-return block above the restriction
  check (matches Telegram/Discord/Matrix pattern)
- Fixed media_files being sent on every chunk in the generic loop
- Restored restriction/warning guards to simple form (Signal exits early)
- Fixed non-hermetic test writing to /tmp instead of tmp_path
The Activity accordion in ToolTrail tints red (via metaTone) when an error
item is present, but stays collapsed — the error is invisible until the
user clicks. Track the latest error id and force-open openMeta whenever
it advances. Users can still manually collapse; a new error re-opens.
…ity-autoexpand-on-error

fix(tui): auto-expand Activity section on error
…ersion

The link regex in format_message used [^)]+ for the URL portion, which
  stopped at the first ) character. URLs with nested parentheses (e.g.
  Wikipedia links like Python_(programming_language)) were improperly parsed.

  Use a better regex, which is the same the Slack adapter uses.
… gibberish

The colored ✓/✗ marks in /tools list, /tools enable, and /tools disable
  were showing up as "?[32m✓ enabled?[0m" instead of green and red. The
  colors come out as ANSI escape codes, but the tui eats
  the ESC byte and replaces it with "?" when those codes are printed
  straight to stdout. They need to go through prompt_toolkit's renderer.

  Fix: capture the command's output and re-print each line through
  _cprint(), the same workaround used elsewhere for NousResearch#2262. The capture
  buffer fakes isatty()=True so the color helper still emits escapes
  (StringIO.isatty() is False, which would otherwise strip colors).
  The capture path only runs inside the TUI; standalone CLI and tests
  go straight through to real stdout where colors already work.
@alt-glitch alt-glitch added tool/tts Text-to-speech and transcription platform/telegram Telegram bot adapter labels Apr 22, 2026
@szafranski

Copy link
Copy Markdown
Contributor Author

I noticed #17092 was closed in favor of this PR before #17885 landed, and now #17885 covers the main native Piper provider path. Given that, I do not want to keep a large overlapping PR open unnecessarily.

What would maintainers prefer here?

I can either:

Potential follow-up areas I see:

  • Piper in hermes setup tts / setup UX
  • Back navigation and voice selection in setup
  • deterministic pinned voice catalog handling

Happy to trim this down aggressively if any of those are wanted.

@szafranski

Copy link
Copy Markdown
Contributor Author

Closing this PR to reduce review noise.

The useful pieces are now covered by smaller, cleaner threads:

This PR also has stale branch history and a very noisy diff, so keeping it open would make review harder rather than easier. Thanks.

@szafranski szafranski reopened this Jun 16, 2026
@szafranski szafranski closed this Jun 16, 2026
szafranski added a commit to szafranski/hermes-agent that referenced this pull request Jul 11, 2026
…t show 0:00

Telegram only auto-derives a voice/audio clip's duration from container
metadata for short recordings; clips longer than ~4:50 are delivered with
duration 0 and render as 0:00 in the player. Probe the length locally
(stdlib wave -> mutagen -> ffprobe) and pass duration explicitly to
sendVoice/sendAudio. Best-effort: when nothing can read the file we omit
duration and fall back to Telegram's prior behavior.

Extracts and hardens the Telegram-only part of the stale, Piper-bundled
PR NousResearch#7815 (ffprobe-only, predates the send_voice retry/anchor refactor);
relates to NousResearch#8508.
teknium1 pushed a commit that referenced this pull request Jul 16, 2026
…t show 0:00

Telegram only auto-derives a voice/audio clip's duration from container
metadata for short recordings; clips longer than ~4:50 are delivered with
duration 0 and render as 0:00 in the player. Probe the length locally
(stdlib wave -> mutagen -> ffprobe) and pass duration explicitly to
sendVoice/sendAudio. Best-effort: when nothing can read the file we omit
duration and fall back to Telegram's prior behavior.

Extracts and hardens the Telegram-only part of the stale, Piper-bundled
PR #7815 (ffprobe-only, predates the send_voice retry/anchor refactor);
relates to #8508.
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
…t show 0:00

Telegram only auto-derives a voice/audio clip's duration from container
metadata for short recordings; clips longer than ~4:50 are delivered with
duration 0 and render as 0:00 in the player. Probe the length locally
(stdlib wave -> mutagen -> ffprobe) and pass duration explicitly to
sendVoice/sendAudio. Best-effort: when nothing can read the file we omit
duration and fall back to Telegram's prior behavior.

Extracts and hardens the Telegram-only part of the stale, Piper-bundled
PR NousResearch#7815 (ffprobe-only, predates the send_voice retry/anchor refactor);
relates to NousResearch#8508.
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…t show 0:00

Telegram only auto-derives a voice/audio clip's duration from container
metadata for short recordings; clips longer than ~4:50 are delivered with
duration 0 and render as 0:00 in the player. Probe the length locally
(stdlib wave -> mutagen -> ffprobe) and pass duration explicitly to
sendVoice/sendAudio. Best-effort: when nothing can read the file we omit
duration and fall back to Telegram's prior behavior.

Extracts and hardens the Telegram-only part of the stale, Piper-bundled
PR NousResearch#7815 (ffprobe-only, predates the send_voice retry/anchor refactor);
relates to NousResearch#8508.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have platform/telegram Telegram bot adapter 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.

[Feature]: Improve built-in local TTS language coverage with Piper