@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,132 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
normalizePreviewText,
|
|
4
|
+
normalizeCommittedText,
|
|
5
|
+
resolveVoiceFormattingConfig,
|
|
6
|
+
} from "./voice-formatting.normalizer";
|
|
7
|
+
|
|
8
|
+
const dictation = { mode: "dictation" as const };
|
|
9
|
+
const none = { mode: "none" as const };
|
|
10
|
+
|
|
11
|
+
describe("resolveVoiceFormattingConfig", () => {
|
|
12
|
+
it("défaut mode none", () => {
|
|
13
|
+
expect(resolveVoiceFormattingConfig()).toEqual({ mode: "none" });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("map legacy enabled=true vers dictation", () => {
|
|
17
|
+
expect(resolveVoiceFormattingConfig({ enabled: true } as never)).toEqual({
|
|
18
|
+
mode: "dictation",
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("map legacy enabled=false vers none", () => {
|
|
23
|
+
expect(resolveVoiceFormattingConfig({ enabled: false } as never)).toEqual({
|
|
24
|
+
mode: "none",
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("normalizePreviewText — feature désactivée", () => {
|
|
30
|
+
it("retourne le texte intact si mode=none", () => {
|
|
31
|
+
expect(
|
|
32
|
+
normalizePreviewText("foie droit point à la ligne taille normale", none)
|
|
33
|
+
).toBe("foie droit point à la ligne taille normale");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("retourne le texte intact si config undefined", () => {
|
|
37
|
+
expect(normalizePreviewText("virgule rien", undefined)).toBe("virgule rien");
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("normalizePreviewText — sauts de ligne", () => {
|
|
42
|
+
it("à la ligne → \\n + capitalisation", () =>
|
|
43
|
+
expect(normalizePreviewText("foie normal à la ligne vésicule", dictation)).toBe(
|
|
44
|
+
"foie normal \nVésicule"
|
|
45
|
+
));
|
|
46
|
+
|
|
47
|
+
it("nouveau paragraphe → \\n\\n + capitalisation", () =>
|
|
48
|
+
expect(
|
|
49
|
+
normalizePreviewText("conclusion nouveau paragraphe rien à signaler", dictation)
|
|
50
|
+
).toBe("conclusion \n\nRien à signaler"));
|
|
51
|
+
|
|
52
|
+
it("point à la ligne → .\\n + capitalisation", () =>
|
|
53
|
+
expect(
|
|
54
|
+
normalizePreviewText("taille normale point à la ligne échogénicité", dictation)
|
|
55
|
+
).toBe("taille normale .\nÉchogénicité"));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("normalizePreviewText — ponctuation", () => {
|
|
59
|
+
it("virgule → ,", () =>
|
|
60
|
+
expect(normalizePreviewText("rein droit virgule taille normale", dictation)).toBe(
|
|
61
|
+
"rein droit , taille normale"
|
|
62
|
+
));
|
|
63
|
+
|
|
64
|
+
it("point d'interrogation → ? + capitalisation si suite", () =>
|
|
65
|
+
expect(normalizePreviewText("hématome point d'interrogation suite", dictation)).toBe(
|
|
66
|
+
"hématome ? Suite"
|
|
67
|
+
));
|
|
68
|
+
|
|
69
|
+
it("point seul → . + capitalisation", () =>
|
|
70
|
+
expect(normalizePreviewText("taille normale point foie", dictation)).toBe(
|
|
71
|
+
"taille normale . Foie"
|
|
72
|
+
));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("normalizePreviewText — locale en-US", () => {
|
|
76
|
+
it("period → . + capitalisation", () =>
|
|
77
|
+
expect(normalizePreviewText("liver period normal", { mode: "dictation", locale: "en-US" })).toBe(
|
|
78
|
+
"liver . Normal"
|
|
79
|
+
));
|
|
80
|
+
|
|
81
|
+
it("comma → ,", () =>
|
|
82
|
+
expect(
|
|
83
|
+
normalizePreviewText("right kidney comma size normal", { mode: "dictation", locale: "en-US" })
|
|
84
|
+
).toBe("right kidney , size normal"));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("normalizePreviewText — ambiguïté médicale 'point'", () => {
|
|
88
|
+
it("documente le comportement actuel de 'point de rehaussement'", () => {
|
|
89
|
+
expect(normalizePreviewText("point de rehaussement", dictation)).toBe(
|
|
90
|
+
". De rehaussement"
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("normalizeCommittedText", () => {
|
|
96
|
+
it("retourne le texte intact si mode=none", () => {
|
|
97
|
+
expect(normalizeCommittedText("bonjour virgule monde", none)).toBe(
|
|
98
|
+
"bonjour virgule monde"
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("ne convertit pas 'virgule' en mode dictation (ponctuation exclue du committed)", () => {
|
|
103
|
+
expect(normalizeCommittedText("bonjour virgule monde", dictation)).toBe(
|
|
104
|
+
"bonjour virgule monde"
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("ne crée pas de double point sur 'Point' commité (ponctuation exclue du committed)", () => {
|
|
109
|
+
expect(normalizeCommittedText("aiguë. Point reste.", dictation)).toBe(
|
|
110
|
+
"aiguë. Point reste."
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("à la ligne + virgule Voxtral → \\n + majuscule sans virgule", () => {
|
|
115
|
+
// Voxtral sort "À la ligne, petite dilatation..." → doit devenir "\nPetite dilatation..."
|
|
116
|
+
expect(
|
|
117
|
+
normalizeCommittedText("À la ligne, petite dilatation modérée.", dictation)
|
|
118
|
+
).toBe("\nPetite dilatation modérée.");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("à la ligne + point-virgule Voxtral → \\n + majuscule sans point-virgule", () => {
|
|
122
|
+
expect(
|
|
123
|
+
normalizeCommittedText("À la ligne; bonne aération sinusienne.", dictation)
|
|
124
|
+
).toBe("\nBonne aération sinusienne.");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("à la ligne sans ponctuation parasite → \\n + majuscule", () => {
|
|
128
|
+
expect(
|
|
129
|
+
normalizeCommittedText("À la ligne bonne aération sinusienne.", dictation)
|
|
130
|
+
).toBe("\nBonne aération sinusienne.");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voice Formatting Normalizer
|
|
3
|
+
*
|
|
4
|
+
* Intercepte les commandes vocales dictées et les substitue par leur
|
|
5
|
+
* équivalent typographique (preview realtime et texte committé).
|
|
6
|
+
* Désactivé par défaut (mode: "none").
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { EphiaAudioEvent } from "ephia-protocol";
|
|
10
|
+
|
|
11
|
+
export type VoiceFormattingMode = "none" | "dictation";
|
|
12
|
+
|
|
13
|
+
export interface VoiceFormattingConfig {
|
|
14
|
+
/**
|
|
15
|
+
* Mode de traitement de la ponctuation vocale.
|
|
16
|
+
* - "none" : aucun traitement (défaut)
|
|
17
|
+
* - "dictation" : substitution des commandes vocales sur preview ET commit
|
|
18
|
+
*/
|
|
19
|
+
mode: VoiceFormattingMode;
|
|
20
|
+
/** Locale pour le dictionnaire de commandes. Défaut: 'fr-FR'. */
|
|
21
|
+
locale?: "fr-FR" | "en-US";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Legacy shape still accepted by resolveVoiceFormattingConfig. */
|
|
25
|
+
type LegacyVoiceFormattingConfig = VoiceFormattingConfig & {
|
|
26
|
+
enabled?: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
interface VoiceCommandEntry {
|
|
30
|
+
/** Texte de remplacement */
|
|
31
|
+
replace: string;
|
|
32
|
+
/** Si true, le prochain caractère alphabétique sera capitalisé */
|
|
33
|
+
capitalize: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type VoiceCommandMap = Record<string, VoiceCommandEntry>;
|
|
37
|
+
|
|
38
|
+
// ─── Dictionnaires ────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
const PREVIEW_COMMANDS_FR: VoiceCommandMap = {
|
|
41
|
+
// Sauts de ligne / structure
|
|
42
|
+
"point à la ligne": { replace: ".\n", capitalize: true },
|
|
43
|
+
"à la ligne": { replace: "\n", capitalize: true },
|
|
44
|
+
"retour à la ligne": { replace: "\n", capitalize: true },
|
|
45
|
+
"saut de ligne": { replace: "\n", capitalize: true },
|
|
46
|
+
"nouveau paragraphe": { replace: "\n\n", capitalize: true },
|
|
47
|
+
"nouvelle ligne": { replace: "\n", capitalize: true },
|
|
48
|
+
|
|
49
|
+
// Ponctuation de fin
|
|
50
|
+
point: { replace: ". ", capitalize: true },
|
|
51
|
+
"point final": { replace: ". ", capitalize: true },
|
|
52
|
+
virgule: { replace: ", ", capitalize: false },
|
|
53
|
+
"point-virgule": { replace: "; ", capitalize: false },
|
|
54
|
+
"point virgule": { replace: "; ", capitalize: false },
|
|
55
|
+
"deux points": { replace: " : ", capitalize: false },
|
|
56
|
+
"point d'interrogation": { replace: " ? ", capitalize: true },
|
|
57
|
+
"point d'exclamation": { replace: " ! ", capitalize: true },
|
|
58
|
+
tiret: { replace: " — ", capitalize: false },
|
|
59
|
+
"tiret long": { replace: " — ", capitalize: false },
|
|
60
|
+
|
|
61
|
+
// Parenthèses / guillemets
|
|
62
|
+
"ouvrir parenthèse": { replace: " (", capitalize: false },
|
|
63
|
+
"fermer parenthèse": { replace: ") ", capitalize: false },
|
|
64
|
+
"ouvrir guillemets": { replace: " « ", capitalize: false },
|
|
65
|
+
"fermer guillemets": { replace: " » ", capitalize: false },
|
|
66
|
+
"guillemet ouvrant": { replace: " « ", capitalize: false },
|
|
67
|
+
"guillemet fermant": { replace: " » ", capitalize: false },
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Texte commité uniquement : structure/sauts de ligne.
|
|
71
|
+
// Ponctuation exclue — déjà résolue côté backend (Voxtral prompt, apply_dictation_commands).
|
|
72
|
+
const COMMITTED_COMMANDS_FR: VoiceCommandMap = {
|
|
73
|
+
"point à la ligne": { replace: ".\n", capitalize: true },
|
|
74
|
+
"à la ligne": { replace: "\n", capitalize: true },
|
|
75
|
+
"retour à la ligne": { replace: "\n", capitalize: true },
|
|
76
|
+
"saut de ligne": { replace: "\n", capitalize: true },
|
|
77
|
+
"nouveau paragraphe":{ replace: "\n\n", capitalize: true },
|
|
78
|
+
"nouvelle ligne": { replace: "\n", capitalize: true },
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const COMMANDS_EN: VoiceCommandMap = {
|
|
82
|
+
"new line": { replace: "\n", capitalize: true },
|
|
83
|
+
"new paragraph": { replace: "\n\n", capitalize: true },
|
|
84
|
+
period: { replace: ". ", capitalize: true },
|
|
85
|
+
"full stop": { replace: ". ", capitalize: true },
|
|
86
|
+
comma: { replace: ", ", capitalize: false },
|
|
87
|
+
semicolon: { replace: "; ", capitalize: false },
|
|
88
|
+
colon: { replace: ": ", capitalize: false },
|
|
89
|
+
"question mark": { replace: "? ", capitalize: true },
|
|
90
|
+
"exclamation mark": { replace: "! ", capitalize: true },
|
|
91
|
+
"open parenthesis": { replace: " (", capitalize: false },
|
|
92
|
+
"close parenthesis": { replace: ") ", capitalize: false },
|
|
93
|
+
"open quote": { replace: ' "', capitalize: false },
|
|
94
|
+
"close quote": { replace: '" ', capitalize: false },
|
|
95
|
+
dash: { replace: " — ", capitalize: false },
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const DEFAULT_VOICE_FORMATTING: VoiceFormattingConfig = { mode: "none" };
|
|
99
|
+
|
|
100
|
+
export function resolveVoiceFormattingConfig(
|
|
101
|
+
input?: LegacyVoiceFormattingConfig | null
|
|
102
|
+
): VoiceFormattingConfig {
|
|
103
|
+
if (!input) return DEFAULT_VOICE_FORMATTING;
|
|
104
|
+
|
|
105
|
+
if ("mode" in input && input.mode) {
|
|
106
|
+
return {
|
|
107
|
+
mode: input.mode,
|
|
108
|
+
locale: input.locale,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if ("enabled" in input && typeof input.enabled === "boolean") {
|
|
113
|
+
return {
|
|
114
|
+
mode: input.enabled ? "dictation" : "none",
|
|
115
|
+
locale: input.locale,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return DEFAULT_VOICE_FORMATTING;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Builder ──────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
function buildCommandMap(
|
|
125
|
+
locale: VoiceFormattingConfig["locale"] = "fr-FR",
|
|
126
|
+
baseOverride?: VoiceCommandMap,
|
|
127
|
+
): VoiceCommandMap {
|
|
128
|
+
return baseOverride ?? (locale === "en-US" ? COMMANDS_EN : PREVIEW_COMMANDS_FR);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Trie les commandes par longueur décroissante pour garantir que
|
|
133
|
+
* "point d'interrogation" est testé avant "point".
|
|
134
|
+
*/
|
|
135
|
+
function sortedCommands(map: VoiceCommandMap): Array<[string, VoiceCommandEntry]> {
|
|
136
|
+
return Object.entries(map).sort((a, b) => b[0].length - a[0].length);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ─── Moteur de substitution ───────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
const _regexpCache = new Map<string, RegExp>();
|
|
142
|
+
|
|
143
|
+
function getCommandPattern(cmd: string): RegExp {
|
|
144
|
+
if (!_regexpCache.has(cmd)) {
|
|
145
|
+
const escaped = cmd.replace(/[-]/g, "\\-");
|
|
146
|
+
_regexpCache.set(cmd, new RegExp(`(?<![\\wÀ-ÿ])(${escaped})(?![\\wÀ-ÿ])`, "gi"));
|
|
147
|
+
}
|
|
148
|
+
return _regexpCache.get(cmd)!;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function applyCommands(text: string, commands: Array<[string, VoiceCommandEntry]>): string {
|
|
152
|
+
let result = text;
|
|
153
|
+
for (const [cmd, entry] of commands) {
|
|
154
|
+
const pattern = getCommandPattern(cmd);
|
|
155
|
+
result = result.replace(pattern, () => {
|
|
156
|
+
if (entry.capitalize) {
|
|
157
|
+
return entry.replace + "\u0001";
|
|
158
|
+
}
|
|
159
|
+
return entry.replace;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function applyCapitalization(text: string): string {
|
|
166
|
+
// After a newline command (\n\u0001), strip orphaned comma/semicolon that Voxtral
|
|
167
|
+
// appends naturally to "à la ligne" — e.g. "à la ligne, petite" → "\nPetite" not "\n, petite"
|
|
168
|
+
let result = text.replace(/\n\u0001[ \t]*[,;][ \t]*/g, "\n\u0001");
|
|
169
|
+
result = result.replace(/\u0001\s*([a-zA-ZÀ-ÿ])/g, (_, letter) => letter.toUpperCase());
|
|
170
|
+
result = result.replace(/\u0001/g, "");
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function normalizeVoiceText(text: string, config?: VoiceFormattingConfig, baseOverride?: VoiceCommandMap): string {
|
|
175
|
+
if (!text) return text;
|
|
176
|
+
if (!config || config.mode === "none") return text;
|
|
177
|
+
|
|
178
|
+
const commandMap = buildCommandMap(config.locale, baseOverride);
|
|
179
|
+
const commands = sortedCommands(commandMap);
|
|
180
|
+
|
|
181
|
+
const lower = text.toLowerCase();
|
|
182
|
+
const hasCandidate = commands.some(([cmd]) => lower.includes(cmd.split(" ")[0]));
|
|
183
|
+
if (!hasCandidate) return text;
|
|
184
|
+
|
|
185
|
+
let result = applyCommands(text, commands);
|
|
186
|
+
result = applyCapitalization(result);
|
|
187
|
+
result = result.replace(/ {2,}/g, " ");
|
|
188
|
+
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─── Export principal ─────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Normalise un texte de preview realtime en appliquant les substitutions
|
|
196
|
+
* de commandes vocales si voiceFormatting.mode === "dictation".
|
|
197
|
+
*/
|
|
198
|
+
export function normalizePreviewText(
|
|
199
|
+
text: string,
|
|
200
|
+
config?: VoiceFormattingConfig
|
|
201
|
+
): string {
|
|
202
|
+
return normalizeVoiceText(text, config);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Normalise le texte d'un segment committé (transcript.segment.committed).
|
|
207
|
+
* En mode dictation : applique uniquement les commandes structurelles (sauts de ligne).
|
|
208
|
+
* La ponctuation n'est pas retraduite — elle a déjà été résolue côté backend.
|
|
209
|
+
*/
|
|
210
|
+
export function normalizeCommittedText(
|
|
211
|
+
text: string,
|
|
212
|
+
config?: VoiceFormattingConfig
|
|
213
|
+
): string {
|
|
214
|
+
return normalizeVoiceText(text, config, COMMITTED_COMMANDS_FR);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function formatEventPayloadText(
|
|
218
|
+
eventType: string,
|
|
219
|
+
payload: Record<string, unknown>,
|
|
220
|
+
config: VoiceFormattingConfig
|
|
221
|
+
): Record<string, unknown> {
|
|
222
|
+
const next = { ...payload };
|
|
223
|
+
|
|
224
|
+
if (
|
|
225
|
+
eventType === "transcript.preview" ||
|
|
226
|
+
eventType === "transcript.preview.stable"
|
|
227
|
+
) {
|
|
228
|
+
if (typeof payload.text === "string") {
|
|
229
|
+
next.text = normalizePreviewText(payload.text, config);
|
|
230
|
+
}
|
|
231
|
+
return next;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (eventType === "transcript.final") {
|
|
235
|
+
if (typeof payload.text === "string") {
|
|
236
|
+
next.text = normalizeCommittedText(payload.text, config);
|
|
237
|
+
}
|
|
238
|
+
return next;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (eventType === "transcript.segment.ready") {
|
|
242
|
+
const text = typeof payload.text === "string" ? payload.text : undefined;
|
|
243
|
+
const delta = typeof payload.delta === "string" ? payload.delta : undefined;
|
|
244
|
+
const effective = delta ?? text;
|
|
245
|
+
if (effective) {
|
|
246
|
+
const normalized = normalizeCommittedText(effective, config);
|
|
247
|
+
if (delta) next.delta = normalized;
|
|
248
|
+
else next.text = normalized;
|
|
249
|
+
}
|
|
250
|
+
return next;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (eventType === "transcript.segment.committed") {
|
|
254
|
+
const text = typeof payload.text === "string" ? payload.text : "";
|
|
255
|
+
const delta = typeof payload.delta === "string" ? payload.delta : undefined;
|
|
256
|
+
const effective = delta ?? text;
|
|
257
|
+
const normalized = normalizeCommittedText(effective, config);
|
|
258
|
+
if (delta) next.delta = normalized;
|
|
259
|
+
next.text = normalized;
|
|
260
|
+
return next;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return next;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Retourne une copie de l'event avec le texte normalisé (mode dictation).
|
|
268
|
+
* L'event d'origine n'est pas muté.
|
|
269
|
+
*/
|
|
270
|
+
export function applyVoiceFormattingToEvent(
|
|
271
|
+
event: EphiaAudioEvent,
|
|
272
|
+
config?: VoiceFormattingConfig
|
|
273
|
+
): EphiaAudioEvent {
|
|
274
|
+
const resolved = resolveVoiceFormattingConfig(config);
|
|
275
|
+
if (resolved.mode === "none") return event;
|
|
276
|
+
|
|
277
|
+
const payload = event.payload as Record<string, unknown> | undefined;
|
|
278
|
+
if (!payload) return event;
|
|
279
|
+
|
|
280
|
+
const nextPayload = formatEventPayloadText(event.type, payload, resolved);
|
|
281
|
+
if (nextPayload === payload) return event;
|
|
282
|
+
|
|
283
|
+
return { ...event, payload: nextPayload } as EphiaAudioEvent;
|
|
284
|
+
}
|