@dxos/pipeline-transcription 0.0.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 (127) hide show
  1. package/dist/lib/neutral/chunk-Y44GOMGN.mjs +22 -0
  2. package/dist/lib/neutral/chunk-Y44GOMGN.mjs.map +7 -0
  3. package/dist/lib/neutral/index.mjs +886 -0
  4. package/dist/lib/neutral/index.mjs.map +7 -0
  5. package/dist/lib/neutral/meta.json +1 -0
  6. package/dist/lib/neutral/testing/index.mjs +192 -0
  7. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  8. package/dist/types/src/PipelineConfig.d.ts +49 -0
  9. package/dist/types/src/PipelineConfig.d.ts.map +1 -0
  10. package/dist/types/src/PipelineConfig.test.d.ts +2 -0
  11. package/dist/types/src/PipelineConfig.test.d.ts.map +1 -0
  12. package/dist/types/src/PipelineRuntime.d.ts +46 -0
  13. package/dist/types/src/PipelineRuntime.d.ts.map +1 -0
  14. package/dist/types/src/PipelineRuntime.test.d.ts +2 -0
  15. package/dist/types/src/PipelineRuntime.test.d.ts.map +1 -0
  16. package/dist/types/src/Stage.d.ts +80 -0
  17. package/dist/types/src/Stage.d.ts.map +1 -0
  18. package/dist/types/src/TranscriptEvent.d.ts +23 -0
  19. package/dist/types/src/TranscriptEvent.d.ts.map +1 -0
  20. package/dist/types/src/asr/audio-recorder.d.ts +47 -0
  21. package/dist/types/src/asr/audio-recorder.d.ts.map +1 -0
  22. package/dist/types/src/asr/index.d.ts +5 -0
  23. package/dist/types/src/asr/index.d.ts.map +1 -0
  24. package/dist/types/src/asr/pipeline.d.ts +42 -0
  25. package/dist/types/src/asr/pipeline.d.ts.map +1 -0
  26. package/dist/types/src/asr/sentence-buffer.d.ts +25 -0
  27. package/dist/types/src/asr/sentence-buffer.d.ts.map +1 -0
  28. package/dist/types/src/asr/sentence-buffer.test.d.ts +2 -0
  29. package/dist/types/src/asr/sentence-buffer.test.d.ts.map +1 -0
  30. package/dist/types/src/asr/transcriber.d.ts +115 -0
  31. package/dist/types/src/asr/transcriber.d.ts.map +1 -0
  32. package/dist/types/src/asr/transcriber.test.d.ts +2 -0
  33. package/dist/types/src/asr/transcriber.test.d.ts.map +1 -0
  34. package/dist/types/src/asr/util.d.ts +2 -0
  35. package/dist/types/src/asr/util.d.ts.map +1 -0
  36. package/dist/types/src/dispatch.d.ts +19 -0
  37. package/dist/types/src/dispatch.d.ts.map +1 -0
  38. package/dist/types/src/dispatch.test.d.ts +2 -0
  39. package/dist/types/src/dispatch.test.d.ts.map +1 -0
  40. package/dist/types/src/index.d.ts +11 -0
  41. package/dist/types/src/index.d.ts.map +1 -0
  42. package/dist/types/src/live.d.ts +24 -0
  43. package/dist/types/src/live.d.ts.map +1 -0
  44. package/dist/types/src/live.test.d.ts +2 -0
  45. package/dist/types/src/live.test.d.ts.map +1 -0
  46. package/dist/types/src/lookup.d.ts +35 -0
  47. package/dist/types/src/lookup.d.ts.map +1 -0
  48. package/dist/types/src/lookup.test.d.ts +2 -0
  49. package/dist/types/src/lookup.test.d.ts.map +1 -0
  50. package/dist/types/src/model-routing.d.ts +12 -0
  51. package/dist/types/src/model-routing.d.ts.map +1 -0
  52. package/dist/types/src/pipeline-integration.test.d.ts +2 -0
  53. package/dist/types/src/pipeline-integration.test.d.ts.map +1 -0
  54. package/dist/types/src/stages/correction-llm.d.ts +14 -0
  55. package/dist/types/src/stages/correction-llm.d.ts.map +1 -0
  56. package/dist/types/src/stages/correction-llm.test.d.ts +2 -0
  57. package/dist/types/src/stages/correction-llm.test.d.ts.map +1 -0
  58. package/dist/types/src/stages/correction.d.ts +29 -0
  59. package/dist/types/src/stages/correction.d.ts.map +1 -0
  60. package/dist/types/src/stages/correction.test.d.ts +2 -0
  61. package/dist/types/src/stages/correction.test.d.ts.map +1 -0
  62. package/dist/types/src/stages/diarization.d.ts +11 -0
  63. package/dist/types/src/stages/diarization.d.ts.map +1 -0
  64. package/dist/types/src/stages/extraction.d.ts +40 -0
  65. package/dist/types/src/stages/extraction.d.ts.map +1 -0
  66. package/dist/types/src/stages/extraction.test.d.ts +2 -0
  67. package/dist/types/src/stages/extraction.test.d.ts.map +1 -0
  68. package/dist/types/src/stages/index.d.ts +18 -0
  69. package/dist/types/src/stages/index.d.ts.map +1 -0
  70. package/dist/types/src/stages/summarization.d.ts +12 -0
  71. package/dist/types/src/stages/summarization.d.ts.map +1 -0
  72. package/dist/types/src/stages/translation.d.ts +11 -0
  73. package/dist/types/src/stages/translation.d.ts.map +1 -0
  74. package/dist/types/src/testing/conversation-parser.d.ts +26 -0
  75. package/dist/types/src/testing/conversation-parser.d.ts.map +1 -0
  76. package/dist/types/src/testing/conversation.test.d.ts +2 -0
  77. package/dist/types/src/testing/conversation.test.d.ts.map +1 -0
  78. package/dist/types/src/testing/fixtures.d.ts +6 -0
  79. package/dist/types/src/testing/fixtures.d.ts.map +1 -0
  80. package/dist/types/src/testing/index.d.ts +5 -0
  81. package/dist/types/src/testing/index.d.ts.map +1 -0
  82. package/dist/types/src/testing/scripted-source.d.ts +16 -0
  83. package/dist/types/src/testing/scripted-source.d.ts.map +1 -0
  84. package/dist/types/src/testing/stream-simulator.d.ts +38 -0
  85. package/dist/types/src/testing/stream-simulator.d.ts.map +1 -0
  86. package/dist/types/tsconfig.tsbuildinfo +1 -0
  87. package/package.json +61 -0
  88. package/src/PipelineConfig.test.ts +39 -0
  89. package/src/PipelineConfig.ts +71 -0
  90. package/src/PipelineRuntime.test.ts +74 -0
  91. package/src/PipelineRuntime.ts +192 -0
  92. package/src/Stage.ts +86 -0
  93. package/src/TranscriptEvent.ts +23 -0
  94. package/src/asr/audio-recorder.ts +56 -0
  95. package/src/asr/index.ts +8 -0
  96. package/src/asr/pipeline.ts +105 -0
  97. package/src/asr/sentence-buffer.test.ts +53 -0
  98. package/src/asr/sentence-buffer.ts +88 -0
  99. package/src/asr/transcriber.test.ts +104 -0
  100. package/src/asr/transcriber.ts +337 -0
  101. package/src/asr/util.ts +14 -0
  102. package/src/dispatch.test.ts +37 -0
  103. package/src/dispatch.ts +56 -0
  104. package/src/index.ts +14 -0
  105. package/src/live.test.ts +63 -0
  106. package/src/live.ts +56 -0
  107. package/src/lookup.test.ts +41 -0
  108. package/src/lookup.ts +60 -0
  109. package/src/model-routing.ts +25 -0
  110. package/src/pipeline-integration.test.ts +70 -0
  111. package/src/stages/correction-llm.test.ts +46 -0
  112. package/src/stages/correction-llm.ts +40 -0
  113. package/src/stages/correction.test.ts +26 -0
  114. package/src/stages/correction.ts +72 -0
  115. package/src/stages/diarization.ts +24 -0
  116. package/src/stages/extraction.test.ts +79 -0
  117. package/src/stages/extraction.ts +151 -0
  118. package/src/stages/index.ts +44 -0
  119. package/src/stages/summarization.ts +37 -0
  120. package/src/stages/translation.ts +26 -0
  121. package/src/testing/conversation-parser.ts +101 -0
  122. package/src/testing/conversation.test.ts +53 -0
  123. package/src/testing/conversation.txt +23 -0
  124. package/src/testing/fixtures.ts +18 -0
  125. package/src/testing/index.ts +8 -0
  126. package/src/testing/scripted-source.ts +35 -0
  127. package/src/testing/stream-simulator.ts +125 -0
@@ -0,0 +1,886 @@
1
+ import {
2
+ TranscriptEvent
3
+ } from "./chunk-Y44GOMGN.mjs";
4
+
5
+ // src/Stage.ts
6
+ var StageWrite = Object.freeze({
7
+ blocks: (blockUpdates) => ({
8
+ blockUpdates
9
+ }),
10
+ transcript: (transcriptUpdate) => ({
11
+ transcriptUpdate
12
+ }),
13
+ empty: () => ({})
14
+ });
15
+
16
+ // src/lookup.ts
17
+ import { Filter, Obj, Query, Ref } from "@dxos/echo";
18
+ var makeDatabaseLookup = (db, { searchKind = "full-text", limit = 5 } = {}) => async (noun, context) => {
19
+ const entries = await db.query(Query.select(Filter.text(noun, {
20
+ type: searchKind
21
+ }))).runEntries();
22
+ const types = context?.types;
23
+ return entries.flatMap((entry) => {
24
+ if (!entry.result || types?.length && !types.includes(Obj.getTypename(entry.result) ?? "")) {
25
+ return [];
26
+ }
27
+ return [
28
+ {
29
+ id: entry.id,
30
+ ref: Ref.make(entry.result),
31
+ score: entry.match?.rank ?? 0
32
+ }
33
+ ];
34
+ }).sort((a, b) => b.score - a.score).slice(0, limit);
35
+ };
36
+
37
+ // src/PipelineConfig.ts
38
+ import * as Schema from "effect/Schema";
39
+ import { DXN, Obj as Obj2, Type } from "@dxos/echo";
40
+ var DEFAULT_STAGE_MODEL = DXN.make("com.anthropic.model.claude-haiku-4-5.default");
41
+ var StageConfig = Schema.Struct({
42
+ /** Stage id (matches a registered `Stage.id`). */
43
+ id: Schema.String,
44
+ enabled: Schema.Boolean,
45
+ /** Optional per-stage model override (an NSID name); resolved by the runtime. */
46
+ model: Schema.optional(DXN.NameSchema),
47
+ /** Optional sliding-window override in blocks. Positive integer; bad values fail validation. */
48
+ window: Schema.optional(Schema.Struct({
49
+ blocks: Schema.Number.pipe(Schema.int(), Schema.positive())
50
+ }))
51
+ });
52
+ var PipelineConfig = class extends Type.makeObject(DXN.make("org.dxos.type.transcriptionPipelineConfig", "0.1.0"))(Schema.Struct({
53
+ name: Schema.String,
54
+ stages: Schema.mutable(Schema.Array(StageConfig))
55
+ })) {
56
+ };
57
+ var PIPELINE_PRESETS = Object.freeze([
58
+ {
59
+ name: "Meeting",
60
+ stages: [
61
+ {
62
+ id: "correct",
63
+ enabled: true
64
+ },
65
+ {
66
+ id: "extract",
67
+ enabled: true
68
+ },
69
+ {
70
+ id: "summarize",
71
+ enabled: true
72
+ }
73
+ ]
74
+ },
75
+ {
76
+ name: "Notes",
77
+ stages: [
78
+ {
79
+ id: "correct",
80
+ enabled: true
81
+ },
82
+ {
83
+ id: "extract",
84
+ enabled: false
85
+ },
86
+ {
87
+ id: "summarize",
88
+ enabled: false
89
+ }
90
+ ]
91
+ }
92
+ ]);
93
+ var findPreset = (name) => PIPELINE_PRESETS.find((preset) => preset.name === name);
94
+ var makeConfigFromPreset = (preset) => Obj2.make(PipelineConfig, {
95
+ name: preset.name,
96
+ stages: preset.stages.map((stage) => ({
97
+ ...stage
98
+ }))
99
+ });
100
+
101
+ // src/model-routing.ts
102
+ import { DXN as DXN2 } from "@dxos/echo";
103
+ var resolveModel = (stageConfig, stage, presetDefault) => (stageConfig?.model ? DXN2.tryMake(stageConfig.model) : void 0) ?? stage.model ?? presetDefault ?? DEFAULT_STAGE_MODEL;
104
+
105
+ // src/PipelineRuntime.ts
106
+ import * as Cause from "effect/Cause";
107
+ import * as Context from "effect/Context";
108
+ import * as Effect from "effect/Effect";
109
+ import { pipe } from "effect/Function";
110
+ import * as Stream from "effect/Stream";
111
+ import { log } from "@dxos/log";
112
+ import { Pipeline, Stage as PipelineStage } from "@dxos/pipeline";
113
+ var __dxlog_file = "/Users/jdw/Code/dxos/packages/core/compute/pipeline-transcription/src/PipelineRuntime.ts";
114
+ var MAX_WINDOW = 128;
115
+ var BROADCAST_BUFFER = 128;
116
+ var StageContextService = class extends Context.Tag("@dxos/pipeline-transcription/StageContextService")() {
117
+ };
118
+ var triggerMatches = (stage, kind) => stage.trigger === "per-block" && kind === "block" || stage.trigger === "on-silence" && kind === "silence" || stage.trigger === "periodic" && kind === "tick";
119
+ var triggerKind = (stage) => stage.trigger === "per-block" ? "block" : stage.trigger === "on-silence" ? "silence" : "tick";
120
+ var windowTrigger = (stage, config) => {
121
+ const blocks = config?.window?.blocks ?? stage.window?.blocks;
122
+ return (input) => input.pipe(Stream.mapAccum([], (window, event) => {
123
+ const next = event.kind === "block" ? [
124
+ ...window,
125
+ event.block
126
+ ].slice(-MAX_WINDOW) : window;
127
+ const slice = triggerMatches(stage, event.kind) ? blocks ? next.slice(-blocks) : next : void 0;
128
+ return [
129
+ next,
130
+ slice
131
+ ];
132
+ }), Stream.filter((slice) => slice !== void 0));
133
+ };
134
+ var runStage = (stage, config, presetDefault, onTelemetry) => {
135
+ const model = resolveModel(config, stage, presetDefault);
136
+ const trigger = triggerKind(stage);
137
+ return PipelineStage.map(stage.id, (slice) => Effect.gen(function* () {
138
+ const context = yield* StageContextService;
139
+ const started = yield* Effect.sync(() => Date.now());
140
+ const input = stage.select ? stage.select(slice) : {
141
+ window: slice
142
+ };
143
+ const write = yield* stage.run(input, context);
144
+ const durationMs = (yield* Effect.sync(() => Date.now())) - started;
145
+ onTelemetry?.({
146
+ stageId: stage.id,
147
+ model,
148
+ trigger,
149
+ durationMs,
150
+ outcome: "committed"
151
+ });
152
+ return {
153
+ write,
154
+ window: slice
155
+ };
156
+ }).pipe(Effect.catchAllCause((cause) => Effect.sync(() => {
157
+ log.catch(Cause.squash(cause), void 0, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 70, S: void 0 });
158
+ onTelemetry?.({
159
+ stageId: stage.id,
160
+ model,
161
+ trigger,
162
+ durationMs: 0,
163
+ outcome: "error"
164
+ });
165
+ return void 0;
166
+ }))), {
167
+ overflow: stage.concurrency === "latest-wins" ? "sliding" : "dropping",
168
+ bufferSize: 1
169
+ });
170
+ };
171
+ var run = (options) => Effect.scoped(Effect.gen(function* () {
172
+ const { source, stages, commit, lookup, configs, presetDefault, onTelemetry } = options;
173
+ const configFor = (id) => configs?.find((config) => config.id === id);
174
+ const enabled = stages.filter((stage) => configFor(stage.id)?.enabled ?? true);
175
+ if (enabled.length === 0) {
176
+ return yield* Stream.runDrain(source);
177
+ }
178
+ const sink = ({ write, window }) => commit(write, window).pipe(Effect.catchAllCause((cause) => Effect.sync(() => log.catch(Cause.squash(cause), void 0, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 98, S: this }))));
179
+ const branches = yield* source.pipe(Stream.broadcast(enabled.length, BROADCAST_BUFFER));
180
+ yield* Effect.forEach(branches, (branch, index) => {
181
+ const stage = enabled[index];
182
+ const config = configFor(stage.id);
183
+ const context = {
184
+ lookup,
185
+ model: resolveModel(config, stage, presetDefault)
186
+ };
187
+ return pipe(branch, windowTrigger(stage, config), runStage(stage, config, presetDefault, onTelemetry), Pipeline.run({
188
+ sink
189
+ }), Effect.provideService(StageContextService, context));
190
+ }, {
191
+ concurrency: "unbounded",
192
+ discard: true
193
+ });
194
+ }));
195
+ var PipelineRuntime = Object.freeze({
196
+ run
197
+ });
198
+
199
+ // src/live.ts
200
+ import * as Effect2 from "effect/Effect";
201
+ import * as Queue from "effect/Queue";
202
+ import * as Stream2 from "effect/Stream";
203
+ import { EffectEx } from "@dxos/effect";
204
+ var END = /* @__PURE__ */ Symbol("end");
205
+ var runLivePipeline = (options) => {
206
+ const queue = Effect2.runSync(Queue.unbounded());
207
+ const source = Stream2.fromQueue(queue).pipe(Stream2.takeWhile((item) => item !== END));
208
+ const done = EffectEx.runPromise(PipelineRuntime.run({
209
+ ...options,
210
+ source
211
+ }));
212
+ const offer2 = (item) => Effect2.runSync(Queue.offer(queue, item));
213
+ return {
214
+ block: (block) => offer2(TranscriptEvent.block(block)),
215
+ silence: (sinceMs) => offer2(TranscriptEvent.silence(sinceMs)),
216
+ tick: () => offer2(TranscriptEvent.tick()),
217
+ end: async () => {
218
+ offer2(END);
219
+ await done;
220
+ Effect2.runSync(Queue.shutdown(queue));
221
+ }
222
+ };
223
+ };
224
+
225
+ // src/asr/sentence-buffer.ts
226
+ var splitSentences = (text) => {
227
+ const sentences = [];
228
+ let start = 0;
229
+ for (let index = 0; index < text.length; index++) {
230
+ const char = text[index];
231
+ if (char !== "." && char !== "!" && char !== "?") {
232
+ continue;
233
+ }
234
+ let end = index;
235
+ while (end + 1 < text.length && (text[end + 1] === "." || text[end + 1] === "!" || text[end + 1] === "?")) {
236
+ end++;
237
+ }
238
+ const isEllipsis = text.slice(index, end + 1).startsWith("..");
239
+ const next = text[end + 1];
240
+ const atBoundary = next === void 0 || /\s/.test(next);
241
+ if (!isEllipsis && atBoundary) {
242
+ const sentence = text.slice(start, end + 1).trim();
243
+ if (sentence.length > 0) {
244
+ sentences.push(sentence);
245
+ }
246
+ start = end + 1;
247
+ }
248
+ index = end;
249
+ }
250
+ return {
251
+ sentences,
252
+ rest: text.slice(start).trim()
253
+ };
254
+ };
255
+ var SentenceBuffer = class {
256
+ #text = "";
257
+ #started;
258
+ /** Append a fragment; returns any complete sentences it produced. */
259
+ push(block) {
260
+ const fragment = block.text.trim();
261
+ if (fragment.length === 0) {
262
+ return [];
263
+ }
264
+ if (this.#text.length === 0) {
265
+ this.#started = block.started;
266
+ }
267
+ this.#text = this.#text.length === 0 ? fragment : `${this.#text} ${fragment}`;
268
+ const { sentences, rest } = splitSentences(this.#text);
269
+ if (sentences.length === 0) {
270
+ return [];
271
+ }
272
+ const startedForSentences = this.#started;
273
+ this.#text = rest;
274
+ this.#started = rest.length > 0 ? this.#started : void 0;
275
+ return sentences.map((text, index) => ({
276
+ _tag: "transcript",
277
+ started: index === 0 ? startedForSentences ?? block.started : block.started,
278
+ text
279
+ }));
280
+ }
281
+ /** Emit any buffered text as a final sentence. */
282
+ flush() {
283
+ const remaining = this.#text.trim();
284
+ this.#text = "";
285
+ const started = this.#started;
286
+ this.#started = void 0;
287
+ if (remaining.length === 0) {
288
+ return [];
289
+ }
290
+ return [
291
+ {
292
+ _tag: "transcript",
293
+ started: started ?? (/* @__PURE__ */ new Date(0)).toISOString(),
294
+ text: remaining
295
+ }
296
+ ];
297
+ }
298
+ };
299
+
300
+ // src/asr/transcriber.ts
301
+ import { WaveFile } from "wavefile";
302
+ import { DeferredTask, Trigger, synchronized } from "@dxos/async";
303
+ import { EDGE_SERVICE_DEFAULTS, EdgeServiceName } from "@dxos/config";
304
+ import { LifecycleState, Resource } from "@dxos/context";
305
+ import { log as log2 } from "@dxos/log";
306
+ import { trace } from "@dxos/tracing";
307
+
308
+ // src/asr/util.ts
309
+ var mergeFloat64Arrays = (arrays) => {
310
+ const newArray = new Float64Array(arrays.reduce((acc, array) => array.length + acc, 0));
311
+ let offset = 0;
312
+ for (const array of arrays) {
313
+ newArray.set(array, offset);
314
+ offset += array.length;
315
+ }
316
+ return newArray;
317
+ };
318
+
319
+ // src/asr/transcriber.ts
320
+ var __dxlog_file2 = "/Users/jdw/Code/dxos/packages/core/compute/pipeline-transcription/src/asr/transcriber.ts";
321
+ function _ts_decorate(decorators, target, key, desc) {
322
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
323
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
324
+ 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;
325
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
326
+ }
327
+ var Transcriber = class extends Resource {
328
+ _audioChunks = [];
329
+ _lastTimestamp = 0;
330
+ _openTrigger = new Trigger({
331
+ autoReset: true
332
+ });
333
+ _config;
334
+ _recorder;
335
+ _onSegments;
336
+ _transcribeFn;
337
+ _recording = false;
338
+ _transcribeTask = void 0;
339
+ /** Aborts the in-flight transcription request (e.g. when the user cancels the drain). */
340
+ _transcribeAbort = void 0;
341
+ constructor({ config, recorder, onSegments, transcribe }) {
342
+ super();
343
+ this._config = config;
344
+ this._recorder = recorder;
345
+ this._onSegments = onSegments;
346
+ this._transcribeFn = transcribe;
347
+ }
348
+ async _open(ctx) {
349
+ log2.info("opening", void 0, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 42, S: this });
350
+ this._recorder.setOnChunk((chunk) => this._saveAudioChunk(chunk));
351
+ await this._recorder.start();
352
+ this._transcribeTask = new DeferredTask(ctx, async () => this._transcribe());
353
+ this._openTrigger.wake();
354
+ }
355
+ async _close() {
356
+ log2.info("closing", void 0, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 49, S: this });
357
+ this._recording = false;
358
+ this._transcribeTask = void 0;
359
+ this._transcribeAbort?.abort();
360
+ await this._recorder.stop();
361
+ }
362
+ /** Abort the in-flight transcription request (e.g. user cancelled the drain). */
363
+ abort() {
364
+ this._transcribeAbort?.abort();
365
+ }
366
+ startChunksRecording() {
367
+ log2.info("starting", void 0, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 59, S: this });
368
+ if (this._lifecycleState !== LifecycleState.OPEN) {
369
+ return;
370
+ }
371
+ this._recording = true;
372
+ }
373
+ stopChunksRecording() {
374
+ if (this._lifecycleState !== LifecycleState.OPEN || !this._recording) {
375
+ return;
376
+ }
377
+ this._recording = false;
378
+ this._transcribeTask?.schedule();
379
+ log2.info("stopped", void 0, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 71, S: this });
380
+ }
381
+ /**
382
+ * Stop recording and transcribe any buffered audio, resolving once the final segments have been
383
+ * delivered via `onSegments`. Used to drain the pipeline when recording stops so the tail of speech
384
+ * (and its downstream enrichment) is not lost.
385
+ */
386
+ async flush() {
387
+ if (this._lifecycleState !== LifecycleState.OPEN) {
388
+ return;
389
+ }
390
+ this._recording = false;
391
+ await this._transcribe();
392
+ }
393
+ _saveAudioChunk(chunk) {
394
+ log2("saving audio chunk", {
395
+ chunk
396
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 85, S: this });
397
+ this._audioChunks.push(chunk);
398
+ if (!this._recording && (!this._transcribeTask || !this._transcribeTask.scheduled) && this._audioChunks.length >= this._config.prefixBufferChunksAmount) {
399
+ this._audioChunks = this._dropOldChunks();
400
+ } else if (this._audioChunks.length >= this._config.transcribeAfterChunksAmount && this._recording) {
401
+ this._transcribeTask?.schedule();
402
+ }
403
+ }
404
+ // Serialized so a threshold-scheduled run and a flush()/deferred run cannot read the same buffer
405
+ // concurrently and duplicate or drop tail segments.
406
+ async _transcribe() {
407
+ log2("transcribing", {
408
+ chunks: this._audioChunks.length
409
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 99, S: this });
410
+ const chunks = this._audioChunks;
411
+ this._audioChunks = this._dropOldChunks();
412
+ if (chunks.length === 0) {
413
+ return;
414
+ }
415
+ const audio = await this._mergeAudioChunks(chunks);
416
+ const segments = await this._fetchTranscription(audio);
417
+ if (!Array.isArray(segments) || segments.length === 0) {
418
+ return;
419
+ }
420
+ const alignedSegments = this._alignSegments(segments, chunks);
421
+ if (alignedSegments.length > 0) {
422
+ await this._onSegments(alignedSegments);
423
+ }
424
+ }
425
+ _dropOldChunks() {
426
+ return this._config.prefixBufferChunksAmount > 0 ? this._audioChunks.slice(-this._config.prefixBufferChunksAmount) : [];
427
+ }
428
+ async _mergeAudioChunks(chunks) {
429
+ const file = new WaveFile();
430
+ const wavConfig = this._recorder.wavConfig;
431
+ file.fromScratch(wavConfig.channels, wavConfig.sampleRate, wavConfig.bitDepthCode, mergeFloat64Arrays(chunks.map(({ data }) => data)));
432
+ return file.toBase64();
433
+ }
434
+ async _fetchTranscription(audio) {
435
+ if (audio.length === 0) {
436
+ throw new Error("No audio to send for transcribing");
437
+ }
438
+ let segments;
439
+ if (this._transcribeFn) {
440
+ segments = await this._transcribeFn(audio);
441
+ } else {
442
+ const endpoint = this._config.endpoint ?? EDGE_SERVICE_DEFAULTS[EdgeServiceName.Transcription];
443
+ this._transcribeAbort = new AbortController();
444
+ let response;
445
+ try {
446
+ response = await fetch(`${endpoint}/transcribe`, {
447
+ method: "POST",
448
+ body: JSON.stringify({
449
+ audio
450
+ }),
451
+ headers: {
452
+ "Content-Type": "application/json"
453
+ },
454
+ signal: this._transcribeAbort.signal
455
+ });
456
+ } catch (err) {
457
+ if (err instanceof DOMException && err.name === "AbortError") {
458
+ return [];
459
+ }
460
+ throw err;
461
+ } finally {
462
+ this._transcribeAbort = void 0;
463
+ }
464
+ if (!response.ok) {
465
+ throw new Error(`Transcription failed: ${response.statusText}`);
466
+ }
467
+ ({ segments } = await response.json());
468
+ }
469
+ if (!Array.isArray(segments)) {
470
+ throw new Error("Transcription response payload is invalid");
471
+ }
472
+ log2.info("transcription response", {
473
+ segments: segments.length
474
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 166, S: this });
475
+ return segments;
476
+ }
477
+ _alignSegments(segments, originalChunks) {
478
+ const result = alignWhisperSegments(segments, originalChunks, this._lastTimestamp);
479
+ if (result.lastTimestamp !== void 0) {
480
+ this._lastTimestamp = result.lastTimestamp;
481
+ }
482
+ return result.transcripts;
483
+ }
484
+ };
485
+ _ts_decorate([
486
+ synchronized
487
+ ], Transcriber.prototype, "_transcribe", null);
488
+ _ts_decorate([
489
+ trace.span({
490
+ showInBrowserTimeline: true
491
+ })
492
+ ], Transcriber.prototype, "_mergeAudioChunks", null);
493
+ _ts_decorate([
494
+ trace.span({
495
+ showInBrowserTimeline: true
496
+ })
497
+ ], Transcriber.prototype, "_fetchTranscription", null);
498
+ var alignWhisperSegments = (segments, originalChunks, lastTimestamp) => {
499
+ if (originalChunks.length === 0) {
500
+ return {
501
+ transcripts: []
502
+ };
503
+ }
504
+ const zeroTimestamp = originalChunks[0].timestamp;
505
+ const filteredSegments = segments.filter((segment) => zeroTimestamp + segment.end * 1e3 >= lastTimestamp).map((segment) => {
506
+ const words = segment.words.filter((word) => zeroTimestamp + word.start * 1e3 >= lastTimestamp);
507
+ return {
508
+ ...segment,
509
+ words,
510
+ text: words.map((word) => word.word).join(""),
511
+ start: words.at(0)?.start ?? segment.end
512
+ };
513
+ }).filter((segment) => segment.words.length > 0);
514
+ if (filteredSegments.length === 0) {
515
+ return {
516
+ transcripts: []
517
+ };
518
+ }
519
+ return {
520
+ lastTimestamp: zeroTimestamp + filteredSegments.at(-1).end * 1e3,
521
+ transcripts: filteredSegments.map((segment) => ({
522
+ _tag: "transcript",
523
+ started: new Date(zeroTimestamp + segment.start * 1e3).toISOString(),
524
+ text: segment.text.trim()
525
+ }))
526
+ };
527
+ };
528
+
529
+ // src/asr/pipeline.ts
530
+ var runAsrPipeline = ({ recorder, transcribe, config, segmentSentences = false, onSegment, drainSilenceMs = 5e3, ...runOptions }) => {
531
+ const live = runLivePipeline(runOptions);
532
+ const sentences = segmentSentences ? new SentenceBuffer() : void 0;
533
+ const emit = (block) => {
534
+ onSegment?.(block);
535
+ live.block(block);
536
+ };
537
+ const handle = (blocks) => {
538
+ for (const block of blocks) {
539
+ if (sentences) {
540
+ for (const sentence of sentences.push(block)) {
541
+ emit(sentence);
542
+ }
543
+ } else {
544
+ emit(block);
545
+ }
546
+ }
547
+ };
548
+ const transcriber = new Transcriber({
549
+ config,
550
+ recorder,
551
+ transcribe,
552
+ onSegments: async (blocks) => handle(blocks)
553
+ });
554
+ return {
555
+ open: async () => {
556
+ await transcriber.open();
557
+ },
558
+ start: () => transcriber.startChunksRecording(),
559
+ stop: () => transcriber.stopChunksRecording(),
560
+ abort: () => transcriber.abort(),
561
+ end: async () => {
562
+ await transcriber.flush();
563
+ if (sentences) {
564
+ for (const sentence of sentences.flush()) {
565
+ emit(sentence);
566
+ }
567
+ }
568
+ live.silence(drainSilenceMs);
569
+ await live.end();
570
+ await transcriber.close();
571
+ }
572
+ };
573
+ };
574
+
575
+ // src/dispatch.ts
576
+ import * as Effect3 from "effect/Effect";
577
+ import { Obj as Obj3 } from "@dxos/echo";
578
+ var captureCommit = () => {
579
+ const writes = [];
580
+ const commit = (write) => Effect3.sync(() => {
581
+ writes.push(write);
582
+ });
583
+ return {
584
+ commit,
585
+ writes
586
+ };
587
+ };
588
+ var makeEchoCommit = (transcript) => (write, window) => Effect3.sync(() => {
589
+ for (const update of write.blockUpdates ?? []) {
590
+ const block = window[update.index];
591
+ if (!block) {
592
+ continue;
593
+ }
594
+ const { index: _index, ...patch } = update;
595
+ if (Obj3.isObject(block)) {
596
+ Obj3.update(block, (block2) => Object.assign(block2, patch));
597
+ } else {
598
+ Object.assign(block, patch);
599
+ }
600
+ }
601
+ if (write.transcriptUpdate) {
602
+ Obj3.update(transcript, (transcript2) => Object.assign(transcript2, write.transcriptUpdate));
603
+ }
604
+ });
605
+
606
+ // src/stages/index.ts
607
+ import { log as log3 } from "@dxos/log";
608
+
609
+ // src/stages/correction.ts
610
+ import * as Effect4 from "effect/Effect";
611
+ var correctText = (text) => {
612
+ const trimmed = text.trim();
613
+ if (trimmed.length === 0) {
614
+ return trimmed;
615
+ }
616
+ const capitalized = trimmed.charAt(0).toUpperCase() + trimmed.slice(1);
617
+ if (/[.!?]$/.test(capitalized)) {
618
+ return capitalized;
619
+ }
620
+ return `${capitalized.replace(/[,;:]+$/, "")}.`;
621
+ };
622
+ var makeCorrectionStage = ({ correct } = {}) => ({
623
+ id: "correct",
624
+ trigger: "per-block",
625
+ window: {
626
+ blocks: 8
627
+ },
628
+ concurrency: "latest-wins",
629
+ model: DEFAULT_STAGE_MODEL,
630
+ run: ({ window }) => correct ? Effect4.promise(() => correct(window)).pipe(Effect4.map((updates) => updates.length > 0 ? StageWrite.blocks([
631
+ ...updates
632
+ ]) : StageWrite.empty())) : Effect4.sync(() => {
633
+ const updates = window.map((block, index) => ({
634
+ block,
635
+ index
636
+ })).filter(({ block }) => block.corrected === void 0).map(({ block, index }) => ({
637
+ index,
638
+ corrected: correctText(block.text)
639
+ }));
640
+ return updates.length > 0 ? StageWrite.blocks(updates) : StageWrite.empty();
641
+ })
642
+ });
643
+
644
+ // src/stages/diarization.ts
645
+ import * as Effect5 from "effect/Effect";
646
+ var makeDiarizationStage = () => ({
647
+ id: "diarize",
648
+ trigger: "per-block",
649
+ window: {
650
+ blocks: 1
651
+ },
652
+ concurrency: "latest-wins",
653
+ // TODO: implement diarization (assign speaker identity to the block).
654
+ run: () => Effect5.succeed(StageWrite.empty())
655
+ });
656
+
657
+ // src/stages/extraction.ts
658
+ import * as Effect6 from "effect/Effect";
659
+ import { insertReferences } from "@dxos/assistant/extraction";
660
+ var STOP_WORDS = /* @__PURE__ */ new Set([
661
+ "I",
662
+ "Yes",
663
+ "No",
664
+ "But",
665
+ "And",
666
+ "Or",
667
+ "So",
668
+ "They",
669
+ "We",
670
+ "He",
671
+ "She",
672
+ "It",
673
+ "The",
674
+ "A",
675
+ "An",
676
+ "Is",
677
+ "Are",
678
+ "Was",
679
+ "Were",
680
+ "Be",
681
+ "Been",
682
+ "This",
683
+ "That"
684
+ ]);
685
+ var extractProperNouns = (text) => {
686
+ const matches = text.match(/\b[A-Z][a-zA-Z]+(?:\s+[A-Z][a-zA-Z]+)*\b/g) ?? [];
687
+ return Array.from(new Set(matches.filter((word) => !STOP_WORDS.has(word))));
688
+ };
689
+ var isLinked = (text) => text.includes("](echo:");
690
+ var resolveEntities = async (text, lookup) => {
691
+ const nouns = extractProperNouns(text);
692
+ const references = [];
693
+ const quotes = [];
694
+ const matched = /* @__PURE__ */ new Set();
695
+ if (lookup) {
696
+ for (const noun of nouns) {
697
+ const candidates2 = await lookup(noun);
698
+ const unambiguous = candidates2.length === 1 || candidates2.length > 1 && candidates2[0].score > candidates2[1].score;
699
+ if (candidates2.length > 0 && unambiguous) {
700
+ references.push(candidates2[0].ref);
701
+ quotes.push({
702
+ quote: noun,
703
+ id: candidates2[0].id
704
+ });
705
+ matched.add(noun.toLowerCase());
706
+ }
707
+ }
708
+ }
709
+ const candidates = nouns.filter((noun) => !matched.has(noun.toLowerCase())).map((noun) => {
710
+ const start = Math.max(0, text.indexOf(noun));
711
+ return {
712
+ text: noun,
713
+ kind: "proper-noun",
714
+ start,
715
+ end: start + noun.length
716
+ };
717
+ });
718
+ return {
719
+ references,
720
+ quotes,
721
+ candidates
722
+ };
723
+ };
724
+ var linkEntities = async (text, lookup) => {
725
+ if (isLinked(text)) {
726
+ return text;
727
+ }
728
+ const { quotes } = await resolveEntities(text, lookup);
729
+ return quotes.length > 0 ? insertReferences(text, {
730
+ references: quotes
731
+ }) : text;
732
+ };
733
+ var makeExtractionStage = () => ({
734
+ id: "extract",
735
+ trigger: "per-block",
736
+ window: {
737
+ blocks: 8
738
+ },
739
+ concurrency: "latest-wins",
740
+ model: DEFAULT_STAGE_MODEL,
741
+ run: ({ window }, ctx) => Effect6.gen(function* () {
742
+ const updates = [];
743
+ for (let index = 0; index < window.length; index++) {
744
+ const block = window[index];
745
+ const text = block.corrected ?? block.text;
746
+ if (isLinked(text)) {
747
+ continue;
748
+ }
749
+ const { references, quotes, candidates } = yield* Effect6.promise(() => resolveEntities(text, ctx.lookup));
750
+ const update = {
751
+ index
752
+ };
753
+ if (references.length > 0) {
754
+ update.references = references;
755
+ }
756
+ if (candidates.length > 0) {
757
+ update.candidates = candidates;
758
+ }
759
+ if (quotes.length > 0) {
760
+ update.corrected = insertReferences(text, {
761
+ references: quotes
762
+ });
763
+ }
764
+ if (update.references || update.candidates || update.corrected) {
765
+ updates.push(update);
766
+ }
767
+ }
768
+ return updates.length > 0 ? StageWrite.blocks(updates) : StageWrite.empty();
769
+ })
770
+ });
771
+
772
+ // src/stages/summarization.ts
773
+ import * as Effect7 from "effect/Effect";
774
+ var makeSummarizationStage = () => ({
775
+ id: "summarize",
776
+ trigger: "on-silence",
777
+ concurrency: "skip-if-busy",
778
+ model: DEFAULT_STAGE_MODEL,
779
+ run: ({ window }) => Effect7.sync(() => {
780
+ const utterances = window.map((block) => (block.corrected ?? block.text).trim()).filter(Boolean);
781
+ if (utterances.length === 0) {
782
+ return StageWrite.empty();
783
+ }
784
+ return StageWrite.transcript({
785
+ summary: utterances.join(" "),
786
+ summaryUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
787
+ });
788
+ })
789
+ });
790
+
791
+ // src/stages/translation.ts
792
+ import * as Effect8 from "effect/Effect";
793
+ var makeTranslationStage = (_targetLanguage = "en") => ({
794
+ id: "translate",
795
+ trigger: "per-block",
796
+ window: {
797
+ blocks: 1
798
+ },
799
+ concurrency: "latest-wins",
800
+ model: DEFAULT_STAGE_MODEL,
801
+ // TODO: implement translation (per-block; writes block.translation via a model call).
802
+ run: () => Effect8.succeed(StageWrite.empty())
803
+ });
804
+
805
+ // src/stages/correction-llm.ts
806
+ import * as LanguageModel from "@effect/ai/LanguageModel";
807
+ import * as Effect9 from "effect/Effect";
808
+ import * as Layer from "effect/Layer";
809
+ import * as Schema2 from "effect/Schema";
810
+ import { AiService } from "@dxos/ai";
811
+ import { DXN as DXN3 } from "@dxos/echo";
812
+ var CorrectionPayload = Schema2.Struct({
813
+ blocks: Schema2.Array(Schema2.Struct({
814
+ index: Schema2.Number,
815
+ corrected: Schema2.String
816
+ }))
817
+ });
818
+ var PROMPT = `You repair raw speech-to-text fragments. For each numbered fragment, return a cleaned version: fix capitalization and terminal punctuation, join words split across fragment boundaries, and drop filler/garbage tokens \u2014 without changing the meaning. Return exactly one entry per input fragment, preserving its index.`;
819
+ var correctWithLanguageModel = (blocks, model = DEFAULT_STAGE_MODEL) => Effect9.gen(function* () {
820
+ const input = blocks.map((block, index) => `${index}: ${block.corrected ?? block.text}`).join("\n");
821
+ const response = yield* LanguageModel.generateObject({
822
+ schema: CorrectionPayload,
823
+ prompt: `${PROMPT}
824
+
825
+ ${input}`
826
+ });
827
+ return response.value.blocks.map((block) => ({
828
+ index: block.index,
829
+ corrected: block.corrected
830
+ }));
831
+ }).pipe(Effect9.provide(AiService.model(DXN3.getName(model)).pipe(Layer.orDie)));
832
+
833
+ // src/stages/index.ts
834
+ var __dxlog_file3 = "/Users/jdw/Code/dxos/packages/core/compute/pipeline-transcription/src/stages/index.ts";
835
+ var STAGE_FACTORIES = Object.freeze({
836
+ correct: makeCorrectionStage,
837
+ extract: makeExtractionStage,
838
+ summarize: makeSummarizationStage,
839
+ translate: makeTranslationStage,
840
+ diarize: makeDiarizationStage
841
+ });
842
+ var buildStages = (configs) => {
843
+ const unknown = configs.filter((config) => config.enabled && !STAGE_FACTORIES[config.id]).map((config) => config.id);
844
+ if (unknown.length > 0) {
845
+ log3.warn("ignoring unknown pipeline stage ids", {
846
+ unknown,
847
+ known: Object.keys(STAGE_FACTORIES)
848
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file3, L: 31, S: void 0 });
849
+ }
850
+ return configs.filter((config) => config.enabled && STAGE_FACTORIES[config.id]).map((config) => STAGE_FACTORIES[config.id]());
851
+ };
852
+ export {
853
+ DEFAULT_STAGE_MODEL,
854
+ PIPELINE_PRESETS,
855
+ PipelineConfig,
856
+ PipelineRuntime,
857
+ STAGE_FACTORIES,
858
+ SentenceBuffer,
859
+ StageConfig,
860
+ StageWrite,
861
+ Transcriber,
862
+ TranscriptEvent,
863
+ alignWhisperSegments,
864
+ buildStages,
865
+ captureCommit,
866
+ correctText,
867
+ correctWithLanguageModel,
868
+ extractProperNouns,
869
+ findPreset,
870
+ isLinked,
871
+ linkEntities,
872
+ makeConfigFromPreset,
873
+ makeCorrectionStage,
874
+ makeDatabaseLookup,
875
+ makeDiarizationStage,
876
+ makeEchoCommit,
877
+ makeExtractionStage,
878
+ makeSummarizationStage,
879
+ makeTranslationStage,
880
+ resolveEntities,
881
+ resolveModel,
882
+ runAsrPipeline,
883
+ runLivePipeline,
884
+ splitSentences
885
+ };
886
+ //# sourceMappingURL=index.mjs.map