Skip to content

feat(plugins): add Gnani plugin#2004

Open
rosetta-livekit-bot[bot] wants to merge 1 commit into
mainfrom
pocketed-retreats-symptoms
Open

feat(plugins): add Gnani plugin#2004
rosetta-livekit-bot[bot] wants to merge 1 commit into
mainfrom
pocketed-retreats-symptoms

Conversation

@rosetta-livekit-bot

@rosetta-livekit-bot rosetta-livekit-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Ports livekit/agents#6004 for Gnani into agents-js by adding a new @livekit/agents-plugin-gnani package, since the target repo had no existing Gnani counterpart.

  • Adds Gnani STT with REST recognition and WebSocket streaming, including format, preferredLanguage, and itnNativeNumerals support.
  • Adds Gnani TTS with REST, SSE, and WebSocket synthesis transports plus streaming SynthesizeStream.
  • Adds package README, package/build/API Extractor metadata, API report, lockfile integration, and a minor changeset.
  • Ports the source PR's Gnani configuration tests to Vitest with network stubs.

Source diff coverage

File-by-file classification
Source file Classification Target coverage
livekit-plugins/livekit-plugins-gnani/README.md Ported/adapted plugins/gnani/README.md; content ported and adapted from Python install/import examples and snake_case options to JS package/imports and camelCase options.
livekit-plugins/livekit-plugins-gnani/livekit/plugins/gnani/__init__.py Adapted plugins/gnani/src/index.ts; maps Python exports/plugin registration to JS exports and Plugin.registerPlugin(). Source __all__ parity kept for STT, SpeechStream, TTS, and SynthesizeStream.
livekit-plugins/livekit-plugins-gnani/livekit/plugins/gnani/log.py Adapted No standalone target file; JS implementation uses the framework log() helper directly in plugins/gnani/src/stt.ts and plugins/gnani/src/tts.ts.
livekit-plugins/livekit-plugins-gnani/livekit/plugins/gnani/stt.py Ported/adapted plugins/gnani/src/stt.ts; ports REST form fields, WebSocket headers, sample-rate validation, API-key env fallback, model/provider properties, and stream language inheritance. Adapted from Python aiohttp/websockets and snake_case args to JS fetch/ws and camelCase options.
livekit-plugins/livekit-plugins-gnani/livekit/plugins/gnani/tts.py Ported/adapted plugins/gnani/src/tts.ts; ports voice/sample-rate validation, bitrate payload support, REST/SSE/WebSocket synthesis modes, stream() WebSocket synthesis, and updateOptions() behavior. Adapted to JS tts.ChunkedStream/tts.SynthesizeStream APIs.
livekit-plugins/livekit-plugins-gnani/pyproject.toml Adapted plugins/gnani/package.json, plugins/gnani/tsconfig.json, plugins/gnani/tsup.config.ts, plugins/gnani/api-extractor.json, pnpm-lock.yaml, and .changeset/green-gnani-speak.md; maps Python package metadata/dependencies/version bump to JS workspace package metadata, build metadata, lockfile, and minor changeset for a new public package.
tests/test_plugin_gnani_stt.py Ported/adapted plugins/gnani/src/stt.test.ts; ports the source configuration and stream-construction tests to Vitest with JS option names and hermetic ws stubs.
tests/test_plugin_gnani_tts.py Ported/adapted plugins/gnani/src/tts.test.ts; ports the source configuration, routing, update, bitrate, sample-rate, and stream-construction tests to Vitest with JS option names and hermetic fetch/ws stubs.

Infrastructure gap

The target repo lacked any Gnani plugin package. This PR ports that missing infrastructure as plugins/gnani rather than dropping the behavior.

Validation

  • pnpm vitest run plugins/gnani
  • pnpm --filter @livekit/agents-plugin-gnani lint
  • pnpm --filter @livekit/agents-plugin-gnani build
  • pnpm --filter @livekit/agents-plugin-gnani api:update
  • pnpm --filter @livekit/agents-plugin-gnani api:check
  • pnpm build
  • pnpm lint (passes; existing warnings in unrelated packages remain)

Ported from livekit/agents#6004

Original PR description
  • Clarify STT usage as REST and ASR (real-time streaming).
  • Remove detailed STT/TTS access-method tables from the Gnani README.
  • Keep concise code examples for stt.recognize(), stt.stream(), and TTS synthesize/stream flows.
  • Removing the usage of organization_id and user_id.

@rosetta-livekit-bot rosetta-livekit-bot Bot requested a review from a team as a code owner July 9, 2026 05:19
@changeset-bot

changeset-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ee097ad

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 37 packages
Name Type
@livekit/agents-plugin-gnani Major
@livekit/agents Major
@livekit/agents-plugin-anam Major
@livekit/agents-plugin-anthropic Major
@livekit/agents-plugin-assemblyai Major
@livekit/agents-plugin-baseten Major
@livekit/agents-plugin-bey Major
@livekit/agents-plugin-cartesia Major
@livekit/agents-plugin-cerebras Major
@livekit/agents-plugin-deepgram Major
@livekit/agents-plugin-did Major
@livekit/agents-plugin-elevenlabs Major
@livekit/agents-plugin-fishaudio Major
@livekit/agents-plugin-google Major
@livekit/agents-plugin-hedra Major
@livekit/agents-plugin-hume Major
@livekit/agents-plugin-inworld Major
@livekit/agents-plugin-lemonslice Major
@livekit/agents-plugin-liveavatar Major
@livekit/agents-plugin-livekit Major
@livekit/agents-plugin-minimax Major
@livekit/agents-plugin-mistral Major
@livekit/agents-plugin-mistralai Major
@livekit/agents-plugin-neuphonic Major
@livekit/agents-plugin-openai Major
@livekit/agents-plugin-perplexity Major
@livekit/agents-plugin-phonic Major
@livekit/agents-plugin-resemble Major
@livekit/agents-plugin-rime Major
@livekit/agents-plugin-runway Major
@livekit/agents-plugin-sarvam Major
@livekit/agents-plugin-silero Major
@livekit/agents-plugin-soniox Major
@livekit/agents-plugin-tavus Major
@livekit/agents-plugin-trugen Major
@livekit/agents-plugin-xai Major
@livekit/agents-plugins-test Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

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

Open in Devin Review

Comment thread plugins/gnani/src/tts.ts

const audioBytes = Buffer.from(await response.arrayBuffer());
const requestId = shortuuid();
putFrames(this.queue, framesFromAudio(audioBytes, this.opts), requestId, requestId);

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.

🔴 REST text-to-speech produces corrupted audio because the file header is not stripped

The audio response is fed directly into the frame decoder (framesFromAudio(audioBytes, this.opts) at plugins/gnani/src/tts.ts:290) without first removing the container header, so the header bytes are treated as audio samples and produce a noise burst at the start of every synthesized utterance.

Impact: Every REST-mode TTS synthesis (the default mode) begins with audible corruption.

WAV header not stripped in REST path but correctly stripped in SSE and WebSocket paths

The decodeAudioChunk() function at plugins/gnani/src/tts.ts:166-168 strips the 44-byte WAV header when container === 'wav' (the default). The SSE path correctly calls decodeAudioChunk() before framesFromAudio() at plugins/gnani/src/tts.ts:342, and the WebSocket path does the same at plugins/gnani/src/tts.ts:481 and plugins/gnani/src/tts.ts:493. However, the REST path at line 290 skips decodeAudioChunk() entirely, passing raw response bytes (including the WAV header) directly to framesFromAudio(). The AudioByteStream inside framesFromAudio() (plugins/gnani/src/tts.ts:170-173) interprets every byte as PCM sample data, so the 44-byte RIFF/WAV header is decoded as audio samples, producing a short burst of noise/distortion at the beginning of the output.

Suggested change
putFrames(this.queue, framesFromAudio(audioBytes, this.opts), requestId, requestId);
putFrames(this.queue, framesFromAudio(decodeAudioChunk(audioBytes, this.opts), this.opts), requestId, requestId);
Open in Devin Review

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

Comment thread plugins/gnani/src/stt.ts
Comment on lines +298 to +302
try {
await Promise.race([this.sendAudio(ws), this.receiveMessages(ws)]);
} finally {
ws.close();
}

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 streaming race between send and receive may drop final transcripts

In SpeechStream.run() at plugins/gnani/src/stt.ts:299, sendAudio and receiveMessages are raced with Promise.race. When the audio input is exhausted, sendAudio completes and run() enters the finally block which calls ws.close(). This closes the WebSocket before receiveMessages has a chance to process any final transcript messages the server may still be sending. If the Gnani server sends the final transcript after receiving all audio but before the client-side close, those transcripts could be lost. Other STT plugins (e.g. Deepgram) typically send an explicit close/finalize message and wait for the server's response before closing. This depends on the Gnani WebSocket protocol — if the server always sends transcripts before the client finishes sending audio, this is fine.

Open in Devin Review

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

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.

0 participants