@livekit/agents-plugin-deepgram 1.4.9 → 1.5.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/dist/index.cjs +4 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/models.cjs.map +1 -1
- package/dist/models.d.cts +3 -3
- package/dist/models.d.ts +3 -3
- package/dist/models.d.ts.map +1 -1
- package/dist/stt.cjs +140 -26
- package/dist/stt.cjs.map +1 -1
- package/dist/stt.d.cts +5 -2
- package/dist/stt.d.ts +5 -2
- package/dist/stt.d.ts.map +1 -1
- package/dist/stt.js +141 -26
- package/dist/stt.js.map +1 -1
- package/dist/stt.test.cjs +11 -0
- package/dist/stt.test.cjs.map +1 -1
- package/dist/stt.test.js +12 -1
- package/dist/stt.test.js.map +1 -1
- package/dist/tts.cjs +21 -1
- package/dist/tts.cjs.map +1 -1
- package/dist/tts.d.cts +4 -0
- package/dist/tts.d.ts +4 -0
- package/dist/tts.d.ts.map +1 -1
- package/dist/tts.js +21 -1
- package/dist/tts.js.map +1 -1
- package/package.json +8 -8
- package/src/index.ts +1 -0
- package/src/models.ts +58 -7
- package/src/stt.test.ts +18 -1
- package/src/stt.ts +173 -30
- package/src/tts.ts +29 -1
package/dist/tts.d.ts
CHANGED
|
@@ -4,7 +4,9 @@ export interface TTSOptions {
|
|
|
4
4
|
model: TTSModels | string;
|
|
5
5
|
encoding: TTSEncoding;
|
|
6
6
|
sampleRate: number;
|
|
7
|
+
bitRate?: number | null;
|
|
7
8
|
speed?: number;
|
|
9
|
+
mipOptOut: boolean;
|
|
8
10
|
apiKey?: string;
|
|
9
11
|
baseUrl?: string;
|
|
10
12
|
sentenceTokenizer: tokenize.SentenceTokenizer;
|
|
@@ -15,11 +17,13 @@ export declare class TTS extends tts.TTS {
|
|
|
15
17
|
label: string;
|
|
16
18
|
get model(): string;
|
|
17
19
|
get provider(): string;
|
|
20
|
+
get sampleRate(): number;
|
|
18
21
|
constructor(opts?: Partial<TTSOptions>);
|
|
19
22
|
synthesize(text: string, connOptions?: APIConnectOptions, abortSignal?: AbortSignal): tts.ChunkedStream;
|
|
20
23
|
stream(options?: {
|
|
21
24
|
connOptions?: APIConnectOptions;
|
|
22
25
|
}): tts.SynthesizeStream;
|
|
26
|
+
updateOptions(opts: Partial<Pick<TTSOptions, 'model' | 'encoding' | 'sampleRate' | 'bitRate' | 'speed' | 'mipOptOut'>>): void;
|
|
23
27
|
}
|
|
24
28
|
export declare class ChunkedStream extends tts.ChunkedStream {
|
|
25
29
|
#private;
|
package/dist/tts.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tts.d.ts","sourceRoot":"","sources":["../src/tts.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,iBAAiB,EAQtB,QAAQ,EACR,GAAG,EACJ,MAAM,iBAAiB,CAAC;AAIzB,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAM1D,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC;IAC1B,QAAQ,EAAE,WAAW,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,QAAQ,CAAC,iBAAiB,CAAC;IAC9C,YAAY,EAAE,GAAG,CAAC,eAAe,CAAC;CACnC;
|
|
1
|
+
{"version":3,"file":"tts.d.ts","sourceRoot":"","sources":["../src/tts.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,iBAAiB,EAQtB,QAAQ,EACR,GAAG,EACJ,MAAM,iBAAiB,CAAC;AAIzB,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAM1D,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC;IAC1B,QAAQ,EAAE,WAAW,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,QAAQ,CAAC,iBAAiB,CAAC;IAC9C,YAAY,EAAE,GAAG,CAAC,eAAe,CAAC;CACnC;AAkBD,qBAAa,GAAI,SAAQ,GAAG,CAAC,GAAG;IAC9B,OAAO,CAAC,IAAI,CAAa;IACzB,KAAK,SAAkB;IAEvB,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,IAAa,UAAU,IAAI,MAAM,CAEhC;gBAEW,IAAI,GAAE,OAAO,CAAC,UAAU,CAAM;IAqB1C,UAAU,CACR,IAAI,EAAE,MAAM,EACZ,WAAW,CAAC,EAAE,iBAAiB,EAC/B,WAAW,CAAC,EAAE,WAAW,GACxB,GAAG,CAAC,aAAa;IAIpB,MAAM,CAAC,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,iBAAiB,CAAA;KAAE,GAAG,GAAG,CAAC,gBAAgB;IAI3E,aAAa,CACX,IAAI,EAAE,OAAO,CACX,IAAI,CAAC,UAAU,EAAE,OAAO,GAAG,UAAU,GAAG,YAAY,GAAG,SAAS,GAAG,OAAO,GAAG,WAAW,CAAC,CAC1F;CAOJ;AAED,qBAAa,aAAc,SAAQ,GAAG,CAAC,aAAa;;IAClD,KAAK,SAA4B;IAEjC,OAAO,CAAC,IAAI,CAAa;IACzB,OAAO,CAAC,IAAI,CAAS;gBAGnB,GAAG,EAAE,GAAG,EACR,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,UAAU,EAChB,WAAW,CAAC,EAAE,iBAAiB,EAC/B,WAAW,CAAC,EAAE,WAAW;cAOX,GAAG;CAmGpB;AAED,qBAAa,gBAAiB,SAAQ,GAAG,CAAC,gBAAgB;;IACxD,OAAO,CAAC,IAAI,CAAa;IACzB,OAAO,CAAC,SAAS,CAA0B;IAE3C,KAAK,SAA+B;IAEpC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAqC;IACtE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAqC;gBAE1D,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,CAAC,EAAE,iBAAiB;YAMzD,cAAc;cA4CZ,GAAG;CAyLpB"}
|
package/dist/tts.js
CHANGED
|
@@ -15,9 +15,11 @@ const AUTHORIZATION_HEADER = "Authorization";
|
|
|
15
15
|
const NUM_CHANNELS = 1;
|
|
16
16
|
const MIN_SENTENCE_LENGTH = 8;
|
|
17
17
|
const defaultTTSOptions = {
|
|
18
|
-
model: "aura-
|
|
18
|
+
model: "aura-2-andromeda-en",
|
|
19
19
|
encoding: "linear16",
|
|
20
20
|
sampleRate: 24e3,
|
|
21
|
+
bitRate: null,
|
|
22
|
+
mipOptOut: false,
|
|
21
23
|
apiKey: process.env.DEEPGRAM_API_KEY,
|
|
22
24
|
baseUrl: "https://api.deepgram.com",
|
|
23
25
|
capabilities: {
|
|
@@ -36,6 +38,9 @@ class TTS extends tts.TTS {
|
|
|
36
38
|
get provider() {
|
|
37
39
|
return "Deepgram";
|
|
38
40
|
}
|
|
41
|
+
get sampleRate() {
|
|
42
|
+
return this.opts.sampleRate;
|
|
43
|
+
}
|
|
39
44
|
constructor(opts = {}) {
|
|
40
45
|
var _a;
|
|
41
46
|
super(opts.sampleRate || defaultTTSOptions.sampleRate, NUM_CHANNELS, {
|
|
@@ -60,6 +65,12 @@ class TTS extends tts.TTS {
|
|
|
60
65
|
stream(options) {
|
|
61
66
|
return new SynthesizeStream(this, this.opts, options == null ? void 0 : options.connOptions);
|
|
62
67
|
}
|
|
68
|
+
updateOptions(opts) {
|
|
69
|
+
this.opts = {
|
|
70
|
+
...this.opts,
|
|
71
|
+
...opts
|
|
72
|
+
};
|
|
73
|
+
}
|
|
63
74
|
}
|
|
64
75
|
class ChunkedStream extends tts.ChunkedStream {
|
|
65
76
|
label = "deepgram.ChunkedStream";
|
|
@@ -79,6 +90,11 @@ class ChunkedStream extends tts.ChunkedStream {
|
|
|
79
90
|
url.searchParams.append("sample_rate", this.opts.sampleRate.toString());
|
|
80
91
|
url.searchParams.append("model", this.opts.model);
|
|
81
92
|
url.searchParams.append("encoding", this.opts.encoding);
|
|
93
|
+
url.searchParams.append("container", "none");
|
|
94
|
+
url.searchParams.append("mip_opt_out", String(this.opts.mipOptOut));
|
|
95
|
+
if (this.opts.bitRate !== void 0 && this.opts.bitRate !== null) {
|
|
96
|
+
url.searchParams.append("bit_rate", this.opts.bitRate.toString());
|
|
97
|
+
}
|
|
82
98
|
if (this.opts.speed !== void 0) {
|
|
83
99
|
url.searchParams.append("speed", this.opts.speed.toString());
|
|
84
100
|
}
|
|
@@ -213,6 +229,10 @@ class SynthesizeStream extends tts.SynthesizeStream {
|
|
|
213
229
|
url.searchParams.append("sample_rate", this.opts.sampleRate.toString());
|
|
214
230
|
url.searchParams.append("model", this.opts.model);
|
|
215
231
|
url.searchParams.append("encoding", this.opts.encoding);
|
|
232
|
+
url.searchParams.append("mip_opt_out", String(this.opts.mipOptOut));
|
|
233
|
+
if (this.opts.bitRate !== void 0 && this.opts.bitRate !== null) {
|
|
234
|
+
url.searchParams.append("bit_rate", this.opts.bitRate.toString());
|
|
235
|
+
}
|
|
216
236
|
if (this.opts.speed !== void 0) {
|
|
217
237
|
url.searchParams.append("speed", this.opts.speed.toString());
|
|
218
238
|
}
|
package/dist/tts.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tts.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n type APIConnectOptions,\n APIConnectionError,\n APIError,\n APIStatusError,\n APITimeoutError,\n AudioByteStream,\n log,\n shortuuid,\n tokenize,\n tts,\n} from '@livekit/agents';\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport { request } from 'node:https';\nimport { type RawData, WebSocket } from 'ws';\nimport type { TTSEncoding, TTSModels } from './models.js';\n\nconst AUTHORIZATION_HEADER = 'Authorization';\nconst NUM_CHANNELS = 1;\nconst MIN_SENTENCE_LENGTH = 8;\n\nexport interface TTSOptions {\n model: TTSModels | string;\n encoding: TTSEncoding;\n sampleRate: number;\n speed?: number;\n apiKey?: string;\n baseUrl?: string;\n sentenceTokenizer: tokenize.SentenceTokenizer;\n capabilities: tts.TTSCapabilities;\n}\n\nconst defaultTTSOptions: TTSOptions = {\n model: 'aura-asteria-en',\n encoding: 'linear16',\n sampleRate: 24000,\n apiKey: process.env.DEEPGRAM_API_KEY,\n baseUrl: 'https://api.deepgram.com',\n capabilities: {\n streaming: true,\n },\n sentenceTokenizer: new tokenize.basic.SentenceTokenizer({\n minSentenceLength: MIN_SENTENCE_LENGTH,\n }),\n};\n\nexport class TTS extends tts.TTS {\n private opts: TTSOptions;\n label = 'deepgram.TTS';\n\n get model(): string {\n return this.opts.model;\n }\n\n get provider(): string {\n return 'Deepgram';\n }\n\n constructor(opts: Partial<TTSOptions> = {}) {\n super(opts.sampleRate || defaultTTSOptions.sampleRate, NUM_CHANNELS, {\n streaming: opts.capabilities?.streaming ?? defaultTTSOptions.capabilities.streaming,\n });\n\n this.opts = {\n ...defaultTTSOptions,\n ...opts,\n };\n\n if (this.opts.apiKey === undefined) {\n throw new Error(\n 'Deepgram API key is required, whether as an argument or as $DEEPGRAM_API_KEY',\n );\n }\n\n if (this.opts.speed !== undefined && (this.opts.speed < 0.7 || this.opts.speed > 1.5)) {\n throw new Error(`Deepgram TTS speed must be between 0.7 and 1.5, got ${this.opts.speed}`);\n }\n }\n\n synthesize(\n text: string,\n connOptions?: APIConnectOptions,\n abortSignal?: AbortSignal,\n ): tts.ChunkedStream {\n return new ChunkedStream(this, text, this.opts, connOptions, abortSignal);\n }\n\n stream(options?: { connOptions?: APIConnectOptions }): tts.SynthesizeStream {\n return new SynthesizeStream(this, this.opts, options?.connOptions);\n }\n}\n\nexport class ChunkedStream extends tts.ChunkedStream {\n label = 'deepgram.ChunkedStream';\n #logger = log();\n private opts: TTSOptions;\n private text: string;\n\n constructor(\n tts: TTS,\n text: string,\n opts: TTSOptions,\n connOptions?: APIConnectOptions,\n abortSignal?: AbortSignal,\n ) {\n super(text, tts, connOptions, abortSignal);\n this.text = text;\n this.opts = opts;\n }\n\n protected async run() {\n const requestId = shortuuid();\n const bstream = new AudioByteStream(this.opts.sampleRate, NUM_CHANNELS);\n const json = { text: this.text };\n const url = new URL(`${this.opts.baseUrl!}/v1/speak`);\n url.searchParams.append('sample_rate', this.opts.sampleRate.toString());\n url.searchParams.append('model', this.opts.model);\n url.searchParams.append('encoding', this.opts.encoding);\n if (this.opts.speed !== undefined) {\n url.searchParams.append('speed', this.opts.speed.toString());\n }\n\n await new Promise<void>((resolve, reject) => {\n let settled = false;\n const settle = (fn: () => void) => {\n if (!settled) {\n settled = true;\n fn();\n }\n };\n\n const req = request(\n {\n hostname: url.hostname,\n port: 443,\n path: url.pathname + url.search,\n method: 'POST',\n headers: {\n [AUTHORIZATION_HEADER]: `Token ${this.opts.apiKey!}`,\n 'Content-Type': 'application/json',\n },\n signal: this.abortSignal,\n },\n (res) => {\n if (res.statusCode !== 200) {\n settle(() =>\n reject(\n new Error(\n `Deepgram TTS HTTP request failed: ${res.statusCode} ${res.statusMessage}`,\n ),\n ),\n );\n return;\n }\n\n res.on('data', (chunk) => {\n for (const frame of bstream.write(chunk)) {\n if (!this.queue.closed) {\n this.queue.put({\n requestId,\n frame,\n final: false,\n segmentId: requestId,\n });\n }\n }\n });\n\n res.on('error', (err) => {\n if (err.message === 'aborted') return;\n this.#logger.error({ err }, 'Deepgram TTS response error');\n settle(() => reject(err));\n });\n\n res.on('close', () => {\n for (const frame of bstream.flush()) {\n if (!this.queue.closed) {\n this.queue.put({\n requestId,\n frame,\n final: false,\n segmentId: requestId,\n });\n }\n }\n if (!this.queue.closed) {\n this.queue.close();\n }\n settle(() => resolve());\n });\n },\n );\n\n req.on('error', (err) => {\n if (err.name === 'AbortError') return;\n this.#logger.error({ err }, 'Deepgram TTS request error');\n settle(() => reject(err));\n });\n\n req.on('close', () => settle(() => resolve()));\n req.write(JSON.stringify(json));\n req.end();\n });\n }\n}\n\nexport class SynthesizeStream extends tts.SynthesizeStream {\n private opts: TTSOptions;\n private tokenizer: tokenize.SentenceStream;\n #logger = log();\n label = 'deepgram.SynthesizeStream';\n\n private static readonly FLUSH_MSG = JSON.stringify({ type: 'Flush' });\n private static readonly CLOSE_MSG = JSON.stringify({ type: 'Close' });\n\n constructor(tts: TTS, opts: TTSOptions, connOptions?: APIConnectOptions) {\n super(tts, connOptions);\n this.opts = opts;\n this.tokenizer = opts.sentenceTokenizer.stream();\n }\n\n private async closeWebSocket(ws: WebSocket): Promise<void> {\n try {\n // Send Flush and Close messages to ensure Deepgram processes all remaining audio\n // and properly terminates the session, preventing lingering TTS sessions\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(SynthesizeStream.FLUSH_MSG);\n ws.send(SynthesizeStream.CLOSE_MSG);\n\n // Wait for server acknowledgment to prevent race conditions and ensure\n // proper cleanup, avoiding 429 Too Many Requests errors from lingering sessions\n try {\n await new Promise<void>((resolve, _reject) => {\n const timeout = setTimeout(() => {\n resolve();\n }, 1000);\n\n ws.once('message', () => {\n clearTimeout(timeout);\n resolve();\n });\n\n ws.once('close', () => {\n clearTimeout(timeout);\n resolve();\n });\n\n ws.once('error', () => {\n clearTimeout(timeout);\n resolve();\n });\n });\n } catch (e) {\n // Ignore timeout or other errors during close sequence\n }\n }\n } catch (e) {\n console.warn(`Error during WebSocket close sequence: ${e}`);\n } finally {\n if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {\n ws.close();\n }\n }\n }\n\n protected async run() {\n const requestId = shortuuid();\n const segmentId = shortuuid();\n\n const wsUrl = this.opts.baseUrl!.replace(/^http/, 'ws');\n const url = new URL(`${wsUrl}/v1/speak`);\n url.searchParams.append('sample_rate', this.opts.sampleRate.toString());\n url.searchParams.append('model', this.opts.model);\n url.searchParams.append('encoding', this.opts.encoding);\n if (this.opts.speed !== undefined) {\n url.searchParams.append('speed', this.opts.speed.toString());\n }\n\n const ws = new WebSocket(url, {\n headers: {\n [AUTHORIZATION_HEADER]: `Token ${this.opts.apiKey!}`,\n },\n });\n\n await new Promise((resolve, reject) => {\n ws.on('open', resolve);\n ws.on('error', (error) => reject(error));\n ws.on('close', (code) => reject(`WebSocket returned ${code}`));\n });\n\n const inputTask = async () => {\n for await (const data of this.input) {\n if (data === SynthesizeStream.FLUSH_SENTINEL) {\n this.tokenizer.flush();\n continue;\n }\n this.tokenizer.pushText(data);\n }\n this.tokenizer.endInput();\n this.tokenizer.close();\n };\n\n let markInputSent: () => void = () => {};\n const inputSent = new Promise<void>((resolve) => {\n markInputSent = resolve;\n });\n\n const sendTask = async () => {\n try {\n for await (const event of this.tokenizer) {\n if (this.abortController.signal.aborted) break;\n\n let text = event.token;\n if (!text.endsWith(' ')) {\n text += ' ';\n }\n\n const message = JSON.stringify({\n type: 'Speak',\n text: text,\n });\n\n ws.send(message);\n markInputSent();\n }\n\n if (!this.abortController.signal.aborted) {\n ws.send(SynthesizeStream.FLUSH_MSG);\n markInputSent();\n }\n } finally {\n markInputSent();\n }\n };\n\n const recvTask = async () => {\n const bstream = new AudioByteStream(this.opts.sampleRate, NUM_CHANNELS);\n let finalReceived = false;\n let timeout: NodeJS.Timeout | null = null;\n let lastFrame: AudioFrame | undefined;\n\n const sendLastFrame = (segmentId: string, final: boolean) => {\n if (lastFrame && !this.queue.closed) {\n this.queue.put({ requestId, segmentId, frame: lastFrame, final });\n lastFrame = undefined;\n }\n };\n\n const clearMessageTimeout = () => {\n if (timeout) {\n clearTimeout(timeout);\n timeout = null;\n }\n };\n\n const resetMessageTimeout = (reject: (reason?: unknown) => void) => {\n clearMessageTimeout();\n timeout = setTimeout(() => {\n reject(new APITimeoutError({ message: 'Deepgram TTS recv idle timeout' }));\n }, this.connOptions.timeoutMs);\n };\n\n await inputSent;\n if (this.abortController.signal.aborted) return;\n\n return new Promise<void>((resolve, reject) => {\n resetMessageTimeout(reject);\n\n ws.on('message', (data: RawData, isBinary: boolean) => {\n clearMessageTimeout();\n\n if (!isBinary) {\n const message = JSON.parse(data.toString());\n if (message.type === 'Flushed') {\n finalReceived = true;\n for (const frame of bstream.flush()) {\n sendLastFrame(segmentId, false);\n lastFrame = frame;\n }\n sendLastFrame(segmentId, true);\n\n if (!this.queue.closed) {\n this.queue.put(SynthesizeStream.END_OF_STREAM);\n }\n resolve();\n return;\n } else if (message.type === 'Warning') {\n this.#logger.warn(`Deepgram warning: ${message.warn_msg}`);\n } else if (message.type === 'Error' || message.type === 'error') {\n reject(new APIError('Deepgram TTS returned error', { body: message }));\n return;\n } else if (message.type !== 'Metadata') {\n this.#logger.warn({ message }, 'Unknown Deepgram message type');\n }\n\n resetMessageTimeout(reject);\n return;\n }\n\n const buffer =\n data instanceof Buffer\n ? data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)\n : (data as ArrayBuffer);\n for (const frame of bstream.write(buffer as ArrayBuffer)) {\n sendLastFrame(segmentId, false);\n lastFrame = frame;\n }\n resetMessageTimeout(reject);\n });\n\n ws.on('close', (code, reason) => {\n clearMessageTimeout();\n if (!finalReceived) {\n reject(\n new APIStatusError({\n message: 'Deepgram websocket connection closed unexpectedly',\n options: {\n statusCode: code || -1,\n body: { reason: reason.toString() },\n },\n }),\n );\n return;\n }\n resolve();\n });\n\n ws.on('error', (error) => {\n clearMessageTimeout();\n reject(error);\n });\n });\n };\n\n try {\n await Promise.all([inputTask(), sendTask(), recvTask()]);\n } catch (e) {\n if (this.abortController.signal.aborted) return;\n if (e instanceof APIError) throw e;\n throw new APIConnectionError({\n message: `Deepgram TTS WebSocket failed: ${(e as Error).message ?? 'unknown error'}`,\n });\n } finally {\n await this.closeWebSocket(ws);\n }\n }\n}\n"],"mappings":"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","names":["tts","segmentId"]}
|
|
1
|
+
{"version":3,"sources":["../src/tts.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n type APIConnectOptions,\n APIConnectionError,\n APIError,\n APIStatusError,\n APITimeoutError,\n AudioByteStream,\n log,\n shortuuid,\n tokenize,\n tts,\n} from '@livekit/agents';\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport { request } from 'node:https';\nimport { type RawData, WebSocket } from 'ws';\nimport type { TTSEncoding, TTSModels } from './models.js';\n\nconst AUTHORIZATION_HEADER = 'Authorization';\nconst NUM_CHANNELS = 1;\nconst MIN_SENTENCE_LENGTH = 8;\n\nexport interface TTSOptions {\n model: TTSModels | string;\n encoding: TTSEncoding;\n sampleRate: number;\n bitRate?: number | null;\n speed?: number;\n mipOptOut: boolean;\n apiKey?: string;\n baseUrl?: string;\n sentenceTokenizer: tokenize.SentenceTokenizer;\n capabilities: tts.TTSCapabilities;\n}\n\nconst defaultTTSOptions: TTSOptions = {\n model: 'aura-2-andromeda-en',\n encoding: 'linear16',\n sampleRate: 24000,\n bitRate: null,\n mipOptOut: false,\n apiKey: process.env.DEEPGRAM_API_KEY,\n baseUrl: 'https://api.deepgram.com',\n capabilities: {\n streaming: true,\n },\n sentenceTokenizer: new tokenize.basic.SentenceTokenizer({\n minSentenceLength: MIN_SENTENCE_LENGTH,\n }),\n};\n\nexport class TTS extends tts.TTS {\n private opts: TTSOptions;\n label = 'deepgram.TTS';\n\n get model(): string {\n return this.opts.model;\n }\n\n get provider(): string {\n return 'Deepgram';\n }\n\n override get sampleRate(): number {\n return this.opts.sampleRate;\n }\n\n constructor(opts: Partial<TTSOptions> = {}) {\n super(opts.sampleRate || defaultTTSOptions.sampleRate, NUM_CHANNELS, {\n streaming: opts.capabilities?.streaming ?? defaultTTSOptions.capabilities.streaming,\n });\n\n this.opts = {\n ...defaultTTSOptions,\n ...opts,\n };\n\n if (this.opts.apiKey === undefined) {\n throw new Error(\n 'Deepgram API key is required, whether as an argument or as $DEEPGRAM_API_KEY',\n );\n }\n\n if (this.opts.speed !== undefined && (this.opts.speed < 0.7 || this.opts.speed > 1.5)) {\n throw new Error(`Deepgram TTS speed must be between 0.7 and 1.5, got ${this.opts.speed}`);\n }\n }\n\n synthesize(\n text: string,\n connOptions?: APIConnectOptions,\n abortSignal?: AbortSignal,\n ): tts.ChunkedStream {\n return new ChunkedStream(this, text, this.opts, connOptions, abortSignal);\n }\n\n stream(options?: { connOptions?: APIConnectOptions }): tts.SynthesizeStream {\n return new SynthesizeStream(this, this.opts, options?.connOptions);\n }\n\n updateOptions(\n opts: Partial<\n Pick<TTSOptions, 'model' | 'encoding' | 'sampleRate' | 'bitRate' | 'speed' | 'mipOptOut'>\n >,\n ) {\n this.opts = {\n ...this.opts,\n ...opts,\n };\n }\n}\n\nexport class ChunkedStream extends tts.ChunkedStream {\n label = 'deepgram.ChunkedStream';\n #logger = log();\n private opts: TTSOptions;\n private text: string;\n\n constructor(\n tts: TTS,\n text: string,\n opts: TTSOptions,\n connOptions?: APIConnectOptions,\n abortSignal?: AbortSignal,\n ) {\n super(text, tts, connOptions, abortSignal);\n this.text = text;\n this.opts = opts;\n }\n\n protected async run() {\n const requestId = shortuuid();\n const bstream = new AudioByteStream(this.opts.sampleRate, NUM_CHANNELS);\n const json = { text: this.text };\n const url = new URL(`${this.opts.baseUrl!}/v1/speak`);\n url.searchParams.append('sample_rate', this.opts.sampleRate.toString());\n url.searchParams.append('model', this.opts.model);\n url.searchParams.append('encoding', this.opts.encoding);\n url.searchParams.append('container', 'none');\n url.searchParams.append('mip_opt_out', String(this.opts.mipOptOut));\n if (this.opts.bitRate !== undefined && this.opts.bitRate !== null) {\n url.searchParams.append('bit_rate', this.opts.bitRate.toString());\n }\n if (this.opts.speed !== undefined) {\n url.searchParams.append('speed', this.opts.speed.toString());\n }\n\n await new Promise<void>((resolve, reject) => {\n let settled = false;\n const settle = (fn: () => void) => {\n if (!settled) {\n settled = true;\n fn();\n }\n };\n\n const req = request(\n {\n hostname: url.hostname,\n port: 443,\n path: url.pathname + url.search,\n method: 'POST',\n headers: {\n [AUTHORIZATION_HEADER]: `Token ${this.opts.apiKey!}`,\n 'Content-Type': 'application/json',\n },\n signal: this.abortSignal,\n },\n (res) => {\n if (res.statusCode !== 200) {\n settle(() =>\n reject(\n new Error(\n `Deepgram TTS HTTP request failed: ${res.statusCode} ${res.statusMessage}`,\n ),\n ),\n );\n return;\n }\n\n res.on('data', (chunk) => {\n for (const frame of bstream.write(chunk)) {\n if (!this.queue.closed) {\n this.queue.put({\n requestId,\n frame,\n final: false,\n segmentId: requestId,\n });\n }\n }\n });\n\n res.on('error', (err) => {\n if (err.message === 'aborted') return;\n this.#logger.error({ err }, 'Deepgram TTS response error');\n settle(() => reject(err));\n });\n\n res.on('close', () => {\n for (const frame of bstream.flush()) {\n if (!this.queue.closed) {\n this.queue.put({\n requestId,\n frame,\n final: false,\n segmentId: requestId,\n });\n }\n }\n if (!this.queue.closed) {\n this.queue.close();\n }\n settle(() => resolve());\n });\n },\n );\n\n req.on('error', (err) => {\n if (err.name === 'AbortError') return;\n this.#logger.error({ err }, 'Deepgram TTS request error');\n settle(() => reject(err));\n });\n\n req.on('close', () => settle(() => resolve()));\n req.write(JSON.stringify(json));\n req.end();\n });\n }\n}\n\nexport class SynthesizeStream extends tts.SynthesizeStream {\n private opts: TTSOptions;\n private tokenizer: tokenize.SentenceStream;\n #logger = log();\n label = 'deepgram.SynthesizeStream';\n\n private static readonly FLUSH_MSG = JSON.stringify({ type: 'Flush' });\n private static readonly CLOSE_MSG = JSON.stringify({ type: 'Close' });\n\n constructor(tts: TTS, opts: TTSOptions, connOptions?: APIConnectOptions) {\n super(tts, connOptions);\n this.opts = opts;\n this.tokenizer = opts.sentenceTokenizer.stream();\n }\n\n private async closeWebSocket(ws: WebSocket): Promise<void> {\n try {\n // Send Flush and Close messages to ensure Deepgram processes all remaining audio\n // and properly terminates the session, preventing lingering TTS sessions\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(SynthesizeStream.FLUSH_MSG);\n ws.send(SynthesizeStream.CLOSE_MSG);\n\n // Wait for server acknowledgment to prevent race conditions and ensure\n // proper cleanup, avoiding 429 Too Many Requests errors from lingering sessions\n try {\n await new Promise<void>((resolve, _reject) => {\n const timeout = setTimeout(() => {\n resolve();\n }, 1000);\n\n ws.once('message', () => {\n clearTimeout(timeout);\n resolve();\n });\n\n ws.once('close', () => {\n clearTimeout(timeout);\n resolve();\n });\n\n ws.once('error', () => {\n clearTimeout(timeout);\n resolve();\n });\n });\n } catch (e) {\n // Ignore timeout or other errors during close sequence\n }\n }\n } catch (e) {\n console.warn(`Error during WebSocket close sequence: ${e}`);\n } finally {\n if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {\n ws.close();\n }\n }\n }\n\n protected async run() {\n const requestId = shortuuid();\n const segmentId = shortuuid();\n\n const wsUrl = this.opts.baseUrl!.replace(/^http/, 'ws');\n const url = new URL(`${wsUrl}/v1/speak`);\n url.searchParams.append('sample_rate', this.opts.sampleRate.toString());\n url.searchParams.append('model', this.opts.model);\n url.searchParams.append('encoding', this.opts.encoding);\n url.searchParams.append('mip_opt_out', String(this.opts.mipOptOut));\n if (this.opts.bitRate !== undefined && this.opts.bitRate !== null) {\n url.searchParams.append('bit_rate', this.opts.bitRate.toString());\n }\n if (this.opts.speed !== undefined) {\n url.searchParams.append('speed', this.opts.speed.toString());\n }\n\n const ws = new WebSocket(url, {\n headers: {\n [AUTHORIZATION_HEADER]: `Token ${this.opts.apiKey!}`,\n },\n });\n\n await new Promise((resolve, reject) => {\n ws.on('open', resolve);\n ws.on('error', (error) => reject(error));\n ws.on('close', (code) => reject(`WebSocket returned ${code}`));\n });\n\n const inputTask = async () => {\n for await (const data of this.input) {\n if (data === SynthesizeStream.FLUSH_SENTINEL) {\n this.tokenizer.flush();\n continue;\n }\n this.tokenizer.pushText(data);\n }\n this.tokenizer.endInput();\n this.tokenizer.close();\n };\n\n let markInputSent: () => void = () => {};\n const inputSent = new Promise<void>((resolve) => {\n markInputSent = resolve;\n });\n\n const sendTask = async () => {\n try {\n for await (const event of this.tokenizer) {\n if (this.abortController.signal.aborted) break;\n\n let text = event.token;\n if (!text.endsWith(' ')) {\n text += ' ';\n }\n\n const message = JSON.stringify({\n type: 'Speak',\n text: text,\n });\n\n ws.send(message);\n markInputSent();\n }\n\n if (!this.abortController.signal.aborted) {\n ws.send(SynthesizeStream.FLUSH_MSG);\n markInputSent();\n }\n } finally {\n markInputSent();\n }\n };\n\n const recvTask = async () => {\n const bstream = new AudioByteStream(this.opts.sampleRate, NUM_CHANNELS);\n let finalReceived = false;\n let timeout: NodeJS.Timeout | null = null;\n let lastFrame: AudioFrame | undefined;\n\n const sendLastFrame = (segmentId: string, final: boolean) => {\n if (lastFrame && !this.queue.closed) {\n this.queue.put({ requestId, segmentId, frame: lastFrame, final });\n lastFrame = undefined;\n }\n };\n\n const clearMessageTimeout = () => {\n if (timeout) {\n clearTimeout(timeout);\n timeout = null;\n }\n };\n\n const resetMessageTimeout = (reject: (reason?: unknown) => void) => {\n clearMessageTimeout();\n timeout = setTimeout(() => {\n reject(new APITimeoutError({ message: 'Deepgram TTS recv idle timeout' }));\n }, this.connOptions.timeoutMs);\n };\n\n await inputSent;\n if (this.abortController.signal.aborted) return;\n\n return new Promise<void>((resolve, reject) => {\n resetMessageTimeout(reject);\n\n ws.on('message', (data: RawData, isBinary: boolean) => {\n clearMessageTimeout();\n\n if (!isBinary) {\n const message = JSON.parse(data.toString());\n if (message.type === 'Flushed') {\n finalReceived = true;\n for (const frame of bstream.flush()) {\n sendLastFrame(segmentId, false);\n lastFrame = frame;\n }\n sendLastFrame(segmentId, true);\n\n if (!this.queue.closed) {\n this.queue.put(SynthesizeStream.END_OF_STREAM);\n }\n resolve();\n return;\n } else if (message.type === 'Warning') {\n this.#logger.warn(`Deepgram warning: ${message.warn_msg}`);\n } else if (message.type === 'Error' || message.type === 'error') {\n reject(new APIError('Deepgram TTS returned error', { body: message }));\n return;\n } else if (message.type !== 'Metadata') {\n this.#logger.warn({ message }, 'Unknown Deepgram message type');\n }\n\n resetMessageTimeout(reject);\n return;\n }\n\n const buffer =\n data instanceof Buffer\n ? data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)\n : (data as ArrayBuffer);\n for (const frame of bstream.write(buffer as ArrayBuffer)) {\n sendLastFrame(segmentId, false);\n lastFrame = frame;\n }\n resetMessageTimeout(reject);\n });\n\n ws.on('close', (code, reason) => {\n clearMessageTimeout();\n if (!finalReceived) {\n reject(\n new APIStatusError({\n message: 'Deepgram websocket connection closed unexpectedly',\n options: {\n statusCode: code || -1,\n body: { reason: reason.toString() },\n },\n }),\n );\n return;\n }\n resolve();\n });\n\n ws.on('error', (error) => {\n clearMessageTimeout();\n reject(error);\n });\n });\n };\n\n try {\n await Promise.all([inputTask(), sendTask(), recvTask()]);\n } catch (e) {\n if (this.abortController.signal.aborted) return;\n if (e instanceof APIError) throw e;\n throw new APIConnectionError({\n message: `Deepgram TTS WebSocket failed: ${(e as Error).message ?? 'unknown error'}`,\n });\n } finally {\n await this.closeWebSocket(ws);\n }\n }\n}\n"],"mappings":"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","names":["tts","segmentId"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livekit/agents-plugin-deepgram",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Deepgram plugin for LiveKit Agents for Node.js",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"require": "dist/index.cjs",
|
|
@@ -25,22 +25,22 @@
|
|
|
25
25
|
"README.md"
|
|
26
26
|
],
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@livekit/rtc-node": "^0.13.
|
|
28
|
+
"@livekit/rtc-node": "^0.13.30",
|
|
29
29
|
"@microsoft/api-extractor": "^7.35.0",
|
|
30
30
|
"@types/ws": "^8.5.10",
|
|
31
31
|
"tsup": "^8.3.5",
|
|
32
32
|
"typescript": "^5.0.0",
|
|
33
|
-
"@livekit/agents": "1.
|
|
34
|
-
"@livekit/agents-plugin-openai": "^1.
|
|
35
|
-
"@livekit/agents-plugin-silero": "1.
|
|
36
|
-
"@livekit/agents-plugins-test": "1.
|
|
33
|
+
"@livekit/agents": "1.5.0",
|
|
34
|
+
"@livekit/agents-plugin-openai": "^1.5.0",
|
|
35
|
+
"@livekit/agents-plugin-silero": "1.5.0",
|
|
36
|
+
"@livekit/agents-plugins-test": "1.5.0"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"ws": "^8.18.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@livekit/rtc-node": "^0.13.
|
|
43
|
-
"@livekit/agents": "1.
|
|
42
|
+
"@livekit/rtc-node": "^0.13.30",
|
|
43
|
+
"@livekit/agents": "1.5.0"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "tsup --onSuccess \"pnpm build:types\"",
|
package/src/index.ts
CHANGED
package/src/models.ts
CHANGED
|
@@ -2,7 +2,54 @@
|
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
|
+
// https://developers.deepgram.com/docs/tts-models
|
|
5
6
|
export type TTSModels =
|
|
7
|
+
| 'aura-2-andromeda-en'
|
|
8
|
+
| 'aura-2-apollo-en'
|
|
9
|
+
| 'aura-2-arcas-en'
|
|
10
|
+
| 'aura-2-aries-en'
|
|
11
|
+
| 'aura-2-artemis-en'
|
|
12
|
+
| 'aura-2-asteria-en'
|
|
13
|
+
| 'aura-2-atlas-en'
|
|
14
|
+
| 'aura-2-aurora-en'
|
|
15
|
+
| 'aura-2-callisto-en'
|
|
16
|
+
| 'aura-2-cetus-en'
|
|
17
|
+
| 'aura-2-chiron-en'
|
|
18
|
+
| 'aura-2-columbia-en'
|
|
19
|
+
| 'aura-2-cordelia-en'
|
|
20
|
+
| 'aura-2-crina-en'
|
|
21
|
+
| 'aura-2-draco-en'
|
|
22
|
+
| 'aura-2-electra-en'
|
|
23
|
+
| 'aura-2-eos-en'
|
|
24
|
+
| 'aura-2-harmonia-en'
|
|
25
|
+
| 'aura-2-helios-en'
|
|
26
|
+
| 'aura-2-hera-en'
|
|
27
|
+
| 'aura-2-hermes-en'
|
|
28
|
+
| 'aura-2-hyperion-en'
|
|
29
|
+
| 'aura-2-io-en'
|
|
30
|
+
| 'aura-2-iris-en'
|
|
31
|
+
| 'aura-2-janus-en'
|
|
32
|
+
| 'aura-2-juno-en'
|
|
33
|
+
| 'aura-2-jupiter-en'
|
|
34
|
+
| 'aura-2-luna-en'
|
|
35
|
+
| 'aura-2-mars-en'
|
|
36
|
+
| 'aura-2-minerva-en'
|
|
37
|
+
| 'aura-2-mira-en'
|
|
38
|
+
| 'aura-2-neptune-en'
|
|
39
|
+
| 'aura-2-odysseus-en'
|
|
40
|
+
| 'aura-2-ophiuchus-en'
|
|
41
|
+
| 'aura-2-orion-en'
|
|
42
|
+
| 'aura-2-orpheus-en'
|
|
43
|
+
| 'aura-2-phoebe-en'
|
|
44
|
+
| 'aura-2-pluto-en'
|
|
45
|
+
| 'aura-2-saturn-en'
|
|
46
|
+
| 'aura-2-selene-en'
|
|
47
|
+
| 'aura-2-theia-en'
|
|
48
|
+
| 'aura-2-titan-en'
|
|
49
|
+
| 'aura-2-triton-en'
|
|
50
|
+
| 'aura-2-vega-en'
|
|
51
|
+
| 'aura-2-venus-en'
|
|
52
|
+
| 'aura-2-zeus-en'
|
|
6
53
|
| 'aura-asteria-en'
|
|
7
54
|
| 'aura-luna-en'
|
|
8
55
|
| 'aura-stella-en'
|
|
@@ -35,9 +82,11 @@ export type STTModels =
|
|
|
35
82
|
| 'nova-2-medical'
|
|
36
83
|
| 'nova-2-drivethru'
|
|
37
84
|
| 'nova-2-automotive'
|
|
85
|
+
| 'nova-2-atc'
|
|
38
86
|
| 'nova-3'
|
|
39
87
|
| 'nova-3-general'
|
|
40
88
|
| 'nova-3-medical'
|
|
89
|
+
| 'nova-3-multilingual'
|
|
41
90
|
| 'enhanced-general'
|
|
42
91
|
| 'enhanced-meeting'
|
|
43
92
|
| 'enhanced-phonecall'
|
|
@@ -53,10 +102,16 @@ export type STTModels =
|
|
|
53
102
|
| 'whisper-base'
|
|
54
103
|
| 'whisper-small'
|
|
55
104
|
| 'whisper-medium'
|
|
56
|
-
| 'whisper-large'
|
|
105
|
+
| 'whisper-large'
|
|
106
|
+
| 'flux-general-en';
|
|
57
107
|
|
|
108
|
+
// https://developers.deepgram.com/docs/models-languages-overview
|
|
58
109
|
export type STTLanguages =
|
|
110
|
+
| 'zh'
|
|
111
|
+
| 'zh-CN'
|
|
112
|
+
| 'zh-TW'
|
|
59
113
|
| 'da'
|
|
114
|
+
| 'nl'
|
|
60
115
|
| 'de'
|
|
61
116
|
| 'en'
|
|
62
117
|
| 'en-AU'
|
|
@@ -67,15 +122,14 @@ export type STTLanguages =
|
|
|
67
122
|
| 'es'
|
|
68
123
|
| 'es-419'
|
|
69
124
|
| 'es-LATAM'
|
|
70
|
-
| 'fr'
|
|
71
|
-
| 'fr-CA'
|
|
72
125
|
| 'hi'
|
|
73
126
|
| 'hi-Latn'
|
|
127
|
+
| 'fr'
|
|
128
|
+
| 'fr-CA'
|
|
74
129
|
| 'id'
|
|
75
130
|
| 'it'
|
|
76
131
|
| 'ja'
|
|
77
132
|
| 'ko'
|
|
78
|
-
| 'nl'
|
|
79
133
|
| 'no'
|
|
80
134
|
| 'pl'
|
|
81
135
|
| 'pt'
|
|
@@ -87,7 +141,4 @@ export type STTLanguages =
|
|
|
87
141
|
| 'th'
|
|
88
142
|
| 'tr'
|
|
89
143
|
| 'uk'
|
|
90
|
-
| 'zh'
|
|
91
|
-
| 'zh-CN'
|
|
92
|
-
| 'zh-TW'
|
|
93
144
|
| 'multi';
|
package/src/stt.test.ts
CHANGED
|
@@ -3,11 +3,28 @@
|
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
import { VAD } from '@livekit/agents-plugin-silero';
|
|
5
5
|
import { stt } from '@livekit/agents-plugins-test';
|
|
6
|
-
import { describe, it } from 'vitest';
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
7
|
import { STT } from './stt.js';
|
|
8
8
|
|
|
9
9
|
const hasDeepgramApiKey = Boolean(process.env.DEEPGRAM_API_KEY);
|
|
10
10
|
|
|
11
|
+
describe('Deepgram streaming language detection', () => {
|
|
12
|
+
// Deepgram only supports language detection for prerecorded audio, so a
|
|
13
|
+
// streaming session must reject it rather than silently default to English.
|
|
14
|
+
// Mirrors livekit-plugins-deepgram (Python).
|
|
15
|
+
it('throws when starting a stream with detectLanguage enabled', () => {
|
|
16
|
+
const stt = new STT({ apiKey: 'test', detectLanguage: true });
|
|
17
|
+
expect(() => stt.stream()).toThrow('language detection is not supported in streaming mode');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('allows streaming with an explicit language', () => {
|
|
21
|
+
const stt = new STT({ apiKey: 'test', language: 'en-US' });
|
|
22
|
+
const stream = stt.stream();
|
|
23
|
+
// Close immediately so the connection loop never starts (no network in unit tests).
|
|
24
|
+
stream.close();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
11
28
|
if (hasDeepgramApiKey) {
|
|
12
29
|
describe('Deepgram', async () => {
|
|
13
30
|
await stt(new STT(), await VAD.load(), { nonStreaming: false });
|
package/src/stt.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
createTimedString,
|
|
12
12
|
getBaseLanguage,
|
|
13
13
|
log,
|
|
14
|
+
mergeFrames,
|
|
14
15
|
normalizeLanguage,
|
|
15
16
|
stt,
|
|
16
17
|
waitForAbort,
|
|
@@ -26,7 +27,7 @@ export interface STTOptions {
|
|
|
26
27
|
detectLanguage: boolean;
|
|
27
28
|
interimResults: boolean;
|
|
28
29
|
punctuate: boolean;
|
|
29
|
-
model: STTModels;
|
|
30
|
+
model: STTModels | string;
|
|
30
31
|
smartFormat: boolean;
|
|
31
32
|
noDelay: boolean;
|
|
32
33
|
endpointing: number;
|
|
@@ -42,6 +43,9 @@ export interface STTOptions {
|
|
|
42
43
|
* See https://developers.deepgram.com/docs/redaction for details.
|
|
43
44
|
*/
|
|
44
45
|
redact: string | string[];
|
|
46
|
+
utteranceEndMs?: number | null;
|
|
47
|
+
replace?: Record<string, string> | null;
|
|
48
|
+
search?: string[] | null;
|
|
45
49
|
dictation: boolean;
|
|
46
50
|
diarize: boolean;
|
|
47
51
|
numerals: boolean;
|
|
@@ -66,6 +70,9 @@ const defaultSTTOptions: STTOptions = {
|
|
|
66
70
|
keyterm: [],
|
|
67
71
|
profanityFilter: false,
|
|
68
72
|
redact: [],
|
|
73
|
+
utteranceEndMs: null,
|
|
74
|
+
replace: null,
|
|
75
|
+
search: null,
|
|
69
76
|
dictation: false,
|
|
70
77
|
diarize: false,
|
|
71
78
|
numerals: false,
|
|
@@ -107,9 +114,74 @@ export class STT extends stt.STT {
|
|
|
107
114
|
|
|
108
115
|
if (this.#opts.detectLanguage) {
|
|
109
116
|
this.#opts.language = undefined;
|
|
110
|
-
} else
|
|
111
|
-
this.#opts.language
|
|
112
|
-
|
|
117
|
+
} else {
|
|
118
|
+
this.#opts.model = this.#validateModel(this.#opts.model, this.#opts.language);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async _recognize(buffer: AudioBuffer, abortSignal?: AbortSignal): Promise<stt.SpeechEvent> {
|
|
123
|
+
const frame = mergeFrames(buffer);
|
|
124
|
+
const listenURL = new URL(`${this.#opts.baseUrl.replace(/^ws/, 'http')}/v1/listen`);
|
|
125
|
+
const params = {
|
|
126
|
+
model: this.#opts.model,
|
|
127
|
+
punctuate: this.#opts.punctuate,
|
|
128
|
+
detect_language: this.#opts.detectLanguage,
|
|
129
|
+
smart_format: this.#opts.smartFormat,
|
|
130
|
+
keywords: this.#opts.keywords.map((x) => x.join(':')),
|
|
131
|
+
profanity_filter: this.#opts.profanityFilter,
|
|
132
|
+
numerals: this.#opts.numerals,
|
|
133
|
+
mip_opt_out: this.#opts.mipOptOut,
|
|
134
|
+
language: this.#opts.language,
|
|
135
|
+
...(this.#opts.keyterm.length > 0 ? { keyterm: this.#opts.keyterm } : {}),
|
|
136
|
+
...(this.#opts.redact.length > 0 ? { redact: this.#opts.redact } : {}),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
appendSearchParams(listenURL, params);
|
|
140
|
+
|
|
141
|
+
if (this.#opts.diarize) {
|
|
142
|
+
this.#logger.warn('speaker diarization is not supported in non-streaming mode, ignoring');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const resp = await fetch(listenURL, {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
headers: {
|
|
148
|
+
Authorization: `Token ${this.#opts.apiKey}`,
|
|
149
|
+
Accept: 'application/json',
|
|
150
|
+
'Content-Type': 'audio/wav',
|
|
151
|
+
},
|
|
152
|
+
body: new Blob([new Uint8Array(createWav(frame))], { type: 'audio/wav' }),
|
|
153
|
+
signal: abortSignal ?? null,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (!resp.ok) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Deepgram STT REST request failed with status ${resp.status}: ${await resp.text()}`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return prerecordedTranscriptionToSpeechEvent(this.#opts.language, await resp.json());
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
updateOptions(opts: Partial<STTOptions>) {
|
|
166
|
+
const language =
|
|
167
|
+
opts.language !== undefined ? normalizeLanguage(opts.language) : this.#opts.language;
|
|
168
|
+
const model =
|
|
169
|
+
opts.model !== undefined || opts.language !== undefined
|
|
170
|
+
? this.#validateModel(opts.model ?? this.#opts.model, language)
|
|
171
|
+
: this.#opts.model;
|
|
172
|
+
|
|
173
|
+
this.#opts = {
|
|
174
|
+
...this.#opts,
|
|
175
|
+
...opts,
|
|
176
|
+
language,
|
|
177
|
+
model,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#validateModel(model: STTModels | string, language?: string) {
|
|
182
|
+
if (
|
|
183
|
+
language &&
|
|
184
|
+
getBaseLanguage(language) !== 'en' &&
|
|
113
185
|
[
|
|
114
186
|
'nova-2-meeting',
|
|
115
187
|
'nova-2-phonecall',
|
|
@@ -121,30 +193,24 @@ export class STT extends stt.STT {
|
|
|
121
193
|
'nova-2-drivethru',
|
|
122
194
|
'nova-2-automotive',
|
|
123
195
|
'nova-3-general',
|
|
124
|
-
].includes(
|
|
196
|
+
].includes(model)
|
|
125
197
|
) {
|
|
126
198
|
this.#logger.warn(
|
|
127
|
-
`${
|
|
199
|
+
`${model} does not support language ${language}, falling back to nova-2-general`,
|
|
128
200
|
);
|
|
129
|
-
|
|
201
|
+
return 'nova-2-general';
|
|
130
202
|
}
|
|
131
|
-
}
|
|
132
203
|
|
|
133
|
-
|
|
134
|
-
async _recognize(_: AudioBuffer): Promise<stt.SpeechEvent> {
|
|
135
|
-
throw new Error('Recognize is not supported on Deepgram STT');
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
updateOptions(opts: Partial<STTOptions>) {
|
|
139
|
-
this.#opts = {
|
|
140
|
-
...this.#opts,
|
|
141
|
-
...opts,
|
|
142
|
-
language:
|
|
143
|
-
opts.language !== undefined ? normalizeLanguage(opts.language) : this.#opts.language,
|
|
144
|
-
};
|
|
204
|
+
return model;
|
|
145
205
|
}
|
|
146
206
|
|
|
147
207
|
stream(options?: { connOptions?: APIConnectOptions }): SpeechStream {
|
|
208
|
+
// Deepgram only supports language detection for prerecorded audio, not streaming.
|
|
209
|
+
if (this.#opts.detectLanguage || this.#opts.language === undefined) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
'language detection is not supported in streaming mode, please disable it and specify a language',
|
|
212
|
+
);
|
|
213
|
+
}
|
|
148
214
|
return new SpeechStream(this, this.#opts, options?.connOptions);
|
|
149
215
|
}
|
|
150
216
|
|
|
@@ -199,19 +265,14 @@ export class SpeechStream extends stt.SpeechStream {
|
|
|
199
265
|
keywords: this.#opts.keywords.map((x) => x.join(':')),
|
|
200
266
|
keyterm: this.#opts.keyterm,
|
|
201
267
|
profanity_filter: this.#opts.profanityFilter,
|
|
202
|
-
|
|
268
|
+
utterance_end_ms: this.#opts.utteranceEndMs,
|
|
269
|
+
replace: this.#opts.replace,
|
|
270
|
+
search: this.#opts.search,
|
|
203
271
|
language: this.#opts.language,
|
|
204
272
|
mip_opt_out: this.#opts.mipOptOut,
|
|
273
|
+
...(this.#opts.redact.length > 0 ? { redact: this.#opts.redact } : {}),
|
|
205
274
|
};
|
|
206
|
-
|
|
207
|
-
if (v !== undefined) {
|
|
208
|
-
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
|
|
209
|
-
streamURL.searchParams.append(k, String(v));
|
|
210
|
-
} else {
|
|
211
|
-
v.forEach((x) => streamURL.searchParams.append(k, String(x)));
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
});
|
|
275
|
+
appendSearchParams(streamURL, params);
|
|
215
276
|
|
|
216
277
|
ws = new WebSocket(streamURL, {
|
|
217
278
|
headers: { Authorization: `Token ${this.#opts.apiKey}` },
|
|
@@ -411,6 +472,13 @@ export class SpeechStream extends stt.SpeechStream {
|
|
|
411
472
|
case 'Metadata': {
|
|
412
473
|
break;
|
|
413
474
|
}
|
|
475
|
+
case 'UtteranceEnd': {
|
|
476
|
+
if (this.#speaking) {
|
|
477
|
+
this.#speaking = false;
|
|
478
|
+
putMessage({ type: stt.SpeechEventType.END_OF_SPEECH });
|
|
479
|
+
}
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
414
482
|
default: {
|
|
415
483
|
this.#logger.child({ msg: json }).warn('received unexpected message from Deepgram');
|
|
416
484
|
break;
|
|
@@ -451,6 +519,46 @@ export class SpeechStream extends stt.SpeechStream {
|
|
|
451
519
|
}
|
|
452
520
|
}
|
|
453
521
|
|
|
522
|
+
function appendSearchParams(url: URL, params: Record<string, unknown>) {
|
|
523
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
524
|
+
if (value === undefined || value === null) return;
|
|
525
|
+
|
|
526
|
+
if (Array.isArray(value)) {
|
|
527
|
+
value.forEach((item) => url.searchParams.append(key, String(item)));
|
|
528
|
+
} else if (typeof value === 'object') {
|
|
529
|
+
Object.entries(value).forEach(([term, replacement]) => {
|
|
530
|
+
url.searchParams.append(key, `${term}:${replacement}`);
|
|
531
|
+
});
|
|
532
|
+
} else {
|
|
533
|
+
url.searchParams.append(key, String(value));
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function createWav(frame: AudioFrame): Buffer {
|
|
539
|
+
const bitsPerSample = 16;
|
|
540
|
+
const byteRate = (frame.sampleRate * frame.channels * bitsPerSample) / 8;
|
|
541
|
+
const blockAlign = (frame.channels * bitsPerSample) / 8;
|
|
542
|
+
|
|
543
|
+
const header = Buffer.alloc(44);
|
|
544
|
+
header.write('RIFF', 0);
|
|
545
|
+
header.writeUInt32LE(36 + frame.data.byteLength, 4);
|
|
546
|
+
header.write('WAVE', 8);
|
|
547
|
+
header.write('fmt ', 12);
|
|
548
|
+
header.writeUInt32LE(16, 16);
|
|
549
|
+
header.writeUInt16LE(1, 20);
|
|
550
|
+
header.writeUInt16LE(frame.channels, 22);
|
|
551
|
+
header.writeUInt32LE(frame.sampleRate, 24);
|
|
552
|
+
header.writeUInt32LE(byteRate, 28);
|
|
553
|
+
header.writeUInt16LE(blockAlign, 32);
|
|
554
|
+
header.writeUInt16LE(bitsPerSample, 34);
|
|
555
|
+
header.write('data', 36);
|
|
556
|
+
header.writeUInt32LE(frame.data.byteLength, 40);
|
|
557
|
+
|
|
558
|
+
const pcm = Buffer.from(frame.data.buffer, frame.data.byteOffset, frame.data.byteLength);
|
|
559
|
+
return Buffer.concat([header, pcm]);
|
|
560
|
+
}
|
|
561
|
+
|
|
454
562
|
const liveTranscriptionToSpeechData = (
|
|
455
563
|
language: STTLanguages | string,
|
|
456
564
|
data: { [id: string]: any },
|
|
@@ -483,3 +591,38 @@ const liveTranscriptionToSpeechData = (
|
|
|
483
591
|
};
|
|
484
592
|
});
|
|
485
593
|
};
|
|
594
|
+
|
|
595
|
+
function prerecordedTranscriptionToSpeechEvent(
|
|
596
|
+
language: STTLanguages | string | undefined,
|
|
597
|
+
data: { [id: string]: any },
|
|
598
|
+
): stt.SpeechEvent {
|
|
599
|
+
const channel = data['results']['channels'][0];
|
|
600
|
+
const detectedLanguage = channel['detected_language'] ?? '';
|
|
601
|
+
const alternatives = channel['alternatives'].map((alt: { [id: string]: any }) => {
|
|
602
|
+
const wordsData: any[] = alt['words'] ?? [];
|
|
603
|
+
|
|
604
|
+
return {
|
|
605
|
+
language: normalizeLanguage(language ?? detectedLanguage),
|
|
606
|
+
startTime: wordsData.length ? wordsData[0]['start'] : 0,
|
|
607
|
+
endTime: wordsData.length ? wordsData[wordsData.length - 1]['end'] : 0,
|
|
608
|
+
confidence: alt['confidence'],
|
|
609
|
+
text: alt['transcript'],
|
|
610
|
+
words: wordsData.map((word) =>
|
|
611
|
+
createTimedString({
|
|
612
|
+
text: word['word'] ?? '',
|
|
613
|
+
startTime: word['start'] ?? 0,
|
|
614
|
+
endTime: word['end'] ?? 0,
|
|
615
|
+
confidence: word['confidence'] ?? 0.0,
|
|
616
|
+
}),
|
|
617
|
+
),
|
|
618
|
+
};
|
|
619
|
+
}) as stt.SpeechData[];
|
|
620
|
+
|
|
621
|
+
return {
|
|
622
|
+
type: stt.SpeechEventType.FINAL_TRANSCRIPT,
|
|
623
|
+
requestId: data['metadata']['request_id'],
|
|
624
|
+
...(alternatives.length
|
|
625
|
+
? { alternatives: alternatives as [stt.SpeechData, ...stt.SpeechData[]] }
|
|
626
|
+
: {}),
|
|
627
|
+
};
|
|
628
|
+
}
|