naijalingo 0.1.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 +268 -0
- package/dist/index.cjs +707 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +215 -0
- package/dist/index.d.ts +215 -0
- package/dist/index.js +670 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AudioResponse: () => AudioResponse,
|
|
24
|
+
AudioStream: () => AudioStream,
|
|
25
|
+
AuthenticationError: () => AuthenticationError,
|
|
26
|
+
ConnectionError: () => ConnectionError,
|
|
27
|
+
NaijaLingo: () => NaijaLingo,
|
|
28
|
+
NaijaLingoError: () => NaijaLingoError,
|
|
29
|
+
NotFoundError: () => NotFoundError,
|
|
30
|
+
RateLimitError: () => RateLimitError,
|
|
31
|
+
ServerError: () => ServerError,
|
|
32
|
+
TTS: () => TTS,
|
|
33
|
+
VERSION: () => VERSION
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
|
|
37
|
+
// src/errors.ts
|
|
38
|
+
var NaijaLingoError = class extends Error {
|
|
39
|
+
statusCode;
|
|
40
|
+
response;
|
|
41
|
+
constructor(message, statusCode, response) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "NaijaLingoError";
|
|
44
|
+
this.statusCode = statusCode;
|
|
45
|
+
this.response = response;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var AuthenticationError = class extends NaijaLingoError {
|
|
49
|
+
constructor(message, statusCode, response) {
|
|
50
|
+
super(message, statusCode, response);
|
|
51
|
+
this.name = "AuthenticationError";
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var RateLimitError = class extends NaijaLingoError {
|
|
55
|
+
constructor(message, statusCode, response) {
|
|
56
|
+
super(message, statusCode, response);
|
|
57
|
+
this.name = "RateLimitError";
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var NotFoundError = class extends NaijaLingoError {
|
|
61
|
+
constructor(message, statusCode, response) {
|
|
62
|
+
super(message, statusCode, response);
|
|
63
|
+
this.name = "NotFoundError";
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var ServerError = class extends NaijaLingoError {
|
|
67
|
+
constructor(message, statusCode, response) {
|
|
68
|
+
super(message, statusCode, response);
|
|
69
|
+
this.name = "ServerError";
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var ConnectionError = class extends NaijaLingoError {
|
|
73
|
+
constructor(message, statusCode, response) {
|
|
74
|
+
super(message, statusCode, response);
|
|
75
|
+
this.name = "ConnectionError";
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// src/client.ts
|
|
80
|
+
var DEFAULT_BASE_URL = "https://api.9jalingo.org";
|
|
81
|
+
var DEFAULT_TIMEOUT_MS = 3e5;
|
|
82
|
+
var BaseClient = class {
|
|
83
|
+
apiKey;
|
|
84
|
+
baseUrl;
|
|
85
|
+
timeout;
|
|
86
|
+
defaultHeaders;
|
|
87
|
+
constructor(options = {}) {
|
|
88
|
+
this.apiKey = options.apiKey ?? process.env.NAIJALINGO_API_KEY ?? "";
|
|
89
|
+
const base = options.baseUrl ?? process.env.NAIJALINGO_BASE_URL ?? DEFAULT_BASE_URL;
|
|
90
|
+
this.baseUrl = base.replace(/\/+$/, "");
|
|
91
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
92
|
+
this.defaultHeaders = {
|
|
93
|
+
"User-Agent": "naijalingo-js/0.1.0"
|
|
94
|
+
};
|
|
95
|
+
if (this.apiKey) {
|
|
96
|
+
this.defaultHeaders["X-API-Key"] = this.apiKey;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async request(method, path, init = {}) {
|
|
100
|
+
const url = `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
|
|
101
|
+
const timeoutMs = init.timeoutMs ?? this.timeout;
|
|
102
|
+
let lastError;
|
|
103
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
106
|
+
try {
|
|
107
|
+
const response = await fetch(url, {
|
|
108
|
+
...init,
|
|
109
|
+
method,
|
|
110
|
+
headers: {
|
|
111
|
+
...this.defaultHeaders,
|
|
112
|
+
...init.headers
|
|
113
|
+
},
|
|
114
|
+
signal: controller.signal
|
|
115
|
+
});
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
if (response.ok) {
|
|
118
|
+
return response;
|
|
119
|
+
}
|
|
120
|
+
await this.raiseForStatus(response);
|
|
121
|
+
return response;
|
|
122
|
+
} catch (err) {
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
if (err instanceof NaijaLingoError) {
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
lastError = err;
|
|
128
|
+
const isAbort = err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
|
|
129
|
+
if (isAbort) {
|
|
130
|
+
throw new ConnectionError(
|
|
131
|
+
`Request timed out after ${timeoutMs}ms: ${String(err)}`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (attempt < 2) {
|
|
135
|
+
await sleep(1500);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
throw new ConnectionError(
|
|
141
|
+
`Unable to connect to ${this.baseUrl}: ${String(lastError)}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
async raiseForStatus(response) {
|
|
145
|
+
const status = response.status;
|
|
146
|
+
let detail;
|
|
147
|
+
let body;
|
|
148
|
+
try {
|
|
149
|
+
body = await response.json();
|
|
150
|
+
detail = typeof body === "object" && body !== null && "detail" in body ? String(body.detail) : JSON.stringify(body);
|
|
151
|
+
} catch {
|
|
152
|
+
detail = await response.text();
|
|
153
|
+
}
|
|
154
|
+
if (status === 401 || status === 403) {
|
|
155
|
+
throw new AuthenticationError(detail, status, body);
|
|
156
|
+
}
|
|
157
|
+
if (status === 404) {
|
|
158
|
+
throw new NotFoundError(detail, status, body);
|
|
159
|
+
}
|
|
160
|
+
if (status === 429) {
|
|
161
|
+
throw new RateLimitError(detail, status, body);
|
|
162
|
+
}
|
|
163
|
+
if (status >= 500) {
|
|
164
|
+
throw new ServerError(detail, status, body);
|
|
165
|
+
}
|
|
166
|
+
throw new NaijaLingoError(detail, status, body);
|
|
167
|
+
}
|
|
168
|
+
async getJson(path, params) {
|
|
169
|
+
let url = path;
|
|
170
|
+
if (params) {
|
|
171
|
+
const qs = new URLSearchParams();
|
|
172
|
+
for (const [k, v] of Object.entries(params)) {
|
|
173
|
+
if (v != null) qs.set(k, v);
|
|
174
|
+
}
|
|
175
|
+
const s = qs.toString();
|
|
176
|
+
if (s) url = `${path}?${s}`;
|
|
177
|
+
}
|
|
178
|
+
const resp = await this.request("GET", url);
|
|
179
|
+
return await resp.json();
|
|
180
|
+
}
|
|
181
|
+
async postBytes(path, body) {
|
|
182
|
+
const resp = await this.request("POST", path, {
|
|
183
|
+
headers: { "Content-Type": "application/json" },
|
|
184
|
+
body: JSON.stringify(body)
|
|
185
|
+
});
|
|
186
|
+
return new Uint8Array(await resp.arrayBuffer());
|
|
187
|
+
}
|
|
188
|
+
async *postStream(path, body) {
|
|
189
|
+
let lastError;
|
|
190
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
191
|
+
const controller = new AbortController();
|
|
192
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
193
|
+
try {
|
|
194
|
+
const response = await fetch(
|
|
195
|
+
`${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`,
|
|
196
|
+
{
|
|
197
|
+
method: "POST",
|
|
198
|
+
headers: {
|
|
199
|
+
...this.defaultHeaders,
|
|
200
|
+
"Content-Type": "application/json"
|
|
201
|
+
},
|
|
202
|
+
body: JSON.stringify(body),
|
|
203
|
+
signal: controller.signal
|
|
204
|
+
}
|
|
205
|
+
);
|
|
206
|
+
clearTimeout(timer);
|
|
207
|
+
if (!response.ok) {
|
|
208
|
+
await this.raiseForStatus(response);
|
|
209
|
+
}
|
|
210
|
+
if (!response.body) {
|
|
211
|
+
throw new ConnectionError("No response body for stream");
|
|
212
|
+
}
|
|
213
|
+
const reader = response.body.getReader();
|
|
214
|
+
try {
|
|
215
|
+
while (true) {
|
|
216
|
+
const { done, value } = await reader.read();
|
|
217
|
+
if (done) break;
|
|
218
|
+
if (value && value.length > 0) {
|
|
219
|
+
yield value;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} finally {
|
|
223
|
+
reader.releaseLock();
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
} catch (err) {
|
|
227
|
+
clearTimeout(timer);
|
|
228
|
+
if (err instanceof NaijaLingoError) throw err;
|
|
229
|
+
lastError = err;
|
|
230
|
+
const isAbort = err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
|
|
231
|
+
if (isAbort) {
|
|
232
|
+
throw new ConnectionError(`Request timed out: ${String(err)}`);
|
|
233
|
+
}
|
|
234
|
+
if (attempt < 2) {
|
|
235
|
+
await sleep(1500);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
throw new ConnectionError(
|
|
241
|
+
`Unable to connect to ${this.baseUrl}: ${String(lastError)}`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
async postMultipart(path, form, timeoutMs) {
|
|
245
|
+
const resp = await this.request("POST", path, {
|
|
246
|
+
body: form,
|
|
247
|
+
timeoutMs: timeoutMs ?? Math.max(this.timeout, 6e5)
|
|
248
|
+
// Let fetch set multipart boundary — do not set Content-Type
|
|
249
|
+
});
|
|
250
|
+
return new Uint8Array(await resp.arrayBuffer());
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
function sleep(ms) {
|
|
254
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/tts.ts
|
|
258
|
+
var import_promises = require("fs/promises");
|
|
259
|
+
var import_node_path = require("path");
|
|
260
|
+
var import_node_fs = require("fs");
|
|
261
|
+
|
|
262
|
+
// src/types.ts
|
|
263
|
+
function speakerFromDict(data) {
|
|
264
|
+
return {
|
|
265
|
+
id: String(data.id ?? data.speaker_id ?? ""),
|
|
266
|
+
name: String(data.name ?? data.id ?? ""),
|
|
267
|
+
language: String(data.language ?? ""),
|
|
268
|
+
gender: data.gender ?? null,
|
|
269
|
+
domain: data.domain ?? null
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function speakerListFromDict(data) {
|
|
273
|
+
const raw = data.speakers ?? [];
|
|
274
|
+
const speakers = raw.map(speakerFromDict);
|
|
275
|
+
const total = typeof data.total === "number" ? data.total : speakers.length;
|
|
276
|
+
const byLanguage = data.by_language ?? {};
|
|
277
|
+
return {
|
|
278
|
+
speakers,
|
|
279
|
+
total,
|
|
280
|
+
byLanguage,
|
|
281
|
+
get length() {
|
|
282
|
+
return total;
|
|
283
|
+
},
|
|
284
|
+
[Symbol.iterator]() {
|
|
285
|
+
return speakers[Symbol.iterator]();
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function languageFromDict(data) {
|
|
290
|
+
return {
|
|
291
|
+
code: String(data.code ?? ""),
|
|
292
|
+
name: String(data.name ?? ""),
|
|
293
|
+
speakerCount: Number(data.speaker_count ?? 0)
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function languageListFromDict(data) {
|
|
297
|
+
const rawLangs = data.languages;
|
|
298
|
+
let languages;
|
|
299
|
+
if (rawLangs && typeof rawLangs === "object" && !Array.isArray(rawLangs)) {
|
|
300
|
+
languages = Object.entries(rawLangs).map(
|
|
301
|
+
([code, value]) => {
|
|
302
|
+
if (value && typeof value === "object") {
|
|
303
|
+
const v = value;
|
|
304
|
+
return {
|
|
305
|
+
code,
|
|
306
|
+
name: String(v.name ?? code),
|
|
307
|
+
speakerCount: Number(v.speaker_count ?? 0)
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
return { code, name: String(value), speakerCount: 0 };
|
|
311
|
+
}
|
|
312
|
+
);
|
|
313
|
+
} else {
|
|
314
|
+
const arr = rawLangs ?? [];
|
|
315
|
+
languages = arr.map(languageFromDict);
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
languages,
|
|
319
|
+
domains: data.domains ?? []
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function healthStatusFromDict(data) {
|
|
323
|
+
const supported = data.supported_languages;
|
|
324
|
+
let languages = [];
|
|
325
|
+
if (Array.isArray(data.languages)) {
|
|
326
|
+
languages = data.languages;
|
|
327
|
+
} else if (supported && typeof supported === "object") {
|
|
328
|
+
languages = Object.keys(supported);
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
status: String(data.status ?? "unknown"),
|
|
332
|
+
engineReady: Boolean(data.engine_ready ?? data.tts_initialized ?? false),
|
|
333
|
+
codecReady: Boolean(data.codec_ready ?? false),
|
|
334
|
+
totalSpeakers: Number(data.total_speakers ?? data.speakers_loaded ?? 0),
|
|
335
|
+
languages,
|
|
336
|
+
voiceCloningAvailable: Boolean(data.voice_cloning_available ?? false),
|
|
337
|
+
speakerProjectionLoaded: Boolean(data.speaker_projection_loaded ?? false)
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function modelFromDict(data) {
|
|
341
|
+
return {
|
|
342
|
+
id: String(data.id ?? ""),
|
|
343
|
+
object: String(data.object ?? "model"),
|
|
344
|
+
ownedBy: String(data.owned_by ?? "9jalingo")
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function modelListFromDict(data) {
|
|
348
|
+
const raw = data.data ?? [];
|
|
349
|
+
const models = raw.map(modelFromDict);
|
|
350
|
+
return {
|
|
351
|
+
models,
|
|
352
|
+
object: String(data.object ?? "list"),
|
|
353
|
+
get length() {
|
|
354
|
+
return models.length;
|
|
355
|
+
},
|
|
356
|
+
[Symbol.iterator]() {
|
|
357
|
+
return models[Symbol.iterator]();
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function apiInfoFromDict(data) {
|
|
362
|
+
return {
|
|
363
|
+
name: String(data.name ?? ""),
|
|
364
|
+
version: String(data.version ?? ""),
|
|
365
|
+
description: String(data.description ?? ""),
|
|
366
|
+
endpoints: data.endpoints ?? {}
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function serviceInfoFromDict(data) {
|
|
370
|
+
return {
|
|
371
|
+
object: String(data.object ?? "service"),
|
|
372
|
+
name: String(data.name ?? ""),
|
|
373
|
+
modelsUrl: String(data.models_url ?? ""),
|
|
374
|
+
speechUrl: String(data.speech_url ?? ""),
|
|
375
|
+
speechStreamUrl: String(data.speech_stream_url ?? "")
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/voice.ts
|
|
380
|
+
var LANG_ALIASES = {
|
|
381
|
+
yo: "yo",
|
|
382
|
+
yor: "yo",
|
|
383
|
+
yoruba: "yo",
|
|
384
|
+
ig: "ig",
|
|
385
|
+
ibo: "ig",
|
|
386
|
+
igbo: "ig",
|
|
387
|
+
ha: "ha",
|
|
388
|
+
hau: "ha",
|
|
389
|
+
hausa: "ha",
|
|
390
|
+
pcm: "pcm",
|
|
391
|
+
pidgin: "pcm"
|
|
392
|
+
};
|
|
393
|
+
function normalizeLang(code) {
|
|
394
|
+
return LANG_ALIASES[code.trim().toLowerCase()] ?? null;
|
|
395
|
+
}
|
|
396
|
+
function resolveTtsVoiceAndLang(options) {
|
|
397
|
+
let resolvedLang = null;
|
|
398
|
+
if (options.lang != null) {
|
|
399
|
+
resolvedLang = normalizeLang(options.lang);
|
|
400
|
+
if (resolvedLang == null) {
|
|
401
|
+
throw new Error(
|
|
402
|
+
`Unsupported language '${options.lang}'. Use ha, ig, yo, or pcm.`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
} else if (options.language != null) {
|
|
406
|
+
resolvedLang = normalizeLang(options.language);
|
|
407
|
+
if (resolvedLang == null) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
`Unsupported language '${options.language}'. Use ha, ig, yo, or pcm.`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const candidate = options.speaker != null ? options.speaker : options.voice;
|
|
414
|
+
if (candidate == null || !String(candidate).trim()) {
|
|
415
|
+
return [null, resolvedLang];
|
|
416
|
+
}
|
|
417
|
+
const resolved = String(candidate).trim();
|
|
418
|
+
const normalized = resolved.toLowerCase();
|
|
419
|
+
if (normalized in LANG_ALIASES) {
|
|
420
|
+
const langCode = LANG_ALIASES[normalized];
|
|
421
|
+
throw new Error(
|
|
422
|
+
`'${resolved}' is a language code, not a speaker ID. Pass speaker/voice='ada_pcm' (or another speaker ID) and lang='${langCode}' for the language. Browse speakers with: client.tts.listSpeakers({ language: '${langCode}' })`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
if (resolvedLang == null && resolved.includes("_")) {
|
|
426
|
+
const suffix = resolved.split("_").pop();
|
|
427
|
+
resolvedLang = normalizeLang(suffix);
|
|
428
|
+
}
|
|
429
|
+
return [resolved, resolvedLang];
|
|
430
|
+
}
|
|
431
|
+
function resolveCloneLangAndSpeaker(voice, options = {}) {
|
|
432
|
+
let resolvedLang = options.lang != null ? normalizeLang(options.lang) : null;
|
|
433
|
+
let resolvedSpeaker = options.speaker?.trim() || null;
|
|
434
|
+
const voiceStr = (voice || "").trim();
|
|
435
|
+
const voiceNorm = voiceStr.toLowerCase();
|
|
436
|
+
if (voiceNorm in LANG_ALIASES) {
|
|
437
|
+
if (resolvedLang == null) {
|
|
438
|
+
resolvedLang = LANG_ALIASES[voiceNorm];
|
|
439
|
+
}
|
|
440
|
+
} else if (voiceStr.includes("_")) {
|
|
441
|
+
if (resolvedSpeaker == null) {
|
|
442
|
+
resolvedSpeaker = voiceStr;
|
|
443
|
+
}
|
|
444
|
+
if (resolvedLang == null) {
|
|
445
|
+
const suffix = voiceStr.split("_").pop();
|
|
446
|
+
resolvedLang = normalizeLang(suffix);
|
|
447
|
+
}
|
|
448
|
+
} else if (voiceStr && resolvedLang == null) {
|
|
449
|
+
throw new Error(
|
|
450
|
+
`'${voice}' is not a language code (ha/ig/yo/pcm) or a speaker ID with a language suffix (e.g. 'daniel_pcm'). Pass voice='pcm' or lang='pcm'.`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
if (!resolvedLang) {
|
|
454
|
+
throw new Error(
|
|
455
|
+
"Clone requires a language. Pass voice='pcm' (or 'ha'/'ig'/'yo'), lang='pcm', or a speaker ID ending in _pcm/_ha/_ig/_yo."
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
return [resolvedLang, resolvedSpeaker];
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/tts.ts
|
|
462
|
+
var DEFAULT_MODEL_NAME = "9jalingo-tts-1";
|
|
463
|
+
var AudioResponse = class {
|
|
464
|
+
content;
|
|
465
|
+
mediaType;
|
|
466
|
+
constructor(content, mediaType = "audio/wav") {
|
|
467
|
+
this.content = content;
|
|
468
|
+
this.mediaType = mediaType;
|
|
469
|
+
}
|
|
470
|
+
/** Write the audio to a file. */
|
|
471
|
+
async save(path) {
|
|
472
|
+
await (0, import_promises.mkdir)((0, import_node_path.dirname)(path), { recursive: true });
|
|
473
|
+
await (0, import_promises.writeFile)(path, this.content);
|
|
474
|
+
return path;
|
|
475
|
+
}
|
|
476
|
+
get length() {
|
|
477
|
+
return this.content.length;
|
|
478
|
+
}
|
|
479
|
+
toString() {
|
|
480
|
+
return `AudioResponse(${this.content.length} bytes, ${this.mediaType})`;
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
var AudioStream = class {
|
|
484
|
+
chunks;
|
|
485
|
+
collected = null;
|
|
486
|
+
constructor(chunks) {
|
|
487
|
+
this.chunks = chunks;
|
|
488
|
+
}
|
|
489
|
+
async *[Symbol.asyncIterator]() {
|
|
490
|
+
for await (const chunk of this.chunks) {
|
|
491
|
+
yield chunk;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
/** Consume the entire stream and return an AudioResponse. */
|
|
495
|
+
async collect() {
|
|
496
|
+
if (this.collected == null) {
|
|
497
|
+
const parts = [];
|
|
498
|
+
let total = 0;
|
|
499
|
+
for await (const chunk of this.chunks) {
|
|
500
|
+
parts.push(chunk);
|
|
501
|
+
total += chunk.length;
|
|
502
|
+
}
|
|
503
|
+
const out = new Uint8Array(total);
|
|
504
|
+
let offset = 0;
|
|
505
|
+
for (const part of parts) {
|
|
506
|
+
out.set(part, offset);
|
|
507
|
+
offset += part.length;
|
|
508
|
+
}
|
|
509
|
+
this.collected = out;
|
|
510
|
+
}
|
|
511
|
+
return new AudioResponse(this.collected, "audio/wav");
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
var TTS = class {
|
|
515
|
+
client;
|
|
516
|
+
constructor(client) {
|
|
517
|
+
this.client = client;
|
|
518
|
+
}
|
|
519
|
+
/** Generate speech from text. */
|
|
520
|
+
async generate(text, options = {}) {
|
|
521
|
+
let [resolvedVoice, resolvedLang] = resolveTtsVoiceAndLang(options);
|
|
522
|
+
if (resolvedVoice == null && options.speakerEmbedding == null) {
|
|
523
|
+
resolvedVoice = "blessing_pcm";
|
|
524
|
+
}
|
|
525
|
+
const responseFormat = options.responseFormat ?? "wav";
|
|
526
|
+
const body = buildBody({
|
|
527
|
+
input: text,
|
|
528
|
+
voice: resolvedVoice,
|
|
529
|
+
lang: resolvedLang,
|
|
530
|
+
model: options.modelName ?? DEFAULT_MODEL_NAME,
|
|
531
|
+
speaker_embedding: options.speakerEmbedding,
|
|
532
|
+
response_format: responseFormat,
|
|
533
|
+
temperature: options.temperature,
|
|
534
|
+
top_p: options.topP,
|
|
535
|
+
repetition_penalty: options.repetitionPenalty,
|
|
536
|
+
enable_long_form: options.enableLongForm ?? true,
|
|
537
|
+
max_chunk_duration: options.maxChunkDuration,
|
|
538
|
+
silence_duration: options.silenceDuration
|
|
539
|
+
});
|
|
540
|
+
const content = await this.client.postBytes("/v1/audio/speech", body);
|
|
541
|
+
const mediaType = MEDIA_TYPES[responseFormat] ?? "application/octet-stream";
|
|
542
|
+
return new AudioResponse(content, mediaType);
|
|
543
|
+
}
|
|
544
|
+
/** Stream speech generation — audio arrives as it's produced. */
|
|
545
|
+
stream(text, options = {}) {
|
|
546
|
+
let [resolvedVoice, resolvedLang] = resolveTtsVoiceAndLang(options);
|
|
547
|
+
if (resolvedVoice == null && options.speakerEmbedding == null) {
|
|
548
|
+
resolvedVoice = "blessing_pcm";
|
|
549
|
+
}
|
|
550
|
+
const body = buildBody({
|
|
551
|
+
input: text,
|
|
552
|
+
voice: resolvedVoice,
|
|
553
|
+
lang: resolvedLang,
|
|
554
|
+
speaker_embedding: options.speakerEmbedding,
|
|
555
|
+
response_format: "wav",
|
|
556
|
+
temperature: options.temperature,
|
|
557
|
+
top_p: options.topP,
|
|
558
|
+
repetition_penalty: options.repetitionPenalty,
|
|
559
|
+
enable_long_form: options.enableLongForm ?? true,
|
|
560
|
+
max_chunk_duration: options.maxChunkDuration,
|
|
561
|
+
silence_duration: options.silenceDuration
|
|
562
|
+
});
|
|
563
|
+
return new AudioStream(this.client.postStream("/v1/audio/speech/stream", body));
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Generate speech using a cloned voice from a reference audio file.
|
|
567
|
+
* `audioFile` may be a filesystem path, Buffer, Uint8Array, Blob, or File.
|
|
568
|
+
*/
|
|
569
|
+
async clone(text, audioFile, options = {}) {
|
|
570
|
+
const voice = options.voice ?? "pcm";
|
|
571
|
+
const [resolvedLang, resolvedSpeaker] = resolveCloneLangAndSpeaker(
|
|
572
|
+
voice,
|
|
573
|
+
{ lang: options.lang, speaker: options.speaker }
|
|
574
|
+
);
|
|
575
|
+
const responseFormat = options.responseFormat ?? "wav";
|
|
576
|
+
const form = new FormData();
|
|
577
|
+
form.append("text", text);
|
|
578
|
+
form.append("lang", resolvedLang);
|
|
579
|
+
form.append("model_name", options.modelName ?? DEFAULT_MODEL_NAME);
|
|
580
|
+
form.append("response_format", responseFormat);
|
|
581
|
+
if (resolvedSpeaker) {
|
|
582
|
+
form.append("voice", resolvedSpeaker);
|
|
583
|
+
}
|
|
584
|
+
if (options.temperature != null) {
|
|
585
|
+
form.append("temperature", String(options.temperature));
|
|
586
|
+
}
|
|
587
|
+
if (options.topP != null) {
|
|
588
|
+
form.append("top_p", String(options.topP));
|
|
589
|
+
}
|
|
590
|
+
if (options.repetitionPenalty != null) {
|
|
591
|
+
form.append("repetition_penalty", String(options.repetitionPenalty));
|
|
592
|
+
}
|
|
593
|
+
const { blob, filename } = await toAudioBlob(audioFile);
|
|
594
|
+
form.append("audio", blob, filename);
|
|
595
|
+
const content = await this.client.postMultipart("/v1/audio/clone", form);
|
|
596
|
+
const mediaType = MEDIA_TYPES[responseFormat] ?? "application/octet-stream";
|
|
597
|
+
return new AudioResponse(content, mediaType);
|
|
598
|
+
}
|
|
599
|
+
async listSpeakers(options = {}) {
|
|
600
|
+
const data = await this.client.getJson("/v1/speakers", {
|
|
601
|
+
language: options.language,
|
|
602
|
+
gender: options.gender,
|
|
603
|
+
domain: options.domain
|
|
604
|
+
});
|
|
605
|
+
return speakerListFromDict(data);
|
|
606
|
+
}
|
|
607
|
+
async getSpeaker(speakerId) {
|
|
608
|
+
const data = await this.client.getJson(`/v1/speakers/${speakerId}`);
|
|
609
|
+
return speakerFromDict(data);
|
|
610
|
+
}
|
|
611
|
+
async listLanguages() {
|
|
612
|
+
const data = await this.client.getJson("/v1/languages");
|
|
613
|
+
return languageListFromDict(data);
|
|
614
|
+
}
|
|
615
|
+
async health() {
|
|
616
|
+
const data = await this.client.getJson("/v1/health");
|
|
617
|
+
return healthStatusFromDict(data);
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
var MEDIA_TYPES = {
|
|
621
|
+
wav: "audio/wav",
|
|
622
|
+
pcm: "application/octet-stream",
|
|
623
|
+
flac: "audio/flac",
|
|
624
|
+
aac: "audio/aac",
|
|
625
|
+
ogg: "audio/ogg",
|
|
626
|
+
mp3: "audio/mpeg",
|
|
627
|
+
alac: "audio/alac"
|
|
628
|
+
};
|
|
629
|
+
function buildBody(kwargs) {
|
|
630
|
+
const out = {};
|
|
631
|
+
for (const [k, v] of Object.entries(kwargs)) {
|
|
632
|
+
if (v !== void 0 && v !== null) out[k] = v;
|
|
633
|
+
}
|
|
634
|
+
return out;
|
|
635
|
+
}
|
|
636
|
+
async function toAudioBlob(audioFile) {
|
|
637
|
+
if (typeof audioFile === "string") {
|
|
638
|
+
const bytes2 = (0, import_node_fs.readFileSync)(audioFile);
|
|
639
|
+
const filename = (0, import_node_path.basename)(audioFile);
|
|
640
|
+
const mime = guessMime(filename);
|
|
641
|
+
return {
|
|
642
|
+
blob: new Blob([bytes2], { type: mime }),
|
|
643
|
+
filename
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
if (audioFile instanceof Blob) {
|
|
647
|
+
return {
|
|
648
|
+
blob: audioFile,
|
|
649
|
+
filename: "audio.wav"
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
const bytes = audioFile instanceof Uint8Array ? audioFile : new Uint8Array(audioFile);
|
|
653
|
+
return {
|
|
654
|
+
blob: new Blob([bytes], { type: "application/octet-stream" }),
|
|
655
|
+
filename: "audio.wav"
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
function guessMime(filename) {
|
|
659
|
+
const lower = filename.toLowerCase();
|
|
660
|
+
if (lower.endsWith(".wav")) return "audio/wav";
|
|
661
|
+
if (lower.endsWith(".mp3")) return "audio/mpeg";
|
|
662
|
+
if (lower.endsWith(".flac")) return "audio/flac";
|
|
663
|
+
if (lower.endsWith(".ogg")) return "audio/ogg";
|
|
664
|
+
if (lower.endsWith(".aac") || lower.endsWith(".m4a")) return "audio/aac";
|
|
665
|
+
return "application/octet-stream";
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// src/index.ts
|
|
669
|
+
var VERSION = "0.1.0";
|
|
670
|
+
var NaijaLingo = class {
|
|
671
|
+
_client;
|
|
672
|
+
tts;
|
|
673
|
+
constructor(options = {}) {
|
|
674
|
+
this._client = new BaseClient(options);
|
|
675
|
+
this.tts = new TTS(this._client);
|
|
676
|
+
}
|
|
677
|
+
/** List available TTS models. GET /v1/models */
|
|
678
|
+
async listModels() {
|
|
679
|
+
const data = await this._client.getJson("/v1/models");
|
|
680
|
+
return modelListFromDict(data);
|
|
681
|
+
}
|
|
682
|
+
/** Root API information. GET / */
|
|
683
|
+
async apiInfo() {
|
|
684
|
+
const data = await this._client.getJson("/");
|
|
685
|
+
return apiInfoFromDict(data);
|
|
686
|
+
}
|
|
687
|
+
/** v1 service metadata. GET /v1 */
|
|
688
|
+
async serviceInfo() {
|
|
689
|
+
const data = await this._client.getJson("/v1");
|
|
690
|
+
return serviceInfoFromDict(data);
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
694
|
+
0 && (module.exports = {
|
|
695
|
+
AudioResponse,
|
|
696
|
+
AudioStream,
|
|
697
|
+
AuthenticationError,
|
|
698
|
+
ConnectionError,
|
|
699
|
+
NaijaLingo,
|
|
700
|
+
NaijaLingoError,
|
|
701
|
+
NotFoundError,
|
|
702
|
+
RateLimitError,
|
|
703
|
+
ServerError,
|
|
704
|
+
TTS,
|
|
705
|
+
VERSION
|
|
706
|
+
});
|
|
707
|
+
//# sourceMappingURL=index.cjs.map
|