@dxos/plugin-transcription 0.10.0 → 0.11.1
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/TranscriptionPlugin.mjs +2 -0
- package/dist/lib/TranscriptionPlugin.node.mjs +11 -0
- package/dist/lib/TranscriptionPlugin.node.mjs.map +1 -0
- package/dist/lib/TranscriptionPlugin.workerd.mjs +11 -0
- package/dist/lib/TranscriptionPlugin.workerd.mjs.map +1 -0
- package/dist/lib/capabilities/node.mjs +9 -0
- package/dist/lib/capabilities/node.mjs.map +1 -0
- package/dist/lib/capabilities.mjs +18 -0
- package/dist/lib/capabilities.mjs.map +1 -0
- package/dist/lib/chunk-TranscriptionArticle.mjs +49 -0
- package/dist/lib/chunk-TranscriptionArticle.mjs.map +1 -0
- package/dist/lib/chunk-TranscriptionPlugin.mjs +39 -0
- package/dist/lib/chunk-TranscriptionPlugin.mjs.map +1 -0
- package/dist/lib/chunk-app-graph-builder.mjs +33 -0
- package/dist/lib/chunk-app-graph-builder.mjs.map +1 -0
- package/dist/lib/chunk-create.mjs +14 -0
- package/dist/lib/chunk-create.mjs.map +1 -0
- package/dist/lib/chunk-enrich-message.mjs +18 -0
- package/dist/lib/chunk-enrich-message.mjs.map +1 -0
- package/dist/lib/chunk-entity-lookup.mjs +26 -0
- package/dist/lib/chunk-entity-lookup.mjs.map +1 -0
- package/dist/lib/{neutral/markdown-extension-CCDXXY7E.mjs → chunk-markdown-extension.mjs} +7 -11
- package/dist/lib/chunk-markdown-extension.mjs.map +1 -0
- package/dist/lib/{neutral/chunk-5BVHY75B.mjs → chunk-meta.mjs} +20 -27
- package/dist/lib/chunk-meta.mjs.map +1 -0
- package/dist/lib/chunk-open.mjs +16 -0
- package/dist/lib/chunk-open.mjs.map +1 -0
- package/dist/lib/chunk-operation-handler.mjs +11 -0
- package/dist/lib/chunk-operation-handler.mjs.map +1 -0
- package/dist/lib/chunk-pipeline-status.mjs +17 -0
- package/dist/lib/chunk-pipeline-status.mjs.map +1 -0
- package/dist/lib/chunk-react-surface.mjs +21 -0
- package/dist/lib/chunk-react-surface.mjs.map +1 -0
- package/dist/lib/chunk-recording-session.mjs +13 -0
- package/dist/lib/chunk-recording-session.mjs.map +1 -0
- package/dist/lib/chunk-render-byline.mjs +15 -0
- package/dist/lib/chunk-render-byline.mjs.map +1 -0
- package/dist/lib/chunk-settings.mjs +26 -0
- package/dist/lib/chunk-settings.mjs.map +1 -0
- package/dist/lib/chunk-skill-definition.mjs +10 -0
- package/dist/lib/chunk-skill-definition.mjs.map +1 -0
- package/dist/lib/chunk-skills.mjs +21 -0
- package/dist/lib/chunk-skills.mjs.map +1 -0
- package/dist/lib/{neutral/summarize-LBUZTOTH.mjs → chunk-summarize.mjs} +25 -38
- package/dist/lib/chunk-summarize.mjs.map +1 -0
- package/dist/lib/chunk-text-content.mjs +28 -0
- package/dist/lib/chunk-text-content.mjs.map +1 -0
- package/dist/lib/chunk-transcriber.mjs +161 -0
- package/dist/lib/chunk-transcriber.mjs.map +1 -0
- package/dist/lib/chunk-transcription-driver.mjs +165 -0
- package/dist/lib/chunk-transcription-driver.mjs.map +1 -0
- package/dist/lib/chunk-types.mjs +141 -0
- package/dist/lib/chunk-types.mjs.map +1 -0
- package/dist/lib/components.mjs +108 -0
- package/dist/lib/components.mjs.map +1 -0
- package/dist/lib/containers.mjs +7 -0
- package/dist/lib/containers.mjs.map +1 -0
- package/dist/lib/hooks.mjs +87 -0
- package/dist/lib/hooks.mjs.map +1 -0
- package/dist/lib/index.mjs +4 -0
- package/dist/lib/meta.mjs +2 -0
- package/dist/lib/operations.mjs +7 -0
- package/dist/lib/operations.mjs.map +1 -0
- package/dist/lib/plugin.mjs +9 -0
- package/dist/lib/plugin.mjs.map +1 -0
- package/dist/lib/skills.mjs +2 -0
- package/dist/lib/testing.mjs +118 -0
- package/dist/lib/testing.mjs.map +1 -0
- package/dist/lib/translations.mjs +23 -0
- package/dist/lib/translations.mjs.map +1 -0
- package/dist/lib/types.mjs +2 -0
- package/dist/types/src/capabilities/app-graph-builder.d.ts +1 -1
- package/dist/types/src/capabilities/entity-lookup.d.ts +1 -1
- package/dist/types/src/capabilities/markdown-extension.d.ts +1 -1
- package/dist/types/src/capabilities/node.d.ts +6 -0
- package/dist/types/src/capabilities/node.d.ts.map +1 -0
- package/dist/types/src/capabilities/operation-handler.d.ts +1 -1
- package/dist/types/src/capabilities/pipeline-status.d.ts +1 -1
- package/dist/types/src/capabilities/react-surface.d.ts +1 -1
- package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
- package/dist/types/src/capabilities/recording-session.d.ts +1 -1
- package/dist/types/src/capabilities/settings.d.ts +1 -1
- package/dist/types/src/capabilities/text-content.d.ts +1 -1
- package/dist/types/src/capabilities/transcriber.d.ts +1 -1
- package/dist/types/src/capabilities/transcriber.d.ts.map +1 -1
- package/dist/types/src/capabilities/transcription-driver.d.ts +1 -1
- package/dist/types/src/containers/TranscriptionArticle/TranscriptionArticle.d.ts +4 -1
- package/dist/types/src/containers/TranscriptionArticle/TranscriptionArticle.d.ts.map +1 -1
- package/dist/types/src/containers/TranscriptionArticle/TranscriptionArticle.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/useTranscriptionRecording.d.ts.map +1 -1
- package/dist/types/src/normalization/message-normalizer.d.ts +4 -4
- package/dist/types/src/normalization/message-normalizer.d.ts.map +1 -1
- package/dist/types/src/normalization/normalization.d.ts +97 -87
- package/dist/types/src/normalization/normalization.d.ts.map +1 -1
- package/dist/types/src/operations/open.d.ts.map +1 -1
- package/dist/types/src/stories/Pipeline.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Transcription.stories.d.ts.map +1 -1
- package/dist/types/src/transcription-manager.d.ts +1 -1
- package/dist/types/src/transcription-manager.d.ts.map +1 -1
- package/dist/types/src/types/TranscriptOperation.d.ts +97 -87
- package/dist/types/src/types/TranscriptOperation.d.ts.map +1 -1
- package/dist/types/src/types/TranscriptionCapabilities.d.ts +2 -2
- package/dist/types/src/types/TranscriptionCapabilities.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +1 -0
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/render-byline.d.ts +11 -0
- package/dist/types/src/util/render-byline.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/dx.config.ts +1 -1
- package/package.json +78 -70
- package/src/capabilities/entity-lookup.ts +1 -1
- package/src/capabilities/node.ts +17 -0
- package/src/capabilities/pipeline-status.ts +1 -1
- package/src/capabilities/{react-surface.tsx → react-surface.ts} +2 -4
- package/src/capabilities/recording-session.ts +1 -1
- package/src/capabilities/text-content.ts +9 -4
- package/src/capabilities/transcriber.ts +3 -1
- package/src/capabilities/transcription-driver.tsx +1 -1
- package/src/containers/TranscriptionArticle/TranscriptionArticle.stories.tsx +2 -1
- package/src/containers/TranscriptionArticle/TranscriptionArticle.tsx +8 -3
- package/src/hooks/useTranscriptionRecording.ts +7 -8
- package/src/normalization/message-normalizer.ts +10 -7
- package/src/normalization/normalization.test.ts +41 -44
- package/src/operations/open.ts +1 -1
- package/src/stories/Pipeline.stories.tsx +15 -14
- package/src/stories/Transcription.stories.tsx +3 -8
- package/src/transcription-manager.ts +2 -2
- package/src/types/TranscriptionCapabilities.ts +2 -2
- package/src/util/index.ts +1 -0
- package/src/util/render-byline.ts +40 -0
- package/dist/lib/neutral/TranscriptionArticle-FEWCWG4A.mjs +0 -51
- package/dist/lib/neutral/TranscriptionArticle-FEWCWG4A.mjs.map +0 -7
- package/dist/lib/neutral/TranscriptionPlugin.mjs +0 -69
- package/dist/lib/neutral/TranscriptionPlugin.mjs.map +0 -7
- package/dist/lib/neutral/TranscriptionPlugin.node.mjs +0 -25
- package/dist/lib/neutral/TranscriptionPlugin.node.mjs.map +0 -7
- package/dist/lib/neutral/TranscriptionPlugin.workerd.mjs +0 -21
- package/dist/lib/neutral/TranscriptionPlugin.workerd.mjs.map +0 -7
- package/dist/lib/neutral/app-graph-builder-GQRMI22E.mjs +0 -48
- package/dist/lib/neutral/app-graph-builder-GQRMI22E.mjs.map +0 -7
- package/dist/lib/neutral/capabilities/index.mjs +0 -31
- package/dist/lib/neutral/capabilities/index.mjs.map +0 -7
- package/dist/lib/neutral/chunk-5BVHY75B.mjs.map +0 -7
- package/dist/lib/neutral/chunk-HNXSU7HJ.mjs +0 -8
- package/dist/lib/neutral/chunk-HNXSU7HJ.mjs.map +0 -7
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs +0 -10
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +0 -7
- package/dist/lib/neutral/chunk-KOXLGGSL.mjs +0 -30
- package/dist/lib/neutral/chunk-KOXLGGSL.mjs.map +0 -7
- package/dist/lib/neutral/chunk-MDV7NBFJ.mjs +0 -182
- package/dist/lib/neutral/chunk-MDV7NBFJ.mjs.map +0 -7
- package/dist/lib/neutral/components/index.mjs +0 -125
- package/dist/lib/neutral/components/index.mjs.map +0 -7
- package/dist/lib/neutral/containers/index.mjs +0 -9
- package/dist/lib/neutral/containers/index.mjs.map +0 -7
- package/dist/lib/neutral/create-HNHGCKZ4.mjs +0 -21
- package/dist/lib/neutral/create-HNHGCKZ4.mjs.map +0 -7
- package/dist/lib/neutral/enrich-message-DSJU4TXC.mjs +0 -24
- package/dist/lib/neutral/enrich-message-DSJU4TXC.mjs.map +0 -7
- package/dist/lib/neutral/entity-lookup-PCDW7NC4.mjs +0 -25
- package/dist/lib/neutral/entity-lookup-PCDW7NC4.mjs.map +0 -7
- package/dist/lib/neutral/hooks/index.mjs +0 -97
- package/dist/lib/neutral/hooks/index.mjs.map +0 -7
- package/dist/lib/neutral/index.mjs +0 -20
- package/dist/lib/neutral/index.mjs.map +0 -7
- package/dist/lib/neutral/markdown-extension-CCDXXY7E.mjs.map +0 -7
- package/dist/lib/neutral/meta.json +0 -1
- package/dist/lib/neutral/meta.mjs +0 -8
- package/dist/lib/neutral/meta.mjs.map +0 -7
- package/dist/lib/neutral/open-SNMZXXYO.mjs +0 -25
- package/dist/lib/neutral/open-SNMZXXYO.mjs.map +0 -7
- package/dist/lib/neutral/operation-handler-NXMOAXFP.mjs +0 -13
- package/dist/lib/neutral/operation-handler-NXMOAXFP.mjs.map +0 -7
- package/dist/lib/neutral/operations/index.mjs +0 -8
- package/dist/lib/neutral/operations/index.mjs.map +0 -7
- package/dist/lib/neutral/pipeline-status-XMWMAUJY.mjs +0 -17
- package/dist/lib/neutral/pipeline-status-XMWMAUJY.mjs.map +0 -7
- package/dist/lib/neutral/plugin.mjs +0 -16
- package/dist/lib/neutral/plugin.mjs.map +0 -7
- package/dist/lib/neutral/react-surface-YMZRO242.mjs +0 -26
- package/dist/lib/neutral/react-surface-YMZRO242.mjs.map +0 -7
- package/dist/lib/neutral/recording-session-IADO7ATU.mjs +0 -15
- package/dist/lib/neutral/recording-session-IADO7ATU.mjs.map +0 -7
- package/dist/lib/neutral/settings-7KE4MABW.mjs +0 -31
- package/dist/lib/neutral/settings-7KE4MABW.mjs.map +0 -7
- package/dist/lib/neutral/skill-definition-DXNK324V.mjs +0 -15
- package/dist/lib/neutral/skill-definition-DXNK324V.mjs.map +0 -7
- package/dist/lib/neutral/skills/index.mjs +0 -8
- package/dist/lib/neutral/skills/index.mjs.map +0 -7
- package/dist/lib/neutral/summarize-LBUZTOTH.mjs.map +0 -7
- package/dist/lib/neutral/text-content-NOWR642C.mjs +0 -30
- package/dist/lib/neutral/text-content-NOWR642C.mjs.map +0 -7
- package/dist/lib/neutral/transcriber-6XZHUUHK.mjs +0 -174
- package/dist/lib/neutral/transcriber-6XZHUUHK.mjs.map +0 -7
- package/dist/lib/neutral/transcription-driver-DN6ELQXZ.mjs +0 -207
- package/dist/lib/neutral/transcription-driver-DN6ELQXZ.mjs.map +0 -7
- package/dist/lib/neutral/translations.mjs +0 -31
- package/dist/lib/neutral/translations.mjs.map +0 -7
- package/dist/lib/neutral/types/index.mjs +0 -12
- package/dist/lib/neutral/types/index.mjs.map +0 -7
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Capability } from "@dxos/app-framework";
|
|
2
|
+
import { Settings, TranscriptionCapabilities } from "#types";
|
|
3
|
+
import { meta } from "#meta";
|
|
4
|
+
import { AppCapabilities } from "@dxos/app-toolkit";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
import { createKvsStore } from "@dxos/effect";
|
|
7
|
+
//#region src/capabilities/settings.ts
|
|
8
|
+
var settings_default = Capability.makeModule(() => Effect.sync(() => {
|
|
9
|
+
const settingsAtom = createKvsStore({
|
|
10
|
+
key: meta.profile.key,
|
|
11
|
+
schema: Settings.Settings,
|
|
12
|
+
defaultValue: () => ({
|
|
13
|
+
entityExtraction: true,
|
|
14
|
+
recordMode: "toggle"
|
|
15
|
+
})
|
|
16
|
+
});
|
|
17
|
+
return [Capability.contributes(TranscriptionCapabilities.Settings, settingsAtom), Capability.contributes(AppCapabilities.Settings, {
|
|
18
|
+
prefix: meta.profile.key,
|
|
19
|
+
schema: Settings.Settings,
|
|
20
|
+
atom: settingsAtom
|
|
21
|
+
})];
|
|
22
|
+
}));
|
|
23
|
+
//#endregion
|
|
24
|
+
export { settings_default as default };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=chunk-settings.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-settings.mjs","names":[],"sources":["../../src/capabilities/settings.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\nimport { Capability } from '@dxos/app-framework';\nimport { AppCapabilities } from '@dxos/app-toolkit';\nimport { createKvsStore } from '@dxos/effect';\n\nimport { meta } from '#meta';\nimport { Settings, TranscriptionCapabilities } from '#types';\n\nexport default Capability.makeModule(() =>\n Effect.sync(() => {\n const settingsAtom = createKvsStore({\n key: meta.profile.key,\n schema: Settings.Settings,\n defaultValue: () => ({\n entityExtraction: true,\n recordMode: 'toggle' as const,\n }),\n });\n\n return [\n Capability.contributes(TranscriptionCapabilities.Settings, settingsAtom),\n Capability.contributes(AppCapabilities.Settings, {\n prefix: meta.profile.key,\n schema: Settings.Settings,\n atom: settingsAtom,\n }),\n ];\n }),\n);\n"],"mappings":";;;;;;;AAaA,IAAA,mBAAe,WAAW,iBACxB,OAAO,WAAW;CAChB,MAAM,eAAe,eAAe;EAClC,KAAK,KAAK,QAAQ;EAClB,QAAQ,SAAS;EACjB,qBAAqB;GACnB,kBAAkB;GAClB,YAAY;EACd;CACF,CAAC;CAED,OAAO,CACL,WAAW,YAAY,0BAA0B,UAAU,YAAY,GACvE,WAAW,YAAY,gBAAgB,UAAU;EAC/C,QAAQ,KAAK,QAAQ;EACrB,QAAQ,SAAS;EACjB,MAAM;CACR,CAAC,CACH;AACF,CAAC,CACH"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Capability } from "@dxos/app-framework";
|
|
2
|
+
import { AppCapabilities } from "@dxos/app-toolkit";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import { TranscriptionSkill } from "#skills";
|
|
5
|
+
//#region src/capabilities/skill-definition.ts
|
|
6
|
+
var skillDefinition = () => Effect.succeed([Capability.contributes(AppCapabilities.SkillDefinition, TranscriptionSkill)]);
|
|
7
|
+
//#endregion
|
|
8
|
+
export { skillDefinition as default };
|
|
9
|
+
|
|
10
|
+
//# sourceMappingURL=chunk-skill-definition.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-skill-definition.mjs","names":[],"sources":["../../src/capabilities/skill-definition.ts"],"sourcesContent":["//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\nimport { Capability } from '@dxos/app-framework';\nimport { AppCapabilities } from '@dxos/app-toolkit';\n\nimport { TranscriptionSkill } from '#skills';\n\nconst skillDefinition = () =>\n Effect.succeed([Capability.contributes(AppCapabilities.SkillDefinition, TranscriptionSkill)]);\n\nexport default skillDefinition;\n"],"mappings":";;;;;AAWA,IAAM,wBACJ,OAAO,QAAQ,CAAC,WAAW,YAAY,gBAAgB,iBAAiB,kBAAkB,CAAC,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { trim } from "@dxos/util";
|
|
2
|
+
import { Skill, Template } from "@dxos/compute";
|
|
3
|
+
import { TranscriptOperation } from "#types";
|
|
4
|
+
//#region src/skills/transcription-skill.ts
|
|
5
|
+
var SKILL_KEY = "org.dxos.skill.transcription";
|
|
6
|
+
var make = () => Skill.make({
|
|
7
|
+
key: SKILL_KEY,
|
|
8
|
+
name: "Transcription",
|
|
9
|
+
tools: Skill.toolDefinitions({ operations: [TranscriptOperation.Open, TranscriptOperation.Summarize] }),
|
|
10
|
+
instructions: Template.make({ source: trim`
|
|
11
|
+
You can open and summarize a meeting transcript.
|
|
12
|
+
` })
|
|
13
|
+
});
|
|
14
|
+
var skill = {
|
|
15
|
+
key: SKILL_KEY,
|
|
16
|
+
make
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
export { skill as t };
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=chunk-skills.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-skills.mjs","names":[],"sources":["../../src/skills/transcription-skill.ts"],"sourcesContent":["//\n// Copyright 2025 DXOS.org\n//\n\nimport { Skill, Template } from '@dxos/compute';\nimport { trim } from '@dxos/util';\n\nimport { TranscriptOperation } from '#types';\n\nconst SKILL_KEY = 'org.dxos.skill.transcription';\n\nconst make = () =>\n Skill.make({\n key: SKILL_KEY,\n name: 'Transcription',\n tools: Skill.toolDefinitions({ operations: [TranscriptOperation.Open, TranscriptOperation.Summarize] }),\n instructions: Template.make({\n source: trim`\n You can open and summarize a meeting transcript.\n `,\n }),\n });\n\nconst skill: Skill.Definition = {\n key: SKILL_KEY,\n make,\n};\n\nexport default skill;\n"],"mappings":";;;;AASA,IAAM,YAAY;AAElB,IAAM,aACJ,MAAM,KAAK;CACT,KAAK;CACL,MAAM;CACN,OAAO,MAAM,gBAAgB,EAAE,YAAY,CAAC,oBAAoB,MAAM,oBAAoB,SAAS,EAAE,CAAC;CACtG,cAAc,SAAS,KAAK,EAC1B,QAAQ,IAAI;;QAGd,CAAC;AACH,CAAC;AAEH,IAAM,QAA0B;CAC9B,KAAK;CACL;AACF"}
|
|
@@ -1,44 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
} from "
|
|
4
|
-
import "
|
|
5
|
-
|
|
6
|
-
// src/operations/summarize.ts
|
|
7
|
-
import * as Array from "effect/Array";
|
|
1
|
+
import { i as Summarize } from "./chunk-types.mjs";
|
|
2
|
+
import { trim } from "@dxos/util";
|
|
3
|
+
import { Operation, Trace } from "@dxos/compute";
|
|
4
|
+
import { AiService, ConsolePrinter, ToolExecutionService, ToolResolverService } from "@dxos/ai";
|
|
5
|
+
import { Database } from "@dxos/echo";
|
|
8
6
|
import * as Effect from "effect/Effect";
|
|
7
|
+
import * as Option from "effect/Option";
|
|
8
|
+
import * as Array from "effect/Array";
|
|
9
9
|
import * as Function from "effect/Function";
|
|
10
10
|
import * as Layer from "effect/Layer";
|
|
11
|
-
import * as Option from "effect/Option";
|
|
12
|
-
import { AiService, ConsolePrinter, ToolExecutionService, ToolResolverService } from "@dxos/ai";
|
|
13
11
|
import { AiRequest, GenerationObserver } from "@dxos/assistant";
|
|
14
|
-
import { Operation, Trace } from "@dxos/compute";
|
|
15
|
-
import { Database } from "@dxos/echo";
|
|
16
12
|
import { registryLayerNoop } from "@dxos/echo/testing";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
system: systemPrompt
|
|
29
|
-
});
|
|
30
|
-
const summary = Function.pipe(result, Array.findLast((msg) => msg.sender.role === "assistant" && msg.blocks.some((block) => block._tag === "text")), Option.flatMap((msg) => Function.pipe(msg.blocks, Array.findLast((block) => block._tag === "text"), Option.map((block) => block.text))), Option.getOrThrowWith(() => new Error("No summary found")));
|
|
31
|
-
return {
|
|
32
|
-
summary
|
|
33
|
-
};
|
|
13
|
+
//#region src/operations/summarize.ts
|
|
14
|
+
/**
|
|
15
|
+
* Summarize a transcript of a meeting.
|
|
16
|
+
*/
|
|
17
|
+
var handler = Summarize.pipe(Operation.withHandler(Effect.fnUntraced(function* ({ transcript, notes }) {
|
|
18
|
+
const result = yield* new AiRequest.Request({ observer: GenerationObserver.fromPrinter(new ConsolePrinter({ tag: "summarize" })) }).run({
|
|
19
|
+
prompt: `Transcript: ${transcript}\n\nNotes: ${notes}`,
|
|
20
|
+
history: [],
|
|
21
|
+
system: systemPrompt
|
|
22
|
+
});
|
|
23
|
+
return { summary: Function.pipe(result, Array.findLast((msg) => msg.sender.role === "assistant" && msg.blocks.some((block) => block._tag === "text")), Option.flatMap((msg) => Function.pipe(msg.blocks, Array.findLast((block) => block._tag === "text"), Option.map((block) => block.text))), Option.getOrThrowWith(() => /* @__PURE__ */ new Error("No summary found"))) };
|
|
34
24
|
}, Effect.provide(Layer.mergeAll(AiService.model("com.anthropic.model.claude-sonnet-4-6.default"), ToolResolverService.layerEmpty, ToolExecutionService.layerEmpty, Trace.writerLayerNoop, Database.notAvailable, Layer.succeed(Operation.Service, {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
error: new Error("Not available.")
|
|
39
|
-
})
|
|
25
|
+
invoke: () => Effect.die("Not available."),
|
|
26
|
+
schedule: () => Effect.die("Not available."),
|
|
27
|
+
invokePromise: async () => ({ error: /* @__PURE__ */ new Error("Not available.") })
|
|
40
28
|
}), registryLayerNoop)))));
|
|
41
|
-
var summarize_default = handler;
|
|
42
29
|
var systemPrompt = trim`
|
|
43
30
|
You are a helpful assistant that summarizes transcripts of meetings.
|
|
44
31
|
|
|
@@ -71,7 +58,7 @@ var systemPrompt = trim`
|
|
|
71
58
|
|
|
72
59
|
If no actionable tasks are found, omit this tasks section.
|
|
73
60
|
`;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
//# sourceMappingURL=summarize
|
|
61
|
+
//#endregion
|
|
62
|
+
export { handler as default };
|
|
63
|
+
|
|
64
|
+
//# sourceMappingURL=chunk-summarize.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-summarize.mjs","names":[],"sources":["../../src/operations/summarize.ts"],"sourcesContent":["//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Array from 'effect/Array';\nimport * as Effect from 'effect/Effect';\nimport * as Function from 'effect/Function';\nimport * as Layer from 'effect/Layer';\nimport * as Option from 'effect/Option';\n\nimport { AiService, ConsolePrinter, ToolExecutionService, ToolResolverService } from '@dxos/ai';\nimport { AiRequest, GenerationObserver } from '@dxos/assistant';\nimport { Operation, Trace } from '@dxos/compute';\nimport { Database } from '@dxos/echo';\nimport { registryLayerNoop } from '@dxos/echo/testing';\nimport { trim } from '@dxos/util';\n\nimport { TranscriptOperation } from '../types';\n\n/**\n * Summarize a transcript of a meeting.\n */\nconst handler: Operation.WithHandler<typeof TranscriptOperation.Summarize> = TranscriptOperation.Summarize.pipe(\n Operation.withHandler(\n Effect.fnUntraced(\n function* ({ transcript, notes }) {\n const result = yield* new AiRequest.Request({\n observer: GenerationObserver.fromPrinter(new ConsolePrinter({ tag: 'summarize' })),\n }).run({\n prompt: `Transcript: ${transcript}\\n\\nNotes: ${notes}`,\n history: [],\n system: systemPrompt,\n });\n\n const summary = Function.pipe(\n result,\n Array.findLast((msg) => msg.sender.role === 'assistant' && msg.blocks.some((block) => block._tag === 'text')),\n Option.flatMap((msg) =>\n Function.pipe(\n msg.blocks,\n Array.findLast((block) => block._tag === 'text'),\n Option.map((block) => block.text),\n ),\n ),\n Option.getOrThrowWith(() => new Error('No summary found')),\n );\n\n return { summary };\n },\n Effect.provide(\n Layer.mergeAll(\n AiService.model('com.anthropic.model.claude-sonnet-4-6.default'),\n ToolResolverService.layerEmpty,\n ToolExecutionService.layerEmpty,\n Trace.writerLayerNoop,\n Database.notAvailable,\n Layer.succeed(Operation.Service, {\n invoke: () => Effect.die('Not available.'),\n schedule: () => Effect.die('Not available.'),\n invokePromise: async () => ({ error: new Error('Not available.') }),\n } as any),\n registryLayerNoop,\n ),\n ),\n ),\n ),\n);\n\nexport default handler;\n\nconst systemPrompt = trim`\n You are a helpful assistant that summarizes transcripts of meetings.\n\n # Goal\n Create a markdown summary of the meeting transcript with text notes provided.\n Notes are very important so make sure to include them in the summary if they contain meaningful information.\n\n # Formatting\n - Format the summary as a markdown document without extra comments like \"Here is the summary of the transcript:\".\n - Use markdown formatting for headings and bullet points.\n - Format the summary as a list of key points and takeaways.\n - All names of people should be in bold.\n\n # Note Taking\n - Correlate items in the summary with the person of origin to build a coherent narrative.\n - Include short quotes verbatim where appropriate. Especially when concerned with design decisions and problem descriptions.\n\n # Tasks\n At the end of the summary include tasks.\n Extract only the tasks that are:\n - Directly actionable.\n - Clearly assigned to a person or team (or can easily be inferred).\n - Strongly implied by the conversation and/or user note (no speculative tasks).\n - Specific enough that someone reading them would know exactly what to do next.\n\n Format all tasks as markdown checkboxes using the syntax:\n - [ ] Task description.\n\n Additional information can be included (indented).\n\n If no actionable tasks are found, omit this tasks section.\n`;\n"],"mappings":";;;;;;;;;;;;;;;;AAsBA,IAAM,UAAA,UAAqG,KACzG,UAAU,YACR,OAAO,WACL,WAAW,EAAE,YAAY,SAAS;CAChC,MAAM,SAAS,OAAO,IAAI,UAAU,QAAQ,EAC1C,UAAU,mBAAmB,YAAY,IAAI,eAAe,EAAE,KAAK,YAAY,CAAC,CAAC,EACnF,CAAC,CAAC,CAAC,IAAI;EACL,QAAQ,eAAe,WAAW,aAAa;EAC/C,SAAS,CAAC;EACV,QAAQ;CACV,CAAC;CAeD,OAAO,EAAE,SAbO,SAAS,KACvB,QACA,MAAM,UAAU,QAAQ,IAAI,OAAO,SAAS,eAAe,IAAI,OAAO,MAAM,UAAU,MAAM,SAAS,MAAM,CAAC,GAC5G,OAAO,SAAS,QACd,SAAS,KACP,IAAI,QACJ,MAAM,UAAU,UAAU,MAAM,SAAS,MAAM,GAC/C,OAAO,KAAK,UAAU,MAAM,IAAI,CAClC,CACF,GACA,OAAO,qCAAqB,IAAI,MAAM,kBAAkB,CAAC,CAGlD,EAAQ;AACnB,GACA,OAAO,QACL,MAAM,SACJ,UAAU,MAAM,+CAA+C,GAC/D,oBAAoB,YACpB,qBAAqB,YACrB,MAAM,iBACN,SAAS,cACT,MAAM,QAAQ,UAAU,SAAS;CAC/B,cAAc,OAAO,IAAI,gBAAgB;CACzC,gBAAgB,OAAO,IAAI,gBAAgB;CAC3C,eAAe,aAAa,EAAE,uBAAO,IAAI,MAAM,gBAAgB,EAAE;AACnE,CAAQ,GACR,iBACF,CACF,CACF,CACF,CACF;AAIA,IAAM,eAAe,IAAI"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { t as renderByline } from "./chunk-render-byline.mjs";
|
|
2
|
+
import { Capability } from "@dxos/app-framework";
|
|
3
|
+
import { getSpace } from "@dxos/client/echo";
|
|
4
|
+
import { Feed, Filter, Obj, Query, Scope, Type } from "@dxos/echo";
|
|
5
|
+
import { Message, Transcript } from "@dxos/types";
|
|
6
|
+
import { AppCapabilities } from "@dxos/app-toolkit";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
//#region src/capabilities/text-content.ts
|
|
9
|
+
var text_content_default = Capability.makeModule(Effect.fnUntraced(function* () {
|
|
10
|
+
return Capability.contributes(AppCapabilities.TextContent, {
|
|
11
|
+
id: Type.getTypename(Transcript.Transcript),
|
|
12
|
+
getTextContent: async (transcript) => {
|
|
13
|
+
const space = getSpace(transcript);
|
|
14
|
+
const members = space?.members.get().map((member) => ({
|
|
15
|
+
did: member.identity?.did,
|
|
16
|
+
displayName: member.identity?.profile?.displayName
|
|
17
|
+
})) ?? [];
|
|
18
|
+
const feed = await transcript.feed.load();
|
|
19
|
+
const feedDXN = feed ? Feed.getFeedUri(feed) : void 0;
|
|
20
|
+
if (!space || !feedDXN) return;
|
|
21
|
+
return (await space.db.query(Query.select(Filter.type(Message.Message)).from(Scope.feed(feedDXN))).run()).filter((message) => Obj.instanceOf(Message.Message, message)).flatMap((message, index) => renderByline(members)(message, index)).join("\n\n");
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}));
|
|
25
|
+
//#endregion
|
|
26
|
+
export { text_content_default as default };
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=chunk-text-content.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-text-content.mjs","names":[],"sources":["../../src/capabilities/text-content.ts"],"sourcesContent":["//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\nimport { Capability } from '@dxos/app-framework';\nimport { AppCapabilities } from '@dxos/app-toolkit';\nimport { getSpace } from '@dxos/client/echo';\nimport { Feed, Filter, Obj, Query, Scope, Type } from '@dxos/echo';\nimport { Message, Transcript } from '@dxos/types';\n\nimport { renderByline } from '../util';\n\nexport default Capability.makeModule(\n Effect.fnUntraced(function* () {\n return Capability.contributes(AppCapabilities.TextContent, {\n id: Type.getTypename(Transcript.Transcript),\n getTextContent: async (transcript: Transcript.Transcript) => {\n const space = getSpace(transcript);\n const members =\n space?.members.get().map((member) => ({\n did: member.identity?.did,\n displayName: member.identity?.profile?.displayName,\n })) ?? [];\n const feed = await transcript.feed.load();\n const feedDXN = feed ? Feed.getFeedUri(feed) : undefined;\n if (!space || !feedDXN) {\n return undefined;\n }\n const messages = await space.db\n .query(Query.select(Filter.type(Message.Message)).from(Scope.feed(feedDXN)))\n .run();\n return messages\n .filter((message) => Obj.instanceOf(Message.Message, message))\n .flatMap((message, index) => renderByline(members)(message, index))\n .join('\\n\\n');\n },\n });\n }),\n);\n"],"mappings":";;;;;;;;AAcA,IAAA,uBAAe,WAAW,WACxB,OAAO,WAAW,aAAa;CAC7B,OAAO,WAAW,YAAY,gBAAgB,aAAa;EACzD,IAAI,KAAK,YAAY,WAAW,UAAU;EAC1C,gBAAgB,OAAO,eAAsC;GAC3D,MAAM,QAAQ,SAAS,UAAU;GACjC,MAAM,UACJ,OAAO,QAAQ,IAAI,CAAC,CAAC,KAAK,YAAY;IACpC,KAAK,OAAO,UAAU;IACtB,aAAa,OAAO,UAAU,SAAS;GACzC,EAAE,KAAK,CAAC;GACV,MAAM,OAAO,MAAM,WAAW,KAAK,KAAK;GACxC,MAAM,UAAU,OAAO,KAAK,WAAW,IAAI,IAAI,KAAA;GAC/C,IAAI,CAAC,SAAS,CAAC,SACb;GAKF,QAAO,MAHgB,MAAM,GAC1B,MAAM,MAAM,OAAO,OAAO,KAAK,QAAQ,OAAO,CAAC,CAAC,CAAC,KAAK,MAAM,KAAK,OAAO,CAAC,CAAC,CAAA,CAC1E,IAAI,EAAA,CAEJ,QAAQ,YAAY,IAAI,WAAW,QAAQ,SAAS,OAAO,CAAC,CAAA,CAC5D,SAAS,SAAS,UAAU,aAAa,OAAO,CAAC,CAAC,SAAS,KAAK,CAAC,CAAA,CACjE,KAAK,MAAM;EAChB;CACF,CAAC;AACH,CAAC,CACH"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Capabilities, Capability } from "@dxos/app-framework";
|
|
2
|
+
import { TranscriptionCapabilities } from "#types";
|
|
3
|
+
import { Database, Feed, Obj } from "@dxos/echo";
|
|
4
|
+
import { Message } from "@dxos/types";
|
|
5
|
+
import { MediaStreamRecorder } from "@dxos/react-ui-transcription";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import { EffectEx } from "@dxos/effect";
|
|
8
|
+
import { Transcriber } from "@dxos/pipeline-transcription";
|
|
9
|
+
import { ClientCapabilities } from "@dxos/plugin-client";
|
|
10
|
+
import { Atom } from "@effect-atom/atom";
|
|
11
|
+
import * as Option from "effect/Option";
|
|
12
|
+
import { synchronized } from "@dxos/async";
|
|
13
|
+
import { Resource } from "@dxos/context";
|
|
14
|
+
//#region \0@oxc-project+runtime@0.139.0/helpers/esm/decorate.js
|
|
15
|
+
function __decorate(decorators, target, key, desc) {
|
|
16
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
17
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
18
|
+
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;
|
|
19
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/transcription-manager.ts
|
|
23
|
+
/**
|
|
24
|
+
* Length of the chunk in ms.
|
|
25
|
+
*/
|
|
26
|
+
var RECORD_INTERVAL = 200;
|
|
27
|
+
/**
|
|
28
|
+
* Number of chunks to save before the user starts speaking.
|
|
29
|
+
*/
|
|
30
|
+
var PREFIXED_CHUNKS_AMOUNT = 10;
|
|
31
|
+
/**
|
|
32
|
+
* Number of chunks to transcribe automatically after.
|
|
33
|
+
* Combined should be mess than 25MB or whisper would fail.
|
|
34
|
+
*/
|
|
35
|
+
var TRANSCRIBE_AFTER_CHUNKS_AMOUNT = 50;
|
|
36
|
+
/**
|
|
37
|
+
* Manages transcription state for a meeting. Concrete implementation of the
|
|
38
|
+
* {@link TranscriptionCapabilities.TranscriptionManager} service contract; consumers depend on the
|
|
39
|
+
* interface (via the `TranscriptionManagerProvider` capability), not this class.
|
|
40
|
+
*/
|
|
41
|
+
var TranscriptionManagerImpl = class extends Resource {
|
|
42
|
+
_edgeClient;
|
|
43
|
+
_messageEnricher;
|
|
44
|
+
_registry;
|
|
45
|
+
_audioStreamTrack = void 0;
|
|
46
|
+
_identityDid = void 0;
|
|
47
|
+
_mediaRecorder = void 0;
|
|
48
|
+
_transcriber = void 0;
|
|
49
|
+
_feed = void 0;
|
|
50
|
+
_feedServiceLayer = void 0;
|
|
51
|
+
_enabledAtom = Atom.make(false);
|
|
52
|
+
constructor(options) {
|
|
53
|
+
super();
|
|
54
|
+
this._edgeClient = options.edgeClient;
|
|
55
|
+
this._messageEnricher = options.messageEnricher;
|
|
56
|
+
this._registry = options.registry;
|
|
57
|
+
}
|
|
58
|
+
get enabled() {
|
|
59
|
+
return this._enabledAtom;
|
|
60
|
+
}
|
|
61
|
+
getEnabled() {
|
|
62
|
+
return this._registry.get(this._enabledAtom);
|
|
63
|
+
}
|
|
64
|
+
setFeed(space, feed) {
|
|
65
|
+
this._feed = feed;
|
|
66
|
+
this._feedServiceLayer = Database.layer(space.db);
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
setIdentityDid(did) {
|
|
70
|
+
if (this._identityDid !== did) this._identityDid = did;
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
setRecording(recording) {
|
|
74
|
+
if (!this.isOpen || !this.getEnabled()) return this;
|
|
75
|
+
if (recording) this._transcriber?.startChunksRecording();
|
|
76
|
+
else this._transcriber?.stopChunksRecording();
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
async setEnabled(enabled) {
|
|
80
|
+
if (this.getEnabled() === enabled) return;
|
|
81
|
+
this._registry.set(this._enabledAtom, enabled ?? false);
|
|
82
|
+
if (enabled) await this._maybeRestartTranscriber();
|
|
83
|
+
else await this._stopTranscriber();
|
|
84
|
+
}
|
|
85
|
+
async setAudioTrack(track) {
|
|
86
|
+
if (this._audioStreamTrack === track) return;
|
|
87
|
+
this._audioStreamTrack = track;
|
|
88
|
+
await this._maybeRestartTranscriber();
|
|
89
|
+
}
|
|
90
|
+
async _open() {
|
|
91
|
+
await this._maybeRestartTranscriber();
|
|
92
|
+
}
|
|
93
|
+
async _close() {
|
|
94
|
+
await this._stopTranscriber();
|
|
95
|
+
}
|
|
96
|
+
async _maybeRestartTranscriber() {
|
|
97
|
+
if (!this._audioStreamTrack || !this.getEnabled() || !this.isOpen) return;
|
|
98
|
+
let needReinit = false;
|
|
99
|
+
if (this._audioStreamTrack !== this._mediaRecorder?.mediaStreamTrack) {
|
|
100
|
+
this._mediaRecorder = new MediaStreamRecorder({
|
|
101
|
+
mediaStreamTrack: this._audioStreamTrack,
|
|
102
|
+
config: { interval: RECORD_INTERVAL }
|
|
103
|
+
});
|
|
104
|
+
needReinit = true;
|
|
105
|
+
}
|
|
106
|
+
if (needReinit) {
|
|
107
|
+
await this._transcriber?.close();
|
|
108
|
+
this._transcriber = new Transcriber({
|
|
109
|
+
config: {
|
|
110
|
+
transcribeAfterChunksAmount: TRANSCRIBE_AFTER_CHUNKS_AMOUNT,
|
|
111
|
+
prefixBufferChunksAmount: PREFIXED_CHUNKS_AMOUNT
|
|
112
|
+
},
|
|
113
|
+
recorder: this._mediaRecorder,
|
|
114
|
+
onSegments: (segments) => this._onSegments(segments)
|
|
115
|
+
});
|
|
116
|
+
await this._transcriber?.open();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async _stopTranscriber() {
|
|
120
|
+
await this._transcriber?.close();
|
|
121
|
+
}
|
|
122
|
+
async _onSegments(segments) {
|
|
123
|
+
if (!this.isOpen || !this._feed || !this._feedServiceLayer) return;
|
|
124
|
+
let block = Obj.make(Message.Message, {
|
|
125
|
+
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
126
|
+
blocks: segments,
|
|
127
|
+
sender: { identityDid: this._identityDid }
|
|
128
|
+
});
|
|
129
|
+
if (this._messageEnricher) block = await this._messageEnricher(block);
|
|
130
|
+
await Feed.append(this._feed, [block]).pipe(Effect.provide(this._feedServiceLayer), EffectEx.runAndForwardErrors);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
__decorate([synchronized], TranscriptionManagerImpl.prototype, "setAudioTrack", null);
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region src/capabilities/transcriber.ts
|
|
136
|
+
/**
|
|
137
|
+
* Provides the higher-level transcription manager to the app-framework so other plugins can obtain it
|
|
138
|
+
* via DI. The low-level construction lives in `@dxos/react-ui-transcription`; this module is the
|
|
139
|
+
* provision seam.
|
|
140
|
+
*/
|
|
141
|
+
var transcriber_default = Capability.makeModule(Effect.fnUntraced(function* () {
|
|
142
|
+
const capabilities = yield* Capability.Service;
|
|
143
|
+
const registry = yield* Capability.get(Capabilities.AtomRegistry);
|
|
144
|
+
const transcriptionManagerProvider = ({ messageEnricher }) => {
|
|
145
|
+
const client = capabilities.get(ClientCapabilities.Client);
|
|
146
|
+
const haloIdentity = capabilities.get(ClientCapabilities.IdentityService);
|
|
147
|
+
const transcriptionManager = new TranscriptionManagerImpl({
|
|
148
|
+
edgeClient: client.edge.http,
|
|
149
|
+
messageEnricher,
|
|
150
|
+
registry
|
|
151
|
+
});
|
|
152
|
+
const identity = Option.getOrUndefined(haloIdentity.getSnapshot());
|
|
153
|
+
if (identity) transcriptionManager.setIdentityDid(identity.did);
|
|
154
|
+
return transcriptionManager;
|
|
155
|
+
};
|
|
156
|
+
return [Capability.contributes(TranscriptionCapabilities.TranscriptionManagerProvider, transcriptionManagerProvider)];
|
|
157
|
+
}));
|
|
158
|
+
//#endregion
|
|
159
|
+
export { transcriber_default as default };
|
|
160
|
+
|
|
161
|
+
//# sourceMappingURL=chunk-transcriber.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-transcriber.mjs","names":[],"sources":["../../src/transcription-manager.ts","../../src/capabilities/transcriber.ts"],"sourcesContent":["//\n// Copyright 2025 DXOS.org\n//\n\nimport { Atom, type Registry } from '@effect-atom/atom';\nimport * as Effect from 'effect/Effect';\nimport * as Layer from 'effect/Layer';\n\nimport { synchronized } from '@dxos/async';\nimport { type Space } from '@dxos/client/echo';\nimport { Resource } from '@dxos/context';\nimport { Database, Feed, Obj } from '@dxos/echo';\nimport { EffectEx } from '@dxos/effect';\nimport { Transcriber } from '@dxos/pipeline-transcription';\nimport { type EdgeHttpClient } from '@dxos/react-edge-client';\nimport { MediaStreamRecorder } from '@dxos/react-ui-transcription';\nimport { type ContentBlock, Message } from '@dxos/types';\n\nimport { type TranscriptionCapabilities } from '#types';\n\n/**\n * Length of the chunk in ms.\n */\nconst RECORD_INTERVAL = 200;\n\n/**\n * Number of chunks to save before the user starts speaking.\n */\nconst PREFIXED_CHUNKS_AMOUNT = 10;\n\n/**\n * Number of chunks to transcribe automatically after.\n * Combined should be mess than 25MB or whisper would fail.\n */\nconst TRANSCRIBE_AFTER_CHUNKS_AMOUNT = 50;\n\nexport type TranscriptionManagerOptions = {\n edgeClient: EdgeHttpClient;\n registry: Registry.Registry;\n\n /**\n * Enrich the message before it is written to the transcription feed.\n */\n messageEnricher?: TranscriptionCapabilities.TranscriptMessageEnricher;\n};\n\n/**\n * Manages transcription state for a meeting. Concrete implementation of the\n * {@link TranscriptionCapabilities.TranscriptionManager} service contract; consumers depend on the\n * interface (via the `TranscriptionManagerProvider` capability), not this class.\n */\nexport class TranscriptionManagerImpl extends Resource implements TranscriptionCapabilities.TranscriptionManager {\n private readonly _edgeClient: EdgeHttpClient;\n private readonly _messageEnricher?: TranscriptionCapabilities.TranscriptMessageEnricher;\n private readonly _registry: Registry.Registry;\n private _audioStreamTrack?: MediaStreamTrack = undefined;\n private _identityDid?: string = undefined;\n private _mediaRecorder?: MediaStreamRecorder = undefined;\n private _transcriber?: Transcriber = undefined;\n private _feed?: Feed.Feed = undefined;\n private _feedServiceLayer?: Layer.Layer<Database.Service> = undefined;\n private _enabledAtom = Atom.make(false);\n\n constructor(options: TranscriptionManagerOptions) {\n super();\n this._edgeClient = options.edgeClient;\n this._messageEnricher = options.messageEnricher;\n this._registry = options.registry;\n }\n\n get enabled(): Atom.Atom<boolean> {\n return this._enabledAtom;\n }\n\n getEnabled(): boolean {\n return this._registry.get(this._enabledAtom);\n }\n\n setFeed(space: Space, feed: Feed.Feed): this {\n this._feed = feed;\n this._feedServiceLayer = Database.layer(space.db);\n return this;\n }\n\n setIdentityDid(did: string): this {\n if (this._identityDid !== did) {\n this._identityDid = did;\n }\n return this;\n }\n\n setRecording(recording?: boolean): this {\n if (!this.isOpen || !this.getEnabled()) {\n return this;\n }\n\n if (recording) {\n this._transcriber?.startChunksRecording();\n } else {\n this._transcriber?.stopChunksRecording();\n }\n return this;\n }\n\n async setEnabled(enabled: boolean): Promise<void> {\n if (this.getEnabled() === enabled) {\n return;\n }\n\n this._registry.set(this._enabledAtom, enabled ?? false);\n if (enabled) {\n await this._maybeRestartTranscriber();\n } else {\n await this._stopTranscriber();\n }\n }\n\n @synchronized\n async setAudioTrack(track?: MediaStreamTrack): Promise<void> {\n if (this._audioStreamTrack === track) {\n return;\n }\n\n this._audioStreamTrack = track;\n await this._maybeRestartTranscriber();\n }\n\n protected override async _open(): Promise<void> {\n await this._maybeRestartTranscriber();\n }\n\n protected override async _close(): Promise<void> {\n await this._stopTranscriber();\n }\n\n private async _maybeRestartTranscriber(): Promise<void> {\n if (!this._audioStreamTrack || !this.getEnabled() || !this.isOpen) {\n return;\n }\n\n // Reinitialize transcriber if feed or media stream track has changed.\n let needReinit = false;\n if (this._audioStreamTrack !== this._mediaRecorder?.mediaStreamTrack) {\n this._mediaRecorder = new MediaStreamRecorder({\n mediaStreamTrack: this._audioStreamTrack,\n config: { interval: RECORD_INTERVAL },\n });\n needReinit = true;\n }\n if (needReinit) {\n await this._transcriber?.close();\n this._transcriber = new Transcriber({\n config: {\n transcribeAfterChunksAmount: TRANSCRIBE_AFTER_CHUNKS_AMOUNT,\n prefixBufferChunksAmount: PREFIXED_CHUNKS_AMOUNT,\n },\n recorder: this._mediaRecorder,\n onSegments: (segments) => this._onSegments(segments),\n });\n\n await this._transcriber?.open();\n }\n }\n\n private async _stopTranscriber(): Promise<void> {\n await this._transcriber?.close();\n }\n\n private async _onSegments(segments: ContentBlock.Transcript[]): Promise<void> {\n if (!this.isOpen || !this._feed || !this._feedServiceLayer) {\n return;\n }\n\n let block = Obj.make(Message.Message, {\n created: new Date().toISOString(),\n blocks: segments,\n sender: { identityDid: this._identityDid },\n });\n\n if (this._messageEnricher) {\n block = await this._messageEnricher(block);\n }\n\n await Feed.append(this._feed, [block]).pipe(Effect.provide(this._feedServiceLayer), EffectEx.runAndForwardErrors);\n }\n}\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\nimport * as Option from 'effect/Option';\n\nimport { Capabilities, Capability } from '@dxos/app-framework';\nimport { ClientCapabilities } from '@dxos/plugin-client';\n\nimport { TranscriptionCapabilities } from '#types';\n\nimport { TranscriptionManagerImpl } from '../transcription-manager';\n\n/**\n * Provides the higher-level transcription manager to the app-framework so other plugins can obtain it\n * via DI. The low-level construction lives in `@dxos/react-ui-transcription`; this module is the\n * provision seam.\n */\nexport default Capability.makeModule(\n Effect.fnUntraced(function* () {\n // Get context for lazy capability access in callbacks.\n const capabilities = yield* Capability.Service;\n const registry = yield* Capability.get(Capabilities.AtomRegistry);\n\n const transcriptionManagerProvider: TranscriptionCapabilities.TranscriptionManagerProvider = ({\n messageEnricher,\n }) => {\n const client = capabilities.get(ClientCapabilities.Client);\n const haloIdentity = capabilities.get(ClientCapabilities.IdentityService);\n const transcriptionManager = new TranscriptionManagerImpl({\n edgeClient: client.edge.http,\n messageEnricher,\n registry,\n });\n\n const identity = Option.getOrUndefined(haloIdentity.getSnapshot());\n if (identity) {\n transcriptionManager.setIdentityDid(identity.did);\n }\n\n return transcriptionManager;\n };\n\n return [\n Capability.contributes(TranscriptionCapabilities.TranscriptionManagerProvider, transcriptionManagerProvider),\n ];\n }),\n);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAuBA,IAAM,kBAAkB;;;;AAKxB,IAAM,yBAAyB;;;;;AAM/B,IAAM,iCAAiC;;;;;;AAiBvC,IAAa,2BAAb,cAA8C,SAAmE;CAC/G;CACA;CACA;CACA,oBAA+C,KAAA;CAC/C,eAAgC,KAAA;CAChC,iBAA+C,KAAA;CAC/C,eAAqC,KAAA;CACrC,QAA4B,KAAA;CAC5B,oBAA4D,KAAA;CAC5D,eAAuB,KAAK,KAAK,KAAK;CAEtC,YAAY,SAAsC;EAChD,MAAM;EACN,KAAK,cAAc,QAAQ;EAC3B,KAAK,mBAAmB,QAAQ;EAChC,KAAK,YAAY,QAAQ;CAC3B;CAEA,IAAI,UAA8B;EAChC,OAAO,KAAK;CACd;CAEA,aAAsB;EACpB,OAAO,KAAK,UAAU,IAAI,KAAK,YAAY;CAC7C;CAEA,QAAQ,OAAc,MAAuB;EAC3C,KAAK,QAAQ;EACb,KAAK,oBAAoB,SAAS,MAAM,MAAM,EAAE;EAChD,OAAO;CACT;CAEA,eAAe,KAAmB;EAChC,IAAI,KAAK,iBAAiB,KACxB,KAAK,eAAe;EAEtB,OAAO;CACT;CAEA,aAAa,WAA2B;EACtC,IAAI,CAAC,KAAK,UAAU,CAAC,KAAK,WAAW,GACnC,OAAO;EAGT,IAAI,WACF,KAAK,cAAc,qBAAqB;OAExC,KAAK,cAAc,oBAAoB;EAEzC,OAAO;CACT;CAEA,MAAM,WAAW,SAAiC;EAChD,IAAI,KAAK,WAAW,MAAM,SACxB;EAGF,KAAK,UAAU,IAAI,KAAK,cAAc,WAAW,KAAK;EACtD,IAAI,SACF,MAAM,KAAK,yBAAyB;OAEpC,MAAM,KAAK,iBAAiB;CAEhC;CAEA,MACM,cAAc,OAAyC;EAC3D,IAAI,KAAK,sBAAsB,OAC7B;EAGF,KAAK,oBAAoB;EACzB,MAAM,KAAK,yBAAyB;CACtC;CAEA,MAAyB,QAAuB;EAC9C,MAAM,KAAK,yBAAyB;CACtC;CAEA,MAAyB,SAAwB;EAC/C,MAAM,KAAK,iBAAiB;CAC9B;CAEA,MAAc,2BAA0C;EACtD,IAAI,CAAC,KAAK,qBAAqB,CAAC,KAAK,WAAW,KAAK,CAAC,KAAK,QACzD;EAIF,IAAI,aAAa;EACjB,IAAI,KAAK,sBAAsB,KAAK,gBAAgB,kBAAkB;GACpE,KAAK,iBAAiB,IAAI,oBAAoB;IAC5C,kBAAkB,KAAK;IACvB,QAAQ,EAAE,UAAU,gBAAgB;GACtC,CAAC;GACD,aAAa;EACf;EACA,IAAI,YAAY;GACd,MAAM,KAAK,cAAc,MAAM;GAC/B,KAAK,eAAe,IAAI,YAAY;IAClC,QAAQ;KACN,6BAA6B;KAC7B,0BAA0B;IAC5B;IACA,UAAU,KAAK;IACf,aAAa,aAAa,KAAK,YAAY,QAAQ;GACrD,CAAC;GAED,MAAM,KAAK,cAAc,KAAK;EAChC;CACF;CAEA,MAAc,mBAAkC;EAC9C,MAAM,KAAK,cAAc,MAAM;CACjC;CAEA,MAAc,YAAY,UAAoD;EAC5E,IAAI,CAAC,KAAK,UAAU,CAAC,KAAK,SAAS,CAAC,KAAK,mBACvC;EAGF,IAAI,QAAQ,IAAI,KAAK,QAAQ,SAAS;GACpC,0BAAS,IAAI,KAAK,EAAA,CAAE,YAAY;GAChC,QAAQ;GACR,QAAQ,EAAE,aAAa,KAAK,aAAa;EAC3C,CAAC;EAED,IAAI,KAAK,kBACP,QAAQ,MAAM,KAAK,iBAAiB,KAAK;EAG3C,MAAM,KAAK,OAAO,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,OAAO,QAAQ,KAAK,iBAAiB,GAAG,SAAS,mBAAmB;CAClH;AACF;YApEG,YAAA,GAAA,yBAAA,WAAA,iBAAA,IAAA;;;;;;;;AClGH,IAAA,sBAAe,WAAW,WACxB,OAAO,WAAW,aAAa;CAE7B,MAAM,eAAe,OAAO,WAAW;CACvC,MAAM,WAAW,OAAO,WAAW,IAAI,aAAa,YAAY;CAEhE,MAAM,gCAAwF,EAC5F,sBACI;EACJ,MAAM,SAAS,aAAa,IAAI,mBAAmB,MAAM;EACzD,MAAM,eAAe,aAAa,IAAI,mBAAmB,eAAe;EACxE,MAAM,uBAAuB,IAAI,yBAAyB;GACxD,YAAY,OAAO,KAAK;GACxB;GACA;EACF,CAAC;EAED,MAAM,WAAW,OAAO,eAAe,aAAa,YAAY,CAAC;EACjE,IAAI,UACF,qBAAqB,eAAe,SAAS,GAAG;EAGlD,OAAO;CACT;CAEA,OAAO,CACL,WAAW,YAAY,0BAA0B,8BAA8B,4BAA4B,CAC7G;AACF,CAAC,CACH"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { Capabilities, Capability } from "@dxos/app-framework";
|
|
2
|
+
import { TranscriptionCapabilities } from "#types";
|
|
3
|
+
import { meta } from "#meta";
|
|
4
|
+
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { useAtomCapability, useAtomCapabilityState, useCapabilities } from "@dxos/app-framework/ui";
|
|
6
|
+
import { useAudioTrack, useTranscriber } from "@dxos/react-ui-transcription";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import * as Effect from "effect/Effect";
|
|
9
|
+
import { log } from "@dxos/log";
|
|
10
|
+
import { MarkdownCapabilities } from "@dxos/plugin-markdown/types";
|
|
11
|
+
import { linkEntities } from "@dxos/pipeline-transcription";
|
|
12
|
+
import { PendingTextStreamer, cancelPendingText, editorPendingTextSink, pendingTextState } from "@dxos/ui-editor";
|
|
13
|
+
//#region src/capabilities/transcription-driver.tsx
|
|
14
|
+
var __dxlog_file = "/__w/dxos/dxos/packages/plugins/plugin-transcription/src/capabilities/transcription-driver.tsx";
|
|
15
|
+
var RECORDER_INTERVAL_MS = 200;
|
|
16
|
+
/**
|
|
17
|
+
* Headless controller driving live transcription into a Markdown editor. Reads the active recording
|
|
18
|
+
* session, captures audio while recording, and dispatches the transcribed text into the target
|
|
19
|
+
* editor as pending-text effects. The user confirms or discards the pending text via the editor's
|
|
20
|
+
* inline affordances. Mounted once, app-wide, via `Capabilities.ReactContext`.
|
|
21
|
+
*/
|
|
22
|
+
var TranscriptionDriver = () => {
|
|
23
|
+
const [session, setSession] = useAtomCapabilityState(TranscriptionCapabilities.RecordingSession);
|
|
24
|
+
const [editorViews] = useCapabilities(MarkdownCapabilities.EditorViews);
|
|
25
|
+
const [lookup] = useCapabilities(TranscriptionCapabilities.EntityLookup);
|
|
26
|
+
const settings = useAtomCapability(TranscriptionCapabilities.Settings);
|
|
27
|
+
const [, setStatus] = useAtomCapabilityState(TranscriptionCapabilities.PipelineStatus);
|
|
28
|
+
const recording = session?.recording ?? false;
|
|
29
|
+
const liveSessionId = session?.id;
|
|
30
|
+
const hasEditorForSession = !!(liveSessionId && editorViews?.get(liveSessionId));
|
|
31
|
+
const [phase, setPhase] = useState("idle");
|
|
32
|
+
const active = phase !== "idle";
|
|
33
|
+
const [sessionId, setSessionId] = useState();
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (recording && liveSessionId && hasEditorForSession) setSessionId(liveSessionId);
|
|
36
|
+
}, [
|
|
37
|
+
recording,
|
|
38
|
+
liveSessionId,
|
|
39
|
+
hasEditorForSession
|
|
40
|
+
]);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
setPhase((current) => recording && hasEditorForSession ? "recording" : current === "recording" ? "draining" : current);
|
|
43
|
+
}, [recording, hasEditorForSession]);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (phase === "idle") setSessionId(void 0);
|
|
46
|
+
}, [phase]);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
setStatus(() => ({ phase }));
|
|
49
|
+
}, [phase, setStatus]);
|
|
50
|
+
const track = useAudioTrack(active, useMemo(() => settings?.audioDeviceId ? { deviceId: { ideal: settings.audioDeviceId } } : void 0, [settings?.audioDeviceId]));
|
|
51
|
+
const sessionIdRef = useRef(sessionId);
|
|
52
|
+
sessionIdRef.current = sessionId;
|
|
53
|
+
const editorViewsRef = useRef(editorViews);
|
|
54
|
+
editorViewsRef.current = editorViews;
|
|
55
|
+
const isCancelled = useCallback(() => {
|
|
56
|
+
const id = sessionIdRef.current;
|
|
57
|
+
const view = id ? editorViewsRef.current?.get(id)?.view : void 0;
|
|
58
|
+
return view ? view.state.field(pendingTextState, false) == null : false;
|
|
59
|
+
}, []);
|
|
60
|
+
const streamerRef = useRef(null);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!active || !editorViews || !sessionId) return;
|
|
63
|
+
const entry = editorViews.get(sessionId);
|
|
64
|
+
if (!entry) return;
|
|
65
|
+
const entityExtraction = settings?.entityExtraction !== false;
|
|
66
|
+
const streamer = new PendingTextStreamer(editorPendingTextSink(entry.view), {
|
|
67
|
+
mode: settings?.streamMode ?? "word",
|
|
68
|
+
wordIntervalMs: settings?.wordIntervalMs ?? 80,
|
|
69
|
+
postProcess: entityExtraction && lookup ? (text) => linkEntities(text, lookup) : void 0
|
|
70
|
+
});
|
|
71
|
+
streamer.start({
|
|
72
|
+
anchor: entry.view.state.selection.main.head,
|
|
73
|
+
placeholder: "Recording…"
|
|
74
|
+
});
|
|
75
|
+
streamerRef.current = streamer;
|
|
76
|
+
return () => {
|
|
77
|
+
streamer.dispose();
|
|
78
|
+
streamerRef.current = null;
|
|
79
|
+
const current = editorViews.get(sessionId);
|
|
80
|
+
const pending = current?.view.state.field(pendingTextState, false);
|
|
81
|
+
if (current && pending && pending.final.length === 0) current.view.dispatch({ effects: cancelPendingText.of() });
|
|
82
|
+
};
|
|
83
|
+
}, [
|
|
84
|
+
active,
|
|
85
|
+
sessionId,
|
|
86
|
+
editorViews,
|
|
87
|
+
lookup,
|
|
88
|
+
settings?.streamMode,
|
|
89
|
+
settings?.wordIntervalMs,
|
|
90
|
+
settings?.entityExtraction
|
|
91
|
+
]);
|
|
92
|
+
const transcriber = useTranscriber({
|
|
93
|
+
audioStreamTrack: track,
|
|
94
|
+
onSegments: useCallback(async (segments) => {
|
|
95
|
+
const text = segments.map((segment) => segment.text).join(" ").trim();
|
|
96
|
+
if (text.length > 0 && !isCancelled()) streamerRef.current?.push(text);
|
|
97
|
+
}, [isCancelled]),
|
|
98
|
+
transcriberConfig: useMemo(() => ({ transcribeAfterChunksAmount: Math.max(1, Math.round((settings?.transcribeAfterMs ?? 4e3) / RECORDER_INTERVAL_MS)) }), [settings?.transcribeAfterMs]),
|
|
99
|
+
recorderConfig: useMemo(() => ({ interval: RECORDER_INTERVAL_MS }), [])
|
|
100
|
+
});
|
|
101
|
+
const transcriberRef = useRef(transcriber);
|
|
102
|
+
transcriberRef.current = transcriber;
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (!transcriber) return;
|
|
105
|
+
let cancelled = false;
|
|
106
|
+
transcriber.open().then(() => {
|
|
107
|
+
if (!cancelled && phase === "recording") transcriber.startChunksRecording();
|
|
108
|
+
}, (err) => {
|
|
109
|
+
if (!cancelled) {
|
|
110
|
+
log.catch(err, void 0, {
|
|
111
|
+
"~LogMeta": "~LogMeta",
|
|
112
|
+
F: __dxlog_file,
|
|
113
|
+
L: 202,
|
|
114
|
+
S: void 0
|
|
115
|
+
});
|
|
116
|
+
setSession(() => null);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
return () => {
|
|
120
|
+
cancelled = true;
|
|
121
|
+
};
|
|
122
|
+
}, [
|
|
123
|
+
transcriber,
|
|
124
|
+
phase,
|
|
125
|
+
setSession
|
|
126
|
+
]);
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (phase !== "draining") return;
|
|
129
|
+
let cancelled = false;
|
|
130
|
+
const abortTimer = setInterval(() => {
|
|
131
|
+
if (isCancelled()) transcriberRef.current?.abort();
|
|
132
|
+
}, 150);
|
|
133
|
+
(async () => {
|
|
134
|
+
try {
|
|
135
|
+
await transcriberRef.current?.flush();
|
|
136
|
+
if (!isCancelled()) await streamerRef.current?.flush();
|
|
137
|
+
} catch (err) {
|
|
138
|
+
log.catch(err, void 0, {
|
|
139
|
+
"~LogMeta": "~LogMeta",
|
|
140
|
+
F: __dxlog_file,
|
|
141
|
+
L: 234,
|
|
142
|
+
S: void 0
|
|
143
|
+
});
|
|
144
|
+
} finally {
|
|
145
|
+
clearInterval(abortTimer);
|
|
146
|
+
if (!cancelled) setPhase((current) => current === "draining" ? "idle" : current);
|
|
147
|
+
}
|
|
148
|
+
})();
|
|
149
|
+
return () => {
|
|
150
|
+
cancelled = true;
|
|
151
|
+
clearInterval(abortTimer);
|
|
152
|
+
};
|
|
153
|
+
}, [phase, isCancelled]);
|
|
154
|
+
return null;
|
|
155
|
+
};
|
|
156
|
+
var transcription_driver_default = Capability.makeModule(Effect.fnUntraced(function* () {
|
|
157
|
+
return Capability.contributes(Capabilities.ReactContext, {
|
|
158
|
+
id: meta.profile.key,
|
|
159
|
+
context: ({ children }) => /* @__PURE__ */ jsxs(Fragment, { children: [children, /* @__PURE__ */ jsx(TranscriptionDriver, {})] })
|
|
160
|
+
});
|
|
161
|
+
}));
|
|
162
|
+
//#endregion
|
|
163
|
+
export { transcription_driver_default as default };
|
|
164
|
+
|
|
165
|
+
//# sourceMappingURL=chunk-transcription-driver.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-transcription-driver.mjs","names":[],"sources":["../../src/capabilities/transcription-driver.tsx"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\nimport React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport { Capabilities, Capability } from '@dxos/app-framework';\nimport { useAtomCapability, useAtomCapabilityState, useCapabilities } from '@dxos/app-framework/ui';\nimport { log } from '@dxos/log';\nimport { linkEntities } from '@dxos/pipeline-transcription';\nimport { MarkdownCapabilities } from '@dxos/plugin-markdown/types';\nimport { useAudioTrack, useTranscriber } from '@dxos/react-ui-transcription';\nimport { type ContentBlock } from '@dxos/types';\nimport { PendingTextStreamer, cancelPendingText, editorPendingTextSink, pendingTextState } from '@dxos/ui-editor';\n\nimport { meta } from '#meta';\nimport { TranscriptionCapabilities } from '#types';\n\n// Recorder chunk interval; the transcriber's chunk threshold is derived from the buffering setting.\nconst RECORDER_INTERVAL_MS = 200;\n\n/**\n * Headless controller driving live transcription into a Markdown editor. Reads the active recording\n * session, captures audio while recording, and dispatches the transcribed text into the target\n * editor as pending-text effects. The user confirms or discards the pending text via the editor's\n * inline affordances. Mounted once, app-wide, via `Capabilities.ReactContext`.\n */\nconst TranscriptionDriver = () => {\n const [session, setSession] = useAtomCapabilityState(TranscriptionCapabilities.RecordingSession);\n // `useCapabilities` (not `useCapability`) so the driver does not throw when plugin-markdown is absent.\n const [editorViews] = useCapabilities(MarkdownCapabilities.EditorViews);\n // Injected entity resolver (full-text/vector/…); the driver stays decoupled from the database.\n const [lookup] = useCapabilities(TranscriptionCapabilities.EntityLookup);\n const settings = useAtomCapability(TranscriptionCapabilities.Settings);\n\n const [, setStatus] = useAtomCapabilityState(TranscriptionCapabilities.PipelineStatus);\n\n const recording = session?.recording ?? false;\n const liveSessionId = session?.id;\n\n // Only this driver's editor-backed sessions are captured here: if no editor view is registered for\n // the session there is nowhere to stream transcription, so the driver stays idle and leaves the\n // mic to whoever owns that session (e.g. a non-editor consumer running its own transcriber).\n const hasEditorForSession = !!(liveSessionId && editorViews?.get(liveSessionId));\n\n // Lifecycle phase. Driven from `recording` via an effect (not derived synchronously) so that when\n // the mic switches off the phase is still 'recording' for that commit — nothing tears down until\n // the drain finishes and sets 'idle'. This lets the final transcription + enrichment complete.\n const [phase, setPhase] = useState<'idle' | 'recording' | 'draining'>('idle');\n const active = phase !== 'idle';\n\n // The toolbar clears the RecordingSession on stop, but the driver must keep targeting the same\n // editor through the drain — capture the session id for the lifetime of the active phase (the\n // editor stays open). Cleared once the phase returns to idle.\n const [sessionId, setSessionId] = useState<string>();\n useEffect(() => {\n if (recording && liveSessionId && hasEditorForSession) {\n setSessionId(liveSessionId);\n }\n }, [recording, liveSessionId, hasEditorForSession]);\n\n useEffect(() => {\n setPhase((current) =>\n recording && hasEditorForSession ? 'recording' : current === 'recording' ? 'draining' : current,\n );\n }, [recording, hasEditorForSession]);\n\n useEffect(() => {\n if (phase === 'idle') {\n setSessionId(undefined);\n }\n }, [phase]);\n\n // Publish the phase so the toolbar / testbench can reflect mic + pipeline state in real time.\n useEffect(() => {\n setStatus(() => ({ phase }));\n }, [phase, setStatus]);\n\n // `ideal` (not `exact`) so an unavailable device falls back to the default rather than failing.\n const audioConstraints = useMemo<MediaTrackConstraints | undefined>(\n () => (settings?.audioDeviceId ? { deviceId: { ideal: settings.audioDeviceId } } : undefined),\n [settings?.audioDeviceId],\n );\n\n // Keep the track (and thus the transcriber) alive across the whole active session — including the\n // drain — so the transcriber can flush its buffered audio before teardown. Released at idle. The\n // mic indicator therefore lingers for the brief drain after stop.\n const track = useAudioTrack(active, audioConstraints);\n\n // Refs so `handleSegments` stays stable (changing it would recreate the transcriber).\n const sessionIdRef = useRef(sessionId);\n sessionIdRef.current = sessionId;\n const editorViewsRef = useRef(editorViews);\n editorViewsRef.current = editorViews;\n\n // The user cancelled (✕) if the pending decoration was cleared mid-session; guards against\n // resurrecting it with late transcription / post-process writes during the drain.\n const isCancelled = useCallback(() => {\n const id = sessionIdRef.current;\n const view = id ? editorViewsRef.current?.get(id)?.view : undefined;\n return view ? view.state.field(pendingTextState, false) == null : false;\n }, []);\n\n // Streams transcribed text into the editor's pending-text block (buffering + word-by-word reveal).\n // Kept alive across the whole session (incl. drain); disposed only when the phase returns to idle.\n const streamerRef = useRef<PendingTextStreamer | null>(null);\n\n useEffect(() => {\n if (!active || !editorViews || !sessionId) {\n return;\n }\n const entry = editorViews.get(sessionId);\n if (!entry) {\n return;\n }\n\n // Clicking the toolbar blurs the editor and hides the caret, so anchor + placeholder give\n // immediate feedback while the first transcription is buffered.\n // When entity extraction is enabled and a lookup is available, rewrite settled text with inline\n // `[noun](echo:/<id>)` links — decorated as dx-anchors by the markdown editor's preview extension.\n const entityExtraction = settings?.entityExtraction !== false;\n const streamer = new PendingTextStreamer(editorPendingTextSink(entry.view), {\n mode: settings?.streamMode ?? 'word',\n wordIntervalMs: settings?.wordIntervalMs ?? 80,\n postProcess: entityExtraction && lookup ? (text) => linkEntities(text, lookup) : undefined,\n });\n streamer.start({ anchor: entry.view.state.selection.main.head, placeholder: 'Recording…' });\n streamerRef.current = streamer;\n\n return () => {\n streamer.dispose();\n streamerRef.current = null;\n // On teardown, drop the placeholder if nothing was transcribed; otherwise leave it for review.\n const current = editorViews.get(sessionId);\n const pending = current?.view.state.field(pendingTextState, false);\n if (current && pending && pending.final.length === 0) {\n current.view.dispatch({ effects: cancelPendingText.of() });\n }\n };\n }, [\n active,\n sessionId,\n editorViews,\n lookup,\n settings?.streamMode,\n settings?.wordIntervalMs,\n settings?.entityExtraction,\n ]);\n\n const handleSegments = useCallback(\n async (segments: ContentBlock.Transcript[]) => {\n const text = segments\n .map((segment) => segment.text)\n .join(' ')\n .trim();\n if (text.length > 0 && !isCancelled()) {\n streamerRef.current?.push(text);\n }\n },\n [isCancelled],\n );\n\n // Derive the transcriber's chunk threshold from the configured initial buffering time.\n const transcriberConfig = useMemo(\n () => ({\n transcribeAfterChunksAmount: Math.max(\n 1,\n Math.round((settings?.transcribeAfterMs ?? 4000) / RECORDER_INTERVAL_MS),\n ),\n }),\n [settings?.transcribeAfterMs],\n );\n\n // Stable identity: a fresh object would change useTranscriber's memo deps every render, recreating\n // (and reopening) the transcriber and discarding its buffered audio.\n const recorderConfig = useMemo(() => ({ interval: RECORDER_INTERVAL_MS }), []);\n const transcriber = useTranscriber({\n audioStreamTrack: track,\n onSegments: handleSegments,\n transcriberConfig,\n recorderConfig,\n });\n const transcriberRef = useRef(transcriber);\n transcriberRef.current = transcriber;\n\n useEffect(() => {\n if (!transcriber) {\n return;\n }\n let cancelled = false;\n // Clear the session if `open()` rejects (e.g. mic permission denied) so the toolbar does not\n // report \"stop\" forever while nothing is captured.\n void transcriber.open().then(\n () => {\n if (!cancelled && phase === 'recording') {\n transcriber.startChunksRecording();\n }\n },\n (err) => {\n if (!cancelled) {\n log.catch(err);\n setSession(() => null);\n }\n },\n );\n return () => {\n cancelled = true;\n };\n }, [transcriber, phase, setSession]);\n\n // Drain on stop: flush buffered audio (final transcription) and the streamer's post-process\n // (entity linking) before tearing down — so the tail of speech and its enrichment are not lost.\n useEffect(() => {\n if (phase !== 'draining') {\n return;\n }\n let cancelled = false;\n // Hard-abort the in-flight transcription request if the user cancels (✕) during the drain.\n const abortTimer = setInterval(() => {\n if (isCancelled()) {\n transcriberRef.current?.abort();\n }\n }, 150);\n void (async () => {\n try {\n // Flush buffered audio (final transcription) then settle the streamer's post-process, unless\n // the user cancelled (✕) — in which case skip the writes so the decoration stays cleared.\n await transcriberRef.current?.flush();\n if (!isCancelled()) {\n await streamerRef.current?.flush();\n }\n } catch (err) {\n log.catch(err);\n } finally {\n clearInterval(abortTimer);\n if (!cancelled) {\n setPhase((current) => (current === 'draining' ? 'idle' : current));\n }\n }\n })();\n return () => {\n cancelled = true;\n clearInterval(abortTimer);\n };\n }, [phase, isCancelled]);\n\n return null;\n};\n\nexport default Capability.makeModule(\n Effect.fnUntraced(function* () {\n return Capability.contributes(Capabilities.ReactContext, {\n id: meta.profile.key,\n context: ({ children }) => (\n <Fragment>\n {children}\n <TranscriptionDriver />\n </Fragment>\n ),\n });\n }),\n);\n"],"mappings":"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"}
|