@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,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P3 (post-P0 dictée) — conversion texte ↔ DocumentOperation[].
|
|
3
|
+
*
|
|
4
|
+
* Découpe un texte contenant des \n / \n\n structurels (déjà décidés par le
|
|
5
|
+
* backend — cf. merge_internal_layout côté ephia_transcribe_agent) en une
|
|
6
|
+
* séquence d'opérations neutres, consommable aussi bien par un binding riche
|
|
7
|
+
* (TipTap, cf. P4) qu'un consommateur plain-text/headless (documentOperationsToPlainText).
|
|
8
|
+
*/
|
|
9
|
+
import type {
|
|
10
|
+
DocumentOperation,
|
|
11
|
+
InsertTextOperation,
|
|
12
|
+
LineBreakOperation,
|
|
13
|
+
ParagraphBreakOperation,
|
|
14
|
+
} from "ephia-protocol";
|
|
15
|
+
|
|
16
|
+
function localUuid(): string {
|
|
17
|
+
const c = globalThis.crypto;
|
|
18
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
19
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TextToDocumentOperationsOptions {
|
|
23
|
+
position?: number | "cursor" | "start" | "end";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* \n → LineBreakOperation
|
|
28
|
+
* \n\n (ou plus) → ParagraphBreakOperation
|
|
29
|
+
* reste → InsertTextOperation
|
|
30
|
+
*/
|
|
31
|
+
export function textToDocumentOperations(
|
|
32
|
+
text: string,
|
|
33
|
+
options?: TextToDocumentOperationsOptions
|
|
34
|
+
): DocumentOperation[] {
|
|
35
|
+
const position = options?.position ?? "cursor";
|
|
36
|
+
const operations: DocumentOperation[] = [];
|
|
37
|
+
|
|
38
|
+
for (const part of text.split(/(\n+)/g)) {
|
|
39
|
+
if (!part) continue;
|
|
40
|
+
if (part[0] === "\n") {
|
|
41
|
+
if (part.length > 1) {
|
|
42
|
+
operations.push({
|
|
43
|
+
id: localUuid(),
|
|
44
|
+
type: "paragraph_break",
|
|
45
|
+
position,
|
|
46
|
+
} as ParagraphBreakOperation);
|
|
47
|
+
} else {
|
|
48
|
+
operations.push({
|
|
49
|
+
id: localUuid(),
|
|
50
|
+
type: "line_break",
|
|
51
|
+
position,
|
|
52
|
+
} as LineBreakOperation);
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
operations.push({
|
|
56
|
+
id: localUuid(),
|
|
57
|
+
type: "insert_text",
|
|
58
|
+
position,
|
|
59
|
+
text: part,
|
|
60
|
+
} as InsertTextOperation);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return operations;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Fonction inverse — round-trip sans perte avec textToDocumentOperations(),
|
|
68
|
+
* indispensable pour les consommateurs plain-text/headless qui n'ont pas de
|
|
69
|
+
* binding riche capable d'interpréter line_break/paragraph_break nativement. */
|
|
70
|
+
export function documentOperationsToPlainText(operations: DocumentOperation[]): string {
|
|
71
|
+
let out = "";
|
|
72
|
+
for (const operation of operations) {
|
|
73
|
+
switch (operation.type) {
|
|
74
|
+
case "insert_text":
|
|
75
|
+
case "insert":
|
|
76
|
+
out += operation.text;
|
|
77
|
+
break;
|
|
78
|
+
case "line_break":
|
|
79
|
+
out += "\n";
|
|
80
|
+
break;
|
|
81
|
+
case "paragraph_break":
|
|
82
|
+
out += "\n\n";
|
|
83
|
+
break;
|
|
84
|
+
default:
|
|
85
|
+
// Les opérations de remplacement/section/highlight n'ont pas de
|
|
86
|
+
// représentation texte-plat universelle — ignorées ici, gérées par
|
|
87
|
+
// le store headless qui les interprète (cf. P3, plan post-P0).
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import type { EphiaAudioEvent } from "ephia-protocol";
|
|
5
|
+
import type { TargetBinding } from "../bindings/TargetBinding";
|
|
6
|
+
import {
|
|
7
|
+
DictationRuntime,
|
|
8
|
+
type DictationPartialState,
|
|
9
|
+
type DictationTarget,
|
|
10
|
+
type DictationTargetInsertionMode,
|
|
11
|
+
type DictationTargetRegistry,
|
|
12
|
+
type DictationRuntimeStatus,
|
|
13
|
+
} from "./DictationRuntime";
|
|
14
|
+
|
|
15
|
+
type BindingCall =
|
|
16
|
+
| ["insertPartial", string, string, number]
|
|
17
|
+
| ["commitFinal", string, string, unknown]
|
|
18
|
+
| ["clearPartial", string]
|
|
19
|
+
| ["clearAll"];
|
|
20
|
+
|
|
21
|
+
function event(type: string, payload: Record<string, unknown> = {}): EphiaAudioEvent {
|
|
22
|
+
return { type, payload } as unknown as EphiaAudioEvent;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createBinding(): TargetBinding & { calls: BindingCall[] } {
|
|
26
|
+
const calls: BindingCall[] = [];
|
|
27
|
+
return {
|
|
28
|
+
kind: "test",
|
|
29
|
+
calls,
|
|
30
|
+
attach: vi.fn(),
|
|
31
|
+
detach: vi.fn(),
|
|
32
|
+
beginSession: vi.fn(),
|
|
33
|
+
endSession: vi.fn(),
|
|
34
|
+
insertPartial: vi.fn((segmentId: string, text: string, revision: number) => {
|
|
35
|
+
calls.push(["insertPartial", segmentId, text, revision]);
|
|
36
|
+
}),
|
|
37
|
+
commitFinal: vi.fn((segmentId: string, text: string, options?: unknown) => {
|
|
38
|
+
calls.push(["commitFinal", segmentId, text, options]);
|
|
39
|
+
}),
|
|
40
|
+
clearPartial: vi.fn((segmentId: string) => {
|
|
41
|
+
calls.push(["clearPartial", segmentId]);
|
|
42
|
+
}),
|
|
43
|
+
clearAll: vi.fn(() => {
|
|
44
|
+
calls.push(["clearAll"]);
|
|
45
|
+
}),
|
|
46
|
+
getText: vi.fn(() => ""),
|
|
47
|
+
getSegmentRange: vi.fn(() => ({ start: 1, end: 5 })),
|
|
48
|
+
getRangeRect: vi.fn(),
|
|
49
|
+
applyOperation: vi.fn(),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
class RuntimeHarness {
|
|
54
|
+
activeTargetId: string | null = "report";
|
|
55
|
+
status: DictationRuntimeStatus = "recording";
|
|
56
|
+
partial: DictationPartialState | null = null;
|
|
57
|
+
error: { code: string; message: string } | null = null;
|
|
58
|
+
resetCalls: Array<{ waitForAck?: boolean } | undefined> = [];
|
|
59
|
+
flashCalls: Array<[TargetBinding, number, number, "revised" | "committed" | undefined]> = [];
|
|
60
|
+
readonly reportBinding = createBinding();
|
|
61
|
+
readonly otherBinding = createBinding();
|
|
62
|
+
readonly noneBinding = createBinding();
|
|
63
|
+
readonly targets = new Map<string, DictationTarget>();
|
|
64
|
+
readonly runtime: DictationRuntime;
|
|
65
|
+
|
|
66
|
+
constructor(voiceFormatting?: { mode: "none" | "dictation" }) {
|
|
67
|
+
this.targets.set("report", this.createTarget("report", "preview-inline", this.reportBinding));
|
|
68
|
+
this.targets.set("other", this.createTarget("other", "preview-inline", this.otherBinding));
|
|
69
|
+
this.targets.set("none", this.createTarget("none", "none", this.noneBinding));
|
|
70
|
+
|
|
71
|
+
const registry: DictationTargetRegistry = {
|
|
72
|
+
get: (id) => this.targets.get(id),
|
|
73
|
+
getActiveTargetId: () => this.activeTargetId,
|
|
74
|
+
getActiveTarget: () => (this.activeTargetId ? this.targets.get(this.activeTargetId) : undefined),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
this.runtime = new DictationRuntime({
|
|
78
|
+
registry,
|
|
79
|
+
getStatus: () => this.status,
|
|
80
|
+
setPartial: (partial) => {
|
|
81
|
+
this.partial = partial;
|
|
82
|
+
},
|
|
83
|
+
setError: (error) => {
|
|
84
|
+
this.error = error;
|
|
85
|
+
},
|
|
86
|
+
setStatus: (status) => {
|
|
87
|
+
this.status = status;
|
|
88
|
+
},
|
|
89
|
+
resetSessionContext: async (options) => {
|
|
90
|
+
this.resetCalls.push(options);
|
|
91
|
+
},
|
|
92
|
+
flashRange: (binding, start, end, variant) => {
|
|
93
|
+
this.flashCalls.push([binding, start, end, variant]);
|
|
94
|
+
},
|
|
95
|
+
voiceFormatting,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private createTarget(
|
|
100
|
+
id: string,
|
|
101
|
+
insertion: DictationTargetInsertionMode,
|
|
102
|
+
binding: TargetBinding
|
|
103
|
+
): DictationTarget {
|
|
104
|
+
const element = document.createElement("div");
|
|
105
|
+
element.dataset.targetId = id;
|
|
106
|
+
document.body.appendChild(element);
|
|
107
|
+
return { id, insertion, binding, element };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
describe("DictationRuntime", () => {
|
|
112
|
+
beforeEach(() => {
|
|
113
|
+
document.body.innerHTML = "";
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("routes previews to the active target and keeps the segment mapping", () => {
|
|
117
|
+
const h = new RuntimeHarness();
|
|
118
|
+
|
|
119
|
+
h.runtime.handleEvent(event("transcript.preview", {
|
|
120
|
+
segmentId: "s1",
|
|
121
|
+
text: "Bonjour",
|
|
122
|
+
revision: 1,
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
expect(h.partial).toEqual({ targetId: "report", text: "Bonjour", segmentId: "s1" });
|
|
126
|
+
expect(h.reportBinding.calls).toContainEqual(["insertPartial", "s1", "Bonjour", 1]);
|
|
127
|
+
expect(h.targets.get("report")?.element.getAttribute("data-ephia-target-processing")).toBe("true");
|
|
128
|
+
|
|
129
|
+
h.runtime.handleEvent(event("transcript.segment.committed", {
|
|
130
|
+
segmentId: "s1",
|
|
131
|
+
text: "Bonjour corrigé",
|
|
132
|
+
source: "live",
|
|
133
|
+
}));
|
|
134
|
+
|
|
135
|
+
expect(h.reportBinding.calls).toContainEqual(["commitFinal", "s1", "Bonjour corrigé", undefined]);
|
|
136
|
+
expect(h.otherBinding.calls).toHaveLength(0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("commits merged segments with absorbed ids and clears absorbed mappings", () => {
|
|
140
|
+
const h = new RuntimeHarness();
|
|
141
|
+
h.runtime.handleEvent(event("transcript.preview", { segmentId: "s1", text: "Bonjour", revision: 1 }));
|
|
142
|
+
h.runtime.handleEvent(event("transcript.preview", { segmentId: "s2", text: "parasite", revision: 1 }));
|
|
143
|
+
|
|
144
|
+
h.runtime.handleEvent(event("transcript.segment.committed", {
|
|
145
|
+
segmentId: "s1",
|
|
146
|
+
text: "Bonjour.",
|
|
147
|
+
mergedWith: ["s2"],
|
|
148
|
+
source: "live",
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
expect(h.partial).toBeNull();
|
|
152
|
+
expect(h.reportBinding.calls).toContainEqual([
|
|
153
|
+
"commitFinal",
|
|
154
|
+
"s1",
|
|
155
|
+
"Bonjour.",
|
|
156
|
+
{ absorbedSegmentIds: ["s2"] },
|
|
157
|
+
]);
|
|
158
|
+
expect(h.flashCalls).toHaveLength(1);
|
|
159
|
+
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("clears preview and resets backend context on user interrupt", async () => {
|
|
163
|
+
const h = new RuntimeHarness();
|
|
164
|
+
const target = h.targets.get("report")!;
|
|
165
|
+
target.element.appendChild(document.createElement("span"));
|
|
166
|
+
const child = target.element.firstChild!;
|
|
167
|
+
|
|
168
|
+
h.runtime.handleEvent(event("transcript.preview", {
|
|
169
|
+
segmentId: "s1",
|
|
170
|
+
text: "preview",
|
|
171
|
+
revision: 1,
|
|
172
|
+
}));
|
|
173
|
+
h.runtime.handleUserInteraction(new KeyboardEvent("keydown", { key: "a", bubbles: true }));
|
|
174
|
+
expect(h.resetCalls).toHaveLength(0);
|
|
175
|
+
|
|
176
|
+
child.dispatchEvent(new KeyboardEvent("keydown", { key: "a", bubbles: true }));
|
|
177
|
+
h.runtime.handleUserInteraction(new KeyboardEvent("keydown", { key: "a", bubbles: true }));
|
|
178
|
+
expect(h.reportBinding.calls).not.toContainEqual(["clearAll"]);
|
|
179
|
+
|
|
180
|
+
const domEvent = new KeyboardEvent("keydown", { key: "a", bubbles: true });
|
|
181
|
+
Object.defineProperty(domEvent, "target", { value: child });
|
|
182
|
+
h.runtime.handleUserInteraction(domEvent);
|
|
183
|
+
await Promise.resolve();
|
|
184
|
+
|
|
185
|
+
expect(h.partial).toBeNull();
|
|
186
|
+
expect(h.reportBinding.calls).toContainEqual(["clearAll"]);
|
|
187
|
+
expect(h.resetCalls).toEqual([{ waitForAck: false }]);
|
|
188
|
+
expect(target.element.hasAttribute("data-ephia-target-processing")).toBe(false);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("respects final-only and none insertion modes", () => {
|
|
192
|
+
const h = new RuntimeHarness();
|
|
193
|
+
h.targets.set("report", {
|
|
194
|
+
...h.targets.get("report")!,
|
|
195
|
+
insertion: "final-only",
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
h.runtime.handleEvent(event("transcript.preview", {
|
|
199
|
+
segmentId: "s1",
|
|
200
|
+
text: "preview",
|
|
201
|
+
revision: 1,
|
|
202
|
+
}));
|
|
203
|
+
expect(h.partial?.text).toBe("preview");
|
|
204
|
+
expect(h.reportBinding.calls).not.toContainEqual(["insertPartial", "s1", "preview", 1]);
|
|
205
|
+
|
|
206
|
+
h.runtime.handleEvent(event("transcript.segment.committed", {
|
|
207
|
+
segmentId: "s1",
|
|
208
|
+
text: "final",
|
|
209
|
+
}));
|
|
210
|
+
expect(h.reportBinding.calls).toContainEqual(["commitFinal", "s1", "final", undefined]);
|
|
211
|
+
|
|
212
|
+
h.activeTargetId = "none";
|
|
213
|
+
h.runtime.handleEvent(event("transcript.preview", {
|
|
214
|
+
segmentId: "s2",
|
|
215
|
+
text: "none preview",
|
|
216
|
+
revision: 1,
|
|
217
|
+
}));
|
|
218
|
+
h.runtime.handleEvent(event("transcript.segment.committed", {
|
|
219
|
+
segmentId: "s2",
|
|
220
|
+
text: "none final",
|
|
221
|
+
}));
|
|
222
|
+
expect(h.noneBinding.calls).toHaveLength(0);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("ignores late transcript events after session closed until a new ready event", () => {
|
|
226
|
+
const h = new RuntimeHarness();
|
|
227
|
+
|
|
228
|
+
h.runtime.handleEvent(event("transcript.preview", { segmentId: "s1", text: "preview", revision: 1 }));
|
|
229
|
+
h.runtime.handleEvent(event("session.closed"));
|
|
230
|
+
h.runtime.handleEvent(event("transcript.segment.committed", { segmentId: "s1", text: "late" }));
|
|
231
|
+
|
|
232
|
+
expect(h.reportBinding.calls).not.toContainEqual(["commitFinal", "s1", "late", undefined]);
|
|
233
|
+
|
|
234
|
+
h.runtime.handleEvent(event("session.ready"));
|
|
235
|
+
h.runtime.handleEvent(event("transcript.preview", { segmentId: "s2", text: "new", revision: 1 }));
|
|
236
|
+
|
|
237
|
+
expect(h.reportBinding.calls).toContainEqual(["insertPartial", "s2", "new", 1]);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("clears only in-flight partial (not committed text) on reset, and clears visual state attributes", () => {
|
|
241
|
+
const h = new RuntimeHarness();
|
|
242
|
+
const target = h.targets.get("report")!;
|
|
243
|
+
|
|
244
|
+
h.runtime.handleEvent(event("transcript.preview", {
|
|
245
|
+
segmentId: "s1",
|
|
246
|
+
text: "preview",
|
|
247
|
+
revision: 1,
|
|
248
|
+
}));
|
|
249
|
+
expect(target.element.getAttribute("data-ephia-target-processing")).toBe("true");
|
|
250
|
+
|
|
251
|
+
h.runtime.handleEvent(event("session.context.reset"));
|
|
252
|
+
expect(h.partial).toBeNull();
|
|
253
|
+
expect(h.reportBinding.calls).toContainEqual(["clearPartial", "s1"]);
|
|
254
|
+
expect(h.reportBinding.calls).not.toContainEqual(["clearAll"]);
|
|
255
|
+
expect(target.element.hasAttribute("data-ephia-target-processing")).toBe(false);
|
|
256
|
+
|
|
257
|
+
h.runtime.handleEvent(event("transcript.preview", {
|
|
258
|
+
segmentId: "s2",
|
|
259
|
+
text: "preview erreur",
|
|
260
|
+
revision: 1,
|
|
261
|
+
}));
|
|
262
|
+
h.runtime.handleEvent(event("transcript.segment.error", {
|
|
263
|
+
segmentId: "s2",
|
|
264
|
+
code: "backend.error",
|
|
265
|
+
message: "Erreur",
|
|
266
|
+
recoverable: true,
|
|
267
|
+
}));
|
|
268
|
+
expect(h.reportBinding.calls).toContainEqual(["clearPartial", "s2"]);
|
|
269
|
+
expect(target.element.hasAttribute("data-ephia-target-processing")).toBe(false);
|
|
270
|
+
|
|
271
|
+
h.runtime.handleEvent(event("transcript.preview", {
|
|
272
|
+
segmentId: "s3",
|
|
273
|
+
text: "preview session error",
|
|
274
|
+
revision: 1,
|
|
275
|
+
}));
|
|
276
|
+
h.runtime.handleEvent(event("session.error", {
|
|
277
|
+
code: "session.error",
|
|
278
|
+
message: "Session error",
|
|
279
|
+
}));
|
|
280
|
+
expect(h.partial).toBeNull();
|
|
281
|
+
expect(h.error).toEqual({ code: "session.error", message: "Session error" });
|
|
282
|
+
expect(h.status).toBe("error");
|
|
283
|
+
expect(target.element.hasAttribute("data-ephia-target-processing")).toBe(false);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("normalizes committed text in dictation mode", () => {
|
|
287
|
+
const h = new RuntimeHarness({ mode: "dictation" });
|
|
288
|
+
|
|
289
|
+
h.runtime.handleEvent(event("transcript.segment.committed", {
|
|
290
|
+
segmentId: "s1",
|
|
291
|
+
text: "rein à la ligne normal",
|
|
292
|
+
}));
|
|
293
|
+
|
|
294
|
+
expect(h.reportBinding.calls).toContainEqual([
|
|
295
|
+
"commitFinal",
|
|
296
|
+
"s1",
|
|
297
|
+
"rein \nNormal",
|
|
298
|
+
undefined,
|
|
299
|
+
]);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("leaves committed text unchanged when voice formatting mode is none", () => {
|
|
303
|
+
const h = new RuntimeHarness({ mode: "none" });
|
|
304
|
+
|
|
305
|
+
h.runtime.handleEvent(event("transcript.segment.committed", {
|
|
306
|
+
segmentId: "s1",
|
|
307
|
+
text: "rein virgule normal",
|
|
308
|
+
}));
|
|
309
|
+
|
|
310
|
+
expect(h.reportBinding.calls).toContainEqual([
|
|
311
|
+
"commitFinal",
|
|
312
|
+
"s1",
|
|
313
|
+
"rein virgule normal",
|
|
314
|
+
undefined,
|
|
315
|
+
]);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("does not call clearAll on binding after session.context.reset — committed text must survive", () => {
|
|
319
|
+
const h = new RuntimeHarness();
|
|
320
|
+
|
|
321
|
+
h.runtime.handleEvent(event("transcript.segment.committed", { segmentId: "s1", text: "texte dicté" }));
|
|
322
|
+
const callsBeforeReset = h.reportBinding.calls.length;
|
|
323
|
+
|
|
324
|
+
h.runtime.handleEvent(event("session.context.reset"));
|
|
325
|
+
|
|
326
|
+
const newCalls = h.reportBinding.calls.slice(callsBeforeReset);
|
|
327
|
+
expect(newCalls).not.toContainEqual(["clearAll"]);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe("RealtimeCommitGate", () => {
|
|
331
|
+
beforeEach(() => {
|
|
332
|
+
vi.useFakeTimers();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
afterEach(() => {
|
|
336
|
+
vi.useRealTimers();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("does not commit a realtime_final immediately when correctionPending is true", () => {
|
|
340
|
+
const h = new RuntimeHarness();
|
|
341
|
+
|
|
342
|
+
h.runtime.handleEvent(event("transcript.segment.committed", {
|
|
343
|
+
segmentId: "s1",
|
|
344
|
+
text: "mauvais début",
|
|
345
|
+
source: "dova-real-time",
|
|
346
|
+
correctionPending: true,
|
|
347
|
+
}));
|
|
348
|
+
|
|
349
|
+
expect(h.reportBinding.calls).not.toContainEqual(["commitFinal", "s1", "mauvais début", undefined]);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("commits the Small correction directly when it arrives before the hold expires, discarding the realtime text", () => {
|
|
353
|
+
const h = new RuntimeHarness();
|
|
354
|
+
|
|
355
|
+
h.runtime.handleEvent(event("transcript.segment.committed", {
|
|
356
|
+
segmentId: "s1",
|
|
357
|
+
text: "mauvais début",
|
|
358
|
+
source: "dova-real-time",
|
|
359
|
+
correctionPending: true,
|
|
360
|
+
}));
|
|
361
|
+
|
|
362
|
+
vi.advanceTimersByTime(100);
|
|
363
|
+
|
|
364
|
+
h.runtime.handleEvent(event("transcript.segment.committed", {
|
|
365
|
+
segmentId: "s1",
|
|
366
|
+
text: "Bon début corrigé.",
|
|
367
|
+
source: "dova-medical",
|
|
368
|
+
corrected: true,
|
|
369
|
+
}));
|
|
370
|
+
|
|
371
|
+
const commitCalls = h.reportBinding.calls.filter((c) => c[0] === "commitFinal");
|
|
372
|
+
expect(commitCalls).toEqual([["commitFinal", "s1", "Bon début corrigé.", undefined]]);
|
|
373
|
+
|
|
374
|
+
// Le timer du realtime ne doit pas committer une seconde fois plus tard.
|
|
375
|
+
vi.advanceTimersByTime(1000);
|
|
376
|
+
expect(h.reportBinding.calls.filter((c) => c[0] === "commitFinal")).toHaveLength(1);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("sends Small final as a revision of the same realtime segmentId after realtime hold", () => {
|
|
380
|
+
const h = new RuntimeHarness();
|
|
381
|
+
|
|
382
|
+
h.runtime.handleEvent(event("transcript.segment.committed", {
|
|
383
|
+
segmentId: "efde10b0",
|
|
384
|
+
text: "\nTechnique :\nacquisition",
|
|
385
|
+
source: "dova-real-time",
|
|
386
|
+
correctionPending: true,
|
|
387
|
+
}));
|
|
388
|
+
|
|
389
|
+
vi.advanceTimersByTime(200);
|
|
390
|
+
|
|
391
|
+
h.runtime.handleEvent(event("transcript.segment.committed", {
|
|
392
|
+
segmentId: "efde10b0",
|
|
393
|
+
text: "\nTechnique :\nacquisition hélicoïdale.",
|
|
394
|
+
textForInsertion: "\nTechnique :\nacquisition hélicoïdale.",
|
|
395
|
+
source: "dova-medical",
|
|
396
|
+
corrected: true,
|
|
397
|
+
correctionPending: false,
|
|
398
|
+
}));
|
|
399
|
+
|
|
400
|
+
expect(h.reportBinding.calls.filter((c) => c[0] === "commitFinal")).toEqual([
|
|
401
|
+
["commitFinal", "efde10b0", "\nTechnique :\nacquisition", undefined],
|
|
402
|
+
["commitFinal", "efde10b0", "\nTechnique :\nacquisition hélicoïdale.", undefined],
|
|
403
|
+
]);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("does not insert an absorbed segment when a merge arrives before its realtime hold expires", () => {
|
|
407
|
+
const h = new RuntimeHarness();
|
|
408
|
+
|
|
409
|
+
h.runtime.handleEvent(event("transcript.segment.committed", {
|
|
410
|
+
segmentId: "s7",
|
|
411
|
+
text: "amincissement des parois adjacentes.",
|
|
412
|
+
source: "dova-real-time",
|
|
413
|
+
correctionPending: true,
|
|
414
|
+
}));
|
|
415
|
+
|
|
416
|
+
h.runtime.handleEvent(event("transcript.segment.committed", {
|
|
417
|
+
segmentId: "s6",
|
|
418
|
+
text: "avec aspect inflammatoire et amincissement des parois adjacentes.",
|
|
419
|
+
source: "dova-medical",
|
|
420
|
+
corrected: true,
|
|
421
|
+
mergedWith: ["s7"],
|
|
422
|
+
}));
|
|
423
|
+
|
|
424
|
+
vi.advanceTimersByTime(1000);
|
|
425
|
+
|
|
426
|
+
const commitCalls = h.reportBinding.calls.filter((c) => c[0] === "commitFinal");
|
|
427
|
+
expect(commitCalls).toEqual([[
|
|
428
|
+
"commitFinal",
|
|
429
|
+
"s6",
|
|
430
|
+
"avec aspect inflammatoire et amincissement des parois adjacentes.",
|
|
431
|
+
{ absorbedSegmentIds: ["s7"] },
|
|
432
|
+
]]);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("ignores a later commit for a segment already absorbed by a merge", () => {
|
|
436
|
+
const h = new RuntimeHarness();
|
|
437
|
+
|
|
438
|
+
h.runtime.handleEvent(event("transcript.segment.committed", {
|
|
439
|
+
segmentId: "s6",
|
|
440
|
+
text: "texte fusionné",
|
|
441
|
+
source: "dova-medical",
|
|
442
|
+
mergedWith: ["s7"],
|
|
443
|
+
}));
|
|
444
|
+
|
|
445
|
+
h.runtime.handleEvent(event("transcript.segment.committed", {
|
|
446
|
+
segmentId: "s7",
|
|
447
|
+
text: "texte absorbé tardif",
|
|
448
|
+
source: "dova-medical",
|
|
449
|
+
}));
|
|
450
|
+
|
|
451
|
+
const commitCalls = h.reportBinding.calls.filter((c) => c[0] === "commitFinal");
|
|
452
|
+
expect(commitCalls).toEqual([[
|
|
453
|
+
"commitFinal",
|
|
454
|
+
"s6",
|
|
455
|
+
"texte fusionné",
|
|
456
|
+
{ absorbedSegmentIds: ["s7"] },
|
|
457
|
+
]]);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("falls back to committing the realtime text after the hold timeout if nothing else arrives", () => {
|
|
461
|
+
const h = new RuntimeHarness();
|
|
462
|
+
|
|
463
|
+
h.runtime.handleEvent(event("transcript.segment.committed", {
|
|
464
|
+
segmentId: "s1",
|
|
465
|
+
text: "texte realtime",
|
|
466
|
+
source: "dova-real-time",
|
|
467
|
+
correctionPending: true,
|
|
468
|
+
}));
|
|
469
|
+
|
|
470
|
+
expect(h.reportBinding.calls).toHaveLength(0);
|
|
471
|
+
|
|
472
|
+
vi.advanceTimersByTime(850);
|
|
473
|
+
|
|
474
|
+
expect(h.reportBinding.calls).toContainEqual(["commitFinal", "s1", "texte realtime", undefined]);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("cancels the pending hold on clear()", () => {
|
|
478
|
+
const h = new RuntimeHarness();
|
|
479
|
+
|
|
480
|
+
h.runtime.handleEvent(event("transcript.segment.committed", {
|
|
481
|
+
segmentId: "s1",
|
|
482
|
+
text: "texte realtime",
|
|
483
|
+
source: "dova-real-time",
|
|
484
|
+
correctionPending: true,
|
|
485
|
+
}));
|
|
486
|
+
|
|
487
|
+
h.runtime.clear();
|
|
488
|
+
vi.advanceTimersByTime(2000);
|
|
489
|
+
|
|
490
|
+
expect(h.reportBinding.calls).toHaveLength(0);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("cancels the pending hold on session.closed", () => {
|
|
494
|
+
const h = new RuntimeHarness();
|
|
495
|
+
|
|
496
|
+
h.runtime.handleEvent(event("transcript.segment.committed", {
|
|
497
|
+
segmentId: "s1",
|
|
498
|
+
text: "texte realtime",
|
|
499
|
+
source: "dova-real-time",
|
|
500
|
+
correctionPending: true,
|
|
501
|
+
}));
|
|
502
|
+
|
|
503
|
+
h.runtime.handleEvent(event("session.closed"));
|
|
504
|
+
vi.advanceTimersByTime(2000);
|
|
505
|
+
|
|
506
|
+
expect(h.reportBinding.calls).not.toContainEqual(["commitFinal", "s1", "texte realtime", undefined]);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("cancels the pending hold when the segment is dropped", () => {
|
|
510
|
+
const h = new RuntimeHarness();
|
|
511
|
+
|
|
512
|
+
h.runtime.handleEvent(event("transcript.segment.committed", {
|
|
513
|
+
segmentId: "s1",
|
|
514
|
+
text: "texte realtime",
|
|
515
|
+
source: "dova-real-time",
|
|
516
|
+
correctionPending: true,
|
|
517
|
+
}));
|
|
518
|
+
|
|
519
|
+
h.runtime.handleEvent(event("segment.dropped", { segmentId: "s1" }));
|
|
520
|
+
vi.advanceTimersByTime(2000);
|
|
521
|
+
|
|
522
|
+
expect(h.reportBinding.calls).not.toContainEqual(["commitFinal", "s1", "texte realtime", undefined]);
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it("uses textForInsertion instead of full committed text", () => {
|
|
527
|
+
const h = new RuntimeHarness();
|
|
528
|
+
|
|
529
|
+
h.runtime.handleEvent(event("transcript.segment.committed", {
|
|
530
|
+
segmentId: "s-insert",
|
|
531
|
+
text: "Le foie est homogène.",
|
|
532
|
+
textForInsertion: "homogène.",
|
|
533
|
+
source: "dova-medical",
|
|
534
|
+
}));
|
|
535
|
+
|
|
536
|
+
expect(h.reportBinding.calls).toContainEqual([
|
|
537
|
+
"commitFinal",
|
|
538
|
+
"s-insert",
|
|
539
|
+
"homogène.",
|
|
540
|
+
undefined,
|
|
541
|
+
]);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("routes backend-targeted finals to their original target instead of the active target", () => {
|
|
545
|
+
const h = new RuntimeHarness();
|
|
546
|
+
h.activeTargetId = "other";
|
|
547
|
+
|
|
548
|
+
h.runtime.handleEvent(event("transcript.segment.committed", {
|
|
549
|
+
segmentId: "s-target",
|
|
550
|
+
targetId: "report",
|
|
551
|
+
text: "Texte cible.",
|
|
552
|
+
source: "dova-medical",
|
|
553
|
+
}));
|
|
554
|
+
|
|
555
|
+
expect(h.reportBinding.calls).toContainEqual([
|
|
556
|
+
"commitFinal",
|
|
557
|
+
"s-target",
|
|
558
|
+
"Texte cible.",
|
|
559
|
+
undefined,
|
|
560
|
+
]);
|
|
561
|
+
expect(h.otherBinding.calls).toHaveLength(0);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it("drops backend-targeted finals for unknown targets instead of using the active target", () => {
|
|
565
|
+
const h = new RuntimeHarness();
|
|
566
|
+
h.activeTargetId = "other";
|
|
567
|
+
|
|
568
|
+
h.runtime.handleEvent(event("transcript.segment.committed", {
|
|
569
|
+
segmentId: "s-missing-target",
|
|
570
|
+
targetId: "missing",
|
|
571
|
+
text: "Ne pas insérer",
|
|
572
|
+
source: "dova-medical",
|
|
573
|
+
}));
|
|
574
|
+
|
|
575
|
+
expect(h.reportBinding.calls).toHaveLength(0);
|
|
576
|
+
expect(h.otherBinding.calls).toHaveLength(0);
|
|
577
|
+
});
|
|
578
|
+
});
|