Skip to content

Release 1.14.1a1 - #148

Open
github-actions[bot] wants to merge 18 commits into
masterfrom
release-1.14.1a1
Open

Release 1.14.1a1#148
github-actions[bot] wants to merge 18 commits into
masterfrom
release-1.14.1a1

Conversation

@github-actions

Copy link
Copy Markdown

Human review requested!

JarbasAl and others added 18 commits July 14, 2026 17:36
* fix: transcode non-WAV plugin output for compat routers

TTS plugins may declare a non-WAV audio_ext (e.g. an mp3-only engine),
producing audio the WAV decode path cannot read and causing the
ElevenLabs-compatible endpoint to fail. Add _ensure_wav to transcode
such output to WAV before decoding so all compat routers can serve
any plugin regardless of its native audio format.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: transcode non-WAV plugin output on the PCM path

The ElevenLabs PCM output path decodes plugin audio through _read_samples,
which opens the file directly with wave.open and bypasses convert_audio.
For plugins that emit non-WAV audio (e.g. an mp3-only engine) output_format
pcm_* raised wave.Error. Route _read_samples through _ensure_wav so the PCM
branch transcodes non-WAV input first, matching the container output path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
TTSEngineWrapper now runs the OVOS transformer pipelines around
synthesis: dialog transformers rewrite the utterance before synth and
tts transformers post-process the audio. Because every native, vendor
compat, websocket, MCP and UTCP surface flows through
TTSEngineWrapper.synthesize(), all endpoints get the pipelines.

Loading is config-gated and opt-in via the mycroft.conf
dialog_transformers / tts_transformers sections; with no config the
server behaves exactly as before. TTS transformers operate on a temp
copy so the plugin's audio cache is never mutated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: run blocking synth off the event loop in async endpoints

All /synthesize, /v2/synthesize and the vendor-compat routers were async def
handlers calling the blocking tts_engine.synthesize() directly on the running
event loop. For any plugin using opm's base streaming get_tts (which drives its
own loop via run_until_complete), this raised 'Cannot run the event loop while
another loop is running' (HTTP 500). Wrap every synth call in
starlette run_in_threadpool so it executes off-loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: correct threadpool wrap to only truly-async handlers

The previous commit added `await run_in_threadpool(engine.synthesize, ...)`
to every vendor-compat router, but 9 of them (amazon_polly, cartesia, coqui,
deepgram_aura, elevenlabs, google_tts, marytts, openai_tts, playht) use plain
sync `def` handlers, which FastAPI already runs in its own threadpool — so
`await` there is a SyntaxError that made those modules unimportable. It also
placed the import above `from __future__ import annotations` in azure_ws.py,
another SyntaxError.

Revert the sync routers to direct `engine.synthesize(...)` calls (already
off-loop via FastAPI's sync-endpoint threadpool) and fix the import ordering.
The threadpool wrap now applies only where handlers are genuinely `async def`
and would otherwise nest the plugin's `run_until_complete` on the request loop:
the core /synthesize and /v2/synthesize endpoints, the async azure_tts handler,
and the azure_ws websocket handler.

Add a regression test: a plugin whose get_tts drives its own event loop (as
opm's base StreamingTTS does) hitting the async /synthesize endpoints, which
returns HTTP 500 without the fix and 200 with it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
feat: dialog and tts transformer pipelines
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Four request-handling paths ran blocking work directly on the asyncio
event loop, each capable of stalling the whole server (all other
requests, including /status) for the duration of a single synthesis:

- elevenlabs stream-input websocket: engine.synthesize() (fixed here)
- elevenlabs stream-input websocket: encode_audio(), three lines below
  the synthesize() fix. DEFAULT_OUTPUT_FORMAT is "mp3_44100_128", so
  the default path still ran a pydub/ffmpeg mp3 export (and potentially
  a synchronous ffmpeg subprocess via _ensure_wav) inline.
- azure cognitiveservices websocket: convert_audio(), immediately after
  an already-threadpooled engine.synthesize() call -- same omission.
- MCP synthesize tool: a sync `def` tool calling engine.synthesize()
  and reading the resulting file. The MCP SDK invokes sync tools
  directly on the event loop (mcp/server/fastmcp/utilities/
  func_metadata.py: `fn_is_async` branches between `await fn(...)` and
  `fn(...)`), and the MCP app is mounted onto the same FastAPI app, so
  one /mcp synthesize call froze /status and everything else for the
  full synthesis -- the exact production symptom (observed live:
  /status unanswered for 30s at 0.59% CPU).

All four now hand the blocking call to run_in_threadpool. The MCP tool
is now `async def` and awaits run_in_threadpool for both the engine
call and the file read; verified against func_metadata.py that the SDK
awaits async tools correctly, and test/unittests/test_mcp_server.py
was updated to await the now-async tool via asyncio.run(...).

Also extends test/unittests/test_concurrency.py:
- adds a websocket regression test with a patched, deliberately slow
  encode_audio() (the existing test's 100-frame wav makes real
  encoding free, so it could not have caught the encode_audio
  regression)
- tightens the two "concurrent requests overlap" assertions from
  `elapsed < SLEEP_SECONDS * 1.5` (elapsed can never go below
  SLEEP_SECONDS, so this left only ~1.0s of real headroom) to
  `elapsed - SLEEP_SECONDS < 0.5`, which discriminates cleanly between
  fixed (~0s overhead) and broken (~+SLEEP_SECONDS overhead)

Each of the three new/updated fixes was verified by reverting it in
isolation (file copied aside, never git stash) and confirming the
corresponding test/script fails before the fix and passes after.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant