@lokutor/sdk 1.1.17 → 1.1.19
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 +119 -4
- package/dist/index.d.ts +119 -4
- package/dist/index.js +387 -219
- package/dist/index.mjs +385 -63
- package/package.json +13 -4
- package/src/audio-utils.ts +253 -0
- package/src/browser-audio.ts +395 -0
- package/src/client.ts +886 -0
- package/src/index.ts +25 -0
- package/src/node-audio.ts +115 -0
- package/src/types.ts +270 -0
- package/dist/chunk-UI24THO7.mjs +0 -44
- package/dist/node-audio-5HOWE6MC.mjs +0 -94
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export * from './types';
|
|
2
|
+
export * from './client';
|
|
3
|
+
export * from './audio-utils';
|
|
4
|
+
export * from './browser-audio';
|
|
5
|
+
export { VoiceAgentClient, TTSClient, simpleConversation, simpleTTS } from './client';
|
|
6
|
+
export { BrowserAudioManager } from './browser-audio';
|
|
7
|
+
export {
|
|
8
|
+
pcm16ToFloat32,
|
|
9
|
+
float32ToPcm16,
|
|
10
|
+
resample,
|
|
11
|
+
resampleWithAntiAliasing,
|
|
12
|
+
calculateRMS,
|
|
13
|
+
normalizeAudio,
|
|
14
|
+
StreamResampler,
|
|
15
|
+
} from './audio-utils';
|
|
16
|
+
export type {
|
|
17
|
+
VoiceAgentOptions,
|
|
18
|
+
Viseme,
|
|
19
|
+
VoiceInfo,
|
|
20
|
+
LanguageInfo,
|
|
21
|
+
ModelInfo,
|
|
22
|
+
ServerConfig,
|
|
23
|
+
ServerStatus,
|
|
24
|
+
HealthStatus,
|
|
25
|
+
} from './types';
|
|
@@ -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
|
+
}
|
package/dist/chunk-UI24THO7.mjs
DELETED
|
@@ -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
|
-
};
|