Skip to content

feat(convozen): add Akshara STT and Ragini TTS plugin - #7319

Open
aminaashrafch wants to merge 10 commits into
livekit:mainfrom
aminaashrafch:plugin/convozen
Open

aminaashrafch wants to merge 10 commits into
livekit:mainfrom
aminaashrafch:plugin/convozen

Conversation

@aminaashrafch

Copy link
Copy Markdown

Adds livekit-plugins-convozen, integrating ConvoZen's Akshara speech-to-text and Ragini text-to-speech models. Both cover nine Indian languages (Bengali, English, Gujarati, Hindi, Kannada, Malayalam, Marathi, Tamil, Telugu), including code-mixed speech.

Install with pip install "livekit-agents[convozen]".

Design

  • STT: Akshara returns one transcript per request with no interim results, so STT declares streaming=False. Agent.stt_node wraps it in stt.StreamAdapter, so a VAD is required on the AgentSession.
  • TTS: Ragini synthesizes a whole request at a time, so TTS declares streaming=False and is wrapped by tts.StreamAdapter. Audio streams back as a chunked WAV, decoded by AudioEmitter (hence the livekit-agents[codecs] dependency).
  • Calls the ConvoZen HTTP API directly with the shared aiohttp session; no vendor SDK dependency.
  • Session keyterms are forwarded to Akshara's keywords field (keyterms=True).
  • Akshara's score is an unbounded log-probability rather than a [0, 1] confidence, so it's exposed via SpeechData.metadata and confidence is left unset.
  • Error paths re-raise from None and report only the exception type, since aiohttp errors carry request headers including the API key.

Testing

  • tests/test_plugin_convozen.py: 29 hermetic unit tests (pytest --unit)
  • make format-check and make lint pass; livekit.plugins.convozen type-checks under make type-check
  • Verified against the live ConvoZen API: TTS streaming and sample rates, STT transcription in English and Hindi, and a bad-key error path
  • End-to-end run of an AgentSession with Silero VAD against a local LiveKit server: caller speech → STT → agent turn → TTS audio back to the caller

Notes

  • uv.lock isn't included; regenerating it locally changed unrelated Python version markers, and CI's uv sync resolves it.
  • Happy to provide a test API key privately if you'd like to verify against the live ConvoZen API

aminaashrafch and others added 4 commits September 17, 2026 05:51
Adds livekit-plugins-convozen for ConvoZen's Akshara speech-to-text and
Ragini text-to-speech, covering nine Indian languages including code-mixed
speech.

Akshara returns one transcript per request and emits no interim results, so
the STT declares streaming=False and Agent.stt_node wraps it in
stt.StreamAdapter using the session VAD to segment utterances. Ragini
synthesizes a whole request at a time, so the TTS declares streaming=False
and is wrapped by tts.StreamAdapter. Neither has a websocket path.

Ragini returns a chunked WAV container whose RIFF length fields are
placeholders, so the package depends on livekit-agents[codecs] and lets
AudioEmitter decode it rather than parsing headers itself.

Keyterms configured on the AgentSession are forwarded to Akshara's keywords
field. Akshara also returns a score, which is an unbounded log-probability
rather than a [0, 1] confidence; it is exposed through SpeechData.metadata
and confidence is left at its default.
@aminaashrafch
aminaashrafch requested a review from a team as a code owner September 17, 2026 10:55
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@devin-ai-integration devin-ai-integration Bot 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.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 2 potential issues.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment on lines +264 to +271
if res.status != 200:
body = (await res.text())[:_MAX_ERROR_BODY]
raise APIStatusError(
message=f"ConvoZen Akshara returned {res.status}: {body}",
status_code=res.status,
request_id=None,
body=body,
)

@devin-ai-integration devin-ai-integration Bot Sep 17, 2026

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.

🟨 STT errors expose provider content

A non-200 response embeds body in APIStatusError. Retry logs and exception telemetry can expose provider content without PII tagging.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 5e45276 I set aligned_transcript=False explicitly, documented that word_timestamps values are relative to each recognized utterance (docstring and README), and added a test pinning the capability with word_timestamps=True. If you'd like batch STTs to support alignment, I'm happy to follow up with a StreamAdapter change that propagates the capability and offsets forwarded timings by each segment's position in the stream.

Akshara returns word timings relative to each recognized utterance, and
stt.StreamAdapter forwards them without offsetting them onto the audio
stream, so they do not meet the aligned_transcript contract. Declare
aligned_transcript=False explicitly, document the timing reference in the
word_timestamps docstring and README, and pin the capability in tests.
# Conflicts:
#	livekit-agents/pyproject.toml

@devin-ai-integration devin-ai-integration Bot 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.

Devin Review found 2 new potential issues.

2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

Comment on lines +213 to +219
if is_given(language) or is_given(lang_tags):
# Re-derive so that changing the language alone also moves the hint,
# and so an explicit tag list is validated the same way as at __init__.
self._opts.lang_tags = _resolve_lang_tags(
self._opts.language,
lang_tags if is_given(lang_tags) else NOT_GIVEN,
)

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.

🟡 Explicit language hints are discarded

Calling update_options(language=...) replaces explicit lang_tags with one derived tag. Later code-mixed recognition loses its configured language hints.

Learn more

lang_tags can either be explicit configuration or a default derived from language. The current options object stores only the resolved list, so update_options cannot distinguish those cases. A language-only update always resolves a fresh list and overwrites explicit code-mixing hints.

Example: Construct STT(language="hi", lang_tags=["hi", "en"]), then call update_options(language="ta"). The next request sends ["ta"] instead of retaining the explicitly configured ["hi", "en"].

Recommended fix: Track whether lang_tags is explicit or derived. Recompute it on a language-only update only when it was derived; update that state whenever lang_tags itself is supplied.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread pyproject.toml
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.

2 participants