@convbased/sdk 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.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +235 -0
  3. package/dist/cjs/client.js +635 -0
  4. package/dist/cjs/client.js.map +1 -0
  5. package/dist/cjs/endpoints.js +10 -0
  6. package/dist/cjs/endpoints.js.map +1 -0
  7. package/dist/cjs/events.js +39 -0
  8. package/dist/cjs/events.js.map +1 -0
  9. package/dist/cjs/graphql.js +40 -0
  10. package/dist/cjs/graphql.js.map +1 -0
  11. package/dist/cjs/index.js +24 -0
  12. package/dist/cjs/index.js.map +1 -0
  13. package/dist/cjs/package.json +3 -0
  14. package/dist/cjs/rtcServers.js +35 -0
  15. package/dist/cjs/rtcServers.js.map +1 -0
  16. package/dist/cjs/sdp.js +37 -0
  17. package/dist/cjs/sdp.js.map +1 -0
  18. package/dist/cjs/signaling.js +146 -0
  19. package/dist/cjs/signaling.js.map +1 -0
  20. package/dist/cjs/tts.js +227 -0
  21. package/dist/cjs/tts.js.map +1 -0
  22. package/dist/cjs/types.js +26 -0
  23. package/dist/cjs/types.js.map +1 -0
  24. package/dist/cjs/upload.js +87 -0
  25. package/dist/cjs/upload.js.map +1 -0
  26. package/dist/client.d.ts +169 -0
  27. package/dist/client.d.ts.map +1 -0
  28. package/dist/client.js +631 -0
  29. package/dist/client.js.map +1 -0
  30. package/dist/convbased-sdk.global.js +1291 -0
  31. package/dist/endpoints.d.ts +3 -0
  32. package/dist/endpoints.d.ts.map +1 -0
  33. package/dist/endpoints.js +7 -0
  34. package/dist/endpoints.js.map +1 -0
  35. package/dist/events.d.ts +9 -0
  36. package/dist/events.d.ts.map +1 -0
  37. package/dist/events.js +35 -0
  38. package/dist/events.js.map +1 -0
  39. package/dist/graphql.d.ts +18 -0
  40. package/dist/graphql.d.ts.map +1 -0
  41. package/dist/graphql.js +37 -0
  42. package/dist/graphql.js.map +1 -0
  43. package/dist/index.d.ts +9 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +9 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/rtcServers.d.ts +13 -0
  48. package/dist/rtcServers.d.ts.map +1 -0
  49. package/dist/rtcServers.js +31 -0
  50. package/dist/rtcServers.js.map +1 -0
  51. package/dist/sdp.d.ts +6 -0
  52. package/dist/sdp.d.ts.map +1 -0
  53. package/dist/sdp.js +34 -0
  54. package/dist/sdp.js.map +1 -0
  55. package/dist/signaling.d.ts +33 -0
  56. package/dist/signaling.d.ts.map +1 -0
  57. package/dist/signaling.js +142 -0
  58. package/dist/signaling.js.map +1 -0
  59. package/dist/tts.d.ts +111 -0
  60. package/dist/tts.d.ts.map +1 -0
  61. package/dist/tts.js +223 -0
  62. package/dist/tts.js.map +1 -0
  63. package/dist/types.d.ts +194 -0
  64. package/dist/types.d.ts.map +1 -0
  65. package/dist/types.js +23 -0
  66. package/dist/types.js.map +1 -0
  67. package/dist/upload.d.ts +46 -0
  68. package/dist/upload.d.ts.map +1 -0
  69. package/dist/upload.js +82 -0
  70. package/dist/upload.js.map +1 -0
  71. package/package.json +57 -0
  72. package/src/client.ts +839 -0
  73. package/src/endpoints.ts +8 -0
  74. package/src/events.ts +38 -0
  75. package/src/graphql.ts +58 -0
  76. package/src/index.ts +50 -0
  77. package/src/rtcServers.ts +38 -0
  78. package/src/sdp.ts +45 -0
  79. package/src/signaling.ts +172 -0
  80. package/src/tts.ts +364 -0
  81. package/src/types.ts +201 -0
  82. package/src/upload.ts +132 -0
@@ -0,0 +1,8 @@
1
+ // Production endpoints baked into the SDK. These mirror Convbased-Web's
2
+ // `.env` (VITE_WS_ENDPOINT / VITE_API_BASE_URL) and are considered stable —
3
+ // they're consumed by every Convbased client today. Override only for
4
+ // self-hosted deployments or local development.
5
+ export const DEFAULT_SIGNALING_URL =
6
+ "wss://api.weights.chat/api/signaling/ws";
7
+ export const DEFAULT_GRAPHQL_URL =
8
+ "https://api.weights.chat/api/v1/graphql";
package/src/events.ts ADDED
@@ -0,0 +1,38 @@
1
+ // Minimal typed event emitter; we avoid pulling Node's EventEmitter so the
2
+ // SDK stays browser-first with no polyfill chain.
3
+
4
+ export type Listener<T> = (payload: T) => void;
5
+
6
+ export class TypedEmitter<TEvents extends Record<string, unknown>> {
7
+ private readonly listeners: {
8
+ [K in keyof TEvents]?: Set<Listener<TEvents[K]>>;
9
+ } = {};
10
+
11
+ on<K extends keyof TEvents>(event: K, fn: Listener<TEvents[K]>): () => void {
12
+ (this.listeners[event] ??= new Set()).add(fn);
13
+ return () => this.off(event, fn);
14
+ }
15
+
16
+ off<K extends keyof TEvents>(event: K, fn: Listener<TEvents[K]>): void {
17
+ this.listeners[event]?.delete(fn);
18
+ }
19
+
20
+ emit<K extends keyof TEvents>(event: K, payload: TEvents[K]): void {
21
+ const set = this.listeners[event];
22
+ if (!set) return;
23
+ for (const fn of set) {
24
+ try {
25
+ fn(payload);
26
+ } catch (e) {
27
+ // Listeners shouldn't crash the emitter.
28
+ console.error(`[convbased-sdk] listener for "${String(event)}" threw:`, e);
29
+ }
30
+ }
31
+ }
32
+
33
+ removeAllListeners(): void {
34
+ for (const key of Object.keys(this.listeners)) {
35
+ delete this.listeners[key as keyof TEvents];
36
+ }
37
+ }
38
+ }
package/src/graphql.ts ADDED
@@ -0,0 +1,58 @@
1
+ // Shared GraphQL transport for the Convbased service. Authentication mirrors
2
+ // the server middleware: `x-api-key: <key>` for API keys, or
3
+ // `Authorization: Bearer <jwt>` for access tokens.
4
+
5
+ export interface GraphQLAuth {
6
+ apiKey?: string;
7
+ accessToken?: string;
8
+ }
9
+
10
+ export interface GraphQLRequestArgs<V> extends GraphQLAuth {
11
+ graphqlUrl: string;
12
+ query: string;
13
+ variables?: V;
14
+ signal?: AbortSignal;
15
+ }
16
+
17
+ /**
18
+ * Issue a single GraphQL operation and return `data`. Throws on HTTP failure
19
+ * or when the response carries a non-empty `errors` array — the thrown
20
+ * `Error.message` is the first server-reported message (often an i18n key the
21
+ * caller can localize).
22
+ */
23
+ export async function graphqlRequest<T, V = Record<string, unknown>>(
24
+ args: GraphQLRequestArgs<V>
25
+ ): Promise<T> {
26
+ const headers: Record<string, string> = {
27
+ "content-type": "application/json",
28
+ };
29
+ if (args.apiKey) headers["x-api-key"] = args.apiKey;
30
+ else if (args.accessToken) {
31
+ headers["authorization"] = `Bearer ${args.accessToken}`;
32
+ }
33
+
34
+ const res = await fetch(args.graphqlUrl, {
35
+ method: "POST",
36
+ headers,
37
+ body: JSON.stringify({ query: args.query, variables: args.variables }),
38
+ signal: args.signal,
39
+ });
40
+
41
+ if (!res.ok) {
42
+ throw new Error(
43
+ `GraphQL request failed: HTTP ${res.status} ${res.statusText}`
44
+ );
45
+ }
46
+
47
+ const json = (await res.json()) as {
48
+ data?: T;
49
+ errors?: Array<{ message: string }>;
50
+ };
51
+ if (json.errors?.length) {
52
+ throw new Error(json.errors.map((e) => e.message).join("; "));
53
+ }
54
+ if (json.data === undefined || json.data === null) {
55
+ throw new Error("GraphQL response contained no data");
56
+ }
57
+ return json.data;
58
+ }
package/src/index.ts ADDED
@@ -0,0 +1,50 @@
1
+ export {
2
+ ConvbasedClient,
3
+ type TaskAckEvent,
4
+ type TaskProgressEvent,
5
+ type TaskFinishedEvent,
6
+ type StartTaskOptions,
7
+ type RunFileInferenceOptions,
8
+ } from "./client.js";
9
+ export {
10
+ TtsClient,
11
+ type TtsClientOptions,
12
+ type TtsParams,
13
+ type TtsJob,
14
+ type TtsJobStatus,
15
+ type TtsResult,
16
+ type TtsPricing,
17
+ type SubmitTtsOptions,
18
+ type SynthesizeOptions,
19
+ } from "./tts.js";
20
+ export {
21
+ uploadAudio,
22
+ requestAudioUpload,
23
+ putToPresigned,
24
+ type PresignedUpload,
25
+ type HeaderKV,
26
+ } from "./upload.js";
27
+ export { graphqlRequest, type GraphQLAuth } from "./graphql.js";
28
+ export { applyOpusSdpOptions } from "./sdp.js";
29
+ export {
30
+ fetchRTCServers,
31
+ DEFAULT_STUN_SERVERS,
32
+ } from "./rtcServers.js";
33
+ export {
34
+ DEFAULT_SIGNALING_URL,
35
+ DEFAULT_GRAPHQL_URL,
36
+ } from "./endpoints.js";
37
+ export {
38
+ RTCStatusCode,
39
+ type ConnectOptions,
40
+ type ConnectionState,
41
+ type ConnectionStats,
42
+ type ConvbasedClientOptions,
43
+ type FileInferencePreferences,
44
+ type IncomingMessage,
45
+ type OutgoingMessage,
46
+ type RTCPreferences,
47
+ type RTCServersConfig,
48
+ type ServerMessageEvent,
49
+ type TaskStatus,
50
+ } from "./types.js";
@@ -0,0 +1,38 @@
1
+ import { graphqlRequest, type GraphQLAuth } from "./graphql.js";
2
+ import type { RTCServersConfig } from "./types.js";
3
+
4
+ /**
5
+ * Fetch TURN credentials from the Convbased GraphQL service. The query matches
6
+ * the one used by Convbased-Web's `getRTCServers`. Authentication uses the
7
+ * same API key / access token passed to the SDK.
8
+ */
9
+ export async function fetchRTCServers(
10
+ args: GraphQLAuth & {
11
+ graphqlUrl: string;
12
+ signal?: AbortSignal;
13
+ }
14
+ ): Promise<RTCServersConfig> {
15
+ const data = await graphqlRequest<{ rtcServers?: RTCServersConfig }>({
16
+ graphqlUrl: args.graphqlUrl,
17
+ apiKey: args.apiKey,
18
+ accessToken: args.accessToken,
19
+ signal: args.signal,
20
+ query: /* GraphQL */ `
21
+ query {
22
+ rtcServers {
23
+ urls
24
+ username
25
+ credential
26
+ }
27
+ }
28
+ `,
29
+ });
30
+ if (!data.rtcServers) {
31
+ throw new Error("rtcServers returned an empty payload");
32
+ }
33
+ return data.rtcServers;
34
+ }
35
+
36
+ export const DEFAULT_STUN_SERVERS: RTCServersConfig[] = [
37
+ { urls: ["stun:stun.l.google.com:19302"] },
38
+ ];
package/src/sdp.ts ADDED
@@ -0,0 +1,45 @@
1
+ // SDP mangling helpers. Mirrors `setAudioParameters` in Convbased-Web — we
2
+ // rewrite the Opus `a=fmtp:` line so the offer requests our target bitrate,
3
+ // stereo mode, and inband FEC. The signaling node passes the SDP through to
4
+ // aiortc largely unchanged, so changes here directly shape the negotiated
5
+ // audio.
6
+
7
+ export interface OpusSdpOptions {
8
+ bitrateKbps: number;
9
+ stereo: boolean;
10
+ }
11
+
12
+ export function applyOpusSdpOptions(sdp: string, opts: OpusSdpOptions): string {
13
+ const lines = sdp.split("\r\n");
14
+ let payloadType: string | null = null;
15
+
16
+ for (const line of lines) {
17
+ const match = line.match(/a=rtpmap:(\d+) opus\/48000\/2/);
18
+ if (match) {
19
+ payloadType = match[1]!;
20
+ break;
21
+ }
22
+ }
23
+
24
+ if (!payloadType) return sdp;
25
+
26
+ let fmtpIndex = lines.findIndex((line) =>
27
+ line.startsWith(`a=fmtp:${payloadType}`)
28
+ );
29
+
30
+ if (fmtpIndex === -1) {
31
+ lines.push(`a=fmtp:${payloadType}`);
32
+ fmtpIndex = lines.length - 1;
33
+ }
34
+
35
+ let fmtp = lines[fmtpIndex]!;
36
+ fmtp = fmtp.replace(/;\s*stereo=\d/, "");
37
+ fmtp = fmtp.replace(/;\s*maxaveragebitrate=\d+/, "");
38
+
39
+ if (opts.stereo) fmtp += "; stereo=1";
40
+ fmtp += `; maxaveragebitrate=${opts.bitrateKbps * 1000}`;
41
+ if (!fmtp.includes("useinbandfec=1")) fmtp += "; useinbandfec=1";
42
+
43
+ lines[fmtpIndex] = fmtp;
44
+ return lines.join("\r\n");
45
+ }
@@ -0,0 +1,172 @@
1
+ import type { IncomingMessage, OutgoingMessage } from "./types.js";
2
+
3
+ export interface SignalingChannelOptions {
4
+ signalingUrl: string;
5
+ apiKey?: string;
6
+ accessToken?: string;
7
+ connectTimeoutMs: number;
8
+ logger: Pick<Console, "debug" | "info" | "warn" | "error">;
9
+ }
10
+
11
+ export interface SignalingHandlers {
12
+ onMessage: (msg: IncomingMessage) => void;
13
+ onClose: (event: CloseEvent) => void;
14
+ onError: (event: Event) => void;
15
+ }
16
+
17
+ /**
18
+ * Thin WebSocket wrapper for the Convbased signaling endpoint. Builds the URL
19
+ * (`${base}/signaling/ws?api_key=…`), waits for `open`, and exposes JSON
20
+ * send/recv plus a 10s keepalive ping to match the server's expectations.
21
+ */
22
+ export class SignalingChannel {
23
+ private ws: WebSocket | null = null;
24
+ private pingTimer: ReturnType<typeof setInterval> | null = null;
25
+ private handlers: SignalingHandlers | null = null;
26
+
27
+ constructor(private readonly opts: SignalingChannelOptions) {}
28
+
29
+ get isOpen(): boolean {
30
+ return this.ws?.readyState === WebSocket.OPEN;
31
+ }
32
+
33
+ async connect(handlers: SignalingHandlers): Promise<void> {
34
+ if (this.ws) {
35
+ throw new Error("SignalingChannel is already connected");
36
+ }
37
+ this.handlers = handlers;
38
+
39
+ const url = this.buildUrl();
40
+ this.opts.logger.debug?.("[convbased-sdk] connecting signaling:", url);
41
+ const ws = new WebSocket(url);
42
+ this.ws = ws;
43
+
44
+ await new Promise<void>((resolve, reject) => {
45
+ const timeout = setTimeout(() => {
46
+ cleanup();
47
+ try {
48
+ ws.close();
49
+ } catch {
50
+ /* ignore */
51
+ }
52
+ reject(new Error("Signaling WebSocket connect timeout"));
53
+ }, this.opts.connectTimeoutMs);
54
+
55
+ const onOpen = () => {
56
+ cleanup();
57
+ resolve();
58
+ };
59
+ const onError = (e: Event) => {
60
+ cleanup();
61
+ reject(
62
+ new Error(
63
+ `Signaling WebSocket failed to open: ${describeEvent(e)}`
64
+ )
65
+ );
66
+ };
67
+ const onClose = (e: CloseEvent) => {
68
+ cleanup();
69
+ reject(
70
+ new Error(
71
+ `Signaling WebSocket closed before open (code=${e.code}, reason=${e.reason || "?"})`
72
+ )
73
+ );
74
+ };
75
+ const cleanup = () => {
76
+ clearTimeout(timeout);
77
+ ws.removeEventListener("open", onOpen);
78
+ ws.removeEventListener("error", onError);
79
+ ws.removeEventListener("close", onClose);
80
+ };
81
+ ws.addEventListener("open", onOpen);
82
+ ws.addEventListener("error", onError);
83
+ ws.addEventListener("close", onClose);
84
+ });
85
+
86
+ ws.addEventListener("message", (e) => this.handleMessage(e));
87
+ ws.addEventListener("close", (e) => this.handleClose(e));
88
+ ws.addEventListener("error", (e) => this.handlers?.onError(e));
89
+
90
+ // The signaling server pings every 10s and expects us to keep the
91
+ // socket warm. We mirror that cadence rather than relying on TCP
92
+ // keepalive, which most browsers don't expose.
93
+ this.pingTimer = setInterval(() => {
94
+ if (this.isOpen) this.send({ type: "ping" });
95
+ }, 10_000);
96
+ }
97
+
98
+ send(msg: OutgoingMessage): void {
99
+ if (!this.isOpen) {
100
+ throw new Error("Signaling WebSocket is not open");
101
+ }
102
+ this.ws!.send(JSON.stringify(msg));
103
+ }
104
+
105
+ close(code = 1000, reason = "client close"): void {
106
+ if (this.pingTimer) {
107
+ clearInterval(this.pingTimer);
108
+ this.pingTimer = null;
109
+ }
110
+ if (this.ws && this.ws.readyState <= WebSocket.OPEN) {
111
+ try {
112
+ this.ws.close(code, reason);
113
+ } catch {
114
+ /* ignore */
115
+ }
116
+ }
117
+ this.ws = null;
118
+ }
119
+
120
+ private handleMessage(event: MessageEvent): void {
121
+ if (typeof event.data !== "string") return;
122
+ let parsed: IncomingMessage;
123
+ try {
124
+ parsed = JSON.parse(event.data) as IncomingMessage;
125
+ } catch (e) {
126
+ this.opts.logger.warn?.(
127
+ "[convbased-sdk] dropping non-JSON signaling frame:",
128
+ event.data
129
+ );
130
+ return;
131
+ }
132
+ this.handlers?.onMessage(parsed);
133
+ }
134
+
135
+ private handleClose(event: CloseEvent): void {
136
+ if (this.pingTimer) {
137
+ clearInterval(this.pingTimer);
138
+ this.pingTimer = null;
139
+ }
140
+ this.handlers?.onClose(event);
141
+ }
142
+
143
+ private buildUrl(): string {
144
+ // `signalingUrl` may be:
145
+ // - the full final URL ending in `/ws` (production default — used as-is)
146
+ // - a bare host (e.g. `ws://localhost:3010`) — we append `/signaling/ws`
147
+ // - something already containing `/signaling` — we append `/ws`
148
+ let base = this.opts.signalingUrl.trim();
149
+ if (base.endsWith("/")) base = base.slice(0, -1);
150
+
151
+ let finalUrl: string;
152
+ if (/\/ws$/i.test(base)) {
153
+ finalUrl = base;
154
+ } else if (/\/signaling$/i.test(base)) {
155
+ finalUrl = `${base}/ws`;
156
+ } else {
157
+ finalUrl = `${base}/signaling/ws`;
158
+ }
159
+
160
+ const url = new URL(finalUrl);
161
+ if (this.opts.apiKey) url.searchParams.set("api_key", this.opts.apiKey);
162
+ if (this.opts.accessToken && !this.opts.apiKey) {
163
+ url.searchParams.set("token", this.opts.accessToken);
164
+ }
165
+ return url.toString();
166
+ }
167
+ }
168
+
169
+ function describeEvent(e: Event): string {
170
+ if (e instanceof ErrorEvent && e.message) return e.message;
171
+ return e.type || "unknown error";
172
+ }