@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,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AudioCaptureManager — micro, MediaStream, niveaux audio, buffers de capture.
|
|
3
|
+
*
|
|
4
|
+
* Rôle : getUserMedia, warmup, VoiceLevelMeter, startup/reconnect capture.
|
|
5
|
+
* Possède : streams actif/warmup, meter, silence tracking.
|
|
6
|
+
* Ne fait pas : publish transport, routing events.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Room } from "livekit-client";
|
|
10
|
+
import { VoiceLevelMeter } from "../audio/voice-level-meter";
|
|
11
|
+
import { EphiaSdkError } from "../../shared/errors/EphiaSdkError";
|
|
12
|
+
import {
|
|
13
|
+
initialAudioState,
|
|
14
|
+
type EphiaAudioState,
|
|
15
|
+
} from "../../shared/state/audio-state";
|
|
16
|
+
|
|
17
|
+
export type AudioCaptureDeps = {
|
|
18
|
+
onAudioState: (state: EphiaAudioState) => void;
|
|
19
|
+
getLocalAudioPublished: () => boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export class AudioCaptureManager {
|
|
23
|
+
readonly voiceLevelMeter = new VoiceLevelMeter();
|
|
24
|
+
private _activeStream: MediaStream | null = null;
|
|
25
|
+
private _warmupStream: MediaStream | null = null;
|
|
26
|
+
private _warmupPromise: Promise<void> | null = null;
|
|
27
|
+
private _levelUnsubscribe?: () => void;
|
|
28
|
+
private _silenceStartMs: number | null = null;
|
|
29
|
+
private _lastAudioState: EphiaAudioState = { ...initialAudioState };
|
|
30
|
+
private readonly deps: AudioCaptureDeps;
|
|
31
|
+
|
|
32
|
+
constructor(deps: AudioCaptureDeps) {
|
|
33
|
+
this.deps = deps;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get lastAudioState(): EphiaAudioState {
|
|
37
|
+
return this._lastAudioState;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get silenceStartMs(): number | null {
|
|
41
|
+
return this._silenceStartMs;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get hasActiveStream(): boolean {
|
|
45
|
+
return !!this._activeStream;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get hasWarmupStream(): boolean {
|
|
49
|
+
return !!this._warmupStream;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setActiveStream(stream: MediaStream | null): void {
|
|
53
|
+
this._activeStream = stream;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
emitAudioState(state: EphiaAudioState): void {
|
|
57
|
+
this._lastAudioState = { ...state };
|
|
58
|
+
this.deps.onAudioState(this._lastAudioState);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
patchAudioState(patch: Partial<EphiaAudioState>): void {
|
|
62
|
+
this.emitAudioState({ ...this._lastAudioState, ...patch });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
stopAudioLevelImmediately(): void {
|
|
66
|
+
this._levelUnsubscribe?.();
|
|
67
|
+
this._levelUnsubscribe = undefined;
|
|
68
|
+
this.voiceLevelMeter.detach();
|
|
69
|
+
this.patchAudioState({
|
|
70
|
+
level: 0,
|
|
71
|
+
rms: 0,
|
|
72
|
+
peak: 0,
|
|
73
|
+
isSilent: true,
|
|
74
|
+
silenceDurationMs: 0,
|
|
75
|
+
localAudioPublished: false,
|
|
76
|
+
muted: false,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async warmupMic(): Promise<void> {
|
|
81
|
+
if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) return;
|
|
82
|
+
if (
|
|
83
|
+
this._warmupStream &&
|
|
84
|
+
this._warmupStream.getAudioTracks().some((t) => t.readyState === "live")
|
|
85
|
+
) {
|
|
86
|
+
console.info("[EphiaAudioClient] warmupMic: already warmed up");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (this._warmupPromise) {
|
|
90
|
+
return this._warmupPromise;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this._warmupPromise = this.acquireMicStream()
|
|
94
|
+
.then(async (stream) => {
|
|
95
|
+
this._warmupStream = stream;
|
|
96
|
+
await this.voiceLevelMeter.attach(stream);
|
|
97
|
+
console.info("[EphiaAudioClient] warmupMic:done", {
|
|
98
|
+
trackCount: stream.getAudioTracks().length,
|
|
99
|
+
});
|
|
100
|
+
})
|
|
101
|
+
.catch((err) => {
|
|
102
|
+
console.warn("[EphiaAudioClient] warmupMic:failed", err);
|
|
103
|
+
})
|
|
104
|
+
.finally(() => {
|
|
105
|
+
this._warmupPromise = null;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return this._warmupPromise;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
injectWarmupStream(stream: MediaStream): void {
|
|
112
|
+
this.cleanupWarmupStream();
|
|
113
|
+
this._warmupStream = stream;
|
|
114
|
+
this.voiceLevelMeter.attach(stream).catch(() => {});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
enableStartupCapture(maxSamples: number): void {
|
|
118
|
+
this.voiceLevelMeter.enableStartupCapture(maxSamples);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
drainStartupCapture(): ArrayBuffer | null {
|
|
122
|
+
return this.voiceLevelMeter.drainStartupCapture();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async acquireMicStream(deviceId?: string): Promise<MediaStream> {
|
|
126
|
+
if (
|
|
127
|
+
this._warmupStream &&
|
|
128
|
+
this._warmupStream.getAudioTracks().some((t) => t.readyState === "live")
|
|
129
|
+
) {
|
|
130
|
+
const warmupTracks = this._warmupStream.getAudioTracks();
|
|
131
|
+
const warmupDeviceId = warmupTracks[0]?.getSettings().deviceId;
|
|
132
|
+
// N'utilise le warmup que s'il correspond au deviceId demandé.
|
|
133
|
+
if (!deviceId || warmupDeviceId === deviceId) {
|
|
134
|
+
console.info("[EphiaAudioClient] getUserMedia: using warmed-up stream", {
|
|
135
|
+
warmupDeviceId,
|
|
136
|
+
});
|
|
137
|
+
const stream = this._warmupStream;
|
|
138
|
+
this._warmupStream = null;
|
|
139
|
+
return stream;
|
|
140
|
+
}
|
|
141
|
+
console.info("[EphiaAudioClient] getUserMedia: warmup device mismatch, releasing warmup", {
|
|
142
|
+
warmupDeviceId,
|
|
143
|
+
requestedDeviceId: deviceId,
|
|
144
|
+
});
|
|
145
|
+
this.cleanupWarmupStream();
|
|
146
|
+
}
|
|
147
|
+
if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) {
|
|
148
|
+
const isSecure = typeof window !== "undefined" && window.isSecureContext === false;
|
|
149
|
+
throw new EphiaSdkError(
|
|
150
|
+
"audio.no_input_device",
|
|
151
|
+
isSecure
|
|
152
|
+
? "getUserMedia not available in non-secure context (requires HTTPS or localhost)"
|
|
153
|
+
: "getUserMedia not available in this browser/context"
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
console.info("[EphiaAudioClient] getUserMedia:start", deviceId ? { deviceId } : undefined);
|
|
157
|
+
const audioConstraints: MediaTrackConstraints = {
|
|
158
|
+
echoCancellation: { ideal: false },
|
|
159
|
+
noiseSuppression: { ideal: false },
|
|
160
|
+
autoGainControl: { ideal: false },
|
|
161
|
+
channelCount: { ideal: 1 },
|
|
162
|
+
sampleRate: { ideal: 16000 },
|
|
163
|
+
};
|
|
164
|
+
if (deviceId) {
|
|
165
|
+
audioConstraints.deviceId = { exact: deviceId };
|
|
166
|
+
}
|
|
167
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: audioConstraints });
|
|
168
|
+
const tracks = stream.getAudioTracks();
|
|
169
|
+
console.info("[EphiaAudioClient] getUserMedia:done", {
|
|
170
|
+
trackCount: tracks.length,
|
|
171
|
+
tracks: tracks.map((t) => ({
|
|
172
|
+
id: t.id,
|
|
173
|
+
label: t.label,
|
|
174
|
+
readyState: t.readyState,
|
|
175
|
+
})),
|
|
176
|
+
});
|
|
177
|
+
return stream;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async setupAudioLevel(stream: MediaStream): Promise<void> {
|
|
181
|
+
const track = stream.getAudioTracks()[0];
|
|
182
|
+
if (!track) return;
|
|
183
|
+
|
|
184
|
+
const trackSettings = track.getSettings();
|
|
185
|
+
const deviceId = trackSettings.deviceId ?? "";
|
|
186
|
+
const deviceLabel = track.label ?? "";
|
|
187
|
+
|
|
188
|
+
this.emitAudioState({
|
|
189
|
+
permission: "granted",
|
|
190
|
+
hasInputDevice: true,
|
|
191
|
+
inputDeviceId: deviceId,
|
|
192
|
+
inputDeviceLabel: deviceLabel,
|
|
193
|
+
level: 0,
|
|
194
|
+
rms: 0,
|
|
195
|
+
peak: 0,
|
|
196
|
+
isSilent: false,
|
|
197
|
+
silenceDurationMs: 0,
|
|
198
|
+
muted: track.muted,
|
|
199
|
+
localAudioPublished: false,
|
|
200
|
+
micReady: true,
|
|
201
|
+
speaking: false,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
this._levelUnsubscribe?.();
|
|
205
|
+
if (!this.voiceLevelMeter.isAttached()) {
|
|
206
|
+
try {
|
|
207
|
+
await this.voiceLevelMeter.attach(stream);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
console.warn(
|
|
210
|
+
"[EphiaAudioClient] _setupAudioLevel: voiceLevelMeter attach failed (non-fatal)",
|
|
211
|
+
err
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
this._levelUnsubscribe = this.voiceLevelMeter.onLevel((snapshot) => {
|
|
216
|
+
const isSilent = snapshot.level < 0.005;
|
|
217
|
+
if (isSilent && this._silenceStartMs === null) {
|
|
218
|
+
this._silenceStartMs = Date.now();
|
|
219
|
+
} else if (!isSilent) {
|
|
220
|
+
this._silenceStartMs = null;
|
|
221
|
+
}
|
|
222
|
+
const silenceDurationMs =
|
|
223
|
+
isSilent && this._silenceStartMs !== null ? Date.now() - this._silenceStartMs : 0;
|
|
224
|
+
this.patchAudioState({
|
|
225
|
+
permission: "granted",
|
|
226
|
+
hasInputDevice: true,
|
|
227
|
+
inputDeviceId: deviceId,
|
|
228
|
+
inputDeviceLabel: deviceLabel,
|
|
229
|
+
level: snapshot.level,
|
|
230
|
+
rms: snapshot.rms,
|
|
231
|
+
peak: snapshot.rms,
|
|
232
|
+
isSilent,
|
|
233
|
+
silenceDurationMs,
|
|
234
|
+
muted: track.muted,
|
|
235
|
+
localAudioPublished: this.deps.getLocalAudioPublished(),
|
|
236
|
+
micReady: true,
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
cleanupActiveStream(): void {
|
|
242
|
+
this._levelUnsubscribe?.();
|
|
243
|
+
this._levelUnsubscribe = undefined;
|
|
244
|
+
this._silenceStartMs = null;
|
|
245
|
+
this.voiceLevelMeter.detach();
|
|
246
|
+
if (this._activeStream) {
|
|
247
|
+
this._activeStream.getTracks().forEach((t) => t.stop());
|
|
248
|
+
this._activeStream = null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
cleanupWarmupStream(): void {
|
|
253
|
+
if (this._warmupStream) {
|
|
254
|
+
this._warmupStream.getTracks().forEach((t) => t.stop());
|
|
255
|
+
this._warmupStream = null;
|
|
256
|
+
}
|
|
257
|
+
this._warmupPromise = null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async getAvailableMicrophones(): Promise<MediaDeviceInfo[]> {
|
|
261
|
+
return Room.getLocalDevices("audioinput");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { EphiaAudioEvent } from "ephia-protocol";
|
|
2
|
+
import type { Transport } from "../transport/Transport";
|
|
3
|
+
import type { EphiaSessionOptions } from "../../shared/types/session";
|
|
4
|
+
import type { EphiaClientState } from "./client-state";
|
|
5
|
+
import type { EphiaAudioState } from "../../shared/state/audio-state";
|
|
6
|
+
|
|
7
|
+
export type EphiaStartOptions = {
|
|
8
|
+
startupBufferMs?: number;
|
|
9
|
+
/**
|
|
10
|
+
* Target SDK initiale à transmettre au backend dès la création de session.
|
|
11
|
+
* Évite que l'agent démarre sur sa target par défaut avant le premier event data-channel.
|
|
12
|
+
*/
|
|
13
|
+
initialTargetId?: string;
|
|
14
|
+
/** Contexte local envoyé au backend lors d'un session.resume. */
|
|
15
|
+
context?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type EphiaStartResult = {
|
|
19
|
+
/** Identifiant de la session démarrée, ou null si aucune session n'a été créée. */
|
|
20
|
+
sessionId: string | null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export interface EphiaAudioClientOptions {
|
|
24
|
+
apiKey?: string;
|
|
25
|
+
bearerToken?: string;
|
|
26
|
+
clientType?: string;
|
|
27
|
+
apiUrl?: string;
|
|
28
|
+
transport?: Transport;
|
|
29
|
+
sessionOptions?: EphiaSessionOptions;
|
|
30
|
+
noiseFilter?: boolean;
|
|
31
|
+
/** Active le VAD gate RMS (mute track en silence). Défaut: true */
|
|
32
|
+
vadGate?: boolean;
|
|
33
|
+
/** deviceId audioinput à utiliser pour acquireMicStream (ex. BlackHole). */
|
|
34
|
+
preferredAudioInputDeviceId?: string;
|
|
35
|
+
onEvent?: (event: EphiaAudioEvent) => void;
|
|
36
|
+
onStateChange?: (state: EphiaClientState) => void;
|
|
37
|
+
onError?: (error: { code: string; message: string }) => void;
|
|
38
|
+
onAudioState?: (state: EphiaAudioState) => void;
|
|
39
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Alias de EphiaSessionStatus — tous les statuts sont des états valides de la machine. */
|
|
2
|
+
export type EphiaClientStatus = import("../session/session-machine").EphiaSessionStatus;
|
|
3
|
+
|
|
4
|
+
export interface EphiaClientState {
|
|
5
|
+
status: EphiaClientStatus;
|
|
6
|
+
sessionId: string | null;
|
|
7
|
+
roomName: string | null;
|
|
8
|
+
error: { code: string; message: string } | null;
|
|
9
|
+
isMicEnabled: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const initialClientState: EphiaClientState = {
|
|
13
|
+
status: "idle",
|
|
14
|
+
sessionId: null,
|
|
15
|
+
roomName: null,
|
|
16
|
+
error: null,
|
|
17
|
+
isMicEnabled: false,
|
|
18
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { PROTOCOL_VERSION as WIRE_PROTOCOL_VERSION } from "ephia-protocol";
|
|
2
|
+
|
|
3
|
+
export const SDK_VERSION = "2.0.0";
|
|
4
|
+
export const PROTOCOL_VERSION = WIRE_PROTOCOL_VERSION;
|
|
5
|
+
export const AUDIO_SESSIONS_PATH = "/api/v1/audio/sessions";
|
|
6
|
+
|
|
7
|
+
/** Durée max de validité d'une session préchargée avant refetch (5 min). */
|
|
8
|
+
export const TOKEN_MAX_AGE_MS = 5 * 60 * 1000;
|
|
9
|
+
/** TTL token LiveKit côté backend (6h). Refresh à 80%. */
|
|
10
|
+
export const ACTIVE_TOKEN_TTL_MS = 6 * 60 * 60 * 1000;
|
|
11
|
+
/** Timeout attente session.closed — backend draine en ≤5s, marge incluse. */
|
|
12
|
+
export const SESSION_END_CLOSED_TIMEOUT_MS = 10_000;
|
|
13
|
+
/** Grace period avant error sur déconnexion transport inattendue. */
|
|
14
|
+
export const DISCONNECT_GRACE_MS = 6_000;
|
|
15
|
+
|
|
16
|
+
export function createCommandId(prefix: string): string {
|
|
17
|
+
const cryptoLike = globalThis.crypto as Crypto | undefined;
|
|
18
|
+
const id =
|
|
19
|
+
typeof cryptoLike?.randomUUID === "function"
|
|
20
|
+
? cryptoLike.randomUUID()
|
|
21
|
+
: `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
22
|
+
return `${prefix}:${id}`;
|
|
23
|
+
}
|