@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.
- package/README.md +89 -0
- package/dist/EphiaBinding-BvRmlqqC.d.ts +36 -0
- package/dist/EphiaFloatingButton-CxiF86VW.d.ts +65 -0
- package/dist/EphiaTextarea-B4_CAVUg.d.ts +183 -0
- package/dist/NativeBinding-ChG0GeSz.d.ts +53 -0
- package/dist/TargetBinding-BKGQwUMc.d.ts +89 -0
- package/dist/TiptapBinding-B-agfV2H.d.ts +45 -0
- package/dist/Transport-zdeA4Pou.d.ts +63 -0
- package/dist/audio-state-kZ3KSvux.d.ts +39 -0
- package/dist/chunk-35AJK2IO.js +1 -0
- package/dist/chunk-35AJK2IO.js.map +1 -0
- package/dist/chunk-3LXZODL4.js +886 -0
- package/dist/chunk-3LXZODL4.js.map +1 -0
- package/dist/chunk-5IK5TLSK.js +67 -0
- package/dist/chunk-5IK5TLSK.js.map +1 -0
- package/dist/chunk-7E43RY75.js +9 -0
- package/dist/chunk-7E43RY75.js.map +1 -0
- package/dist/chunk-A5UEXJ5R.js +183 -0
- package/dist/chunk-A5UEXJ5R.js.map +1 -0
- package/dist/chunk-AEE554FT.js +51 -0
- package/dist/chunk-AEE554FT.js.map +1 -0
- package/dist/chunk-DIEWY3IT.js +1332 -0
- package/dist/chunk-DIEWY3IT.js.map +1 -0
- package/dist/chunk-EGIAN7FH.js +18 -0
- package/dist/chunk-EGIAN7FH.js.map +1 -0
- package/dist/chunk-EMOEAPVU.js +486 -0
- package/dist/chunk-EMOEAPVU.js.map +1 -0
- package/dist/chunk-IDC7FHIZ.js +40 -0
- package/dist/chunk-IDC7FHIZ.js.map +1 -0
- package/dist/chunk-ITJFN3VM.js +601 -0
- package/dist/chunk-ITJFN3VM.js.map +1 -0
- package/dist/chunk-K24GNU27.js +22 -0
- package/dist/chunk-K24GNU27.js.map +1 -0
- package/dist/chunk-LXMCRXXF.js +778 -0
- package/dist/chunk-LXMCRXXF.js.map +1 -0
- package/dist/chunk-MJCEOOLW.js +122 -0
- package/dist/chunk-MJCEOOLW.js.map +1 -0
- package/dist/chunk-N7U5M3VZ.js +33 -0
- package/dist/chunk-N7U5M3VZ.js.map +1 -0
- package/dist/chunk-PSYX674B.js +27 -0
- package/dist/chunk-PSYX674B.js.map +1 -0
- package/dist/chunk-RFQRV7ML.js +33 -0
- package/dist/chunk-RFQRV7ML.js.map +1 -0
- package/dist/chunk-THNHRV2B.js +18 -0
- package/dist/chunk-THNHRV2B.js.map +1 -0
- package/dist/chunk-VSLGR64U.js +62 -0
- package/dist/chunk-VSLGR64U.js.map +1 -0
- package/dist/chunk-W2ZP674X.js +346 -0
- package/dist/chunk-W2ZP674X.js.map +1 -0
- package/dist/chunk-YWZUMUYE.js +695 -0
- package/dist/chunk-YWZUMUYE.js.map +1 -0
- package/dist/client-options-Uo6jXO8k.d.ts +64 -0
- package/dist/connection-state-Bk33YprE.d.ts +32 -0
- package/dist/core/bindings/index.d.ts +24 -0
- package/dist/core/bindings/index.js +1025 -0
- package/dist/core/bindings/index.js.map +1 -0
- package/dist/core/index.d.ts +383 -0
- package/dist/core/index.js +1284 -0
- package/dist/core/index.js.map +1 -0
- package/dist/createEphiaClient-BhdZ183V.d.ts +69 -0
- package/dist/devices/speechmike/index.d.ts +148 -0
- package/dist/devices/speechmike/index.js +40 -0
- package/dist/devices/speechmike/index.js.map +1 -0
- package/dist/headless/index.d.ts +10 -0
- package/dist/headless/index.js +25 -0
- package/dist/headless/index.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +119 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.d.ts +38 -0
- package/dist/react/index.js +70 -0
- package/dist/react/index.js.map +1 -0
- package/dist/rich-editor/index.d.ts +46 -0
- package/dist/rich-editor/index.js +13 -0
- package/dist/rich-editor/index.js.map +1 -0
- package/dist/schema-B2ycPlNB.d.ts +87 -0
- package/dist/session-APaXR48R.d.ts +12 -0
- package/dist/shared/index.d.ts +16 -0
- package/dist/shared/index.js +30 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/style.css +1093 -0
- package/dist/testing/index.d.ts +84 -0
- package/dist/testing/index.js +36 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/types-D5SXPSwR.d.ts +32 -0
- package/dist/ui/index.d.ts +30 -0
- package/dist/ui/index.js +34 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/useEphiaSpeechMike-CjD7DWnh.d.ts +64 -0
- package/package.json +110 -0
- package/src/core/audio/audio-worklet-source.ts +30 -0
- package/src/core/audio/index.ts +3 -0
- package/src/core/audio/voice-level-meter.test.ts +27 -0
- package/src/core/audio/voice-level-meter.ts +270 -0
- package/src/core/bindings/EphiaBinding.ts +41 -0
- package/src/core/bindings/SegmentBindingBridge.test.ts +422 -0
- package/src/core/bindings/SegmentBindingBridge.ts +377 -0
- package/src/core/bindings/TargetBinding.ts +142 -0
- package/src/core/bindings/adapters/NativeAdapter.test.ts +85 -0
- package/src/core/bindings/adapters/NativeAdapter.ts +216 -0
- package/src/core/bindings/adapters/ProseMirrorAdapter.ts +231 -0
- package/src/core/bindings/adapters/index.ts +2 -0
- package/src/core/bindings/binding-factory.ts +78 -0
- package/src/core/bindings/detect-editor-type.ts +87 -0
- package/src/core/bindings/index.ts +13 -0
- package/src/core/bindings/insertion-boundary.test.ts +38 -0
- package/src/core/bindings/insertion-boundary.ts +26 -0
- package/src/core/bindings/native/NativeBinding.test.ts +277 -0
- package/src/core/bindings/native/NativeBinding.ts +239 -0
- package/src/core/bindings/resolver.ts +18 -0
- package/src/core/bindings/targets/codemirror.binding.ts +293 -0
- package/src/core/bindings/targets/contenteditable.binding.ts +452 -0
- package/src/core/bindings/targets/index.ts +10 -0
- package/src/core/bindings/targets/monaco.binding.ts +315 -0
- package/src/core/bindings/targets/tiptap.binding.test.ts +417 -0
- package/src/core/bindings/targets/tiptap.binding.ts +1192 -0
- package/src/core/bindings/tiptap/TiptapBinding.test.ts +63 -0
- package/src/core/bindings/tiptap/TiptapBinding.ts +464 -0
- package/src/core/bindings/types.ts +41 -0
- package/src/core/client/EphiaAudioClient.ts +654 -0
- package/src/core/client/audio-capture.ts +263 -0
- package/src/core/client/client-options.ts +39 -0
- package/src/core/client/client-state.ts +18 -0
- package/src/core/client/constants.ts +23 -0
- package/src/core/client/session-api.ts +415 -0
- package/src/core/connection/connection-state.ts +78 -0
- package/src/core/connection/index.ts +6 -0
- package/src/core/index.ts +47 -0
- package/src/core/operations/textToDocumentOperations.test.ts +69 -0
- package/src/core/operations/textToDocumentOperations.ts +92 -0
- package/src/core/runtime/DictationRuntime.test.ts +578 -0
- package/src/core/runtime/DictationRuntime.ts +434 -0
- package/src/core/runtime/TranscriptApplier.test.ts +355 -0
- package/src/core/runtime/TranscriptApplier.ts +229 -0
- package/src/core/runtime/index.ts +18 -0
- package/src/core/session/index.ts +2 -0
- package/src/core/session/session-machine.test.ts +16 -0
- package/src/core/session/session-machine.ts +59 -0
- package/src/core/targets/EditorContextCollector.ts +71 -0
- package/src/core/targets/TargetManager.test.ts +194 -0
- package/src/core/targets/TargetManager.ts +194 -0
- package/src/core/targets/index.ts +10 -0
- package/src/core/text-processing/index.ts +11 -0
- package/src/core/text-processing/overlap.test.ts +35 -0
- package/src/core/text-processing/overlap.ts +101 -0
- package/src/core/text-processing/voice-formatting.normalizer.test.ts +132 -0
- package/src/core/text-processing/voice-formatting.normalizer.ts +284 -0
- package/src/core/transcript/client-transcript.reducer.ts +366 -0
- package/src/core/transcript/client-transcript.state.ts +25 -0
- package/src/core/transcript/index.ts +19 -0
- package/src/core/transcript/transcript.assembler.test.ts +205 -0
- package/src/core/transcript/transcript.assembler.ts +152 -0
- package/src/core/transcript/transcript.reducer.test.ts +199 -0
- package/src/core/transcript/transcript.reducer.ts +771 -0
- package/src/core/transcript/transcript.state.ts +123 -0
- package/src/core/transport/LiveKitTransport.publish.test.ts +226 -0
- package/src/core/transport/LiveKitTransport.ts +459 -0
- package/src/core/transport/MockTransport.ts +231 -0
- package/src/core/transport/Transport.ts +82 -0
- package/src/debug/sdk-debug-collector.ts +79 -0
- package/src/devices/index.ts +2 -0
- package/src/devices/speechmike/__tests__/EphiaSpeechMikeProvider.test.tsx +99 -0
- package/src/devices/speechmike/__tests__/speechmike-audio-resolver.test.ts +96 -0
- package/src/devices/speechmike/__tests__/speechmike-button-router.test.ts +66 -0
- package/src/devices/speechmike/__tests__/speechmike-device-manager.test.ts +201 -0
- package/src/devices/speechmike/__tests__/speechmike-led-controller.test.ts +68 -0
- package/src/devices/speechmike/browser.ts +80 -0
- package/src/devices/speechmike/constants.ts +74 -0
- package/src/devices/speechmike/dictation-support-loader.ts +81 -0
- package/src/devices/speechmike/index.ts +11 -0
- package/src/devices/speechmike/react/EphiaSpeechMikeContext.ts +34 -0
- package/src/devices/speechmike/react/EphiaSpeechMikeProvider.tsx +287 -0
- package/src/devices/speechmike/react/useEphiaSpeechMike.ts +26 -0
- package/src/devices/speechmike/speechmike-audio-resolver.ts +58 -0
- package/src/devices/speechmike/speechmike-button-router.ts +73 -0
- package/src/devices/speechmike/speechmike-device-manager.ts +461 -0
- package/src/devices/speechmike/speechmike-led-controller.ts +78 -0
- package/src/devices/speechmike/types.ts +96 -0
- package/src/dictation_support.d.ts +31 -0
- package/src/global.d.ts +10 -0
- package/src/headless/createEphiaClient.ts +220 -0
- package/src/headless/index.ts +18 -0
- package/src/index.ts +89 -0
- package/src/react/EphiaAuto.tsx +87 -0
- package/src/react/components/EphiaDictationButton.tsx +88 -0
- package/src/react/components/EphiaStatusBar.tsx +59 -0
- package/src/react/components/EphiaTextarea.tsx +295 -0
- package/src/react/ephia-react.css +318 -0
- package/src/react/hooks/targets/index.ts +3 -0
- package/src/react/hooks/targets/useEphiaCodemirror.ts +35 -0
- package/src/react/hooks/targets/useEphiaMonaco.ts +35 -0
- package/src/react/hooks/targets/useEphiaTiptap.ts +23 -0
- package/src/react/hooks/useEphia.lifecycle.test.tsx +389 -0
- package/src/react/hooks/useEphia.ts +367 -0
- package/src/react/hooks/useEphiaDiscardTarget.ts +53 -0
- package/src/react/hooks/useEphiaServerEvent.ts +33 -0
- package/src/react/hooks/useEphiaTarget.ts +47 -0
- package/src/react/hooks/useEphiaTranscript.ts +22 -0
- package/src/react/index.ts +58 -0
- package/src/react/provider/EphiaContext.ts +63 -0
- package/src/react/provider/EphiaInternalContext.ts +32 -0
- package/src/react/provider/EphiaProvider.tsx +373 -0
- package/src/react/registry/binding-factory.ts +7 -0
- package/src/react/registry/detect-editor-type.ts +2 -0
- package/src/react/registry/events.ts +37 -0
- package/src/react/registry/registries/CodeMirrorInstanceRegistry.ts +24 -0
- package/src/react/registry/registries/MonacoInstanceRegistry.ts +23 -0
- package/src/react/registry/registries/TargetRegistry.ts +327 -0
- package/src/react/registry/registries/TiptapInstanceRegistry.ts +43 -0
- package/src/react/registry/registries/index.ts +5 -0
- package/src/react/store/create-ephia-store.ts +36 -0
- package/src/react/store/types.ts +41 -0
- package/src/react/utils/flash-range.ts +24 -0
- package/src/react/utils/index.ts +1 -0
- package/src/rich-editor/adapters/tiptap.test.ts +86 -0
- package/src/rich-editor/adapters/tiptap.ts +23 -0
- package/src/rich-editor/index.ts +3 -0
- package/src/rich-editor/types.ts +24 -0
- package/src/rich-editor/use-ephia-rich-editor.test.tsx +202 -0
- package/src/rich-editor/use-ephia-rich-editor.ts +47 -0
- package/src/shared/config/endpoint.test.ts +45 -0
- package/src/shared/config/endpoint.ts +39 -0
- package/src/shared/config/schema.ts +32 -0
- package/src/shared/effective-text.ts +13 -0
- package/src/shared/errors/EphiaSdkError.ts +54 -0
- package/src/shared/errors/messages.ts +40 -0
- package/src/shared/index.ts +27 -0
- package/src/shared/state/audio-state.ts +45 -0
- package/src/shared/state/index.ts +2 -0
- package/src/shared/store/document-store.ts +32 -0
- package/src/shared/store/index.ts +2 -0
- package/src/shared/types/editors.ts +28 -0
- package/src/shared/types/session.ts +12 -0
- package/src/style.css +2 -0
- package/src/testing/index.tsx +60 -0
- package/src/ui/assets/ephia-logo.svg +4 -0
- package/src/ui/components/EphiaLogo.tsx +77 -0
- package/src/ui/index.ts +24 -0
- package/src/ui/primitives/Button.tsx +53 -0
- package/src/ui/primitives/Spinner.tsx +21 -0
- package/src/ui/primitives/index.ts +5 -0
- package/src/ui/recorder/EphiaFloatingButton.tsx +489 -0
- package/src/ui/recorder/MinimalProcessingBars.tsx +122 -0
- package/src/ui/recorder/StandardIntensityVisualizer.tsx +148 -0
- package/src/ui/recorder/appearance.ts +9 -0
- package/src/ui/recorder/index.ts +8 -0
- 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
|
+
}
|