Skip to content

feat(nabrah): add Nabrah Arabic STT plugin - #7337

Open
MagdiWaleed wants to merge 1 commit into
livekit:mainfrom
MagdiWaleed:magdi/nabrah-stt
Open

MagdiWaleed wants to merge 1 commit into
livekit:mainfrom
MagdiWaleed:magdi/nabrah-stt

Conversation

@MagdiWaleed

@MagdiWaleed MagdiWaleed commented Sep 18, 2026

Copy link
Copy Markdown

Summary

Adds livekit-plugins-nabrah, a streaming speech-to-text plugin for Nabrah's Arabic recognizer.

  • Streams audio over WebSocket (wss://api.nabrah.ai/api/ext/stt/ws) at 16 kHz mono
  • Sends interim, preflight, and final transcripts, plus START/END_OF_SPEECH events
  • Detects end of turn from the <eot> token or closing punctuation, with a configurable confirm delay
  • Handles transcript corrections, word timings, and RECOGNITION_USAGE metrics
  • Options: language, recognition_model, priority_words, disable_number_normalization, silence and inactivity timeouts
  • Registered as the nabrah extra in livekit-agents
from livekit.plugins import nabrah

session = AgentSession(stt=nabrah.STT(language="ar-SA"), ...)

Needs NABRAH_API_KEY.

Testing

  • uv run pytest tests/test_plugin_nabrah_stt.py: 10 passed
  • make check (format, lint, mypy strict including livekit.plugins.nabrah): passes
  • Live test against the Nabrah API using 30 s of Arabic speech, which returned correct final transcripts with matching start/end-of-speech events

Replaces #6873, rebuilt on current main.

@MagdiWaleed
MagdiWaleed requested a review from a team as a code owner September 18, 2026 13:19

@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 potential issues.

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

Devin Review

Comment on lines +391 to +396
async for data in self._input_ch:
if isinstance(data, rtc.AudioFrame):
audio_bytes = data.data.tobytes()
if audio_bytes:
self._audio_position += data.samples_per_channel / data.sample_rate
await ws.send_bytes(audio_bytes)

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.

🟡 Stream flush requests are ignored

When callers invoke flush(), _send_task discards its sentinel instead of ending the current segment. Text remains unfinalized until provider EOT or stream closure.

Learn more

RecognizeStream.flush() marks the end of the current input segment. The input channel represents this call with _FlushSentinel, which this loop currently consumes without action because it handles only audio frames. Unlike provider-managed streams that explicitly document and warn about unsupported flushes, this plugin silently keeps the segment open.

Example: A direct streaming caller pushes "مرحبا", calls flush(), and waits for a final transcript. Without an EOT token, no final event arrives and later speech joins the same turn.

Recommended fix: Handle _FlushSentinel using Nabrah's provider-side finalize operation, then drain the corresponding transcript before emitting the segment boundary. If Nabrah cannot finalize on demand, explicitly override or reject flush() rather than silently accepting it.

Devin Review


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

Comment on lines +294 to +299
except (
aiohttp.ClientConnectorError,
aiohttp.WSServerHandshakeError,
asyncio.TimeoutError,
) as e:
raise APIConnectionError("failed to connect to nabrah STT") from e

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.

🟡 Permanent handshake failures are retried

When the handshake rejects, _connect_ws marks every HTTP status retryable. Invalid endpoints repeat until the retry budget expires, delaying failure.

Learn more

aiohttp.WSServerHandshakeError carries the HTTP status returned while upgrading the connection. Wrapping every such error in the default retryable APIConnectionError erases the distinction between permanent client failures and transient server failures. The base stream retries retryable API errors.

Example: A caller supplies a stale base_url that returns HTTP 404. The stream reconnects with the same URL until max_retry is exhausted instead of failing immediately with the 404.

Recommended fix: Catch WSServerHandshakeError separately and raise APIStatusError with its status, no response body, and the appropriate retryability. Keep transport and timeout failures as APIConnectionError.

Devin Review


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

Adds livekit-plugins-nabrah, a streaming speech-to-text plugin for
Nabrah's Arabic recognizer over WebSocket, with interim/final
transcripts, end-of-turn detection, word timings, and usage metrics.
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