@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,31 @@
|
|
|
1
|
+
declare module "dictation_support" {
|
|
2
|
+
export type DictationDevice = unknown;
|
|
3
|
+
|
|
4
|
+
export const ButtonEvent: Record<number, string> & Record<string, number>;
|
|
5
|
+
export const MotionEvent: (Record<number, string> & Record<string, number>) | undefined;
|
|
6
|
+
|
|
7
|
+
export class DictationDeviceManager {
|
|
8
|
+
init?: () => Promise<void>;
|
|
9
|
+
requestDevice(): Promise<DictationDevice[]>;
|
|
10
|
+
getDeviceInfo?: (device: DictationDevice) => Promise<unknown>;
|
|
11
|
+
getDevices?: () => DictationDevice[];
|
|
12
|
+
addButtonEventListener?: (
|
|
13
|
+
cb: (device: DictationDevice, bitMask: number) => void,
|
|
14
|
+
) => void;
|
|
15
|
+
removeButtonEventListener?: (
|
|
16
|
+
cb: (device: DictationDevice, bitMask: number) => void,
|
|
17
|
+
) => void;
|
|
18
|
+
addDeviceConnectedEventListener?: (cb: (device: DictationDevice) => void) => void;
|
|
19
|
+
removeDeviceConnectedEventListener?: (cb: (device: DictationDevice) => void) => void;
|
|
20
|
+
addDeviceDisconnectedEventListener?: (cb: (device: DictationDevice) => void) => void;
|
|
21
|
+
removeDeviceDisconnectedEventListener?: (cb: (device: DictationDevice) => void) => void;
|
|
22
|
+
addMotionEventListener?: (
|
|
23
|
+
cb: (device: DictationDevice, motion: number) => void,
|
|
24
|
+
) => void;
|
|
25
|
+
removeMotionEventListener?: (
|
|
26
|
+
cb: (device: DictationDevice, motion: number) => void,
|
|
27
|
+
) => void;
|
|
28
|
+
disconnect?: () => void;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
interface Window {
|
|
3
|
+
/** Timestamp (Date.now()) du clic Enregistrer — utilisé pour mesurer la latence jusqu'au premier partial/final. */
|
|
4
|
+
__ephia_record_click_ts?: number;
|
|
5
|
+
/** Collecteur de debug runtime exposé par le SDK en mode dev. */
|
|
6
|
+
__ephia_sdk_debug__?: import("./debug/sdk-debug-collector").SdkDebugCollector;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { EphiaAudioClient } from "../core/client/EphiaAudioClient";
|
|
2
|
+
import type { EphiaAudioClientOptions, EphiaStartOptions, EphiaStartResult } from "../core/client/EphiaAudioClient";
|
|
3
|
+
import { TranscriptApplier } from "../core/runtime/TranscriptApplier";
|
|
4
|
+
import { TargetManager } from "../core/targets/TargetManager";
|
|
5
|
+
import type { EphiaBinding } from "../core/bindings/EphiaBinding";
|
|
6
|
+
import type { EditorContext, EphiaServerEvent, ResetReason } from "ephia-protocol";
|
|
7
|
+
import type { Transport } from "../core/transport/Transport";
|
|
8
|
+
|
|
9
|
+
export type EphiaHeadlessTarget = {
|
|
10
|
+
id: string;
|
|
11
|
+
binding: EphiaBinding;
|
|
12
|
+
mode?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type EphiaHeadlessOptions = {
|
|
16
|
+
apiUrl?: string;
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
bearerToken?: string;
|
|
19
|
+
clientType?: string;
|
|
20
|
+
language?: string;
|
|
21
|
+
/** Pre-registered targets. Additional targets can be registered via registerTarget(). */
|
|
22
|
+
targets?: EphiaHeadlessTarget[];
|
|
23
|
+
/** Single target shorthand; equivalent to targets: [{ id: "default", binding }]. */
|
|
24
|
+
binding?: EphiaBinding;
|
|
25
|
+
/** Transport override for testing. */
|
|
26
|
+
transport?: Transport;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type EphiaHeadlessClient = {
|
|
30
|
+
/**
|
|
31
|
+
* Subscribe to a typed V2 server event. Returns an unsubscriber.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* const unsub = client.on("segment.operation", (event) => { ... });
|
|
35
|
+
* // later:
|
|
36
|
+
* unsub();
|
|
37
|
+
*/
|
|
38
|
+
on<T extends EphiaServerEvent["type"]>(
|
|
39
|
+
type: T,
|
|
40
|
+
handler: (event: Extract<EphiaServerEvent, { type: T }>) => void,
|
|
41
|
+
): () => void;
|
|
42
|
+
|
|
43
|
+
start(targetId?: string, options?: EphiaStartOptions): Promise<EphiaStartResult>;
|
|
44
|
+
start(options?: EphiaStartOptions): Promise<EphiaStartResult>;
|
|
45
|
+
stop(): Promise<void>;
|
|
46
|
+
pause(): Promise<void>;
|
|
47
|
+
resume(options?: EphiaStartOptions): Promise<void>;
|
|
48
|
+
endSession(): Promise<void>;
|
|
49
|
+
warmupMic(): Promise<void>;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Registers a new target binding. Returns an unsubscriber.
|
|
53
|
+
*/
|
|
54
|
+
registerTarget(target: EphiaHeadlessTarget): () => void;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Notifies the backend that a specific target is now active.
|
|
58
|
+
* Sends session.target.changed + editor_context.update.
|
|
59
|
+
*/
|
|
60
|
+
setActiveTarget(targetId: string): Promise<void>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Sends a manual editor context snapshot to the backend.
|
|
64
|
+
* Use when the backend needs cursor/text state without a target switch.
|
|
65
|
+
*/
|
|
66
|
+
sendEditorContext(context: EditorContext): Promise<void>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resets the dictation context for a specific target (clears pending segments).
|
|
70
|
+
*/
|
|
71
|
+
resetTarget(targetId: string, reason?: ResetReason): Promise<void>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resets the global session context (all targets).
|
|
75
|
+
*/
|
|
76
|
+
resetSession(reason?: ResetReason): Promise<void>;
|
|
77
|
+
|
|
78
|
+
dispose(): Promise<void>;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export function createEphiaClient(options: EphiaHeadlessOptions): EphiaHeadlessClient {
|
|
82
|
+
const audioClient = new EphiaAudioClient({
|
|
83
|
+
apiUrl: options.apiUrl,
|
|
84
|
+
apiKey: options.apiKey,
|
|
85
|
+
bearerToken: options.bearerToken,
|
|
86
|
+
clientType: options.clientType,
|
|
87
|
+
sessionOptions: options.language ? { language: options.language } : undefined,
|
|
88
|
+
transport: options.transport,
|
|
89
|
+
} as EphiaAudioClientOptions);
|
|
90
|
+
|
|
91
|
+
const targetManager = new TargetManager({
|
|
92
|
+
sendMessage: (message) => audioClient.sendMessage(message as any),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const applier = new TranscriptApplier({ targetManager });
|
|
96
|
+
const unsubServerEvent = audioClient.onServerEvent((event) => applier.handleEvent(event));
|
|
97
|
+
|
|
98
|
+
function register(target: EphiaHeadlessTarget): () => void {
|
|
99
|
+
target.binding.attach();
|
|
100
|
+
const token = targetManager.register({ id: target.id, binding: target.binding, mode: target.mode ?? "write" });
|
|
101
|
+
return () => {
|
|
102
|
+
const removed = targetManager.unregister(target.id, token);
|
|
103
|
+
if (removed) target.binding.detach();
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const initialTargets = options.targets ?? [];
|
|
108
|
+
if (options.binding) {
|
|
109
|
+
initialTargets.push({ id: "default", binding: options.binding, mode: "write" });
|
|
110
|
+
}
|
|
111
|
+
const cleanupTargets = initialTargets.map(register);
|
|
112
|
+
|
|
113
|
+
async function sendWireMessage(type: string, payload?: Record<string, unknown>): Promise<void> {
|
|
114
|
+
await audioClient.sendMessage({ type, payload } as any);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
on<T extends EphiaServerEvent["type"]>(
|
|
119
|
+
type: T,
|
|
120
|
+
handler: (event: Extract<EphiaServerEvent, { type: T }>) => void,
|
|
121
|
+
): () => void {
|
|
122
|
+
return audioClient.onServerEvent((event) => {
|
|
123
|
+
if (event.type === type) {
|
|
124
|
+
handler(event as Extract<EphiaServerEvent, { type: T }>);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
async start(
|
|
130
|
+
targetIdOrOptions?: string | EphiaStartOptions,
|
|
131
|
+
maybeOptions?: EphiaStartOptions,
|
|
132
|
+
): Promise<EphiaStartResult> {
|
|
133
|
+
const targetId =
|
|
134
|
+
typeof targetIdOrOptions === "string" ? targetIdOrOptions : undefined;
|
|
135
|
+
const startOptions =
|
|
136
|
+
typeof targetIdOrOptions === "string" ? maybeOptions : targetIdOrOptions;
|
|
137
|
+
|
|
138
|
+
if (targetId) {
|
|
139
|
+
targetManager.setActive(targetId);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const activeTargetId = targetId ?? targetManager.getActiveTargetId() ?? undefined;
|
|
143
|
+
targetManager.prepareForDictation(activeTargetId);
|
|
144
|
+
|
|
145
|
+
const result = await audioClient.start(
|
|
146
|
+
targetId ? { ...startOptions, initialTargetId: targetId } : startOptions,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Mirror what the React hook does: resync target + editor context after start.
|
|
150
|
+
targetManager.syncActiveTarget();
|
|
151
|
+
targetManager.flushEditorContext(
|
|
152
|
+
targetId ?? targetManager.getActiveTargetId() ?? undefined,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return result;
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
async stop(): Promise<void> {
|
|
159
|
+
await audioClient.stop();
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
async pause(): Promise<void> {
|
|
163
|
+
await audioClient.stop();
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
async resume(resumeOptions?: EphiaStartOptions): Promise<void> {
|
|
167
|
+
const status = audioClient.getState().status;
|
|
168
|
+
if (status === "paused") {
|
|
169
|
+
targetManager.prepareForDictation(targetManager.getActiveTargetId() ?? undefined);
|
|
170
|
+
await audioClient.start(resumeOptions);
|
|
171
|
+
targetManager.syncActiveTarget();
|
|
172
|
+
targetManager.flushEditorContext(targetManager.getActiveTargetId() ?? undefined);
|
|
173
|
+
} else {
|
|
174
|
+
throw new Error("[ephia] resume() requires a paused session; call start() to create a new session");
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
async endSession(): Promise<void> {
|
|
179
|
+
await audioClient.endSession();
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
async warmupMic(): Promise<void> {
|
|
183
|
+
await audioClient.warmupMic();
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
registerTarget(target: EphiaHeadlessTarget): () => void {
|
|
187
|
+
return register(target);
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
async setActiveTarget(targetId: string): Promise<void> {
|
|
191
|
+
targetManager.setActive(targetId);
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
async sendEditorContext(context: EditorContext): Promise<void> {
|
|
195
|
+
await sendWireMessage("session.editor_context.update", context as unknown as Record<string, unknown>);
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
async resetTarget(targetId: string, reason?: ResetReason): Promise<void> {
|
|
199
|
+
await sendWireMessage("session.reset", {
|
|
200
|
+
scope: "target",
|
|
201
|
+
targetId,
|
|
202
|
+
reason: reason ?? "user_explicit",
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
async resetSession(reason?: ResetReason): Promise<void> {
|
|
207
|
+
await sendWireMessage("session.reset", {
|
|
208
|
+
scope: "global",
|
|
209
|
+
reason: reason ?? "user_explicit",
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
async dispose(): Promise<void> {
|
|
214
|
+
unsubServerEvent();
|
|
215
|
+
cleanupTargets.forEach((cleanup) => cleanup());
|
|
216
|
+
targetManager.dispose();
|
|
217
|
+
await audioClient.dispose();
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { createEphiaClient } from "./createEphiaClient";
|
|
2
|
+
export type {
|
|
3
|
+
EphiaHeadlessOptions,
|
|
4
|
+
EphiaHeadlessClient,
|
|
5
|
+
EphiaHeadlessTarget,
|
|
6
|
+
} from "./createEphiaClient";
|
|
7
|
+
|
|
8
|
+
// Bindings V2 — exposed so headless consumers can construct their own targets.
|
|
9
|
+
export type { EphiaBinding, UpsertSegmentInput, PreviewSegmentInput } from "../core/bindings/EphiaBinding";
|
|
10
|
+
export { NativeBinding } from "../core/bindings/native/NativeBinding";
|
|
11
|
+
export {
|
|
12
|
+
TiptapBinding,
|
|
13
|
+
EphiaV2PreviewMark,
|
|
14
|
+
EphiaV2CommittedMark,
|
|
15
|
+
} from "../core/bindings/tiptap/TiptapBinding";
|
|
16
|
+
|
|
17
|
+
// Protocol types commonly needed by headless integrations.
|
|
18
|
+
export type { EditorContext, ResetReason, EphiaServerEvent } from "ephia-protocol";
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// ── React public API ───────────────────────────────────────────────────────────
|
|
2
|
+
export {
|
|
3
|
+
EphiaProvider,
|
|
4
|
+
EphiaAuto,
|
|
5
|
+
useEphia,
|
|
6
|
+
useEphiaStatus,
|
|
7
|
+
useOptionalEphiaStatus,
|
|
8
|
+
useEphiaError,
|
|
9
|
+
useEphiaPartial,
|
|
10
|
+
useEphiaConnection,
|
|
11
|
+
useOptionalEphiaConnection,
|
|
12
|
+
useEphiaAudioLevel,
|
|
13
|
+
useEphiaSpeaking,
|
|
14
|
+
useEphiaNativeTarget,
|
|
15
|
+
useEphiaTarget,
|
|
16
|
+
useEphiaTiptap,
|
|
17
|
+
EphiaDictationButton,
|
|
18
|
+
EphiaStatusBar,
|
|
19
|
+
EphiaTextarea,
|
|
20
|
+
EphiaSpeechMikeProvider,
|
|
21
|
+
useEphiaSpeechMike,
|
|
22
|
+
} from "./react";
|
|
23
|
+
|
|
24
|
+
export type {
|
|
25
|
+
EphiaProviderProps,
|
|
26
|
+
UseEphiaReturn,
|
|
27
|
+
EphiaStatus,
|
|
28
|
+
EphiaStartResult,
|
|
29
|
+
EphiaSpeechMikeAction,
|
|
30
|
+
EphiaSpeechMikeAudioStatus,
|
|
31
|
+
EphiaSpeechMikeButton,
|
|
32
|
+
EphiaSpeechMikeDeviceInfo,
|
|
33
|
+
EphiaSpeechMikeError,
|
|
34
|
+
EphiaSpeechMikeHidStatus,
|
|
35
|
+
EphiaSpeechMikeLedIntent,
|
|
36
|
+
EphiaSpeechMikeProviderProps,
|
|
37
|
+
EphiaSpeechMikeState,
|
|
38
|
+
EphiaSpeechMikeStatus,
|
|
39
|
+
} from "./react";
|
|
40
|
+
|
|
41
|
+
export * from "./devices/speechmike";
|
|
42
|
+
|
|
43
|
+
// ── Headless public API ────────────────────────────────────────────────────────
|
|
44
|
+
export {
|
|
45
|
+
createEphiaClient,
|
|
46
|
+
} from "./headless";
|
|
47
|
+
|
|
48
|
+
export type {
|
|
49
|
+
EphiaHeadlessOptions,
|
|
50
|
+
EphiaHeadlessClient,
|
|
51
|
+
EphiaHeadlessTarget,
|
|
52
|
+
} from "./headless";
|
|
53
|
+
|
|
54
|
+
// ── UI ─────────────────────────────────────────────────────────────────────────
|
|
55
|
+
export {
|
|
56
|
+
Button,
|
|
57
|
+
Spinner,
|
|
58
|
+
EphiaFloatingButton,
|
|
59
|
+
} from "./ui";
|
|
60
|
+
|
|
61
|
+
export type {
|
|
62
|
+
ButtonProps,
|
|
63
|
+
SpinnerProps,
|
|
64
|
+
EphiaFloatingButtonProps,
|
|
65
|
+
EphiaFloatingButtonPosition,
|
|
66
|
+
EphiaFloatingButtonTheme,
|
|
67
|
+
EphiaFloatingButtonSize,
|
|
68
|
+
EphiaFloatingButtonBorderRadius,
|
|
69
|
+
} from "./ui";
|
|
70
|
+
|
|
71
|
+
// ── Shared essentials ──────────────────────────────────────────────────────────
|
|
72
|
+
export {
|
|
73
|
+
EphiaSdkError,
|
|
74
|
+
getMessage,
|
|
75
|
+
DEFAULT_EPHIA_SDK_API_URL,
|
|
76
|
+
resolveEphiaSdkApiUrl,
|
|
77
|
+
initialAudioState,
|
|
78
|
+
} from "./shared";
|
|
79
|
+
|
|
80
|
+
export type {
|
|
81
|
+
EphiaSdkErrorCode,
|
|
82
|
+
EphiaLang,
|
|
83
|
+
EphiaClientConfig,
|
|
84
|
+
EphiaSessionOptionsConfig,
|
|
85
|
+
Mode,
|
|
86
|
+
EphiaSessionOptions,
|
|
87
|
+
EphiaAudioPermission,
|
|
88
|
+
EphiaAudioState,
|
|
89
|
+
} from "./shared";
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useRef } from "react";
|
|
4
|
+
import { useEphiaContext } from "./provider/EphiaContext";
|
|
5
|
+
import { useEphiaInternal } from "./provider/EphiaInternalContext";
|
|
6
|
+
import { NativeBinding } from "../core/bindings/native/NativeBinding";
|
|
7
|
+
|
|
8
|
+
export interface EphiaAutoProps {
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
/** CSS selector for target elements. Defaults to [data-ephia-target]. */
|
|
11
|
+
selector?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Scans its descendants for [data-ephia-target] elements (textarea/input only)
|
|
16
|
+
* and registers them automatically as V2 NativeBinding targets.
|
|
17
|
+
*
|
|
18
|
+
* Uses a MutationObserver to handle dynamically added/removed elements.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* <EphiaProvider getToken={getToken}>
|
|
22
|
+
* <EphiaAuto>
|
|
23
|
+
* <textarea data-ephia-target="report" />
|
|
24
|
+
* </EphiaAuto>
|
|
25
|
+
* </EphiaProvider>
|
|
26
|
+
*/
|
|
27
|
+
export function EphiaAuto({ children, selector = "[data-ephia-target]" }: EphiaAutoProps) {
|
|
28
|
+
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
29
|
+
const { registerTarget } = useEphiaContext();
|
|
30
|
+
const { clientEpoch } = useEphiaInternal();
|
|
31
|
+
const registerTargetRef = useRef(registerTarget);
|
|
32
|
+
registerTargetRef.current = registerTarget;
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const root = rootRef.current;
|
|
36
|
+
if (!root) return;
|
|
37
|
+
|
|
38
|
+
const registered = new Map<Element, () => void>();
|
|
39
|
+
|
|
40
|
+
const tryRegister = (el: Element) => {
|
|
41
|
+
if (registered.has(el)) return;
|
|
42
|
+
if (
|
|
43
|
+
!(el instanceof HTMLTextAreaElement) &&
|
|
44
|
+
!(el instanceof HTMLInputElement)
|
|
45
|
+
) {
|
|
46
|
+
console.warn("[ephia:auto] unsupported element type — only textarea/input supported", el);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const id = el.getAttribute("data-ephia-target");
|
|
50
|
+
if (!id) return;
|
|
51
|
+
|
|
52
|
+
const mode = el.getAttribute("data-ephia-mode") ?? "write";
|
|
53
|
+
const binding = new NativeBinding(el);
|
|
54
|
+
const cleanup = registerTargetRef.current(id, binding, { mode, element: el });
|
|
55
|
+
registered.set(el, cleanup);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const tryUnregister = (el: Element) => {
|
|
59
|
+
const cleanup = registered.get(el);
|
|
60
|
+
if (cleanup) {
|
|
61
|
+
cleanup();
|
|
62
|
+
registered.delete(el);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const scan = () => {
|
|
67
|
+
const elements = Array.from(root.querySelectorAll(selector));
|
|
68
|
+
for (const el of elements) tryRegister(el);
|
|
69
|
+
for (const [el] of registered) {
|
|
70
|
+
if (!root.contains(el)) tryUnregister(el);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
scan();
|
|
75
|
+
|
|
76
|
+
const observer = new MutationObserver(scan);
|
|
77
|
+
observer.observe(root, { childList: true, subtree: true });
|
|
78
|
+
|
|
79
|
+
return () => {
|
|
80
|
+
observer.disconnect();
|
|
81
|
+
for (const [, cleanup] of registered) cleanup();
|
|
82
|
+
registered.clear();
|
|
83
|
+
};
|
|
84
|
+
}, [selector, clientEpoch]);
|
|
85
|
+
|
|
86
|
+
return <div ref={rootRef}>{children}</div>;
|
|
87
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useCallback, useState } from "react";
|
|
4
|
+
import { useEphia } from "../hooks/useEphia";
|
|
5
|
+
import type { EphiaStatus } from "../store/types";
|
|
6
|
+
|
|
7
|
+
export interface EphiaDictationButtonRenderProps {
|
|
8
|
+
status: EphiaStatus;
|
|
9
|
+
isRecording: boolean;
|
|
10
|
+
toggle: () => void;
|
|
11
|
+
disabled: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface EphiaDictationButtonProps {
|
|
15
|
+
targetId?: string;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
className?: string;
|
|
18
|
+
onError?: (error: unknown) => void;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Render prop — le dev fournit son propre rendu.
|
|
22
|
+
* Si absent, un bouton minimaliste est rendu.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* <EphiaDictationButton>
|
|
26
|
+
* {({ isRecording, toggle }) => (
|
|
27
|
+
* <MyButton onClick={toggle} variant={isRecording ? 'danger' : 'primary'} />
|
|
28
|
+
* )}
|
|
29
|
+
* </EphiaDictationButton>
|
|
30
|
+
*/
|
|
31
|
+
children?: (props: EphiaDictationButtonRenderProps) => React.ReactNode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function EphiaDictationButton({
|
|
35
|
+
targetId,
|
|
36
|
+
disabled = false,
|
|
37
|
+
className,
|
|
38
|
+
onError,
|
|
39
|
+
children,
|
|
40
|
+
}: EphiaDictationButtonProps) {
|
|
41
|
+
const { status, isRecording, isProcessing, start, stop } = useEphia();
|
|
42
|
+
const [localError, setLocalError] = useState<string | null>(null);
|
|
43
|
+
|
|
44
|
+
const isDisabled = disabled || isProcessing;
|
|
45
|
+
|
|
46
|
+
const toggle = useCallback(async () => {
|
|
47
|
+
setLocalError(null);
|
|
48
|
+
try {
|
|
49
|
+
if (isRecording) {
|
|
50
|
+
await stop();
|
|
51
|
+
} else {
|
|
52
|
+
await start(targetId);
|
|
53
|
+
}
|
|
54
|
+
} catch (err) {
|
|
55
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
56
|
+
setLocalError(message);
|
|
57
|
+
onError?.(err);
|
|
58
|
+
}
|
|
59
|
+
}, [isRecording, start, stop, targetId, onError]);
|
|
60
|
+
|
|
61
|
+
const renderProps: EphiaDictationButtonRenderProps = {
|
|
62
|
+
status,
|
|
63
|
+
isRecording,
|
|
64
|
+
toggle,
|
|
65
|
+
disabled: isDisabled,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (children) {
|
|
69
|
+
return <>{children(renderProps)}</>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
onClick={toggle}
|
|
76
|
+
disabled={isDisabled}
|
|
77
|
+
className={className}
|
|
78
|
+
aria-label={isRecording ? "Arrêter la dictée" : "Démarrer la dictée"}
|
|
79
|
+
aria-pressed={isRecording}
|
|
80
|
+
aria-busy={isProcessing}
|
|
81
|
+
data-ephia-control="true"
|
|
82
|
+
data-ephia-status={status}
|
|
83
|
+
title={localError ?? undefined}
|
|
84
|
+
>
|
|
85
|
+
{isRecording ? "⏹" : "🎤"}
|
|
86
|
+
</button>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { useEphiaStatus, useEphiaPartial } from "../hooks/useEphia";
|
|
5
|
+
import type { EphiaStatus } from "../store/types";
|
|
6
|
+
|
|
7
|
+
export interface EphiaStatusBarRenderProps {
|
|
8
|
+
status: EphiaStatus;
|
|
9
|
+
partialText: string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface EphiaStatusBarProps {
|
|
13
|
+
className?: string;
|
|
14
|
+
children?: (props: EphiaStatusBarRenderProps) => React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function EphiaStatusBar({
|
|
18
|
+
className,
|
|
19
|
+
children,
|
|
20
|
+
}: EphiaStatusBarProps) {
|
|
21
|
+
const status = useEphiaStatus();
|
|
22
|
+
const partial = useEphiaPartial();
|
|
23
|
+
|
|
24
|
+
if (status === "idle" || status === "recording") return null;
|
|
25
|
+
|
|
26
|
+
const partialText = partial?.text ?? null;
|
|
27
|
+
|
|
28
|
+
const renderProps: EphiaStatusBarRenderProps = {
|
|
29
|
+
status,
|
|
30
|
+
partialText,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (children) {
|
|
34
|
+
return <>{children(renderProps)}</>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
className={className}
|
|
40
|
+
data-ephia-control="true"
|
|
41
|
+
data-ephia-status={status}
|
|
42
|
+
role="status"
|
|
43
|
+
aria-live="polite"
|
|
44
|
+
aria-atomic="false"
|
|
45
|
+
>
|
|
46
|
+
<span data-ephia-indicator aria-hidden="true" />
|
|
47
|
+
|
|
48
|
+
{status === "processing" && (
|
|
49
|
+
<span data-ephia-state="processing" aria-label="Traitement en cours" />
|
|
50
|
+
)}
|
|
51
|
+
|
|
52
|
+
{status === "error" && (
|
|
53
|
+
<span data-ephia-state="error" role="alert">
|
|
54
|
+
Erreur de connexion
|
|
55
|
+
</span>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|