livekit-plugin-parrot-stt 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -0
- package/dist/index.cjs +303 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +89 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.js +280 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
- package/src/index.ts +18 -0
- package/src/models.ts +121 -0
- package/src/stt.ts +280 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { Plugin } from "@livekit/agents";
|
|
3
|
+
|
|
4
|
+
// src/stt.ts
|
|
5
|
+
import {
|
|
6
|
+
AudioByteStream,
|
|
7
|
+
asLanguageCode,
|
|
8
|
+
log,
|
|
9
|
+
stt
|
|
10
|
+
} from "@livekit/agents";
|
|
11
|
+
import { WebSocket } from "ws";
|
|
12
|
+
|
|
13
|
+
// src/models.ts
|
|
14
|
+
var envInt = (name, fallback) => {
|
|
15
|
+
const v = process.env[name];
|
|
16
|
+
if (v === void 0 || v === "") return fallback;
|
|
17
|
+
const n = Number(v);
|
|
18
|
+
return Number.isFinite(n) ? n : fallback;
|
|
19
|
+
};
|
|
20
|
+
var defaultRinggSTTOptions = {
|
|
21
|
+
url: process.env.RINGG_STT_URL ?? "wss://prod-api.ringg.ai/stt/v1/stream",
|
|
22
|
+
apiKey: process.env.RINGG_STT_API_KEY ?? "",
|
|
23
|
+
language: process.env.RINGG_STT_LANGUAGE ?? "en",
|
|
24
|
+
sampleRate: envInt("RINGG_STT_SAMPLE_RATE", 16e3),
|
|
25
|
+
encoding: process.env.RINGG_STT_ENCODING ?? "int16",
|
|
26
|
+
mode: process.env.RINGG_STT_MODE ?? "stream",
|
|
27
|
+
enableCapPunc: true,
|
|
28
|
+
vadTailSilMs: 200,
|
|
29
|
+
vadConfidence: 0.55,
|
|
30
|
+
acceptClientVadEvents: false,
|
|
31
|
+
keepaliveIntervalMs: 3e4,
|
|
32
|
+
finalizeGraceMs: 1500
|
|
33
|
+
};
|
|
34
|
+
var buildStartPayload = (opts) => ({
|
|
35
|
+
type: "start",
|
|
36
|
+
sample_rate: opts.sampleRate,
|
|
37
|
+
encoding: opts.encoding,
|
|
38
|
+
language: opts.language,
|
|
39
|
+
mode: opts.mode,
|
|
40
|
+
vad_tail_sil_ms: opts.vadTailSilMs,
|
|
41
|
+
vad_confidence: opts.vadConfidence,
|
|
42
|
+
enable_cap_punc: opts.enableCapPunc,
|
|
43
|
+
accept_client_vad_events: opts.acceptClientVadEvents,
|
|
44
|
+
api_key: opts.apiKey
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// src/stt.ts
|
|
48
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
49
|
+
var STT = class extends stt.STT {
|
|
50
|
+
label = "ringg.STT";
|
|
51
|
+
#opts;
|
|
52
|
+
constructor(opts = {}) {
|
|
53
|
+
super({ streaming: true, interimResults: true });
|
|
54
|
+
this.#opts = { ...defaultRinggSTTOptions, ...opts };
|
|
55
|
+
if (!this.#opts.apiKey) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
"Ringg STT: API key is required \u2014 set $RINGG_STT_API_KEY or pass { apiKey }."
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
if (!this.#opts.url) {
|
|
61
|
+
throw new Error("Ringg STT: url is required \u2014 set $RINGG_STT_URL or pass { url }.");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
get provider() {
|
|
65
|
+
return "ringg";
|
|
66
|
+
}
|
|
67
|
+
get model() {
|
|
68
|
+
return `ringg-parrot-${this.#opts.language}`;
|
|
69
|
+
}
|
|
70
|
+
updateOptions(opts) {
|
|
71
|
+
this.#opts = { ...this.#opts, ...opts };
|
|
72
|
+
}
|
|
73
|
+
async _recognize(_frame) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
"Ringg STT plugin is streaming-only; use stream(). (Ringg also exposes a REST /transcriptions endpoint, but it is not wired into this plugin.)"
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
stream(options) {
|
|
79
|
+
return new SpeechStream(this, this.#opts, options?.connOptions);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
var SpeechStream = class _SpeechStream extends stt.SpeechStream {
|
|
83
|
+
label = "ringg.SpeechStream";
|
|
84
|
+
#opts;
|
|
85
|
+
#logger = log();
|
|
86
|
+
#speaking = false;
|
|
87
|
+
#requestId;
|
|
88
|
+
constructor(sttImpl, opts, connOptions) {
|
|
89
|
+
super(sttImpl, opts.sampleRate, connOptions);
|
|
90
|
+
this.#opts = opts;
|
|
91
|
+
}
|
|
92
|
+
async run() {
|
|
93
|
+
const ws = new WebSocket(this.#opts.url);
|
|
94
|
+
ws.binaryType = "nodebuffer";
|
|
95
|
+
await new Promise((resolve, reject) => {
|
|
96
|
+
const onErr = (err) => reject(err);
|
|
97
|
+
ws.once("open", () => {
|
|
98
|
+
ws.off("error", onErr);
|
|
99
|
+
resolve();
|
|
100
|
+
});
|
|
101
|
+
ws.once("error", onErr);
|
|
102
|
+
});
|
|
103
|
+
ws.send(JSON.stringify(buildStartPayload(this.#opts)));
|
|
104
|
+
await new Promise((resolve, reject) => {
|
|
105
|
+
const onMsg = (data, isBinary) => {
|
|
106
|
+
if (isBinary) return;
|
|
107
|
+
let ev;
|
|
108
|
+
try {
|
|
109
|
+
ev = JSON.parse(data.toString());
|
|
110
|
+
} catch {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (ev.type === "ready") {
|
|
114
|
+
this.#requestId = ev.request_id;
|
|
115
|
+
cleanup();
|
|
116
|
+
resolve();
|
|
117
|
+
} else if (ev.type === "error") {
|
|
118
|
+
cleanup();
|
|
119
|
+
reject(
|
|
120
|
+
new Error(
|
|
121
|
+
`Ringg STT: handshake rejected \u2014 ${ev.detail ?? "unknown error"}`
|
|
122
|
+
)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const onClose = (code) => {
|
|
127
|
+
cleanup();
|
|
128
|
+
reject(new Error(`Ringg STT: connection closed during handshake (code ${code})`));
|
|
129
|
+
};
|
|
130
|
+
const cleanup = () => {
|
|
131
|
+
ws.off("message", onMsg);
|
|
132
|
+
ws.off("close", onClose);
|
|
133
|
+
};
|
|
134
|
+
ws.on("message", onMsg);
|
|
135
|
+
ws.once("close", onClose);
|
|
136
|
+
});
|
|
137
|
+
this.#logger.child({ url: this.#opts.url, language: this.#opts.language, requestId: this.#requestId }).debug("ringg stt: connected");
|
|
138
|
+
let closed = false;
|
|
139
|
+
const closeWs = () => {
|
|
140
|
+
if (!closed) {
|
|
141
|
+
closed = true;
|
|
142
|
+
try {
|
|
143
|
+
ws.close();
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
let keepalive;
|
|
149
|
+
if (this.#opts.keepaliveIntervalMs > 0) {
|
|
150
|
+
keepalive = setInterval(() => {
|
|
151
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
152
|
+
try {
|
|
153
|
+
ws.send(JSON.stringify({ type: "ping" }));
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}, this.#opts.keepaliveIntervalMs);
|
|
158
|
+
}
|
|
159
|
+
const recvTask = new Promise((resolve, reject) => {
|
|
160
|
+
ws.on("message", (data, isBinary) => {
|
|
161
|
+
if (isBinary) return;
|
|
162
|
+
let ev;
|
|
163
|
+
try {
|
|
164
|
+
ev = JSON.parse(data.toString());
|
|
165
|
+
} catch {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
this.#handleEvent(ev);
|
|
169
|
+
});
|
|
170
|
+
ws.once("close", () => resolve());
|
|
171
|
+
ws.once("error", (err) => reject(err));
|
|
172
|
+
});
|
|
173
|
+
const sendTask = async () => {
|
|
174
|
+
const samples20ms = Math.floor(this.#opts.sampleRate / 50);
|
|
175
|
+
const bstream = new AudioByteStream(this.#opts.sampleRate, 1, samples20ms);
|
|
176
|
+
const shipFrames = (frames) => {
|
|
177
|
+
for (const f of frames) {
|
|
178
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
179
|
+
ws.send(new Uint8Array(f.data.buffer, f.data.byteOffset, f.data.byteLength));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
for (; ; ) {
|
|
184
|
+
if (closed) break;
|
|
185
|
+
const { done, value } = await this.input.next();
|
|
186
|
+
if (done) break;
|
|
187
|
+
if (value === _SpeechStream.FLUSH_SENTINEL) {
|
|
188
|
+
shipFrames(bstream.flush());
|
|
189
|
+
} else {
|
|
190
|
+
shipFrames(bstream.write(value.data.buffer));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
shipFrames(bstream.flush());
|
|
194
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
195
|
+
ws.send(JSON.stringify({ type: "end" }));
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
const abortPromise = new Promise((resolve) => {
|
|
199
|
+
const signal = this.abortController.signal;
|
|
200
|
+
if (signal.aborted) return resolve();
|
|
201
|
+
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
202
|
+
});
|
|
203
|
+
const finiteInputPath = (async () => {
|
|
204
|
+
await sendTask();
|
|
205
|
+
await sleep(this.#opts.finalizeGraceMs);
|
|
206
|
+
})();
|
|
207
|
+
try {
|
|
208
|
+
await Promise.race([abortPromise, recvTask, finiteInputPath]);
|
|
209
|
+
} finally {
|
|
210
|
+
if (keepalive) clearInterval(keepalive);
|
|
211
|
+
closeWs();
|
|
212
|
+
await recvTask.catch(() => {
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
#handleEvent(ev) {
|
|
217
|
+
switch (ev.type) {
|
|
218
|
+
case "transcript": {
|
|
219
|
+
const t = ev;
|
|
220
|
+
const text = (t.transcription ?? "").trim();
|
|
221
|
+
if (!text) return;
|
|
222
|
+
if (!this.#speaking) {
|
|
223
|
+
this.#speaking = true;
|
|
224
|
+
this.queue.put({ type: stt.SpeechEventType.START_OF_SPEECH });
|
|
225
|
+
}
|
|
226
|
+
const data = {
|
|
227
|
+
language: asLanguageCode(t.language ?? this.#opts.language),
|
|
228
|
+
text,
|
|
229
|
+
startTime: 0,
|
|
230
|
+
endTime: 0,
|
|
231
|
+
confidence: 1
|
|
232
|
+
};
|
|
233
|
+
if (t.is_final) {
|
|
234
|
+
this.queue.put({
|
|
235
|
+
type: stt.SpeechEventType.FINAL_TRANSCRIPT,
|
|
236
|
+
alternatives: [data],
|
|
237
|
+
requestId: this.#requestId
|
|
238
|
+
});
|
|
239
|
+
if (this.#speaking) {
|
|
240
|
+
this.#speaking = false;
|
|
241
|
+
this.queue.put({ type: stt.SpeechEventType.END_OF_SPEECH });
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
this.queue.put({
|
|
245
|
+
type: stt.SpeechEventType.INTERIM_TRANSCRIPT,
|
|
246
|
+
alternatives: [data],
|
|
247
|
+
requestId: this.#requestId
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
case "error": {
|
|
253
|
+
this.#logger.child({ event: ev }).error("ringg stt: server error");
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
// ready (duplicate), ack, pong, and unknown types are ignored
|
|
257
|
+
default:
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// src/index.ts
|
|
264
|
+
var RinggPlugin = class extends Plugin {
|
|
265
|
+
constructor() {
|
|
266
|
+
super({
|
|
267
|
+
title: "ringg",
|
|
268
|
+
version: "0.1.0",
|
|
269
|
+
package: "livekit-plugin-parrot-stt"
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
Plugin.registerPlugin(new RinggPlugin());
|
|
274
|
+
export {
|
|
275
|
+
STT,
|
|
276
|
+
SpeechStream,
|
|
277
|
+
buildStartPayload,
|
|
278
|
+
defaultRinggSTTOptions
|
|
279
|
+
};
|
|
280
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/stt.ts","../src/models.ts"],"sourcesContent":["// livekit-plugin-parrot-stt — Ringg Parrot STT for LiveKit Agents (Node.js)\n\nimport { Plugin } from '@livekit/agents';\n\nexport * from './stt.js';\nexport * from './models.js';\n\nclass RinggPlugin extends Plugin {\n constructor() {\n super({\n title: 'ringg',\n version: '0.1.0',\n package: 'livekit-plugin-parrot-stt',\n });\n }\n}\n\nPlugin.registerPlugin(new RinggPlugin());\n","// Ringg Parrot STT plugin for LiveKit Agents.\n//\n// Implements the streaming STT interface (`stt.STT` + `stt.SpeechStream`) by\n// speaking the Ringg WebSocket protocol directly (there is no JS SDK).\n\nimport {\n type APIConnectOptions,\n type AudioBuffer,\n AudioByteStream,\n asLanguageCode,\n log,\n stt,\n} from '@livekit/agents';\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport { type RawData, WebSocket } from 'ws';\nimport {\n type RinggSTTOptions,\n type RinggServerEvent,\n type RinggTranscriptEvent,\n buildStartPayload,\n defaultRinggSTTOptions,\n} from './models.js';\n\nconst sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));\n\nexport class STT extends stt.STT {\n label = 'ringg.STT';\n #opts: RinggSTTOptions;\n\n constructor(opts: Partial<RinggSTTOptions> = {}) {\n super({ streaming: true, interimResults: true });\n this.#opts = { ...defaultRinggSTTOptions, ...opts };\n if (!this.#opts.apiKey) {\n throw new Error(\n 'Ringg STT: API key is required — set $RINGG_STT_API_KEY or pass { apiKey }.',\n );\n }\n if (!this.#opts.url) {\n throw new Error('Ringg STT: url is required — set $RINGG_STT_URL or pass { url }.');\n }\n }\n\n get provider(): string {\n return 'ringg';\n }\n\n get model(): string {\n return `ringg-parrot-${this.#opts.language}`;\n }\n\n updateOptions(opts: Partial<RinggSTTOptions>): void {\n this.#opts = { ...this.#opts, ...opts };\n }\n\n async _recognize(_frame: AudioBuffer): Promise<stt.SpeechEvent> {\n throw new Error(\n 'Ringg STT plugin is streaming-only; use stream(). ' +\n '(Ringg also exposes a REST /transcriptions endpoint, but it is not wired into this plugin.)',\n );\n }\n\n stream(options?: { connOptions?: APIConnectOptions }): stt.SpeechStream {\n return new SpeechStream(this, this.#opts, options?.connOptions);\n }\n}\n\nexport class SpeechStream extends stt.SpeechStream {\n label = 'ringg.SpeechStream';\n #opts: RinggSTTOptions;\n #logger = log();\n #speaking = false;\n #requestId?: string;\n\n constructor(sttImpl: STT, opts: RinggSTTOptions, connOptions?: APIConnectOptions) {\n // Passing sampleRate as `neededSampleRate` makes the base class resample\n // incoming (typically 48kHz) LiveKit frames down to Ringg's rate for us.\n super(sttImpl, opts.sampleRate, connOptions);\n this.#opts = opts;\n }\n\n protected async run(): Promise<void> {\n const ws = new WebSocket(this.#opts.url);\n ws.binaryType = 'nodebuffer';\n\n // 1) wait for the socket to open\n await new Promise<void>((resolve, reject) => {\n const onErr = (err: Error) => reject(err);\n ws.once('open', () => {\n ws.off('error', onErr);\n resolve();\n });\n ws.once('error', onErr);\n });\n\n // 2) send the `start` handshake and wait for the `ready` reply\n ws.send(JSON.stringify(buildStartPayload(this.#opts)));\n await new Promise<void>((resolve, reject) => {\n const onMsg = (data: RawData, isBinary: boolean) => {\n if (isBinary) return;\n let ev: RinggServerEvent;\n try {\n ev = JSON.parse(data.toString());\n } catch {\n return;\n }\n if (ev.type === 'ready') {\n this.#requestId = (ev as { request_id?: string }).request_id;\n cleanup();\n resolve();\n } else if (ev.type === 'error') {\n cleanup();\n reject(\n new Error(\n `Ringg STT: handshake rejected — ${(ev as { detail?: string }).detail ?? 'unknown error'}`,\n ),\n );\n }\n };\n const onClose = (code: number) => {\n cleanup();\n reject(new Error(`Ringg STT: connection closed during handshake (code ${code})`));\n };\n const cleanup = () => {\n ws.off('message', onMsg);\n ws.off('close', onClose);\n };\n ws.on('message', onMsg);\n ws.once('close', onClose);\n });\n\n this.#logger\n .child({ url: this.#opts.url, language: this.#opts.language, requestId: this.#requestId })\n .debug('ringg stt: connected');\n\n let closed = false;\n const closeWs = () => {\n if (!closed) {\n closed = true;\n try {\n ws.close();\n } catch {\n /* ignore */\n }\n }\n };\n\n // keepalive: application-level JSON ping (Ringg replies with `pong`)\n let keepalive: ReturnType<typeof setInterval> | undefined;\n if (this.#opts.keepaliveIntervalMs > 0) {\n keepalive = setInterval(() => {\n if (ws.readyState === WebSocket.OPEN) {\n try {\n ws.send(JSON.stringify({ type: 'ping' }));\n } catch {\n /* ignore */\n }\n }\n }, this.#opts.keepaliveIntervalMs);\n }\n\n // 3) receive loop — resolves when the server closes the socket\n const recvTask = new Promise<void>((resolve, reject) => {\n ws.on('message', (data: RawData, isBinary: boolean) => {\n if (isBinary) return; // Ringg only sends JSON text frames\n let ev: RinggServerEvent;\n try {\n ev = JSON.parse(data.toString());\n } catch {\n return;\n }\n this.#handleEvent(ev);\n });\n ws.once('close', () => resolve());\n ws.once('error', (err) => reject(err));\n });\n\n // 4) send loop — reads resampled frames from `this.input`, ships raw PCM16\n const sendTask = async () => {\n const samples20ms = Math.floor(this.#opts.sampleRate / 50); // 20ms chunks\n const bstream = new AudioByteStream(this.#opts.sampleRate, 1, samples20ms);\n const shipFrames = (frames: AudioFrame[]) => {\n for (const f of frames) {\n if (ws.readyState === WebSocket.OPEN) {\n // send exactly this frame's bytes (raw PCM16 LE), regardless of view offset\n ws.send(new Uint8Array(f.data.buffer, f.data.byteOffset, f.data.byteLength));\n }\n }\n };\n\n for (;;) {\n if (closed) break;\n const { done, value } = await this.input.next();\n if (done) break;\n if (value === SpeechStream.FLUSH_SENTINEL) {\n shipFrames(bstream.flush());\n } else {\n shipFrames(bstream.write((value as AudioFrame).data.buffer));\n }\n }\n\n // input exhausted: flush remaining audio and ask the server to finalize\n shipFrames(bstream.flush());\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({ type: 'end' }));\n }\n };\n\n // abort (session close / cancel) → stop promptly\n const abortPromise = new Promise<void>((resolve) => {\n const signal = this.abortController.signal;\n if (signal.aborted) return resolve();\n signal.addEventListener('abort', () => resolve(), { once: true });\n });\n\n // finite input (a file): after the send loop finishes, wait a grace period\n // for trailing final transcripts, then let the socket close.\n const finiteInputPath = (async () => {\n await sendTask();\n await sleep(this.#opts.finalizeGraceMs);\n })();\n\n try {\n await Promise.race([abortPromise, recvTask, finiteInputPath]);\n } finally {\n if (keepalive) clearInterval(keepalive);\n closeWs();\n // let recvTask settle so its listeners are cleaned up\n await recvTask.catch(() => {});\n }\n }\n\n #handleEvent(ev: RinggServerEvent): void {\n switch (ev.type) {\n case 'transcript': {\n const t = ev as RinggTranscriptEvent;\n const text = (t.transcription ?? '').trim();\n if (!text) return;\n\n if (!this.#speaking) {\n this.#speaking = true;\n this.queue.put({ type: stt.SpeechEventType.START_OF_SPEECH });\n }\n\n const data: stt.SpeechData = {\n language: asLanguageCode(t.language ?? this.#opts.language),\n text,\n startTime: 0,\n endTime: 0,\n confidence: 1.0,\n };\n\n if (t.is_final) {\n this.queue.put({\n type: stt.SpeechEventType.FINAL_TRANSCRIPT,\n alternatives: [data],\n requestId: this.#requestId,\n });\n if (this.#speaking) {\n this.#speaking = false;\n this.queue.put({ type: stt.SpeechEventType.END_OF_SPEECH });\n }\n } else {\n this.queue.put({\n type: stt.SpeechEventType.INTERIM_TRANSCRIPT,\n alternatives: [data],\n requestId: this.#requestId,\n });\n }\n break;\n }\n case 'error': {\n this.#logger.child({ event: ev }).error('ringg stt: server error');\n break;\n }\n // ready (duplicate), ack, pong, and unknown types are ignored\n default:\n break;\n }\n }\n}\n","// Types and defaults for the Ringg Parrot STT plugin.\n//\n// The wire protocol is the one implemented by the Python `ringglabs` SDK\n// (ringglabs.stt) and used by the Pipecat `RinggSTTService`:\n// - connect to a WebSocket\n// - send a JSON `start` frame (auth = `api_key` inside this frame)\n// - stream raw binary PCM16 (little-endian, mono) audio frames\n// - receive JSON `transcript` events discriminated by `is_final`\n\nexport type RinggEncoding = 'int16' | 'linear16' | 'float32' | 'int32';\nexport type RinggMode = 'stream' | 'on_final';\n// hi | en are the documented values; keep it open for forward-compat.\nexport type RinggLanguage = 'hi' | 'en' | (string & {});\n\n/** Resolved options for {@link STT}. */\nexport interface RinggSTTOptions {\n /** Full WebSocket URL, e.g. `wss://prod-api.ringg.ai/stt/v1/stream`. */\n url: string;\n /** Ringg API key. Sent inside the `start` JSON frame (not a header). */\n apiKey: string;\n /** Language code sent to the server (default `en`). */\n language: RinggLanguage;\n /** Audio sample rate the plugin sends to Ringg. LiveKit frames are resampled to this. */\n sampleRate: number;\n /** Audio encoding declared to the server. LiveKit gives PCM16, so `int16`. */\n encoding: RinggEncoding;\n /** `stream` = interim + final; `on_final` = finals only. */\n mode: RinggMode;\n /** Ask the server for capitalisation/punctuation. */\n enableCapPunc: boolean;\n /** Server-VAD trailing silence (ms) before finalizing a segment. */\n vadTailSilMs: number;\n /** Server-VAD confidence threshold (0..1). */\n vadConfidence: number;\n /** If true, the client is expected to send speaking-boundary events (advanced). */\n acceptClientVadEvents: boolean;\n /** Application-level keepalive ping interval (ms). 0 disables. */\n keepaliveIntervalMs: number;\n /** After the input ends, how long to keep reading for trailing finals (ms). */\n finalizeGraceMs: number;\n}\n\nconst envInt = (name: string, fallback: number): number => {\n const v = process.env[name];\n if (v === undefined || v === '') return fallback;\n const n = Number(v);\n return Number.isFinite(n) ? n : fallback;\n};\n\nexport const defaultRinggSTTOptions: RinggSTTOptions = {\n url: process.env.RINGG_STT_URL ?? 'wss://prod-api.ringg.ai/stt/v1/stream',\n apiKey: process.env.RINGG_STT_API_KEY ?? '',\n language: (process.env.RINGG_STT_LANGUAGE as RinggLanguage) ?? 'en',\n sampleRate: envInt('RINGG_STT_SAMPLE_RATE', 16000),\n encoding: (process.env.RINGG_STT_ENCODING as RinggEncoding) ?? 'int16',\n mode: (process.env.RINGG_STT_MODE as RinggMode) ?? 'stream',\n enableCapPunc: true,\n vadTailSilMs: 200,\n vadConfidence: 0.55,\n acceptClientVadEvents: false,\n keepaliveIntervalMs: 30000,\n finalizeGraceMs: 1500,\n};\n\n// ---- Server → client wire events (JSON text frames) ----\n\nexport interface RinggReadyEvent {\n type: 'ready';\n request_id: string;\n [k: string]: unknown;\n}\n\nexport interface RinggTranscriptEvent {\n type: 'transcript';\n transcription: string;\n is_final: boolean;\n language?: string;\n request_id?: string;\n segment_idx?: number;\n [k: string]: unknown;\n}\n\nexport interface RinggErrorEvent {\n type: 'error';\n detail: string;\n code?: string;\n status_code?: number;\n}\n\nexport interface RinggAckEvent {\n type: 'ack';\n ack_for: string;\n state: string;\n}\n\nexport interface RinggPongEvent {\n type: 'pong';\n timestamp: number;\n}\n\nexport type RinggServerEvent =\n | RinggReadyEvent\n | RinggTranscriptEvent\n | RinggErrorEvent\n | RinggAckEvent\n | RinggPongEvent\n | { type: string; [k: string]: unknown };\n\n/** Build the JSON `start` handshake payload. */\nexport const buildStartPayload = (opts: RinggSTTOptions): Record<string, unknown> => ({\n type: 'start',\n sample_rate: opts.sampleRate,\n encoding: opts.encoding,\n language: opts.language,\n mode: opts.mode,\n vad_tail_sil_ms: opts.vadTailSilMs,\n vad_confidence: opts.vadConfidence,\n enable_cap_punc: opts.enableCapPunc,\n accept_client_vad_events: opts.acceptClientVadEvents,\n api_key: opts.apiKey,\n});\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "livekit-plugin-parrot-stt",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Ringg Parrot Speech-to-Text plugin for LiveKit Agents (Node.js)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"livekit",
|
|
7
|
+
"livekit-agents",
|
|
8
|
+
"stt",
|
|
9
|
+
"speech-to-text",
|
|
10
|
+
"ringg",
|
|
11
|
+
"parrot",
|
|
12
|
+
"asr"
|
|
13
|
+
],
|
|
14
|
+
"license": "Apache-2.0",
|
|
15
|
+
"author": "Ringg AI",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/Stonkr/livekit-plugin-parrot-stt.git"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/Stonkr/livekit-plugin-parrot-stt#readme",
|
|
21
|
+
"bugs": "https://github.com/Stonkr/livekit-plugin-parrot-stt/issues",
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "dist/index.cjs",
|
|
27
|
+
"module": "dist/index.js",
|
|
28
|
+
"types": "dist/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"import": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"require": {
|
|
36
|
+
"types": "./dist/index.d.cts",
|
|
37
|
+
"default": "./dist/index.cjs"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist",
|
|
43
|
+
"src",
|
|
44
|
+
"README.md"
|
|
45
|
+
],
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsup",
|
|
48
|
+
"typecheck": "tsc --noEmit",
|
|
49
|
+
"clean": "rm -rf dist"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"ws": "^8.21.1"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"@livekit/agents": "^1.5.0",
|
|
56
|
+
"@livekit/rtc-node": "^0.13.0"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@livekit/agents": "^1.5.3",
|
|
60
|
+
"@livekit/rtc-node": "^0.13.31",
|
|
61
|
+
"@types/node": "^22.10.0",
|
|
62
|
+
"@types/ws": "^8.18.1",
|
|
63
|
+
"tsup": "^8.5.1",
|
|
64
|
+
"typescript": "^5.7.3"
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// livekit-plugin-parrot-stt — Ringg Parrot STT for LiveKit Agents (Node.js)
|
|
2
|
+
|
|
3
|
+
import { Plugin } from '@livekit/agents';
|
|
4
|
+
|
|
5
|
+
export * from './stt.js';
|
|
6
|
+
export * from './models.js';
|
|
7
|
+
|
|
8
|
+
class RinggPlugin extends Plugin {
|
|
9
|
+
constructor() {
|
|
10
|
+
super({
|
|
11
|
+
title: 'ringg',
|
|
12
|
+
version: '0.1.0',
|
|
13
|
+
package: 'livekit-plugin-parrot-stt',
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Plugin.registerPlugin(new RinggPlugin());
|
package/src/models.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Types and defaults for the Ringg Parrot STT plugin.
|
|
2
|
+
//
|
|
3
|
+
// The wire protocol is the one implemented by the Python `ringglabs` SDK
|
|
4
|
+
// (ringglabs.stt) and used by the Pipecat `RinggSTTService`:
|
|
5
|
+
// - connect to a WebSocket
|
|
6
|
+
// - send a JSON `start` frame (auth = `api_key` inside this frame)
|
|
7
|
+
// - stream raw binary PCM16 (little-endian, mono) audio frames
|
|
8
|
+
// - receive JSON `transcript` events discriminated by `is_final`
|
|
9
|
+
|
|
10
|
+
export type RinggEncoding = 'int16' | 'linear16' | 'float32' | 'int32';
|
|
11
|
+
export type RinggMode = 'stream' | 'on_final';
|
|
12
|
+
// hi | en are the documented values; keep it open for forward-compat.
|
|
13
|
+
export type RinggLanguage = 'hi' | 'en' | (string & {});
|
|
14
|
+
|
|
15
|
+
/** Resolved options for {@link STT}. */
|
|
16
|
+
export interface RinggSTTOptions {
|
|
17
|
+
/** Full WebSocket URL, e.g. `wss://prod-api.ringg.ai/stt/v1/stream`. */
|
|
18
|
+
url: string;
|
|
19
|
+
/** Ringg API key. Sent inside the `start` JSON frame (not a header). */
|
|
20
|
+
apiKey: string;
|
|
21
|
+
/** Language code sent to the server (default `en`). */
|
|
22
|
+
language: RinggLanguage;
|
|
23
|
+
/** Audio sample rate the plugin sends to Ringg. LiveKit frames are resampled to this. */
|
|
24
|
+
sampleRate: number;
|
|
25
|
+
/** Audio encoding declared to the server. LiveKit gives PCM16, so `int16`. */
|
|
26
|
+
encoding: RinggEncoding;
|
|
27
|
+
/** `stream` = interim + final; `on_final` = finals only. */
|
|
28
|
+
mode: RinggMode;
|
|
29
|
+
/** Ask the server for capitalisation/punctuation. */
|
|
30
|
+
enableCapPunc: boolean;
|
|
31
|
+
/** Server-VAD trailing silence (ms) before finalizing a segment. */
|
|
32
|
+
vadTailSilMs: number;
|
|
33
|
+
/** Server-VAD confidence threshold (0..1). */
|
|
34
|
+
vadConfidence: number;
|
|
35
|
+
/** If true, the client is expected to send speaking-boundary events (advanced). */
|
|
36
|
+
acceptClientVadEvents: boolean;
|
|
37
|
+
/** Application-level keepalive ping interval (ms). 0 disables. */
|
|
38
|
+
keepaliveIntervalMs: number;
|
|
39
|
+
/** After the input ends, how long to keep reading for trailing finals (ms). */
|
|
40
|
+
finalizeGraceMs: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const envInt = (name: string, fallback: number): number => {
|
|
44
|
+
const v = process.env[name];
|
|
45
|
+
if (v === undefined || v === '') return fallback;
|
|
46
|
+
const n = Number(v);
|
|
47
|
+
return Number.isFinite(n) ? n : fallback;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const defaultRinggSTTOptions: RinggSTTOptions = {
|
|
51
|
+
url: process.env.RINGG_STT_URL ?? 'wss://prod-api.ringg.ai/stt/v1/stream',
|
|
52
|
+
apiKey: process.env.RINGG_STT_API_KEY ?? '',
|
|
53
|
+
language: (process.env.RINGG_STT_LANGUAGE as RinggLanguage) ?? 'en',
|
|
54
|
+
sampleRate: envInt('RINGG_STT_SAMPLE_RATE', 16000),
|
|
55
|
+
encoding: (process.env.RINGG_STT_ENCODING as RinggEncoding) ?? 'int16',
|
|
56
|
+
mode: (process.env.RINGG_STT_MODE as RinggMode) ?? 'stream',
|
|
57
|
+
enableCapPunc: true,
|
|
58
|
+
vadTailSilMs: 200,
|
|
59
|
+
vadConfidence: 0.55,
|
|
60
|
+
acceptClientVadEvents: false,
|
|
61
|
+
keepaliveIntervalMs: 30000,
|
|
62
|
+
finalizeGraceMs: 1500,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// ---- Server → client wire events (JSON text frames) ----
|
|
66
|
+
|
|
67
|
+
export interface RinggReadyEvent {
|
|
68
|
+
type: 'ready';
|
|
69
|
+
request_id: string;
|
|
70
|
+
[k: string]: unknown;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface RinggTranscriptEvent {
|
|
74
|
+
type: 'transcript';
|
|
75
|
+
transcription: string;
|
|
76
|
+
is_final: boolean;
|
|
77
|
+
language?: string;
|
|
78
|
+
request_id?: string;
|
|
79
|
+
segment_idx?: number;
|
|
80
|
+
[k: string]: unknown;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface RinggErrorEvent {
|
|
84
|
+
type: 'error';
|
|
85
|
+
detail: string;
|
|
86
|
+
code?: string;
|
|
87
|
+
status_code?: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface RinggAckEvent {
|
|
91
|
+
type: 'ack';
|
|
92
|
+
ack_for: string;
|
|
93
|
+
state: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface RinggPongEvent {
|
|
97
|
+
type: 'pong';
|
|
98
|
+
timestamp: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type RinggServerEvent =
|
|
102
|
+
| RinggReadyEvent
|
|
103
|
+
| RinggTranscriptEvent
|
|
104
|
+
| RinggErrorEvent
|
|
105
|
+
| RinggAckEvent
|
|
106
|
+
| RinggPongEvent
|
|
107
|
+
| { type: string; [k: string]: unknown };
|
|
108
|
+
|
|
109
|
+
/** Build the JSON `start` handshake payload. */
|
|
110
|
+
export const buildStartPayload = (opts: RinggSTTOptions): Record<string, unknown> => ({
|
|
111
|
+
type: 'start',
|
|
112
|
+
sample_rate: opts.sampleRate,
|
|
113
|
+
encoding: opts.encoding,
|
|
114
|
+
language: opts.language,
|
|
115
|
+
mode: opts.mode,
|
|
116
|
+
vad_tail_sil_ms: opts.vadTailSilMs,
|
|
117
|
+
vad_confidence: opts.vadConfidence,
|
|
118
|
+
enable_cap_punc: opts.enableCapPunc,
|
|
119
|
+
accept_client_vad_events: opts.acceptClientVadEvents,
|
|
120
|
+
api_key: opts.apiKey,
|
|
121
|
+
});
|