@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,152 @@
|
|
|
1
|
+
import type { LiveTranscriptChunk } from "./transcript.state";
|
|
2
|
+
|
|
3
|
+
function trimBoundarySpaces(text: string): string {
|
|
4
|
+
return text
|
|
5
|
+
.replace(/\r\n/g, "\n")
|
|
6
|
+
.replace(/\r/g, "\n")
|
|
7
|
+
.replace(/^[ \t]+|[ \t]+$/g, "");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Joint deux morceaux de texte avec gestion intelligente des espaces et de la ponctuation.
|
|
12
|
+
* Exported pour être réutilisé dans les autres reducers.
|
|
13
|
+
*/
|
|
14
|
+
export function joinWithSpacing(prev: string, next: string): string {
|
|
15
|
+
if (!prev) return next;
|
|
16
|
+
if (!next) return prev;
|
|
17
|
+
const lastChar = prev.slice(-1);
|
|
18
|
+
const firstChar = next.charAt(0);
|
|
19
|
+
if (lastChar === " " && firstChar === " ") return prev + next.trimStart();
|
|
20
|
+
if (/\s/.test(lastChar) || /\s/.test(firstChar)) return prev + next;
|
|
21
|
+
if (/[,.;:!?)\]]/.test(firstChar)) return prev + next;
|
|
22
|
+
return prev + " " + next;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Assemble les chunks en texte cohérent.
|
|
27
|
+
*
|
|
28
|
+
* Règles :
|
|
29
|
+
* - tri par segmentSeq
|
|
30
|
+
* - pas de double espace
|
|
31
|
+
* - espace après ponctuation si manquant
|
|
32
|
+
* - ignorer les chunks error sans texte
|
|
33
|
+
*/
|
|
34
|
+
export function assembleTranscript(chunks: LiveTranscriptChunk[]): string {
|
|
35
|
+
const sorted = [...chunks]
|
|
36
|
+
.filter((c) => (c.status === "done" || c.finalizedText) && (c.text || c.finalizedText))
|
|
37
|
+
.sort((a, b) => a.segmentSeq - b.segmentSeq);
|
|
38
|
+
|
|
39
|
+
if (sorted.length === 0) return "";
|
|
40
|
+
|
|
41
|
+
let result = "";
|
|
42
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
43
|
+
const chunk = sorted[i];
|
|
44
|
+
let text = trimBoundarySpaces(chunk.text || chunk.finalizedText || "");
|
|
45
|
+
|
|
46
|
+
if (i === 0) {
|
|
47
|
+
result = text;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const prev = result;
|
|
52
|
+
const lastChar = prev.slice(-1);
|
|
53
|
+
const firstChar = text.charAt(0);
|
|
54
|
+
|
|
55
|
+
// Règles d'espacement
|
|
56
|
+
if (lastChar.match(/[.!?;,:\)]/) && firstChar.match(/[a-zA-ZÀ-ÿ0-9]/)) {
|
|
57
|
+
result = prev + " " + text;
|
|
58
|
+
} else if (/\s/.test(lastChar) && /\s/.test(firstChar)) {
|
|
59
|
+
result = prev + text.trimStart();
|
|
60
|
+
} else if (
|
|
61
|
+
!/\s/.test(lastChar) &&
|
|
62
|
+
!/\s/.test(firstChar) &&
|
|
63
|
+
firstChar.match(/[a-zA-ZÀ-ÿ0-9]/)
|
|
64
|
+
) {
|
|
65
|
+
result = prev + " " + text;
|
|
66
|
+
} else {
|
|
67
|
+
result = prev + text;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Assemble les chunks commités en document texte.
|
|
76
|
+
*
|
|
77
|
+
* Règles :
|
|
78
|
+
* - tri par segmentSeq
|
|
79
|
+
* - utilise committedText ou text si disponible
|
|
80
|
+
* - ignore les chunks fusionnés (mergedWith présent)
|
|
81
|
+
*/
|
|
82
|
+
export function assembleCommittedDocument(
|
|
83
|
+
chunks: Record<string, LiveTranscriptChunk>,
|
|
84
|
+
orderedSegmentIds: string[]
|
|
85
|
+
): string {
|
|
86
|
+
const sorted = [...orderedSegmentIds]
|
|
87
|
+
.map((id) => chunks[id])
|
|
88
|
+
.filter((c): c is LiveTranscriptChunk => !!c && c.status === "done")
|
|
89
|
+
.filter((c) => !c.mergedWith || Array.isArray(c.mergedWith))
|
|
90
|
+
.sort((a, b) => a.segmentSeq - b.segmentSeq);
|
|
91
|
+
|
|
92
|
+
if (sorted.length === 0) return "";
|
|
93
|
+
|
|
94
|
+
let result = "";
|
|
95
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
96
|
+
const chunk = sorted[i];
|
|
97
|
+
const text = trimBoundarySpaces(chunk.committedText ?? chunk.text ?? "");
|
|
98
|
+
if (!text) continue;
|
|
99
|
+
|
|
100
|
+
if (i === 0) {
|
|
101
|
+
result = text;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const prev = result;
|
|
106
|
+
const lastChar = prev.slice(-1);
|
|
107
|
+
const firstChar = text.charAt(0);
|
|
108
|
+
|
|
109
|
+
if (lastChar.match(/[.!?;,:\)]/) && firstChar.match(/[a-zA-ZÀ-ÿ0-9]/)) {
|
|
110
|
+
result = prev + " " + text;
|
|
111
|
+
} else if (/\s/.test(lastChar) && /\s/.test(firstChar)) {
|
|
112
|
+
result = prev + text.trimStart();
|
|
113
|
+
} else if (
|
|
114
|
+
!/\s/.test(lastChar) &&
|
|
115
|
+
!/\s/.test(firstChar) &&
|
|
116
|
+
firstChar.match(/[a-zA-ZÀ-ÿ0-9]/)
|
|
117
|
+
) {
|
|
118
|
+
result = prev + " " + text;
|
|
119
|
+
} else {
|
|
120
|
+
result = prev + text;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Construit le texte d'affichage complet :
|
|
129
|
+
* finalText + previewText du chunk courant.
|
|
130
|
+
*/
|
|
131
|
+
const PUNCTUATION_NO_SPACE = new Set([",", ".", ";", ":", "!", "?", ")", "]", "}"]);
|
|
132
|
+
|
|
133
|
+
export function buildDisplayText(
|
|
134
|
+
finalText: string,
|
|
135
|
+
previewText: string,
|
|
136
|
+
previewContinuesPreviousWord = false
|
|
137
|
+
): string {
|
|
138
|
+
if (!finalText && !previewText) return "";
|
|
139
|
+
if (!finalText) return previewText;
|
|
140
|
+
if (!previewText) return finalText;
|
|
141
|
+
// trimEnd() supprimerait les \n intentionnels (commandes "à la ligne")
|
|
142
|
+
const prev = finalText.replace(/[ \t]+$/, "");
|
|
143
|
+
const next = previewText.trimStart();
|
|
144
|
+
if (!next) return prev;
|
|
145
|
+
if (previewContinuesPreviousWord) return prev + next;
|
|
146
|
+
const firstChar = next[0];
|
|
147
|
+
if (firstChar && PUNCTUATION_NO_SPACE.has(firstChar)) {
|
|
148
|
+
return prev + next;
|
|
149
|
+
}
|
|
150
|
+
if (prev.endsWith("\n")) return prev + next;
|
|
151
|
+
return prev + " " + next;
|
|
152
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { liveTranscriptReducer } from "./transcript.reducer";
|
|
3
|
+
import { initialLiveTranscriptState } from "./transcript.state";
|
|
4
|
+
import type { LiveTranscriptAction } from "./transcript.reducer";
|
|
5
|
+
import { PROTOCOL_VERSION, type EphiaAudioEvent } from "ephia-protocol";
|
|
6
|
+
|
|
7
|
+
function makeEnvelope<T extends string>(
|
|
8
|
+
type: T,
|
|
9
|
+
payload: Record<string, unknown>,
|
|
10
|
+
seq = 1
|
|
11
|
+
): EphiaAudioEvent {
|
|
12
|
+
return {
|
|
13
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
14
|
+
eventId: `00000000-0000-4000-8000-${String(seq).padStart(12, "0")}`,
|
|
15
|
+
sessionId: "test-session",
|
|
16
|
+
type,
|
|
17
|
+
seq,
|
|
18
|
+
timestampMs: Date.now(),
|
|
19
|
+
payload,
|
|
20
|
+
} as unknown as EphiaAudioEvent;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function event(type: string, payload: Record<string, unknown>, seq = 1): LiveTranscriptAction {
|
|
24
|
+
return { type: "event", event: makeEnvelope(type, payload, seq) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ------------------------------------------------------------------
|
|
28
|
+
// segment.audio.closed — conserver le preview jusqu'au committed
|
|
29
|
+
// ------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
describe("segment.audio.closed", () => {
|
|
32
|
+
it("keeps preview text visible while waiting for committed", () => {
|
|
33
|
+
let state = initialLiveTranscriptState;
|
|
34
|
+
|
|
35
|
+
state = liveTranscriptReducer(
|
|
36
|
+
state,
|
|
37
|
+
event("segment.opened", { segmentId: "seg1", segmentSeq: 1, startMs: 0 }, 1)
|
|
38
|
+
);
|
|
39
|
+
state = liveTranscriptReducer(
|
|
40
|
+
state,
|
|
41
|
+
event(
|
|
42
|
+
"transcript.preview",
|
|
43
|
+
{ segmentId: "seg1", segmentSeq: 1, text: "Bonjour docteur", revision: 1 },
|
|
44
|
+
2
|
|
45
|
+
)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
state = liveTranscriptReducer(
|
|
49
|
+
state,
|
|
50
|
+
event(
|
|
51
|
+
"segment.audio.closed",
|
|
52
|
+
{ segmentId: "seg1", segmentSeq: 1, endMs: 1000, reason: "vad_end" },
|
|
53
|
+
3
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
expect(state.chunks["seg1"]?.previewText).toBe("Bonjour docteur");
|
|
58
|
+
expect(state.chunks["seg1"]?.status).toBe("processing");
|
|
59
|
+
expect(state.displayText).toContain("Bonjour docteur");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("enriches preview after audio closed when a later partial arrives", () => {
|
|
63
|
+
let state = initialLiveTranscriptState;
|
|
64
|
+
|
|
65
|
+
state = liveTranscriptReducer(
|
|
66
|
+
state,
|
|
67
|
+
event("segment.opened", { segmentId: "seg1", segmentSeq: 1, startMs: 0 }, 1)
|
|
68
|
+
);
|
|
69
|
+
state = liveTranscriptReducer(
|
|
70
|
+
state,
|
|
71
|
+
event(
|
|
72
|
+
"transcript.preview",
|
|
73
|
+
{ segmentId: "seg1", segmentSeq: 1, text: "Bonjour", revision: 1 },
|
|
74
|
+
2
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
state = liveTranscriptReducer(
|
|
78
|
+
state,
|
|
79
|
+
event(
|
|
80
|
+
"segment.audio.closed",
|
|
81
|
+
{ segmentId: "seg1", segmentSeq: 1, endMs: 1000, reason: "vad_end" },
|
|
82
|
+
3
|
|
83
|
+
)
|
|
84
|
+
);
|
|
85
|
+
state = liveTranscriptReducer(
|
|
86
|
+
state,
|
|
87
|
+
event(
|
|
88
|
+
"transcript.preview",
|
|
89
|
+
{ segmentId: "seg1", segmentSeq: 1, text: "Bonjour docteur", revision: 6 },
|
|
90
|
+
4
|
|
91
|
+
)
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(state.chunks["seg1"]?.status).toBe("preview");
|
|
95
|
+
expect(state.chunks["seg1"]?.previewText).toBe("Bonjour docteur");
|
|
96
|
+
expect(state.chunks["seg1"]?.revision).toBe(6);
|
|
97
|
+
expect(state.displayText).toContain("Bonjour docteur");
|
|
98
|
+
expect(state.orderedSegmentIds).toEqual(["seg1"]);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ------------------------------------------------------------------
|
|
103
|
+
// Replace Model: partial ignoré après committed
|
|
104
|
+
// ------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
describe("Replace Model — partial after committed", () => {
|
|
107
|
+
it("does not overwrite committed text when a late partial arrives", () => {
|
|
108
|
+
let state = initialLiveTranscriptState;
|
|
109
|
+
|
|
110
|
+
// Segment opened
|
|
111
|
+
state = liveTranscriptReducer(state, event("segment.opened", { segmentId: "seg1", segmentSeq: 1, startMs: 0 }, 1));
|
|
112
|
+
// Committed
|
|
113
|
+
state = liveTranscriptReducer(state, event("transcript.segment.committed", {
|
|
114
|
+
segmentId: "seg1",
|
|
115
|
+
segmentSeq: 1,
|
|
116
|
+
text: "Texte final commité.",
|
|
117
|
+
source: "live",
|
|
118
|
+
}, 2));
|
|
119
|
+
|
|
120
|
+
expect(state.chunks["seg1"]?.status).toBe("done");
|
|
121
|
+
expect(state.chunks["seg1"]?.text).toBe("Texte final commité.");
|
|
122
|
+
|
|
123
|
+
// Late partial — status is already "done", partial should be ignored
|
|
124
|
+
state = liveTranscriptReducer(state, event("transcript.preview", {
|
|
125
|
+
segmentId: "seg1",
|
|
126
|
+
segmentSeq: 1,
|
|
127
|
+
text: "texte partiel tardif",
|
|
128
|
+
revision: 99,
|
|
129
|
+
}, 3));
|
|
130
|
+
|
|
131
|
+
expect(state.chunks["seg1"]?.text).toBe("Texte final commité.");
|
|
132
|
+
expect(state.chunks["seg1"]?.previewText).toBeUndefined();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ------------------------------------------------------------------
|
|
137
|
+
// session.context.reset — conservation des chunks finalisés
|
|
138
|
+
// ------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
describe("session.context.reset", () => {
|
|
141
|
+
it("keeps finalized chunks without committedText on contextOnly reset", () => {
|
|
142
|
+
let state = initialLiveTranscriptState;
|
|
143
|
+
|
|
144
|
+
// Simulate a finalized segment arriving via transcript.final
|
|
145
|
+
// (no committedText set, only status="done" + text)
|
|
146
|
+
state = liveTranscriptReducer(
|
|
147
|
+
state,
|
|
148
|
+
event("transcript.final", {
|
|
149
|
+
segmentId: "seg-final",
|
|
150
|
+
segmentSeq: 1,
|
|
151
|
+
text: "Texte finalisé sans committedText.",
|
|
152
|
+
provider: "voxtral-small",
|
|
153
|
+
}, 1)
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
expect(state.chunks["seg-final"]?.status).toBe("done");
|
|
157
|
+
expect(state.chunks["seg-final"]?.committedText).toBeUndefined();
|
|
158
|
+
|
|
159
|
+
// Context-only reset (emitted by the backend when the client stops)
|
|
160
|
+
state = liveTranscriptReducer(
|
|
161
|
+
state,
|
|
162
|
+
event("session.context.reset", { contextOnly: true }, 2)
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// The finalized chunk must survive the reset
|
|
166
|
+
expect(state.chunks["seg-final"]).toBeDefined();
|
|
167
|
+
expect(state.chunks["seg-final"]?.status).toBe("done");
|
|
168
|
+
expect(state.chunks["seg-final"]?.text).toBe("Texte finalisé sans committedText.");
|
|
169
|
+
expect(state.orderedSegmentIds).toContain("seg-final");
|
|
170
|
+
expect(state.documentText).toContain("Texte finalisé sans committedText.");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ------------------------------------------------------------------
|
|
175
|
+
// Sequence gap detection
|
|
176
|
+
// ------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
describe("sequence gap detection", () => {
|
|
179
|
+
it("marks seqValid false when events arrive out of order", () => {
|
|
180
|
+
let state = initialLiveTranscriptState;
|
|
181
|
+
|
|
182
|
+
state = liveTranscriptReducer(state, event("session.ready", { roomName: "room1" }, 1));
|
|
183
|
+
expect(state.diagnostics.seqValid).toBe(true);
|
|
184
|
+
|
|
185
|
+
// Skip seq 2, jump to 3
|
|
186
|
+
state = liveTranscriptReducer(state, event("audio.vad.start", { timestampMs: 0 }, 3));
|
|
187
|
+
expect(state.diagnostics.seqValid).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("keeps seqValid true for monotone consecutive events", () => {
|
|
191
|
+
let state = initialLiveTranscriptState;
|
|
192
|
+
|
|
193
|
+
state = liveTranscriptReducer(state, event("session.ready", { roomName: "room1" }, 1));
|
|
194
|
+
state = liveTranscriptReducer(state, event("audio.vad.start", { timestampMs: 0 }, 2));
|
|
195
|
+
state = liveTranscriptReducer(state, event("audio.vad.end", { timestampMs: 100, durationMs: 100 }, 3));
|
|
196
|
+
|
|
197
|
+
expect(state.diagnostics.seqValid).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
});
|