@athenaintel/react 0.23.3 → 0.24.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 +6 -0
- package/dist/assets/AssetPanel.cjs +1 -1
- package/dist/assets/AssetPanel.cjs.map +1 -1
- package/dist/assets/AssetPanel.js +1 -1
- package/dist/assets/AssetPanel.js.map +1 -1
- package/dist/assets/AthenaAssetEmbed.cjs +1 -1
- package/dist/assets/AthenaAssetEmbed.cjs.map +1 -1
- package/dist/assets/AthenaAssetEmbed.js +1 -1
- package/dist/assets/AthenaAssetEmbed.js.map +1 -1
- package/dist/chat/AthenaChat.cjs +15 -23
- package/dist/chat/AthenaChat.cjs.map +1 -1
- package/dist/chat/AthenaChat.d.ts +9 -4
- package/dist/chat/AthenaChat.js +15 -23
- package/dist/chat/AthenaChat.js.map +1 -1
- package/dist/chat/StatewireApprovalCard.cjs +3 -18
- package/dist/chat/StatewireApprovalCard.cjs.map +1 -1
- package/dist/chat/StatewireApprovalCard.js +3 -18
- package/dist/chat/StatewireApprovalCard.js.map +1 -1
- package/dist/chat/StatewireClientToolBridge.cjs +2 -16
- package/dist/chat/StatewireClientToolBridge.cjs.map +1 -1
- package/dist/chat/StatewireClientToolBridge.js +2 -16
- package/dist/chat/StatewireClientToolBridge.js.map +1 -1
- package/dist/chat/StatewireQueuedMessages.cjs +5 -16
- package/dist/chat/StatewireQueuedMessages.cjs.map +1 -1
- package/dist/chat/StatewireQueuedMessages.js +5 -16
- package/dist/chat/StatewireQueuedMessages.js.map +1 -1
- package/dist/chat/WaitingOnYouBanner.cjs +3 -18
- package/dist/chat/WaitingOnYouBanner.cjs.map +1 -1
- package/dist/chat/WaitingOnYouBanner.js +3 -18
- package/dist/chat/WaitingOnYouBanner.js.map +1 -1
- package/dist/chat/run-activity.cjs +0 -16
- package/dist/chat/run-activity.cjs.map +1 -1
- package/dist/chat/run-activity.js +0 -16
- package/dist/chat/run-activity.js.map +1 -1
- package/dist/chat/statewire-client-tools.cjs +1 -17
- package/dist/chat/statewire-client-tools.cjs.map +1 -1
- package/dist/chat/statewire-client-tools.js +1 -17
- package/dist/chat/statewire-client-tools.js.map +1 -1
- package/dist/chat/super-grouping/activity-timeline.cjs +0 -16
- package/dist/chat/super-grouping/activity-timeline.cjs.map +1 -1
- package/dist/chat/super-grouping/activity-timeline.js +0 -16
- package/dist/chat/super-grouping/activity-timeline.js.map +1 -1
- package/dist/chat/tiptap-text.cjs +1 -17
- package/dist/chat/tiptap-text.cjs.map +1 -1
- package/dist/chat/tiptap-text.js +1 -17
- package/dist/chat/tiptap-text.js.map +1 -1
- package/dist/chat/use-athena-run-live.cjs +0 -15
- package/dist/chat/use-athena-run-live.cjs.map +1 -1
- package/dist/chat/use-athena-run-live.js +0 -15
- package/dist/chat/use-athena-run-live.js.map +1 -1
- package/dist/chat/use-statewire-approval-request.cjs +4 -16
- package/dist/chat/use-statewire-approval-request.cjs.map +1 -1
- package/dist/chat/use-statewire-approval-request.d.ts +2 -0
- package/dist/chat/use-statewire-approval-request.js +4 -16
- package/dist/chat/use-statewire-approval-request.js.map +1 -1
- package/dist/chat-ui/composer/tiptap-composer.cjs +1 -16
- package/dist/chat-ui/composer/tiptap-composer.cjs.map +1 -1
- package/dist/chat-ui/composer/tiptap-composer.js +1 -16
- package/dist/chat-ui/composer/tiptap-composer.js.map +1 -1
- package/dist/chat-ui/index.cjs +0 -16
- package/dist/chat-ui/index.cjs.map +1 -1
- package/dist/chat-ui/index.js +0 -16
- package/dist/chat-ui/index.js.map +1 -1
- package/dist/chat-ui/mentions/mention-chip.cjs +1 -16
- package/dist/chat-ui/mentions/mention-chip.cjs.map +1 -1
- package/dist/chat-ui/mentions/mention-chip.js +1 -16
- package/dist/chat-ui/mentions/mention-chip.js.map +1 -1
- package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.cjs +4 -6
- package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.cjs.map +1 -1
- package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.js +4 -6
- package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.js.map +1 -1
- package/dist/chat-ui/mentions/suggestions/data/sources/index.cjs +2 -9
- package/dist/chat-ui/mentions/suggestions/data/sources/index.cjs.map +1 -1
- package/dist/chat-ui/mentions/suggestions/data/sources/index.js +2 -9
- package/dist/chat-ui/mentions/suggestions/data/sources/index.js.map +1 -1
- package/dist/chat-ui/messages/message-components.cjs +5 -19
- package/dist/chat-ui/messages/message-components.cjs.map +1 -1
- package/dist/chat-ui/messages/message-components.js +5 -19
- package/dist/chat-ui/messages/message-components.js.map +1 -1
- package/dist/chat-ui/messages/runtime-message.cjs +1 -16
- package/dist/chat-ui/messages/runtime-message.cjs.map +1 -1
- package/dist/chat-ui/messages/runtime-message.js +1 -16
- package/dist/chat-ui/messages/runtime-message.js.map +1 -1
- package/dist/chat-ui/messages/statewire-continue-run.cjs +3 -15
- package/dist/chat-ui/messages/statewire-continue-run.cjs.map +1 -1
- package/dist/chat-ui/messages/statewire-continue-run.js +3 -15
- package/dist/chat-ui/messages/statewire-continue-run.js.map +1 -1
- package/dist/chat-ui/messages/user-edit-composer.cjs +0 -16
- package/dist/chat-ui/messages/user-edit-composer.cjs.map +1 -1
- package/dist/chat-ui/messages/user-edit-composer.js +0 -16
- package/dist/chat-ui/messages/user-edit-composer.js.map +1 -1
- package/dist/chat-ui/messages/wake-marker.cjs +1 -16
- package/dist/chat-ui/messages/wake-marker.cjs.map +1 -1
- package/dist/chat-ui/messages/wake-marker.js +1 -16
- package/dist/chat-ui/messages/wake-marker.js.map +1 -1
- package/dist/chat-ui/statewire.cjs +0 -16
- package/dist/chat-ui/statewire.cjs.map +1 -1
- package/dist/chat-ui/statewire.js +0 -16
- package/dist/chat-ui/statewire.js.map +1 -1
- package/dist/chat-ui/thread/statewire-history-edge.cjs +2 -17
- package/dist/chat-ui/thread/statewire-history-edge.cjs.map +1 -1
- package/dist/chat-ui/thread/statewire-history-edge.js +2 -17
- package/dist/chat-ui/thread/statewire-history-edge.js.map +1 -1
- package/dist/dictation/DictationStrip.cjs +2 -1
- package/dist/dictation/DictationStrip.cjs.map +1 -1
- package/dist/dictation/DictationStrip.d.ts +2 -0
- package/dist/dictation/DictationStrip.js +2 -1
- package/dist/dictation/DictationStrip.js.map +1 -1
- package/dist/dictation/dictation-engine.cjs.map +1 -1
- package/dist/dictation/dictation-engine.d.ts +8 -0
- package/dist/dictation/dictation-engine.js.map +1 -1
- package/dist/dictation/recorded-dictation-engine.cjs +153 -0
- package/dist/dictation/recorded-dictation-engine.cjs.map +1 -0
- package/dist/dictation/recorded-dictation-engine.d.ts +66 -0
- package/dist/dictation/recorded-dictation-engine.js +153 -0
- package/dist/dictation/recorded-dictation-engine.js.map +1 -0
- package/dist/dictation/use-athena-dictation-engine.cjs +64 -0
- package/dist/dictation/use-athena-dictation-engine.cjs.map +1 -0
- package/dist/dictation/use-athena-dictation-engine.d.ts +25 -0
- package/dist/dictation/use-athena-dictation-engine.js +64 -0
- package/dist/dictation/use-athena-dictation-engine.js.map +1 -0
- package/dist/dictation/use-dictation.cjs +17 -6
- package/dist/dictation/use-dictation.cjs.map +1 -1
- package/dist/dictation/use-dictation.d.ts +20 -4
- package/dist/dictation/use-dictation.js +17 -6
- package/dist/dictation/use-dictation.js.map +1 -1
- package/dist/index.cjs +28 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +10 -21
- package/dist/index.js.map +1 -1
- package/dist/packages/agent-runtime/src/edit-command.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/edit-command.js.map +1 -1
- package/dist/packages/agent-runtime/src/errors.cjs +18 -0
- package/dist/packages/agent-runtime/src/errors.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/errors.js +19 -1
- package/dist/packages/agent-runtime/src/errors.js.map +1 -1
- package/dist/packages/agent-runtime/src/generated/runconfig-contract.cjs +1 -0
- package/dist/packages/agent-runtime/src/generated/runconfig-contract.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/generated/runconfig-contract.js +1 -0
- package/dist/packages/agent-runtime/src/generated/runconfig-contract.js.map +1 -1
- package/dist/packages/agent-runtime/src/interrupts.cjs +61 -12
- package/dist/packages/agent-runtime/src/interrupts.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/interrupts.js +61 -12
- package/dist/packages/agent-runtime/src/interrupts.js.map +1 -1
- package/dist/packages/agent-runtime/src/quota-safe-local-storage-session.cjs +49 -0
- package/dist/packages/agent-runtime/src/quota-safe-local-storage-session.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/quota-safe-local-storage-session.js +49 -0
- package/dist/packages/agent-runtime/src/quota-safe-local-storage-session.js.map +1 -1
- package/dist/packages/agent-runtime/src/thread-status.js +1 -1
- package/dist/packages/agent-runtime/src/tool-metadata.cjs +1 -37
- package/dist/packages/agent-runtime/src/tool-metadata.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/tool-metadata.js +1 -37
- package/dist/packages/agent-runtime/src/tool-metadata.js.map +1 -1
- package/dist/packages/agent-runtime/src/use-deep-agent-config.cjs +0 -1
- package/dist/packages/agent-runtime/src/use-deep-agent-config.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/use-deep-agent-config.js +0 -1
- package/dist/packages/agent-runtime/src/use-deep-agent-config.js.map +1 -1
- package/dist/packages/agent-runtime/src/use-deep-agent-thread.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/use-deep-agent-thread.js.map +1 -1
- package/dist/packages/react/package.json.cjs +1 -1
- package/dist/packages/react/package.json.js +1 -1
- package/dist/provider/AthenaProvider.cjs +3 -17
- package/dist/provider/AthenaProvider.cjs.map +1 -1
- package/dist/provider/AthenaProvider.js +3 -17
- package/dist/provider/AthenaProvider.js.map +1 -1
- package/dist/runtime/statewire-lifecycle.cjs +0 -16
- package/dist/runtime/statewire-lifecycle.cjs.map +1 -1
- package/dist/runtime/statewire-lifecycle.js +0 -16
- package/dist/runtime/statewire-lifecycle.js.map +1 -1
- package/dist/runtime/useAthenaStatewireRuntime.cjs +3 -17
- package/dist/runtime/useAthenaStatewireRuntime.cjs.map +1 -1
- package/dist/runtime/useAthenaStatewireRuntime.js +3 -17
- package/dist/runtime/useAthenaStatewireRuntime.js.map +1 -1
- package/dist/threads/statewire-thread-list.cjs +4 -17
- package/dist/threads/statewire-thread-list.cjs.map +1 -1
- package/dist/threads/statewire-thread-list.js +4 -17
- package/dist/threads/statewire-thread-list.js.map +1 -1
- package/dist/threads/use-rest-conversation-history.cjs +2 -16
- package/dist/threads/use-rest-conversation-history.cjs.map +1 -1
- package/dist/threads/use-rest-conversation-history.js +2 -16
- package/dist/threads/use-rest-conversation-history.js.map +1 -1
- package/dist/threads/use-statewire-conversation-history.cjs +2 -16
- package/dist/threads/use-statewire-conversation-history.cjs.map +1 -1
- package/dist/threads/use-statewire-conversation-history.js +2 -16
- package/dist/threads/use-statewire-conversation-history.js.map +1 -1
- package/dist/toolkits/index.cjs +0 -7
- package/dist/toolkits/index.cjs.map +1 -1
- package/dist/toolkits/index.d.ts +0 -2
- package/dist/toolkits/index.js +0 -7
- package/dist/toolkits/index.js.map +1 -1
- package/dist/tools/index.cjs +4 -20
- package/dist/tools/index.cjs.map +1 -1
- package/dist/tools/index.js +1 -17
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/misc-tool-uis.cjs +2 -17
- package/dist/tools/misc-tool-uis.cjs.map +1 -1
- package/dist/tools/misc-tool-uis.js +2 -17
- package/dist/tools/misc-tool-uis.js.map +1 -1
- package/dist/tools/primitives.cjs +1 -16
- package/dist/tools/primitives.cjs.map +1 -1
- package/dist/tools/primitives.js +1 -16
- package/dist/tools/primitives.js.map +1 -1
- package/dist/tools/tool-fallback.cjs +2 -17
- package/dist/tools/tool-fallback.cjs.map +1 -1
- package/dist/tools/tool-fallback.js +2 -17
- package/dist/tools/tool-fallback.js.map +1 -1
- package/dist/tools/voice-update-tool-ui.cjs +1 -17
- package/dist/tools/voice-update-tool-ui.cjs.map +1 -1
- package/dist/tools/voice-update-tool-ui.js +1 -17
- package/dist/tools/voice-update-tool-ui.js.map +1 -1
- package/package.json +1 -1
- package/dist/node_modules/qrcode-generator/dist/qrcode.cjs +0 -32
- package/dist/node_modules/qrcode-generator/dist/qrcode.cjs.map +0 -1
- package/dist/node_modules/qrcode-generator/dist/qrcode.js +0 -31
- package/dist/node_modules/qrcode-generator/dist/qrcode.js.map +0 -1
- package/dist/node_modules/zod/v4/classic/errors.cjs +0 -44
- package/dist/node_modules/zod/v4/classic/errors.cjs.map +0 -1
- package/dist/node_modules/zod/v4/classic/errors.js +0 -44
- package/dist/node_modules/zod/v4/classic/errors.js.map +0 -1
- package/dist/node_modules/zod/v4/classic/iso.cjs +0 -43
- package/dist/node_modules/zod/v4/classic/iso.cjs.map +0 -1
- package/dist/node_modules/zod/v4/classic/iso.js +0 -43
- package/dist/node_modules/zod/v4/classic/iso.js.map +0 -1
- package/dist/node_modules/zod/v4/classic/parse.cjs +0 -29
- package/dist/node_modules/zod/v4/classic/parse.cjs.map +0 -1
- package/dist/node_modules/zod/v4/classic/parse.js +0 -29
- package/dist/node_modules/zod/v4/classic/parse.js.map +0 -1
- package/dist/node_modules/zod/v4/classic/schemas.cjs +0 -871
- package/dist/node_modules/zod/v4/classic/schemas.cjs.map +0 -1
- package/dist/node_modules/zod/v4/classic/schemas.js +0 -871
- package/dist/node_modules/zod/v4/classic/schemas.js.map +0 -1
- package/dist/node_modules/zod/v4/core/api.cjs +0 -570
- package/dist/node_modules/zod/v4/core/api.cjs.map +0 -1
- package/dist/node_modules/zod/v4/core/api.js +0 -570
- package/dist/node_modules/zod/v4/core/api.js.map +0 -1
- package/dist/node_modules/zod/v4/core/checks.cjs +0 -411
- package/dist/node_modules/zod/v4/core/checks.cjs.map +0 -1
- package/dist/node_modules/zod/v4/core/checks.js +0 -411
- package/dist/node_modules/zod/v4/core/checks.js.map +0 -1
- package/dist/node_modules/zod/v4/core/core.cjs +0 -77
- package/dist/node_modules/zod/v4/core/core.cjs.map +0 -1
- package/dist/node_modules/zod/v4/core/core.js +0 -77
- package/dist/node_modules/zod/v4/core/core.js.map +0 -1
- package/dist/node_modules/zod/v4/core/doc.cjs +0 -38
- package/dist/node_modules/zod/v4/core/doc.cjs.map +0 -1
- package/dist/node_modules/zod/v4/core/doc.js +0 -38
- package/dist/node_modules/zod/v4/core/doc.js.map +0 -1
- package/dist/node_modules/zod/v4/core/errors.cjs +0 -76
- package/dist/node_modules/zod/v4/core/errors.cjs.map +0 -1
- package/dist/node_modules/zod/v4/core/errors.js +0 -76
- package/dist/node_modules/zod/v4/core/errors.js.map +0 -1
- package/dist/node_modules/zod/v4/core/json-schema-processors.cjs +0 -312
- package/dist/node_modules/zod/v4/core/json-schema-processors.cjs.map +0 -1
- package/dist/node_modules/zod/v4/core/json-schema-processors.js +0 -312
- package/dist/node_modules/zod/v4/core/json-schema-processors.js.map +0 -1
- package/dist/node_modules/zod/v4/core/parse.cjs +0 -96
- package/dist/node_modules/zod/v4/core/parse.cjs.map +0 -1
- package/dist/node_modules/zod/v4/core/parse.js +0 -96
- package/dist/node_modules/zod/v4/core/parse.js.map +0 -1
- package/dist/node_modules/zod/v4/core/regexes.cjs +0 -86
- package/dist/node_modules/zod/v4/core/regexes.cjs.map +0 -1
- package/dist/node_modules/zod/v4/core/regexes.js +0 -86
- package/dist/node_modules/zod/v4/core/regexes.js.map +0 -1
- package/dist/node_modules/zod/v4/core/registries.cjs +0 -52
- package/dist/node_modules/zod/v4/core/registries.cjs.map +0 -1
- package/dist/node_modules/zod/v4/core/registries.js +0 -52
- package/dist/node_modules/zod/v4/core/registries.js.map +0 -1
- package/dist/node_modules/zod/v4/core/schemas.cjs +0 -1412
- package/dist/node_modules/zod/v4/core/schemas.cjs.map +0 -1
- package/dist/node_modules/zod/v4/core/schemas.js +0 -1413
- package/dist/node_modules/zod/v4/core/schemas.js.map +0 -1
- package/dist/node_modules/zod/v4/core/to-json-schema.cjs +0 -370
- package/dist/node_modules/zod/v4/core/to-json-schema.cjs.map +0 -1
- package/dist/node_modules/zod/v4/core/to-json-schema.js +0 -370
- package/dist/node_modules/zod/v4/core/to-json-schema.js.map +0 -1
- package/dist/node_modules/zod/v4/core/util.cjs +0 -444
- package/dist/node_modules/zod/v4/core/util.cjs.map +0 -1
- package/dist/node_modules/zod/v4/core/util.js +0 -444
- package/dist/node_modules/zod/v4/core/util.js.map +0 -1
- package/dist/node_modules/zod/v4/core/versions.cjs +0 -9
- package/dist/node_modules/zod/v4/core/versions.cjs.map +0 -1
- package/dist/node_modules/zod/v4/core/versions.js +0 -9
- package/dist/node_modules/zod/v4/core/versions.js.map +0 -1
- package/dist/packages/agent-runtime/src/thread-handoff.cjs +0 -85
- package/dist/packages/agent-runtime/src/thread-handoff.cjs.map +0 -1
- package/dist/packages/agent-runtime/src/thread-handoff.js +0 -85
- package/dist/packages/agent-runtime/src/thread-handoff.js.map +0 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { DICTATION_ENGINE_ERROR_KINDS } from "./dictation-engine.js";
|
|
2
|
+
const DEFAULT_MIME_TYPES = ["audio/webm;codecs=opus", "audio/webm", "audio/mp4", "audio/ogg;codecs=opus"];
|
|
3
|
+
const DEFAULT_FLUSH_TIMEOUT_MS = 9e4;
|
|
4
|
+
const DEFAULT_MAX_RECORDING_MS = 5 * 6e4;
|
|
5
|
+
function resolveMediaRecorderCtor() {
|
|
6
|
+
if (typeof window === "undefined") return null;
|
|
7
|
+
return window.MediaRecorder ?? null;
|
|
8
|
+
}
|
|
9
|
+
function pickRecorderMimeType(Ctor, candidates) {
|
|
10
|
+
if (!Ctor.isTypeSupported) return void 0;
|
|
11
|
+
return candidates.find((type) => {
|
|
12
|
+
var _a;
|
|
13
|
+
return (_a = Ctor.isTypeSupported) == null ? void 0 : _a.call(Ctor, type);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function transcriptionError(error) {
|
|
17
|
+
return {
|
|
18
|
+
kind: DICTATION_ENGINE_ERROR_KINDS.connection,
|
|
19
|
+
message: error instanceof Error && error.message ? error.message : "Transcription failed. Try again."
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function createRecordedDictationEngine(options) {
|
|
23
|
+
const {
|
|
24
|
+
transcribe,
|
|
25
|
+
mimeTypes = DEFAULT_MIME_TYPES,
|
|
26
|
+
flushTimeoutMs = DEFAULT_FLUSH_TIMEOUT_MS,
|
|
27
|
+
maxDurationMs = DEFAULT_MAX_RECORDING_MS
|
|
28
|
+
} = options;
|
|
29
|
+
return {
|
|
30
|
+
id: "recorded-upload",
|
|
31
|
+
flushTimeoutMs,
|
|
32
|
+
stoppingLabel: "Transcribing…",
|
|
33
|
+
isSupported: () => {
|
|
34
|
+
var _a;
|
|
35
|
+
return resolveMediaRecorderCtor() !== null && typeof navigator !== "undefined" && typeof ((_a = navigator.mediaDevices) == null ? void 0 : _a.getUserMedia) === "function";
|
|
36
|
+
},
|
|
37
|
+
start: ({ stream, lang, callbacks }) => {
|
|
38
|
+
const Ctor = resolveMediaRecorderCtor();
|
|
39
|
+
const noop = { stop: () => void 0, abort: () => void 0 };
|
|
40
|
+
if (!Ctor || !stream) {
|
|
41
|
+
callbacks.onError({
|
|
42
|
+
kind: !Ctor ? DICTATION_ENGINE_ERROR_KINDS.unsupported : DICTATION_ENGINE_ERROR_KINDS.permission,
|
|
43
|
+
message: !Ctor ? "This browser cannot record audio." : "No microphone stream was available."
|
|
44
|
+
});
|
|
45
|
+
callbacks.onEnd();
|
|
46
|
+
return noop;
|
|
47
|
+
}
|
|
48
|
+
const mimeType = pickRecorderMimeType(Ctor, mimeTypes);
|
|
49
|
+
let recorder;
|
|
50
|
+
try {
|
|
51
|
+
recorder = new Ctor(stream, mimeType ? { mimeType } : void 0);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
callbacks.onError({
|
|
54
|
+
kind: DICTATION_ENGINE_ERROR_KINDS.unknown,
|
|
55
|
+
message: error instanceof Error ? error.message : "Recording could not start."
|
|
56
|
+
});
|
|
57
|
+
callbacks.onEnd();
|
|
58
|
+
return noop;
|
|
59
|
+
}
|
|
60
|
+
const chunks = [];
|
|
61
|
+
const upload = new AbortController();
|
|
62
|
+
let ended = false;
|
|
63
|
+
let aborted = false;
|
|
64
|
+
let limitTimer;
|
|
65
|
+
const clearLimit = () => {
|
|
66
|
+
if (limitTimer === void 0) return;
|
|
67
|
+
clearTimeout(limitTimer);
|
|
68
|
+
limitTimer = void 0;
|
|
69
|
+
};
|
|
70
|
+
const end = () => {
|
|
71
|
+
if (ended) return;
|
|
72
|
+
ended = true;
|
|
73
|
+
clearLimit();
|
|
74
|
+
recorder.ondataavailable = null;
|
|
75
|
+
recorder.onstop = null;
|
|
76
|
+
recorder.onerror = null;
|
|
77
|
+
callbacks.onEnd();
|
|
78
|
+
};
|
|
79
|
+
const stopRecorder = () => {
|
|
80
|
+
if (recorder.state === "inactive") return false;
|
|
81
|
+
try {
|
|
82
|
+
recorder.stop();
|
|
83
|
+
return true;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
recorder.ondataavailable = (event) => {
|
|
89
|
+
if (event.data && event.data.size > 0) chunks.push(event.data);
|
|
90
|
+
};
|
|
91
|
+
recorder.onerror = () => {
|
|
92
|
+
callbacks.onError({
|
|
93
|
+
kind: DICTATION_ENGINE_ERROR_KINDS.unknown,
|
|
94
|
+
message: "Recording stopped unexpectedly."
|
|
95
|
+
});
|
|
96
|
+
end();
|
|
97
|
+
};
|
|
98
|
+
recorder.onstop = () => {
|
|
99
|
+
if (aborted || ended) {
|
|
100
|
+
end();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const clip = new Blob(chunks, { type: recorder.mimeType || mimeType || "audio/webm" });
|
|
104
|
+
if (clip.size === 0) {
|
|
105
|
+
end();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
void transcribe(clip, { lang, signal: upload.signal }).then((text) => {
|
|
109
|
+
if (ended) return;
|
|
110
|
+
const trimmed = text.trim();
|
|
111
|
+
if (trimmed) callbacks.onTranscript({ final: trimmed, interim: "" });
|
|
112
|
+
}).catch((error) => {
|
|
113
|
+
if (ended) return;
|
|
114
|
+
callbacks.onError(transcriptionError(error));
|
|
115
|
+
}).finally(end);
|
|
116
|
+
};
|
|
117
|
+
try {
|
|
118
|
+
recorder.start();
|
|
119
|
+
} catch (error) {
|
|
120
|
+
callbacks.onError({
|
|
121
|
+
kind: DICTATION_ENGINE_ERROR_KINDS.unknown,
|
|
122
|
+
message: error instanceof Error ? error.message : "Recording could not start."
|
|
123
|
+
});
|
|
124
|
+
end();
|
|
125
|
+
return noop;
|
|
126
|
+
}
|
|
127
|
+
if (Number.isFinite(maxDurationMs) && maxDurationMs > 0) {
|
|
128
|
+
limitTimer = setTimeout(() => {
|
|
129
|
+
limitTimer = void 0;
|
|
130
|
+
if (!ended) stopRecorder();
|
|
131
|
+
}, maxDurationMs);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
stop: () => {
|
|
135
|
+
clearLimit();
|
|
136
|
+
if (!stopRecorder()) end();
|
|
137
|
+
},
|
|
138
|
+
abort: () => {
|
|
139
|
+
aborted = true;
|
|
140
|
+
upload.abort();
|
|
141
|
+
stopRecorder();
|
|
142
|
+
end();
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
export {
|
|
149
|
+
DEFAULT_MAX_RECORDING_MS,
|
|
150
|
+
createRecordedDictationEngine,
|
|
151
|
+
pickRecorderMimeType
|
|
152
|
+
};
|
|
153
|
+
//# sourceMappingURL=recorded-dictation-engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recorded-dictation-engine.js","sources":["../../src/dictation/recorded-dictation-engine.ts"],"sourcesContent":["import {\n DICTATION_ENGINE_ERROR_KINDS,\n type DictationEngine,\n type DictationEngineError,\n} from './dictation-engine';\n\nexport interface DictationTranscribeContext {\n /** BCP-47 tag the session was started with (host `lang` or the document's). */\n lang: string;\n /** Fires when the user cancels or the composer gives up waiting. */\n signal: AbortSignal;\n}\n\n/**\n * Turn a finished recording into text. Receives the whole clip once the user\n * stops; resolves with the transcript (empty when nothing was said). Should\n * honour `signal` so a cancelled dictation does not keep uploading.\n */\nexport type DictationTranscriber = (clip: Blob, context: DictationTranscribeContext) => Promise<string>;\n\nexport interface RecordedDictationEngineOptions {\n transcribe: DictationTranscriber;\n /**\n * Preferred `MediaRecorder` container, tried in order until the browser\n * accepts one. Defaults to Opus in WebM (Chrome, Firefox, Edge) then MP4\n * (Safari); the browser's own default is the last resort.\n */\n mimeTypes?: readonly string[];\n /**\n * Upper bound on the transcription round trip; the in-flight request is\n * aborted when it elapses. Defaults to 90s, above agora's 60s Deepgram\n * budget plus upload time.\n */\n flushTimeoutMs?: number;\n /**\n * Longest recording the engine accepts before it stops on its own and\n * transcribes what it has, exactly as if Stop had been pressed. Bounds a\n * mic left open by mistake: every recorded minute is a minute billed by the\n * recognizer, and agora refuses clips over its size cap. Defaults to\n * 5 minutes; pass `Infinity` to disable.\n */\n maxDurationMs?: number;\n}\n\nconst DEFAULT_MIME_TYPES = ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4', 'audio/ogg;codecs=opus'];\nconst DEFAULT_FLUSH_TIMEOUT_MS = 90_000;\nexport const DEFAULT_MAX_RECORDING_MS = 5 * 60_000;\n\n// lib.dom typings for MediaRecorder vary across the TypeScript versions the\n// SDK supports; describe only what the engine touches.\ninterface MediaRecorderLike extends EventTarget {\n readonly state: 'inactive' | 'recording' | 'paused';\n readonly mimeType: string;\n start: (timeslice?: number) => void;\n stop: () => void;\n ondataavailable: ((event: { data: Blob }) => void) | null;\n onstop: (() => void) | null;\n onerror: ((event: Event) => void) | null;\n}\ninterface MediaRecorderCtor {\n new (stream: MediaStream, options?: { mimeType?: string }): MediaRecorderLike;\n isTypeSupported?: (type: string) => boolean;\n}\n\nfunction resolveMediaRecorderCtor(): MediaRecorderCtor | null {\n if (typeof window === 'undefined') return null;\n return (window as unknown as { MediaRecorder?: MediaRecorderCtor }).MediaRecorder ?? null;\n}\n\n/** First container the recorder accepts, or undefined for the browser default. */\nexport function pickRecorderMimeType(\n Ctor: Pick<MediaRecorderCtor, 'isTypeSupported'>,\n candidates: readonly string[],\n): string | undefined {\n if (!Ctor.isTypeSupported) return undefined;\n return candidates.find((type) => Ctor.isTypeSupported?.(type));\n}\n\nfunction transcriptionError(error: unknown): DictationEngineError {\n return {\n kind: DICTATION_ENGINE_ERROR_KINDS.connection,\n message: error instanceof Error && error.message ? error.message : 'Transcription failed. Try again.',\n };\n}\n\n/**\n * Record-then-transcribe: capture the microphone with `MediaRecorder` while\n * the user talks, then hand the finished clip to `transcribe` on stop. No\n * interim words are shown; the composer shows a \"transcribing\" state until\n * the text lands. Works in every browser with `MediaRecorder`, so the mic is\n * available where Web Speech is not (Firefox) and the quality is whatever the\n * server-side recognizer provides.\n */\nexport function createRecordedDictationEngine(options: RecordedDictationEngineOptions): DictationEngine {\n const {\n transcribe,\n mimeTypes = DEFAULT_MIME_TYPES,\n flushTimeoutMs = DEFAULT_FLUSH_TIMEOUT_MS,\n maxDurationMs = DEFAULT_MAX_RECORDING_MS,\n } = options;\n return {\n id: 'recorded-upload',\n flushTimeoutMs,\n stoppingLabel: 'Transcribing…',\n isSupported: () =>\n resolveMediaRecorderCtor() !== null &&\n typeof navigator !== 'undefined' &&\n typeof navigator.mediaDevices?.getUserMedia === 'function',\n start: ({ stream, lang, callbacks }) => {\n const Ctor = resolveMediaRecorderCtor();\n const noop = { stop: () => undefined, abort: () => undefined };\n if (!Ctor || !stream) {\n callbacks.onError({\n kind: !Ctor ? DICTATION_ENGINE_ERROR_KINDS.unsupported : DICTATION_ENGINE_ERROR_KINDS.permission,\n message: !Ctor ? 'This browser cannot record audio.' : 'No microphone stream was available.',\n });\n callbacks.onEnd();\n return noop;\n }\n\n const mimeType = pickRecorderMimeType(Ctor, mimeTypes);\n let recorder: MediaRecorderLike;\n try {\n recorder = new Ctor(stream, mimeType ? { mimeType } : undefined);\n } catch (error) {\n callbacks.onError({\n kind: DICTATION_ENGINE_ERROR_KINDS.unknown,\n message: error instanceof Error ? error.message : 'Recording could not start.',\n });\n callbacks.onEnd();\n return noop;\n }\n\n const chunks: Blob[] = [];\n const upload = new AbortController();\n let ended = false;\n let aborted = false;\n let limitTimer: ReturnType<typeof setTimeout> | undefined;\n const clearLimit = () => {\n if (limitTimer === undefined) return;\n clearTimeout(limitTimer);\n limitTimer = undefined;\n };\n const end = () => {\n if (ended) return;\n ended = true;\n clearLimit();\n recorder.ondataavailable = null;\n recorder.onstop = null;\n recorder.onerror = null;\n callbacks.onEnd();\n };\n const stopRecorder = () => {\n if (recorder.state === 'inactive') return false;\n try {\n recorder.stop();\n return true;\n } catch {\n return false;\n }\n };\n\n recorder.ondataavailable = (event) => {\n if (event.data && event.data.size > 0) chunks.push(event.data);\n };\n recorder.onerror = () => {\n callbacks.onError({\n kind: DICTATION_ENGINE_ERROR_KINDS.unknown,\n message: 'Recording stopped unexpectedly.',\n });\n end();\n };\n recorder.onstop = () => {\n if (aborted || ended) {\n end();\n return;\n }\n const clip = new Blob(chunks, { type: recorder.mimeType || mimeType || 'audio/webm' });\n if (clip.size === 0) {\n end();\n return;\n }\n void transcribe(clip, { lang, signal: upload.signal })\n .then((text) => {\n if (ended) return;\n const trimmed = text.trim();\n if (trimmed) callbacks.onTranscript({ final: trimmed, interim: '' });\n })\n .catch((error: unknown) => {\n if (ended) return;\n callbacks.onError(transcriptionError(error));\n })\n .finally(end);\n };\n\n try {\n recorder.start();\n } catch (error) {\n callbacks.onError({\n kind: DICTATION_ENGINE_ERROR_KINDS.unknown,\n message: error instanceof Error ? error.message : 'Recording could not start.',\n });\n end();\n return noop;\n }\n\n if (Number.isFinite(maxDurationMs) && maxDurationMs > 0) {\n limitTimer = setTimeout(() => {\n limitTimer = undefined;\n // Same path as the user's Stop: onstop transcribes the clip and the\n // words land in the draft. A recorder that is already inactive is a\n // stop in flight, which must run to completion untouched.\n if (!ended) stopRecorder();\n }, maxDurationMs);\n }\n\n return {\n stop: () => {\n clearLimit();\n if (!stopRecorder()) end();\n },\n abort: () => {\n aborted = true;\n upload.abort();\n stopRecorder();\n end();\n },\n };\n },\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const react = require("react");
|
|
4
|
+
const AthenaContext = require("../provider/AthenaContext.cjs");
|
|
5
|
+
const statewireAuthHeaders = require("../runtime/statewire-auth-headers.cjs");
|
|
6
|
+
const dictationEngine = require("./dictation-engine.cjs");
|
|
7
|
+
const recordedDictationEngine = require("./recorded-dictation-engine.cjs");
|
|
8
|
+
function transcriptionUploadUrl(backendUrl) {
|
|
9
|
+
return `${backendUrl.replace(/\/api\/assistant-ui\/?$/, "").replace(/\/$/, "")}/api/transcription/upload`;
|
|
10
|
+
}
|
|
11
|
+
function transcriptionUploadErrorMessage(status) {
|
|
12
|
+
switch (status) {
|
|
13
|
+
case 401:
|
|
14
|
+
case 403:
|
|
15
|
+
return "Dictation is not available for this sign-in. Reload and try again.";
|
|
16
|
+
case 413:
|
|
17
|
+
return "That recording is too long to transcribe.";
|
|
18
|
+
case 422:
|
|
19
|
+
return "The dictation language was rejected. Use a BCP-47 tag such as en-US.";
|
|
20
|
+
case 429:
|
|
21
|
+
return "Too many dictations in a row. Wait a moment and try again.";
|
|
22
|
+
case 503:
|
|
23
|
+
return "Dictation is not set up on this server.";
|
|
24
|
+
default:
|
|
25
|
+
return `Transcription failed (${status}).`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function createAthenaUploadTranscriber(getConfig) {
|
|
29
|
+
return async (clip, { lang, signal }) => {
|
|
30
|
+
const config = getConfig();
|
|
31
|
+
const headers = await statewireAuthHeaders.resolveStatewireAuthHeaders(config, { staleAuth: false });
|
|
32
|
+
const form = new FormData();
|
|
33
|
+
const extension = clip.type.includes("mp4") ? "mp4" : clip.type.includes("ogg") ? "ogg" : "webm";
|
|
34
|
+
form.append("file", clip, `dictation.${extension}`);
|
|
35
|
+
if (lang) form.append("language", lang);
|
|
36
|
+
const response = await fetch(transcriptionUploadUrl(config.backendUrl), {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers,
|
|
39
|
+
body: form,
|
|
40
|
+
credentials: "include",
|
|
41
|
+
signal
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok) throw new Error(transcriptionUploadErrorMessage(response.status));
|
|
44
|
+
const payload = await response.json();
|
|
45
|
+
return typeof payload.transcript === "string" ? payload.transcript : "";
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function useAthenaDictationEngine(override) {
|
|
49
|
+
const config = AthenaContext.useAthenaConfig();
|
|
50
|
+
const configRef = react.useRef(config);
|
|
51
|
+
configRef.current = config;
|
|
52
|
+
return react.useMemo(() => {
|
|
53
|
+
if (override) return override;
|
|
54
|
+
const recorded = recordedDictationEngine.createRecordedDictationEngine({
|
|
55
|
+
transcribe: createAthenaUploadTranscriber(() => configRef.current)
|
|
56
|
+
});
|
|
57
|
+
return recorded.isSupported() ? recorded : dictationEngine.createWebSpeechDictationEngine();
|
|
58
|
+
}, [override]);
|
|
59
|
+
}
|
|
60
|
+
exports.createAthenaUploadTranscriber = createAthenaUploadTranscriber;
|
|
61
|
+
exports.transcriptionUploadErrorMessage = transcriptionUploadErrorMessage;
|
|
62
|
+
exports.transcriptionUploadUrl = transcriptionUploadUrl;
|
|
63
|
+
exports.useAthenaDictationEngine = useAthenaDictationEngine;
|
|
64
|
+
//# sourceMappingURL=use-athena-dictation-engine.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-athena-dictation-engine.cjs","sources":["../../src/dictation/use-athena-dictation-engine.ts"],"sourcesContent":["import { useMemo, useRef } from 'react';\nimport { type AthenaConfig, useAthenaConfig } from '../provider/AthenaContext';\nimport { resolveStatewireAuthHeaders } from '../runtime/statewire-auth-headers';\nimport { createWebSpeechDictationEngine, type DictationEngine } from './dictation-engine';\nimport { createRecordedDictationEngine, type DictationTranscriber } from './recorded-dictation-engine';\n\ninterface UploadTranscriptionResponse {\n transcript: string;\n}\n\n/** Agora's REST base from the configured backend URL (which may point at a nested endpoint). */\nexport function transcriptionUploadUrl(backendUrl: string): string {\n return `${backendUrl.replace(/\\/api\\/assistant-ui\\/?$/, '').replace(/\\/$/, '')}/api/transcription/upload`;\n}\n\n/**\n * One line for the composer per way `/transcription/upload` refuses a clip.\n * The copy lives here, apart from the fetch, so it can be pinned by a test.\n */\nexport function transcriptionUploadErrorMessage(status: number): string {\n switch (status) {\n case 401:\n case 403:\n return 'Dictation is not available for this sign-in. Reload and try again.';\n case 413:\n return 'That recording is too long to transcribe.';\n case 422:\n return 'The dictation language was rejected. Use a BCP-47 tag such as en-US.';\n case 429:\n return 'Too many dictations in a row. Wait a moment and try again.';\n case 503:\n return 'Dictation is not set up on this server.';\n default:\n return `Transcription failed (${status}).`;\n }\n}\n\n/**\n * Post a finished clip to agora's `/transcription/upload` route, which runs\n * it through Deepgram and returns the text. Credentials are read at call\n * time so a rotated token is picked up. The session language (host `lang`\n * or the document's) is forwarded so non-English speech is recognised as\n * such; the request is dropped when the engine aborts.\n */\nexport function createAthenaUploadTranscriber(\n getConfig: () => Pick<AthenaConfig, 'backendUrl' | 'apiKey' | 'token' | 'getToken'>,\n): DictationTranscriber {\n return async (clip, { lang, signal }) => {\n const config = getConfig();\n const headers = await resolveStatewireAuthHeaders(config, { staleAuth: false });\n const form = new FormData();\n const extension = clip.type.includes('mp4') ? 'mp4' : clip.type.includes('ogg') ? 'ogg' : 'webm';\n form.append('file', clip, `dictation.${extension}`);\n if (lang) form.append('language', lang);\n const response = await fetch(transcriptionUploadUrl(config.backendUrl), {\n method: 'POST',\n headers,\n body: form,\n credentials: 'include',\n signal,\n });\n if (!response.ok) throw new Error(transcriptionUploadErrorMessage(response.status));\n const payload = (await response.json()) as UploadTranscriptionResponse;\n return typeof payload.transcript === 'string' ? payload.transcript : '';\n };\n}\n\n/**\n * The composer's default speech engine: record with `MediaRecorder`, then\n * transcribe the clip through agora (Deepgram). Falls back to the browser's\n * Web Speech API only where `MediaRecorder` is missing. A host-provided\n * `engine` always wins.\n */\nexport function useAthenaDictationEngine(override?: DictationEngine): DictationEngine {\n const config = useAthenaConfig();\n const configRef = useRef(config);\n configRef.current = config;\n return useMemo(() => {\n if (override) return override;\n const recorded = createRecordedDictationEngine({\n transcribe: createAthenaUploadTranscriber(() => configRef.current),\n });\n return recorded.isSupported() ? recorded : createWebSpeechDictationEngine();\n }, [override]);\n}\n"],"names":["resolveStatewireAuthHeaders","useAthenaConfig","useRef","useMemo","createRecordedDictationEngine","createWebSpeechDictationEngine"],"mappings":";;;;;;;AAWO,SAAS,uBAAuB,YAA4B;AACjE,SAAO,GAAG,WAAW,QAAQ,2BAA2B,EAAE,EAAE,QAAQ,OAAO,EAAE,CAAC;AAChF;AAMO,SAAS,gCAAgC,QAAwB;AACtE,UAAQ,QAAA;AAAA,IACN,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO,yBAAyB,MAAM;AAAA,EAAA;AAE5C;AASO,SAAS,8BACd,WACsB;AACtB,SAAO,OAAO,MAAM,EAAE,MAAM,aAAa;AACvC,UAAM,SAAS,UAAA;AACf,UAAM,UAAU,MAAMA,qBAAAA,4BAA4B,QAAQ,EAAE,WAAW,OAAO;AAC9E,UAAM,OAAO,IAAI,SAAA;AACjB,UAAM,YAAY,KAAK,KAAK,SAAS,KAAK,IAAI,QAAQ,KAAK,KAAK,SAAS,KAAK,IAAI,QAAQ;AAC1F,SAAK,OAAO,QAAQ,MAAM,aAAa,SAAS,EAAE;AAClD,QAAI,KAAM,MAAK,OAAO,YAAY,IAAI;AACtC,UAAM,WAAW,MAAM,MAAM,uBAAuB,OAAO,UAAU,GAAG;AAAA,MACtE,QAAQ;AAAA,MACR;AAAA,MACA,MAAM;AAAA,MACN,aAAa;AAAA,MACb;AAAA,IAAA,CACD;AACD,QAAI,CAAC,SAAS,GAAI,OAAM,IAAI,MAAM,gCAAgC,SAAS,MAAM,CAAC;AAClF,UAAM,UAAW,MAAM,SAAS,KAAA;AAChC,WAAO,OAAO,QAAQ,eAAe,WAAW,QAAQ,aAAa;AAAA,EACvE;AACF;AAQO,SAAS,yBAAyB,UAA6C;AACpF,QAAM,SAASC,cAAAA,gBAAA;AACf,QAAM,YAAYC,MAAAA,OAAO,MAAM;AAC/B,YAAU,UAAU;AACpB,SAAOC,MAAAA,QAAQ,MAAM;AACnB,QAAI,SAAU,QAAO;AACrB,UAAM,WAAWC,wBAAAA,8BAA8B;AAAA,MAC7C,YAAY,8BAA8B,MAAM,UAAU,OAAO;AAAA,IAAA,CAClE;AACD,WAAO,SAAS,gBAAgB,WAAWC,gBAAAA,+BAAA;AAAA,EAC7C,GAAG,CAAC,QAAQ,CAAC;AACf;;;;;"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type AthenaConfig } from '../provider/AthenaContext';
|
|
2
|
+
import { type DictationEngine } from './dictation-engine';
|
|
3
|
+
import { type DictationTranscriber } from './recorded-dictation-engine';
|
|
4
|
+
/** Agora's REST base from the configured backend URL (which may point at a nested endpoint). */
|
|
5
|
+
export declare function transcriptionUploadUrl(backendUrl: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* One line for the composer per way `/transcription/upload` refuses a clip.
|
|
8
|
+
* The copy lives here, apart from the fetch, so it can be pinned by a test.
|
|
9
|
+
*/
|
|
10
|
+
export declare function transcriptionUploadErrorMessage(status: number): string;
|
|
11
|
+
/**
|
|
12
|
+
* Post a finished clip to agora's `/transcription/upload` route, which runs
|
|
13
|
+
* it through Deepgram and returns the text. Credentials are read at call
|
|
14
|
+
* time so a rotated token is picked up. The session language (host `lang`
|
|
15
|
+
* or the document's) is forwarded so non-English speech is recognised as
|
|
16
|
+
* such; the request is dropped when the engine aborts.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createAthenaUploadTranscriber(getConfig: () => Pick<AthenaConfig, 'backendUrl' | 'apiKey' | 'token' | 'getToken'>): DictationTranscriber;
|
|
19
|
+
/**
|
|
20
|
+
* The composer's default speech engine: record with `MediaRecorder`, then
|
|
21
|
+
* transcribe the clip through agora (Deepgram). Falls back to the browser's
|
|
22
|
+
* Web Speech API only where `MediaRecorder` is missing. A host-provided
|
|
23
|
+
* `engine` always wins.
|
|
24
|
+
*/
|
|
25
|
+
export declare function useAthenaDictationEngine(override?: DictationEngine): DictationEngine;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useRef, useMemo } from "react";
|
|
2
|
+
import { useAthenaConfig } from "../provider/AthenaContext.js";
|
|
3
|
+
import { resolveStatewireAuthHeaders } from "../runtime/statewire-auth-headers.js";
|
|
4
|
+
import { createWebSpeechDictationEngine } from "./dictation-engine.js";
|
|
5
|
+
import { createRecordedDictationEngine } from "./recorded-dictation-engine.js";
|
|
6
|
+
function transcriptionUploadUrl(backendUrl) {
|
|
7
|
+
return `${backendUrl.replace(/\/api\/assistant-ui\/?$/, "").replace(/\/$/, "")}/api/transcription/upload`;
|
|
8
|
+
}
|
|
9
|
+
function transcriptionUploadErrorMessage(status) {
|
|
10
|
+
switch (status) {
|
|
11
|
+
case 401:
|
|
12
|
+
case 403:
|
|
13
|
+
return "Dictation is not available for this sign-in. Reload and try again.";
|
|
14
|
+
case 413:
|
|
15
|
+
return "That recording is too long to transcribe.";
|
|
16
|
+
case 422:
|
|
17
|
+
return "The dictation language was rejected. Use a BCP-47 tag such as en-US.";
|
|
18
|
+
case 429:
|
|
19
|
+
return "Too many dictations in a row. Wait a moment and try again.";
|
|
20
|
+
case 503:
|
|
21
|
+
return "Dictation is not set up on this server.";
|
|
22
|
+
default:
|
|
23
|
+
return `Transcription failed (${status}).`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function createAthenaUploadTranscriber(getConfig) {
|
|
27
|
+
return async (clip, { lang, signal }) => {
|
|
28
|
+
const config = getConfig();
|
|
29
|
+
const headers = await resolveStatewireAuthHeaders(config, { staleAuth: false });
|
|
30
|
+
const form = new FormData();
|
|
31
|
+
const extension = clip.type.includes("mp4") ? "mp4" : clip.type.includes("ogg") ? "ogg" : "webm";
|
|
32
|
+
form.append("file", clip, `dictation.${extension}`);
|
|
33
|
+
if (lang) form.append("language", lang);
|
|
34
|
+
const response = await fetch(transcriptionUploadUrl(config.backendUrl), {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers,
|
|
37
|
+
body: form,
|
|
38
|
+
credentials: "include",
|
|
39
|
+
signal
|
|
40
|
+
});
|
|
41
|
+
if (!response.ok) throw new Error(transcriptionUploadErrorMessage(response.status));
|
|
42
|
+
const payload = await response.json();
|
|
43
|
+
return typeof payload.transcript === "string" ? payload.transcript : "";
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function useAthenaDictationEngine(override) {
|
|
47
|
+
const config = useAthenaConfig();
|
|
48
|
+
const configRef = useRef(config);
|
|
49
|
+
configRef.current = config;
|
|
50
|
+
return useMemo(() => {
|
|
51
|
+
if (override) return override;
|
|
52
|
+
const recorded = createRecordedDictationEngine({
|
|
53
|
+
transcribe: createAthenaUploadTranscriber(() => configRef.current)
|
|
54
|
+
});
|
|
55
|
+
return recorded.isSupported() ? recorded : createWebSpeechDictationEngine();
|
|
56
|
+
}, [override]);
|
|
57
|
+
}
|
|
58
|
+
export {
|
|
59
|
+
createAthenaUploadTranscriber,
|
|
60
|
+
transcriptionUploadErrorMessage,
|
|
61
|
+
transcriptionUploadUrl,
|
|
62
|
+
useAthenaDictationEngine
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=use-athena-dictation-engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-athena-dictation-engine.js","sources":["../../src/dictation/use-athena-dictation-engine.ts"],"sourcesContent":["import { useMemo, useRef } from 'react';\nimport { type AthenaConfig, useAthenaConfig } from '../provider/AthenaContext';\nimport { resolveStatewireAuthHeaders } from '../runtime/statewire-auth-headers';\nimport { createWebSpeechDictationEngine, type DictationEngine } from './dictation-engine';\nimport { createRecordedDictationEngine, type DictationTranscriber } from './recorded-dictation-engine';\n\ninterface UploadTranscriptionResponse {\n transcript: string;\n}\n\n/** Agora's REST base from the configured backend URL (which may point at a nested endpoint). */\nexport function transcriptionUploadUrl(backendUrl: string): string {\n return `${backendUrl.replace(/\\/api\\/assistant-ui\\/?$/, '').replace(/\\/$/, '')}/api/transcription/upload`;\n}\n\n/**\n * One line for the composer per way `/transcription/upload` refuses a clip.\n * The copy lives here, apart from the fetch, so it can be pinned by a test.\n */\nexport function transcriptionUploadErrorMessage(status: number): string {\n switch (status) {\n case 401:\n case 403:\n return 'Dictation is not available for this sign-in. Reload and try again.';\n case 413:\n return 'That recording is too long to transcribe.';\n case 422:\n return 'The dictation language was rejected. Use a BCP-47 tag such as en-US.';\n case 429:\n return 'Too many dictations in a row. Wait a moment and try again.';\n case 503:\n return 'Dictation is not set up on this server.';\n default:\n return `Transcription failed (${status}).`;\n }\n}\n\n/**\n * Post a finished clip to agora's `/transcription/upload` route, which runs\n * it through Deepgram and returns the text. Credentials are read at call\n * time so a rotated token is picked up. The session language (host `lang`\n * or the document's) is forwarded so non-English speech is recognised as\n * such; the request is dropped when the engine aborts.\n */\nexport function createAthenaUploadTranscriber(\n getConfig: () => Pick<AthenaConfig, 'backendUrl' | 'apiKey' | 'token' | 'getToken'>,\n): DictationTranscriber {\n return async (clip, { lang, signal }) => {\n const config = getConfig();\n const headers = await resolveStatewireAuthHeaders(config, { staleAuth: false });\n const form = new FormData();\n const extension = clip.type.includes('mp4') ? 'mp4' : clip.type.includes('ogg') ? 'ogg' : 'webm';\n form.append('file', clip, `dictation.${extension}`);\n if (lang) form.append('language', lang);\n const response = await fetch(transcriptionUploadUrl(config.backendUrl), {\n method: 'POST',\n headers,\n body: form,\n credentials: 'include',\n signal,\n });\n if (!response.ok) throw new Error(transcriptionUploadErrorMessage(response.status));\n const payload = (await response.json()) as UploadTranscriptionResponse;\n return typeof payload.transcript === 'string' ? payload.transcript : '';\n };\n}\n\n/**\n * The composer's default speech engine: record with `MediaRecorder`, then\n * transcribe the clip through agora (Deepgram). Falls back to the browser's\n * Web Speech API only where `MediaRecorder` is missing. A host-provided\n * `engine` always wins.\n */\nexport function useAthenaDictationEngine(override?: DictationEngine): DictationEngine {\n const config = useAthenaConfig();\n const configRef = useRef(config);\n configRef.current = config;\n return useMemo(() => {\n if (override) return override;\n const recorded = createRecordedDictationEngine({\n transcribe: createAthenaUploadTranscriber(() => configRef.current),\n });\n return recorded.isSupported() ? recorded : createWebSpeechDictationEngine();\n }, [override]);\n}\n"],"names":[],"mappings":";;;;;AAWO,SAAS,uBAAuB,YAA4B;AACjE,SAAO,GAAG,WAAW,QAAQ,2BAA2B,EAAE,EAAE,QAAQ,OAAO,EAAE,CAAC;AAChF;AAMO,SAAS,gCAAgC,QAAwB;AACtE,UAAQ,QAAA;AAAA,IACN,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO,yBAAyB,MAAM;AAAA,EAAA;AAE5C;AASO,SAAS,8BACd,WACsB;AACtB,SAAO,OAAO,MAAM,EAAE,MAAM,aAAa;AACvC,UAAM,SAAS,UAAA;AACf,UAAM,UAAU,MAAM,4BAA4B,QAAQ,EAAE,WAAW,OAAO;AAC9E,UAAM,OAAO,IAAI,SAAA;AACjB,UAAM,YAAY,KAAK,KAAK,SAAS,KAAK,IAAI,QAAQ,KAAK,KAAK,SAAS,KAAK,IAAI,QAAQ;AAC1F,SAAK,OAAO,QAAQ,MAAM,aAAa,SAAS,EAAE;AAClD,QAAI,KAAM,MAAK,OAAO,YAAY,IAAI;AACtC,UAAM,WAAW,MAAM,MAAM,uBAAuB,OAAO,UAAU,GAAG;AAAA,MACtE,QAAQ;AAAA,MACR;AAAA,MACA,MAAM;AAAA,MACN,aAAa;AAAA,MACb;AAAA,IAAA,CACD;AACD,QAAI,CAAC,SAAS,GAAI,OAAM,IAAI,MAAM,gCAAgC,SAAS,MAAM,CAAC;AAClF,UAAM,UAAW,MAAM,SAAS,KAAA;AAChC,WAAO,OAAO,QAAQ,eAAe,WAAW,QAAQ,aAAa;AAAA,EACvE;AACF;AAQO,SAAS,yBAAyB,UAA6C;AACpF,QAAM,SAAS,gBAAA;AACf,QAAM,YAAY,OAAO,MAAM;AAC/B,YAAU,UAAU;AACpB,SAAO,QAAQ,MAAM;AACnB,QAAI,SAAU,QAAO;AACrB,UAAM,WAAW,8BAA8B;AAAA,MAC7C,YAAY,8BAA8B,MAAM,UAAU,OAAO;AAAA,IAAA,CAClE;AACD,WAAO,SAAS,gBAAgB,WAAW,+BAAA;AAAA,EAC7C,GAAG,CAAC,QAAQ,CAAC;AACf;"}
|
|
@@ -4,7 +4,7 @@ const react = require("react");
|
|
|
4
4
|
const athenaDiagnostics = require("../diagnostics/athena-diagnostics.cjs");
|
|
5
5
|
const dictationEngine = require("./dictation-engine.cjs");
|
|
6
6
|
const dictationState = require("./dictation-state.cjs");
|
|
7
|
-
const
|
|
7
|
+
const DEFAULT_STOP_FLUSH_TIMEOUT_MS = 1500;
|
|
8
8
|
const EMPTY_TRANSCRIPT = { final: "", interim: "" };
|
|
9
9
|
function documentLanguage() {
|
|
10
10
|
if (typeof document !== "undefined" && document.documentElement.lang) {
|
|
@@ -36,6 +36,7 @@ function useDictation(options = {}) {
|
|
|
36
36
|
const onAutoEndRef = react.useRef(onAutoEnd);
|
|
37
37
|
onAutoEndRef.current = onAutoEnd;
|
|
38
38
|
const stopWaitersRef = react.useRef([]);
|
|
39
|
+
const sessionErrorRef = react.useRef(null);
|
|
39
40
|
const generationRef = react.useRef(0);
|
|
40
41
|
react.useEffect(() => {
|
|
41
42
|
setIsSupported(engine.isSupported());
|
|
@@ -43,7 +44,8 @@ function useDictation(options = {}) {
|
|
|
43
44
|
const settleWaiters = react.useCallback((spoken) => {
|
|
44
45
|
const waiters = stopWaitersRef.current;
|
|
45
46
|
stopWaitersRef.current = [];
|
|
46
|
-
|
|
47
|
+
const result = { text: spoken, error: sessionErrorRef.current };
|
|
48
|
+
for (const resolve of waiters) resolve(result);
|
|
47
49
|
}, []);
|
|
48
50
|
const releaseSession = react.useCallback(() => {
|
|
49
51
|
sessionRef.current = null;
|
|
@@ -65,6 +67,7 @@ function useDictation(options = {}) {
|
|
|
65
67
|
level: "warn",
|
|
66
68
|
data: { phase: "error", engine: engine.id, kind: engineError.kind }
|
|
67
69
|
});
|
|
70
|
+
sessionErrorRef.current = engineError;
|
|
68
71
|
dispatch({ type: "error", error: engineError });
|
|
69
72
|
},
|
|
70
73
|
[engine.id]
|
|
@@ -74,6 +77,7 @@ function useDictation(options = {}) {
|
|
|
74
77
|
const generation = generationRef.current + 1;
|
|
75
78
|
generationRef.current = generation;
|
|
76
79
|
transcriptRef.current = EMPTY_TRANSCRIPT;
|
|
80
|
+
sessionErrorRef.current = null;
|
|
77
81
|
dispatch({ type: "start" });
|
|
78
82
|
athenaDiagnostics.athenaDiagnostics.emit("composer.dictation", {
|
|
79
83
|
level: "debug",
|
|
@@ -162,25 +166,31 @@ function useDictation(options = {}) {
|
|
|
162
166
|
if (stateRef.current.status === dictationState.DICTATION_STATUSES.starting) {
|
|
163
167
|
teardown();
|
|
164
168
|
dispatch({ type: "cancel" });
|
|
165
|
-
return Promise.resolve("");
|
|
169
|
+
return Promise.resolve({ text: "", error: null });
|
|
166
170
|
}
|
|
167
|
-
return Promise.resolve(dictationState.spokenText(transcriptRef.current));
|
|
171
|
+
return Promise.resolve({ text: dictationState.spokenText(transcriptRef.current), error: sessionErrorRef.current });
|
|
168
172
|
}
|
|
169
173
|
dispatch({ type: "stop" });
|
|
170
174
|
const flushed = new Promise((resolve) => {
|
|
171
175
|
stopWaitersRef.current.push(resolve);
|
|
172
176
|
window.setTimeout(() => {
|
|
173
177
|
if (sessionRef.current === session) {
|
|
178
|
+
if (!dictationState.spokenText(transcriptRef.current)) {
|
|
179
|
+
reportEngineError({
|
|
180
|
+
kind: dictationEngine.DICTATION_ENGINE_ERROR_KINDS.connection,
|
|
181
|
+
message: "Transcription timed out. Try again."
|
|
182
|
+
});
|
|
183
|
+
}
|
|
174
184
|
session.abort();
|
|
175
185
|
dispatch({ type: "ended" });
|
|
176
186
|
releaseSession();
|
|
177
187
|
settleWaiters(dictationState.spokenText(transcriptRef.current));
|
|
178
188
|
}
|
|
179
|
-
},
|
|
189
|
+
}, engine.flushTimeoutMs ?? DEFAULT_STOP_FLUSH_TIMEOUT_MS);
|
|
180
190
|
});
|
|
181
191
|
session.stop();
|
|
182
192
|
return flushed;
|
|
183
|
-
}, [releaseSession, settleWaiters, teardown]);
|
|
193
|
+
}, [engine.flushTimeoutMs, releaseSession, reportEngineError, settleWaiters, teardown]);
|
|
184
194
|
const cancel = react.useCallback(() => {
|
|
185
195
|
teardown();
|
|
186
196
|
dispatch({ type: "cancel" });
|
|
@@ -195,6 +205,7 @@ function useDictation(options = {}) {
|
|
|
195
205
|
error: state.error,
|
|
196
206
|
stream,
|
|
197
207
|
isSupported,
|
|
208
|
+
engine,
|
|
198
209
|
start,
|
|
199
210
|
stop,
|
|
200
211
|
cancel
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-dictation.cjs","sources":["../../src/dictation/use-dictation.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';\nimport { athenaDiagnostics } from '../diagnostics/athena-diagnostics';\nimport {\n createWebSpeechDictationEngine,\n DICTATION_ENGINE_ERROR_KINDS,\n type DictationEngine,\n type DictationEngineError,\n type DictationEngineSession,\n} from './dictation-engine';\nimport {\n DICTATION_STATUSES,\n dictationReducer,\n INITIAL_DICTATION_STATE,\n joinSpeech,\n spokenText,\n type DictationStatus,\n type DictationTranscript,\n} from './dictation-state';\n\nexport interface UseDictationOptions {\n /** Speech engine. Defaults to the browser's Web Speech API. */\n engine?: DictationEngine;\n /** BCP-47 language tag. Defaults to the document language, else `en-US`. */\n lang?: string;\n /**\n * Open the microphone through `getUserMedia` so callers get a `stream` for\n * a waveform. Web Speech opens its own capture; this second capture is\n * what the level meter reads. Defaults to true.\n */\n captureStream?: boolean;\n /**\n * The engine ended on its own (silence window, recognizer timeout) while\n * listening, without `stop()` or `cancel()`. Receives the spoken text so\n * the words are not lost; the composer puts them into the draft.\n */\n onAutoEnd?: (spoken: string) => void;\n}\n\nexport interface DictationController {\n status: DictationStatus;\n /** True while starting, listening or stopping. */\n isActive: boolean;\n transcript: DictationTranscript;\n /** `final` plus the interim tail, ready to hand to the composer. */\n text: string;\n error: DictationEngineError | null;\n /** The microphone stream while active (for a waveform), else null. */\n stream: MediaStream | null;\n /** Whether the engine can run here. Hide the control when false. */\n isSupported: boolean;\n start: () => void;\n /**\n * Finish recording. Resolves with the spoken text once the engine has\n * flushed (bounded wait), so trailing words are not lost.\n */\n stop: () => Promise<string>;\n /** Discard the session and its transcript. */\n cancel: () => void;\n}\n\n/** How long `stop()` waits for the engine's final flush before giving up. */\nconst STOP_FLUSH_TIMEOUT_MS = 1500;\n\ntype StopResolver = (spoken: string) => void;\n\nconst EMPTY_TRANSCRIPT: DictationTranscript = { final: '', interim: '' };\n\nfunction documentLanguage(): string {\n if (typeof document !== 'undefined' && document.documentElement.lang) {\n return document.documentElement.lang;\n }\n if (typeof navigator !== 'undefined' && navigator.language) return navigator.language;\n return 'en-US';\n}\n\nasync function openMicrophone(): Promise<MediaStream | null> {\n if (typeof navigator === 'undefined' || !navigator.mediaDevices?.getUserMedia) return null;\n return navigator.mediaDevices.getUserMedia({ audio: true });\n}\n\nfunction releaseStream(stream: MediaStream | null): void {\n if (!stream) return;\n for (const track of stream.getTracks()) track.stop();\n}\n\n/**\n * Drive a dictation session: microphone, speech engine, and the transcript\n * state machine. The composer renders the waveform from `stream` and the\n * live caption from `transcript`; on `stop()` it appends `text` to the draft.\n */\nexport function useDictation(options: UseDictationOptions = {}): DictationController {\n const engine = useMemo(() => options.engine ?? createWebSpeechDictationEngine(), [options.engine]);\n const { lang, captureStream = true, onAutoEnd } = options;\n const [state, dispatch] = useReducer(dictationReducer, INITIAL_DICTATION_STATE);\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [isSupported, setIsSupported] = useState(false);\n\n const sessionRef = useRef<DictationEngineSession | null>(null);\n const streamRef = useRef<MediaStream | null>(null);\n const stateRef = useRef(state);\n stateRef.current = state;\n // The authoritative transcript, updated synchronously as the engine reports\n // it. The reducer's copy renders the caption; this one is what `stop()` and\n // `onAutoEnd` hand over, so an engine that flushes its last words and ends\n // in the same tick — before React has rendered — loses nothing.\n const transcriptRef = useRef<DictationTranscript>(EMPTY_TRANSCRIPT);\n const onAutoEndRef = useRef(onAutoEnd);\n onAutoEndRef.current = onAutoEnd;\n // stop() callers waiting for the engine's final flush.\n const stopWaitersRef = useRef<StopResolver[]>([]);\n // Distinguishes a fresh session from the callbacks of a torn-down one.\n const generationRef = useRef(0);\n\n useEffect(() => {\n setIsSupported(engine.isSupported());\n }, [engine]);\n\n const settleWaiters = useCallback((spoken: string) => {\n const waiters = stopWaitersRef.current;\n stopWaitersRef.current = [];\n for (const resolve of waiters) resolve(spoken);\n }, []);\n\n const releaseSession = useCallback(() => {\n sessionRef.current = null;\n releaseStream(streamRef.current);\n streamRef.current = null;\n setStream(null);\n }, []);\n\n const teardown = useCallback(() => {\n generationRef.current += 1;\n sessionRef.current?.abort();\n releaseSession();\n transcriptRef.current = EMPTY_TRANSCRIPT;\n settleWaiters('');\n }, [releaseSession, settleWaiters]);\n\n const reportEngineError = useCallback(\n (engineError: DictationEngineError) => {\n athenaDiagnostics.emit('composer.dictation', {\n level: 'warn',\n data: { phase: 'error', engine: engine.id, kind: engineError.kind },\n });\n dispatch({ type: 'error', error: engineError });\n },\n [engine.id],\n );\n\n const start = useCallback(() => {\n if (sessionRef.current) return;\n const generation = generationRef.current + 1;\n generationRef.current = generation;\n transcriptRef.current = EMPTY_TRANSCRIPT;\n dispatch({ type: 'start' });\n athenaDiagnostics.emit('composer.dictation', {\n level: 'debug',\n data: { phase: 'start', engine: engine.id },\n });\n\n const run = async () => {\n let microphone: MediaStream | null = null;\n if (captureStream) {\n try {\n microphone = await openMicrophone();\n } catch (error) {\n if (generationRef.current !== generation) {\n releaseStream(microphone);\n return;\n }\n dispatch({\n type: 'error',\n error: {\n kind: DICTATION_ENGINE_ERROR_KINDS.permission,\n message:\n error instanceof DOMException && error.name === 'NotFoundError'\n ? 'No microphone was found.'\n : 'Microphone access was denied. Allow the microphone for this site and try again.',\n },\n });\n return;\n }\n }\n if (generationRef.current !== generation) {\n releaseStream(microphone);\n return;\n }\n streamRef.current = microphone;\n setStream(microphone);\n\n // An engine may fail synchronously inside start() (Web Speech throws\n // when recognition.start() is refused) and run onError/onEnd before we\n // hold the session. Those callbacks flip this flag so a dead session is\n // never stored, which would make every later mic click return early.\n let terminated = false;\n const endSession = () => {\n terminated = true;\n const current = stateRef.current;\n const spoken = spokenText(transcriptRef.current);\n // No stop() is waiting: the engine gave up on its own. Hand the\n // words over so they land in the draft instead of vanishing.\n const endedOnItsOwn =\n stopWaitersRef.current.length === 0 && current.status === DICTATION_STATUSES.listening;\n // The reducer keeps an error status through 'ended', so an error\n // reported just before the engine closed stays on screen.\n dispatch({ type: 'ended' });\n releaseSession();\n settleWaiters(spoken);\n if (endedOnItsOwn) onAutoEndRef.current?.(spoken);\n };\n let session: DictationEngineSession;\n try {\n session = engine.start({\n stream: microphone,\n lang: lang ?? documentLanguage(),\n callbacks: {\n onTranscript: (update) => {\n if (generationRef.current !== generation) return;\n transcriptRef.current = {\n final: joinSpeech(transcriptRef.current.final, update.final),\n interim: update.interim,\n };\n dispatch({ type: 'transcript', update });\n },\n onError: (engineError) => {\n if (generationRef.current !== generation) return;\n // A silence timeout is the engine giving up politely; keep the\n // words we have instead of showing an error.\n if (engineError.kind === DICTATION_ENGINE_ERROR_KINDS.noSpeech) return;\n reportEngineError(engineError);\n },\n onEnd: () => {\n if (generationRef.current !== generation) return;\n endSession();\n },\n },\n });\n } catch (error) {\n // A relay engine that cannot build its session must not strand the\n // microphone or leave the composer in `starting`.\n releaseSession();\n reportEngineError({\n kind: DICTATION_ENGINE_ERROR_KINDS.connection,\n message: error instanceof Error ? error.message : 'Dictation could not start.',\n });\n return;\n }\n if (terminated) return;\n sessionRef.current = session;\n dispatch({ type: 'listening' });\n };\n void run();\n }, [captureStream, engine, lang, releaseSession, reportEngineError, settleWaiters]);\n\n const stop = useCallback((): Promise<string> => {\n const session = sessionRef.current;\n if (!session) {\n // Stopped while the microphone prompt was still open: nothing was heard\n // yet, and the pending start must not bring the engine up afterwards.\n if (stateRef.current.status === DICTATION_STATUSES.starting) {\n teardown();\n dispatch({ type: 'cancel' });\n return Promise.resolve('');\n }\n return Promise.resolve(spokenText(transcriptRef.current));\n }\n dispatch({ type: 'stop' });\n const flushed = new Promise<string>((resolve) => {\n stopWaitersRef.current.push(resolve);\n window.setTimeout(() => {\n // Bounded wait: a recognizer that never fires onend must not hold the\n // composer hostage. Force the session closed and hand over what was\n // heard so far.\n if (sessionRef.current === session) {\n session.abort();\n dispatch({ type: 'ended' });\n releaseSession();\n settleWaiters(spokenText(transcriptRef.current));\n }\n }, STOP_FLUSH_TIMEOUT_MS);\n });\n session.stop();\n return flushed;\n }, [releaseSession, settleWaiters, teardown]);\n\n const cancel = useCallback(() => {\n teardown();\n dispatch({ type: 'cancel' });\n }, [teardown]);\n\n // Release the microphone with the component.\n useEffect(() => () => teardown(), [teardown]);\n\n const isActive =\n state.status === DICTATION_STATUSES.starting ||\n state.status === DICTATION_STATUSES.listening ||\n state.status === DICTATION_STATUSES.stopping;\n\n return {\n status: state.status,\n isActive,\n transcript: state.transcript,\n text: spokenText(state.transcript),\n error: state.error,\n stream,\n isSupported,\n start,\n stop,\n cancel,\n };\n}\n"],"names":["useMemo","createWebSpeechDictationEngine","useReducer","dictationReducer","INITIAL_DICTATION_STATE","useState","useRef","useEffect","useCallback","athenaDiagnostics","DICTATION_ENGINE_ERROR_KINDS","spokenText","DICTATION_STATUSES","joinSpeech"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-dictation.cjs","sources":["../../src/dictation/use-dictation.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';\nimport { athenaDiagnostics } from '../diagnostics/athena-diagnostics';\nimport {\n createWebSpeechDictationEngine,\n DICTATION_ENGINE_ERROR_KINDS,\n type DictationEngine,\n type DictationEngineError,\n type DictationEngineSession,\n} from './dictation-engine';\nimport {\n DICTATION_STATUSES,\n dictationReducer,\n INITIAL_DICTATION_STATE,\n joinSpeech,\n spokenText,\n type DictationStatus,\n type DictationTranscript,\n} from './dictation-state';\n\nexport interface UseDictationOptions {\n /**\n * Speech engine. Defaults to the browser's Web Speech API; `AthenaChat`\n * passes the record-then-transcribe engine from `useAthenaDictationEngine`.\n */\n engine?: DictationEngine;\n /** BCP-47 language tag. Defaults to the document language, else `en-US`. */\n lang?: string;\n /**\n * Open the microphone through `getUserMedia` so callers get a `stream` for\n * a waveform. Web Speech opens its own capture; this second capture is\n * what the level meter reads. Defaults to true.\n */\n captureStream?: boolean;\n /**\n * The engine ended on its own (silence window, recognizer timeout) while\n * listening, without `stop()` or `cancel()`. Receives the spoken text so\n * the words are not lost; the composer puts them into the draft.\n */\n onAutoEnd?: (spoken: string) => void;\n}\n\nexport interface DictationStopResult {\n /** Spoken text (final plus interim tail) gathered before the engine closed. */\n text: string;\n /**\n * The engine error reported during this session, if any. When set the\n * transcript may be incomplete: a recorded engine whose upload failed has no\n * words to hand over, so callers should not send the draft as if it were.\n */\n error: DictationEngineError | null;\n}\n\nexport interface DictationController {\n status: DictationStatus;\n /** True while starting, listening or stopping. */\n isActive: boolean;\n transcript: DictationTranscript;\n /** `final` plus the interim tail, ready to hand to the composer. */\n text: string;\n error: DictationEngineError | null;\n /** The microphone stream while active (for a waveform), else null. */\n stream: MediaStream | null;\n /** Whether the engine can run here. Hide the control when false. */\n isSupported: boolean;\n /** The engine in use (for its `stoppingLabel` and diagnostics). */\n engine: DictationEngine;\n start: () => void;\n /**\n * Finish recording. Resolves once the engine has flushed (bounded wait), so\n * trailing words are not lost, with the spoken text and any error raised\n * along the way.\n */\n stop: () => Promise<DictationStopResult>;\n /** Discard the session and its transcript. */\n cancel: () => void;\n}\n\n/** How long `stop()` waits for a live engine's final flush before giving up. */\nconst DEFAULT_STOP_FLUSH_TIMEOUT_MS = 1500;\n\ntype StopResolver = (result: DictationStopResult) => void;\n\nconst EMPTY_TRANSCRIPT: DictationTranscript = { final: '', interim: '' };\n\nfunction documentLanguage(): string {\n if (typeof document !== 'undefined' && document.documentElement.lang) {\n return document.documentElement.lang;\n }\n if (typeof navigator !== 'undefined' && navigator.language) return navigator.language;\n return 'en-US';\n}\n\nasync function openMicrophone(): Promise<MediaStream | null> {\n if (typeof navigator === 'undefined' || !navigator.mediaDevices?.getUserMedia) return null;\n return navigator.mediaDevices.getUserMedia({ audio: true });\n}\n\nfunction releaseStream(stream: MediaStream | null): void {\n if (!stream) return;\n for (const track of stream.getTracks()) track.stop();\n}\n\n/**\n * Drive a dictation session: microphone, speech engine, and the transcript\n * state machine. The composer renders the waveform from `stream` and the\n * live caption from `transcript`; on `stop()` it appends `text` to the draft.\n */\nexport function useDictation(options: UseDictationOptions = {}): DictationController {\n const engine = useMemo(() => options.engine ?? createWebSpeechDictationEngine(), [options.engine]);\n const { lang, captureStream = true, onAutoEnd } = options;\n const [state, dispatch] = useReducer(dictationReducer, INITIAL_DICTATION_STATE);\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [isSupported, setIsSupported] = useState(false);\n\n const sessionRef = useRef<DictationEngineSession | null>(null);\n const streamRef = useRef<MediaStream | null>(null);\n const stateRef = useRef(state);\n stateRef.current = state;\n // The authoritative transcript, updated synchronously as the engine reports\n // it. The reducer's copy renders the caption; this one is what `stop()` and\n // `onAutoEnd` hand over, so an engine that flushes its last words and ends\n // in the same tick — before React has rendered — loses nothing.\n const transcriptRef = useRef<DictationTranscript>(EMPTY_TRANSCRIPT);\n const onAutoEndRef = useRef(onAutoEnd);\n onAutoEndRef.current = onAutoEnd;\n // stop() callers waiting for the engine's final flush.\n const stopWaitersRef = useRef<StopResolver[]>([]);\n // Last engine error of the current session, handed to stop() callers\n // synchronously (the reducer's copy may not have rendered yet).\n const sessionErrorRef = useRef<DictationEngineError | null>(null);\n // Distinguishes a fresh session from the callbacks of a torn-down one.\n const generationRef = useRef(0);\n\n useEffect(() => {\n setIsSupported(engine.isSupported());\n }, [engine]);\n\n const settleWaiters = useCallback((spoken: string) => {\n const waiters = stopWaitersRef.current;\n stopWaitersRef.current = [];\n const result: DictationStopResult = { text: spoken, error: sessionErrorRef.current };\n for (const resolve of waiters) resolve(result);\n }, []);\n\n const releaseSession = useCallback(() => {\n sessionRef.current = null;\n releaseStream(streamRef.current);\n streamRef.current = null;\n setStream(null);\n }, []);\n\n const teardown = useCallback(() => {\n generationRef.current += 1;\n sessionRef.current?.abort();\n releaseSession();\n transcriptRef.current = EMPTY_TRANSCRIPT;\n settleWaiters('');\n }, [releaseSession, settleWaiters]);\n\n const reportEngineError = useCallback(\n (engineError: DictationEngineError) => {\n athenaDiagnostics.emit('composer.dictation', {\n level: 'warn',\n data: { phase: 'error', engine: engine.id, kind: engineError.kind },\n });\n sessionErrorRef.current = engineError;\n dispatch({ type: 'error', error: engineError });\n },\n [engine.id],\n );\n\n const start = useCallback(() => {\n if (sessionRef.current) return;\n const generation = generationRef.current + 1;\n generationRef.current = generation;\n transcriptRef.current = EMPTY_TRANSCRIPT;\n sessionErrorRef.current = null;\n dispatch({ type: 'start' });\n athenaDiagnostics.emit('composer.dictation', {\n level: 'debug',\n data: { phase: 'start', engine: engine.id },\n });\n\n const run = async () => {\n let microphone: MediaStream | null = null;\n if (captureStream) {\n try {\n microphone = await openMicrophone();\n } catch (error) {\n if (generationRef.current !== generation) {\n releaseStream(microphone);\n return;\n }\n dispatch({\n type: 'error',\n error: {\n kind: DICTATION_ENGINE_ERROR_KINDS.permission,\n message:\n error instanceof DOMException && error.name === 'NotFoundError'\n ? 'No microphone was found.'\n : 'Microphone access was denied. Allow the microphone for this site and try again.',\n },\n });\n return;\n }\n }\n if (generationRef.current !== generation) {\n releaseStream(microphone);\n return;\n }\n streamRef.current = microphone;\n setStream(microphone);\n\n // An engine may fail synchronously inside start() (Web Speech throws\n // when recognition.start() is refused) and run onError/onEnd before we\n // hold the session. Those callbacks flip this flag so a dead session is\n // never stored, which would make every later mic click return early.\n let terminated = false;\n const endSession = () => {\n terminated = true;\n const current = stateRef.current;\n const spoken = spokenText(transcriptRef.current);\n // No stop() is waiting: the engine gave up on its own. Hand the\n // words over so they land in the draft instead of vanishing.\n const endedOnItsOwn =\n stopWaitersRef.current.length === 0 && current.status === DICTATION_STATUSES.listening;\n // The reducer keeps an error status through 'ended', so an error\n // reported just before the engine closed stays on screen.\n dispatch({ type: 'ended' });\n releaseSession();\n settleWaiters(spoken);\n if (endedOnItsOwn) onAutoEndRef.current?.(spoken);\n };\n let session: DictationEngineSession;\n try {\n session = engine.start({\n stream: microphone,\n lang: lang ?? documentLanguage(),\n callbacks: {\n onTranscript: (update) => {\n if (generationRef.current !== generation) return;\n transcriptRef.current = {\n final: joinSpeech(transcriptRef.current.final, update.final),\n interim: update.interim,\n };\n dispatch({ type: 'transcript', update });\n },\n onError: (engineError) => {\n if (generationRef.current !== generation) return;\n // A silence timeout is the engine giving up politely; keep the\n // words we have instead of showing an error.\n if (engineError.kind === DICTATION_ENGINE_ERROR_KINDS.noSpeech) return;\n reportEngineError(engineError);\n },\n onEnd: () => {\n if (generationRef.current !== generation) return;\n endSession();\n },\n },\n });\n } catch (error) {\n // A relay engine that cannot build its session must not strand the\n // microphone or leave the composer in `starting`.\n releaseSession();\n reportEngineError({\n kind: DICTATION_ENGINE_ERROR_KINDS.connection,\n message: error instanceof Error ? error.message : 'Dictation could not start.',\n });\n return;\n }\n if (terminated) return;\n sessionRef.current = session;\n dispatch({ type: 'listening' });\n };\n void run();\n }, [captureStream, engine, lang, releaseSession, reportEngineError, settleWaiters]);\n\n const stop = useCallback((): Promise<DictationStopResult> => {\n const session = sessionRef.current;\n if (!session) {\n // Stopped while the microphone prompt was still open: nothing was heard\n // yet, and the pending start must not bring the engine up afterwards.\n if (stateRef.current.status === DICTATION_STATUSES.starting) {\n teardown();\n dispatch({ type: 'cancel' });\n return Promise.resolve({ text: '', error: null });\n }\n return Promise.resolve({ text: spokenText(transcriptRef.current), error: sessionErrorRef.current });\n }\n dispatch({ type: 'stop' });\n const flushed = new Promise<DictationStopResult>((resolve) => {\n stopWaitersRef.current.push(resolve);\n window.setTimeout(() => {\n // Bounded wait: a recognizer that never fires onend must not hold the\n // composer hostage. Force the session closed and hand over what was\n // heard so far. An engine that had nothing to show yet (a recorded\n // clip still transcribing) surfaces that as an error rather than\n // passing off silence as the result.\n if (sessionRef.current === session) {\n // Report before abort(): an engine that ends synchronously on\n // abort settles the waiters, and they must see this error.\n if (!spokenText(transcriptRef.current)) {\n reportEngineError({\n kind: DICTATION_ENGINE_ERROR_KINDS.connection,\n message: 'Transcription timed out. Try again.',\n });\n }\n session.abort();\n dispatch({ type: 'ended' });\n releaseSession();\n settleWaiters(spokenText(transcriptRef.current));\n }\n }, engine.flushTimeoutMs ?? DEFAULT_STOP_FLUSH_TIMEOUT_MS);\n });\n session.stop();\n return flushed;\n }, [engine.flushTimeoutMs, releaseSession, reportEngineError, settleWaiters, teardown]);\n\n const cancel = useCallback(() => {\n teardown();\n dispatch({ type: 'cancel' });\n }, [teardown]);\n\n // Release the microphone with the component.\n useEffect(() => () => teardown(), [teardown]);\n\n const isActive =\n state.status === DICTATION_STATUSES.starting ||\n state.status === DICTATION_STATUSES.listening ||\n state.status === DICTATION_STATUSES.stopping;\n\n return {\n status: state.status,\n isActive,\n transcript: state.transcript,\n text: spokenText(state.transcript),\n error: state.error,\n stream,\n isSupported,\n engine,\n start,\n stop,\n cancel,\n };\n}\n"],"names":["useMemo","createWebSpeechDictationEngine","useReducer","dictationReducer","INITIAL_DICTATION_STATE","useState","useRef","useEffect","useCallback","athenaDiagnostics","DICTATION_ENGINE_ERROR_KINDS","spokenText","DICTATION_STATUSES","joinSpeech"],"mappings":"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"}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { type DictationEngine, type DictationEngineError } from './dictation-engine';
|
|
2
2
|
import { type DictationStatus, type DictationTranscript } from './dictation-state';
|
|
3
3
|
export interface UseDictationOptions {
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Speech engine. Defaults to the browser's Web Speech API; `AthenaChat`
|
|
6
|
+
* passes the record-then-transcribe engine from `useAthenaDictationEngine`.
|
|
7
|
+
*/
|
|
5
8
|
engine?: DictationEngine;
|
|
6
9
|
/** BCP-47 language tag. Defaults to the document language, else `en-US`. */
|
|
7
10
|
lang?: string;
|
|
@@ -18,6 +21,16 @@ export interface UseDictationOptions {
|
|
|
18
21
|
*/
|
|
19
22
|
onAutoEnd?: (spoken: string) => void;
|
|
20
23
|
}
|
|
24
|
+
export interface DictationStopResult {
|
|
25
|
+
/** Spoken text (final plus interim tail) gathered before the engine closed. */
|
|
26
|
+
text: string;
|
|
27
|
+
/**
|
|
28
|
+
* The engine error reported during this session, if any. When set the
|
|
29
|
+
* transcript may be incomplete: a recorded engine whose upload failed has no
|
|
30
|
+
* words to hand over, so callers should not send the draft as if it were.
|
|
31
|
+
*/
|
|
32
|
+
error: DictationEngineError | null;
|
|
33
|
+
}
|
|
21
34
|
export interface DictationController {
|
|
22
35
|
status: DictationStatus;
|
|
23
36
|
/** True while starting, listening or stopping. */
|
|
@@ -30,12 +43,15 @@ export interface DictationController {
|
|
|
30
43
|
stream: MediaStream | null;
|
|
31
44
|
/** Whether the engine can run here. Hide the control when false. */
|
|
32
45
|
isSupported: boolean;
|
|
46
|
+
/** The engine in use (for its `stoppingLabel` and diagnostics). */
|
|
47
|
+
engine: DictationEngine;
|
|
33
48
|
start: () => void;
|
|
34
49
|
/**
|
|
35
|
-
* Finish recording. Resolves
|
|
36
|
-
*
|
|
50
|
+
* Finish recording. Resolves once the engine has flushed (bounded wait), so
|
|
51
|
+
* trailing words are not lost, with the spoken text and any error raised
|
|
52
|
+
* along the way.
|
|
37
53
|
*/
|
|
38
|
-
stop: () => Promise<
|
|
54
|
+
stop: () => Promise<DictationStopResult>;
|
|
39
55
|
/** Discard the session and its transcript. */
|
|
40
56
|
cancel: () => void;
|
|
41
57
|
}
|