@ephia/dova-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/README.md +89 -0
  2. package/dist/EphiaBinding-BvRmlqqC.d.ts +36 -0
  3. package/dist/EphiaFloatingButton-CxiF86VW.d.ts +65 -0
  4. package/dist/EphiaTextarea-B4_CAVUg.d.ts +183 -0
  5. package/dist/NativeBinding-ChG0GeSz.d.ts +53 -0
  6. package/dist/TargetBinding-BKGQwUMc.d.ts +89 -0
  7. package/dist/TiptapBinding-B-agfV2H.d.ts +45 -0
  8. package/dist/Transport-zdeA4Pou.d.ts +63 -0
  9. package/dist/audio-state-kZ3KSvux.d.ts +39 -0
  10. package/dist/chunk-35AJK2IO.js +1 -0
  11. package/dist/chunk-35AJK2IO.js.map +1 -0
  12. package/dist/chunk-3LXZODL4.js +886 -0
  13. package/dist/chunk-3LXZODL4.js.map +1 -0
  14. package/dist/chunk-5IK5TLSK.js +67 -0
  15. package/dist/chunk-5IK5TLSK.js.map +1 -0
  16. package/dist/chunk-7E43RY75.js +9 -0
  17. package/dist/chunk-7E43RY75.js.map +1 -0
  18. package/dist/chunk-A5UEXJ5R.js +183 -0
  19. package/dist/chunk-A5UEXJ5R.js.map +1 -0
  20. package/dist/chunk-AEE554FT.js +51 -0
  21. package/dist/chunk-AEE554FT.js.map +1 -0
  22. package/dist/chunk-DIEWY3IT.js +1332 -0
  23. package/dist/chunk-DIEWY3IT.js.map +1 -0
  24. package/dist/chunk-EGIAN7FH.js +18 -0
  25. package/dist/chunk-EGIAN7FH.js.map +1 -0
  26. package/dist/chunk-EMOEAPVU.js +486 -0
  27. package/dist/chunk-EMOEAPVU.js.map +1 -0
  28. package/dist/chunk-IDC7FHIZ.js +40 -0
  29. package/dist/chunk-IDC7FHIZ.js.map +1 -0
  30. package/dist/chunk-ITJFN3VM.js +601 -0
  31. package/dist/chunk-ITJFN3VM.js.map +1 -0
  32. package/dist/chunk-K24GNU27.js +22 -0
  33. package/dist/chunk-K24GNU27.js.map +1 -0
  34. package/dist/chunk-LXMCRXXF.js +778 -0
  35. package/dist/chunk-LXMCRXXF.js.map +1 -0
  36. package/dist/chunk-MJCEOOLW.js +122 -0
  37. package/dist/chunk-MJCEOOLW.js.map +1 -0
  38. package/dist/chunk-N7U5M3VZ.js +33 -0
  39. package/dist/chunk-N7U5M3VZ.js.map +1 -0
  40. package/dist/chunk-PSYX674B.js +27 -0
  41. package/dist/chunk-PSYX674B.js.map +1 -0
  42. package/dist/chunk-RFQRV7ML.js +33 -0
  43. package/dist/chunk-RFQRV7ML.js.map +1 -0
  44. package/dist/chunk-THNHRV2B.js +18 -0
  45. package/dist/chunk-THNHRV2B.js.map +1 -0
  46. package/dist/chunk-VSLGR64U.js +62 -0
  47. package/dist/chunk-VSLGR64U.js.map +1 -0
  48. package/dist/chunk-W2ZP674X.js +346 -0
  49. package/dist/chunk-W2ZP674X.js.map +1 -0
  50. package/dist/chunk-YWZUMUYE.js +695 -0
  51. package/dist/chunk-YWZUMUYE.js.map +1 -0
  52. package/dist/client-options-Uo6jXO8k.d.ts +64 -0
  53. package/dist/connection-state-Bk33YprE.d.ts +32 -0
  54. package/dist/core/bindings/index.d.ts +24 -0
  55. package/dist/core/bindings/index.js +1025 -0
  56. package/dist/core/bindings/index.js.map +1 -0
  57. package/dist/core/index.d.ts +383 -0
  58. package/dist/core/index.js +1284 -0
  59. package/dist/core/index.js.map +1 -0
  60. package/dist/createEphiaClient-BhdZ183V.d.ts +69 -0
  61. package/dist/devices/speechmike/index.d.ts +148 -0
  62. package/dist/devices/speechmike/index.js +40 -0
  63. package/dist/devices/speechmike/index.js.map +1 -0
  64. package/dist/headless/index.d.ts +10 -0
  65. package/dist/headless/index.js +25 -0
  66. package/dist/headless/index.js.map +1 -0
  67. package/dist/index.d.ts +18 -0
  68. package/dist/index.js +119 -0
  69. package/dist/index.js.map +1 -0
  70. package/dist/react/index.d.ts +38 -0
  71. package/dist/react/index.js +70 -0
  72. package/dist/react/index.js.map +1 -0
  73. package/dist/rich-editor/index.d.ts +46 -0
  74. package/dist/rich-editor/index.js +13 -0
  75. package/dist/rich-editor/index.js.map +1 -0
  76. package/dist/schema-B2ycPlNB.d.ts +87 -0
  77. package/dist/session-APaXR48R.d.ts +12 -0
  78. package/dist/shared/index.d.ts +16 -0
  79. package/dist/shared/index.js +30 -0
  80. package/dist/shared/index.js.map +1 -0
  81. package/dist/style.css +1093 -0
  82. package/dist/testing/index.d.ts +84 -0
  83. package/dist/testing/index.js +36 -0
  84. package/dist/testing/index.js.map +1 -0
  85. package/dist/types-D5SXPSwR.d.ts +32 -0
  86. package/dist/ui/index.d.ts +30 -0
  87. package/dist/ui/index.js +34 -0
  88. package/dist/ui/index.js.map +1 -0
  89. package/dist/useEphiaSpeechMike-CjD7DWnh.d.ts +64 -0
  90. package/package.json +110 -0
  91. package/src/core/audio/audio-worklet-source.ts +30 -0
  92. package/src/core/audio/index.ts +3 -0
  93. package/src/core/audio/voice-level-meter.test.ts +27 -0
  94. package/src/core/audio/voice-level-meter.ts +270 -0
  95. package/src/core/bindings/EphiaBinding.ts +41 -0
  96. package/src/core/bindings/SegmentBindingBridge.test.ts +422 -0
  97. package/src/core/bindings/SegmentBindingBridge.ts +377 -0
  98. package/src/core/bindings/TargetBinding.ts +142 -0
  99. package/src/core/bindings/adapters/NativeAdapter.test.ts +85 -0
  100. package/src/core/bindings/adapters/NativeAdapter.ts +216 -0
  101. package/src/core/bindings/adapters/ProseMirrorAdapter.ts +231 -0
  102. package/src/core/bindings/adapters/index.ts +2 -0
  103. package/src/core/bindings/binding-factory.ts +78 -0
  104. package/src/core/bindings/detect-editor-type.ts +87 -0
  105. package/src/core/bindings/index.ts +13 -0
  106. package/src/core/bindings/insertion-boundary.test.ts +38 -0
  107. package/src/core/bindings/insertion-boundary.ts +26 -0
  108. package/src/core/bindings/native/NativeBinding.test.ts +277 -0
  109. package/src/core/bindings/native/NativeBinding.ts +239 -0
  110. package/src/core/bindings/resolver.ts +18 -0
  111. package/src/core/bindings/targets/codemirror.binding.ts +293 -0
  112. package/src/core/bindings/targets/contenteditable.binding.ts +452 -0
  113. package/src/core/bindings/targets/index.ts +10 -0
  114. package/src/core/bindings/targets/monaco.binding.ts +315 -0
  115. package/src/core/bindings/targets/tiptap.binding.test.ts +417 -0
  116. package/src/core/bindings/targets/tiptap.binding.ts +1192 -0
  117. package/src/core/bindings/tiptap/TiptapBinding.test.ts +63 -0
  118. package/src/core/bindings/tiptap/TiptapBinding.ts +464 -0
  119. package/src/core/bindings/types.ts +41 -0
  120. package/src/core/client/EphiaAudioClient.ts +654 -0
  121. package/src/core/client/audio-capture.ts +263 -0
  122. package/src/core/client/client-options.ts +39 -0
  123. package/src/core/client/client-state.ts +18 -0
  124. package/src/core/client/constants.ts +23 -0
  125. package/src/core/client/session-api.ts +415 -0
  126. package/src/core/connection/connection-state.ts +78 -0
  127. package/src/core/connection/index.ts +6 -0
  128. package/src/core/index.ts +47 -0
  129. package/src/core/operations/textToDocumentOperations.test.ts +69 -0
  130. package/src/core/operations/textToDocumentOperations.ts +92 -0
  131. package/src/core/runtime/DictationRuntime.test.ts +578 -0
  132. package/src/core/runtime/DictationRuntime.ts +434 -0
  133. package/src/core/runtime/TranscriptApplier.test.ts +355 -0
  134. package/src/core/runtime/TranscriptApplier.ts +229 -0
  135. package/src/core/runtime/index.ts +18 -0
  136. package/src/core/session/index.ts +2 -0
  137. package/src/core/session/session-machine.test.ts +16 -0
  138. package/src/core/session/session-machine.ts +59 -0
  139. package/src/core/targets/EditorContextCollector.ts +71 -0
  140. package/src/core/targets/TargetManager.test.ts +194 -0
  141. package/src/core/targets/TargetManager.ts +194 -0
  142. package/src/core/targets/index.ts +10 -0
  143. package/src/core/text-processing/index.ts +11 -0
  144. package/src/core/text-processing/overlap.test.ts +35 -0
  145. package/src/core/text-processing/overlap.ts +101 -0
  146. package/src/core/text-processing/voice-formatting.normalizer.test.ts +132 -0
  147. package/src/core/text-processing/voice-formatting.normalizer.ts +284 -0
  148. package/src/core/transcript/client-transcript.reducer.ts +366 -0
  149. package/src/core/transcript/client-transcript.state.ts +25 -0
  150. package/src/core/transcript/index.ts +19 -0
  151. package/src/core/transcript/transcript.assembler.test.ts +205 -0
  152. package/src/core/transcript/transcript.assembler.ts +152 -0
  153. package/src/core/transcript/transcript.reducer.test.ts +199 -0
  154. package/src/core/transcript/transcript.reducer.ts +771 -0
  155. package/src/core/transcript/transcript.state.ts +123 -0
  156. package/src/core/transport/LiveKitTransport.publish.test.ts +226 -0
  157. package/src/core/transport/LiveKitTransport.ts +459 -0
  158. package/src/core/transport/MockTransport.ts +231 -0
  159. package/src/core/transport/Transport.ts +82 -0
  160. package/src/debug/sdk-debug-collector.ts +79 -0
  161. package/src/devices/index.ts +2 -0
  162. package/src/devices/speechmike/__tests__/EphiaSpeechMikeProvider.test.tsx +99 -0
  163. package/src/devices/speechmike/__tests__/speechmike-audio-resolver.test.ts +96 -0
  164. package/src/devices/speechmike/__tests__/speechmike-button-router.test.ts +66 -0
  165. package/src/devices/speechmike/__tests__/speechmike-device-manager.test.ts +201 -0
  166. package/src/devices/speechmike/__tests__/speechmike-led-controller.test.ts +68 -0
  167. package/src/devices/speechmike/browser.ts +80 -0
  168. package/src/devices/speechmike/constants.ts +74 -0
  169. package/src/devices/speechmike/dictation-support-loader.ts +81 -0
  170. package/src/devices/speechmike/index.ts +11 -0
  171. package/src/devices/speechmike/react/EphiaSpeechMikeContext.ts +34 -0
  172. package/src/devices/speechmike/react/EphiaSpeechMikeProvider.tsx +287 -0
  173. package/src/devices/speechmike/react/useEphiaSpeechMike.ts +26 -0
  174. package/src/devices/speechmike/speechmike-audio-resolver.ts +58 -0
  175. package/src/devices/speechmike/speechmike-button-router.ts +73 -0
  176. package/src/devices/speechmike/speechmike-device-manager.ts +461 -0
  177. package/src/devices/speechmike/speechmike-led-controller.ts +78 -0
  178. package/src/devices/speechmike/types.ts +96 -0
  179. package/src/dictation_support.d.ts +31 -0
  180. package/src/global.d.ts +10 -0
  181. package/src/headless/createEphiaClient.ts +220 -0
  182. package/src/headless/index.ts +18 -0
  183. package/src/index.ts +89 -0
  184. package/src/react/EphiaAuto.tsx +87 -0
  185. package/src/react/components/EphiaDictationButton.tsx +88 -0
  186. package/src/react/components/EphiaStatusBar.tsx +59 -0
  187. package/src/react/components/EphiaTextarea.tsx +295 -0
  188. package/src/react/ephia-react.css +318 -0
  189. package/src/react/hooks/targets/index.ts +3 -0
  190. package/src/react/hooks/targets/useEphiaCodemirror.ts +35 -0
  191. package/src/react/hooks/targets/useEphiaMonaco.ts +35 -0
  192. package/src/react/hooks/targets/useEphiaTiptap.ts +23 -0
  193. package/src/react/hooks/useEphia.lifecycle.test.tsx +389 -0
  194. package/src/react/hooks/useEphia.ts +367 -0
  195. package/src/react/hooks/useEphiaDiscardTarget.ts +53 -0
  196. package/src/react/hooks/useEphiaServerEvent.ts +33 -0
  197. package/src/react/hooks/useEphiaTarget.ts +47 -0
  198. package/src/react/hooks/useEphiaTranscript.ts +22 -0
  199. package/src/react/index.ts +58 -0
  200. package/src/react/provider/EphiaContext.ts +63 -0
  201. package/src/react/provider/EphiaInternalContext.ts +32 -0
  202. package/src/react/provider/EphiaProvider.tsx +373 -0
  203. package/src/react/registry/binding-factory.ts +7 -0
  204. package/src/react/registry/detect-editor-type.ts +2 -0
  205. package/src/react/registry/events.ts +37 -0
  206. package/src/react/registry/registries/CodeMirrorInstanceRegistry.ts +24 -0
  207. package/src/react/registry/registries/MonacoInstanceRegistry.ts +23 -0
  208. package/src/react/registry/registries/TargetRegistry.ts +327 -0
  209. package/src/react/registry/registries/TiptapInstanceRegistry.ts +43 -0
  210. package/src/react/registry/registries/index.ts +5 -0
  211. package/src/react/store/create-ephia-store.ts +36 -0
  212. package/src/react/store/types.ts +41 -0
  213. package/src/react/utils/flash-range.ts +24 -0
  214. package/src/react/utils/index.ts +1 -0
  215. package/src/rich-editor/adapters/tiptap.test.ts +86 -0
  216. package/src/rich-editor/adapters/tiptap.ts +23 -0
  217. package/src/rich-editor/index.ts +3 -0
  218. package/src/rich-editor/types.ts +24 -0
  219. package/src/rich-editor/use-ephia-rich-editor.test.tsx +202 -0
  220. package/src/rich-editor/use-ephia-rich-editor.ts +47 -0
  221. package/src/shared/config/endpoint.test.ts +45 -0
  222. package/src/shared/config/endpoint.ts +39 -0
  223. package/src/shared/config/schema.ts +32 -0
  224. package/src/shared/effective-text.ts +13 -0
  225. package/src/shared/errors/EphiaSdkError.ts +54 -0
  226. package/src/shared/errors/messages.ts +40 -0
  227. package/src/shared/index.ts +27 -0
  228. package/src/shared/state/audio-state.ts +45 -0
  229. package/src/shared/state/index.ts +2 -0
  230. package/src/shared/store/document-store.ts +32 -0
  231. package/src/shared/store/index.ts +2 -0
  232. package/src/shared/types/editors.ts +28 -0
  233. package/src/shared/types/session.ts +12 -0
  234. package/src/style.css +2 -0
  235. package/src/testing/index.tsx +60 -0
  236. package/src/ui/assets/ephia-logo.svg +4 -0
  237. package/src/ui/components/EphiaLogo.tsx +77 -0
  238. package/src/ui/index.ts +24 -0
  239. package/src/ui/primitives/Button.tsx +53 -0
  240. package/src/ui/primitives/Spinner.tsx +21 -0
  241. package/src/ui/primitives/index.ts +5 -0
  242. package/src/ui/recorder/EphiaFloatingButton.tsx +489 -0
  243. package/src/ui/recorder/MinimalProcessingBars.tsx +122 -0
  244. package/src/ui/recorder/StandardIntensityVisualizer.tsx +148 -0
  245. package/src/ui/recorder/appearance.ts +9 -0
  246. package/src/ui/recorder/index.ts +8 -0
  247. package/src/ui/theme.css +775 -0
@@ -0,0 +1,1284 @@
1
+ import {
2
+ pickEffectiveText
3
+ } from "../chunk-7E43RY75.js";
4
+ import {
5
+ initialConnectionState,
6
+ mapTransportStateToConnectionState
7
+ } from "../chunk-RFQRV7ML.js";
8
+ import {
9
+ LiveKitTransport,
10
+ TargetManager,
11
+ TranscriptApplier,
12
+ canTransition,
13
+ initialClientState,
14
+ transitionSessionStatus
15
+ } from "../chunk-LXMCRXXF.js";
16
+ import "../chunk-N7U5M3VZ.js";
17
+ import {
18
+ documentOperationsToPlainText,
19
+ textToDocumentOperations
20
+ } from "../chunk-VSLGR64U.js";
21
+
22
+ // src/core/transport/MockTransport.ts
23
+ import { PROTOCOL_VERSION, ephiaAudioEventSchema } from "ephia-protocol";
24
+ var MockTransport = class {
25
+ _state = {
26
+ status: "idle",
27
+ localAudioPublished: false,
28
+ localAudioMuted: false,
29
+ reconnectCount: 0
30
+ };
31
+ _eventCallbacks = [];
32
+ _serverEventCallbacks = [];
33
+ _stateCallbacks = [];
34
+ _errorCallbacks = [];
35
+ _running = false;
36
+ _scriptTimer = null;
37
+ _options;
38
+ constructor(options = {}) {
39
+ this._options = options;
40
+ }
41
+ async connect(params) {
42
+ this._running = true;
43
+ this._setState({ status: "connecting", roomName: params.roomName });
44
+ await this._delay(this._options.latencyMs ?? 100);
45
+ if (!this._running) return;
46
+ this._setState({ status: "connected" });
47
+ if (this._options.autoEmitReady !== false) {
48
+ this._emit({
49
+ protocolVersion: PROTOCOL_VERSION,
50
+ eventId: "00000000-0000-4000-8000-000000000001",
51
+ sessionId: "mock-session",
52
+ type: "session.ready",
53
+ seq: 0,
54
+ timestampMs: Date.now(),
55
+ payload: { roomName: params.roomName }
56
+ });
57
+ }
58
+ this._runScript();
59
+ }
60
+ async disconnect(_reason) {
61
+ this._running = false;
62
+ if (this._scriptTimer) {
63
+ clearTimeout(this._scriptTimer);
64
+ this._scriptTimer = null;
65
+ }
66
+ this._setState({ status: "disconnected" });
67
+ }
68
+ async publishAudio(_track) {
69
+ this._setState({ localAudioPublished: true });
70
+ }
71
+ async unpublishAudio() {
72
+ this._setState({ localAudioPublished: false });
73
+ }
74
+ async sendMessage(message) {
75
+ if (message.type === "session.stop") {
76
+ this._emit({
77
+ protocolVersion: PROTOCOL_VERSION,
78
+ eventId: "00000000-0000-4000-8000-000000000002",
79
+ sessionId: "mock-session",
80
+ type: "session.closed",
81
+ seq: 999,
82
+ timestampMs: Date.now(),
83
+ payload: { reason: "user_stop" }
84
+ });
85
+ }
86
+ }
87
+ onEvent(callback) {
88
+ this._eventCallbacks.push(callback);
89
+ return () => {
90
+ this._eventCallbacks = this._eventCallbacks.filter((c) => c !== callback);
91
+ };
92
+ }
93
+ onServerEvent(callback) {
94
+ this._serverEventCallbacks.push(callback);
95
+ return () => {
96
+ this._serverEventCallbacks = this._serverEventCallbacks.filter((c) => c !== callback);
97
+ };
98
+ }
99
+ onTransportState(callback) {
100
+ this._stateCallbacks.push(callback);
101
+ return () => {
102
+ this._stateCallbacks = this._stateCallbacks.filter((c) => c !== callback);
103
+ };
104
+ }
105
+ onError(callback) {
106
+ this._errorCallbacks.push(callback);
107
+ return () => {
108
+ this._errorCallbacks = this._errorCallbacks.filter((c) => c !== callback);
109
+ };
110
+ }
111
+ getState() {
112
+ return { ...this._state };
113
+ }
114
+ async prepareConnection(_url, _token) {
115
+ }
116
+ async performRpc(method, payload, _timeout) {
117
+ if (method === "ephia.session.sync") {
118
+ const lastSeq = Number(payload?.lastSeq ?? 0);
119
+ return JSON.stringify({
120
+ status: "ok",
121
+ mode: "replay",
122
+ fromSeq: lastSeq,
123
+ toSeq: lastSeq,
124
+ events: []
125
+ });
126
+ }
127
+ const msgType = method.replace("ephia.", "");
128
+ await this.sendMessage({ type: msgType, payload });
129
+ return JSON.stringify({ status: "ok" });
130
+ }
131
+ /** Émet manuellement un event legacy (utile pour les tests). */
132
+ simulateEvent(event) {
133
+ this._emit(event);
134
+ }
135
+ /** Émet manuellement un event serveur V2 (utile pour les tests de TranscriptApplier). */
136
+ simulateServerEvent(event) {
137
+ this._serverEventCallbacks.forEach((cb) => cb(event));
138
+ }
139
+ /** Force manuellement un état transport (utile pour les tests). */
140
+ simulateState(state) {
141
+ this._setState(state);
142
+ }
143
+ // ------------------------------------------------------------------
144
+ // Internals
145
+ // ------------------------------------------------------------------
146
+ _setState(partial) {
147
+ this._state = { ...this._state, ...partial };
148
+ this._stateCallbacks.forEach((cb) => cb(this._state));
149
+ }
150
+ _emit(event) {
151
+ const parsed = ephiaAudioEventSchema.safeParse(event);
152
+ if (!parsed.success) {
153
+ console.warn(
154
+ "[MockTransport] invalid event schema (ignored):",
155
+ parsed.error.issues
156
+ );
157
+ return;
158
+ }
159
+ this._eventCallbacks.forEach((cb) => cb(parsed.data));
160
+ }
161
+ async _delay(ms) {
162
+ return new Promise((resolve) => setTimeout(resolve, ms));
163
+ }
164
+ _runScript() {
165
+ const script = this._options.script ?? [];
166
+ let index = 0;
167
+ const runNext = () => {
168
+ if (!this._running || index >= script.length) return;
169
+ const step = script[index];
170
+ index++;
171
+ this._scriptTimer = setTimeout(() => {
172
+ if (!this._running) return;
173
+ const baseEvent = {
174
+ protocolVersion: PROTOCOL_VERSION,
175
+ eventId: `00000000-0000-4000-8000-${String(index).padStart(12, "0")}`,
176
+ sessionId: "mock-session",
177
+ type: step.type,
178
+ seq: index,
179
+ timestampMs: Date.now(),
180
+ payload: step.payload ?? {}
181
+ };
182
+ this._emit(baseEvent);
183
+ runNext();
184
+ }, step.delayMs);
185
+ };
186
+ runNext();
187
+ }
188
+ };
189
+
190
+ // src/core/transcript/client-transcript.state.ts
191
+ var initialTranscriptState = {
192
+ segments: [],
193
+ currentSegmentId: null,
194
+ documentText: ""
195
+ };
196
+
197
+ // src/core/transcript/transcript.assembler.ts
198
+ function trimBoundarySpaces(text) {
199
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/^[ \t]+|[ \t]+$/g, "");
200
+ }
201
+ function joinWithSpacing(prev, next) {
202
+ if (!prev) return next;
203
+ if (!next) return prev;
204
+ const lastChar = prev.slice(-1);
205
+ const firstChar = next.charAt(0);
206
+ if (lastChar === " " && firstChar === " ") return prev + next.trimStart();
207
+ if (/\s/.test(lastChar) || /\s/.test(firstChar)) return prev + next;
208
+ if (/[,.;:!?)\]]/.test(firstChar)) return prev + next;
209
+ return prev + " " + next;
210
+ }
211
+ function assembleTranscript(chunks) {
212
+ const sorted = [...chunks].filter((c) => (c.status === "done" || c.finalizedText) && (c.text || c.finalizedText)).sort((a, b) => a.segmentSeq - b.segmentSeq);
213
+ if (sorted.length === 0) return "";
214
+ let result = "";
215
+ for (let i = 0; i < sorted.length; i++) {
216
+ const chunk = sorted[i];
217
+ let text = trimBoundarySpaces(chunk.text || chunk.finalizedText || "");
218
+ if (i === 0) {
219
+ result = text;
220
+ continue;
221
+ }
222
+ const prev = result;
223
+ const lastChar = prev.slice(-1);
224
+ const firstChar = text.charAt(0);
225
+ if (lastChar.match(/[.!?;,:\)]/) && firstChar.match(/[a-zA-ZÀ-ÿ0-9]/)) {
226
+ result = prev + " " + text;
227
+ } else if (/\s/.test(lastChar) && /\s/.test(firstChar)) {
228
+ result = prev + text.trimStart();
229
+ } else if (!/\s/.test(lastChar) && !/\s/.test(firstChar) && firstChar.match(/[a-zA-ZÀ-ÿ0-9]/)) {
230
+ result = prev + " " + text;
231
+ } else {
232
+ result = prev + text;
233
+ }
234
+ }
235
+ return result;
236
+ }
237
+ function assembleCommittedDocument(chunks, orderedSegmentIds) {
238
+ const sorted = [...orderedSegmentIds].map((id) => chunks[id]).filter((c) => !!c && c.status === "done").filter((c) => !c.mergedWith || Array.isArray(c.mergedWith)).sort((a, b) => a.segmentSeq - b.segmentSeq);
239
+ if (sorted.length === 0) return "";
240
+ let result = "";
241
+ for (let i = 0; i < sorted.length; i++) {
242
+ const chunk = sorted[i];
243
+ const text = trimBoundarySpaces(chunk.committedText ?? chunk.text ?? "");
244
+ if (!text) continue;
245
+ if (i === 0) {
246
+ result = text;
247
+ continue;
248
+ }
249
+ const prev = result;
250
+ const lastChar = prev.slice(-1);
251
+ const firstChar = text.charAt(0);
252
+ if (lastChar.match(/[.!?;,:\)]/) && firstChar.match(/[a-zA-ZÀ-ÿ0-9]/)) {
253
+ result = prev + " " + text;
254
+ } else if (/\s/.test(lastChar) && /\s/.test(firstChar)) {
255
+ result = prev + text.trimStart();
256
+ } else if (!/\s/.test(lastChar) && !/\s/.test(firstChar) && firstChar.match(/[a-zA-ZÀ-ÿ0-9]/)) {
257
+ result = prev + " " + text;
258
+ } else {
259
+ result = prev + text;
260
+ }
261
+ }
262
+ return result;
263
+ }
264
+ var PUNCTUATION_NO_SPACE = /* @__PURE__ */ new Set([",", ".", ";", ":", "!", "?", ")", "]", "}"]);
265
+ function buildDisplayText(finalText, previewText, previewContinuesPreviousWord2 = false) {
266
+ if (!finalText && !previewText) return "";
267
+ if (!finalText) return previewText;
268
+ if (!previewText) return finalText;
269
+ const prev = finalText.replace(/[ \t]+$/, "");
270
+ const next = previewText.trimStart();
271
+ if (!next) return prev;
272
+ if (previewContinuesPreviousWord2) return prev + next;
273
+ const firstChar = next[0];
274
+ if (firstChar && PUNCTUATION_NO_SPACE.has(firstChar)) {
275
+ return prev + next;
276
+ }
277
+ if (prev.endsWith("\n")) return prev + next;
278
+ return prev + " " + next;
279
+ }
280
+
281
+ // src/core/transcript/client-transcript.reducer.ts
282
+ function eventReducer(state, event) {
283
+ switch (event.type) {
284
+ case "transcript.preview":
285
+ return handlePartial(state, event);
286
+ case "transcript.preview.stable":
287
+ return handleStable(state, event);
288
+ case "transcript.final":
289
+ return handleFinal(state, event);
290
+ case "transcript.segment.ready":
291
+ return handleChunkFinalized(state, event);
292
+ case "transcript.segment.committed":
293
+ return handleChunkCommitted(state, event);
294
+ case "session.context.reset":
295
+ return handleSessionContextReset(state, event);
296
+ case "document.patch":
297
+ return handleDocumentPatch(state, event);
298
+ case "segment.absorbed": {
299
+ const absorbedId = event.payload.segmentId;
300
+ const absorbedInto = event.payload.absorbedInto;
301
+ const newSegments = state.segments.map(
302
+ (s) => s.segmentId === absorbedId ? {
303
+ ...s,
304
+ isCommitted: true,
305
+ committedText: null,
306
+ partialText: null,
307
+ finalText: null,
308
+ mergedWith: absorbedInto
309
+ } : s
310
+ );
311
+ return { ...state, segments: newSegments };
312
+ }
313
+ default:
314
+ return state;
315
+ }
316
+ }
317
+ function handlePartial(state, event) {
318
+ const existing = state.segments.find(
319
+ (s) => s.segmentId === event.payload.segmentId
320
+ );
321
+ if (existing) {
322
+ if (existing.isCommitted) {
323
+ return state;
324
+ }
325
+ if (event.payload.revision < existing.revision || event.payload.revision === existing.revision && event.payload.text === existing.partialText) {
326
+ return state;
327
+ }
328
+ return {
329
+ ...state,
330
+ segments: state.segments.map(
331
+ (s) => s.segmentId === event.payload.segmentId ? {
332
+ ...s,
333
+ partialText: event.payload.text,
334
+ revision: event.payload.revision,
335
+ stable: event.payload.stable ?? false
336
+ } : s
337
+ )
338
+ };
339
+ }
340
+ return {
341
+ ...state,
342
+ currentSegmentId: event.payload.segmentId,
343
+ segments: [
344
+ ...state.segments,
345
+ {
346
+ segmentId: event.payload.segmentId,
347
+ segmentSeq: event.payload.segmentSeq,
348
+ partialText: event.payload.text,
349
+ finalText: null,
350
+ committedText: null,
351
+ isFinalized: false,
352
+ isCommitted: false,
353
+ revision: event.payload.revision,
354
+ stable: event.payload.stable ?? false,
355
+ source: null,
356
+ mergedWith: null
357
+ }
358
+ ]
359
+ };
360
+ }
361
+ function handleStable(state, event) {
362
+ return {
363
+ ...state,
364
+ segments: state.segments.map(
365
+ (s) => s.segmentId === event.payload.segmentId ? { ...s, stable: true, partialText: event.payload.text } : s
366
+ )
367
+ };
368
+ }
369
+ function handleFinal(state, event) {
370
+ const existing = state.segments.find(
371
+ (s) => s.segmentId === event.payload.segmentId
372
+ );
373
+ if (!existing) {
374
+ return {
375
+ ...state,
376
+ segments: [
377
+ ...state.segments,
378
+ {
379
+ segmentId: event.payload.segmentId,
380
+ segmentSeq: event.payload.segmentSeq,
381
+ partialText: null,
382
+ finalText: event.payload.text,
383
+ committedText: null,
384
+ isFinalized: true,
385
+ isCommitted: false,
386
+ revision: 0,
387
+ stable: true,
388
+ source: null,
389
+ mergedWith: null
390
+ }
391
+ ]
392
+ };
393
+ }
394
+ return {
395
+ ...state,
396
+ segments: state.segments.map(
397
+ (s) => s.segmentId === event.payload.segmentId ? {
398
+ ...s,
399
+ finalText: event.payload.text,
400
+ isFinalized: true,
401
+ partialText: null
402
+ } : s
403
+ )
404
+ };
405
+ }
406
+ function handleChunkFinalized(state, event) {
407
+ const effectiveText = pickEffectiveText(event.payload);
408
+ const existing = state.segments.find(
409
+ (s) => s.segmentId === event.payload.segmentId
410
+ );
411
+ if (!existing) {
412
+ return {
413
+ ...state,
414
+ segments: [
415
+ ...state.segments,
416
+ {
417
+ segmentId: event.payload.segmentId,
418
+ segmentSeq: event.payload.segmentSeq,
419
+ partialText: null,
420
+ finalText: effectiveText,
421
+ committedText: null,
422
+ isFinalized: true,
423
+ isCommitted: false,
424
+ revision: 0,
425
+ stable: true,
426
+ source: event.payload.provider ?? null,
427
+ mergedWith: null
428
+ }
429
+ ]
430
+ };
431
+ }
432
+ return {
433
+ ...state,
434
+ segments: state.segments.map(
435
+ (s) => s.segmentId === event.payload.segmentId ? {
436
+ ...s,
437
+ finalText: effectiveText,
438
+ isFinalized: true
439
+ } : s
440
+ )
441
+ };
442
+ }
443
+ function handleChunkCommitted(state, event) {
444
+ const { segmentId, source, mergedWith } = event.payload;
445
+ const text = pickEffectiveText(event.payload);
446
+ const mergedIds = mergedWith ? Array.isArray(mergedWith) ? mergedWith : [mergedWith] : [];
447
+ const existing = state.segments.find(
448
+ (s) => s.segmentId === segmentId
449
+ );
450
+ let newSegments = existing ? state.segments.map(
451
+ (s) => s.segmentId === segmentId ? {
452
+ ...s,
453
+ committedText: text,
454
+ isCommitted: true,
455
+ partialText: null,
456
+ finalText: text,
457
+ source: source ?? null,
458
+ mergedWith: mergedIds.length > 0 ? mergedIds : null
459
+ } : s
460
+ ) : [
461
+ ...state.segments,
462
+ {
463
+ segmentId,
464
+ segmentSeq: event.payload.segmentSeq,
465
+ partialText: null,
466
+ finalText: text,
467
+ committedText: text,
468
+ isFinalized: true,
469
+ isCommitted: true,
470
+ revision: 0,
471
+ stable: true,
472
+ source: source ?? null,
473
+ mergedWith: mergedIds.length > 0 ? mergedIds : null
474
+ }
475
+ ];
476
+ for (const absorbedId of mergedIds) {
477
+ newSegments = newSegments.map(
478
+ (s) => s.segmentId === absorbedId ? {
479
+ ...s,
480
+ isCommitted: true,
481
+ committedText: null,
482
+ partialText: null,
483
+ finalText: null,
484
+ mergedWith: segmentId
485
+ } : s
486
+ );
487
+ }
488
+ const documentText = newSegments.filter(
489
+ (s) => s.isCommitted && s.committedText && typeof s.mergedWith !== "string"
490
+ ).map((s) => s.committedText).reduce((acc, text2) => joinWithSpacing(acc, text2), "");
491
+ return {
492
+ ...state,
493
+ segments: newSegments,
494
+ documentText
495
+ };
496
+ }
497
+ function handleSessionContextReset(state, event) {
498
+ const contextOnly = event.payload.contextOnly ?? false;
499
+ if (!contextOnly) {
500
+ return { ...initialTranscriptState };
501
+ }
502
+ const committedSegments = state.segments.filter((s) => s.isCommitted);
503
+ const documentText = committedSegments.filter(
504
+ (s) => s.committedText && typeof s.mergedWith !== "string"
505
+ ).map((s) => s.committedText).reduce((acc, t) => joinWithSpacing(acc, t), "");
506
+ return {
507
+ ...state,
508
+ segments: committedSegments,
509
+ currentSegmentId: null,
510
+ documentText
511
+ };
512
+ }
513
+ function handleDocumentPatch(state, event) {
514
+ let segments = [...state.segments];
515
+ for (const op of event.payload.ops) {
516
+ switch (op.type) {
517
+ case "merge": {
518
+ const fromSeg = segments.find((s) => s.segmentId === op.from);
519
+ const toSeg = segments.find((s) => s.segmentId === op.to);
520
+ if (fromSeg && toSeg) {
521
+ segments = segments.map(
522
+ (s) => s.segmentId === op.to ? { ...s, committedText: op.text ?? null, mergedWith: op.from ?? null } : s
523
+ );
524
+ }
525
+ break;
526
+ }
527
+ case "replace": {
528
+ segments = segments.map(
529
+ (s) => s.segmentId === op.blockId ? { ...s, committedText: op.text ?? null } : s
530
+ );
531
+ break;
532
+ }
533
+ }
534
+ }
535
+ const documentText = segments.filter(
536
+ (s) => s.isCommitted && s.committedText && typeof s.mergedWith !== "string"
537
+ ).map((s) => s.committedText).reduce((acc, text) => joinWithSpacing(acc, text), "");
538
+ return { ...state, segments, documentText };
539
+ }
540
+
541
+ // src/core/transcript/transcript.state.ts
542
+ var initialLiveTranscriptState = {
543
+ sessionId: null,
544
+ status: "idle",
545
+ events: [],
546
+ chunks: {},
547
+ orderedSegmentIds: [],
548
+ finalText: "",
549
+ previewText: "",
550
+ displayText: "",
551
+ documentText: "",
552
+ lastEvent: null,
553
+ diagnostics: {
554
+ eventsCount: 0,
555
+ chunksCount: 0,
556
+ processingChunks: 0,
557
+ doneChunks: 0,
558
+ errorChunks: 0,
559
+ seqValid: true,
560
+ finalTextLength: 0,
561
+ previewActive: false,
562
+ uncertainChunks: 0,
563
+ parseFallbackChunks: 0,
564
+ contextChars: 0
565
+ },
566
+ error: null,
567
+ spinnerTick: 0
568
+ };
569
+
570
+ // src/core/transcript/transcript.reducer.ts
571
+ function liveTranscriptReducer(state, action) {
572
+ if (action.type === "reset") {
573
+ return { ...initialLiveTranscriptState };
574
+ }
575
+ if (action.type === "remove_segments") {
576
+ const removeSet = new Set(action.segmentIds);
577
+ if (removeSet.size === 0) return state;
578
+ const newChunks = {};
579
+ for (const [id, chunk] of Object.entries(state.chunks)) {
580
+ if (!removeSet.has(id)) {
581
+ newChunks[id] = chunk;
582
+ }
583
+ }
584
+ const orderedSegmentIds = state.orderedSegmentIds.filter(
585
+ (id) => !removeSet.has(id)
586
+ );
587
+ const documentText = assembleCommittedDocument(newChunks, orderedSegmentIds);
588
+ return {
589
+ ...state,
590
+ chunks: newChunks,
591
+ orderedSegmentIds,
592
+ documentText,
593
+ diagnostics: computeDiagnostics(
594
+ newChunks,
595
+ orderedSegmentIds,
596
+ state.diagnostics,
597
+ state.finalText,
598
+ state.previewText
599
+ )
600
+ };
601
+ }
602
+ if (action.type === "client_error") {
603
+ return {
604
+ ...state,
605
+ status: "error",
606
+ error: action.error
607
+ };
608
+ }
609
+ if (action.type === "tick") {
610
+ const hasProcessing = Object.values(state.chunks).some(
611
+ (c) => c.status === "opened" || c.status === "processing"
612
+ );
613
+ if (!hasProcessing) return state;
614
+ const nextTick = state.spinnerTick + 1;
615
+ const previewText = buildPreviewText(state.chunks, state.orderedSegmentIds, nextTick);
616
+ return {
617
+ ...state,
618
+ spinnerTick: nextTick,
619
+ previewText,
620
+ displayText: buildLiveDisplayText(state.chunks, state.orderedSegmentIds, state.finalText, previewText),
621
+ diagnostics: computeDiagnostics(state.chunks, state.orderedSegmentIds, state.diagnostics, state.finalText, previewText)
622
+ };
623
+ }
624
+ if (action.type !== "event") {
625
+ return state;
626
+ }
627
+ const event = action.event;
628
+ const lastSeq = state.lastEvent?.seq;
629
+ const isFirstEvent = lastSeq === void 0;
630
+ const isConsecutive = lastSeq !== void 0 && event.seq === lastSeq + 1;
631
+ const isRecoveredFromSync = !state.diagnostics.seqValid && isConsecutive;
632
+ const seqValid = (state.diagnostics.seqValid || isRecoveredFromSync) && (isFirstEvent || isConsecutive);
633
+ const base = {
634
+ ...state,
635
+ events: [...state.events, event],
636
+ lastEvent: event,
637
+ diagnostics: {
638
+ ...state.diagnostics,
639
+ eventsCount: state.diagnostics.eventsCount + 1,
640
+ seqValid
641
+ }
642
+ };
643
+ switch (event.type) {
644
+ case "session.started":
645
+ return {
646
+ ...base,
647
+ sessionId: event.sessionId,
648
+ status: "connecting"
649
+ };
650
+ case "session.ready":
651
+ return {
652
+ ...base,
653
+ status: "recording"
654
+ };
655
+ case "session.paused":
656
+ return {
657
+ ...base,
658
+ status: "ended",
659
+ diagnostics: {
660
+ ...base.diagnostics,
661
+ previewActive: false
662
+ }
663
+ };
664
+ case "session.closed":
665
+ return {
666
+ ...base,
667
+ status: "ended",
668
+ diagnostics: {
669
+ ...base.diagnostics,
670
+ previewActive: false
671
+ }
672
+ };
673
+ case "session.error":
674
+ return {
675
+ ...base,
676
+ status: "error",
677
+ error: {
678
+ code: event.payload.code,
679
+ message: event.payload.message
680
+ }
681
+ };
682
+ case "session.context.reset": {
683
+ const contextOnly = event.payload.contextOnly ?? false;
684
+ if (!contextOnly) {
685
+ const finalizedBeforeReset = {};
686
+ for (const [id, chunk] of Object.entries(base.chunks)) {
687
+ if (chunk.status === "done") finalizedBeforeReset[id] = true;
688
+ }
689
+ return { ...initialLiveTranscriptState, finalizedBeforeReset };
690
+ }
691
+ const newChunks = {};
692
+ const newOrdered = [];
693
+ for (const id of base.orderedSegmentIds) {
694
+ const chunk = base.chunks[id];
695
+ if (chunk && (chunk.committedText || chunk.status === "done")) {
696
+ newChunks[id] = { ...chunk, previewText: void 0 };
697
+ newOrdered.push(id);
698
+ }
699
+ }
700
+ const documentText = assembleCommittedDocument(newChunks, newOrdered);
701
+ return {
702
+ ...base,
703
+ chunks: newChunks,
704
+ orderedSegmentIds: newOrdered,
705
+ documentText,
706
+ previewText: "",
707
+ displayText: documentText,
708
+ diagnostics: computeDiagnostics(newChunks, newOrdered, base.diagnostics, base.finalText, "")
709
+ };
710
+ }
711
+ case "transcript.segment.opened": {
712
+ const chunk = {
713
+ segmentId: event.payload.segmentId,
714
+ segmentSeq: event.payload.segmentSeq,
715
+ status: "opened",
716
+ text: "",
717
+ revision: 0,
718
+ startedAtMs: event.payload.startedAtMs
719
+ };
720
+ const ordered = base.orderedSegmentIds.includes(event.payload.segmentId) ? base.orderedSegmentIds : [...base.orderedSegmentIds, event.payload.segmentId];
721
+ const newChunks = { ...base.chunks, [event.payload.segmentId]: chunk };
722
+ const previewText = buildPreviewText(newChunks, ordered, base.spinnerTick);
723
+ return {
724
+ ...base,
725
+ chunks: newChunks,
726
+ orderedSegmentIds: ordered,
727
+ previewText,
728
+ displayText: buildLiveDisplayText(newChunks, ordered, base.finalText, previewText),
729
+ diagnostics: computeDiagnostics(newChunks, ordered, base.diagnostics, base.finalText, previewText)
730
+ };
731
+ }
732
+ case "transcript.segment.processing": {
733
+ const existing = base.chunks[event.payload.segmentId];
734
+ if (!existing) return base;
735
+ const newChunks = {
736
+ ...base.chunks,
737
+ [event.payload.segmentId]: {
738
+ ...existing,
739
+ status: "processing",
740
+ durationMs: event.payload.durationMs
741
+ }
742
+ };
743
+ const previewText = buildPreviewText(newChunks, base.orderedSegmentIds, base.spinnerTick);
744
+ return {
745
+ ...base,
746
+ chunks: newChunks,
747
+ previewText,
748
+ displayText: buildLiveDisplayText(newChunks, base.orderedSegmentIds, base.finalText, previewText),
749
+ diagnostics: computeDiagnostics(newChunks, base.orderedSegmentIds, base.diagnostics, base.finalText, previewText)
750
+ };
751
+ }
752
+ case "transcript.preview": {
753
+ const existing = base.chunks[event.payload.segmentId];
754
+ const partialText = event.payload.text;
755
+ if (!partialText) {
756
+ return base;
757
+ }
758
+ if (existing) {
759
+ if (existing.status === "done") {
760
+ return base;
761
+ }
762
+ if (event.payload.revision <= existing.revision) {
763
+ return base;
764
+ }
765
+ const newChunks2 = {
766
+ ...base.chunks,
767
+ [event.payload.segmentId]: {
768
+ ...existing,
769
+ status: "preview",
770
+ previewText: partialText,
771
+ previewContinuesPreviousWord: false,
772
+ stablePrefix: event.payload.text,
773
+ revision: event.payload.revision
774
+ }
775
+ };
776
+ const previewText2 = buildPreviewText(newChunks2, base.orderedSegmentIds, base.spinnerTick);
777
+ return {
778
+ ...base,
779
+ chunks: newChunks2,
780
+ previewText: previewText2,
781
+ displayText: buildLiveDisplayText(newChunks2, base.orderedSegmentIds, base.finalText, previewText2),
782
+ diagnostics: computeDiagnostics(newChunks2, base.orderedSegmentIds, base.diagnostics, base.finalText, previewText2)
783
+ };
784
+ }
785
+ const chunk = {
786
+ segmentId: event.payload.segmentId,
787
+ segmentSeq: event.payload.segmentSeq,
788
+ status: "preview",
789
+ text: "",
790
+ previewText: partialText,
791
+ previewContinuesPreviousWord: false,
792
+ stablePrefix: event.payload.text,
793
+ revision: event.payload.revision
794
+ };
795
+ const ordered = base.orderedSegmentIds.includes(event.payload.segmentId) ? base.orderedSegmentIds : [...base.orderedSegmentIds, event.payload.segmentId];
796
+ const newChunks = { ...base.chunks, [event.payload.segmentId]: chunk };
797
+ const previewText = buildPreviewText(newChunks, ordered, base.spinnerTick);
798
+ return {
799
+ ...base,
800
+ chunks: newChunks,
801
+ orderedSegmentIds: ordered,
802
+ previewText,
803
+ displayText: buildLiveDisplayText(newChunks, ordered, base.finalText, previewText),
804
+ diagnostics: computeDiagnostics(newChunks, ordered, base.diagnostics, base.finalText, previewText)
805
+ };
806
+ }
807
+ case "segment.dropped": {
808
+ const newChunks = { ...base.chunks };
809
+ delete newChunks[event.payload.segmentId];
810
+ const orderedSegmentIds = base.orderedSegmentIds.filter(
811
+ (id) => id !== event.payload.segmentId
812
+ );
813
+ const finalText = assembleTranscript(Object.values(newChunks));
814
+ const documentText = assembleCommittedDocument(newChunks, orderedSegmentIds);
815
+ const previewText = buildPreviewText(newChunks, orderedSegmentIds, base.spinnerTick);
816
+ return {
817
+ ...base,
818
+ chunks: newChunks,
819
+ orderedSegmentIds,
820
+ finalText,
821
+ documentText,
822
+ previewText,
823
+ displayText: buildLiveDisplayText(newChunks, orderedSegmentIds, finalText, previewText),
824
+ diagnostics: computeDiagnostics(
825
+ newChunks,
826
+ orderedSegmentIds,
827
+ base.diagnostics,
828
+ finalText,
829
+ previewText
830
+ )
831
+ };
832
+ }
833
+ case "segment.absorbed": {
834
+ if (base.finalizedBeforeReset?.[event.payload.segmentId]) return base;
835
+ const absorbedId = event.payload.segmentId;
836
+ const existing = base.chunks[absorbedId];
837
+ if (!existing) return base;
838
+ const newChunks = {
839
+ ...base.chunks,
840
+ [absorbedId]: {
841
+ ...existing,
842
+ status: "done",
843
+ text: "",
844
+ committedText: "",
845
+ previewText: void 0,
846
+ mergedWith: event.payload.absorbedInto
847
+ }
848
+ };
849
+ const orderedSegmentIds = base.orderedSegmentIds.filter((id) => id !== absorbedId);
850
+ const documentText = assembleCommittedDocument(newChunks, orderedSegmentIds);
851
+ const previewText = buildPreviewText(newChunks, orderedSegmentIds, base.spinnerTick);
852
+ return {
853
+ ...base,
854
+ chunks: newChunks,
855
+ orderedSegmentIds,
856
+ documentText,
857
+ previewText,
858
+ displayText: buildLiveDisplayText(newChunks, orderedSegmentIds, base.finalText, previewText),
859
+ diagnostics: computeDiagnostics(newChunks, orderedSegmentIds, base.diagnostics, base.finalText, previewText)
860
+ };
861
+ }
862
+ case "transcript.preview.stable": {
863
+ const existing = base.chunks[event.payload.segmentId];
864
+ if (!existing) return base;
865
+ const newChunks = {
866
+ ...base.chunks,
867
+ [event.payload.segmentId]: {
868
+ ...existing,
869
+ status: "preview",
870
+ previewText: event.payload.text,
871
+ previewContinuesPreviousWord: false,
872
+ stablePrefix: event.payload.text,
873
+ revision: event.payload.revision
874
+ }
875
+ };
876
+ const previewText = buildPreviewText(newChunks, base.orderedSegmentIds, base.spinnerTick);
877
+ return {
878
+ ...base,
879
+ chunks: newChunks,
880
+ previewText,
881
+ displayText: buildLiveDisplayText(newChunks, base.orderedSegmentIds, base.finalText, previewText),
882
+ diagnostics: computeDiagnostics(newChunks, base.orderedSegmentIds, base.diagnostics, base.finalText, previewText)
883
+ };
884
+ }
885
+ case "transcript.final": {
886
+ const existing = base.chunks[event.payload.segmentId];
887
+ let newChunks;
888
+ let ordered = base.orderedSegmentIds;
889
+ const meta = event.meta || {};
890
+ const chunkUpdate = {
891
+ status: "done",
892
+ text: event.payload.text,
893
+ previewText: void 0,
894
+ provider: event.payload.provider,
895
+ uncertain: meta.uncertain,
896
+ reason: meta.reason ? meta.reason : void 0,
897
+ reasonCodes: meta.reasonCodes,
898
+ parseFallback: meta.parseFallback
899
+ };
900
+ if (!existing) {
901
+ const chunk = {
902
+ segmentId: event.payload.segmentId,
903
+ segmentSeq: event.payload.segmentSeq,
904
+ revision: 0,
905
+ ...chunkUpdate
906
+ };
907
+ ordered = base.orderedSegmentIds.includes(event.payload.segmentId) ? base.orderedSegmentIds : [...base.orderedSegmentIds, event.payload.segmentId];
908
+ newChunks = { ...base.chunks, [event.payload.segmentId]: chunk };
909
+ } else {
910
+ newChunks = {
911
+ ...base.chunks,
912
+ [event.payload.segmentId]: {
913
+ ...existing,
914
+ ...chunkUpdate
915
+ }
916
+ };
917
+ }
918
+ const finalText = assembleTranscript(Object.values(newChunks));
919
+ const previewText = buildPreviewText(newChunks, ordered, base.spinnerTick);
920
+ const latency = event.meta?.latencyMs;
921
+ const contextChars = event.meta?.contextChars;
922
+ const provider = event.payload.provider;
923
+ return {
924
+ ...base,
925
+ chunks: newChunks,
926
+ orderedSegmentIds: ordered,
927
+ finalText,
928
+ previewText,
929
+ displayText: buildLiveDisplayText(newChunks, ordered, finalText, previewText),
930
+ diagnostics: computeDiagnostics(newChunks, ordered, base.diagnostics, finalText, previewText, latency, contextChars, provider)
931
+ };
932
+ }
933
+ case "transcript.segment.ready": {
934
+ const existing = base.chunks[event.payload.segmentId];
935
+ const effectiveText = pickEffectiveText(event.payload) || event.payload.text;
936
+ const ordered = base.orderedSegmentIds.includes(event.payload.segmentId) ? base.orderedSegmentIds : [...base.orderedSegmentIds, event.payload.segmentId];
937
+ const newChunks = {
938
+ ...base.chunks,
939
+ [event.payload.segmentId]: {
940
+ ...existing ?? {
941
+ segmentId: event.payload.segmentId,
942
+ segmentSeq: event.payload.segmentSeq,
943
+ status: "processing",
944
+ text: "",
945
+ revision: 0
946
+ },
947
+ finalizedText: effectiveText,
948
+ provider: event.payload.provider,
949
+ previewText: void 0
950
+ }
951
+ };
952
+ const previewText = buildPreviewText(newChunks, ordered, base.spinnerTick);
953
+ return {
954
+ ...base,
955
+ chunks: newChunks,
956
+ orderedSegmentIds: ordered,
957
+ previewText,
958
+ displayText: buildLiveDisplayText(newChunks, ordered, base.finalText, previewText),
959
+ diagnostics: computeDiagnostics(newChunks, ordered, base.diagnostics, base.finalText, previewText)
960
+ };
961
+ }
962
+ case "transcript.segment.committed": {
963
+ if (base.finalizedBeforeReset?.[event.payload.segmentId]) return base;
964
+ const existing = base.chunks[event.payload.segmentId];
965
+ const effectiveText = pickEffectiveText(event.payload) || event.payload.text;
966
+ const ordered = base.orderedSegmentIds.includes(event.payload.segmentId) ? base.orderedSegmentIds : [...base.orderedSegmentIds, event.payload.segmentId];
967
+ const mergedIds = event.payload.mergedWith ? Array.isArray(event.payload.mergedWith) ? event.payload.mergedWith : [event.payload.mergedWith] : [];
968
+ let newChunks = {
969
+ ...base.chunks,
970
+ [event.payload.segmentId]: {
971
+ ...existing ?? {
972
+ segmentId: event.payload.segmentId,
973
+ segmentSeq: event.payload.segmentSeq,
974
+ revision: 0
975
+ },
976
+ status: "done",
977
+ committedText: effectiveText,
978
+ text: effectiveText,
979
+ source: event.payload.source,
980
+ mergedWith: mergedIds.length > 0 ? mergedIds : void 0,
981
+ previewText: void 0,
982
+ // P1 backend : additif, n'affecte ni le statut ni le texte affiché.
983
+ effectiveSource: event.payload.effectiveSource,
984
+ correctionPending: event.payload.correctionPending,
985
+ correctionAccepted: event.payload.correctionAccepted,
986
+ correctionRejectedReason: event.payload.correctionRejectedReason,
987
+ commandsApplied: event.payload.commandsApplied,
988
+ normalizationsApplied: event.payload.normalizationsApplied,
989
+ safety: event.payload.safety,
990
+ layout: event.payload.layout,
991
+ rawStt: event.payload.rawStt,
992
+ realtimeText: event.payload.realtimeText,
993
+ hintSentToSmall: event.payload.hintSentToSmall,
994
+ fallbackText: event.payload.fallbackText,
995
+ smallRawText: event.payload.smallRawText,
996
+ correctionOutcome: event.payload.correctionOutcome,
997
+ debug: event.payload.debug
998
+ }
999
+ };
1000
+ for (const absorbedId of mergedIds) {
1001
+ const absorbed = newChunks[absorbedId];
1002
+ if (absorbed) {
1003
+ newChunks = {
1004
+ ...newChunks,
1005
+ [absorbedId]: {
1006
+ ...absorbed,
1007
+ status: "done",
1008
+ text: "",
1009
+ committedText: "",
1010
+ previewText: void 0,
1011
+ mergedWith: event.payload.segmentId
1012
+ }
1013
+ };
1014
+ }
1015
+ }
1016
+ const finalText = assembleTranscript(Object.values(newChunks));
1017
+ const documentText = assembleCommittedDocument(newChunks, ordered);
1018
+ const previewText = buildPreviewText(newChunks, ordered, base.spinnerTick);
1019
+ return {
1020
+ ...base,
1021
+ chunks: newChunks,
1022
+ orderedSegmentIds: ordered,
1023
+ finalText,
1024
+ documentText,
1025
+ previewText,
1026
+ displayText: buildLiveDisplayText(newChunks, ordered, finalText, previewText),
1027
+ diagnostics: computeDiagnostics(newChunks, ordered, base.diagnostics, finalText, previewText)
1028
+ };
1029
+ }
1030
+ case "segment.opened": {
1031
+ const id = event.payload.segmentId;
1032
+ if (base.chunks[id]) return base;
1033
+ const chunk = {
1034
+ segmentId: id,
1035
+ segmentSeq: event.payload.segmentSeq,
1036
+ status: "opened",
1037
+ text: "",
1038
+ revision: 0,
1039
+ startedAtMs: event.payload.startMs
1040
+ };
1041
+ const ordered = [...base.orderedSegmentIds, id];
1042
+ const newChunks = { ...base.chunks, [id]: chunk };
1043
+ return {
1044
+ ...base,
1045
+ chunks: newChunks,
1046
+ orderedSegmentIds: ordered,
1047
+ diagnostics: computeDiagnostics(newChunks, ordered, base.diagnostics, base.finalText, base.previewText)
1048
+ };
1049
+ }
1050
+ case "document.patch": {
1051
+ let newChunks = { ...base.chunks };
1052
+ for (const op of event.payload.ops) {
1053
+ if (op.type === "merge" && op.to && newChunks[op.to]) {
1054
+ newChunks = {
1055
+ ...newChunks,
1056
+ [op.to]: {
1057
+ ...newChunks[op.to],
1058
+ committedText: op.text ?? newChunks[op.to].committedText,
1059
+ text: op.text ?? newChunks[op.to].text,
1060
+ mergedWith: op.from
1061
+ }
1062
+ };
1063
+ } else if (op.type === "replace" && op.blockId && newChunks[op.blockId]) {
1064
+ newChunks = {
1065
+ ...newChunks,
1066
+ [op.blockId]: {
1067
+ ...newChunks[op.blockId],
1068
+ committedText: op.text ?? newChunks[op.blockId].committedText,
1069
+ text: op.text ?? newChunks[op.blockId].text
1070
+ }
1071
+ };
1072
+ }
1073
+ }
1074
+ const documentText = assembleCommittedDocument(newChunks, base.orderedSegmentIds);
1075
+ return {
1076
+ ...base,
1077
+ chunks: newChunks,
1078
+ documentText,
1079
+ diagnostics: computeDiagnostics(newChunks, base.orderedSegmentIds, base.diagnostics, base.finalText, base.previewText)
1080
+ };
1081
+ }
1082
+ case "transcript.segment.completed": {
1083
+ const existing = base.chunks[event.payload.segmentId];
1084
+ if (!existing) return base;
1085
+ const newChunks = {
1086
+ ...base.chunks,
1087
+ [event.payload.segmentId]: {
1088
+ ...existing,
1089
+ status: existing.status === "done" || existing.status === "preview" ? existing.status : "done",
1090
+ durationMs: event.payload.durationMs,
1091
+ provider: event.payload.provider
1092
+ }
1093
+ };
1094
+ return {
1095
+ ...base,
1096
+ chunks: newChunks,
1097
+ diagnostics: computeDiagnostics(newChunks, base.orderedSegmentIds, base.diagnostics, base.finalText, base.previewText)
1098
+ };
1099
+ }
1100
+ case "transcript.segment.error": {
1101
+ const existing = base.chunks[event.payload.segmentId];
1102
+ const chunk = {
1103
+ ...existing ?? {
1104
+ segmentId: event.payload.segmentId,
1105
+ segmentSeq: event.payload.segmentSeq,
1106
+ text: "",
1107
+ revision: 0
1108
+ },
1109
+ status: "error",
1110
+ error: `${event.payload.code}: ${event.payload.message}`
1111
+ };
1112
+ const ordered = base.orderedSegmentIds.includes(event.payload.segmentId) ? base.orderedSegmentIds : [...base.orderedSegmentIds, event.payload.segmentId];
1113
+ const newChunks = { ...base.chunks, [event.payload.segmentId]: chunk };
1114
+ return {
1115
+ ...base,
1116
+ chunks: newChunks,
1117
+ orderedSegmentIds: ordered,
1118
+ diagnostics: computeDiagnostics(newChunks, ordered, base.diagnostics, base.finalText, base.previewText)
1119
+ };
1120
+ }
1121
+ case "audio.vad.start":
1122
+ return {
1123
+ ...base,
1124
+ diagnostics: { ...base.diagnostics, mode: "vad" }
1125
+ };
1126
+ case "segment.audio.closed": {
1127
+ const existing = base.chunks[event.payload.segmentId];
1128
+ if (!existing) return base;
1129
+ const newChunks = {
1130
+ ...base.chunks,
1131
+ [event.payload.segmentId]: {
1132
+ ...existing,
1133
+ audioClosedAtMs: event.payload.endMs,
1134
+ // Conserver le dernier preview jusqu'au committed — évite le flash vide
1135
+ // entre la fin audio VAD et la finalisation Voxtral.
1136
+ status: existing.status === "preview" ? "processing" : existing.status
1137
+ }
1138
+ };
1139
+ const previewText = buildPreviewText(newChunks, base.orderedSegmentIds, base.spinnerTick);
1140
+ return {
1141
+ ...base,
1142
+ chunks: newChunks,
1143
+ previewText,
1144
+ displayText: buildLiveDisplayText(newChunks, base.orderedSegmentIds, base.finalText, previewText),
1145
+ diagnostics: computeDiagnostics(newChunks, base.orderedSegmentIds, base.diagnostics, base.finalText, previewText)
1146
+ };
1147
+ }
1148
+ case "audio.vad.end":
1149
+ case "session.pong":
1150
+ default:
1151
+ return base;
1152
+ }
1153
+ }
1154
+ var SPINNER_CHARS = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1155
+ function buildPreviewText(chunks, orderedSegmentIds, spinnerTick = 0) {
1156
+ const sorted = [...orderedSegmentIds].map((id) => chunks[id]).filter((c) => !!c).sort((a, b) => a.segmentSeq - b.segmentSeq);
1157
+ let result = "";
1158
+ let hasPendingWithoutPreview = false;
1159
+ for (const chunk of sorted) {
1160
+ if (chunk.status === "done") continue;
1161
+ if ((chunk.status === "preview" || chunk.status === "processing" || chunk.status === "opened") && chunk.previewText) {
1162
+ result = joinWithSpacing(result, chunk.previewText);
1163
+ } else if (chunk.status === "processing" || chunk.status === "opened") {
1164
+ hasPendingWithoutPreview = true;
1165
+ }
1166
+ }
1167
+ if (hasPendingWithoutPreview && !result) {
1168
+ return SPINNER_CHARS[spinnerTick % SPINNER_CHARS.length];
1169
+ }
1170
+ return result;
1171
+ }
1172
+ function previewContinuesPreviousWord(chunks, orderedSegmentIds) {
1173
+ const sorted = [...orderedSegmentIds].map((id) => chunks[id]).filter((c) => !!c).sort((a, b) => a.segmentSeq - b.segmentSeq);
1174
+ const firstPreview = sorted.find(
1175
+ (chunk) => chunk.status !== "done" && !!chunk.previewText && (chunk.status === "preview" || chunk.status === "processing" || chunk.status === "opened")
1176
+ );
1177
+ return !!firstPreview?.previewContinuesPreviousWord;
1178
+ }
1179
+ function buildLiveDisplayText(chunks, orderedSegmentIds, finalText, previewText) {
1180
+ return buildDisplayText(
1181
+ finalText,
1182
+ previewText,
1183
+ previewContinuesPreviousWord(chunks, orderedSegmentIds)
1184
+ );
1185
+ }
1186
+ function computeDiagnostics(chunks, orderedSegmentIds, prevDiagnostics, finalText, previewText, newLatencyMs, newContextChars, newProvider) {
1187
+ const all = Object.values(chunks);
1188
+ const processingChunks = all.filter((c) => c.status === "opened" || c.status === "processing").length;
1189
+ const doneChunks = all.filter((c) => c.status === "done").length;
1190
+ const errorChunks = all.filter((c) => c.status === "error").length;
1191
+ const previewActive = all.some(
1192
+ (c) => (c.status === "preview" || c.status === "processing" || c.status === "opened") && !!c.previewText
1193
+ );
1194
+ const uncertainChunks = all.filter((c) => c.uncertain).length;
1195
+ const parseFallbackChunks = all.filter((c) => c.parseFallback).length;
1196
+ let averageLatencyMs = prevDiagnostics.averageLatencyMs;
1197
+ let lastLatencyMs = prevDiagnostics.lastLatencyMs;
1198
+ if (newLatencyMs !== void 0) {
1199
+ lastLatencyMs = newLatencyMs;
1200
+ const prevAvg = prevDiagnostics.averageLatencyMs ?? 0;
1201
+ const count = doneChunks;
1202
+ if (count <= 1) {
1203
+ averageLatencyMs = newLatencyMs;
1204
+ } else {
1205
+ averageLatencyMs = Math.round((prevAvg * (count - 1) + newLatencyMs) / count);
1206
+ }
1207
+ }
1208
+ return {
1209
+ ...prevDiagnostics,
1210
+ chunksCount: orderedSegmentIds.length,
1211
+ processingChunks,
1212
+ doneChunks,
1213
+ errorChunks,
1214
+ averageLatencyMs,
1215
+ lastLatencyMs,
1216
+ lastProvider: newProvider ?? prevDiagnostics.lastProvider,
1217
+ finalTextLength: finalText.length,
1218
+ previewActive,
1219
+ uncertainChunks,
1220
+ parseFallbackChunks,
1221
+ contextChars: newContextChars ?? prevDiagnostics.contextChars ?? 0
1222
+ };
1223
+ }
1224
+
1225
+ // src/core/targets/EditorContextCollector.ts
1226
+ var EditorContextCollector = class {
1227
+ maxContextChars;
1228
+ constructor(options = {}) {
1229
+ this.maxContextChars = options.maxContextChars ?? 600;
1230
+ }
1231
+ collect(target) {
1232
+ const text = target.binding?.getText?.() ?? "";
1233
+ const selection = target.binding?.getSelection?.() ?? null;
1234
+ const cursorOffset = this.resolveCursorOffset(target.binding, text, selection);
1235
+ const leftRaw = text.slice(0, cursorOffset);
1236
+ const rightRaw = text.slice(cursorOffset);
1237
+ return {
1238
+ targetId: target.id,
1239
+ documentEmpty: text.trim().length === 0,
1240
+ insertionMode: this.resolveInsertionMode(text, cursorOffset, selection),
1241
+ leftContext: leftRaw.slice(-this.maxContextChars),
1242
+ rightContext: rightRaw.slice(0, this.maxContextChars),
1243
+ selectedText: selection?.text || null,
1244
+ cursorOffset
1245
+ };
1246
+ }
1247
+ resolveCursorOffset(binding, text, selection) {
1248
+ if (selection?.range) return clamp(selection.range.start, 0, text.length);
1249
+ const cursor = binding?.getCursorOffset?.();
1250
+ if (typeof cursor === "number") return clamp(cursor, 0, text.length);
1251
+ return text.length;
1252
+ }
1253
+ resolveInsertionMode(text, cursorOffset, selection) {
1254
+ if (selection?.text) return "replace_selection";
1255
+ if (text.trim().length === 0) return "document_start";
1256
+ if (cursorOffset >= text.length) return "append";
1257
+ return "middle_of_sentence";
1258
+ }
1259
+ };
1260
+ function clamp(value, min, max) {
1261
+ return Math.min(Math.max(value, min), max);
1262
+ }
1263
+ export {
1264
+ EditorContextCollector,
1265
+ LiveKitTransport,
1266
+ MockTransport,
1267
+ TargetManager,
1268
+ TranscriptApplier,
1269
+ assembleCommittedDocument,
1270
+ assembleTranscript,
1271
+ buildDisplayText,
1272
+ canTransition,
1273
+ documentOperationsToPlainText,
1274
+ eventReducer,
1275
+ initialClientState,
1276
+ initialConnectionState,
1277
+ initialLiveTranscriptState,
1278
+ initialTranscriptState,
1279
+ liveTranscriptReducer,
1280
+ mapTransportStateToConnectionState,
1281
+ textToDocumentOperations,
1282
+ transitionSessionStatus
1283
+ };
1284
+ //# sourceMappingURL=index.js.map