@livekit/agents 1.0.40 → 1.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +20 -18
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +20 -18
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +5 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/inference/stt.cjs +2 -1
- package/dist/inference/stt.cjs.map +1 -1
- package/dist/inference/stt.d.ts.map +1 -1
- package/dist/inference/stt.js +2 -1
- package/dist/inference/stt.js.map +1 -1
- package/dist/llm/realtime.cjs.map +1 -1
- package/dist/llm/realtime.d.cts +5 -1
- package/dist/llm/realtime.d.ts +5 -1
- package/dist/llm/realtime.d.ts.map +1 -1
- package/dist/llm/realtime.js.map +1 -1
- package/dist/tts/stream_adapter.cjs +15 -1
- package/dist/tts/stream_adapter.cjs.map +1 -1
- package/dist/tts/stream_adapter.d.ts.map +1 -1
- package/dist/tts/stream_adapter.js +15 -1
- package/dist/tts/stream_adapter.js.map +1 -1
- package/dist/tts/tts.cjs.map +1 -1
- package/dist/tts/tts.d.cts +9 -1
- package/dist/tts/tts.d.ts +9 -1
- package/dist/tts/tts.d.ts.map +1 -1
- package/dist/tts/tts.js.map +1 -1
- package/dist/types.cjs +3 -0
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +4 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/dist/voice/agent.cjs +11 -1
- package/dist/voice/agent.cjs.map +1 -1
- package/dist/voice/agent.d.cts +7 -3
- package/dist/voice/agent.d.ts +7 -3
- package/dist/voice/agent.d.ts.map +1 -1
- package/dist/voice/agent.js +11 -1
- package/dist/voice/agent.js.map +1 -1
- package/dist/voice/agent_activity.cjs +30 -14
- package/dist/voice/agent_activity.cjs.map +1 -1
- package/dist/voice/agent_activity.d.cts +1 -0
- package/dist/voice/agent_activity.d.ts +1 -0
- package/dist/voice/agent_activity.d.ts.map +1 -1
- package/dist/voice/agent_activity.js +30 -14
- package/dist/voice/agent_activity.js.map +1 -1
- package/dist/voice/agent_session.cjs +5 -1
- package/dist/voice/agent_session.cjs.map +1 -1
- package/dist/voice/agent_session.d.cts +2 -0
- package/dist/voice/agent_session.d.ts +2 -0
- package/dist/voice/agent_session.d.ts.map +1 -1
- package/dist/voice/agent_session.js +5 -1
- package/dist/voice/agent_session.js.map +1 -1
- package/dist/voice/audio_recognition.cjs +1 -1
- package/dist/voice/audio_recognition.cjs.map +1 -1
- package/dist/voice/audio_recognition.d.ts.map +1 -1
- package/dist/voice/audio_recognition.js +1 -1
- package/dist/voice/audio_recognition.js.map +1 -1
- package/dist/voice/background_audio.cjs +2 -1
- package/dist/voice/background_audio.cjs.map +1 -1
- package/dist/voice/background_audio.d.cts +4 -2
- package/dist/voice/background_audio.d.ts +4 -2
- package/dist/voice/background_audio.d.ts.map +1 -1
- package/dist/voice/background_audio.js +2 -1
- package/dist/voice/background_audio.js.map +1 -1
- package/dist/voice/generation.cjs +58 -5
- package/dist/voice/generation.cjs.map +1 -1
- package/dist/voice/generation.d.cts +17 -3
- package/dist/voice/generation.d.ts +17 -3
- package/dist/voice/generation.d.ts.map +1 -1
- package/dist/voice/generation.js +63 -6
- package/dist/voice/generation.js.map +1 -1
- package/dist/voice/index.cjs.map +1 -1
- package/dist/voice/index.d.cts +1 -1
- package/dist/voice/index.d.ts +1 -1
- package/dist/voice/index.d.ts.map +1 -1
- package/dist/voice/index.js.map +1 -1
- package/dist/voice/io.cjs +22 -2
- package/dist/voice/io.cjs.map +1 -1
- package/dist/voice/io.d.cts +21 -5
- package/dist/voice/io.d.ts +21 -5
- package/dist/voice/io.d.ts.map +1 -1
- package/dist/voice/io.js +18 -1
- package/dist/voice/io.js.map +1 -1
- package/dist/voice/room_io/_output.cjs +3 -2
- package/dist/voice/room_io/_output.cjs.map +1 -1
- package/dist/voice/room_io/_output.d.cts +3 -3
- package/dist/voice/room_io/_output.d.ts +3 -3
- package/dist/voice/room_io/_output.d.ts.map +1 -1
- package/dist/voice/room_io/_output.js +4 -3
- package/dist/voice/room_io/_output.js.map +1 -1
- package/dist/voice/transcription/synchronizer.cjs +137 -13
- package/dist/voice/transcription/synchronizer.cjs.map +1 -1
- package/dist/voice/transcription/synchronizer.d.cts +34 -4
- package/dist/voice/transcription/synchronizer.d.ts +34 -4
- package/dist/voice/transcription/synchronizer.d.ts.map +1 -1
- package/dist/voice/transcription/synchronizer.js +141 -14
- package/dist/voice/transcription/synchronizer.js.map +1 -1
- package/dist/voice/transcription/synchronizer.test.cjs +151 -0
- package/dist/voice/transcription/synchronizer.test.cjs.map +1 -0
- package/dist/voice/transcription/synchronizer.test.js +150 -0
- package/dist/voice/transcription/synchronizer.test.js.map +1 -0
- package/package.json +1 -1
- package/src/cli.ts +20 -18
- package/src/index.ts +1 -0
- package/src/inference/stt.ts +9 -8
- package/src/llm/realtime.ts +5 -1
- package/src/tts/stream_adapter.ts +23 -1
- package/src/tts/tts.ts +10 -1
- package/src/types.ts +5 -0
- package/src/voice/agent.ts +19 -4
- package/src/voice/agent_activity.ts +38 -13
- package/src/voice/agent_session.ts +6 -0
- package/src/voice/audio_recognition.ts +2 -1
- package/src/voice/background_audio.ts +6 -3
- package/src/voice/generation.ts +115 -10
- package/src/voice/index.ts +1 -1
- package/src/voice/io.ts +40 -5
- package/src/voice/room_io/_output.ts +6 -5
- package/src/voice/transcription/synchronizer.test.ts +206 -0
- package/src/voice/transcription/synchronizer.ts +202 -17
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/voice/transcription/synchronizer.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport type { ReadableStream, WritableStreamDefaultWriter } from 'node:stream/web';\nimport { log } from '../../log.js';\nimport { IdentityTransform } from '../../stream/identity_transform.js';\nimport type { SentenceStream, SentenceTokenizer } from '../../tokenize/index.js';\nimport { basic } from '../../tokenize/index.js';\nimport { Future, Task, delay } from '../../utils.js';\nimport { AudioOutput, type PlaybackFinishedEvent, TextOutput } from '../io.js';\n\nconst STANDARD_SPEECH_RATE = 3.83; // hyphens (syllables) per second\n\ninterface TextSyncOptions {\n speed: number;\n hyphenateWord: (word: string) => string[];\n splitWords: (words: string) => [string, number, number][];\n sentenceTokenizer: SentenceTokenizer;\n}\n\ninterface TextData {\n sentenceStream: SentenceStream;\n pushedText: string;\n done: boolean;\n forwardedHyphens: number;\n forwardedText: string;\n}\n\ninterface AudioData {\n pushedDuration: number;\n done: boolean;\n}\n\nclass SegmentSynchronizerImpl {\n private textData: TextData;\n private audioData: AudioData;\n private speed: number;\n private outputStream: IdentityTransform<string>;\n private outputStreamWriter: WritableStreamDefaultWriter<string>;\n private captureTask: Promise<void>;\n private startWallTime?: number;\n\n private startFuture: Future = new Future();\n private closedFuture: Future = new Future();\n private playbackCompleted: boolean = false;\n\n private logger = log();\n\n constructor(\n private readonly options: TextSyncOptions,\n private readonly nextInChain: TextOutput,\n ) {\n this.speed = options.speed * STANDARD_SPEECH_RATE; // hyphens per second\n this.textData = {\n sentenceStream: options.sentenceTokenizer.stream(),\n pushedText: '',\n done: false,\n forwardedHyphens: 0,\n forwardedText: '',\n };\n this.audioData = {\n pushedDuration: 0,\n done: false,\n };\n this.outputStream = new IdentityTransform();\n this.outputStreamWriter = this.outputStream.writable.getWriter();\n\n this.mainTask()\n .then(() => {\n this.outputStreamWriter.close();\n })\n .catch((error) => {\n this.logger.error({ error }, 'mainTask SegmentSynchronizerImpl');\n });\n this.captureTask = this.captureTaskImpl();\n }\n\n get closed() {\n return this.closedFuture.done;\n }\n\n get audioInputEnded() {\n return this.audioData.done;\n }\n\n get textInputEnded() {\n return this.textData.done;\n }\n\n get readable(): ReadableStream<string> {\n return this.outputStream.readable;\n }\n\n pushAudio(frame: AudioFrame) {\n if (this.closed) {\n this.logger.warn('SegmentSynchronizerImpl.pushAudio called after close');\n return;\n }\n // TODO(AJS-102): use frame.durationMs once available in rtc-node\n const frameDuration = frame.samplesPerChannel / frame.sampleRate;\n\n if (!this.startWallTime && frameDuration > 0) {\n this.startWallTime = Date.now();\n this.startFuture.resolve();\n }\n\n this.audioData.pushedDuration += frameDuration;\n }\n\n endAudioInput() {\n if (this.closed) {\n this.logger.warn('SegmentSynchronizerImpl.endAudioInput called after close');\n return;\n }\n\n this.audioData.done = true;\n }\n\n pushText(text: string) {\n if (this.closed) {\n this.logger.warn('SegmentSynchronizerImpl.pushText called after close');\n return;\n }\n\n this.textData.sentenceStream.pushText(text);\n this.textData.pushedText += text;\n }\n\n endTextInput() {\n if (this.closed) {\n this.logger.warn('SegmentSynchronizerImpl.endTextInput called after close');\n return;\n }\n\n this.textData.done = true;\n this.textData.sentenceStream.endInput();\n }\n\n markPlaybackFinished(_playbackPosition: number, interrupted: boolean) {\n if (this.closed) {\n this.logger.warn('SegmentSynchronizerImpl.markPlaybackFinished called after close');\n return;\n }\n\n if (!this.textData.done || !this.audioData.done) {\n this.logger.warn(\n { textDone: this.textData.done, audioDone: this.audioData.done },\n 'SegmentSynchronizerImpl.markPlaybackFinished called before text/audio input is done',\n );\n return;\n }\n\n if (!interrupted) {\n this.playbackCompleted = true;\n }\n }\n\n get synchronizedTranscript(): string {\n if (this.playbackCompleted) {\n return this.textData.pushedText;\n }\n return this.textData.forwardedText;\n }\n\n private async captureTaskImpl() {\n // Don't use a for-await loop here, because exiting the loop will close the writer in the\n // outputStream, which will cause an error in the mainTask.then method.\n const reader = this.outputStream.readable.getReader();\n while (true) {\n const { done, value: text } = await reader.read();\n if (done) {\n break;\n }\n this.textData.forwardedText += text;\n await this.nextInChain.captureText(text);\n }\n reader.releaseLock();\n this.nextInChain.flush();\n }\n\n private async mainTask(): Promise<void> {\n await this.startFuture.await;\n\n if (this.closed && !this.playbackCompleted) {\n return;\n }\n\n if (!this.startWallTime) {\n throw new Error('startWallTime is not set when starting SegmentSynchronizerImpl.mainTask');\n }\n\n for await (const textSegment of this.textData.sentenceStream) {\n const sentence = textSegment.token;\n\n let textCursor = 0;\n if (this.closed && !this.playbackCompleted) {\n return;\n }\n\n for (const [word, _, endPos] of this.options.splitWords(sentence)) {\n if (this.closed && !this.playbackCompleted) {\n return;\n }\n\n if (this.playbackCompleted) {\n this.outputStreamWriter.write(sentence.slice(textCursor, endPos));\n textCursor = endPos;\n continue;\n }\n\n const wordHphens = this.options.hyphenateWord(word).length;\n const elapsedSeconds = (Date.now() - this.startWallTime) / 1000;\n const targetHyphens = elapsedSeconds * this.options.speed;\n const hyphensBehind = Math.max(0, targetHyphens - this.textData.forwardedHyphens);\n let delay = Math.max(0, wordHphens - hyphensBehind) / this.speed;\n\n if (this.playbackCompleted) {\n delay = 0;\n }\n\n await this.sleepIfNotClosed(delay / 2);\n this.outputStreamWriter.write(sentence.slice(textCursor, endPos));\n await this.sleepIfNotClosed(delay / 2);\n\n this.textData.forwardedHyphens += wordHphens;\n textCursor = endPos;\n }\n\n if (textCursor < sentence.length) {\n const remaining = sentence.slice(textCursor);\n this.outputStreamWriter.write(remaining);\n }\n }\n }\n\n private async sleepIfNotClosed(sleepTimeSeconds: number) {\n if (this.closed) {\n return;\n }\n await delay(sleepTimeSeconds * 1000);\n }\n\n async close(): Promise<void> {\n if (this.closed) {\n return;\n }\n this.closedFuture.resolve();\n this.startFuture.resolve(); // avoid deadlock of mainTaskImpl in case it never started\n this.textData.sentenceStream.close();\n await this.captureTask;\n }\n}\n\nexport interface TranscriptionSynchronizerOptions {\n speed: number;\n hyphenateWord: (word: string) => string[];\n splitWords: (words: string) => [string, number, number][];\n sentenceTokenizer: SentenceTokenizer;\n}\n\nexport const defaultTextSyncOptions: TranscriptionSynchronizerOptions = {\n speed: 1,\n hyphenateWord: basic.hyphenateWord,\n splitWords: basic.splitWords,\n sentenceTokenizer: new basic.SentenceTokenizer({\n retainFormat: true,\n }),\n};\n\nexport class TranscriptionSynchronizer {\n readonly audioOutput: SyncedAudioOutput;\n readonly textOutput: SyncedTextOutput;\n\n private options: TextSyncOptions;\n private rotateSegmentTask: Task<void>;\n private _enabled: boolean = true;\n private closed: boolean = false;\n\n /** @internal */\n _impl: SegmentSynchronizerImpl;\n\n private logger = log();\n\n constructor(\n nextInChainAudio: AudioOutput,\n nextInChainText: TextOutput,\n options: TranscriptionSynchronizerOptions = defaultTextSyncOptions,\n ) {\n this.audioOutput = new SyncedAudioOutput(this, nextInChainAudio);\n this.textOutput = new SyncedTextOutput(this, nextInChainText);\n this.options = {\n speed: options.speed,\n hyphenateWord: options.hyphenateWord,\n splitWords: options.splitWords,\n sentenceTokenizer: options.sentenceTokenizer,\n };\n\n // initial segment/first segment, recreated for each new segment\n this._impl = new SegmentSynchronizerImpl(this.options, nextInChainText);\n this.rotateSegmentTask = Task.from((controller) =>\n this.rotateSegmentTaskImpl(controller.signal),\n );\n }\n\n get enabled(): boolean {\n return this._enabled;\n }\n\n set enabled(enabled: boolean) {\n if (this._enabled === enabled) {\n return;\n }\n\n this._enabled = enabled;\n this.rotateSegment();\n }\n\n rotateSegment() {\n if (this.closed) {\n return;\n }\n\n if (!this.rotateSegmentTask.done) {\n this.logger.warn('rotateSegment called while previous segment is still being rotated');\n }\n this.rotateSegmentTask = Task.from((controller) =>\n this.rotateSegmentTaskImpl(controller.signal, this.rotateSegmentTask),\n );\n }\n\n async close(): Promise<void> {\n this.closed = true;\n await this.rotateSegmentTask.cancelAndWait();\n await this._impl.close();\n }\n\n async barrier(): Promise<void> {\n if (this.rotateSegmentTask.done) {\n return;\n }\n await this.rotateSegmentTask.result;\n }\n\n private async rotateSegmentTaskImpl(abort: AbortSignal, oldTask?: Task<void>) {\n if (oldTask) {\n await oldTask.result;\n }\n\n if (abort.aborted) {\n return;\n }\n await this._impl.close();\n this._impl = new SegmentSynchronizerImpl(this.options, this.textOutput.nextInChain);\n }\n}\n\nclass SyncedAudioOutput extends AudioOutput {\n private pushedDuration: number = 0.0;\n\n constructor(\n public synchronizer: TranscriptionSynchronizer,\n private nextInChainAudio: AudioOutput,\n ) {\n super(nextInChainAudio.sampleRate, nextInChainAudio, { pause: true });\n }\n\n async captureFrame(frame: AudioFrame): Promise<void> {\n // using barrier() on capture should be sufficient, flush() must not be called if\n // capture_frame isn't completed\n await this.synchronizer.barrier();\n\n await super.captureFrame(frame);\n await this.nextInChainAudio.captureFrame(frame); // passthrough audio\n\n // TODO(AJS-102): use frame.durationMs once available in rtc-node\n this.pushedDuration += frame.samplesPerChannel / frame.sampleRate;\n\n if (!this.synchronizer.enabled) {\n return;\n }\n\n if (this.synchronizer._impl.audioInputEnded) {\n this.logger.warn(\n 'SegmentSynchronizerImpl audio marked as ended in capture audio, rotating segment',\n );\n this.synchronizer.rotateSegment();\n await this.synchronizer.barrier();\n }\n this.synchronizer._impl.pushAudio(frame);\n }\n\n flush() {\n super.flush();\n this.nextInChainAudio.flush();\n\n if (!this.synchronizer.enabled) {\n return;\n }\n\n if (!this.pushedDuration) {\n // in case there is no audio after the text was pushed, rotate the segment\n this.synchronizer.rotateSegment();\n return;\n }\n\n this.synchronizer._impl.endAudioInput();\n }\n\n clearBuffer() {\n this.nextInChainAudio.clearBuffer();\n }\n\n // this is going to be automatically called by the next_in_chain\n onPlaybackFinished(ev: PlaybackFinishedEvent) {\n if (!this.synchronizer.enabled) {\n super.onPlaybackFinished(ev);\n return;\n }\n\n this.synchronizer._impl.markPlaybackFinished(ev.playbackPosition, ev.interrupted);\n super.onPlaybackFinished({\n playbackPosition: ev.playbackPosition,\n interrupted: ev.interrupted,\n synchronizedTranscript: this.synchronizer._impl.synchronizedTranscript,\n });\n\n this.synchronizer.rotateSegment();\n this.pushedDuration = 0.0;\n }\n}\n\nclass SyncedTextOutput extends TextOutput {\n private capturing: boolean = false;\n private logger = log();\n\n constructor(\n private readonly synchronizer: TranscriptionSynchronizer,\n public readonly nextInChain: TextOutput,\n ) {\n super(nextInChain);\n }\n\n async captureText(text: string): Promise<void> {\n await this.synchronizer.barrier();\n\n if (!this.synchronizer.enabled) {\n // pass through to the next in chain\n await this.nextInChain.captureText(text);\n return;\n }\n\n this.capturing = true;\n if (this.synchronizer._impl.textInputEnded) {\n this.logger.warn(\n 'SegmentSynchronizerImpl text marked as ended in capture text, rotating segment',\n );\n this.synchronizer.rotateSegment();\n await this.synchronizer.barrier();\n }\n this.synchronizer._impl.pushText(text);\n }\n\n flush() {\n if (!this.synchronizer.enabled) {\n this.nextInChain.flush(); // passthrough text if the synchronizer is disabled\n return;\n }\n\n if (!this.capturing) {\n return;\n }\n\n this.capturing = false;\n this.synchronizer._impl.endTextInput();\n }\n}\n"],"mappings":"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","names":["delay"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/voice/transcription/synchronizer.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport type { ReadableStream, WritableStreamDefaultWriter } from 'node:stream/web';\nimport { log } from '../../log.js';\nimport { IdentityTransform } from '../../stream/identity_transform.js';\nimport type { SentenceStream, SentenceTokenizer } from '../../tokenize/index.js';\nimport { basic } from '../../tokenize/index.js';\nimport { Future, Task, delay } from '../../utils.js';\nimport {\n AudioOutput,\n type PlaybackFinishedEvent,\n TextOutput,\n type TimedString,\n isTimedString,\n} from '../io.js';\n\nconst STANDARD_SPEECH_RATE = 3.83; // hyphens (syllables) per second\n\ninterface TextSyncOptions {\n speed: number;\n hyphenateWord: (word: string) => string[];\n splitWords: (words: string) => [string, number, number][];\n sentenceTokenizer: SentenceTokenizer;\n}\n\ninterface TextData {\n sentenceStream: SentenceStream;\n pushedText: string;\n done: boolean;\n forwardedHyphens: number;\n forwardedText: string;\n}\n\n/**\n * Tracks speaking rate data from TTS timing annotations.\n * @internal Exported for testing purposes.\n */\nexport class SpeakingRateData {\n /** Timestamps of the speaking rate. */\n timestamps: number[] = [];\n /** Speed at the timestamp. */\n speakingRate: number[] = [];\n /** Accumulated speaking units up to the timestamp. */\n speakIntegrals: number[] = [];\n /** Buffer for text without timing annotations yet. */\n private textBuffer: string[] = [];\n\n /**\n * Add by speaking rate estimation.\n */\n addByRate(timestamp: number, speakingRate: number): void {\n const integral =\n this.speakIntegrals.length > 0 ? this.speakIntegrals[this.speakIntegrals.length - 1]! : 0;\n const dt = timestamp - this.pushedDuration;\n const newIntegral = integral + speakingRate * dt;\n\n this.timestamps.push(timestamp);\n this.speakingRate.push(speakingRate);\n this.speakIntegrals.push(newIntegral);\n }\n\n /**\n * Add annotation from TimedString with start_time/end_time.\n */\n addByAnnotation(text: string, startTime: number | undefined, endTime: number | undefined): void {\n if (startTime !== undefined) {\n // Calculate the integral of the speaking rate up to the start time\n const integral =\n this.speakIntegrals.length > 0 ? this.speakIntegrals[this.speakIntegrals.length - 1]! : 0;\n\n const dt = startTime - this.pushedDuration;\n // Use the length of the text directly instead of hyphens\n const textLen = this.textBuffer.reduce((sum, t) => sum + t.length, 0);\n const newIntegral = integral + textLen;\n const rate = dt > 0 ? textLen / dt : 0;\n\n this.timestamps.push(startTime);\n this.speakingRate.push(rate);\n this.speakIntegrals.push(newIntegral);\n this.textBuffer = [];\n }\n\n this.textBuffer.push(text);\n\n if (endTime !== undefined) {\n this.addByAnnotation('', endTime, undefined);\n }\n }\n\n /**\n * Get accumulated speaking units up to the given timestamp.\n */\n accumulateTo(timestamp: number): number {\n if (this.timestamps.length === 0) {\n return 0;\n }\n\n // Binary search for the right position (equivalent to np.searchsorted with side=\"right\")\n let idx = 0;\n for (let i = 0; i < this.timestamps.length; i++) {\n if (this.timestamps[i]! <= timestamp) {\n idx = i + 1;\n } else {\n break;\n }\n }\n\n if (idx === 0) {\n return 0;\n }\n\n let integralT = this.speakIntegrals[idx - 1]!;\n\n // Fill the tail assuming the speaking rate is constant\n const dt = timestamp - this.timestamps[idx - 1]!;\n const rate =\n idx < this.speakingRate.length ? this.speakingRate[idx]! : this.speakingRate[idx - 1]!;\n integralT += rate * dt;\n\n // If there is a next timestamp, make sure the integral does not exceed the next\n if (idx < this.timestamps.length) {\n integralT = Math.min(integralT, this.speakIntegrals[idx]!);\n }\n\n return integralT;\n }\n\n /** Get the last pushed timestamp. */\n get pushedDuration(): number {\n return this.timestamps.length > 0 ? this.timestamps[this.timestamps.length - 1]! : 0;\n }\n}\n\ninterface AudioData {\n pushedDuration: number;\n done: boolean;\n annotatedRate: SpeakingRateData | null;\n}\n\nclass SegmentSynchronizerImpl {\n private textData: TextData;\n private audioData: AudioData;\n private speed: number;\n private outputStream: IdentityTransform<string>;\n private outputStreamWriter: WritableStreamDefaultWriter<string>;\n private captureTask: Promise<void>;\n private startWallTime?: number;\n\n private startFuture: Future = new Future();\n private closedFuture: Future = new Future();\n private playbackCompleted: boolean = false;\n\n private logger = log();\n\n constructor(\n private readonly options: TextSyncOptions,\n private readonly nextInChain: TextOutput,\n ) {\n this.speed = options.speed * STANDARD_SPEECH_RATE; // hyphens per second\n this.textData = {\n sentenceStream: options.sentenceTokenizer.stream(),\n pushedText: '',\n done: false,\n forwardedHyphens: 0,\n forwardedText: '',\n };\n this.audioData = {\n pushedDuration: 0,\n done: false,\n annotatedRate: null,\n };\n this.outputStream = new IdentityTransform();\n this.outputStreamWriter = this.outputStream.writable.getWriter();\n\n this.mainTask()\n .then(() => {\n this.outputStreamWriter.close();\n })\n .catch((error) => {\n this.logger.error({ error }, 'mainTask SegmentSynchronizerImpl');\n });\n this.captureTask = this.captureTaskImpl();\n }\n\n get closed() {\n return this.closedFuture.done;\n }\n\n get audioInputEnded() {\n return this.audioData.done;\n }\n\n get textInputEnded() {\n return this.textData.done;\n }\n\n get hasPendingText(): boolean {\n return this.textData.pushedText.length > this.textData.forwardedText.length;\n }\n\n get readable(): ReadableStream<string> {\n return this.outputStream.readable;\n }\n\n pushAudio(frame: AudioFrame) {\n if (this.closed) {\n this.logger.warn('SegmentSynchronizerImpl.pushAudio called after close');\n return;\n }\n // TODO(AJS-102): use frame.durationMs once available in rtc-node\n const frameDuration = frame.samplesPerChannel / frame.sampleRate;\n\n if (!this.startWallTime && frameDuration > 0) {\n this.startWallTime = Date.now();\n this.startFuture.resolve();\n }\n\n this.audioData.pushedDuration += frameDuration;\n }\n\n endAudioInput() {\n if (this.closed) {\n this.logger.warn('SegmentSynchronizerImpl.endAudioInput called after close');\n return;\n }\n\n this.audioData.done = true;\n }\n\n pushText(text: string | TimedString) {\n if (this.closed) {\n this.logger.warn('SegmentSynchronizerImpl.pushText called after close');\n return;\n }\n\n // Check if text is a TimedString (has timing information)\n let textStr: string;\n let startTime: number | undefined;\n let endTime: number | undefined;\n\n if (isTimedString(text)) {\n // This is a TimedString\n textStr = text.text;\n startTime = text.startTime;\n endTime = text.endTime;\n\n // Create annotatedRate if it doesn't exist\n if (!this.audioData.annotatedRate) {\n this.audioData.annotatedRate = new SpeakingRateData();\n }\n\n // Add the timing annotation\n this.audioData.annotatedRate.addByAnnotation(textStr, startTime, endTime);\n } else {\n textStr = text;\n }\n\n this.textData.sentenceStream.pushText(textStr);\n this.textData.pushedText += textStr;\n }\n\n endTextInput() {\n if (this.closed) {\n this.logger.warn('SegmentSynchronizerImpl.endTextInput called after close');\n return;\n }\n\n this.textData.done = true;\n this.textData.sentenceStream.endInput();\n }\n\n markPlaybackFinished(_playbackPosition: number, interrupted: boolean) {\n if (this.closed) {\n this.logger.warn('SegmentSynchronizerImpl.markPlaybackFinished called after close');\n return;\n }\n\n if (!this.textData.done || !this.audioData.done) {\n this.logger.warn(\n { textDone: this.textData.done, audioDone: this.audioData.done },\n 'SegmentSynchronizerImpl.markPlaybackFinished called before text/audio input is done',\n );\n // This allows mainTask to flush remaining text even if audio wasn't formally ended\n if (!interrupted) {\n this.playbackCompleted = true;\n }\n return;\n }\n\n if (!interrupted) {\n this.playbackCompleted = true;\n }\n }\n\n get synchronizedTranscript(): string {\n if (this.playbackCompleted) {\n return this.textData.pushedText;\n }\n return this.textData.forwardedText;\n }\n\n private async captureTaskImpl() {\n // Don't use a for-await loop here, because exiting the loop will close the writer in the\n // outputStream, which will cause an error in the mainTask.then method.\n // NOTE: forwardedText is updated in mainTask, NOT here\n const reader = this.outputStream.readable.getReader();\n while (true) {\n const { done, value: text } = await reader.read();\n if (done) {\n break;\n }\n await this.nextInChain.captureText(text);\n }\n reader.releaseLock();\n this.nextInChain.flush();\n }\n\n private async mainTask(): Promise<void> {\n await this.startFuture.await;\n\n if (this.closed && !this.playbackCompleted) {\n return;\n }\n\n if (!this.startWallTime) {\n throw new Error('startWallTime is not set when starting SegmentSynchronizerImpl.mainTask');\n }\n\n for await (const textSegment of this.textData.sentenceStream) {\n const sentence = textSegment.token;\n\n let textCursor = 0;\n if (this.closed && !this.playbackCompleted) {\n return;\n }\n\n for (const [word, _, endPos] of this.options.splitWords(sentence)) {\n if (this.closed && !this.playbackCompleted) {\n return;\n }\n\n if (this.playbackCompleted) {\n this.outputStreamWriter.write(sentence.slice(textCursor, endPos));\n textCursor = endPos;\n continue;\n }\n\n const wordHphens = this.options.hyphenateWord(word).length;\n const elapsedSeconds = (Date.now() - this.startWallTime) / 1000;\n\n let dHyphens = 0;\n const annotated = this.audioData.annotatedRate;\n\n if (annotated && annotated.pushedDuration >= elapsedSeconds) {\n // Use actual TTS timing annotations for accurate sync\n const targetLen = Math.floor(annotated.accumulateTo(elapsedSeconds));\n const forwardedLen = this.textData.forwardedText.length;\n\n if (targetLen >= forwardedLen) {\n const dText = this.textData.pushedText.slice(forwardedLen, targetLen);\n dHyphens = this.calcHyphens(dText).length;\n } else {\n const dText = this.textData.pushedText.slice(targetLen, forwardedLen);\n dHyphens = -this.calcHyphens(dText).length;\n }\n } else {\n // Fall back to estimated hyphens-per-second calculation\n const targetHyphens = elapsedSeconds * this.options.speed;\n dHyphens = Math.max(0, targetHyphens - this.textData.forwardedHyphens);\n }\n\n let delayTime = Math.max(0, wordHphens - dHyphens) / this.speed;\n\n if (this.playbackCompleted) {\n delayTime = 0;\n }\n\n await this.sleepIfNotClosed(delayTime / 2);\n const forwardedWord = sentence.slice(textCursor, endPos);\n this.outputStreamWriter.write(forwardedWord);\n\n await this.sleepIfNotClosed(delayTime / 2);\n\n this.textData.forwardedHyphens += wordHphens;\n this.textData.forwardedText += forwardedWord;\n textCursor = endPos;\n }\n\n if (textCursor < sentence.length) {\n const remaining = sentence.slice(textCursor);\n this.outputStreamWriter.write(remaining);\n }\n }\n }\n\n private calcHyphens(text: string): string[] {\n const words = this.options.splitWords(text);\n const hyphens: string[] = [];\n for (const [word] of words) {\n hyphens.push(...this.options.hyphenateWord(word));\n }\n return hyphens;\n }\n\n private async sleepIfNotClosed(sleepTimeSeconds: number) {\n if (this.closed) {\n return;\n }\n await delay(sleepTimeSeconds * 1000);\n }\n\n async close(): Promise<void> {\n if (this.closed) {\n return;\n }\n this.closedFuture.resolve();\n this.startFuture.resolve(); // avoid deadlock of mainTaskImpl in case it never started\n this.textData.sentenceStream.close();\n await this.captureTask;\n }\n}\n\nexport interface TranscriptionSynchronizerOptions {\n speed: number;\n hyphenateWord: (word: string) => string[];\n splitWords: (words: string) => [string, number, number][];\n sentenceTokenizer: SentenceTokenizer;\n}\n\nexport const defaultTextSyncOptions: TranscriptionSynchronizerOptions = {\n speed: 1,\n hyphenateWord: basic.hyphenateWord,\n splitWords: basic.splitWords,\n sentenceTokenizer: new basic.SentenceTokenizer({\n retainFormat: true,\n }),\n};\n\nexport class TranscriptionSynchronizer {\n readonly audioOutput: SyncedAudioOutput;\n readonly textOutput: SyncedTextOutput;\n\n private options: TextSyncOptions;\n private rotateSegmentTask: Task<void>;\n private _enabled: boolean = true;\n private closed: boolean = false;\n\n /** @internal */\n _impl: SegmentSynchronizerImpl;\n\n private logger = log();\n\n constructor(\n nextInChainAudio: AudioOutput,\n nextInChainText: TextOutput,\n options: TranscriptionSynchronizerOptions = defaultTextSyncOptions,\n ) {\n this.audioOutput = new SyncedAudioOutput(this, nextInChainAudio);\n this.textOutput = new SyncedTextOutput(this, nextInChainText);\n this.options = {\n speed: options.speed,\n hyphenateWord: options.hyphenateWord,\n splitWords: options.splitWords,\n sentenceTokenizer: options.sentenceTokenizer,\n };\n\n // initial segment/first segment, recreated for each new segment\n this._impl = new SegmentSynchronizerImpl(this.options, nextInChainText);\n this.rotateSegmentTask = Task.from((controller) =>\n this.rotateSegmentTaskImpl(controller.signal),\n );\n }\n\n get enabled(): boolean {\n return this._enabled;\n }\n\n set enabled(enabled: boolean) {\n if (this._enabled === enabled) {\n return;\n }\n\n this._enabled = enabled;\n this.rotateSegment();\n }\n\n rotateSegment() {\n if (this.closed) {\n return;\n }\n\n if (!this.rotateSegmentTask.done) {\n this.logger.warn('rotateSegment called while previous segment is still being rotated');\n }\n this.rotateSegmentTask = Task.from((controller) =>\n this.rotateSegmentTaskImpl(controller.signal, this.rotateSegmentTask),\n );\n }\n\n async close(): Promise<void> {\n this.closed = true;\n await this.rotateSegmentTask.cancelAndWait();\n await this._impl.close();\n }\n\n async barrier(): Promise<void> {\n if (this.rotateSegmentTask.done) {\n return;\n }\n await this.rotateSegmentTask.result;\n }\n\n private async rotateSegmentTaskImpl(abort: AbortSignal, oldTask?: Task<void>) {\n if (oldTask) {\n await oldTask.result;\n }\n\n if (abort.aborted) {\n return;\n }\n\n await this._impl.close();\n this._impl = new SegmentSynchronizerImpl(this.options, this.textOutput.nextInChain);\n }\n}\n\nclass SyncedAudioOutput extends AudioOutput {\n private pushedDuration: number = 0.0;\n\n constructor(\n public synchronizer: TranscriptionSynchronizer,\n private nextInChainAudio: AudioOutput,\n ) {\n super(nextInChainAudio.sampleRate, nextInChainAudio, { pause: true });\n }\n\n async captureFrame(frame: AudioFrame): Promise<void> {\n // using barrier() on capture should be sufficient, flush() must not be called if\n // capture_frame isn't completed\n await this.synchronizer.barrier();\n\n await super.captureFrame(frame);\n await this.nextInChainAudio.captureFrame(frame); // passthrough audio\n\n // TODO(AJS-102): use frame.durationMs once available in rtc-node\n this.pushedDuration += frame.samplesPerChannel / frame.sampleRate;\n\n if (!this.synchronizer.enabled) {\n return;\n }\n\n if (this.synchronizer._impl.audioInputEnded) {\n this.logger.warn(\n 'SegmentSynchronizerImpl audio marked as ended in capture audio, rotating segment',\n );\n this.synchronizer.rotateSegment();\n await this.synchronizer.barrier();\n }\n this.synchronizer._impl.pushAudio(frame);\n }\n\n flush() {\n super.flush();\n this.nextInChainAudio.flush();\n\n if (!this.synchronizer.enabled) {\n return;\n }\n\n if (!this.pushedDuration) {\n // For timed texts, audio goes directly to room without going through synchronizer.\n // If text was pushed but no audio, still end audio input so text can be processed.\n // Only rotate if there's also no text (truly empty segment).\n if (this.synchronizer._impl.hasPendingText) {\n // Text is pending - end audio input to allow text processing\n this.synchronizer._impl.endAudioInput();\n return;\n }\n // No text and no audio - rotate the segment\n this.synchronizer.rotateSegment();\n return;\n }\n\n this.synchronizer._impl.endAudioInput();\n }\n\n clearBuffer() {\n this.nextInChainAudio.clearBuffer();\n }\n\n // this is going to be automatically called by the next_in_chain\n onPlaybackFinished(ev: PlaybackFinishedEvent) {\n if (!this.synchronizer.enabled) {\n super.onPlaybackFinished(ev);\n return;\n }\n\n this.synchronizer._impl.markPlaybackFinished(ev.playbackPosition, ev.interrupted);\n super.onPlaybackFinished({\n playbackPosition: ev.playbackPosition,\n interrupted: ev.interrupted,\n synchronizedTranscript: this.synchronizer._impl.synchronizedTranscript,\n });\n\n this.synchronizer.rotateSegment();\n this.pushedDuration = 0.0;\n }\n}\n\nclass SyncedTextOutput extends TextOutput {\n private capturing: boolean = false;\n private logger = log();\n\n constructor(\n private readonly synchronizer: TranscriptionSynchronizer,\n public readonly nextInChain: TextOutput,\n ) {\n super(nextInChain);\n }\n\n async captureText(text: string | TimedString): Promise<void> {\n await this.synchronizer.barrier();\n\n const textStr = isTimedString(text) ? text.text : text;\n\n if (!this.synchronizer.enabled) {\n // pass through to the next in chain (extract string from TimedString if needed)\n await this.nextInChain.captureText(textStr);\n return;\n }\n\n this.capturing = true;\n if (this.synchronizer._impl.textInputEnded) {\n this.logger.warn(\n 'SegmentSynchronizerImpl text marked as ended in capture text, rotating segment',\n );\n this.synchronizer.rotateSegment();\n await this.synchronizer.barrier();\n }\n // Pass the TimedString to pushText for timing extraction\n this.synchronizer._impl.pushText(text);\n }\n\n async flush() {\n // Wait for any pending rotation to complete before accessing _impl\n await this.synchronizer.barrier();\n\n if (!this.synchronizer.enabled) {\n this.nextInChain.flush(); // passthrough text if the synchronizer is disabled\n return;\n }\n\n if (!this.capturing) {\n return;\n }\n\n this.capturing = false;\n this.synchronizer._impl.endTextInput();\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -2,13 +2,41 @@
|
|
|
2
2
|
import type { AudioFrame } from '@livekit/rtc-node';
|
|
3
3
|
import type { ReadableStream } from 'node:stream/web';
|
|
4
4
|
import type { SentenceTokenizer } from '../../tokenize/index.js';
|
|
5
|
-
import { AudioOutput, type PlaybackFinishedEvent, TextOutput } from '../io.js';
|
|
5
|
+
import { AudioOutput, type PlaybackFinishedEvent, TextOutput, type TimedString } from '../io.js';
|
|
6
6
|
interface TextSyncOptions {
|
|
7
7
|
speed: number;
|
|
8
8
|
hyphenateWord: (word: string) => string[];
|
|
9
9
|
splitWords: (words: string) => [string, number, number][];
|
|
10
10
|
sentenceTokenizer: SentenceTokenizer;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Tracks speaking rate data from TTS timing annotations.
|
|
14
|
+
* @internal Exported for testing purposes.
|
|
15
|
+
*/
|
|
16
|
+
export declare class SpeakingRateData {
|
|
17
|
+
/** Timestamps of the speaking rate. */
|
|
18
|
+
timestamps: number[];
|
|
19
|
+
/** Speed at the timestamp. */
|
|
20
|
+
speakingRate: number[];
|
|
21
|
+
/** Accumulated speaking units up to the timestamp. */
|
|
22
|
+
speakIntegrals: number[];
|
|
23
|
+
/** Buffer for text without timing annotations yet. */
|
|
24
|
+
private textBuffer;
|
|
25
|
+
/**
|
|
26
|
+
* Add by speaking rate estimation.
|
|
27
|
+
*/
|
|
28
|
+
addByRate(timestamp: number, speakingRate: number): void;
|
|
29
|
+
/**
|
|
30
|
+
* Add annotation from TimedString with start_time/end_time.
|
|
31
|
+
*/
|
|
32
|
+
addByAnnotation(text: string, startTime: number | undefined, endTime: number | undefined): void;
|
|
33
|
+
/**
|
|
34
|
+
* Get accumulated speaking units up to the given timestamp.
|
|
35
|
+
*/
|
|
36
|
+
accumulateTo(timestamp: number): number;
|
|
37
|
+
/** Get the last pushed timestamp. */
|
|
38
|
+
get pushedDuration(): number;
|
|
39
|
+
}
|
|
12
40
|
declare class SegmentSynchronizerImpl {
|
|
13
41
|
private readonly options;
|
|
14
42
|
private readonly nextInChain;
|
|
@@ -27,15 +55,17 @@ declare class SegmentSynchronizerImpl {
|
|
|
27
55
|
get closed(): boolean;
|
|
28
56
|
get audioInputEnded(): boolean;
|
|
29
57
|
get textInputEnded(): boolean;
|
|
58
|
+
get hasPendingText(): boolean;
|
|
30
59
|
get readable(): ReadableStream<string>;
|
|
31
60
|
pushAudio(frame: AudioFrame): void;
|
|
32
61
|
endAudioInput(): void;
|
|
33
|
-
pushText(text: string): void;
|
|
62
|
+
pushText(text: string | TimedString): void;
|
|
34
63
|
endTextInput(): void;
|
|
35
64
|
markPlaybackFinished(_playbackPosition: number, interrupted: boolean): void;
|
|
36
65
|
get synchronizedTranscript(): string;
|
|
37
66
|
private captureTaskImpl;
|
|
38
67
|
private mainTask;
|
|
68
|
+
private calcHyphens;
|
|
39
69
|
private sleepIfNotClosed;
|
|
40
70
|
close(): Promise<void>;
|
|
41
71
|
}
|
|
@@ -80,8 +110,8 @@ declare class SyncedTextOutput extends TextOutput {
|
|
|
80
110
|
private capturing;
|
|
81
111
|
private logger;
|
|
82
112
|
constructor(synchronizer: TranscriptionSynchronizer, nextInChain: TextOutput);
|
|
83
|
-
captureText(text: string): Promise<void>;
|
|
84
|
-
flush(): void
|
|
113
|
+
captureText(text: string | TimedString): Promise<void>;
|
|
114
|
+
flush(): Promise<void>;
|
|
85
115
|
}
|
|
86
116
|
export {};
|
|
87
117
|
//# sourceMappingURL=synchronizer.d.ts.map
|
|
@@ -2,13 +2,41 @@
|
|
|
2
2
|
import type { AudioFrame } from '@livekit/rtc-node';
|
|
3
3
|
import type { ReadableStream } from 'node:stream/web';
|
|
4
4
|
import type { SentenceTokenizer } from '../../tokenize/index.js';
|
|
5
|
-
import { AudioOutput, type PlaybackFinishedEvent, TextOutput } from '../io.js';
|
|
5
|
+
import { AudioOutput, type PlaybackFinishedEvent, TextOutput, type TimedString } from '../io.js';
|
|
6
6
|
interface TextSyncOptions {
|
|
7
7
|
speed: number;
|
|
8
8
|
hyphenateWord: (word: string) => string[];
|
|
9
9
|
splitWords: (words: string) => [string, number, number][];
|
|
10
10
|
sentenceTokenizer: SentenceTokenizer;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Tracks speaking rate data from TTS timing annotations.
|
|
14
|
+
* @internal Exported for testing purposes.
|
|
15
|
+
*/
|
|
16
|
+
export declare class SpeakingRateData {
|
|
17
|
+
/** Timestamps of the speaking rate. */
|
|
18
|
+
timestamps: number[];
|
|
19
|
+
/** Speed at the timestamp. */
|
|
20
|
+
speakingRate: number[];
|
|
21
|
+
/** Accumulated speaking units up to the timestamp. */
|
|
22
|
+
speakIntegrals: number[];
|
|
23
|
+
/** Buffer for text without timing annotations yet. */
|
|
24
|
+
private textBuffer;
|
|
25
|
+
/**
|
|
26
|
+
* Add by speaking rate estimation.
|
|
27
|
+
*/
|
|
28
|
+
addByRate(timestamp: number, speakingRate: number): void;
|
|
29
|
+
/**
|
|
30
|
+
* Add annotation from TimedString with start_time/end_time.
|
|
31
|
+
*/
|
|
32
|
+
addByAnnotation(text: string, startTime: number | undefined, endTime: number | undefined): void;
|
|
33
|
+
/**
|
|
34
|
+
* Get accumulated speaking units up to the given timestamp.
|
|
35
|
+
*/
|
|
36
|
+
accumulateTo(timestamp: number): number;
|
|
37
|
+
/** Get the last pushed timestamp. */
|
|
38
|
+
get pushedDuration(): number;
|
|
39
|
+
}
|
|
12
40
|
declare class SegmentSynchronizerImpl {
|
|
13
41
|
private readonly options;
|
|
14
42
|
private readonly nextInChain;
|
|
@@ -27,15 +55,17 @@ declare class SegmentSynchronizerImpl {
|
|
|
27
55
|
get closed(): boolean;
|
|
28
56
|
get audioInputEnded(): boolean;
|
|
29
57
|
get textInputEnded(): boolean;
|
|
58
|
+
get hasPendingText(): boolean;
|
|
30
59
|
get readable(): ReadableStream<string>;
|
|
31
60
|
pushAudio(frame: AudioFrame): void;
|
|
32
61
|
endAudioInput(): void;
|
|
33
|
-
pushText(text: string): void;
|
|
62
|
+
pushText(text: string | TimedString): void;
|
|
34
63
|
endTextInput(): void;
|
|
35
64
|
markPlaybackFinished(_playbackPosition: number, interrupted: boolean): void;
|
|
36
65
|
get synchronizedTranscript(): string;
|
|
37
66
|
private captureTaskImpl;
|
|
38
67
|
private mainTask;
|
|
68
|
+
private calcHyphens;
|
|
39
69
|
private sleepIfNotClosed;
|
|
40
70
|
close(): Promise<void>;
|
|
41
71
|
}
|
|
@@ -80,8 +110,8 @@ declare class SyncedTextOutput extends TextOutput {
|
|
|
80
110
|
private capturing;
|
|
81
111
|
private logger;
|
|
82
112
|
constructor(synchronizer: TranscriptionSynchronizer, nextInChain: TextOutput);
|
|
83
|
-
captureText(text: string): Promise<void>;
|
|
84
|
-
flush(): void
|
|
113
|
+
captureText(text: string | TimedString): Promise<void>;
|
|
114
|
+
flush(): Promise<void>;
|
|
85
115
|
}
|
|
86
116
|
export {};
|
|
87
117
|
//# sourceMappingURL=synchronizer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"synchronizer.d.ts","sourceRoot":"","sources":["../../../src/voice/transcription/synchronizer.ts"],"names":[],"mappings":";AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAA+B,MAAM,iBAAiB,CAAC;AAGnF,OAAO,KAAK,EAAkB,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAGjF,OAAO,
|
|
1
|
+
{"version":3,"file":"synchronizer.d.ts","sourceRoot":"","sources":["../../../src/voice/transcription/synchronizer.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,8 +2,84 @@ import { log } from "../../log.js";
|
|
|
2
2
|
import { IdentityTransform } from "../../stream/identity_transform.js";
|
|
3
3
|
import { basic } from "../../tokenize/index.js";
|
|
4
4
|
import { Future, Task, delay } from "../../utils.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
AudioOutput,
|
|
7
|
+
TextOutput,
|
|
8
|
+
isTimedString
|
|
9
|
+
} from "../io.js";
|
|
6
10
|
const STANDARD_SPEECH_RATE = 3.83;
|
|
11
|
+
class SpeakingRateData {
|
|
12
|
+
/** Timestamps of the speaking rate. */
|
|
13
|
+
timestamps = [];
|
|
14
|
+
/** Speed at the timestamp. */
|
|
15
|
+
speakingRate = [];
|
|
16
|
+
/** Accumulated speaking units up to the timestamp. */
|
|
17
|
+
speakIntegrals = [];
|
|
18
|
+
/** Buffer for text without timing annotations yet. */
|
|
19
|
+
textBuffer = [];
|
|
20
|
+
/**
|
|
21
|
+
* Add by speaking rate estimation.
|
|
22
|
+
*/
|
|
23
|
+
addByRate(timestamp, speakingRate) {
|
|
24
|
+
const integral = this.speakIntegrals.length > 0 ? this.speakIntegrals[this.speakIntegrals.length - 1] : 0;
|
|
25
|
+
const dt = timestamp - this.pushedDuration;
|
|
26
|
+
const newIntegral = integral + speakingRate * dt;
|
|
27
|
+
this.timestamps.push(timestamp);
|
|
28
|
+
this.speakingRate.push(speakingRate);
|
|
29
|
+
this.speakIntegrals.push(newIntegral);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Add annotation from TimedString with start_time/end_time.
|
|
33
|
+
*/
|
|
34
|
+
addByAnnotation(text, startTime, endTime) {
|
|
35
|
+
if (startTime !== void 0) {
|
|
36
|
+
const integral = this.speakIntegrals.length > 0 ? this.speakIntegrals[this.speakIntegrals.length - 1] : 0;
|
|
37
|
+
const dt = startTime - this.pushedDuration;
|
|
38
|
+
const textLen = this.textBuffer.reduce((sum, t) => sum + t.length, 0);
|
|
39
|
+
const newIntegral = integral + textLen;
|
|
40
|
+
const rate = dt > 0 ? textLen / dt : 0;
|
|
41
|
+
this.timestamps.push(startTime);
|
|
42
|
+
this.speakingRate.push(rate);
|
|
43
|
+
this.speakIntegrals.push(newIntegral);
|
|
44
|
+
this.textBuffer = [];
|
|
45
|
+
}
|
|
46
|
+
this.textBuffer.push(text);
|
|
47
|
+
if (endTime !== void 0) {
|
|
48
|
+
this.addByAnnotation("", endTime, void 0);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get accumulated speaking units up to the given timestamp.
|
|
53
|
+
*/
|
|
54
|
+
accumulateTo(timestamp) {
|
|
55
|
+
if (this.timestamps.length === 0) {
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
let idx = 0;
|
|
59
|
+
for (let i = 0; i < this.timestamps.length; i++) {
|
|
60
|
+
if (this.timestamps[i] <= timestamp) {
|
|
61
|
+
idx = i + 1;
|
|
62
|
+
} else {
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (idx === 0) {
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
let integralT = this.speakIntegrals[idx - 1];
|
|
70
|
+
const dt = timestamp - this.timestamps[idx - 1];
|
|
71
|
+
const rate = idx < this.speakingRate.length ? this.speakingRate[idx] : this.speakingRate[idx - 1];
|
|
72
|
+
integralT += rate * dt;
|
|
73
|
+
if (idx < this.timestamps.length) {
|
|
74
|
+
integralT = Math.min(integralT, this.speakIntegrals[idx]);
|
|
75
|
+
}
|
|
76
|
+
return integralT;
|
|
77
|
+
}
|
|
78
|
+
/** Get the last pushed timestamp. */
|
|
79
|
+
get pushedDuration() {
|
|
80
|
+
return this.timestamps.length > 0 ? this.timestamps[this.timestamps.length - 1] : 0;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
7
83
|
class SegmentSynchronizerImpl {
|
|
8
84
|
constructor(options, nextInChain) {
|
|
9
85
|
this.options = options;
|
|
@@ -18,7 +94,8 @@ class SegmentSynchronizerImpl {
|
|
|
18
94
|
};
|
|
19
95
|
this.audioData = {
|
|
20
96
|
pushedDuration: 0,
|
|
21
|
-
done: false
|
|
97
|
+
done: false,
|
|
98
|
+
annotatedRate: null
|
|
22
99
|
};
|
|
23
100
|
this.outputStream = new IdentityTransform();
|
|
24
101
|
this.outputStreamWriter = this.outputStream.writable.getWriter();
|
|
@@ -49,6 +126,9 @@ class SegmentSynchronizerImpl {
|
|
|
49
126
|
get textInputEnded() {
|
|
50
127
|
return this.textData.done;
|
|
51
128
|
}
|
|
129
|
+
get hasPendingText() {
|
|
130
|
+
return this.textData.pushedText.length > this.textData.forwardedText.length;
|
|
131
|
+
}
|
|
52
132
|
get readable() {
|
|
53
133
|
return this.outputStream.readable;
|
|
54
134
|
}
|
|
@@ -76,8 +156,22 @@ class SegmentSynchronizerImpl {
|
|
|
76
156
|
this.logger.warn("SegmentSynchronizerImpl.pushText called after close");
|
|
77
157
|
return;
|
|
78
158
|
}
|
|
79
|
-
|
|
80
|
-
|
|
159
|
+
let textStr;
|
|
160
|
+
let startTime;
|
|
161
|
+
let endTime;
|
|
162
|
+
if (isTimedString(text)) {
|
|
163
|
+
textStr = text.text;
|
|
164
|
+
startTime = text.startTime;
|
|
165
|
+
endTime = text.endTime;
|
|
166
|
+
if (!this.audioData.annotatedRate) {
|
|
167
|
+
this.audioData.annotatedRate = new SpeakingRateData();
|
|
168
|
+
}
|
|
169
|
+
this.audioData.annotatedRate.addByAnnotation(textStr, startTime, endTime);
|
|
170
|
+
} else {
|
|
171
|
+
textStr = text;
|
|
172
|
+
}
|
|
173
|
+
this.textData.sentenceStream.pushText(textStr);
|
|
174
|
+
this.textData.pushedText += textStr;
|
|
81
175
|
}
|
|
82
176
|
endTextInput() {
|
|
83
177
|
if (this.closed) {
|
|
@@ -97,6 +191,9 @@ class SegmentSynchronizerImpl {
|
|
|
97
191
|
{ textDone: this.textData.done, audioDone: this.audioData.done },
|
|
98
192
|
"SegmentSynchronizerImpl.markPlaybackFinished called before text/audio input is done"
|
|
99
193
|
);
|
|
194
|
+
if (!interrupted) {
|
|
195
|
+
this.playbackCompleted = true;
|
|
196
|
+
}
|
|
100
197
|
return;
|
|
101
198
|
}
|
|
102
199
|
if (!interrupted) {
|
|
@@ -116,7 +213,6 @@ class SegmentSynchronizerImpl {
|
|
|
116
213
|
if (done) {
|
|
117
214
|
break;
|
|
118
215
|
}
|
|
119
|
-
this.textData.forwardedText += text;
|
|
120
216
|
await this.nextInChain.captureText(text);
|
|
121
217
|
}
|
|
122
218
|
reader.releaseLock();
|
|
@@ -147,16 +243,32 @@ class SegmentSynchronizerImpl {
|
|
|
147
243
|
}
|
|
148
244
|
const wordHphens = this.options.hyphenateWord(word).length;
|
|
149
245
|
const elapsedSeconds = (Date.now() - this.startWallTime) / 1e3;
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
246
|
+
let dHyphens = 0;
|
|
247
|
+
const annotated = this.audioData.annotatedRate;
|
|
248
|
+
if (annotated && annotated.pushedDuration >= elapsedSeconds) {
|
|
249
|
+
const targetLen = Math.floor(annotated.accumulateTo(elapsedSeconds));
|
|
250
|
+
const forwardedLen = this.textData.forwardedText.length;
|
|
251
|
+
if (targetLen >= forwardedLen) {
|
|
252
|
+
const dText = this.textData.pushedText.slice(forwardedLen, targetLen);
|
|
253
|
+
dHyphens = this.calcHyphens(dText).length;
|
|
254
|
+
} else {
|
|
255
|
+
const dText = this.textData.pushedText.slice(targetLen, forwardedLen);
|
|
256
|
+
dHyphens = -this.calcHyphens(dText).length;
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
const targetHyphens = elapsedSeconds * this.options.speed;
|
|
260
|
+
dHyphens = Math.max(0, targetHyphens - this.textData.forwardedHyphens);
|
|
261
|
+
}
|
|
262
|
+
let delayTime = Math.max(0, wordHphens - dHyphens) / this.speed;
|
|
153
263
|
if (this.playbackCompleted) {
|
|
154
|
-
|
|
264
|
+
delayTime = 0;
|
|
155
265
|
}
|
|
156
|
-
await this.sleepIfNotClosed(
|
|
157
|
-
|
|
158
|
-
|
|
266
|
+
await this.sleepIfNotClosed(delayTime / 2);
|
|
267
|
+
const forwardedWord = sentence.slice(textCursor, endPos);
|
|
268
|
+
this.outputStreamWriter.write(forwardedWord);
|
|
269
|
+
await this.sleepIfNotClosed(delayTime / 2);
|
|
159
270
|
this.textData.forwardedHyphens += wordHphens;
|
|
271
|
+
this.textData.forwardedText += forwardedWord;
|
|
160
272
|
textCursor = endPos;
|
|
161
273
|
}
|
|
162
274
|
if (textCursor < sentence.length) {
|
|
@@ -165,6 +277,14 @@ class SegmentSynchronizerImpl {
|
|
|
165
277
|
}
|
|
166
278
|
}
|
|
167
279
|
}
|
|
280
|
+
calcHyphens(text) {
|
|
281
|
+
const words = this.options.splitWords(text);
|
|
282
|
+
const hyphens = [];
|
|
283
|
+
for (const [word] of words) {
|
|
284
|
+
hyphens.push(...this.options.hyphenateWord(word));
|
|
285
|
+
}
|
|
286
|
+
return hyphens;
|
|
287
|
+
}
|
|
168
288
|
async sleepIfNotClosed(sleepTimeSeconds) {
|
|
169
289
|
if (this.closed) {
|
|
170
290
|
return;
|
|
@@ -287,6 +407,10 @@ class SyncedAudioOutput extends AudioOutput {
|
|
|
287
407
|
return;
|
|
288
408
|
}
|
|
289
409
|
if (!this.pushedDuration) {
|
|
410
|
+
if (this.synchronizer._impl.hasPendingText) {
|
|
411
|
+
this.synchronizer._impl.endAudioInput();
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
290
414
|
this.synchronizer.rotateSegment();
|
|
291
415
|
return;
|
|
292
416
|
}
|
|
@@ -321,8 +445,9 @@ class SyncedTextOutput extends TextOutput {
|
|
|
321
445
|
logger = log();
|
|
322
446
|
async captureText(text) {
|
|
323
447
|
await this.synchronizer.barrier();
|
|
448
|
+
const textStr = isTimedString(text) ? text.text : text;
|
|
324
449
|
if (!this.synchronizer.enabled) {
|
|
325
|
-
await this.nextInChain.captureText(
|
|
450
|
+
await this.nextInChain.captureText(textStr);
|
|
326
451
|
return;
|
|
327
452
|
}
|
|
328
453
|
this.capturing = true;
|
|
@@ -335,7 +460,8 @@ class SyncedTextOutput extends TextOutput {
|
|
|
335
460
|
}
|
|
336
461
|
this.synchronizer._impl.pushText(text);
|
|
337
462
|
}
|
|
338
|
-
flush() {
|
|
463
|
+
async flush() {
|
|
464
|
+
await this.synchronizer.barrier();
|
|
339
465
|
if (!this.synchronizer.enabled) {
|
|
340
466
|
this.nextInChain.flush();
|
|
341
467
|
return;
|
|
@@ -348,6 +474,7 @@ class SyncedTextOutput extends TextOutput {
|
|
|
348
474
|
}
|
|
349
475
|
}
|
|
350
476
|
export {
|
|
477
|
+
SpeakingRateData,
|
|
351
478
|
TranscriptionSynchronizer,
|
|
352
479
|
defaultTextSyncOptions
|
|
353
480
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/voice/transcription/synchronizer.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport type { ReadableStream, WritableStreamDefaultWriter } from 'node:stream/web';\nimport { log } from '../../log.js';\nimport { IdentityTransform } from '../../stream/identity_transform.js';\nimport type { SentenceStream, SentenceTokenizer } from '../../tokenize/index.js';\nimport { basic } from '../../tokenize/index.js';\nimport { Future, Task, delay } from '../../utils.js';\nimport { AudioOutput, type PlaybackFinishedEvent, TextOutput } from '../io.js';\n\nconst STANDARD_SPEECH_RATE = 3.83; // hyphens (syllables) per second\n\ninterface TextSyncOptions {\n speed: number;\n hyphenateWord: (word: string) => string[];\n splitWords: (words: string) => [string, number, number][];\n sentenceTokenizer: SentenceTokenizer;\n}\n\ninterface TextData {\n sentenceStream: SentenceStream;\n pushedText: string;\n done: boolean;\n forwardedHyphens: number;\n forwardedText: string;\n}\n\ninterface AudioData {\n pushedDuration: number;\n done: boolean;\n}\n\nclass SegmentSynchronizerImpl {\n private textData: TextData;\n private audioData: AudioData;\n private speed: number;\n private outputStream: IdentityTransform<string>;\n private outputStreamWriter: WritableStreamDefaultWriter<string>;\n private captureTask: Promise<void>;\n private startWallTime?: number;\n\n private startFuture: Future = new Future();\n private closedFuture: Future = new Future();\n private playbackCompleted: boolean = false;\n\n private logger = log();\n\n constructor(\n private readonly options: TextSyncOptions,\n private readonly nextInChain: TextOutput,\n ) {\n this.speed = options.speed * STANDARD_SPEECH_RATE; // hyphens per second\n this.textData = {\n sentenceStream: options.sentenceTokenizer.stream(),\n pushedText: '',\n done: false,\n forwardedHyphens: 0,\n forwardedText: '',\n };\n this.audioData = {\n pushedDuration: 0,\n done: false,\n };\n this.outputStream = new IdentityTransform();\n this.outputStreamWriter = this.outputStream.writable.getWriter();\n\n this.mainTask()\n .then(() => {\n this.outputStreamWriter.close();\n })\n .catch((error) => {\n this.logger.error({ error }, 'mainTask SegmentSynchronizerImpl');\n });\n this.captureTask = this.captureTaskImpl();\n }\n\n get closed() {\n return this.closedFuture.done;\n }\n\n get audioInputEnded() {\n return this.audioData.done;\n }\n\n get textInputEnded() {\n return this.textData.done;\n }\n\n get readable(): ReadableStream<string> {\n return this.outputStream.readable;\n }\n\n pushAudio(frame: AudioFrame) {\n if (this.closed) {\n this.logger.warn('SegmentSynchronizerImpl.pushAudio called after close');\n return;\n }\n // TODO(AJS-102): use frame.durationMs once available in rtc-node\n const frameDuration = frame.samplesPerChannel / frame.sampleRate;\n\n if (!this.startWallTime && frameDuration > 0) {\n this.startWallTime = Date.now();\n this.startFuture.resolve();\n }\n\n this.audioData.pushedDuration += frameDuration;\n }\n\n endAudioInput() {\n if (this.closed) {\n this.logger.warn('SegmentSynchronizerImpl.endAudioInput called after close');\n return;\n }\n\n this.audioData.done = true;\n }\n\n pushText(text: string) {\n if (this.closed) {\n this.logger.warn('SegmentSynchronizerImpl.pushText called after close');\n return;\n }\n\n this.textData.sentenceStream.pushText(text);\n this.textData.pushedText += text;\n }\n\n endTextInput() {\n if (this.closed) {\n this.logger.warn('SegmentSynchronizerImpl.endTextInput called after close');\n return;\n }\n\n this.textData.done = true;\n this.textData.sentenceStream.endInput();\n }\n\n markPlaybackFinished(_playbackPosition: number, interrupted: boolean) {\n if (this.closed) {\n this.logger.warn('SegmentSynchronizerImpl.markPlaybackFinished called after close');\n return;\n }\n\n if (!this.textData.done || !this.audioData.done) {\n this.logger.warn(\n { textDone: this.textData.done, audioDone: this.audioData.done },\n 'SegmentSynchronizerImpl.markPlaybackFinished called before text/audio input is done',\n );\n return;\n }\n\n if (!interrupted) {\n this.playbackCompleted = true;\n }\n }\n\n get synchronizedTranscript(): string {\n if (this.playbackCompleted) {\n return this.textData.pushedText;\n }\n return this.textData.forwardedText;\n }\n\n private async captureTaskImpl() {\n // Don't use a for-await loop here, because exiting the loop will close the writer in the\n // outputStream, which will cause an error in the mainTask.then method.\n const reader = this.outputStream.readable.getReader();\n while (true) {\n const { done, value: text } = await reader.read();\n if (done) {\n break;\n }\n this.textData.forwardedText += text;\n await this.nextInChain.captureText(text);\n }\n reader.releaseLock();\n this.nextInChain.flush();\n }\n\n private async mainTask(): Promise<void> {\n await this.startFuture.await;\n\n if (this.closed && !this.playbackCompleted) {\n return;\n }\n\n if (!this.startWallTime) {\n throw new Error('startWallTime is not set when starting SegmentSynchronizerImpl.mainTask');\n }\n\n for await (const textSegment of this.textData.sentenceStream) {\n const sentence = textSegment.token;\n\n let textCursor = 0;\n if (this.closed && !this.playbackCompleted) {\n return;\n }\n\n for (const [word, _, endPos] of this.options.splitWords(sentence)) {\n if (this.closed && !this.playbackCompleted) {\n return;\n }\n\n if (this.playbackCompleted) {\n this.outputStreamWriter.write(sentence.slice(textCursor, endPos));\n textCursor = endPos;\n continue;\n }\n\n const wordHphens = this.options.hyphenateWord(word).length;\n const elapsedSeconds = (Date.now() - this.startWallTime) / 1000;\n const targetHyphens = elapsedSeconds * this.options.speed;\n const hyphensBehind = Math.max(0, targetHyphens - this.textData.forwardedHyphens);\n let delay = Math.max(0, wordHphens - hyphensBehind) / this.speed;\n\n if (this.playbackCompleted) {\n delay = 0;\n }\n\n await this.sleepIfNotClosed(delay / 2);\n this.outputStreamWriter.write(sentence.slice(textCursor, endPos));\n await this.sleepIfNotClosed(delay / 2);\n\n this.textData.forwardedHyphens += wordHphens;\n textCursor = endPos;\n }\n\n if (textCursor < sentence.length) {\n const remaining = sentence.slice(textCursor);\n this.outputStreamWriter.write(remaining);\n }\n }\n }\n\n private async sleepIfNotClosed(sleepTimeSeconds: number) {\n if (this.closed) {\n return;\n }\n await delay(sleepTimeSeconds * 1000);\n }\n\n async close(): Promise<void> {\n if (this.closed) {\n return;\n }\n this.closedFuture.resolve();\n this.startFuture.resolve(); // avoid deadlock of mainTaskImpl in case it never started\n this.textData.sentenceStream.close();\n await this.captureTask;\n }\n}\n\nexport interface TranscriptionSynchronizerOptions {\n speed: number;\n hyphenateWord: (word: string) => string[];\n splitWords: (words: string) => [string, number, number][];\n sentenceTokenizer: SentenceTokenizer;\n}\n\nexport const defaultTextSyncOptions: TranscriptionSynchronizerOptions = {\n speed: 1,\n hyphenateWord: basic.hyphenateWord,\n splitWords: basic.splitWords,\n sentenceTokenizer: new basic.SentenceTokenizer({\n retainFormat: true,\n }),\n};\n\nexport class TranscriptionSynchronizer {\n readonly audioOutput: SyncedAudioOutput;\n readonly textOutput: SyncedTextOutput;\n\n private options: TextSyncOptions;\n private rotateSegmentTask: Task<void>;\n private _enabled: boolean = true;\n private closed: boolean = false;\n\n /** @internal */\n _impl: SegmentSynchronizerImpl;\n\n private logger = log();\n\n constructor(\n nextInChainAudio: AudioOutput,\n nextInChainText: TextOutput,\n options: TranscriptionSynchronizerOptions = defaultTextSyncOptions,\n ) {\n this.audioOutput = new SyncedAudioOutput(this, nextInChainAudio);\n this.textOutput = new SyncedTextOutput(this, nextInChainText);\n this.options = {\n speed: options.speed,\n hyphenateWord: options.hyphenateWord,\n splitWords: options.splitWords,\n sentenceTokenizer: options.sentenceTokenizer,\n };\n\n // initial segment/first segment, recreated for each new segment\n this._impl = new SegmentSynchronizerImpl(this.options, nextInChainText);\n this.rotateSegmentTask = Task.from((controller) =>\n this.rotateSegmentTaskImpl(controller.signal),\n );\n }\n\n get enabled(): boolean {\n return this._enabled;\n }\n\n set enabled(enabled: boolean) {\n if (this._enabled === enabled) {\n return;\n }\n\n this._enabled = enabled;\n this.rotateSegment();\n }\n\n rotateSegment() {\n if (this.closed) {\n return;\n }\n\n if (!this.rotateSegmentTask.done) {\n this.logger.warn('rotateSegment called while previous segment is still being rotated');\n }\n this.rotateSegmentTask = Task.from((controller) =>\n this.rotateSegmentTaskImpl(controller.signal, this.rotateSegmentTask),\n );\n }\n\n async close(): Promise<void> {\n this.closed = true;\n await this.rotateSegmentTask.cancelAndWait();\n await this._impl.close();\n }\n\n async barrier(): Promise<void> {\n if (this.rotateSegmentTask.done) {\n return;\n }\n await this.rotateSegmentTask.result;\n }\n\n private async rotateSegmentTaskImpl(abort: AbortSignal, oldTask?: Task<void>) {\n if (oldTask) {\n await oldTask.result;\n }\n\n if (abort.aborted) {\n return;\n }\n await this._impl.close();\n this._impl = new SegmentSynchronizerImpl(this.options, this.textOutput.nextInChain);\n }\n}\n\nclass SyncedAudioOutput extends AudioOutput {\n private pushedDuration: number = 0.0;\n\n constructor(\n public synchronizer: TranscriptionSynchronizer,\n private nextInChainAudio: AudioOutput,\n ) {\n super(nextInChainAudio.sampleRate, nextInChainAudio, { pause: true });\n }\n\n async captureFrame(frame: AudioFrame): Promise<void> {\n // using barrier() on capture should be sufficient, flush() must not be called if\n // capture_frame isn't completed\n await this.synchronizer.barrier();\n\n await super.captureFrame(frame);\n await this.nextInChainAudio.captureFrame(frame); // passthrough audio\n\n // TODO(AJS-102): use frame.durationMs once available in rtc-node\n this.pushedDuration += frame.samplesPerChannel / frame.sampleRate;\n\n if (!this.synchronizer.enabled) {\n return;\n }\n\n if (this.synchronizer._impl.audioInputEnded) {\n this.logger.warn(\n 'SegmentSynchronizerImpl audio marked as ended in capture audio, rotating segment',\n );\n this.synchronizer.rotateSegment();\n await this.synchronizer.barrier();\n }\n this.synchronizer._impl.pushAudio(frame);\n }\n\n flush() {\n super.flush();\n this.nextInChainAudio.flush();\n\n if (!this.synchronizer.enabled) {\n return;\n }\n\n if (!this.pushedDuration) {\n // in case there is no audio after the text was pushed, rotate the segment\n this.synchronizer.rotateSegment();\n return;\n }\n\n this.synchronizer._impl.endAudioInput();\n }\n\n clearBuffer() {\n this.nextInChainAudio.clearBuffer();\n }\n\n // this is going to be automatically called by the next_in_chain\n onPlaybackFinished(ev: PlaybackFinishedEvent) {\n if (!this.synchronizer.enabled) {\n super.onPlaybackFinished(ev);\n return;\n }\n\n this.synchronizer._impl.markPlaybackFinished(ev.playbackPosition, ev.interrupted);\n super.onPlaybackFinished({\n playbackPosition: ev.playbackPosition,\n interrupted: ev.interrupted,\n synchronizedTranscript: this.synchronizer._impl.synchronizedTranscript,\n });\n\n this.synchronizer.rotateSegment();\n this.pushedDuration = 0.0;\n }\n}\n\nclass SyncedTextOutput extends TextOutput {\n private capturing: boolean = false;\n private logger = log();\n\n constructor(\n private readonly synchronizer: TranscriptionSynchronizer,\n public readonly nextInChain: TextOutput,\n ) {\n super(nextInChain);\n }\n\n async captureText(text: string): Promise<void> {\n await this.synchronizer.barrier();\n\n if (!this.synchronizer.enabled) {\n // pass through to the next in chain\n await this.nextInChain.captureText(text);\n return;\n }\n\n this.capturing = true;\n if (this.synchronizer._impl.textInputEnded) {\n this.logger.warn(\n 'SegmentSynchronizerImpl text marked as ended in capture text, rotating segment',\n );\n this.synchronizer.rotateSegment();\n await this.synchronizer.barrier();\n }\n this.synchronizer._impl.pushText(text);\n }\n\n flush() {\n if (!this.synchronizer.enabled) {\n this.nextInChain.flush(); // passthrough text if the synchronizer is disabled\n return;\n }\n\n if (!this.capturing) {\n return;\n }\n\n this.capturing = false;\n this.synchronizer._impl.endTextInput();\n }\n}\n"],"mappings":"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","names":["delay"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/voice/transcription/synchronizer.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport type { ReadableStream, WritableStreamDefaultWriter } from 'node:stream/web';\nimport { log } from '../../log.js';\nimport { IdentityTransform } from '../../stream/identity_transform.js';\nimport type { SentenceStream, SentenceTokenizer } from '../../tokenize/index.js';\nimport { basic } from '../../tokenize/index.js';\nimport { Future, Task, delay } from '../../utils.js';\nimport {\n AudioOutput,\n type PlaybackFinishedEvent,\n TextOutput,\n type TimedString,\n isTimedString,\n} from '../io.js';\n\nconst STANDARD_SPEECH_RATE = 3.83; // hyphens (syllables) per second\n\ninterface TextSyncOptions {\n speed: number;\n hyphenateWord: (word: string) => string[];\n splitWords: (words: string) => [string, number, number][];\n sentenceTokenizer: SentenceTokenizer;\n}\n\ninterface TextData {\n sentenceStream: SentenceStream;\n pushedText: string;\n done: boolean;\n forwardedHyphens: number;\n forwardedText: string;\n}\n\n/**\n * Tracks speaking rate data from TTS timing annotations.\n * @internal Exported for testing purposes.\n */\nexport class SpeakingRateData {\n /** Timestamps of the speaking rate. */\n timestamps: number[] = [];\n /** Speed at the timestamp. */\n speakingRate: number[] = [];\n /** Accumulated speaking units up to the timestamp. */\n speakIntegrals: number[] = [];\n /** Buffer for text without timing annotations yet. */\n private textBuffer: string[] = [];\n\n /**\n * Add by speaking rate estimation.\n */\n addByRate(timestamp: number, speakingRate: number): void {\n const integral =\n this.speakIntegrals.length > 0 ? this.speakIntegrals[this.speakIntegrals.length - 1]! : 0;\n const dt = timestamp - this.pushedDuration;\n const newIntegral = integral + speakingRate * dt;\n\n this.timestamps.push(timestamp);\n this.speakingRate.push(speakingRate);\n this.speakIntegrals.push(newIntegral);\n }\n\n /**\n * Add annotation from TimedString with start_time/end_time.\n */\n addByAnnotation(text: string, startTime: number | undefined, endTime: number | undefined): void {\n if (startTime !== undefined) {\n // Calculate the integral of the speaking rate up to the start time\n const integral =\n this.speakIntegrals.length > 0 ? this.speakIntegrals[this.speakIntegrals.length - 1]! : 0;\n\n const dt = startTime - this.pushedDuration;\n // Use the length of the text directly instead of hyphens\n const textLen = this.textBuffer.reduce((sum, t) => sum + t.length, 0);\n const newIntegral = integral + textLen;\n const rate = dt > 0 ? textLen / dt : 0;\n\n this.timestamps.push(startTime);\n this.speakingRate.push(rate);\n this.speakIntegrals.push(newIntegral);\n this.textBuffer = [];\n }\n\n this.textBuffer.push(text);\n\n if (endTime !== undefined) {\n this.addByAnnotation('', endTime, undefined);\n }\n }\n\n /**\n * Get accumulated speaking units up to the given timestamp.\n */\n accumulateTo(timestamp: number): number {\n if (this.timestamps.length === 0) {\n return 0;\n }\n\n // Binary search for the right position (equivalent to np.searchsorted with side=\"right\")\n let idx = 0;\n for (let i = 0; i < this.timestamps.length; i++) {\n if (this.timestamps[i]! <= timestamp) {\n idx = i + 1;\n } else {\n break;\n }\n }\n\n if (idx === 0) {\n return 0;\n }\n\n let integralT = this.speakIntegrals[idx - 1]!;\n\n // Fill the tail assuming the speaking rate is constant\n const dt = timestamp - this.timestamps[idx - 1]!;\n const rate =\n idx < this.speakingRate.length ? this.speakingRate[idx]! : this.speakingRate[idx - 1]!;\n integralT += rate * dt;\n\n // If there is a next timestamp, make sure the integral does not exceed the next\n if (idx < this.timestamps.length) {\n integralT = Math.min(integralT, this.speakIntegrals[idx]!);\n }\n\n return integralT;\n }\n\n /** Get the last pushed timestamp. */\n get pushedDuration(): number {\n return this.timestamps.length > 0 ? this.timestamps[this.timestamps.length - 1]! : 0;\n }\n}\n\ninterface AudioData {\n pushedDuration: number;\n done: boolean;\n annotatedRate: SpeakingRateData | null;\n}\n\nclass SegmentSynchronizerImpl {\n private textData: TextData;\n private audioData: AudioData;\n private speed: number;\n private outputStream: IdentityTransform<string>;\n private outputStreamWriter: WritableStreamDefaultWriter<string>;\n private captureTask: Promise<void>;\n private startWallTime?: number;\n\n private startFuture: Future = new Future();\n private closedFuture: Future = new Future();\n private playbackCompleted: boolean = false;\n\n private logger = log();\n\n constructor(\n private readonly options: TextSyncOptions,\n private readonly nextInChain: TextOutput,\n ) {\n this.speed = options.speed * STANDARD_SPEECH_RATE; // hyphens per second\n this.textData = {\n sentenceStream: options.sentenceTokenizer.stream(),\n pushedText: '',\n done: false,\n forwardedHyphens: 0,\n forwardedText: '',\n };\n this.audioData = {\n pushedDuration: 0,\n done: false,\n annotatedRate: null,\n };\n this.outputStream = new IdentityTransform();\n this.outputStreamWriter = this.outputStream.writable.getWriter();\n\n this.mainTask()\n .then(() => {\n this.outputStreamWriter.close();\n })\n .catch((error) => {\n this.logger.error({ error }, 'mainTask SegmentSynchronizerImpl');\n });\n this.captureTask = this.captureTaskImpl();\n }\n\n get closed() {\n return this.closedFuture.done;\n }\n\n get audioInputEnded() {\n return this.audioData.done;\n }\n\n get textInputEnded() {\n return this.textData.done;\n }\n\n get hasPendingText(): boolean {\n return this.textData.pushedText.length > this.textData.forwardedText.length;\n }\n\n get readable(): ReadableStream<string> {\n return this.outputStream.readable;\n }\n\n pushAudio(frame: AudioFrame) {\n if (this.closed) {\n this.logger.warn('SegmentSynchronizerImpl.pushAudio called after close');\n return;\n }\n // TODO(AJS-102): use frame.durationMs once available in rtc-node\n const frameDuration = frame.samplesPerChannel / frame.sampleRate;\n\n if (!this.startWallTime && frameDuration > 0) {\n this.startWallTime = Date.now();\n this.startFuture.resolve();\n }\n\n this.audioData.pushedDuration += frameDuration;\n }\n\n endAudioInput() {\n if (this.closed) {\n this.logger.warn('SegmentSynchronizerImpl.endAudioInput called after close');\n return;\n }\n\n this.audioData.done = true;\n }\n\n pushText(text: string | TimedString) {\n if (this.closed) {\n this.logger.warn('SegmentSynchronizerImpl.pushText called after close');\n return;\n }\n\n // Check if text is a TimedString (has timing information)\n let textStr: string;\n let startTime: number | undefined;\n let endTime: number | undefined;\n\n if (isTimedString(text)) {\n // This is a TimedString\n textStr = text.text;\n startTime = text.startTime;\n endTime = text.endTime;\n\n // Create annotatedRate if it doesn't exist\n if (!this.audioData.annotatedRate) {\n this.audioData.annotatedRate = new SpeakingRateData();\n }\n\n // Add the timing annotation\n this.audioData.annotatedRate.addByAnnotation(textStr, startTime, endTime);\n } else {\n textStr = text;\n }\n\n this.textData.sentenceStream.pushText(textStr);\n this.textData.pushedText += textStr;\n }\n\n endTextInput() {\n if (this.closed) {\n this.logger.warn('SegmentSynchronizerImpl.endTextInput called after close');\n return;\n }\n\n this.textData.done = true;\n this.textData.sentenceStream.endInput();\n }\n\n markPlaybackFinished(_playbackPosition: number, interrupted: boolean) {\n if (this.closed) {\n this.logger.warn('SegmentSynchronizerImpl.markPlaybackFinished called after close');\n return;\n }\n\n if (!this.textData.done || !this.audioData.done) {\n this.logger.warn(\n { textDone: this.textData.done, audioDone: this.audioData.done },\n 'SegmentSynchronizerImpl.markPlaybackFinished called before text/audio input is done',\n );\n // This allows mainTask to flush remaining text even if audio wasn't formally ended\n if (!interrupted) {\n this.playbackCompleted = true;\n }\n return;\n }\n\n if (!interrupted) {\n this.playbackCompleted = true;\n }\n }\n\n get synchronizedTranscript(): string {\n if (this.playbackCompleted) {\n return this.textData.pushedText;\n }\n return this.textData.forwardedText;\n }\n\n private async captureTaskImpl() {\n // Don't use a for-await loop here, because exiting the loop will close the writer in the\n // outputStream, which will cause an error in the mainTask.then method.\n // NOTE: forwardedText is updated in mainTask, NOT here\n const reader = this.outputStream.readable.getReader();\n while (true) {\n const { done, value: text } = await reader.read();\n if (done) {\n break;\n }\n await this.nextInChain.captureText(text);\n }\n reader.releaseLock();\n this.nextInChain.flush();\n }\n\n private async mainTask(): Promise<void> {\n await this.startFuture.await;\n\n if (this.closed && !this.playbackCompleted) {\n return;\n }\n\n if (!this.startWallTime) {\n throw new Error('startWallTime is not set when starting SegmentSynchronizerImpl.mainTask');\n }\n\n for await (const textSegment of this.textData.sentenceStream) {\n const sentence = textSegment.token;\n\n let textCursor = 0;\n if (this.closed && !this.playbackCompleted) {\n return;\n }\n\n for (const [word, _, endPos] of this.options.splitWords(sentence)) {\n if (this.closed && !this.playbackCompleted) {\n return;\n }\n\n if (this.playbackCompleted) {\n this.outputStreamWriter.write(sentence.slice(textCursor, endPos));\n textCursor = endPos;\n continue;\n }\n\n const wordHphens = this.options.hyphenateWord(word).length;\n const elapsedSeconds = (Date.now() - this.startWallTime) / 1000;\n\n let dHyphens = 0;\n const annotated = this.audioData.annotatedRate;\n\n if (annotated && annotated.pushedDuration >= elapsedSeconds) {\n // Use actual TTS timing annotations for accurate sync\n const targetLen = Math.floor(annotated.accumulateTo(elapsedSeconds));\n const forwardedLen = this.textData.forwardedText.length;\n\n if (targetLen >= forwardedLen) {\n const dText = this.textData.pushedText.slice(forwardedLen, targetLen);\n dHyphens = this.calcHyphens(dText).length;\n } else {\n const dText = this.textData.pushedText.slice(targetLen, forwardedLen);\n dHyphens = -this.calcHyphens(dText).length;\n }\n } else {\n // Fall back to estimated hyphens-per-second calculation\n const targetHyphens = elapsedSeconds * this.options.speed;\n dHyphens = Math.max(0, targetHyphens - this.textData.forwardedHyphens);\n }\n\n let delayTime = Math.max(0, wordHphens - dHyphens) / this.speed;\n\n if (this.playbackCompleted) {\n delayTime = 0;\n }\n\n await this.sleepIfNotClosed(delayTime / 2);\n const forwardedWord = sentence.slice(textCursor, endPos);\n this.outputStreamWriter.write(forwardedWord);\n\n await this.sleepIfNotClosed(delayTime / 2);\n\n this.textData.forwardedHyphens += wordHphens;\n this.textData.forwardedText += forwardedWord;\n textCursor = endPos;\n }\n\n if (textCursor < sentence.length) {\n const remaining = sentence.slice(textCursor);\n this.outputStreamWriter.write(remaining);\n }\n }\n }\n\n private calcHyphens(text: string): string[] {\n const words = this.options.splitWords(text);\n const hyphens: string[] = [];\n for (const [word] of words) {\n hyphens.push(...this.options.hyphenateWord(word));\n }\n return hyphens;\n }\n\n private async sleepIfNotClosed(sleepTimeSeconds: number) {\n if (this.closed) {\n return;\n }\n await delay(sleepTimeSeconds * 1000);\n }\n\n async close(): Promise<void> {\n if (this.closed) {\n return;\n }\n this.closedFuture.resolve();\n this.startFuture.resolve(); // avoid deadlock of mainTaskImpl in case it never started\n this.textData.sentenceStream.close();\n await this.captureTask;\n }\n}\n\nexport interface TranscriptionSynchronizerOptions {\n speed: number;\n hyphenateWord: (word: string) => string[];\n splitWords: (words: string) => [string, number, number][];\n sentenceTokenizer: SentenceTokenizer;\n}\n\nexport const defaultTextSyncOptions: TranscriptionSynchronizerOptions = {\n speed: 1,\n hyphenateWord: basic.hyphenateWord,\n splitWords: basic.splitWords,\n sentenceTokenizer: new basic.SentenceTokenizer({\n retainFormat: true,\n }),\n};\n\nexport class TranscriptionSynchronizer {\n readonly audioOutput: SyncedAudioOutput;\n readonly textOutput: SyncedTextOutput;\n\n private options: TextSyncOptions;\n private rotateSegmentTask: Task<void>;\n private _enabled: boolean = true;\n private closed: boolean = false;\n\n /** @internal */\n _impl: SegmentSynchronizerImpl;\n\n private logger = log();\n\n constructor(\n nextInChainAudio: AudioOutput,\n nextInChainText: TextOutput,\n options: TranscriptionSynchronizerOptions = defaultTextSyncOptions,\n ) {\n this.audioOutput = new SyncedAudioOutput(this, nextInChainAudio);\n this.textOutput = new SyncedTextOutput(this, nextInChainText);\n this.options = {\n speed: options.speed,\n hyphenateWord: options.hyphenateWord,\n splitWords: options.splitWords,\n sentenceTokenizer: options.sentenceTokenizer,\n };\n\n // initial segment/first segment, recreated for each new segment\n this._impl = new SegmentSynchronizerImpl(this.options, nextInChainText);\n this.rotateSegmentTask = Task.from((controller) =>\n this.rotateSegmentTaskImpl(controller.signal),\n );\n }\n\n get enabled(): boolean {\n return this._enabled;\n }\n\n set enabled(enabled: boolean) {\n if (this._enabled === enabled) {\n return;\n }\n\n this._enabled = enabled;\n this.rotateSegment();\n }\n\n rotateSegment() {\n if (this.closed) {\n return;\n }\n\n if (!this.rotateSegmentTask.done) {\n this.logger.warn('rotateSegment called while previous segment is still being rotated');\n }\n this.rotateSegmentTask = Task.from((controller) =>\n this.rotateSegmentTaskImpl(controller.signal, this.rotateSegmentTask),\n );\n }\n\n async close(): Promise<void> {\n this.closed = true;\n await this.rotateSegmentTask.cancelAndWait();\n await this._impl.close();\n }\n\n async barrier(): Promise<void> {\n if (this.rotateSegmentTask.done) {\n return;\n }\n await this.rotateSegmentTask.result;\n }\n\n private async rotateSegmentTaskImpl(abort: AbortSignal, oldTask?: Task<void>) {\n if (oldTask) {\n await oldTask.result;\n }\n\n if (abort.aborted) {\n return;\n }\n\n await this._impl.close();\n this._impl = new SegmentSynchronizerImpl(this.options, this.textOutput.nextInChain);\n }\n}\n\nclass SyncedAudioOutput extends AudioOutput {\n private pushedDuration: number = 0.0;\n\n constructor(\n public synchronizer: TranscriptionSynchronizer,\n private nextInChainAudio: AudioOutput,\n ) {\n super(nextInChainAudio.sampleRate, nextInChainAudio, { pause: true });\n }\n\n async captureFrame(frame: AudioFrame): Promise<void> {\n // using barrier() on capture should be sufficient, flush() must not be called if\n // capture_frame isn't completed\n await this.synchronizer.barrier();\n\n await super.captureFrame(frame);\n await this.nextInChainAudio.captureFrame(frame); // passthrough audio\n\n // TODO(AJS-102): use frame.durationMs once available in rtc-node\n this.pushedDuration += frame.samplesPerChannel / frame.sampleRate;\n\n if (!this.synchronizer.enabled) {\n return;\n }\n\n if (this.synchronizer._impl.audioInputEnded) {\n this.logger.warn(\n 'SegmentSynchronizerImpl audio marked as ended in capture audio, rotating segment',\n );\n this.synchronizer.rotateSegment();\n await this.synchronizer.barrier();\n }\n this.synchronizer._impl.pushAudio(frame);\n }\n\n flush() {\n super.flush();\n this.nextInChainAudio.flush();\n\n if (!this.synchronizer.enabled) {\n return;\n }\n\n if (!this.pushedDuration) {\n // For timed texts, audio goes directly to room without going through synchronizer.\n // If text was pushed but no audio, still end audio input so text can be processed.\n // Only rotate if there's also no text (truly empty segment).\n if (this.synchronizer._impl.hasPendingText) {\n // Text is pending - end audio input to allow text processing\n this.synchronizer._impl.endAudioInput();\n return;\n }\n // No text and no audio - rotate the segment\n this.synchronizer.rotateSegment();\n return;\n }\n\n this.synchronizer._impl.endAudioInput();\n }\n\n clearBuffer() {\n this.nextInChainAudio.clearBuffer();\n }\n\n // this is going to be automatically called by the next_in_chain\n onPlaybackFinished(ev: PlaybackFinishedEvent) {\n if (!this.synchronizer.enabled) {\n super.onPlaybackFinished(ev);\n return;\n }\n\n this.synchronizer._impl.markPlaybackFinished(ev.playbackPosition, ev.interrupted);\n super.onPlaybackFinished({\n playbackPosition: ev.playbackPosition,\n interrupted: ev.interrupted,\n synchronizedTranscript: this.synchronizer._impl.synchronizedTranscript,\n });\n\n this.synchronizer.rotateSegment();\n this.pushedDuration = 0.0;\n }\n}\n\nclass SyncedTextOutput extends TextOutput {\n private capturing: boolean = false;\n private logger = log();\n\n constructor(\n private readonly synchronizer: TranscriptionSynchronizer,\n public readonly nextInChain: TextOutput,\n ) {\n super(nextInChain);\n }\n\n async captureText(text: string | TimedString): Promise<void> {\n await this.synchronizer.barrier();\n\n const textStr = isTimedString(text) ? text.text : text;\n\n if (!this.synchronizer.enabled) {\n // pass through to the next in chain (extract string from TimedString if needed)\n await this.nextInChain.captureText(textStr);\n return;\n }\n\n this.capturing = true;\n if (this.synchronizer._impl.textInputEnded) {\n this.logger.warn(\n 'SegmentSynchronizerImpl text marked as ended in capture text, rotating segment',\n );\n this.synchronizer.rotateSegment();\n await this.synchronizer.barrier();\n }\n // Pass the TimedString to pushText for timing extraction\n this.synchronizer._impl.pushText(text);\n }\n\n async flush() {\n // Wait for any pending rotation to complete before accessing _impl\n await this.synchronizer.barrier();\n\n if (!this.synchronizer.enabled) {\n this.nextInChain.flush(); // passthrough text if the synchronizer is disabled\n return;\n }\n\n if (!this.capturing) {\n return;\n }\n\n this.capturing = false;\n this.synchronizer._impl.endTextInput();\n }\n}\n"],"mappings":"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","names":[]}
|