Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sarvam-realtime-stt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@livekit/agents-plugin-sarvam': minor
---

Add `STTRealtime`, a Sarvam realtime speech-to-text plugin (`saaras:v3-realtime`) with server-side VAD, partial/final transcript gating, and per-connection usage reporting. Ported from `livekit/agents` (Python) PR #6562. Note: this stream never reconnects after a socket failure — Sarvam bills per connection, so `stream()` forces `connOptions.maxRetry` to `0`.
22 changes: 22 additions & 0 deletions plugins/sarvam/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,28 @@ const stt = new sarvam.STT({

Set the `SARVAM_API_KEY` environment variable or pass `apiKey` directly.

### STT (Realtime)

```typescript
import * as sarvam from '@livekit/agents-plugin-sarvam';

const stt = new sarvam.STTRealtime({
language: 'en-IN',
streamType: 'balanced',
endpointing: 'vad',
});
```

`STTRealtime` connects to Sarvam's realtime API (`saaras:v3-realtime`, not configurable), which
streams partial transcripts and uses Sarvam's own VAD for turn detection by default. Set
`endpointing: 'manual'` to delimit turns from your application instead — the plugin then emits
`START_OF_SPEECH` on the first audio frame of a turn and `END_OF_SPEECH` when you flush the
stream.

Realtime streams don't reconnect after a socket failure, because Sarvam bills per connection —
`stream()` forces `connOptions.maxRetry` to `0`. Create a new stream (or restart the session) if
the connection drops.

## STT Models

| Model | Endpoint | Languages | Modes | Prompt |
Expand Down
80 changes: 80 additions & 0 deletions plugins/sarvam/etc/agents-plugin-sarvam.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,37 @@ export class ChunkedStream extends tts.ChunkedStream {
protected run(): Promise<void>;
}

// Warning: (ae-missing-release-tag) "RealtimeEncoding" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export type RealtimeEncoding = 'linear16' | 'linear32' | 'mulaw' | 'alaw';

// Warning: (ae-missing-release-tag) "RealtimeEndpointing" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export type RealtimeEndpointing = 'vad' | 'manual';

// Warning: (ae-forgotten-export) The symbol "stt" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "RealtimeSpeechStream" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export class RealtimeSpeechStream extends stt.SpeechStream {
// Warning: (ae-forgotten-export) The symbol "ResolvedRealtimeOptions" needs to be exported by the entry point index.d.ts
constructor(sttInstance: STTRealtime, opts: ResolvedRealtimeOptions, connOptions: APIConnectOptions, onClose?: () => void);
// (undocumented)
close(): void;
// (undocumented)
label: string;
// (undocumented)
protected run(): Promise<void>;
updateOptions(opts: Partial<STTRealtimeOptions>): void;
}

// Warning: (ae-missing-release-tag) "RealtimeStreamType" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export type RealtimeStreamType = 'fast' | 'balanced' | 'simulated';

// Warning: (ae-missing-release-tag) "SpeechStream" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
Expand Down Expand Up @@ -84,6 +114,56 @@ export type STTModes = 'transcribe' | 'translate' | 'verbatim' | 'translit' | 'c
// @public
export type STTOptions = STTV2Options | STTTranslateOptions | STTV3Options;

// Warning: (ae-missing-release-tag) "STTRealtime" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export class STTRealtime extends stt.STT {
constructor(opts?: Partial<STTRealtimeOptions>);
// (undocumented)
label: string;
// (undocumented)
get model(): string;
// (undocumented)
get provider(): string;
// (undocumented)
_recognize(): Promise<stt.SpeechEvent>;
// (undocumented)
stream(options?: {
language?: string;
connOptions?: APIConnectOptions;
}): RealtimeSpeechStream;
updateOptions(opts: Partial<STTRealtimeOptions>): void;
}

// Warning: (ae-missing-release-tag) "STTRealtimeLanguages" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export type STTRealtimeLanguages = 'auto' | 'as-IN' | 'bn-IN' | 'brx-IN' | 'doi-IN' | 'en-IN' | 'gu-IN' | 'hi-IN' | 'kn-IN' | 'kok-IN' | 'ks-IN' | 'mai-IN' | 'ml-IN' | 'mni-IN' | 'mr-IN' | 'ne-IN' | 'or-IN' | 'pa-IN' | 'sa-IN' | 'sat-IN' | 'sd-IN' | 'ta-IN' | 'te-IN' | 'ur-IN';

// Warning: (ae-missing-release-tag) "STTRealtimeModel" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export type STTRealtimeModel = 'saaras:v3-realtime';

// Warning: (ae-missing-release-tag) "STTRealtimeOptions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export interface STTRealtimeOptions {
apiKey?: string;
encoding?: RealtimeEncoding;
endpointing?: RealtimeEndpointing;
language?: STTRealtimeLanguages | string;
mode?: STTModes | string;
prompt?: string;
returnTimestamps?: boolean;
sampleRate?: number;
streamType?: RealtimeStreamType;
vadMinSilenceMs?: number;
vadMinSpeechMs?: number;
vadPrefixPaddingMs?: number;
vadSotThreshold?: number;
}

// Warning: (ae-forgotten-export) The symbol "STTBaseOptions" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "STTTranslateOptions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
Expand Down
39 changes: 39 additions & 0 deletions plugins/sarvam/src/_utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2026 LiveKit, Inc.
//
// SPDX-License-Identifier: Apache-2.0

/** Accumulates pushed numeric values and reports the total on a fixed interval. */
export class PeriodicCollector {
private duration: number;
private callback: (value: number) => void;
private lastFlushTime: number;
private total: number | null = null;

/**
* @param callback - function to call with the accumulated value when the duration expires
* @param options - options object
*/
constructor(callback: (value: number) => void, options: { duration: number }) {
this.duration = options.duration;
this.callback = callback;
this.lastFlushTime = performance.now() / 1000;
}

/** Add a value to the accumulator. */
push(value: number): void {
this.total = this.total === null ? value : this.total + value;

if (performance.now() / 1000 - this.lastFlushTime >= this.duration) {
this.flush();
}
}

/** Force the callback to be called with the current total if non-zero. */
flush(): void {
if (this.total !== null) {
this.callback(this.total);
this.total = null;
}
this.lastFlushTime = performance.now() / 1000;
}
}
1 change: 1 addition & 0 deletions plugins/sarvam/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export {
type STTTranslateOptions,
type STTV3Options,
} from './stt.js';
export { STTRealtime, RealtimeSpeechStream, type STTRealtimeOptions } from './stt_realtime.js';
export {
ChunkedStream,
SynthesizeStream,
Expand Down
53 changes: 53 additions & 0 deletions plugins/sarvam/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,56 @@ export type STTV3Languages =

/** All supported STT language codes */
export type STTLanguages = STTV2Languages | STTV3Languages;

// ---------------------------------------------------------------------------
// Realtime STT model types
// ---------------------------------------------------------------------------

/**
* Model used by {@link STTRealtime}. Pinned to Sarvam's realtime API and not configurable.
*
* @see {@link https://docs.sarvam.ai/api-reference/speech-to-text/transcribe/realtime/ws | Sarvam realtime STT WebSocket docs}
*/
export type STTRealtimeModel = 'saaras:v3-realtime';

/** Latency profile for the realtime stream. */
export type RealtimeStreamType = 'fast' | 'balanced' | 'simulated';

/** How turn boundaries are determined on the realtime API. */
export type RealtimeEndpointing = 'vad' | 'manual';

/** Wire audio encodings accepted by the realtime API. */
export type RealtimeEncoding = 'linear16' | 'linear32' | 'mulaw' | 'alaw';

/**
* Languages supported by the realtime API (BCP-47), plus `'auto'` for adaptive language
* identification.
*
* @remarks
* Odia is `or-IN` on this API, unlike the legacy {@link STTLanguages} which uses `od-IN`.
*/
export type STTRealtimeLanguages =
| 'auto'
| 'as-IN'
| 'bn-IN'
| 'brx-IN'
| 'doi-IN'
| 'en-IN'
| 'gu-IN'
| 'hi-IN'
| 'kn-IN'
| 'kok-IN'
| 'ks-IN'
| 'mai-IN'
| 'ml-IN'
| 'mni-IN'
| 'mr-IN'
| 'ne-IN'
| 'or-IN'
| 'pa-IN'
| 'sa-IN'
| 'sat-IN'
| 'sd-IN'
| 'ta-IN'
| 'te-IN'
| 'ur-IN';
Loading