@lokutor/sdk 1.1.17 → 1.1.20

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.
@@ -0,0 +1,115 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+
3
+ import { AudioManager } from './client';
4
+ import { AUDIO_CONFIG } from './types';
5
+
6
+ /**
7
+ * Node.js-specific AudioManager implementation.
8
+ * Note: These require 'speaker' and 'node-record-lpcm16' to be installed by the user.
9
+ */
10
+ export class NodeAudioManager implements AudioManager {
11
+ private speaker: any = null;
12
+ private recorder: any = null;
13
+ private recordingStream: any = null;
14
+ private isMuted: boolean = false;
15
+ private isListening: boolean = false;
16
+
17
+ constructor() {}
18
+
19
+ async init(): Promise<void> {
20
+ try {
21
+ // Dynamic imports to avoid crashing if dependencies are missing at build time
22
+ // The user must install these manually for managed Node.js audio
23
+ const Speaker = await import('speaker').catch(() => null);
24
+ if (!Speaker) {
25
+ console.warn('⚠️ Package "speaker" is missing. Hardware output will be disabled.');
26
+ console.warn('👉 Run: npm install speaker');
27
+ }
28
+ } catch (e) {
29
+ console.error('Error initializing Node audio:', e);
30
+ }
31
+ }
32
+
33
+ async startMicrophone(onAudioInput: (pcm16Data: Uint8Array) => void): Promise<void> {
34
+ if (this.isListening) return;
35
+
36
+ try {
37
+ const recorder = await import('node-record-lpcm16').catch(() => null);
38
+ if (!recorder) {
39
+ throw new Error('Package "node-record-lpcm16" is missing. Microphone input failed.\n👉 Run: npm install node-record-lpcm16');
40
+ }
41
+
42
+ console.log('🎤 Starting microphone (Node.js)...');
43
+
44
+ this.recordingStream = recorder.record({
45
+ sampleRate: AUDIO_CONFIG.SAMPLE_RATE,
46
+ threshold: 0,
47
+ verbose: false,
48
+ recordProgram: 'sox', // default
49
+ });
50
+
51
+ this.recordingStream.stream().on('data', (chunk: Buffer) => {
52
+ if (!this.isMuted && onAudioInput) {
53
+ onAudioInput(new Uint8Array(chunk));
54
+ }
55
+ });
56
+
57
+ this.isListening = true;
58
+ } catch (e: any) {
59
+ console.error('Failed to start microphone:', e.message);
60
+ throw e;
61
+ }
62
+ }
63
+
64
+ stopMicrophone(): void {
65
+ if (this.recordingStream) {
66
+ this.recordingStream.stop();
67
+ this.recordingStream = null;
68
+ }
69
+ this.isListening = false;
70
+ }
71
+
72
+ async playAudio(pcm16Data: Uint8Array): Promise<void> {
73
+ try {
74
+ if (!this.speaker) {
75
+ const Speaker = (await import('speaker')).default;
76
+ this.speaker = new Speaker({
77
+ channels: AUDIO_CONFIG.CHANNELS,
78
+ bitDepth: 16,
79
+ sampleRate: AUDIO_CONFIG.SPEAKER_SAMPLE_RATE,
80
+ });
81
+ }
82
+
83
+ // Node.js 'speaker' accepts Buffers
84
+ this.speaker.write(Buffer.from(pcm16Data));
85
+ } catch (e) {
86
+ console.error('NodeAudioManager: speaker playback failed:', e);
87
+ }
88
+ }
89
+
90
+ stopPlayback(): void {
91
+ if (this.speaker) {
92
+ this.speaker.end();
93
+ this.speaker = null;
94
+ }
95
+ }
96
+
97
+ cleanup(): void {
98
+ this.stopMicrophone();
99
+ this.stopPlayback();
100
+ }
101
+
102
+ isMicMuted(): boolean {
103
+ return this.isMuted;
104
+ }
105
+
106
+ setMuted(muted: boolean): void {
107
+ this.isMuted = muted;
108
+ }
109
+
110
+ getAmplitude(): number {
111
+ // Amplitude tracking implementation for Node.js would require extra processing
112
+ // leaving as stub for now
113
+ return 0;
114
+ }
115
+ }
package/src/types.ts ADDED
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Available voice styles for the Lokutor AI Agent
3
+ */
4
+ export enum VoiceStyle {
5
+ // Female voices
6
+ F1 = "F1",
7
+ F2 = "F2",
8
+ F3 = "F3",
9
+ F4 = "F4",
10
+ F5 = "F5",
11
+
12
+ // Male voices
13
+ M1 = "M1",
14
+ M2 = "M2",
15
+ M3 = "M3",
16
+ M4 = "M4",
17
+ M5 = "M5",
18
+ }
19
+
20
+ /**
21
+ * Supported languages for speech and text
22
+ */
23
+ export enum Language {
24
+ ENGLISH = "en",
25
+ SPANISH = "es",
26
+ FRENCH = "fr",
27
+ PORTUGUESE = "pt",
28
+ KOREAN = "ko",
29
+ }
30
+
31
+ /**
32
+ * Audio configuration constants
33
+ */
34
+ export const AUDIO_CONFIG = {
35
+ SAMPLE_RATE: 16000,
36
+ SAMPLE_RATE_INPUT: 16000,
37
+ SPEAKER_SAMPLE_RATE: 44100,
38
+ SAMPLE_RATE_OUTPUT: 44100,
39
+ CHANNELS: 1,
40
+ CHUNK_DURATION_MS: 20,
41
+ get CHUNK_SIZE() {
42
+ return Math.floor((this.SAMPLE_RATE * this.CHUNK_DURATION_MS) / 1000);
43
+ }
44
+ };
45
+
46
+ /**
47
+ * Default WebSocket URLs
48
+ */
49
+ export const DEFAULT_URLS = {
50
+ VOICE_AGENT: "wss://api.lokutor.com/ws/agent",
51
+ TTS: "wss://api.lokutor.com/ws/tts",
52
+ };
53
+
54
+ /**
55
+ * SDK Configuration interface
56
+ */
57
+ export interface LokutorConfig {
58
+ apiKey: string;
59
+ onTranscription?: (text: string) => void;
60
+ onResponse?: (text: string) => void;
61
+ onAudio?: (data: Uint8Array) => void;
62
+ onStatus?: (status: string) => void;
63
+ onError?: (error: any) => void;
64
+ }
65
+
66
+ /**
67
+ * Text-to-Speech synthesis request options
68
+ */
69
+ export interface SynthesizeOptions {
70
+ text: string;
71
+ voice?: VoiceStyle;
72
+ language?: Language;
73
+ speed?: number;
74
+ steps?: number;
75
+ visemes?: boolean;
76
+ }
77
+
78
+ /**
79
+ * Browser audio configuration options
80
+ */
81
+ export interface BrowserAudioOptions {
82
+ inputSampleRate?: number;
83
+ outputSampleRate?: number;
84
+ autoGainControl?: boolean;
85
+ echoCancellation?: boolean;
86
+ noiseSuppression?: boolean;
87
+ analyserEnabled?: boolean;
88
+ onInputError?: (error: Error) => void;
89
+ }
90
+
91
+ /**
92
+ * Voice agent conversation options
93
+ */
94
+ export interface VoiceAgentOptions {
95
+ prompt?: string;
96
+ voice?: VoiceStyle;
97
+ language?: Language;
98
+ serverUrl?: string;
99
+ visemes?: boolean;
100
+ onTranscription?: (text: string) => void;
101
+ onVisemes?: (visemes: Viseme[]) => void;
102
+ onStatusChange?: (status: string) => void;
103
+ onError?: (err: LokutorError) => void;
104
+ }
105
+
106
+ /**
107
+ * REST API response types for discovery endpoints.
108
+ */
109
+
110
+ export interface VoiceInfo {
111
+ id: string;
112
+ gender?: string;
113
+ languages?: string[];
114
+ }
115
+
116
+ export interface LanguageInfo {
117
+ code: string;
118
+ name: string;
119
+ }
120
+
121
+ export interface ModelInfo {
122
+ name: string;
123
+ description?: string;
124
+ default?: boolean;
125
+ }
126
+
127
+ export interface ServerConfig {
128
+ max_text_length: number;
129
+ min_speed: number;
130
+ max_speed: number;
131
+ min_steps: number;
132
+ max_steps: number;
133
+ sample_rate: number;
134
+ channels: number;
135
+ }
136
+
137
+ export interface ServerStatus {
138
+ status: string;
139
+ timestamp: string;
140
+ version: string;
141
+ runtime: string;
142
+ inference: string;
143
+ uptime_seconds: number;
144
+ goroutines: number;
145
+ mem_alloc_bytes: number;
146
+ active_connections: number;
147
+ failed_requests: number;
148
+ ready: boolean;
149
+ }
150
+
151
+ export interface HealthStatus {
152
+ status: string;
153
+ timestamp: string;
154
+ version: string;
155
+ runtime: string;
156
+ inference: string;
157
+ load: number;
158
+ }
159
+
160
+ /**
161
+ * Viseme data for lip-sync animation
162
+ * Format: {"v": index, "c": character, "t": timestamp}
163
+ */
164
+ export interface Viseme {
165
+ /** Text-position index (which input character the model is attending to), not a stable viseme ID */
166
+ v: number;
167
+ /** Character/phoneme being spoken (reduced set: a,e,i,o,u,m,p,b,f,v,t,d,s,z,l,n,r,k,g,sil) */
168
+ c: string;
169
+ /** Offset in seconds from the start of the audio stream */
170
+ t: number;
171
+ }
172
+
173
+ /**
174
+ * Tool definition for LLM function calling (OpenAI format)
175
+ */
176
+ export interface ToolDefinition {
177
+ type: 'function';
178
+ function: {
179
+ name: string;
180
+ description: string;
181
+ parameters: {
182
+ type: 'object';
183
+ properties: Record<string, any>;
184
+ required?: string[];
185
+ };
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Event data for tool execution
191
+ */
192
+ export interface ToolCall {
193
+ name: string;
194
+ arguments: string;
195
+ }
196
+
197
+ /**
198
+ * Error code enum matching the backend API error catalog
199
+ */
200
+ export type ErrorCode =
201
+ | 'auth.missing_key'
202
+ | 'auth.invalid_key'
203
+ | 'auth.rate_limited'
204
+ | 'auth.time_limited'
205
+ | 'validation.invalid_voice'
206
+ | 'validation.invalid_language'
207
+ | 'validation.text_too_long'
208
+ | 'validation.speed_out_of_range'
209
+ | 'validation.steps_out_of_range'
210
+ | 'validation.invalid_request_format'
211
+ | 'tts.synthesis_failed'
212
+ | 'tts.voice_unavailable'
213
+ | 'tts.model_not_found'
214
+ | 'tts.session_limit_reached'
215
+ | 'stt.not_configured'
216
+ | 'stt.stream_create_failed'
217
+ | 'stt.language_not_supported'
218
+ | 'agent.session_failed'
219
+ | 'agent.provider_error'
220
+ | 'internal.error'
221
+ | 'internal.timeout'
222
+ | 'internal.cancelled'
223
+ | 'ws.close';
224
+
225
+ /**
226
+ * Typed error class for all Lokutor SDK errors.
227
+ * Includes the backend error code, human-readable message,
228
+ * optional detail, and whether the operation is retryable.
229
+ */
230
+ export class LokutorError extends Error {
231
+ public readonly code: ErrorCode;
232
+ public readonly detail?: string;
233
+ public readonly retryable: boolean;
234
+ public readonly original?: unknown;
235
+
236
+ constructor(code: ErrorCode, message: string, opts?: { detail?: string; retryable?: boolean; original?: unknown }) {
237
+ super(message);
238
+ this.name = 'LokutorError';
239
+ this.code = code;
240
+ this.detail = opts?.detail;
241
+ this.retryable = opts?.retryable ?? isRetryableCode(code);
242
+ this.original = opts?.original;
243
+ }
244
+ }
245
+
246
+ function isRetryableCode(code: ErrorCode): boolean {
247
+ const fatal: ErrorCode[] = [
248
+ 'auth.missing_key',
249
+ 'auth.invalid_key',
250
+ 'auth.time_limited',
251
+ 'validation.invalid_voice',
252
+ 'validation.invalid_language',
253
+ 'validation.text_too_long',
254
+ 'validation.speed_out_of_range',
255
+ 'validation.steps_out_of_range',
256
+ 'validation.invalid_request_format',
257
+ 'internal.cancelled',
258
+ ];
259
+ return !fatal.includes(code);
260
+ }
261
+
262
+ /**
263
+ * Returns true if the given error is a retryable LokutorError.
264
+ */
265
+ export function isRetryable(error: unknown): boolean {
266
+ if (error instanceof LokutorError) {
267
+ return error.retryable;
268
+ }
269
+ return false;
270
+ }
@@ -1,44 +0,0 @@
1
- // src/types.ts
2
- var VoiceStyle = /* @__PURE__ */ ((VoiceStyle2) => {
3
- VoiceStyle2["F1"] = "F1";
4
- VoiceStyle2["F2"] = "F2";
5
- VoiceStyle2["F3"] = "F3";
6
- VoiceStyle2["F4"] = "F4";
7
- VoiceStyle2["F5"] = "F5";
8
- VoiceStyle2["M1"] = "M1";
9
- VoiceStyle2["M2"] = "M2";
10
- VoiceStyle2["M3"] = "M3";
11
- VoiceStyle2["M4"] = "M4";
12
- VoiceStyle2["M5"] = "M5";
13
- return VoiceStyle2;
14
- })(VoiceStyle || {});
15
- var Language = /* @__PURE__ */ ((Language2) => {
16
- Language2["ENGLISH"] = "en";
17
- Language2["SPANISH"] = "es";
18
- Language2["FRENCH"] = "fr";
19
- Language2["PORTUGUESE"] = "pt";
20
- Language2["KOREAN"] = "ko";
21
- return Language2;
22
- })(Language || {});
23
- var AUDIO_CONFIG = {
24
- SAMPLE_RATE: 16e3,
25
- SAMPLE_RATE_INPUT: 16e3,
26
- SPEAKER_SAMPLE_RATE: 44100,
27
- SAMPLE_RATE_OUTPUT: 44100,
28
- CHANNELS: 1,
29
- CHUNK_DURATION_MS: 20,
30
- get CHUNK_SIZE() {
31
- return Math.floor(this.SAMPLE_RATE * this.CHUNK_DURATION_MS / 1e3);
32
- }
33
- };
34
- var DEFAULT_URLS = {
35
- VOICE_AGENT: "wss://api.lokutor.com/ws/agent",
36
- TTS: "wss://api.lokutor.com/ws/tts"
37
- };
38
-
39
- export {
40
- VoiceStyle,
41
- Language,
42
- AUDIO_CONFIG,
43
- DEFAULT_URLS
44
- };
@@ -1,94 +0,0 @@
1
- import {
2
- AUDIO_CONFIG
3
- } from "./chunk-UI24THO7.mjs";
4
-
5
- // src/node-audio.ts
6
- var NodeAudioManager = class {
7
- speaker = null;
8
- recorder = null;
9
- recordingStream = null;
10
- isMuted = false;
11
- isListening = false;
12
- constructor() {
13
- }
14
- async init() {
15
- try {
16
- const Speaker = await import("speaker").catch(() => null);
17
- if (!Speaker) {
18
- console.warn('\u26A0\uFE0F Package "speaker" is missing. Hardware output will be disabled.');
19
- console.warn("\u{1F449} Run: npm install speaker");
20
- }
21
- } catch (e) {
22
- console.error("Error initializing Node audio:", e);
23
- }
24
- }
25
- async startMicrophone(onAudioInput) {
26
- if (this.isListening) return;
27
- try {
28
- const recorder = await import("node-record-lpcm16").catch(() => null);
29
- if (!recorder) {
30
- throw new Error('Package "node-record-lpcm16" is missing. Microphone input failed.\n\u{1F449} Run: npm install node-record-lpcm16');
31
- }
32
- console.log("\u{1F3A4} Starting microphone (Node.js)...");
33
- this.recordingStream = recorder.record({
34
- sampleRate: AUDIO_CONFIG.SAMPLE_RATE,
35
- threshold: 0,
36
- verbose: false,
37
- recordProgram: "sox"
38
- // default
39
- });
40
- this.recordingStream.stream().on("data", (chunk) => {
41
- if (!this.isMuted && onAudioInput) {
42
- onAudioInput(new Uint8Array(chunk));
43
- }
44
- });
45
- this.isListening = true;
46
- } catch (e) {
47
- console.error("Failed to start microphone:", e.message);
48
- throw e;
49
- }
50
- }
51
- stopMicrophone() {
52
- if (this.recordingStream) {
53
- this.recordingStream.stop();
54
- this.recordingStream = null;
55
- }
56
- this.isListening = false;
57
- }
58
- async playAudio(pcm16Data) {
59
- try {
60
- if (!this.speaker) {
61
- const Speaker = (await import("speaker")).default;
62
- this.speaker = new Speaker({
63
- channels: AUDIO_CONFIG.CHANNELS,
64
- bitDepth: 16,
65
- sampleRate: AUDIO_CONFIG.SPEAKER_SAMPLE_RATE
66
- });
67
- }
68
- this.speaker.write(Buffer.from(pcm16Data));
69
- } catch (e) {
70
- }
71
- }
72
- stopPlayback() {
73
- if (this.speaker) {
74
- this.speaker.end();
75
- this.speaker = null;
76
- }
77
- }
78
- cleanup() {
79
- this.stopMicrophone();
80
- this.stopPlayback();
81
- }
82
- isMicMuted() {
83
- return this.isMuted;
84
- }
85
- setMuted(muted) {
86
- this.isMuted = muted;
87
- }
88
- getAmplitude() {
89
- return 0;
90
- }
91
- };
92
- export {
93
- NodeAudioManager
94
- };