Skip to content

feat: add Techmo TTS text-to-speech - #5

Merged
tomjad merged 4 commits into
masterfrom
feat/tts
Oct 23, 2026
Merged

tomjad merged 4 commits into
masterfrom
feat/tts

Conversation

@tomjad

@tomjad tomjad commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds Techmo TTS to the package, next to the existing ASR plugin:

from livekit.plugins.techmo import STT, TTS

session = AgentSession(stt=STT(language_group="pl"), llm=..., tts=TTS(voice_name="ewa"))

The code is a port of the internal Techmo plugin livekit-plugins-techmo-tts 0.3.2, used so far alongside this package. It goes in as the livekit/plugins/techmo/tts.py module of the single livekit-plugins-techmo distribution (the same layout as the official plugins, e.g. livekit.plugins.google.STT/.TTS). ASR users need no change, and the internal import path from livekit.plugins.techmo.tts import TTS keeps working.

What TTS provides

  • native streaming synthesis (TTS.stream(): sentence by sentence, no StreamAdapter) and TTS.synthesize()
  • voice / language / prosody options, update_options(), list_voices(); without voice_name / language_code the service's configured defaults are used (the internal plugin forced "michal" / "pl-PL"); prewarm() warns when the voice name is not offered by the service, since the service silently falls back to its default voice
  • SSML <mark/> markers as MarkerEvent (on_marker callback)
  • TLS / mutual TLS from bytes or file paths
  • gRPC channel pool with lifetime rotation, prewarm(), reconnect on transient UNAVAILABLE; gRPC errors mapped to APIConnectionError / APITimeoutError, so livekit-agents' retry logic applies
  • session_id sent as session_id gRPC metadata; not sent when unset (the internal plugin sent a random UUID). This matches tts-client-python without --session-id and the ASR plugin

Changes

File Change
livekit/plugins/techmo/tts.py Ported module. Logic unchanged: the AST is identical to the source except for the logger (child livekit.plugins.techmo.tts of the plugin logger), the user-agent (livekit-plugins-techmo/<version>) and the missing-stubs hint (git URL, since tts-api is not on PyPI). Long strings re-wrapped for the 120-column limit.
livekit/plugins/techmo/__init__.py Exports TTS, TTSOptions, TechmoSynthesizeStream, MarkerEvent, VoiceInfo
pyproject.toml tts-api @ git+…@v3.2.1; livekit-agents>=1.2 (was >=1.0); description, keywords
tests/test_tts.py, tests/test_tts_integration.py 100 unit tests + integration tests (skipped without TECHMO_TTS_ADDRESS); patch() targets moved to the new module path
.github/workflows/ci.yml New min-deps job: unit tests with livekit-agents==1.2.0
README.md Split into ASR and TTS sections (TTS configuration, prosody, TLS, markers, warm-up, session ID); requirements updated
KNOWN_ISSUES.md Workarounds for Techmo TTS services older than 3.2.4 (internal tracker links removed)
examples/voice_agent.py Voice agent with Techmo ASR + TTS (AgentSession API)
CHANGELOG.md, Makefile Unreleased entry; make test skips integration tests, new make test-integration

Why livekit-agents>=1.2

TTS splits text into sentences with tokenize.blingfire, which livekit-agents 1.0 and 1.1 don't have: on 1.1.7, 70 tests fail. On 1.2.0 all pass. The new CI job keeps that floor tested; the matrix jobs test the newest version.

Dependencies

tts-api 3.2.1 ships stubs generated with grpcio 1.70 / protobuf 5.29, the same floors techmo-asr-api already requires, so grpcio>=1.70.0 / protobuf>=5.29.0 stay as they are. No file is shared between tts-api and techmo-asr-api (checked against both RECORDs) and, unlike techmo-asr-api, tts-api does not install anything under google/.

Release

This PR also bumps the version to 0.4.0 (pyproject.toml, version.py, CHANGELOG [0.4.0] - 2026-10-23), as was done for 0.2.0 in #1. After merge: tag 0.4.0 on the merge commit and create the GitHub release from the CHANGELOG section. If the merge slips by more than a day or two, update the date in CHANGELOG first.

Verification

  • ruff, ruff format, mypy (5 files): clean
  • unit tests: 129 passed (100 TTS + 19 ASR) with livekit-agents 1.2.0, 1.5.1 and 1.8.8, Python 3.10 and 3.12; clean editable install, as in CI
  • uv build: wheel contains tts.py; Requires-Dist includes tts-api
  • examples/voice_agent.py imports and constructs STT/TTS on livekit-agents 1.2.0 and 1.8.8
  • Live Techmo TTS service (3.2.x, 210 voices): integration tests 8/8. A request built by the plugin without a voice gives the same audio as a request without synthesis_config (the service default); an unknown name such as MASZA triggers the prewarm() warning with Masza, masza as suggestions. Synthesis also checked from testenv.

Open points for review

  • Session-ID metadata key: TTS sends session_id (underscore), ASR sends session-id. Confirmed against the reference client: tts-client-python uses ("session_id", …) and sends it only when --session-id is given (tts_client_python/general.py), so the keys stay as they are.
  • examples/basic_stt.py (existing) still uses the pre-1.x VoicePipelineAgent API, which livekit-agents 1.x no longer has. Not changed here.
  • README links to LICENSE, but the repository has no LICENSE file. Not changed here.

🤖 Generated with Claude Code

Tomasz Jadczyk and others added 4 commits October 22, 2026 23:37
Port of the internal Techmo plugin livekit-plugins-techmo-tts 0.3.2 as a
`tts` module of this package, so one install provides both STT and TTS:
`from livekit.plugins.techmo import STT, TTS` (the internal import path
`livekit.plugins.techmo.tts` keeps working).

- livekit/plugins/techmo/tts.py: logic unchanged (AST-identical to the
  source except below); logger is the plugin logger's `tts` child,
  user-agent is `livekit-plugins-techmo/<version>`, the missing-stubs
  hint points at the git URL (tts-api is not on PyPI)
- tests: 100 TTS unit tests and the integration tests (skipped without
  TECHMO_TTS_ADDRESS), patch targets moved to the new module path
- dependencies: tts-api 3.2.1 by git URL; livekit-agents>=1.2, because
  1.0/1.1 lack tokenize.blingfire used for sentence splitting (70 tests
  fail on 1.1.7, all pass on 1.2.0)
- CI: unit tests also run with livekit-agents 1.2.0
- README (ASR and TTS sections), KNOWN_ISSUES.md without internal
  tracker links, examples/voice_agent.py, CHANGELOG, Makefile

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The internal plugin generated a UUID when session_id was not set, so
every TTS instance sent a session_id header the caller never chose.
tts-client-python sends no header without --session-id and lets the
service assign the ID; the ASR plugin behaves the same way. TTS now
does too: session_id defaults to None, an empty string counts as unset,
and _build_metadata omits the header.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…wn voice

The TTS API does not require a voice: SynthesisConfig is optional and an
empty Voice.name is ignored in voice selection (tts-api docs). The
plugin forced "michal" / "pl-PL" when nothing was given, overriding the
voice a deployment is configured with. Checked on a live service: with
an empty name it uses its default voice (same audio as a request with no
synthesis_config), which is not "michal" there.

- voice_name / language_code default to None (TECHMO_TTS_VOICE /
  TECHMO_TTS_LANGUAGE still apply; empty = unset) and are sent as empty
  strings; update_options(voice_name=None) goes back to the default
- the service silently falls back to its default voice for an unknown
  (or wrongly cased) name, so prewarm() now checks voice_name against
  list_voices() and warns, suggesting case-insensitive matches; a failing
  check is only logged at debug level

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@tomjad
tomjad merged commit 1cb3750 into master Oct 23, 2026
5 checks passed
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