@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,270 @@
1
+ /**
2
+ * Niveau vocal minimal pour la barre Standard (RMS brut + enveloppe attack/release).
3
+ */
4
+
5
+ export type VoiceLevelSnapshot = {
6
+ level: number;
7
+ rms: number;
8
+ };
9
+
10
+ import { getAudioWorkletModuleUrl } from "./audio-worklet-source";
11
+
12
+ const VOLUME_THROTTLE_MS = 33;
13
+ const SILENCE_THRESHOLD_RMS = 0.005;
14
+ const RAW_RMS_SCALE = 7;
15
+ const TARGET_SAMPLE_RATE = 16_000;
16
+
17
+ function clamp01(value: number): number {
18
+ return Math.max(0, Math.min(1, value));
19
+ }
20
+
21
+ function isAudioContextClosed(audioContext: AudioContext): boolean {
22
+ return (audioContext.state as AudioContextState) === "closed";
23
+ }
24
+
25
+ function resampleLinear(
26
+ input: Float32Array,
27
+ sourceSampleRate: number,
28
+ targetSampleRate: number
29
+ ): Float32Array {
30
+ if (sourceSampleRate === targetSampleRate || input.length <= 1) {
31
+ return input;
32
+ }
33
+
34
+ const outputLength = Math.max(
35
+ 1,
36
+ Math.round((input.length * targetSampleRate) / sourceSampleRate)
37
+ );
38
+ const output = new Float32Array(outputLength);
39
+ const scale = sourceSampleRate / targetSampleRate;
40
+
41
+ for (let i = 0; i < outputLength; i += 1) {
42
+ const sourceIndex = i * scale;
43
+ const leftIndex = Math.floor(sourceIndex);
44
+ const rightIndex = Math.min(leftIndex + 1, input.length - 1);
45
+ const fraction = sourceIndex - leftIndex;
46
+ const left = input[leftIndex] ?? 0;
47
+ const right = input[rightIndex] ?? left;
48
+ output[i] = left + (right - left) * fraction;
49
+ }
50
+
51
+ return output;
52
+ }
53
+
54
+ export class VoiceLevelMeter {
55
+ private audioContext: AudioContext | null = null;
56
+ private processor: AudioWorkletNode | null = null;
57
+ private silentGain: GainNode | null = null;
58
+ private source: MediaStreamAudioSourceNode | null = null;
59
+ private callbacks = new Set<(snapshot: VoiceLevelSnapshot) => void>();
60
+ private pendingRms = 0;
61
+ private lastVolumeUpdateMs = 0;
62
+ private volumeTimer: ReturnType<typeof setTimeout> | null = null;
63
+ private _pipelineReady: Promise<void> | null = null;
64
+ // Startup capture state (shared pipeline avec StartupAudioBuffer)
65
+ private _captureBuffers: Float32Array[] = [];
66
+ private _captureTotalSamples = 0;
67
+ private _captureMaxSamples = 0;
68
+ private _captureEnabled = false;
69
+ private _actualSampleRate = TARGET_SAMPLE_RATE;
70
+
71
+ attach(stream: MediaStream): Promise<void> {
72
+ this.detach();
73
+
74
+ const AudioContextClass =
75
+ window.AudioContext ||
76
+ (window as Window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
77
+ if (!AudioContextClass) {
78
+ console.warn("[VoiceLevelMeter] AudioContext not supported");
79
+ return Promise.resolve();
80
+ }
81
+
82
+ this._pipelineReady = this._startWorkletPipeline(stream, AudioContextClass);
83
+ return this._pipelineReady;
84
+ }
85
+
86
+ isAttached(): boolean {
87
+ return this.processor !== null;
88
+ }
89
+
90
+ private async _startWorkletPipeline(
91
+ stream: MediaStream,
92
+ AudioContextClass: typeof AudioContext,
93
+ ): Promise<void> {
94
+ try {
95
+ const audioContext = new AudioContextClass({
96
+ sampleRate: TARGET_SAMPLE_RATE,
97
+ latencyHint: "interactive",
98
+ });
99
+ this.audioContext = audioContext;
100
+ this._actualSampleRate = audioContext.sampleRate || TARGET_SAMPLE_RATE;
101
+
102
+ if (audioContext.state === "suspended") {
103
+ await audioContext.resume();
104
+ }
105
+ // Guard: detach() a été appelé entre-temps (ex: dispose pendant navigation)
106
+ if (this.audioContext !== audioContext || isAudioContextClosed(audioContext)) {
107
+ return;
108
+ }
109
+
110
+ await audioContext.audioWorklet.addModule(getAudioWorkletModuleUrl());
111
+ // Guard: même vérification après le module load async
112
+ if (this.audioContext !== audioContext || isAudioContextClosed(audioContext)) {
113
+ return;
114
+ }
115
+
116
+ const source = audioContext.createMediaStreamSource(stream);
117
+ const processor = new AudioWorkletNode(audioContext, "ephia-audio-capture");
118
+ const silentGain = audioContext.createGain();
119
+ silentGain.gain.value = 0;
120
+
121
+ processor.connect(silentGain);
122
+ silentGain.connect(audioContext.destination);
123
+
124
+ processor.port.onmessage = (event: MessageEvent<Float32Array>) => {
125
+ const inputData = event.data;
126
+ if (!inputData?.length) return;
127
+
128
+ // Startup capture — accumulation FIFO partagée avec le pipeline level
129
+ if (this._captureEnabled) {
130
+ const chunk = new Float32Array(inputData);
131
+ this._captureBuffers.push(chunk);
132
+ this._captureTotalSamples += chunk.length;
133
+ while (this._captureTotalSamples > this._captureMaxSamples && this._captureBuffers.length > 0) {
134
+ const removed = this._captureBuffers.shift()!;
135
+ this._captureTotalSamples -= removed.length;
136
+ }
137
+ }
138
+
139
+ let sum = 0;
140
+ for (let i = 0; i < inputData.length; i++) {
141
+ const x = inputData[i];
142
+ sum += x * x;
143
+ }
144
+ const rawRms = Math.sqrt(sum / inputData.length);
145
+ this.pendingRms = clamp01(rawRms * RAW_RMS_SCALE);
146
+
147
+ const now = Date.now();
148
+ if (
149
+ this.volumeTimer === null &&
150
+ now - this.lastVolumeUpdateMs > VOLUME_THROTTLE_MS
151
+ ) {
152
+ this.volumeTimer = setTimeout(() => {
153
+ this.volumeTimer = null;
154
+ this.lastVolumeUpdateMs = Date.now();
155
+ this._emitSnapshot();
156
+ }, VOLUME_THROTTLE_MS);
157
+ }
158
+ };
159
+
160
+ this.source = source;
161
+ this.processor = processor;
162
+ this.silentGain = silentGain;
163
+ source.connect(processor);
164
+ } catch (err) {
165
+ console.warn("[VoiceLevelMeter] worklet pipeline failed", err);
166
+ this.detach();
167
+ }
168
+ }
169
+
170
+ private _emitSnapshot(): void {
171
+ const rms = this.pendingRms;
172
+ const snapshot: VoiceLevelSnapshot = {
173
+ level: clamp01(rms),
174
+ rms,
175
+ };
176
+ const isSilent = rms < SILENCE_THRESHOLD_RMS;
177
+ if (isSilent) {
178
+ snapshot.level = 0;
179
+ snapshot.rms = 0;
180
+ }
181
+ this.callbacks.forEach((cb) => cb(snapshot));
182
+ }
183
+
184
+ detach(): void {
185
+ if (this.volumeTimer !== null) {
186
+ clearTimeout(this.volumeTimer);
187
+ this.volumeTimer = null;
188
+ }
189
+ this.pendingRms = 0;
190
+ this.lastVolumeUpdateMs = 0;
191
+
192
+ if (this.source) {
193
+ try {
194
+ this.source.disconnect();
195
+ } catch {
196
+ /* ignore */
197
+ }
198
+ this.source = null;
199
+ }
200
+ if (this.processor) {
201
+ try {
202
+ this.processor.port.onmessage = null;
203
+ this.processor.disconnect();
204
+ } catch {
205
+ /* ignore */
206
+ }
207
+ this.processor = null;
208
+ }
209
+ if (this.silentGain) {
210
+ try {
211
+ this.silentGain.disconnect();
212
+ } catch {
213
+ /* ignore */
214
+ }
215
+ this.silentGain = null;
216
+ }
217
+ if (this.audioContext) {
218
+ this.audioContext.close().catch(() => {});
219
+ this.audioContext = null;
220
+ }
221
+ }
222
+
223
+ onLevel(callback: (snapshot: VoiceLevelSnapshot) => void): () => void {
224
+ this.callbacks.add(callback);
225
+ return () => {
226
+ this.callbacks.delete(callback);
227
+ };
228
+ }
229
+
230
+ /** Active la capture startup sur le pipeline existant. maxSamples = taille FIFO. */
231
+ enableStartupCapture(maxSamples: number): void {
232
+ this._captureBuffers = [];
233
+ this._captureTotalSamples = 0;
234
+ this._captureMaxSamples = Math.ceil(
235
+ (maxSamples * this._actualSampleRate) / TARGET_SAMPLE_RATE
236
+ );
237
+ this._captureEnabled = true;
238
+ }
239
+
240
+ /** Stoppe la capture et retourne le PCM16 accumulé (ou null si vide). Le level meter continue. */
241
+ drainStartupCapture(): ArrayBuffer | null {
242
+ this._captureEnabled = false;
243
+ const buffers = this._captureBuffers;
244
+ const totalSamples = this._captureTotalSamples;
245
+ this._captureBuffers = [];
246
+ this._captureTotalSamples = 0;
247
+
248
+ if (buffers.length === 0) return null;
249
+
250
+ const float32 = new Float32Array(totalSamples);
251
+ let offset = 0;
252
+ for (const buf of buffers) {
253
+ float32.set(buf, offset);
254
+ offset += buf.length;
255
+ }
256
+
257
+ const resampled = resampleLinear(
258
+ float32,
259
+ this._actualSampleRate,
260
+ TARGET_SAMPLE_RATE
261
+ );
262
+
263
+ const pcm16 = new Int16Array(resampled.length);
264
+ for (let i = 0; i < resampled.length; i++) {
265
+ const s = Math.max(-1, Math.min(1, resampled[i] ?? 0));
266
+ pcm16[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
267
+ }
268
+ return pcm16.buffer;
269
+ }
270
+ }
@@ -0,0 +1,41 @@
1
+ import type { EditorContext, SegmentStage } from "ephia-protocol";
2
+
3
+ export type UpsertSegmentInput = {
4
+ id: string;
5
+ text: string;
6
+ stage: SegmentStage;
7
+ source?: string;
8
+ };
9
+
10
+ export type PreviewSegmentInput = {
11
+ id: string;
12
+ text: string;
13
+ };
14
+
15
+ /**
16
+ * Interface V2 unifiée pour tous les bindings éditeurs Ephia.
17
+ *
18
+ * Règles :
19
+ * - même segmentId + upsertSegment = update en place (never append)
20
+ * - segmentId connu mais range perdu = warning + no append
21
+ * - removeSegments est idempotent
22
+ * - cleanup visuel ne supprime jamais l'identité segment
23
+ */
24
+ export interface EphiaBinding {
25
+ readonly kind: string;
26
+ /** Stable reference for this binding instance. Used for idempotent lifecycle management. */
27
+ readonly identity?: object | string;
28
+
29
+ attach(): void;
30
+ detach(): void;
31
+
32
+ getText(): string;
33
+ getEditorContext(targetId?: string): EditorContext;
34
+
35
+ previewSegment?(input: PreviewSegmentInput): void;
36
+
37
+ upsertSegment(input: UpsertSegmentInput): void;
38
+
39
+ removeSegment(id: string): void;
40
+ removeSegments(ids: string[]): void;
41
+ }