@livekit/agents 1.0.38 → 1.0.40

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.
Files changed (80) hide show
  1. package/dist/http_server.cjs +9 -6
  2. package/dist/http_server.cjs.map +1 -1
  3. package/dist/http_server.d.cts +5 -1
  4. package/dist/http_server.d.ts +5 -1
  5. package/dist/http_server.d.ts.map +1 -1
  6. package/dist/http_server.js +9 -6
  7. package/dist/http_server.js.map +1 -1
  8. package/dist/inference/llm.cjs +7 -3
  9. package/dist/inference/llm.cjs.map +1 -1
  10. package/dist/inference/llm.d.cts +5 -6
  11. package/dist/inference/llm.d.ts +5 -6
  12. package/dist/inference/llm.d.ts.map +1 -1
  13. package/dist/inference/llm.js +7 -3
  14. package/dist/inference/llm.js.map +1 -1
  15. package/dist/inference/stt.cjs.map +1 -1
  16. package/dist/inference/stt.d.cts +5 -4
  17. package/dist/inference/stt.d.ts +5 -4
  18. package/dist/inference/stt.d.ts.map +1 -1
  19. package/dist/inference/stt.js.map +1 -1
  20. package/dist/inference/tts.cjs.map +1 -1
  21. package/dist/inference/tts.d.cts +10 -7
  22. package/dist/inference/tts.d.ts +10 -7
  23. package/dist/inference/tts.d.ts.map +1 -1
  24. package/dist/inference/tts.js.map +1 -1
  25. package/dist/ipc/supervised_proc.cjs +4 -0
  26. package/dist/ipc/supervised_proc.cjs.map +1 -1
  27. package/dist/ipc/supervised_proc.d.cts +1 -0
  28. package/dist/ipc/supervised_proc.d.ts +1 -0
  29. package/dist/ipc/supervised_proc.d.ts.map +1 -1
  30. package/dist/ipc/supervised_proc.js +4 -0
  31. package/dist/ipc/supervised_proc.js.map +1 -1
  32. package/dist/stt/stream_adapter.cjs +9 -1
  33. package/dist/stt/stream_adapter.cjs.map +1 -1
  34. package/dist/stt/stream_adapter.d.ts.map +1 -1
  35. package/dist/stt/stream_adapter.js +9 -1
  36. package/dist/stt/stream_adapter.js.map +1 -1
  37. package/dist/tokenize/basic/sentence.cjs +3 -3
  38. package/dist/tokenize/basic/sentence.cjs.map +1 -1
  39. package/dist/tokenize/basic/sentence.js +3 -3
  40. package/dist/tokenize/basic/sentence.js.map +1 -1
  41. package/dist/tokenize/tokenizer.test.cjs +3 -1
  42. package/dist/tokenize/tokenizer.test.cjs.map +1 -1
  43. package/dist/tokenize/tokenizer.test.js +3 -1
  44. package/dist/tokenize/tokenizer.test.js.map +1 -1
  45. package/dist/utils.cjs +5 -0
  46. package/dist/utils.cjs.map +1 -1
  47. package/dist/utils.d.cts +8 -0
  48. package/dist/utils.d.ts +8 -0
  49. package/dist/utils.d.ts.map +1 -1
  50. package/dist/utils.js +4 -0
  51. package/dist/utils.js.map +1 -1
  52. package/dist/voice/agent.cjs +1 -2
  53. package/dist/voice/agent.cjs.map +1 -1
  54. package/dist/voice/agent.js +1 -2
  55. package/dist/voice/agent.js.map +1 -1
  56. package/dist/voice/agent_activity.cjs +23 -14
  57. package/dist/voice/agent_activity.cjs.map +1 -1
  58. package/dist/voice/agent_activity.d.cts +1 -0
  59. package/dist/voice/agent_activity.d.ts +1 -0
  60. package/dist/voice/agent_activity.d.ts.map +1 -1
  61. package/dist/voice/agent_activity.js +23 -14
  62. package/dist/voice/agent_activity.js.map +1 -1
  63. package/dist/worker.cjs +12 -2
  64. package/dist/worker.cjs.map +1 -1
  65. package/dist/worker.d.ts.map +1 -1
  66. package/dist/worker.js +12 -2
  67. package/dist/worker.js.map +1 -1
  68. package/package.json +2 -2
  69. package/src/http_server.ts +18 -6
  70. package/src/inference/llm.ts +20 -15
  71. package/src/inference/stt.ts +9 -7
  72. package/src/inference/tts.ts +36 -16
  73. package/src/ipc/supervised_proc.ts +4 -0
  74. package/src/stt/stream_adapter.ts +12 -1
  75. package/src/tokenize/basic/sentence.ts +3 -3
  76. package/src/tokenize/tokenizer.test.ts +4 -0
  77. package/src/utils.ts +14 -0
  78. package/src/voice/agent.ts +2 -2
  79. package/src/voice/agent_activity.ts +36 -15
  80. package/src/worker.ts +24 -2
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/inference/tts.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport { WebSocket } from 'ws';\nimport { APIError, APIStatusError } from '../_exceptions.js';\nimport { AudioByteStream } from '../audio.js';\nimport { ConnectionPool } from '../connection_pool.js';\nimport { log } from '../log.js';\nimport { createStreamChannel } from '../stream/stream_channel.js';\nimport { basic as tokenizeBasic } from '../tokenize/index.js';\nimport type { ChunkedStream } from '../tts/index.js';\nimport { SynthesizeStream as BaseSynthesizeStream, TTS as BaseTTS } from '../tts/index.js';\nimport { type APIConnectOptions, DEFAULT_API_CONNECT_OPTIONS } from '../types.js';\nimport { Event, Future, Task, cancelAndWait, combineSignals, shortuuid } from '../utils.js';\nimport {\n type TtsClientEvent,\n type TtsServerEvent,\n type TtsSessionCreateEvent,\n ttsClientEventSchema,\n ttsServerEventSchema,\n} from './api_protos.js';\nimport { type AnyString, connectWs, createAccessToken } from './utils.js';\n\nexport type CartesiaModels =\n | 'cartesia'\n | 'cartesia/sonic'\n | 'cartesia/sonic-2'\n | 'cartesia/sonic-turbo';\n\nexport type ElevenlabsModels =\n | 'elevenlabs'\n | 'elevenlabs/eleven_flash_v2'\n | 'elevenlabs/eleven_flash_v2_5'\n | 'elevenlabs/eleven_turbo_v2'\n | 'elevenlabs/eleven_turbo_v2_5'\n | 'elevenlabs/eleven_multilingual_v2';\n\nexport type RimeModels = 'rime' | 'rime/mist' | 'rime/mistv2' | 'rime/arcana';\n\nexport type InworldModels = 'inworld' | 'inworld/inworld-tts-1';\n\nexport interface CartesiaOptions {\n duration?: number; // max duration of audio in seconds\n speed?: 'slow' | 'normal' | 'fast'; // default: not specified\n}\n\nexport interface ElevenlabsOptions {\n inactivity_timeout?: number; // default: 60\n apply_text_normalization?: 'auto' | 'off' | 'on'; // default: \"auto\"\n}\n\nexport interface RimeOptions {}\n\nexport interface InworldOptions {}\n\ntype _TTSModels = CartesiaModels | ElevenlabsModels | RimeModels | InworldModels;\n\nexport type TTSModels = CartesiaModels | ElevenlabsModels | RimeModels | InworldModels | AnyString;\n\nexport type ModelWithVoice = `${_TTSModels}:${string}` | TTSModels;\n\nexport type TTSOptions<TModel extends TTSModels> = TModel extends CartesiaModels\n ? CartesiaOptions\n : TModel extends ElevenlabsModels\n ? ElevenlabsOptions\n : TModel extends RimeOptions\n ? RimeOptions\n : TModel extends InworldOptions\n ? InworldOptions\n : Record<string, unknown>;\n\ntype TTSEncoding = 'pcm_s16le';\n\nconst DEFAULT_ENCODING: TTSEncoding = 'pcm_s16le';\nconst DEFAULT_SAMPLE_RATE = 16000;\nconst DEFAULT_BASE_URL = 'https://agent-gateway.livekit.cloud/v1';\nconst NUM_CHANNELS = 1;\nconst DEFAULT_LANGUAGE = 'en';\n\nexport interface InferenceTTSOptions<TModel extends TTSModels> {\n model?: TModel;\n voice?: string;\n language?: string;\n encoding: TTSEncoding;\n sampleRate: number;\n baseURL: string;\n apiKey: string;\n apiSecret: string;\n modelOptions: TTSOptions<TModel>;\n}\n\n/**\n * Livekit Cloud Inference TTS\n */\nexport class TTS<TModel extends TTSModels> extends BaseTTS {\n private opts: InferenceTTSOptions<TModel>;\n private streams: Set<SynthesizeStream<TModel>> = new Set();\n pool: ConnectionPool<WebSocket>;\n\n #logger = log();\n\n constructor(opts: {\n model: TModel;\n voice?: string;\n language?: string;\n baseURL?: string;\n encoding?: TTSEncoding;\n sampleRate?: number;\n apiKey?: string;\n apiSecret?: string;\n modelOptions?: TTSOptions<TModel>;\n }) {\n const sampleRate = opts?.sampleRate ?? DEFAULT_SAMPLE_RATE;\n super(sampleRate, 1, { streaming: true });\n\n const {\n model,\n voice,\n language = DEFAULT_LANGUAGE,\n baseURL,\n encoding = DEFAULT_ENCODING,\n apiKey,\n apiSecret,\n modelOptions = {} as TTSOptions<TModel>,\n } = opts || {};\n\n const lkBaseURL = baseURL || process.env.LIVEKIT_INFERENCE_URL || DEFAULT_BASE_URL;\n const lkApiKey = apiKey || process.env.LIVEKIT_INFERENCE_API_KEY || process.env.LIVEKIT_API_KEY;\n if (!lkApiKey) {\n throw new Error('apiKey is required: pass apiKey or set LIVEKIT_API_KEY');\n }\n\n const lkApiSecret =\n apiSecret || process.env.LIVEKIT_INFERENCE_API_SECRET || process.env.LIVEKIT_API_SECRET;\n if (!lkApiSecret) {\n throw new Error('apiSecret is required: pass apiSecret or set LIVEKIT_API_SECRET');\n }\n\n // read voice id from the model if provided: \"provider/model:voice_id\"\n let nextModel = model;\n let nextVoice = voice;\n if (typeof nextModel === 'string') {\n const idx = nextModel.lastIndexOf(':');\n if (idx !== -1) {\n const voiceFromModel = nextModel.slice(idx + 1);\n if (nextVoice && nextVoice !== voiceFromModel) {\n this.#logger.warn(\n '`voice` is provided via both argument and model, using the one from the argument',\n { voice: nextVoice, model: nextModel },\n );\n } else {\n nextVoice = voiceFromModel;\n }\n nextModel = nextModel.slice(0, idx) as TModel;\n }\n }\n\n this.opts = {\n model: nextModel,\n voice: nextVoice,\n language,\n encoding,\n sampleRate,\n baseURL: lkBaseURL,\n apiKey: lkApiKey,\n apiSecret: lkApiSecret,\n modelOptions,\n };\n\n // Initialize connection pool\n this.pool = new ConnectionPool<WebSocket>({\n connectCb: (timeout) => this.connectWs(timeout),\n closeCb: (ws) => this.closeWs(ws),\n maxSessionDuration: 300_000,\n markRefreshedOnGet: true,\n connectTimeout: 10_000, // 10 seconds default\n });\n }\n\n get label() {\n return 'inference.TTS';\n }\n\n static fromModelString(modelString: string): TTS<AnyString> {\n if (modelString.includes(':')) {\n const [model, voice] = modelString.split(':') as [TTSModels, string];\n return new TTS({ model, voice });\n }\n return new TTS({ model: modelString });\n }\n\n updateOptions(opts: Partial<Pick<InferenceTTSOptions<TModel>, 'model' | 'voice' | 'language'>>) {\n this.opts = { ...this.opts, ...opts };\n for (const stream of this.streams) {\n stream.updateOptions(opts);\n }\n }\n\n synthesize(_: string): ChunkedStream {\n throw new Error('ChunkedStream is not implemented');\n }\n\n stream(options?: { connOptions?: APIConnectOptions }): SynthesizeStream<TModel> {\n const { connOptions = DEFAULT_API_CONNECT_OPTIONS } = options || {};\n const stream = new SynthesizeStream(this, { ...this.opts }, connOptions);\n this.streams.add(stream);\n return stream;\n }\n\n async connectWs(timeout: number): Promise<WebSocket> {\n let baseURL = this.opts.baseURL;\n if (baseURL.startsWith('http://') || baseURL.startsWith('https://')) {\n baseURL = baseURL.replace('http', 'ws');\n }\n\n const token = await createAccessToken(this.opts.apiKey, this.opts.apiSecret);\n const url = `${baseURL}/tts`;\n const headers = { Authorization: `Bearer ${token}` } as Record<string, string>;\n\n const params = {\n type: 'session.create',\n sample_rate: String(this.opts.sampleRate),\n encoding: this.opts.encoding,\n extra: this.opts.modelOptions,\n } as TtsSessionCreateEvent;\n\n if (this.opts.voice) params.voice = this.opts.voice;\n if (this.opts.model) params.model = this.opts.model;\n if (this.opts.language) params.language = this.opts.language;\n\n this.#logger.debug({ url }, 'inference.TTS creating new websocket connection (pool miss)');\n const socket = await connectWs(url, headers, timeout);\n socket.send(JSON.stringify(params));\n return socket;\n }\n\n async closeWs(ws: WebSocket) {\n await ws.close();\n }\n\n prewarm(): void {\n this.pool.prewarm();\n }\n\n async close() {\n for (const stream of this.streams) {\n await stream.close();\n }\n this.streams.clear();\n await this.pool.close();\n }\n}\n\nexport class SynthesizeStream<TModel extends TTSModels> extends BaseSynthesizeStream {\n private opts: InferenceTTSOptions<TModel>;\n private tts: TTS<TModel>;\n\n #logger = log();\n\n constructor(tts: TTS<TModel>, opts: InferenceTTSOptions<TModel>, connOptions: APIConnectOptions) {\n super(tts, connOptions);\n this.opts = opts;\n this.tts = tts;\n }\n\n get label() {\n return 'inference.SynthesizeStream';\n }\n\n updateOptions(opts: Partial<Pick<InferenceTTSOptions<TModel>, 'model' | 'voice' | 'language'>>) {\n this.opts = { ...this.opts, ...opts };\n }\n\n protected async run(): Promise<void> {\n let closing = false;\n let lastFrame: AudioFrame | undefined;\n\n const sendTokenizerStream = new tokenizeBasic.SentenceTokenizer().stream();\n const eventChannel = createStreamChannel<TtsServerEvent>();\n const requestId = shortuuid('tts_request_');\n const inputSentEvent = new Event();\n\n // Signal for protocol-driven completion (when 'done' message is received)\n const completionFuture = new Future<void>();\n\n const resourceCleanup = async () => {\n if (closing) return;\n closing = true;\n sendTokenizerStream.close();\n // close() returns a promise; don't leak it\n await eventChannel.close();\n };\n\n const sendClientEvent = async (event: TtsClientEvent, ws: WebSocket, signal: AbortSignal) => {\n // Don't send events to a closed WebSocket or aborted controller\n if (signal.aborted || closing) return;\n\n const validatedEvent = await ttsClientEventSchema.parseAsync(event);\n if (ws.readyState !== WebSocket.OPEN) {\n this.#logger.warn('Trying to send client TTS event to a closed WebSocket');\n return;\n }\n ws.send(JSON.stringify(validatedEvent));\n };\n\n const sendLastFrame = (segmentId: string, final: boolean) => {\n if (lastFrame) {\n this.queue.put({ requestId, segmentId, frame: lastFrame, final });\n lastFrame = undefined;\n }\n };\n\n const createInputTask = async (signal: AbortSignal) => {\n for await (const data of this.input) {\n if (signal.aborted || closing) break;\n if (data === SynthesizeStream.FLUSH_SENTINEL) {\n sendTokenizerStream.flush();\n continue;\n }\n sendTokenizerStream.pushText(data);\n }\n // Only call endInput if the stream hasn't been closed by cleanup\n if (!closing) {\n sendTokenizerStream.endInput();\n }\n };\n\n const createSentenceStreamTask = async (ws: WebSocket, signal: AbortSignal) => {\n for await (const ev of sendTokenizerStream) {\n if (signal.aborted || closing) break;\n\n await sendClientEvent(\n {\n type: 'input_transcript',\n transcript: ev.token + ' ',\n },\n ws,\n signal,\n );\n inputSentEvent.set();\n }\n\n await sendClientEvent({ type: 'session.flush' }, ws, signal);\n // needed in case empty input is sent\n inputSentEvent.set();\n };\n\n // Handles WebSocket message routing and error handling\n // Completes based on protocol messages, NOT on ws.close()\n const createWsListenerTask = async (ws: WebSocket, signal: AbortSignal) => {\n const onMessage = (data: Buffer) => {\n try {\n const eventJson = JSON.parse(data.toString()) as Record<string, unknown>;\n const validatedEvent = ttsServerEventSchema.parse(eventJson);\n // writer.write returns a promise; avoid unhandled rejections if stream is closed\n void eventChannel.write(validatedEvent).catch((error) => {\n this.#logger.debug(\n { error },\n 'Failed writing TTS event to stream channel (likely closed)',\n );\n });\n } catch (e) {\n this.#logger.error({ error: e }, 'Error parsing WebSocket message');\n }\n };\n\n const onError = (e: Error) => {\n this.#logger.error({ error: e }, 'WebSocket error');\n void resourceCleanup();\n try {\n // If the ws is misbehaving, hard-stop it immediately to avoid buffering.\n ws.terminate?.();\n } catch {\n // ignore\n }\n // Ensure this ws is not reused\n this.tts.pool.remove(ws);\n completionFuture.reject(e);\n };\n\n const onClose = () => {\n // WebSocket closed unexpectedly (not by us)\n if (!closing) {\n this.#logger.error('WebSocket closed unexpectedly');\n void resourceCleanup();\n // Ensure this ws is not reused\n this.tts.pool.remove(ws);\n completionFuture.reject(\n new APIStatusError({\n message: 'Gateway connection closed unexpectedly',\n options: { requestId },\n }),\n );\n }\n };\n\n const onAbort = () => {\n void resourceCleanup();\n try {\n // On interruption/abort, close the websocket immediately so the server stops streaming\n // and the ws library doesn't buffer unread frames in memory.\n ws.terminate?.();\n } catch {\n // ignore\n }\n this.tts.pool.remove(ws);\n inputSentEvent.set();\n completionFuture.resolve();\n };\n\n // Attach listeners\n ws.on('message', onMessage);\n ws.on('error', onError);\n ws.on('close', onClose);\n signal.addEventListener('abort', onAbort);\n\n try {\n // Wait for protocol-driven completion or error\n await completionFuture.await;\n } finally {\n // IMPORTANT: Remove listeners so connection can be reused\n ws.off('message', onMessage);\n ws.off('error', onError);\n ws.off('close', onClose);\n signal.removeEventListener('abort', onAbort);\n }\n };\n\n const createRecvTask = async (signal: AbortSignal) => {\n let currentSessionId: string | null = null;\n\n const bstream = new AudioByteStream(this.opts.sampleRate, NUM_CHANNELS);\n const serverEventStream = eventChannel.stream();\n const reader = serverEventStream.getReader();\n\n try {\n await inputSentEvent.wait();\n\n while (!this.closed && !signal.aborted) {\n const result = await reader.read();\n if (signal.aborted) return;\n if (result.done) return;\n\n const serverEvent = result.value;\n switch (serverEvent.type) {\n case 'session.created':\n currentSessionId = serverEvent.session_id;\n break;\n case 'output_audio':\n const base64Data = new Int8Array(Buffer.from(serverEvent.audio, 'base64'));\n for (const frame of bstream.write(base64Data.buffer)) {\n sendLastFrame(currentSessionId!, false);\n lastFrame = frame;\n }\n break;\n case 'done':\n for (const frame of bstream.flush()) {\n sendLastFrame(currentSessionId!, false);\n lastFrame = frame;\n }\n sendLastFrame(currentSessionId!, true);\n this.queue.put(SynthesizeStream.END_OF_STREAM);\n await resourceCleanup();\n completionFuture.resolve();\n return;\n case 'session.closed':\n await resourceCleanup();\n completionFuture.resolve();\n return;\n case 'error':\n this.#logger.error(\n { serverEvent },\n 'Received error message from LiveKit TTS WebSocket',\n );\n await resourceCleanup();\n completionFuture.reject(\n new APIError(`LiveKit TTS returned error: ${serverEvent.message}`),\n );\n return;\n default:\n this.#logger.warn('Unexpected message %s', serverEvent);\n break;\n }\n }\n } finally {\n reader.releaseLock();\n try {\n await serverEventStream.cancel();\n } catch (e) {\n this.#logger.debug('Error cancelling serverEventStream (may already be cancelled):', e);\n }\n }\n };\n\n try {\n await this.tts.pool.withConnection(\n async (ws: WebSocket) => {\n try {\n // IMPORTANT: don't cancel the stream's controller on normal completion,\n // otherwise the pool will remove+close the ws and every run becomes a pool miss.\n const runController = new AbortController();\n const onStreamAbort = () => runController.abort(this.abortController.signal.reason);\n this.abortController.signal.addEventListener('abort', onStreamAbort, { once: true });\n\n const tasks = [\n Task.from(\n async (controller) => {\n const combined = combineSignals(runController.signal, controller.signal);\n await createInputTask(combined);\n },\n undefined,\n 'inference-tts-input',\n ),\n Task.from(\n async (controller) => {\n const combined = combineSignals(runController.signal, controller.signal);\n await createSentenceStreamTask(ws, combined);\n },\n undefined,\n 'inference-tts-sentence',\n ),\n Task.from(\n async (controller) => {\n const combined = combineSignals(runController.signal, controller.signal);\n await createWsListenerTask(ws, combined);\n },\n undefined,\n 'inference-tts-ws-listener',\n ),\n Task.from(\n async (controller) => {\n const combined = combineSignals(runController.signal, controller.signal);\n await createRecvTask(combined);\n },\n undefined,\n 'inference-tts-recv',\n ),\n ];\n\n try {\n await Promise.all(tasks.map((t) => t.result));\n } finally {\n // Mirror python finally: unblock recv and cancel all tasks.\n inputSentEvent.set();\n await resourceCleanup();\n await cancelAndWait(tasks, 5000);\n this.abortController.signal.removeEventListener('abort', onStreamAbort);\n }\n } catch (e) {\n // If aborted, don't throw - let cleanup handle it\n if (e instanceof Error && e.name === 'AbortError') {\n return;\n }\n throw e;\n }\n },\n {\n timeout: this.connOptions.timeoutMs,\n },\n );\n } catch (e) {\n // Handle connection errors\n if (e instanceof Error && e.name === 'AbortError') {\n // Abort is expected during normal shutdown\n return;\n }\n throw e;\n } finally {\n // Ensure cleanup always runs (and don't leak the promise)\n await resourceCleanup();\n }\n }\n}\n"],"mappings":"AAIA,SAAS,iBAAiB;AAC1B,SAAS,UAAU,sBAAsB;AACzC,SAAS,uBAAuB;AAChC,SAAS,sBAAsB;AAC/B,SAAS,WAAW;AACpB,SAAS,2BAA2B;AACpC,SAAS,SAAS,qBAAqB;AAEvC,SAAS,oBAAoB,sBAAsB,OAAO,eAAe;AACzE,SAAiC,mCAAmC;AACpE,SAAS,OAAO,QAAQ,MAAM,eAAe,gBAAgB,iBAAiB;AAC9E;AAAA,EAIE;AAAA,EACA;AAAA,OACK;AACP,SAAyB,WAAW,yBAAyB;AAoD7D,MAAM,mBAAgC;AACtC,MAAM,sBAAsB;AAC5B,MAAM,mBAAmB;AACzB,MAAM,eAAe;AACrB,MAAM,mBAAmB;AAiBlB,MAAM,YAAsC,QAAQ;AAAA,EACjD;AAAA,EACA,UAAyC,oBAAI,IAAI;AAAA,EACzD;AAAA,EAEA,UAAU,IAAI;AAAA,EAEd,YAAY,MAUT;AACD,UAAM,cAAa,6BAAM,eAAc;AACvC,UAAM,YAAY,GAAG,EAAE,WAAW,KAAK,CAAC;AAExC,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA,WAAW;AAAA,MACX;AAAA,MACA,WAAW;AAAA,MACX;AAAA,MACA;AAAA,MACA,eAAe,CAAC;AAAA,IAClB,IAAI,QAAQ,CAAC;AAEb,UAAM,YAAY,WAAW,QAAQ,IAAI,yBAAyB;AAClE,UAAM,WAAW,UAAU,QAAQ,IAAI,6BAA6B,QAAQ,IAAI;AAChF,QAAI,CAAC,UAAU;AACb,YAAM,IAAI,MAAM,wDAAwD;AAAA,IAC1E;AAEA,UAAM,cACJ,aAAa,QAAQ,IAAI,gCAAgC,QAAQ,IAAI;AACvE,QAAI,CAAC,aAAa;AAChB,YAAM,IAAI,MAAM,iEAAiE;AAAA,IACnF;AAGA,QAAI,YAAY;AAChB,QAAI,YAAY;AAChB,QAAI,OAAO,cAAc,UAAU;AACjC,YAAM,MAAM,UAAU,YAAY,GAAG;AACrC,UAAI,QAAQ,IAAI;AACd,cAAM,iBAAiB,UAAU,MAAM,MAAM,CAAC;AAC9C,YAAI,aAAa,cAAc,gBAAgB;AAC7C,eAAK,QAAQ;AAAA,YACX;AAAA,YACA,EAAE,OAAO,WAAW,OAAO,UAAU;AAAA,UACvC;AAAA,QACF,OAAO;AACL,sBAAY;AAAA,QACd;AACA,oBAAY,UAAU,MAAM,GAAG,GAAG;AAAA,MACpC;AAAA,IACF;AAEA,SAAK,OAAO;AAAA,MACV,OAAO;AAAA,MACP,OAAO;AAAA,MACP;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,WAAW;AAAA,MACX;AAAA,IACF;AAGA,SAAK,OAAO,IAAI,eAA0B;AAAA,MACxC,WAAW,CAAC,YAAY,KAAK,UAAU,OAAO;AAAA,MAC9C,SAAS,CAAC,OAAO,KAAK,QAAQ,EAAE;AAAA,MAChC,oBAAoB;AAAA,MACpB,oBAAoB;AAAA,MACpB,gBAAgB;AAAA;AAAA,IAClB,CAAC;AAAA,EACH;AAAA,EAEA,IAAI,QAAQ;AACV,WAAO;AAAA,EACT;AAAA,EAEA,OAAO,gBAAgB,aAAqC;AAC1D,QAAI,YAAY,SAAS,GAAG,GAAG;AAC7B,YAAM,CAAC,OAAO,KAAK,IAAI,YAAY,MAAM,GAAG;AAC5C,aAAO,IAAI,IAAI,EAAE,OAAO,MAAM,CAAC;AAAA,IACjC;AACA,WAAO,IAAI,IAAI,EAAE,OAAO,YAAY,CAAC;AAAA,EACvC;AAAA,EAEA,cAAc,MAAkF;AAC9F,SAAK,OAAO,EAAE,GAAG,KAAK,MAAM,GAAG,KAAK;AACpC,eAAW,UAAU,KAAK,SAAS;AACjC,aAAO,cAAc,IAAI;AAAA,IAC3B;AAAA,EACF;AAAA,EAEA,WAAW,GAA0B;AACnC,UAAM,IAAI,MAAM,kCAAkC;AAAA,EACpD;AAAA,EAEA,OAAO,SAAyE;AAC9E,UAAM,EAAE,cAAc,4BAA4B,IAAI,WAAW,CAAC;AAClE,UAAM,SAAS,IAAI,iBAAiB,MAAM,EAAE,GAAG,KAAK,KAAK,GAAG,WAAW;AACvE,SAAK,QAAQ,IAAI,MAAM;AACvB,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,UAAU,SAAqC;AACnD,QAAI,UAAU,KAAK,KAAK;AACxB,QAAI,QAAQ,WAAW,SAAS,KAAK,QAAQ,WAAW,UAAU,GAAG;AACnE,gBAAU,QAAQ,QAAQ,QAAQ,IAAI;AAAA,IACxC;AAEA,UAAM,QAAQ,MAAM,kBAAkB,KAAK,KAAK,QAAQ,KAAK,KAAK,SAAS;AAC3E,UAAM,MAAM,GAAG,OAAO;AACtB,UAAM,UAAU,EAAE,eAAe,UAAU,KAAK,GAAG;AAEnD,UAAM,SAAS;AAAA,MACb,MAAM;AAAA,MACN,aAAa,OAAO,KAAK,KAAK,UAAU;AAAA,MACxC,UAAU,KAAK,KAAK;AAAA,MACpB,OAAO,KAAK,KAAK;AAAA,IACnB;AAEA,QAAI,KAAK,KAAK,MAAO,QAAO,QAAQ,KAAK,KAAK;AAC9C,QAAI,KAAK,KAAK,MAAO,QAAO,QAAQ,KAAK,KAAK;AAC9C,QAAI,KAAK,KAAK,SAAU,QAAO,WAAW,KAAK,KAAK;AAEpD,SAAK,QAAQ,MAAM,EAAE,IAAI,GAAG,6DAA6D;AACzF,UAAM,SAAS,MAAM,UAAU,KAAK,SAAS,OAAO;AACpD,WAAO,KAAK,KAAK,UAAU,MAAM,CAAC;AAClC,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,QAAQ,IAAe;AAC3B,UAAM,GAAG,MAAM;AAAA,EACjB;AAAA,EAEA,UAAgB;AACd,SAAK,KAAK,QAAQ;AAAA,EACpB;AAAA,EAEA,MAAM,QAAQ;AACZ,eAAW,UAAU,KAAK,SAAS;AACjC,YAAM,OAAO,MAAM;AAAA,IACrB;AACA,SAAK,QAAQ,MAAM;AACnB,UAAM,KAAK,KAAK,MAAM;AAAA,EACxB;AACF;AAEO,MAAM,yBAAmD,qBAAqB;AAAA,EAC3E;AAAA,EACA;AAAA,EAER,UAAU,IAAI;AAAA,EAEd,YAAY,KAAkB,MAAmC,aAAgC;AAC/F,UAAM,KAAK,WAAW;AACtB,SAAK,OAAO;AACZ,SAAK,MAAM;AAAA,EACb;AAAA,EAEA,IAAI,QAAQ;AACV,WAAO;AAAA,EACT;AAAA,EAEA,cAAc,MAAkF;AAC9F,SAAK,OAAO,EAAE,GAAG,KAAK,MAAM,GAAG,KAAK;AAAA,EACtC;AAAA,EAEA,MAAgB,MAAqB;AACnC,QAAI,UAAU;AACd,QAAI;AAEJ,UAAM,sBAAsB,IAAI,cAAc,kBAAkB,EAAE,OAAO;AACzE,UAAM,eAAe,oBAAoC;AACzD,UAAM,YAAY,UAAU,cAAc;AAC1C,UAAM,iBAAiB,IAAI,MAAM;AAGjC,UAAM,mBAAmB,IAAI,OAAa;AAE1C,UAAM,kBAAkB,YAAY;AAClC,UAAI,QAAS;AACb,gBAAU;AACV,0BAAoB,MAAM;AAE1B,YAAM,aAAa,MAAM;AAAA,IAC3B;AAEA,UAAM,kBAAkB,OAAO,OAAuB,IAAe,WAAwB;AAE3F,UAAI,OAAO,WAAW,QAAS;AAE/B,YAAM,iBAAiB,MAAM,qBAAqB,WAAW,KAAK;AAClE,UAAI,GAAG,eAAe,UAAU,MAAM;AACpC,aAAK,QAAQ,KAAK,uDAAuD;AACzE;AAAA,MACF;AACA,SAAG,KAAK,KAAK,UAAU,cAAc,CAAC;AAAA,IACxC;AAEA,UAAM,gBAAgB,CAAC,WAAmB,UAAmB;AAC3D,UAAI,WAAW;AACb,aAAK,MAAM,IAAI,EAAE,WAAW,WAAW,OAAO,WAAW,MAAM,CAAC;AAChE,oBAAY;AAAA,MACd;AAAA,IACF;AAEA,UAAM,kBAAkB,OAAO,WAAwB;AACrD,uBAAiB,QAAQ,KAAK,OAAO;AACnC,YAAI,OAAO,WAAW,QAAS;AAC/B,YAAI,SAAS,iBAAiB,gBAAgB;AAC5C,8BAAoB,MAAM;AAC1B;AAAA,QACF;AACA,4BAAoB,SAAS,IAAI;AAAA,MACnC;AAEA,UAAI,CAAC,SAAS;AACZ,4BAAoB,SAAS;AAAA,MAC/B;AAAA,IACF;AAEA,UAAM,2BAA2B,OAAO,IAAe,WAAwB;AAC7E,uBAAiB,MAAM,qBAAqB;AAC1C,YAAI,OAAO,WAAW,QAAS;AAE/B,cAAM;AAAA,UACJ;AAAA,YACE,MAAM;AAAA,YACN,YAAY,GAAG,QAAQ;AAAA,UACzB;AAAA,UACA;AAAA,UACA;AAAA,QACF;AACA,uBAAe,IAAI;AAAA,MACrB;AAEA,YAAM,gBAAgB,EAAE,MAAM,gBAAgB,GAAG,IAAI,MAAM;AAE3D,qBAAe,IAAI;AAAA,IACrB;AAIA,UAAM,uBAAuB,OAAO,IAAe,WAAwB;AACzE,YAAM,YAAY,CAAC,SAAiB;AAClC,YAAI;AACF,gBAAM,YAAY,KAAK,MAAM,KAAK,SAAS,CAAC;AAC5C,gBAAM,iBAAiB,qBAAqB,MAAM,SAAS;AAE3D,eAAK,aAAa,MAAM,cAAc,EAAE,MAAM,CAAC,UAAU;AACvD,iBAAK,QAAQ;AAAA,cACX,EAAE,MAAM;AAAA,cACR;AAAA,YACF;AAAA,UACF,CAAC;AAAA,QACH,SAAS,GAAG;AACV,eAAK,QAAQ,MAAM,EAAE,OAAO,EAAE,GAAG,iCAAiC;AAAA,QACpE;AAAA,MACF;AAEA,YAAM,UAAU,CAAC,MAAa;AA/WpC;AAgXQ,aAAK,QAAQ,MAAM,EAAE,OAAO,EAAE,GAAG,iBAAiB;AAClD,aAAK,gBAAgB;AACrB,YAAI;AAEF,mBAAG,cAAH;AAAA,QACF,QAAQ;AAAA,QAER;AAEA,aAAK,IAAI,KAAK,OAAO,EAAE;AACvB,yBAAiB,OAAO,CAAC;AAAA,MAC3B;AAEA,YAAM,UAAU,MAAM;AAEpB,YAAI,CAAC,SAAS;AACZ,eAAK,QAAQ,MAAM,+BAA+B;AAClD,eAAK,gBAAgB;AAErB,eAAK,IAAI,KAAK,OAAO,EAAE;AACvB,2BAAiB;AAAA,YACf,IAAI,eAAe;AAAA,cACjB,SAAS;AAAA,cACT,SAAS,EAAE,UAAU;AAAA,YACvB,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACF;AAEA,YAAM,UAAU,MAAM;AA7Y5B;AA8YQ,aAAK,gBAAgB;AACrB,YAAI;AAGF,mBAAG,cAAH;AAAA,QACF,QAAQ;AAAA,QAER;AACA,aAAK,IAAI,KAAK,OAAO,EAAE;AACvB,uBAAe,IAAI;AACnB,yBAAiB,QAAQ;AAAA,MAC3B;AAGA,SAAG,GAAG,WAAW,SAAS;AAC1B,SAAG,GAAG,SAAS,OAAO;AACtB,SAAG,GAAG,SAAS,OAAO;AACtB,aAAO,iBAAiB,SAAS,OAAO;AAExC,UAAI;AAEF,cAAM,iBAAiB;AAAA,MACzB,UAAE;AAEA,WAAG,IAAI,WAAW,SAAS;AAC3B,WAAG,IAAI,SAAS,OAAO;AACvB,WAAG,IAAI,SAAS,OAAO;AACvB,eAAO,oBAAoB,SAAS,OAAO;AAAA,MAC7C;AAAA,IACF;AAEA,UAAM,iBAAiB,OAAO,WAAwB;AACpD,UAAI,mBAAkC;AAEtC,YAAM,UAAU,IAAI,gBAAgB,KAAK,KAAK,YAAY,YAAY;AACtE,YAAM,oBAAoB,aAAa,OAAO;AAC9C,YAAM,SAAS,kBAAkB,UAAU;AAE3C,UAAI;AACF,cAAM,eAAe,KAAK;AAE1B,eAAO,CAAC,KAAK,UAAU,CAAC,OAAO,SAAS;AACtC,gBAAM,SAAS,MAAM,OAAO,KAAK;AACjC,cAAI,OAAO,QAAS;AACpB,cAAI,OAAO,KAAM;AAEjB,gBAAM,cAAc,OAAO;AAC3B,kBAAQ,YAAY,MAAM;AAAA,YACxB,KAAK;AACH,iCAAmB,YAAY;AAC/B;AAAA,YACF,KAAK;AACH,oBAAM,aAAa,IAAI,UAAU,OAAO,KAAK,YAAY,OAAO,QAAQ,CAAC;AACzE,yBAAW,SAAS,QAAQ,MAAM,WAAW,MAAM,GAAG;AACpD,8BAAc,kBAAmB,KAAK;AACtC,4BAAY;AAAA,cACd;AACA;AAAA,YACF,KAAK;AACH,yBAAW,SAAS,QAAQ,MAAM,GAAG;AACnC,8BAAc,kBAAmB,KAAK;AACtC,4BAAY;AAAA,cACd;AACA,4BAAc,kBAAmB,IAAI;AACrC,mBAAK,MAAM,IAAI,iBAAiB,aAAa;AAC7C,oBAAM,gBAAgB;AACtB,+BAAiB,QAAQ;AACzB;AAAA,YACF,KAAK;AACH,oBAAM,gBAAgB;AACtB,+BAAiB,QAAQ;AACzB;AAAA,YACF,KAAK;AACH,mBAAK,QAAQ;AAAA,gBACX,EAAE,YAAY;AAAA,gBACd;AAAA,cACF;AACA,oBAAM,gBAAgB;AACtB,+BAAiB;AAAA,gBACf,IAAI,SAAS,+BAA+B,YAAY,OAAO,EAAE;AAAA,cACnE;AACA;AAAA,YACF;AACE,mBAAK,QAAQ,KAAK,yBAAyB,WAAW;AACtD;AAAA,UACJ;AAAA,QACF;AAAA,MACF,UAAE;AACA,eAAO,YAAY;AACnB,YAAI;AACF,gBAAM,kBAAkB,OAAO;AAAA,QACjC,SAAS,GAAG;AACV,eAAK,QAAQ,MAAM,kEAAkE,CAAC;AAAA,QACxF;AAAA,MACF;AAAA,IACF;AAEA,QAAI;AACF,YAAM,KAAK,IAAI,KAAK;AAAA,QAClB,OAAO,OAAkB;AACvB,cAAI;AAGF,kBAAM,gBAAgB,IAAI,gBAAgB;AAC1C,kBAAM,gBAAgB,MAAM,cAAc,MAAM,KAAK,gBAAgB,OAAO,MAAM;AAClF,iBAAK,gBAAgB,OAAO,iBAAiB,SAAS,eAAe,EAAE,MAAM,KAAK,CAAC;AAEnF,kBAAM,QAAQ;AAAA,cACZ,KAAK;AAAA,gBACH,OAAO,eAAe;AACpB,wBAAM,WAAW,eAAe,cAAc,QAAQ,WAAW,MAAM;AACvE,wBAAM,gBAAgB,QAAQ;AAAA,gBAChC;AAAA,gBACA;AAAA,gBACA;AAAA,cACF;AAAA,cACA,KAAK;AAAA,gBACH,OAAO,eAAe;AACpB,wBAAM,WAAW,eAAe,cAAc,QAAQ,WAAW,MAAM;AACvE,wBAAM,yBAAyB,IAAI,QAAQ;AAAA,gBAC7C;AAAA,gBACA;AAAA,gBACA;AAAA,cACF;AAAA,cACA,KAAK;AAAA,gBACH,OAAO,eAAe;AACpB,wBAAM,WAAW,eAAe,cAAc,QAAQ,WAAW,MAAM;AACvE,wBAAM,qBAAqB,IAAI,QAAQ;AAAA,gBACzC;AAAA,gBACA;AAAA,gBACA;AAAA,cACF;AAAA,cACA,KAAK;AAAA,gBACH,OAAO,eAAe;AACpB,wBAAM,WAAW,eAAe,cAAc,QAAQ,WAAW,MAAM;AACvE,wBAAM,eAAe,QAAQ;AAAA,gBAC/B;AAAA,gBACA;AAAA,gBACA;AAAA,cACF;AAAA,YACF;AAEA,gBAAI;AACF,oBAAM,QAAQ,IAAI,MAAM,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;AAAA,YAC9C,UAAE;AAEA,6BAAe,IAAI;AACnB,oBAAM,gBAAgB;AACtB,oBAAM,cAAc,OAAO,GAAI;AAC/B,mBAAK,gBAAgB,OAAO,oBAAoB,SAAS,aAAa;AAAA,YACxE;AAAA,UACF,SAAS,GAAG;AAEV,gBAAI,aAAa,SAAS,EAAE,SAAS,cAAc;AACjD;AAAA,YACF;AACA,kBAAM;AAAA,UACR;AAAA,QACF;AAAA,QACA;AAAA,UACE,SAAS,KAAK,YAAY;AAAA,QAC5B;AAAA,MACF;AAAA,IACF,SAAS,GAAG;AAEV,UAAI,aAAa,SAAS,EAAE,SAAS,cAAc;AAEjD;AAAA,MACF;AACA,YAAM;AAAA,IACR,UAAE;AAEA,YAAM,gBAAgB;AAAA,IACxB;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/inference/tts.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport { WebSocket } from 'ws';\nimport { APIError, APIStatusError } from '../_exceptions.js';\nimport { AudioByteStream } from '../audio.js';\nimport { ConnectionPool } from '../connection_pool.js';\nimport { log } from '../log.js';\nimport { createStreamChannel } from '../stream/stream_channel.js';\nimport { basic as tokenizeBasic } from '../tokenize/index.js';\nimport type { ChunkedStream } from '../tts/index.js';\nimport { SynthesizeStream as BaseSynthesizeStream, TTS as BaseTTS } from '../tts/index.js';\nimport { type APIConnectOptions, DEFAULT_API_CONNECT_OPTIONS } from '../types.js';\nimport { Event, Future, Task, cancelAndWait, combineSignals, shortuuid } from '../utils.js';\nimport {\n type TtsClientEvent,\n type TtsServerEvent,\n type TtsSessionCreateEvent,\n ttsClientEventSchema,\n ttsServerEventSchema,\n} from './api_protos.js';\nimport { type AnyString, connectWs, createAccessToken } from './utils.js';\n\nexport type CartesiaModels =\n | 'cartesia/sonic-3'\n | 'cartesia/sonic-2'\n | 'cartesia/sonic-turbo'\n | 'cartesia/sonic';\n\nexport type DeepgramTTSModels = 'deepgram/aura' | 'deepgram/aura-2';\n\nexport type ElevenlabsModels =\n | 'elevenlabs/eleven_flash_v2'\n | 'elevenlabs/eleven_flash_v2_5'\n | 'elevenlabs/eleven_turbo_v2'\n | 'elevenlabs/eleven_turbo_v2_5'\n | 'elevenlabs/eleven_multilingual_v2';\n\nexport type InworldModels =\n | 'inworld/inworld-tts-1.5-max'\n | 'inworld/inworld-tts-1.5-mini'\n | 'inworld/inworld-tts-1-max'\n | 'inworld/inworld-tts-1';\n\nexport type RimeModels = 'rime/arcana' | 'rime/mistv2';\n\nexport interface CartesiaOptions {\n duration?: number; // max duration of audio in seconds\n speed?: 'slow' | 'normal' | 'fast'; // default: not specified\n}\n\nexport interface ElevenlabsOptions {\n inactivity_timeout?: number; // default: 60\n apply_text_normalization?: 'auto' | 'off' | 'on'; // default: \"auto\"\n}\n\nexport interface DeepgramTTSOptions {}\n\nexport interface RimeOptions {}\n\nexport interface InworldOptions {}\n\ntype _TTSModels =\n | CartesiaModels\n | DeepgramTTSModels\n | ElevenlabsModels\n | RimeModels\n | InworldModels;\n\nexport type TTSModels =\n | CartesiaModels\n | DeepgramTTSModels\n | ElevenlabsModels\n | RimeModels\n | InworldModels\n | AnyString;\n\nexport type ModelWithVoice = `${_TTSModels}:${string}` | TTSModels;\n\nexport type TTSOptions<TModel extends TTSModels> = TModel extends CartesiaModels\n ? CartesiaOptions\n : TModel extends DeepgramTTSModels\n ? DeepgramTTSOptions\n : TModel extends ElevenlabsModels\n ? ElevenlabsOptions\n : TModel extends RimeModels\n ? RimeOptions\n : TModel extends InworldModels\n ? InworldOptions\n : Record<string, unknown>;\n\ntype TTSEncoding = 'pcm_s16le';\n\nconst DEFAULT_ENCODING: TTSEncoding = 'pcm_s16le';\nconst DEFAULT_SAMPLE_RATE = 16000;\nconst DEFAULT_BASE_URL = 'https://agent-gateway.livekit.cloud/v1';\nconst NUM_CHANNELS = 1;\nconst DEFAULT_LANGUAGE = 'en';\n\nexport interface InferenceTTSOptions<TModel extends TTSModels> {\n model?: TModel;\n voice?: string;\n language?: string;\n encoding: TTSEncoding;\n sampleRate: number;\n baseURL: string;\n apiKey: string;\n apiSecret: string;\n modelOptions: TTSOptions<TModel>;\n}\n\n/**\n * Livekit Cloud Inference TTS\n */\nexport class TTS<TModel extends TTSModels> extends BaseTTS {\n private opts: InferenceTTSOptions<TModel>;\n private streams: Set<SynthesizeStream<TModel>> = new Set();\n pool: ConnectionPool<WebSocket>;\n\n #logger = log();\n\n constructor(opts: {\n model: TModel;\n voice?: string;\n language?: string;\n baseURL?: string;\n encoding?: TTSEncoding;\n sampleRate?: number;\n apiKey?: string;\n apiSecret?: string;\n modelOptions?: TTSOptions<TModel>;\n }) {\n const sampleRate = opts?.sampleRate ?? DEFAULT_SAMPLE_RATE;\n super(sampleRate, 1, { streaming: true });\n\n const {\n model,\n voice,\n language = DEFAULT_LANGUAGE,\n baseURL,\n encoding = DEFAULT_ENCODING,\n apiKey,\n apiSecret,\n modelOptions = {} as TTSOptions<TModel>,\n } = opts || {};\n\n const lkBaseURL = baseURL || process.env.LIVEKIT_INFERENCE_URL || DEFAULT_BASE_URL;\n const lkApiKey = apiKey || process.env.LIVEKIT_INFERENCE_API_KEY || process.env.LIVEKIT_API_KEY;\n if (!lkApiKey) {\n throw new Error('apiKey is required: pass apiKey or set LIVEKIT_API_KEY');\n }\n\n const lkApiSecret =\n apiSecret || process.env.LIVEKIT_INFERENCE_API_SECRET || process.env.LIVEKIT_API_SECRET;\n if (!lkApiSecret) {\n throw new Error('apiSecret is required: pass apiSecret or set LIVEKIT_API_SECRET');\n }\n\n // read voice id from the model if provided: \"provider/model:voice_id\"\n let nextModel = model;\n let nextVoice = voice;\n if (typeof nextModel === 'string') {\n const idx = nextModel.lastIndexOf(':');\n if (idx !== -1) {\n const voiceFromModel = nextModel.slice(idx + 1);\n if (nextVoice && nextVoice !== voiceFromModel) {\n this.#logger.warn(\n '`voice` is provided via both argument and model, using the one from the argument',\n { voice: nextVoice, model: nextModel },\n );\n } else {\n nextVoice = voiceFromModel;\n }\n nextModel = nextModel.slice(0, idx) as TModel;\n }\n }\n\n this.opts = {\n model: nextModel,\n voice: nextVoice,\n language,\n encoding,\n sampleRate,\n baseURL: lkBaseURL,\n apiKey: lkApiKey,\n apiSecret: lkApiSecret,\n modelOptions,\n };\n\n // Initialize connection pool\n this.pool = new ConnectionPool<WebSocket>({\n connectCb: (timeout) => this.connectWs(timeout),\n closeCb: (ws) => this.closeWs(ws),\n maxSessionDuration: 300_000,\n markRefreshedOnGet: true,\n connectTimeout: 10_000, // 10 seconds default\n });\n }\n\n get label() {\n return 'inference.TTS';\n }\n\n static fromModelString(modelString: string): TTS<AnyString> {\n if (modelString.includes(':')) {\n const [model, voice] = modelString.split(':') as [TTSModels, string];\n return new TTS({ model, voice });\n }\n return new TTS({ model: modelString });\n }\n\n updateOptions(opts: Partial<Pick<InferenceTTSOptions<TModel>, 'model' | 'voice' | 'language'>>) {\n this.opts = { ...this.opts, ...opts };\n for (const stream of this.streams) {\n stream.updateOptions(opts);\n }\n }\n\n synthesize(_: string): ChunkedStream {\n throw new Error('ChunkedStream is not implemented');\n }\n\n stream(options?: { connOptions?: APIConnectOptions }): SynthesizeStream<TModel> {\n const { connOptions = DEFAULT_API_CONNECT_OPTIONS } = options || {};\n const stream = new SynthesizeStream(this, { ...this.opts }, connOptions);\n this.streams.add(stream);\n return stream;\n }\n\n async connectWs(timeout: number): Promise<WebSocket> {\n let baseURL = this.opts.baseURL;\n if (baseURL.startsWith('http://') || baseURL.startsWith('https://')) {\n baseURL = baseURL.replace('http', 'ws');\n }\n\n const token = await createAccessToken(this.opts.apiKey, this.opts.apiSecret);\n const url = `${baseURL}/tts`;\n const headers = { Authorization: `Bearer ${token}` } as Record<string, string>;\n\n const params = {\n type: 'session.create',\n sample_rate: String(this.opts.sampleRate),\n encoding: this.opts.encoding,\n extra: this.opts.modelOptions,\n } as TtsSessionCreateEvent;\n\n if (this.opts.voice) params.voice = this.opts.voice;\n if (this.opts.model) params.model = this.opts.model;\n if (this.opts.language) params.language = this.opts.language;\n\n this.#logger.debug({ url }, 'inference.TTS creating new websocket connection (pool miss)');\n const socket = await connectWs(url, headers, timeout);\n socket.send(JSON.stringify(params));\n return socket;\n }\n\n async closeWs(ws: WebSocket) {\n await ws.close();\n }\n\n prewarm(): void {\n this.pool.prewarm();\n }\n\n async close() {\n for (const stream of this.streams) {\n await stream.close();\n }\n this.streams.clear();\n await this.pool.close();\n }\n}\n\nexport class SynthesizeStream<TModel extends TTSModels> extends BaseSynthesizeStream {\n private opts: InferenceTTSOptions<TModel>;\n private tts: TTS<TModel>;\n\n #logger = log();\n\n constructor(tts: TTS<TModel>, opts: InferenceTTSOptions<TModel>, connOptions: APIConnectOptions) {\n super(tts, connOptions);\n this.opts = opts;\n this.tts = tts;\n }\n\n get label() {\n return 'inference.SynthesizeStream';\n }\n\n updateOptions(opts: Partial<Pick<InferenceTTSOptions<TModel>, 'model' | 'voice' | 'language'>>) {\n this.opts = { ...this.opts, ...opts };\n }\n\n protected async run(): Promise<void> {\n let closing = false;\n let lastFrame: AudioFrame | undefined;\n\n const sendTokenizerStream = new tokenizeBasic.SentenceTokenizer().stream();\n const eventChannel = createStreamChannel<TtsServerEvent>();\n const requestId = shortuuid('tts_request_');\n const inputSentEvent = new Event();\n\n // Signal for protocol-driven completion (when 'done' message is received)\n const completionFuture = new Future<void>();\n\n const resourceCleanup = async () => {\n if (closing) return;\n closing = true;\n sendTokenizerStream.close();\n // close() returns a promise; don't leak it\n await eventChannel.close();\n };\n\n const sendClientEvent = async (event: TtsClientEvent, ws: WebSocket, signal: AbortSignal) => {\n // Don't send events to a closed WebSocket or aborted controller\n if (signal.aborted || closing) return;\n\n const validatedEvent = await ttsClientEventSchema.parseAsync(event);\n if (ws.readyState !== WebSocket.OPEN) {\n this.#logger.warn('Trying to send client TTS event to a closed WebSocket');\n return;\n }\n ws.send(JSON.stringify(validatedEvent));\n };\n\n const sendLastFrame = (segmentId: string, final: boolean) => {\n if (lastFrame) {\n this.queue.put({ requestId, segmentId, frame: lastFrame, final });\n lastFrame = undefined;\n }\n };\n\n const createInputTask = async (signal: AbortSignal) => {\n for await (const data of this.input) {\n if (signal.aborted || closing) break;\n if (data === SynthesizeStream.FLUSH_SENTINEL) {\n sendTokenizerStream.flush();\n continue;\n }\n sendTokenizerStream.pushText(data);\n }\n // Only call endInput if the stream hasn't been closed by cleanup\n if (!closing) {\n sendTokenizerStream.endInput();\n }\n };\n\n const createSentenceStreamTask = async (ws: WebSocket, signal: AbortSignal) => {\n for await (const ev of sendTokenizerStream) {\n if (signal.aborted || closing) break;\n\n await sendClientEvent(\n {\n type: 'input_transcript',\n transcript: ev.token + ' ',\n },\n ws,\n signal,\n );\n inputSentEvent.set();\n }\n\n await sendClientEvent({ type: 'session.flush' }, ws, signal);\n // needed in case empty input is sent\n inputSentEvent.set();\n };\n\n // Handles WebSocket message routing and error handling\n // Completes based on protocol messages, NOT on ws.close()\n const createWsListenerTask = async (ws: WebSocket, signal: AbortSignal) => {\n const onMessage = (data: Buffer) => {\n try {\n const eventJson = JSON.parse(data.toString()) as Record<string, unknown>;\n const validatedEvent = ttsServerEventSchema.parse(eventJson);\n // writer.write returns a promise; avoid unhandled rejections if stream is closed\n void eventChannel.write(validatedEvent).catch((error) => {\n this.#logger.debug(\n { error },\n 'Failed writing TTS event to stream channel (likely closed)',\n );\n });\n } catch (e) {\n this.#logger.error({ error: e }, 'Error parsing WebSocket message');\n }\n };\n\n const onError = (e: Error) => {\n this.#logger.error({ error: e }, 'WebSocket error');\n void resourceCleanup();\n try {\n // If the ws is misbehaving, hard-stop it immediately to avoid buffering.\n ws.terminate?.();\n } catch {\n // ignore\n }\n // Ensure this ws is not reused\n this.tts.pool.remove(ws);\n completionFuture.reject(e);\n };\n\n const onClose = () => {\n // WebSocket closed unexpectedly (not by us)\n if (!closing) {\n this.#logger.error('WebSocket closed unexpectedly');\n void resourceCleanup();\n // Ensure this ws is not reused\n this.tts.pool.remove(ws);\n completionFuture.reject(\n new APIStatusError({\n message: 'Gateway connection closed unexpectedly',\n options: { requestId },\n }),\n );\n }\n };\n\n const onAbort = () => {\n void resourceCleanup();\n try {\n // On interruption/abort, close the websocket immediately so the server stops streaming\n // and the ws library doesn't buffer unread frames in memory.\n ws.terminate?.();\n } catch {\n // ignore\n }\n this.tts.pool.remove(ws);\n inputSentEvent.set();\n completionFuture.resolve();\n };\n\n // Attach listeners\n ws.on('message', onMessage);\n ws.on('error', onError);\n ws.on('close', onClose);\n signal.addEventListener('abort', onAbort);\n\n try {\n // Wait for protocol-driven completion or error\n await completionFuture.await;\n } finally {\n // IMPORTANT: Remove listeners so connection can be reused\n ws.off('message', onMessage);\n ws.off('error', onError);\n ws.off('close', onClose);\n signal.removeEventListener('abort', onAbort);\n }\n };\n\n const createRecvTask = async (signal: AbortSignal) => {\n let currentSessionId: string | null = null;\n\n const bstream = new AudioByteStream(this.opts.sampleRate, NUM_CHANNELS);\n const serverEventStream = eventChannel.stream();\n const reader = serverEventStream.getReader();\n\n try {\n await inputSentEvent.wait();\n\n while (!this.closed && !signal.aborted) {\n const result = await reader.read();\n if (signal.aborted) return;\n if (result.done) return;\n\n const serverEvent = result.value;\n switch (serverEvent.type) {\n case 'session.created':\n currentSessionId = serverEvent.session_id;\n break;\n case 'output_audio':\n const base64Data = new Int8Array(Buffer.from(serverEvent.audio, 'base64'));\n for (const frame of bstream.write(base64Data.buffer)) {\n sendLastFrame(currentSessionId!, false);\n lastFrame = frame;\n }\n break;\n case 'done':\n for (const frame of bstream.flush()) {\n sendLastFrame(currentSessionId!, false);\n lastFrame = frame;\n }\n sendLastFrame(currentSessionId!, true);\n this.queue.put(SynthesizeStream.END_OF_STREAM);\n await resourceCleanup();\n completionFuture.resolve();\n return;\n case 'session.closed':\n await resourceCleanup();\n completionFuture.resolve();\n return;\n case 'error':\n this.#logger.error(\n { serverEvent },\n 'Received error message from LiveKit TTS WebSocket',\n );\n await resourceCleanup();\n completionFuture.reject(\n new APIError(`LiveKit TTS returned error: ${serverEvent.message}`),\n );\n return;\n default:\n this.#logger.warn('Unexpected message %s', serverEvent);\n break;\n }\n }\n } finally {\n reader.releaseLock();\n try {\n await serverEventStream.cancel();\n } catch (e) {\n this.#logger.debug('Error cancelling serverEventStream (may already be cancelled):', e);\n }\n }\n };\n\n try {\n await this.tts.pool.withConnection(\n async (ws: WebSocket) => {\n try {\n // IMPORTANT: don't cancel the stream's controller on normal completion,\n // otherwise the pool will remove+close the ws and every run becomes a pool miss.\n const runController = new AbortController();\n const onStreamAbort = () => runController.abort(this.abortController.signal.reason);\n this.abortController.signal.addEventListener('abort', onStreamAbort, { once: true });\n\n const tasks = [\n Task.from(\n async (controller) => {\n const combined = combineSignals(runController.signal, controller.signal);\n await createInputTask(combined);\n },\n undefined,\n 'inference-tts-input',\n ),\n Task.from(\n async (controller) => {\n const combined = combineSignals(runController.signal, controller.signal);\n await createSentenceStreamTask(ws, combined);\n },\n undefined,\n 'inference-tts-sentence',\n ),\n Task.from(\n async (controller) => {\n const combined = combineSignals(runController.signal, controller.signal);\n await createWsListenerTask(ws, combined);\n },\n undefined,\n 'inference-tts-ws-listener',\n ),\n Task.from(\n async (controller) => {\n const combined = combineSignals(runController.signal, controller.signal);\n await createRecvTask(combined);\n },\n undefined,\n 'inference-tts-recv',\n ),\n ];\n\n try {\n await Promise.all(tasks.map((t) => t.result));\n } finally {\n // Mirror python finally: unblock recv and cancel all tasks.\n inputSentEvent.set();\n await resourceCleanup();\n await cancelAndWait(tasks, 5000);\n this.abortController.signal.removeEventListener('abort', onStreamAbort);\n }\n } catch (e) {\n // If aborted, don't throw - let cleanup handle it\n if (e instanceof Error && e.name === 'AbortError') {\n return;\n }\n throw e;\n }\n },\n {\n timeout: this.connOptions.timeoutMs,\n },\n );\n } catch (e) {\n // Handle connection errors\n if (e instanceof Error && e.name === 'AbortError') {\n // Abort is expected during normal shutdown\n return;\n }\n throw e;\n } finally {\n // Ensure cleanup always runs (and don't leak the promise)\n await resourceCleanup();\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -61,6 +61,10 @@ class SupervisedProc {
61
61
  get started() {
62
62
  return this.#started;
63
63
  }
64
+ get isAlive() {
65
+ var _a;
66
+ return this.#started && !this.#closing && !!((_a = this.proc) == null ? void 0 : _a.connected);
67
+ }
64
68
  get runningJob() {
65
69
  return this.#runningJob;
66
70
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/ipc/supervised_proc.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { ChildProcess } from 'node:child_process';\nimport { once } from 'node:events';\nimport pidusage from 'pidusage';\nimport type { RunningJobInfo } from '../job.js';\nimport { log, loggerOptions } from '../log.js';\nimport { Future } from '../utils.js';\nimport type { IPCMessage } from './message.js';\n\nexport interface ProcOpts {\n initializeTimeout: number;\n closeTimeout: number;\n memoryWarnMB: number;\n memoryLimitMB: number;\n pingInterval: number;\n pingTimeout: number;\n highPingThreshold: number;\n}\n\nexport abstract class SupervisedProc {\n #opts: ProcOpts;\n #started = false;\n #closing = false;\n #runningJob?: RunningJobInfo = undefined;\n proc?: ChildProcess;\n #pingInterval?: ReturnType<typeof setInterval>;\n #memoryMonitorInterval?: ReturnType<typeof setInterval>;\n #pongTimeout?: ReturnType<typeof setTimeout>;\n protected init = new Future();\n #join = new Future();\n #logger = log().child({ runningJob: this.#runningJob });\n\n constructor(\n initializeTimeout: number,\n closeTimeout: number,\n memoryWarnMB: number,\n memoryLimitMB: number,\n pingInterval: number,\n pingTimeout: number,\n highPingThreshold: number,\n ) {\n this.#opts = {\n initializeTimeout,\n closeTimeout,\n memoryWarnMB,\n memoryLimitMB,\n pingInterval,\n pingTimeout,\n highPingThreshold,\n };\n }\n\n abstract createProcess(): ChildProcess;\n abstract mainTask(child: ChildProcess): Promise<void>;\n\n get started(): boolean {\n return this.#started;\n }\n\n get runningJob(): RunningJobInfo | undefined {\n return this.#runningJob;\n }\n\n async start() {\n if (this.#started) {\n throw new Error('runner already started');\n } else if (this.#closing) {\n throw new Error('runner is closed');\n }\n\n this.proc = this.createProcess();\n\n this.#started = true;\n this.run();\n }\n\n async run() {\n await this.init.await;\n\n this.#pingInterval = setInterval(() => {\n if (this.proc?.connected) {\n this.proc.send({ case: 'pingRequest', value: { timestamp: Date.now() } });\n }\n }, this.#opts.pingInterval);\n\n this.#pongTimeout = setTimeout(() => {\n this.#logger.warn('job is unresponsive');\n clearTimeout(this.#pongTimeout);\n clearInterval(this.#pingInterval);\n this.proc!.kill();\n this.#join.resolve();\n }, this.#opts.pingTimeout);\n\n this.#memoryMonitorInterval = setInterval(async () => {\n const memoryMB = await this.getChildMemoryUsageMB();\n if (this.#opts.memoryLimitMB > 0 && memoryMB > this.#opts.memoryLimitMB) {\n this.#logger\n .child({ memoryUsageMB: memoryMB, memoryLimitMB: this.#opts.memoryLimitMB })\n .error('process exceeded memory limit, killing process');\n this.close();\n } else if (this.#opts.memoryWarnMB > 0 && memoryMB > this.#opts.memoryWarnMB) {\n this.#logger\n .child({\n memoryUsageMB: memoryMB,\n memoryWarnMB: this.#opts.memoryWarnMB,\n memoryLimitMB: this.#opts.memoryLimitMB,\n })\n .warn('process memory usage is high');\n }\n }, 5000);\n\n const listener = (msg: IPCMessage) => {\n switch (msg.case) {\n case 'pongResponse': {\n const delay = Date.now() - msg.value.timestamp;\n if (delay > this.#opts.highPingThreshold) {\n this.#logger.child({ delay }).warn('job executor is unresponsive');\n }\n this.#pongTimeout?.refresh();\n break;\n }\n case 'exiting': {\n this.#logger.child({ reason: msg.value.reason }).debug('job exiting');\n break;\n }\n case 'done': {\n this.#closing = true;\n this.proc!.off('message', listener);\n break;\n }\n }\n };\n this.proc!.on('message', listener);\n this.proc!.on('error', (err) => {\n if (this.#closing) return;\n this.#logger\n .child({ err })\n .warn('job process exited unexpectedly; this likely means the error above caused a crash');\n this.clearTimers();\n this.#join.resolve();\n });\n\n this.proc!.on('exit', () => {\n this.clearTimers();\n this.#join.resolve();\n });\n\n this.mainTask(this.proc!);\n\n await this.#join.await;\n }\n\n async join() {\n if (!this.#started) {\n throw new Error('runner not started');\n }\n\n await this.#join.await;\n }\n\n async initialize() {\n const timer = setTimeout(() => {\n this.init.reject(new Error('runner initialization timed out'));\n }, this.#opts.initializeTimeout);\n if (!this.proc?.connected) {\n this.init.reject(new Error('process not connected'));\n return;\n }\n this.proc.send({\n case: 'initializeRequest',\n value: {\n loggerOptions,\n pingInterval: this.#opts.pingInterval,\n pingTimeout: this.#opts.pingTimeout,\n highPingThreshold: this.#opts.highPingThreshold,\n },\n });\n await once(this.proc!, 'message').then(([msg]: IPCMessage[]) => {\n clearTimeout(timer);\n if (msg!.case !== 'initializeResponse') {\n throw new Error('first message must be InitializeResponse');\n }\n });\n this.init.resolve();\n }\n\n async close() {\n if (!this.#started) {\n return;\n }\n this.#closing = true;\n\n if (this.proc?.connected) {\n this.proc.send({ case: 'shutdownRequest' });\n }\n\n const timer = setTimeout(() => {\n this.#logger.error('job shutdown is taking too much time');\n this.proc!.kill();\n }, this.#opts.closeTimeout);\n await this.#join.await.then(() => {\n clearTimeout(timer);\n this.clearTimers();\n });\n }\n\n async launchJob(info: RunningJobInfo) {\n if (this.#runningJob) {\n throw new Error('executor already has a running job');\n }\n if (!this.proc?.connected) {\n throw new Error('process not connected');\n }\n this.#runningJob = info;\n this.proc.send({ case: 'startJobRequest', value: { runningJob: info } });\n }\n\n private async getChildMemoryUsageMB(): Promise<number> {\n const pid = this.proc?.pid;\n if (!pid) {\n return 0;\n }\n try {\n const stats = await pidusage(pid);\n return stats.memory / (1024 * 1024);\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === 'ENOENT' || code === 'ESRCH') {\n return 0;\n }\n throw err;\n }\n }\n\n private clearTimers() {\n clearTimeout(this.#pongTimeout);\n clearInterval(this.#pingInterval);\n clearInterval(this.#memoryMonitorInterval);\n }\n}\n"],"mappings":"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","names":["pidusage"]}
1
+ {"version":3,"sources":["../../src/ipc/supervised_proc.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { ChildProcess } from 'node:child_process';\nimport { once } from 'node:events';\nimport pidusage from 'pidusage';\nimport type { RunningJobInfo } from '../job.js';\nimport { log, loggerOptions } from '../log.js';\nimport { Future } from '../utils.js';\nimport type { IPCMessage } from './message.js';\n\nexport interface ProcOpts {\n initializeTimeout: number;\n closeTimeout: number;\n memoryWarnMB: number;\n memoryLimitMB: number;\n pingInterval: number;\n pingTimeout: number;\n highPingThreshold: number;\n}\n\nexport abstract class SupervisedProc {\n #opts: ProcOpts;\n #started = false;\n #closing = false;\n #runningJob?: RunningJobInfo = undefined;\n proc?: ChildProcess;\n #pingInterval?: ReturnType<typeof setInterval>;\n #memoryMonitorInterval?: ReturnType<typeof setInterval>;\n #pongTimeout?: ReturnType<typeof setTimeout>;\n protected init = new Future();\n #join = new Future();\n #logger = log().child({ runningJob: this.#runningJob });\n\n constructor(\n initializeTimeout: number,\n closeTimeout: number,\n memoryWarnMB: number,\n memoryLimitMB: number,\n pingInterval: number,\n pingTimeout: number,\n highPingThreshold: number,\n ) {\n this.#opts = {\n initializeTimeout,\n closeTimeout,\n memoryWarnMB,\n memoryLimitMB,\n pingInterval,\n pingTimeout,\n highPingThreshold,\n };\n }\n\n abstract createProcess(): ChildProcess;\n abstract mainTask(child: ChildProcess): Promise<void>;\n\n get started(): boolean {\n return this.#started;\n }\n\n get isAlive(): boolean {\n return this.#started && !this.#closing && !!this.proc?.connected;\n }\n\n get runningJob(): RunningJobInfo | undefined {\n return this.#runningJob;\n }\n\n async start() {\n if (this.#started) {\n throw new Error('runner already started');\n } else if (this.#closing) {\n throw new Error('runner is closed');\n }\n\n this.proc = this.createProcess();\n\n this.#started = true;\n this.run();\n }\n\n async run() {\n await this.init.await;\n\n this.#pingInterval = setInterval(() => {\n if (this.proc?.connected) {\n this.proc.send({ case: 'pingRequest', value: { timestamp: Date.now() } });\n }\n }, this.#opts.pingInterval);\n\n this.#pongTimeout = setTimeout(() => {\n this.#logger.warn('job is unresponsive');\n clearTimeout(this.#pongTimeout);\n clearInterval(this.#pingInterval);\n this.proc!.kill();\n this.#join.resolve();\n }, this.#opts.pingTimeout);\n\n this.#memoryMonitorInterval = setInterval(async () => {\n const memoryMB = await this.getChildMemoryUsageMB();\n if (this.#opts.memoryLimitMB > 0 && memoryMB > this.#opts.memoryLimitMB) {\n this.#logger\n .child({ memoryUsageMB: memoryMB, memoryLimitMB: this.#opts.memoryLimitMB })\n .error('process exceeded memory limit, killing process');\n this.close();\n } else if (this.#opts.memoryWarnMB > 0 && memoryMB > this.#opts.memoryWarnMB) {\n this.#logger\n .child({\n memoryUsageMB: memoryMB,\n memoryWarnMB: this.#opts.memoryWarnMB,\n memoryLimitMB: this.#opts.memoryLimitMB,\n })\n .warn('process memory usage is high');\n }\n }, 5000);\n\n const listener = (msg: IPCMessage) => {\n switch (msg.case) {\n case 'pongResponse': {\n const delay = Date.now() - msg.value.timestamp;\n if (delay > this.#opts.highPingThreshold) {\n this.#logger.child({ delay }).warn('job executor is unresponsive');\n }\n this.#pongTimeout?.refresh();\n break;\n }\n case 'exiting': {\n this.#logger.child({ reason: msg.value.reason }).debug('job exiting');\n break;\n }\n case 'done': {\n this.#closing = true;\n this.proc!.off('message', listener);\n break;\n }\n }\n };\n this.proc!.on('message', listener);\n this.proc!.on('error', (err) => {\n if (this.#closing) return;\n this.#logger\n .child({ err })\n .warn('job process exited unexpectedly; this likely means the error above caused a crash');\n this.clearTimers();\n this.#join.resolve();\n });\n\n this.proc!.on('exit', () => {\n this.clearTimers();\n this.#join.resolve();\n });\n\n this.mainTask(this.proc!);\n\n await this.#join.await;\n }\n\n async join() {\n if (!this.#started) {\n throw new Error('runner not started');\n }\n\n await this.#join.await;\n }\n\n async initialize() {\n const timer = setTimeout(() => {\n this.init.reject(new Error('runner initialization timed out'));\n }, this.#opts.initializeTimeout);\n if (!this.proc?.connected) {\n this.init.reject(new Error('process not connected'));\n return;\n }\n this.proc.send({\n case: 'initializeRequest',\n value: {\n loggerOptions,\n pingInterval: this.#opts.pingInterval,\n pingTimeout: this.#opts.pingTimeout,\n highPingThreshold: this.#opts.highPingThreshold,\n },\n });\n await once(this.proc!, 'message').then(([msg]: IPCMessage[]) => {\n clearTimeout(timer);\n if (msg!.case !== 'initializeResponse') {\n throw new Error('first message must be InitializeResponse');\n }\n });\n this.init.resolve();\n }\n\n async close() {\n if (!this.#started) {\n return;\n }\n this.#closing = true;\n\n if (this.proc?.connected) {\n this.proc.send({ case: 'shutdownRequest' });\n }\n\n const timer = setTimeout(() => {\n this.#logger.error('job shutdown is taking too much time');\n this.proc!.kill();\n }, this.#opts.closeTimeout);\n await this.#join.await.then(() => {\n clearTimeout(timer);\n this.clearTimers();\n });\n }\n\n async launchJob(info: RunningJobInfo) {\n if (this.#runningJob) {\n throw new Error('executor already has a running job');\n }\n if (!this.proc?.connected) {\n throw new Error('process not connected');\n }\n this.#runningJob = info;\n this.proc.send({ case: 'startJobRequest', value: { runningJob: info } });\n }\n\n private async getChildMemoryUsageMB(): Promise<number> {\n const pid = this.proc?.pid;\n if (!pid) {\n return 0;\n }\n try {\n const stats = await pidusage(pid);\n return stats.memory / (1024 * 1024);\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === 'ENOENT' || code === 'ESRCH') {\n return 0;\n }\n throw err;\n }\n }\n\n private clearTimers() {\n clearTimeout(this.#pongTimeout);\n clearInterval(this.#pingInterval);\n clearInterval(this.#memoryMonitorInterval);\n }\n}\n"],"mappings":"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","names":["pidusage"]}
@@ -19,6 +19,7 @@ export declare abstract class SupervisedProc {
19
19
  abstract createProcess(): ChildProcess;
20
20
  abstract mainTask(child: ChildProcess): Promise<void>;
21
21
  get started(): boolean;
22
+ get isAlive(): boolean;
22
23
  get runningJob(): RunningJobInfo | undefined;
23
24
  start(): Promise<void>;
24
25
  run(): Promise<void>;
@@ -19,6 +19,7 @@ export declare abstract class SupervisedProc {
19
19
  abstract createProcess(): ChildProcess;
20
20
  abstract mainTask(child: ChildProcess): Promise<void>;
21
21
  get started(): boolean;
22
+ get isAlive(): boolean;
22
23
  get runningJob(): RunningJobInfo | undefined;
23
24
  start(): Promise<void>;
24
25
  run(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"supervised_proc.d.ts","sourceRoot":"","sources":["../../src/ipc/supervised_proc.ts"],"names":[],"mappings":";AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,MAAM,WAAW,QAAQ;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,8BAAsB,cAAc;;IAKlC,IAAI,CAAC,EAAE,YAAY,CAAC;IAIpB,SAAS,CAAC,IAAI,eAAgB;gBAK5B,iBAAiB,EAAE,MAAM,EACzB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,EACnB,iBAAiB,EAAE,MAAM;IAa3B,QAAQ,CAAC,aAAa,IAAI,YAAY;IACtC,QAAQ,CAAC,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAErD,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,UAAU,IAAI,cAAc,GAAG,SAAS,CAE3C;IAEK,KAAK;IAaL,GAAG;IA4EH,IAAI;IAQJ,UAAU;IA0BV,KAAK;IAoBL,SAAS,CAAC,IAAI,EAAE,cAAc;YAWtB,qBAAqB;IAiBnC,OAAO,CAAC,WAAW;CAKpB"}
1
+ {"version":3,"file":"supervised_proc.d.ts","sourceRoot":"","sources":["../../src/ipc/supervised_proc.ts"],"names":[],"mappings":";AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,MAAM,WAAW,QAAQ;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,8BAAsB,cAAc;;IAKlC,IAAI,CAAC,EAAE,YAAY,CAAC;IAIpB,SAAS,CAAC,IAAI,eAAgB;gBAK5B,iBAAiB,EAAE,MAAM,EACzB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,EACnB,iBAAiB,EAAE,MAAM;IAa3B,QAAQ,CAAC,aAAa,IAAI,YAAY;IACtC,QAAQ,CAAC,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAErD,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,UAAU,IAAI,cAAc,GAAG,SAAS,CAE3C;IAEK,KAAK;IAaL,GAAG;IA4EH,IAAI;IAQJ,UAAU;IA0BV,KAAK;IAoBL,SAAS,CAAC,IAAI,EAAE,cAAc;YAWtB,qBAAqB;IAiBnC,OAAO,CAAC,WAAW;CAKpB"}
@@ -28,6 +28,10 @@ class SupervisedProc {
28
28
  get started() {
29
29
  return this.#started;
30
30
  }
31
+ get isAlive() {
32
+ var _a;
33
+ return this.#started && !this.#closing && !!((_a = this.proc) == null ? void 0 : _a.connected);
34
+ }
31
35
  get runningJob() {
32
36
  return this.#runningJob;
33
37
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/ipc/supervised_proc.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { ChildProcess } from 'node:child_process';\nimport { once } from 'node:events';\nimport pidusage from 'pidusage';\nimport type { RunningJobInfo } from '../job.js';\nimport { log, loggerOptions } from '../log.js';\nimport { Future } from '../utils.js';\nimport type { IPCMessage } from './message.js';\n\nexport interface ProcOpts {\n initializeTimeout: number;\n closeTimeout: number;\n memoryWarnMB: number;\n memoryLimitMB: number;\n pingInterval: number;\n pingTimeout: number;\n highPingThreshold: number;\n}\n\nexport abstract class SupervisedProc {\n #opts: ProcOpts;\n #started = false;\n #closing = false;\n #runningJob?: RunningJobInfo = undefined;\n proc?: ChildProcess;\n #pingInterval?: ReturnType<typeof setInterval>;\n #memoryMonitorInterval?: ReturnType<typeof setInterval>;\n #pongTimeout?: ReturnType<typeof setTimeout>;\n protected init = new Future();\n #join = new Future();\n #logger = log().child({ runningJob: this.#runningJob });\n\n constructor(\n initializeTimeout: number,\n closeTimeout: number,\n memoryWarnMB: number,\n memoryLimitMB: number,\n pingInterval: number,\n pingTimeout: number,\n highPingThreshold: number,\n ) {\n this.#opts = {\n initializeTimeout,\n closeTimeout,\n memoryWarnMB,\n memoryLimitMB,\n pingInterval,\n pingTimeout,\n highPingThreshold,\n };\n }\n\n abstract createProcess(): ChildProcess;\n abstract mainTask(child: ChildProcess): Promise<void>;\n\n get started(): boolean {\n return this.#started;\n }\n\n get runningJob(): RunningJobInfo | undefined {\n return this.#runningJob;\n }\n\n async start() {\n if (this.#started) {\n throw new Error('runner already started');\n } else if (this.#closing) {\n throw new Error('runner is closed');\n }\n\n this.proc = this.createProcess();\n\n this.#started = true;\n this.run();\n }\n\n async run() {\n await this.init.await;\n\n this.#pingInterval = setInterval(() => {\n if (this.proc?.connected) {\n this.proc.send({ case: 'pingRequest', value: { timestamp: Date.now() } });\n }\n }, this.#opts.pingInterval);\n\n this.#pongTimeout = setTimeout(() => {\n this.#logger.warn('job is unresponsive');\n clearTimeout(this.#pongTimeout);\n clearInterval(this.#pingInterval);\n this.proc!.kill();\n this.#join.resolve();\n }, this.#opts.pingTimeout);\n\n this.#memoryMonitorInterval = setInterval(async () => {\n const memoryMB = await this.getChildMemoryUsageMB();\n if (this.#opts.memoryLimitMB > 0 && memoryMB > this.#opts.memoryLimitMB) {\n this.#logger\n .child({ memoryUsageMB: memoryMB, memoryLimitMB: this.#opts.memoryLimitMB })\n .error('process exceeded memory limit, killing process');\n this.close();\n } else if (this.#opts.memoryWarnMB > 0 && memoryMB > this.#opts.memoryWarnMB) {\n this.#logger\n .child({\n memoryUsageMB: memoryMB,\n memoryWarnMB: this.#opts.memoryWarnMB,\n memoryLimitMB: this.#opts.memoryLimitMB,\n })\n .warn('process memory usage is high');\n }\n }, 5000);\n\n const listener = (msg: IPCMessage) => {\n switch (msg.case) {\n case 'pongResponse': {\n const delay = Date.now() - msg.value.timestamp;\n if (delay > this.#opts.highPingThreshold) {\n this.#logger.child({ delay }).warn('job executor is unresponsive');\n }\n this.#pongTimeout?.refresh();\n break;\n }\n case 'exiting': {\n this.#logger.child({ reason: msg.value.reason }).debug('job exiting');\n break;\n }\n case 'done': {\n this.#closing = true;\n this.proc!.off('message', listener);\n break;\n }\n }\n };\n this.proc!.on('message', listener);\n this.proc!.on('error', (err) => {\n if (this.#closing) return;\n this.#logger\n .child({ err })\n .warn('job process exited unexpectedly; this likely means the error above caused a crash');\n this.clearTimers();\n this.#join.resolve();\n });\n\n this.proc!.on('exit', () => {\n this.clearTimers();\n this.#join.resolve();\n });\n\n this.mainTask(this.proc!);\n\n await this.#join.await;\n }\n\n async join() {\n if (!this.#started) {\n throw new Error('runner not started');\n }\n\n await this.#join.await;\n }\n\n async initialize() {\n const timer = setTimeout(() => {\n this.init.reject(new Error('runner initialization timed out'));\n }, this.#opts.initializeTimeout);\n if (!this.proc?.connected) {\n this.init.reject(new Error('process not connected'));\n return;\n }\n this.proc.send({\n case: 'initializeRequest',\n value: {\n loggerOptions,\n pingInterval: this.#opts.pingInterval,\n pingTimeout: this.#opts.pingTimeout,\n highPingThreshold: this.#opts.highPingThreshold,\n },\n });\n await once(this.proc!, 'message').then(([msg]: IPCMessage[]) => {\n clearTimeout(timer);\n if (msg!.case !== 'initializeResponse') {\n throw new Error('first message must be InitializeResponse');\n }\n });\n this.init.resolve();\n }\n\n async close() {\n if (!this.#started) {\n return;\n }\n this.#closing = true;\n\n if (this.proc?.connected) {\n this.proc.send({ case: 'shutdownRequest' });\n }\n\n const timer = setTimeout(() => {\n this.#logger.error('job shutdown is taking too much time');\n this.proc!.kill();\n }, this.#opts.closeTimeout);\n await this.#join.await.then(() => {\n clearTimeout(timer);\n this.clearTimers();\n });\n }\n\n async launchJob(info: RunningJobInfo) {\n if (this.#runningJob) {\n throw new Error('executor already has a running job');\n }\n if (!this.proc?.connected) {\n throw new Error('process not connected');\n }\n this.#runningJob = info;\n this.proc.send({ case: 'startJobRequest', value: { runningJob: info } });\n }\n\n private async getChildMemoryUsageMB(): Promise<number> {\n const pid = this.proc?.pid;\n if (!pid) {\n return 0;\n }\n try {\n const stats = await pidusage(pid);\n return stats.memory / (1024 * 1024);\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === 'ENOENT' || code === 'ESRCH') {\n return 0;\n }\n throw err;\n }\n }\n\n private clearTimers() {\n clearTimeout(this.#pongTimeout);\n clearInterval(this.#pingInterval);\n clearInterval(this.#memoryMonitorInterval);\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/ipc/supervised_proc.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { ChildProcess } from 'node:child_process';\nimport { once } from 'node:events';\nimport pidusage from 'pidusage';\nimport type { RunningJobInfo } from '../job.js';\nimport { log, loggerOptions } from '../log.js';\nimport { Future } from '../utils.js';\nimport type { IPCMessage } from './message.js';\n\nexport interface ProcOpts {\n initializeTimeout: number;\n closeTimeout: number;\n memoryWarnMB: number;\n memoryLimitMB: number;\n pingInterval: number;\n pingTimeout: number;\n highPingThreshold: number;\n}\n\nexport abstract class SupervisedProc {\n #opts: ProcOpts;\n #started = false;\n #closing = false;\n #runningJob?: RunningJobInfo = undefined;\n proc?: ChildProcess;\n #pingInterval?: ReturnType<typeof setInterval>;\n #memoryMonitorInterval?: ReturnType<typeof setInterval>;\n #pongTimeout?: ReturnType<typeof setTimeout>;\n protected init = new Future();\n #join = new Future();\n #logger = log().child({ runningJob: this.#runningJob });\n\n constructor(\n initializeTimeout: number,\n closeTimeout: number,\n memoryWarnMB: number,\n memoryLimitMB: number,\n pingInterval: number,\n pingTimeout: number,\n highPingThreshold: number,\n ) {\n this.#opts = {\n initializeTimeout,\n closeTimeout,\n memoryWarnMB,\n memoryLimitMB,\n pingInterval,\n pingTimeout,\n highPingThreshold,\n };\n }\n\n abstract createProcess(): ChildProcess;\n abstract mainTask(child: ChildProcess): Promise<void>;\n\n get started(): boolean {\n return this.#started;\n }\n\n get isAlive(): boolean {\n return this.#started && !this.#closing && !!this.proc?.connected;\n }\n\n get runningJob(): RunningJobInfo | undefined {\n return this.#runningJob;\n }\n\n async start() {\n if (this.#started) {\n throw new Error('runner already started');\n } else if (this.#closing) {\n throw new Error('runner is closed');\n }\n\n this.proc = this.createProcess();\n\n this.#started = true;\n this.run();\n }\n\n async run() {\n await this.init.await;\n\n this.#pingInterval = setInterval(() => {\n if (this.proc?.connected) {\n this.proc.send({ case: 'pingRequest', value: { timestamp: Date.now() } });\n }\n }, this.#opts.pingInterval);\n\n this.#pongTimeout = setTimeout(() => {\n this.#logger.warn('job is unresponsive');\n clearTimeout(this.#pongTimeout);\n clearInterval(this.#pingInterval);\n this.proc!.kill();\n this.#join.resolve();\n }, this.#opts.pingTimeout);\n\n this.#memoryMonitorInterval = setInterval(async () => {\n const memoryMB = await this.getChildMemoryUsageMB();\n if (this.#opts.memoryLimitMB > 0 && memoryMB > this.#opts.memoryLimitMB) {\n this.#logger\n .child({ memoryUsageMB: memoryMB, memoryLimitMB: this.#opts.memoryLimitMB })\n .error('process exceeded memory limit, killing process');\n this.close();\n } else if (this.#opts.memoryWarnMB > 0 && memoryMB > this.#opts.memoryWarnMB) {\n this.#logger\n .child({\n memoryUsageMB: memoryMB,\n memoryWarnMB: this.#opts.memoryWarnMB,\n memoryLimitMB: this.#opts.memoryLimitMB,\n })\n .warn('process memory usage is high');\n }\n }, 5000);\n\n const listener = (msg: IPCMessage) => {\n switch (msg.case) {\n case 'pongResponse': {\n const delay = Date.now() - msg.value.timestamp;\n if (delay > this.#opts.highPingThreshold) {\n this.#logger.child({ delay }).warn('job executor is unresponsive');\n }\n this.#pongTimeout?.refresh();\n break;\n }\n case 'exiting': {\n this.#logger.child({ reason: msg.value.reason }).debug('job exiting');\n break;\n }\n case 'done': {\n this.#closing = true;\n this.proc!.off('message', listener);\n break;\n }\n }\n };\n this.proc!.on('message', listener);\n this.proc!.on('error', (err) => {\n if (this.#closing) return;\n this.#logger\n .child({ err })\n .warn('job process exited unexpectedly; this likely means the error above caused a crash');\n this.clearTimers();\n this.#join.resolve();\n });\n\n this.proc!.on('exit', () => {\n this.clearTimers();\n this.#join.resolve();\n });\n\n this.mainTask(this.proc!);\n\n await this.#join.await;\n }\n\n async join() {\n if (!this.#started) {\n throw new Error('runner not started');\n }\n\n await this.#join.await;\n }\n\n async initialize() {\n const timer = setTimeout(() => {\n this.init.reject(new Error('runner initialization timed out'));\n }, this.#opts.initializeTimeout);\n if (!this.proc?.connected) {\n this.init.reject(new Error('process not connected'));\n return;\n }\n this.proc.send({\n case: 'initializeRequest',\n value: {\n loggerOptions,\n pingInterval: this.#opts.pingInterval,\n pingTimeout: this.#opts.pingTimeout,\n highPingThreshold: this.#opts.highPingThreshold,\n },\n });\n await once(this.proc!, 'message').then(([msg]: IPCMessage[]) => {\n clearTimeout(timer);\n if (msg!.case !== 'initializeResponse') {\n throw new Error('first message must be InitializeResponse');\n }\n });\n this.init.resolve();\n }\n\n async close() {\n if (!this.#started) {\n return;\n }\n this.#closing = true;\n\n if (this.proc?.connected) {\n this.proc.send({ case: 'shutdownRequest' });\n }\n\n const timer = setTimeout(() => {\n this.#logger.error('job shutdown is taking too much time');\n this.proc!.kill();\n }, this.#opts.closeTimeout);\n await this.#join.await.then(() => {\n clearTimeout(timer);\n this.clearTimers();\n });\n }\n\n async launchJob(info: RunningJobInfo) {\n if (this.#runningJob) {\n throw new Error('executor already has a running job');\n }\n if (!this.proc?.connected) {\n throw new Error('process not connected');\n }\n this.#runningJob = info;\n this.proc.send({ case: 'startJobRequest', value: { runningJob: info } });\n }\n\n private async getChildMemoryUsageMB(): Promise<number> {\n const pid = this.proc?.pid;\n if (!pid) {\n return 0;\n }\n try {\n const stats = await pidusage(pid);\n return stats.memory / (1024 * 1024);\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === 'ENOENT' || code === 'ESRCH') {\n return 0;\n }\n throw err;\n }\n }\n\n private clearTimers() {\n clearTimeout(this.#pongTimeout);\n clearInterval(this.#pingInterval);\n clearInterval(this.#memoryMonitorInterval);\n }\n}\n"],"mappings":"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","names":[]}
@@ -23,6 +23,7 @@ __export(stream_adapter_exports, {
23
23
  });
24
24
  module.exports = __toCommonJS(stream_adapter_exports);
25
25
  var import_log = require("../log.cjs");
26
+ var import_utils = require("../utils.cjs");
26
27
  var import_vad = require("../vad.cjs");
27
28
  var import_stt = require("./stt.cjs");
28
29
  class StreamAdapter extends import_stt.STT {
@@ -74,7 +75,14 @@ class StreamAdapterWrapper extends import_stt.SpeechStream {
74
75
  this.#vadStream.pushFrame(input);
75
76
  }
76
77
  }
77
- this.#vadStream.endInput();
78
+ try {
79
+ this.#vadStream.endInput();
80
+ } catch (e) {
81
+ if ((0, import_utils.isStreamClosedError)(e)) {
82
+ return;
83
+ }
84
+ throw e;
85
+ }
78
86
  };
79
87
  const recognize = async () => {
80
88
  for await (const ev of this.#vadStream) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/stt/stream_adapter.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport { log } from '../log.js';\nimport type { APIConnectOptions } from '../types.js';\nimport type { VAD, VADStream } from '../vad.js';\nimport { VADEventType } from '../vad.js';\nimport type { SpeechEvent } from './stt.js';\nimport { STT, SpeechEventType, SpeechStream } from './stt.js';\n\nexport class StreamAdapter extends STT {\n #stt: STT;\n #vad: VAD;\n label: string;\n\n constructor(stt: STT, vad: VAD) {\n super({ streaming: true, interimResults: false });\n this.#stt = stt;\n this.#vad = vad;\n this.label = `stt.StreamAdapter<${this.#stt.label}>`;\n\n this.#stt.on('metrics_collected', (metrics) => {\n this.emit('metrics_collected', metrics);\n });\n\n this.#stt.on('error', (error) => {\n this.emit('error', error);\n });\n }\n\n _recognize(frame: AudioFrame, abortSignal?: AbortSignal): Promise<SpeechEvent> {\n return this.#stt.recognize(frame, abortSignal);\n }\n\n stream(options?: { connOptions?: APIConnectOptions }): StreamAdapterWrapper {\n return new StreamAdapterWrapper(this.#stt, this.#vad, options?.connOptions);\n }\n}\n\nexport class StreamAdapterWrapper extends SpeechStream {\n #stt: STT;\n #vadStream: VADStream;\n label: string;\n\n constructor(stt: STT, vad: VAD, connOptions?: APIConnectOptions) {\n super(stt, undefined, connOptions);\n this.#stt = stt;\n this.#vadStream = vad.stream();\n this.label = `stt.StreamAdapterWrapper<${this.#stt.label}>`;\n }\n\n close() {\n super.close();\n this.#vadStream.close();\n }\n\n async monitorMetrics() {\n return; // do nothing\n }\n\n protected async run() {\n const forwardInput = async () => {\n for await (const input of this.input) {\n if (input === SpeechStream.FLUSH_SENTINEL) {\n this.#vadStream.flush();\n } else {\n this.#vadStream.pushFrame(input);\n }\n }\n this.#vadStream.endInput();\n };\n\n const recognize = async () => {\n for await (const ev of this.#vadStream) {\n switch (ev.type) {\n case VADEventType.START_OF_SPEECH:\n this.output.put({ type: SpeechEventType.START_OF_SPEECH });\n break;\n case VADEventType.END_OF_SPEECH:\n this.output.put({ type: SpeechEventType.END_OF_SPEECH });\n\n try {\n const event = await this.#stt.recognize(ev.frames, this.abortSignal);\n if (!event.alternatives![0].text) {\n continue;\n }\n\n this.output.put(event);\n break;\n } catch (error) {\n let logger = log();\n if (error instanceof Error) {\n logger = logger.child({ error: error.message });\n } else {\n logger = logger.child({ error });\n }\n logger.error(`${this.label}: provider recognize task failed`);\n continue;\n }\n }\n }\n };\n\n await Promise.all([forwardInput(), recognize()]);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAIA,iBAAoB;AAGpB,iBAA6B;AAE7B,iBAAmD;AAE5C,MAAM,sBAAsB,eAAI;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YAAY,KAAU,KAAU;AAC9B,UAAM,EAAE,WAAW,MAAM,gBAAgB,MAAM,CAAC;AAChD,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,QAAQ,qBAAqB,KAAK,KAAK,KAAK;AAEjD,SAAK,KAAK,GAAG,qBAAqB,CAAC,YAAY;AAC7C,WAAK,KAAK,qBAAqB,OAAO;AAAA,IACxC,CAAC;AAED,SAAK,KAAK,GAAG,SAAS,CAAC,UAAU;AAC/B,WAAK,KAAK,SAAS,KAAK;AAAA,IAC1B,CAAC;AAAA,EACH;AAAA,EAEA,WAAW,OAAmB,aAAiD;AAC7E,WAAO,KAAK,KAAK,UAAU,OAAO,WAAW;AAAA,EAC/C;AAAA,EAEA,OAAO,SAAqE;AAC1E,WAAO,IAAI,qBAAqB,KAAK,MAAM,KAAK,MAAM,mCAAS,WAAW;AAAA,EAC5E;AACF;AAEO,MAAM,6BAA6B,wBAAa;AAAA,EACrD;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YAAY,KAAU,KAAU,aAAiC;AAC/D,UAAM,KAAK,QAAW,WAAW;AACjC,SAAK,OAAO;AACZ,SAAK,aAAa,IAAI,OAAO;AAC7B,SAAK,QAAQ,4BAA4B,KAAK,KAAK,KAAK;AAAA,EAC1D;AAAA,EAEA,QAAQ;AACN,UAAM,MAAM;AACZ,SAAK,WAAW,MAAM;AAAA,EACxB;AAAA,EAEA,MAAM,iBAAiB;AACrB;AAAA,EACF;AAAA,EAEA,MAAgB,MAAM;AACpB,UAAM,eAAe,YAAY;AAC/B,uBAAiB,SAAS,KAAK,OAAO;AACpC,YAAI,UAAU,wBAAa,gBAAgB;AACzC,eAAK,WAAW,MAAM;AAAA,QACxB,OAAO;AACL,eAAK,WAAW,UAAU,KAAK;AAAA,QACjC;AAAA,MACF;AACA,WAAK,WAAW,SAAS;AAAA,IAC3B;AAEA,UAAM,YAAY,YAAY;AAC5B,uBAAiB,MAAM,KAAK,YAAY;AACtC,gBAAQ,GAAG,MAAM;AAAA,UACf,KAAK,wBAAa;AAChB,iBAAK,OAAO,IAAI,EAAE,MAAM,2BAAgB,gBAAgB,CAAC;AACzD;AAAA,UACF,KAAK,wBAAa;AAChB,iBAAK,OAAO,IAAI,EAAE,MAAM,2BAAgB,cAAc,CAAC;AAEvD,gBAAI;AACF,oBAAM,QAAQ,MAAM,KAAK,KAAK,UAAU,GAAG,QAAQ,KAAK,WAAW;AACnE,kBAAI,CAAC,MAAM,aAAc,CAAC,EAAE,MAAM;AAChC;AAAA,cACF;AAEA,mBAAK,OAAO,IAAI,KAAK;AACrB;AAAA,YACF,SAAS,OAAO;AACd,kBAAI,aAAS,gBAAI;AACjB,kBAAI,iBAAiB,OAAO;AAC1B,yBAAS,OAAO,MAAM,EAAE,OAAO,MAAM,QAAQ,CAAC;AAAA,cAChD,OAAO;AACL,yBAAS,OAAO,MAAM,EAAE,MAAM,CAAC;AAAA,cACjC;AACA,qBAAO,MAAM,GAAG,KAAK,KAAK,kCAAkC;AAC5D;AAAA,YACF;AAAA,QACJ;AAAA,MACF;AAAA,IACF;AAEA,UAAM,QAAQ,IAAI,CAAC,aAAa,GAAG,UAAU,CAAC,CAAC;AAAA,EACjD;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/stt/stream_adapter.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport { log } from '../log.js';\nimport type { APIConnectOptions } from '../types.js';\nimport { isStreamClosedError } from '../utils.js';\nimport type { VAD, VADStream } from '../vad.js';\nimport { VADEventType } from '../vad.js';\nimport type { SpeechEvent } from './stt.js';\nimport { STT, SpeechEventType, SpeechStream } from './stt.js';\n\nexport class StreamAdapter extends STT {\n #stt: STT;\n #vad: VAD;\n label: string;\n\n constructor(stt: STT, vad: VAD) {\n super({ streaming: true, interimResults: false });\n this.#stt = stt;\n this.#vad = vad;\n this.label = `stt.StreamAdapter<${this.#stt.label}>`;\n\n this.#stt.on('metrics_collected', (metrics) => {\n this.emit('metrics_collected', metrics);\n });\n\n this.#stt.on('error', (error) => {\n this.emit('error', error);\n });\n }\n\n _recognize(frame: AudioFrame, abortSignal?: AbortSignal): Promise<SpeechEvent> {\n return this.#stt.recognize(frame, abortSignal);\n }\n\n stream(options?: { connOptions?: APIConnectOptions }): StreamAdapterWrapper {\n return new StreamAdapterWrapper(this.#stt, this.#vad, options?.connOptions);\n }\n}\n\nexport class StreamAdapterWrapper extends SpeechStream {\n #stt: STT;\n #vadStream: VADStream;\n label: string;\n\n constructor(stt: STT, vad: VAD, connOptions?: APIConnectOptions) {\n super(stt, undefined, connOptions);\n this.#stt = stt;\n this.#vadStream = vad.stream();\n this.label = `stt.StreamAdapterWrapper<${this.#stt.label}>`;\n }\n\n close() {\n super.close();\n this.#vadStream.close();\n }\n\n async monitorMetrics() {\n return; // do nothing\n }\n\n protected async run() {\n const forwardInput = async () => {\n for await (const input of this.input) {\n if (input === SpeechStream.FLUSH_SENTINEL) {\n this.#vadStream.flush();\n } else {\n this.#vadStream.pushFrame(input);\n }\n }\n\n // Guard against calling endInput() on already-closed stream\n // This happens during handover when close() is called while forwardInput is running\n try {\n this.#vadStream.endInput();\n } catch (e) {\n if (isStreamClosedError(e)) {\n return;\n }\n throw e;\n }\n };\n\n const recognize = async () => {\n for await (const ev of this.#vadStream) {\n switch (ev.type) {\n case VADEventType.START_OF_SPEECH:\n this.output.put({ type: SpeechEventType.START_OF_SPEECH });\n break;\n case VADEventType.END_OF_SPEECH:\n this.output.put({ type: SpeechEventType.END_OF_SPEECH });\n\n try {\n const event = await this.#stt.recognize(ev.frames, this.abortSignal);\n if (!event.alternatives![0].text) {\n continue;\n }\n\n this.output.put(event);\n break;\n } catch (error) {\n let logger = log();\n if (error instanceof Error) {\n logger = logger.child({ error: error.message });\n } else {\n logger = logger.child({ error });\n }\n logger.error(`${this.label}: provider recognize task failed`);\n continue;\n }\n }\n }\n };\n\n await Promise.all([forwardInput(), recognize()]);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAIA,iBAAoB;AAEpB,mBAAoC;AAEpC,iBAA6B;AAE7B,iBAAmD;AAE5C,MAAM,sBAAsB,eAAI;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YAAY,KAAU,KAAU;AAC9B,UAAM,EAAE,WAAW,MAAM,gBAAgB,MAAM,CAAC;AAChD,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,QAAQ,qBAAqB,KAAK,KAAK,KAAK;AAEjD,SAAK,KAAK,GAAG,qBAAqB,CAAC,YAAY;AAC7C,WAAK,KAAK,qBAAqB,OAAO;AAAA,IACxC,CAAC;AAED,SAAK,KAAK,GAAG,SAAS,CAAC,UAAU;AAC/B,WAAK,KAAK,SAAS,KAAK;AAAA,IAC1B,CAAC;AAAA,EACH;AAAA,EAEA,WAAW,OAAmB,aAAiD;AAC7E,WAAO,KAAK,KAAK,UAAU,OAAO,WAAW;AAAA,EAC/C;AAAA,EAEA,OAAO,SAAqE;AAC1E,WAAO,IAAI,qBAAqB,KAAK,MAAM,KAAK,MAAM,mCAAS,WAAW;AAAA,EAC5E;AACF;AAEO,MAAM,6BAA6B,wBAAa;AAAA,EACrD;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YAAY,KAAU,KAAU,aAAiC;AAC/D,UAAM,KAAK,QAAW,WAAW;AACjC,SAAK,OAAO;AACZ,SAAK,aAAa,IAAI,OAAO;AAC7B,SAAK,QAAQ,4BAA4B,KAAK,KAAK,KAAK;AAAA,EAC1D;AAAA,EAEA,QAAQ;AACN,UAAM,MAAM;AACZ,SAAK,WAAW,MAAM;AAAA,EACxB;AAAA,EAEA,MAAM,iBAAiB;AACrB;AAAA,EACF;AAAA,EAEA,MAAgB,MAAM;AACpB,UAAM,eAAe,YAAY;AAC/B,uBAAiB,SAAS,KAAK,OAAO;AACpC,YAAI,UAAU,wBAAa,gBAAgB;AACzC,eAAK,WAAW,MAAM;AAAA,QACxB,OAAO;AACL,eAAK,WAAW,UAAU,KAAK;AAAA,QACjC;AAAA,MACF;AAIA,UAAI;AACF,aAAK,WAAW,SAAS;AAAA,MAC3B,SAAS,GAAG;AACV,gBAAI,kCAAoB,CAAC,GAAG;AAC1B;AAAA,QACF;AACA,cAAM;AAAA,MACR;AAAA,IACF;AAEA,UAAM,YAAY,YAAY;AAC5B,uBAAiB,MAAM,KAAK,YAAY;AACtC,gBAAQ,GAAG,MAAM;AAAA,UACf,KAAK,wBAAa;AAChB,iBAAK,OAAO,IAAI,EAAE,MAAM,2BAAgB,gBAAgB,CAAC;AACzD;AAAA,UACF,KAAK,wBAAa;AAChB,iBAAK,OAAO,IAAI,EAAE,MAAM,2BAAgB,cAAc,CAAC;AAEvD,gBAAI;AACF,oBAAM,QAAQ,MAAM,KAAK,KAAK,UAAU,GAAG,QAAQ,KAAK,WAAW;AACnE,kBAAI,CAAC,MAAM,aAAc,CAAC,EAAE,MAAM;AAChC;AAAA,cACF;AAEA,mBAAK,OAAO,IAAI,KAAK;AACrB;AAAA,YACF,SAAS,OAAO;AACd,kBAAI,aAAS,gBAAI;AACjB,kBAAI,iBAAiB,OAAO;AAC1B,yBAAS,OAAO,MAAM,EAAE,OAAO,MAAM,QAAQ,CAAC;AAAA,cAChD,OAAO;AACL,yBAAS,OAAO,MAAM,EAAE,MAAM,CAAC;AAAA,cACjC;AACA,qBAAO,MAAM,GAAG,KAAK,KAAK,kCAAkC;AAC5D;AAAA,YACF;AAAA,QACJ;AAAA,MACF;AAAA,IACF;AAEA,UAAM,QAAQ,IAAI,CAAC,aAAa,GAAG,UAAU,CAAC,CAAC;AAAA,EACjD;AACF;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"stream_adapter.d.ts","sourceRoot":"","sources":["../../src/stt/stream_adapter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,KAAK,EAAE,GAAG,EAAa,MAAM,WAAW,CAAC;AAEhD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,GAAG,EAAmB,YAAY,EAAE,MAAM,UAAU,CAAC;AAE9D,qBAAa,aAAc,SAAQ,GAAG;;IAGpC,KAAK,EAAE,MAAM,CAAC;gBAEF,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG;IAe9B,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,WAAW,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAI9E,MAAM,CAAC,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,iBAAiB,CAAA;KAAE,GAAG,oBAAoB;CAG5E;AAED,qBAAa,oBAAqB,SAAQ,YAAY;;IAGpD,KAAK,EAAE,MAAM,CAAC;gBAEF,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,WAAW,CAAC,EAAE,iBAAiB;IAO/D,KAAK;IAKC,cAAc;cAIJ,GAAG;CA6CpB"}
1
+ {"version":3,"file":"stream_adapter.d.ts","sourceRoot":"","sources":["../../src/stt/stream_adapter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD,OAAO,KAAK,EAAE,GAAG,EAAa,MAAM,WAAW,CAAC;AAEhD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,GAAG,EAAmB,YAAY,EAAE,MAAM,UAAU,CAAC;AAE9D,qBAAa,aAAc,SAAQ,GAAG;;IAGpC,KAAK,EAAE,MAAM,CAAC;gBAEF,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG;IAe9B,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,WAAW,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAI9E,MAAM,CAAC,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,iBAAiB,CAAA;KAAE,GAAG,oBAAoB;CAG5E;AAED,qBAAa,oBAAqB,SAAQ,YAAY;;IAGpD,KAAK,EAAE,MAAM,CAAC;gBAEF,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,WAAW,CAAC,EAAE,iBAAiB;IAO/D,KAAK;IAKC,cAAc;cAIJ,GAAG;CAuDpB"}
@@ -1,4 +1,5 @@
1
1
  import { log } from "../log.js";
2
+ import { isStreamClosedError } from "../utils.js";
2
3
  import { VADEventType } from "../vad.js";
3
4
  import { STT, SpeechEventType, SpeechStream } from "./stt.js";
4
5
  class StreamAdapter extends STT {
@@ -50,7 +51,14 @@ class StreamAdapterWrapper extends SpeechStream {
50
51
  this.#vadStream.pushFrame(input);
51
52
  }
52
53
  }
53
- this.#vadStream.endInput();
54
+ try {
55
+ this.#vadStream.endInput();
56
+ } catch (e) {
57
+ if (isStreamClosedError(e)) {
58
+ return;
59
+ }
60
+ throw e;
61
+ }
54
62
  };
55
63
  const recognize = async () => {
56
64
  for await (const ev of this.#vadStream) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/stt/stream_adapter.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport { log } from '../log.js';\nimport type { APIConnectOptions } from '../types.js';\nimport type { VAD, VADStream } from '../vad.js';\nimport { VADEventType } from '../vad.js';\nimport type { SpeechEvent } from './stt.js';\nimport { STT, SpeechEventType, SpeechStream } from './stt.js';\n\nexport class StreamAdapter extends STT {\n #stt: STT;\n #vad: VAD;\n label: string;\n\n constructor(stt: STT, vad: VAD) {\n super({ streaming: true, interimResults: false });\n this.#stt = stt;\n this.#vad = vad;\n this.label = `stt.StreamAdapter<${this.#stt.label}>`;\n\n this.#stt.on('metrics_collected', (metrics) => {\n this.emit('metrics_collected', metrics);\n });\n\n this.#stt.on('error', (error) => {\n this.emit('error', error);\n });\n }\n\n _recognize(frame: AudioFrame, abortSignal?: AbortSignal): Promise<SpeechEvent> {\n return this.#stt.recognize(frame, abortSignal);\n }\n\n stream(options?: { connOptions?: APIConnectOptions }): StreamAdapterWrapper {\n return new StreamAdapterWrapper(this.#stt, this.#vad, options?.connOptions);\n }\n}\n\nexport class StreamAdapterWrapper extends SpeechStream {\n #stt: STT;\n #vadStream: VADStream;\n label: string;\n\n constructor(stt: STT, vad: VAD, connOptions?: APIConnectOptions) {\n super(stt, undefined, connOptions);\n this.#stt = stt;\n this.#vadStream = vad.stream();\n this.label = `stt.StreamAdapterWrapper<${this.#stt.label}>`;\n }\n\n close() {\n super.close();\n this.#vadStream.close();\n }\n\n async monitorMetrics() {\n return; // do nothing\n }\n\n protected async run() {\n const forwardInput = async () => {\n for await (const input of this.input) {\n if (input === SpeechStream.FLUSH_SENTINEL) {\n this.#vadStream.flush();\n } else {\n this.#vadStream.pushFrame(input);\n }\n }\n this.#vadStream.endInput();\n };\n\n const recognize = async () => {\n for await (const ev of this.#vadStream) {\n switch (ev.type) {\n case VADEventType.START_OF_SPEECH:\n this.output.put({ type: SpeechEventType.START_OF_SPEECH });\n break;\n case VADEventType.END_OF_SPEECH:\n this.output.put({ type: SpeechEventType.END_OF_SPEECH });\n\n try {\n const event = await this.#stt.recognize(ev.frames, this.abortSignal);\n if (!event.alternatives![0].text) {\n continue;\n }\n\n this.output.put(event);\n break;\n } catch (error) {\n let logger = log();\n if (error instanceof Error) {\n logger = logger.child({ error: error.message });\n } else {\n logger = logger.child({ error });\n }\n logger.error(`${this.label}: provider recognize task failed`);\n continue;\n }\n }\n }\n };\n\n await Promise.all([forwardInput(), recognize()]);\n }\n}\n"],"mappings":"AAIA,SAAS,WAAW;AAGpB,SAAS,oBAAoB;AAE7B,SAAS,KAAK,iBAAiB,oBAAoB;AAE5C,MAAM,sBAAsB,IAAI;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YAAY,KAAU,KAAU;AAC9B,UAAM,EAAE,WAAW,MAAM,gBAAgB,MAAM,CAAC;AAChD,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,QAAQ,qBAAqB,KAAK,KAAK,KAAK;AAEjD,SAAK,KAAK,GAAG,qBAAqB,CAAC,YAAY;AAC7C,WAAK,KAAK,qBAAqB,OAAO;AAAA,IACxC,CAAC;AAED,SAAK,KAAK,GAAG,SAAS,CAAC,UAAU;AAC/B,WAAK,KAAK,SAAS,KAAK;AAAA,IAC1B,CAAC;AAAA,EACH;AAAA,EAEA,WAAW,OAAmB,aAAiD;AAC7E,WAAO,KAAK,KAAK,UAAU,OAAO,WAAW;AAAA,EAC/C;AAAA,EAEA,OAAO,SAAqE;AAC1E,WAAO,IAAI,qBAAqB,KAAK,MAAM,KAAK,MAAM,mCAAS,WAAW;AAAA,EAC5E;AACF;AAEO,MAAM,6BAA6B,aAAa;AAAA,EACrD;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YAAY,KAAU,KAAU,aAAiC;AAC/D,UAAM,KAAK,QAAW,WAAW;AACjC,SAAK,OAAO;AACZ,SAAK,aAAa,IAAI,OAAO;AAC7B,SAAK,QAAQ,4BAA4B,KAAK,KAAK,KAAK;AAAA,EAC1D;AAAA,EAEA,QAAQ;AACN,UAAM,MAAM;AACZ,SAAK,WAAW,MAAM;AAAA,EACxB;AAAA,EAEA,MAAM,iBAAiB;AACrB;AAAA,EACF;AAAA,EAEA,MAAgB,MAAM;AACpB,UAAM,eAAe,YAAY;AAC/B,uBAAiB,SAAS,KAAK,OAAO;AACpC,YAAI,UAAU,aAAa,gBAAgB;AACzC,eAAK,WAAW,MAAM;AAAA,QACxB,OAAO;AACL,eAAK,WAAW,UAAU,KAAK;AAAA,QACjC;AAAA,MACF;AACA,WAAK,WAAW,SAAS;AAAA,IAC3B;AAEA,UAAM,YAAY,YAAY;AAC5B,uBAAiB,MAAM,KAAK,YAAY;AACtC,gBAAQ,GAAG,MAAM;AAAA,UACf,KAAK,aAAa;AAChB,iBAAK,OAAO,IAAI,EAAE,MAAM,gBAAgB,gBAAgB,CAAC;AACzD;AAAA,UACF,KAAK,aAAa;AAChB,iBAAK,OAAO,IAAI,EAAE,MAAM,gBAAgB,cAAc,CAAC;AAEvD,gBAAI;AACF,oBAAM,QAAQ,MAAM,KAAK,KAAK,UAAU,GAAG,QAAQ,KAAK,WAAW;AACnE,kBAAI,CAAC,MAAM,aAAc,CAAC,EAAE,MAAM;AAChC;AAAA,cACF;AAEA,mBAAK,OAAO,IAAI,KAAK;AACrB;AAAA,YACF,SAAS,OAAO;AACd,kBAAI,SAAS,IAAI;AACjB,kBAAI,iBAAiB,OAAO;AAC1B,yBAAS,OAAO,MAAM,EAAE,OAAO,MAAM,QAAQ,CAAC;AAAA,cAChD,OAAO;AACL,yBAAS,OAAO,MAAM,EAAE,MAAM,CAAC;AAAA,cACjC;AACA,qBAAO,MAAM,GAAG,KAAK,KAAK,kCAAkC;AAC5D;AAAA,YACF;AAAA,QACJ;AAAA,MACF;AAAA,IACF;AAEA,UAAM,QAAQ,IAAI,CAAC,aAAa,GAAG,UAAU,CAAC,CAAC;AAAA,EACjD;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/stt/stream_adapter.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport { log } from '../log.js';\nimport type { APIConnectOptions } from '../types.js';\nimport { isStreamClosedError } from '../utils.js';\nimport type { VAD, VADStream } from '../vad.js';\nimport { VADEventType } from '../vad.js';\nimport type { SpeechEvent } from './stt.js';\nimport { STT, SpeechEventType, SpeechStream } from './stt.js';\n\nexport class StreamAdapter extends STT {\n #stt: STT;\n #vad: VAD;\n label: string;\n\n constructor(stt: STT, vad: VAD) {\n super({ streaming: true, interimResults: false });\n this.#stt = stt;\n this.#vad = vad;\n this.label = `stt.StreamAdapter<${this.#stt.label}>`;\n\n this.#stt.on('metrics_collected', (metrics) => {\n this.emit('metrics_collected', metrics);\n });\n\n this.#stt.on('error', (error) => {\n this.emit('error', error);\n });\n }\n\n _recognize(frame: AudioFrame, abortSignal?: AbortSignal): Promise<SpeechEvent> {\n return this.#stt.recognize(frame, abortSignal);\n }\n\n stream(options?: { connOptions?: APIConnectOptions }): StreamAdapterWrapper {\n return new StreamAdapterWrapper(this.#stt, this.#vad, options?.connOptions);\n }\n}\n\nexport class StreamAdapterWrapper extends SpeechStream {\n #stt: STT;\n #vadStream: VADStream;\n label: string;\n\n constructor(stt: STT, vad: VAD, connOptions?: APIConnectOptions) {\n super(stt, undefined, connOptions);\n this.#stt = stt;\n this.#vadStream = vad.stream();\n this.label = `stt.StreamAdapterWrapper<${this.#stt.label}>`;\n }\n\n close() {\n super.close();\n this.#vadStream.close();\n }\n\n async monitorMetrics() {\n return; // do nothing\n }\n\n protected async run() {\n const forwardInput = async () => {\n for await (const input of this.input) {\n if (input === SpeechStream.FLUSH_SENTINEL) {\n this.#vadStream.flush();\n } else {\n this.#vadStream.pushFrame(input);\n }\n }\n\n // Guard against calling endInput() on already-closed stream\n // This happens during handover when close() is called while forwardInput is running\n try {\n this.#vadStream.endInput();\n } catch (e) {\n if (isStreamClosedError(e)) {\n return;\n }\n throw e;\n }\n };\n\n const recognize = async () => {\n for await (const ev of this.#vadStream) {\n switch (ev.type) {\n case VADEventType.START_OF_SPEECH:\n this.output.put({ type: SpeechEventType.START_OF_SPEECH });\n break;\n case VADEventType.END_OF_SPEECH:\n this.output.put({ type: SpeechEventType.END_OF_SPEECH });\n\n try {\n const event = await this.#stt.recognize(ev.frames, this.abortSignal);\n if (!event.alternatives![0].text) {\n continue;\n }\n\n this.output.put(event);\n break;\n } catch (error) {\n let logger = log();\n if (error instanceof Error) {\n logger = logger.child({ error: error.message });\n } else {\n logger = logger.child({ error });\n }\n logger.error(`${this.label}: provider recognize task failed`);\n continue;\n }\n }\n }\n };\n\n await Promise.all([forwardInput(), recognize()]);\n }\n}\n"],"mappings":"AAIA,SAAS,WAAW;AAEpB,SAAS,2BAA2B;AAEpC,SAAS,oBAAoB;AAE7B,SAAS,KAAK,iBAAiB,oBAAoB;AAE5C,MAAM,sBAAsB,IAAI;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YAAY,KAAU,KAAU;AAC9B,UAAM,EAAE,WAAW,MAAM,gBAAgB,MAAM,CAAC;AAChD,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,QAAQ,qBAAqB,KAAK,KAAK,KAAK;AAEjD,SAAK,KAAK,GAAG,qBAAqB,CAAC,YAAY;AAC7C,WAAK,KAAK,qBAAqB,OAAO;AAAA,IACxC,CAAC;AAED,SAAK,KAAK,GAAG,SAAS,CAAC,UAAU;AAC/B,WAAK,KAAK,SAAS,KAAK;AAAA,IAC1B,CAAC;AAAA,EACH;AAAA,EAEA,WAAW,OAAmB,aAAiD;AAC7E,WAAO,KAAK,KAAK,UAAU,OAAO,WAAW;AAAA,EAC/C;AAAA,EAEA,OAAO,SAAqE;AAC1E,WAAO,IAAI,qBAAqB,KAAK,MAAM,KAAK,MAAM,mCAAS,WAAW;AAAA,EAC5E;AACF;AAEO,MAAM,6BAA6B,aAAa;AAAA,EACrD;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YAAY,KAAU,KAAU,aAAiC;AAC/D,UAAM,KAAK,QAAW,WAAW;AACjC,SAAK,OAAO;AACZ,SAAK,aAAa,IAAI,OAAO;AAC7B,SAAK,QAAQ,4BAA4B,KAAK,KAAK,KAAK;AAAA,EAC1D;AAAA,EAEA,QAAQ;AACN,UAAM,MAAM;AACZ,SAAK,WAAW,MAAM;AAAA,EACxB;AAAA,EAEA,MAAM,iBAAiB;AACrB;AAAA,EACF;AAAA,EAEA,MAAgB,MAAM;AACpB,UAAM,eAAe,YAAY;AAC/B,uBAAiB,SAAS,KAAK,OAAO;AACpC,YAAI,UAAU,aAAa,gBAAgB;AACzC,eAAK,WAAW,MAAM;AAAA,QACxB,OAAO;AACL,eAAK,WAAW,UAAU,KAAK;AAAA,QACjC;AAAA,MACF;AAIA,UAAI;AACF,aAAK,WAAW,SAAS;AAAA,MAC3B,SAAS,GAAG;AACV,YAAI,oBAAoB,CAAC,GAAG;AAC1B;AAAA,QACF;AACA,cAAM;AAAA,MACR;AAAA,IACF;AAEA,UAAM,YAAY,YAAY;AAC5B,uBAAiB,MAAM,KAAK,YAAY;AACtC,gBAAQ,GAAG,MAAM;AAAA,UACf,KAAK,aAAa;AAChB,iBAAK,OAAO,IAAI,EAAE,MAAM,gBAAgB,gBAAgB,CAAC;AACzD;AAAA,UACF,KAAK,aAAa;AAChB,iBAAK,OAAO,IAAI,EAAE,MAAM,gBAAgB,cAAc,CAAC;AAEvD,gBAAI;AACF,oBAAM,QAAQ,MAAM,KAAK,KAAK,UAAU,GAAG,QAAQ,KAAK,WAAW;AACnE,kBAAI,CAAC,MAAM,aAAc,CAAC,EAAE,MAAM;AAChC;AAAA,cACF;AAEA,mBAAK,OAAO,IAAI,KAAK;AACrB;AAAA,YACF,SAAS,OAAO;AACd,kBAAI,SAAS,IAAI;AACjB,kBAAI,iBAAiB,OAAO;AAC1B,yBAAS,OAAO,MAAM,EAAE,OAAO,MAAM,QAAQ,CAAC;AAAA,cAChD,OAAO;AACL,yBAAS,OAAO,MAAM,EAAE,MAAM,CAAC;AAAA,cACjC;AACA,qBAAO,MAAM,GAAG,KAAK,KAAK,kCAAkC;AAC5D;AAAA,YACF;AAAA,QACJ;AAAA,MACF;AAAA,IACF;AAEA,UAAM,QAAQ,IAAI,CAAC,aAAa,GAAG,UAAU,CAAC,CAAC;AAAA,EACjD;AACF;","names":[]}
@@ -27,7 +27,7 @@ const splitSentences = (text, minLength = 20, retainFormat = false) => {
27
27
  const suffixes = /(Inc|Ltd|Jr|Sr|Co)/g;
28
28
  const starters = /(Mr|Mrs|Ms|Dr|Prof|Capt|Cpt|Lt|He\s|She\s|It\s|They\s|Their\s|Our\s|We\s|But\s|However\s|That\s|This\s|Wherever)/g;
29
29
  const acronyms = /([A-Z][.][A-Z][.](?:[A-Z][.])?)/g;
30
- const websites = /[.](com|net|org|io|gov|edu|me)/g;
30
+ const websites = /(\w+\.)+(com|net|org|io|gov|edu|me)/g;
31
31
  const digits = /([0-9])/g;
32
32
  const dots = /\.{2,}/g;
33
33
  if (retainFormat) {
@@ -36,7 +36,7 @@ const splitSentences = (text, minLength = 20, retainFormat = false) => {
36
36
  text = text.replaceAll("\n", " ");
37
37
  }
38
38
  text = text.replaceAll(prefixes, "$1<prd>");
39
- text = text.replaceAll(websites, "<prd>$2");
39
+ text = text.replace(websites, (match) => match.replaceAll(".", "<prd>"));
40
40
  text = text.replaceAll(new RegExp(`${digits.source}[.]${digits.source}`, "g"), "$1<prd>$2");
41
41
  text = text.replaceAll(dots, (match) => "<prd>".repeat(match.length));
42
42
  text = text.replaceAll("Ph.D.", "Ph<prd>D<prd>");
@@ -60,7 +60,7 @@ const splitSentences = (text, minLength = 20, retainFormat = false) => {
60
60
  text = text.replaceAll('."', '".');
61
61
  text = text.replaceAll('!"', '"!');
62
62
  text = text.replaceAll('?"', '"?');
63
- text = text.replaceAll(".", ".<stop>");
63
+ text = text.replace(/\.(?=\s|$)/g, ".<stop>");
64
64
  text = text.replaceAll("?", "?<stop>");
65
65
  text = text.replaceAll("!", "!<stop>");
66
66
  text = text.replaceAll("<prd>", ".");
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/tokenize/basic/sentence.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Split the text into sentences.\n */\nexport const splitSentences = (\n text: string,\n minLength = 20,\n retainFormat: boolean = false,\n): [string, number, number][] => {\n const alphabets = /([A-Za-z])/g;\n const prefixes = /(Mr|St|Mrs|Ms|Dr)[.]/g;\n const suffixes = /(Inc|Ltd|Jr|Sr|Co)/g;\n const starters =\n /(Mr|Mrs|Ms|Dr|Prof|Capt|Cpt|Lt|He\\s|She\\s|It\\s|They\\s|Their\\s|Our\\s|We\\s|But\\s|However\\s|That\\s|This\\s|Wherever)/g;\n const acronyms = /([A-Z][.][A-Z][.](?:[A-Z][.])?)/g;\n const websites = /[.](com|net|org|io|gov|edu|me)/g;\n const digits = /([0-9])/g;\n const dots = /\\.{2,}/g;\n\n if (retainFormat) {\n text = text.replaceAll('\\n', '<nel><stop>');\n } else {\n text = text.replaceAll('\\n', ' ');\n }\n\n text = text.replaceAll(prefixes, '$1<prd>');\n text = text.replaceAll(websites, '<prd>$2');\n text = text.replaceAll(new RegExp(`${digits.source}[.]${digits.source}`, 'g'), '$1<prd>$2');\n text = text.replaceAll(dots, (match) => '<prd>'.repeat(match.length));\n text = text.replaceAll('Ph.D.', 'Ph<prd>D<prd>');\n text = text.replaceAll(new RegExp(`\\\\s${alphabets.source}[.] `, 'g'), ' $1<prd> ');\n text = text.replaceAll(new RegExp(`${acronyms.source} ${starters.source}`, 'g'), '$1<stop> $2');\n text = text.replaceAll(\n new RegExp(`${alphabets.source}[.]${alphabets.source}[.]${alphabets.source}[.]`, 'g'),\n '$1<prd>$2<prd>$3<prd>',\n );\n text = text.replaceAll(\n new RegExp(`${alphabets.source}[.]${alphabets.source}[.]`, 'g'),\n '$1<prd>$2<prd>',\n );\n text = text.replaceAll(\n new RegExp(` ${suffixes.source}[.] ${starters.source}`, 'g'),\n '$1<stop> $2',\n );\n text = text.replaceAll(new RegExp(` ${suffixes.source}[.]`, 'g'), '$1<prd>');\n text = text.replaceAll(new RegExp(` ${alphabets.source}[.]`, 'g'), '$1<prd>');\n text = text.replaceAll('.”', '”.');\n text = text.replaceAll('.\"', '\".');\n text = text.replaceAll('!\"', '\"!');\n text = text.replaceAll('?\"', '\"?');\n text = text.replaceAll('.', '.<stop>');\n text = text.replaceAll('?', '?<stop>');\n text = text.replaceAll('!', '!<stop>');\n text = text.replaceAll('<prd>', '.');\n\n if (retainFormat) {\n text = text.replaceAll('<nel>', '\\n');\n }\n\n const split = text.split('<stop>');\n text = text.replaceAll('<stop>', '');\n\n const sentences: [string, number, number][] = [];\n let buf = '';\n let start = 0;\n let end = 0;\n const prePad = retainFormat ? '' : ' ';\n for (const match of split) {\n const sentence = retainFormat ? match : match.trim();\n if (!sentence) continue;\n\n buf += prePad + sentence;\n end += match.length;\n if (buf.length > minLength) {\n sentences.push([buf.slice(prePad.length), start, end]);\n start = end;\n buf = '';\n }\n }\n\n if (buf) {\n sentences.push([buf.slice(prePad.length), start, text.length - 1]);\n }\n\n return sentences;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAOO,MAAM,iBAAiB,CAC5B,MACA,YAAY,IACZ,eAAwB,UACO;AAC/B,QAAM,YAAY;AAClB,QAAM,WAAW;AACjB,QAAM,WAAW;AACjB,QAAM,WACJ;AACF,QAAM,WAAW;AACjB,QAAM,WAAW;AACjB,QAAM,SAAS;AACf,QAAM,OAAO;AAEb,MAAI,cAAc;AAChB,WAAO,KAAK,WAAW,MAAM,aAAa;AAAA,EAC5C,OAAO;AACL,WAAO,KAAK,WAAW,MAAM,GAAG;AAAA,EAClC;AAEA,SAAO,KAAK,WAAW,UAAU,SAAS;AAC1C,SAAO,KAAK,WAAW,UAAU,SAAS;AAC1C,SAAO,KAAK,WAAW,IAAI,OAAO,GAAG,OAAO,MAAM,MAAM,OAAO,MAAM,IAAI,GAAG,GAAG,WAAW;AAC1F,SAAO,KAAK,WAAW,MAAM,CAAC,UAAU,QAAQ,OAAO,MAAM,MAAM,CAAC;AACpE,SAAO,KAAK,WAAW,SAAS,eAAe;AAC/C,SAAO,KAAK,WAAW,IAAI,OAAO,MAAM,UAAU,MAAM,QAAQ,GAAG,GAAG,WAAW;AACjF,SAAO,KAAK,WAAW,IAAI,OAAO,GAAG,SAAS,MAAM,IAAI,SAAS,MAAM,IAAI,GAAG,GAAG,aAAa;AAC9F,SAAO,KAAK;AAAA,IACV,IAAI,OAAO,GAAG,UAAU,MAAM,MAAM,UAAU,MAAM,MAAM,UAAU,MAAM,OAAO,GAAG;AAAA,IACpF;AAAA,EACF;AACA,SAAO,KAAK;AAAA,IACV,IAAI,OAAO,GAAG,UAAU,MAAM,MAAM,UAAU,MAAM,OAAO,GAAG;AAAA,IAC9D;AAAA,EACF;AACA,SAAO,KAAK;AAAA,IACV,IAAI,OAAO,IAAI,SAAS,MAAM,OAAO,SAAS,MAAM,IAAI,GAAG;AAAA,IAC3D;AAAA,EACF;AACA,SAAO,KAAK,WAAW,IAAI,OAAO,IAAI,SAAS,MAAM,OAAO,GAAG,GAAG,SAAS;AAC3E,SAAO,KAAK,WAAW,IAAI,OAAO,IAAI,UAAU,MAAM,OAAO,GAAG,GAAG,SAAS;AAC5E,SAAO,KAAK,WAAW,WAAM,SAAI;AACjC,SAAO,KAAK,WAAW,MAAM,IAAI;AACjC,SAAO,KAAK,WAAW,MAAM,IAAI;AACjC,SAAO,KAAK,WAAW,MAAM,IAAI;AACjC,SAAO,KAAK,WAAW,KAAK,SAAS;AACrC,SAAO,KAAK,WAAW,KAAK,SAAS;AACrC,SAAO,KAAK,WAAW,KAAK,SAAS;AACrC,SAAO,KAAK,WAAW,SAAS,GAAG;AAEnC,MAAI,cAAc;AAChB,WAAO,KAAK,WAAW,SAAS,IAAI;AAAA,EACtC;AAEA,QAAM,QAAQ,KAAK,MAAM,QAAQ;AACjC,SAAO,KAAK,WAAW,UAAU,EAAE;AAEnC,QAAM,YAAwC,CAAC;AAC/C,MAAI,MAAM;AACV,MAAI,QAAQ;AACZ,MAAI,MAAM;AACV,QAAM,SAAS,eAAe,KAAK;AACnC,aAAW,SAAS,OAAO;AACzB,UAAM,WAAW,eAAe,QAAQ,MAAM,KAAK;AACnD,QAAI,CAAC,SAAU;AAEf,WAAO,SAAS;AAChB,WAAO,MAAM;AACb,QAAI,IAAI,SAAS,WAAW;AAC1B,gBAAU,KAAK,CAAC,IAAI,MAAM,OAAO,MAAM,GAAG,OAAO,GAAG,CAAC;AACrD,cAAQ;AACR,YAAM;AAAA,IACR;AAAA,EACF;AAEA,MAAI,KAAK;AACP,cAAU,KAAK,CAAC,IAAI,MAAM,OAAO,MAAM,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC;AAAA,EACnE;AAEA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../../src/tokenize/basic/sentence.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Split the text into sentences.\n */\nexport const splitSentences = (\n text: string,\n minLength = 20,\n retainFormat: boolean = false,\n): [string, number, number][] => {\n const alphabets = /([A-Za-z])/g;\n const prefixes = /(Mr|St|Mrs|Ms|Dr)[.]/g;\n const suffixes = /(Inc|Ltd|Jr|Sr|Co)/g;\n const starters =\n /(Mr|Mrs|Ms|Dr|Prof|Capt|Cpt|Lt|He\\s|She\\s|It\\s|They\\s|Their\\s|Our\\s|We\\s|But\\s|However\\s|That\\s|This\\s|Wherever)/g;\n const acronyms = /([A-Z][.][A-Z][.](?:[A-Z][.])?)/g;\n const websites = /(\\w+\\.)+(com|net|org|io|gov|edu|me)/g;\n const digits = /([0-9])/g;\n const dots = /\\.{2,}/g;\n\n if (retainFormat) {\n text = text.replaceAll('\\n', '<nel><stop>');\n } else {\n text = text.replaceAll('\\n', ' ');\n }\n\n text = text.replaceAll(prefixes, '$1<prd>');\n text = text.replace(websites, (match) => match.replaceAll('.', '<prd>'));\n text = text.replaceAll(new RegExp(`${digits.source}[.]${digits.source}`, 'g'), '$1<prd>$2');\n text = text.replaceAll(dots, (match) => '<prd>'.repeat(match.length));\n text = text.replaceAll('Ph.D.', 'Ph<prd>D<prd>');\n text = text.replaceAll(new RegExp(`\\\\s${alphabets.source}[.] `, 'g'), ' $1<prd> ');\n text = text.replaceAll(new RegExp(`${acronyms.source} ${starters.source}`, 'g'), '$1<stop> $2');\n text = text.replaceAll(\n new RegExp(`${alphabets.source}[.]${alphabets.source}[.]${alphabets.source}[.]`, 'g'),\n '$1<prd>$2<prd>$3<prd>',\n );\n text = text.replaceAll(\n new RegExp(`${alphabets.source}[.]${alphabets.source}[.]`, 'g'),\n '$1<prd>$2<prd>',\n );\n text = text.replaceAll(\n new RegExp(` ${suffixes.source}[.] ${starters.source}`, 'g'),\n '$1<stop> $2',\n );\n text = text.replaceAll(new RegExp(` ${suffixes.source}[.]`, 'g'), '$1<prd>');\n text = text.replaceAll(new RegExp(` ${alphabets.source}[.]`, 'g'), '$1<prd>');\n text = text.replaceAll('.”', '”.');\n text = text.replaceAll('.\"', '\".');\n text = text.replaceAll('!\"', '\"!');\n text = text.replaceAll('?\"', '\"?');\n text = text.replace(/\\.(?=\\s|$)/g, '.<stop>');\n text = text.replaceAll('?', '?<stop>');\n text = text.replaceAll('!', '!<stop>');\n text = text.replaceAll('<prd>', '.');\n\n if (retainFormat) {\n text = text.replaceAll('<nel>', '\\n');\n }\n\n const split = text.split('<stop>');\n text = text.replaceAll('<stop>', '');\n\n const sentences: [string, number, number][] = [];\n let buf = '';\n let start = 0;\n let end = 0;\n const prePad = retainFormat ? '' : ' ';\n for (const match of split) {\n const sentence = retainFormat ? match : match.trim();\n if (!sentence) continue;\n\n buf += prePad + sentence;\n end += match.length;\n if (buf.length > minLength) {\n sentences.push([buf.slice(prePad.length), start, end]);\n start = end;\n buf = '';\n }\n }\n\n if (buf) {\n sentences.push([buf.slice(prePad.length), start, text.length - 1]);\n }\n\n return sentences;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAOO,MAAM,iBAAiB,CAC5B,MACA,YAAY,IACZ,eAAwB,UACO;AAC/B,QAAM,YAAY;AAClB,QAAM,WAAW;AACjB,QAAM,WAAW;AACjB,QAAM,WACJ;AACF,QAAM,WAAW;AACjB,QAAM,WAAW;AACjB,QAAM,SAAS;AACf,QAAM,OAAO;AAEb,MAAI,cAAc;AAChB,WAAO,KAAK,WAAW,MAAM,aAAa;AAAA,EAC5C,OAAO;AACL,WAAO,KAAK,WAAW,MAAM,GAAG;AAAA,EAClC;AAEA,SAAO,KAAK,WAAW,UAAU,SAAS;AAC1C,SAAO,KAAK,QAAQ,UAAU,CAAC,UAAU,MAAM,WAAW,KAAK,OAAO,CAAC;AACvE,SAAO,KAAK,WAAW,IAAI,OAAO,GAAG,OAAO,MAAM,MAAM,OAAO,MAAM,IAAI,GAAG,GAAG,WAAW;AAC1F,SAAO,KAAK,WAAW,MAAM,CAAC,UAAU,QAAQ,OAAO,MAAM,MAAM,CAAC;AACpE,SAAO,KAAK,WAAW,SAAS,eAAe;AAC/C,SAAO,KAAK,WAAW,IAAI,OAAO,MAAM,UAAU,MAAM,QAAQ,GAAG,GAAG,WAAW;AACjF,SAAO,KAAK,WAAW,IAAI,OAAO,GAAG,SAAS,MAAM,IAAI,SAAS,MAAM,IAAI,GAAG,GAAG,aAAa;AAC9F,SAAO,KAAK;AAAA,IACV,IAAI,OAAO,GAAG,UAAU,MAAM,MAAM,UAAU,MAAM,MAAM,UAAU,MAAM,OAAO,GAAG;AAAA,IACpF;AAAA,EACF;AACA,SAAO,KAAK;AAAA,IACV,IAAI,OAAO,GAAG,UAAU,MAAM,MAAM,UAAU,MAAM,OAAO,GAAG;AAAA,IAC9D;AAAA,EACF;AACA,SAAO,KAAK;AAAA,IACV,IAAI,OAAO,IAAI,SAAS,MAAM,OAAO,SAAS,MAAM,IAAI,GAAG;AAAA,IAC3D;AAAA,EACF;AACA,SAAO,KAAK,WAAW,IAAI,OAAO,IAAI,SAAS,MAAM,OAAO,GAAG,GAAG,SAAS;AAC3E,SAAO,KAAK,WAAW,IAAI,OAAO,IAAI,UAAU,MAAM,OAAO,GAAG,GAAG,SAAS;AAC5E,SAAO,KAAK,WAAW,WAAM,SAAI;AACjC,SAAO,KAAK,WAAW,MAAM,IAAI;AACjC,SAAO,KAAK,WAAW,MAAM,IAAI;AACjC,SAAO,KAAK,WAAW,MAAM,IAAI;AACjC,SAAO,KAAK,QAAQ,eAAe,SAAS;AAC5C,SAAO,KAAK,WAAW,KAAK,SAAS;AACrC,SAAO,KAAK,WAAW,KAAK,SAAS;AACrC,SAAO,KAAK,WAAW,SAAS,GAAG;AAEnC,MAAI,cAAc;AAChB,WAAO,KAAK,WAAW,SAAS,IAAI;AAAA,EACtC;AAEA,QAAM,QAAQ,KAAK,MAAM,QAAQ;AACjC,SAAO,KAAK,WAAW,UAAU,EAAE;AAEnC,QAAM,YAAwC,CAAC;AAC/C,MAAI,MAAM;AACV,MAAI,QAAQ;AACZ,MAAI,MAAM;AACV,QAAM,SAAS,eAAe,KAAK;AACnC,aAAW,SAAS,OAAO;AACzB,UAAM,WAAW,eAAe,QAAQ,MAAM,KAAK;AACnD,QAAI,CAAC,SAAU;AAEf,WAAO,SAAS;AAChB,WAAO,MAAM;AACb,QAAI,IAAI,SAAS,WAAW;AAC1B,gBAAU,KAAK,CAAC,IAAI,MAAM,OAAO,MAAM,GAAG,OAAO,GAAG,CAAC;AACrD,cAAQ;AACR,YAAM;AAAA,IACR;AAAA,EACF;AAEA,MAAI,KAAK;AACP,cAAU,KAAK,CAAC,IAAI,MAAM,OAAO,MAAM,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC;AAAA,EACnE;AAEA,SAAO;AACT;","names":[]}
@@ -4,7 +4,7 @@ const splitSentences = (text, minLength = 20, retainFormat = false) => {
4
4
  const suffixes = /(Inc|Ltd|Jr|Sr|Co)/g;
5
5
  const starters = /(Mr|Mrs|Ms|Dr|Prof|Capt|Cpt|Lt|He\s|She\s|It\s|They\s|Their\s|Our\s|We\s|But\s|However\s|That\s|This\s|Wherever)/g;
6
6
  const acronyms = /([A-Z][.][A-Z][.](?:[A-Z][.])?)/g;
7
- const websites = /[.](com|net|org|io|gov|edu|me)/g;
7
+ const websites = /(\w+\.)+(com|net|org|io|gov|edu|me)/g;
8
8
  const digits = /([0-9])/g;
9
9
  const dots = /\.{2,}/g;
10
10
  if (retainFormat) {
@@ -13,7 +13,7 @@ const splitSentences = (text, minLength = 20, retainFormat = false) => {
13
13
  text = text.replaceAll("\n", " ");
14
14
  }
15
15
  text = text.replaceAll(prefixes, "$1<prd>");
16
- text = text.replaceAll(websites, "<prd>$2");
16
+ text = text.replace(websites, (match) => match.replaceAll(".", "<prd>"));
17
17
  text = text.replaceAll(new RegExp(`${digits.source}[.]${digits.source}`, "g"), "$1<prd>$2");
18
18
  text = text.replaceAll(dots, (match) => "<prd>".repeat(match.length));
19
19
  text = text.replaceAll("Ph.D.", "Ph<prd>D<prd>");
@@ -37,7 +37,7 @@ const splitSentences = (text, minLength = 20, retainFormat = false) => {
37
37
  text = text.replaceAll('."', '".');
38
38
  text = text.replaceAll('!"', '"!');
39
39
  text = text.replaceAll('?"', '"?');
40
- text = text.replaceAll(".", ".<stop>");
40
+ text = text.replace(/\.(?=\s|$)/g, ".<stop>");
41
41
  text = text.replaceAll("?", "?<stop>");
42
42
  text = text.replaceAll("!", "!<stop>");
43
43
  text = text.replaceAll("<prd>", ".");
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/tokenize/basic/sentence.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Split the text into sentences.\n */\nexport const splitSentences = (\n text: string,\n minLength = 20,\n retainFormat: boolean = false,\n): [string, number, number][] => {\n const alphabets = /([A-Za-z])/g;\n const prefixes = /(Mr|St|Mrs|Ms|Dr)[.]/g;\n const suffixes = /(Inc|Ltd|Jr|Sr|Co)/g;\n const starters =\n /(Mr|Mrs|Ms|Dr|Prof|Capt|Cpt|Lt|He\\s|She\\s|It\\s|They\\s|Their\\s|Our\\s|We\\s|But\\s|However\\s|That\\s|This\\s|Wherever)/g;\n const acronyms = /([A-Z][.][A-Z][.](?:[A-Z][.])?)/g;\n const websites = /[.](com|net|org|io|gov|edu|me)/g;\n const digits = /([0-9])/g;\n const dots = /\\.{2,}/g;\n\n if (retainFormat) {\n text = text.replaceAll('\\n', '<nel><stop>');\n } else {\n text = text.replaceAll('\\n', ' ');\n }\n\n text = text.replaceAll(prefixes, '$1<prd>');\n text = text.replaceAll(websites, '<prd>$2');\n text = text.replaceAll(new RegExp(`${digits.source}[.]${digits.source}`, 'g'), '$1<prd>$2');\n text = text.replaceAll(dots, (match) => '<prd>'.repeat(match.length));\n text = text.replaceAll('Ph.D.', 'Ph<prd>D<prd>');\n text = text.replaceAll(new RegExp(`\\\\s${alphabets.source}[.] `, 'g'), ' $1<prd> ');\n text = text.replaceAll(new RegExp(`${acronyms.source} ${starters.source}`, 'g'), '$1<stop> $2');\n text = text.replaceAll(\n new RegExp(`${alphabets.source}[.]${alphabets.source}[.]${alphabets.source}[.]`, 'g'),\n '$1<prd>$2<prd>$3<prd>',\n );\n text = text.replaceAll(\n new RegExp(`${alphabets.source}[.]${alphabets.source}[.]`, 'g'),\n '$1<prd>$2<prd>',\n );\n text = text.replaceAll(\n new RegExp(` ${suffixes.source}[.] ${starters.source}`, 'g'),\n '$1<stop> $2',\n );\n text = text.replaceAll(new RegExp(` ${suffixes.source}[.]`, 'g'), '$1<prd>');\n text = text.replaceAll(new RegExp(` ${alphabets.source}[.]`, 'g'), '$1<prd>');\n text = text.replaceAll('.”', '”.');\n text = text.replaceAll('.\"', '\".');\n text = text.replaceAll('!\"', '\"!');\n text = text.replaceAll('?\"', '\"?');\n text = text.replaceAll('.', '.<stop>');\n text = text.replaceAll('?', '?<stop>');\n text = text.replaceAll('!', '!<stop>');\n text = text.replaceAll('<prd>', '.');\n\n if (retainFormat) {\n text = text.replaceAll('<nel>', '\\n');\n }\n\n const split = text.split('<stop>');\n text = text.replaceAll('<stop>', '');\n\n const sentences: [string, number, number][] = [];\n let buf = '';\n let start = 0;\n let end = 0;\n const prePad = retainFormat ? '' : ' ';\n for (const match of split) {\n const sentence = retainFormat ? match : match.trim();\n if (!sentence) continue;\n\n buf += prePad + sentence;\n end += match.length;\n if (buf.length > minLength) {\n sentences.push([buf.slice(prePad.length), start, end]);\n start = end;\n buf = '';\n }\n }\n\n if (buf) {\n sentences.push([buf.slice(prePad.length), start, text.length - 1]);\n }\n\n return sentences;\n};\n"],"mappings":"AAOO,MAAM,iBAAiB,CAC5B,MACA,YAAY,IACZ,eAAwB,UACO;AAC/B,QAAM,YAAY;AAClB,QAAM,WAAW;AACjB,QAAM,WAAW;AACjB,QAAM,WACJ;AACF,QAAM,WAAW;AACjB,QAAM,WAAW;AACjB,QAAM,SAAS;AACf,QAAM,OAAO;AAEb,MAAI,cAAc;AAChB,WAAO,KAAK,WAAW,MAAM,aAAa;AAAA,EAC5C,OAAO;AACL,WAAO,KAAK,WAAW,MAAM,GAAG;AAAA,EAClC;AAEA,SAAO,KAAK,WAAW,UAAU,SAAS;AAC1C,SAAO,KAAK,WAAW,UAAU,SAAS;AAC1C,SAAO,KAAK,WAAW,IAAI,OAAO,GAAG,OAAO,MAAM,MAAM,OAAO,MAAM,IAAI,GAAG,GAAG,WAAW;AAC1F,SAAO,KAAK,WAAW,MAAM,CAAC,UAAU,QAAQ,OAAO,MAAM,MAAM,CAAC;AACpE,SAAO,KAAK,WAAW,SAAS,eAAe;AAC/C,SAAO,KAAK,WAAW,IAAI,OAAO,MAAM,UAAU,MAAM,QAAQ,GAAG,GAAG,WAAW;AACjF,SAAO,KAAK,WAAW,IAAI,OAAO,GAAG,SAAS,MAAM,IAAI,SAAS,MAAM,IAAI,GAAG,GAAG,aAAa;AAC9F,SAAO,KAAK;AAAA,IACV,IAAI,OAAO,GAAG,UAAU,MAAM,MAAM,UAAU,MAAM,MAAM,UAAU,MAAM,OAAO,GAAG;AAAA,IACpF;AAAA,EACF;AACA,SAAO,KAAK;AAAA,IACV,IAAI,OAAO,GAAG,UAAU,MAAM,MAAM,UAAU,MAAM,OAAO,GAAG;AAAA,IAC9D;AAAA,EACF;AACA,SAAO,KAAK;AAAA,IACV,IAAI,OAAO,IAAI,SAAS,MAAM,OAAO,SAAS,MAAM,IAAI,GAAG;AAAA,IAC3D;AAAA,EACF;AACA,SAAO,KAAK,WAAW,IAAI,OAAO,IAAI,SAAS,MAAM,OAAO,GAAG,GAAG,SAAS;AAC3E,SAAO,KAAK,WAAW,IAAI,OAAO,IAAI,UAAU,MAAM,OAAO,GAAG,GAAG,SAAS;AAC5E,SAAO,KAAK,WAAW,WAAM,SAAI;AACjC,SAAO,KAAK,WAAW,MAAM,IAAI;AACjC,SAAO,KAAK,WAAW,MAAM,IAAI;AACjC,SAAO,KAAK,WAAW,MAAM,IAAI;AACjC,SAAO,KAAK,WAAW,KAAK,SAAS;AACrC,SAAO,KAAK,WAAW,KAAK,SAAS;AACrC,SAAO,KAAK,WAAW,KAAK,SAAS;AACrC,SAAO,KAAK,WAAW,SAAS,GAAG;AAEnC,MAAI,cAAc;AAChB,WAAO,KAAK,WAAW,SAAS,IAAI;AAAA,EACtC;AAEA,QAAM,QAAQ,KAAK,MAAM,QAAQ;AACjC,SAAO,KAAK,WAAW,UAAU,EAAE;AAEnC,QAAM,YAAwC,CAAC;AAC/C,MAAI,MAAM;AACV,MAAI,QAAQ;AACZ,MAAI,MAAM;AACV,QAAM,SAAS,eAAe,KAAK;AACnC,aAAW,SAAS,OAAO;AACzB,UAAM,WAAW,eAAe,QAAQ,MAAM,KAAK;AACnD,QAAI,CAAC,SAAU;AAEf,WAAO,SAAS;AAChB,WAAO,MAAM;AACb,QAAI,IAAI,SAAS,WAAW;AAC1B,gBAAU,KAAK,CAAC,IAAI,MAAM,OAAO,MAAM,GAAG,OAAO,GAAG,CAAC;AACrD,cAAQ;AACR,YAAM;AAAA,IACR;AAAA,EACF;AAEA,MAAI,KAAK;AACP,cAAU,KAAK,CAAC,IAAI,MAAM,OAAO,MAAM,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC;AAAA,EACnE;AAEA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../../src/tokenize/basic/sentence.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Split the text into sentences.\n */\nexport const splitSentences = (\n text: string,\n minLength = 20,\n retainFormat: boolean = false,\n): [string, number, number][] => {\n const alphabets = /([A-Za-z])/g;\n const prefixes = /(Mr|St|Mrs|Ms|Dr)[.]/g;\n const suffixes = /(Inc|Ltd|Jr|Sr|Co)/g;\n const starters =\n /(Mr|Mrs|Ms|Dr|Prof|Capt|Cpt|Lt|He\\s|She\\s|It\\s|They\\s|Their\\s|Our\\s|We\\s|But\\s|However\\s|That\\s|This\\s|Wherever)/g;\n const acronyms = /([A-Z][.][A-Z][.](?:[A-Z][.])?)/g;\n const websites = /(\\w+\\.)+(com|net|org|io|gov|edu|me)/g;\n const digits = /([0-9])/g;\n const dots = /\\.{2,}/g;\n\n if (retainFormat) {\n text = text.replaceAll('\\n', '<nel><stop>');\n } else {\n text = text.replaceAll('\\n', ' ');\n }\n\n text = text.replaceAll(prefixes, '$1<prd>');\n text = text.replace(websites, (match) => match.replaceAll('.', '<prd>'));\n text = text.replaceAll(new RegExp(`${digits.source}[.]${digits.source}`, 'g'), '$1<prd>$2');\n text = text.replaceAll(dots, (match) => '<prd>'.repeat(match.length));\n text = text.replaceAll('Ph.D.', 'Ph<prd>D<prd>');\n text = text.replaceAll(new RegExp(`\\\\s${alphabets.source}[.] `, 'g'), ' $1<prd> ');\n text = text.replaceAll(new RegExp(`${acronyms.source} ${starters.source}`, 'g'), '$1<stop> $2');\n text = text.replaceAll(\n new RegExp(`${alphabets.source}[.]${alphabets.source}[.]${alphabets.source}[.]`, 'g'),\n '$1<prd>$2<prd>$3<prd>',\n );\n text = text.replaceAll(\n new RegExp(`${alphabets.source}[.]${alphabets.source}[.]`, 'g'),\n '$1<prd>$2<prd>',\n );\n text = text.replaceAll(\n new RegExp(` ${suffixes.source}[.] ${starters.source}`, 'g'),\n '$1<stop> $2',\n );\n text = text.replaceAll(new RegExp(` ${suffixes.source}[.]`, 'g'), '$1<prd>');\n text = text.replaceAll(new RegExp(` ${alphabets.source}[.]`, 'g'), '$1<prd>');\n text = text.replaceAll('.”', '”.');\n text = text.replaceAll('.\"', '\".');\n text = text.replaceAll('!\"', '\"!');\n text = text.replaceAll('?\"', '\"?');\n text = text.replace(/\\.(?=\\s|$)/g, '.<stop>');\n text = text.replaceAll('?', '?<stop>');\n text = text.replaceAll('!', '!<stop>');\n text = text.replaceAll('<prd>', '.');\n\n if (retainFormat) {\n text = text.replaceAll('<nel>', '\\n');\n }\n\n const split = text.split('<stop>');\n text = text.replaceAll('<stop>', '');\n\n const sentences: [string, number, number][] = [];\n let buf = '';\n let start = 0;\n let end = 0;\n const prePad = retainFormat ? '' : ' ';\n for (const match of split) {\n const sentence = retainFormat ? match : match.trim();\n if (!sentence) continue;\n\n buf += prePad + sentence;\n end += match.length;\n if (buf.length > minLength) {\n sentences.push([buf.slice(prePad.length), start, end]);\n start = end;\n buf = '';\n }\n }\n\n if (buf) {\n sentences.push([buf.slice(prePad.length), start, text.length - 1]);\n }\n\n return sentences;\n};\n"],"mappings":"AAOO,MAAM,iBAAiB,CAC5B,MACA,YAAY,IACZ,eAAwB,UACO;AAC/B,QAAM,YAAY;AAClB,QAAM,WAAW;AACjB,QAAM,WAAW;AACjB,QAAM,WACJ;AACF,QAAM,WAAW;AACjB,QAAM,WAAW;AACjB,QAAM,SAAS;AACf,QAAM,OAAO;AAEb,MAAI,cAAc;AAChB,WAAO,KAAK,WAAW,MAAM,aAAa;AAAA,EAC5C,OAAO;AACL,WAAO,KAAK,WAAW,MAAM,GAAG;AAAA,EAClC;AAEA,SAAO,KAAK,WAAW,UAAU,SAAS;AAC1C,SAAO,KAAK,QAAQ,UAAU,CAAC,UAAU,MAAM,WAAW,KAAK,OAAO,CAAC;AACvE,SAAO,KAAK,WAAW,IAAI,OAAO,GAAG,OAAO,MAAM,MAAM,OAAO,MAAM,IAAI,GAAG,GAAG,WAAW;AAC1F,SAAO,KAAK,WAAW,MAAM,CAAC,UAAU,QAAQ,OAAO,MAAM,MAAM,CAAC;AACpE,SAAO,KAAK,WAAW,SAAS,eAAe;AAC/C,SAAO,KAAK,WAAW,IAAI,OAAO,MAAM,UAAU,MAAM,QAAQ,GAAG,GAAG,WAAW;AACjF,SAAO,KAAK,WAAW,IAAI,OAAO,GAAG,SAAS,MAAM,IAAI,SAAS,MAAM,IAAI,GAAG,GAAG,aAAa;AAC9F,SAAO,KAAK;AAAA,IACV,IAAI,OAAO,GAAG,UAAU,MAAM,MAAM,UAAU,MAAM,MAAM,UAAU,MAAM,OAAO,GAAG;AAAA,IACpF;AAAA,EACF;AACA,SAAO,KAAK;AAAA,IACV,IAAI,OAAO,GAAG,UAAU,MAAM,MAAM,UAAU,MAAM,OAAO,GAAG;AAAA,IAC9D;AAAA,EACF;AACA,SAAO,KAAK;AAAA,IACV,IAAI,OAAO,IAAI,SAAS,MAAM,OAAO,SAAS,MAAM,IAAI,GAAG;AAAA,IAC3D;AAAA,EACF;AACA,SAAO,KAAK,WAAW,IAAI,OAAO,IAAI,SAAS,MAAM,OAAO,GAAG,GAAG,SAAS;AAC3E,SAAO,KAAK,WAAW,IAAI,OAAO,IAAI,UAAU,MAAM,OAAO,GAAG,GAAG,SAAS;AAC5E,SAAO,KAAK,WAAW,WAAM,SAAI;AACjC,SAAO,KAAK,WAAW,MAAM,IAAI;AACjC,SAAO,KAAK,WAAW,MAAM,IAAI;AACjC,SAAO,KAAK,WAAW,MAAM,IAAI;AACjC,SAAO,KAAK,QAAQ,eAAe,SAAS;AAC5C,SAAO,KAAK,WAAW,KAAK,SAAS;AACrC,SAAO,KAAK,WAAW,KAAK,SAAS;AACrC,SAAO,KAAK,WAAW,SAAS,GAAG;AAEnC,MAAI,cAAc;AAChB,WAAO,KAAK,WAAW,SAAS,IAAI;AAAA,EACtC;AAEA,QAAM,QAAQ,KAAK,MAAM,QAAQ;AACjC,SAAO,KAAK,WAAW,UAAU,EAAE;AAEnC,QAAM,YAAwC,CAAC;AAC/C,MAAI,MAAM;AACV,MAAI,QAAQ;AACZ,MAAI,MAAM;AACV,QAAM,SAAS,eAAe,KAAK;AACnC,aAAW,SAAS,OAAO;AACzB,UAAM,WAAW,eAAe,QAAQ,MAAM,KAAK;AACnD,QAAI,CAAC,SAAU;AAEf,WAAO,SAAS;AAChB,WAAO,MAAM;AACb,QAAI,IAAI,SAAS,WAAW;AAC1B,gBAAU,KAAK,CAAC,IAAI,MAAM,OAAO,MAAM,GAAG,OAAO,GAAG,CAAC;AACrD,cAAQ;AACR,YAAM;AAAA,IACR;AAAA,EACF;AAEA,MAAI,KAAK;AACP,cAAU,KAAK,CAAC,IAAI,MAAM,OAAO,MAAM,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC;AAAA,EACnE;AAEA,SAAO;AACT;","names":[]}
@@ -2,13 +2,15 @@
2
2
  var import_vitest = require("vitest");
3
3
  var import_basic = require("./basic/index.cjs");
4
4
  var import_paragraph = require("./basic/paragraph.cjs");
5
- const TEXT = "Hi! LiveKit is a platform for live audio and video applications and services. R.T.C stands for Real-Time Communication... again R.T.C. Mr. Theo is testing the sentence tokenizer. This is a test. Another test. A short sentence. A longer sentence that is longer than the previous sentence. f(x) = x * 2.54 + 42. Hey! Hi! Hello! ";
5
+ const TEXT = "Hi! LiveKit is a platform for live audio and video applications and services. R.T.C stands for Real-Time Communication... again R.T.C. Mr. Theo is testing the sentence tokenizer. This is a test. Another test. A short sentence. A longer sentence that is longer than the previous sentence. Find additional resources on livekit.com. Find additional resources on docs.livekit.com. f(x) = x * 2.54 + 42. Hey! Hi! Hello! ";
6
6
  const EXPECTED_MIN_20 = [
7
7
  "Hi! LiveKit is a platform for live audio and video applications and services.",
8
8
  "R.T.C stands for Real-Time Communication... again R.T.C.",
9
9
  "Mr. Theo is testing the sentence tokenizer.",
10
10
  "This is a test. Another test.",
11
11
  "A short sentence. A longer sentence that is longer than the previous sentence.",
12
+ "Find additional resources on livekit.com.",
13
+ "Find additional resources on docs.livekit.com.",
12
14
  "f(x) = x * 2.54 + 42.",
13
15
  "Hey! Hi! Hello!"
14
16
  ];