@lokutor/sdk 1.5.0 → 1.6.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 CHANGED
@@ -11,7 +11,7 @@ Official JavaScript/TypeScript SDK for [Lokutor AI](https://lokutor.com), provid
11
11
 
12
12
  - ⚡ **Ultra-low latency**: Optimized WebSocket streaming for real-time interactions.
13
13
  - 🗣️ **Conversational Intelligence**: Integrated STT, LLM, and TTS with VAD and barge-in support.
14
- - 🌍 **Multi-lingual**: Support for English, Spanish, French, Portuguese, and Korean.
14
+ - 🌍 **Multi-lingual**: English, Spanish, Catalan, Galician, Basque, Portuguese, French, Italian and German (Catalan, Galician and Basque are speak-only: no speech recognition yet).
15
15
  - 🎨 **Natural Voices**: Multiple high-quality male and female voice styles.
16
16
  - 💻 **TypeScript First**: Full type definitions included.
17
17
 
@@ -63,9 +63,36 @@ await client.synthesize({
63
63
  });
64
64
  ```
65
65
 
66
+ ## Using the SDK in a browser without exposing your key
67
+
68
+ Never ship an API key in browser code. Mint a short-lived ticket on your server and let the SDK fetch a
69
+ fresh one before every connect (tickets last 10 minutes, so reconnects need a new one):
70
+
71
+ ```js
72
+ // Your server (Node 22+, key stays here)
73
+ app.post('/lokutor-ticket', async (_req, res) => {
74
+ const r = await fetch('https://api.lokutor.com/ws-ticket', {
75
+ method: 'POST',
76
+ headers: { Authorization: `Bearer ${process.env.LOKUTOR_API_KEY}` },
77
+ });
78
+ res.json(await r.json()); // { ticket: "wst_...", expires_in: 600 }
79
+ });
80
+
81
+ // The page
82
+ const agent = new VoiceAgentClient({
83
+ auth: async () => (await (await fetch('/lokutor-ticket', { method: 'POST' })).json()).ticket,
84
+ prompt: 'You are a helpful assistant.',
85
+ enableAudio: true,
86
+ });
87
+ await agent.connect();
88
+ ```
89
+
90
+ To run an agent you configured in the dashboard, pass `agentId` and leave out `prompt`, `voice` and
91
+ `language`: only what you set explicitly overrides the agent.
92
+
66
93
  ## Hardware Compatibility
67
94
 
68
- This SDK is platform-agnostic and works in both **Node.js** and **Browser** environments.
95
+ This SDK is platform-agnostic and works in both **Node.js** (22 or newer, for the global `WebSocket`) and **Browser** environments.
69
96
 
70
97
  ### For Node.js
71
98
  We recommend using `node-record-lpcm16` for recording and `speaker` for playback. See the [examples](./examples) folder for a complete Node.js CLI implementation.
package/dist/index.d.mts CHANGED
@@ -4,13 +4,13 @@
4
4
  /**
5
5
  * Built-in voices.
6
6
  *
7
- * Versa 2.0 names each voice for the language its reference speaker was recorded in: `EN_*` are
8
- * English speakers, `ESCA_*` Spanish/Catalan. That is a guide to how a voice sounds, not a
9
- * restriction — the model is zero-shot, so any voice can speak any supported language.
7
+ * Use `F1`–`F5` / `M1`–`M5` (what GET /voices lists). Each is a slot: the server resolves it to the
8
+ * native speaker of the call's language in that slot, so `F1` in Spanish is a Spanish speaker and `F1`
9
+ * in English an English one.
10
10
  *
11
- * The bare `F1`–`M5` values are the Versa 1.x names. They still work: the server maps them onto
12
- * the matching Versa 2.0 voice, keeping the language family of the call. Prefer the explicit names
13
- * in new code.
11
+ * `EN_*` pin the English speakers regardless of language. `ESCA_*` is the retired Spanish/Catalan pack
12
+ * (its recordings measure as noisy in the top octave); do not use it in new code — `F1`–`M5` with
13
+ * `language: 'es'` gives the vetted Spanish voices.
14
14
  *
15
15
  * This enum is a convenience, not the full set — a cloned voice's id (`clone_…`) is also a valid
16
16
  * `voice`, which is why the config fields accept any string.
@@ -26,35 +26,35 @@ declare enum VoiceStyle {
26
26
  EN_M3 = "en_m3",
27
27
  EN_M4 = "en_m4",
28
28
  EN_M5 = "en_m5",
29
+ /** @deprecated Retired pack; use F1 with language "es". */
29
30
  ESCA_F1 = "esca_f1",
31
+ /** @deprecated Retired pack; use F2 with language "es". */
30
32
  ESCA_F2 = "esca_f2",
33
+ /** @deprecated Retired pack; use F3 with language "es". */
31
34
  ESCA_F3 = "esca_f3",
35
+ /** @deprecated Retired pack; use F4 with language "es". */
32
36
  ESCA_F4 = "esca_f4",
37
+ /** @deprecated Retired pack; use F5 with language "es". */
33
38
  ESCA_F5 = "esca_f5",
39
+ /** @deprecated Retired pack; use M1 with language "es". */
34
40
  ESCA_M1 = "esca_m1",
41
+ /** @deprecated Retired pack; use M2 with language "es". */
35
42
  ESCA_M2 = "esca_m2",
43
+ /** @deprecated Retired pack; use M3 with language "es". */
36
44
  ESCA_M3 = "esca_m3",
45
+ /** @deprecated Retired pack; use M4 with language "es". */
37
46
  ESCA_M4 = "esca_m4",
47
+ /** @deprecated Retired pack; use M5 with language "es". */
38
48
  ESCA_M5 = "esca_m5",
39
- /** @deprecated Versa 1.x name, mapped server-side. Use EN_F1 / ESCA_F1. */
40
49
  F1 = "F1",
41
- /** @deprecated Versa 1.x name, mapped server-side. */
42
50
  F2 = "F2",
43
- /** @deprecated Versa 1.x name, mapped server-side. */
44
51
  F3 = "F3",
45
- /** @deprecated Versa 1.x name, mapped server-side. */
46
52
  F4 = "F4",
47
- /** @deprecated Versa 1.x name, mapped server-side. */
48
53
  F5 = "F5",
49
- /** @deprecated Versa 1.x name, mapped server-side. */
50
54
  M1 = "M1",
51
- /** @deprecated Versa 1.x name, mapped server-side. */
52
55
  M2 = "M2",
53
- /** @deprecated Versa 1.x name, mapped server-side. */
54
56
  M3 = "M3",
55
- /** @deprecated Versa 1.x name, mapped server-side. */
56
57
  M4 = "M4",
57
- /** @deprecated Versa 1.x name, mapped server-side. */
58
58
  M5 = "M5"
59
59
  }
60
60
  /**
@@ -107,7 +107,14 @@ declare const DEFAULT_URLS: {
107
107
  * SDK Configuration interface
108
108
  */
109
109
  interface LokutorConfig {
110
- apiKey: string;
110
+ /** An API key, or a short-lived wst_ ticket from POST /ws-ticket. Optional when `auth` is given. */
111
+ apiKey?: string;
112
+ /**
113
+ * Called before every connect (and reconnect) for the credential to use. In a browser, have it ask
114
+ * your backend for a fresh ticket (POST https://api.lokutor.com/ws-ticket with your API key) so the
115
+ * key itself never reaches the page; a ticket lasts 10 minutes, so reconnects need a new one.
116
+ */
117
+ auth?: () => string | Promise<string>;
111
118
  agentId?: string;
112
119
  onTranscription?: (text: string) => void;
113
120
  onResponse?: (text: string) => void;
@@ -285,11 +292,13 @@ interface ToolDefinition {
285
292
  interface ToolCall {
286
293
  name: string;
287
294
  arguments: string;
295
+ /** Pass to submitToolResult(call_id, result). */
296
+ call_id: string;
288
297
  }
289
298
  /**
290
299
  * Error code enum matching the backend API error catalog
291
300
  */
292
- type ErrorCode = 'auth.missing_key' | 'auth.invalid_key' | 'auth.rate_limited' | 'auth.time_limited' | 'validation.invalid_voice' | 'validation.invalid_language' | 'validation.text_too_long' | 'validation.speed_out_of_range' | 'validation.steps_out_of_range' | 'validation.invalid_request_format' | 'tts.synthesis_failed' | 'tts.voice_unavailable' | 'tts.model_not_found' | 'tts.session_limit_reached' | 'stt.not_configured' | 'stt.stream_create_failed' | 'stt.language_not_supported' | 'agent.session_failed' | 'agent.provider_error' | 'session.ended' | 'internal.error' | 'internal.timeout' | 'internal.cancelled' | 'ws.close';
301
+ type ErrorCode = 'auth.missing_key' | 'auth.invalid_key' | 'auth.rate_limited' | 'auth.time_limited' | 'validation.invalid_voice' | 'validation.invalid_language' | 'validation.text_too_long' | 'validation.speed_out_of_range' | 'validation.steps_out_of_range' | 'validation.invalid_request_format' | 'tts.synthesis_failed' | 'tts.voice_unavailable' | 'tts.model_not_found' | 'tts.session_limit_reached' | 'stt.not_configured' | 'stt.stream_create_failed' | 'stt.language_not_supported' | 'agent.session_failed' | 'agent.provider_error' | 'agent.not_found' | 'billing.insufficient_balance' | 'billing.topup_required' | 'billing.limit_reached' | 'billing.account_disputed' | 'session.ended' | 'audio.microphone_unavailable' | 'internal.error' | 'internal.timeout' | 'internal.cancelled' | 'ws.close';
293
302
  /**
294
303
  * Typed error class for all Lokutor SDK errors.
295
304
  * Includes the backend error code, human-readable message,
@@ -369,8 +378,12 @@ declare class VoiceAgentClient {
369
378
  private serverPlaybackRate;
370
379
  private serverInputRate;
371
380
  private serverUrl;
381
+ private authProvider?;
382
+ private voiceExplicit;
383
+ private languageExplicit;
372
384
  constructor(config: LokutorConfig & {
373
- prompt: string;
385
+ /** Required unless agentId is set (a dashboard agent brings its own prompt). */
386
+ prompt?: string;
374
387
  voice?: VoiceId;
375
388
  language?: Language;
376
389
  visemes?: boolean;
@@ -466,8 +479,8 @@ declare class VoiceAgentClient {
466
479
  */
467
480
  static fetchStatus(baseUrl?: string): Promise<ServerStatus>;
468
481
  /**
469
- * Fetch health/liveness status from the server.
470
- * No authentication required.
482
+ * Fetch readiness from the server (GET /ready: 200 once it can serve).
483
+ * No authentication required. (/health is the operator dashboard's and needs its password.)
471
484
  */
472
485
  static fetchHealth(baseUrl?: string): Promise<HealthStatus>;
473
486
  /**
@@ -699,8 +712,26 @@ interface BrowserAudioConfig {
699
712
  * the call goes silent in both directions. Set false if your app manages this itself.
700
713
  */
701
714
  keepAwake?: boolean;
715
+ /**
716
+ * A microphone stream the page already asked for, ideally with requestMicrophone() inside the tap that
717
+ * started the call. startMicrophone uses it instead of calling getUserMedia again later, after the
718
+ * connection opens -- which iOS Safari can refuse without showing its prompt.
719
+ */
720
+ microphone?: MediaStream | Promise<MediaStream>;
702
721
  onInputError?: (error: Error) => void;
703
722
  }
723
+ /**
724
+ * Ask for the microphone now, from inside the tap or click that starts a call, and return the pending stream.
725
+ *
726
+ * Call it synchronously in the gesture handler, before any await, and pass the result to BrowserAudioManager
727
+ * as `microphone` (ConversationalPanel does this itself). Two iOS Safari behaviours made a later request fail
728
+ * with no prompt at all -- the call connected, the agent spoke, and nothing the caller said was heard:
729
+ * - a request outside a user gesture may be refused instead of prompting;
730
+ * - while the page's audio session is in 'playback' mode (set by pages so audio plays through the mute
731
+ * switch), capture is refused outright. It is put back to 'auto', under which Safari switches to
732
+ * play-and-record itself when capture starts and keeps the loudspeaker.
733
+ */
734
+ declare function requestMicrophone(constraints?: Pick<BrowserAudioConfig, 'autoGainControl' | 'echoCancellation' | 'noiseSuppression'>): Promise<MediaStream>;
704
735
  /**
705
736
  * Analyser configuration for audio visualization
706
737
  */
@@ -729,6 +760,7 @@ declare class BrowserAudioManager {
729
760
  private echoCancellation;
730
761
  private noiseSuppression;
731
762
  private keepAwake;
763
+ private providedMicrophone;
732
764
  private wakeLock;
733
765
  private visibilityHandler;
734
766
  private onAudioInput?;
@@ -848,7 +880,11 @@ interface ConversationalPanelConfig {
848
880
  backgroundColor?: string;
849
881
  /** API key for connecting to the voice agent */
850
882
  apiKey: string;
851
- /** Reference to the ConvoAgent class */
883
+ /**
884
+ * Reference to the ConvoAgent class. Its constructor options include `microphone`, a
885
+ * Promise<MediaStream> requested inside the Start tap (see requestMicrophone); pass it on to
886
+ * BrowserAudioManager as `microphone` so the call uses it instead of asking again later.
887
+ */
852
888
  ConvoAgent: any;
853
889
  /** Reference to the SphereVisualizer class */
854
890
  SphereVisualizer: any;
@@ -863,6 +899,9 @@ interface ConversationalPanelConfig {
863
899
  /** Seconds of silence before auto-close (default 60) */
864
900
  silenceTimeout?: number;
865
901
  }
902
+ /** What to tell the caller when the microphone did not start: a DOMException from getUserMedia, or the
903
+ * SDK's 'audio.microphone_unavailable' error wrapping one. */
904
+ declare function microphoneMessage(err: any): string;
866
905
  declare class ConversationalPanel {
867
906
  private cfg;
868
907
  private container;
@@ -892,6 +931,8 @@ declare class ConversationalPanel {
892
931
  private countdownEl;
893
932
  private noteEl;
894
933
  private countdownTicker;
934
+ private microphone;
935
+ private micError;
895
936
  onTranscription?: (text: string) => void;
896
937
  onResponse?: (text: string) => void;
897
938
  onStart?: () => void;
@@ -907,8 +948,12 @@ declare class ConversationalPanel {
907
948
  constructor(cfg: ConversationalPanelConfig);
908
949
  private buildDOM;
909
950
  private esc;
910
- /** Start the conversation (called when user clicks "Start Conversation" or externally) */
911
- start(): Promise<void>;
951
+ /**
952
+ * Start the conversation (called when user clicks "Start Conversation" or externally). Call it from
953
+ * inside the click or tap, before any await: it asks for the microphone there. Resolves true once the
954
+ * call is live, false when it could not start (the reason is shown, and passed to onError).
955
+ */
956
+ start(): Promise<boolean>;
912
957
  /** Stop / disconnect the conversation */
913
958
  stop(): void;
914
959
  /** Update accent color dynamically */
@@ -941,4 +986,4 @@ declare class ConversationalPanel {
941
986
  private playErrorTone;
942
987
  }
943
988
 
944
- export { AUDIO_CONFIG, type AnalyserConfig, type AudioManager, type BrowserAudioConfig, BrowserAudioManager, type BrowserAudioOptions, ConversationalPanel, type ConversationalPanelConfig, DEFAULT_URLS, type ErrorCode, type HealthStatus, Language, type LanguageInfo, type LokutorConfig, LokutorError, type ModelInfo, NodeAudioManager, STTClient, type ServerConfig, type ServerStatus, type SessionLimit, SpeechToTextClient, type SpeechToTextOptions, StreamResampler, type SynthesizeOptions, TTSClient, type ToolCall, type ToolDefinition, type TranscribeOptions, type TranscribeResult, type TranscribeSegment, type Viseme, VoiceAgentClient, type VoiceAgentOptions, type VoiceId, type VoiceInfo, VoiceStyle, applyLowPassFilter, bytesToPcm16, calculateRMS, float32ToPcm16, isRetryable, normalizeAudio, pcm16ToBytes, pcm16ToFloat32, resample, resampleWithAntiAliasing, simpleConversation, simpleTTS, simpleTranscribe };
989
+ export { AUDIO_CONFIG, type AnalyserConfig, type AudioManager, type BrowserAudioConfig, BrowserAudioManager, type BrowserAudioOptions, ConversationalPanel, type ConversationalPanelConfig, DEFAULT_URLS, type ErrorCode, type HealthStatus, Language, type LanguageInfo, type LokutorConfig, LokutorError, type ModelInfo, NodeAudioManager, STTClient, type ServerConfig, type ServerStatus, type SessionLimit, SpeechToTextClient, type SpeechToTextOptions, StreamResampler, type SynthesizeOptions, TTSClient, type ToolCall, type ToolDefinition, type TranscribeOptions, type TranscribeResult, type TranscribeSegment, type Viseme, VoiceAgentClient, type VoiceAgentOptions, type VoiceId, type VoiceInfo, VoiceStyle, applyLowPassFilter, bytesToPcm16, calculateRMS, float32ToPcm16, isRetryable, microphoneMessage, normalizeAudio, pcm16ToBytes, pcm16ToFloat32, requestMicrophone, resample, resampleWithAntiAliasing, simpleConversation, simpleTTS, simpleTranscribe };
package/dist/index.d.ts CHANGED
@@ -4,13 +4,13 @@
4
4
  /**
5
5
  * Built-in voices.
6
6
  *
7
- * Versa 2.0 names each voice for the language its reference speaker was recorded in: `EN_*` are
8
- * English speakers, `ESCA_*` Spanish/Catalan. That is a guide to how a voice sounds, not a
9
- * restriction — the model is zero-shot, so any voice can speak any supported language.
7
+ * Use `F1`–`F5` / `M1`–`M5` (what GET /voices lists). Each is a slot: the server resolves it to the
8
+ * native speaker of the call's language in that slot, so `F1` in Spanish is a Spanish speaker and `F1`
9
+ * in English an English one.
10
10
  *
11
- * The bare `F1`–`M5` values are the Versa 1.x names. They still work: the server maps them onto
12
- * the matching Versa 2.0 voice, keeping the language family of the call. Prefer the explicit names
13
- * in new code.
11
+ * `EN_*` pin the English speakers regardless of language. `ESCA_*` is the retired Spanish/Catalan pack
12
+ * (its recordings measure as noisy in the top octave); do not use it in new code — `F1`–`M5` with
13
+ * `language: 'es'` gives the vetted Spanish voices.
14
14
  *
15
15
  * This enum is a convenience, not the full set — a cloned voice's id (`clone_…`) is also a valid
16
16
  * `voice`, which is why the config fields accept any string.
@@ -26,35 +26,35 @@ declare enum VoiceStyle {
26
26
  EN_M3 = "en_m3",
27
27
  EN_M4 = "en_m4",
28
28
  EN_M5 = "en_m5",
29
+ /** @deprecated Retired pack; use F1 with language "es". */
29
30
  ESCA_F1 = "esca_f1",
31
+ /** @deprecated Retired pack; use F2 with language "es". */
30
32
  ESCA_F2 = "esca_f2",
33
+ /** @deprecated Retired pack; use F3 with language "es". */
31
34
  ESCA_F3 = "esca_f3",
35
+ /** @deprecated Retired pack; use F4 with language "es". */
32
36
  ESCA_F4 = "esca_f4",
37
+ /** @deprecated Retired pack; use F5 with language "es". */
33
38
  ESCA_F5 = "esca_f5",
39
+ /** @deprecated Retired pack; use M1 with language "es". */
34
40
  ESCA_M1 = "esca_m1",
41
+ /** @deprecated Retired pack; use M2 with language "es". */
35
42
  ESCA_M2 = "esca_m2",
43
+ /** @deprecated Retired pack; use M3 with language "es". */
36
44
  ESCA_M3 = "esca_m3",
45
+ /** @deprecated Retired pack; use M4 with language "es". */
37
46
  ESCA_M4 = "esca_m4",
47
+ /** @deprecated Retired pack; use M5 with language "es". */
38
48
  ESCA_M5 = "esca_m5",
39
- /** @deprecated Versa 1.x name, mapped server-side. Use EN_F1 / ESCA_F1. */
40
49
  F1 = "F1",
41
- /** @deprecated Versa 1.x name, mapped server-side. */
42
50
  F2 = "F2",
43
- /** @deprecated Versa 1.x name, mapped server-side. */
44
51
  F3 = "F3",
45
- /** @deprecated Versa 1.x name, mapped server-side. */
46
52
  F4 = "F4",
47
- /** @deprecated Versa 1.x name, mapped server-side. */
48
53
  F5 = "F5",
49
- /** @deprecated Versa 1.x name, mapped server-side. */
50
54
  M1 = "M1",
51
- /** @deprecated Versa 1.x name, mapped server-side. */
52
55
  M2 = "M2",
53
- /** @deprecated Versa 1.x name, mapped server-side. */
54
56
  M3 = "M3",
55
- /** @deprecated Versa 1.x name, mapped server-side. */
56
57
  M4 = "M4",
57
- /** @deprecated Versa 1.x name, mapped server-side. */
58
58
  M5 = "M5"
59
59
  }
60
60
  /**
@@ -107,7 +107,14 @@ declare const DEFAULT_URLS: {
107
107
  * SDK Configuration interface
108
108
  */
109
109
  interface LokutorConfig {
110
- apiKey: string;
110
+ /** An API key, or a short-lived wst_ ticket from POST /ws-ticket. Optional when `auth` is given. */
111
+ apiKey?: string;
112
+ /**
113
+ * Called before every connect (and reconnect) for the credential to use. In a browser, have it ask
114
+ * your backend for a fresh ticket (POST https://api.lokutor.com/ws-ticket with your API key) so the
115
+ * key itself never reaches the page; a ticket lasts 10 minutes, so reconnects need a new one.
116
+ */
117
+ auth?: () => string | Promise<string>;
111
118
  agentId?: string;
112
119
  onTranscription?: (text: string) => void;
113
120
  onResponse?: (text: string) => void;
@@ -285,11 +292,13 @@ interface ToolDefinition {
285
292
  interface ToolCall {
286
293
  name: string;
287
294
  arguments: string;
295
+ /** Pass to submitToolResult(call_id, result). */
296
+ call_id: string;
288
297
  }
289
298
  /**
290
299
  * Error code enum matching the backend API error catalog
291
300
  */
292
- type ErrorCode = 'auth.missing_key' | 'auth.invalid_key' | 'auth.rate_limited' | 'auth.time_limited' | 'validation.invalid_voice' | 'validation.invalid_language' | 'validation.text_too_long' | 'validation.speed_out_of_range' | 'validation.steps_out_of_range' | 'validation.invalid_request_format' | 'tts.synthesis_failed' | 'tts.voice_unavailable' | 'tts.model_not_found' | 'tts.session_limit_reached' | 'stt.not_configured' | 'stt.stream_create_failed' | 'stt.language_not_supported' | 'agent.session_failed' | 'agent.provider_error' | 'session.ended' | 'internal.error' | 'internal.timeout' | 'internal.cancelled' | 'ws.close';
301
+ type ErrorCode = 'auth.missing_key' | 'auth.invalid_key' | 'auth.rate_limited' | 'auth.time_limited' | 'validation.invalid_voice' | 'validation.invalid_language' | 'validation.text_too_long' | 'validation.speed_out_of_range' | 'validation.steps_out_of_range' | 'validation.invalid_request_format' | 'tts.synthesis_failed' | 'tts.voice_unavailable' | 'tts.model_not_found' | 'tts.session_limit_reached' | 'stt.not_configured' | 'stt.stream_create_failed' | 'stt.language_not_supported' | 'agent.session_failed' | 'agent.provider_error' | 'agent.not_found' | 'billing.insufficient_balance' | 'billing.topup_required' | 'billing.limit_reached' | 'billing.account_disputed' | 'session.ended' | 'audio.microphone_unavailable' | 'internal.error' | 'internal.timeout' | 'internal.cancelled' | 'ws.close';
293
302
  /**
294
303
  * Typed error class for all Lokutor SDK errors.
295
304
  * Includes the backend error code, human-readable message,
@@ -369,8 +378,12 @@ declare class VoiceAgentClient {
369
378
  private serverPlaybackRate;
370
379
  private serverInputRate;
371
380
  private serverUrl;
381
+ private authProvider?;
382
+ private voiceExplicit;
383
+ private languageExplicit;
372
384
  constructor(config: LokutorConfig & {
373
- prompt: string;
385
+ /** Required unless agentId is set (a dashboard agent brings its own prompt). */
386
+ prompt?: string;
374
387
  voice?: VoiceId;
375
388
  language?: Language;
376
389
  visemes?: boolean;
@@ -466,8 +479,8 @@ declare class VoiceAgentClient {
466
479
  */
467
480
  static fetchStatus(baseUrl?: string): Promise<ServerStatus>;
468
481
  /**
469
- * Fetch health/liveness status from the server.
470
- * No authentication required.
482
+ * Fetch readiness from the server (GET /ready: 200 once it can serve).
483
+ * No authentication required. (/health is the operator dashboard's and needs its password.)
471
484
  */
472
485
  static fetchHealth(baseUrl?: string): Promise<HealthStatus>;
473
486
  /**
@@ -699,8 +712,26 @@ interface BrowserAudioConfig {
699
712
  * the call goes silent in both directions. Set false if your app manages this itself.
700
713
  */
701
714
  keepAwake?: boolean;
715
+ /**
716
+ * A microphone stream the page already asked for, ideally with requestMicrophone() inside the tap that
717
+ * started the call. startMicrophone uses it instead of calling getUserMedia again later, after the
718
+ * connection opens -- which iOS Safari can refuse without showing its prompt.
719
+ */
720
+ microphone?: MediaStream | Promise<MediaStream>;
702
721
  onInputError?: (error: Error) => void;
703
722
  }
723
+ /**
724
+ * Ask for the microphone now, from inside the tap or click that starts a call, and return the pending stream.
725
+ *
726
+ * Call it synchronously in the gesture handler, before any await, and pass the result to BrowserAudioManager
727
+ * as `microphone` (ConversationalPanel does this itself). Two iOS Safari behaviours made a later request fail
728
+ * with no prompt at all -- the call connected, the agent spoke, and nothing the caller said was heard:
729
+ * - a request outside a user gesture may be refused instead of prompting;
730
+ * - while the page's audio session is in 'playback' mode (set by pages so audio plays through the mute
731
+ * switch), capture is refused outright. It is put back to 'auto', under which Safari switches to
732
+ * play-and-record itself when capture starts and keeps the loudspeaker.
733
+ */
734
+ declare function requestMicrophone(constraints?: Pick<BrowserAudioConfig, 'autoGainControl' | 'echoCancellation' | 'noiseSuppression'>): Promise<MediaStream>;
704
735
  /**
705
736
  * Analyser configuration for audio visualization
706
737
  */
@@ -729,6 +760,7 @@ declare class BrowserAudioManager {
729
760
  private echoCancellation;
730
761
  private noiseSuppression;
731
762
  private keepAwake;
763
+ private providedMicrophone;
732
764
  private wakeLock;
733
765
  private visibilityHandler;
734
766
  private onAudioInput?;
@@ -848,7 +880,11 @@ interface ConversationalPanelConfig {
848
880
  backgroundColor?: string;
849
881
  /** API key for connecting to the voice agent */
850
882
  apiKey: string;
851
- /** Reference to the ConvoAgent class */
883
+ /**
884
+ * Reference to the ConvoAgent class. Its constructor options include `microphone`, a
885
+ * Promise<MediaStream> requested inside the Start tap (see requestMicrophone); pass it on to
886
+ * BrowserAudioManager as `microphone` so the call uses it instead of asking again later.
887
+ */
852
888
  ConvoAgent: any;
853
889
  /** Reference to the SphereVisualizer class */
854
890
  SphereVisualizer: any;
@@ -863,6 +899,9 @@ interface ConversationalPanelConfig {
863
899
  /** Seconds of silence before auto-close (default 60) */
864
900
  silenceTimeout?: number;
865
901
  }
902
+ /** What to tell the caller when the microphone did not start: a DOMException from getUserMedia, or the
903
+ * SDK's 'audio.microphone_unavailable' error wrapping one. */
904
+ declare function microphoneMessage(err: any): string;
866
905
  declare class ConversationalPanel {
867
906
  private cfg;
868
907
  private container;
@@ -892,6 +931,8 @@ declare class ConversationalPanel {
892
931
  private countdownEl;
893
932
  private noteEl;
894
933
  private countdownTicker;
934
+ private microphone;
935
+ private micError;
895
936
  onTranscription?: (text: string) => void;
896
937
  onResponse?: (text: string) => void;
897
938
  onStart?: () => void;
@@ -907,8 +948,12 @@ declare class ConversationalPanel {
907
948
  constructor(cfg: ConversationalPanelConfig);
908
949
  private buildDOM;
909
950
  private esc;
910
- /** Start the conversation (called when user clicks "Start Conversation" or externally) */
911
- start(): Promise<void>;
951
+ /**
952
+ * Start the conversation (called when user clicks "Start Conversation" or externally). Call it from
953
+ * inside the click or tap, before any await: it asks for the microphone there. Resolves true once the
954
+ * call is live, false when it could not start (the reason is shown, and passed to onError).
955
+ */
956
+ start(): Promise<boolean>;
912
957
  /** Stop / disconnect the conversation */
913
958
  stop(): void;
914
959
  /** Update accent color dynamically */
@@ -941,4 +986,4 @@ declare class ConversationalPanel {
941
986
  private playErrorTone;
942
987
  }
943
988
 
944
- export { AUDIO_CONFIG, type AnalyserConfig, type AudioManager, type BrowserAudioConfig, BrowserAudioManager, type BrowserAudioOptions, ConversationalPanel, type ConversationalPanelConfig, DEFAULT_URLS, type ErrorCode, type HealthStatus, Language, type LanguageInfo, type LokutorConfig, LokutorError, type ModelInfo, NodeAudioManager, STTClient, type ServerConfig, type ServerStatus, type SessionLimit, SpeechToTextClient, type SpeechToTextOptions, StreamResampler, type SynthesizeOptions, TTSClient, type ToolCall, type ToolDefinition, type TranscribeOptions, type TranscribeResult, type TranscribeSegment, type Viseme, VoiceAgentClient, type VoiceAgentOptions, type VoiceId, type VoiceInfo, VoiceStyle, applyLowPassFilter, bytesToPcm16, calculateRMS, float32ToPcm16, isRetryable, normalizeAudio, pcm16ToBytes, pcm16ToFloat32, resample, resampleWithAntiAliasing, simpleConversation, simpleTTS, simpleTranscribe };
989
+ export { AUDIO_CONFIG, type AnalyserConfig, type AudioManager, type BrowserAudioConfig, BrowserAudioManager, type BrowserAudioOptions, ConversationalPanel, type ConversationalPanelConfig, DEFAULT_URLS, type ErrorCode, type HealthStatus, Language, type LanguageInfo, type LokutorConfig, LokutorError, type ModelInfo, NodeAudioManager, STTClient, type ServerConfig, type ServerStatus, type SessionLimit, SpeechToTextClient, type SpeechToTextOptions, StreamResampler, type SynthesizeOptions, TTSClient, type ToolCall, type ToolDefinition, type TranscribeOptions, type TranscribeResult, type TranscribeSegment, type Viseme, VoiceAgentClient, type VoiceAgentOptions, type VoiceId, type VoiceInfo, VoiceStyle, applyLowPassFilter, bytesToPcm16, calculateRMS, float32ToPcm16, isRetryable, microphoneMessage, normalizeAudio, pcm16ToBytes, pcm16ToFloat32, requestMicrophone, resample, resampleWithAntiAliasing, simpleConversation, simpleTTS, simpleTranscribe };