@lokutor/sdk 1.1.43 → 1.2.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/dist/index.d.mts +150 -5
- package/dist/index.d.ts +150 -5
- package/dist/index.js +339 -15
- package/dist/index.mjs +334 -14
- package/package.json +1 -1
- package/src/browser-audio.ts +32 -5
- package/src/client.ts +219 -0
- package/src/conversational-panel.ts +56 -12
- package/src/index.ts +15 -1
- package/src/node-audio.ts +15 -3
- package/src/types.ts +59 -0
package/src/client.ts
CHANGED
|
@@ -16,6 +16,9 @@ import {
|
|
|
16
16
|
ServerConfig,
|
|
17
17
|
ServerStatus,
|
|
18
18
|
HealthStatus,
|
|
19
|
+
TranscribeOptions,
|
|
20
|
+
TranscribeResult,
|
|
21
|
+
SpeechToTextOptions,
|
|
19
22
|
} from './types';
|
|
20
23
|
import { BrowserAudioManager } from './browser-audio';
|
|
21
24
|
|
|
@@ -97,6 +100,16 @@ function base64ToUint8Array(base64: string): Uint8Array {
|
|
|
97
100
|
return bytes;
|
|
98
101
|
}
|
|
99
102
|
|
|
103
|
+
// Browser-compatible Uint8Array to base64
|
|
104
|
+
function uint8ArrayToBase64(bytes: Uint8Array): string {
|
|
105
|
+
let binaryString = '';
|
|
106
|
+
const chunkSize = 0x8000; // avoid call-stack limits on String.fromCharCode(...large array)
|
|
107
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
108
|
+
binaryString += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
|
|
109
|
+
}
|
|
110
|
+
return btoa(binaryString);
|
|
111
|
+
}
|
|
112
|
+
|
|
100
113
|
function normalizeVisemes(payload: any): Viseme[] {
|
|
101
114
|
if (!Array.isArray(payload)) return [];
|
|
102
115
|
const normalized: Viseme[] = [];
|
|
@@ -144,6 +157,7 @@ function extractVisemePayload(msg: any): Viseme[] {
|
|
|
144
157
|
export class VoiceAgentClient {
|
|
145
158
|
private ws: WebSocket | null = null;
|
|
146
159
|
private apiKey: string;
|
|
160
|
+
private agentId: string = "";
|
|
147
161
|
public prompt: string;
|
|
148
162
|
public voice: VoiceStyle;
|
|
149
163
|
public language: Language;
|
|
@@ -192,6 +206,7 @@ export class VoiceAgentClient {
|
|
|
192
206
|
serverUrl?: string,
|
|
193
207
|
}) {
|
|
194
208
|
this.apiKey = config.apiKey;
|
|
209
|
+
this.agentId = config.agentId || "";
|
|
195
210
|
this.prompt = config.prompt;
|
|
196
211
|
this.voice = config.voice || VoiceStyle.F1;
|
|
197
212
|
this.language = config.language || Language.ENGLISH;
|
|
@@ -242,6 +257,10 @@ export class VoiceAgentClient {
|
|
|
242
257
|
const separator = url.includes('?') ? '&' : '?';
|
|
243
258
|
url += `${separator}api_key=${this.apiKey}`;
|
|
244
259
|
}
|
|
260
|
+
if (this.agentId) {
|
|
261
|
+
const separator = url.includes('?') ? '&' : '?';
|
|
262
|
+
url += `${separator}agent_id=${this.agentId}`;
|
|
263
|
+
}
|
|
245
264
|
|
|
246
265
|
const redactedUrl = url.replace(/api_key=[^&]+/, 'api_key=***');
|
|
247
266
|
sdkTrace('ws.connect', {
|
|
@@ -955,6 +974,198 @@ export class TTSClient {
|
|
|
955
974
|
}
|
|
956
975
|
}
|
|
957
976
|
|
|
977
|
+
/**
|
|
978
|
+
* Standalone speech-to-text — batch transcription of a single, complete
|
|
979
|
+
* audio clip (POST /stt/transcribe). No LLM, no TTS, no voice-agent session.
|
|
980
|
+
* For continuous/live transcription (e.g. dictation, live captions), use
|
|
981
|
+
* SpeechToTextClient instead.
|
|
982
|
+
*/
|
|
983
|
+
export class STTClient {
|
|
984
|
+
private apiKey: string;
|
|
985
|
+
private baseUrl: string;
|
|
986
|
+
|
|
987
|
+
constructor(config: { apiKey: string; serverUrl?: string }) {
|
|
988
|
+
this.apiKey = config.apiKey;
|
|
989
|
+
this.baseUrl = wsToHttp(config.serverUrl || DEFAULT_URLS.STT).replace(/\/ws\/stt\/?$/, '');
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Transcribe a complete audio clip. Pass a Blob/File (e.g. from a file
|
|
994
|
+
* input or MediaRecorder) for WAV/compressed audio, or a raw PCM16
|
|
995
|
+
* buffer with `format: "pcm16"` and `sampleRate` set.
|
|
996
|
+
*/
|
|
997
|
+
public async transcribe(options: TranscribeOptions): Promise<TranscribeResult> {
|
|
998
|
+
const url = `${this.baseUrl}/stt/transcribe`;
|
|
999
|
+
let res: Response;
|
|
1000
|
+
|
|
1001
|
+
if (typeof Blob !== 'undefined' && options.audio instanceof Blob) {
|
|
1002
|
+
const form = new FormData();
|
|
1003
|
+
form.append('audio', options.audio, 'audio.wav');
|
|
1004
|
+
if (options.language) form.append('lang', options.language);
|
|
1005
|
+
if (options.sampleRate) form.append('sample_rate', String(options.sampleRate));
|
|
1006
|
+
res = await fetch(url, {
|
|
1007
|
+
method: 'POST',
|
|
1008
|
+
headers: { 'X-API-Key': this.apiKey },
|
|
1009
|
+
body: form,
|
|
1010
|
+
});
|
|
1011
|
+
} else {
|
|
1012
|
+
const bytes = options.audio instanceof Uint8Array
|
|
1013
|
+
? options.audio
|
|
1014
|
+
: new Uint8Array(options.audio as ArrayBuffer);
|
|
1015
|
+
res = await fetch(url, {
|
|
1016
|
+
method: 'POST',
|
|
1017
|
+
headers: { 'X-API-Key': this.apiKey, 'Content-Type': 'application/json' },
|
|
1018
|
+
body: JSON.stringify({
|
|
1019
|
+
audio: uint8ArrayToBase64(bytes),
|
|
1020
|
+
format: options.format || 'pcm16',
|
|
1021
|
+
sample_rate: options.sampleRate,
|
|
1022
|
+
lang: options.language,
|
|
1023
|
+
}),
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
if (!res.ok) {
|
|
1028
|
+
const detail = await res.text().catch(() => '');
|
|
1029
|
+
throw new LokutorError('internal.error', `HTTP ${res.status} from ${url}`, {
|
|
1030
|
+
detail,
|
|
1031
|
+
retryable: res.status >= 500,
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const data = await res.json();
|
|
1036
|
+
return {
|
|
1037
|
+
text: data.text ?? '',
|
|
1038
|
+
latencyMs: data.latency_ms ?? 0,
|
|
1039
|
+
engine: data.engine ?? '',
|
|
1040
|
+
sampleRate: data.sample_rate ?? 0,
|
|
1041
|
+
language: data.language ?? '',
|
|
1042
|
+
durationSeconds: data.duration_seconds ?? 0,
|
|
1043
|
+
segments: data.segments,
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Continuous speech-to-text (WS /ws/stt) — streams microphone audio to the
|
|
1050
|
+
* server and receives partial transcripts while the user is speaking and a
|
|
1051
|
+
* final transcript when the server's VAD detects the utterance ended.
|
|
1052
|
+
* Standalone: no LLM turn, no TTS, no voice-agent session — just
|
|
1053
|
+
* transcription. For a single pre-recorded clip, use STTClient instead.
|
|
1054
|
+
*/
|
|
1055
|
+
export class SpeechToTextClient {
|
|
1056
|
+
private apiKey: string;
|
|
1057
|
+
private serverUrl: string;
|
|
1058
|
+
private language?: Language;
|
|
1059
|
+
private vad: 'silero' | 'rms';
|
|
1060
|
+
private onPartialTranscript?: (text: string) => void;
|
|
1061
|
+
private onFinalTranscript?: (text: string) => void;
|
|
1062
|
+
private onError?: (error: LokutorError) => void;
|
|
1063
|
+
private onStatusChange?: (status: 'connecting' | 'connected' | 'disconnected') => void;
|
|
1064
|
+
|
|
1065
|
+
private ws: WebSocket | null = null;
|
|
1066
|
+
private audioManager: AudioManager | null = null;
|
|
1067
|
+
private isConnected = false;
|
|
1068
|
+
|
|
1069
|
+
constructor(config: SpeechToTextOptions) {
|
|
1070
|
+
this.apiKey = config.apiKey;
|
|
1071
|
+
this.serverUrl = config.serverUrl || DEFAULT_URLS.STT;
|
|
1072
|
+
this.language = config.language;
|
|
1073
|
+
this.vad = config.vad || 'silero';
|
|
1074
|
+
this.onPartialTranscript = config.onPartialTranscript;
|
|
1075
|
+
this.onFinalTranscript = config.onFinalTranscript;
|
|
1076
|
+
this.onError = config.onError;
|
|
1077
|
+
this.onStatusChange = config.onStatusChange;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* Connect and start streaming microphone audio.
|
|
1082
|
+
* @param customAudioManager Optional replacement for the default audio hardware handler (e.g. NodeAudioManager for CLI use)
|
|
1083
|
+
*/
|
|
1084
|
+
public async connect(customAudioManager?: AudioManager): Promise<boolean> {
|
|
1085
|
+
this.audioManager = customAudioManager
|
|
1086
|
+
|| (typeof window !== 'undefined' ? new BrowserAudioManager() : null);
|
|
1087
|
+
if (!this.audioManager) {
|
|
1088
|
+
throw new LokutorError('internal.error', 'No audio manager available — pass one explicitly outside the browser (e.g. NodeAudioManager).');
|
|
1089
|
+
}
|
|
1090
|
+
await this.audioManager.init();
|
|
1091
|
+
|
|
1092
|
+
this.onStatusChange?.('connecting');
|
|
1093
|
+
|
|
1094
|
+
return new Promise((resolve, reject) => {
|
|
1095
|
+
let settled = false;
|
|
1096
|
+
const settle = (fn: () => void) => { if (!settled) { settled = true; fn(); } };
|
|
1097
|
+
|
|
1098
|
+
try {
|
|
1099
|
+
let url = this.serverUrl;
|
|
1100
|
+
const separator = url.includes('?') ? '&' : '?';
|
|
1101
|
+
url += `${separator}api_key=${this.apiKey}`;
|
|
1102
|
+
|
|
1103
|
+
this.ws = new WebSocket(url);
|
|
1104
|
+
|
|
1105
|
+
this.ws.onopen = async () => {
|
|
1106
|
+
this.isConnected = true;
|
|
1107
|
+
this.onStatusChange?.('connected');
|
|
1108
|
+
this.ws!.send(JSON.stringify({ lang: this.language || Language.ENGLISH, vad: this.vad }));
|
|
1109
|
+
|
|
1110
|
+
await this.audioManager!.startMicrophone((data) => {
|
|
1111
|
+
if (this.isConnected && this.ws?.readyState === WebSocket.OPEN) {
|
|
1112
|
+
this.ws.send(data);
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
settle(() => resolve(true));
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1119
|
+
this.ws.onmessage = (event) => {
|
|
1120
|
+
if (typeof event.data !== 'string') return; // this endpoint only ever sends JSON text frames
|
|
1121
|
+
try {
|
|
1122
|
+
const msg = JSON.parse(event.data);
|
|
1123
|
+
if (msg.type === 'transcript') {
|
|
1124
|
+
if (msg.isFinal) {
|
|
1125
|
+
this.onFinalTranscript?.(msg.data ?? '');
|
|
1126
|
+
} else {
|
|
1127
|
+
this.onPartialTranscript?.(msg.data ?? '');
|
|
1128
|
+
}
|
|
1129
|
+
} else if (msg.type === 'error') {
|
|
1130
|
+
this.onError?.(new LokutorError('internal.error', msg.data ?? 'STT error'));
|
|
1131
|
+
}
|
|
1132
|
+
} catch {
|
|
1133
|
+
// ignore malformed frames
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
this.ws.onerror = (err) => {
|
|
1138
|
+
const lokutorErr = new LokutorError('internal.error', 'WebSocket error', { original: err });
|
|
1139
|
+
this.onError?.(lokutorErr);
|
|
1140
|
+
settle(() => reject(lokutorErr));
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
this.ws.onclose = () => {
|
|
1144
|
+
this.isConnected = false;
|
|
1145
|
+
this.onStatusChange?.('disconnected');
|
|
1146
|
+
};
|
|
1147
|
+
} catch (err) {
|
|
1148
|
+
settle(() => reject(err));
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/** Force-finalize whatever utterance is currently in progress. */
|
|
1154
|
+
public endUtterance(): void {
|
|
1155
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
1156
|
+
this.ws.send(JSON.stringify({ type: 'end' }));
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
public disconnect(): void {
|
|
1161
|
+
this.isConnected = false;
|
|
1162
|
+
this.audioManager?.stopMicrophone();
|
|
1163
|
+
this.audioManager?.cleanup();
|
|
1164
|
+
this.ws?.close();
|
|
1165
|
+
this.ws = null;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
958
1169
|
/**
|
|
959
1170
|
* Quick function to start a conversation (requires manual audio piping in JS)
|
|
960
1171
|
*/
|
|
@@ -971,3 +1182,11 @@ export async function simpleTTS(options: SynthesizeOptions & { apiKey: string, o
|
|
|
971
1182
|
const client = new TTSClient({ apiKey: options.apiKey });
|
|
972
1183
|
return client.synthesize(options);
|
|
973
1184
|
}
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* Quick function for standalone, one-shot transcription of a complete audio clip.
|
|
1188
|
+
*/
|
|
1189
|
+
export async function simpleTranscribe(options: TranscribeOptions & { apiKey: string; serverUrl?: string }): Promise<TranscribeResult> {
|
|
1190
|
+
const client = new STTClient({ apiKey: options.apiKey, serverUrl: options.serverUrl });
|
|
1191
|
+
return client.transcribe(options);
|
|
1192
|
+
}
|
|
@@ -89,7 +89,7 @@ const PANEL_CSS = /*css*/ `
|
|
|
89
89
|
}
|
|
90
90
|
.cv-curtain-btn svg { transition: transform 0.3s ease; }
|
|
91
91
|
.cv-curtain-btn:hover svg { transform: translateX(4px); }
|
|
92
|
-
.cv-
|
|
92
|
+
.cv-error {
|
|
93
93
|
display: flex;
|
|
94
94
|
flex-direction: column;
|
|
95
95
|
align-items: center;
|
|
@@ -215,12 +215,47 @@ const PANEL_CSS = /*css*/ `
|
|
|
215
215
|
}
|
|
216
216
|
.cv-error.is-visible { display: flex; }
|
|
217
217
|
.cv-error-icon { color: var(--cv-accent); flex-shrink: 0; }
|
|
218
|
+
.cv-voice-picker {
|
|
219
|
+
display: flex;
|
|
220
|
+
flex-wrap: wrap;
|
|
221
|
+
justify-content: center;
|
|
222
|
+
gap: 0.35rem;
|
|
223
|
+
z-index: 2;
|
|
224
|
+
}
|
|
225
|
+
.cv-voice-btn {
|
|
226
|
+
padding: 0.25rem 0.5rem;
|
|
227
|
+
border-radius: 100px;
|
|
228
|
+
border: 1px solid rgba(255,255,255,0.15);
|
|
229
|
+
background: rgba(255,255,255,0.05);
|
|
230
|
+
color: rgba(255,255,255,0.6);
|
|
231
|
+
font-size: 0.65rem;
|
|
232
|
+
font-weight: 600;
|
|
233
|
+
cursor: pointer;
|
|
234
|
+
transition: all 0.2s ease;
|
|
235
|
+
}
|
|
236
|
+
.cv-voice-btn:hover {
|
|
237
|
+
background: rgba(255,255,255,0.12);
|
|
238
|
+
color: #fff;
|
|
239
|
+
}
|
|
240
|
+
.cv-voice-btn.is-selected {
|
|
241
|
+
background: #fff;
|
|
242
|
+
color: #000;
|
|
243
|
+
border-color: #fff;
|
|
244
|
+
}
|
|
245
|
+
.cv-voice-label {
|
|
246
|
+
font-size: 0.55rem;
|
|
247
|
+
text-transform: uppercase;
|
|
248
|
+
letter-spacing: 0.08em;
|
|
249
|
+
color: rgba(255,255,255,0.35);
|
|
250
|
+
z-index: 2;
|
|
251
|
+
}
|
|
218
252
|
|
|
219
253
|
/* === COMPACT MODE: < 300px width === */
|
|
220
254
|
@container (max-width: 299px) {
|
|
221
255
|
.cv-curtain-content { padding: 0.5rem; gap: 0.3rem; }
|
|
222
256
|
.cv-curtain-title { font-size: 0.8rem; }
|
|
223
257
|
.cv-curtain-desc { display: none; }
|
|
258
|
+
.cv-voice-picker { display: none; }
|
|
224
259
|
.cv-curtain-btn { padding: 0.3rem 0.6rem; font-size: 0.6rem; gap: 0.3rem; }
|
|
225
260
|
.cv-curtain-btn svg { display: none; }
|
|
226
261
|
.cv-visualizer-wrap { width: 60px; height: 60px; }
|
|
@@ -297,13 +332,19 @@ function injectStyles() {
|
|
|
297
332
|
}
|
|
298
333
|
|
|
299
334
|
export interface ConversationalPanelConfig {
|
|
335
|
+
/** Container element to mount the panel */
|
|
300
336
|
container: HTMLElement;
|
|
301
|
-
title
|
|
302
|
-
|
|
303
|
-
|
|
337
|
+
/** Agent name or title shown in the curtain */
|
|
338
|
+
title?: string;
|
|
339
|
+
/** Short description below the title */
|
|
340
|
+
description?: string;
|
|
341
|
+
/** System prompt for the voice agent */
|
|
342
|
+
prompt?: string;
|
|
343
|
+
/** Voice style identifier (default: M1) */
|
|
304
344
|
voice?: string;
|
|
345
|
+
/** Language code (default: en) */
|
|
305
346
|
language?: string;
|
|
306
|
-
/** Accent color for the sphere glow and highlights, e.g. '#
|
|
347
|
+
/** Accent color for the sphere glow and highlights, e.g. '#e74c3b' */
|
|
307
348
|
accentColor?: string;
|
|
308
349
|
/** Background color, e.g. '#0a0a0a' */
|
|
309
350
|
backgroundColor?: string;
|
|
@@ -336,12 +377,13 @@ export class ConversationalPanel {
|
|
|
336
377
|
private isRunning = false;
|
|
337
378
|
private _locked = false;
|
|
338
379
|
private _lastSpeechTime = 0;
|
|
339
|
-
|
|
340
|
-
// Cached DOM refs
|
|
380
|
+
private selectedVoice = 'M1';
|
|
341
381
|
private el!: HTMLElement;
|
|
342
382
|
private curtain!: HTMLElement;
|
|
343
383
|
private curtainTitle!: HTMLElement;
|
|
344
384
|
private curtainDesc!: HTMLElement;
|
|
385
|
+
private curtainBg!: HTMLElement;
|
|
386
|
+
private curtainOverlay!: HTMLElement;
|
|
345
387
|
private startBtn!: HTMLButtonElement;
|
|
346
388
|
private errorEl!: HTMLElement;
|
|
347
389
|
private errorText!: HTMLElement;
|
|
@@ -363,6 +405,7 @@ export class ConversationalPanel {
|
|
|
363
405
|
constructor(cfg: ConversationalPanelConfig) {
|
|
364
406
|
this.cfg = cfg;
|
|
365
407
|
this.container = cfg.container;
|
|
408
|
+
this.selectedVoice = cfg.voice || 'M1';
|
|
366
409
|
injectStyles();
|
|
367
410
|
this.buildDOM();
|
|
368
411
|
}
|
|
@@ -381,9 +424,9 @@ export class ConversationalPanel {
|
|
|
381
424
|
<div class="cv-curtain-bg"></div>
|
|
382
425
|
<div class="cv-curtain-overlay"></div>
|
|
383
426
|
<div class="cv-curtain-content">
|
|
384
|
-
<h3 class="cv-curtain-title">${this.esc(this.cfg.title)}</h3>
|
|
385
|
-
<p class="cv-curtain-desc">${this.esc(this.cfg.description)}</p>
|
|
386
|
-
|
|
427
|
+
<h3 class="cv-curtain-title">${this.esc(this.cfg.title || "Voice Chat")}</h3>
|
|
428
|
+
<p class="cv-curtain-desc">${this.esc(this.cfg.description || "")}</p>
|
|
429
|
+
<button class="cv-curtain-btn">
|
|
387
430
|
<span>Start Conversation</span>
|
|
388
431
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="20" height="20">
|
|
389
432
|
<path d="M5 12h14M12 5l7 7-7 7"/>
|
|
@@ -401,7 +444,7 @@ export class ConversationalPanel {
|
|
|
401
444
|
</div>
|
|
402
445
|
</div>
|
|
403
446
|
<div class="cv-header">
|
|
404
|
-
<h2 class="cv-title">${this.esc(this.cfg.title)} <span class="cv-timer">00:00</span></h2>
|
|
447
|
+
<h2 class="cv-title">${this.esc(this.cfg.title || "Voice Chat")} <span class="cv-timer">00:00</span></h2>
|
|
405
448
|
</div>
|
|
406
449
|
<div class="cv-visualizer-wrap">
|
|
407
450
|
<canvas class="cv-canvas"></canvas>
|
|
@@ -493,7 +536,7 @@ export class ConversationalPanel {
|
|
|
493
536
|
apiKey: this.cfg.apiKey,
|
|
494
537
|
tools: this.cfg.tools,
|
|
495
538
|
prompt: this.cfg.prompt,
|
|
496
|
-
voice: this.
|
|
539
|
+
voice: this.selectedVoice,
|
|
497
540
|
language: this.cfg.language || 'en',
|
|
498
541
|
onStatusChange: (status: string) => {
|
|
499
542
|
this.el.classList.remove('cv-is-speaking', 'cv-is-thinking');
|
|
@@ -633,6 +676,7 @@ export class ConversationalPanel {
|
|
|
633
676
|
if (h2) h2.innerHTML = `${this.esc(title)} <span class="cv-timer">${this.timerEl?.textContent || '00:00'}</span>`;
|
|
634
677
|
}
|
|
635
678
|
|
|
679
|
+
/** Select a voice from the picker */
|
|
636
680
|
/** Update description text */
|
|
637
681
|
setDescription(desc: string) {
|
|
638
682
|
this.cfg.description = desc;
|
package/src/index.ts
CHANGED
|
@@ -2,8 +2,18 @@ export * from './types';
|
|
|
2
2
|
export * from './client';
|
|
3
3
|
export * from './audio-utils';
|
|
4
4
|
export * from './browser-audio';
|
|
5
|
-
export
|
|
5
|
+
export * from './node-audio';
|
|
6
|
+
export {
|
|
7
|
+
VoiceAgentClient,
|
|
8
|
+
TTSClient,
|
|
9
|
+
STTClient,
|
|
10
|
+
SpeechToTextClient,
|
|
11
|
+
simpleConversation,
|
|
12
|
+
simpleTTS,
|
|
13
|
+
simpleTranscribe,
|
|
14
|
+
} from './client';
|
|
6
15
|
export { BrowserAudioManager } from './browser-audio';
|
|
16
|
+
export { NodeAudioManager } from './node-audio';
|
|
7
17
|
export { ConversationalPanel } from './conversational-panel';
|
|
8
18
|
export type { ConversationalPanelConfig } from './conversational-panel';
|
|
9
19
|
export {
|
|
@@ -24,4 +34,8 @@ export type {
|
|
|
24
34
|
ServerConfig,
|
|
25
35
|
ServerStatus,
|
|
26
36
|
HealthStatus,
|
|
37
|
+
TranscribeOptions,
|
|
38
|
+
TranscribeResult,
|
|
39
|
+
TranscribeSegment,
|
|
40
|
+
SpeechToTextOptions,
|
|
27
41
|
} from './types';
|
package/src/node-audio.ts
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
import { AudioManager } from './client';
|
|
4
4
|
import { AUDIO_CONFIG } from './types';
|
|
5
5
|
|
|
6
|
+
// 'speaker' and 'node-record-lpcm16' are optional peer dependencies — Node.js
|
|
7
|
+
// users who want managed microphone/speaker I/O install them themselves (see
|
|
8
|
+
// the warnings below); they are not bundled or required by this package, and
|
|
9
|
+
// aren't installed while building the SDK itself. Importing by a non-literal
|
|
10
|
+
// string (rather than `import('speaker')` directly) keeps TypeScript from
|
|
11
|
+
// trying to resolve either module's types at the SDK's own build time —
|
|
12
|
+
// this always resolves to `any`, and at runtime falls through to the
|
|
13
|
+
// .catch(() => null) handling below if the consumer hasn't installed it.
|
|
14
|
+
function optionalImport(moduleName: string): Promise<any> {
|
|
15
|
+
return import(moduleName);
|
|
16
|
+
}
|
|
17
|
+
|
|
6
18
|
/**
|
|
7
19
|
* Node.js-specific AudioManager implementation.
|
|
8
20
|
* Note: These require 'speaker' and 'node-record-lpcm16' to be installed by the user.
|
|
@@ -20,7 +32,7 @@ export class NodeAudioManager implements AudioManager {
|
|
|
20
32
|
try {
|
|
21
33
|
// Dynamic imports to avoid crashing if dependencies are missing at build time
|
|
22
34
|
// The user must install these manually for managed Node.js audio
|
|
23
|
-
const Speaker = await
|
|
35
|
+
const Speaker = await optionalImport('speaker').catch(() => null);
|
|
24
36
|
if (!Speaker) {
|
|
25
37
|
console.warn('⚠️ Package "speaker" is missing. Hardware output will be disabled.');
|
|
26
38
|
console.warn('👉 Run: npm install speaker');
|
|
@@ -34,7 +46,7 @@ export class NodeAudioManager implements AudioManager {
|
|
|
34
46
|
if (this.isListening) return;
|
|
35
47
|
|
|
36
48
|
try {
|
|
37
|
-
const recorder = await
|
|
49
|
+
const recorder = await optionalImport('node-record-lpcm16').catch(() => null);
|
|
38
50
|
if (!recorder) {
|
|
39
51
|
throw new Error('Package "node-record-lpcm16" is missing. Microphone input failed.\n👉 Run: npm install node-record-lpcm16');
|
|
40
52
|
}
|
|
@@ -72,7 +84,7 @@ export class NodeAudioManager implements AudioManager {
|
|
|
72
84
|
async playAudio(pcm16Data: Uint8Array): Promise<void> {
|
|
73
85
|
try {
|
|
74
86
|
if (!this.speaker) {
|
|
75
|
-
const Speaker = (await
|
|
87
|
+
const Speaker = (await optionalImport('speaker')).default;
|
|
76
88
|
this.speaker = new Speaker({
|
|
77
89
|
channels: AUDIO_CONFIG.CHANNELS,
|
|
78
90
|
bitDepth: 16,
|
package/src/types.ts
CHANGED
|
@@ -76,6 +76,7 @@ export const AUDIO_CONFIG = {
|
|
|
76
76
|
export const DEFAULT_URLS = {
|
|
77
77
|
VOICE_AGENT: "wss://api.lokutor.com/ws/agent",
|
|
78
78
|
TTS: "wss://api.lokutor.com/ws/tts",
|
|
79
|
+
STT: "wss://api.lokutor.com/ws/stt",
|
|
79
80
|
};
|
|
80
81
|
|
|
81
82
|
/**
|
|
@@ -83,6 +84,7 @@ export const DEFAULT_URLS = {
|
|
|
83
84
|
*/
|
|
84
85
|
export interface LokutorConfig {
|
|
85
86
|
apiKey: string;
|
|
87
|
+
agentId?: string;
|
|
86
88
|
onTranscription?: (text: string) => void;
|
|
87
89
|
onResponse?: (text: string) => void;
|
|
88
90
|
onAudio?: (data: Uint8Array) => void;
|
|
@@ -102,6 +104,63 @@ export interface SynthesizeOptions {
|
|
|
102
104
|
visemes?: boolean;
|
|
103
105
|
}
|
|
104
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Options for one-shot batch transcription via STTClient.transcribe()
|
|
109
|
+
* (POST /stt/transcribe) — pass a complete recording and get a transcript
|
|
110
|
+
* back. For continuous/live transcription, use SpeechToTextClient instead.
|
|
111
|
+
*/
|
|
112
|
+
export interface TranscribeOptions {
|
|
113
|
+
/** Complete audio to transcribe. A Blob/File (e.g. from a file input or
|
|
114
|
+
* MediaRecorder) is sent as multipart/form-data; a raw PCM16 buffer is
|
|
115
|
+
* sent as base64 JSON with `format: "pcm16"`. */
|
|
116
|
+
audio: Blob | ArrayBuffer | Uint8Array;
|
|
117
|
+
/** Required when `audio` is raw PCM16 (ignored for WAV/Blob input, whose
|
|
118
|
+
* rate is read from the file header). */
|
|
119
|
+
sampleRate?: number;
|
|
120
|
+
/** Set when passing a raw PCM16 buffer instead of a WAV Blob. */
|
|
121
|
+
format?: 'wav' | 'pcm16';
|
|
122
|
+
language?: Language;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** One transcribed segment with timing, when the engine provides them. */
|
|
126
|
+
export interface TranscribeSegment {
|
|
127
|
+
text: string;
|
|
128
|
+
start: number;
|
|
129
|
+
end: number;
|
|
130
|
+
duration: number;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Result of STTClient.transcribe() — mirrors POST /stt/transcribe's response body. */
|
|
134
|
+
export interface TranscribeResult {
|
|
135
|
+
text: string;
|
|
136
|
+
latencyMs: number;
|
|
137
|
+
engine: string;
|
|
138
|
+
sampleRate: number;
|
|
139
|
+
language: string;
|
|
140
|
+
durationSeconds: number;
|
|
141
|
+
segments?: TranscribeSegment[];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Continuous speech-to-text options (WS /ws/stt) — standalone transcription
|
|
146
|
+
* with server-side VAD, independent of the full voice-agent pipeline (no
|
|
147
|
+
* LLM turn, no TTS). Use this for live captioning/dictation; use
|
|
148
|
+
* STTClient.transcribe() for a single pre-recorded clip.
|
|
149
|
+
*/
|
|
150
|
+
export interface SpeechToTextOptions {
|
|
151
|
+
apiKey: string;
|
|
152
|
+
serverUrl?: string;
|
|
153
|
+
language?: Language;
|
|
154
|
+
/** VAD engine: "silero" (default, neural) or "rms" (energy-threshold fallback). */
|
|
155
|
+
vad?: 'silero' | 'rms';
|
|
156
|
+
/** Fires repeatedly while the user is mid-utterance, with the best partial guess so far. */
|
|
157
|
+
onPartialTranscript?: (text: string) => void;
|
|
158
|
+
/** Fires once VAD detects the utterance ended. */
|
|
159
|
+
onFinalTranscript?: (text: string) => void;
|
|
160
|
+
onError?: (error: LokutorError) => void;
|
|
161
|
+
onStatusChange?: (status: 'connecting' | 'connected' | 'disconnected') => void;
|
|
162
|
+
}
|
|
163
|
+
|
|
105
164
|
/**
|
|
106
165
|
* Browser audio configuration options
|
|
107
166
|
*/
|