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/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
+ }