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/src/client.ts
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KugelAudio API Client.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
InsufficientCreditsError,
|
|
8
|
+
KugelAudioError,
|
|
9
|
+
RateLimitError,
|
|
10
|
+
} from './errors';
|
|
11
|
+
import type {
|
|
12
|
+
AudioChunk,
|
|
13
|
+
AudioResponse,
|
|
14
|
+
GenerateOptions,
|
|
15
|
+
GenerationStats,
|
|
16
|
+
KugelAudioOptions,
|
|
17
|
+
Model,
|
|
18
|
+
StreamCallbacks,
|
|
19
|
+
Voice
|
|
20
|
+
} from './types';
|
|
21
|
+
import { base64ToArrayBuffer } from './utils';
|
|
22
|
+
|
|
23
|
+
const DEFAULT_API_URL = 'https://api.kugelaudio.com';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Models resource for listing TTS models.
|
|
27
|
+
*/
|
|
28
|
+
class ModelsResource {
|
|
29
|
+
constructor(private client: KugelAudio) {}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* List available TTS models.
|
|
33
|
+
*/
|
|
34
|
+
async list(): Promise<Model[]> {
|
|
35
|
+
const response = await this.client.request<{ models: any[] }>('GET', '/v1/models');
|
|
36
|
+
return response.models.map((m) => ({
|
|
37
|
+
id: m.id,
|
|
38
|
+
name: m.name,
|
|
39
|
+
description: m.description || '',
|
|
40
|
+
parameters: m.parameters || '',
|
|
41
|
+
maxInputLength: m.max_input_length || 5000,
|
|
42
|
+
sampleRate: m.sample_rate || 24000,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Voices resource for managing voices.
|
|
49
|
+
*/
|
|
50
|
+
class VoicesResource {
|
|
51
|
+
constructor(private client: KugelAudio) {}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* List available voices.
|
|
55
|
+
*/
|
|
56
|
+
async list(options?: {
|
|
57
|
+
language?: string;
|
|
58
|
+
includePublic?: boolean;
|
|
59
|
+
limit?: number;
|
|
60
|
+
}): Promise<Voice[]> {
|
|
61
|
+
const params = new URLSearchParams();
|
|
62
|
+
if (options?.language) params.set('language', options.language);
|
|
63
|
+
if (options?.includePublic !== undefined) {
|
|
64
|
+
params.set('include_public', String(options.includePublic));
|
|
65
|
+
}
|
|
66
|
+
if (options?.limit) params.set('limit', String(options.limit));
|
|
67
|
+
|
|
68
|
+
const query = params.toString();
|
|
69
|
+
const path = query ? `/v1/voices?${query}` : '/v1/voices';
|
|
70
|
+
const response = await this.client.request<{ voices: any[] }>('GET', path);
|
|
71
|
+
|
|
72
|
+
return response.voices.map((v) => ({
|
|
73
|
+
id: v.id,
|
|
74
|
+
name: v.name,
|
|
75
|
+
description: v.description,
|
|
76
|
+
category: v.category,
|
|
77
|
+
sex: v.sex,
|
|
78
|
+
age: v.age,
|
|
79
|
+
supportedLanguages: v.supported_languages || [],
|
|
80
|
+
sampleText: v.sample_text,
|
|
81
|
+
avatarUrl: v.avatar_url,
|
|
82
|
+
sampleUrl: v.sample_url,
|
|
83
|
+
isPublic: v.is_public || false,
|
|
84
|
+
verified: v.verified || false,
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get a specific voice by ID.
|
|
90
|
+
*/
|
|
91
|
+
async get(voiceId: number): Promise<Voice> {
|
|
92
|
+
const v = await this.client.request<any>('GET', `/v1/voices/${voiceId}`);
|
|
93
|
+
return {
|
|
94
|
+
id: v.id,
|
|
95
|
+
name: v.name,
|
|
96
|
+
description: v.description,
|
|
97
|
+
category: v.category,
|
|
98
|
+
sex: v.sex,
|
|
99
|
+
age: v.age,
|
|
100
|
+
supportedLanguages: v.supported_languages || [],
|
|
101
|
+
sampleText: v.sample_text,
|
|
102
|
+
avatarUrl: v.avatar_url,
|
|
103
|
+
sampleUrl: v.sample_url,
|
|
104
|
+
isPublic: v.is_public || false,
|
|
105
|
+
verified: v.verified || false,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* TTS resource for text-to-speech generation.
|
|
112
|
+
*/
|
|
113
|
+
class TTSResource {
|
|
114
|
+
constructor(private client: KugelAudio) {}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Generate audio from text with streaming via WebSocket.
|
|
118
|
+
* Returns complete audio after all chunks are received.
|
|
119
|
+
*/
|
|
120
|
+
async generate(options: GenerateOptions): Promise<AudioResponse> {
|
|
121
|
+
const chunks: ArrayBuffer[] = [];
|
|
122
|
+
let finalStats: GenerationStats | undefined;
|
|
123
|
+
|
|
124
|
+
await this.stream(options, {
|
|
125
|
+
onChunk: (chunk) => {
|
|
126
|
+
chunks.push(base64ToArrayBuffer(chunk.audio));
|
|
127
|
+
},
|
|
128
|
+
onFinal: (stats) => {
|
|
129
|
+
finalStats = stats;
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Combine all chunks
|
|
134
|
+
const totalLength = chunks.reduce((acc, c) => acc + c.byteLength, 0);
|
|
135
|
+
const combined = new Uint8Array(totalLength);
|
|
136
|
+
let offset = 0;
|
|
137
|
+
for (const chunk of chunks) {
|
|
138
|
+
combined.set(new Uint8Array(chunk), offset);
|
|
139
|
+
offset += chunk.byteLength;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
audio: combined.buffer,
|
|
144
|
+
sampleRate: options.sampleRate || 24000,
|
|
145
|
+
samples: finalStats ? finalStats.totalSamples : totalLength / 2,
|
|
146
|
+
durationMs: finalStats ? finalStats.durationMs : 0,
|
|
147
|
+
generationMs: finalStats ? finalStats.generationMs : 0,
|
|
148
|
+
rtf: finalStats ? finalStats.rtf : 0,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Stream audio from text via WebSocket.
|
|
154
|
+
*/
|
|
155
|
+
stream(options: GenerateOptions, callbacks: StreamCallbacks): Promise<void> {
|
|
156
|
+
return new Promise((resolve, reject) => {
|
|
157
|
+
const wsUrl = this.client.ttsUrl
|
|
158
|
+
.replace('https://', 'wss://')
|
|
159
|
+
.replace('http://', 'ws://');
|
|
160
|
+
const url = `${wsUrl}/ws/tts?api_key=${this.client.apiKey}`;
|
|
161
|
+
|
|
162
|
+
const ws = new WebSocket(url);
|
|
163
|
+
|
|
164
|
+
ws.onopen = () => {
|
|
165
|
+
callbacks.onOpen?.();
|
|
166
|
+
// Send TTS request
|
|
167
|
+
ws.send(JSON.stringify({
|
|
168
|
+
text: options.text,
|
|
169
|
+
model: options.model || 'kugel-one-turbo',
|
|
170
|
+
voice_id: options.voiceId,
|
|
171
|
+
cfg_scale: options.cfgScale ?? 2.0,
|
|
172
|
+
max_new_tokens: options.maxNewTokens ?? 2048,
|
|
173
|
+
sample_rate: options.sampleRate ?? 24000,
|
|
174
|
+
speaker_prefix: options.speakerPrefix ?? true,
|
|
175
|
+
}));
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
ws.onmessage = (event) => {
|
|
179
|
+
try {
|
|
180
|
+
const data = JSON.parse(event.data);
|
|
181
|
+
|
|
182
|
+
if (data.error) {
|
|
183
|
+
const error = this.parseError(data.error);
|
|
184
|
+
callbacks.onError?.(error);
|
|
185
|
+
ws.close();
|
|
186
|
+
reject(error);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (data.final) {
|
|
191
|
+
const stats: GenerationStats = {
|
|
192
|
+
final: true,
|
|
193
|
+
chunks: data.chunks,
|
|
194
|
+
totalSamples: data.total_samples,
|
|
195
|
+
durationMs: data.dur_ms,
|
|
196
|
+
generationMs: data.gen_ms,
|
|
197
|
+
ttfaMs: data.ttfa_ms,
|
|
198
|
+
rtf: data.rtf,
|
|
199
|
+
error: data.error,
|
|
200
|
+
};
|
|
201
|
+
callbacks.onFinal?.(stats);
|
|
202
|
+
ws.close();
|
|
203
|
+
resolve();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (data.audio) {
|
|
208
|
+
const chunk: AudioChunk = {
|
|
209
|
+
audio: data.audio,
|
|
210
|
+
encoding: data.enc || 'pcm_s16le',
|
|
211
|
+
index: data.idx,
|
|
212
|
+
sampleRate: data.sr,
|
|
213
|
+
samples: data.samples,
|
|
214
|
+
};
|
|
215
|
+
callbacks.onChunk?.(chunk);
|
|
216
|
+
}
|
|
217
|
+
} catch (e) {
|
|
218
|
+
console.error('Failed to parse WebSocket message:', e);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
ws.onerror = () => {
|
|
223
|
+
const error = new KugelAudioError('WebSocket connection error');
|
|
224
|
+
callbacks.onError?.(error);
|
|
225
|
+
reject(error);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
ws.onclose = (event) => {
|
|
229
|
+
callbacks.onClose?.();
|
|
230
|
+
if (event.code === 4001) {
|
|
231
|
+
reject(new AuthenticationError('Authentication failed'));
|
|
232
|
+
} else if (event.code === 4003) {
|
|
233
|
+
reject(new InsufficientCreditsError('Insufficient credits'));
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private parseError(message: string): Error {
|
|
240
|
+
const lower = message.toLowerCase();
|
|
241
|
+
if (lower.includes('auth') || lower.includes('unauthorized')) {
|
|
242
|
+
return new AuthenticationError(message);
|
|
243
|
+
}
|
|
244
|
+
if (lower.includes('credit')) {
|
|
245
|
+
return new InsufficientCreditsError(message);
|
|
246
|
+
}
|
|
247
|
+
return new KugelAudioError(message);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* KugelAudio API client.
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* ```typescript
|
|
256
|
+
* const client = new KugelAudio({ apiKey: 'your_api_key' });
|
|
257
|
+
*
|
|
258
|
+
* // List models
|
|
259
|
+
* const models = await client.models.list();
|
|
260
|
+
*
|
|
261
|
+
* // List voices
|
|
262
|
+
* const voices = await client.voices.list();
|
|
263
|
+
*
|
|
264
|
+
* // Generate audio
|
|
265
|
+
* const audio = await client.tts.generate({
|
|
266
|
+
* text: 'Hello, world!',
|
|
267
|
+
* model: 'kugel-one-turbo',
|
|
268
|
+
* });
|
|
269
|
+
* ```
|
|
270
|
+
*/
|
|
271
|
+
export class KugelAudio {
|
|
272
|
+
private _apiKey: string;
|
|
273
|
+
private _apiUrl: string;
|
|
274
|
+
private _ttsUrl: string;
|
|
275
|
+
private _timeout: number;
|
|
276
|
+
|
|
277
|
+
/** Models resource */
|
|
278
|
+
public readonly models: ModelsResource;
|
|
279
|
+
/** Voices resource */
|
|
280
|
+
public readonly voices: VoicesResource;
|
|
281
|
+
/** TTS resource */
|
|
282
|
+
public readonly tts: TTSResource;
|
|
283
|
+
|
|
284
|
+
constructor(options: KugelAudioOptions) {
|
|
285
|
+
if (!options.apiKey) {
|
|
286
|
+
throw new Error('API key is required');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
this._apiKey = options.apiKey;
|
|
290
|
+
this._apiUrl = (options.apiUrl || DEFAULT_API_URL).replace(/\/$/, '');
|
|
291
|
+
// If ttsUrl not specified, use apiUrl (backend proxies to TTS server)
|
|
292
|
+
this._ttsUrl = (options.ttsUrl || this._apiUrl).replace(/\/$/, '');
|
|
293
|
+
this._timeout = options.timeout || 60000;
|
|
294
|
+
|
|
295
|
+
this.models = new ModelsResource(this);
|
|
296
|
+
this.voices = new VoicesResource(this);
|
|
297
|
+
this.tts = new TTSResource(this);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Get API key */
|
|
301
|
+
get apiKey(): string {
|
|
302
|
+
return this._apiKey;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Get TTS URL */
|
|
306
|
+
get ttsUrl(): string {
|
|
307
|
+
return this._ttsUrl;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Make an HTTP request to the API.
|
|
312
|
+
* @internal
|
|
313
|
+
*/
|
|
314
|
+
async request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
315
|
+
const url = `${this._apiUrl}${path}`;
|
|
316
|
+
|
|
317
|
+
const headers: Record<string, string> = {
|
|
318
|
+
'Content-Type': 'application/json',
|
|
319
|
+
'X-API-Key': this._apiKey,
|
|
320
|
+
'Authorization': `Bearer ${this._apiKey}`,
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const controller = new AbortController();
|
|
324
|
+
const timeoutId = setTimeout(() => controller.abort(), this._timeout);
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const response = await fetch(url, {
|
|
328
|
+
method,
|
|
329
|
+
headers,
|
|
330
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
331
|
+
signal: controller.signal,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
clearTimeout(timeoutId);
|
|
335
|
+
|
|
336
|
+
if (response.status === 401) {
|
|
337
|
+
throw new AuthenticationError('Invalid API key');
|
|
338
|
+
}
|
|
339
|
+
if (response.status === 403) {
|
|
340
|
+
throw new InsufficientCreditsError('Access denied');
|
|
341
|
+
}
|
|
342
|
+
if (response.status === 429) {
|
|
343
|
+
throw new RateLimitError('Rate limit exceeded');
|
|
344
|
+
}
|
|
345
|
+
if (!response.ok) {
|
|
346
|
+
const text = await response.text();
|
|
347
|
+
let message = `HTTP ${response.status}`;
|
|
348
|
+
try {
|
|
349
|
+
const json = JSON.parse(text);
|
|
350
|
+
message = json.detail || json.error || message;
|
|
351
|
+
} catch {
|
|
352
|
+
message = text || message;
|
|
353
|
+
}
|
|
354
|
+
throw new KugelAudioError(message, response.status);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return await response.json();
|
|
358
|
+
} catch (error) {
|
|
359
|
+
clearTimeout(timeoutId);
|
|
360
|
+
if (error instanceof KugelAudioError) {
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
if ((error as Error).name === 'AbortError') {
|
|
364
|
+
throw new KugelAudioError('Request timed out');
|
|
365
|
+
}
|
|
366
|
+
throw new KugelAudioError(`Request failed: ${(error as Error).message}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom errors for KugelAudio SDK.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Base error class for KugelAudio SDK.
|
|
7
|
+
*/
|
|
8
|
+
export class KugelAudioError extends Error {
|
|
9
|
+
public readonly statusCode?: number;
|
|
10
|
+
|
|
11
|
+
constructor(message: string, statusCode?: number) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'KugelAudioError';
|
|
14
|
+
this.statusCode = statusCode;
|
|
15
|
+
Object.setPrototypeOf(this, KugelAudioError.prototype);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Thrown when authentication fails.
|
|
21
|
+
*/
|
|
22
|
+
export class AuthenticationError extends KugelAudioError {
|
|
23
|
+
constructor(message: string = 'Authentication failed') {
|
|
24
|
+
super(message, 401);
|
|
25
|
+
this.name = 'AuthenticationError';
|
|
26
|
+
Object.setPrototypeOf(this, AuthenticationError.prototype);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Thrown when rate limit is exceeded.
|
|
32
|
+
*/
|
|
33
|
+
export class RateLimitError extends KugelAudioError {
|
|
34
|
+
constructor(message: string = 'Rate limit exceeded') {
|
|
35
|
+
super(message, 429);
|
|
36
|
+
this.name = 'RateLimitError';
|
|
37
|
+
Object.setPrototypeOf(this, RateLimitError.prototype);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Thrown when user has insufficient credits.
|
|
43
|
+
*/
|
|
44
|
+
export class InsufficientCreditsError extends KugelAudioError {
|
|
45
|
+
constructor(message: string = 'Insufficient credits') {
|
|
46
|
+
super(message, 403);
|
|
47
|
+
this.name = 'InsufficientCreditsError';
|
|
48
|
+
Object.setPrototypeOf(this, InsufficientCreditsError.prototype);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Thrown when request validation fails.
|
|
54
|
+
*/
|
|
55
|
+
export class ValidationError extends KugelAudioError {
|
|
56
|
+
constructor(message: string) {
|
|
57
|
+
super(message, 400);
|
|
58
|
+
this.name = 'ValidationError';
|
|
59
|
+
Object.setPrototypeOf(this, ValidationError.prototype);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Thrown when connection to server fails.
|
|
65
|
+
*/
|
|
66
|
+
export class ConnectionError extends KugelAudioError {
|
|
67
|
+
constructor(message: string = 'Failed to connect to server') {
|
|
68
|
+
super(message, 503);
|
|
69
|
+
this.name = 'ConnectionError';
|
|
70
|
+
Object.setPrototypeOf(this, ConnectionError.prototype);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KugelAudio JavaScript/TypeScript SDK
|
|
3
|
+
*
|
|
4
|
+
* Official client for KugelAudio TTS API.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { KugelAudio } from 'kugelaudio';
|
|
9
|
+
*
|
|
10
|
+
* const client = new KugelAudio({ apiKey: 'your_api_key' });
|
|
11
|
+
*
|
|
12
|
+
* // List available models
|
|
13
|
+
* const models = await client.models.list();
|
|
14
|
+
*
|
|
15
|
+
* // List available voices
|
|
16
|
+
* const voices = await client.voices.list();
|
|
17
|
+
*
|
|
18
|
+
* // Generate audio (non-streaming)
|
|
19
|
+
* const audio = await client.tts.generate({
|
|
20
|
+
* text: 'Hello, world!',
|
|
21
|
+
* model: 'kugel-one-turbo',
|
|
22
|
+
* voiceId: 123,
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // Generate audio (streaming)
|
|
26
|
+
* await client.tts.stream(
|
|
27
|
+
* { text: 'Hello, world!', model: 'kugel-one-turbo' },
|
|
28
|
+
* {
|
|
29
|
+
* onChunk: (chunk) => {
|
|
30
|
+
* // Process audio chunk
|
|
31
|
+
* },
|
|
32
|
+
* onFinal: (stats) => {
|
|
33
|
+
* console.log(`Generated ${stats.durationMs}ms of audio`);
|
|
34
|
+
* },
|
|
35
|
+
* }
|
|
36
|
+
* );
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @packageDocumentation
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
// Main client
|
|
43
|
+
export { KugelAudio } from './client';
|
|
44
|
+
|
|
45
|
+
// Types
|
|
46
|
+
export type {
|
|
47
|
+
AudioChunk,
|
|
48
|
+
AudioResponse,
|
|
49
|
+
GenerateOptions,
|
|
50
|
+
GenerationStats,
|
|
51
|
+
KugelAudioOptions,
|
|
52
|
+
Model,
|
|
53
|
+
StreamCallbacks,
|
|
54
|
+
StreamConfig,
|
|
55
|
+
Voice,
|
|
56
|
+
VoiceAge,
|
|
57
|
+
VoiceCategory,
|
|
58
|
+
VoiceSex
|
|
59
|
+
} from './types';
|
|
60
|
+
|
|
61
|
+
// Errors
|
|
62
|
+
export {
|
|
63
|
+
AuthenticationError,
|
|
64
|
+
ConnectionError,
|
|
65
|
+
InsufficientCreditsError,
|
|
66
|
+
KugelAudioError,
|
|
67
|
+
RateLimitError,
|
|
68
|
+
ValidationError
|
|
69
|
+
} from './errors';
|
|
70
|
+
|
|
71
|
+
// Utilities
|
|
72
|
+
export {
|
|
73
|
+
base64ToArrayBuffer,
|
|
74
|
+
createWavBlob,
|
|
75
|
+
createWavFile,
|
|
76
|
+
decodePCM16
|
|
77
|
+
} from './utils';
|
|
78
|
+
|
|
79
|
+
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for KugelAudio SDK.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TTS model information.
|
|
7
|
+
*/
|
|
8
|
+
export interface Model {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
parameters: string;
|
|
13
|
+
maxInputLength: number;
|
|
14
|
+
sampleRate: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Voice category types.
|
|
19
|
+
*/
|
|
20
|
+
export type VoiceCategory = 'premade' | 'cloned' | 'designed';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Voice sex types.
|
|
24
|
+
*/
|
|
25
|
+
export type VoiceSex = 'male' | 'female' | 'neutral';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Voice age types.
|
|
29
|
+
*/
|
|
30
|
+
export type VoiceAge = 'young' | 'middle_aged' | 'old';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Voice information.
|
|
34
|
+
*/
|
|
35
|
+
export interface Voice {
|
|
36
|
+
id: number;
|
|
37
|
+
name: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
category?: VoiceCategory;
|
|
40
|
+
sex?: VoiceSex;
|
|
41
|
+
age?: VoiceAge;
|
|
42
|
+
supportedLanguages: string[];
|
|
43
|
+
sampleText?: string;
|
|
44
|
+
avatarUrl?: string;
|
|
45
|
+
sampleUrl?: string;
|
|
46
|
+
isPublic: boolean;
|
|
47
|
+
verified: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* TTS generation request options.
|
|
52
|
+
*/
|
|
53
|
+
export interface GenerateOptions {
|
|
54
|
+
/** Text to synthesize */
|
|
55
|
+
text: string;
|
|
56
|
+
/** Model to use (default: 'kugel-one-turbo') */
|
|
57
|
+
model?: string;
|
|
58
|
+
/** Voice ID to use */
|
|
59
|
+
voiceId?: number;
|
|
60
|
+
/** CFG scale for generation (default: 2.0) */
|
|
61
|
+
cfgScale?: number;
|
|
62
|
+
/** Maximum tokens to generate (default: 2048) */
|
|
63
|
+
maxNewTokens?: number;
|
|
64
|
+
/** Output sample rate (default: 24000) */
|
|
65
|
+
sampleRate?: number;
|
|
66
|
+
/** Whether to add speaker prefix (default: true) */
|
|
67
|
+
speakerPrefix?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Streaming session configuration.
|
|
72
|
+
*/
|
|
73
|
+
export interface StreamConfig {
|
|
74
|
+
/** Voice ID to use */
|
|
75
|
+
voiceId?: number;
|
|
76
|
+
/** CFG scale for generation */
|
|
77
|
+
cfgScale?: number;
|
|
78
|
+
/** Maximum tokens per generation */
|
|
79
|
+
maxNewTokens?: number;
|
|
80
|
+
/** Output sample rate */
|
|
81
|
+
sampleRate?: number;
|
|
82
|
+
/** Whether to add speaker prefix */
|
|
83
|
+
speakerPrefix?: boolean;
|
|
84
|
+
/** Auto-flush timeout in milliseconds */
|
|
85
|
+
flushTimeoutMs?: number;
|
|
86
|
+
/** Maximum buffer length */
|
|
87
|
+
maxBufferLength?: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Audio chunk from streaming TTS.
|
|
92
|
+
*/
|
|
93
|
+
export interface AudioChunk {
|
|
94
|
+
/** Raw PCM16 audio as base64 */
|
|
95
|
+
audio: string;
|
|
96
|
+
/** Encoding format */
|
|
97
|
+
encoding: 'pcm_s16le';
|
|
98
|
+
/** Chunk index */
|
|
99
|
+
index: number;
|
|
100
|
+
/** Sample rate */
|
|
101
|
+
sampleRate: number;
|
|
102
|
+
/** Number of samples */
|
|
103
|
+
samples: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Final message from TTS generation.
|
|
108
|
+
*/
|
|
109
|
+
export interface GenerationStats {
|
|
110
|
+
/** Indicates this is the final message */
|
|
111
|
+
final: true;
|
|
112
|
+
/** Number of chunks generated */
|
|
113
|
+
chunks: number;
|
|
114
|
+
/** Total samples generated */
|
|
115
|
+
totalSamples: number;
|
|
116
|
+
/** Duration of audio in milliseconds */
|
|
117
|
+
durationMs: number;
|
|
118
|
+
/** Generation time in milliseconds */
|
|
119
|
+
generationMs: number;
|
|
120
|
+
/** Time to first audio in milliseconds */
|
|
121
|
+
ttfaMs: number | null;
|
|
122
|
+
/** Real-time factor */
|
|
123
|
+
rtf: number;
|
|
124
|
+
/** Error message if any */
|
|
125
|
+
error?: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Complete audio response from TTS generation.
|
|
130
|
+
*/
|
|
131
|
+
export interface AudioResponse {
|
|
132
|
+
/** Raw PCM16 audio bytes as ArrayBuffer */
|
|
133
|
+
audio: ArrayBuffer;
|
|
134
|
+
/** Sample rate */
|
|
135
|
+
sampleRate: number;
|
|
136
|
+
/** Number of samples */
|
|
137
|
+
samples: number;
|
|
138
|
+
/** Duration in milliseconds */
|
|
139
|
+
durationMs: number;
|
|
140
|
+
/** Generation time in milliseconds */
|
|
141
|
+
generationMs: number;
|
|
142
|
+
/** Real-time factor */
|
|
143
|
+
rtf: number;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Event callbacks for streaming.
|
|
148
|
+
*/
|
|
149
|
+
export interface StreamCallbacks {
|
|
150
|
+
/** Called when an audio chunk is received */
|
|
151
|
+
onChunk?: (chunk: AudioChunk) => void;
|
|
152
|
+
/** Called when generation is complete */
|
|
153
|
+
onFinal?: (stats: GenerationStats) => void;
|
|
154
|
+
/** Called on error */
|
|
155
|
+
onError?: (error: Error) => void;
|
|
156
|
+
/** Called when connection opens */
|
|
157
|
+
onOpen?: () => void;
|
|
158
|
+
/** Called when connection closes */
|
|
159
|
+
onClose?: () => void;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* KugelAudio client options.
|
|
164
|
+
*/
|
|
165
|
+
export interface KugelAudioOptions {
|
|
166
|
+
/** Your KugelAudio API key */
|
|
167
|
+
apiKey: string;
|
|
168
|
+
/** API base URL (default: https://api.kugelaudio.com) */
|
|
169
|
+
apiUrl?: string;
|
|
170
|
+
/** TTS server URL (default: https://eu.kugelaudio.com) */
|
|
171
|
+
ttsUrl?: string;
|
|
172
|
+
/** Request timeout in milliseconds (default: 60000) */
|
|
173
|
+
timeout?: number;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* API error response.
|
|
178
|
+
*/
|
|
179
|
+
export interface ApiError {
|
|
180
|
+
error: string;
|
|
181
|
+
detail?: string;
|
|
182
|
+
statusCode?: number;
|
|
183
|
+
}
|
|
184
|
+
|