@convbased/sdk
v0.1.0
Published
Convbased real-time voice conversion SDK — connect to the signaling service with an API key and stream audio through WebRTC.
Maintainers
Readme
@convbased/sdk
Browser SDK for the Convbased voice services. Authenticate with an API key and:
- Real-time voice conversion — capture the microphone and stream the
converted audio back over WebRTC (
ConvbasedClient). - File inference (voice-to-voice) — upload an audio file and convert it as
a discrete task within a live session (
ConvbasedClient.runFileInference). - Text-to-speech — synthesize speech from text with a reference voice over
GraphQL (
TtsClient).
The SDK speaks the same signaling protocol as the official Convbased Web
console (/signaling/ws on ServerAPI/signaling) and is the easiest way to
embed Convbased voice features in third-party web apps.
Demo
Two ready-made examples ship in examples/:
examples/h5/— single-file HTML demo that loads the SDK from the official CDN:<script src="https://cdn.weights.chat/sdk/convbased-sdk.global.js"></script>examples/vanilla-ts/main.ts— a framework-free TypeScript snippet you can drop into a Vite / Vue / React project to wire the SDK to an existing<audio>tag. Seetts.tsandfile-inference.tsalongside it for the text-to-speech and voice-to-voice flows.
Install
bun add @convbased/sdk
# or
npm install @convbased/sdkQuick start
import { ConvbasedClient } from "@convbased/sdk";
const client = new ConvbasedClient({
apiKey: "sk_********************************",
// Signaling + GraphQL endpoints default to the production Convbased
// service (wss://api.weights.chat/api/signaling/ws), so an API key is
// the only required field. Override `signalingUrl` / `graphqlUrl` for
// self-hosted deployments.
});
client.on("track", ({ stream }) => {
// `stream` is your own voice after conversion — play it to hear the result.
const audio = document.querySelector<HTMLAudioElement>("#converted")!;
audio.srcObject = stream;
void audio.play();
});
client.on("state", ({ state }) => console.log("state:", state));
client.on("error", (err) => console.error(err));
await client.connect({
modelId: "model_xxx",
preferences: {
pitch: 0,
rms_mix_rate: 0.25,
f0_autotune: false,
},
});
// Adjust pitch live:
client.updateConfig({ pitch: 2 });
// End the session:
await client.disconnect();Endpoints
The production endpoints are baked into the SDK and exposed as constants:
import {
DEFAULT_SIGNALING_URL, // wss://api.weights.chat/api/signaling/ws
DEFAULT_GRAPHQL_URL, // https://api.weights.chat/api/v1/graphql
} from "@convbased/sdk";- For the official Convbased service, don't pass
signalingUrl/graphqlUrl— the defaults are correct. - For a self-hosted deployment, override either or both.
signalingUrlaccepts a full final URL (ending in/ws), the bare…/signalingpath, or just a host (ws://localhost:3010→ SDK appends/signaling/ws). - To disable the TURN auto-fetch entirely, pass
graphqlUrl: false. The SDK will then useiceServersif provided, or public Google STUN as a last resort.
Authentication
Pass either:
apiKey— long-lived key created in the Convbased dashboard. Sent as?api_key=…on the WebSocket andx-api-keyon the GraphQL fetch.accessToken— short-lived JWT obtained via/auth/login. Sent as?token=…andAuthorization: Bearer ….
The signaling server rejects connections with code 1008 if the credential is
invalid or the account has zero balance.
Reusing an existing MediaStream
Pass a captured stream to skip getUserMedia (useful with custom effect
pipelines like noise suppression or pitch-shift workers):
const raw = await navigator.mediaDevices.getUserMedia({ audio: true });
const processed = await applyEffects(raw);
await client.connect({ modelId, audio: processed });File inference (voice-to-voice)
Convert a whole audio file through the model instead of the live mic. This runs
as a discrete task inside an existing live session, so you must connect()
first (that's what provisions the inference node). The converted result is
returned as a presigned download URL, not as a live track.
await client.connect({ modelId: "model_xxx" });
// One-call helper: upload, start the task, wait for completion.
const result = await client.runFileInference({
audio: fileInput.files[0], // a File/Blob, or pass `audioKey` if already uploaded
preferences: { pitch: 2, f0_method: "rmvpe" },
onProgress: ({ progress }) => console.log(`progress: ${progress}`),
});
console.log("converted audio:", result.downloadUrl);Prefer the lower-level API when you want to drive the task lifecycle yourself:
const { key } = await client.uploadAudio(file);
const taskId = client.startTask({ audioKey: key, preferences: { pitch: 2 } });
client.on("taskAck", ({ status, queuePosition }) => { /* queued | started */ });
client.on("taskProgress", ({ progress }) => { /* 0..1 */ });
client.on("taskFinished", ({ status, downloadUrl, error }) => { /* … */ });
client.stopTask(taskId); // cancelMust be connected first. The inference node enforces that a task only runs once the live session is
SERVICE_READY(offer → answer → model loaded). CallingstartTask/runFileInferencebeforeconnect()resolves throws; sending a task to a not-yet-ready node fails it with"service not ready".
Text-to-speech
TtsClient is a standalone, GraphQL-only client — it does not open a WebSocket
or a peer connection. Synthesis is asynchronous (submit → poll), wrapped by a
one-call synthesize():
import { TtsClient } from "@convbased/sdk";
const tts = new TtsClient({ apiKey: "sk_********************************" });
// `referenceAudio` is the voice to clone; pass a previously uploaded
// `referenceKey` instead to skip the upload.
const result = await tts.synthesize({
referenceAudio: referenceFile, // a File/Blob
text: "你好,这是一段合成语音。",
params: { temperature: 0.8 },
onJob: (job) => console.log(job.status, "queue:", job.position),
});
const audio = document.querySelector<HTMLAudioElement>("#tts")!;
audio.src = result.url!; // presigned, valid ~1hLower-level pieces are available too: tts.uploadReferenceAudio(file),
tts.submit({ referenceKey, text, params }), tts.getJob(jobId),
tts.cancel(jobId), and tts.getPricing().
Audio upload limits. Both
client.uploadAudioandtts.uploadReferenceAudioare validated by the service on filename extension — one ofmp3,wav,ogg,flac,m4a,aac— and a max size of 100 MB. When uploading a bareBlob(no filename), pass{ filename: "source.wav" }so the extension check passes.
Events
| Event | Payload | Notes |
| -------------- | -------------------------------------------------- | --------------------------------------------------------------------- |
| state | { state, previous } | idle → signaling → negotiating → connecting → connected → closed |
| message | { code?, message?, raw } | Every JSON frame the server sends. |
| ready | { code: 3009, message? } | Inference node has loaded the model and converted audio is flowing. |
| track | { stream, track } | The converted audio track to wire into an <audio> element. |
| taskAck | { taskId, status, queuePosition?, code? } | File-inference task accepted (queued / started). |
| taskProgress | { taskId, progress, code? } | File-inference progress, progress in [0, 1]. |
| taskFinished | { taskId, status, resultKey?, downloadUrl?, error? } | File-inference terminal state (success / failure / cancelled). |
| error | Error | Connection or server-reported failure. |
| closed | { code?, reason? } | The signaling socket has gone away. |
API surface
ConvbasedClient — live conversion + file inference
new ConvbasedClient(options)client.connect({ modelId, audio?, preferences?, sampleRate? })client.updateConfig(preferences)client.setMuted(muted)client.replaceLocalStream(stream)client.getStats()—{ rttMs, jitter, packetsLost }client.getConvertedStream()/client.getPeerConnection()client.uploadAudio(file, opts?)—{ key }client.runFileInference({ audio? | audioKey?, preferences?, ... })—Promise<TaskFinishedEvent>client.startTask({ audioKey, preferences?, ... })/client.stopTask(taskId?)client.disconnect()
TtsClient — text-to-speech (GraphQL only)
new TtsClient(options)tts.synthesize({ referenceAudio? | referenceKey?, text, params?, ... })—Promise<TtsResult>tts.uploadReferenceAudio(file, opts?)—{ key }tts.submit({ referenceKey, text, params? })/tts.getJob(jobId)/tts.cancel(jobId)tts.getPricing()—{ pricePerToken, minCharge }
Notes
- The SDK is browser-only. WebRTC support in pure Node requires
wrtcoraiortc, which are out of scope here. - One client = one session. Create a new instance after
disconnect(). - The server rejects upfront if the wallet is empty, so a thrown error during
connect()may indicate insufficient balance — checkerror.message.
