@rimelabs/sdk 0.1.0-alpha.1 → 0.1.0-alpha.12

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.
Files changed (67) hide show
  1. package/CHANGELOG.md +96 -11
  2. package/LICENSE-PRISM +201 -0
  3. package/NOTICE-PRISM +9 -0
  4. package/README.md +175 -63
  5. package/dist/auth.d.ts +9 -3
  6. package/dist/auth.js +12 -4
  7. package/dist/cancellation.d.ts +6 -0
  8. package/dist/cancellation.js +54 -0
  9. package/dist/client.d.ts +23 -23
  10. package/dist/client.js +39 -92
  11. package/dist/grpc.d.ts +27 -0
  12. package/dist/{transport.js → grpc.js} +36 -106
  13. package/dist/index.d.ts +6 -2
  14. package/dist/index.js +5 -2
  15. package/dist/pcm.d.ts +21 -0
  16. package/dist/pcm.js +52 -0
  17. package/dist/realtime/async.d.ts +23 -0
  18. package/dist/realtime/async.js +87 -0
  19. package/dist/realtime/audio.d.ts +2 -0
  20. package/dist/realtime/audio.js +11 -0
  21. package/dist/realtime/client.d.ts +12 -0
  22. package/dist/realtime/client.js +156 -0
  23. package/dist/realtime/protocol.d.ts +80 -0
  24. package/dist/realtime/protocol.js +354 -0
  25. package/dist/realtime/session.d.ts +67 -0
  26. package/dist/realtime/session.js +731 -0
  27. package/dist/realtime/transport.d.ts +16 -0
  28. package/dist/realtime/transport.js +83 -0
  29. package/dist/realtime/types.d.ts +186 -0
  30. package/dist/realtime/types.js +10 -0
  31. package/dist/stt/audio.d.ts +15 -0
  32. package/dist/stt/audio.js +46 -0
  33. package/dist/stt/client.d.ts +13 -0
  34. package/dist/stt/client.js +73 -0
  35. package/dist/stt/policy.d.ts +11 -0
  36. package/dist/stt/policy.js +11 -0
  37. package/dist/stt/protocol.d.ts +13 -0
  38. package/dist/stt/protocol.js +61 -0
  39. package/dist/stt/queue.d.ts +16 -0
  40. package/dist/stt/queue.js +45 -0
  41. package/dist/stt/stream.d.ts +48 -0
  42. package/dist/stt/stream.js +224 -0
  43. package/dist/stt/transport.d.ts +13 -0
  44. package/dist/stt/transport.js +45 -0
  45. package/dist/stt/types.d.ts +34 -0
  46. package/dist/stt/types.js +1 -0
  47. package/dist/{audio.js → tts/audio.js} +3 -2
  48. package/dist/tts/client.d.ts +39 -0
  49. package/dist/tts/client.js +121 -0
  50. package/dist/{policy.d.ts → tts/policy.d.ts} +2 -2
  51. package/dist/tts/policy.js +41 -0
  52. package/dist/{sentences.js → tts/sentences.js} +2 -2
  53. package/dist/{stream.d.ts → tts/stream.d.ts} +1 -1
  54. package/dist/{stream.js → tts/stream.js} +5 -8
  55. package/dist/{transport.d.ts → tts/transport.d.ts} +3 -11
  56. package/dist/tts/transport.js +116 -0
  57. package/dist/validation.d.ts +5 -0
  58. package/dist/validation.js +28 -0
  59. package/docs/realtime.md +220 -0
  60. package/docs/stt.md +103 -0
  61. package/docs/tts.md +139 -0
  62. package/package.json +14 -6
  63. package/dist/policy.js +0 -73
  64. /package/dist/{audio.d.ts → tts/audio.d.ts} +0 -0
  65. /package/dist/{queue.d.ts → tts/queue.d.ts} +0 -0
  66. /package/dist/{queue.js → tts/queue.js} +0 -0
  67. /package/dist/{sentences.d.ts → tts/sentences.d.ts} +0 -0
@@ -0,0 +1,23 @@
1
+ export { scope } from "../cancellation.js";
2
+ export declare class Deferred<T> {
3
+ readonly promise: Promise<T>;
4
+ resolve: (value: T) => void;
5
+ reject: (error: unknown) => void;
6
+ constructor();
7
+ }
8
+ export declare class Flag {
9
+ private waiters;
10
+ isSet: boolean;
11
+ set(): void;
12
+ clear(): void;
13
+ wait(signal: AbortSignal): Promise<void>;
14
+ }
15
+ export declare class Mutex {
16
+ private locked;
17
+ private waiters;
18
+ acquire(signal: AbortSignal): Promise<() => void>;
19
+ }
20
+ export declare function cancellation(signal?: AbortSignal): {
21
+ signal: AbortSignal;
22
+ dispose: () => void | undefined;
23
+ };
@@ -0,0 +1,87 @@
1
+ export { scope } from "../cancellation.js";
2
+ import { abortable } from "../cancellation.js";
3
+ import { RimeCancelledError } from "../errors.js";
4
+ export class Deferred {
5
+ promise;
6
+ resolve;
7
+ reject;
8
+ constructor() {
9
+ this.promise = new Promise((resolve, reject) => {
10
+ this.resolve = resolve;
11
+ this.reject = reject;
12
+ });
13
+ // Session failures can arrive before a caller starts waiting.
14
+ this.promise.catch(() => { });
15
+ }
16
+ }
17
+ export class Flag {
18
+ waiters = new Set();
19
+ isSet = false;
20
+ set() {
21
+ this.isSet = true;
22
+ for (const wake of this.waiters)
23
+ wake();
24
+ }
25
+ clear() {
26
+ this.isSet = false;
27
+ }
28
+ wait(signal) {
29
+ if (signal.aborted)
30
+ return Promise.reject(signal.reason);
31
+ if (this.isSet)
32
+ return Promise.resolve();
33
+ return new Promise((resolve, reject) => {
34
+ const cleanup = () => {
35
+ this.waiters.delete(wake);
36
+ signal.removeEventListener("abort", abort);
37
+ };
38
+ const wake = () => {
39
+ cleanup();
40
+ resolve();
41
+ };
42
+ const abort = () => {
43
+ cleanup();
44
+ reject(signal.reason);
45
+ };
46
+ this.waiters.add(wake);
47
+ signal.addEventListener("abort", abort, { once: true });
48
+ });
49
+ }
50
+ }
51
+ export class Mutex {
52
+ locked = false;
53
+ waiters = new Set();
54
+ async acquire(signal) {
55
+ signal.throwIfAborted();
56
+ while (this.locked) {
57
+ const wake = new Deferred();
58
+ const notify = () => wake.resolve();
59
+ this.waiters.add(notify);
60
+ try {
61
+ await abortable(wake.promise, signal);
62
+ }
63
+ finally {
64
+ this.waiters.delete(notify);
65
+ }
66
+ signal.throwIfAborted();
67
+ }
68
+ this.locked = true;
69
+ return () => {
70
+ this.locked = false;
71
+ for (const wake of this.waiters)
72
+ wake();
73
+ };
74
+ }
75
+ }
76
+ export function cancellation(signal) {
77
+ const controller = new AbortController();
78
+ const abort = () => controller.abort(new RimeCancelledError("Realtime operation cancelled"));
79
+ if (signal?.aborted)
80
+ abort();
81
+ else
82
+ signal?.addEventListener("abort", abort, { once: true });
83
+ return {
84
+ signal: controller.signal,
85
+ dispose: () => signal?.removeEventListener("abort", abort),
86
+ };
87
+ }
@@ -0,0 +1,2 @@
1
+ import type { AudioChunk, PCMFormat } from "./types.js";
2
+ export declare function formatOf(chunk: AudioChunk): Required<PCMFormat>;
@@ -0,0 +1,11 @@
1
+ import { RimeAudioFormatError } from "../errors.js";
2
+ import { resolvePCMFormat } from "../pcm.js";
3
+ export function formatOf(chunk) {
4
+ const format = resolvePCMFormat(chunk.format);
5
+ if (!(chunk.data instanceof Uint8Array) ||
6
+ chunk.data.length % (2 * format.channels))
7
+ throw new RimeAudioFormatError("Audio must contain complete PCM16 frames as Uint8Array");
8
+ if (chunk.data.length > 192000)
9
+ throw new RimeAudioFormatError("Send audio in chunks of at most 192000 bytes");
10
+ return format;
11
+ }
@@ -0,0 +1,12 @@
1
+ import { Credentials } from "../auth.js";
2
+ import { RealtimeSession } from "./session.js";
3
+ import type { RealtimeConnectOptions } from "./types.js";
4
+ export declare class Realtime {
5
+ private readonly credentials;
6
+ private readonly checkOpen;
7
+ private readonly sessions;
8
+ private readonly opening;
9
+ constructor(credentials: Credentials, checkOpen: () => void);
10
+ connect(options: RealtimeConnectOptions): Promise<RealtimeSession>;
11
+ close(): Promise<void>;
12
+ }
@@ -0,0 +1,156 @@
1
+ import { abortable } from "../cancellation.js";
2
+ import { RimeCancelledError, RimeInputError, RimeUnavailableError, } from "../errors.js";
3
+ import { cancellation, scope } from "./async.js";
4
+ import { RealtimeSession } from "./session.js";
5
+ import { RealtimeConnection } from "./transport.js";
6
+ function endpoint(value) {
7
+ let url;
8
+ try {
9
+ url = new URL(value);
10
+ }
11
+ catch {
12
+ throw new RimeInputError("endpoint must be a full ws:// or wss:// URL ending in /v1/realtime");
13
+ }
14
+ if (!["ws:", "wss:"].includes(url.protocol) ||
15
+ !url.hostname ||
16
+ url.username ||
17
+ url.password ||
18
+ url.hash ||
19
+ url.pathname.replace(/\/+$/, "") !== "/v1/realtime")
20
+ throw new RimeInputError("endpoint must be a full ws:// or wss:// URL ending in /v1/realtime");
21
+ if (url.protocol === "ws:" &&
22
+ !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))
23
+ throw new RimeInputError("Use wss:// outside localhost to protect credentials");
24
+ return url.href;
25
+ }
26
+ function timeouts(options = {}) {
27
+ const result = {
28
+ connectS: options.connectS ?? 10,
29
+ readyS: options.readyS ?? 30,
30
+ requestS: options.requestS ?? 10,
31
+ };
32
+ if (Object.values(result).some((value) => typeof value !== "number" || !Number.isFinite(value) || value <= 0))
33
+ throw new RimeInputError("Timeouts must be finite and positive");
34
+ return result;
35
+ }
36
+ /** Reject values that JSON.stringify would silently drop or change. */
37
+ function copyJson(value) {
38
+ if (value === null || typeof value === "string" || typeof value === "boolean")
39
+ return value;
40
+ if (typeof value === "number" && Number.isFinite(value))
41
+ return value;
42
+ if (Array.isArray(value))
43
+ return Array.from(value, copyJson);
44
+ if (value &&
45
+ typeof value === "object" &&
46
+ [Object.prototype, null].includes(Object.getPrototypeOf(value)))
47
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, copyJson(item)]));
48
+ throw new RimeInputError("Tool schemas must contain finite JSON values");
49
+ }
50
+ export class Realtime {
51
+ credentials;
52
+ checkOpen;
53
+ sessions = new Set();
54
+ opening = new Map();
55
+ constructor(credentials, checkOpen) {
56
+ this.credentials = credentials;
57
+ this.checkOpen = checkOpen;
58
+ }
59
+ async connect(options) {
60
+ this.checkOpen();
61
+ const address = endpoint(options.endpoint);
62
+ const limits = timeouts(options.timeouts);
63
+ if ((options.model ?? "prism") !== "prism")
64
+ throw new RimeInputError("The realtime model must be prism");
65
+ if (options.interruptOnSpeech !== undefined &&
66
+ typeof options.interruptOnSpeech !== "boolean")
67
+ throw new RimeInputError("interruptOnSpeech must be a boolean");
68
+ const names = new Set();
69
+ const settings = {
70
+ modalities: ["text", "audio"],
71
+ input_audio_format: "pcm16",
72
+ turn_detection: { interrupt_response: options.interruptOnSpeech ?? true },
73
+ tools: (options.tools ?? []).map((tool) => {
74
+ if (typeof tool.name !== "string" ||
75
+ !tool.name.trim() ||
76
+ names.has(tool.name))
77
+ throw new RimeInputError("Tool names must be nonblank and unique");
78
+ names.add(tool.name);
79
+ if (!tool.parameters ||
80
+ Array.isArray(tool.parameters) ||
81
+ typeof tool.parameters !== "object" ||
82
+ (tool.description !== undefined &&
83
+ typeof tool.description !== "string"))
84
+ throw new RimeInputError("Tool parameters must be a JSON object and description must be a string");
85
+ let parameters;
86
+ try {
87
+ parameters = copyJson(tool.parameters);
88
+ }
89
+ catch {
90
+ throw new RimeInputError("Tool schemas must contain finite JSON values");
91
+ }
92
+ if (parameters === null ||
93
+ typeof parameters !== "object" ||
94
+ Array.isArray(parameters))
95
+ throw new RimeInputError("Tool parameters must be an object");
96
+ return {
97
+ type: "function",
98
+ function: {
99
+ name: tool.name,
100
+ description: tool.description ?? "",
101
+ parameters,
102
+ },
103
+ };
104
+ }),
105
+ };
106
+ for (const name of ["voice", "instructions"]) {
107
+ const value = options[name];
108
+ if (value !== undefined && value !== null) {
109
+ if (typeof value !== "string" || !value.trim())
110
+ throw new RimeInputError(`${name} must be nonblank`);
111
+ settings[name] = value;
112
+ }
113
+ }
114
+ const owner = new AbortController();
115
+ const caller = cancellation(options.signal);
116
+ const openingScope = scope([caller.signal, owner.signal]);
117
+ const task = (async () => {
118
+ let session;
119
+ try {
120
+ openingScope.signal.throwIfAborted();
121
+ let socket;
122
+ try {
123
+ socket = new RealtimeConnection(address, this.credentials.realtimeAuthorization());
124
+ }
125
+ catch {
126
+ throw new RimeUnavailableError("Realtime handshake failed");
127
+ }
128
+ session = new RealtimeSession(socket, limits, () => this.sessions.delete(session));
129
+ this.sessions.add(session);
130
+ await abortable(session.initialize(settings, openingScope.signal), openingScope.signal);
131
+ openingScope.signal.throwIfAborted();
132
+ this.checkOpen();
133
+ return session;
134
+ }
135
+ catch (error) {
136
+ await session?.close();
137
+ throw error;
138
+ }
139
+ })();
140
+ this.opening.set(owner, task);
141
+ try {
142
+ return await task;
143
+ }
144
+ finally {
145
+ this.opening.delete(owner);
146
+ openingScope.dispose();
147
+ caller.dispose();
148
+ }
149
+ }
150
+ async close() {
151
+ for (const controller of this.opening.keys())
152
+ controller.abort(new RimeCancelledError("Client closed"));
153
+ await Promise.allSettled(this.opening.values());
154
+ await Promise.all([...this.sessions].map((session) => session.close()));
155
+ }
156
+ }
@@ -0,0 +1,80 @@
1
+ import * as t from "./types.js";
2
+ export interface SessionSettings {
3
+ modalities: ("text" | "audio")[];
4
+ input_audio_format: "pcm16";
5
+ turn_detection: {
6
+ interrupt_response: boolean;
7
+ };
8
+ tools: {
9
+ type: "function";
10
+ function: {
11
+ name: string;
12
+ description: string;
13
+ parameters: t.JsonObject;
14
+ };
15
+ }[];
16
+ voice?: string;
17
+ instructions?: string;
18
+ }
19
+ export interface SessionView {
20
+ id: string;
21
+ voice: string;
22
+ interruptOnSpeech: boolean | null;
23
+ toolResultTimeoutS: number | null;
24
+ toolContinuationTimeoutS: number | null;
25
+ }
26
+ export interface SessionCreated {
27
+ kind: "session.created";
28
+ session: SessionView;
29
+ }
30
+ export interface SessionUpdated {
31
+ kind: "session.updated";
32
+ session: SessionView;
33
+ }
34
+ export interface ItemCreated {
35
+ kind: "item.created";
36
+ itemId: string;
37
+ }
38
+ export interface ToolResultCreated {
39
+ kind: "tool.result.created";
40
+ callId: string;
41
+ }
42
+ export interface AudioCleared {
43
+ kind: "audio.cleared";
44
+ }
45
+ export interface ItemTruncated {
46
+ kind: "item.truncated";
47
+ itemId: string;
48
+ contentIndex: number;
49
+ }
50
+ export interface ResponseEnded extends t.ResponseEnded {
51
+ drainToken: string | null;
52
+ }
53
+ export type Acknowledgment = SessionUpdated | ItemCreated | ToolResultCreated | AudioCleared | ItemTruncated | t.ResponseStarted | ResponseEnded;
54
+ export type ServerPayload = SessionCreated | Acknowledgment | t.InputEvent | t.MessageStarted | t.TextDelta | t.TextDone | t.AudioDelta | t.AudioDone | t.ToolCall | t.FaultEvent;
55
+ export interface ServerEvent {
56
+ eventId: string;
57
+ requestId: string | null;
58
+ payload: ServerPayload;
59
+ }
60
+ export type RequestKind = "session.update" | "conversation.item.create" | "response.create" | "response.cancel" | "input_audio_buffer.clear" | "conversation.item.truncate";
61
+ /** A request carries its result type and checks it before exposing the result. */
62
+ export declare class Request<T extends Acknowledgment> {
63
+ readonly kind: RequestKind;
64
+ private readonly accepts;
65
+ readonly allowsItemCreated: boolean;
66
+ constructor(kind: RequestKind, accepts: (event: Acknowledgment) => event is T, allowsItemCreated?: boolean);
67
+ result(event: Acknowledgment): T;
68
+ check(event: Acknowledgment): void;
69
+ }
70
+ export declare const requests: {
71
+ update: Request<SessionUpdated>;
72
+ item: Request<ItemCreated>;
73
+ toolResult: Request<ToolResultCreated>;
74
+ create: Request<t.ResponseStarted>;
75
+ text: Request<t.ResponseStarted>;
76
+ cancel: Request<ResponseEnded>;
77
+ clear: Request<AudioCleared>;
78
+ truncate: Request<ItemTruncated>;
79
+ };
80
+ export declare function decode(raw: string, sessionId: string | null): ServerEvent | null;