@dxos/react-ui-transcription 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lib/capture.mjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { MediaStreamRecorder, createTranscriber, installMicrophoneBridge, isNativeAudioInput, listAudioInputs, setPreferredAudioInput, stopMicrophoneBridge };
|
|
1
|
+
export { i as MediaStreamRecorder, r as createTranscriber, t as installMicrophoneBridge, a as isNativeAudioInput, o as listAudioInputs, s as setPreferredAudioInput, n as stopMicrophoneBridge } from "./chunk-capture.mjs";
|
|
@@ -75,7 +75,7 @@ var setPreferredAudioInput = async (deviceId) => {
|
|
|
75
75
|
}
|
|
76
76
|
};
|
|
77
77
|
//#endregion
|
|
78
|
-
//#region \0@oxc-project+runtime@0.
|
|
78
|
+
//#region \0@oxc-project+runtime@0.154.0/helpers/esm/decorate.js
|
|
79
79
|
function __decorate(decorators, target, key, desc) {
|
|
80
80
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
81
81
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chunk-capture.mjs","names":[],"sources":["../../src/capture/audio-inputs.ts","../../src/capture/media-stream-recorder.ts","../../src/capture/create-transcriber.ts","../../src/capture/microphone-bridge.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n// Copyright 2026 Daniel Thompson-Yvetot\n//\n\nimport { log } from '@dxos/log';\n\nexport type AudioInputDevice = {\n /** Passed to `getUserMedia` on the web; an `AVAudioSession` port UID under the native bridge. */\n deviceId: string;\n label: string;\n};\n\n/**\n * Marks an id as an `AVAudioSession` port rather than a WebKit device. `getUserMedia` rejects an\n * unknown `deviceId` outright, so a native id must never reach its constraints — selection is applied\n * by routing the audio session instead. {@link isNativeAudioInput} is the check callers use.\n */\nconst NATIVE_PREFIX = 'native:';\n\nexport const isNativeAudioInput = (deviceId: string): boolean => deviceId.startsWith(NATIVE_PREFIX);\n\n/** Shape of the commands in `composer-app/src-tauri/src/audio_input.rs`. */\ntype NativeAudioInput = { id: string; name: string; selected: boolean };\n\n/**\n * The app exposes Tauri's bridge on `globalThis` (`withGlobalTauri`), so the command can be invoked\n * without this package depending on `@tauri-apps/api` — it is also loaded in plain browsers and in\n * node tests, where that dependency would be dead weight.\n */\ntype TauriGlobal = {\n __TAURI__?: { core?: { invoke?: (command: string, args?: Record<string, unknown>) => Promise<unknown> } };\n};\n\nconst getInvoke = () => (globalThis as TauriGlobal).__TAURI__?.core?.invoke;\n\n/**\n * Lists the microphones available for capture.\n *\n * Prefers the native command on iOS: WebKit reports no `deviceId` or `label` from\n * `enumerateDevices` until the page holds a capture grant, and never lists the simulator's\n * synthesised device at all — leaving the picker empty. `AVAudioSession` knows the real inputs.\n * Everywhere else (and whenever the command is absent) this falls back to the web API.\n */\nexport const listAudioInputs = async (): Promise<AudioInputDevice[]> => {\n const invoke = getInvoke();\n if (invoke) {\n try {\n const native = (await invoke('list_audio_inputs')) as NativeAudioInput[];\n return native.map(({ id, name }) => ({ deviceId: `${NATIVE_PREFIX}${id}`, label: name }));\n } catch (err) {\n // Absent on every platform but iOS; falling through to the web API is the expected path there.\n log('native audio inputs unavailable', { err });\n }\n }\n\n if (!navigator.mediaDevices?.enumerateDevices) {\n return [];\n }\n\n // Callers discard the promise, so a rejection here would surface as an unhandled rejection.\n try {\n const all = await navigator.mediaDevices.enumerateDevices();\n return all\n .filter((device) => device.kind === 'audioinput' && device.deviceId)\n .map((device, index) => ({\n deviceId: device.deviceId,\n // Labels stay blank until a capture grant exists; an ordinal keeps the entry selectable.\n label: device.label || `Microphone ${index + 1}`,\n }));\n } catch (err) {\n log('could not enumerate audio inputs', { err });\n return [];\n }\n};\n\n/**\n * Routes capture to the given input where the platform allows it.\n *\n * Only meaningful under the native bridge: `AVAudioSession.setPreferredInput` is scoped to this app\n * and the webview's capture follows it. On the web the selection is applied by `getUserMedia`\n * constraints instead, so this is a no-op.\n */\nexport const setPreferredAudioInput = async (deviceId: string): Promise<void> => {\n const invoke = getInvoke();\n if (!invoke) {\n return;\n }\n\n try {\n await invoke('set_preferred_audio_input', {\n id: isNativeAudioInput(deviceId) ? deviceId.slice(NATIVE_PREFIX.length) : deviceId,\n });\n } catch (err) {\n log('could not set preferred audio input', { err, deviceId });\n }\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { synchronized } from '@dxos/async';\nimport { log } from '@dxos/log';\nimport { type AudioChunk, type AudioRecorder, type WavConfig } from '@dxos/pipeline-transcription';\n\n/** Name the worklet registers itself under; must match `pcm-processor.js`. */\nconst PROCESSOR_NAME = 'dxos-pcm-processor';\n\n/** Fallback buffer size (frames) when `ScriptProcessorNode` stands in for the worklet. */\nconst SCRIPT_PROCESSOR_BUFFER_SIZE = 4096;\n\n/** Full-scale value for the 16-bit PCM the transcription pipeline expects. */\nconst INT16_SCALE = 0x7fff;\n\nexport type MediaStreamRecorderProps = {\n mediaStreamTrack: MediaStreamTrack;\n config: {\n interval: number;\n };\n};\n\n/**\n * Records a microphone track to 16-bit PCM chunks.\n *\n * Capture is done directly against the Web Audio graph rather than through a `MediaRecorder` WAV\n * polyfill. The polyfill produced empty buffers under WebKit — Safari, and therefore the Tauri\n * webview on both macOS and iOS — while working in Chromium, which made transcription look like a\n * service failure: audio was captured, uploaded and answered, but the upload was a bare WAV header.\n * Owning the graph also means owning the {@link AudioContext}, which is what lets us resume it:\n * WebKit starts every context suspended, and a suspended context feeds an audio node nothing.\n */\nexport class MediaStreamRecorder implements AudioRecorder {\n private readonly _mediaStreamTrack: MediaStreamTrack;\n private readonly _config: MediaStreamRecorderProps['config'];\n private _onChunk?: (chunk: AudioChunk) => void;\n\n private _context?: AudioContext = undefined;\n private _source?: MediaStreamAudioSourceNode = undefined;\n private _node?: AudioWorkletNode | ScriptProcessorNode = undefined;\n private _flushTimer?: ReturnType<typeof setInterval> = undefined;\n\n /** Frames captured since the last flush, in render-quantum sized pieces. */\n private _pending: Float32Array[] = [];\n private _pendingLength = 0;\n private _chunkStartedAt = 0;\n\n constructor({ mediaStreamTrack, config }: MediaStreamRecorderProps) {\n this._mediaStreamTrack = mediaStreamTrack;\n this._config = config;\n }\n\n /**\n * Reported from the {@link AudioContext}, not the track: the context is what actually resamples\n * the input, so its rate is the one the captured samples are at.\n */\n get wavConfig(): WavConfig {\n return {\n channels: 1,\n sampleRate: this._context?.sampleRate ?? this._mediaStreamTrack.getSettings().sampleRate ?? 48_000,\n bitDepthCode: '16',\n };\n }\n\n get mediaStreamTrack() {\n return this._mediaStreamTrack;\n }\n\n setOnChunk(onChunk: (chunk: AudioChunk) => void): void {\n this._onChunk = onChunk;\n }\n\n // `@synchronized` serializes start/stop so a rapid stop→start cycle (e.g. React StrictMode's\n // double-mount) cannot build a second graph over a context that is still tearing down.\n @synchronized\n async start(): Promise<void> {\n if (this._context) {\n return;\n }\n\n const context = new AudioContext();\n let source: MediaStreamAudioSourceNode;\n let node: AudioWorkletNode | ScriptProcessorNode;\n try {\n // WebKit hands back a suspended context and only a user gesture may resume it. Recording always\n // begins from a tap, so this resolves — but without it every downstream node receives silence,\n // which is indistinguishable from a muted microphone.\n if (context.state === 'suspended') {\n await context.resume();\n }\n\n source = context.createMediaStreamSource(new MediaStream([this._mediaStreamTrack]));\n node = await this.#createCaptureNode(context);\n source.connect(node);\n // The worklet emits nothing, but Chromium only pulls a graph that reaches a destination; a zeroed\n // gain keeps the microphone from being played back to the user.\n const sink = context.createGain();\n sink.gain.value = 0;\n node.connect(sink);\n sink.connect(context.destination);\n } catch (err) {\n // The context is not held on the instance until setup succeeds, so close it here or it leaks.\n await context.close().catch(() => {});\n throw err;\n }\n\n this._context = context;\n this._source = source;\n this._node = node;\n this._pending = [];\n this._pendingLength = 0;\n this._chunkStartedAt = Date.now();\n this._flushTimer = setInterval(() => this.#flush(), this._config.interval);\n }\n\n @synchronized\n async stop(): Promise<void> {\n if (!this._context) {\n return;\n }\n\n if (this._flushTimer !== undefined) {\n clearInterval(this._flushTimer);\n this._flushTimer = undefined;\n }\n // Emit whatever the final interval captured before tearing the graph down, so the tail of an\n // utterance is not dropped on stop.\n this.#flush();\n\n this._node?.disconnect();\n this._source?.disconnect();\n const context = this._context;\n this._node = undefined;\n this._source = undefined;\n this._context = undefined;\n await context.close();\n }\n\n /**\n * Prefers an {@link AudioWorkletNode}, whose processing runs off the main thread. Falls back to the\n * deprecated `ScriptProcessorNode` when the worklet module cannot be loaded — a bundler that does\n * not emit it, or a page whose CSP refuses the fetch — because a deprecated capture path is worth\n * more than none, and the previous silent failure is exactly what this replaces.\n */\n async #createCaptureNode(context: AudioContext): Promise<AudioWorkletNode | ScriptProcessorNode> {\n try {\n await context.audioWorklet.addModule(new URL('./pcm-processor.js', import.meta.url));\n const node = new AudioWorkletNode(context, PROCESSOR_NAME, {\n numberOfInputs: 1,\n numberOfOutputs: 1,\n channelCount: 1,\n });\n node.port.onmessage = (event: MessageEvent<Float32Array>) => this.#collect(event.data);\n return node;\n } catch (err) {\n log.warn('audio worklet unavailable; falling back to ScriptProcessorNode', { err });\n const node = context.createScriptProcessor(SCRIPT_PROCESSOR_BUFFER_SIZE, 1, 1);\n node.onaudioprocess = (event) => this.#collect(new Float32Array(event.inputBuffer.getChannelData(0)));\n return node;\n }\n }\n\n #collect(frames: Float32Array): void {\n this._pending.push(frames);\n this._pendingLength += frames.length;\n }\n\n /**\n * Emits the frames captured since the last flush as one chunk, converted to the 16-bit PCM the\n * pipeline merges with `wavefile`. Values are integers held in a `Float64Array` because that is\n * what {@link AudioChunk} carries.\n */\n #flush(): void {\n if (this._pendingLength === 0 || !this._onChunk) {\n return;\n }\n\n const frames = this._pending;\n const length = this._pendingLength;\n this._pending = [];\n this._pendingLength = 0;\n\n const samples = new Float64Array(length);\n let offset = 0;\n for (const frame of frames) {\n for (let index = 0; index < frame.length; index++) {\n // Clamped before scaling: a sample slightly outside [-1, 1] would wrap when written as int16.\n const value = Math.max(-1, Math.min(1, frame[index]));\n samples[offset + index] = Math.round(value * INT16_SCALE);\n }\n offset += frame.length;\n }\n\n const timestamp = this._chunkStartedAt;\n this._chunkStartedAt = Date.now();\n this._onChunk({ timestamp, data: samples });\n }\n}\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { type TranscribeConfig, type TranscribeFn, Transcriber } from '@dxos/pipeline-transcription';\nimport { type ContentBlock } from '@dxos/types';\n\nimport { MediaStreamRecorder } from './media-stream-recorder.ts';\n\n// Recorder chunk interval (ms).\nconst RECORD_INTERVAL = 200;\n// Chunks retained before the user starts speaking, to catch the onset of speech.\nconst PREFIXED_CHUNKS_AMOUNT = 10;\n// Chunks transcribed automatically; combined should stay under ~25MB or Whisper fails.\nconst TRANSCRIBE_AFTER_CHUNKS_AMOUNT = 50;\n\nexport type CreateTranscriberOptions = {\n audioStreamTrack: MediaStreamTrack;\n onSegments: (segments: ContentBlock.Transcript[]) => Promise<void>;\n /**\n * Transcriber configuration. Must carry `endpoint` unless `transcribe` is provided; a\n * transcriber with neither rejects on `open()` with `TranscriptionEndpointNotConfiguredError`.\n */\n transcriberConfig?: Partial<TranscribeConfig>;\n recorderConfig?: { interval?: number };\n transcribe?: TranscribeFn;\n};\n\n/**\n * Builds a {@link Transcriber} backed by a browser {@link MediaStreamRecorder} for the given track —\n * the low-level audio→text construction, co-located with the recorder so it can be tested outside the\n * plugin. Consumed directly via the {@link useTranscriber} hook.\n */\nexport const createTranscriber = ({\n audioStreamTrack,\n onSegments,\n transcriberConfig,\n recorderConfig,\n transcribe,\n}: CreateTranscriberOptions): Transcriber =>\n new Transcriber({\n config: {\n transcribeAfterChunksAmount: TRANSCRIBE_AFTER_CHUNKS_AMOUNT,\n prefixBufferChunksAmount: PREFIXED_CHUNKS_AMOUNT,\n ...transcriberConfig,\n },\n recorder: new MediaStreamRecorder({\n mediaStreamTrack: audioStreamTrack,\n config: { interval: RECORD_INTERVAL, ...recorderConfig },\n }),\n onSegments,\n transcribe,\n });\n","//\n// Copyright 2026 DXOS.org\n// Copyright 2026 Daniel Thompson-Yvetot\n//\n\nimport { log } from '@dxos/log';\n\n/** Sample rate the native bridge converts to; see `ios/MicrophoneBridge.m`. */\nconst BRIDGE_SAMPLE_RATE = 16_000;\n\n/** Event the native side dispatches, carrying base64 Float32 PCM. */\nconst CHUNK_EVENT = 'dxos-mic-chunk';\n\ntype TauriGlobal = {\n __TAURI__?: { core?: { invoke?: (command: string, args?: Record<string, unknown>) => Promise<unknown> } };\n};\n\nconst getInvoke = () => (globalThis as TauriGlobal).__TAURI__?.core?.invoke;\n\n/** Undefined for a payload that is not valid base64-encoded Float32 PCM; the chunk is dropped. */\nconst decodeChunk = (encoded: string): Float32Array | undefined => {\n try {\n const binary = atob(encoded);\n if (binary.length % Float32Array.BYTES_PER_ELEMENT !== 0) {\n return undefined;\n }\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index++) {\n bytes[index] = binary.charCodeAt(index);\n }\n // The native side writes host-endian Float32; every platform this runs on is little-endian.\n return new Float32Array(bytes.buffer);\n } catch {\n return undefined;\n }\n};\n\nlet installed = false;\n/** Whether native capture is currently running; {@link stopMicrophoneBridge} clears it. */\nlet capturing = false;\n\n/**\n * Routes `getUserMedia` audio through native capture.\n *\n * WebKit substitutes a synthetic device in the iOS Simulator: `getUserMedia` resolves, the track\n * reports live and unmuted, and it carries no sound — so transcription runs end to end against\n * silence and the failure looks like a broken service. Native code does reach the host microphone,\n * so it captures there and pushes PCM into the page, which is replayed through an\n * {@link AudioWorkletNode} into a {@link MediaStreamAudioDestinationNode} and handed back as an\n * ordinary stream.\n *\n * Shimming at the `getUserMedia` boundary is deliberate: everything downstream — the recorder, the\n * transcriber, any other consumer of a microphone stream — then runs exactly the production path,\n * rather than the simulator becoming a special case inside each of them.\n *\n * A development aid, and inert unless {@link installMicrophoneBridge} is called: on a real device\n * WebKit captures correctly and nothing here is used.\n */\nexport const installMicrophoneBridge = async (): Promise<boolean> => {\n const invoke = getInvoke();\n if (!invoke || !navigator.mediaDevices?.getUserMedia) {\n return installed;\n }\n if (installed && capturing) {\n return true;\n }\n\n try {\n await invoke('start_microphone_bridge');\n } catch (err) {\n // Absent off iOS, and expected there — the caller carries on with WebKit's own capture.\n log('microphone bridge unavailable', { err });\n return false;\n }\n capturing = true;\n\n // The shim and graph survive a stop; a reinstall only needed native capture restarted.\n if (installed) {\n return true;\n }\n\n let context: AudioContext | undefined;\n try {\n context = new AudioContext({ sampleRate: BRIDGE_SAMPLE_RATE });\n if (context.state === 'suspended') {\n await context.resume();\n }\n await context.audioWorklet.addModule(new URL('./bridge-processor.js', import.meta.url));\n\n const node = new AudioWorkletNode(context, 'dxos-bridge-processor', {\n numberOfInputs: 0,\n outputChannelCount: [1],\n });\n const destination = context.createMediaStreamDestination();\n node.connect(destination);\n\n globalThis.addEventListener(CHUNK_EVENT, (event) => {\n if (!(event instanceof CustomEvent)) {\n return;\n }\n const detail: unknown = event.detail;\n if (typeof detail === 'string' && detail.length > 0) {\n const frames = decodeChunk(detail);\n if (frames) {\n node.port.postMessage(frames, [frames.buffer]);\n }\n }\n });\n\n // Replaced rather than wrapped conditionally: audio requests are served from the bridge for as long\n // as it is installed, and video requests fall through untouched.\n const original = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);\n navigator.mediaDevices.getUserMedia = async (constraints?: MediaStreamConstraints) => {\n if (!constraints?.audio) {\n return original(constraints);\n }\n if (constraints.video) {\n const video = await original({ ...constraints, audio: false });\n for (const track of destination.stream.getAudioTracks()) {\n // Cloned so a caller stopping its track does not end the shared destination track.\n video.addTrack(track.clone());\n }\n return video;\n }\n return new MediaStream(destination.stream.getAudioTracks().map((track) => track.clone()));\n };\n\n installed = true;\n log.info('microphone bridge installed');\n return true;\n } catch (err) {\n // Native capture without a graph would hold the microphone open for nothing. Cleanup failures\n // are logged rather than thrown so the original setup failure is the error that propagates.\n capturing = false;\n await invoke('stop_microphone_bridge').catch((stopErr) => {\n capturing = true;\n log.warn('could not stop native capture after failed bridge setup', { err: stopErr });\n });\n await context?.close().catch((closeErr) => {\n log.warn('could not close the bridge audio context', { err: closeErr });\n });\n throw err;\n }\n};\n\n/** Stops native capture. The `getUserMedia` shim stays in place; a restart reuses the same graph. */\nexport const stopMicrophoneBridge = async (): Promise<void> => {\n const invoke = getInvoke();\n if (!invoke) {\n return;\n }\n try {\n await invoke('stop_microphone_bridge');\n capturing = false;\n } catch (err) {\n log('could not stop the microphone bridge', { err });\n }\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chunk-capture.mjs","names":["__dxlog_file","getInvoke","__dxlog_file","_decorate"],"sources":["../../src/capture/audio-inputs.ts","../../src/capture/media-stream-recorder.ts","../../src/capture/create-transcriber.ts","../../src/capture/microphone-bridge.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n// Copyright 2026 Daniel Thompson-Yvetot\n//\n\nimport { log } from '@dxos/log';\n\nexport type AudioInputDevice = {\n /** Passed to `getUserMedia` on the web; an `AVAudioSession` port UID under the native bridge. */\n deviceId: string;\n label: string;\n};\n\n/**\n * Marks an id as an `AVAudioSession` port rather than a WebKit device. `getUserMedia` rejects an\n * unknown `deviceId` outright, so a native id must never reach its constraints — selection is applied\n * by routing the audio session instead. {@link isNativeAudioInput} is the check callers use.\n */\nconst NATIVE_PREFIX = 'native:';\n\nexport const isNativeAudioInput = (deviceId: string): boolean => deviceId.startsWith(NATIVE_PREFIX);\n\n/** Shape of the commands in `composer-app/src-tauri/src/audio_input.rs`. */\ntype NativeAudioInput = { id: string; name: string; selected: boolean };\n\n/**\n * The app exposes Tauri's bridge on `globalThis` (`withGlobalTauri`), so the command can be invoked\n * without this package depending on `@tauri-apps/api` — it is also loaded in plain browsers and in\n * node tests, where that dependency would be dead weight.\n */\ntype TauriGlobal = {\n __TAURI__?: { core?: { invoke?: (command: string, args?: Record<string, unknown>) => Promise<unknown> } };\n};\n\nconst getInvoke = () => (globalThis as TauriGlobal).__TAURI__?.core?.invoke;\n\n/**\n * Lists the microphones available for capture.\n *\n * Prefers the native command on iOS: WebKit reports no `deviceId` or `label` from\n * `enumerateDevices` until the page holds a capture grant, and never lists the simulator's\n * synthesised device at all — leaving the picker empty. `AVAudioSession` knows the real inputs.\n * Everywhere else (and whenever the command is absent) this falls back to the web API.\n */\nexport const listAudioInputs = async (): Promise<AudioInputDevice[]> => {\n const invoke = getInvoke();\n if (invoke) {\n try {\n const native = (await invoke('list_audio_inputs')) as NativeAudioInput[];\n return native.map(({ id, name }) => ({ deviceId: `${NATIVE_PREFIX}${id}`, label: name }));\n } catch (err) {\n // Absent on every platform but iOS; falling through to the web API is the expected path there.\n log('native audio inputs unavailable', { err });\n }\n }\n\n if (!navigator.mediaDevices?.enumerateDevices) {\n return [];\n }\n\n // Callers discard the promise, so a rejection here would surface as an unhandled rejection.\n try {\n const all = await navigator.mediaDevices.enumerateDevices();\n return all\n .filter((device) => device.kind === 'audioinput' && device.deviceId)\n .map((device, index) => ({\n deviceId: device.deviceId,\n // Labels stay blank until a capture grant exists; an ordinal keeps the entry selectable.\n label: device.label || `Microphone ${index + 1}`,\n }));\n } catch (err) {\n log('could not enumerate audio inputs', { err });\n return [];\n }\n};\n\n/**\n * Routes capture to the given input where the platform allows it.\n *\n * Only meaningful under the native bridge: `AVAudioSession.setPreferredInput` is scoped to this app\n * and the webview's capture follows it. On the web the selection is applied by `getUserMedia`\n * constraints instead, so this is a no-op.\n */\nexport const setPreferredAudioInput = async (deviceId: string): Promise<void> => {\n const invoke = getInvoke();\n if (!invoke) {\n return;\n }\n\n try {\n await invoke('set_preferred_audio_input', {\n id: isNativeAudioInput(deviceId) ? deviceId.slice(NATIVE_PREFIX.length) : deviceId,\n });\n } catch (err) {\n log('could not set preferred audio input', { err, deviceId });\n }\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { synchronized } from '@dxos/async';\nimport { log } from '@dxos/log';\nimport { type AudioChunk, type AudioRecorder, type WavConfig } from '@dxos/pipeline-transcription';\n\n/** Name the worklet registers itself under; must match `pcm-processor.js`. */\nconst PROCESSOR_NAME = 'dxos-pcm-processor';\n\n/** Fallback buffer size (frames) when `ScriptProcessorNode` stands in for the worklet. */\nconst SCRIPT_PROCESSOR_BUFFER_SIZE = 4096;\n\n/** Full-scale value for the 16-bit PCM the transcription pipeline expects. */\nconst INT16_SCALE = 0x7fff;\n\nexport type MediaStreamRecorderProps = {\n mediaStreamTrack: MediaStreamTrack;\n config: {\n interval: number;\n };\n};\n\n/**\n * Records a microphone track to 16-bit PCM chunks.\n *\n * Capture is done directly against the Web Audio graph rather than through a `MediaRecorder` WAV\n * polyfill. The polyfill produced empty buffers under WebKit — Safari, and therefore the Tauri\n * webview on both macOS and iOS — while working in Chromium, which made transcription look like a\n * service failure: audio was captured, uploaded and answered, but the upload was a bare WAV header.\n * Owning the graph also means owning the {@link AudioContext}, which is what lets us resume it:\n * WebKit starts every context suspended, and a suspended context feeds an audio node nothing.\n */\nexport class MediaStreamRecorder implements AudioRecorder {\n private readonly _mediaStreamTrack: MediaStreamTrack;\n private readonly _config: MediaStreamRecorderProps['config'];\n private _onChunk?: (chunk: AudioChunk) => void;\n\n private _context?: AudioContext = undefined;\n private _source?: MediaStreamAudioSourceNode = undefined;\n private _node?: AudioWorkletNode | ScriptProcessorNode = undefined;\n private _flushTimer?: ReturnType<typeof setInterval> = undefined;\n\n /** Frames captured since the last flush, in render-quantum sized pieces. */\n private _pending: Float32Array[] = [];\n private _pendingLength = 0;\n private _chunkStartedAt = 0;\n\n constructor({ mediaStreamTrack, config }: MediaStreamRecorderProps) {\n this._mediaStreamTrack = mediaStreamTrack;\n this._config = config;\n }\n\n /**\n * Reported from the {@link AudioContext}, not the track: the context is what actually resamples\n * the input, so its rate is the one the captured samples are at.\n */\n get wavConfig(): WavConfig {\n return {\n channels: 1,\n sampleRate: this._context?.sampleRate ?? this._mediaStreamTrack.getSettings().sampleRate ?? 48_000,\n bitDepthCode: '16',\n };\n }\n\n get mediaStreamTrack() {\n return this._mediaStreamTrack;\n }\n\n setOnChunk(onChunk: (chunk: AudioChunk) => void): void {\n this._onChunk = onChunk;\n }\n\n // `@synchronized` serializes start/stop so a rapid stop→start cycle (e.g. React StrictMode's\n // double-mount) cannot build a second graph over a context that is still tearing down.\n @synchronized\n async start(): Promise<void> {\n if (this._context) {\n return;\n }\n\n const context = new AudioContext();\n let source: MediaStreamAudioSourceNode;\n let node: AudioWorkletNode | ScriptProcessorNode;\n try {\n // WebKit hands back a suspended context and only a user gesture may resume it. Recording always\n // begins from a tap, so this resolves — but without it every downstream node receives silence,\n // which is indistinguishable from a muted microphone.\n if (context.state === 'suspended') {\n await context.resume();\n }\n\n source = context.createMediaStreamSource(new MediaStream([this._mediaStreamTrack]));\n node = await this.#createCaptureNode(context);\n source.connect(node);\n // The worklet emits nothing, but Chromium only pulls a graph that reaches a destination; a zeroed\n // gain keeps the microphone from being played back to the user.\n const sink = context.createGain();\n sink.gain.value = 0;\n node.connect(sink);\n sink.connect(context.destination);\n } catch (err) {\n // The context is not held on the instance until setup succeeds, so close it here or it leaks.\n await context.close().catch(() => {});\n throw err;\n }\n\n this._context = context;\n this._source = source;\n this._node = node;\n this._pending = [];\n this._pendingLength = 0;\n this._chunkStartedAt = Date.now();\n this._flushTimer = setInterval(() => this.#flush(), this._config.interval);\n }\n\n @synchronized\n async stop(): Promise<void> {\n if (!this._context) {\n return;\n }\n\n if (this._flushTimer !== undefined) {\n clearInterval(this._flushTimer);\n this._flushTimer = undefined;\n }\n // Emit whatever the final interval captured before tearing the graph down, so the tail of an\n // utterance is not dropped on stop.\n this.#flush();\n\n this._node?.disconnect();\n this._source?.disconnect();\n const context = this._context;\n this._node = undefined;\n this._source = undefined;\n this._context = undefined;\n await context.close();\n }\n\n /**\n * Prefers an {@link AudioWorkletNode}, whose processing runs off the main thread. Falls back to the\n * deprecated `ScriptProcessorNode` when the worklet module cannot be loaded — a bundler that does\n * not emit it, or a page whose CSP refuses the fetch — because a deprecated capture path is worth\n * more than none, and the previous silent failure is exactly what this replaces.\n */\n async #createCaptureNode(context: AudioContext): Promise<AudioWorkletNode | ScriptProcessorNode> {\n try {\n await context.audioWorklet.addModule(new URL('./pcm-processor.js', import.meta.url));\n const node = new AudioWorkletNode(context, PROCESSOR_NAME, {\n numberOfInputs: 1,\n numberOfOutputs: 1,\n channelCount: 1,\n });\n node.port.onmessage = (event: MessageEvent<Float32Array>) => this.#collect(event.data);\n return node;\n } catch (err) {\n log.warn('audio worklet unavailable; falling back to ScriptProcessorNode', { err });\n const node = context.createScriptProcessor(SCRIPT_PROCESSOR_BUFFER_SIZE, 1, 1);\n node.onaudioprocess = (event) => this.#collect(new Float32Array(event.inputBuffer.getChannelData(0)));\n return node;\n }\n }\n\n #collect(frames: Float32Array): void {\n this._pending.push(frames);\n this._pendingLength += frames.length;\n }\n\n /**\n * Emits the frames captured since the last flush as one chunk, converted to the 16-bit PCM the\n * pipeline merges with `wavefile`. Values are integers held in a `Float64Array` because that is\n * what {@link AudioChunk} carries.\n */\n #flush(): void {\n if (this._pendingLength === 0 || !this._onChunk) {\n return;\n }\n\n const frames = this._pending;\n const length = this._pendingLength;\n this._pending = [];\n this._pendingLength = 0;\n\n const samples = new Float64Array(length);\n let offset = 0;\n for (const frame of frames) {\n for (let index = 0; index < frame.length; index++) {\n // Clamped before scaling: a sample slightly outside [-1, 1] would wrap when written as int16.\n const value = Math.max(-1, Math.min(1, frame[index]));\n samples[offset + index] = Math.round(value * INT16_SCALE);\n }\n offset += frame.length;\n }\n\n const timestamp = this._chunkStartedAt;\n this._chunkStartedAt = Date.now();\n this._onChunk({ timestamp, data: samples });\n }\n}\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { type TranscribeConfig, type TranscribeFn, Transcriber } from '@dxos/pipeline-transcription';\nimport { type ContentBlock } from '@dxos/types';\n\nimport { MediaStreamRecorder } from './media-stream-recorder.ts';\n\n// Recorder chunk interval (ms).\nconst RECORD_INTERVAL = 200;\n// Chunks retained before the user starts speaking, to catch the onset of speech.\nconst PREFIXED_CHUNKS_AMOUNT = 10;\n// Chunks transcribed automatically; combined should stay under ~25MB or Whisper fails.\nconst TRANSCRIBE_AFTER_CHUNKS_AMOUNT = 50;\n\nexport type CreateTranscriberOptions = {\n audioStreamTrack: MediaStreamTrack;\n onSegments: (segments: ContentBlock.Transcript[]) => Promise<void>;\n /**\n * Transcriber configuration. Must carry `endpoint` unless `transcribe` is provided; a\n * transcriber with neither rejects on `open()` with `TranscriptionEndpointNotConfiguredError`.\n */\n transcriberConfig?: Partial<TranscribeConfig>;\n recorderConfig?: { interval?: number };\n transcribe?: TranscribeFn;\n};\n\n/**\n * Builds a {@link Transcriber} backed by a browser {@link MediaStreamRecorder} for the given track —\n * the low-level audio→text construction, co-located with the recorder so it can be tested outside the\n * plugin. Consumed directly via the {@link useTranscriber} hook.\n */\nexport const createTranscriber = ({\n audioStreamTrack,\n onSegments,\n transcriberConfig,\n recorderConfig,\n transcribe,\n}: CreateTranscriberOptions): Transcriber =>\n new Transcriber({\n config: {\n transcribeAfterChunksAmount: TRANSCRIBE_AFTER_CHUNKS_AMOUNT,\n prefixBufferChunksAmount: PREFIXED_CHUNKS_AMOUNT,\n ...transcriberConfig,\n },\n recorder: new MediaStreamRecorder({\n mediaStreamTrack: audioStreamTrack,\n config: { interval: RECORD_INTERVAL, ...recorderConfig },\n }),\n onSegments,\n transcribe,\n });\n","//\n// Copyright 2026 DXOS.org\n// Copyright 2026 Daniel Thompson-Yvetot\n//\n\nimport { log } from '@dxos/log';\n\n/** Sample rate the native bridge converts to; see `ios/MicrophoneBridge.m`. */\nconst BRIDGE_SAMPLE_RATE = 16_000;\n\n/** Event the native side dispatches, carrying base64 Float32 PCM. */\nconst CHUNK_EVENT = 'dxos-mic-chunk';\n\ntype TauriGlobal = {\n __TAURI__?: { core?: { invoke?: (command: string, args?: Record<string, unknown>) => Promise<unknown> } };\n};\n\nconst getInvoke = () => (globalThis as TauriGlobal).__TAURI__?.core?.invoke;\n\n/** Undefined for a payload that is not valid base64-encoded Float32 PCM; the chunk is dropped. */\nconst decodeChunk = (encoded: string): Float32Array | undefined => {\n try {\n const binary = atob(encoded);\n if (binary.length % Float32Array.BYTES_PER_ELEMENT !== 0) {\n return undefined;\n }\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index++) {\n bytes[index] = binary.charCodeAt(index);\n }\n // The native side writes host-endian Float32; every platform this runs on is little-endian.\n return new Float32Array(bytes.buffer);\n } catch {\n return undefined;\n }\n};\n\nlet installed = false;\n/** Whether native capture is currently running; {@link stopMicrophoneBridge} clears it. */\nlet capturing = false;\n\n/**\n * Routes `getUserMedia` audio through native capture.\n *\n * WebKit substitutes a synthetic device in the iOS Simulator: `getUserMedia` resolves, the track\n * reports live and unmuted, and it carries no sound — so transcription runs end to end against\n * silence and the failure looks like a broken service. Native code does reach the host microphone,\n * so it captures there and pushes PCM into the page, which is replayed through an\n * {@link AudioWorkletNode} into a {@link MediaStreamAudioDestinationNode} and handed back as an\n * ordinary stream.\n *\n * Shimming at the `getUserMedia` boundary is deliberate: everything downstream — the recorder, the\n * transcriber, any other consumer of a microphone stream — then runs exactly the production path,\n * rather than the simulator becoming a special case inside each of them.\n *\n * A development aid, and inert unless {@link installMicrophoneBridge} is called: on a real device\n * WebKit captures correctly and nothing here is used.\n */\nexport const installMicrophoneBridge = async (): Promise<boolean> => {\n const invoke = getInvoke();\n if (!invoke || !navigator.mediaDevices?.getUserMedia) {\n return installed;\n }\n if (installed && capturing) {\n return true;\n }\n\n try {\n await invoke('start_microphone_bridge');\n } catch (err) {\n // Absent off iOS, and expected there — the caller carries on with WebKit's own capture.\n log('microphone bridge unavailable', { err });\n return false;\n }\n capturing = true;\n\n // The shim and graph survive a stop; a reinstall only needed native capture restarted.\n if (installed) {\n return true;\n }\n\n let context: AudioContext | undefined;\n try {\n context = new AudioContext({ sampleRate: BRIDGE_SAMPLE_RATE });\n if (context.state === 'suspended') {\n await context.resume();\n }\n await context.audioWorklet.addModule(new URL('./bridge-processor.js', import.meta.url));\n\n const node = new AudioWorkletNode(context, 'dxos-bridge-processor', {\n numberOfInputs: 0,\n outputChannelCount: [1],\n });\n const destination = context.createMediaStreamDestination();\n node.connect(destination);\n\n globalThis.addEventListener(CHUNK_EVENT, (event) => {\n if (!(event instanceof CustomEvent)) {\n return;\n }\n const detail: unknown = event.detail;\n if (typeof detail === 'string' && detail.length > 0) {\n const frames = decodeChunk(detail);\n if (frames) {\n node.port.postMessage(frames, [frames.buffer]);\n }\n }\n });\n\n // Replaced rather than wrapped conditionally: audio requests are served from the bridge for as long\n // as it is installed, and video requests fall through untouched.\n const original = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);\n navigator.mediaDevices.getUserMedia = async (constraints?: MediaStreamConstraints) => {\n if (!constraints?.audio) {\n return original(constraints);\n }\n if (constraints.video) {\n const video = await original({ ...constraints, audio: false });\n for (const track of destination.stream.getAudioTracks()) {\n // Cloned so a caller stopping its track does not end the shared destination track.\n video.addTrack(track.clone());\n }\n return video;\n }\n return new MediaStream(destination.stream.getAudioTracks().map((track) => track.clone()));\n };\n\n installed = true;\n log.info('microphone bridge installed');\n return true;\n } catch (err) {\n // Native capture without a graph would hold the microphone open for nothing. Cleanup failures\n // are logged rather than thrown so the original setup failure is the error that propagates.\n capturing = false;\n await invoke('stop_microphone_bridge').catch((stopErr) => {\n capturing = true;\n log.warn('could not stop native capture after failed bridge setup', { err: stopErr });\n });\n await context?.close().catch((closeErr) => {\n log.warn('could not close the bridge audio context', { err: closeErr });\n });\n throw err;\n }\n};\n\n/** Stops native capture. The `getUserMedia` shim stays in place; a restart reuses the same graph. */\nexport const stopMicrophoneBridge = async (): Promise<void> => {\n const invoke = getInvoke();\n if (!invoke) {\n return;\n }\n try {\n await invoke('stop_microphone_bridge');\n capturing = false;\n } catch (err) {\n log('could not stop the microphone bridge', { err });\n }\n};\n"],"mappings":";;;;;;;;;;AAkBA,IAAM,gBAAgB;AAEtB,IAAa,sBAAsB,aAA8B,SAAS,WAAW,aAAa;AAclG,IAAMC,oBAAmB,WAA2B,WAAW,MAAM;;;;;;;;;AAUrE,IAAa,kBAAkB,YAAyC;CACtE,MAAM,SAASA,YAAU;CACzB,IAAI,QACF,IAAI;EAEF,QAAO,MADe,OAAO,mBAAmB,EAAA,CAClC,KAAK,EAAE,IAAI,YAAY;GAAE,UAAU,GAAG,gBAAgB;GAAM,OAAO;EAAK,EAAE;CAC1F,SAAS,KAAK;EAEZ,IAAI,mCAAmC,EAAE,IAAI,GAAA;GAAA,YAAA;GAAA,GAAAD;GAAA,GAAA;GAAA,GAAA,KAAA;EAAA,CAAC;CAChD;CAGF,IAAI,CAAC,UAAU,cAAc,kBAC3B,OAAO,CAAC;CAIV,IAAI;EAEF,QAAO,MADW,UAAU,aAAa,iBAAiB,EAAA,CAEvD,QAAQ,WAAW,OAAO,SAAS,gBAAgB,OAAO,QAAQ,CAAA,CAClE,KAAK,QAAQ,WAAW;GACvB,UAAU,OAAO;GAEjB,OAAO,OAAO,SAAS,cAAc,QAAQ;EAC/C,EAAE;CACN,SAAS,KAAK;EACZ,IAAI,oCAAoC,EAAE,IAAI,GAAA;GAAA,YAAA;GAAA,GAAAA;GAAA,GAAA;GAAA,GAAA,KAAA;EAAA,CAAC;EAC/C,OAAO,CAAC;CACV;AACF;;;;;;;;AASA,IAAa,yBAAyB,OAAO,aAAoC;CAC/E,MAAM,SAASC,YAAU;CACzB,IAAI,CAAC,QACH;CAGF,IAAI;EACF,MAAM,OAAO,6BAA6B,EACxC,IAAI,mBAAmB,QAAQ,IAAI,SAAS,MAAM,CAAoB,IAAI,SAC5E,CAAC;CACH,SAAS,KAAK;EACZ,IAAI,uCAAuC;GAAE;GAAK;EAAS,GAAA;GAAA,YAAA;GAAA,GAAAD;GAAA,GAAA;GAAA,GAAA,KAAA;EAAA,CAAC;CAC9D;AACF;;;;;;;;;;;;;ACvFA,IAAM,iBAAiB;;AAGvB,IAAM,+BAA+B;;AAGrC,IAAM,cAAc;;;;;;;;;;;AAmBpB,IAAa,sBAAb,MAA0D;CACxD;CACA;CACA;CAEA,WAAkC,KAAA;CAClC,UAA+C,KAAA;CAC/C,QAAyD,KAAA;CACzD,cAAuD,KAAA;;CAGvD,WAAmC,CAAC;CACpC,iBAAyB;CACzB,kBAA0B;CAE1B,YAAY,EAAE,kBAAkB,UAAoC;EAClE,KAAK,oBAAoB;EACzB,KAAK,UAAU;CACjB;;;;;CAMA,IAAI,YAAuB;EACzB,OAAO;GACL,UAAU;GACV,YAAY,KAAK,UAAU,cAAc,KAAK,kBAAkB,YAAY,CAAC,CAAC,cAAc;GAC5F,cAAc;EAChB;CACF;CAEA,IAAI,mBAAmB;EACrB,OAAO,KAAK;CACd;CAEA,WAAW,SAA4C;EACrD,KAAK,WAAW;CAClB;CAIA,MACM,QAAuB;EAC3B,IAAI,KAAK,UACP;EAGF,MAAM,UAAU,IAAI,aAAa;EACjC,IAAI;EACJ,IAAI;EACJ,IAAI;GAIF,IAAI,QAAQ,UAAU,aACpB,MAAM,QAAQ,OAAO;GAGvB,SAAS,QAAQ,wBAAwB,IAAI,YAAY,CAAC,KAAK,iBAAiB,CAAC,CAAC;GAClF,OAAO,MAAM,KAAK,mBAAmB,OAAO;GAC5C,OAAO,QAAQ,IAAI;GAGnB,MAAM,OAAO,QAAQ,WAAW;GAChC,KAAK,KAAK,QAAQ;GAClB,KAAK,QAAQ,IAAI;GACjB,KAAK,QAAQ,QAAQ,WAAW;EAClC,SAAS,KAAK;GAEZ,MAAM,QAAQ,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;GACpC,MAAM;EACR;EAEA,KAAK,WAAW;EAChB,KAAK,UAAU;EACf,KAAK,QAAQ;EACb,KAAK,WAAW,CAAC;EACjB,KAAK,iBAAiB;EACtB,KAAK,kBAAkB,KAAK,IAAI;EAChC,KAAK,cAAc,kBAAkB,KAAK,OAAO,GAAG,KAAK,QAAQ,QAAQ;CAC3E;CAEA,MACM,OAAsB;EAC1B,IAAI,CAAC,KAAK,UACR;EAGF,IAAI,KAAK,gBAAgB,KAAA,GAAW;GAClC,cAAc,KAAK,WAAW;GAC9B,KAAK,cAAc,KAAA;EACrB;EAGA,KAAK,OAAO;EAEZ,KAAK,OAAO,WAAW;EACvB,KAAK,SAAS,WAAW;EACzB,MAAM,UAAU,KAAK;EACrB,KAAK,QAAQ,KAAA;EACb,KAAK,UAAU,KAAA;EACf,KAAK,WAAW,KAAA;EAChB,MAAM,QAAQ,MAAM;CACtB;;;;;;;CAQA,MAAM,mBAAmB,SAAwE;EAC/F,IAAI;GACF,MAAM,QAAQ,aAAa,UAAU,IAAA,IAAA,4wCAAA,KAAA,YAAA,GAAA,CAA8C;GACnF,MAAM,OAAO,IAAI,iBAAiB,SAAS,gBAAgB;IACzD,gBAAgB;IAChB,iBAAiB;IACjB,cAAc;GAChB,CAAC;GACD,KAAK,KAAK,aAAa,UAAsC,KAAK,SAAS,MAAM,IAAI;GACrF,OAAO;EACT,SAAS,KAAK;GACZ,IAAI,KAAK,kEAAkE,EAAE,IAAI,GAAA;IAAA,YAAA;IAAA,GAAAE;IAAA,GAAA;IAAA,GAAA;GAAA,CAAC;GAClF,MAAM,OAAO,QAAQ,sBAAsB,8BAA8B,GAAG,CAAC;GAC7E,KAAK,kBAAkB,UAAU,KAAK,SAAS,IAAI,aAAa,MAAM,YAAY,eAAe,CAAC,CAAC,CAAC;GACpG,OAAO;EACT;CACF;CAEA,SAAS,QAA4B;EACnC,KAAK,SAAS,KAAK,MAAM;EACzB,KAAK,kBAAkB,OAAO;CAChC;;;;;;CAOA,SAAe;EACb,IAAI,KAAK,mBAAmB,KAAK,CAAC,KAAK,UACrC;EAGF,MAAM,SAAS,KAAK;EACpB,MAAM,SAAS,KAAK;EACpB,KAAK,WAAW,CAAC;EACjB,KAAK,iBAAiB;EAEtB,MAAM,UAAU,IAAI,aAAa,MAAM;EACvC,IAAI,SAAS;EACb,KAAK,MAAM,SAAS,QAAQ;GAC1B,KAAK,IAAI,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS;IAEjD,MAAM,QAAQ,KAAK,IAAI,IAAI,KAAK,IAAI,GAAG,MAAM,MAAM,CAAC;IACpD,QAAQ,SAAS,SAAS,KAAK,MAAM,QAAQ,WAAW;GAC1D;GACA,UAAU,MAAM;EAClB;EAEA,MAAM,YAAY,KAAK;EACvB,KAAK,kBAAkB,KAAK,IAAI;EAChC,KAAK,SAAS;GAAE;GAAW,MAAM;EAAQ,CAAC;CAC5C;AACF;AA3HGC,WAAA,CAAA,YAAA,GAAA,oBAAA,WAAA,SAAA,IAAA;AAyCAA,WAAA,CAAA,YAAA,GAAA,oBAAA,WAAA,QAAA,IAAA;;;AC3GH,IAAM,kBAAkB;AAExB,IAAM,yBAAyB;AAE/B,IAAM,iCAAiC;;;;;;AAmBvC,IAAa,qBAAqB,EAChC,kBACA,YACA,mBACA,gBACA,iBAEA,IAAI,YAAY;CACd,QAAQ;EACN,6BAA6B;EAC7B,0BAA0B;EAC1B,GAAG;CACL;CACA,UAAU,IAAI,oBAAoB;EAChC,kBAAkB;EAClB,QAAQ;GAAE,UAAU;GAAiB,GAAG;EAAe;CACzD,CAAC;CACD;CACA;AACF,CAAC;;;;;AC5CH,IAAM,qBAAqB;;AAG3B,IAAM,cAAc;AAMpB,IAAM,kBAAmB,WAA2B,WAAW,MAAM;;AAGrE,IAAM,eAAe,YAA8C;CACjE,IAAI;EACF,MAAM,SAAS,KAAK,OAAO;EAC3B,IAAI,OAAO,SAAS,aAAa,sBAAsB,GACrD;EAEF,MAAM,QAAQ,IAAI,WAAW,OAAO,MAAM;EAC1C,KAAK,IAAI,QAAQ,GAAG,QAAQ,OAAO,QAAQ,SACzC,MAAM,SAAS,OAAO,WAAW,KAAK;EAGxC,OAAO,IAAI,aAAa,MAAM,MAAM;CACtC,QAAQ;EACN;CACF;AACF;AAEA,IAAI,YAAY;;AAEhB,IAAI,YAAY;;;;;;;;;;;;;;;;;;AAmBhB,IAAa,0BAA0B,YAA8B;CACnE,MAAM,SAAS,UAAU;CACzB,IAAI,CAAC,UAAU,CAAC,UAAU,cAAc,cACtC,OAAO;CAET,IAAI,aAAa,WACf,OAAO;CAGT,IAAI;EACF,MAAM,OAAO,yBAAyB;CACxC,SAAS,KAAK;EAEZ,IAAI,iCAAiC,EAAE,IAAI,GAAA;GAAA,YAAA;GAAA,GAAA;GAAA,GAAA;GAAA,GAAA,KAAA;EAAA,CAAC;EAC5C,OAAO;CACT;CACA,YAAY;CAGZ,IAAI,WACF,OAAO;CAGT,IAAI;CACJ,IAAI;EACF,UAAU,IAAI,aAAa,EAAE,YAAY,mBAAmB,CAAC;EAC7D,IAAI,QAAQ,UAAU,aACpB,MAAM,QAAQ,OAAO;EAEvB,MAAM,QAAQ,aAAa,UAAU,IAAA,IAAA,49DAAA,KAAA,YAAA,GAAA,CAAiD;EAEtF,MAAM,OAAO,IAAI,iBAAiB,SAAS,yBAAyB;GAClE,gBAAgB;GAChB,oBAAoB,CAAC,CAAC;EACxB,CAAC;EACD,MAAM,cAAc,QAAQ,6BAA6B;EACzD,KAAK,QAAQ,WAAW;EAExB,WAAW,iBAAiB,cAAc,UAAU;GAClD,IAAI,EAAE,iBAAiB,cACrB;GAEF,MAAM,SAAkB,MAAM;GAC9B,IAAI,OAAO,WAAW,YAAY,OAAO,SAAS,GAAG;IACnD,MAAM,SAAS,YAAY,MAAM;IACjC,IAAI,QACF,KAAK,KAAK,YAAY,QAAQ,CAAC,OAAO,MAAM,CAAC;GAEjD;EACF,CAAC;EAID,MAAM,WAAW,UAAU,aAAa,aAAa,KAAK,UAAU,YAAY;EAChF,UAAU,aAAa,eAAe,OAAO,gBAAyC;GACpF,IAAI,CAAC,aAAa,OAChB,OAAO,SAAS,WAAW;GAE7B,IAAI,YAAY,OAAO;IACrB,MAAM,QAAQ,MAAM,SAAS;KAAE,GAAG;KAAa,OAAO;IAAM,CAAC;IAC7D,KAAK,MAAM,SAAS,YAAY,OAAO,eAAe,GAEpD,MAAM,SAAS,MAAM,MAAM,CAAC;IAE9B,OAAO;GACT;GACA,OAAO,IAAI,YAAY,YAAY,OAAO,eAAe,CAAC,CAAC,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC;EAC1F;EAEA,YAAY;EACZ,IAAI,KAAK,+BAA4B,KAAA,GAAA;GAAA,YAAA;GAAA,GAAA;GAAA,GAAA;GAAA,GAAA,KAAA;EAAA,CAAC;EACtC,OAAO;CACT,SAAS,KAAK;EAGZ,YAAY;EACZ,MAAM,OAAO,wBAAwB,CAAC,CAAC,OAAO,YAAY;GACxD,YAAY;GACZ,IAAI,KAAK,2DAA2D,EAAE,KAAK,QAAQ,GAAA;IAAA,YAAA;IAAA,GAAA;IAAA,GAAA;IAAA,GAAA,KAAA;GAAA,CAAC;EACtF,CAAC;EACD,MAAM,SAAS,MAAM,CAAC,CAAC,OAAO,aAAa;GACzC,IAAI,KAAK,4CAA4C,EAAE,KAAK,SAAS,GAAA;IAAA,YAAA;IAAA,GAAA;IAAA,GAAA;IAAA,GAAA,KAAA;GAAA,CAAC;EACxE,CAAC;EACD,MAAM;CACR;AACF;;AAGA,IAAa,uBAAuB,YAA2B;CAC7D,MAAM,SAAS,UAAU;CACzB,IAAI,CAAC,QACH;CAEF,IAAI;EACF,MAAM,OAAO,wBAAwB;EACrC,YAAY;CACd,SAAS,KAAK;EACZ,IAAI,wCAAwC,EAAE,IAAI,GAAA;GAAA,YAAA;GAAA,GAAA;GAAA,GAAA;GAAA,GAAA,KAAA;EAAA,CAAC;CACrD;AACF"}
|
package/dist/lib/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/components/MicSettings/MicSettings.tsx","../../src/components/PipelineStatus/PipelineStatus.tsx","../../src/model/transcript-model.ts","../../src/components/Transcription/transcription-extension.ts","../../src/components/Transcription/Transcription.tsx","../../src/hooks/useAudioFile.ts","../../src/hooks/useAudioTrack.ts","../../src/hooks/useFeedModelAdapter.ts","../../src/hooks/useIsSpeaking.ts","../../src/hooks/useRecordingPipeline.ts","../../src/hooks/useSpeechRecognition.ts","../../src/hooks/useTranscriber.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport React from 'react';\n\nimport * as Button from '@dxos/react-ui/Button';\nimport * as Hooks from '@dxos/react-ui/Hooks';\n\nimport { type AudioInputDevice } from '../../capture/index.ts';\nimport { translationKey } from '../../translations.ts';\n\n/** Recording trigger mode: toggle on/off, or hold-to-record (push-to-talk). */\nexport type RecordMode = 'toggle' | 'hold';\n\nexport type MicSettingsProps = {\n recordMode: RecordMode;\n entityExtraction: boolean;\n devices: AudioInputDevice[];\n /** Empty string selects the system default device. */\n selectedDeviceId: string;\n onRecordModeChange: (mode: RecordMode) => void;\n onEntityExtractionChange: (value: boolean) => void;\n onSelectDevice: (deviceId: string) => void;\n};\n\n/**\n * Recording options for the mic: record mode (toggle/push-to-talk), the input device, and whether\n * entity extraction is applied. Presentational — state lives in the consumer.\n *\n * The menu itself is `MenuButton`: what is specific here is which options exist, not how a caret\n * opens a list of them.\n */\nexport const MicSettings = ({\n recordMode,\n entityExtraction,\n devices,\n selectedDeviceId,\n onRecordModeChange,\n onEntityExtractionChange,\n onSelectDevice,\n}: MicSettingsProps) => {\n const { t } = Hooks.useTranslation(translationKey);\n\n const items: Button.MenuItem[] = [\n { type: 'group', label: t('record-mode.label') },\n {\n type: 'option',\n label: t('record-mode.toggle.label'),\n selected: recordMode === 'toggle',\n onSelect: () => onRecordModeChange('toggle'),\n },\n {\n type: 'option',\n label: t('record-mode.hold.label'),\n selected: recordMode === 'hold',\n onSelect: () => onRecordModeChange('hold'),\n },\n {\n type: 'separator',\n },\n {\n type: 'group',\n label: t('audio-device.label'),\n },\n {\n type: 'option',\n label: t('audio-device.default.label'),\n selected: selectedDeviceId === '',\n onSelect: () => onSelectDevice(''),\n },\n ...devices.map((device): Button.MenuItem => ({\n type: 'option',\n label: device.label,\n selected: selectedDeviceId === device.deviceId,\n onSelect: () => onSelectDevice(device.deviceId),\n })),\n { type: 'separator' },\n {\n type: 'checkbox',\n label: t('settings.entity-extraction.label'),\n checked: entityExtraction,\n onCheckedChange: onEntityExtractionChange,\n testId: 'transcription.entity-extraction',\n },\n ];\n\n return (\n <Button.Menu\n icon='ph--caret-down--regular'\n iconOnly\n compact\n variant='ghost'\n label={t('recording-options.label')}\n data-testid='transcription.record.options'\n items={items}\n />\n );\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport React, { Fragment } from 'react';\n\nimport { type TelemetryEvent } from '@dxos/pipeline-transcription';\n\nimport { type PipelinePhase } from '../../types.ts';\n\nexport type PipelineStatusProps = {\n /** Live lifecycle phase published by the transcription driver. */\n phase?: PipelinePhase;\n /** Ordered stage ids being run (e.g. correct → extract → summarize). */\n stages?: readonly string[];\n /** Per-stage telemetry emitted by `PipelineRuntime`. */\n telemetry?: readonly TelemetryEvent[];\n /** Cumulative summary produced by the summarization stage. */\n summary?: string;\n};\n\n/**\n * Read-only status panel reflecting the transcription/pipeline state: mic + pipeline phase, the\n * active stage list, per-stage telemetry, and the running summary. Driven by the `PipelineStatus`\n * capability (phase) and `PipelineRuntime` telemetry.\n */\nexport const PipelineStatus = ({ phase = 'idle', stages, telemetry, summary }: PipelineStatusProps) => (\n <div className='flex flex-col gap-3 overflow-y-auto p-2 text-sm'>\n <Section title='Status'>\n <div className='grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-fg-muted'>\n <span>Mic</span>\n <span>{phase === 'recording' ? '● on' : '○ off'}</span>\n <span>Pipeline</span>\n <span>{phase}</span>\n </div>\n </Section>\n\n {stages && (\n <Section title='Stages'>\n <div className='text-fg-muted'>{stages.join(' → ') || '(none)'}</div>\n </Section>\n )}\n\n {telemetry && telemetry.length > 0 && (\n <Section title='Telemetry'>\n <div className='grid w-fit grid-cols-[1fr_auto_auto] gap-x-4 gap-y-1'>\n {telemetry.map((event, index) => (\n <Fragment key={index}>\n <span>{event.stageId}</span>\n <span className='text-fg-muted'>{event.outcome}</span>\n <span className='text-right tabular-nums text-fg-muted'>{event.durationMs}ms</span>\n </Fragment>\n ))}\n </div>\n </Section>\n )}\n\n {summary && (\n <Section title='Summary'>\n <p className='text-fg-muted'>{summary}</p>\n </Section>\n )}\n </div>\n);\n\nconst Section = ({ title, children }: { title: string; children: React.ReactNode }) => (\n <div>\n <h3 className='mb-1 font-medium'>{title}</h3>\n {children}\n </div>\n);\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { Text } from '@codemirror/state';\nimport { type EditorView } from '@codemirror/view';\n\nimport { Event } from '@dxos/async';\nimport { invariant } from '@dxos/invariant';\nimport { log } from '@dxos/log';\n\nexport type Chunk = { id: string };\n\n/**\n * Covert block to a set of markdown lines.\n */\nexport type ChunkRenderer<T extends Chunk> = (chunk: T, index: number, debug?: boolean) => string[];\n\n/**\n * Abstract representation of the document model.\n */\nexport interface ChunkDocument {\n lineCount(): number;\n replaceLines(from: number, remove: number, insert: string[]): void;\n}\n\n/**\n * CodeMirror-backed {@link ChunkDocument}.\n */\nexport class EditorChunkDocument implements ChunkDocument {\n constructor(private readonly _view: EditorView) {}\n\n /** The document must always have at least one line. */\n lineCount(): number {\n return this._view.state.doc.lines - 1;\n }\n\n replaceLines(from: number, remove: number, insert: string[]): void {\n log('replaceLines', { count: this._view.state.doc.lines, from, remove, insert });\n\n const numLines = this._view.state.doc.lines;\n const posFrom = this._view.state.doc.line(from).from;\n const posTo = remove === 0 ? posFrom : this._view.state.doc.line(Math.min(numLines, from + remove)).from;\n\n // Don't remove the last line.\n let text = insert.join('\\n');\n if (posTo === this._view.state.doc.length) {\n text += '\\n';\n }\n\n this._view.dispatch({\n changes: { from: posFrom, to: posTo, insert: text },\n });\n }\n}\n\ntype ChunkChange<T extends Chunk> =\n | {\n type: 'append';\n index: number;\n chunk: T;\n }\n | {\n type: 'update';\n index: number;\n chunk: T;\n }\n | {\n type: 'delete';\n index: number;\n id: string;\n };\n\n/**\n * Each Chunk can be converted into a set of lines.\n * Chunks can be added, updated or deleted from the Queue.\n * A Document is an immutable projection that represents a flat list of lines corresponding to the Queue.\n *\n * Ideally we would implement a custom virtual Text model for the View, but this currently isn't possible in Codemirror.\n * Instead this model tracks changes and syncs them with the ChunkDocument.\n */\nexport class TranscriptModel<T extends Chunk> {\n /** Emits when the document is updated. */\n public readonly update = new Event<void>();\n\n /** Ordered set of chunks. */\n private readonly _chunks: T[] = [];\n\n /** Map of chunk IDs to their current line counts. */\n private readonly _chunkLineCounts = new Map<string, number>();\n\n /** Map of line numbers to chunk IDs. */\n private readonly _lineToChunk = new Map<number, T>();\n\n /** Track chunk changes since last sync. */\n private readonly _changes: Array<ChunkChange<T>> = [];\n\n constructor(\n private readonly _renderer: ChunkRenderer<T>,\n initialChunks: T[] = [],\n ) {\n initialChunks.forEach((chunk) => {\n this.appendChunk(chunk);\n });\n }\n\n toJSON(): { chunks: number; changes: number } {\n return {\n chunks: this._chunks.length,\n changes: this._changes.length,\n };\n }\n\n get doc() {\n return Text.of([...this._chunks.flatMap((chunk, index) => this._renderer(chunk, index)), '']);\n }\n\n get chunks() {\n return this._chunks;\n }\n\n /**\n * Get the block ID at the given line number.\n */\n getChunkAtLine(line: number): T | undefined {\n return this._lineToChunk.get(line);\n }\n\n reset(): this {\n this._chunks.length = 0;\n this._chunkLineCounts.clear();\n this._lineToChunk.clear();\n this.update.emit();\n return this;\n }\n\n appendChunk(chunk: T): this {\n const index = this._chunks.length;\n this._chunks.push(chunk);\n this._changes.push({ type: 'append', index, chunk });\n this.update.emit();\n return this;\n }\n\n updateChunk(chunk: T): this {\n const index = this._chunks.findIndex((c) => c.id === chunk.id);\n invariant(index !== -1);\n this._chunks[index] = chunk;\n this._changes.push({ type: 'update', index, chunk });\n this.update.emit();\n return this;\n }\n\n deleteBlock(id: string): this {\n const index = this._chunks.findIndex((c) => c.id === id);\n invariant(index !== -1);\n this._chunks.splice(index, 1);\n this._changes.push({ type: 'delete', index, id });\n this.update.emit();\n return this;\n }\n\n /**\n * Syncs the tracked changes with the ChunkDocument.\n */\n sync(document: ChunkDocument): this {\n log('sync', { changes: this._changes });\n\n // Process each change in order.\n for (const change of this._changes) {\n switch (change.type) {\n case 'append': {\n // For appends, convert block to lines and add at the end.\n const lines = this._renderer(change.chunk, change.index);\n const lastLine = document.lineCount();\n document.replaceLines(lastLine + 1, 0, lines);\n break;\n }\n\n case 'update': {\n // For updates, find the block's line range and replace with new lines.\n let lineStart = 1;\n for (let i = 0; i < change.index; i++) {\n lineStart += this._chunkLineCounts.get(this._chunks[i].id)!;\n }\n const previousLineCount = this._chunkLineCounts.get(change.chunk.id)!;\n const newLines = this._renderer(change.chunk, change.index);\n document.replaceLines(lineStart, previousLineCount, newLines);\n break;\n }\n\n case 'delete': {\n // For deletes, find the block's line range and remove those lines.\n let lineStart = 1;\n for (let i = 0; i < change.index; i++) {\n lineStart += this._chunkLineCounts.get(this._chunks[i].id)!;\n }\n const previousLineCount = this._chunkLineCounts.get(change.id)!;\n document.replaceLines(lineStart, previousLineCount, []);\n break;\n }\n }\n }\n\n // Clear the line-to-block mapping.\n this._lineToChunk.clear();\n\n // Update all block line counts and line-to-block mapping after processing changes.\n let currentLine = 1;\n for (const [index, chunk] of this._chunks.entries()) {\n const lineCount = this._renderer(chunk, index).length;\n this._chunkLineCounts.set(chunk.id, lineCount);\n this._lineToChunk.set(currentLine, chunk);\n currentLine += lineCount;\n }\n\n // Clean up deleted blocks.\n const chunkIds = new Set(this._chunks.map((chunk) => chunk.id));\n for (const id of this._chunkLineCounts.keys()) {\n if (!chunkIds.has(id)) {\n this._chunkLineCounts.delete(id);\n }\n }\n\n // Clear changes after sync.\n this._changes.length = 0;\n return this;\n }\n}\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { type Extension, RangeSetBuilder } from '@codemirror/state';\nimport { EditorView, GutterMarker, ViewPlugin, type ViewUpdate, gutter } from '@codemirror/view';\nimport { format } from 'date-fns/format';\nimport { intervalToDuration } from 'date-fns/intervalToDuration';\n\nimport { type CleanupFn, addEventListener, combine } from '@dxos/async';\nimport { type Message } from '@dxos/types';\n\nimport { EditorChunkDocument, type TranscriptModel } from '../../model/index.ts';\n\n/**\n * Data structure that maps Chunks queue to lines with transcript state.\n */\nexport type TranscriptionOptions = {\n model: TranscriptModel<Message.Message>;\n started?: Date;\n};\n\n/**\n * Scrolling transcript with timestamps.\n */\nexport const transcription = ({ model, started }: TranscriptionOptions): Extension => {\n return [\n // Show timestamps in the gutter.\n gutter({\n class: 'cm-timestamp-gutter',\n lineMarkerChange: (update) => update.docChanged || update.viewportChanged,\n markers: (view) => {\n const start = getStartTime(started, model.chunks[0]);\n const builder = new RangeSetBuilder<GutterMarker>();\n for (const { from, to } of view.visibleRanges) {\n let line = view.state.doc.lineAt(from);\n while (line.from <= to) {\n const block = model.getChunkAtLine(line.number)?.blocks[0];\n const timestamp = block?._tag === 'transcript' && block.started;\n if (timestamp) {\n builder.add(line.from, line.from, new TimestampMarker(line.number, new Date(timestamp), start));\n }\n\n if (line.to + 1 > view.state.doc.length) {\n break;\n }\n\n line = view.state.doc.lineAt(line.to + 1);\n }\n }\n\n return builder.finish();\n },\n }),\n\n // Listen for model updates.\n ViewPlugin.fromClass(\n class {\n private readonly _controls?: HTMLDivElement;\n private readonly _cleanup: CleanupFn;\n private readonly _adapter: EditorChunkDocument;\n private _initialized = false;\n\n constructor(view: EditorView) {\n this._adapter = new EditorChunkDocument(view);\n\n const scroller = view.scrollDOM;\n let isAutoScrolling = false;\n let hasScrolled = false;\n\n let timeout: NodeJS.Timeout | undefined;\n const scrollToBottom = (smooth = false) => {\n scroller.style.scrollBehavior = smooth ? 'smooth' : '';\n\n // Temporarily hide scrollbar to prevent flicker.\n scroller.classList.add('cm-hide-scrollbar');\n isAutoScrolling = true;\n clearTimeout(timeout);\n timeout = setTimeout(() => {\n this._controls?.classList.add('opacity-0');\n scroller.classList.remove('cm-hide-scrollbar');\n isAutoScrolling = false;\n }, 500);\n\n // Scroll to bottom.\n view.dispatch({\n effects: EditorView.scrollIntoView(view.state.doc.length, { y: 'end' }),\n });\n };\n\n // Event listeners.\n this._cleanup = combine(\n addEventListener(view.scrollDOM, 'scroll', () => {\n if (!isAutoScrolling) {\n hasScrolled = true;\n this._controls?.classList.remove('opacity-0');\n }\n }),\n\n model.update.on(() => {\n // Check if clamped to bottom.\n const autoScroll =\n scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight === 0 || !hasScrolled;\n\n // Sync.\n model.sync(this._adapter);\n\n // Scroll.\n if (autoScroll) {\n scrollToBottom(true);\n }\n }),\n );\n }\n\n update(_update: ViewUpdate) {\n // Initial sync.\n if (!this._initialized) {\n this._initialized = true;\n setTimeout(() => {\n model.sync(this._adapter);\n });\n }\n }\n\n destroy() {\n this._controls?.remove();\n this._cleanup();\n }\n },\n ),\n\n EditorView.theme({\n '.cm-gutters': {\n backgroundColor: 'var(--color-base-surface)',\n },\n '.cm-timestamp-gutter': {\n width: '6rem',\n paddingLeft: '0.5rem',\n paddingRight: '1rem',\n },\n '.cm-timestamp-gutter > .cm-gutterElement > div': {\n display: 'inline-flex',\n textAlign: 'right',\n padding: '3px',\n },\n }),\n ];\n};\n\n/**\n * Gutter marker that displays a timestamp.\n */\nclass TimestampMarker extends GutterMarker {\n constructor(\n readonly _line: number,\n readonly _timestamp: Date,\n readonly _started?: Date,\n ) {\n super();\n }\n\n override eq(other: this) {\n return this._timestamp === other._timestamp;\n }\n\n override toDOM(view: EditorView) {\n const el = document.createElement('div');\n el.className = 'text-sm text-fg-subtle hover:bg-hover-surface cursor-pointer';\n el.textContent = formatTimestamp(this._timestamp, this._started);\n // TODO(burdon): Click to bookmark or copy hyperlink.\n el.onclick = () => {\n const pos = view.state.doc.line(this._line).from;\n view.dispatch({\n effects: EditorView.scrollIntoView(pos, { y: 'start' }),\n });\n };\n\n return el;\n }\n}\n\nconst getStartTime = (started?: Date, message?: Message.Message): Date | undefined => {\n if (started) {\n return started;\n }\n\n if (message?.blocks[0]?._tag === 'transcript' && message.blocks[0].started) {\n return new Date(message.blocks[0].started);\n }\n\n return undefined;\n};\n\nconst formatTimestamp = (timestamp: Date, relative?: Date) => {\n if (relative) {\n const pad = (n = 0) => String(n).padStart(2, '0');\n const { hours, minutes, seconds } = intervalToDuration({ start: relative, end: timestamp });\n return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;\n } else {\n return format(timestamp, 'HH:mm:ss');\n }\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport React from 'react';\n\nimport { useTextEditor } from '@dxos/react-ui-editor';\nimport * as Hooks from '@dxos/react-ui/Hooks';\nimport * as Util from '@dxos/react-ui/Util';\nimport { type Message, type Transcript } from '@dxos/types';\nimport {\n createBasicExtensions,\n createMarkdownExtensions,\n createThemeExtensions,\n decorateMarkdown,\n documentSlots,\n objectLinks,\n scroller,\n} from '@dxos/ui-editor';\n\nimport { type TranscriptModel } from '../../model/index.ts';\nimport { transcription } from './transcription-extension.ts';\n\nexport type TranscriptionProps = {\n transcript?: Transcript.Transcript;\n model: TranscriptModel<Message.Message>;\n};\n\n// TODO(burdon): Rename Transcript.\nexport const Transcription = Util.composable<HTMLDivElement, TranscriptionProps>(\n ({ transcript: object, model, children, ...props }, forwardedRef) => {\n const themeMode = Hooks.useThemeMode();\n const { parentRef } = useTextEditor(() => {\n return {\n extensions: [\n createBasicExtensions({ readOnly: true, lineWrapping: true, search: true }),\n createThemeExtensions({ themeMode, slots: documentSlots }),\n createMarkdownExtensions(),\n decorateMarkdown(),\n objectLinks(),\n transcription({ model, started: object?.started ? new Date(object.started) : undefined }),\n scroller(),\n ],\n };\n }, [model, themeMode]);\n\n return (\n <div\n {...Util.composableProps(props, { classNames: 'dx-expand' })}\n data-popover-collision-boundary={true}\n ref={parentRef}\n />\n );\n },\n);\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { useEffect, useState } from 'react';\n\nimport { scheduleTask } from '@dxos/async';\nimport { Context } from '@dxos/context';\nimport { log } from '@dxos/log';\n\nexport type UseAudioState = {\n audio?: HTMLAudioElement;\n stream?: MediaStream;\n track?: MediaStreamTrack;\n};\n\nexport const useAudioFile = (audioUrl: string, constraints?: MediaTrackConstraints): UseAudioState => {\n const [{ audio, stream, track }, setStream] = useState<UseAudioState>({});\n useEffect(() => {\n const ctx = new Context();\n\n scheduleTask(ctx, async () => {\n // TODO(wittjosiah): Fetch to external url fails in headless storybook test.\n try {\n const response = await fetch(audioUrl);\n const blob = await response.blob();\n const objectUrl = URL.createObjectURL(blob);\n ctx.onDispose(() => {\n URL.revokeObjectURL(objectUrl);\n });\n\n const audio = new Audio();\n audio.src = objectUrl;\n\n await new Promise<void>((resolve, reject) => {\n audio.addEventListener(\n 'error',\n (err) => {\n log.error('error', { err });\n reject(err);\n },\n { once: true },\n );\n audio.addEventListener(\n 'canplay',\n async () => {\n resolve();\n },\n { once: true },\n );\n audio.load();\n });\n\n const audioCtx = new AudioContext();\n\n // Resume AudioContext if it's suspended.\n if (audioCtx.state === 'suspended') {\n await audioCtx.resume();\n }\n\n const destination = audioCtx.createMediaStreamDestination();\n destination.channelCount = 1;\n\n const source = audioCtx.createMediaElementSource(audio);\n source.connect(destination);\n const track = destination.stream.getAudioTracks()[0];\n if (constraints) {\n await track.applyConstraints(constraints).catch((err) => log.catch(err));\n }\n\n // Also connect to speakers so audio is audible.\n setStream({\n audio,\n stream: destination.stream,\n track: destination.stream.getAudioTracks()[0],\n });\n } catch (err) {\n log.catch(err);\n }\n });\n\n return () => {\n void ctx.dispose();\n };\n }, [audioUrl]);\n\n return { audio, stream, track };\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { useEffect, useState } from 'react';\n\n/**\n * Acquires a microphone audio track while `active`. Re-acquires when the `constraints` change (e.g.\n * a different input device is selected) so the new device takes effect without a remount.\n */\n// TODO(burdon): Reconcile with react-ui-experimental and plugin-calls.\nexport const useAudioTrack = (active?: boolean, constraints?: MediaTrackConstraints): MediaStreamTrack | undefined => {\n const [track, setTrack] = useState<MediaStreamTrack>();\n // Re-acquire on a value change, not on every re-created constraints object identity.\n const constraintsKey = constraints ? JSON.stringify(constraints) : '';\n useEffect(() => {\n if (!active) {\n track?.stop();\n setTrack(undefined);\n return;\n }\n\n let cancelled = false;\n let acquired: MediaStreamTrack | undefined;\n // Drop any previous (now-stopped) track while reacquiring, so callers never hold a dead track.\n setTrack(undefined);\n const initAudio = async () => {\n try {\n const stream = await navigator.mediaDevices.getUserMedia({ audio: constraints ?? true });\n const [next] = stream.getAudioTracks();\n if (cancelled || !next) {\n next?.stop();\n return;\n }\n acquired = next;\n setTrack(next);\n } catch {\n // Permission denied / device unavailable: leave the track cleared rather than rejecting.\n if (!cancelled) {\n setTrack(undefined);\n }\n }\n };\n\n void initAudio();\n\n return () => {\n cancelled = true;\n acquired?.stop();\n };\n // `constraints` is read via the serialized `constraintsKey`.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [active, constraintsKey]);\n\n return track;\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { useEffect, useMemo } from 'react';\n\nimport type { Obj } from '@dxos/echo';\n\nimport { type ChunkRenderer, TranscriptModel } from '../model/index.ts';\n\n/**\n * Feeds an array of objects (typically from `useQuery(db, Query.from(feed))`) into a\n * `TranscriptModel`, appending each new tail item as the array grows.\n *\n * @param renderer - chunk renderer.\n * @param objects - current snapshot of feed items.\n * @param initialChunks - chunks to seed the model with on first render.\n */\nexport const useFeedModelAdapter = <T extends Obj.Unknown>(\n renderer: ChunkRenderer<T>,\n objects: readonly T[],\n initialChunks: T[] = [],\n): TranscriptModel<T> => {\n const model = useMemo(() => new TranscriptModel<T>(renderer, initialChunks), [renderer]);\n\n // Seed the model with the current snapshot on first render and whenever the\n // identity of `objects` changes (e.g. feed swapped).\n useEffect(() => {\n for (const chunk of objects) {\n model.appendChunk(chunk);\n }\n }, [model, objects]);\n\n // Append new tail items as the array grows.\n useEffect(() => {\n if (model.chunks.length === objects.length) {\n return;\n }\n const chunk = objects.at(-1);\n if (chunk) {\n model.appendChunk(chunk);\n }\n }, [model, objects.length]);\n\n return model;\n};\n","//\n// Copyright 2024 DXOS.org\n//\n\nimport { useEffect, useMemo, useState } from 'react';\n\nimport { scheduleTask } from '@dxos/async';\nimport { SpeakingMonitor } from '@dxos/av';\nimport { Context } from '@dxos/context';\n\n/**\n * Tracks voice activity on an audio track via {@link SpeakingMonitor}.\n */\nexport const useIsSpeaking = (track?: MediaStreamTrack) => {\n const [isSpeaking, setIsSpeaking] = useState(false);\n const speakingMonitor = useMemo(() => {\n if (!track) {\n return;\n }\n\n return new SpeakingMonitor(track);\n }, [track]);\n\n useEffect(() => {\n if (!speakingMonitor) {\n return;\n }\n\n const ctx = new Context();\n scheduleTask(ctx, async () => {\n speakingMonitor.speakingChanged.on(ctx, () => setIsSpeaking(speakingMonitor.isSpeaking));\n await speakingMonitor.open();\n ctx.onDispose(() => speakingMonitor.close());\n });\n\n return () => {\n void ctx.dispose();\n };\n }, [speakingMonitor]);\n\n return isSpeaking;\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { useEffect, useMemo, useRef, useState } from 'react';\n\nimport { log } from '@dxos/log';\nimport {\n type AsrPipeline,\n type CommitFn,\n type EntityLookup,\n type Stage,\n type TranscribeConfig,\n runAsrPipeline,\n} from '@dxos/pipeline-transcription';\nimport { type ContentBlock } from '@dxos/types';\n\nimport { MediaStreamRecorder } from '../capture/index.ts';\nimport { type PipelinePhase } from '../types.ts';\nimport { useAudioTrack } from './useAudioTrack.ts';\n\n// Recorder chunk interval; the transcriber's chunk threshold derives from this.\nconst RECORDER_INTERVAL_MS = 200;\n\n// Silence reported on drain so on-silence stages (e.g. summarization) fire over the final window.\nconst DRAIN_SILENCE_MS = 5_000;\n\nconst DEFAULT_TRANSCRIBE_CONFIG: TranscribeConfig = {\n transcribeAfterChunksAmount: 50,\n prefixBufferChunksAmount: 10,\n};\n\nexport type RecordingPipelineOptions = {\n /**\n * Transcriber configuration. Must carry `endpoint` (there is no built-in one); a transcriber\n * without it rejects on `open()` with `TranscriptionEndpointNotConfiguredError`.\n */\n config?: Partial<TranscribeConfig>;\n /** Re-segment ASR output into complete sentences before the pipeline (merges mid-sentence cuts). */\n segmentSentences?: boolean;\n /**\n * Raw capture flag — recording (mic) or playing (file). When it flips false the hook drains\n * (flushing the transcriber's buffered audio) before returning to idle.\n */\n active: boolean;\n /** Use the microphone as the ASR source; the hook owns the track and keeps it alive through drain. */\n microphone?: boolean;\n /** Externally-supplied track (file source). Used only when `microphone` is false. */\n track?: MediaStreamTrack;\n /** Audio constraints for the microphone source. */\n audioConstraints?: MediaTrackConstraints;\n /** Ordered pipeline stages applied to captured blocks. */\n stages: readonly Stage<any, any>[];\n /** Commit sink for enriched stage output. */\n commit: CommitFn;\n /** Optional entity-reference resolver injected into stages. */\n lookup?: EntityLookup;\n /** Optional raw-segment hook (pre-pipeline), e.g. for live display before enrichment. */\n onSegment?: (block: ContentBlock.Transcript) => void;\n};\n\nexport type RecordingPipeline = {\n /** Lifecycle phase (idle → recording → draining → idle). */\n phase: PipelinePhase;\n};\n\n/**\n * Single ASR-capture engine binding for React consumers: drives an {@link AsrPipeline}\n * ({@link runAsrPipeline}) off a capture flag, sourcing audio from the microphone (owned here) or an\n * external track. The editor driver, message-list recording, and stories all build on this — varying\n * only by source, stages, and commit sink.\n */\nexport const useRecordingPipeline = ({\n active,\n microphone,\n track: externalTrack,\n audioConstraints,\n stages,\n commit,\n lookup,\n onSegment,\n config,\n segmentSentences,\n}: RecordingPipelineOptions): RecordingPipeline => {\n // Phase machine: hold 'recording' for the commit when `active` flips off, then 'draining' until the\n // transcriber flushes, then 'idle' — so the tail of speech and its enrichment are not lost.\n const [phase, setPhase] = useState<PipelinePhase>('idle');\n const inUse = phase !== 'idle';\n useEffect(() => {\n setPhase((current) => (active ? 'recording' : current === 'recording' ? 'draining' : current));\n }, [active]);\n\n // Keep the mic alive through the drain; external (file) tracks are owned by the caller.\n const micTrack = useAudioTrack(!!microphone && inUse, audioConstraints);\n const track = microphone ? micTrack : externalTrack;\n\n // A fresh recorder per track so a stopped (encoder-flushed) instance is never reused.\n const recorder = useMemo(\n () =>\n track\n ? new MediaStreamRecorder({ mediaStreamTrack: track, config: { interval: RECORDER_INTERVAL_MS } })\n : undefined,\n [track],\n );\n\n const pipelineRef = useRef<AsrPipeline | null>(null);\n\n // Start a live ASR pipeline session when recording. Stages/commit/lookup are captured for the\n // lifetime of the session (a config change mid-recording would orphan the run).\n useEffect(() => {\n if (phase !== 'recording' || !recorder) {\n return;\n }\n const asr = runAsrPipeline({\n recorder,\n config: { ...DEFAULT_TRANSCRIBE_CONFIG, ...config },\n segmentSentences,\n stages,\n commit,\n lookup,\n onSegment,\n drainSilenceMs: DRAIN_SILENCE_MS,\n });\n pipelineRef.current = asr;\n let cancelled = false;\n void asr.open().then(\n () => {\n if (!cancelled) {\n asr.start();\n }\n },\n (err) => {\n if (!cancelled) {\n log.catch(err);\n setPhase('idle');\n }\n },\n );\n return () => {\n cancelled = true;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [phase, recorder]);\n\n // Drain on stop: flush buffered audio, signal silence, end the session.\n useEffect(() => {\n if (phase !== 'draining') {\n return;\n }\n let cancelled = false;\n void (async () => {\n try {\n await pipelineRef.current?.end();\n } catch (err) {\n log.catch(err);\n } finally {\n pipelineRef.current = null;\n if (!cancelled) {\n setPhase((current) => (current === 'draining' ? 'idle' : current));\n }\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [phase]);\n\n return { phase };\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { useEffect, useMemo } from 'react';\n\nimport { log } from '@dxos/log';\n\nconst cleanText = (text: string) =>\n text\n .toLowerCase()\n .replace(/[.,!?]/g, '')\n .replace(/\\s+/g, ' ')\n .trim();\n\nexport type UseSpeechRecognitionProps = {\n active: boolean;\n onTranscript: (transcript: string) => void;\n};\n\n/**\n * Use browser speech recognition API to transcribe speech to text.\n */\nexport const useSpeechRecognition = ({ active, onTranscript }: UseSpeechRecognitionProps) => {\n const recognition = useMemo(() => {\n const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;\n const SpeechGrammarList = (window as any).SpeechGrammarList || (window as any).webkitSpeechGrammarList;\n if (!SpeechRecognition || !SpeechGrammarList) {\n log.error('Speech recognition not supported');\n return;\n }\n\n return Object.assign(new SpeechRecognition(), {\n grammars: new SpeechGrammarList(),\n continuous: true,\n interimResults: true,\n lang: 'en-US',\n maxAlternatives: 1,\n onresult: (event: any) => {\n const transcript = cleanText(event.results[event.resultIndex][0].transcript);\n onTranscript(transcript);\n },\n onerror: (event: any) => {\n log.error('Speech recognition error', { error: event.error });\n },\n });\n }, [onTranscript]);\n\n useEffect(() => {\n if (active) {\n recognition?.start();\n } else {\n recognition?.stop();\n }\n }, [active, recognition]);\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { useEffect, useMemo } from 'react';\n\nimport { type Transcriber } from '@dxos/pipeline-transcription';\n\nimport { type CreateTranscriberOptions, createTranscriber } from '../capture/index.ts';\n\n/**\n * Records audio while the user is speaking and transcribes it after they pause. Builds a\n * {@link Transcriber} directly from {@link createTranscriber} (no app-framework capability needed,\n * since the browser construction lives in this package); returns `undefined` until a track and\n * `onSegments` handler are supplied.\n */\nexport const useTranscriber = ({\n audioStreamTrack,\n recorderConfig,\n transcriberConfig,\n transcribe,\n onSegments,\n}: Partial<CreateTranscriberOptions>): Transcriber | undefined => {\n const transcriber = useMemo<Transcriber | undefined>(() => {\n if (!audioStreamTrack || !onSegments) {\n return undefined;\n }\n\n return createTranscriber({\n audioStreamTrack,\n recorderConfig,\n transcriberConfig,\n transcribe,\n onSegments,\n });\n }, [audioStreamTrack, recorderConfig, transcriberConfig, transcribe, onSegments]);\n\n useEffect(() => {\n return () => {\n void transcriber?.close();\n };\n }, [transcriber]);\n\n return transcriber;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["__dxlog_file","__dxlog_file","__dxlog_file","__dxlog_file"],"sources":["../../src/components/MicSettings/MicSettings.tsx","../../src/components/PipelineStatus/PipelineStatus.tsx","../../src/model/transcript-model.ts","../../src/components/Transcription/transcription-extension.ts","../../src/components/Transcription/Transcription.tsx","../../src/hooks/useAudioFile.ts","../../src/hooks/useAudioTrack.ts","../../src/hooks/useFeedModelAdapter.ts","../../src/hooks/useIsSpeaking.ts","../../src/hooks/useRecordingPipeline.ts","../../src/hooks/useSpeechRecognition.ts","../../src/hooks/useTranscriber.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport React from 'react';\n\nimport * as Button from '@dxos/react-ui/Button';\nimport * as Hooks from '@dxos/react-ui/Hooks';\n\nimport { type AudioInputDevice } from '../../capture/index.ts';\nimport { translationKey } from '../../translations.ts';\n\n/** Recording trigger mode: toggle on/off, or hold-to-record (push-to-talk). */\nexport type RecordMode = 'toggle' | 'hold';\n\nexport type MicSettingsProps = {\n recordMode: RecordMode;\n entityExtraction: boolean;\n devices: AudioInputDevice[];\n /** Empty string selects the system default device. */\n selectedDeviceId: string;\n onRecordModeChange: (mode: RecordMode) => void;\n onEntityExtractionChange: (value: boolean) => void;\n onSelectDevice: (deviceId: string) => void;\n};\n\n/**\n * Recording options for the mic: record mode (toggle/push-to-talk), the input device, and whether\n * entity extraction is applied. Presentational — state lives in the consumer.\n *\n * The menu itself is `MenuButton`: what is specific here is which options exist, not how a caret\n * opens a list of them.\n */\nexport const MicSettings = ({\n recordMode,\n entityExtraction,\n devices,\n selectedDeviceId,\n onRecordModeChange,\n onEntityExtractionChange,\n onSelectDevice,\n}: MicSettingsProps) => {\n const { t } = Hooks.useTranslation(translationKey);\n\n const items: Button.MenuItem[] = [\n { type: 'group', label: t('record-mode.label') },\n {\n type: 'option',\n label: t('record-mode.toggle.label'),\n selected: recordMode === 'toggle',\n onSelect: () => onRecordModeChange('toggle'),\n },\n {\n type: 'option',\n label: t('record-mode.hold.label'),\n selected: recordMode === 'hold',\n onSelect: () => onRecordModeChange('hold'),\n },\n {\n type: 'separator',\n },\n {\n type: 'group',\n label: t('audio-device.label'),\n },\n {\n type: 'option',\n label: t('audio-device.default.label'),\n selected: selectedDeviceId === '',\n onSelect: () => onSelectDevice(''),\n },\n ...devices.map((device): Button.MenuItem => ({\n type: 'option',\n label: device.label,\n selected: selectedDeviceId === device.deviceId,\n onSelect: () => onSelectDevice(device.deviceId),\n })),\n { type: 'separator' },\n {\n type: 'checkbox',\n label: t('settings.entity-extraction.label'),\n checked: entityExtraction,\n onCheckedChange: onEntityExtractionChange,\n testId: 'transcription.entity-extraction',\n },\n ];\n\n return (\n <Button.Menu\n icon='ph--caret-down--regular'\n iconOnly\n compact\n variant='ghost'\n label={t('recording-options.label')}\n data-testid='transcription.record.options'\n items={items}\n />\n );\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport React, { Fragment } from 'react';\n\nimport { type TelemetryEvent } from '@dxos/pipeline-transcription';\n\nimport { type PipelinePhase } from '../../types.ts';\n\nexport type PipelineStatusProps = {\n /** Live lifecycle phase published by the transcription driver. */\n phase?: PipelinePhase;\n /** Ordered stage ids being run (e.g. correct → extract → summarize). */\n stages?: readonly string[];\n /** Per-stage telemetry emitted by `PipelineRuntime`. */\n telemetry?: readonly TelemetryEvent[];\n /** Cumulative summary produced by the summarization stage. */\n summary?: string;\n};\n\n/**\n * Read-only status panel reflecting the transcription/pipeline state: mic + pipeline phase, the\n * active stage list, per-stage telemetry, and the running summary. Driven by the `PipelineStatus`\n * capability (phase) and `PipelineRuntime` telemetry.\n */\nexport const PipelineStatus = ({ phase = 'idle', stages, telemetry, summary }: PipelineStatusProps) => (\n <div className='flex flex-col gap-3 overflow-y-auto p-2 text-sm'>\n <Section title='Status'>\n <div className='grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-fg-muted'>\n <span>Mic</span>\n <span>{phase === 'recording' ? '● on' : '○ off'}</span>\n <span>Pipeline</span>\n <span>{phase}</span>\n </div>\n </Section>\n\n {stages && (\n <Section title='Stages'>\n <div className='text-fg-muted'>{stages.join(' → ') || '(none)'}</div>\n </Section>\n )}\n\n {telemetry && telemetry.length > 0 && (\n <Section title='Telemetry'>\n <div className='grid w-fit grid-cols-[1fr_auto_auto] gap-x-4 gap-y-1'>\n {telemetry.map((event, index) => (\n <Fragment key={index}>\n <span>{event.stageId}</span>\n <span className='text-fg-muted'>{event.outcome}</span>\n <span className='text-right tabular-nums text-fg-muted'>{event.durationMs}ms</span>\n </Fragment>\n ))}\n </div>\n </Section>\n )}\n\n {summary && (\n <Section title='Summary'>\n <p className='text-fg-muted'>{summary}</p>\n </Section>\n )}\n </div>\n);\n\nconst Section = ({ title, children }: { title: string; children: React.ReactNode }) => (\n <div>\n <h3 className='mb-1 font-medium'>{title}</h3>\n {children}\n </div>\n);\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { Text } from '@codemirror/state';\nimport { type EditorView } from '@codemirror/view';\n\nimport { Event } from '@dxos/async';\nimport { invariant } from '@dxos/invariant';\nimport { log } from '@dxos/log';\n\nexport type Chunk = { id: string };\n\n/**\n * Covert block to a set of markdown lines.\n */\nexport type ChunkRenderer<T extends Chunk> = (chunk: T, index: number, debug?: boolean) => string[];\n\n/**\n * Abstract representation of the document model.\n */\nexport interface ChunkDocument {\n lineCount(): number;\n replaceLines(from: number, remove: number, insert: string[]): void;\n}\n\n/**\n * CodeMirror-backed {@link ChunkDocument}.\n */\nexport class EditorChunkDocument implements ChunkDocument {\n constructor(private readonly _view: EditorView) {}\n\n /** The document must always have at least one line. */\n lineCount(): number {\n return this._view.state.doc.lines - 1;\n }\n\n replaceLines(from: number, remove: number, insert: string[]): void {\n log('replaceLines', { count: this._view.state.doc.lines, from, remove, insert });\n\n const numLines = this._view.state.doc.lines;\n const posFrom = this._view.state.doc.line(from).from;\n const posTo = remove === 0 ? posFrom : this._view.state.doc.line(Math.min(numLines, from + remove)).from;\n\n // Don't remove the last line.\n let text = insert.join('\\n');\n if (posTo === this._view.state.doc.length) {\n text += '\\n';\n }\n\n this._view.dispatch({\n changes: { from: posFrom, to: posTo, insert: text },\n });\n }\n}\n\ntype ChunkChange<T extends Chunk> =\n | {\n type: 'append';\n index: number;\n chunk: T;\n }\n | {\n type: 'update';\n index: number;\n chunk: T;\n }\n | {\n type: 'delete';\n index: number;\n id: string;\n };\n\n/**\n * Each Chunk can be converted into a set of lines.\n * Chunks can be added, updated or deleted from the Queue.\n * A Document is an immutable projection that represents a flat list of lines corresponding to the Queue.\n *\n * Ideally we would implement a custom virtual Text model for the View, but this currently isn't possible in Codemirror.\n * Instead this model tracks changes and syncs them with the ChunkDocument.\n */\nexport class TranscriptModel<T extends Chunk> {\n /** Emits when the document is updated. */\n public readonly update = new Event<void>();\n\n /** Ordered set of chunks. */\n private readonly _chunks: T[] = [];\n\n /** Map of chunk IDs to their current line counts. */\n private readonly _chunkLineCounts = new Map<string, number>();\n\n /** Map of line numbers to chunk IDs. */\n private readonly _lineToChunk = new Map<number, T>();\n\n /** Track chunk changes since last sync. */\n private readonly _changes: Array<ChunkChange<T>> = [];\n\n constructor(\n private readonly _renderer: ChunkRenderer<T>,\n initialChunks: T[] = [],\n ) {\n initialChunks.forEach((chunk) => {\n this.appendChunk(chunk);\n });\n }\n\n toJSON(): { chunks: number; changes: number } {\n return {\n chunks: this._chunks.length,\n changes: this._changes.length,\n };\n }\n\n get doc() {\n return Text.of([...this._chunks.flatMap((chunk, index) => this._renderer(chunk, index)), '']);\n }\n\n get chunks() {\n return this._chunks;\n }\n\n /**\n * Get the block ID at the given line number.\n */\n getChunkAtLine(line: number): T | undefined {\n return this._lineToChunk.get(line);\n }\n\n reset(): this {\n this._chunks.length = 0;\n this._chunkLineCounts.clear();\n this._lineToChunk.clear();\n this.update.emit();\n return this;\n }\n\n appendChunk(chunk: T): this {\n const index = this._chunks.length;\n this._chunks.push(chunk);\n this._changes.push({ type: 'append', index, chunk });\n this.update.emit();\n return this;\n }\n\n updateChunk(chunk: T): this {\n const index = this._chunks.findIndex((c) => c.id === chunk.id);\n invariant(index !== -1);\n this._chunks[index] = chunk;\n this._changes.push({ type: 'update', index, chunk });\n this.update.emit();\n return this;\n }\n\n deleteBlock(id: string): this {\n const index = this._chunks.findIndex((c) => c.id === id);\n invariant(index !== -1);\n this._chunks.splice(index, 1);\n this._changes.push({ type: 'delete', index, id });\n this.update.emit();\n return this;\n }\n\n /**\n * Syncs the tracked changes with the ChunkDocument.\n */\n sync(document: ChunkDocument): this {\n log('sync', { changes: this._changes });\n\n // Process each change in order.\n for (const change of this._changes) {\n switch (change.type) {\n case 'append': {\n // For appends, convert block to lines and add at the end.\n const lines = this._renderer(change.chunk, change.index);\n const lastLine = document.lineCount();\n document.replaceLines(lastLine + 1, 0, lines);\n break;\n }\n\n case 'update': {\n // For updates, find the block's line range and replace with new lines.\n let lineStart = 1;\n for (let i = 0; i < change.index; i++) {\n lineStart += this._chunkLineCounts.get(this._chunks[i].id)!;\n }\n const previousLineCount = this._chunkLineCounts.get(change.chunk.id)!;\n const newLines = this._renderer(change.chunk, change.index);\n document.replaceLines(lineStart, previousLineCount, newLines);\n break;\n }\n\n case 'delete': {\n // For deletes, find the block's line range and remove those lines.\n let lineStart = 1;\n for (let i = 0; i < change.index; i++) {\n lineStart += this._chunkLineCounts.get(this._chunks[i].id)!;\n }\n const previousLineCount = this._chunkLineCounts.get(change.id)!;\n document.replaceLines(lineStart, previousLineCount, []);\n break;\n }\n }\n }\n\n // Clear the line-to-block mapping.\n this._lineToChunk.clear();\n\n // Update all block line counts and line-to-block mapping after processing changes.\n let currentLine = 1;\n for (const [index, chunk] of this._chunks.entries()) {\n const lineCount = this._renderer(chunk, index).length;\n this._chunkLineCounts.set(chunk.id, lineCount);\n this._lineToChunk.set(currentLine, chunk);\n currentLine += lineCount;\n }\n\n // Clean up deleted blocks.\n const chunkIds = new Set(this._chunks.map((chunk) => chunk.id));\n for (const id of this._chunkLineCounts.keys()) {\n if (!chunkIds.has(id)) {\n this._chunkLineCounts.delete(id);\n }\n }\n\n // Clear changes after sync.\n this._changes.length = 0;\n return this;\n }\n}\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { type Extension, RangeSetBuilder } from '@codemirror/state';\nimport { EditorView, GutterMarker, ViewPlugin, type ViewUpdate, gutter } from '@codemirror/view';\nimport { format } from 'date-fns/format';\nimport { intervalToDuration } from 'date-fns/intervalToDuration';\n\nimport { type CleanupFn, addEventListener, combine } from '@dxos/async';\nimport { type Message } from '@dxos/types';\n\nimport { EditorChunkDocument, type TranscriptModel } from '../../model/index.ts';\n\n/**\n * Data structure that maps Chunks queue to lines with transcript state.\n */\nexport type TranscriptionOptions = {\n model: TranscriptModel<Message.Message>;\n started?: Date;\n};\n\n/**\n * Scrolling transcript with timestamps.\n */\nexport const transcription = ({ model, started }: TranscriptionOptions): Extension => {\n return [\n // Show timestamps in the gutter.\n gutter({\n class: 'cm-timestamp-gutter',\n lineMarkerChange: (update) => update.docChanged || update.viewportChanged,\n markers: (view) => {\n const start = getStartTime(started, model.chunks[0]);\n const builder = new RangeSetBuilder<GutterMarker>();\n for (const { from, to } of view.visibleRanges) {\n let line = view.state.doc.lineAt(from);\n while (line.from <= to) {\n const block = model.getChunkAtLine(line.number)?.blocks[0];\n const timestamp = block?._tag === 'transcript' && block.started;\n if (timestamp) {\n builder.add(line.from, line.from, new TimestampMarker(line.number, new Date(timestamp), start));\n }\n\n if (line.to + 1 > view.state.doc.length) {\n break;\n }\n\n line = view.state.doc.lineAt(line.to + 1);\n }\n }\n\n return builder.finish();\n },\n }),\n\n // Listen for model updates.\n ViewPlugin.fromClass(\n class {\n private readonly _controls?: HTMLDivElement;\n private readonly _cleanup: CleanupFn;\n private readonly _adapter: EditorChunkDocument;\n private _initialized = false;\n\n constructor(view: EditorView) {\n this._adapter = new EditorChunkDocument(view);\n\n const scroller = view.scrollDOM;\n let isAutoScrolling = false;\n let hasScrolled = false;\n\n let timeout: NodeJS.Timeout | undefined;\n const scrollToBottom = (smooth = false) => {\n scroller.style.scrollBehavior = smooth ? 'smooth' : '';\n\n // Temporarily hide scrollbar to prevent flicker.\n scroller.classList.add('cm-hide-scrollbar');\n isAutoScrolling = true;\n clearTimeout(timeout);\n timeout = setTimeout(() => {\n this._controls?.classList.add('opacity-0');\n scroller.classList.remove('cm-hide-scrollbar');\n isAutoScrolling = false;\n }, 500);\n\n // Scroll to bottom.\n view.dispatch({\n effects: EditorView.scrollIntoView(view.state.doc.length, { y: 'end' }),\n });\n };\n\n // Event listeners.\n this._cleanup = combine(\n addEventListener(view.scrollDOM, 'scroll', () => {\n if (!isAutoScrolling) {\n hasScrolled = true;\n this._controls?.classList.remove('opacity-0');\n }\n }),\n\n model.update.on(() => {\n // Check if clamped to bottom.\n const autoScroll =\n scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight === 0 || !hasScrolled;\n\n // Sync.\n model.sync(this._adapter);\n\n // Scroll.\n if (autoScroll) {\n scrollToBottom(true);\n }\n }),\n );\n }\n\n update(_update: ViewUpdate) {\n // Initial sync.\n if (!this._initialized) {\n this._initialized = true;\n setTimeout(() => {\n model.sync(this._adapter);\n });\n }\n }\n\n destroy() {\n this._controls?.remove();\n this._cleanup();\n }\n },\n ),\n\n EditorView.theme({\n '.cm-gutters': {\n backgroundColor: 'var(--color-base-surface)',\n },\n '.cm-timestamp-gutter': {\n width: '6rem',\n paddingLeft: '0.5rem',\n paddingRight: '1rem',\n },\n '.cm-timestamp-gutter > .cm-gutterElement > div': {\n display: 'inline-flex',\n textAlign: 'right',\n padding: '3px',\n },\n }),\n ];\n};\n\n/**\n * Gutter marker that displays a timestamp.\n */\nclass TimestampMarker extends GutterMarker {\n constructor(\n readonly _line: number,\n readonly _timestamp: Date,\n readonly _started?: Date,\n ) {\n super();\n }\n\n override eq(other: this) {\n return this._timestamp === other._timestamp;\n }\n\n override toDOM(view: EditorView) {\n const el = document.createElement('div');\n el.className = 'text-sm text-fg-subtle hover:bg-hover-surface cursor-pointer';\n el.textContent = formatTimestamp(this._timestamp, this._started);\n // TODO(burdon): Click to bookmark or copy hyperlink.\n el.onclick = () => {\n const pos = view.state.doc.line(this._line).from;\n view.dispatch({\n effects: EditorView.scrollIntoView(pos, { y: 'start' }),\n });\n };\n\n return el;\n }\n}\n\nconst getStartTime = (started?: Date, message?: Message.Message): Date | undefined => {\n if (started) {\n return started;\n }\n\n if (message?.blocks[0]?._tag === 'transcript' && message.blocks[0].started) {\n return new Date(message.blocks[0].started);\n }\n\n return undefined;\n};\n\nconst formatTimestamp = (timestamp: Date, relative?: Date) => {\n if (relative) {\n const pad = (n = 0) => String(n).padStart(2, '0');\n const { hours, minutes, seconds } = intervalToDuration({ start: relative, end: timestamp });\n return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;\n } else {\n return format(timestamp, 'HH:mm:ss');\n }\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport React from 'react';\n\nimport { useTextEditor } from '@dxos/react-ui-editor';\nimport * as Hooks from '@dxos/react-ui/Hooks';\nimport * as Util from '@dxos/react-ui/Util';\nimport { type Message, type Transcript } from '@dxos/types';\nimport {\n createBasicExtensions,\n createMarkdownExtensions,\n createThemeExtensions,\n decorateMarkdown,\n documentSlots,\n objectLinks,\n scroller,\n} from '@dxos/ui-editor';\n\nimport { type TranscriptModel } from '../../model/index.ts';\nimport { transcription } from './transcription-extension.ts';\n\nexport type TranscriptionProps = {\n transcript?: Transcript.Transcript;\n model: TranscriptModel<Message.Message>;\n};\n\n// TODO(burdon): Rename Transcript.\nexport const Transcription = Util.composable<HTMLDivElement, TranscriptionProps>(\n ({ transcript: object, model, children, ...props }, forwardedRef) => {\n const themeMode = Hooks.useThemeMode();\n const { parentRef } = useTextEditor(() => {\n return {\n extensions: [\n createBasicExtensions({ readOnly: true, lineWrapping: true, search: true }),\n createThemeExtensions({ themeMode, slots: documentSlots }),\n createMarkdownExtensions(),\n decorateMarkdown(),\n objectLinks(),\n transcription({ model, started: object?.started ? new Date(object.started) : undefined }),\n scroller(),\n ],\n };\n }, [model, themeMode]);\n\n return (\n <div\n {...Util.composableProps(props, { classNames: 'dx-expand' })}\n data-popover-collision-boundary={true}\n ref={parentRef}\n />\n );\n },\n);\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { useEffect, useState } from 'react';\n\nimport { scheduleTask } from '@dxos/async';\nimport { Context } from '@dxos/context';\nimport { log } from '@dxos/log';\n\nexport type UseAudioState = {\n audio?: HTMLAudioElement;\n stream?: MediaStream;\n track?: MediaStreamTrack;\n};\n\nexport const useAudioFile = (audioUrl: string, constraints?: MediaTrackConstraints): UseAudioState => {\n const [{ audio, stream, track }, setStream] = useState<UseAudioState>({});\n useEffect(() => {\n const ctx = new Context();\n\n scheduleTask(ctx, async () => {\n // TODO(wittjosiah): Fetch to external url fails in headless storybook test.\n try {\n const response = await fetch(audioUrl);\n const blob = await response.blob();\n const objectUrl = URL.createObjectURL(blob);\n ctx.onDispose(() => {\n URL.revokeObjectURL(objectUrl);\n });\n\n const audio = new Audio();\n audio.src = objectUrl;\n\n await new Promise<void>((resolve, reject) => {\n audio.addEventListener(\n 'error',\n (err) => {\n log.error('error', { err });\n reject(err);\n },\n { once: true },\n );\n audio.addEventListener(\n 'canplay',\n async () => {\n resolve();\n },\n { once: true },\n );\n audio.load();\n });\n\n const audioCtx = new AudioContext();\n\n // Resume AudioContext if it's suspended.\n if (audioCtx.state === 'suspended') {\n await audioCtx.resume();\n }\n\n const destination = audioCtx.createMediaStreamDestination();\n destination.channelCount = 1;\n\n const source = audioCtx.createMediaElementSource(audio);\n source.connect(destination);\n const track = destination.stream.getAudioTracks()[0];\n if (constraints) {\n await track.applyConstraints(constraints).catch((err) => log.catch(err));\n }\n\n // Also connect to speakers so audio is audible.\n setStream({\n audio,\n stream: destination.stream,\n track: destination.stream.getAudioTracks()[0],\n });\n } catch (err) {\n log.catch(err);\n }\n });\n\n return () => {\n void ctx.dispose();\n };\n }, [audioUrl]);\n\n return { audio, stream, track };\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { useEffect, useState } from 'react';\n\n/**\n * Acquires a microphone audio track while `active`. Re-acquires when the `constraints` change (e.g.\n * a different input device is selected) so the new device takes effect without a remount.\n */\n// TODO(burdon): Reconcile with react-ui-experimental and plugin-calls.\nexport const useAudioTrack = (active?: boolean, constraints?: MediaTrackConstraints): MediaStreamTrack | undefined => {\n const [track, setTrack] = useState<MediaStreamTrack>();\n // Re-acquire on a value change, not on every re-created constraints object identity.\n const constraintsKey = constraints ? JSON.stringify(constraints) : '';\n useEffect(() => {\n if (!active) {\n track?.stop();\n setTrack(undefined);\n return;\n }\n\n let cancelled = false;\n let acquired: MediaStreamTrack | undefined;\n // Drop any previous (now-stopped) track while reacquiring, so callers never hold a dead track.\n setTrack(undefined);\n const initAudio = async () => {\n try {\n const stream = await navigator.mediaDevices.getUserMedia({ audio: constraints ?? true });\n const [next] = stream.getAudioTracks();\n if (cancelled || !next) {\n next?.stop();\n return;\n }\n acquired = next;\n setTrack(next);\n } catch {\n // Permission denied / device unavailable: leave the track cleared rather than rejecting.\n if (!cancelled) {\n setTrack(undefined);\n }\n }\n };\n\n void initAudio();\n\n return () => {\n cancelled = true;\n acquired?.stop();\n };\n // `constraints` is read via the serialized `constraintsKey`.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [active, constraintsKey]);\n\n return track;\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { useEffect, useMemo } from 'react';\n\nimport type { Obj } from '@dxos/echo';\n\nimport { type ChunkRenderer, TranscriptModel } from '../model/index.ts';\n\n/**\n * Feeds an array of objects (typically from `useQuery(db, Query.from(feed))`) into a\n * `TranscriptModel`, appending each new tail item as the array grows.\n *\n * @param renderer - chunk renderer.\n * @param objects - current snapshot of feed items.\n * @param initialChunks - chunks to seed the model with on first render.\n */\nexport const useFeedModelAdapter = <T extends Obj.Unknown>(\n renderer: ChunkRenderer<T>,\n objects: readonly T[],\n initialChunks: T[] = [],\n): TranscriptModel<T> => {\n const model = useMemo(() => new TranscriptModel<T>(renderer, initialChunks), [renderer]);\n\n // Seed the model with the current snapshot on first render and whenever the\n // identity of `objects` changes (e.g. feed swapped).\n useEffect(() => {\n for (const chunk of objects) {\n model.appendChunk(chunk);\n }\n }, [model, objects]);\n\n // Append new tail items as the array grows.\n useEffect(() => {\n if (model.chunks.length === objects.length) {\n return;\n }\n const chunk = objects.at(-1);\n if (chunk) {\n model.appendChunk(chunk);\n }\n }, [model, objects.length]);\n\n return model;\n};\n","//\n// Copyright 2024 DXOS.org\n//\n\nimport { useEffect, useMemo, useState } from 'react';\n\nimport { scheduleTask } from '@dxos/async';\nimport { SpeakingMonitor } from '@dxos/av';\nimport { Context } from '@dxos/context';\n\n/**\n * Tracks voice activity on an audio track via {@link SpeakingMonitor}.\n */\nexport const useIsSpeaking = (track?: MediaStreamTrack) => {\n const [isSpeaking, setIsSpeaking] = useState(false);\n const speakingMonitor = useMemo(() => {\n if (!track) {\n return;\n }\n\n return new SpeakingMonitor(track);\n }, [track]);\n\n useEffect(() => {\n if (!speakingMonitor) {\n return;\n }\n\n const ctx = new Context();\n scheduleTask(ctx, async () => {\n speakingMonitor.speakingChanged.on(ctx, () => setIsSpeaking(speakingMonitor.isSpeaking));\n await speakingMonitor.open();\n ctx.onDispose(() => speakingMonitor.close());\n });\n\n return () => {\n void ctx.dispose();\n };\n }, [speakingMonitor]);\n\n return isSpeaking;\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { useEffect, useMemo, useRef, useState } from 'react';\n\nimport { log } from '@dxos/log';\nimport {\n type AsrPipeline,\n type CommitFn,\n type EntityLookup,\n type Stage,\n type TranscribeConfig,\n runAsrPipeline,\n} from '@dxos/pipeline-transcription';\nimport { type ContentBlock } from '@dxos/types';\n\nimport { MediaStreamRecorder } from '../capture/index.ts';\nimport { type PipelinePhase } from '../types.ts';\nimport { useAudioTrack } from './useAudioTrack.ts';\n\n// Recorder chunk interval; the transcriber's chunk threshold derives from this.\nconst RECORDER_INTERVAL_MS = 200;\n\n// Silence reported on drain so on-silence stages (e.g. summarization) fire over the final window.\nconst DRAIN_SILENCE_MS = 5_000;\n\nconst DEFAULT_TRANSCRIBE_CONFIG: TranscribeConfig = {\n transcribeAfterChunksAmount: 50,\n prefixBufferChunksAmount: 10,\n};\n\nexport type RecordingPipelineOptions = {\n /**\n * Transcriber configuration. Must carry `endpoint` (there is no built-in one); a transcriber\n * without it rejects on `open()` with `TranscriptionEndpointNotConfiguredError`.\n */\n config?: Partial<TranscribeConfig>;\n /** Re-segment ASR output into complete sentences before the pipeline (merges mid-sentence cuts). */\n segmentSentences?: boolean;\n /**\n * Raw capture flag — recording (mic) or playing (file). When it flips false the hook drains\n * (flushing the transcriber's buffered audio) before returning to idle.\n */\n active: boolean;\n /** Use the microphone as the ASR source; the hook owns the track and keeps it alive through drain. */\n microphone?: boolean;\n /** Externally-supplied track (file source). Used only when `microphone` is false. */\n track?: MediaStreamTrack;\n /** Audio constraints for the microphone source. */\n audioConstraints?: MediaTrackConstraints;\n /** Ordered pipeline stages applied to captured blocks. */\n stages: readonly Stage<any, any>[];\n /** Commit sink for enriched stage output. */\n commit: CommitFn;\n /** Optional entity-reference resolver injected into stages. */\n lookup?: EntityLookup;\n /** Optional raw-segment hook (pre-pipeline), e.g. for live display before enrichment. */\n onSegment?: (block: ContentBlock.Transcript) => void;\n};\n\nexport type RecordingPipeline = {\n /** Lifecycle phase (idle → recording → draining → idle). */\n phase: PipelinePhase;\n};\n\n/**\n * Single ASR-capture engine binding for React consumers: drives an {@link AsrPipeline}\n * ({@link runAsrPipeline}) off a capture flag, sourcing audio from the microphone (owned here) or an\n * external track. The editor driver, message-list recording, and stories all build on this — varying\n * only by source, stages, and commit sink.\n */\nexport const useRecordingPipeline = ({\n active,\n microphone,\n track: externalTrack,\n audioConstraints,\n stages,\n commit,\n lookup,\n onSegment,\n config,\n segmentSentences,\n}: RecordingPipelineOptions): RecordingPipeline => {\n // Phase machine: hold 'recording' for the commit when `active` flips off, then 'draining' until the\n // transcriber flushes, then 'idle' — so the tail of speech and its enrichment are not lost.\n const [phase, setPhase] = useState<PipelinePhase>('idle');\n const inUse = phase !== 'idle';\n useEffect(() => {\n setPhase((current) => (active ? 'recording' : current === 'recording' ? 'draining' : current));\n }, [active]);\n\n // Keep the mic alive through the drain; external (file) tracks are owned by the caller.\n const micTrack = useAudioTrack(!!microphone && inUse, audioConstraints);\n const track = microphone ? micTrack : externalTrack;\n\n // A fresh recorder per track so a stopped (encoder-flushed) instance is never reused.\n const recorder = useMemo(\n () =>\n track\n ? new MediaStreamRecorder({ mediaStreamTrack: track, config: { interval: RECORDER_INTERVAL_MS } })\n : undefined,\n [track],\n );\n\n const pipelineRef = useRef<AsrPipeline | null>(null);\n\n // Start a live ASR pipeline session when recording. Stages/commit/lookup are captured for the\n // lifetime of the session (a config change mid-recording would orphan the run).\n useEffect(() => {\n if (phase !== 'recording' || !recorder) {\n return;\n }\n const asr = runAsrPipeline({\n recorder,\n config: { ...DEFAULT_TRANSCRIBE_CONFIG, ...config },\n segmentSentences,\n stages,\n commit,\n lookup,\n onSegment,\n drainSilenceMs: DRAIN_SILENCE_MS,\n });\n pipelineRef.current = asr;\n let cancelled = false;\n void asr.open().then(\n () => {\n if (!cancelled) {\n asr.start();\n }\n },\n (err) => {\n if (!cancelled) {\n log.catch(err);\n setPhase('idle');\n }\n },\n );\n return () => {\n cancelled = true;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [phase, recorder]);\n\n // Drain on stop: flush buffered audio, signal silence, end the session.\n useEffect(() => {\n if (phase !== 'draining') {\n return;\n }\n let cancelled = false;\n void (async () => {\n try {\n await pipelineRef.current?.end();\n } catch (err) {\n log.catch(err);\n } finally {\n pipelineRef.current = null;\n if (!cancelled) {\n setPhase((current) => (current === 'draining' ? 'idle' : current));\n }\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [phase]);\n\n return { phase };\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { useEffect, useMemo } from 'react';\n\nimport { log } from '@dxos/log';\n\nconst cleanText = (text: string) =>\n text\n .toLowerCase()\n .replace(/[.,!?]/g, '')\n .replace(/\\s+/g, ' ')\n .trim();\n\nexport type UseSpeechRecognitionProps = {\n active: boolean;\n onTranscript: (transcript: string) => void;\n};\n\n/**\n * Use browser speech recognition API to transcribe speech to text.\n */\nexport const useSpeechRecognition = ({ active, onTranscript }: UseSpeechRecognitionProps) => {\n const recognition = useMemo(() => {\n const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;\n const SpeechGrammarList = (window as any).SpeechGrammarList || (window as any).webkitSpeechGrammarList;\n if (!SpeechRecognition || !SpeechGrammarList) {\n log.error('Speech recognition not supported');\n return;\n }\n\n return Object.assign(new SpeechRecognition(), {\n grammars: new SpeechGrammarList(),\n continuous: true,\n interimResults: true,\n lang: 'en-US',\n maxAlternatives: 1,\n onresult: (event: any) => {\n const transcript = cleanText(event.results[event.resultIndex][0].transcript);\n onTranscript(transcript);\n },\n onerror: (event: any) => {\n log.error('Speech recognition error', { error: event.error });\n },\n });\n }, [onTranscript]);\n\n useEffect(() => {\n if (active) {\n recognition?.start();\n } else {\n recognition?.stop();\n }\n }, [active, recognition]);\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { useEffect, useMemo } from 'react';\n\nimport { type Transcriber } from '@dxos/pipeline-transcription';\n\nimport { type CreateTranscriberOptions, createTranscriber } from '../capture/index.ts';\n\n/**\n * Records audio while the user is speaking and transcribes it after they pause. Builds a\n * {@link Transcriber} directly from {@link createTranscriber} (no app-framework capability needed,\n * since the browser construction lives in this package); returns `undefined` until a track and\n * `onSegments` handler are supplied.\n */\nexport const useTranscriber = ({\n audioStreamTrack,\n recorderConfig,\n transcriberConfig,\n transcribe,\n onSegments,\n}: Partial<CreateTranscriberOptions>): Transcriber | undefined => {\n const transcriber = useMemo<Transcriber | undefined>(() => {\n if (!audioStreamTrack || !onSegments) {\n return undefined;\n }\n\n return createTranscriber({\n audioStreamTrack,\n recorderConfig,\n transcriberConfig,\n transcribe,\n onSegments,\n });\n }, [audioStreamTrack, recorderConfig, transcriberConfig, transcribe, onSegments]);\n\n useEffect(() => {\n return () => {\n void transcriber?.close();\n };\n }, [transcriber]);\n\n return transcriber;\n};\n"],"mappings":"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"}
|