Skip to content

fix(gateway): support Feishu voice TTS opus delivery - #37804

Closed
realkcn wants to merge 1 commit into
NousResearch:mainfrom
realkcn:fix/feishu-voice-tts-opus
Closed

fix(gateway): support Feishu voice TTS opus delivery#37804
realkcn wants to merge 1 commit into
NousResearch:mainfrom
realkcn:fix/feishu-voice-tts-opus

Conversation

@realkcn

@realkcn realkcn commented Jun 3, 2026

Copy link
Copy Markdown

What does this PR do?

Fixes Feishu voice/TTS delivery so generated TTS audio can render as native Feishu voice bubbles instead of generic file attachments.

The Feishu path had two related gaps:

  • TTS only selected Opus voice output automatically for Telegram, so Feishu sessions could generate MP3 output and miss native voice-message delivery.
  • Feishu audio messages need duration metadata to render correctly as voice bubbles with a visible pre-play duration.

This draft combines those fixes and also allows command TTS providers to declare .opus output directly.

Related Issue

Fixes #16524

Related existing PRs found during duplicate search:

This draft is intentionally scoped as a combined alternative because it covers both areas plus command-provider .opus support.

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

  • tools/tts_tool.py — treat Feishu like Telegram for Opus voice output, recognize .opus as a voice-compatible payload, and allow command providers to output opus.
  • gateway/platforms/feishu.py — probe Opus audio duration and include duration metadata in Feishu upload/message payload construction for audio sends.
  • tests/tools/test_tts_command_providers.py — update accepted command TTS output formats.
  • tests/gateway/test_feishu.py — assert Feishu voice sends include duration metadata in the upload body and audio message payload.

How to Test

Automated checks run on macOS 26.4.1 with Python 3.11.15 virtualenv:

python -m py_compile gateway/platforms/feishu.py tools/tts_tool.py tests/tools/test_tts_command_providers.py tests/gateway/test_feishu.py
pytest tests/tools/test_tts_command_providers.py tests/gateway/test_feishu.py -q

Result:

257 passed, 2 warnings in 10.41s

Manual behavior to verify with a real Feishu bot:

  1. Start Hermes gateway with Feishu enabled and TTS configured.
  2. Trigger a TTS/voice response in Feishu.
  3. Confirm the outbound message renders as a native Feishu voice bubble with visible duration, not as a generic file attachment.

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 26.4.1, Python 3.11.15

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

Screenshots / Logs

........................................................................ [ 28%]
........................................................................ [ 56%]
........................................................................ [ 84%]
.........................................                                [100%]
257 passed, 2 warnings in 10.41s

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery platform/feishu Feishu / Lark adapter tool/tts Text-to-speech and transcription labels Jun 3, 2026
@realkcn
realkcn force-pushed the fix/feishu-voice-tts-opus branch from 10d021a to 32d47bf Compare July 5, 2026 06:19
@realkcn

realkcn commented Jul 5, 2026

Copy link
Copy Markdown
Author

Updated this PR branch onto latest upstream main (1388cd1c0) and re-applied the Feishu Opus voice patch as 32d47bf4d.

Conflict resolution was limited to the Feishu test mock path, keeping it on plugins.platforms.feishu.adapter while preserving the new duration probe mock.

Validation run:

  • python -m py_compile plugins/platforms/feishu/adapter.py tools/tts_tool.py tests/gateway/test_feishu.py tests/tools/test_tts_command_providers.py
  • python -m pytest tests/gateway/test_feishu.py::TestAdapterBehavior::test_send_voice_uploads_opus_and_sends_audio_message tests/tools/test_tts_command_providers.py::TestConfigGetters::test_output_format_supported_set

Result: 2 passed.

@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 combining the two verified Feishu voice-delivery gaps. Current main still excludes Feishu from want_opus at tools/tts_tool.py:2202, and sends an audio payload with only file_key at plugins/platforms/feishu/adapter.py:4591.

Problems

  • The new duration probe is called directly inside async _send_uploaded_file_message (plugins/platforms/feishu/adapter.py:4506 in the PR diff) but runs subprocess.run(..., timeout=10). The live adapter has _run_blocking() at plugins/platforms/feishu/adapter.py:1692-1695; use it so an audio probe cannot stall the gateway loop.
  • _probe_audio_duration_ms() returns None when ffmpeg is unavailable. Native-Opus providers use .ogg without needing ffmpeg (tools/tts_tool.py:2241-2244), so that path would still send the duration-less payload and preserve the reported behavior.
  • tests/tools/test_tts_command_providers.py:207 snapshots the entire format set. Replace it with an Opus acceptance behavior test, and add Feishu routing coverage analogous to the Telegram-only test in tests/tools/test_tts_opus_routing.py:47-70.

Suggested changes

  • Offload the probe and provide a positive non-ffmpeg fallback before adding the upload/message duration.
  • Add Feishu Opus-routing and no-ffmpeg duration-fallback tests.

Automated hermes-sweeper review.

requested_message_type=outbound_message_type,
)
upload_duration_ms = (
self._probe_audio_duration_ms(file_path)

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.

This synchronous probe runs subprocess.run(..., timeout=10) on the gateway event loop. Please await the adapter's existing _run_blocking(self._probe_audio_duration_ms, file_path) (or an equivalent nonblocking call), and ensure the probe still supplies a positive duration when ffmpeg is unavailable.

def _probe_audio_duration_ms(file_path: str) -> Optional[int]:
ffmpeg = shutil.which("ffmpeg")
if not ffmpeg:
return None

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.

Please pass creationflags=windows_hide_flags() here, as existing subprocess call sites do, so Feishu TTS delivery does not flash a console window on native Windows.


def test_output_format_supported_set(self):
assert COMMAND_TTS_OUTPUT_FORMATS == frozenset({"mp3", "wav", "ogg", "flac"})
assert COMMAND_TTS_OUTPUT_FORMATS == frozenset({"mp3", "wav", "ogg", "opus", "flac"})

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.

This freezes the full mutable format catalog. Test the intended contract instead: assert that _get_command_tts_output_format({"format": "opus"}) returns "opus".

@teknium1 teknium1 added sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 14, 2026
@teknium1

Copy link
Copy Markdown
Contributor

The root cause here (gateway auto-TTS choosing MP3 vs Ogg/Opus via the cleared HERMES_SESSION_PLATFORM contextvar, so opus platforms got audio attachments instead of native voice bubbles) was fixed class-wide in #73508: platform-awareness now comes from the caller via build_auto_tts_output_path(platform) keyed off OPUS_VOICE_PLATFORMS (based on @giladbau's #62040), with the central container repair guaranteeing real Ogg/Opus bytes.

(Landed via #73508, merge f440a44753.) Closing.

@teknium1 teknium1 closed this Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/feishu Feishu / Lark adapter sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows tool/tts Text-to-speech and transcription type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Include audio duration in Feishu voice message payload

3 participants