@dxos/plugin-transcription 0.8.2-staging.7ac8446 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/dist/lib/browser/TranscriptContainer-PFKLKRPQ.mjs +11 -0
  2. package/dist/lib/browser/{chunk-YCZISX63.mjs → chunk-4KCUYW2F.mjs} +139 -95
  3. package/dist/lib/browser/chunk-4KCUYW2F.mjs.map +7 -0
  4. package/dist/lib/browser/{chunk-6ZR5MDK3.mjs → chunk-GOF7MJNV.mjs} +3 -3
  5. package/dist/lib/browser/{chunk-6ZR5MDK3.mjs.map → chunk-GOF7MJNV.mjs.map} +1 -1
  6. package/dist/lib/browser/{chunk-K7FOYAXO.mjs → chunk-IGEKO7ZU.mjs} +2 -2
  7. package/dist/lib/browser/chunk-NODIXNQP.mjs +54 -0
  8. package/dist/lib/browser/chunk-NODIXNQP.mjs.map +7 -0
  9. package/dist/lib/browser/chunk-TJPJBE45.mjs +764 -0
  10. package/dist/lib/browser/chunk-TJPJBE45.mjs.map +7 -0
  11. package/dist/lib/browser/{chunk-YEPT334A.mjs → chunk-W2KNYJD6.mjs} +4 -4
  12. package/dist/lib/browser/chunk-W2KNYJD6.mjs.map +7 -0
  13. package/dist/lib/browser/index.mjs +30 -216
  14. package/dist/lib/browser/index.mjs.map +4 -4
  15. package/dist/lib/browser/{intent-resolver-4R66ZCKT.mjs → intent-resolver-I2LJSQDR.mjs} +7 -11
  16. package/dist/lib/browser/intent-resolver-I2LJSQDR.mjs.map +7 -0
  17. package/dist/lib/browser/meta.json +1 -1
  18. package/dist/lib/browser/{react-surface-FQUHU5G7.mjs → react-surface-ID2MLJHZ.mjs} +11 -9
  19. package/dist/lib/browser/react-surface-ID2MLJHZ.mjs.map +7 -0
  20. package/dist/lib/browser/transcriber-NA7CIFIT.mjs +56 -0
  21. package/dist/lib/browser/transcriber-NA7CIFIT.mjs.map +7 -0
  22. package/dist/lib/browser/types/index.mjs +4 -8
  23. package/dist/lib/node/{TranscriptContainer-QEYTTT2V.cjs → TranscriptContainer-LHMC7553.cjs} +9 -8
  24. package/dist/lib/node/TranscriptContainer-LHMC7553.cjs.map +7 -0
  25. package/dist/lib/node/chunk-3GS5UNO5.cjs +778 -0
  26. package/dist/lib/node/chunk-3GS5UNO5.cjs.map +7 -0
  27. package/dist/lib/node/{chunk-LP4FG423.cjs → chunk-5J3FBCON.cjs} +5 -5
  28. package/dist/lib/node/{chunk-NVZWGFJL.cjs → chunk-AFUZI63A.cjs} +6 -6
  29. package/dist/lib/node/{chunk-NVZWGFJL.cjs.map → chunk-AFUZI63A.cjs.map} +1 -1
  30. package/dist/lib/node/chunk-EIMUQYCZ.cjs +74 -0
  31. package/dist/lib/node/chunk-EIMUQYCZ.cjs.map +7 -0
  32. package/dist/lib/node/{chunk-CZMLKKWT.cjs → chunk-FKDJNJ57.cjs} +8 -8
  33. package/dist/lib/node/chunk-FKDJNJ57.cjs.map +7 -0
  34. package/dist/lib/node/{chunk-FCCJALOT.cjs → chunk-ZFTXJZXH.cjs} +138 -95
  35. package/dist/lib/node/chunk-ZFTXJZXH.cjs.map +7 -0
  36. package/dist/lib/node/index.cjs +59 -251
  37. package/dist/lib/node/index.cjs.map +4 -4
  38. package/dist/lib/node/{intent-resolver-WMMCOUEH.cjs → intent-resolver-LLMGF5I5.cjs} +10 -11
  39. package/dist/lib/node/intent-resolver-LLMGF5I5.cjs.map +7 -0
  40. package/dist/lib/node/meta.json +1 -1
  41. package/dist/lib/node/{react-surface-WI2MGWCC.cjs → react-surface-SEQLEL4K.cjs} +13 -11
  42. package/dist/lib/node/react-surface-SEQLEL4K.cjs.map +7 -0
  43. package/dist/lib/node/transcriber-RKKN7LDU.cjs +68 -0
  44. package/dist/lib/node/transcriber-RKKN7LDU.cjs.map +7 -0
  45. package/dist/lib/node/types/index.cjs +7 -11
  46. package/dist/lib/node/types/index.cjs.map +2 -2
  47. package/dist/lib/node-esm/{TranscriptContainer-ZXUCOWGY.mjs → TranscriptContainer-DLNRFVTA.mjs} +4 -3
  48. package/dist/lib/node-esm/chunk-BRPFVRAD.mjs +55 -0
  49. package/dist/lib/node-esm/chunk-BRPFVRAD.mjs.map +7 -0
  50. package/dist/lib/node-esm/{chunk-YQYBLFNI.mjs → chunk-EFIWBQDF.mjs} +139 -95
  51. package/dist/lib/node-esm/chunk-EFIWBQDF.mjs.map +7 -0
  52. package/dist/lib/node-esm/{chunk-J4D2RBBH.mjs → chunk-KUWWHMOA.mjs} +2 -2
  53. package/dist/lib/node-esm/{chunk-PTZHDZ36.mjs → chunk-MFJPEIVB.mjs} +4 -4
  54. package/dist/lib/node-esm/chunk-MFJPEIVB.mjs.map +7 -0
  55. package/dist/lib/node-esm/chunk-PR7EYODX.mjs +765 -0
  56. package/dist/lib/node-esm/chunk-PR7EYODX.mjs.map +7 -0
  57. package/dist/lib/node-esm/{chunk-X4DDJIVL.mjs → chunk-TXMDLLBR.mjs} +3 -3
  58. package/dist/lib/node-esm/{chunk-X4DDJIVL.mjs.map → chunk-TXMDLLBR.mjs.map} +1 -1
  59. package/dist/lib/node-esm/index.mjs +30 -216
  60. package/dist/lib/node-esm/index.mjs.map +4 -4
  61. package/dist/lib/node-esm/{intent-resolver-QN2LO5TX.mjs → intent-resolver-WOZKOEBW.mjs} +7 -11
  62. package/dist/lib/node-esm/intent-resolver-WOZKOEBW.mjs.map +7 -0
  63. package/dist/lib/node-esm/meta.json +1 -1
  64. package/dist/lib/node-esm/{react-surface-PCH6NTVB.mjs → react-surface-7MXUUTAL.mjs} +11 -9
  65. package/dist/lib/node-esm/react-surface-7MXUUTAL.mjs.map +7 -0
  66. package/dist/lib/node-esm/transcriber-5WSI6DYK.mjs +57 -0
  67. package/dist/lib/node-esm/transcriber-5WSI6DYK.mjs.map +7 -0
  68. package/dist/lib/node-esm/types/index.mjs +4 -8
  69. package/dist/types/src/TranscriptionPlugin.d.ts.map +1 -1
  70. package/dist/types/src/capabilities/capabilities.d.ts +8 -10
  71. package/dist/types/src/capabilities/capabilities.d.ts.map +1 -1
  72. package/dist/types/src/capabilities/index.d.ts +2 -183
  73. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  74. package/dist/types/src/capabilities/intent-resolver.d.ts +2 -2
  75. package/dist/types/src/capabilities/intent-resolver.d.ts.map +1 -1
  76. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  77. package/dist/types/src/capabilities/transcriber.d.ts +2 -1
  78. package/dist/types/src/capabilities/transcriber.d.ts.map +1 -1
  79. package/dist/types/src/components/Transcript/Transcript.d.ts +14 -4
  80. package/dist/types/src/components/Transcript/Transcript.d.ts.map +1 -1
  81. package/dist/types/src/components/Transcript/Transcript.stories.d.ts +19 -4
  82. package/dist/types/src/components/Transcript/Transcript.stories.d.ts.map +1 -1
  83. package/dist/types/src/components/Transcript/deprecated/OldTranscript.d.ts +27 -0
  84. package/dist/types/src/components/Transcript/deprecated/OldTranscript.d.ts.map +1 -0
  85. package/dist/types/src/components/Transcript/transcript-extension.d.ts +19 -0
  86. package/dist/types/src/components/Transcript/transcript-extension.d.ts.map +1 -0
  87. package/dist/types/src/components/TranscriptContainer.d.ts.map +1 -1
  88. package/dist/types/src/components/stories/FileTranscription.stories.d.ts +17 -0
  89. package/dist/types/src/components/stories/FileTranscription.stories.d.ts.map +1 -0
  90. package/dist/types/src/components/stories/KeyWordRecognition.stories.d.ts +10 -0
  91. package/dist/types/src/components/stories/KeyWordRecognition.stories.d.ts.map +1 -0
  92. package/dist/types/src/components/stories/MicrophoneTranscription.stories.d.ts +19 -0
  93. package/dist/types/src/components/stories/MicrophoneTranscription.stories.d.ts.map +1 -0
  94. package/dist/types/src/components/stories/TranscriptionStory.d.ts +11 -0
  95. package/dist/types/src/components/stories/TranscriptionStory.d.ts.map +1 -0
  96. package/dist/types/src/components/stories/useIsSpeaking.d.ts +3 -0
  97. package/dist/types/src/components/stories/useIsSpeaking.d.ts.map +1 -0
  98. package/dist/types/src/hooks/index.d.ts +1 -0
  99. package/dist/types/src/hooks/index.d.ts.map +1 -1
  100. package/dist/types/src/hooks/useAudioFile.d.ts +1 -1
  101. package/dist/types/src/hooks/useAudioFile.d.ts.map +1 -1
  102. package/dist/types/src/hooks/useAudioTrack.d.ts +1 -1
  103. package/dist/types/src/hooks/useAudioTrack.d.ts.map +1 -1
  104. package/dist/types/src/hooks/useQueueModelAdapter.d.ts +7 -0
  105. package/dist/types/src/hooks/useQueueModelAdapter.d.ts.map +1 -0
  106. package/dist/types/src/hooks/useTranscriber.d.ts +1 -1
  107. package/dist/types/src/hooks/useTranscriber.d.ts.map +1 -1
  108. package/dist/types/src/hooks/useVoiceInput.d.ts.map +1 -1
  109. package/dist/types/src/model/index.d.ts +2 -0
  110. package/dist/types/src/model/index.d.ts.map +1 -0
  111. package/dist/types/src/model/model.d.ts +68 -0
  112. package/dist/types/src/model/model.d.ts.map +1 -0
  113. package/dist/types/src/model/model.test.d.ts +2 -0
  114. package/dist/types/src/model/model.test.d.ts.map +1 -0
  115. package/dist/types/src/segments-normalization/index.d.ts +4 -0
  116. package/dist/types/src/segments-normalization/index.d.ts.map +1 -0
  117. package/dist/types/src/segments-normalization/message-normalizer.d.ts +26 -0
  118. package/dist/types/src/segments-normalization/message-normalizer.d.ts.map +1 -0
  119. package/dist/types/src/segments-normalization/normalization.d.ts +520 -0
  120. package/dist/types/src/segments-normalization/normalization.d.ts.map +1 -0
  121. package/dist/types/src/segments-normalization/normalization.test.d.ts +2 -0
  122. package/dist/types/src/segments-normalization/normalization.test.d.ts.map +1 -0
  123. package/dist/types/src/segments-normalization/utils.d.ts +3 -0
  124. package/dist/types/src/segments-normalization/utils.d.ts.map +1 -0
  125. package/dist/types/src/testing/index.d.ts +2 -0
  126. package/dist/types/src/testing/index.d.ts.map +1 -0
  127. package/dist/types/src/testing/testing.d.ts +40 -0
  128. package/dist/types/src/testing/testing.d.ts.map +1 -0
  129. package/dist/types/src/transcriber/media-stream-recorder.d.ts +8 -5
  130. package/dist/types/src/transcriber/media-stream-recorder.d.ts.map +1 -1
  131. package/dist/types/src/transcriber/transcriber.browser.test.d.ts +2 -0
  132. package/dist/types/src/transcriber/transcriber.browser.test.d.ts.map +1 -0
  133. package/dist/types/src/transcriber/transcriber.d.ts +6 -2
  134. package/dist/types/src/transcriber/transcriber.d.ts.map +1 -1
  135. package/dist/types/src/transcriber/transcription-manager.d.ts +19 -15
  136. package/dist/types/src/transcriber/transcription-manager.d.ts.map +1 -1
  137. package/dist/types/src/translations.d.ts +3 -20
  138. package/dist/types/src/translations.d.ts.map +1 -1
  139. package/dist/types/src/types/schema.d.ts +23 -40
  140. package/dist/types/src/types/schema.d.ts.map +1 -1
  141. package/dist/types/src/types/types.d.ts +15 -8
  142. package/dist/types/src/types/types.d.ts.map +1 -1
  143. package/dist/types/src/util/array.d.ts.map +1 -1
  144. package/dist/types/src/util/get-time-str.d.ts.map +1 -1
  145. package/dist/types/src/util/index.d.ts +0 -1
  146. package/dist/types/src/util/index.d.ts.map +1 -1
  147. package/dist/types/src/util/monitor-audio-level.d.ts.map +1 -1
  148. package/dist/types/tsconfig.tsbuildinfo +1 -1
  149. package/package.json +62 -41
  150. package/src/TranscriptionPlugin.tsx +13 -18
  151. package/src/capabilities/capabilities.ts +15 -8
  152. package/src/capabilities/index.ts +0 -2
  153. package/src/capabilities/intent-resolver.ts +7 -8
  154. package/src/capabilities/react-surface.tsx +5 -4
  155. package/src/capabilities/transcriber.ts +34 -6
  156. package/src/components/Transcript/Transcript.stories.tsx +201 -43
  157. package/src/components/Transcript/Transcript.tsx +83 -208
  158. package/src/components/Transcript/deprecated/OldTranscript.tsx +260 -0
  159. package/src/components/Transcript/transcript-extension.ts +236 -0
  160. package/src/components/TranscriptContainer.tsx +11 -6
  161. package/src/components/stories/FileTranscription.stories.tsx +240 -0
  162. package/src/components/stories/KeyWordRecognition.stories.tsx +128 -0
  163. package/src/components/stories/MicrophoneTranscription.stories.tsx +274 -0
  164. package/src/components/stories/TranscriptionStory.tsx +41 -0
  165. package/src/components/stories/useIsSpeaking.tsx +40 -0
  166. package/src/hooks/index.ts +1 -0
  167. package/src/hooks/useAudioFile.ts +39 -9
  168. package/src/hooks/useAudioTrack.ts +2 -2
  169. package/src/hooks/useQueueModelAdapter.ts +65 -0
  170. package/src/hooks/useTranscriber.ts +4 -2
  171. package/src/meta.ts +2 -2
  172. package/src/model/index.ts +5 -0
  173. package/src/model/model.test.ts +169 -0
  174. package/src/model/model.ts +229 -0
  175. package/src/segments-normalization/index.ts +7 -0
  176. package/src/segments-normalization/message-normalizer.ts +108 -0
  177. package/src/segments-normalization/normalization.test.ts +142 -0
  178. package/src/segments-normalization/normalization.ts +103 -0
  179. package/src/segments-normalization/utils.ts +9 -0
  180. package/src/testing/index.ts +5 -0
  181. package/src/testing/testing.ts +216 -0
  182. package/src/transcriber/media-stream-recorder.ts +14 -7
  183. package/src/transcriber/{transcriber.test.ts → transcriber.browser.test.ts} +7 -7
  184. package/src/transcriber/transcriber.ts +33 -20
  185. package/src/transcriber/transcription-manager.ts +87 -71
  186. package/src/translations.ts +2 -3
  187. package/src/types/schema.ts +11 -33
  188. package/src/types/types.ts +8 -12
  189. package/src/util/index.ts +0 -1
  190. package/dist/lib/browser/TranscriptContainer-JQ3HFHLQ.mjs +0 -10
  191. package/dist/lib/browser/app-graph-builder-VDQOVX7P.mjs +0 -191
  192. package/dist/lib/browser/app-graph-builder-VDQOVX7P.mjs.map +0 -7
  193. package/dist/lib/browser/chunk-G4B6CPC2.mjs +0 -253
  194. package/dist/lib/browser/chunk-G4B6CPC2.mjs.map +0 -7
  195. package/dist/lib/browser/chunk-GU2BNYKZ.mjs +0 -35
  196. package/dist/lib/browser/chunk-GU2BNYKZ.mjs.map +0 -7
  197. package/dist/lib/browser/chunk-OGNXNGJB.mjs +0 -73
  198. package/dist/lib/browser/chunk-OGNXNGJB.mjs.map +0 -7
  199. package/dist/lib/browser/chunk-YCZISX63.mjs.map +0 -7
  200. package/dist/lib/browser/chunk-YEPT334A.mjs.map +0 -7
  201. package/dist/lib/browser/intent-resolver-4R66ZCKT.mjs.map +0 -7
  202. package/dist/lib/browser/meeting-transcription-state-DLFHQK72.mjs +0 -18
  203. package/dist/lib/browser/meeting-transcription-state-DLFHQK72.mjs.map +0 -7
  204. package/dist/lib/browser/react-surface-FQUHU5G7.mjs.map +0 -7
  205. package/dist/lib/browser/transcriber-LGMIWDYI.mjs +0 -36
  206. package/dist/lib/browser/transcriber-LGMIWDYI.mjs.map +0 -7
  207. package/dist/lib/node/TranscriptContainer-QEYTTT2V.cjs.map +0 -7
  208. package/dist/lib/node/app-graph-builder-IFSIVNUR.cjs +0 -200
  209. package/dist/lib/node/app-graph-builder-IFSIVNUR.cjs.map +0 -7
  210. package/dist/lib/node/chunk-3KHUSQNW.cjs +0 -55
  211. package/dist/lib/node/chunk-3KHUSQNW.cjs.map +0 -7
  212. package/dist/lib/node/chunk-CZMLKKWT.cjs.map +0 -7
  213. package/dist/lib/node/chunk-FCCJALOT.cjs.map +0 -7
  214. package/dist/lib/node/chunk-P4VVD6ZO.cjs +0 -95
  215. package/dist/lib/node/chunk-P4VVD6ZO.cjs.map +0 -7
  216. package/dist/lib/node/chunk-UOFHT4GZ.cjs +0 -280
  217. package/dist/lib/node/chunk-UOFHT4GZ.cjs.map +0 -7
  218. package/dist/lib/node/intent-resolver-WMMCOUEH.cjs.map +0 -7
  219. package/dist/lib/node/meeting-transcription-state-URIYILEQ.cjs +0 -34
  220. package/dist/lib/node/meeting-transcription-state-URIYILEQ.cjs.map +0 -7
  221. package/dist/lib/node/react-surface-WI2MGWCC.cjs.map +0 -7
  222. package/dist/lib/node/transcriber-QG5WPKX4.cjs +0 -49
  223. package/dist/lib/node/transcriber-QG5WPKX4.cjs.map +0 -7
  224. package/dist/lib/node-esm/app-graph-builder-UL6XLIZY.mjs +0 -192
  225. package/dist/lib/node-esm/app-graph-builder-UL6XLIZY.mjs.map +0 -7
  226. package/dist/lib/node-esm/chunk-6X7X2UZF.mjs +0 -254
  227. package/dist/lib/node-esm/chunk-6X7X2UZF.mjs.map +0 -7
  228. package/dist/lib/node-esm/chunk-GIO6J6FE.mjs +0 -74
  229. package/dist/lib/node-esm/chunk-GIO6J6FE.mjs.map +0 -7
  230. package/dist/lib/node-esm/chunk-HKATMD34.mjs +0 -37
  231. package/dist/lib/node-esm/chunk-HKATMD34.mjs.map +0 -7
  232. package/dist/lib/node-esm/chunk-PTZHDZ36.mjs.map +0 -7
  233. package/dist/lib/node-esm/chunk-YQYBLFNI.mjs.map +0 -7
  234. package/dist/lib/node-esm/intent-resolver-QN2LO5TX.mjs.map +0 -7
  235. package/dist/lib/node-esm/meeting-transcription-state-ZYVRAW6Y.mjs +0 -19
  236. package/dist/lib/node-esm/meeting-transcription-state-ZYVRAW6Y.mjs.map +0 -7
  237. package/dist/lib/node-esm/react-surface-PCH6NTVB.mjs.map +0 -7
  238. package/dist/lib/node-esm/transcriber-TMPYZ5L4.mjs +0 -37
  239. package/dist/lib/node-esm/transcriber-TMPYZ5L4.mjs.map +0 -7
  240. package/dist/types/src/capabilities/app-graph-builder.d.ts +0 -181
  241. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +0 -1
  242. package/dist/types/src/capabilities/meeting-transcription-state.d.ts +0 -6
  243. package/dist/types/src/capabilities/meeting-transcription-state.d.ts.map +0 -1
  244. package/dist/types/src/components/transcription.stories.d.ts +0 -15
  245. package/dist/types/src/components/transcription.stories.d.ts.map +0 -1
  246. package/dist/types/src/transcriber/transcriber.test.d.ts +0 -2
  247. package/dist/types/src/transcriber/transcriber.test.d.ts.map +0 -1
  248. package/dist/types/src/util/random-queue-dxn.d.ts +0 -3
  249. package/dist/types/src/util/random-queue-dxn.d.ts.map +0 -1
  250. package/src/capabilities/app-graph-builder.ts +0 -152
  251. package/src/capabilities/meeting-transcription-state.ts +0 -16
  252. package/src/components/transcription.stories.tsx +0 -196
  253. package/src/util/random-queue-dxn.ts +0 -9
  254. /package/dist/lib/browser/{TranscriptContainer-JQ3HFHLQ.mjs.map → TranscriptContainer-PFKLKRPQ.mjs.map} +0 -0
  255. /package/dist/lib/browser/{chunk-K7FOYAXO.mjs.map → chunk-IGEKO7ZU.mjs.map} +0 -0
  256. /package/dist/lib/node/{chunk-LP4FG423.cjs.map → chunk-5J3FBCON.cjs.map} +0 -0
  257. /package/dist/lib/node-esm/{TranscriptContainer-ZXUCOWGY.mjs.map → TranscriptContainer-DLNRFVTA.mjs.map} +0 -0
  258. /package/dist/lib/node-esm/{chunk-J4D2RBBH.mjs.map → chunk-KUWWHMOA.mjs.map} +0 -0
@@ -0,0 +1,236 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Extension, RangeSetBuilder } from '@codemirror/state';
6
+ import { EditorView, GutterMarker, ViewPlugin, type ViewUpdate, gutter } from '@codemirror/view';
7
+ import { format } from 'date-fns/format';
8
+ import { intervalToDuration } from 'date-fns/intervalToDuration';
9
+
10
+ import { type CleanupFn, addEventListener, combine } from '@dxos/async';
11
+ import { type RenderCallback } from '@dxos/react-ui-editor';
12
+ import { type DataType } from '@dxos/schema';
13
+
14
+ import { DocumentAdapter, type SerializationModel } from '../../model';
15
+
16
+ /**
17
+ * Data structure that maps Chunks queue to lines with transcript state.
18
+ */
19
+ export type TranscriptOptions = {
20
+ model: SerializationModel<DataType.Message>;
21
+ started?: Date;
22
+ renderButton?: RenderCallback<{ onClick: () => void }>;
23
+ };
24
+
25
+ /**
26
+ * Scrolling transcript with timestamps.
27
+ */
28
+ export const transcript = ({ model, started, renderButton }: TranscriptOptions): Extension => {
29
+ return [
30
+ // Show timestamps in the gutter.
31
+ gutter({
32
+ class: 'cm-timestamp-gutter',
33
+ lineMarkerChange: (update) => update.docChanged || update.viewportChanged,
34
+ markers: (view) => {
35
+ const start = getStartTime(started, model.chunks[0]);
36
+ const builder = new RangeSetBuilder<GutterMarker>();
37
+ for (const { from, to } of view.visibleRanges) {
38
+ let line = view.state.doc.lineAt(from);
39
+ while (line.from <= to) {
40
+ const block = model.getChunkAtLine(line.number)?.blocks[0];
41
+ const timestamp = block?.type === 'transcription' && block.started;
42
+ if (timestamp) {
43
+ builder.add(line.from, line.from, new TimestampMarker(line.number, new Date(timestamp), start));
44
+ }
45
+
46
+ if (line.to + 1 > view.state.doc.length) {
47
+ break;
48
+ }
49
+
50
+ line = view.state.doc.lineAt(line.to + 1);
51
+ }
52
+ }
53
+
54
+ return builder.finish();
55
+ },
56
+ }),
57
+
58
+ // Listen for model updates.
59
+ ViewPlugin.fromClass(
60
+ class {
61
+ private readonly _controls?: HTMLDivElement;
62
+ private readonly _cleanup: CleanupFn;
63
+ private readonly _adapter: DocumentAdapter;
64
+ private _initialized = false;
65
+
66
+ constructor(view: EditorView) {
67
+ this._adapter = new DocumentAdapter(view);
68
+
69
+ const scroller = view.scrollDOM;
70
+ let isAutoScrolling = false;
71
+ let hasScrolled = false;
72
+
73
+ let timeout: NodeJS.Timeout | undefined;
74
+ const scrollToBottom = (smooth = false) => {
75
+ scroller.style.scrollBehavior = smooth ? 'smooth' : '';
76
+
77
+ // Temporarily hide scrollbar to prevent flicker.
78
+ scroller.classList.add('cm-hide-scrollbar');
79
+ isAutoScrolling = true;
80
+ clearTimeout(timeout);
81
+ timeout = setTimeout(() => {
82
+ this._controls?.classList.add('opacity-0');
83
+ scroller.classList.remove('cm-hide-scrollbar');
84
+ isAutoScrolling = false;
85
+ }, 500);
86
+
87
+ // Scroll to bottom.
88
+ view.dispatch({
89
+ effects: EditorView.scrollIntoView(view.state.doc.length, { y: 'end' }),
90
+ });
91
+ };
92
+
93
+ // Scroll button.
94
+ if (renderButton) {
95
+ this._controls = document.createElement('div');
96
+ this._controls.classList.add('cm-controls', 'transition-opacity', 'duration-300', 'opacity-0');
97
+ view.dom.appendChild(this._controls);
98
+ renderButton(
99
+ this._controls,
100
+ {
101
+ onClick: () => {
102
+ scrollToBottom(false);
103
+ },
104
+ },
105
+ view,
106
+ );
107
+ }
108
+
109
+ // Event listeners.
110
+ this._cleanup = combine(
111
+ addEventListener(view.scrollDOM, 'scroll', () => {
112
+ if (!isAutoScrolling) {
113
+ hasScrolled = true;
114
+ this._controls?.classList.remove('opacity-0');
115
+ }
116
+ }),
117
+
118
+ model.update.on(() => {
119
+ // Check if clamped to bottom.
120
+ const autoScroll =
121
+ scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight === 0 || !hasScrolled;
122
+
123
+ // Sync.
124
+ model.sync(this._adapter);
125
+
126
+ // Scroll.
127
+ if (autoScroll) {
128
+ scrollToBottom(true);
129
+ }
130
+ }),
131
+ );
132
+ }
133
+
134
+ update(update: ViewUpdate) {
135
+ // Initial sync.
136
+ if (!this._initialized) {
137
+ this._initialized = true;
138
+ setTimeout(() => {
139
+ model.sync(this._adapter);
140
+ });
141
+ }
142
+ }
143
+
144
+ destroy() {
145
+ this._controls?.remove();
146
+ this._cleanup();
147
+ }
148
+ },
149
+ ),
150
+
151
+ EditorView.theme({
152
+ '.cm-scroller': {
153
+ overflowY: 'scroll',
154
+ },
155
+ '.cm-hide-scrollbar': {
156
+ scrollbarWidth: 'none',
157
+ '-ms-overflow-style': 'none',
158
+ },
159
+ '.cm-hide-scrollbar::-webkit-scrollbar': {
160
+ display: 'none',
161
+ },
162
+ '.cm-controls': {
163
+ position: 'absolute',
164
+ bottom: '0.5rem',
165
+ right: '0.5rem',
166
+ zIndex: 1000,
167
+ },
168
+ '.cm-line': {
169
+ paddingRight: '1rem',
170
+ },
171
+ '.cm-timestamp-gutter': {
172
+ width: '6rem',
173
+ paddingRight: '1rem',
174
+ },
175
+ '.cm-timestamp-gutter > .cm-gutterElement > div': {
176
+ display: 'inline-flex',
177
+ textAlign: 'right',
178
+ padding: '3px',
179
+ },
180
+ }),
181
+ ];
182
+ };
183
+
184
+ /**
185
+ * Gutter marker that displays a timestamp.
186
+ */
187
+ class TimestampMarker extends GutterMarker {
188
+ constructor(
189
+ readonly _line: number,
190
+ readonly _timestamp: Date,
191
+ readonly _started?: Date,
192
+ ) {
193
+ super();
194
+ }
195
+
196
+ override eq(other: this): boolean {
197
+ return other._timestamp === this._timestamp;
198
+ }
199
+
200
+ override toDOM(view: EditorView): HTMLDivElement {
201
+ const el = document.createElement('div');
202
+ el.className = 'text-sm text-subdued hover:bg-hoverSurface cursor-pointer';
203
+ el.textContent = formatTimestamp(this._timestamp, this._started);
204
+ // TODO(burdon): Click to bookmark or copy hyperlink.
205
+ el.onclick = () => {
206
+ const pos = view.state.doc.line(this._line).from;
207
+ view.dispatch({
208
+ effects: EditorView.scrollIntoView(pos, { y: 'start' }),
209
+ });
210
+ };
211
+
212
+ return el;
213
+ }
214
+ }
215
+
216
+ const getStartTime = (started?: Date, message?: DataType.Message): Date | undefined => {
217
+ if (started) {
218
+ return started;
219
+ }
220
+
221
+ if (message?.blocks[0]?.type === 'transcription' && message.blocks[0].started) {
222
+ return new Date(message.blocks[0].started);
223
+ }
224
+
225
+ return undefined;
226
+ };
227
+
228
+ const formatTimestamp = (timestamp: Date, relative?: Date) => {
229
+ if (relative) {
230
+ const pad = (n = 0) => String(n).padStart(2, '0');
231
+ const { hours, minutes, seconds } = intervalToDuration({ start: relative, end: timestamp });
232
+ return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
233
+ } else {
234
+ return format(timestamp, 'HH:mm:ss');
235
+ }
236
+ };
@@ -4,20 +4,25 @@
4
4
 
5
5
  import React, { type FC } from 'react';
6
6
 
7
- import { fullyQualifiedId } from '@dxos/client/echo';
8
- import { useQueue } from '@dxos/react-client/echo';
7
+ import { fullyQualifiedId, getSpace } from '@dxos/client/echo';
8
+ import { useMembers, useQueue } from '@dxos/react-client/echo';
9
9
  import { StackItem } from '@dxos/react-ui-stack';
10
+ import { type DataType } from '@dxos/schema';
10
11
 
11
- import { Transcript } from './Transcript';
12
- import { type TranscriptBlock, type TranscriptType } from '../types';
12
+ import { Transcript, renderMarkdown } from './Transcript';
13
+ import { useQueueModelAdapter } from '../hooks';
14
+ import { type TranscriptType } from '../types';
13
15
 
14
16
  export const TranscriptionContainer: FC<{ role: string; transcript: TranscriptType }> = ({ role, transcript }) => {
15
17
  const attendableId = fullyQualifiedId(transcript);
16
- const queue = useQueue<TranscriptBlock>(transcript.queue.dxn, { pollInterval: 1_000 });
18
+ const space = getSpace(transcript);
19
+ const members = useMembers(space?.key).map((member) => member.identity);
20
+ const queue = useQueue<DataType.Message>(transcript.queue.dxn, { pollInterval: 1_000 });
21
+ const model = useQueueModelAdapter(renderMarkdown(members), queue);
17
22
 
18
23
  return (
19
24
  <StackItem.Content role={role} classNames='container-max-width pli-2'>
20
- <Transcript attendableId={attendableId} blocks={queue?.items} />
25
+ <Transcript attendableId={attendableId} space={space} model={model} transcript={transcript} />
21
26
  </StackItem.Content>
22
27
  );
23
28
  };
@@ -0,0 +1,240 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import '@dxos-theme';
6
+
7
+ import { type Meta, type StoryObj } from '@storybook/react';
8
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
9
+
10
+ import { AIServiceEdgeClient } from '@dxos/ai';
11
+ import { AI_SERVICE_ENDPOINT } from '@dxos/ai/testing';
12
+ import { Events, IntentPlugin, SettingsPlugin } from '@dxos/app-framework';
13
+ import { withPluginManager } from '@dxos/app-framework/testing';
14
+ import { scheduleTask } from '@dxos/async';
15
+ import { Context } from '@dxos/context';
16
+ import { MemoryQueue } from '@dxos/echo-db';
17
+ import { create, createQueueDxn } from '@dxos/echo-schema';
18
+ import { FunctionExecutor, ServiceContainer } from '@dxos/functions';
19
+ import { log } from '@dxos/log';
20
+ import { ClientPlugin } from '@dxos/plugin-client';
21
+ import { PreviewPlugin } from '@dxos/plugin-preview';
22
+ import { SpacePlugin } from '@dxos/plugin-space';
23
+ import { StorybookLayoutPlugin } from '@dxos/plugin-storybook-layout';
24
+ import { ThemePlugin } from '@dxos/plugin-theme';
25
+ import { defaultTx } from '@dxos/react-ui-theme';
26
+ import { DataType } from '@dxos/schema';
27
+ import { seedTestData, Testing } from '@dxos/schema/testing';
28
+ import { withLayout, withTheme } from '@dxos/storybook-utils';
29
+
30
+ import { TranscriptionStory } from './TranscriptionStory';
31
+ import { useIsSpeaking } from './useIsSpeaking';
32
+ import { TranscriptionPlugin } from '../../TranscriptionPlugin';
33
+ import { useAudioFile, useQueueModelAdapter, useTranscriber } from '../../hooks';
34
+ import { getActorId, MessageNormalizer } from '../../segments-normalization';
35
+ import { TestItem } from '../../testing';
36
+ import { type MediaStreamRecorderParams, type TranscriberParams } from '../../transcriber';
37
+ import { renderMarkdown } from '../Transcript';
38
+
39
+ const AudioFile = ({
40
+ detectSpeaking,
41
+ audioUrl,
42
+ normalizeSentences,
43
+ transcriberConfig,
44
+ recorderConfig,
45
+ audioConstraints,
46
+ }: {
47
+ detectSpeaking?: boolean;
48
+ normalizeSentences?: boolean;
49
+ audioUrl: string;
50
+ transcriberConfig?: TranscriberParams['config'];
51
+ recorderConfig?: MediaStreamRecorderParams['config'];
52
+ audioConstraints?: MediaTrackConstraints;
53
+ }) => {
54
+ const [running, setRunning] = useState(false);
55
+ const actor: DataType.Actor = useMemo(() => ({ name: 'You' }), []);
56
+
57
+ // Audio.
58
+ const { audio, track, stream } = useAudioFile(audioUrl, audioConstraints);
59
+
60
+ // Speaking.
61
+ const isSpeaking = detectSpeaking ? useIsSpeaking(track) : true;
62
+ const ref = useRef<HTMLAudioElement>(null);
63
+ useEffect(() => {
64
+ if (ref.current && stream) {
65
+ ref.current.srcObject = stream;
66
+ }
67
+ }, [stream]);
68
+
69
+ useEffect(() => {
70
+ if (!audio) {
71
+ log.warn('no audio');
72
+ return;
73
+ }
74
+
75
+ if (running) {
76
+ void audio.play();
77
+ } else {
78
+ void audio.pause();
79
+ }
80
+ }, [audio, running]);
81
+
82
+ // Transcriber.
83
+ const queueDxn = useMemo(() => createQueueDxn(), []);
84
+ const queue = useMemo(() => new MemoryQueue<DataType.Message>(queueDxn), [queueDxn]);
85
+
86
+ const model = useQueueModelAdapter(renderMarkdown([]), queue);
87
+ const handleSegments = useCallback<TranscriberParams['onSegments']>(
88
+ async (blocks) => {
89
+ const message = create(DataType.Message, { sender: actor, created: new Date().toISOString(), blocks });
90
+ void queue?.append([message]);
91
+ },
92
+ [queue],
93
+ );
94
+
95
+ const transcriber = useTranscriber({
96
+ audioStreamTrack: track,
97
+ onSegments: handleSegments,
98
+ transcriberConfig,
99
+ recorderConfig,
100
+ });
101
+
102
+ // Normalize sentences.
103
+ const normalizer = useMemo(() => {
104
+ if (!normalizeSentences) {
105
+ return;
106
+ }
107
+ const executor = new FunctionExecutor(
108
+ new ServiceContainer().setServices({
109
+ ai: {
110
+ client: new AIServiceEdgeClient({
111
+ endpoint: AI_SERVICE_ENDPOINT.REMOTE,
112
+ defaultGenerationOptions: {
113
+ model: '@anthropic/claude-3-5-sonnet-20241022',
114
+ },
115
+ }),
116
+ },
117
+ }),
118
+ );
119
+
120
+ return new MessageNormalizer({
121
+ functionExecutor: executor,
122
+ queue,
123
+ startingCursor: { actorId: getActorId(actor), timestamp: new Date().toISOString() },
124
+ });
125
+ }, [normalizeSentences, queue, actor]);
126
+
127
+ useEffect(() => {
128
+ if (!normalizer) {
129
+ return;
130
+ }
131
+ const ctx = new Context();
132
+ scheduleTask(ctx, async () => {
133
+ await normalizer.open();
134
+ ctx.onDispose(async () => {
135
+ await normalizer.close();
136
+ });
137
+ });
138
+ return () => {
139
+ void ctx.dispose();
140
+ };
141
+ }, [normalizer]);
142
+
143
+ const manageChunkRecording = () => {
144
+ if (running && isSpeaking) {
145
+ log.info('starting transcription');
146
+ transcriber?.startChunksRecording();
147
+ } else if (!isSpeaking || !running) {
148
+ log.info('stopping transcription');
149
+ transcriber?.stopChunksRecording();
150
+ }
151
+ };
152
+
153
+ useEffect(() => {
154
+ const ctx = new Context();
155
+ scheduleTask(ctx, async () => {
156
+ if (transcriber && running) {
157
+ await transcriber.open();
158
+ } else if (!running) {
159
+ await transcriber?.close();
160
+ }
161
+ manageChunkRecording();
162
+ });
163
+
164
+ return () => {
165
+ void ctx.dispose();
166
+ };
167
+ }, [transcriber, running]);
168
+
169
+ useEffect(() => {
170
+ manageChunkRecording();
171
+ }, [isSpeaking, stream]);
172
+
173
+ return <TranscriptionStory model={model} running={running} onRunningChange={setRunning} audioRef={ref} />;
174
+ };
175
+
176
+ const meta: Meta<typeof AudioFile> = {
177
+ title: 'plugins/plugin-transcription/FileTranscription',
178
+ decorators: [
179
+ withPluginManager({
180
+ plugins: [
181
+ ThemePlugin({ tx: defaultTx }),
182
+ StorybookLayoutPlugin(),
183
+ ClientPlugin({
184
+ types: [TestItem, DataType.Person, DataType.Organization, Testing.DocumentType],
185
+ onClientInitialized: async (_, client) => {
186
+ await client.halo.createIdentity();
187
+ await client.spaces.waitUntilReady();
188
+ await client.spaces.default.waitUntilReady();
189
+ await seedTestData(client.spaces.default);
190
+ },
191
+ }),
192
+ SpacePlugin(),
193
+ SettingsPlugin(),
194
+ PreviewPlugin(),
195
+ IntentPlugin(),
196
+ TranscriptionPlugin(),
197
+ ],
198
+ fireEvents: [Events.SetupAppGraph],
199
+ }),
200
+ withTheme,
201
+ withLayout({ fullscreen: true, classNames: 'justify-center' }),
202
+ ],
203
+ };
204
+
205
+ export default meta;
206
+
207
+ const TRANSCRIBER_CONFIG = {
208
+ transcribeAfterChunksAmount: 100,
209
+ prefixBufferChunksAmount: 50,
210
+ normalizeSentences: true,
211
+ };
212
+
213
+ const RECORDER_CONFIG = {
214
+ interval: 200,
215
+ };
216
+
217
+ export const Default: StoryObj<typeof AudioFile> = {
218
+ render: AudioFile,
219
+ args: {
220
+ detectSpeaking: true,
221
+ // https://learnenglish.britishcouncil.org/general-english/audio-zone/living-london
222
+ audioUrl: 'https://dxos.network/audio-london.m4a',
223
+ // textUrl: 'https://dxos.network/audio-london.txt',
224
+ transcriberConfig: TRANSCRIBER_CONFIG,
225
+ recorderConfig: RECORDER_CONFIG,
226
+ },
227
+ };
228
+
229
+ export const WithSentenceNormalization: StoryObj<typeof AudioFile> = {
230
+ render: AudioFile,
231
+ args: {
232
+ detectSpeaking: true,
233
+ normalizeSentences: true,
234
+ // https://learnenglish.britishcouncil.org/general-english/audio-zone/living-london
235
+ audioUrl: 'https://dxos.network/audio-london.m4a',
236
+ // textUrl: 'https://dxos.network/audio-london.txt',
237
+ transcriberConfig: TRANSCRIBER_CONFIG,
238
+ recorderConfig: RECORDER_CONFIG,
239
+ },
240
+ };
@@ -0,0 +1,128 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import '@dxos-theme';
6
+
7
+ import { type StoryObj, type Meta } from '@storybook/react';
8
+ import React, { useEffect, useMemo, useState } from 'react';
9
+
10
+ import { log } from '@dxos/log';
11
+ import { Toolbar } from '@dxos/react-ui';
12
+ import { mx } from '@dxos/react-ui-theme';
13
+ import { withLayout, withTheme } from '@dxos/storybook-utils';
14
+
15
+ type Word = {
16
+ text: string;
17
+ matched: boolean;
18
+ };
19
+
20
+ const SpeechRecognition = ({ keywords }: { keywords: string[] }) => {
21
+ const [running, setRunning] = useState(false);
22
+ const [matchingWords, setMatchingWords] = useState<Word[]>(keywords.map((word) => ({ text: word, matched: false })));
23
+
24
+ const [transcript, setTranscript] = useState('');
25
+ const recognition = useMemo(() => {
26
+ // TODO(mykola): Fix types
27
+ const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
28
+ const SpeechGrammarList = (window as any).SpeechGrammarList || (window as any).webkitSpeechGrammarList;
29
+ if (!SpeechRecognition || !SpeechGrammarList) {
30
+ log.error('Speech recognition not supported in this browser');
31
+ return;
32
+ }
33
+
34
+ const recognition = new SpeechRecognition();
35
+ recognition.continuous = true;
36
+ recognition.interimResults = true;
37
+ recognition.lang = 'en-US';
38
+ recognition.maxAlternatives = 1;
39
+ // Add this line to enable punctuation
40
+ recognition.grammars = new SpeechGrammarList();
41
+
42
+ // TODO(mykola): Fix types
43
+ recognition.onresult = (event: any) => {
44
+ log.info('recognition result', { event });
45
+ const current = event.resultIndex;
46
+ const transcript = event.results[current][0].transcript;
47
+
48
+ // Remove punctuation and normalize whitespace for comparison
49
+ const normalizeText = (text: string) =>
50
+ text
51
+ .toLowerCase()
52
+ .replace(/[.,!?]/g, '')
53
+ .replace(/\s+/g, ' ')
54
+ .trim();
55
+
56
+ const normalizedTranscript = normalizeText(transcript);
57
+
58
+ // Check each keyword and update matching words
59
+ const updatedWords = matchingWords.map((word) => {
60
+ const normalizedKeyword = normalizeText(word.text);
61
+ const matched = normalizedTranscript.includes(normalizedKeyword);
62
+ return { ...word, matched };
63
+ });
64
+
65
+ setMatchingWords(updatedWords);
66
+ setTranscript(transcript);
67
+ };
68
+
69
+ // TODO(mykola): Fix types
70
+ recognition.onerror = (event: any) => {
71
+ log.error('Speech recognition error:', { error: event.error });
72
+ setRunning(false);
73
+ };
74
+
75
+ return recognition;
76
+ }, []);
77
+
78
+ useEffect(() => {
79
+ if (running) {
80
+ recognition?.start();
81
+ } else {
82
+ recognition?.stop();
83
+ }
84
+ }, [running, recognition]);
85
+
86
+ return (
87
+ <div className={mx('flex flex-col gap-2 w-[30rem]')}>
88
+ <Toolbar.Root>
89
+ <Toolbar.IconButton
90
+ iconOnly
91
+ icon={running ? 'ph--pause--regular' : 'ph--play--regular'}
92
+ label={running ? 'Pause' : 'Play'}
93
+ onClick={() => setRunning((running) => !running)}
94
+ />
95
+ </Toolbar.Root>
96
+
97
+ <div className='flex flex-wrap'>
98
+ {matchingWords.map((word, index) => (
99
+ <span
100
+ key={index}
101
+ className={mx(
102
+ 'px-3 py-1 m-2 rounded border',
103
+ word.matched ? 'bg-red-200 text-red-800 border-red-300' : 'bg-gray-200 text-gray-600 border-gray-300',
104
+ )}
105
+ >
106
+ {word.text}
107
+ </span>
108
+ ))}
109
+ </div>
110
+
111
+ <span>{transcript || (running ? 'Listening...' : 'Click play to start speech recognition')}</span>
112
+ </div>
113
+ );
114
+ };
115
+
116
+ const meta: Meta<typeof SpeechRecognition> = {
117
+ title: 'plugins/plugin-transcription/KeyWordDetection',
118
+ decorators: [withTheme, withLayout({ fullscreen: true, classNames: 'justify-center' })],
119
+ };
120
+
121
+ export default meta;
122
+
123
+ export const SpeechRecognitionAPI: StoryObj<typeof SpeechRecognition> = {
124
+ render: SpeechRecognition,
125
+ args: {
126
+ keywords: ['kai', 'computer', 'hello'],
127
+ },
128
+ };