@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.
- package/dist/lib/neutral/chunk-Y44GOMGN.mjs +22 -0
- package/dist/lib/neutral/chunk-Y44GOMGN.mjs.map +7 -0
- package/dist/lib/neutral/index.mjs +886 -0
- package/dist/lib/neutral/index.mjs.map +7 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/lib/neutral/testing/index.mjs +192 -0
- package/dist/lib/neutral/testing/index.mjs.map +7 -0
- package/dist/types/src/PipelineConfig.d.ts +49 -0
- package/dist/types/src/PipelineConfig.d.ts.map +1 -0
- package/dist/types/src/PipelineConfig.test.d.ts +2 -0
- package/dist/types/src/PipelineConfig.test.d.ts.map +1 -0
- package/dist/types/src/PipelineRuntime.d.ts +46 -0
- package/dist/types/src/PipelineRuntime.d.ts.map +1 -0
- package/dist/types/src/PipelineRuntime.test.d.ts +2 -0
- package/dist/types/src/PipelineRuntime.test.d.ts.map +1 -0
- package/dist/types/src/Stage.d.ts +80 -0
- package/dist/types/src/Stage.d.ts.map +1 -0
- package/dist/types/src/TranscriptEvent.d.ts +23 -0
- package/dist/types/src/TranscriptEvent.d.ts.map +1 -0
- package/dist/types/src/asr/audio-recorder.d.ts +47 -0
- package/dist/types/src/asr/audio-recorder.d.ts.map +1 -0
- package/dist/types/src/asr/index.d.ts +5 -0
- package/dist/types/src/asr/index.d.ts.map +1 -0
- package/dist/types/src/asr/pipeline.d.ts +42 -0
- package/dist/types/src/asr/pipeline.d.ts.map +1 -0
- package/dist/types/src/asr/sentence-buffer.d.ts +25 -0
- package/dist/types/src/asr/sentence-buffer.d.ts.map +1 -0
- package/dist/types/src/asr/sentence-buffer.test.d.ts +2 -0
- package/dist/types/src/asr/sentence-buffer.test.d.ts.map +1 -0
- package/dist/types/src/asr/transcriber.d.ts +115 -0
- package/dist/types/src/asr/transcriber.d.ts.map +1 -0
- package/dist/types/src/asr/transcriber.test.d.ts +2 -0
- package/dist/types/src/asr/transcriber.test.d.ts.map +1 -0
- package/dist/types/src/asr/util.d.ts +2 -0
- package/dist/types/src/asr/util.d.ts.map +1 -0
- package/dist/types/src/dispatch.d.ts +19 -0
- package/dist/types/src/dispatch.d.ts.map +1 -0
- package/dist/types/src/dispatch.test.d.ts +2 -0
- package/dist/types/src/dispatch.test.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +11 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/live.d.ts +24 -0
- package/dist/types/src/live.d.ts.map +1 -0
- package/dist/types/src/live.test.d.ts +2 -0
- package/dist/types/src/live.test.d.ts.map +1 -0
- package/dist/types/src/lookup.d.ts +35 -0
- package/dist/types/src/lookup.d.ts.map +1 -0
- package/dist/types/src/lookup.test.d.ts +2 -0
- package/dist/types/src/lookup.test.d.ts.map +1 -0
- package/dist/types/src/model-routing.d.ts +12 -0
- package/dist/types/src/model-routing.d.ts.map +1 -0
- package/dist/types/src/pipeline-integration.test.d.ts +2 -0
- package/dist/types/src/pipeline-integration.test.d.ts.map +1 -0
- package/dist/types/src/stages/correction-llm.d.ts +14 -0
- package/dist/types/src/stages/correction-llm.d.ts.map +1 -0
- package/dist/types/src/stages/correction-llm.test.d.ts +2 -0
- package/dist/types/src/stages/correction-llm.test.d.ts.map +1 -0
- package/dist/types/src/stages/correction.d.ts +29 -0
- package/dist/types/src/stages/correction.d.ts.map +1 -0
- package/dist/types/src/stages/correction.test.d.ts +2 -0
- package/dist/types/src/stages/correction.test.d.ts.map +1 -0
- package/dist/types/src/stages/diarization.d.ts +11 -0
- package/dist/types/src/stages/diarization.d.ts.map +1 -0
- package/dist/types/src/stages/extraction.d.ts +40 -0
- package/dist/types/src/stages/extraction.d.ts.map +1 -0
- package/dist/types/src/stages/extraction.test.d.ts +2 -0
- package/dist/types/src/stages/extraction.test.d.ts.map +1 -0
- package/dist/types/src/stages/index.d.ts +18 -0
- package/dist/types/src/stages/index.d.ts.map +1 -0
- package/dist/types/src/stages/summarization.d.ts +12 -0
- package/dist/types/src/stages/summarization.d.ts.map +1 -0
- package/dist/types/src/stages/translation.d.ts +11 -0
- package/dist/types/src/stages/translation.d.ts.map +1 -0
- package/dist/types/src/testing/conversation-parser.d.ts +26 -0
- package/dist/types/src/testing/conversation-parser.d.ts.map +1 -0
- package/dist/types/src/testing/conversation.test.d.ts +2 -0
- package/dist/types/src/testing/conversation.test.d.ts.map +1 -0
- package/dist/types/src/testing/fixtures.d.ts +6 -0
- package/dist/types/src/testing/fixtures.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +5 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/scripted-source.d.ts +16 -0
- package/dist/types/src/testing/scripted-source.d.ts.map +1 -0
- package/dist/types/src/testing/stream-simulator.d.ts +38 -0
- package/dist/types/src/testing/stream-simulator.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +61 -0
- package/src/PipelineConfig.test.ts +39 -0
- package/src/PipelineConfig.ts +71 -0
- package/src/PipelineRuntime.test.ts +74 -0
- package/src/PipelineRuntime.ts +192 -0
- package/src/Stage.ts +86 -0
- package/src/TranscriptEvent.ts +23 -0
- package/src/asr/audio-recorder.ts +56 -0
- package/src/asr/index.ts +8 -0
- package/src/asr/pipeline.ts +105 -0
- package/src/asr/sentence-buffer.test.ts +53 -0
- package/src/asr/sentence-buffer.ts +88 -0
- package/src/asr/transcriber.test.ts +104 -0
- package/src/asr/transcriber.ts +337 -0
- package/src/asr/util.ts +14 -0
- package/src/dispatch.test.ts +37 -0
- package/src/dispatch.ts +56 -0
- package/src/index.ts +14 -0
- package/src/live.test.ts +63 -0
- package/src/live.ts +56 -0
- package/src/lookup.test.ts +41 -0
- package/src/lookup.ts +60 -0
- package/src/model-routing.ts +25 -0
- package/src/pipeline-integration.test.ts +70 -0
- package/src/stages/correction-llm.test.ts +46 -0
- package/src/stages/correction-llm.ts +40 -0
- package/src/stages/correction.test.ts +26 -0
- package/src/stages/correction.ts +72 -0
- package/src/stages/diarization.ts +24 -0
- package/src/stages/extraction.test.ts +79 -0
- package/src/stages/extraction.ts +151 -0
- package/src/stages/index.ts +44 -0
- package/src/stages/summarization.ts +37 -0
- package/src/stages/translation.ts +26 -0
- package/src/testing/conversation-parser.ts +101 -0
- package/src/testing/conversation.test.ts +53 -0
- package/src/testing/conversation.txt +23 -0
- package/src/testing/fixtures.ts +18 -0
- package/src/testing/index.ts +8 -0
- package/src/testing/scripted-source.ts +35 -0
- 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
|