@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.
Files changed (128) hide show
  1. package/dist/cli.cjs +20 -18
  2. package/dist/cli.cjs.map +1 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +20 -18
  5. package/dist/cli.js.map +1 -1
  6. package/dist/index.cjs +5 -0
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +1 -0
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +3 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/inference/stt.cjs +2 -1
  14. package/dist/inference/stt.cjs.map +1 -1
  15. package/dist/inference/stt.d.ts.map +1 -1
  16. package/dist/inference/stt.js +2 -1
  17. package/dist/inference/stt.js.map +1 -1
  18. package/dist/llm/realtime.cjs.map +1 -1
  19. package/dist/llm/realtime.d.cts +5 -1
  20. package/dist/llm/realtime.d.ts +5 -1
  21. package/dist/llm/realtime.d.ts.map +1 -1
  22. package/dist/llm/realtime.js.map +1 -1
  23. package/dist/tts/stream_adapter.cjs +15 -1
  24. package/dist/tts/stream_adapter.cjs.map +1 -1
  25. package/dist/tts/stream_adapter.d.ts.map +1 -1
  26. package/dist/tts/stream_adapter.js +15 -1
  27. package/dist/tts/stream_adapter.js.map +1 -1
  28. package/dist/tts/tts.cjs.map +1 -1
  29. package/dist/tts/tts.d.cts +9 -1
  30. package/dist/tts/tts.d.ts +9 -1
  31. package/dist/tts/tts.d.ts.map +1 -1
  32. package/dist/tts/tts.js.map +1 -1
  33. package/dist/types.cjs +3 -0
  34. package/dist/types.cjs.map +1 -1
  35. package/dist/types.d.cts +4 -0
  36. package/dist/types.d.ts +4 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/types.js +2 -0
  39. package/dist/types.js.map +1 -1
  40. package/dist/voice/agent.cjs +11 -1
  41. package/dist/voice/agent.cjs.map +1 -1
  42. package/dist/voice/agent.d.cts +7 -3
  43. package/dist/voice/agent.d.ts +7 -3
  44. package/dist/voice/agent.d.ts.map +1 -1
  45. package/dist/voice/agent.js +11 -1
  46. package/dist/voice/agent.js.map +1 -1
  47. package/dist/voice/agent_activity.cjs +30 -14
  48. package/dist/voice/agent_activity.cjs.map +1 -1
  49. package/dist/voice/agent_activity.d.cts +1 -0
  50. package/dist/voice/agent_activity.d.ts +1 -0
  51. package/dist/voice/agent_activity.d.ts.map +1 -1
  52. package/dist/voice/agent_activity.js +30 -14
  53. package/dist/voice/agent_activity.js.map +1 -1
  54. package/dist/voice/agent_session.cjs +5 -1
  55. package/dist/voice/agent_session.cjs.map +1 -1
  56. package/dist/voice/agent_session.d.cts +2 -0
  57. package/dist/voice/agent_session.d.ts +2 -0
  58. package/dist/voice/agent_session.d.ts.map +1 -1
  59. package/dist/voice/agent_session.js +5 -1
  60. package/dist/voice/agent_session.js.map +1 -1
  61. package/dist/voice/audio_recognition.cjs +1 -1
  62. package/dist/voice/audio_recognition.cjs.map +1 -1
  63. package/dist/voice/audio_recognition.d.ts.map +1 -1
  64. package/dist/voice/audio_recognition.js +1 -1
  65. package/dist/voice/audio_recognition.js.map +1 -1
  66. package/dist/voice/background_audio.cjs +2 -1
  67. package/dist/voice/background_audio.cjs.map +1 -1
  68. package/dist/voice/background_audio.d.cts +4 -2
  69. package/dist/voice/background_audio.d.ts +4 -2
  70. package/dist/voice/background_audio.d.ts.map +1 -1
  71. package/dist/voice/background_audio.js +2 -1
  72. package/dist/voice/background_audio.js.map +1 -1
  73. package/dist/voice/generation.cjs +58 -5
  74. package/dist/voice/generation.cjs.map +1 -1
  75. package/dist/voice/generation.d.cts +17 -3
  76. package/dist/voice/generation.d.ts +17 -3
  77. package/dist/voice/generation.d.ts.map +1 -1
  78. package/dist/voice/generation.js +63 -6
  79. package/dist/voice/generation.js.map +1 -1
  80. package/dist/voice/index.cjs.map +1 -1
  81. package/dist/voice/index.d.cts +1 -1
  82. package/dist/voice/index.d.ts +1 -1
  83. package/dist/voice/index.d.ts.map +1 -1
  84. package/dist/voice/index.js.map +1 -1
  85. package/dist/voice/io.cjs +22 -2
  86. package/dist/voice/io.cjs.map +1 -1
  87. package/dist/voice/io.d.cts +21 -5
  88. package/dist/voice/io.d.ts +21 -5
  89. package/dist/voice/io.d.ts.map +1 -1
  90. package/dist/voice/io.js +18 -1
  91. package/dist/voice/io.js.map +1 -1
  92. package/dist/voice/room_io/_output.cjs +3 -2
  93. package/dist/voice/room_io/_output.cjs.map +1 -1
  94. package/dist/voice/room_io/_output.d.cts +3 -3
  95. package/dist/voice/room_io/_output.d.ts +3 -3
  96. package/dist/voice/room_io/_output.d.ts.map +1 -1
  97. package/dist/voice/room_io/_output.js +4 -3
  98. package/dist/voice/room_io/_output.js.map +1 -1
  99. package/dist/voice/transcription/synchronizer.cjs +137 -13
  100. package/dist/voice/transcription/synchronizer.cjs.map +1 -1
  101. package/dist/voice/transcription/synchronizer.d.cts +34 -4
  102. package/dist/voice/transcription/synchronizer.d.ts +34 -4
  103. package/dist/voice/transcription/synchronizer.d.ts.map +1 -1
  104. package/dist/voice/transcription/synchronizer.js +141 -14
  105. package/dist/voice/transcription/synchronizer.js.map +1 -1
  106. package/dist/voice/transcription/synchronizer.test.cjs +151 -0
  107. package/dist/voice/transcription/synchronizer.test.cjs.map +1 -0
  108. package/dist/voice/transcription/synchronizer.test.js +150 -0
  109. package/dist/voice/transcription/synchronizer.test.js.map +1 -0
  110. package/package.json +1 -1
  111. package/src/cli.ts +20 -18
  112. package/src/index.ts +1 -0
  113. package/src/inference/stt.ts +9 -8
  114. package/src/llm/realtime.ts +5 -1
  115. package/src/tts/stream_adapter.ts +23 -1
  116. package/src/tts/tts.ts +10 -1
  117. package/src/types.ts +5 -0
  118. package/src/voice/agent.ts +19 -4
  119. package/src/voice/agent_activity.ts +38 -13
  120. package/src/voice/agent_session.ts +6 -0
  121. package/src/voice/audio_recognition.ts +2 -1
  122. package/src/voice/background_audio.ts +6 -3
  123. package/src/voice/generation.ts +115 -10
  124. package/src/voice/index.ts +1 -1
  125. package/src/voice/io.ts +40 -5
  126. package/src/voice/room_io/_output.ts +6 -5
  127. package/src/voice/transcription/synchronizer.test.ts +206 -0
  128. 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":"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"}
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 { AudioOutput, TextOutput } from "../io.js";
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
- this.textData.sentenceStream.pushText(text);
80
- this.textData.pushedText += text;
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
- const targetHyphens = elapsedSeconds * this.options.speed;
151
- const hyphensBehind = Math.max(0, targetHyphens - this.textData.forwardedHyphens);
152
- let delay2 = Math.max(0, wordHphens - hyphensBehind) / this.speed;
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
- delay2 = 0;
264
+ delayTime = 0;
155
265
  }
156
- await this.sleepIfNotClosed(delay2 / 2);
157
- this.outputStreamWriter.write(sentence.slice(textCursor, endPos));
158
- await this.sleepIfNotClosed(delay2 / 2);
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(text);
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":[]}