@dxos/react-ui-transcription 0.10.0 → 0.11.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.
Files changed (32) hide show
  1. package/dist/lib/index.mjs +963 -0
  2. package/dist/lib/index.mjs.map +1 -0
  3. package/dist/types/src/capture/create-transcriber.d.ts +1 -1
  4. package/dist/types/src/capture/media-stream-recorder.d.ts +1 -1
  5. package/dist/types/src/capture/media-stream-recorder.d.ts.map +1 -1
  6. package/dist/types/src/components/PipelineStatus/PipelineStatus.d.ts +1 -1
  7. package/dist/types/src/components/Transcription/Transcription.d.ts +1 -1
  8. package/dist/types/src/components/Transcription/Transcription.d.ts.map +1 -1
  9. package/dist/types/src/hooks/useRecordingPipeline.d.ts +1 -1
  10. package/dist/types/src/hooks/useTranscriber.d.ts +1 -1
  11. package/dist/types/src/index.d.ts +0 -1
  12. package/dist/types/src/index.d.ts.map +1 -1
  13. package/dist/types/tsconfig.tsbuildinfo +1 -1
  14. package/package.json +23 -24
  15. package/src/capture/create-transcriber.ts +1 -1
  16. package/src/capture/media-stream-recorder.ts +1 -3
  17. package/src/components/PipelineStatus/PipelineStatus.tsx +1 -1
  18. package/src/hooks/useRecordingPipeline.ts +1 -1
  19. package/src/hooks/useTranscriber.ts +1 -1
  20. package/src/index.ts +0 -1
  21. package/dist/lib/browser/index.mjs +0 -1002
  22. package/dist/lib/browser/index.mjs.map +0 -7
  23. package/dist/lib/browser/meta.json +0 -1
  24. package/dist/lib/node-esm/index.mjs +0 -1004
  25. package/dist/lib/node-esm/index.mjs.map +0 -7
  26. package/dist/lib/node-esm/meta.json +0 -1
  27. package/dist/types/src/util/index.d.ts +0 -2
  28. package/dist/types/src/util/index.d.ts.map +0 -1
  29. package/dist/types/src/util/render-byline.d.ts +0 -4
  30. package/dist/types/src/util/render-byline.d.ts.map +0 -1
  31. package/src/util/index.ts +0 -5
  32. package/src/util/render-byline.ts +0 -32
@@ -1,1004 +0,0 @@
1
- import { createRequire } from 'node:module';const require = createRequire(import.meta.url);
2
-
3
- // src/capture/create-transcriber.ts
4
- import { Transcriber } from "@dxos/transcription-pipeline";
5
-
6
- // src/capture/media-stream-recorder.ts
7
- import { WaveFile } from "wavefile";
8
- import { synchronized } from "@dxos/async";
9
- import { invariant } from "@dxos/invariant";
10
- import { log } from "@dxos/log";
11
- import { trace } from "@dxos/tracing";
12
- var __dxlog_file = "/__w/dxos/dxos/packages/ui/react-ui-transcription/src/capture/media-stream-recorder.ts";
13
- function _ts_decorate(decorators, target, key, desc) {
14
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
15
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
16
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
17
- return c > 3 && r && Object.defineProperty(target, key, r), r;
18
- }
19
- var MediaRecorderClass;
20
- var initializingPromise;
21
- var initializeExtendableMediaRecorder = async () => {
22
- const { MediaRecorder, register } = await import("extendable-media-recorder");
23
- const { connect } = await import("extendable-media-recorder-wav-encoder");
24
- await register(await connect());
25
- MediaRecorderClass = MediaRecorder;
26
- };
27
- var MediaStreamRecorder = class {
28
- _mediaStreamTrack;
29
- /**
30
- * Default MediaRecorder implementation do not support wav encoding.
31
- */
32
- _config;
33
- _mediaRecorder = void 0;
34
- _header = void 0;
35
- _onChunk;
36
- constructor({ mediaStreamTrack, config }) {
37
- this._mediaStreamTrack = mediaStreamTrack;
38
- this._config = config;
39
- }
40
- get wavConfig() {
41
- const settings = this._mediaStreamTrack.getSettings();
42
- return {
43
- channels: settings.channelCount ?? 1,
44
- sampleRate: settings.sampleRate ?? 16e3,
45
- bitDepthCode: settings.sampleSize ? String(settings.sampleSize) : "16"
46
- };
47
- }
48
- get mediaStreamTrack() {
49
- return this._mediaStreamTrack;
50
- }
51
- setOnChunk(onChunk) {
52
- this._onChunk = onChunk;
53
- }
54
- // `@synchronized` serializes start/stop (they share the instance lock) so a rapid stop→start cycle
55
- // (e.g. React StrictMode double-mount) cannot initiate a new WAV encoding before the previous one
56
- // has flushed — which throws "Another request was made to initiate an encoding" and kills capture.
57
- async start() {
58
- await (initializingPromise ??= initializeExtendableMediaRecorder()).catch((err) => log.info("initializeExtendableMediaRecorder", {
59
- err
60
- }, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 56, S: this }));
61
- if (this._mediaRecorder?.state === "recording") {
62
- return;
63
- }
64
- invariant(this._onChunk, "MediaStreamRecorder: onChunk is not set", { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 62, S: this, A: ["this._onChunk", "'MediaStreamRecorder: onChunk is not set'"] });
65
- invariant(MediaRecorderClass, "MediaStreamRecorder: extendable-media-recorder not initialized", { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 63, S: this, A: ["MediaRecorderClass", "'MediaStreamRecorder: extendable-media-recorder not initialized'"] });
66
- const stream = new MediaStream([
67
- this._mediaStreamTrack
68
- ]);
69
- const recorder = new MediaRecorderClass(stream, {
70
- mimeType: "audio/wav"
71
- });
72
- recorder.ondataavailable = (event) => this._ondataavailable(event);
73
- this._mediaRecorder = recorder;
74
- recorder.start(this._config.interval);
75
- }
76
- async stop() {
77
- const recorder = this._mediaRecorder;
78
- if (!recorder || recorder.state !== "recording") {
79
- return;
80
- }
81
- await new Promise((resolve) => {
82
- recorder.addEventListener("stop", () => resolve(), {
83
- once: true
84
- });
85
- recorder.stop();
86
- });
87
- this._mediaRecorder = void 0;
88
- }
89
- async _ondataavailable(event) {
90
- const blob = event.data;
91
- const uint8Array = new Uint8Array(await blob.arrayBuffer());
92
- const isHeader = uint8Array[0] === 82 && // R
93
- uint8Array[1] === 73 && // I
94
- uint8Array[2] === 70 && // F
95
- uint8Array[3] === 70;
96
- let wav;
97
- if (isHeader) {
98
- wav = new WaveFile(uint8Array);
99
- this._header = uint8Array.slice(0, 44);
100
- } else if (this._header) {
101
- wav = new WaveFile(new Uint8Array([
102
- ...this._header,
103
- ...uint8Array
104
- ]));
105
- } else {
106
- log.warn("MediaStreamRecorder: no header", void 0, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 108, S: this });
107
- return;
108
- }
109
- this._onChunk({
110
- timestamp: Date.now(),
111
- data: wav.getSamples()
112
- });
113
- }
114
- };
115
- _ts_decorate([
116
- synchronized
117
- ], MediaStreamRecorder.prototype, "start", null);
118
- _ts_decorate([
119
- synchronized
120
- ], MediaStreamRecorder.prototype, "stop", null);
121
- _ts_decorate([
122
- synchronized
123
- ], MediaStreamRecorder.prototype, "_ondataavailable", null);
124
- MediaStreamRecorder = _ts_decorate([
125
- trace.resource()
126
- ], MediaStreamRecorder);
127
-
128
- // src/capture/create-transcriber.ts
129
- var RECORD_INTERVAL = 200;
130
- var PREFIXED_CHUNKS_AMOUNT = 10;
131
- var TRANSCRIBE_AFTER_CHUNKS_AMOUNT = 50;
132
- var createTranscriber = ({ audioStreamTrack, onSegments, transcriberConfig, recorderConfig, transcribe }) => new Transcriber({
133
- config: {
134
- transcribeAfterChunksAmount: TRANSCRIBE_AFTER_CHUNKS_AMOUNT,
135
- prefixBufferChunksAmount: PREFIXED_CHUNKS_AMOUNT,
136
- ...transcriberConfig
137
- },
138
- recorder: new MediaStreamRecorder({
139
- mediaStreamTrack: audioStreamTrack,
140
- config: {
141
- interval: RECORD_INTERVAL,
142
- ...recorderConfig
143
- }
144
- }),
145
- onSegments,
146
- transcribe
147
- });
148
-
149
- // src/components/MicSettings/MicSettings.tsx
150
- import React from "react";
151
- import { DropdownMenu, Icon, IconButton, useTranslation } from "@dxos/react-ui";
152
- var MicSettings = ({ translationNs, recordMode, entityExtraction, devices, selectedDeviceId, onRecordModeChange, onEntityExtractionChange, onSelectDevice }) => {
153
- const { t } = useTranslation(translationNs);
154
- return /* @__PURE__ */ React.createElement(DropdownMenu.Root, null, /* @__PURE__ */ React.createElement(DropdownMenu.Trigger, {
155
- asChild: true
156
- }, /* @__PURE__ */ React.createElement(IconButton, {
157
- icon: "ph--caret-down--regular",
158
- iconOnly: true,
159
- label: t("recording-options.label"),
160
- variant: "ghost",
161
- // Half-width slim caret abutting the mic (split-control affordance).
162
- classNames: "w-4",
163
- "data-testid": "transcription.record.options"
164
- })), /* @__PURE__ */ React.createElement(DropdownMenu.Portal, null, /* @__PURE__ */ React.createElement(DropdownMenu.Content, null, /* @__PURE__ */ React.createElement(DropdownMenu.Viewport, null, /* @__PURE__ */ React.createElement(DropdownMenu.GroupLabel, null, t("record-mode.label")), /* @__PURE__ */ React.createElement(SelectableItem, {
165
- label: t("record-mode.toggle.label"),
166
- selected: recordMode === "toggle",
167
- onSelect: () => onRecordModeChange("toggle")
168
- }), /* @__PURE__ */ React.createElement(SelectableItem, {
169
- label: t("record-mode.hold.label"),
170
- selected: recordMode === "hold",
171
- onSelect: () => onRecordModeChange("hold")
172
- }), /* @__PURE__ */ React.createElement(DropdownMenu.Separator, null), /* @__PURE__ */ React.createElement(DropdownMenu.GroupLabel, null, t("audio-device.label")), /* @__PURE__ */ React.createElement(SelectableItem, {
173
- label: t("audio-device.default.label"),
174
- selected: selectedDeviceId === "",
175
- onSelect: () => onSelectDevice("")
176
- }), devices.map((device) => /* @__PURE__ */ React.createElement(SelectableItem, {
177
- key: device.deviceId,
178
- label: device.label,
179
- selected: selectedDeviceId === device.deviceId,
180
- onSelect: () => onSelectDevice(device.deviceId)
181
- })), /* @__PURE__ */ React.createElement(DropdownMenu.Separator, null), /* @__PURE__ */ React.createElement(DropdownMenu.CheckboxItem, {
182
- checked: entityExtraction,
183
- onCheckedChange: onEntityExtractionChange,
184
- classNames: "gap-2",
185
- "data-testid": "transcription.entity-extraction"
186
- }, /* @__PURE__ */ React.createElement("span", {
187
- className: "grow truncate"
188
- }, t("settings.entity-extraction.label")), /* @__PURE__ */ React.createElement(DropdownMenu.ItemIndicator, {
189
- asChild: true
190
- }, /* @__PURE__ */ React.createElement(Icon, {
191
- icon: "ph--check--regular",
192
- size: 4
193
- })))), /* @__PURE__ */ React.createElement(DropdownMenu.Arrow, null))));
194
- };
195
- var SelectableItem = ({ label, selected, onSelect }) => /* @__PURE__ */ React.createElement(DropdownMenu.Item, {
196
- classNames: "gap-2",
197
- onSelect
198
- }, /* @__PURE__ */ React.createElement("span", {
199
- className: "grow truncate"
200
- }, label), selected && /* @__PURE__ */ React.createElement(Icon, {
201
- icon: "ph--check--regular",
202
- size: 4
203
- }));
204
-
205
- // src/components/PipelineStatus/PipelineStatus.tsx
206
- import React2, { Fragment } from "react";
207
- var PipelineStatus = ({ phase = "idle", stages, telemetry, summary }) => /* @__PURE__ */ React2.createElement("div", {
208
- className: "flex flex-col gap-3 overflow-y-auto p-2 text-sm"
209
- }, /* @__PURE__ */ React2.createElement(Section, {
210
- title: "Status"
211
- }, /* @__PURE__ */ React2.createElement("div", {
212
- className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-description"
213
- }, /* @__PURE__ */ React2.createElement("span", null, "Mic"), /* @__PURE__ */ React2.createElement("span", null, phase === "recording" ? "\u25CF on" : "\u25CB off"), /* @__PURE__ */ React2.createElement("span", null, "Pipeline"), /* @__PURE__ */ React2.createElement("span", null, phase))), stages && /* @__PURE__ */ React2.createElement(Section, {
214
- title: "Stages"
215
- }, /* @__PURE__ */ React2.createElement("div", {
216
- className: "text-description"
217
- }, stages.join(" \u2192 ") || "(none)")), telemetry && telemetry.length > 0 && /* @__PURE__ */ React2.createElement(Section, {
218
- title: "Telemetry"
219
- }, /* @__PURE__ */ React2.createElement("div", {
220
- className: "grid w-fit grid-cols-[1fr_auto_auto] gap-x-4 gap-y-1"
221
- }, telemetry.map((event, index) => /* @__PURE__ */ React2.createElement(Fragment, {
222
- key: index
223
- }, /* @__PURE__ */ React2.createElement("span", null, event.stageId), /* @__PURE__ */ React2.createElement("span", {
224
- className: "text-description"
225
- }, event.outcome), /* @__PURE__ */ React2.createElement("span", {
226
- className: "text-right tabular-nums text-description"
227
- }, event.durationMs, "ms"))))), summary && /* @__PURE__ */ React2.createElement(Section, {
228
- title: "Summary"
229
- }, /* @__PURE__ */ React2.createElement("p", {
230
- className: "text-description"
231
- }, summary)));
232
- var Section = ({ title, children }) => /* @__PURE__ */ React2.createElement("div", null, /* @__PURE__ */ React2.createElement("h3", {
233
- className: "mb-1 font-medium"
234
- }, title), children);
235
-
236
- // src/components/Transcription/Transcription.tsx
237
- import React3 from "react";
238
- import { composable, composableProps, useThemeContext } from "@dxos/react-ui";
239
- import { useTextEditor } from "@dxos/react-ui-editor";
240
- import { AnchorWidget, createBasicExtensions, createMarkdownExtensions, createThemeExtensions, decorateMarkdown, documentSlots, scroller, xmlTags } from "@dxos/ui-editor";
241
-
242
- // src/components/Transcription/transcription-extension.ts
243
- import { RangeSetBuilder } from "@codemirror/state";
244
- import { EditorView, GutterMarker, ViewPlugin, gutter } from "@codemirror/view";
245
- import { format } from "date-fns/format";
246
- import { intervalToDuration } from "date-fns/intervalToDuration";
247
- import { addEventListener, combine } from "@dxos/async";
248
-
249
- // src/model/transcript-model.ts
250
- import { Text } from "@codemirror/state";
251
- import { Event } from "@dxos/async";
252
- import { invariant as invariant2 } from "@dxos/invariant";
253
- import { log as log2 } from "@dxos/log";
254
- var __dxlog_file2 = "/__w/dxos/dxos/packages/ui/react-ui-transcription/src/model/transcript-model.ts";
255
- var EditorChunkDocument = class {
256
- _view;
257
- constructor(_view) {
258
- this._view = _view;
259
- }
260
- /** The document must always have at least one line. */
261
- lineCount() {
262
- return this._view.state.doc.lines - 1;
263
- }
264
- replaceLines(from, remove, insert) {
265
- log2("replaceLines", {
266
- count: this._view.state.doc.lines,
267
- from,
268
- remove,
269
- insert
270
- }, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 19, S: this });
271
- const numLines = this._view.state.doc.lines;
272
- const posFrom = this._view.state.doc.line(from).from;
273
- const posTo = remove === 0 ? posFrom : this._view.state.doc.line(Math.min(numLines, from + remove)).from;
274
- let text = insert.join("\n");
275
- if (posTo === this._view.state.doc.length) {
276
- text += "\n";
277
- }
278
- this._view.dispatch({
279
- changes: {
280
- from: posFrom,
281
- to: posTo,
282
- insert: text
283
- }
284
- });
285
- }
286
- };
287
- var TranscriptModel = class {
288
- _renderer;
289
- /** Emits when the document is updated. */
290
- update = new Event();
291
- /** Ordered set of chunks. */
292
- _chunks = [];
293
- /** Map of chunk IDs to their current line counts. */
294
- _chunkLineCounts = /* @__PURE__ */ new Map();
295
- /** Map of line numbers to chunk IDs. */
296
- _lineToChunk = /* @__PURE__ */ new Map();
297
- /** Track chunk changes since last sync. */
298
- _changes = [];
299
- constructor(_renderer, initialChunks = []) {
300
- this._renderer = _renderer;
301
- initialChunks.forEach((chunk) => {
302
- this.appendChunk(chunk);
303
- });
304
- }
305
- toJSON() {
306
- return {
307
- chunks: this._chunks.length,
308
- changes: this._changes.length
309
- };
310
- }
311
- get doc() {
312
- return Text.of([
313
- ...this._chunks.flatMap((chunk, index) => this._renderer(chunk, index)),
314
- ""
315
- ]);
316
- }
317
- get chunks() {
318
- return this._chunks;
319
- }
320
- /**
321
- * Get the block ID at the given line number.
322
- */
323
- getChunkAtLine(line) {
324
- return this._lineToChunk.get(line);
325
- }
326
- reset() {
327
- this._chunks.length = 0;
328
- this._chunkLineCounts.clear();
329
- this._lineToChunk.clear();
330
- this.update.emit();
331
- return this;
332
- }
333
- appendChunk(chunk) {
334
- const index = this._chunks.length;
335
- this._chunks.push(chunk);
336
- this._changes.push({
337
- type: "append",
338
- index,
339
- chunk
340
- });
341
- this.update.emit();
342
- return this;
343
- }
344
- updateChunk(chunk) {
345
- const index = this._chunks.findIndex((c) => c.id === chunk.id);
346
- invariant2(index !== -1, void 0, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 102, S: this, A: ["index !== -1", ""] });
347
- this._chunks[index] = chunk;
348
- this._changes.push({
349
- type: "update",
350
- index,
351
- chunk
352
- });
353
- this.update.emit();
354
- return this;
355
- }
356
- deleteBlock(id) {
357
- const index = this._chunks.findIndex((c) => c.id === id);
358
- invariant2(index !== -1, void 0, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 114, S: this, A: ["index !== -1", ""] });
359
- this._chunks.splice(index, 1);
360
- this._changes.push({
361
- type: "delete",
362
- index,
363
- id
364
- });
365
- this.update.emit();
366
- return this;
367
- }
368
- /**
369
- * Syncs the tracked changes with the ChunkDocument.
370
- */
371
- sync(document2) {
372
- log2("sync", {
373
- changes: this._changes
374
- }, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 127, S: this });
375
- for (const change of this._changes) {
376
- switch (change.type) {
377
- case "append": {
378
- const lines = this._renderer(change.chunk, change.index);
379
- const lastLine = document2.lineCount();
380
- document2.replaceLines(lastLine + 1, 0, lines);
381
- break;
382
- }
383
- case "update": {
384
- let lineStart = 1;
385
- for (let i = 0; i < change.index; i++) {
386
- lineStart += this._chunkLineCounts.get(this._chunks[i].id);
387
- }
388
- const previousLineCount = this._chunkLineCounts.get(change.chunk.id);
389
- const newLines = this._renderer(change.chunk, change.index);
390
- document2.replaceLines(lineStart, previousLineCount, newLines);
391
- break;
392
- }
393
- case "delete": {
394
- let lineStart = 1;
395
- for (let i = 0; i < change.index; i++) {
396
- lineStart += this._chunkLineCounts.get(this._chunks[i].id);
397
- }
398
- const previousLineCount = this._chunkLineCounts.get(change.id);
399
- document2.replaceLines(lineStart, previousLineCount, []);
400
- break;
401
- }
402
- }
403
- }
404
- this._lineToChunk.clear();
405
- let currentLine = 1;
406
- for (const [index, chunk] of this._chunks.entries()) {
407
- const lineCount = this._renderer(chunk, index).length;
408
- this._chunkLineCounts.set(chunk.id, lineCount);
409
- this._lineToChunk.set(currentLine, chunk);
410
- currentLine += lineCount;
411
- }
412
- const chunkIds = new Set(this._chunks.map((chunk) => chunk.id));
413
- for (const id of this._chunkLineCounts.keys()) {
414
- if (!chunkIds.has(id)) {
415
- this._chunkLineCounts.delete(id);
416
- }
417
- }
418
- this._changes.length = 0;
419
- return this;
420
- }
421
- };
422
-
423
- // src/components/Transcription/transcription-extension.ts
424
- var transcription = ({ model, started }) => {
425
- return [
426
- // Show timestamps in the gutter.
427
- gutter({
428
- class: "cm-timestamp-gutter",
429
- lineMarkerChange: (update) => update.docChanged || update.viewportChanged,
430
- markers: (view) => {
431
- const start = getStartTime(started, model.chunks[0]);
432
- const builder = new RangeSetBuilder();
433
- for (const { from, to } of view.visibleRanges) {
434
- let line = view.state.doc.lineAt(from);
435
- while (line.from <= to) {
436
- const block = model.getChunkAtLine(line.number)?.blocks[0];
437
- const timestamp = block?._tag === "transcript" && block.started;
438
- if (timestamp) {
439
- builder.add(line.from, line.from, new TimestampMarker(line.number, new Date(timestamp), start));
440
- }
441
- if (line.to + 1 > view.state.doc.length) {
442
- break;
443
- }
444
- line = view.state.doc.lineAt(line.to + 1);
445
- }
446
- }
447
- return builder.finish();
448
- }
449
- }),
450
- // Listen for model updates.
451
- ViewPlugin.fromClass(class {
452
- _controls;
453
- _cleanup;
454
- _adapter;
455
- _initialized = false;
456
- constructor(view) {
457
- this._adapter = new EditorChunkDocument(view);
458
- const scroller2 = view.scrollDOM;
459
- let isAutoScrolling = false;
460
- let hasScrolled = false;
461
- let timeout;
462
- const scrollToBottom = (smooth = false) => {
463
- scroller2.style.scrollBehavior = smooth ? "smooth" : "";
464
- scroller2.classList.add("cm-hide-scrollbar");
465
- isAutoScrolling = true;
466
- clearTimeout(timeout);
467
- timeout = setTimeout(() => {
468
- this._controls?.classList.add("opacity-0");
469
- scroller2.classList.remove("cm-hide-scrollbar");
470
- isAutoScrolling = false;
471
- }, 500);
472
- view.dispatch({
473
- effects: EditorView.scrollIntoView(view.state.doc.length, {
474
- y: "end"
475
- })
476
- });
477
- };
478
- this._cleanup = combine(addEventListener(view.scrollDOM, "scroll", () => {
479
- if (!isAutoScrolling) {
480
- hasScrolled = true;
481
- this._controls?.classList.remove("opacity-0");
482
- }
483
- }), model.update.on(() => {
484
- const autoScroll = scroller2.scrollHeight - scroller2.scrollTop - scroller2.clientHeight === 0 || !hasScrolled;
485
- model.sync(this._adapter);
486
- if (autoScroll) {
487
- scrollToBottom(true);
488
- }
489
- }));
490
- }
491
- update(_update) {
492
- if (!this._initialized) {
493
- this._initialized = true;
494
- setTimeout(() => {
495
- model.sync(this._adapter);
496
- });
497
- }
498
- }
499
- destroy() {
500
- this._controls?.remove();
501
- this._cleanup();
502
- }
503
- }),
504
- EditorView.theme({
505
- ".cm-gutters": {
506
- backgroundColor: "var(--color-base-surface)"
507
- },
508
- ".cm-timestamp-gutter": {
509
- width: "6rem",
510
- paddingLeft: "0.5rem",
511
- paddingRight: "1rem"
512
- },
513
- ".cm-timestamp-gutter > .cm-gutterElement > div": {
514
- display: "inline-flex",
515
- textAlign: "right",
516
- padding: "3px"
517
- }
518
- })
519
- ];
520
- };
521
- var TimestampMarker = class extends GutterMarker {
522
- _line;
523
- _timestamp;
524
- _started;
525
- constructor(_line, _timestamp, _started) {
526
- super(), this._line = _line, this._timestamp = _timestamp, this._started = _started;
527
- }
528
- eq(other) {
529
- return this._timestamp === other._timestamp;
530
- }
531
- toDOM(view) {
532
- const el = document.createElement("div");
533
- el.className = "text-sm text-subdued hover:bg-hover-surface cursor-pointer";
534
- el.textContent = formatTimestamp(this._timestamp, this._started);
535
- el.onclick = () => {
536
- const pos = view.state.doc.line(this._line).from;
537
- view.dispatch({
538
- effects: EditorView.scrollIntoView(pos, {
539
- y: "start"
540
- })
541
- });
542
- };
543
- return el;
544
- }
545
- };
546
- var getStartTime = (started, message) => {
547
- if (started) {
548
- return started;
549
- }
550
- if (message?.blocks[0]?._tag === "transcript" && message.blocks[0].started) {
551
- return new Date(message.blocks[0].started);
552
- }
553
- return void 0;
554
- };
555
- var formatTimestamp = (timestamp, relative) => {
556
- if (relative) {
557
- const pad = (n = 0) => String(n).padStart(2, "0");
558
- const { hours, minutes, seconds } = intervalToDuration({
559
- start: relative,
560
- end: timestamp
561
- });
562
- return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
563
- } else {
564
- return format(timestamp, "HH:mm:ss");
565
- }
566
- };
567
-
568
- // src/components/Transcription/Transcription.tsx
569
- var inlinePreviewRegistry = {
570
- "link-preview": {
571
- block: false,
572
- urlSchemes: [
573
- "dxn:",
574
- "echo:"
575
- ],
576
- factory: ({ label, dxn }) => typeof label === "string" && typeof dxn === "string" ? new AnchorWidget(label, dxn) : null
577
- }
578
- };
579
- var Transcription = composable(({ transcript: object, model, children, ...props }, forwardedRef) => {
580
- const { themeMode } = useThemeContext();
581
- const { parentRef } = useTextEditor(() => {
582
- return {
583
- extensions: [
584
- createBasicExtensions({
585
- readOnly: true,
586
- lineWrapping: true,
587
- search: true
588
- }),
589
- createThemeExtensions({
590
- themeMode,
591
- slots: documentSlots
592
- }),
593
- createMarkdownExtensions(),
594
- // xmlTags() handles dxn:/echo: links via url-scheme widgets; skip here to avoid double-processing.
595
- decorateMarkdown({
596
- skip: ({ url }) => url.startsWith("dxn:") || url.startsWith("echo:")
597
- }),
598
- xmlTags({
599
- registry: inlinePreviewRegistry
600
- }),
601
- transcription({
602
- model,
603
- started: object?.started ? new Date(object.started) : void 0
604
- }),
605
- scroller()
606
- ]
607
- };
608
- }, [
609
- model,
610
- themeMode
611
- ]);
612
- return /* @__PURE__ */ React3.createElement("div", {
613
- ...composableProps(props, {
614
- classNames: "dx-container"
615
- }),
616
- "data-popover-collision-boundary": true,
617
- ref: parentRef
618
- });
619
- });
620
-
621
- // src/hooks/useAudioFile.ts
622
- import { useEffect, useState } from "react";
623
- import { scheduleTask } from "@dxos/async";
624
- import { Context } from "@dxos/context";
625
- import { log as log3 } from "@dxos/log";
626
- var __dxlog_file3 = "/__w/dxos/dxos/packages/ui/react-ui-transcription/src/hooks/useAudioFile.ts";
627
- var useAudioFile = (audioUrl, constraints) => {
628
- const [{ audio, stream, track }, setStream] = useState({});
629
- useEffect(() => {
630
- const ctx = new Context(void 0, { "~LogMeta": "~LogMeta", F: __dxlog_file3, L: 11 });
631
- scheduleTask(ctx, async () => {
632
- try {
633
- const response = await fetch(audioUrl);
634
- const blob = await response.blob();
635
- const objectUrl = URL.createObjectURL(blob);
636
- ctx.onDispose(() => {
637
- URL.revokeObjectURL(objectUrl);
638
- });
639
- const audio2 = new Audio();
640
- audio2.src = objectUrl;
641
- await new Promise((resolve, reject) => {
642
- audio2.addEventListener("error", (err) => {
643
- log3.error("error", {
644
- err
645
- }, { "~LogMeta": "~LogMeta", F: __dxlog_file3, L: 25, S: void 0 });
646
- reject(err);
647
- }, {
648
- once: true
649
- });
650
- audio2.addEventListener("canplay", async () => {
651
- resolve();
652
- }, {
653
- once: true
654
- });
655
- audio2.load();
656
- });
657
- const audioCtx = new AudioContext();
658
- if (audioCtx.state === "suspended") {
659
- await audioCtx.resume();
660
- }
661
- const destination = audioCtx.createMediaStreamDestination();
662
- destination.channelCount = 1;
663
- const source = audioCtx.createMediaElementSource(audio2);
664
- source.connect(destination);
665
- const track2 = destination.stream.getAudioTracks()[0];
666
- if (constraints) {
667
- await track2.applyConstraints(constraints).catch((err) => log3.catch(err, void 0, { "~LogMeta": "~LogMeta", F: __dxlog_file3, L: 50, S: void 0 }));
668
- }
669
- setStream({
670
- audio: audio2,
671
- stream: destination.stream,
672
- track: destination.stream.getAudioTracks()[0]
673
- });
674
- } catch (err) {
675
- log3.catch(err, void 0, { "~LogMeta": "~LogMeta", F: __dxlog_file3, L: 59, S: void 0 });
676
- }
677
- });
678
- return () => {
679
- void ctx.dispose();
680
- };
681
- }, [
682
- audioUrl
683
- ]);
684
- return {
685
- audio,
686
- stream,
687
- track
688
- };
689
- };
690
-
691
- // src/hooks/useAudioTrack.ts
692
- import { useEffect as useEffect2, useState as useState2 } from "react";
693
- var useAudioTrack = (active, constraints) => {
694
- const [track, setTrack] = useState2();
695
- const constraintsKey = constraints ? JSON.stringify(constraints) : "";
696
- useEffect2(() => {
697
- if (!active) {
698
- track?.stop();
699
- setTrack(void 0);
700
- return;
701
- }
702
- let cancelled = false;
703
- let acquired;
704
- setTrack(void 0);
705
- const initAudio = async () => {
706
- try {
707
- const stream = await navigator.mediaDevices.getUserMedia({
708
- audio: constraints ?? true
709
- });
710
- const [next] = stream.getAudioTracks();
711
- if (cancelled || !next) {
712
- next?.stop();
713
- return;
714
- }
715
- acquired = next;
716
- setTrack(next);
717
- } catch {
718
- if (!cancelled) {
719
- setTrack(void 0);
720
- }
721
- }
722
- };
723
- void initAudio();
724
- return () => {
725
- cancelled = true;
726
- acquired?.stop();
727
- };
728
- }, [
729
- active,
730
- constraintsKey
731
- ]);
732
- return track;
733
- };
734
-
735
- // src/hooks/useFeedModelAdapter.ts
736
- import { useEffect as useEffect3, useMemo } from "react";
737
- var useFeedModelAdapter = (renderer, objects, initialChunks = []) => {
738
- const model = useMemo(() => new TranscriptModel(renderer, initialChunks), [
739
- renderer
740
- ]);
741
- useEffect3(() => {
742
- for (const chunk of objects) {
743
- model.appendChunk(chunk);
744
- }
745
- }, [
746
- model,
747
- objects
748
- ]);
749
- useEffect3(() => {
750
- if (model.chunks.length === objects.length) {
751
- return;
752
- }
753
- const chunk = objects.at(-1);
754
- if (chunk) {
755
- model.appendChunk(chunk);
756
- }
757
- }, [
758
- model,
759
- objects.length
760
- ]);
761
- return model;
762
- };
763
-
764
- // src/hooks/useIsSpeaking.ts
765
- import { useEffect as useEffect4, useMemo as useMemo2, useState as useState3 } from "react";
766
- import { scheduleTask as scheduleTask2 } from "@dxos/async";
767
- import { SpeakingMonitor } from "@dxos/av";
768
- import { Context as Context2 } from "@dxos/context";
769
- var __dxlog_file4 = "/__w/dxos/dxos/packages/ui/react-ui-transcription/src/hooks/useIsSpeaking.ts";
770
- var useIsSpeaking = (track) => {
771
- const [isSpeaking, setIsSpeaking] = useState3(false);
772
- const speakingMonitor = useMemo2(() => {
773
- if (!track) {
774
- return;
775
- }
776
- return new SpeakingMonitor(track);
777
- }, [
778
- track
779
- ]);
780
- useEffect4(() => {
781
- if (!speakingMonitor) {
782
- return;
783
- }
784
- const ctx = new Context2(void 0, { "~LogMeta": "~LogMeta", F: __dxlog_file4, L: 24 });
785
- scheduleTask2(ctx, async () => {
786
- speakingMonitor.speakingChanged.on(ctx, () => setIsSpeaking(speakingMonitor.isSpeaking));
787
- await speakingMonitor.open();
788
- ctx.onDispose(() => speakingMonitor.close());
789
- });
790
- return () => {
791
- void ctx.dispose();
792
- };
793
- }, [
794
- speakingMonitor
795
- ]);
796
- return isSpeaking;
797
- };
798
-
799
- // src/hooks/useRecordingPipeline.ts
800
- import { useEffect as useEffect5, useMemo as useMemo3, useRef, useState as useState4 } from "react";
801
- import { log as log4 } from "@dxos/log";
802
- import { runAsrPipeline } from "@dxos/transcription-pipeline";
803
- var __dxlog_file5 = "/__w/dxos/dxos/packages/ui/react-ui-transcription/src/hooks/useRecordingPipeline.ts";
804
- var RECORDER_INTERVAL_MS = 200;
805
- var DRAIN_SILENCE_MS = 5e3;
806
- var DEFAULT_TRANSCRIBE_CONFIG = {
807
- transcribeAfterChunksAmount: 50,
808
- prefixBufferChunksAmount: 10
809
- };
810
- var useRecordingPipeline = ({ active, microphone, track: externalTrack, audioConstraints, stages, commit, lookup, onSegment, config, segmentSentences }) => {
811
- const [phase, setPhase] = useState4("idle");
812
- const inUse = phase !== "idle";
813
- useEffect5(() => {
814
- setPhase((current) => active ? "recording" : current === "recording" ? "draining" : current);
815
- }, [
816
- active
817
- ]);
818
- const micTrack = useAudioTrack(!!microphone && inUse, audioConstraints);
819
- const track = microphone ? micTrack : externalTrack;
820
- const recorder = useMemo3(() => track ? new MediaStreamRecorder({
821
- mediaStreamTrack: track,
822
- config: {
823
- interval: RECORDER_INTERVAL_MS
824
- }
825
- }) : void 0, [
826
- track
827
- ]);
828
- const pipelineRef = useRef(null);
829
- useEffect5(() => {
830
- if (phase !== "recording" || !recorder) {
831
- return;
832
- }
833
- const asr = runAsrPipeline({
834
- recorder,
835
- config: {
836
- ...DEFAULT_TRANSCRIBE_CONFIG,
837
- ...config
838
- },
839
- segmentSentences,
840
- stages,
841
- commit,
842
- lookup,
843
- onSegment,
844
- drainSilenceMs: DRAIN_SILENCE_MS
845
- });
846
- pipelineRef.current = asr;
847
- let cancelled = false;
848
- void asr.open().then(() => {
849
- if (!cancelled) {
850
- asr.start();
851
- }
852
- }, (err) => {
853
- if (!cancelled) {
854
- log4.catch(err, void 0, { "~LogMeta": "~LogMeta", F: __dxlog_file5, L: 72, S: void 0 });
855
- setPhase("idle");
856
- }
857
- });
858
- return () => {
859
- cancelled = true;
860
- };
861
- }, [
862
- phase,
863
- recorder
864
- ]);
865
- useEffect5(() => {
866
- if (phase !== "draining") {
867
- return;
868
- }
869
- let cancelled = false;
870
- void (async () => {
871
- try {
872
- await pipelineRef.current?.end();
873
- } catch (err) {
874
- log4.catch(err, void 0, { "~LogMeta": "~LogMeta", F: __dxlog_file5, L: 94, S: void 0 });
875
- } finally {
876
- pipelineRef.current = null;
877
- if (!cancelled) {
878
- setPhase((current) => current === "draining" ? "idle" : current);
879
- }
880
- }
881
- })();
882
- return () => {
883
- cancelled = true;
884
- };
885
- }, [
886
- phase
887
- ]);
888
- return {
889
- phase
890
- };
891
- };
892
-
893
- // src/hooks/useSpeechRecognition.ts
894
- import { useEffect as useEffect6, useMemo as useMemo4 } from "react";
895
- import { log as log5 } from "@dxos/log";
896
- var __dxlog_file6 = "/__w/dxos/dxos/packages/ui/react-ui-transcription/src/hooks/useSpeechRecognition.ts";
897
- var cleanText = (text) => text.toLowerCase().replace(/[.,!?]/g, "").replace(/\s+/g, " ").trim();
898
- var useSpeechRecognition = ({ active, onTranscript }) => {
899
- const recognition = useMemo4(() => {
900
- const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
901
- const SpeechGrammarList = window.SpeechGrammarList || window.webkitSpeechGrammarList;
902
- if (!SpeechRecognition || !SpeechGrammarList) {
903
- log5.error("Speech recognition not supported", void 0, { "~LogMeta": "~LogMeta", F: __dxlog_file6, L: 14, S: void 0 });
904
- return;
905
- }
906
- return Object.assign(new SpeechRecognition(), {
907
- grammars: new SpeechGrammarList(),
908
- continuous: true,
909
- interimResults: true,
910
- lang: "en-US",
911
- maxAlternatives: 1,
912
- onresult: (event) => {
913
- const transcript = cleanText(event.results[event.resultIndex][0].transcript);
914
- onTranscript(transcript);
915
- },
916
- onerror: (event) => {
917
- log5.error("Speech recognition error", {
918
- error: event.error
919
- }, { "~LogMeta": "~LogMeta", F: __dxlog_file6, L: 28, S: void 0 });
920
- }
921
- });
922
- }, [
923
- onTranscript
924
- ]);
925
- useEffect6(() => {
926
- if (active) {
927
- recognition?.start();
928
- } else {
929
- recognition?.stop();
930
- }
931
- }, [
932
- active,
933
- recognition
934
- ]);
935
- };
936
-
937
- // src/hooks/useTranscriber.ts
938
- import { useEffect as useEffect7, useMemo as useMemo5 } from "react";
939
- var useTranscriber = ({ audioStreamTrack, recorderConfig, transcriberConfig, transcribe, onSegments }) => {
940
- const transcriber = useMemo5(() => {
941
- if (!audioStreamTrack || !onSegments) {
942
- return void 0;
943
- }
944
- return createTranscriber({
945
- audioStreamTrack,
946
- recorderConfig,
947
- transcriberConfig,
948
- transcribe,
949
- onSegments
950
- });
951
- }, [
952
- audioStreamTrack,
953
- recorderConfig,
954
- transcriberConfig,
955
- transcribe,
956
- onSegments
957
- ]);
958
- useEffect7(() => {
959
- return () => {
960
- void transcriber?.close();
961
- };
962
- }, [
963
- transcriber
964
- ]);
965
- return transcriber;
966
- };
967
-
968
- // src/util/render-byline.ts
969
- var renderByline = (identities) => (message, index, debug = false) => {
970
- if (message.sender.role === "assistant") {
971
- return [
972
- message.blocks.find((block) => block._tag === "transcript")?.text ?? "",
973
- ""
974
- ];
975
- }
976
- const identity = identities.find((identity2) => identity2.did === message.sender.identityDid);
977
- const name = identity?.profile?.displayName ?? message.sender.contact?.target?.fullName ?? message.sender.name ?? message.sender.email ?? message.sender.identityDid;
978
- const blocks = message.blocks.filter((block) => block._tag === "transcript");
979
- return [
980
- // TODO(thure): Use an XML tag with the bits needed here.
981
- `###### ${name}` + (debug ? ` [${index}]:${message.id}` : ""),
982
- blocks.map((block) => block.text.trim()).join(" "),
983
- ""
984
- ];
985
- };
986
- export {
987
- EditorChunkDocument,
988
- MediaStreamRecorder,
989
- MicSettings,
990
- PipelineStatus,
991
- TranscriptModel,
992
- Transcription,
993
- createTranscriber,
994
- renderByline,
995
- transcription,
996
- useAudioFile,
997
- useAudioTrack,
998
- useFeedModelAdapter,
999
- useIsSpeaking,
1000
- useRecordingPipeline,
1001
- useSpeechRecognition,
1002
- useTranscriber
1003
- };
1004
- //# sourceMappingURL=index.mjs.map