@dxos/plugin-transcription 0.8.0 → 0.8.1-main.013e445
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/browser/{TranscriptContainer-SX6PNFEC.mjs → TranscriptContainer-TJXP6ICP.mjs} +3 -3
- package/dist/lib/browser/app-graph-builder-ZIIDYMHP.mjs +39 -0
- package/dist/lib/browser/app-graph-builder-ZIIDYMHP.mjs.map +7 -0
- package/dist/lib/browser/{chunk-AS364WZZ.mjs → chunk-3NWYXRZS.mjs} +2 -2
- package/dist/lib/browser/chunk-MD4NQO6W.mjs +252 -0
- package/dist/lib/browser/chunk-MD4NQO6W.mjs.map +7 -0
- package/dist/lib/browser/chunk-N3VKF2J3.mjs +415 -0
- package/dist/lib/browser/chunk-N3VKF2J3.mjs.map +7 -0
- package/dist/lib/browser/chunk-YEVVZZTC.mjs +19 -0
- package/dist/lib/browser/chunk-YEVVZZTC.mjs.map +7 -0
- package/dist/lib/browser/{chunk-QIHUPVQD.mjs → chunk-YRW4MNRE.mjs} +23 -24
- package/dist/lib/browser/chunk-YRW4MNRE.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +203 -366
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/intent-resolver-AC6LFBE4.mjs +73 -0
- package/dist/lib/browser/intent-resolver-AC6LFBE4.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/{react-surface-FOEWLLTZ.mjs → react-surface-OM2EECBI.mjs} +15 -8
- package/dist/lib/browser/{react-surface-FOEWLLTZ.mjs.map → react-surface-OM2EECBI.mjs.map} +2 -2
- package/dist/lib/browser/types/index.mjs +2 -1
- package/dist/lib/node/{TranscriptContainer-HU763SN3.cjs → TranscriptContainer-KNTN76RZ.cjs} +8 -8
- package/dist/lib/node/{TranscriptContainer-HU763SN3.cjs.map → TranscriptContainer-KNTN76RZ.cjs.map} +2 -2
- package/dist/lib/node/app-graph-builder-CJSNQYJZ.cjs +52 -0
- package/dist/lib/node/app-graph-builder-CJSNQYJZ.cjs.map +7 -0
- package/dist/lib/node/chunk-EKSRBQX7.cjs +437 -0
- package/dist/lib/node/chunk-EKSRBQX7.cjs.map +7 -0
- package/dist/lib/node/{chunk-QWXLQPP5.cjs → chunk-I6CUIAQH.cjs} +5 -5
- package/dist/lib/node/{app-graph-builder-YFBX3OTP.cjs → chunk-LEHTJHKP.cjs} +23 -7
- package/dist/lib/node/chunk-LEHTJHKP.cjs.map +7 -0
- package/dist/lib/node/chunk-T4W4AKGM.cjs +279 -0
- package/dist/lib/node/chunk-T4W4AKGM.cjs.map +7 -0
- package/dist/lib/node/{chunk-UIHEGU6Q.cjs → chunk-ZAE2IOZC.cjs} +25 -27
- package/dist/lib/node/chunk-ZAE2IOZC.cjs.map +7 -0
- package/dist/lib/node/index.cjs +220 -378
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/intent-resolver-ZSQ2MHJJ.cjs +84 -0
- package/dist/lib/node/intent-resolver-ZSQ2MHJJ.cjs.map +7 -0
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/{react-surface-AGGGDOUC.cjs → react-surface-ZENSCF6S.cjs} +18 -12
- package/dist/lib/node/{react-surface-AGGGDOUC.cjs.map → react-surface-ZENSCF6S.cjs.map} +2 -2
- package/dist/lib/node/types/index.cjs +8 -7
- package/dist/lib/node/types/index.cjs.map +2 -2
- package/dist/lib/node-esm/{TranscriptContainer-DVBXQIQ4.mjs → TranscriptContainer-MOWZQSEO.mjs} +3 -3
- package/dist/lib/node-esm/app-graph-builder-ARTLU4U5.mjs +40 -0
- package/dist/lib/node-esm/app-graph-builder-ARTLU4U5.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-4A6BWPRB.mjs +253 -0
- package/dist/lib/node-esm/chunk-4A6BWPRB.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-KEG3ME4O.mjs → chunk-DLXORY7Y.mjs} +22 -24
- package/dist/lib/node-esm/chunk-DLXORY7Y.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-WEZGRUG6.mjs → chunk-NFPDIQYG.mjs} +2 -2
- package/dist/lib/node-esm/chunk-SB5TLXBU.mjs +21 -0
- package/dist/lib/node-esm/chunk-SB5TLXBU.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-SWV74BYU.mjs +416 -0
- package/dist/lib/node-esm/chunk-SWV74BYU.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +203 -366
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/intent-resolver-XU4BLUSF.mjs +74 -0
- package/dist/lib/node-esm/intent-resolver-XU4BLUSF.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/{react-surface-6AYPLJ2B.mjs → react-surface-5CBEXLXJ.mjs} +15 -8
- package/dist/lib/node-esm/{react-surface-6AYPLJ2B.mjs.map → react-surface-5CBEXLXJ.mjs.map} +2 -2
- package/dist/lib/node-esm/types/index.mjs +2 -1
- package/dist/types/src/TranscriptionPlugin.d.ts.map +1 -1
- package/dist/types/src/capabilities/app-graph-builder.d.ts +1 -0
- package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
- package/dist/types/src/capabilities/index.d.ts.map +1 -1
- package/dist/types/src/capabilities/intent-resolver.d.ts.map +1 -1
- package/dist/types/src/components/Transcript/Transcript.d.ts +2 -0
- package/dist/types/src/components/Transcript/Transcript.d.ts.map +1 -1
- package/dist/types/src/components/Transcript/Transcript.stories.d.ts.map +1 -1
- package/dist/types/src/components/TranscriptContainer.d.ts +1 -0
- package/dist/types/src/components/TranscriptContainer.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +1 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/transcription.stories.d.ts +3 -2
- package/dist/types/src/components/transcription.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +0 -1
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/meta.d.ts +2 -9
- package/dist/types/src/meta.d.ts.map +1 -1
- package/dist/types/src/transcriber/index.d.ts +1 -0
- package/dist/types/src/transcriber/index.d.ts.map +1 -1
- package/dist/types/src/transcriber/media-stream-recorder.d.ts +1 -0
- package/dist/types/src/transcriber/media-stream-recorder.d.ts.map +1 -1
- package/dist/types/src/transcriber/summarize.d.ts +5 -0
- package/dist/types/src/transcriber/summarize.d.ts.map +1 -0
- package/dist/types/src/transcription-manager.d.ts +39 -0
- package/dist/types/src/transcription-manager.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +5 -6
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/schema.d.ts +9 -15
- package/dist/types/src/types/schema.d.ts.map +1 -1
- package/dist/types/src/types/types.d.ts +13 -6
- package/dist/types/src/types/types.d.ts.map +1 -1
- package/dist/types/src/util/random-queue-dxn.d.ts +2 -2
- package/dist/types/src/util/random-queue-dxn.d.ts.map +1 -1
- package/package.json +40 -37
- package/src/TranscriptionPlugin.tsx +1 -2
- package/src/capabilities/app-graph-builder.ts +25 -2
- package/src/capabilities/intent-resolver.ts +24 -2
- package/src/capabilities/react-surface.tsx +2 -2
- package/src/components/Transcript/Transcript.stories.tsx +46 -27
- package/src/components/Transcript/Transcript.tsx +205 -49
- package/src/components/TranscriptContainer.tsx +12 -147
- package/src/components/transcription.stories.tsx +11 -13
- package/src/hooks/index.ts +0 -1
- package/src/index.ts +1 -0
- package/src/meta.ts +2 -2
- package/src/transcriber/index.ts +1 -0
- package/src/transcriber/media-stream-recorder.ts +4 -0
- package/src/transcriber/summarize.ts +77 -0
- package/src/transcription-manager.ts +183 -0
- package/src/translations.ts +2 -2
- package/src/types/schema.ts +5 -3
- package/src/types/types.ts +12 -2
- package/src/util/random-queue-dxn.ts +2 -2
- package/dist/lib/browser/app-graph-builder-ZSSJIFY7.mjs +0 -7
- package/dist/lib/browser/app-graph-builder-ZSSJIFY7.mjs.map +0 -7
- package/dist/lib/browser/chunk-QIHUPVQD.mjs.map +0 -7
- package/dist/lib/browser/chunk-R4HHS6FA.mjs +0 -241
- package/dist/lib/browser/chunk-R4HHS6FA.mjs.map +0 -7
- package/dist/lib/browser/chunk-ZMES3U2D.mjs +0 -78
- package/dist/lib/browser/chunk-ZMES3U2D.mjs.map +0 -7
- package/dist/lib/browser/intent-resolver-EAR2TFK2.mjs +0 -32
- package/dist/lib/browser/intent-resolver-EAR2TFK2.mjs.map +0 -7
- package/dist/lib/node/app-graph-builder-YFBX3OTP.cjs.map +0 -7
- package/dist/lib/node/chunk-JBN3P6ZF.cjs +0 -97
- package/dist/lib/node/chunk-JBN3P6ZF.cjs.map +0 -7
- package/dist/lib/node/chunk-UIHEGU6Q.cjs.map +0 -7
- package/dist/lib/node/chunk-YV74Z74I.cjs +0 -267
- package/dist/lib/node/chunk-YV74Z74I.cjs.map +0 -7
- package/dist/lib/node/intent-resolver-3KJZGGUQ.cjs +0 -44
- package/dist/lib/node/intent-resolver-3KJZGGUQ.cjs.map +0 -7
- package/dist/lib/node-esm/app-graph-builder-77GQG6UF.mjs +0 -9
- package/dist/lib/node-esm/app-graph-builder-77GQG6UF.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-36EQXBWR.mjs +0 -242
- package/dist/lib/node-esm/chunk-36EQXBWR.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-KEG3ME4O.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-XYU6B73V.mjs +0 -80
- package/dist/lib/node-esm/chunk-XYU6B73V.mjs.map +0 -7
- package/dist/lib/node-esm/intent-resolver-UGLYRD4X.mjs +0 -33
- package/dist/lib/node-esm/intent-resolver-UGLYRD4X.mjs.map +0 -7
- package/dist/types/src/hooks/useIsSpeaking.d.ts +0 -2
- package/dist/types/src/hooks/useIsSpeaking.d.ts.map +0 -1
- package/src/hooks/useIsSpeaking.ts +0 -77
- /package/dist/lib/browser/{TranscriptContainer-SX6PNFEC.mjs.map → TranscriptContainer-TJXP6ICP.mjs.map} +0 -0
- /package/dist/lib/browser/{chunk-AS364WZZ.mjs.map → chunk-3NWYXRZS.mjs.map} +0 -0
- /package/dist/lib/node/{chunk-QWXLQPP5.cjs.map → chunk-I6CUIAQH.cjs.map} +0 -0
- /package/dist/lib/node-esm/{TranscriptContainer-DVBXQIQ4.mjs.map → TranscriptContainer-MOWZQSEO.mjs.map} +0 -0
- /package/dist/lib/node-esm/{chunk-WEZGRUG6.mjs.map → chunk-NFPDIQYG.mjs.map} +0 -0
|
@@ -5,31 +5,58 @@
|
|
|
5
5
|
import '@dxos-theme';
|
|
6
6
|
|
|
7
7
|
import { type StoryObj, type Meta } from '@storybook/react';
|
|
8
|
-
import React from 'react';
|
|
8
|
+
import React, { useEffect, useState } from 'react';
|
|
9
9
|
|
|
10
10
|
import { faker } from '@dxos/random';
|
|
11
|
-
import {
|
|
11
|
+
import { hues } from '@dxos/react-ui-theme';
|
|
12
12
|
import { withTheme, withLayout } from '@dxos/storybook-utils';
|
|
13
13
|
|
|
14
14
|
import { Transcript } from './Transcript';
|
|
15
15
|
import translations from '../../translations';
|
|
16
16
|
|
|
17
|
+
let start = new Date(Date.now() - 24 * 60 * 60 * 10_000);
|
|
18
|
+
const next = () => {
|
|
19
|
+
start = new Date(start.getTime() + Math.random() * 10_000);
|
|
20
|
+
return start;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const users = Array.from({ length: 5 }, () => ({
|
|
24
|
+
authorName: faker.person.fullName(),
|
|
25
|
+
authorHue: faker.helpers.arrayElement(hues),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const createBlock = () => {
|
|
29
|
+
const author = faker.helpers.arrayElement(users);
|
|
30
|
+
return {
|
|
31
|
+
id: faker.string.uuid(),
|
|
32
|
+
...author,
|
|
33
|
+
segments: Array.from({ length: 1 + Math.floor(Math.random() * 2) }, () => ({
|
|
34
|
+
started: next(),
|
|
35
|
+
text: faker.lorem.paragraph(),
|
|
36
|
+
})),
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
17
40
|
const meta: Meta<typeof Transcript> = {
|
|
18
41
|
title: 'plugins/plugin-transcription/Transcript',
|
|
19
42
|
component: Transcript,
|
|
20
|
-
render: (args) =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
43
|
+
render: ({ blocks: initialBlocks = [], ...args }) => {
|
|
44
|
+
const [blocks, setBlocks] = useState(initialBlocks);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const i = setInterval(() => {
|
|
47
|
+
setBlocks((blocks) => [...blocks, createBlock()]);
|
|
48
|
+
}, 1_000);
|
|
49
|
+
|
|
50
|
+
return () => clearInterval(i);
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
return <Transcript {...args} blocks={blocks} />;
|
|
54
|
+
},
|
|
27
55
|
decorators: [
|
|
28
56
|
withTheme,
|
|
29
57
|
withLayout({
|
|
30
58
|
tooltips: true,
|
|
31
59
|
fullscreen: true,
|
|
32
|
-
classNames: 'justify-center',
|
|
33
60
|
}),
|
|
34
61
|
],
|
|
35
62
|
parameters: {
|
|
@@ -41,25 +68,17 @@ export default meta;
|
|
|
41
68
|
|
|
42
69
|
type Story = StoryObj<typeof Transcript>;
|
|
43
70
|
|
|
44
|
-
let start = new Date();
|
|
45
|
-
const next = () => {
|
|
46
|
-
start = new Date(start.getTime() - Math.random() * 30_000);
|
|
47
|
-
return start;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const names = Array.from({ length: 3 }, () => faker.person.fullName());
|
|
51
|
-
|
|
52
71
|
export const Default: Story = {
|
|
53
72
|
args: {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
segments: Array.from({ length: 1 + Math.floor(Math.random() * 3) }, () => ({
|
|
58
|
-
started: next(),
|
|
59
|
-
text: faker.lorem.paragraph(),
|
|
60
|
-
})),
|
|
61
|
-
})).reverse(),
|
|
73
|
+
ignoreAttention: true,
|
|
74
|
+
attendableId: 'story',
|
|
75
|
+
blocks: Array.from({ length: 10 }, createBlock),
|
|
62
76
|
},
|
|
63
77
|
};
|
|
64
78
|
|
|
65
|
-
export const Empty: Story = {
|
|
79
|
+
export const Empty: Story = {
|
|
80
|
+
args: {
|
|
81
|
+
ignoreAttention: true,
|
|
82
|
+
attendableId: 'story',
|
|
83
|
+
},
|
|
84
|
+
};
|
|
@@ -2,68 +2,224 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
5
|
+
import { intervalToDuration } from 'date-fns/intervalToDuration';
|
|
6
|
+
import { yieldOrContinue } from 'main-thread-scheduling';
|
|
7
|
+
import React, { type FC, useCallback, useEffect, useMemo, useRef, useState, type WheelEvent } from 'react';
|
|
8
|
+
import { type OnResizeCallback, useResizeDetector } from 'react-resize-detector';
|
|
7
9
|
|
|
8
|
-
import {
|
|
10
|
+
import { IconButton, useTranslation } from '@dxos/react-ui';
|
|
11
|
+
import { useAttention } from '@dxos/react-ui-attention';
|
|
12
|
+
import {
|
|
13
|
+
type DxGridCellValue,
|
|
14
|
+
type DxGridElement,
|
|
15
|
+
type DxGridAxisMeta,
|
|
16
|
+
type DxGridPlaneCells,
|
|
17
|
+
Grid,
|
|
18
|
+
toPlaneCellIndex,
|
|
19
|
+
} from '@dxos/react-ui-grid';
|
|
20
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
9
21
|
|
|
10
22
|
import { TRANSCRIPTION_PLUGIN } from '../../meta';
|
|
11
23
|
import { type TranscriptBlock } from '../../types';
|
|
12
24
|
|
|
13
|
-
// TODO(burdon): react-ui-list.
|
|
14
25
|
// TODO(burdon): Actions (e.g., mark, summarize, translate, label, delete).
|
|
15
26
|
|
|
27
|
+
const lineHeight = 20;
|
|
28
|
+
const cellSpacing = 8 + 2;
|
|
29
|
+
const timestampColumnWidth = 68;
|
|
30
|
+
|
|
31
|
+
const authorClasses = 'font-medium text-base leading-[20px]';
|
|
32
|
+
const timestampClasses = 'text-xs leading-[20px] text-description pie-0 tabular-nums';
|
|
33
|
+
const segmentTextClasses = 'text-sm whitespace-normal hyphens-auto';
|
|
34
|
+
const measureClasses = mx(
|
|
35
|
+
// NOTE(thure): The `inline-start` value must equal `timestampColumnWidth` plus grid’s gap (1px)
|
|
36
|
+
'absolute inline-start-[69px] inline-end-0 invisible z-[-1] border',
|
|
37
|
+
'pli-[--dx-grid-cell-padding-inline] plb-[--dx-grid-cell-padding-block] leading-[20px]',
|
|
38
|
+
segmentTextClasses,
|
|
39
|
+
);
|
|
40
|
+
|
|
16
41
|
export type TranscriptProps = {
|
|
17
42
|
blocks?: TranscriptBlock[];
|
|
43
|
+
attendableId?: string;
|
|
44
|
+
ignoreAttention?: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const rowDefault = {
|
|
48
|
+
grid: { size: lineHeight + cellSpacing },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type QueueRows = [number, number][];
|
|
52
|
+
|
|
53
|
+
const mapTranscriptQueue = (blocks?: TranscriptBlock[]): QueueRows => {
|
|
54
|
+
if (!blocks || !Array.isArray(blocks) || blocks.length === 0) {
|
|
55
|
+
return [];
|
|
56
|
+
} else {
|
|
57
|
+
return blocks.flatMap((block, blockIndex) => {
|
|
58
|
+
return [
|
|
59
|
+
[blockIndex, -1],
|
|
60
|
+
...block.segments.map((segment, segmentIndex) => {
|
|
61
|
+
return [blockIndex, segmentIndex] as [number, number];
|
|
62
|
+
}),
|
|
63
|
+
];
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const pad = (n = 0) => String(n).padStart(2, '0');
|
|
69
|
+
|
|
70
|
+
const formatTimestamp = (start: Date, end: Date) => {
|
|
71
|
+
const { hours, minutes, seconds } = intervalToDuration({ start, end });
|
|
72
|
+
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const measureRows = async (
|
|
76
|
+
host: HTMLDivElement,
|
|
77
|
+
blocks: TranscriptBlock[],
|
|
78
|
+
queueMap: QueueRows,
|
|
79
|
+
signal: AbortSignal,
|
|
80
|
+
) => {
|
|
81
|
+
const result: DxGridAxisMeta = { grid: {} };
|
|
82
|
+
for (let row = 0; row < queueMap.length; row++) {
|
|
83
|
+
await yieldOrContinue('smooth', signal);
|
|
84
|
+
const [blockIndex, segmentIndex] = queueMap[row];
|
|
85
|
+
if (segmentIndex < 0) {
|
|
86
|
+
result.grid[row] = { size: lineHeight + cellSpacing };
|
|
87
|
+
} else {
|
|
88
|
+
host.textContent = blocks[blockIndex]!.segments[segmentIndex]!.text;
|
|
89
|
+
result.grid[row] = { size: host.offsetHeight };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
18
93
|
};
|
|
19
94
|
|
|
20
|
-
export const Transcript: FC<TranscriptProps> = ({ blocks }) => {
|
|
95
|
+
export const Transcript: FC<TranscriptProps> = ({ blocks, attendableId, ignoreAttention }) => {
|
|
21
96
|
const { t } = useTranslation(TRANSCRIPTION_PLUGIN);
|
|
22
|
-
const
|
|
97
|
+
const { hasAttention } = useAttention(attendableId);
|
|
98
|
+
const [dxGrid, setDxGrid] = useState<DxGridElement | null>(null);
|
|
99
|
+
const [rows, setRows] = useState<DxGridAxisMeta | undefined>(undefined);
|
|
100
|
+
const [columns, setColumns] = useState<DxGridAxisMeta | undefined>(undefined);
|
|
101
|
+
const [autoScroll, setAutoScroll] = useState(true);
|
|
102
|
+
|
|
103
|
+
const queueMap = useMemo(() => mapTranscriptQueue(blocks), [blocks]);
|
|
104
|
+
|
|
105
|
+
const handleWheel = useCallback(
|
|
106
|
+
(event: WheelEvent) => {
|
|
107
|
+
setAutoScroll(false);
|
|
108
|
+
if (!ignoreAttention && !hasAttention) {
|
|
109
|
+
event.stopPropagation();
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
[hasAttention, ignoreAttention],
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const abortControllerRef = useRef<AbortController>();
|
|
116
|
+
|
|
117
|
+
const handleResize = useCallback(
|
|
118
|
+
async ({ entry }: { entry: { target: HTMLDivElement } | null }) => {
|
|
119
|
+
if (entry?.target && Array.isArray(blocks) && blocks[0] && queueMap) {
|
|
120
|
+
abortControllerRef.current?.abort();
|
|
121
|
+
abortControllerRef.current = new AbortController();
|
|
122
|
+
setColumns({ grid: { 0: { size: timestampColumnWidth }, 1: { size: entry.target.offsetWidth } } });
|
|
123
|
+
return measureRows(entry.target, blocks, queueMap, abortControllerRef.current.signal)
|
|
124
|
+
.then(setRows)
|
|
125
|
+
.catch(() => {
|
|
126
|
+
// Aborted mid-measurement by new size.
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
[blocks, queueMap],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const { width, ref: measureRef } = useResizeDetector({
|
|
134
|
+
onResize: handleResize as OnResizeCallback,
|
|
135
|
+
refreshOptions: { leading: true },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (queueMap.length !== Object.keys(rows?.grid ?? {}).length) {
|
|
140
|
+
void handleResize({ entry: { target: measureRef.current } }).then(() => {
|
|
141
|
+
if (autoScroll) {
|
|
142
|
+
// TODO(thure): Implement a deterministic way to do this when `rows` has fully settled and grid has a new `maxPosBlock`.
|
|
143
|
+
setTimeout(() => dxGrid?.scrollToEndRow(), 50);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}, [blocks, queueMap, width, autoScroll]);
|
|
148
|
+
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (dxGrid && Array.isArray(blocks) && blocks[0]) {
|
|
151
|
+
const transcriptStart = blocks[0]!.segments[0]!.started;
|
|
152
|
+
dxGrid.getCells = (range, plane) => {
|
|
153
|
+
switch (plane) {
|
|
154
|
+
case 'grid': {
|
|
155
|
+
const cells: DxGridPlaneCells = {};
|
|
156
|
+
for (let row = range.start.row; row <= range.end.row && row < queueMap.length; row++) {
|
|
157
|
+
const [blockIndex, segmentIndex] = queueMap[row];
|
|
158
|
+
|
|
159
|
+
cells[toPlaneCellIndex({ col: 0, row })] = {
|
|
160
|
+
readonly: true,
|
|
161
|
+
value:
|
|
162
|
+
segmentIndex < 0
|
|
163
|
+
? formatTimestamp(transcriptStart, blocks[blockIndex]!.segments[Math.max(0, segmentIndex)]!.started)
|
|
164
|
+
: '',
|
|
165
|
+
className: timestampClasses,
|
|
166
|
+
} satisfies DxGridCellValue;
|
|
167
|
+
|
|
168
|
+
cells[toPlaneCellIndex({ col: 1, row })] = (
|
|
169
|
+
segmentIndex < 0
|
|
170
|
+
? {
|
|
171
|
+
readonly: true,
|
|
172
|
+
accessoryHtml: `<span data-hue="${blocks[blockIndex]!.authorHue}" class="dx-text-hue">${blocks[blockIndex]!.authorName}</span>`,
|
|
173
|
+
className: authorClasses,
|
|
174
|
+
}
|
|
175
|
+
: {
|
|
176
|
+
readonly: true,
|
|
177
|
+
value: blocks[blockIndex]!.segments[segmentIndex]!.text,
|
|
178
|
+
className: segmentTextClasses,
|
|
179
|
+
}
|
|
180
|
+
) satisfies DxGridCellValue;
|
|
181
|
+
}
|
|
182
|
+
return cells;
|
|
183
|
+
}
|
|
184
|
+
default:
|
|
185
|
+
return {};
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}, [dxGrid, blocks]);
|
|
190
|
+
|
|
191
|
+
const handleScrollToEnd = useCallback(() => {
|
|
192
|
+
setAutoScroll(true);
|
|
193
|
+
dxGrid?.scrollToEndRow();
|
|
194
|
+
}, [dxGrid, autoScroll]);
|
|
23
195
|
|
|
24
196
|
return (
|
|
25
|
-
|
|
26
|
-
{
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
{formatDistanceToNow(segment.started, { addSuffix: true })}
|
|
53
|
-
</div>
|
|
54
|
-
<IconButton
|
|
55
|
-
disabled
|
|
56
|
-
icon='ph--bookmark-simple--regular'
|
|
57
|
-
label={t('bookmark button')}
|
|
58
|
-
iconOnly
|
|
59
|
-
size={4}
|
|
60
|
-
classNames={hoverButton}
|
|
61
|
-
/>
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|
|
64
|
-
))}
|
|
65
|
-
</div>
|
|
66
|
-
))}
|
|
67
|
-
</div>
|
|
197
|
+
<>
|
|
198
|
+
<Grid.Root id={`${attendableId}--transcript`}>
|
|
199
|
+
<Grid.Content
|
|
200
|
+
limitColumns={2}
|
|
201
|
+
limitRows={queueMap.length}
|
|
202
|
+
columns={columns}
|
|
203
|
+
rows={rows}
|
|
204
|
+
rowDefault={rowDefault}
|
|
205
|
+
onWheel={handleWheel}
|
|
206
|
+
className='[--dx-grid-base:var(--dx-baseSurface)] [--dx-grid-lines:var(--dx-baseSurface)] [&_.dx-grid]:min-bs-0 [&_.dx-grid]:min-is-0 [&_.dx-grid]:select-auto'
|
|
207
|
+
ref={setDxGrid}
|
|
208
|
+
/>
|
|
209
|
+
</Grid.Root>
|
|
210
|
+
<div role='none' {...{ inert: '' }} aria-hidden className={measureClasses} ref={measureRef} />
|
|
211
|
+
<IconButton
|
|
212
|
+
icon='ph--arrow-line-down--regular'
|
|
213
|
+
iconOnly
|
|
214
|
+
label={t('scroll to end label')}
|
|
215
|
+
tooltipSide='left'
|
|
216
|
+
data-state={autoScroll ? 'invisible' : 'visible'}
|
|
217
|
+
classNames={[
|
|
218
|
+
'absolute inline-end-2 block-end-2 opacity-0 pointer-events-none',
|
|
219
|
+
'data-[state="visible"]:pointer-events-auto data-[state="visible"]:opacity-100 transition-opacity',
|
|
220
|
+
]}
|
|
221
|
+
onClick={handleScrollToEnd}
|
|
222
|
+
/>
|
|
223
|
+
</>
|
|
68
224
|
);
|
|
69
225
|
};
|
|
@@ -2,166 +2,31 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import React, { useCallback, useMemo, useState, type FC } from 'react';
|
|
5
|
+
import React, { type FC, Fragment } from 'react';
|
|
7
6
|
|
|
8
|
-
import {
|
|
9
|
-
import { Message } from '@dxos/artifact';
|
|
10
|
-
import { AIServiceClientImpl, DEFAULT_LLM_MODEL, MixedStreamParser, type AIServiceClient } from '@dxos/assistant';
|
|
11
|
-
import { create, getSpace, makeRef } from '@dxos/client/echo';
|
|
12
|
-
import { QueueImpl } from '@dxos/echo-db';
|
|
13
|
-
import { createStatic, isInstanceOf } from '@dxos/echo-schema';
|
|
14
|
-
import { invariant } from '@dxos/invariant';
|
|
7
|
+
import { fullyQualifiedId } from '@dxos/client/echo';
|
|
15
8
|
import { DXN } from '@dxos/keys';
|
|
16
|
-
import {
|
|
17
|
-
import { DocumentType } from '@dxos/plugin-markdown/types';
|
|
18
|
-
import { CollectionType, SpaceAction } from '@dxos/plugin-space/types';
|
|
19
|
-
import { useConfig } from '@dxos/react-client';
|
|
20
|
-
import { useEdgeClient, useQueue, type EdgeHttpClient } from '@dxos/react-edge-client';
|
|
21
|
-
import { IconButton, Toolbar, useTranslation } from '@dxos/react-ui';
|
|
22
|
-
import { ScrollContainer } from '@dxos/react-ui-components';
|
|
9
|
+
import { useQueue } from '@dxos/react-client/echo';
|
|
23
10
|
import { StackItem } from '@dxos/react-ui-stack';
|
|
24
|
-
import { TextType } from '@dxos/schema';
|
|
25
11
|
|
|
26
12
|
import { Transcript } from './Transcript';
|
|
27
|
-
import {
|
|
28
|
-
import { TranscriptBlock, type TranscriptType } from '../types';
|
|
13
|
+
import { type TranscriptBlock, type TranscriptType } from '../types';
|
|
29
14
|
|
|
30
|
-
export const TranscriptionContainer: FC<{ transcript: TranscriptType }> = ({ transcript }) => {
|
|
31
|
-
const
|
|
32
|
-
const edge = useEdgeClient();
|
|
33
|
-
const aiService = useAiServiceClient();
|
|
34
|
-
const { dispatchPromise: dispatch } = useIntentDispatcher();
|
|
15
|
+
export const TranscriptionContainer: FC<{ transcript: TranscriptType; role: string }> = ({ transcript, role }) => {
|
|
16
|
+
const attendableId = fullyQualifiedId(transcript);
|
|
35
17
|
|
|
36
|
-
const queue = useQueue<TranscriptBlock>(
|
|
18
|
+
const queue = useQueue<TranscriptBlock>(transcript.queue ? DXN.parse(transcript.queue) : undefined, {
|
|
37
19
|
pollInterval: 1_000,
|
|
38
20
|
});
|
|
39
21
|
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
const handleSummarize = useCallback(async () => {
|
|
43
|
-
setIsSummarizing(true);
|
|
44
|
-
try {
|
|
45
|
-
const document = await summarizeTranscript(edge, aiService, transcript);
|
|
46
|
-
const space = getSpace(transcript);
|
|
47
|
-
const target = space?.properties[CollectionType.typename]?.target;
|
|
48
|
-
await dispatch(
|
|
49
|
-
pipe(
|
|
50
|
-
createIntent(SpaceAction.AddObject, { object: document, target }),
|
|
51
|
-
chain(LayoutAction.Open, { part: 'main' }),
|
|
52
|
-
),
|
|
53
|
-
);
|
|
54
|
-
} finally {
|
|
55
|
-
setIsSummarizing(false);
|
|
56
|
-
}
|
|
57
|
-
}, [transcript, edge]);
|
|
22
|
+
const Root = role === 'article' ? StackItem.Content : Fragment;
|
|
23
|
+
const rootProps = role === 'article' ? { toolbar: false } : {};
|
|
58
24
|
|
|
59
|
-
// TODO(dmaretskyi): Move action to menu.
|
|
60
25
|
return (
|
|
61
|
-
<
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
<IconButton
|
|
65
|
-
icon='ph--subtitles--regular'
|
|
66
|
-
iconOnly
|
|
67
|
-
size={5}
|
|
68
|
-
disabled={isSummarizing}
|
|
69
|
-
onClick={handleSummarize}
|
|
70
|
-
label={t('summary button')}
|
|
71
|
-
/>
|
|
72
|
-
{isSummarizing && <div className='text-sm'>{t('summarizing label')}</div>}
|
|
73
|
-
</Toolbar.Root>
|
|
74
|
-
</StackItem.Heading>
|
|
75
|
-
<ScrollContainer>
|
|
76
|
-
<Transcript blocks={queue?.items} />
|
|
77
|
-
</ScrollContainer>
|
|
78
|
-
</StackItem.Content>
|
|
26
|
+
<Root {...(rootProps as any)}>
|
|
27
|
+
<Transcript blocks={queue?.items} attendableId={attendableId} />
|
|
28
|
+
</Root>
|
|
79
29
|
);
|
|
80
30
|
};
|
|
81
31
|
|
|
82
32
|
export default TranscriptionContainer;
|
|
83
|
-
|
|
84
|
-
// TODO(dmaretskyi): Extract to the new file once transcript refactoring has settled.
|
|
85
|
-
const summarizeTranscript = async (
|
|
86
|
-
edgeClient: EdgeHttpClient,
|
|
87
|
-
aiService: AIServiceClient,
|
|
88
|
-
transcript: TranscriptType,
|
|
89
|
-
) => {
|
|
90
|
-
invariant(transcript.queue, 'No queue found for transcript');
|
|
91
|
-
|
|
92
|
-
const queue = new QueueImpl(edgeClient, DXN.parse(transcript.queue));
|
|
93
|
-
await queue.refresh();
|
|
94
|
-
const content = queue.items
|
|
95
|
-
.filter((block) => isInstanceOf(TranscriptBlock, block))
|
|
96
|
-
.map((block) => `${block.author}: ${block.segments.map((segment) => segment.text).join('\n')}`)
|
|
97
|
-
.join('\n\n');
|
|
98
|
-
|
|
99
|
-
const parser = new MixedStreamParser();
|
|
100
|
-
|
|
101
|
-
log.info('summarizing transcript', { blockCount: queue.items.length });
|
|
102
|
-
const output = await parser.parse(
|
|
103
|
-
await aiService.exec({
|
|
104
|
-
model: DEFAULT_LLM_MODEL,
|
|
105
|
-
systemPrompt: SUMMARIZE_PROMPT,
|
|
106
|
-
history: [createStatic(Message, { role: 'user', content: [{ type: 'text', text: content }] })],
|
|
107
|
-
}),
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
log.info('transcript summary', { output });
|
|
111
|
-
invariant(output[0].content[0].type === 'text', 'Expected text content');
|
|
112
|
-
const summary = output[0].content[0].text;
|
|
113
|
-
|
|
114
|
-
// TODO(dmaretskyi): .started is missing.
|
|
115
|
-
const name = `Summary ${(transcript.started ?? new Date()).toLocaleString('en-US', {
|
|
116
|
-
year: 'numeric',
|
|
117
|
-
month: 'long',
|
|
118
|
-
day: 'numeric',
|
|
119
|
-
hour: '2-digit',
|
|
120
|
-
minute: '2-digit',
|
|
121
|
-
})}`;
|
|
122
|
-
|
|
123
|
-
return create(DocumentType, {
|
|
124
|
-
name,
|
|
125
|
-
content: makeRef(create(TextType, { content: summary })),
|
|
126
|
-
threads: [],
|
|
127
|
-
});
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
// TODO(dmaretskyi): Add example to set consistent structure for the summary.
|
|
131
|
-
const SUMMARIZE_PROMPT = `
|
|
132
|
-
# Goal
|
|
133
|
-
Create a markdown summary of the transcript provided.
|
|
134
|
-
|
|
135
|
-
# Formatting
|
|
136
|
-
- Format the summary as a markdown document without extra comments like "Here is the summary of the transcript:".
|
|
137
|
-
- Use markdown formatting for headings and bullet points.
|
|
138
|
-
- Format the summary as a list of key points and takeaways.
|
|
139
|
-
- All names of people should be in bold.
|
|
140
|
-
|
|
141
|
-
# Note Taking
|
|
142
|
-
- Correlate items in the summary with the person of origin to build a coherent narrative.
|
|
143
|
-
- Include short quotes verbatim where appropriate. Especially when concerned with design decisions and problem descriptions.
|
|
144
|
-
|
|
145
|
-
# Tasks
|
|
146
|
-
At the end of the summary include tasks.
|
|
147
|
-
Extract only the tasks that are:
|
|
148
|
-
- Directly actionable
|
|
149
|
-
- Clearly assigned to a person or team (or can easily be inferred)
|
|
150
|
-
- Strongly implied by the conversation and/or user note (no speculative tasks)
|
|
151
|
-
- Specific enough that someone reading them would know exactly what to do next
|
|
152
|
-
|
|
153
|
-
Format all tasks as markdown checkboxes using the syntax:
|
|
154
|
-
- [ ] Task description
|
|
155
|
-
|
|
156
|
-
Additional information can be included (indented).
|
|
157
|
-
|
|
158
|
-
If no actionable tasks are found, omit this tasks section.
|
|
159
|
-
`;
|
|
160
|
-
|
|
161
|
-
// TODO(dmaretskyi): Extract?
|
|
162
|
-
// Also this conflicts with plugin automation providing the ai client via a capability. Need to reconcile.
|
|
163
|
-
const useAiServiceClient = (): AIServiceClient => {
|
|
164
|
-
const config = useConfig();
|
|
165
|
-
const endpoint = config.values.runtime?.services?.ai?.server ?? 'http://localhost:8788'; // TOOD(burdon): Standardize consts.
|
|
166
|
-
return useMemo(() => new AIServiceClientImpl({ endpoint }), [endpoint]);
|
|
167
|
-
};
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import '@dxos-theme';
|
|
6
6
|
|
|
7
|
-
import { type
|
|
7
|
+
import { type Meta, type StoryObj } from '@storybook/react';
|
|
8
8
|
import React, {
|
|
9
9
|
type Dispatch,
|
|
10
10
|
type FC,
|
|
@@ -20,19 +20,19 @@ import { createStatic } from '@dxos/echo-schema';
|
|
|
20
20
|
import { type DXN } from '@dxos/keys';
|
|
21
21
|
import { log } from '@dxos/log';
|
|
22
22
|
import { Config } from '@dxos/react-client';
|
|
23
|
+
import { useQueue } from '@dxos/react-client/echo';
|
|
23
24
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
24
|
-
import { useEdgeClient, useQueue } from '@dxos/react-edge-client';
|
|
25
25
|
import { IconButton, Toolbar } from '@dxos/react-ui';
|
|
26
26
|
import { ScrollContainer } from '@dxos/react-ui-components';
|
|
27
|
-
import {
|
|
27
|
+
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
28
28
|
|
|
29
29
|
import { Transcript } from './Transcript';
|
|
30
|
-
import {
|
|
30
|
+
import { useAudioFile, useAudioTrack, useTranscriber } from '../hooks';
|
|
31
31
|
import { type TranscriberParams } from '../transcriber';
|
|
32
32
|
import { TranscriptBlock } from '../types';
|
|
33
33
|
import { randomQueueDxn } from '../util';
|
|
34
34
|
|
|
35
|
-
const
|
|
35
|
+
const TranscriptionStory: FC<{
|
|
36
36
|
playing: boolean;
|
|
37
37
|
setPlaying: Dispatch<SetStateAction<boolean>>;
|
|
38
38
|
blocks?: TranscriptBlock[];
|
|
@@ -48,7 +48,7 @@ const UX: FC<{
|
|
|
48
48
|
/>
|
|
49
49
|
</Toolbar.Root>
|
|
50
50
|
<ScrollContainer>
|
|
51
|
-
<Transcript blocks={blocks} />
|
|
51
|
+
<Transcript blocks={blocks} attendableId='story' />
|
|
52
52
|
</ScrollContainer>
|
|
53
53
|
</div>
|
|
54
54
|
);
|
|
@@ -62,8 +62,7 @@ const Microphone = () => {
|
|
|
62
62
|
|
|
63
63
|
// Queue.
|
|
64
64
|
const queueDxn = useMemo(() => randomQueueDxn(), []);
|
|
65
|
-
const
|
|
66
|
-
const queue = useQueue<TranscriptBlock>(echoClient, queueDxn, { pollInterval: 500 });
|
|
65
|
+
const queue = useQueue<TranscriptBlock>(queueDxn, { pollInterval: 500 });
|
|
67
66
|
|
|
68
67
|
// Transcriber.
|
|
69
68
|
const handleSegments = useCallback<TranscriberParams['onSegments']>(
|
|
@@ -90,7 +89,7 @@ const Microphone = () => {
|
|
|
90
89
|
}
|
|
91
90
|
}, [transcriber, playing, transcriber?.isOpen]);
|
|
92
91
|
|
|
93
|
-
return <
|
|
92
|
+
return <TranscriptionStory playing={playing} setPlaying={setPlaying} blocks={queue?.items} />;
|
|
94
93
|
};
|
|
95
94
|
|
|
96
95
|
const AudioFile = ({ queueDxn, audioUrl }: { queueDxn: DXN; audioUrl: string; transcriptUrl: string }) => {
|
|
@@ -119,11 +118,10 @@ const AudioFile = ({ queueDxn, audioUrl }: { queueDxn: DXN; audioUrl: string; tr
|
|
|
119
118
|
}, [audio, playing]);
|
|
120
119
|
|
|
121
120
|
// Transcriber.
|
|
122
|
-
const
|
|
123
|
-
const queue = useQueue<TranscriptBlock>(echoClient, queueDxn, { pollInterval: 500 });
|
|
121
|
+
const queue = useQueue<TranscriptBlock>(queueDxn, { pollInterval: 500 });
|
|
124
122
|
const handleSegments = useCallback<TranscriberParams['onSegments']>(
|
|
125
123
|
async (segments) => {
|
|
126
|
-
const block = createStatic(TranscriptBlock, {
|
|
124
|
+
const block = createStatic(TranscriptBlock, { authorName: 'test', authorHue: 'cyan', segments });
|
|
127
125
|
queue?.append([block]);
|
|
128
126
|
},
|
|
129
127
|
[queue],
|
|
@@ -153,7 +151,7 @@ const AudioFile = ({ queueDxn, audioUrl }: { queueDxn: DXN; audioUrl: string; tr
|
|
|
153
151
|
}
|
|
154
152
|
}, [transcriber, track?.readyState, transcriber?.isOpen]);
|
|
155
153
|
|
|
156
|
-
return <
|
|
154
|
+
return <TranscriptionStory playing={playing} setPlaying={setPlaying} blocks={queue?.items} />;
|
|
157
155
|
};
|
|
158
156
|
|
|
159
157
|
const meta: Meta<typeof AudioFile> = {
|
package/src/hooks/index.ts
CHANGED
package/src/index.ts
CHANGED
package/src/meta.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { type PluginMeta } from '@dxos/app-framework';
|
|
|
6
6
|
|
|
7
7
|
export const TRANSCRIPTION_PLUGIN = 'dxos.org/plugin/transcription';
|
|
8
8
|
|
|
9
|
-
export const meta = {
|
|
9
|
+
export const meta: PluginMeta = {
|
|
10
10
|
id: TRANSCRIPTION_PLUGIN,
|
|
11
11
|
name: 'Transcription',
|
|
12
12
|
description: 'Voice transcription.',
|
|
@@ -14,4 +14,4 @@ export const meta = {
|
|
|
14
14
|
source: 'https://github.com/dxos/dxos/tree/main/packages/plugins/experimental/plugin-transcription',
|
|
15
15
|
tags: ['experimental'],
|
|
16
16
|
screenshots: [],
|
|
17
|
-
}
|
|
17
|
+
};
|