kugelaudio 0.2.3 → 0.3.0
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/README.md +37 -13
- package/dist/index.d.mts +518 -26
- package/dist/index.d.ts +518 -26
- package/dist/index.js +864 -112
- package/dist/index.mjs +858 -112
- package/package.json +9 -8
- package/src/client.test.ts +548 -0
- package/src/client.ts +885 -103
- package/src/errors.ts +266 -18
- package/src/index.ts +17 -2
- package/src/types.ts +215 -8
- package/src/websocket.ts +38 -18
package/src/errors.ts
CHANGED
|
@@ -1,73 +1,321 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Custom errors for KugelAudio SDK.
|
|
3
|
+
*
|
|
4
|
+
* All SDK errors inherit from {@link KugelAudioError}. Specific subclasses
|
|
5
|
+
* map to the server's `error_code` field (see the server-side `ErrorCode`
|
|
6
|
+
* enum at `tts/src/serving/deployments/errors.py`) so callers can
|
|
7
|
+
* `instanceof AuthenticationError` without matching on message text.
|
|
3
8
|
*/
|
|
4
9
|
|
|
10
|
+
// Keep in lockstep with the server `ErrorCode` enum.
|
|
11
|
+
export const ErrorCodes = {
|
|
12
|
+
UNAUTHORIZED: 'UNAUTHORIZED',
|
|
13
|
+
RATE_LIMITED: 'RATE_LIMITED',
|
|
14
|
+
INSUFFICIENT_CREDITS: 'INSUFFICIENT_CREDITS',
|
|
15
|
+
MODEL_UNAVAILABLE: 'MODEL_UNAVAILABLE',
|
|
16
|
+
EMPTY_AUDIO: 'EMPTY_AUDIO',
|
|
17
|
+
VALIDATION: 'VALIDATION_ERROR',
|
|
18
|
+
INTERNAL: 'INTERNAL_ERROR',
|
|
19
|
+
NOT_FOUND: 'NOT_FOUND',
|
|
20
|
+
} as const;
|
|
21
|
+
export type ErrorCode = typeof ErrorCodes[keyof typeof ErrorCodes];
|
|
22
|
+
|
|
23
|
+
// Server-defined WebSocket close codes.
|
|
24
|
+
export const WsCloseCodes = {
|
|
25
|
+
UNAUTHORIZED: 4001,
|
|
26
|
+
INSUFFICIENT_CREDITS: 4003,
|
|
27
|
+
RATE_LIMITED: 4029,
|
|
28
|
+
MODEL_UNAVAILABLE: 4500,
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
const API_KEYS_URL = 'https://app.kugelaudio.com/settings/api-keys';
|
|
32
|
+
const BILLING_URL = 'https://app.kugelaudio.com/billing';
|
|
33
|
+
|
|
34
|
+
export interface KugelAudioErrorOptions {
|
|
35
|
+
statusCode?: number;
|
|
36
|
+
errorCode?: string;
|
|
37
|
+
requestId?: string;
|
|
38
|
+
retryAfter?: number;
|
|
39
|
+
cause?: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
5
42
|
/**
|
|
6
43
|
* Base error class for KugelAudio SDK.
|
|
7
44
|
*/
|
|
8
45
|
export class KugelAudioError extends Error {
|
|
9
46
|
public readonly statusCode?: number;
|
|
47
|
+
public readonly errorCode?: string;
|
|
48
|
+
public readonly requestId?: string;
|
|
49
|
+
public readonly retryAfter?: number;
|
|
10
50
|
|
|
11
|
-
constructor(message: string,
|
|
12
|
-
super(message);
|
|
51
|
+
constructor(message: string, options: KugelAudioErrorOptions = {}) {
|
|
52
|
+
super(options.requestId ? `${message} (request_id: ${options.requestId})` : message);
|
|
13
53
|
this.name = 'KugelAudioError';
|
|
14
|
-
this.statusCode = statusCode;
|
|
54
|
+
this.statusCode = options.statusCode;
|
|
55
|
+
this.errorCode = options.errorCode;
|
|
56
|
+
this.requestId = options.requestId;
|
|
57
|
+
this.retryAfter = options.retryAfter;
|
|
15
58
|
Object.setPrototypeOf(this, KugelAudioError.prototype);
|
|
16
59
|
}
|
|
17
60
|
}
|
|
18
61
|
|
|
19
62
|
/**
|
|
20
|
-
*
|
|
63
|
+
* API key was missing, malformed, or rejected by the server.
|
|
21
64
|
*/
|
|
22
65
|
export class AuthenticationError extends KugelAudioError {
|
|
23
|
-
constructor(message
|
|
24
|
-
super(
|
|
66
|
+
constructor(message?: string, options: KugelAudioErrorOptions = {}) {
|
|
67
|
+
super(
|
|
68
|
+
message ?? `KugelAudio rejected the API key. Check it is current at ${API_KEYS_URL}.`,
|
|
69
|
+
{ statusCode: 401, errorCode: ErrorCodes.UNAUTHORIZED, ...options },
|
|
70
|
+
);
|
|
25
71
|
this.name = 'AuthenticationError';
|
|
26
72
|
Object.setPrototypeOf(this, AuthenticationError.prototype);
|
|
27
73
|
}
|
|
28
74
|
}
|
|
29
75
|
|
|
30
76
|
/**
|
|
31
|
-
*
|
|
77
|
+
* Request was rejected by the per-org rate limiter.
|
|
32
78
|
*/
|
|
33
79
|
export class RateLimitError extends KugelAudioError {
|
|
34
|
-
constructor(message
|
|
35
|
-
|
|
80
|
+
constructor(message?: string, options: KugelAudioErrorOptions = {}) {
|
|
81
|
+
const msg =
|
|
82
|
+
message ??
|
|
83
|
+
(options.retryAfter
|
|
84
|
+
? `KugelAudio rate limit hit; retry after ${options.retryAfter}s.`
|
|
85
|
+
: 'KugelAudio rate limit hit; retry shortly.');
|
|
86
|
+
super(msg, { statusCode: 429, errorCode: ErrorCodes.RATE_LIMITED, ...options });
|
|
36
87
|
this.name = 'RateLimitError';
|
|
37
88
|
Object.setPrototypeOf(this, RateLimitError.prototype);
|
|
38
89
|
}
|
|
39
90
|
}
|
|
40
91
|
|
|
41
92
|
/**
|
|
42
|
-
*
|
|
93
|
+
* Account is out of TTS credits.
|
|
43
94
|
*/
|
|
44
95
|
export class InsufficientCreditsError extends KugelAudioError {
|
|
45
|
-
constructor(message
|
|
46
|
-
super(
|
|
96
|
+
constructor(message?: string, options: KugelAudioErrorOptions = {}) {
|
|
97
|
+
super(
|
|
98
|
+
message ?? `Your KugelAudio account is out of credits. Top up at ${BILLING_URL}.`,
|
|
99
|
+
{ statusCode: 402, errorCode: ErrorCodes.INSUFFICIENT_CREDITS, ...options },
|
|
100
|
+
);
|
|
47
101
|
this.name = 'InsufficientCreditsError';
|
|
48
102
|
Object.setPrototypeOf(this, InsufficientCreditsError.prototype);
|
|
49
103
|
}
|
|
50
104
|
}
|
|
51
105
|
|
|
52
106
|
/**
|
|
53
|
-
*
|
|
107
|
+
* Request was rejected as invalid (bad params, missing fields, etc.).
|
|
54
108
|
*/
|
|
55
109
|
export class ValidationError extends KugelAudioError {
|
|
56
|
-
constructor(message: string) {
|
|
57
|
-
super(message, 400);
|
|
110
|
+
constructor(message: string, options: KugelAudioErrorOptions = {}) {
|
|
111
|
+
super(message, { statusCode: 400, errorCode: ErrorCodes.VALIDATION, ...options });
|
|
58
112
|
this.name = 'ValidationError';
|
|
59
113
|
Object.setPrototypeOf(this, ValidationError.prototype);
|
|
60
114
|
}
|
|
61
115
|
}
|
|
62
116
|
|
|
63
117
|
/**
|
|
64
|
-
*
|
|
118
|
+
* The SDK could not reach KugelAudio (network error, server down,
|
|
119
|
+
* or model deployment temporarily unavailable).
|
|
65
120
|
*/
|
|
66
121
|
export class ConnectionError extends KugelAudioError {
|
|
67
|
-
constructor(message: string
|
|
68
|
-
super(message, 503);
|
|
122
|
+
constructor(message: string, options: KugelAudioErrorOptions = {}) {
|
|
123
|
+
super(message, { statusCode: 503, ...options });
|
|
69
124
|
this.name = 'ConnectionError';
|
|
70
125
|
Object.setPrototypeOf(this, ConnectionError.prototype);
|
|
71
126
|
}
|
|
72
127
|
}
|
|
73
128
|
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Classifiers
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
function build(
|
|
134
|
+
status: number | undefined,
|
|
135
|
+
errorCode: string | undefined,
|
|
136
|
+
message: string,
|
|
137
|
+
opts: { requestId?: string; retryAfter?: number; cause?: unknown } = {},
|
|
138
|
+
): KugelAudioError {
|
|
139
|
+
// Only include keys whose value we actually know. Subclass constructors
|
|
140
|
+
// spread `...options` over their canonical defaults (e.g. statusCode: 401),
|
|
141
|
+
// so a key with `undefined` would clobber the default.
|
|
142
|
+
const common: {
|
|
143
|
+
statusCode?: number;
|
|
144
|
+
errorCode?: string;
|
|
145
|
+
requestId?: string;
|
|
146
|
+
retryAfter?: number;
|
|
147
|
+
cause?: unknown;
|
|
148
|
+
} = { ...opts };
|
|
149
|
+
if (status !== undefined) common.statusCode = status;
|
|
150
|
+
if (errorCode !== undefined) common.errorCode = errorCode;
|
|
151
|
+
|
|
152
|
+
if (errorCode === ErrorCodes.UNAUTHORIZED || status === 401) {
|
|
153
|
+
return new AuthenticationError(message || undefined, common);
|
|
154
|
+
}
|
|
155
|
+
if (errorCode === ErrorCodes.INSUFFICIENT_CREDITS || status === 402) {
|
|
156
|
+
return new InsufficientCreditsError(message || undefined, common);
|
|
157
|
+
}
|
|
158
|
+
if (errorCode === ErrorCodes.RATE_LIMITED || status === 429) {
|
|
159
|
+
return new RateLimitError(message || undefined, common);
|
|
160
|
+
}
|
|
161
|
+
if (errorCode === ErrorCodes.VALIDATION || status === 400) {
|
|
162
|
+
return new ValidationError(message || 'Request validation failed.', common);
|
|
163
|
+
}
|
|
164
|
+
if (errorCode === ErrorCodes.MODEL_UNAVAILABLE || status === 503) {
|
|
165
|
+
const detail = message || 'service temporarily unavailable';
|
|
166
|
+
return new ConnectionError(
|
|
167
|
+
`KugelAudio is temporarily unavailable: ${detail}. Retry shortly.`,
|
|
168
|
+
common,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return new KugelAudioError(message || `HTTP ${status}`, common);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
interface HttpResponseLike {
|
|
175
|
+
status: number;
|
|
176
|
+
headers: { get(name: string): string | null } | Record<string, string | undefined>;
|
|
177
|
+
text?: () => Promise<string>;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function readHeader(
|
|
181
|
+
headers: HttpResponseLike['headers'],
|
|
182
|
+
name: string,
|
|
183
|
+
): string | undefined {
|
|
184
|
+
if (headers && typeof (headers as Headers).get === 'function') {
|
|
185
|
+
return (headers as Headers).get(name) ?? undefined;
|
|
186
|
+
}
|
|
187
|
+
const rec = headers as Record<string, string | undefined>;
|
|
188
|
+
return rec[name] ?? rec[name.toLowerCase()] ?? undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Build the appropriate `KugelAudioError` from an HTTP response body that
|
|
193
|
+
* was already parsed. `bodyText` is the raw text fallback.
|
|
194
|
+
*/
|
|
195
|
+
export function classifyHttpError(
|
|
196
|
+
status: number,
|
|
197
|
+
bodyText: string,
|
|
198
|
+
headers: HttpResponseLike['headers'],
|
|
199
|
+
): KugelAudioError {
|
|
200
|
+
let errorCode: string | undefined;
|
|
201
|
+
let message = '';
|
|
202
|
+
let retryAfter: number | undefined;
|
|
203
|
+
|
|
204
|
+
if (bodyText) {
|
|
205
|
+
try {
|
|
206
|
+
const body = JSON.parse(bodyText);
|
|
207
|
+
if (body && typeof body === 'object') {
|
|
208
|
+
errorCode = typeof body.error_code === 'string' ? body.error_code : undefined;
|
|
209
|
+
const msg = body.error ?? body.detail;
|
|
210
|
+
if (Array.isArray(msg)) {
|
|
211
|
+
message = msg.map((m) => String(m)).join('; ');
|
|
212
|
+
} else if (typeof msg === 'string') {
|
|
213
|
+
message = msg;
|
|
214
|
+
}
|
|
215
|
+
if (typeof body.retry_after === 'number') {
|
|
216
|
+
retryAfter = body.retry_after;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} catch {
|
|
220
|
+
// Not JSON; fall back to raw body text below.
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (retryAfter === undefined) {
|
|
225
|
+
const header = readHeader(headers, 'Retry-After') ?? readHeader(headers, 'retry-after');
|
|
226
|
+
if (header) {
|
|
227
|
+
const n = Number(header);
|
|
228
|
+
if (Number.isFinite(n)) retryAfter = n;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const requestId = readHeader(headers, 'x-request-id') ?? readHeader(headers, 'X-Request-Id');
|
|
233
|
+
|
|
234
|
+
if (!message) {
|
|
235
|
+
message = (bodyText || '').trim();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return build(status, errorCode, message, { requestId, retryAfter });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Build a `KugelAudioError` from a server-sent WebSocket error frame
|
|
243
|
+
* (`{error, error_code, retry_after}`).
|
|
244
|
+
*/
|
|
245
|
+
export function classifyWsFrame(data: {
|
|
246
|
+
error?: string;
|
|
247
|
+
error_code?: string;
|
|
248
|
+
retry_after?: number;
|
|
249
|
+
}): KugelAudioError {
|
|
250
|
+
const errorCode = data.error_code;
|
|
251
|
+
const message = data.error ?? 'Server reported an error.';
|
|
252
|
+
const retryAfter = typeof data.retry_after === 'number' ? data.retry_after : undefined;
|
|
253
|
+
return build(undefined, errorCode, message, { retryAfter });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Build a `KugelAudioError` from a WebSocket close code + reason.
|
|
258
|
+
*/
|
|
259
|
+
export function classifyWsClose(
|
|
260
|
+
code: number | undefined,
|
|
261
|
+
reason?: string,
|
|
262
|
+
): KugelAudioError {
|
|
263
|
+
const reasonTxt = (reason ?? '').trim();
|
|
264
|
+
|
|
265
|
+
if (code === WsCloseCodes.UNAUTHORIZED) {
|
|
266
|
+
let msg = `KugelAudio rejected the API key. Check it is current at ${API_KEYS_URL}.`;
|
|
267
|
+
if (reasonTxt) msg = `${msg} (${reasonTxt})`;
|
|
268
|
+
return new AuthenticationError(msg);
|
|
269
|
+
}
|
|
270
|
+
if (code === WsCloseCodes.INSUFFICIENT_CREDITS) {
|
|
271
|
+
return new InsufficientCreditsError();
|
|
272
|
+
}
|
|
273
|
+
if (code === WsCloseCodes.RATE_LIMITED) {
|
|
274
|
+
return new RateLimitError();
|
|
275
|
+
}
|
|
276
|
+
if (code === WsCloseCodes.MODEL_UNAVAILABLE) {
|
|
277
|
+
const suffix = reasonTxt ? ` (${reasonTxt})` : '';
|
|
278
|
+
return new ConnectionError(
|
|
279
|
+
`KugelAudio model is temporarily unavailable. Retry shortly.${suffix}`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const detail = reasonTxt || 'no reason given';
|
|
284
|
+
const codeStr = code !== undefined ? ` (code ${code})` : '';
|
|
285
|
+
return new ConnectionError(
|
|
286
|
+
`KugelAudio WebSocket closed by server: ${detail}${codeStr}.`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Extract the HTTP status from a `ws` package handshake-rejection error and
|
|
292
|
+
* return a typed `KugelAudioError`. Returns `null` if the error doesn't look
|
|
293
|
+
* like a handshake rejection (e.g. pure network failure).
|
|
294
|
+
*
|
|
295
|
+
* The `ws` library surfaces rejected upgrades via:
|
|
296
|
+
* - an Error whose `.message` is `"Unexpected server response: <status>"`
|
|
297
|
+
* - `error.code === 'EUNEXPECTEDRESPONSE'`, with `error.statusCode` on some versions
|
|
298
|
+
*
|
|
299
|
+
* The TTS server rejects WS upgrades with a bare API key using HTTP 403
|
|
300
|
+
* (not 401), so we treat 403 here as an auth failure — HTTP API callers
|
|
301
|
+
* keep the generic 403 semantics via {@link classifyHttpError}.
|
|
302
|
+
*/
|
|
303
|
+
export function classifyWsHandshakeError(err: unknown): KugelAudioError | null {
|
|
304
|
+
if (!err || typeof err !== 'object') return null;
|
|
305
|
+
const e = err as { message?: unknown; statusCode?: unknown; code?: unknown };
|
|
306
|
+
|
|
307
|
+
let status: number | undefined;
|
|
308
|
+
if (typeof e.statusCode === 'number') {
|
|
309
|
+
status = e.statusCode;
|
|
310
|
+
}
|
|
311
|
+
if (status === undefined && typeof e.message === 'string') {
|
|
312
|
+
const m = e.message.match(/Unexpected server response:\s*(\d{3})/i);
|
|
313
|
+
if (m) status = Number(m[1]);
|
|
314
|
+
}
|
|
315
|
+
if (status === undefined) return null;
|
|
316
|
+
|
|
317
|
+
if (status === 403) {
|
|
318
|
+
return new AuthenticationError();
|
|
319
|
+
}
|
|
320
|
+
return build(status, undefined, typeof e.message === 'string' ? e.message : '');
|
|
321
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* @packageDocumentation
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
-
// Main client
|
|
42
|
+
// Main client and session classes
|
|
43
43
|
export { KugelAudio } from './client';
|
|
44
44
|
|
|
45
45
|
// Types
|
|
@@ -47,18 +47,26 @@ export type {
|
|
|
47
47
|
AudioChunk,
|
|
48
48
|
AudioResponse,
|
|
49
49
|
ContextVoiceSettings,
|
|
50
|
+
CreateVoiceOptions,
|
|
50
51
|
GenerateOptions,
|
|
51
52
|
GenerationStats,
|
|
52
53
|
KugelAudioOptions,
|
|
53
54
|
Model,
|
|
55
|
+
Region,
|
|
54
56
|
MultiContextAudioChunk,
|
|
55
57
|
MultiContextCallbacks,
|
|
56
58
|
MultiContextConfig,
|
|
57
59
|
StreamCallbacks,
|
|
58
60
|
StreamConfig,
|
|
61
|
+
StreamingSessionCallbacks,
|
|
62
|
+
UpdateVoiceOptions,
|
|
59
63
|
Voice,
|
|
60
64
|
VoiceAge,
|
|
61
65
|
VoiceCategory,
|
|
66
|
+
VoiceDetail,
|
|
67
|
+
VoiceListResponse,
|
|
68
|
+
VoiceQuality,
|
|
69
|
+
VoiceReference,
|
|
62
70
|
VoiceSex,
|
|
63
71
|
WordTimestamp
|
|
64
72
|
} from './types';
|
|
@@ -67,11 +75,18 @@ export type {
|
|
|
67
75
|
export {
|
|
68
76
|
AuthenticationError,
|
|
69
77
|
ConnectionError,
|
|
78
|
+
ErrorCodes,
|
|
70
79
|
InsufficientCreditsError,
|
|
71
80
|
KugelAudioError,
|
|
72
81
|
RateLimitError,
|
|
73
|
-
ValidationError
|
|
82
|
+
ValidationError,
|
|
83
|
+
WsCloseCodes,
|
|
84
|
+
classifyHttpError,
|
|
85
|
+
classifyWsClose,
|
|
86
|
+
classifyWsFrame,
|
|
87
|
+
classifyWsHandshakeError,
|
|
74
88
|
} from './errors';
|
|
89
|
+
export type { ErrorCode, KugelAudioErrorOptions } from './errors';
|
|
75
90
|
|
|
76
91
|
// Utilities
|
|
77
92
|
export {
|