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/green-gnani-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@livekit/agents-plugin-gnani': minor
---

Add the Gnani Vachana STT/TTS plugin for LiveKit Agents JS.
174 changes: 174 additions & 0 deletions plugins/gnani/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# @livekit/agents-plugin-gnani

[LiveKit Agents](https://github.com/livekit/agents-js) plugin for **[Gnani](https://gnani.ai/)**: high-accuracy Speech-to-Text (Prisma) and low-latency Text-to-Speech (Timbre) for Indian languages.

> [Gnani.ai](https://gnani.ai) featuring **Prisma** (STT) and **Timbre** (TTS) models, supporting 10+ Indian languages with real-time streaming, multilingual transcription, and code-switching capabilities.

## Installation

```bash
pnpm add @livekit/agents-plugin-gnani
```

## Prerequisites

You need a Gnani API key. Email **[speechstack@gnani.ai](mailto:speechstack@gnani.ai)** to get started; all new accounts receive free credits, no credit card required.

### Authentication

All APIs require a single API key: no `organizationId` or `userId` needed.

**Option 1: Environment variable (recommended):**

```bash
export GNANI_API_KEY="your-api-key"
```

**Option 2: Constructor argument:**

```ts
const stt = new gnani.STT({ apiKey: 'your-api-key', language: 'hi-IN' });
const tts = new gnani.TTS({ apiKey: 'your-api-key' });
```

> **Migration note:** If upgrading from an earlier version, remove any `organizationId` and `userId` parameters; they are no longer accepted.

## Quick Start

### Speech-to-Text (REST + Streaming)

```ts
import * as gnani from '@livekit/agents-plugin-gnani';

const stt = new gnani.STT({ language: 'hi-IN' });

// REST STT (file-based transcription)
const speechEvent = await stt.recognize(audioBuffer);

// Streaming STT (real-time WebSocket)
const speechStream = stt.stream();
```

### Text-to-Speech

```ts
import * as gnani from '@livekit/agents-plugin-gnani';

// REST (default): single-request batch synthesis
const ttsRest = new gnani.TTS({ voice: 'Karan' });

// SSE: chunked synthesis via Server-Sent Events (lower latency)
const ttsSse = new gnani.TTS({ voice: 'Karan', synthesizeMethod: 'sse' });

// WebSocket: chunked synthesis over WS (lowest latency)
const ttsWs = new gnani.TTS({ voice: 'Karan', synthesizeMethod: 'websocket' });
```

All three modes work with the standard LiveKit voice agent pipeline. The `synthesizeMethod` controls which transport `synthesize()` uses (REST, SSE, or WebSocket). The `stream()` method always uses WebSocket regardless of this setting.

## Full Constructor Reference

### STT: All Parameters

```ts
const stt = new gnani.STT({
language: 'en-IN', // Default: 'en-IN'
sampleRate: 16000, // Default: 16000 (also: 8000)
format: 'verbatim', // Default: 'verbatim' (also: 'transcribe')
preferredLanguage: undefined, // Default: undefined
itnNativeNumerals: false, // Default: false
apiKey: undefined, // Default: reads GNANI_API_KEY env var
baseURL: 'https://api.vachana.ai', // Default
});
```

### TTS: All Parameters

```ts
const tts = new gnani.TTS({
voice: 'Karan', // Default: 'Karan' (also: Simran, Nara, Riya, Viraj, Raju)
model: 'vachana-voice-v3', // Default: 'vachana-voice-v3'
sampleRate: 16000, // Default: 16000 (also: 8000, 22050, 44100)
encoding: 'linear_pcm', // Default: 'linear_pcm' (also: 'oggopus')
container: 'wav', // Default: 'wav' (also: 'raw', 'mp3', 'mulaw', 'ogg')
numChannels: 1, // Default: 1
bitrate: undefined, // Default: undefined (also: '96k', '128k', '192k')
synthesizeMethod: 'rest', // Default: 'rest' (also: 'sse', 'websocket')
apiKey: undefined, // Default: reads GNANI_API_KEY env var
baseURL: 'https://api.vachana.ai', // Default
});
```

## Features

### STT (Prisma)

- **REST recognition**: REST API (`POST /stt/v3`) for file-based transcription
- **Real-time streaming**: WebSocket API (`wss://api.vachana.ai/stt/v3/stream`) for live audio transcription with VAD
- **10+ Indian languages**: see [supported language codes](https://docs.gnani.ai/api/STT/stt-websocket#supported-languages)
- **Code-switching**: supports multilingual and code-mixed audio
- **Sample rates**: 8 kHz and 16 kHz
- **ITN support**: Inverse Text Normalization via `format: 'transcribe'`

#### Streaming PCM Specification

All streaming audio must be sent as **raw PCM binary frames**: no container format (WAV, MP3) mid-stream.

| Property | 16 kHz | 8 kHz |
| ------------------- | --------------------------------------- | --------------------------------------- |
| Encoding | PCM signed 16-bit little-endian | PCM signed 16-bit little-endian |
| Sample Rate | 16,000 Hz | 8,000 Hz |
| Channels | 1 (mono) | 1 (mono) |
| Samples per chunk | 512 | 512 |
| **Bytes per frame** | **1,024 bytes** (512 samples x 2 bytes) | **1,024 bytes** (512 samples x 2 bytes) |
| Frame duration | 32 ms | 64 ms |

Frames must be sent at **real-time cadence**. See **[STT Realtime: PCM Specification](https://docs.gnani.ai/api/STT/stt-websocket#pcm-specification)** for full details.

### TTS (Timbre)

- **REST synthesis**: single-request batch audio generation (`synthesizeMethod: 'rest'`)
- **SSE streaming**: lower-latency chunked synthesis via Server-Sent Events (`synthesizeMethod: 'sse'`)
- **WebSocket synthesis**: lowest-latency synthesis via `synthesizeMethod: 'websocket'` or the `stream()` method
- **6 voices**: Karan, Simran, Nara, Riya, Viraj, Raju
- **Configurable output**: sample rate (8000-44100), encoding (linear_pcm, oggopus), container (raw, mp3, wav, mulaw, ogg)
- **Runtime updates**: change voice or model via `updateOptions()`

## Supported Languages

### STT Languages (Prisma)

Prisma uses BCP-47 locale codes (e.g. `hi-IN`). Supported:

- **[STT REST: Supported Languages](https://docs.gnani.ai/api/STT/speech-to-text#supported-languages)**
- **[STT Realtime: Supported Languages](https://docs.gnani.ai/api/STT/stt-websocket#supported-languages)**

### TTS Languages (Timbre)

For the full list of supported languages, see **[TTS: Supported Languages](https://docs.gnani.ai/api/TTS/tts-inference#supported-languages)**.

## Available Voices

| Voice | ID | Gender | Description |
| ------ | -------- | ------ | ------------------------ |
| Karan | `Karan` | Male | Bold, Trustworthy |
| Simran | `Simran` | Female | Confident, Bright |
| Nara | `Nara` | Female | Gentle, Expressive |
| Riya | `Riya` | Female | Cheerful, Energetic |
| Viraj | `Viraj` | Male | Commanding, Dynamic |
| Raju | `Raju` | Male | Grounded, Conversational |

## Architecture

This plugin directly implements the Gnani REST, SSE, and WebSocket APIs using `fetch` and `ws`, adapting them into LiveKit's `stt.STT` and `tts.TTS` base classes. It uses the **Prisma** model for speech-to-text and the **Timbre** model for text-to-speech. No external SDK is required; all connection logic, authentication, and audio format handling is self-contained. Authentication uses a single `apiKey` passed via the `X-API-Key-ID` header.

## Documentation

- [Gnani API Docs](https://docs.gnani.ai/)
- [LiveKit Agents Docs](https://docs.livekit.io/agents/)
- [Gnani STT Plugin Guide](https://docs.livekit.io/agents/integrations/stt/gnani/)
- [Gnani TTS Plugin Guide](https://docs.livekit.io/agents/integrations/tts/gnani/)

## License

Apache-2.0
5 changes: 5 additions & 0 deletions plugins/gnani/api-extractor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"extends": "../../api-extractor-shared.json",
"mainEntryPointFilePath": "./dist/index.d.ts"
}
118 changes: 118 additions & 0 deletions plugins/gnani/etc/agents-plugin-gnani.api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
## API Report File for "@livekit/agents-plugin-gnani"

> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).

```ts

import { APIConnectOptions } from '@livekit/agents';
import { AudioBuffer as AudioBuffer_2 } from '@livekit/agents';
import { stt } from '@livekit/agents';
import { tts } from '@livekit/agents';

// @public (undocumented)
export class SpeechStream extends stt.SpeechStream {
constructor(sttInstance: STT, opts: ResolvedSTTOptions, connOptions?: APIConnectOptions);
// (undocumented)
buildWsUrl(): string;
// (undocumented)
label: string;
// Warning: (ae-forgotten-export) The symbol "ResolvedSTTOptions" needs to be exported by the entry point index.d.ts
//
// (undocumented)
_opts: ResolvedSTTOptions;
// (undocumented)
protected run(): Promise<void>;
}

// @public (undocumented)
export class STT extends stt.STT {
constructor(opts?: STTOptions & Record<string, unknown>);
// (undocumented)
label: string;
// (undocumented)
get model(): string;
// (undocumented)
_opts: ResolvedSTTOptions;
// (undocumented)
get provider(): string;
// (undocumented)
protected _recognize(buffer: AudioBuffer_2, abortSignal?: AbortSignal): Promise<stt.SpeechEvent>;
// (undocumented)
stream(options?: {
language?: string;
connOptions?: APIConnectOptions;
}): SpeechStream;
}

// @public (undocumented)
export interface STTOptions {
apiKey?: string;
baseURL?: string;
// Warning: (ae-forgotten-export) The symbol "GnaniSTTFormat" needs to be exported by the entry point index.d.ts
format?: GnaniSTTFormat;
itnNativeNumerals?: boolean;
// Warning: (ae-forgotten-export) The symbol "GnaniSTTLanguages" needs to be exported by the entry point index.d.ts
language?: GnaniSTTLanguages | string;
preferredLanguage?: string;
// Warning: (ae-forgotten-export) The symbol "SAMPLE_RATE_8K" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "SAMPLE_RATE_16K" needs to be exported by the entry point index.d.ts
sampleRate?: typeof SAMPLE_RATE_8K | typeof SAMPLE_RATE_16K | number;
}

// @public (undocumented)
export class SynthesizeStream extends tts.SynthesizeStream {
// Warning: (ae-forgotten-export) The symbol "ResolvedTTSOptions" needs to be exported by the entry point index.d.ts
constructor(ttsInstance: TTS, opts: ResolvedTTSOptions, connOptions?: APIConnectOptions);
// (undocumented)
buildWsUrl(): string;
// (undocumented)
label: string;
// (undocumented)
protected run(): Promise<void>;
}

// @public (undocumented)
export class TTS extends tts.TTS {
constructor(opts?: TTSOptions & Record<string, unknown>);
// (undocumented)
label: string;
// (undocumented)
get model(): string;
// (undocumented)
_opts: ResolvedTTSOptions;
// (undocumented)
get provider(): string;
// (undocumented)
stream(options?: {
connOptions?: APIConnectOptions;
}): SynthesizeStream;
// Warning: (ae-forgotten-export) The symbol "ChunkedStream" needs to be exported by the entry point index.d.ts
//
// (undocumented)
synthesize(text: string, connOptions?: APIConnectOptions, abortSignal?: AbortSignal): ChunkedStream;
// (undocumented)
updateOptions(opts: Partial<TTSOptions> & Record<string, unknown>): void;
}

// @public (undocumented)
export interface TTSOptions {
apiKey?: string;
baseURL?: string;
// Warning: (ae-forgotten-export) The symbol "GnaniTTSBitrates" needs to be exported by the entry point index.d.ts
bitrate?: GnaniTTSBitrates | string;
// Warning: (ae-forgotten-export) The symbol "GnaniTTSContainers" needs to be exported by the entry point index.d.ts
container?: GnaniTTSContainers | string;
// Warning: (ae-forgotten-export) The symbol "GnaniTTSEncodings" needs to be exported by the entry point index.d.ts
encoding?: GnaniTTSEncodings | string;
model?: string;
numChannels?: number;
sampleRate?: number;
// Warning: (ae-forgotten-export) The symbol "GnaniTTSSynthesizeMethod" needs to be exported by the entry point index.d.ts
synthesizeMethod?: GnaniTTSSynthesizeMethod;
// Warning: (ae-forgotten-export) The symbol "GnaniTTSVoices" needs to be exported by the entry point index.d.ts
voice?: GnaniTTSVoices | string;
}

// (No @packageDocumentation comment for this package)

```
51 changes: 51 additions & 0 deletions plugins/gnani/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "@livekit/agents-plugin-gnani",
"version": "1.5.0",
"description": "Gnani Vachana plugin for LiveKit Node Agents",
"main": "dist/index.js",
"require": "dist/index.cjs",
"types": "dist/index.d.ts",
"exports": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"author": "LiveKit",
"type": "module",
"repository": "git@github.com:livekit/agents-js.git",
"license": "Apache-2.0",
"files": [
"dist",
"src",
"README.md"
],
"scripts": {
"build": "tsup --onSuccess \"pnpm build:types\"",
"build:types": "tsc --declaration --emitDeclarationOnly && node ../../scripts/copyDeclarationOutput.js",
"clean": "rm -rf dist",
"clean:build": "pnpm clean && pnpm build",
"lint": "eslint -f unix \"src/**/*.{ts,js}\"",
"api:check": "api-extractor run --typescript-compiler-folder ../../node_modules/typescript",
"api:update": "api-extractor run --local --typescript-compiler-folder ../../node_modules/typescript --verbose"
},
"devDependencies": {
"@livekit/agents": "workspace:*",
"@livekit/rtc-node": "catalog:",
"@microsoft/api-extractor": "^7.35.0",
"@types/ws": "catalog:",
"tsup": "^8.3.5",
"typescript": "^5.0.0"
},
"dependencies": {
"ws": "catalog:"
},
"peerDependencies": {
"@livekit/agents": "workspace:*",
"@livekit/rtc-node": "catalog:"
}
}
19 changes: 19 additions & 0 deletions plugins/gnani/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2026 LiveKit, Inc.
//
// SPDX-License-Identifier: Apache-2.0
import { Plugin } from '@livekit/agents';

export { SpeechStream, STT, type STTOptions } from './stt.js';
export { SynthesizeStream, TTS, type TTSOptions } from './tts.js';

class GnaniPlugin extends Plugin {
constructor() {
super({
title: 'gnani',
version: __PACKAGE_VERSION__,
package: __PACKAGE_NAME__,
});
}
}

Plugin.registerPlugin(new GnaniPlugin());
Loading
Loading