@upliftai/sdk-js 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/dist/index.js ADDED
@@ -0,0 +1,865 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ UpliftAI: () => UpliftAI,
34
+ UpliftAIAuthError: () => UpliftAIAuthError,
35
+ UpliftAIError: () => UpliftAIError,
36
+ UpliftAIInsufficientBalanceError: () => UpliftAIInsufficientBalanceError,
37
+ UpliftAIRateLimitError: () => UpliftAIRateLimitError,
38
+ default: () => index_default
39
+ });
40
+ module.exports = __toCommonJS(index_exports);
41
+
42
+ // src/errors.ts
43
+ var UpliftAIError = class extends Error {
44
+ constructor(message, statusCode, code, requestId) {
45
+ super(message);
46
+ this.statusCode = statusCode;
47
+ this.code = code;
48
+ this.requestId = requestId;
49
+ this.name = "UpliftAIError";
50
+ }
51
+ };
52
+ var UpliftAIAuthError = class extends UpliftAIError {
53
+ constructor(message = "Authentication failed. Check your API key.", requestId) {
54
+ super(message, 401, "auth_error", requestId);
55
+ this.name = "UpliftAIAuthError";
56
+ }
57
+ };
58
+ var UpliftAIInsufficientBalanceError = class extends UpliftAIError {
59
+ constructor(message = "Insufficient balance. Please add credits to your account.", requestId) {
60
+ super(message, 402, "insufficient_balance", requestId);
61
+ this.name = "UpliftAIInsufficientBalanceError";
62
+ }
63
+ };
64
+ var UpliftAIRateLimitError = class extends UpliftAIError {
65
+ constructor(message = "Rate limit exceeded.", requestId) {
66
+ super(message, 429, "rate_limited", requestId);
67
+ this.name = "UpliftAIRateLimitError";
68
+ }
69
+ };
70
+
71
+ // src/http.ts
72
+ var SDK_VERSION = "0.1.0";
73
+ var DEFAULT_TIMEOUT = 3e4;
74
+ var DEFAULT_MAX_RETRIES = 2;
75
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
76
+ var HttpClient = class {
77
+ baseUrl;
78
+ apiKey;
79
+ timeout;
80
+ maxRetries;
81
+ constructor(options) {
82
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
83
+ this.apiKey = options.apiKey;
84
+ this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
85
+ this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
86
+ }
87
+ headers(extra) {
88
+ return {
89
+ Authorization: `Bearer ${this.apiKey}`,
90
+ "User-Agent": `upliftai-js/${SDK_VERSION}`,
91
+ Connection: "keep-alive",
92
+ ...extra
93
+ };
94
+ }
95
+ async fetchWithRetry(url, init, retriesLeft = this.maxRetries) {
96
+ const controller = new AbortController();
97
+ const timer = setTimeout(() => controller.abort(), this.timeout);
98
+ try {
99
+ const res = await fetch(url, { ...init, signal: controller.signal });
100
+ if (!res.ok && retriesLeft > 0 && RETRYABLE_STATUS_CODES.has(res.status)) {
101
+ const delay = this.retryDelay(this.maxRetries - retriesLeft);
102
+ await sleep(delay);
103
+ return this.fetchWithRetry(url, init, retriesLeft - 1);
104
+ }
105
+ return res;
106
+ } catch (err) {
107
+ if (err instanceof DOMException && err.name === "AbortError") {
108
+ throw new UpliftAIError("Request timed out", void 0, "timeout");
109
+ }
110
+ if (retriesLeft > 0) {
111
+ const delay = this.retryDelay(this.maxRetries - retriesLeft);
112
+ await sleep(delay);
113
+ return this.fetchWithRetry(url, init, retriesLeft - 1);
114
+ }
115
+ throw err;
116
+ } finally {
117
+ clearTimeout(timer);
118
+ }
119
+ }
120
+ retryDelay(attempt) {
121
+ const base = Math.min(500 * Math.pow(2, attempt), 5e3);
122
+ const jitter = base * 0.25 * Math.random();
123
+ return base + jitter;
124
+ }
125
+ async postJSON(path, body) {
126
+ const url = `${this.baseUrl}${path}`;
127
+ const res = await this.fetchWithRetry(url, {
128
+ method: "POST",
129
+ headers: this.headers({ "Content-Type": "application/json" }),
130
+ body: JSON.stringify(body)
131
+ });
132
+ if (!res.ok) {
133
+ await this.throwForStatus(res);
134
+ }
135
+ const data = await res.json();
136
+ return { data, headers: res.headers };
137
+ }
138
+ async postJSONForBuffer(path, body) {
139
+ const url = `${this.baseUrl}${path}`;
140
+ const res = await this.fetchWithRetry(url, {
141
+ method: "POST",
142
+ headers: this.headers({ "Content-Type": "application/json" }),
143
+ body: JSON.stringify(body)
144
+ });
145
+ if (!res.ok) {
146
+ await this.throwForStatus(res);
147
+ }
148
+ const buffer = Buffer.from(await res.arrayBuffer());
149
+ return { buffer, headers: res.headers };
150
+ }
151
+ async postJSONForStream(path, body) {
152
+ const url = `${this.baseUrl}${path}`;
153
+ const res = await this.fetchWithRetry(url, {
154
+ method: "POST",
155
+ headers: this.headers({ "Content-Type": "application/json" }),
156
+ body: JSON.stringify(body)
157
+ });
158
+ if (!res.ok) {
159
+ await this.throwForStatus(res);
160
+ }
161
+ if (!res.body) {
162
+ throw new UpliftAIError("Response body is null");
163
+ }
164
+ return { body: res.body, headers: res.headers };
165
+ }
166
+ async postMultipart(path, formData) {
167
+ const url = `${this.baseUrl}${path}`;
168
+ const res = await this.fetchWithRetry(url, {
169
+ method: "POST",
170
+ headers: this.headers(),
171
+ body: formData
172
+ });
173
+ if (!res.ok) {
174
+ await this.throwForStatus(res);
175
+ }
176
+ const data = await res.json();
177
+ return { data, headers: res.headers };
178
+ }
179
+ async get(path) {
180
+ const url = `${this.baseUrl}${path}`;
181
+ const res = await this.fetchWithRetry(url, {
182
+ method: "GET",
183
+ headers: this.headers()
184
+ });
185
+ if (!res.ok) {
186
+ await this.throwForStatus(res);
187
+ }
188
+ const data = await res.json();
189
+ return { data, headers: res.headers };
190
+ }
191
+ async getStream(path, query) {
192
+ const url = new URL(`${this.baseUrl}${path}`);
193
+ if (query) {
194
+ for (const [k, v] of Object.entries(query)) {
195
+ url.searchParams.set(k, v);
196
+ }
197
+ }
198
+ const res = await this.fetchWithRetry(url.toString(), {
199
+ method: "GET",
200
+ headers: this.headers()
201
+ });
202
+ if (!res.ok) {
203
+ await this.throwForStatus(res);
204
+ }
205
+ if (!res.body) {
206
+ throw new UpliftAIError("Response body is null");
207
+ }
208
+ return { body: res.body, headers: res.headers };
209
+ }
210
+ async throwForStatus(res) {
211
+ const requestId = res.headers.get("x-uplift-ai-request-id") ?? void 0;
212
+ const body = await this.safeText(res);
213
+ if (res.status === 401) throw new UpliftAIAuthError(void 0, requestId);
214
+ if (res.status === 402) throw new UpliftAIInsufficientBalanceError(void 0, requestId);
215
+ if (res.status === 429) throw new UpliftAIRateLimitError(void 0, requestId);
216
+ throw new UpliftAIError(`HTTP ${res.status}: ${body}`, res.status, void 0, requestId);
217
+ }
218
+ async safeText(res) {
219
+ try {
220
+ return await res.text();
221
+ } catch {
222
+ return "";
223
+ }
224
+ }
225
+ };
226
+ function sleep(ms) {
227
+ return new Promise((resolve) => setTimeout(resolve, ms));
228
+ }
229
+
230
+ // src/tts.ts
231
+ var import_node_stream = require("stream");
232
+
233
+ // src/phrase-replacements.ts
234
+ var PhraseReplacements = class {
235
+ http;
236
+ constructor(http) {
237
+ this.http = http;
238
+ }
239
+ async create(replacements) {
240
+ const { data } = await this.http.postJSON(
241
+ "/v1/synthesis/phrase-replacement-config",
242
+ { phraseReplacements: replacements }
243
+ );
244
+ return data;
245
+ }
246
+ async get(configId) {
247
+ const { data } = await this.http.get(
248
+ `/v1/synthesis/phrase-replacement-config/${configId}`
249
+ );
250
+ return data;
251
+ }
252
+ async list() {
253
+ const { data } = await this.http.get(
254
+ "/v1/synthesis/phrase-replacement-config"
255
+ );
256
+ return data;
257
+ }
258
+ async update(configId, replacements) {
259
+ const { data } = await this.http.postJSON(
260
+ `/v1/synthesis/phrase-replacement-config/${configId}`,
261
+ { phraseReplacements: replacements }
262
+ );
263
+ return data;
264
+ }
265
+ };
266
+
267
+ // src/ws.ts
268
+ var import_node_crypto = require("crypto");
269
+ var import_ws = __toESM(require("ws"));
270
+ var TTSWebSocketImpl = class {
271
+ ws;
272
+ _sessionId = "";
273
+ streams = /* @__PURE__ */ new Map();
274
+ readyPromise;
275
+ readyResolve;
276
+ readyReject;
277
+ listeners = {
278
+ error: [],
279
+ close: []
280
+ };
281
+ constructor(url, apiKey) {
282
+ this.readyPromise = new Promise((resolve, reject) => {
283
+ this.readyResolve = resolve;
284
+ this.readyReject = reject;
285
+ });
286
+ this.ws = new import_ws.default(url, {
287
+ headers: { Authorization: `Bearer ${apiKey}` }
288
+ });
289
+ this.ws.on("message", (raw) => {
290
+ this.handleMessage(raw);
291
+ });
292
+ this.ws.on("error", (err) => {
293
+ this.readyReject(err);
294
+ for (const listener of this.listeners.error) {
295
+ try {
296
+ listener(err);
297
+ } catch (e) {
298
+ process.emitWarning(`Error in listener callback: ${e}`, "UpliftAI");
299
+ }
300
+ }
301
+ for (const stream of this.streams.values()) stream.error(err);
302
+ });
303
+ this.ws.on("close", (code, reason) => {
304
+ const reasonStr = reason.toString();
305
+ for (const listener of this.listeners.close) {
306
+ try {
307
+ listener(code, reasonStr);
308
+ } catch (e) {
309
+ process.emitWarning(`Error in listener callback: ${e}`, "UpliftAI");
310
+ }
311
+ }
312
+ for (const stream of this.streams.values()) {
313
+ stream.error(new UpliftAIError(`WebSocket closed: ${code} ${reasonStr}`));
314
+ }
315
+ this.streams.clear();
316
+ });
317
+ }
318
+ /** @internal Wait for the server `ready` message. Called by `tts.connect()`. */
319
+ async waitForReady() {
320
+ return this.readyPromise;
321
+ }
322
+ /** Current connection state. */
323
+ get readyState() {
324
+ switch (this.ws.readyState) {
325
+ case import_ws.default.CONNECTING:
326
+ return "connecting";
327
+ case import_ws.default.OPEN:
328
+ return "open";
329
+ case import_ws.default.CLOSING:
330
+ return "closing";
331
+ case import_ws.default.CLOSED:
332
+ return "closed";
333
+ default:
334
+ return "closed";
335
+ }
336
+ }
337
+ /** Server-assigned session ID, available after connection is ready. */
338
+ get sessionId() {
339
+ return this._sessionId;
340
+ }
341
+ on(event, listener) {
342
+ if (event === "error") {
343
+ this.listeners.error.push(listener);
344
+ } else {
345
+ this.listeners.close.push(listener);
346
+ }
347
+ return this;
348
+ }
349
+ /**
350
+ * Start a TTS stream. Sends text to the server and returns an async iterable
351
+ * of audio events (`audio_start`, `audio`, `audio_end`, `error`).
352
+ *
353
+ * Multiple streams can run concurrently on the same connection — each is
354
+ * demuxed by its `requestId`. For real-time conversational AI, break your
355
+ * LLM output into sentence-sized chunks and stream each one as it arrives.
356
+ * This gives the lowest time-to-first-audio since synthesis starts before
357
+ * the LLM finishes generating. If you use LiveKit, the UpliftAI plugin
358
+ * handles this sentence segmentation automatically.
359
+ *
360
+ * **Connection scope:** Use one WebSocket per conversation / user session.
361
+ * Don't multiplex unrelated use cases on a single connection — we may use
362
+ * connection-level context for prosody and other improvements in the future.
363
+ *
364
+ * @example // Simple usage
365
+ * const stream = ws.stream({ text: 'سلام', voiceId: 'v_meklc281' });
366
+ * for await (const event of stream) {
367
+ * if (event.type === 'audio') speaker.write(event.audio);
368
+ * }
369
+ *
370
+ * @example // Real-time: stream sentence-by-sentence as LLM generates
371
+ * for await (const sentence of llm.streamSentences(prompt)) {
372
+ * const stream = ws.stream({ text: sentence, voiceId });
373
+ * for await (const event of stream) {
374
+ * if (event.type === 'audio') speaker.write(event.audio);
375
+ * }
376
+ * }
377
+ *
378
+ * @example // Overlap: fire next sentence before previous finishes
379
+ * const sentences = ['پہلا جملہ۔', 'دوسرا جملہ۔', 'تیسرا جملہ۔'];
380
+ * for (const sentence of sentences) {
381
+ * const stream = ws.stream({ text: sentence, voiceId });
382
+ * consume(stream); // don't await — let them overlap
383
+ * }
384
+ */
385
+ stream(request) {
386
+ const requestId = request.requestId ?? (0, import_node_crypto.randomUUID)();
387
+ const buffer = [];
388
+ let resolve = null;
389
+ let done = false;
390
+ let error = null;
391
+ const cleanup = () => {
392
+ this.streams.delete(requestId);
393
+ buffer.length = 0;
394
+ resolve = null;
395
+ };
396
+ const pending = {
397
+ push(event) {
398
+ if (done) return;
399
+ if (resolve) {
400
+ const r = resolve;
401
+ resolve = null;
402
+ r({ value: event, done: false });
403
+ } else {
404
+ buffer.push(event);
405
+ }
406
+ },
407
+ end() {
408
+ if (done) return;
409
+ done = true;
410
+ if (resolve) {
411
+ const r = resolve;
412
+ resolve = null;
413
+ r({ value: void 0, done: true });
414
+ }
415
+ },
416
+ error(err) {
417
+ if (done) return;
418
+ error = err;
419
+ done = true;
420
+ if (resolve) {
421
+ const r = resolve;
422
+ resolve = null;
423
+ r({ value: void 0, done: true });
424
+ }
425
+ }
426
+ };
427
+ this.streams.set(requestId, pending);
428
+ this.safeSend({
429
+ type: "synthesize",
430
+ requestId,
431
+ text: request.text,
432
+ voiceId: request.voiceId,
433
+ outputFormat: request.outputFormat ?? "PCM_22050_16"
434
+ });
435
+ const iterator = {
436
+ next() {
437
+ if (error || done) {
438
+ if (buffer.length > 0) {
439
+ return Promise.resolve({ value: buffer.shift(), done: false });
440
+ }
441
+ cleanup();
442
+ return Promise.resolve({ value: void 0, done: true });
443
+ }
444
+ if (buffer.length > 0) {
445
+ return Promise.resolve({ value: buffer.shift(), done: false });
446
+ }
447
+ return new Promise((r) => {
448
+ resolve = r;
449
+ });
450
+ },
451
+ return() {
452
+ done = true;
453
+ cleanup();
454
+ return Promise.resolve({ value: void 0, done: true });
455
+ },
456
+ [Symbol.asyncIterator]() {
457
+ return this;
458
+ }
459
+ };
460
+ const stream = {
461
+ requestId,
462
+ /** Cancel this stream. Tells the server to stop generating and ends the iterator. */
463
+ cancel: async () => {
464
+ this.safeSend({ type: "cancel", requestId });
465
+ cleanup();
466
+ pending.end();
467
+ },
468
+ [Symbol.asyncIterator]() {
469
+ return iterator;
470
+ }
471
+ };
472
+ return stream;
473
+ }
474
+ /**
475
+ * Cancel all in-flight streams. Use for barge-in / interruption — when the
476
+ * user starts speaking and you need to immediately stop all TTS playback.
477
+ *
478
+ * Sends a cancel message to the server for each active stream and ends
479
+ * all iterators synchronously.
480
+ *
481
+ * @example
482
+ * ws.stream({ text: 'sentence 1...', voiceId });
483
+ * ws.stream({ text: 'sentence 2...', voiceId });
484
+ * // User interrupts!
485
+ * ws.cancelAll(); // both streams end immediately
486
+ */
487
+ cancelAll() {
488
+ for (const [requestId, stream] of this.streams) {
489
+ this.safeSend({ type: "cancel", requestId });
490
+ stream.end();
491
+ }
492
+ this.streams.clear();
493
+ }
494
+ /** Number of streams currently in-flight on this connection. */
495
+ get activeStreams() {
496
+ return this.streams.size;
497
+ }
498
+ /** Close the WebSocket connection. */
499
+ close() {
500
+ this.ws.close();
501
+ }
502
+ safeSend(msg) {
503
+ try {
504
+ this.ws.send(JSON.stringify(msg));
505
+ } catch (err) {
506
+ const error = err instanceof Error ? err : new Error(String(err));
507
+ for (const stream of this.streams.values()) {
508
+ stream.error(new UpliftAIError(`WebSocket send failed: ${error.message}`));
509
+ }
510
+ }
511
+ }
512
+ handleMessage(raw) {
513
+ let msg;
514
+ try {
515
+ msg = JSON.parse(raw.toString());
516
+ } catch {
517
+ return;
518
+ }
519
+ const type = msg.type;
520
+ if (type === "ready") {
521
+ this._sessionId = msg.sessionId;
522
+ this.readyResolve();
523
+ return;
524
+ }
525
+ const requestId = msg.requestId;
526
+ if (!requestId) return;
527
+ const stream = this.streams.get(requestId);
528
+ if (!stream) return;
529
+ switch (type) {
530
+ case "audio_start":
531
+ stream.push({
532
+ type: "audio_start",
533
+ requestId,
534
+ timestamp: msg.timestamp
535
+ });
536
+ break;
537
+ case "audio": {
538
+ let audio;
539
+ try {
540
+ audio = Buffer.from(msg.audio, "base64");
541
+ } catch {
542
+ stream.push({
543
+ type: "error",
544
+ requestId,
545
+ code: "decode_error",
546
+ message: "Failed to decode base64 audio data"
547
+ });
548
+ stream.end();
549
+ this.streams.delete(requestId);
550
+ return;
551
+ }
552
+ stream.push({
553
+ type: "audio",
554
+ requestId,
555
+ sequence: msg.sequence,
556
+ audio
557
+ });
558
+ break;
559
+ }
560
+ case "audio_end":
561
+ stream.push({
562
+ type: "audio_end",
563
+ requestId,
564
+ timestamp: msg.timestamp
565
+ });
566
+ stream.end();
567
+ this.streams.delete(requestId);
568
+ break;
569
+ case "error":
570
+ stream.push({
571
+ type: "error",
572
+ requestId,
573
+ code: msg.code,
574
+ message: msg.message
575
+ });
576
+ stream.end();
577
+ this.streams.delete(requestId);
578
+ break;
579
+ }
580
+ }
581
+ };
582
+
583
+ // src/tts.ts
584
+ var DEFAULT_OUTPUT_FORMAT = "WAV_22050_32";
585
+ function buildTTSBody(request) {
586
+ const body = {
587
+ text: request.text,
588
+ voiceId: request.voiceId,
589
+ outputFormat: request.outputFormat ?? DEFAULT_OUTPUT_FORMAT
590
+ };
591
+ if (request.phraseReplacementConfigId) body.phraseReplacementConfigId = request.phraseReplacementConfigId;
592
+ return body;
593
+ }
594
+ function parseAudioMetadata(headers) {
595
+ return {
596
+ requestId: headers.get("x-uplift-ai-request-id") ?? "",
597
+ duration: Number(headers.get("x-uplift-ai-audio-duration") ?? 0),
598
+ contentType: headers.get("content-type") ?? "application/octet-stream",
599
+ sampleRate: Number(headers.get("x-uplift-ai-sample-rate") ?? 0),
600
+ bitRate: Number(headers.get("x-uplift-ai-bit-rate") ?? 0)
601
+ };
602
+ }
603
+ function readableFromWeb(webStream) {
604
+ const reader = webStream.getReader();
605
+ return new import_node_stream.Readable({
606
+ async read() {
607
+ const { done, value } = await reader.read();
608
+ if (done) {
609
+ this.push(null);
610
+ } else {
611
+ this.push(Buffer.from(value));
612
+ }
613
+ },
614
+ destroy(_err, callback) {
615
+ reader.cancel().then(() => callback(null), callback);
616
+ }
617
+ });
618
+ }
619
+ var TTS = class {
620
+ http;
621
+ apiKey;
622
+ baseUrl;
623
+ wsBaseUrl;
624
+ /** Manage phrase replacement configs for pronunciation control. */
625
+ phraseReplacements;
626
+ constructor(http, apiKey, baseUrl, wsBaseUrl) {
627
+ this.http = http;
628
+ this.apiKey = apiKey;
629
+ this.baseUrl = baseUrl;
630
+ this.wsBaseUrl = wsBaseUrl;
631
+ this.phraseReplacements = new PhraseReplacements(http);
632
+ }
633
+ /**
634
+ * Synthesize text and return the full audio buffer.
635
+ *
636
+ * Generates the complete audio before returning. Faster end-to-end than
637
+ * streaming, but the caller must wait for the entire file. Best for
638
+ * batch/offline use cases where latency to first byte doesn't matter.
639
+ *
640
+ * @example
641
+ * const { audio, metadata } = await client.tts.create({ text: 'سلام', voiceId: 'v_meklc281' });
642
+ * fs.writeFileSync('output.mp3', audio);
643
+ */
644
+ async create(request) {
645
+ const { buffer, headers } = await this.http.postJSONForBuffer(
646
+ "/v1/synthesis/text-to-speech",
647
+ buildTTSBody(request)
648
+ );
649
+ return {
650
+ audio: buffer,
651
+ metadata: parseAudioMetadata(headers)
652
+ };
653
+ }
654
+ /**
655
+ * Synthesize text and return a readable stream of audio chunks.
656
+ *
657
+ * The first chunk arrives quickly, but total generation is slower than
658
+ * `create()`. Use this in latency-sensitive environments like live agents,
659
+ * phone calls, or real-time playback where you want audio to start playing
660
+ * immediately rather than waiting for the full file.
661
+ *
662
+ * @example
663
+ * const { stream, metadata } = await client.tts.createStream({ text: 'سلام', voiceId: 'v_meklc281' });
664
+ * for await (const chunk of stream) speaker.write(chunk);
665
+ */
666
+ async createStream(request) {
667
+ const { body, headers } = await this.http.postJSONForStream(
668
+ "/v1/synthesis/text-to-speech/stream",
669
+ buildTTSBody(request)
670
+ );
671
+ return {
672
+ stream: readableFromWeb(body),
673
+ metadata: parseAudioMetadata(headers)
674
+ };
675
+ }
676
+ /**
677
+ * Enqueue an async TTS job. Returns a `mediaId` to retrieve the audio later.
678
+ *
679
+ * Use for batch processing or when you don't need audio immediately.
680
+ * Poll or call `retrieve(mediaId)` when the audio is ready.
681
+ *
682
+ * @example
683
+ * const { mediaId, temporaryUrl } = await client.tts.enqueue({ text: 'سلام', voiceId: 'v_meklc281' });
684
+ * // retrieve server-side
685
+ * const audio = await client.tts.retrieve(mediaId);
686
+ * // or pass URL directly to a client/browser
687
+ * console.log(temporaryUrl);
688
+ */
689
+ async enqueue(request) {
690
+ const { data } = await this.http.postJSON(
691
+ "/v1/synthesis/text-to-speech-async",
692
+ buildTTSBody(request)
693
+ );
694
+ return {
695
+ ...data,
696
+ temporaryUrl: this.buildTemporaryUrl(data.mediaId, data.token)
697
+ };
698
+ }
699
+ /**
700
+ * Enqueue an async TTS job with streaming retrieval.
701
+ *
702
+ * Same as `enqueue()`, but when retrieved via `retrieve(mediaId)` the audio
703
+ * streams in chunks instead of arriving as a single buffer.
704
+ *
705
+ * @example
706
+ * const { mediaId, temporaryUrl } = await client.tts.enqueueStream({ text: 'سلام', voiceId: 'v_meklc281' });
707
+ * const stream = await client.tts.retrieve(mediaId);
708
+ * for await (const chunk of stream) speaker.write(chunk);
709
+ */
710
+ async enqueueStream(request) {
711
+ const { data } = await this.http.postJSON(
712
+ "/v1/synthesis/text-to-speech/stream-async",
713
+ buildTTSBody(request)
714
+ );
715
+ return {
716
+ ...data,
717
+ temporaryUrl: this.buildTemporaryUrl(data.mediaId, data.token)
718
+ };
719
+ }
720
+ /**
721
+ * Retrieve audio from a previously enqueued job.
722
+ *
723
+ * Returns the audio stream along with metadata (encoding, sample rate, etc.)
724
+ * from response headers.
725
+ *
726
+ * @example
727
+ * const { stream, metadata } = await client.tts.retrieve('<mediaId from enqueue>');
728
+ * console.log(metadata.contentType); // 'audio/mpeg'
729
+ * for await (const chunk of stream) fs.appendFileSync('out.mp3', chunk);
730
+ */
731
+ async retrieve(mediaId) {
732
+ const { body, headers } = await this.http.getStream(`/v1/synthesis/stream-audio/${mediaId}`);
733
+ return {
734
+ stream: readableFromWeb(body),
735
+ metadata: parseAudioMetadata(headers)
736
+ };
737
+ }
738
+ /**
739
+ * Open a persistent WebSocket connection for low-latency streaming TTS.
740
+ *
741
+ * Supports multiple concurrent streams on one connection, multiplexed by
742
+ * requestId. Use for real-time conversational AI, live agents, and
743
+ * interactive use cases. Resolves once the connection is ready.
744
+ *
745
+ * Open one connection per conversation or user session — don't share across
746
+ * unrelated contexts.
747
+ *
748
+ * @example
749
+ * const ws = await client.tts.connect();
750
+ * // Stream sentence-by-sentence as your LLM generates
751
+ * for await (const sentence of llm.streamSentences(prompt)) {
752
+ * const stream = ws.stream({ text: sentence, voiceId: 'v_meklc281' });
753
+ * for await (const event of stream) {
754
+ * if (event.type === 'audio') speaker.write(event.audio);
755
+ * }
756
+ * }
757
+ * ws.close();
758
+ */
759
+ buildTemporaryUrl(mediaId, token) {
760
+ return `${this.baseUrl}/v1/synthesis/stream-audio/${mediaId}?token=${encodeURIComponent(token)}`;
761
+ }
762
+ async connect() {
763
+ const wsUrl = `${this.wsBaseUrl}/v1/text-to-speech/multi-stream`;
764
+ const ws = new TTSWebSocketImpl(wsUrl, this.apiKey);
765
+ await ws.waitForReady();
766
+ return ws;
767
+ }
768
+ };
769
+
770
+ // src/stt.ts
771
+ var import_node_fs = require("fs");
772
+ var import_node_path = require("path");
773
+ async function streamToBuffer(stream) {
774
+ const chunks = [];
775
+ for await (const chunk of stream) {
776
+ if (Buffer.isBuffer(chunk)) {
777
+ chunks.push(chunk);
778
+ } else if (chunk instanceof Uint8Array) {
779
+ chunks.push(Buffer.from(chunk));
780
+ } else if (typeof chunk === "string") {
781
+ chunks.push(Buffer.from(chunk));
782
+ } else {
783
+ throw new UpliftAIError("Unexpected chunk type in audio stream");
784
+ }
785
+ }
786
+ return Buffer.concat(chunks);
787
+ }
788
+ var STT = class {
789
+ http;
790
+ constructor(http) {
791
+ this.http = http;
792
+ }
793
+ /**
794
+ * Transcribe audio to text.
795
+ *
796
+ * Accepts a file path, Buffer, or readable stream as input.
797
+ *
798
+ * @example
799
+ * // From file path (extension used for content-type detection)
800
+ * const { transcript } = await client.stt.transcribe({ file: './call.mp3', model: 'scribe' });
801
+ *
802
+ * // From Buffer (pass fileName so the server knows the format)
803
+ * const { transcript } = await client.stt.transcribe({ file: audioBuffer, fileName: 'call.mp3', language: 'ur' });
804
+ */
805
+ async transcribe(request) {
806
+ const formData = new FormData();
807
+ if (typeof request.file === "string") {
808
+ const stream = (0, import_node_fs.createReadStream)(request.file);
809
+ const buffer = await streamToBuffer(stream);
810
+ formData.append("file", new Blob([buffer]), (0, import_node_path.basename)(request.file));
811
+ } else if (Buffer.isBuffer(request.file)) {
812
+ formData.append("file", new Blob([request.file]), request.fileName);
813
+ } else {
814
+ const buffer = await streamToBuffer(request.file);
815
+ formData.append("file", new Blob([buffer]), request.fileName);
816
+ }
817
+ if (request.model) formData.append("model", request.model);
818
+ if (request.language) formData.append("language", request.language);
819
+ if (request.domain) formData.append("domain", request.domain);
820
+ const { data } = await this.http.postMultipart(
821
+ "/v1/transcribe/speech-to-text",
822
+ formData
823
+ );
824
+ return data;
825
+ }
826
+ };
827
+
828
+ // src/client.ts
829
+ var DEFAULT_BASE_URL = "https://api.upliftai.org";
830
+ var UpliftAI = class {
831
+ tts;
832
+ stt;
833
+ constructor(options = {}) {
834
+ const apiKey = options.apiKey ?? process.env.UPLIFTAI_API_KEY;
835
+ if (!apiKey) {
836
+ throw new UpliftAIError(
837
+ "apiKey is required. Pass it in options or set the UPLIFTAI_API_KEY environment variable.",
838
+ void 0,
839
+ "invalid_config"
840
+ );
841
+ }
842
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
843
+ const http = new HttpClient({
844
+ baseUrl,
845
+ apiKey,
846
+ timeout: options.timeout,
847
+ maxRetries: options.maxRetries
848
+ });
849
+ const wsBaseUrl = baseUrl.replace(/^http/, "ws").replace(/^https/, "wss");
850
+ this.tts = new TTS(http, apiKey, baseUrl, wsBaseUrl);
851
+ this.stt = new STT(http);
852
+ }
853
+ };
854
+
855
+ // src/index.ts
856
+ var index_default = UpliftAI;
857
+ // Annotate the CommonJS export names for ESM import in node:
858
+ 0 && (module.exports = {
859
+ UpliftAI,
860
+ UpliftAIAuthError,
861
+ UpliftAIError,
862
+ UpliftAIInsufficientBalanceError,
863
+ UpliftAIRateLimitError
864
+ });
865
+ //# sourceMappingURL=index.js.map