kugelaudio 0.1.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/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +499 -0
- package/dist/index.d.mts +319 -0
- package/dist/index.d.ts +319 -0
- package/dist/index.js +421 -0
- package/dist/index.mjs +384 -0
- package/package.json +56 -0
- package/src/client.ts +370 -0
- package/src/errors.ts +73 -0
- package/src/index.ts +79 -0
- package/src/types.ts +184 -0
- package/src/utils.ts +120 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var KugelAudioError = class _KugelAudioError extends Error {
|
|
3
|
+
constructor(message, statusCode) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "KugelAudioError";
|
|
6
|
+
this.statusCode = statusCode;
|
|
7
|
+
Object.setPrototypeOf(this, _KugelAudioError.prototype);
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var AuthenticationError = class _AuthenticationError extends KugelAudioError {
|
|
11
|
+
constructor(message = "Authentication failed") {
|
|
12
|
+
super(message, 401);
|
|
13
|
+
this.name = "AuthenticationError";
|
|
14
|
+
Object.setPrototypeOf(this, _AuthenticationError.prototype);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var RateLimitError = class _RateLimitError extends KugelAudioError {
|
|
18
|
+
constructor(message = "Rate limit exceeded") {
|
|
19
|
+
super(message, 429);
|
|
20
|
+
this.name = "RateLimitError";
|
|
21
|
+
Object.setPrototypeOf(this, _RateLimitError.prototype);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var InsufficientCreditsError = class _InsufficientCreditsError extends KugelAudioError {
|
|
25
|
+
constructor(message = "Insufficient credits") {
|
|
26
|
+
super(message, 403);
|
|
27
|
+
this.name = "InsufficientCreditsError";
|
|
28
|
+
Object.setPrototypeOf(this, _InsufficientCreditsError.prototype);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var ValidationError = class _ValidationError extends KugelAudioError {
|
|
32
|
+
constructor(message) {
|
|
33
|
+
super(message, 400);
|
|
34
|
+
this.name = "ValidationError";
|
|
35
|
+
Object.setPrototypeOf(this, _ValidationError.prototype);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
var ConnectionError = class _ConnectionError extends KugelAudioError {
|
|
39
|
+
constructor(message = "Failed to connect to server") {
|
|
40
|
+
super(message, 503);
|
|
41
|
+
this.name = "ConnectionError";
|
|
42
|
+
Object.setPrototypeOf(this, _ConnectionError.prototype);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/utils.ts
|
|
47
|
+
function base64ToArrayBuffer(base64) {
|
|
48
|
+
if (typeof atob === "function") {
|
|
49
|
+
const binary = atob(base64);
|
|
50
|
+
const bytes = new Uint8Array(binary.length);
|
|
51
|
+
for (let i = 0; i < binary.length; i++) {
|
|
52
|
+
bytes[i] = binary.charCodeAt(i);
|
|
53
|
+
}
|
|
54
|
+
return bytes.buffer;
|
|
55
|
+
} else {
|
|
56
|
+
const buffer = Buffer.from(base64, "base64");
|
|
57
|
+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function decodePCM16(base64) {
|
|
61
|
+
const buffer = base64ToArrayBuffer(base64);
|
|
62
|
+
const int16 = new Int16Array(buffer);
|
|
63
|
+
const float32 = new Float32Array(int16.length);
|
|
64
|
+
for (let i = 0; i < int16.length; i++) {
|
|
65
|
+
float32[i] = int16[i] / 32768;
|
|
66
|
+
}
|
|
67
|
+
return float32;
|
|
68
|
+
}
|
|
69
|
+
function createWavFile(audio, sampleRate) {
|
|
70
|
+
const dataSize = audio.byteLength;
|
|
71
|
+
const fileSize = 44 + dataSize;
|
|
72
|
+
const buffer = new ArrayBuffer(fileSize);
|
|
73
|
+
const view = new DataView(buffer);
|
|
74
|
+
writeString(view, 0, "RIFF");
|
|
75
|
+
view.setUint32(4, fileSize - 8, true);
|
|
76
|
+
writeString(view, 8, "WAVE");
|
|
77
|
+
writeString(view, 12, "fmt ");
|
|
78
|
+
view.setUint32(16, 16, true);
|
|
79
|
+
view.setUint16(20, 1, true);
|
|
80
|
+
view.setUint16(22, 1, true);
|
|
81
|
+
view.setUint32(24, sampleRate, true);
|
|
82
|
+
view.setUint32(28, sampleRate * 2, true);
|
|
83
|
+
view.setUint16(32, 2, true);
|
|
84
|
+
view.setUint16(34, 16, true);
|
|
85
|
+
writeString(view, 36, "data");
|
|
86
|
+
view.setUint32(40, dataSize, true);
|
|
87
|
+
const audioBytes = new Uint8Array(audio);
|
|
88
|
+
const wavBytes = new Uint8Array(buffer);
|
|
89
|
+
wavBytes.set(audioBytes, 44);
|
|
90
|
+
return buffer;
|
|
91
|
+
}
|
|
92
|
+
function writeString(view, offset, str) {
|
|
93
|
+
for (let i = 0; i < str.length; i++) {
|
|
94
|
+
view.setUint8(offset + i, str.charCodeAt(i));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function createWavBlob(audio, sampleRate) {
|
|
98
|
+
const wavBuffer = createWavFile(audio, sampleRate);
|
|
99
|
+
return new Blob([wavBuffer], { type: "audio/wav" });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/client.ts
|
|
103
|
+
var DEFAULT_API_URL = "https://api.kugelaudio.com";
|
|
104
|
+
var ModelsResource = class {
|
|
105
|
+
constructor(client) {
|
|
106
|
+
this.client = client;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* List available TTS models.
|
|
110
|
+
*/
|
|
111
|
+
async list() {
|
|
112
|
+
const response = await this.client.request("GET", "/v1/models");
|
|
113
|
+
return response.models.map((m) => ({
|
|
114
|
+
id: m.id,
|
|
115
|
+
name: m.name,
|
|
116
|
+
description: m.description || "",
|
|
117
|
+
parameters: m.parameters || "",
|
|
118
|
+
maxInputLength: m.max_input_length || 5e3,
|
|
119
|
+
sampleRate: m.sample_rate || 24e3
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
var VoicesResource = class {
|
|
124
|
+
constructor(client) {
|
|
125
|
+
this.client = client;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* List available voices.
|
|
129
|
+
*/
|
|
130
|
+
async list(options) {
|
|
131
|
+
const params = new URLSearchParams();
|
|
132
|
+
if (options?.language) params.set("language", options.language);
|
|
133
|
+
if (options?.includePublic !== void 0) {
|
|
134
|
+
params.set("include_public", String(options.includePublic));
|
|
135
|
+
}
|
|
136
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
137
|
+
const query = params.toString();
|
|
138
|
+
const path = query ? `/v1/voices?${query}` : "/v1/voices";
|
|
139
|
+
const response = await this.client.request("GET", path);
|
|
140
|
+
return response.voices.map((v) => ({
|
|
141
|
+
id: v.id,
|
|
142
|
+
name: v.name,
|
|
143
|
+
description: v.description,
|
|
144
|
+
category: v.category,
|
|
145
|
+
sex: v.sex,
|
|
146
|
+
age: v.age,
|
|
147
|
+
supportedLanguages: v.supported_languages || [],
|
|
148
|
+
sampleText: v.sample_text,
|
|
149
|
+
avatarUrl: v.avatar_url,
|
|
150
|
+
sampleUrl: v.sample_url,
|
|
151
|
+
isPublic: v.is_public || false,
|
|
152
|
+
verified: v.verified || false
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get a specific voice by ID.
|
|
157
|
+
*/
|
|
158
|
+
async get(voiceId) {
|
|
159
|
+
const v = await this.client.request("GET", `/v1/voices/${voiceId}`);
|
|
160
|
+
return {
|
|
161
|
+
id: v.id,
|
|
162
|
+
name: v.name,
|
|
163
|
+
description: v.description,
|
|
164
|
+
category: v.category,
|
|
165
|
+
sex: v.sex,
|
|
166
|
+
age: v.age,
|
|
167
|
+
supportedLanguages: v.supported_languages || [],
|
|
168
|
+
sampleText: v.sample_text,
|
|
169
|
+
avatarUrl: v.avatar_url,
|
|
170
|
+
sampleUrl: v.sample_url,
|
|
171
|
+
isPublic: v.is_public || false,
|
|
172
|
+
verified: v.verified || false
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
var TTSResource = class {
|
|
177
|
+
constructor(client) {
|
|
178
|
+
this.client = client;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Generate audio from text with streaming via WebSocket.
|
|
182
|
+
* Returns complete audio after all chunks are received.
|
|
183
|
+
*/
|
|
184
|
+
async generate(options) {
|
|
185
|
+
const chunks = [];
|
|
186
|
+
let finalStats;
|
|
187
|
+
await this.stream(options, {
|
|
188
|
+
onChunk: (chunk) => {
|
|
189
|
+
chunks.push(base64ToArrayBuffer(chunk.audio));
|
|
190
|
+
},
|
|
191
|
+
onFinal: (stats) => {
|
|
192
|
+
finalStats = stats;
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
const totalLength = chunks.reduce((acc, c) => acc + c.byteLength, 0);
|
|
196
|
+
const combined = new Uint8Array(totalLength);
|
|
197
|
+
let offset = 0;
|
|
198
|
+
for (const chunk of chunks) {
|
|
199
|
+
combined.set(new Uint8Array(chunk), offset);
|
|
200
|
+
offset += chunk.byteLength;
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
audio: combined.buffer,
|
|
204
|
+
sampleRate: options.sampleRate || 24e3,
|
|
205
|
+
samples: finalStats ? finalStats.totalSamples : totalLength / 2,
|
|
206
|
+
durationMs: finalStats ? finalStats.durationMs : 0,
|
|
207
|
+
generationMs: finalStats ? finalStats.generationMs : 0,
|
|
208
|
+
rtf: finalStats ? finalStats.rtf : 0
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Stream audio from text via WebSocket.
|
|
213
|
+
*/
|
|
214
|
+
stream(options, callbacks) {
|
|
215
|
+
return new Promise((resolve, reject) => {
|
|
216
|
+
const wsUrl = this.client.ttsUrl.replace("https://", "wss://").replace("http://", "ws://");
|
|
217
|
+
const url = `${wsUrl}/ws/tts?api_key=${this.client.apiKey}`;
|
|
218
|
+
const ws = new WebSocket(url);
|
|
219
|
+
ws.onopen = () => {
|
|
220
|
+
callbacks.onOpen?.();
|
|
221
|
+
ws.send(JSON.stringify({
|
|
222
|
+
text: options.text,
|
|
223
|
+
model: options.model || "kugel-one-turbo",
|
|
224
|
+
voice_id: options.voiceId,
|
|
225
|
+
cfg_scale: options.cfgScale ?? 2,
|
|
226
|
+
max_new_tokens: options.maxNewTokens ?? 2048,
|
|
227
|
+
sample_rate: options.sampleRate ?? 24e3,
|
|
228
|
+
speaker_prefix: options.speakerPrefix ?? true
|
|
229
|
+
}));
|
|
230
|
+
};
|
|
231
|
+
ws.onmessage = (event) => {
|
|
232
|
+
try {
|
|
233
|
+
const data = JSON.parse(event.data);
|
|
234
|
+
if (data.error) {
|
|
235
|
+
const error = this.parseError(data.error);
|
|
236
|
+
callbacks.onError?.(error);
|
|
237
|
+
ws.close();
|
|
238
|
+
reject(error);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (data.final) {
|
|
242
|
+
const stats = {
|
|
243
|
+
final: true,
|
|
244
|
+
chunks: data.chunks,
|
|
245
|
+
totalSamples: data.total_samples,
|
|
246
|
+
durationMs: data.dur_ms,
|
|
247
|
+
generationMs: data.gen_ms,
|
|
248
|
+
ttfaMs: data.ttfa_ms,
|
|
249
|
+
rtf: data.rtf,
|
|
250
|
+
error: data.error
|
|
251
|
+
};
|
|
252
|
+
callbacks.onFinal?.(stats);
|
|
253
|
+
ws.close();
|
|
254
|
+
resolve();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (data.audio) {
|
|
258
|
+
const chunk = {
|
|
259
|
+
audio: data.audio,
|
|
260
|
+
encoding: data.enc || "pcm_s16le",
|
|
261
|
+
index: data.idx,
|
|
262
|
+
sampleRate: data.sr,
|
|
263
|
+
samples: data.samples
|
|
264
|
+
};
|
|
265
|
+
callbacks.onChunk?.(chunk);
|
|
266
|
+
}
|
|
267
|
+
} catch (e) {
|
|
268
|
+
console.error("Failed to parse WebSocket message:", e);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
ws.onerror = () => {
|
|
272
|
+
const error = new KugelAudioError("WebSocket connection error");
|
|
273
|
+
callbacks.onError?.(error);
|
|
274
|
+
reject(error);
|
|
275
|
+
};
|
|
276
|
+
ws.onclose = (event) => {
|
|
277
|
+
callbacks.onClose?.();
|
|
278
|
+
if (event.code === 4001) {
|
|
279
|
+
reject(new AuthenticationError("Authentication failed"));
|
|
280
|
+
} else if (event.code === 4003) {
|
|
281
|
+
reject(new InsufficientCreditsError("Insufficient credits"));
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
parseError(message) {
|
|
287
|
+
const lower = message.toLowerCase();
|
|
288
|
+
if (lower.includes("auth") || lower.includes("unauthorized")) {
|
|
289
|
+
return new AuthenticationError(message);
|
|
290
|
+
}
|
|
291
|
+
if (lower.includes("credit")) {
|
|
292
|
+
return new InsufficientCreditsError(message);
|
|
293
|
+
}
|
|
294
|
+
return new KugelAudioError(message);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
var KugelAudio = class {
|
|
298
|
+
constructor(options) {
|
|
299
|
+
if (!options.apiKey) {
|
|
300
|
+
throw new Error("API key is required");
|
|
301
|
+
}
|
|
302
|
+
this._apiKey = options.apiKey;
|
|
303
|
+
this._apiUrl = (options.apiUrl || DEFAULT_API_URL).replace(/\/$/, "");
|
|
304
|
+
this._ttsUrl = (options.ttsUrl || this._apiUrl).replace(/\/$/, "");
|
|
305
|
+
this._timeout = options.timeout || 6e4;
|
|
306
|
+
this.models = new ModelsResource(this);
|
|
307
|
+
this.voices = new VoicesResource(this);
|
|
308
|
+
this.tts = new TTSResource(this);
|
|
309
|
+
}
|
|
310
|
+
/** Get API key */
|
|
311
|
+
get apiKey() {
|
|
312
|
+
return this._apiKey;
|
|
313
|
+
}
|
|
314
|
+
/** Get TTS URL */
|
|
315
|
+
get ttsUrl() {
|
|
316
|
+
return this._ttsUrl;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Make an HTTP request to the API.
|
|
320
|
+
* @internal
|
|
321
|
+
*/
|
|
322
|
+
async request(method, path, body) {
|
|
323
|
+
const url = `${this._apiUrl}${path}`;
|
|
324
|
+
const headers = {
|
|
325
|
+
"Content-Type": "application/json",
|
|
326
|
+
"X-API-Key": this._apiKey,
|
|
327
|
+
"Authorization": `Bearer ${this._apiKey}`
|
|
328
|
+
};
|
|
329
|
+
const controller = new AbortController();
|
|
330
|
+
const timeoutId = setTimeout(() => controller.abort(), this._timeout);
|
|
331
|
+
try {
|
|
332
|
+
const response = await fetch(url, {
|
|
333
|
+
method,
|
|
334
|
+
headers,
|
|
335
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
336
|
+
signal: controller.signal
|
|
337
|
+
});
|
|
338
|
+
clearTimeout(timeoutId);
|
|
339
|
+
if (response.status === 401) {
|
|
340
|
+
throw new AuthenticationError("Invalid API key");
|
|
341
|
+
}
|
|
342
|
+
if (response.status === 403) {
|
|
343
|
+
throw new InsufficientCreditsError("Access denied");
|
|
344
|
+
}
|
|
345
|
+
if (response.status === 429) {
|
|
346
|
+
throw new RateLimitError("Rate limit exceeded");
|
|
347
|
+
}
|
|
348
|
+
if (!response.ok) {
|
|
349
|
+
const text = await response.text();
|
|
350
|
+
let message = `HTTP ${response.status}`;
|
|
351
|
+
try {
|
|
352
|
+
const json = JSON.parse(text);
|
|
353
|
+
message = json.detail || json.error || message;
|
|
354
|
+
} catch {
|
|
355
|
+
message = text || message;
|
|
356
|
+
}
|
|
357
|
+
throw new KugelAudioError(message, response.status);
|
|
358
|
+
}
|
|
359
|
+
return await response.json();
|
|
360
|
+
} catch (error) {
|
|
361
|
+
clearTimeout(timeoutId);
|
|
362
|
+
if (error instanceof KugelAudioError) {
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
if (error.name === "AbortError") {
|
|
366
|
+
throw new KugelAudioError("Request timed out");
|
|
367
|
+
}
|
|
368
|
+
throw new KugelAudioError(`Request failed: ${error.message}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
export {
|
|
373
|
+
AuthenticationError,
|
|
374
|
+
ConnectionError,
|
|
375
|
+
InsufficientCreditsError,
|
|
376
|
+
KugelAudio,
|
|
377
|
+
KugelAudioError,
|
|
378
|
+
RateLimitError,
|
|
379
|
+
ValidationError,
|
|
380
|
+
base64ToArrayBuffer,
|
|
381
|
+
createWavBlob,
|
|
382
|
+
createWavFile,
|
|
383
|
+
decodePCM16
|
|
384
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kugelaudio",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Official JavaScript/TypeScript SDK for KugelAudio TTS API",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"CHANGELOG.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
23
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
24
|
+
"lint": "eslint src/",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"test:watch": "vitest",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"tts",
|
|
31
|
+
"text-to-speech",
|
|
32
|
+
"audio",
|
|
33
|
+
"streaming",
|
|
34
|
+
"websocket",
|
|
35
|
+
"kugelaudio"
|
|
36
|
+
],
|
|
37
|
+
"author": "KugelAudio <support@kugelaudio.com>",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/kugelaudio/kugelaudio-js"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://kugelaudio.com",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/kugelaudio/kugelaudio-js/issues"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^20.0.0",
|
|
49
|
+
"tsup": "^8.0.0",
|
|
50
|
+
"typescript": "^5.0.0",
|
|
51
|
+
"vitest": "^1.0.0"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=18.0.0"
|
|
55
|
+
}
|
|
56
|
+
}
|