@djangocfg/ui-tools 2.1.381 → 2.1.383
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 +132 -899
- package/dist/ChatRoot-6IZFM5HM.mjs +5 -0
- package/dist/{ChatRoot-EJC5Y2YM.cjs.map → ChatRoot-6IZFM5HM.mjs.map} +1 -1
- package/dist/ChatRoot-LW4XNIKP.cjs +14 -0
- package/dist/{ChatRoot-QOSKJPM6.mjs.map → ChatRoot-LW4XNIKP.cjs.map} +1 -1
- package/dist/DictationField-U25MEYAL.mjs +4 -0
- package/dist/DictationField-U25MEYAL.mjs.map +1 -0
- package/dist/DictationField-XWR5VOID.cjs +13 -0
- package/dist/DictationField-XWR5VOID.cjs.map +1 -0
- package/dist/{DocsLayout-2YKPXZYO.mjs → DocsLayout-2P3ONDWJ.mjs} +3 -3
- package/dist/{DocsLayout-2YKPXZYO.mjs.map → DocsLayout-2P3ONDWJ.mjs.map} +1 -1
- package/dist/{DocsLayout-Q4KS3QWW.cjs → DocsLayout-2YZNS5VK.cjs} +8 -8
- package/dist/{DocsLayout-Q4KS3QWW.cjs.map → DocsLayout-2YZNS5VK.cjs.map} +1 -1
- package/dist/chunk-4PFW7MIJ.cjs +837 -0
- package/dist/chunk-4PFW7MIJ.cjs.map +1 -0
- package/dist/chunk-C2YN6WEO.mjs +833 -0
- package/dist/chunk-C2YN6WEO.mjs.map +1 -0
- package/dist/{chunk-XACCHZH2.cjs → chunk-FIRK5CEH.cjs} +42 -4
- package/dist/chunk-FIRK5CEH.cjs.map +1 -0
- package/dist/{chunk-NWUT327A.mjs → chunk-HIK6BPL7.mjs} +38 -5
- package/dist/chunk-HIK6BPL7.mjs.map +1 -0
- package/dist/chunk-OZAU3QWD.cjs +2493 -0
- package/dist/chunk-OZAU3QWD.cjs.map +1 -0
- package/dist/chunk-UWVP6LCW.mjs +2447 -0
- package/dist/chunk-UWVP6LCW.mjs.map +1 -0
- package/dist/index.cjs +1668 -99
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1215 -107
- package/dist/index.d.ts +1215 -107
- package/dist/index.mjs +1555 -50
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -15
- package/src/audio-assets.d.ts +8 -0
- package/src/components/markdown/MarkdownMessage/CollapseToggle.tsx +3 -1
- package/src/components/markdown/MarkdownMessage/components.tsx +2 -5
- package/src/tools/Chat/README.md +347 -530
- package/src/tools/Chat/components/Attachments.tsx +6 -1
- package/src/tools/Chat/components/ChatRoot.tsx +30 -2
- package/src/tools/Chat/components/Composer.tsx +20 -3
- package/src/tools/Chat/components/ErrorBanner.tsx +7 -3
- package/src/tools/Chat/components/MessageActions.tsx +3 -1
- package/src/tools/Chat/components/MessageBubble.tsx +6 -5
- package/src/tools/Chat/components/MessageList.tsx +87 -1
- package/src/tools/Chat/components/ToolCalls.tsx +21 -3
- package/src/tools/Chat/context/ChatProvider.tsx +21 -3
- package/src/tools/Chat/core/audio/audioBus.ts +10 -163
- package/src/tools/Chat/core/audio/defaults.ts +43 -0
- package/src/tools/Chat/core/audio/index.ts +1 -0
- package/src/tools/Chat/core/audio/preferences.ts +5 -59
- package/src/tools/Chat/core/audio/sounds/error.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/mention.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/notification.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/received.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/sent.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/start.mp3 +0 -0
- package/src/tools/Chat/core/audio/types.ts +28 -0
- package/src/tools/Chat/core/reducer.ts +33 -0
- package/src/tools/Chat/core/transport/index.ts +13 -0
- package/src/tools/Chat/core/transport/mappers/index.ts +6 -0
- package/src/tools/Chat/core/transport/mappers/pydantic-ai.ts +142 -0
- package/src/tools/Chat/core/transport/pydantic-ai-transport.ts +208 -0
- package/src/tools/Chat/core/transport/sse.ts +18 -5
- package/src/tools/Chat/hooks/index.ts +25 -0
- package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +5 -3
- package/src/tools/Chat/hooks/useChat.ts +28 -0
- package/src/tools/Chat/hooks/useChatAudio.ts +59 -180
- package/src/tools/Chat/hooks/useChatDockPrefs.ts +74 -0
- package/src/tools/Chat/hooks/useChatReset.ts +70 -0
- package/src/tools/Chat/hooks/useChatUnread.ts +87 -0
- package/src/tools/Chat/hooks/useFocusOnEmptyClick.ts +111 -0
- package/src/tools/Chat/hooks/useVisitorFingerprint.ts +48 -0
- package/src/tools/Chat/index.ts +84 -1
- package/src/tools/Chat/launcher/ChatDock.tsx +263 -0
- package/src/tools/Chat/launcher/ChatFAB.tsx +349 -0
- package/src/tools/Chat/launcher/ChatGreeting.tsx +200 -0
- package/src/tools/Chat/launcher/ChatHeader.tsx +76 -0
- package/src/tools/Chat/launcher/ChatHeaderActionButton.tsx +87 -0
- package/src/tools/Chat/launcher/ChatHeaderAudioToggle.tsx +47 -0
- package/src/tools/Chat/launcher/ChatHeaderLanguageButton.tsx +179 -0
- package/src/tools/Chat/launcher/ChatHeaderModeToggle.tsx +57 -0
- package/src/tools/Chat/launcher/ChatHeaderResetButton.tsx +93 -0
- package/src/tools/Chat/launcher/ChatLauncher.tsx +321 -0
- package/src/tools/Chat/launcher/ChatUnreadPreview.tsx +197 -0
- package/src/tools/Chat/launcher/index.ts +46 -0
- package/src/tools/Chat/launcher/useChatPresence.ts +44 -0
- package/src/tools/Chat/styles/bubbleTokens.ts +71 -0
- package/src/tools/Chat/styles/index.ts +16 -0
- package/src/tools/Chat/styles/useChatStyles.ts +101 -0
- package/src/tools/Chat/types/attachment.ts +25 -0
- package/src/tools/Chat/types/config.ts +48 -0
- package/src/tools/Chat/types/events.ts +35 -0
- package/src/tools/Chat/types/index.ts +34 -0
- package/src/tools/Chat/types/labels.ts +38 -0
- package/src/tools/Chat/types/message.ts +32 -0
- package/src/tools/Chat/types/persona.ts +31 -0
- package/src/tools/Chat/types/session.ts +43 -0
- package/src/tools/Chat/types/tool-call.ts +17 -0
- package/src/tools/Chat/types/transport.ts +28 -0
- package/src/tools/Chat/types.ts +5 -240
- package/src/tools/MarkdownEditor/MarkdownEditor.tsx +50 -14
- package/src/tools/MarkdownEditor/index.ts +1 -1
- package/src/tools/SpeechRecognition/README.md +336 -0
- package/src/tools/SpeechRecognition/__tests__/ids.test.ts +15 -0
- package/src/tools/SpeechRecognition/__tests__/language.test.ts +59 -0
- package/src/tools/SpeechRecognition/__tests__/reducer.test.ts +71 -0
- package/src/tools/SpeechRecognition/__tests__/transcript.test.ts +52 -0
- package/src/tools/SpeechRecognition/components/DevicePicker.tsx +49 -0
- package/src/tools/SpeechRecognition/components/DictationButton.tsx +93 -0
- package/src/tools/SpeechRecognition/components/EngineBadge.tsx +30 -0
- package/src/tools/SpeechRecognition/components/ErrorBanner.tsx +52 -0
- package/src/tools/SpeechRecognition/components/LanguagePicker.tsx +63 -0
- package/src/tools/SpeechRecognition/components/MicMeter.tsx +63 -0
- package/src/tools/SpeechRecognition/components/PushToTalkHint.tsx +51 -0
- package/src/tools/SpeechRecognition/components/TranscriptView.tsx +55 -0
- package/src/tools/SpeechRecognition/components/index.ts +16 -0
- package/src/tools/SpeechRecognition/context/SpeechRecognitionProvider.tsx +47 -0
- package/src/tools/SpeechRecognition/context/index.ts +6 -0
- package/src/tools/SpeechRecognition/core/audio/defaults.ts +24 -0
- package/src/tools/SpeechRecognition/core/engine/external.ts +222 -0
- package/src/tools/SpeechRecognition/core/engine/http.ts +147 -0
- package/src/tools/SpeechRecognition/core/engine/index.ts +52 -0
- package/src/tools/SpeechRecognition/core/engine/mediarecorder.ts +105 -0
- package/src/tools/SpeechRecognition/core/engine/websocket.ts +211 -0
- package/src/tools/SpeechRecognition/core/engine/webspeech.ts +188 -0
- package/src/tools/SpeechRecognition/core/ids.ts +11 -0
- package/src/tools/SpeechRecognition/core/index.ts +14 -0
- package/src/tools/SpeechRecognition/core/language.ts +78 -0
- package/src/tools/SpeechRecognition/core/languages-catalog.ts +229 -0
- package/src/tools/SpeechRecognition/core/logger.ts +3 -0
- package/src/tools/SpeechRecognition/core/reducer.ts +105 -0
- package/src/tools/SpeechRecognition/core/transcript.ts +36 -0
- package/src/tools/SpeechRecognition/hooks/index.ts +14 -0
- package/src/tools/SpeechRecognition/hooks/useDictation.ts +59 -0
- package/src/tools/SpeechRecognition/hooks/useEnginePrefs.ts +15 -0
- package/src/tools/SpeechRecognition/hooks/useMicDevices.ts +57 -0
- package/src/tools/SpeechRecognition/hooks/useMicLevel.ts +52 -0
- package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +85 -0
- package/src/tools/SpeechRecognition/hooks/useResolvedLanguage.ts +28 -0
- package/src/tools/SpeechRecognition/hooks/useSpeechLanguageInfo.ts +108 -0
- package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +188 -0
- package/src/tools/SpeechRecognition/hooks/useVoiceSupport.ts +78 -0
- package/src/tools/SpeechRecognition/index.ts +82 -0
- package/src/tools/SpeechRecognition/lazy.tsx +19 -0
- package/src/tools/SpeechRecognition/store/index.ts +2 -0
- package/src/tools/SpeechRecognition/store/prefsStore.ts +54 -0
- package/src/tools/SpeechRecognition/types.ts +133 -0
- package/src/tools/SpeechRecognition/widgets/DictationField.tsx +105 -0
- package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +305 -0
- package/src/tools/SpeechRecognition/widgets/VoiceMessageRecorder.tsx +88 -0
- package/src/tools/SpeechRecognition/widgets/index.ts +6 -0
- package/dist/ChatRoot-EJC5Y2YM.cjs +0 -14
- package/dist/ChatRoot-QOSKJPM6.mjs +0 -5
- package/dist/chunk-NWUT327A.mjs.map +0 -1
- package/dist/chunk-QLMKCSR6.mjs +0 -2420
- package/dist/chunk-QLMKCSR6.mjs.map +0 -1
- package/dist/chunk-SI5RD2GD.cjs +0 -2460
- package/dist/chunk-SI5RD2GD.cjs.map +0 -1
- package/dist/chunk-XACCHZH2.cjs.map +0 -1
- package/src/components/markdown/MarkdownMessage/MarkdownMessage.story.tsx +0 -771
- package/src/stories/index.ts +0 -33
- package/src/tools/AudioPlayer/AudioPlayer.story.tsx +0 -481
- package/src/tools/Chat/Chat.story.tsx +0 -1457
- package/src/tools/CodeEditor/CodeEditor.story.tsx +0 -202
- package/src/tools/CronScheduler/CronScheduler.story.tsx +0 -300
- package/src/tools/Gallery/Gallery.story.tsx +0 -237
- package/src/tools/ImageViewer/ImageViewer.story.tsx +0 -85
- package/src/tools/JsonForm/JsonForm.story.tsx +0 -350
- package/src/tools/JsonTree/JsonTree.story.tsx +0 -141
- package/src/tools/LottiePlayer/LottiePlayer.story.tsx +0 -95
- package/src/tools/Map/Map.story.tsx +0 -458
- package/src/tools/MarkdownEditor/MarkdownEditor.story.tsx +0 -225
- package/src/tools/Mermaid/Mermaid.story.tsx +0 -251
- package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +0 -230
- package/src/tools/PrettyCode/PrettyCode.story.tsx +0 -304
- package/src/tools/Tour/Tour.story.tsx +0 -279
- package/src/tools/Tree/Tree.story.tsx +0 -620
- package/src/tools/Uploader/Uploader.story.tsx +0 -415
- package/src/tools/VideoPlayer/VideoPlayer.story.tsx +0 -87
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useLocaleOptional } from '@djangocfg/i18n';
|
|
4
|
+
|
|
5
|
+
import { resolveSpeechLanguage } from '../core/language';
|
|
6
|
+
import { useSpeechPrefs } from '../store/prefsStore';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolves the BCP-47 language tag a speech session should use.
|
|
10
|
+
*
|
|
11
|
+
* Priority: explicit prop → user-picked `useSpeechPrefs.language` →
|
|
12
|
+
* app i18n locale (when an `<I18nProvider>` is mounted) →
|
|
13
|
+
* `navigator.language` → `en-US`.
|
|
14
|
+
*
|
|
15
|
+
* Uses `useLocaleOptional` (not `useLocale`) so that an unmounted
|
|
16
|
+
* provider doesn't silently inject `'en'`. Without that, the i18n
|
|
17
|
+
* default would shadow the user's real browser language — a Russian
|
|
18
|
+
* speaker would always get `en-US` recognition.
|
|
19
|
+
*/
|
|
20
|
+
export function useResolvedLanguage(explicit?: string): string {
|
|
21
|
+
const prefs = useSpeechPrefs();
|
|
22
|
+
const locale = useLocaleOptional();
|
|
23
|
+
return resolveSpeechLanguage({
|
|
24
|
+
explicit,
|
|
25
|
+
prefs: prefs.language,
|
|
26
|
+
i18n: locale,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
countryFromTag,
|
|
7
|
+
findSpeechLanguage,
|
|
8
|
+
} from '../core/languages-catalog';
|
|
9
|
+
import { useSpeechPrefs } from '../store/prefsStore';
|
|
10
|
+
import { useResolvedLanguage } from './useResolvedLanguage';
|
|
11
|
+
|
|
12
|
+
export interface SpeechLanguageInfo {
|
|
13
|
+
/**
|
|
14
|
+
* BCP-47 tag that `useSpeechRecognition` will actually pass to the
|
|
15
|
+
* engine right now. Always non-null (falls through `prefs → i18n →
|
|
16
|
+
* navigator → 'en-US'`).
|
|
17
|
+
*/
|
|
18
|
+
tag: string;
|
|
19
|
+
/**
|
|
20
|
+
* ISO-639 primary subtag of `tag` (`ru`, `en`, `cmn`). Useful as a
|
|
21
|
+
* map key when the host wires its own per-language behaviour
|
|
22
|
+
* (avatars, copy variants, etc.).
|
|
23
|
+
*/
|
|
24
|
+
iso: string;
|
|
25
|
+
/**
|
|
26
|
+
* ISO-3166 alpha-2 country code extracted from `tag` (`RU`, `US`,
|
|
27
|
+
* `CN`). `null` when the tag has no region subtag — rare for our
|
|
28
|
+
* catalogue (every entry ships at least one regional dialect) but
|
|
29
|
+
* possible for custom-engine tags supplied by hosts.
|
|
30
|
+
*/
|
|
31
|
+
country: string | null;
|
|
32
|
+
/**
|
|
33
|
+
* Native-script language name (`'Русский'`, `'中文'`). `null` for
|
|
34
|
+
* tags outside our catalogue.
|
|
35
|
+
*/
|
|
36
|
+
name: string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Lowercase English name (`'russian'`, `'chinese'`). `null` for
|
|
39
|
+
* tags outside our catalogue. Handy for analytics or English-only
|
|
40
|
+
* UI surfaces.
|
|
41
|
+
*/
|
|
42
|
+
englishName: string | null;
|
|
43
|
+
/**
|
|
44
|
+
* Region label for the current dialect (`'Russia'`, `'United
|
|
45
|
+
* Kingdom'`, `'香港'`). `null` for unknown tags.
|
|
46
|
+
*/
|
|
47
|
+
region: string | null;
|
|
48
|
+
/**
|
|
49
|
+
* `true` iff the user explicitly picked a language via
|
|
50
|
+
* `<ChatHeaderLanguageButton>` / `useSpeechPrefs.setLanguage(...)`.
|
|
51
|
+
* `false` means the resolved tag came from a lower-priority source
|
|
52
|
+
* (i18n locale, `navigator.language`, default).
|
|
53
|
+
*
|
|
54
|
+
* Use this when you want to distinguish "user told us to use ru-RU"
|
|
55
|
+
* from "we guessed ru-RU from browser headers". Analytics often
|
|
56
|
+
* cares about that distinction.
|
|
57
|
+
*/
|
|
58
|
+
hasUserChoice: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* One-shot read of "what speech language is active right now and why".
|
|
63
|
+
*
|
|
64
|
+
* Pulls together `useResolvedLanguage`, `useSpeechPrefs.language`, and
|
|
65
|
+
* the static catalogue (`findSpeechLanguage` / `countryFromTag`) into
|
|
66
|
+
* a single memoised object so consumers don't have to compose them by
|
|
67
|
+
* hand for every header badge / analytics call / persisted-state push.
|
|
68
|
+
*
|
|
69
|
+
* Reactive: re-renders when the user picks a different language in
|
|
70
|
+
* the chat header, when i18n locale changes, or when the underlying
|
|
71
|
+
* `navigator.language` reading flips (mount-time only).
|
|
72
|
+
*
|
|
73
|
+
* @example Render a status badge somewhere in the app shell
|
|
74
|
+
* ```tsx
|
|
75
|
+
* const { tag, name, country } = useSpeechLanguageInfo();
|
|
76
|
+
* return (
|
|
77
|
+
* <Badge>
|
|
78
|
+
* <Flag countryCode={country} />
|
|
79
|
+
* {name ?? tag}
|
|
80
|
+
* </Badge>
|
|
81
|
+
* );
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* @example Sync the user's STT pick to a backend setting
|
|
85
|
+
* ```tsx
|
|
86
|
+
* const { tag, hasUserChoice } = useSpeechLanguageInfo();
|
|
87
|
+
* useEffect(() => {
|
|
88
|
+
* if (!hasUserChoice) return; // skip auto-resolved values
|
|
89
|
+
* void api.user.update({ speechLanguage: tag });
|
|
90
|
+
* }, [tag, hasUserChoice]);
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export function useSpeechLanguageInfo(): SpeechLanguageInfo {
|
|
94
|
+
const prefs = useSpeechPrefs();
|
|
95
|
+
const tag = useResolvedLanguage();
|
|
96
|
+
return useMemo<SpeechLanguageInfo>(() => {
|
|
97
|
+
const found = findSpeechLanguage(tag);
|
|
98
|
+
return {
|
|
99
|
+
tag,
|
|
100
|
+
iso: found?.language.iso ?? tag.split('-')[0].toLowerCase(),
|
|
101
|
+
country: countryFromTag(tag),
|
|
102
|
+
name: found?.language.name ?? null,
|
|
103
|
+
englishName: found?.language.englishName ?? null,
|
|
104
|
+
region: found?.dialect.region ?? null,
|
|
105
|
+
hasUserChoice: prefs.language !== null,
|
|
106
|
+
};
|
|
107
|
+
}, [tag, prefs.language]);
|
|
108
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
EMPTY_TRANSCRIPT,
|
|
7
|
+
INITIAL_STATE,
|
|
8
|
+
buildTranscript,
|
|
9
|
+
reducer,
|
|
10
|
+
sttLogger,
|
|
11
|
+
} from '../core';
|
|
12
|
+
import { createWebSpeechEngine } from '../core/engine/webspeech';
|
|
13
|
+
import { useSpeechPrefs } from '../store/prefsStore';
|
|
14
|
+
import type {
|
|
15
|
+
RecognitionEngine,
|
|
16
|
+
Segment,
|
|
17
|
+
UseSpeechRecognitionConfig,
|
|
18
|
+
UseSpeechRecognitionReturn,
|
|
19
|
+
} from '../types';
|
|
20
|
+
import { useMicLevel } from './useMicLevel';
|
|
21
|
+
import { useResolvedLanguage } from './useResolvedLanguage';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Main entry point. With no config it uses the browser Web Speech API
|
|
25
|
+
* and the persisted language from `useSpeechPrefs`. Pass a custom
|
|
26
|
+
* `engine` to route through Deepgram / Whisper / custom WebSocket.
|
|
27
|
+
*/
|
|
28
|
+
export function useSpeechRecognition(
|
|
29
|
+
config: UseSpeechRecognitionConfig = {},
|
|
30
|
+
): UseSpeechRecognitionReturn {
|
|
31
|
+
const prefs = useSpeechPrefs();
|
|
32
|
+
const language = useResolvedLanguage(config.language);
|
|
33
|
+
const engine = useMemo<RecognitionEngine>(
|
|
34
|
+
() => config.engine ?? createWebSpeechEngine(),
|
|
35
|
+
[config.engine],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const [state, dispatch] = useReducer(reducer, INITIAL_STATE);
|
|
39
|
+
const [stream, setStream] = useState<MediaStream | null>(null);
|
|
40
|
+
const level = useMicLevel(stream);
|
|
41
|
+
|
|
42
|
+
// Latest-callback refs so engine subscriptions never tear down on
|
|
43
|
+
// every render — same trick the Chat reducer uses.
|
|
44
|
+
const cbRef = useRef(config);
|
|
45
|
+
cbRef.current = config;
|
|
46
|
+
|
|
47
|
+
// Engine subscription lifecycle.
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const offs = [
|
|
50
|
+
engine.on('partial', (text, segmentId) => {
|
|
51
|
+
dispatch({ type: 'PARTIAL', text, segmentId });
|
|
52
|
+
const seg: Segment = {
|
|
53
|
+
id: segmentId,
|
|
54
|
+
text,
|
|
55
|
+
isFinal: false,
|
|
56
|
+
startedAt: Date.now(),
|
|
57
|
+
};
|
|
58
|
+
cbRef.current.onPartial?.(text, seg);
|
|
59
|
+
}),
|
|
60
|
+
engine.on('final', (text, segmentId, confidence) => {
|
|
61
|
+
dispatch({ type: 'FINAL', text, segmentId, confidence });
|
|
62
|
+
const seg: Segment = {
|
|
63
|
+
id: segmentId,
|
|
64
|
+
text,
|
|
65
|
+
isFinal: true,
|
|
66
|
+
confidence,
|
|
67
|
+
startedAt: Date.now(),
|
|
68
|
+
endedAt: Date.now(),
|
|
69
|
+
};
|
|
70
|
+
cbRef.current.onFinal?.(text, seg);
|
|
71
|
+
}),
|
|
72
|
+
engine.on('error', (err) => {
|
|
73
|
+
dispatch({ type: 'ERROR', error: err });
|
|
74
|
+
cbRef.current.onError?.(err);
|
|
75
|
+
}),
|
|
76
|
+
engine.on('state', (s) => {
|
|
77
|
+
if (s === 'listening') {
|
|
78
|
+
dispatch({ type: 'STARTED' });
|
|
79
|
+
cbRef.current.onStart?.();
|
|
80
|
+
setStream(engine.getStream?.() ?? null);
|
|
81
|
+
} else if (s === 'closed') {
|
|
82
|
+
dispatch({ type: 'STOPPED' });
|
|
83
|
+
cbRef.current.onStop?.();
|
|
84
|
+
setStream(null);
|
|
85
|
+
}
|
|
86
|
+
}),
|
|
87
|
+
];
|
|
88
|
+
return () => {
|
|
89
|
+
offs.forEach((off) => off());
|
|
90
|
+
};
|
|
91
|
+
}, [engine]);
|
|
92
|
+
|
|
93
|
+
// AutoStop driven by silence + maxMs caps.
|
|
94
|
+
const silenceTimer = useRef<number | null>(null);
|
|
95
|
+
const maxTimer = useRef<number | null>(null);
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (state.status !== 'listening') return undefined;
|
|
98
|
+
const { silenceMs, maxMs, silenceThreshold = 0.02 } = config.autoStop ?? {};
|
|
99
|
+
if (maxMs) {
|
|
100
|
+
maxTimer.current = window.setTimeout(() => {
|
|
101
|
+
sttLogger.debug('[autoStop] max duration hit');
|
|
102
|
+
void engine.stop();
|
|
103
|
+
}, maxMs);
|
|
104
|
+
}
|
|
105
|
+
if (silenceMs) {
|
|
106
|
+
const checkInterval = window.setInterval(() => {
|
|
107
|
+
if (level < silenceThreshold) {
|
|
108
|
+
if (silenceTimer.current == null) {
|
|
109
|
+
silenceTimer.current = window.setTimeout(() => {
|
|
110
|
+
sttLogger.debug('[autoStop] silence detected');
|
|
111
|
+
void engine.stop();
|
|
112
|
+
}, silenceMs);
|
|
113
|
+
}
|
|
114
|
+
} else if (silenceTimer.current != null) {
|
|
115
|
+
clearTimeout(silenceTimer.current);
|
|
116
|
+
silenceTimer.current = null;
|
|
117
|
+
}
|
|
118
|
+
}, 200);
|
|
119
|
+
return () => {
|
|
120
|
+
clearInterval(checkInterval);
|
|
121
|
+
if (silenceTimer.current != null) clearTimeout(silenceTimer.current);
|
|
122
|
+
silenceTimer.current = null;
|
|
123
|
+
if (maxTimer.current != null) clearTimeout(maxTimer.current);
|
|
124
|
+
maxTimer.current = null;
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return () => {
|
|
128
|
+
if (maxTimer.current != null) clearTimeout(maxTimer.current);
|
|
129
|
+
maxTimer.current = null;
|
|
130
|
+
};
|
|
131
|
+
}, [state.status, config.autoStop, level, engine]);
|
|
132
|
+
|
|
133
|
+
const start = useCallback(async () => {
|
|
134
|
+
if (state.status === 'listening' || state.status === 'starting') return;
|
|
135
|
+
dispatch({ type: 'START' });
|
|
136
|
+
try {
|
|
137
|
+
await engine.start({
|
|
138
|
+
language,
|
|
139
|
+
interim: config.interim ?? true,
|
|
140
|
+
deviceId: config.deviceId ?? prefs.deviceId ?? undefined,
|
|
141
|
+
});
|
|
142
|
+
} catch (cause) {
|
|
143
|
+
// engine already emitted 'error'; reducer caught it via subscription
|
|
144
|
+
sttLogger.debug('[start] engine threw', cause);
|
|
145
|
+
}
|
|
146
|
+
}, [engine, language, config.interim, config.deviceId, prefs.deviceId, state.status]);
|
|
147
|
+
|
|
148
|
+
const stop = useCallback(async () => {
|
|
149
|
+
if (state.status === 'idle' || state.status === 'stopping') return;
|
|
150
|
+
dispatch({ type: 'STOP' });
|
|
151
|
+
await engine.stop();
|
|
152
|
+
}, [engine, state.status]);
|
|
153
|
+
|
|
154
|
+
const abort = useCallback(() => {
|
|
155
|
+
engine.abort();
|
|
156
|
+
dispatch({ type: 'ABORT' });
|
|
157
|
+
}, [engine]);
|
|
158
|
+
|
|
159
|
+
const toggle = useCallback(async () => {
|
|
160
|
+
if (state.status === 'listening' || state.status === 'starting') {
|
|
161
|
+
await stop();
|
|
162
|
+
} else {
|
|
163
|
+
await start();
|
|
164
|
+
}
|
|
165
|
+
}, [state.status, start, stop]);
|
|
166
|
+
|
|
167
|
+
const reset = useCallback(() => {
|
|
168
|
+
dispatch({ type: 'RESET' });
|
|
169
|
+
}, []);
|
|
170
|
+
|
|
171
|
+
const transcript = useMemo(
|
|
172
|
+
() => (state.segments.length === 0 ? EMPTY_TRANSCRIPT : buildTranscript(state.segments)),
|
|
173
|
+
[state.segments],
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
status: state.status,
|
|
178
|
+
isSupported: engine.isSupported,
|
|
179
|
+
transcript,
|
|
180
|
+
error: state.error,
|
|
181
|
+
level,
|
|
182
|
+
start,
|
|
183
|
+
stop,
|
|
184
|
+
abort,
|
|
185
|
+
toggle,
|
|
186
|
+
reset,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
|
|
5
|
+
import { useBrowserDetect, useDeviceDetect } from '@djangocfg/ui-core/hooks';
|
|
6
|
+
|
|
7
|
+
import type { RecognitionEngine } from '../types';
|
|
8
|
+
import { createWebSpeechEngine } from '../core/engine/webspeech';
|
|
9
|
+
|
|
10
|
+
export type VoiceUnsupportedReason =
|
|
11
|
+
| 'no-engine'
|
|
12
|
+
| 'no-mediadevices'
|
|
13
|
+
| 'in-app-browser'
|
|
14
|
+
| 'unknown';
|
|
15
|
+
|
|
16
|
+
export interface VoiceSupport {
|
|
17
|
+
/** Should the host render the voice button at all? */
|
|
18
|
+
supported: boolean;
|
|
19
|
+
/** Why is it off? `null` when supported. */
|
|
20
|
+
reason: VoiceUnsupportedReason | null;
|
|
21
|
+
/** Engine id that would be used if rendered (for badges / telemetry). */
|
|
22
|
+
engineId: string | null;
|
|
23
|
+
/** True on a mobile UA — useful for hiding on phones if the host wants. */
|
|
24
|
+
isMobile: boolean;
|
|
25
|
+
/** True inside Facebook / Instagram / TikTok / etc. in-app browsers. */
|
|
26
|
+
isInApp: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Decides whether the `<VoiceComposerSlot>` should render the mic
|
|
31
|
+
* button. Three gates:
|
|
32
|
+
*
|
|
33
|
+
* 1. A `RecognitionEngine` reports `isSupported === true`. With no
|
|
34
|
+
* custom engine passed we probe `createWebSpeechEngine()` —
|
|
35
|
+
* Chrome / Edge / Safari pass, Firefox does not.
|
|
36
|
+
* 2. `navigator.mediaDevices.getUserMedia` exists. Required even when
|
|
37
|
+
* using Web Speech (browser still asks the user for mic access).
|
|
38
|
+
* 3. The host browser is not a known in-app WebView (Instagram,
|
|
39
|
+
* Facebook, TikTok, …) — mic permission flows are broken in most
|
|
40
|
+
* of them. The host can override this gate by passing a custom
|
|
41
|
+
* engine that already handles the case.
|
|
42
|
+
*/
|
|
43
|
+
export function useVoiceSupport(engine?: RecognitionEngine): VoiceSupport {
|
|
44
|
+
const browser = useBrowserDetect();
|
|
45
|
+
const device = useDeviceDetect();
|
|
46
|
+
|
|
47
|
+
return useMemo<VoiceSupport>(() => {
|
|
48
|
+
const isMobile = device.selectors.isMobile;
|
|
49
|
+
const isInApp = browser.isInAppBrowser;
|
|
50
|
+
|
|
51
|
+
const hasMediaDevices =
|
|
52
|
+
typeof navigator !== 'undefined' && !!navigator.mediaDevices?.getUserMedia;
|
|
53
|
+
if (!hasMediaDevices) {
|
|
54
|
+
return { supported: false, reason: 'no-mediadevices', engineId: null, isMobile, isInApp };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// If the host didn't pass a custom engine we can only fall back to
|
|
58
|
+
// the browser Web Speech API — bail when it's not available
|
|
59
|
+
// (Firefox / some WebViews).
|
|
60
|
+
const probe = engine ?? createWebSpeechEngine();
|
|
61
|
+
if (!probe.isSupported) {
|
|
62
|
+
// In-app browsers are the most common reason a probe fails on
|
|
63
|
+
// mobile; surface a friendlier code so the UI can hide silently.
|
|
64
|
+
if (isInApp) {
|
|
65
|
+
return { supported: false, reason: 'in-app-browser', engineId: null, isMobile, isInApp };
|
|
66
|
+
}
|
|
67
|
+
return { supported: false, reason: 'no-engine', engineId: null, isMobile, isInApp };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
supported: true,
|
|
72
|
+
reason: null,
|
|
73
|
+
engineId: probe.id,
|
|
74
|
+
isMobile,
|
|
75
|
+
isInApp,
|
|
76
|
+
};
|
|
77
|
+
}, [engine, browser.isInAppBrowser, device.selectors.isMobile]);
|
|
78
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @djangocfg/ui-tools/speech-recognition
|
|
3
|
+
*
|
|
4
|
+
* Decomposed Speech-to-Text tool. Default backend is the browser Web
|
|
5
|
+
* Speech API; custom engines can be plugged in for cloud or self-hosted
|
|
6
|
+
* STT (Deepgram, Whisper, AssemblyAI, custom WebSocket).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use client';
|
|
10
|
+
|
|
11
|
+
export type {
|
|
12
|
+
AutoStopOptions,
|
|
13
|
+
EngineEventMap,
|
|
14
|
+
EngineStartOptions,
|
|
15
|
+
EngineState,
|
|
16
|
+
RecognitionEngine,
|
|
17
|
+
RecognitionError,
|
|
18
|
+
RecognitionErrorCode,
|
|
19
|
+
RecognitionStatus,
|
|
20
|
+
Segment,
|
|
21
|
+
Transcript,
|
|
22
|
+
Unsub,
|
|
23
|
+
UseSpeechRecognitionConfig,
|
|
24
|
+
UseSpeechRecognitionReturn,
|
|
25
|
+
} from './types';
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
EMPTY_TRANSCRIPT,
|
|
29
|
+
buildTranscript,
|
|
30
|
+
joinFinal,
|
|
31
|
+
newSegmentId,
|
|
32
|
+
normaliseFinal,
|
|
33
|
+
} from './core';
|
|
34
|
+
|
|
35
|
+
export { createEngineBus } from './core/engine';
|
|
36
|
+
export { createWebSpeechEngine } from './core/engine/webspeech';
|
|
37
|
+
export type { WebSpeechEngineOptions } from './core/engine/webspeech';
|
|
38
|
+
export { createHttpEngine } from './core/engine/http';
|
|
39
|
+
export type {
|
|
40
|
+
HttpEngineOptions,
|
|
41
|
+
HttpEngineParseResult,
|
|
42
|
+
} from './core/engine/http';
|
|
43
|
+
export { createWebSocketEngine } from './core/engine/websocket';
|
|
44
|
+
export type {
|
|
45
|
+
WebSocketEngineOptions,
|
|
46
|
+
WsParsedEvent,
|
|
47
|
+
} from './core/engine/websocket';
|
|
48
|
+
export { createExternalEngine } from './core/engine/external';
|
|
49
|
+
export type {
|
|
50
|
+
ExternalEngineHandle,
|
|
51
|
+
ExternalEngineOptions,
|
|
52
|
+
} from './core/engine/external';
|
|
53
|
+
export { pickMime, startMicCapture } from './core/engine/mediarecorder';
|
|
54
|
+
export type {
|
|
55
|
+
MicCaptureHandle,
|
|
56
|
+
MicCaptureOptions,
|
|
57
|
+
} from './core/engine/mediarecorder';
|
|
58
|
+
|
|
59
|
+
export * from './hooks';
|
|
60
|
+
export * from './components';
|
|
61
|
+
export * from './widgets';
|
|
62
|
+
export * from './context';
|
|
63
|
+
export { LazyDictationField } from './lazy';
|
|
64
|
+
export { useSpeechPrefs } from './store';
|
|
65
|
+
export type { SpeechPrefs } from './store';
|
|
66
|
+
export { DEFAULT_VOICE_SOUNDS } from './core/audio/defaults';
|
|
67
|
+
export type { VoiceSoundEvent } from './core/audio/defaults';
|
|
68
|
+
export {
|
|
69
|
+
DEFAULT_ISO_TO_BCP47,
|
|
70
|
+
resolveSpeechLanguage,
|
|
71
|
+
toBCP47,
|
|
72
|
+
} from './core/language';
|
|
73
|
+
export {
|
|
74
|
+
WEB_SPEECH_LANGUAGES,
|
|
75
|
+
WEB_SPEECH_TAGS,
|
|
76
|
+
findSpeechLanguage,
|
|
77
|
+
countryFromTag,
|
|
78
|
+
} from './core/languages-catalog';
|
|
79
|
+
export type {
|
|
80
|
+
SpeechLanguage,
|
|
81
|
+
SpeechLanguageDialect,
|
|
82
|
+
} from './core/languages-catalog';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createLazyComponent } from '../../components';
|
|
4
|
+
import type { DictationFieldProps } from './widgets/DictationField';
|
|
5
|
+
|
|
6
|
+
export const LazyDictationField = createLazyComponent<DictationFieldProps>(
|
|
7
|
+
() =>
|
|
8
|
+
import('./widgets/DictationField').then((mod) => ({
|
|
9
|
+
default: mod.DictationField,
|
|
10
|
+
})),
|
|
11
|
+
{
|
|
12
|
+
displayName: 'LazyDictationField',
|
|
13
|
+
fallback: (
|
|
14
|
+
<div className="rounded-lg border border-border/60 bg-card px-3 py-2 text-xs text-muted-foreground">
|
|
15
|
+
Loading dictation…
|
|
16
|
+
</div>
|
|
17
|
+
),
|
|
18
|
+
},
|
|
19
|
+
);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { create } from 'zustand';
|
|
4
|
+
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
5
|
+
|
|
6
|
+
export interface SpeechPrefs {
|
|
7
|
+
/**
|
|
8
|
+
* BCP-47 tag the user explicitly picked (via `<LanguagePicker>` or
|
|
9
|
+
* programmatically). `null` means "no override" — `useResolvedLanguage`
|
|
10
|
+
* then falls through to the app i18n locale / `navigator.language`.
|
|
11
|
+
* Storing the picker default as `null` is what lets a host's i18n
|
|
12
|
+
* locale take effect when the user never touched the picker.
|
|
13
|
+
*/
|
|
14
|
+
language: string | null;
|
|
15
|
+
deviceId: string | null;
|
|
16
|
+
engineId: string | null;
|
|
17
|
+
earcons: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DEFAULTS: SpeechPrefs = {
|
|
21
|
+
language: null,
|
|
22
|
+
deviceId: null,
|
|
23
|
+
engineId: null,
|
|
24
|
+
earcons: false,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
interface PrefsStore extends SpeechPrefs {
|
|
28
|
+
setLanguage: (v: string | null) => void;
|
|
29
|
+
setDeviceId: (v: string | null) => void;
|
|
30
|
+
setEngineId: (v: string | null) => void;
|
|
31
|
+
setEarcons: (v: boolean) => void;
|
|
32
|
+
reset: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const useSpeechPrefs = create<PrefsStore>()(
|
|
36
|
+
persist(
|
|
37
|
+
(set) => ({
|
|
38
|
+
...DEFAULTS,
|
|
39
|
+
setLanguage: (language) => set({ language }),
|
|
40
|
+
setDeviceId: (deviceId) => set({ deviceId }),
|
|
41
|
+
setEngineId: (engineId) => set({ engineId }),
|
|
42
|
+
setEarcons: (earcons) => set({ earcons }),
|
|
43
|
+
reset: () => set({ ...DEFAULTS }),
|
|
44
|
+
}),
|
|
45
|
+
{
|
|
46
|
+
name: 'djangocfg-stt:prefs',
|
|
47
|
+
storage: createJSONStorage(() =>
|
|
48
|
+
typeof window === 'undefined'
|
|
49
|
+
? (undefined as unknown as Storage)
|
|
50
|
+
: window.localStorage,
|
|
51
|
+
),
|
|
52
|
+
},
|
|
53
|
+
),
|
|
54
|
+
);
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for the SpeechRecognition tool.
|
|
3
|
+
*
|
|
4
|
+
* Design: a small `RecognitionEngine` interface lets consumers plug in
|
|
5
|
+
* any STT backend (browser Web Speech, Deepgram, Whisper, custom WS).
|
|
6
|
+
* The hooks/UI never depend on a specific engine.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type RecognitionStatus =
|
|
10
|
+
| 'idle'
|
|
11
|
+
| 'starting'
|
|
12
|
+
| 'listening'
|
|
13
|
+
| 'stopping'
|
|
14
|
+
| 'error';
|
|
15
|
+
|
|
16
|
+
export type EngineState =
|
|
17
|
+
| 'idle'
|
|
18
|
+
| 'connecting'
|
|
19
|
+
| 'listening'
|
|
20
|
+
| 'closing'
|
|
21
|
+
| 'closed'
|
|
22
|
+
| 'error';
|
|
23
|
+
|
|
24
|
+
export type RecognitionErrorCode =
|
|
25
|
+
| 'unsupported'
|
|
26
|
+
| 'permission-denied'
|
|
27
|
+
| 'no-microphone'
|
|
28
|
+
| 'network'
|
|
29
|
+
| 'aborted'
|
|
30
|
+
| 'no-speech'
|
|
31
|
+
| 'language'
|
|
32
|
+
| 'engine'
|
|
33
|
+
| 'unknown';
|
|
34
|
+
|
|
35
|
+
export interface RecognitionError {
|
|
36
|
+
code: RecognitionErrorCode;
|
|
37
|
+
message: string;
|
|
38
|
+
cause?: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface Segment {
|
|
42
|
+
id: string;
|
|
43
|
+
text: string;
|
|
44
|
+
isFinal: boolean;
|
|
45
|
+
/** Engine-provided confidence 0..1 if available. */
|
|
46
|
+
confidence?: number;
|
|
47
|
+
/** ms since session start. */
|
|
48
|
+
startedAt: number;
|
|
49
|
+
endedAt?: number;
|
|
50
|
+
/** Pass-through metadata from custom engines (diarization, lang, …). */
|
|
51
|
+
metadata?: Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface Transcript {
|
|
55
|
+
/** Latest interim text (not yet final). Empty string when none. */
|
|
56
|
+
interim: string;
|
|
57
|
+
/** Concatenated final text (all segments joined with " "). */
|
|
58
|
+
final: string;
|
|
59
|
+
/** Full segment list including the trailing interim segment if any. */
|
|
60
|
+
segments: Segment[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── engine contract ────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
export interface EngineStartOptions {
|
|
66
|
+
language: string;
|
|
67
|
+
/** Whether the engine should emit partial/interim results. */
|
|
68
|
+
interim: boolean;
|
|
69
|
+
deviceId?: string;
|
|
70
|
+
signal?: AbortSignal;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type EngineEventMap = {
|
|
74
|
+
partial: (text: string, segmentId: string) => void;
|
|
75
|
+
final: (text: string, segmentId: string, confidence?: number) => void;
|
|
76
|
+
error: (err: RecognitionError) => void;
|
|
77
|
+
state: (state: EngineState) => void;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type Unsub = () => void;
|
|
81
|
+
|
|
82
|
+
export interface RecognitionEngine {
|
|
83
|
+
readonly id: string;
|
|
84
|
+
readonly isSupported: boolean;
|
|
85
|
+
start(opts: EngineStartOptions): Promise<void>;
|
|
86
|
+
stop(): Promise<void>;
|
|
87
|
+
abort(): void;
|
|
88
|
+
on<K extends keyof EngineEventMap>(event: K, cb: EngineEventMap[K]): Unsub;
|
|
89
|
+
/**
|
|
90
|
+
* Optional — engines that capture mic audio themselves (HTTP / WS)
|
|
91
|
+
* may expose the active `MediaStream` so consumers can wire up a
|
|
92
|
+
* VU meter or waveform without owning a second `getUserMedia` call.
|
|
93
|
+
*/
|
|
94
|
+
getStream?(): MediaStream | null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── hook config ────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
export interface AutoStopOptions {
|
|
100
|
+
/** Stop after this many ms of silence (RMS below threshold). */
|
|
101
|
+
silenceMs?: number;
|
|
102
|
+
/** Hard cap on session length. */
|
|
103
|
+
maxMs?: number;
|
|
104
|
+
/** RMS threshold below which we count "silence". 0..1. Default 0.02. */
|
|
105
|
+
silenceThreshold?: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface UseSpeechRecognitionConfig {
|
|
109
|
+
engine?: RecognitionEngine;
|
|
110
|
+
language?: string;
|
|
111
|
+
interim?: boolean;
|
|
112
|
+
deviceId?: string;
|
|
113
|
+
autoStop?: AutoStopOptions;
|
|
114
|
+
onFinal?: (text: string, segment: Segment) => void;
|
|
115
|
+
onPartial?: (text: string, segment: Segment) => void;
|
|
116
|
+
onError?: (err: RecognitionError) => void;
|
|
117
|
+
onStart?: () => void;
|
|
118
|
+
onStop?: () => void;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface UseSpeechRecognitionReturn {
|
|
122
|
+
status: RecognitionStatus;
|
|
123
|
+
isSupported: boolean;
|
|
124
|
+
transcript: Transcript;
|
|
125
|
+
error: RecognitionError | null;
|
|
126
|
+
/** RMS level 0..1 for VU-meters. */
|
|
127
|
+
level: number;
|
|
128
|
+
start(): Promise<void>;
|
|
129
|
+
stop(): Promise<void>;
|
|
130
|
+
abort(): void;
|
|
131
|
+
toggle(): Promise<void>;
|
|
132
|
+
reset(): void;
|
|
133
|
+
}
|