@runappsai/sdk 0.6.9 → 0.7.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 +45 -5
- package/dist/audio.d.ts +11 -5
- package/dist/audio.d.ts.map +1 -1
- package/dist/audio.js +6 -0
- package/dist/audio.js.map +1 -1
- package/dist/chat.d.ts +8 -1
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js.map +1 -1
- package/dist/client.d.ts +3 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +4 -0
- package/dist/client.js.map +1 -1
- package/dist/events.d.ts +2 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js.map +1 -1
- package/dist/image.d.ts +6 -6
- package/dist/image.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/realtime.d.ts +137 -0
- package/dist/realtime.d.ts.map +1 -0
- package/dist/realtime.js +359 -0
- package/dist/realtime.js.map +1 -0
- package/dist/sdk.umd.js +3 -3
- package/dist/sdk.umd.js.map +1 -1
- package/dist/transport.d.ts +2 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +4 -0
- package/dist/transport.js.map +1 -1
- package/dist/video.d.ts +4 -4
- package/dist/video.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/audio.ts +16 -5
- package/src/chat.ts +9 -1
- package/src/client.ts +4 -0
- package/src/events.ts +2 -0
- package/src/image.ts +6 -6
- package/src/index.ts +18 -0
- package/src/realtime.ts +446 -0
- package/src/transport.ts +5 -0
- package/src/video.ts +4 -4
package/src/realtime.ts
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import { newRequestId, type SDKEvents } from "./events.js";
|
|
2
|
+
import type { Transport } from "./transport.js";
|
|
3
|
+
|
|
4
|
+
/* ------------------------------------------------------------------ */
|
|
5
|
+
/* Realtime voice */
|
|
6
|
+
/* ------------------------------------------------------------------ */
|
|
7
|
+
/*
|
|
8
|
+
* `client.realtime.connect()` opens a full-duplex voice session over a
|
|
9
|
+
* WebSocket to `/v1/realtime`: stream microphone audio up, get the
|
|
10
|
+
* model's transcripts, text and spoken reply back as they happen, and
|
|
11
|
+
* interrupt it mid-sentence.
|
|
12
|
+
*
|
|
13
|
+
* The wire protocol is the realtime model's own. For the Doubao realtime
|
|
14
|
+
* model it is Realtime-style JSON events, one per frame:
|
|
15
|
+
*
|
|
16
|
+
* → session.create / session.update (connect() sends the first one)
|
|
17
|
+
* → input_audio_buffer.append {audio: base64 PCM16, 16 kHz mono}
|
|
18
|
+
* → speech_text_buffer.commit {text} — have the model say this
|
|
19
|
+
* → response.cancel interrupt the reply
|
|
20
|
+
* → session.close (close() sends it)
|
|
21
|
+
* ← session.created
|
|
22
|
+
* ← conversation.item.input_audio_transcription.delta / .completed (what the user said)
|
|
23
|
+
* ← response.output_text.delta / .done (what the model says)
|
|
24
|
+
* ← response.output_audio.delta {delta: base64 PCM16, 24 kHz mono by default}
|
|
25
|
+
* ← response.done one turn finished, with token usage
|
|
26
|
+
* ← error
|
|
27
|
+
* ← session.closed
|
|
28
|
+
* ← gateway.usage the session's cost, just before the socket closes
|
|
29
|
+
*
|
|
30
|
+
* Audio input must arrive at real-time pace (send ~20 ms chunks as they
|
|
31
|
+
* are captured); the model also expects a steady stream, so keep sending
|
|
32
|
+
* silence — or `mute()` — while the microphone is off.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** One protocol event. `type` names it; the rest depends on the event. */
|
|
36
|
+
export interface RealtimeEvent {
|
|
37
|
+
type: string;
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** What a realtime session cost, from the closing `gateway.usage` event. */
|
|
42
|
+
export interface RealtimeUsage {
|
|
43
|
+
input_tokens?: number;
|
|
44
|
+
output_tokens?: number;
|
|
45
|
+
/** Session length in seconds. */
|
|
46
|
+
seconds?: number;
|
|
47
|
+
/** USD. */
|
|
48
|
+
total_cost?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** How a session ended. */
|
|
52
|
+
export interface RealtimeCloseInfo {
|
|
53
|
+
/** WebSocket close code: 1000 when the session completed; 4000+N for an
|
|
54
|
+
* upstream HTTP error N (e.g. 4429); 4408 idle or over-long session. */
|
|
55
|
+
code: number;
|
|
56
|
+
reason: string;
|
|
57
|
+
/** Null when the session ended before the cost could be reported. */
|
|
58
|
+
usage: RealtimeUsage | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The subset of the WHATWG WebSocket the SDK uses (also what `ws` provides). */
|
|
62
|
+
export interface WebSocketLike {
|
|
63
|
+
readonly readyState: number;
|
|
64
|
+
send(data: string): void;
|
|
65
|
+
close(code?: number, reason?: string): void;
|
|
66
|
+
addEventListener(type: string, listener: (ev: any) => void): void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type WebSocketConstructor = new (url: string, protocols?: string | string[]) => WebSocketLike;
|
|
70
|
+
|
|
71
|
+
export interface RealtimeConnectOptions {
|
|
72
|
+
/** Realtime model name (e.g. "Doubao Realtime Voice"); omit for the platform default. */
|
|
73
|
+
model?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Session settings sent in the opening `session.create`, e.g.
|
|
76
|
+
* `{ instructions: "...", audio: { output: { voice: "zh_female_vv_jupiter_bigtts" } } }`.
|
|
77
|
+
*/
|
|
78
|
+
session?: Record<string, unknown>;
|
|
79
|
+
/** Model-specific extension block (Doubao: `{ asr, tts, dialog }`). */
|
|
80
|
+
extension?: Record<string, unknown>;
|
|
81
|
+
/** Extra query parameters for the handshake. */
|
|
82
|
+
query?: Record<string, string>;
|
|
83
|
+
/**
|
|
84
|
+
* WebSocket implementation. Defaults to the global `WebSocket` (browsers,
|
|
85
|
+
* Node 22+, Deno, Bun); on older Node pass the `ws` package's export.
|
|
86
|
+
*/
|
|
87
|
+
WebSocket?: WebSocketConstructor;
|
|
88
|
+
/** Give up if the session isn't open within this long (default 15 s). */
|
|
89
|
+
timeoutMs?: number;
|
|
90
|
+
signal?: AbortSignal;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type Handler = (event: RealtimeEvent) => void;
|
|
94
|
+
|
|
95
|
+
const OPEN = 1;
|
|
96
|
+
const CLOSE_TIMEOUT_MS = 10_000;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* A live realtime session. Listen with `on(type, handler)` (or iterate
|
|
100
|
+
* it with `for await`), talk with `appendAudio()` / `say()`, and end it
|
|
101
|
+
* with `close()`.
|
|
102
|
+
*/
|
|
103
|
+
export class RealtimeSession {
|
|
104
|
+
/** The upstream session id (from `session.created`); pass it as
|
|
105
|
+
* `session.id` to a later session to continue the conversation. */
|
|
106
|
+
sessionId = "";
|
|
107
|
+
/** Settles when the socket closes, however that happens. */
|
|
108
|
+
readonly closed: Promise<RealtimeCloseInfo>;
|
|
109
|
+
|
|
110
|
+
private readonly handlers = new Map<string, Set<Handler>>();
|
|
111
|
+
private usage: RealtimeUsage | null = null;
|
|
112
|
+
private resolveClosed!: (info: RealtimeCloseInfo) => void;
|
|
113
|
+
private isClosed = false;
|
|
114
|
+
private queue: RealtimeEvent[] = [];
|
|
115
|
+
private waiters: ((r: IteratorResult<RealtimeEvent>) => void)[] = [];
|
|
116
|
+
private iterating = false;
|
|
117
|
+
|
|
118
|
+
/** @internal */
|
|
119
|
+
constructor(private readonly ws: WebSocketLike) {
|
|
120
|
+
this.closed = new Promise((resolve) => {
|
|
121
|
+
this.resolveClosed = resolve;
|
|
122
|
+
});
|
|
123
|
+
ws.addEventListener("message", (ev: { data: unknown }) => {
|
|
124
|
+
if (typeof ev.data !== "string") return;
|
|
125
|
+
let event: RealtimeEvent;
|
|
126
|
+
try {
|
|
127
|
+
event = JSON.parse(ev.data) as RealtimeEvent;
|
|
128
|
+
} catch {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (!event || typeof event.type !== "string") return;
|
|
132
|
+
if (event.type === "session.created") {
|
|
133
|
+
const id = (event.session as { id?: unknown } | undefined)?.id;
|
|
134
|
+
if (typeof id === "string") this.sessionId = id;
|
|
135
|
+
} else if (event.type === "gateway.usage") {
|
|
136
|
+
this.usage = (event.usage as RealtimeUsage) ?? null;
|
|
137
|
+
}
|
|
138
|
+
this.dispatch(event);
|
|
139
|
+
});
|
|
140
|
+
ws.addEventListener("close", (ev: { code?: number; reason?: string }) => {
|
|
141
|
+
if (this.isClosed) return;
|
|
142
|
+
this.isClosed = true;
|
|
143
|
+
for (const w of this.waiters.splice(0)) w({ value: undefined, done: true });
|
|
144
|
+
this.resolveClosed({ code: ev.code ?? 1006, reason: ev.reason ?? "", usage: this.usage });
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Subscribe to events of one type, or `"*"` for all of them. Returns
|
|
150
|
+
* the unsubscribe function.
|
|
151
|
+
*/
|
|
152
|
+
on(type: string, handler: Handler): () => void {
|
|
153
|
+
let set = this.handlers.get(type);
|
|
154
|
+
if (!set) {
|
|
155
|
+
set = new Set();
|
|
156
|
+
this.handlers.set(type, set);
|
|
157
|
+
}
|
|
158
|
+
set.add(handler);
|
|
159
|
+
return () => set!.delete(handler);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Send a raw protocol event. */
|
|
163
|
+
send(event: RealtimeEvent): void {
|
|
164
|
+
if (this.ws.readyState !== OPEN) throw new Error("runapps: realtime session is closed");
|
|
165
|
+
this.ws.send(JSON.stringify(event));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Stream a chunk of microphone audio: 16-bit little-endian PCM, 16 kHz
|
|
170
|
+
* mono (`pcm16FromFloat32` converts Web Audio samples). Send ~20 ms
|
|
171
|
+
* (640 bytes) at a time, as it is captured.
|
|
172
|
+
*/
|
|
173
|
+
appendAudio(pcm16: Int16Array | Uint8Array | ArrayBuffer): void {
|
|
174
|
+
this.send({ type: "input_audio_buffer.append", audio: bytesToBase64(asBytes(pcm16)) });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Tell the model the user has finished speaking (skips end-of-speech detection). */
|
|
178
|
+
commitAudio(): void {
|
|
179
|
+
this.send({ type: "input_audio_buffer.commit" });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The microphone is off: stops the model waiting on audio. */
|
|
183
|
+
mute(): void {
|
|
184
|
+
this.send({ type: "input_audio_mute.commit" });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The microphone is back on. */
|
|
188
|
+
unmute(): void {
|
|
189
|
+
this.send({ type: "input_audio_unmute.commit" });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Have the model speak `text` (a greeting, or a scripted line). */
|
|
193
|
+
say(text: string): void {
|
|
194
|
+
this.send({ type: "speech_text_buffer.commit", text });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Interrupt the model's current reply. */
|
|
198
|
+
cancelResponse(): void {
|
|
199
|
+
this.send({ type: "response.cancel" });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Change session settings mid-session (instructions, voice, tools, …). */
|
|
203
|
+
update(session: Record<string, unknown>): void {
|
|
204
|
+
this.send({ type: "session.update", session });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Answer a `response.function_call_arguments.done` tool call. */
|
|
208
|
+
sendFunctionResult(callId: string, output: string): void {
|
|
209
|
+
this.send({
|
|
210
|
+
type: "conversation.item.create",
|
|
211
|
+
items: [{ call_id: callId, role: "tool", content: [{ type: "input_text", text: output }] }],
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* End the session: asks the model to close, waits for the server to
|
|
217
|
+
* hang up (so the closing `gateway.usage` arrives), and resolves with
|
|
218
|
+
* how it ended, including the cost.
|
|
219
|
+
*/
|
|
220
|
+
async close(): Promise<RealtimeCloseInfo> {
|
|
221
|
+
if (!this.isClosed && this.ws.readyState === OPEN) {
|
|
222
|
+
try {
|
|
223
|
+
this.send({ type: "session.close" });
|
|
224
|
+
} catch {
|
|
225
|
+
// already closing
|
|
226
|
+
}
|
|
227
|
+
const timer = setTimeout(() => this.ws.close(1000, "client timeout"), CLOSE_TIMEOUT_MS);
|
|
228
|
+
try {
|
|
229
|
+
return await this.closed;
|
|
230
|
+
} finally {
|
|
231
|
+
clearTimeout(timer);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (!this.isClosed) this.ws.close(1000);
|
|
235
|
+
return this.closed;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Iterate events until the session closes. */
|
|
239
|
+
[Symbol.asyncIterator](): AsyncIterator<RealtimeEvent> {
|
|
240
|
+
this.iterating = true;
|
|
241
|
+
return {
|
|
242
|
+
next: () => {
|
|
243
|
+
const event = this.queue.shift();
|
|
244
|
+
if (event) return Promise.resolve({ value: event, done: false });
|
|
245
|
+
if (this.isClosed) return Promise.resolve({ value: undefined, done: true });
|
|
246
|
+
return new Promise((resolve) => this.waiters.push(resolve));
|
|
247
|
+
},
|
|
248
|
+
return: () => {
|
|
249
|
+
this.iterating = false;
|
|
250
|
+
this.queue = [];
|
|
251
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private dispatch(event: RealtimeEvent): void {
|
|
257
|
+
for (const type of [event.type, "*"]) {
|
|
258
|
+
const set = this.handlers.get(type);
|
|
259
|
+
if (!set) continue;
|
|
260
|
+
for (const h of [...set]) {
|
|
261
|
+
try {
|
|
262
|
+
h(event);
|
|
263
|
+
} catch (err) {
|
|
264
|
+
// A broken handler must not stop the others or the session.
|
|
265
|
+
setTimeout(() => {
|
|
266
|
+
throw err;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (this.iterating) {
|
|
272
|
+
const w = this.waiters.shift();
|
|
273
|
+
if (w) w({ value: event, done: false });
|
|
274
|
+
else this.queue.push(event);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export class RealtimeService {
|
|
280
|
+
constructor(
|
|
281
|
+
private readonly transport: Transport,
|
|
282
|
+
private readonly events: SDKEvents,
|
|
283
|
+
) {}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Open a realtime voice session. Resolves once the model has accepted
|
|
287
|
+
* it (`session.created`); rejects if the connection fails first.
|
|
288
|
+
*/
|
|
289
|
+
async connect(options: RealtimeConnectOptions = {}): Promise<RealtimeSession> {
|
|
290
|
+
const WS = options.WebSocket ?? (globalThis as { WebSocket?: WebSocketConstructor }).WebSocket;
|
|
291
|
+
if (!WS) {
|
|
292
|
+
throw new Error("runapps: no WebSocket available — pass `WebSocket` (e.g. from the `ws` package) on Node < 22");
|
|
293
|
+
}
|
|
294
|
+
const token = await this.transport.bearerToken();
|
|
295
|
+
const ws = new WS(realtimeURL(this.transport.baseURL, options), ["realtime", "runapps-key." + token]);
|
|
296
|
+
const session = new RealtimeSession(ws);
|
|
297
|
+
const model = options.model ?? "";
|
|
298
|
+
const id = newRequestId();
|
|
299
|
+
const startedAt = Date.now();
|
|
300
|
+
this.events.emit("request:start", { id, model, capability: "realtime", startedAt, streaming: true });
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
await new Promise<void>((resolve, reject) => {
|
|
304
|
+
const timer = setTimeout(() => {
|
|
305
|
+
fail(new Error("runapps: realtime session did not open in time"));
|
|
306
|
+
ws.close(1000, "open timeout");
|
|
307
|
+
}, options.timeoutMs ?? 15_000);
|
|
308
|
+
const onAbort = () => {
|
|
309
|
+
fail(new Error("runapps: realtime connect aborted"));
|
|
310
|
+
ws.close(1000, "aborted");
|
|
311
|
+
};
|
|
312
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
313
|
+
let settled = false;
|
|
314
|
+
function fail(err: Error) {
|
|
315
|
+
if (settled) return;
|
|
316
|
+
settled = true;
|
|
317
|
+
clearTimeout(timer);
|
|
318
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
319
|
+
reject(err);
|
|
320
|
+
}
|
|
321
|
+
const offCreated = session.on("session.created", () => {
|
|
322
|
+
if (settled) return;
|
|
323
|
+
settled = true;
|
|
324
|
+
clearTimeout(timer);
|
|
325
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
326
|
+
offCreated();
|
|
327
|
+
offError();
|
|
328
|
+
resolve();
|
|
329
|
+
});
|
|
330
|
+
// An error before the session opens is fatal to connect().
|
|
331
|
+
const offError = session.on("error", (ev) => {
|
|
332
|
+
const e = (ev.error ?? {}) as { message?: string; code?: unknown };
|
|
333
|
+
fail(new Error(`runapps: realtime error: ${e.message ?? JSON.stringify(ev)}`));
|
|
334
|
+
});
|
|
335
|
+
ws.addEventListener("open", () => {
|
|
336
|
+
ws.send(JSON.stringify(openingEvent(options)));
|
|
337
|
+
});
|
|
338
|
+
void session.closed.then((info) => {
|
|
339
|
+
fail(new Error(`runapps: realtime connection closed (${info.code}${info.reason ? " " + info.reason : ""})`));
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
} catch (err) {
|
|
343
|
+
try {
|
|
344
|
+
ws.close(1000, "connect failed");
|
|
345
|
+
} catch {
|
|
346
|
+
// already closed
|
|
347
|
+
}
|
|
348
|
+
this.events.emit("request:error", {
|
|
349
|
+
id,
|
|
350
|
+
model,
|
|
351
|
+
capability: "realtime",
|
|
352
|
+
latencyMs: Date.now() - startedAt,
|
|
353
|
+
error: err as Error,
|
|
354
|
+
});
|
|
355
|
+
throw err;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
void session.closed.then((info) => {
|
|
359
|
+
this.events.emit("request:end", {
|
|
360
|
+
id,
|
|
361
|
+
model,
|
|
362
|
+
capability: "realtime",
|
|
363
|
+
latencyMs: Date.now() - startedAt,
|
|
364
|
+
totalTokens: info.usage?.output_tokens ?? 0,
|
|
365
|
+
...(info.usage?.total_cost !== undefined && { costUSD: info.usage.total_cost }),
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
return session;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function openingEvent(options: RealtimeConnectOptions): RealtimeEvent {
|
|
373
|
+
const event: RealtimeEvent = { type: "session.create", session: options.session ?? {} };
|
|
374
|
+
if (options.extension) event.extension = options.extension;
|
|
375
|
+
return event;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function realtimeURL(baseURL: string, options: RealtimeConnectOptions): string {
|
|
379
|
+
const u = new URL(baseURL.replace(/\/+$/, "") + "/v1/realtime");
|
|
380
|
+
u.protocol = u.protocol === "http:" ? "ws:" : "wss:";
|
|
381
|
+
for (const [k, v] of Object.entries(options.query ?? {})) u.searchParams.set(k, v);
|
|
382
|
+
if (options.model) u.searchParams.set("model", options.model);
|
|
383
|
+
return u.toString();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/* ------------------------------------------------------------------ */
|
|
387
|
+
/* PCM helpers */
|
|
388
|
+
/* ------------------------------------------------------------------ */
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Convert Web Audio samples (Float32, -1…1) at `sampleRate` to 16-bit PCM
|
|
392
|
+
* at `targetRate` (16 kHz, what realtime input expects), resampling
|
|
393
|
+
* linearly when the rates differ.
|
|
394
|
+
*/
|
|
395
|
+
export function pcm16FromFloat32(samples: Float32Array, sampleRate: number, targetRate = 16000): Int16Array {
|
|
396
|
+
const ratio = sampleRate / targetRate;
|
|
397
|
+
const n = ratio === 1 ? samples.length : Math.floor(samples.length / ratio);
|
|
398
|
+
const out = new Int16Array(n);
|
|
399
|
+
for (let i = 0; i < n; i++) {
|
|
400
|
+
let s: number;
|
|
401
|
+
if (ratio === 1) {
|
|
402
|
+
s = samples[i]!;
|
|
403
|
+
} else {
|
|
404
|
+
const pos = i * ratio;
|
|
405
|
+
const j = Math.floor(pos);
|
|
406
|
+
const frac = pos - j;
|
|
407
|
+
const a = samples[j]!;
|
|
408
|
+
const b = samples[Math.min(j + 1, samples.length - 1)]!;
|
|
409
|
+
s = a + (b - a) * frac;
|
|
410
|
+
}
|
|
411
|
+
s = Math.max(-1, Math.min(1, s));
|
|
412
|
+
out[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
|
|
413
|
+
}
|
|
414
|
+
return out;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Convert 16-bit little-endian PCM bytes (e.g. a decoded audio delta) to Float32 samples for Web Audio. */
|
|
418
|
+
export function float32FromPCM16(bytes: Uint8Array): Float32Array {
|
|
419
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
420
|
+
const out = new Float32Array(bytes.byteLength >> 1);
|
|
421
|
+
for (let i = 0; i < out.length; i++) out[i] = view.getInt16(i * 2, true) / 0x8000;
|
|
422
|
+
return out;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Base64-encode bytes (audio for `input_audio_buffer.append`). */
|
|
426
|
+
export function bytesToBase64(bytes: Uint8Array): string {
|
|
427
|
+
let bin = "";
|
|
428
|
+
for (let i = 0; i < bytes.length; i += 0x8000) {
|
|
429
|
+
bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
430
|
+
}
|
|
431
|
+
return btoa(bin);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Decode base64 (e.g. `response.output_audio.delta`'s `delta`) to bytes. */
|
|
435
|
+
export function base64ToBytes(b64: string): Uint8Array {
|
|
436
|
+
const bin = atob(b64);
|
|
437
|
+
const out = new Uint8Array(bin.length);
|
|
438
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
439
|
+
return out;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function asBytes(v: Int16Array | Uint8Array | ArrayBuffer): Uint8Array {
|
|
443
|
+
if (v instanceof Uint8Array) return v;
|
|
444
|
+
if (v instanceof ArrayBuffer) return new Uint8Array(v);
|
|
445
|
+
return new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
|
|
446
|
+
}
|
package/src/transport.ts
CHANGED
|
@@ -68,6 +68,11 @@ export class Transport {
|
|
|
68
68
|
return this.apiKey ?? "";
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/** The current bearer token, for connections fetch can't make (WebSocket). */
|
|
72
|
+
bearerToken(): Promise<string> {
|
|
73
|
+
return this.resolveToken();
|
|
74
|
+
}
|
|
75
|
+
|
|
71
76
|
/**
|
|
72
77
|
* Wrap a fetch call with single-attempt 401 retry. `build` produces a
|
|
73
78
|
* fresh RequestInit each call (so headers can re-resolve a fresh token,
|
package/src/video.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Transport } from "./transport.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { PromptInput } from "./chat.js";
|
|
3
3
|
import type { Usage } from "./types.js";
|
|
4
4
|
import type { SDKEvents } from "./events.js";
|
|
5
5
|
import { wrapEvents } from "./event-wrap.js";
|
|
@@ -16,9 +16,9 @@ import { wrapEvents } from "./event-wrap.js";
|
|
|
16
16
|
* forces off.
|
|
17
17
|
*/
|
|
18
18
|
export interface VideoGenerateParams {
|
|
19
|
-
/** The prompt text,
|
|
20
|
-
*
|
|
21
|
-
prompt: string |
|
|
19
|
+
/** The prompt text, a private prompt `{ id, vars }`, or a list of
|
|
20
|
+
* those and texts joined in order (see `PromptInput`). */
|
|
21
|
+
prompt: string | PromptInput;
|
|
22
22
|
aspect_ratio?: string;
|
|
23
23
|
duration?: number;
|
|
24
24
|
resolution?: string;
|