@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.
@@ -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 { speechFailureMessage } from '../lib/speech-messages';
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 [error, setError] = useState<string | null>(null);
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 message = speechFailureMessage(failure);
120
- if (message === null) return;
129
+ const code = speechFailureCode(failure);
130
+ if (code === null) return;
121
131
  failed = true;
122
- if (mountedRef.current) setError(message);
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
- }, [lang, setPhase]);
157
+ }, [report, setPhase]);
145
158
 
146
159
  const startRecording = useCallback(async () => {
147
160
  if (stateRef.current !== 'idle') return;
148
- setError(null);
161
+ report(null);
149
162
 
150
163
  if (!isSpeechRecognitionAvailable()) {
151
- setError(speechFailureMessage({ code: 'unsupported' }));
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
- setError(speechFailureMessage(refusal));
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
- setError(null);
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
- error,
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,
@@ -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. Defaults to the build's `PREFERRED_VOICE_MODEL_ID`.
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 ?? PREFERRED_VOICE_MODEL_ID;
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.httpService.getAccessToken();
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 { speechFailureMessage } from '../lib/speech-messages';
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. Defaults to the build's
70
- * `PREFERRED_VOICE_MODEL_ID`, the one profile `/v1/audio/speech` accepts.
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 routing profile the default sender asks for. Ignored when `sendTurn`
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 ?? PREFERRED_VOICE_MODEL_ID;
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
- const [error, setError] = useState<string | null>(null);
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 [turnError, setTurnError] = useState<string | null>(null);
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.httpService.getAccessToken();
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
- setError(message);
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
- { lang: configRef.current.lang, echoCancellation: true },
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(speechFailureMessage(failure) ?? 'Speech recognition failed');
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(speechFailureMessage(failure ?? { code: 'other' }) ?? 'Speech recognition failed');
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) setTurnError(null);
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, 'Voice request failed'));
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) setTurnError('No answer came back — try saying it again');
548
+ if (mountedRef.current) setTurnErrorCode('no-answer');
544
549
  } else if (turn.synthesisFailed) {
545
- if (mountedRef.current) setTurnError('The answer could not be played aloud');
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
- setError(null);
558
- setTurnError(null);
562
+ setCallFailure(null);
563
+ setTurnErrorCode(null);
559
564
  setRoomState('connecting');
560
565
 
561
566
  if (!isSpeechRecognitionAvailable()) {
562
- setError('Voice is not available on this device');
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
- setError('Not authenticated');
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
- setError(speechFailureMessage(refusal) ?? 'Microphone permission required');
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
- setError(null);
596
- setTurnError(null);
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
- error,
641
- turnError,
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 surface the SDK
34
- // reads, rather than hardcoding an identifier the way this package used to.
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 { CatalogueEntry, CatalogueEntryKind, ModelSelection } from './lib/catalogue';
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 {