@alia.onl/sdk 8.0.0 → 10.0.1
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/CHANGELOG.md +82 -1
- package/README.md +11 -3
- package/package.json +4 -4
- package/src/components/AliaChatScreen.tsx +44 -28
- package/src/components/IdentityMark.tsx +3 -0
- package/src/components/Markdown.tsx +89 -8
- package/src/components/voice/VoiceControls.tsx +46 -10
- package/src/hooks/useAliaChat.ts +8 -9
- package/src/hooks/useSpeechToText.ts +48 -14
- package/src/hooks/useTTS.ts +4 -9
- package/src/hooks/useVoiceRoom.ts +51 -30
- package/src/index.ts +6 -5
- package/src/lib/catalogue.ts +116 -103
- package/src/lib/chat-stream.ts +0 -5
- package/src/lib/chat-transport.ts +3 -2
- package/src/lib/keyboard.tsx +19 -1
- package/src/lib/speech-messages.ts +58 -13
- package/src/lib/speech-recognition-types.ts +33 -0
- package/src/lib/speech-recognition.native.ts +55 -0
- package/src/lib/speech-recognition.ts +9 -0
- package/src/lib/speech-synthesis.ts +2 -2
- package/src/lib/voice-turn.ts +3 -4
- package/src/voice.ts +4 -0
- package/src/lib/config.ts +0 -36
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
2
|
import { create } from 'zustand';
|
|
3
3
|
import {
|
|
4
|
+
chooseSpeechRecognizer,
|
|
4
5
|
isSpeechRecognitionAvailable,
|
|
5
6
|
requestSpeechRecognitionPermission,
|
|
6
7
|
startSpeechRecognition,
|
|
7
8
|
} from '../lib/speech-recognition';
|
|
8
9
|
import type { SpeechRecognitionSession } from '../lib/speech-recognition-types';
|
|
9
|
-
import {
|
|
10
|
+
import type { SpeechRecognizerChoice } from '../lib/speech-recognition-types';
|
|
11
|
+
import { speechFailureCode, VOICE_ERROR_MESSAGES, type VoiceErrorCode } from '../lib/speech-messages';
|
|
10
12
|
import { defaultSpeechLanguage } from '../lib/speech-language';
|
|
11
13
|
|
|
12
14
|
// ============== OPTIONS ==============
|
|
@@ -68,7 +70,7 @@ export function useSpeechToText(options: UseSTTOptions = {}) {
|
|
|
68
70
|
const lang = options.lang ?? defaultSpeechLanguage();
|
|
69
71
|
|
|
70
72
|
const [state, setState] = useState<STTState>('idle');
|
|
71
|
-
const [
|
|
73
|
+
const [errorCode, setErrorCode] = useState<VoiceErrorCode | null>(null);
|
|
72
74
|
const [isSupported] = useState(isSpeechRecognitionAvailable);
|
|
73
75
|
|
|
74
76
|
const stateRef = useRef<STTState>('idle');
|
|
@@ -78,10 +80,17 @@ export function useSpeechToText(options: UseSTTOptions = {}) {
|
|
|
78
80
|
/** Text from the live session. */
|
|
79
81
|
const currentTextRef = useRef('');
|
|
80
82
|
const restartsRef = useRef(0);
|
|
83
|
+
/** The recognizer and tag this dictation uses, chosen once when it starts. */
|
|
84
|
+
const recognizerRef = useRef<{ lang: string; service?: string; silent: boolean }>({ lang, silent: false });
|
|
81
85
|
const endWaitersRef = useRef<Array<() => void>>([]);
|
|
82
86
|
const lastMeteringRef = useRef(0);
|
|
83
87
|
const mountedRef = useRef(true);
|
|
84
88
|
|
|
89
|
+
/** What went wrong, as a code; `null` clears it. */
|
|
90
|
+
const report = useCallback((code: VoiceErrorCode | null) => {
|
|
91
|
+
if (mountedRef.current) setErrorCode(code);
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
85
94
|
const setPhase = useCallback((next: STTState) => {
|
|
86
95
|
stateRef.current = next;
|
|
87
96
|
if (mountedRef.current) setState(next);
|
|
@@ -102,8 +111,9 @@ export function useSpeechToText(options: UseSTTOptions = {}) {
|
|
|
102
111
|
const openSession = useCallback(() => {
|
|
103
112
|
currentTextRef.current = '';
|
|
104
113
|
let failed = false;
|
|
114
|
+
const recognizer = recognizerRef.current;
|
|
105
115
|
sessionRef.current = startSpeechRecognition(
|
|
106
|
-
{ lang },
|
|
116
|
+
{ lang: recognizer.lang, service: recognizer.service },
|
|
107
117
|
{
|
|
108
118
|
onResult: ({ transcript }) => {
|
|
109
119
|
currentTextRef.current = transcript;
|
|
@@ -116,10 +126,13 @@ export function useSpeechToText(options: UseSTTOptions = {}) {
|
|
|
116
126
|
useSTTStore.getState().setMetering(level);
|
|
117
127
|
},
|
|
118
128
|
onError: (failure) => {
|
|
119
|
-
const
|
|
120
|
-
if (
|
|
129
|
+
const code = speechFailureCode(failure);
|
|
130
|
+
if (code === null) return;
|
|
121
131
|
failed = true;
|
|
122
|
-
|
|
132
|
+
// Every recognizer on the device named no language, and the
|
|
133
|
+
// default refused this one: it is the device that cannot recognize
|
|
134
|
+
// speech, not the language that is wrong.
|
|
135
|
+
report(code === 'speech-language' && recognizer.silent ? 'speech-unsupported' : code);
|
|
123
136
|
},
|
|
124
137
|
onEnd: () => {
|
|
125
138
|
sessionRef.current = null;
|
|
@@ -141,14 +154,14 @@ export function useSpeechToText(options: UseSTTOptions = {}) {
|
|
|
141
154
|
},
|
|
142
155
|
},
|
|
143
156
|
);
|
|
144
|
-
}, [
|
|
157
|
+
}, [report, setPhase]);
|
|
145
158
|
|
|
146
159
|
const startRecording = useCallback(async () => {
|
|
147
160
|
if (stateRef.current !== 'idle') return;
|
|
148
|
-
|
|
161
|
+
report(null);
|
|
149
162
|
|
|
150
163
|
if (!isSpeechRecognitionAvailable()) {
|
|
151
|
-
|
|
164
|
+
report('speech-unsupported');
|
|
152
165
|
return;
|
|
153
166
|
}
|
|
154
167
|
|
|
@@ -157,16 +170,25 @@ export function useSpeechToText(options: UseSTTOptions = {}) {
|
|
|
157
170
|
const refusal = await requestSpeechRecognitionPermission();
|
|
158
171
|
if ((stateRef.current as STTState) !== 'recording') return; // cancelled while asking
|
|
159
172
|
if (refusal !== null) {
|
|
160
|
-
|
|
173
|
+
report(speechFailureCode(refusal));
|
|
174
|
+
setPhase('idle');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const choice: SpeechRecognizerChoice = await chooseSpeechRecognizer(lang);
|
|
179
|
+
if ((stateRef.current as STTState) !== 'recording') return; // cancelled while choosing
|
|
180
|
+
if ('failure' in choice) {
|
|
181
|
+
report(speechFailureCode(choice.failure));
|
|
161
182
|
setPhase('idle');
|
|
162
183
|
return;
|
|
163
184
|
}
|
|
185
|
+
recognizerRef.current = choice;
|
|
164
186
|
|
|
165
187
|
earlierTextRef.current = '';
|
|
166
188
|
currentTextRef.current = '';
|
|
167
189
|
restartsRef.current = 0;
|
|
168
190
|
openSession();
|
|
169
|
-
}, [openSession, setPhase]);
|
|
191
|
+
}, [lang, openSession, report, setPhase]);
|
|
170
192
|
|
|
171
193
|
const waitForEnd = useCallback((): Promise<void> => {
|
|
172
194
|
if (sessionRef.current === null) return Promise.resolve();
|
|
@@ -201,21 +223,33 @@ export function useSpeechToText(options: UseSTTOptions = {}) {
|
|
|
201
223
|
earlierTextRef.current = '';
|
|
202
224
|
currentTextRef.current = '';
|
|
203
225
|
session?.abort();
|
|
204
|
-
|
|
205
|
-
}, [setPhase]);
|
|
226
|
+
report(null);
|
|
227
|
+
}, [report, setPhase]);
|
|
206
228
|
|
|
207
229
|
useEffect(() => {
|
|
208
230
|
mountedRef.current = true;
|
|
209
231
|
return () => {
|
|
210
232
|
mountedRef.current = false;
|
|
233
|
+
const wasListening = stateRef.current !== 'idle';
|
|
211
234
|
stateRef.current = 'idle';
|
|
212
235
|
sessionRef.current?.abort();
|
|
236
|
+
// The effect that mirrors the phase into the store will not run again,
|
|
237
|
+
// and the store outlives this hook: a composer unmounted mid-dictation —
|
|
238
|
+
// the call controls replacing it — would otherwise leave the ambient
|
|
239
|
+
// field and the dictation bar reading "recording" for the rest of the call.
|
|
240
|
+
if (wasListening) {
|
|
241
|
+
useSTTStore.getState().setRecording(false);
|
|
242
|
+
useSTTStore.getState().setMetering(0);
|
|
243
|
+
}
|
|
213
244
|
};
|
|
214
245
|
}, []);
|
|
215
246
|
|
|
216
247
|
return {
|
|
217
248
|
state,
|
|
218
|
-
|
|
249
|
+
/** What went wrong, in English. */
|
|
250
|
+
error: errorCode === null ? null : VOICE_ERROR_MESSAGES[errorCode],
|
|
251
|
+
/** The same, as a code an app can translate. */
|
|
252
|
+
errorCode,
|
|
219
253
|
/** False where the platform cannot recognize speech at all (Firefox). */
|
|
220
254
|
isSupported,
|
|
221
255
|
startRecording,
|
package/src/hooks/useTTS.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { useOxy } from '@oxy.so/services';
|
|
|
5
5
|
import { errorMessage } from '../lib/utils';
|
|
6
6
|
import { create } from 'zustand';
|
|
7
7
|
import { createAudioLevelMeter } from '../lib/audio-level';
|
|
8
|
-
import { PREFERRED_VOICE_MODEL_ID } from '../lib/config';
|
|
9
8
|
import { requestSpeechClip } from '../lib/speech-synthesis';
|
|
10
9
|
|
|
11
10
|
const API_URL = process.env.EXPO_PUBLIC_ALIA_API_URL ?? 'https://api.alia.onl';
|
|
@@ -18,12 +17,8 @@ export interface UseTTSOptions {
|
|
|
18
17
|
voice?: 'male' | 'female';
|
|
19
18
|
tone?: 'brief' | 'chill' | 'default';
|
|
20
19
|
/**
|
|
21
|
-
* Speech model.
|
|
22
|
-
*
|
|
23
|
-
* Not checked against `GET /catalogue`: that surface describes what a chat
|
|
24
|
-
* picker may offer and its resolver filters to `chat_visible` entries, so
|
|
25
|
-
* passing a voice identifier through it would substitute a chat model that
|
|
26
|
-
* cannot speak. See `lib/config.ts`.
|
|
20
|
+
* Speech model. Omitted, the request carries no `model` and the server's
|
|
21
|
+
* own speech model answers.
|
|
27
22
|
*/
|
|
28
23
|
model?: string;
|
|
29
24
|
}
|
|
@@ -90,7 +85,7 @@ useTTSStore.subscribe((state, previous) => {
|
|
|
90
85
|
export function useTTS(options: UseTTSOptions = {}) {
|
|
91
86
|
const apiUrl = options.apiUrl || API_URL;
|
|
92
87
|
const voicePref = options.voice ?? 'female';
|
|
93
|
-
const voiceModel = options.model
|
|
88
|
+
const voiceModel = options.model;
|
|
94
89
|
const tonePref = options.tone ?? 'default';
|
|
95
90
|
|
|
96
91
|
const { oxyServices } = useOxy();
|
|
@@ -112,7 +107,7 @@ export function useTTS(options: UseTTSOptions = {}) {
|
|
|
112
107
|
|
|
113
108
|
const getToken = useCallback((): string | null => {
|
|
114
109
|
if (options.accessToken) return options.accessToken;
|
|
115
|
-
return oxyServices.
|
|
110
|
+
return oxyServices.session.accessToken;
|
|
116
111
|
}, [options.accessToken, oxyServices]);
|
|
117
112
|
|
|
118
113
|
const getTTSVoice = useCallback(() => {
|
|
@@ -31,14 +31,14 @@ import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
|
|
31
31
|
import { useOxy } from '@oxy.so/services';
|
|
32
32
|
import { errorMessage } from '../lib/utils';
|
|
33
33
|
import type { RoomState, AgentState, VoiceMessage } from '../types';
|
|
34
|
-
import { PREFERRED_VOICE_MODEL_ID } from '../lib/config';
|
|
35
34
|
import {
|
|
36
35
|
isSpeechRecognitionAvailable,
|
|
36
|
+
chooseSpeechRecognizer,
|
|
37
37
|
requestSpeechRecognitionPermission,
|
|
38
38
|
startSpeechRecognition,
|
|
39
39
|
} from '../lib/speech-recognition';
|
|
40
40
|
import type { SpeechRecognitionFailure, SpeechRecognitionSession } from '../lib/speech-recognition-types';
|
|
41
|
-
import {
|
|
41
|
+
import { speechFailureCode, VOICE_ERROR_MESSAGES, type VoiceErrorCode } from '../lib/speech-messages';
|
|
42
42
|
import { defaultSpeechLanguage } from '../lib/speech-language';
|
|
43
43
|
import { playSpeechClip, requestSpeechClip, type ProductVoice } from '../lib/speech-synthesis';
|
|
44
44
|
import { VoiceLevelChannel, type VoiceLevelSource } from '../lib/voice-levels';
|
|
@@ -66,15 +66,13 @@ export interface UseVoiceRoomOptions {
|
|
|
66
66
|
/** Bearer for speech synthesis; defaults to the surrounding Oxy session's. */
|
|
67
67
|
accessToken?: string;
|
|
68
68
|
/**
|
|
69
|
-
* Speech model for the answers.
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* Not checked against `GET /catalogue`, for the reason given in
|
|
73
|
-
* `lib/config.ts`.
|
|
69
|
+
* Speech model for the answers. Omitted, the request carries no `model` and
|
|
70
|
+
* the server's own speech model answers.
|
|
74
71
|
*/
|
|
75
72
|
model?: string;
|
|
76
73
|
/**
|
|
77
|
-
* Chat
|
|
74
|
+
* Chat model (`publisher/model`) the default sender asks for; omitted, the
|
|
75
|
+
* server's default. Ignored when `sendTurn`
|
|
78
76
|
* is given — that sender carries its own conversation's choice.
|
|
79
77
|
*/
|
|
80
78
|
chatModel?: string;
|
|
@@ -136,16 +134,21 @@ interface ActiveTurn {
|
|
|
136
134
|
export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
137
135
|
const apiUrl = options.apiUrl || API_URL;
|
|
138
136
|
const voicePref: ProductVoice = options.voicePreference ?? 'female';
|
|
139
|
-
const speechModel = options.model
|
|
137
|
+
const speechModel = options.model;
|
|
140
138
|
const endOfUtteranceMs = options.endOfUtteranceMs ?? DEFAULT_END_OF_UTTERANCE_MS;
|
|
141
139
|
const bargeIn = options.bargeIn ?? true;
|
|
142
140
|
|
|
143
141
|
const [roomState, setRoomState] = useState<RoomState>('disconnected');
|
|
144
142
|
const [agentState, setAgentStateValue] = useState<AgentState>('idle');
|
|
145
143
|
const [isMuted, setIsMuted] = useState(false);
|
|
146
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Why the call ended, as a code and as the sentence to show. The sentence is
|
|
146
|
+
* the code's English copy, except for `turn-failed`, which carries what the
|
|
147
|
+
* turn sender threw.
|
|
148
|
+
*/
|
|
149
|
+
const [callFailure, setCallFailure] = useState<{ code: VoiceErrorCode; message: string } | null>(null);
|
|
147
150
|
/** A turn that failed without ending the call: no answer, or no audio for it. */
|
|
148
|
-
const [
|
|
151
|
+
const [turnErrorCode, setTurnErrorCode] = useState<VoiceErrorCode | null>(null);
|
|
149
152
|
const [messages, setMessagesState] = useState<VoiceMessage[]>([]);
|
|
150
153
|
const [currentSpeaker, setCurrentSpeaker] = useState<'primary' | 'user' | null>(null);
|
|
151
154
|
|
|
@@ -160,6 +163,8 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
160
163
|
const mountedRef = useRef(true);
|
|
161
164
|
const messagesRef = useRef<VoiceMessage[]>([]);
|
|
162
165
|
const sessionRef = useRef<SpeechRecognitionSession | null>(null);
|
|
166
|
+
/** The recognizer and tag the call listens with, chosen when it connects. */
|
|
167
|
+
const recognizerRef = useRef<{ lang: string; service?: string }>({ lang: '' });
|
|
163
168
|
const heardRef = useRef('');
|
|
164
169
|
const committingRef = useRef(false);
|
|
165
170
|
const silenceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
@@ -220,7 +225,7 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
220
225
|
const getToken = useCallback((): string | null => {
|
|
221
226
|
const { accessToken } = configRef.current;
|
|
222
227
|
if (accessToken) return accessToken;
|
|
223
|
-
return oxyServices.
|
|
228
|
+
return oxyServices.session.accessToken;
|
|
224
229
|
}, [oxyServices]);
|
|
225
230
|
|
|
226
231
|
const clearTimers = (): void => {
|
|
@@ -267,11 +272,11 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
267
272
|
levels.reset();
|
|
268
273
|
}, [endTurn, levels]);
|
|
269
274
|
|
|
270
|
-
const fail = useCallback((message: string) => {
|
|
275
|
+
const fail = useCallback((code: VoiceErrorCode, message: string = VOICE_ERROR_MESSAGES[code]) => {
|
|
271
276
|
teardown();
|
|
272
277
|
discardDraft();
|
|
273
278
|
if (!mountedRef.current) return;
|
|
274
|
-
|
|
279
|
+
setCallFailure({ code, message });
|
|
275
280
|
setRoomState('error');
|
|
276
281
|
setAgentStateValue('idle');
|
|
277
282
|
setCurrentSpeaker(null);
|
|
@@ -381,7 +386,7 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
381
386
|
sessionStartedAtRef.current = Date.now();
|
|
382
387
|
|
|
383
388
|
sessionRef.current = startSpeechRecognition(
|
|
384
|
-
{
|
|
389
|
+
{ ...recognizerRef.current, echoCancellation: true },
|
|
385
390
|
{
|
|
386
391
|
onResult: ({ transcript }) => {
|
|
387
392
|
if (phaseRef.current === 'off' || committingRef.current) return;
|
|
@@ -426,7 +431,7 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
426
431
|
|
|
427
432
|
const failure = sessionFailedRef.current;
|
|
428
433
|
if (failure !== null && failure.code !== 'other') {
|
|
429
|
-
fail(
|
|
434
|
+
fail(speechFailureCode(failure) ?? 'speech-failed');
|
|
430
435
|
return;
|
|
431
436
|
}
|
|
432
437
|
|
|
@@ -440,7 +445,7 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
440
445
|
if (Date.now() - sessionStartedAtRef.current < RAPID_END_MS) {
|
|
441
446
|
rapidEndsRef.current += 1;
|
|
442
447
|
if (rapidEndsRef.current >= MAX_RAPID_ENDS) {
|
|
443
|
-
fail(
|
|
448
|
+
fail(speechFailureCode(failure ?? { code: 'other' }) ?? 'speech-failed');
|
|
444
449
|
return;
|
|
445
450
|
}
|
|
446
451
|
} else {
|
|
@@ -494,7 +499,7 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
494
499
|
};
|
|
495
500
|
turnRef.current = turn;
|
|
496
501
|
setAgent('thinking');
|
|
497
|
-
if (mountedRef.current)
|
|
502
|
+
if (mountedRef.current) setTurnErrorCode(null);
|
|
498
503
|
|
|
499
504
|
// The microphone stays open through thinking and speaking, for barge-in.
|
|
500
505
|
startListeningRef.current();
|
|
@@ -520,7 +525,7 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
520
525
|
} catch (caught: unknown) {
|
|
521
526
|
if (signal.aborted) return;
|
|
522
527
|
console.error('[useVoiceRoom] Turn failed:', caught);
|
|
523
|
-
fail(errorMessage(caught, '
|
|
528
|
+
fail('turn-failed', errorMessage(caught, VOICE_ERROR_MESSAGES['turn-failed']));
|
|
524
529
|
return;
|
|
525
530
|
}
|
|
526
531
|
if (signal.aborted) return;
|
|
@@ -540,9 +545,9 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
540
545
|
sessionRef.current?.abort();
|
|
541
546
|
if (turn.text.trim() === '') {
|
|
542
547
|
updateMessages((previous) => previous.filter((message) => message.id !== assistantId));
|
|
543
|
-
if (mountedRef.current)
|
|
548
|
+
if (mountedRef.current) setTurnErrorCode('no-answer');
|
|
544
549
|
} else if (turn.synthesisFailed) {
|
|
545
|
-
if (mountedRef.current)
|
|
550
|
+
if (mountedRef.current) setTurnErrorCode('not-played');
|
|
546
551
|
}
|
|
547
552
|
setAgent('listening');
|
|
548
553
|
startListeningRef.current();
|
|
@@ -554,17 +559,17 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
554
559
|
|
|
555
560
|
const connect = useCallback(async () => {
|
|
556
561
|
if (phaseRef.current !== 'off') return;
|
|
557
|
-
|
|
558
|
-
|
|
562
|
+
setCallFailure(null);
|
|
563
|
+
setTurnErrorCode(null);
|
|
559
564
|
setRoomState('connecting');
|
|
560
565
|
|
|
561
566
|
if (!isSpeechRecognitionAvailable()) {
|
|
562
|
-
|
|
567
|
+
setCallFailure({ code: 'voice-unavailable', message: VOICE_ERROR_MESSAGES['voice-unavailable'] });
|
|
563
568
|
setRoomState('error');
|
|
564
569
|
return;
|
|
565
570
|
}
|
|
566
571
|
if (getToken() === null) {
|
|
567
|
-
|
|
572
|
+
setCallFailure({ code: 'not-authenticated', message: VOICE_ERROR_MESSAGES['not-authenticated'] });
|
|
568
573
|
setRoomState('error');
|
|
569
574
|
return;
|
|
570
575
|
}
|
|
@@ -572,10 +577,22 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
572
577
|
const refusal = await requestSpeechRecognitionPermission();
|
|
573
578
|
if (!mountedRef.current) return;
|
|
574
579
|
if (refusal !== null) {
|
|
575
|
-
|
|
580
|
+
const code = speechFailureCode(refusal) ?? 'microphone-denied';
|
|
581
|
+
setCallFailure({ code, message: VOICE_ERROR_MESSAGES[code] });
|
|
582
|
+
setRoomState('error');
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// The recognizer that knows the call's language, as dictation picks it.
|
|
587
|
+
const choice = await chooseSpeechRecognizer(configRef.current.lang);
|
|
588
|
+
if (!mountedRef.current) return;
|
|
589
|
+
if ('failure' in choice) {
|
|
590
|
+
const code = speechFailureCode(choice.failure) ?? 'speech-language';
|
|
591
|
+
setCallFailure({ code, message: VOICE_ERROR_MESSAGES[code] });
|
|
576
592
|
setRoomState('error');
|
|
577
593
|
return;
|
|
578
594
|
}
|
|
595
|
+
recognizerRef.current = { lang: choice.lang, service: choice.service };
|
|
579
596
|
|
|
580
597
|
rapidEndsRef.current = 0;
|
|
581
598
|
setRoomState('connected');
|
|
@@ -592,8 +609,8 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
592
609
|
mutedRef.current = false;
|
|
593
610
|
setRoomState('disconnected');
|
|
594
611
|
setAgentStateValue('idle');
|
|
595
|
-
|
|
596
|
-
|
|
612
|
+
setCallFailure(null);
|
|
613
|
+
setTurnErrorCode(null);
|
|
597
614
|
setIsMuted(false);
|
|
598
615
|
setMessagesState([]);
|
|
599
616
|
setCurrentSpeaker(null);
|
|
@@ -637,8 +654,12 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
637
654
|
roomState,
|
|
638
655
|
agentState,
|
|
639
656
|
isMuted,
|
|
640
|
-
|
|
641
|
-
|
|
657
|
+
/** Why the call ended, in English. */
|
|
658
|
+
error: callFailure?.message ?? null,
|
|
659
|
+
/** The same, as a code an app can translate. */
|
|
660
|
+
errorCode: callFailure?.code ?? null,
|
|
661
|
+
turnError: turnErrorCode === null ? null : VOICE_ERROR_MESSAGES[turnErrorCode],
|
|
662
|
+
turnErrorCode,
|
|
642
663
|
messages,
|
|
643
664
|
currentSpeaker,
|
|
644
665
|
connect,
|
package/src/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ export { IdentityMark } from './components/IdentityMark';
|
|
|
13
13
|
export type { IdentityMarkProps, IdentityMarkState } from './components/IdentityMark';
|
|
14
14
|
export { ThinkingIndicator } from './components/ThinkingIndicator';
|
|
15
15
|
export { AliaMarkdown } from './components/Markdown';
|
|
16
|
-
export type { AliaMarkdownProps } from './components/Markdown';
|
|
16
|
+
export type { AliaMarkdownProps, RenderCodeBlock } from './components/Markdown';
|
|
17
17
|
export { Reasoning, ReasoningTrigger, ReasoningContent } from './components/Reasoning';
|
|
18
18
|
export { AliaWelcomeMessage } from './components/AliaWelcomeMessage';
|
|
19
19
|
export { ResearchProgressCard } from './components/ResearchProgressCard';
|
|
@@ -27,11 +27,13 @@ export type { UseAliaChatOptions, UseAliaChatReturn } from './hooks/useAliaChat'
|
|
|
27
27
|
export { useTTS } from './hooks/useTTS';
|
|
28
28
|
export type { UseTTSOptions } from './hooks/useTTS';
|
|
29
29
|
export { useSpeechToText, useSTTStore } from './hooks/useSpeechToText';
|
|
30
|
+
export { VOICE_ERROR_MESSAGES } from './lib/speech-messages';
|
|
31
|
+
export type { VoiceErrorCode } from './lib/speech-messages';
|
|
30
32
|
export type { UseSTTOptions } from './hooks/useSpeechToText';
|
|
31
33
|
|
|
32
34
|
// ── Model catalogue (GET /catalogue) ──
|
|
33
|
-
// Exported so a consumer building its own picker reads the
|
|
34
|
-
//
|
|
35
|
+
// Exported so a consumer building its own picker reads the real models the
|
|
36
|
+
// server offers; the SDK itself ships no model identifier.
|
|
35
37
|
export {
|
|
36
38
|
fetchCatalogue,
|
|
37
39
|
parseCatalogue,
|
|
@@ -39,8 +41,7 @@ export {
|
|
|
39
41
|
resolveModelId,
|
|
40
42
|
clearCatalogueCache,
|
|
41
43
|
} from './lib/catalogue';
|
|
42
|
-
export type {
|
|
43
|
-
export { PREFERRED_CHAT_MODEL_ID, PREFERRED_VOICE_MODEL_ID } from './lib/config';
|
|
44
|
+
export type { Catalogue, CatalogueEntry, ModelSelection, ReasoningEffort } from './lib/catalogue';
|
|
44
45
|
|
|
45
46
|
// ── Types ──
|
|
46
47
|
export type {
|