npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sanas-ai/browser-sdk

v1.0.0

Published

Browser SDK for integrating Sanas audio processing into web applications

Readme

Sanas Browser SDK — Developer Guide

The Sanas Browser SDK lets a web application send a live microphone stream through Sanas's audio processing (accent translation, speech enhancement, language translation) and receive the processed audio back as a MediaStream. It runs entirely in the browser and is transport-agnostic from your point of view: give it an input stream, receive an output stream.

This guide covers everything a developer needs to ship an integration. If you get stuck, jump to Troubleshooting.


Contents

  1. Overview
  2. Installation
  3. Authentication
  4. Quick start
  5. Remote Inferencing mode (accent translation, speech enhancement)
  6. Language Translation mode
  7. The processor lifecycle
  8. API reference
  9. Error handling
  10. Advanced options
  11. Troubleshooting

Overview

The SDK exposes a single top-level object — SanasSDK — that manages a session. From that session you create one or more AudioProcessor instances, each of which:

  • Takes a MediaStream in (typically from navigator.mediaDevices.getUserMedia).
  • Emits a processed MediaStream out. You can attach the output to an <audio> element, feed it into WebRTC, or hand it to any other Web Audio API consumer.
  • Runs one of two modes:
    • Remote Inferencing (RI) — a single Sanas model applied to the stream. Used for accent translation, speech enhancement, and other single-model features. You identify the model by name.
    • Language Translation (LT) — one-direction live translation. Each processor handles a single direction (say English → Spanish). A real two-person conversation uses two processors — one per speaker's direction — linked by a shared conversationId so the server can share context between them. Callbacks stream per-utterance transcripts and language-detection hints back.

Availability of RI and LT is controlled per group in the Sanas Developer Console. The SDK checks entitlement before creating a processor and throws a distinct error if a feature isn't enabled for your group.

Installation

npm install @sanas-ai/browser-sdk

The package is self-contained — no peer dependencies. Works with any modern bundler (Vite, webpack, Next.js, Rollup, Parcel).

Browser support: Any browser that supports MediaStream, RTCPeerConnection, and WebSocket — Chrome 90+, Firefox 90+, Safari 15+, Edge 90+.

Authentication

The SDK never sees your Sanas API key. Instead, it accepts a tokenProvider callback that asks your backend for a short-lived access token, and your backend uses its stored API key to mint one.

Never embed a Sanas API key in browser code. Anyone can view your bundle, extract the key, and use it against your usage budget.

Backend responsibilities

Your backend needs a single endpoint (name it whatever you want) that:

  1. Authenticates the browser request using your app's own session mechanism (cookies, JWT, etc. — that's your call).
  2. Calls Sanas's OAuth token endpoint with your stored API key.
  3. Returns the token payload to the browser as JSON.

Here's a minimal Node.js example (no dependencies beyond Node 18+):

import http from "node:http";

const SANAS_API_KEY = process.env.SANAS_API_KEY;
const SANAS_IDENTITY_URL = "https://identity.sanas.ai";
const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN || "http://localhost:5173";

http.createServer(async (req, res) => {
  // CORS preflight
  if (req.method === "OPTIONS") {
    res.writeHead(204, corsHeaders());
    return res.end();
  }

  if (req.method === "POST" && req.url === "/token") {
    // Authenticate the calling browser session here (skipped for brevity).

    const upstream = await fetch(`${SANAS_IDENTITY_URL}/oauth2/api-key/login`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ apiKey: SANAS_API_KEY }),
    });

    const body = await upstream.text();
    res.writeHead(upstream.status, {
      "Content-Type": "application/json",
      ...corsHeaders(),
    });
    return res.end(body);
  }

  res.writeHead(404, corsHeaders());
  res.end();
}).listen(process.env.PORT || 4000);

function corsHeaders() {
  return {
    "Access-Control-Allow-Origin": ALLOWED_ORIGIN,
    "Access-Control-Allow-Methods": "POST, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type",
  };
}

Run it with SANAS_API_KEY=... node server.js.

Browser side

import { SanasSDK, type TokenResponse } from "@sanas-ai/browser-sdk";

const sdk = new SanasSDK();

await sdk.initialize({
  tokenProvider: async (): Promise<TokenResponse> => {
    const res = await fetch("https://your-backend.example.com/token", {
      method: "POST",
      credentials: "include", // if you use cookie sessions
    });
    if (!res.ok) throw new Error(`Token request failed: ${res.status}`);
    return res.json();
  },
});

tokenProvider is called once at initialize() and again any time the SDK needs a fresh token. Make it idempotent and quick — the SDK will time it out after 30 seconds.

Origin allowlisting

Every group in the Sanas Developer Console has an allowed origins list. All API calls the SDK makes from the browser include the page's origin; requests from a non-allowlisted origin are rejected. Add every origin your app runs on (production, staging, localhost during development) under Project settings → Allowed origins.

Quick start

import { SanasSDK, ProcessorState } from "@sanas-ai/browser-sdk";

const sdk = new SanasSDK();

// 1. Initialize the SDK with your token provider.
await sdk.initialize({
  tokenProvider: () =>
    fetch("/token", { method: "POST" }).then((r) => r.json()),
});

// 2. Capture the microphone.
const input = await navigator.mediaDevices.getUserMedia({ audio: true });

// 3. Create a processor. RI mode with an accent-translation model:
const processor = await sdk.createAudioProcessor(
  { input, modelName: "AT5.2" },
  {
    // Drive your UI off state, not off the reason string.
    // `Initializing` — session is being set up; output is silent.
    // `Ready`        — audio is flowing through the processor.
    // `Failed`       — fatal error; the processor can't recover. Destroy it,
    //                  optionally show `reason` to the user, and create a new
    //                  one if you want to keep going.
    // `Disconnected` — you called destroyAudioProcessor(); terminal.
    onStateChange: (state, reason) => {
      if (state === ProcessorState.Ready) enableMicButton();
      if (state === ProcessorState.Failed) {
        disableMicButton();
        showBanner(reason);
      }
    },
  }
);

// 4. Attach the processed output to an <audio> element.
const audio = document.querySelector("audio")!;
audio.srcObject = processor.outputStream;
audio.play();

// 5. When you're done, tear down.
await sdk.destroyAudioProcessor(processor);
await sdk.shutdown();

Remote Inferencing mode

For accent translation, speech enhancement, and similar single-model features. The model you use is provisioned in your Developer Console; the SDK verifies your group is entitled to the model before starting the call.

const processor = await sdk.createAudioProcessor(
  {
    input: micStream,
    modelName: "AT5.2", // as shown in the Developer Console
  },
  {
    onStateChange: (state, reason) => {
      if (state === ProcessorState.Ready) console.log("live");
      if (state === ProcessorState.Failed) console.error("failed:", reason);
    },
  }
);

audioElement.srcObject = processor.outputStream;

Common errors:

  • CreateProcessorErrorCode.FeatureNotEntitled — Your group doesn't have Remote Inferencing enabled at all. Contact your Sanas administrator.
  • CreateProcessorErrorCode.ModelNotEntitled — Your group has RI but not this specific model. Verify the exact name against the console.

Language Translation mode

Each LT processor handles one direction of translation — one input language, one output language. A processor delivers translated audio out and streams two kinds of side-channel data via callbacks:

  • onTranscript — a per-utterance transcript with partial and complete words for the detected input language.
  • onLanguageRoute — a per-utterance language-detection result telling you which input language the server detected on that utterance (and which output language it will render).

Two-person conversations use two processors

For a real conversation between two people who speak different languages, run two processors on the same page, one per direction, linked by a shared conversationId. For example, an English-speaking agent and a Spanish-speaking customer on the same call:

  • Processor A: agent's mic (languageIn: "en-US") → Spanish audio played to the customer (languageOut: "es-ES").
  • Processor B: customer's mic (languageIn: "es-ES") → English audio played to the agent (languageOut: "en-US").

Give both processors the same conversationId (any UUID you generate per call) and the server shares translation context between them — proper nouns, jargon, and prior context stay consistent across both directions.

const conversationId = crypto.randomUUID();

// Agent's mic → Spanish audio for the customer.
const outbound = await sdk.createAudioProcessor(
  {
    input: agentMicStream,
    languageIn: "en-US",
    languageOut: "es-ES",
    conversationId,
  },
  {
    onStateChange: (state, reason) => console.log("outbound", state, reason),
    onTranscript: (json) => console.log("outbound transcript", JSON.parse(json)),
    onLanguageRoute: (json) => console.log("outbound route", JSON.parse(json)),
  }
);
customerSpeakerElement.srcObject = outbound.outputStream;

// Customer's mic → English audio for the agent.
const inbound = await sdk.createAudioProcessor(
  {
    input: customerMicStream,
    languageIn: "es-ES",
    languageOut: "en-US",
    conversationId, // same UUID — shared context.
  },
  {
    onStateChange: (state, reason) => console.log("inbound", state, reason),
    onTranscript: (json) => console.log("inbound transcript", JSON.parse(json)),
    onLanguageRoute: (json) => console.log("inbound route", JSON.parse(json)),
  }
);
agentSpeakerElement.srcObject = inbound.outputStream;

conversationId is optional. Omit it if you're only running a single direction (e.g. a monologue translation) and don't need context sharing.

Single-direction usage

If you only need to translate one direction (for example, translating a broadcast into another language), a single processor is enough:

const processor = await sdk.createAudioProcessor(
  {
    input: micStream,
    languageIn: "en-US",
    languageOut: "es-MX",
  },
  {
    onStateChange: (state, reason) => console.log(state, reason),
    onTranscript: (json) => console.log("transcript", JSON.parse(json)),
    onLanguageRoute: (json) => console.log("route", JSON.parse(json)),
  }
);

onTranscript payload

Delivered as a JSON string; parse before use. Fires multiple times per utterance as the server refines its hypothesis. Each event carries a mix of complete (newly-finalized words) and partial (still-in-flight words).

{
  "type": "transcription",
  "transcription": {
    "lang": "en-US",
    "utterance_idx": 0,
    "complete": [],
    "partial": [
      { "word": "Hi",      "start": 0.0, "end": 0.0, "probability": 0.900 },
      { "word": ",",       "start": 0.0, "end": 0.0, "probability": 0.881 },
      { "word": " testing","start": 0.0, "end": 0.0, "probability": 0.941 },
      { "word": " of",     "start": 0.0, "end": 0.0, "probability": 0.936 }
    ]
  }
}

How the stream evolves for one utterance. partial is the current best guess and is replaced wholesale each event as the server refines it. complete is a delta of newly-finalized words in this event only — it is not the running total of everything finalized so far. To get the full utterance you must accumulate complete per utterance_idx on your side. end / start timestamps are reserved for future use and can be treated as 0.

For example, an utterance like "Hi, testing of language translation." arrives across several events. The complete deltas are [], [], ["Hi", ",", " testing", " of"], [" language"], [" translation"], ["."] — concatenating those deltas rebuilds the full sentence.

Minimal accumulator:

const utterances = new Map<number, string>();

onTranscript: (json) => {
  const { transcription } = JSON.parse(json);
  const prev = utterances.get(transcription.utterance_idx) ?? "";
  const finalized = transcription.complete.map((w) => w.word).join("");
  const pending = transcription.partial.map((w) => w.word).join("");
  utterances.set(transcription.utterance_idx, prev + finalized);
  render(prev + finalized + pending); // finalized + current guess
};

Trailing punctuation typically arrives in its own event a second after the last word — plan your UI to accept it gracefully rather than assuming the utterance is done as soon as partial empties.

utterance_idx starts at 0 and increments per utterance. It correlates with onLanguageRoute events so you can label transcripts with the language the server detected.

onLanguageRoute payload

Emitted once at the start of each utterance, before the transcript events for that utterance begin arriving. Tells you the input language the server detected on that utterance and the output language it's rendering into.

{
  "type": "language_route_chosen",
  "language_route_chosen": {
    "utterance_idx": 1,
    "lang_in": "en-US",
    "lang_out": "es-ES",
    "is_final": true
  }
}
  • utterance_idx — correlates with onTranscript events for the same utterance.
  • lang_in / lang_out — the concrete input and output languages for this utterance.
  • is_finaltrue when the server has locked in the routing for this utterance. In wildcard-mode edge cases the server may emit tentative routes with is_final: false before it commits; a subsequent event with the same utterance_idx and is_final: true supersedes the earlier one.

Sent for every utterance in both normal and wildcard mode. In wildcard mode, set languageIn or languageOut to "*" when creating the processor and the server detects the actual language for you; the value you get here is the concrete language the server chose. In normal mode (fixed languageIn / languageOut), the values echo back what the server confirmed for that utterance.

Changing languages mid-call

Rather than tearing down and recreating the processor (which drops the call), swap languages in place:

await processor.updateLanguageConfig({
  languageIn: "en-US",
  languageOut: "fr-FR",
});

Usage attribution correctly rotates across language changes — the SDK bills each language segment separately without losing history when you switch back.

Common error:

  • CreateProcessorErrorCode.FeatureNotEntitled — Language Translation isn't enabled for your group. Contact your Sanas administrator.

The processor lifecycle

Every processor moves through a small state machine, observable via onStateChange:

Initializing ──► Ready ──► Disconnected (consumer destroyed)
    │              │
    │              └──► Failed (mid-call error)
    │
    └──► Failed (setup error)
  • Initializing — the SDK is negotiating the media session. outputStream exists and is silent.
  • Ready — the media session is up. outputStream is now delivering processed audio.
  • Failed — a fatal error occurred. Read reason for a short human-readable explanation. The processor cannot be recovered; destroy it and create a new one.
  • Disconnected — the consumer called destroyAudioProcessor. Terminal, expected.

The outputStream reference is stable for the life of the processor — you can attach it to an <audio> element or WebRTC track before the processor reaches Ready, and it will start emitting audio automatically once the media path opens.

API reference

SanasSDK

class SanasSDK {
  initialize(params: InitParams): Promise<void>;
  isInitialized(): boolean;
  createAudioProcessor(
    params: AudioProcessorParams,
    callbacks?: ProcessorCallbacks
  ): Promise<IAudioProcessor>;
  destroyAudioProcessor(processor: IAudioProcessor): Promise<void>;
  getActiveProcessorCount(): number;
  shutdown(): Promise<void>;
}

initialize(params)

Establish a session with your token provider and validate your group's entitlements. Must be called before createAudioProcessor. Calling initialize twice throws InitErrorCode.AlreadyInitialized.

createAudioProcessor(params, callbacks?)

Create a processor for a single audio stream. You can hold multiple active processors on one SDK instance (each processes an independent stream).

Wire callbacks at construction — they are guaranteed to fire even for the initial Initializing → Failed transition on setup errors.

destroyAudioProcessor(processor)

Gracefully tear down a single processor. The output stream ends. Emits a final usage snapshot before returning so that usage accounting is complete.

shutdown()

Destroy all live processors, stop background reporting, clear all session state. Call this on page unload or when your app no longer needs Sanas. After shutdown, you can call initialize again to start a new session.

InitParams

interface InitParams {
  tokenProvider: () => Promise<TokenResponse>;
  logLevel?: "debug" | "info" | "warn" | "error" | "silent";
  jitterBufferTargetMs?: number;
}

TokenResponse

interface TokenResponse {
  access_token: string;
  user_id: string;
  group_id: string;
  expires_in?: number;
  authenticated_at?: string;
  token_type?: string;
}

This is exactly the shape returned by Sanas's OAuth login endpoint — your backend can forward the response body verbatim.

AudioProcessorParams

A discriminated union:

type AudioProcessorParams =
  | AudioProcessorModelParams
  | AudioProcessorLanguageParams;

interface AudioProcessorModelParams {
  input: MediaStream;
  modelName: string;
  sampleRate?: number;
  jitterBufferTargetMs?: number;
}

interface AudioProcessorLanguageParams {
  input: MediaStream;
  languageIn: string;
  languageOut: string;
  conversationId?: string;
  sampleRate?: number;
  jitterBufferTargetMs?: number;
}

ProcessorCallbacks

interface ProcessorCallbacks {
  onStateChange?: (state: ProcessorState, reason: string) => void;
  onTranscript?: (json: string) => void;
  onLanguageRoute?: (json: string) => void;
}

The two JSON callbacks fire only for LT processors. The payload is a raw JSON string — parse with JSON.parse on your side.

IAudioProcessor

interface IAudioProcessor {
  readonly outputStream: MediaStream;
  getState(): ProcessorState;
  updateInputStream(input: MediaStream): Promise<void>;
  updateLanguageConfig(config: {
    languageIn: string;
    languageOut: string;
    conversationId?: string;
  }): Promise<void>;
}

updateInputStream swaps the mic mid-call (device change, headset unplug) without dropping the session.

updateLanguageConfig throws if called on a model-mode processor.

ProcessorState

enum ProcessorState {
  Initializing = "initializing",
  Ready = "ready",
  Failed = "failed",
  Disconnected = "disconnected",
  Unknown = "unknown",
}

Error handling

All SDK errors are instances of SdkError with a machine-readable code and a human-readable message.

import { SdkError, InitErrorCode } from "@sanas-ai/browser-sdk";

try {
  await sdk.initialize({ tokenProvider });
} catch (err) {
  if (err instanceof SdkError && err.code === InitErrorCode.OriginNotAllowed) {
    showBanner("This origin isn't allowed. Contact your admin.");
  } else {
    throw err;
  }
}

InitErrorCode

| Code | Meaning | Recommended action | |---|---|---| | AlreadyInitialized | initialize was called on an already-initialized SDK. | Call shutdown() first, or reuse the existing session. | | InitializationInProgress | A concurrent initialize is still running. | Await the first call before starting another. | | InvalidParameters | tokenProvider was missing or not a function. | Check the call site. | | AuthenticationFailed | Your backend returned an error, or the token payload was malformed. | Log the message; verify your backend is returning {access_token, user_id, group_id, ...}. | | OriginNotAllowed | This page's origin isn't in the group's allowed list. | Add the origin in Project settings → Allowed origins. | | NetworkError | Network / CORS failure reaching either your backend or the Sanas console. | Message includes which stage failed; check CORS on your backend or inspect DevTools network tab for the console request. | | UnknownError | Anything else. | Read the message; open a support ticket if it recurs. |

CreateProcessorErrorCode

| Code | Meaning | Recommended action | |---|---|---| | SdkNotInitialized | You didn't call initialize, or the session was lost. | Initialize (or reinitialize) before creating processors. | | InvalidParameters | Required field missing or unsafe characters in a param. | Check modelName / languageIn / languageOut values. | | UnsupportedSampleRate | The requested sample rate isn't supported. | Use 8000, 16000, or 48000. | | ModelNotEntitled | The model name isn't in this group's entitlements. | Verify the exact name in the console. Case-sensitive. | | FeatureNotEntitled | The feature (RI or LT) isn't enabled at group level. | Contact your Sanas administrator. | | OriginNotAllowed | Same as init — origin got revoked between init and this call. | Update Project settings → Allowed origins. | | NetworkError | Console API unreachable for the entitlement check. | Retry; check DevTools. | | UnknownError | Anything else. | Read the message. |

Runtime state changes

Once a processor is created, transient failures surface through onStateChange(Failed, reason), not by throwing. Wire the callback at construction so you never miss an early failure:

sdk.createAudioProcessor(params, {
  onStateChange: (state, reason) => {
    if (state === ProcessorState.Failed) {
      // reason is a short user-facing string
      // e.g. "Server rejected the call (auth failure)"
      // e.g. "Media connection lost"
      // e.g. "Authentication lost: origin not allowed for this group"
      // e.g. "Entitlement revoked: language_translation is no longer available for this group"
      showErrorBanner(reason);
    }
  },
});

Advanced options

Sample rate

Default is 16000 Hz. Supported: 8000 / 16000 / 48000. Set per processor:

sdk.createAudioProcessor({ input, modelName: "...", sampleRate: 48000 }, ...);

Rates outside the supported set throw UnsupportedSampleRate.

Jitter buffer

For bumpy networks, increase the receive-side buffer to smooth playback at the cost of extra latency. Default is chosen by the SDK; override globally in InitParams or per processor:

await sdk.initialize({ tokenProvider, jitterBufferTargetMs: 200 });
// or
sdk.createAudioProcessor(
  { input, modelName: "...", jitterBufferTargetMs: 100 },
  ...
);

Swapping input mid-call

Users unplug headsets or switch devices. Update in place instead of tearing down:

const newInput = await navigator.mediaDevices.getUserMedia({
  audio: { deviceId: newDeviceId },
});
await processor.updateInputStream(newInput);

The active call stays up; the outgoing track is swapped.

Multiple concurrent processors

You can run more than one processor per SDK instance — e.g. a mic in RI mode and a system-audio stream in LT mode. Each is fully independent.

Logging

Set logLevel: "debug" in InitParams to see the SDK's internal logs in DevTools. Default is warn. Use silent in production if you want no console output.

Troubleshooting

"Origin not allowed" during initialize
Your page's origin (protocol + host + port) isn't in the group's allowed list. Add it under Project settings → Allowed origins. Note that http://localhost:5173 and http://localhost:3000 are different origins — add every dev port you use.

Processor reaches Failed immediately with "Server rejected the call (auth failure)"
The access token expired between tokenProvider returning it and the media session opening, or your backend returned a stale token.

Processor reaches Failed mid-call with "Media connection lost"
Network dropped. The SDK does not auto-reconnect a lost media session. Destroy the processor and create a new one.

Metrics appear delayed
The SDK batches usage reports on a 1-minute cadence. If you tear down a processor, the final snapshot is captured before the underlying session closes.