{"_id":"@aihumanity/voice-sdk","_rev":"2-dccd416ae43612782c145e7d7f5e52d7","name":"@aihumanity/voice-sdk","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@aihumanity/voice-sdk","version":"0.1.0","keywords":["ultravox","voice","ai","sdk","webrtc","transcript","emotion","aihumanity","eimi"],"author":{"name":"AIHumanity"},"license":"MIT","_id":"@aihumanity/voice-sdk@0.1.0","maintainers":[{"name":"fdchiu","email":"fdchiu@gmail.com"}],"homepage":"https://github.com/fdchiu/aihumanity-voice-sdk#readme","bugs":{"url":"https://github.com/fdchiu/aihumanity-voice-sdk/issues"},"dist":{"shasum":"f5860ecb6a3203cf6accb9b6ff6cd36afd097a44","tarball":"https://registry.npmjs.org/@aihumanity/voice-sdk/-/voice-sdk-0.1.0.tgz","fileCount":13,"integrity":"sha512-B4DoiyxxQJl+hTtxukh0ND4C8rD6kJmojDrFosXZW5o6FhRI+5uYXjtEsqaJXJYG9bEpKPwvwYyYQEmMYvwQjg==","signatures":[{"sig":"MEUCIHSjsZsiyoE50Ga4EYozCBzA7qYkDNMOSTDWb56ZPRtnAiEAq6//hhnNlYnNN4QGlZJqtw++k7uBbhQHzKR4PvPN39M=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":262599},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./react":{"types":"./dist/react.d.ts","default":"./dist/react.js"},"./widget":{"types":"./dist/widget.d.ts","default":"./dist/widget.js"},"./package.json":"./package.json"},"gitHead":"37e35c5d54a8872f2f7ba398b078bfd8f3d3a761","scripts":{"dev":"tsup --watch","demo":"npm run build:all && npx http-server -p 5173 -c-1 .","build":"tsup","check":"npm run typecheck && npm run build","clean":"rm -rf dist","prepack":"npm run typecheck && npm run build","build:all":"npm run build && npm run build:iife","typecheck":"tsc --noEmit","build:iife":"tsup --config tsup.iife.config.ts","prebuild:iife":"find dist -name '*.d.ts' -delete && find dist -name '*.map' -delete"},"_npmUser":{"name":"fdchiu","email":"fdchiu@gmail.com"},"overrides":{"csstype":"3.1.3"},"repository":{"url":"git+https://github.com/fdchiu/aihumanity-voice-sdk.git","type":"git"},"_npmVersion":"10.8.2","description":"JavaScript SDK for AIHumanity / eimi voice AI calls — wraps Ultravox with call-state tracking, transcripts, and emotion detection.","directories":{},"_nodeVersion":"20.19.0","dependencies":{"ultravox-client":"^0.5.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","react":"^18.2.0","typescript":"^5.4.0","@types/react":"^18.2.0"},"peerDependencies":{"react":">=17.0.0"},"peerDependenciesMeta":{"react":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/voice-sdk_0.1.0_1778367502783_0.9053885630325551","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@aihumanity/voice-sdk","version":"0.1.1","description":"JavaScript SDK for AIHumanity / eimi voice AI calls — wraps Ultravox with call-state tracking, transcripts, and emotion detection.","license":"MIT","author":{"name":"AIHumanity"},"type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","repository":{"type":"git","url":"git+https://github.com/fdchiu/aihumanity-voice-sdk.git"},"bugs":{"url":"https://github.com/fdchiu/aihumanity-voice-sdk/issues"},"homepage":"https://github.com/fdchiu/aihumanity-voice-sdk#readme","exports":{".":{"types":"./dist/index.d.ts","require":"./dist/index.cjs","default":"./dist/index.js"},"./react":{"types":"./dist/react.d.ts","require":"./dist/react.cjs","default":"./dist/react.js"},"./widget":{"types":"./dist/widget.d.ts","require":"./dist/widget.cjs","default":"./dist/widget.js"},"./package.json":"./package.json"},"engines":{"node":">=18"},"scripts":{"build":"tsup","prebuild:iife":"find dist -name '*.d.ts' -delete && find dist -name '*.map' -delete","build:iife":"tsup --config tsup.iife.config.ts","build:all":"npm run build && npm run build:iife","check":"npm run typecheck && npm run build","typecheck":"tsc --noEmit","dev":"tsup --watch","clean":"rm -rf dist","demo":"npm run build:all && npx http-server -p 5173 -c-1 .","prepack":"npm run typecheck && npm run build"},"keywords":["ultravox","voice","ai","sdk","webrtc","transcript","emotion","aihumanity","eimi"],"dependencies":{"ultravox-client":"^0.5.0"},"peerDependencies":{"react":">=17.0.0"},"peerDependenciesMeta":{"react":{"optional":true}},"devDependencies":{"@types/react":"^18.2.0","react":"^18.2.0","tsup":"^8.0.0","typescript":"^5.4.0"},"overrides":{"csstype":"3.1.3"},"publishConfig":{"access":"public"},"_id":"@aihumanity/voice-sdk@0.1.1","gitHead":"37e35c5d54a8872f2f7ba398b078bfd8f3d3a761","_nodeVersion":"20.19.0","_npmVersion":"10.8.2","dist":{"integrity":"sha512-XB5TiPYieDBBtL8jSWOc0FRoBMQsmJE3VpTEX3gEIRypP0ED3bearxSW4HnTIzCirJPURbvfNVub5TgJF8DmJw==","shasum":"b102bf3255f5601fe9097a59853ff973dfc8f45e","tarball":"https://registry.npmjs.org/@aihumanity/voice-sdk/-/voice-sdk-0.1.1.tgz","fileCount":23,"unpackedSize":506657,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIAxlRwqR5hu5fIZrnPNKxPEuIoQCVz3rPbcLtGmbm6Z3AiBf9pJDSTudlrT04O5tI9j4fWmsmyPVjD+6SE7OqBmI/w=="}]},"_npmUser":{"name":"fdchiu","email":"fdchiu@gmail.com"},"directories":{},"maintainers":[{"name":"fdchiu","email":"fdchiu@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/voice-sdk_0.1.1_1778368539191_0.028067219920760333"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-09T22:58:22.688Z","modified":"2026-05-09T23:15:39.467Z","0.1.0":"2026-05-09T22:58:22.940Z","0.1.1":"2026-05-09T23:15:39.361Z"},"bugs":{"url":"https://github.com/fdchiu/aihumanity-voice-sdk/issues"},"author":{"name":"AIHumanity"},"license":"MIT","homepage":"https://github.com/fdchiu/aihumanity-voice-sdk#readme","keywords":["ultravox","voice","ai","sdk","webrtc","transcript","emotion","aihumanity","eimi"],"repository":{"type":"git","url":"git+https://github.com/fdchiu/aihumanity-voice-sdk.git"},"description":"JavaScript SDK for AIHumanity / eimi voice AI calls — wraps Ultravox with call-state tracking, transcripts, and emotion detection.","maintainers":[{"name":"fdchiu","email":"fdchiu@gmail.com"}],"readme":"# @aihumanity/voice-sdk\n\nA small, batteries-included JavaScript SDK for embedding AIHumanity / **eimi**\nvoice AI calls on any web page.\n\nIt wraps [`ultravox-client`](https://www.npmjs.com/package/ultravox-client) and\nadds the things you almost always end up writing yourself:\n\n- One-call setup: SDK fetches the `joinUrl` from your eimi backend and joins the call for you.\n- A semantic call-state machine: `idle` → `connecting` → `connected` → `listening` / `speaking` / `thinking` → `disconnecting` → `idle`.\n- Live transcripts, with `transcript` / `transcripts` events and a snapshot getter.\n- Vocal-emotion extraction from `[EMOTION_CONTEXT]` data messages produced by the eimi emotion bridge (configurable regex).\n- Mic / speaker mute helpers.\n- A pre-built React hook (`@aihumanity/voice-sdk/react`).\n- A pre-built floating-button widget (`@aihumanity/voice-sdk/widget`) — drop a single `<script>` on any site.\n\n## Installation\n\n```bash\nnpm install @aihumanity/voice-sdk ultravox-client\n# or\npnpm add @aihumanity/voice-sdk ultravox-client\n```\n\n`ultravox-client` is a hard runtime dependency; it ships separately so multiple\nSDKs / apps can dedupe it. React is an *optional* peer dependency — only needed\nif you import the React adapter.\n\nThe SDK is ESM-only because `ultravox-client` is ESM-only. Use `import` syntax\nor a bundler that supports ESM packages.\n\nFor zero-build `<script>`-tag use you can also load the IIFE bundle directly\nfrom `dist/aihumanity-voice.iife.js` (see the demo).\n\n## Getting your credentials\n\nBefore writing any code you need a developer account. The whole process takes\nabout two minutes and is self-service.\n\n### 1 — Sign up at the developer portal\n\nGo to **[portal.eimi.ai](https://portal.eimi.ai)** and create an account.\nOnce verified you land on your dashboard.\n\n### 2 — Note your Key ID\n\nIn the **API Keys** tab you'll see two values:\n\n| Field | What it is | Where you use it |\n| --- | --- | --- |\n| **SDK Key ID** | Identifies your developer account | `publicKey` option *or* as the key ID in HMAC signing |\n| **SDK Key Secret** | Signs server-to-server requests | Never put this in browser code |\n\nThe Key ID is the same value regardless of which auth mode you choose.\n\n### 3 — Choose your integration path\n\n**No backend (simplest)**\n\nUse your **Key ID** directly as `publicKey`. You also need to tell the server\nwhich origins are allowed to use it — otherwise every request is rejected.\n\nIn the portal under **API Keys → Allowed Origins**, add the exact origin(s)\nyour site runs on:\n\n```\nhttps://myapp.com\nhttps://staging.myapp.com\nhttp://localhost:5173     ← add this while developing locally\n```\n\nAn origin is `scheme + host + port` — no path, no trailing slash.\n\nThen in your code:\n\n```ts\nimport { VoiceCall } from \"@aihumanity/voice-sdk\";\n\nconst call = new VoiceCall({\n  apiUrl:    \"https://api.eimi.ai\",\n  publicKey: \"YOUR_KEY_ID\",   // from the portal — safe to commit\n  agentName: \"YourAgent\",\n  username:  \"visitor\",\n});\n```\n\n**With a backend (more control)**\n\nKeep your Key ID and Key Secret on your server and build a small proxy endpoint\nthat HMAC-signs the join request. The browser calls your endpoint via\n`fetchJoinUrl` and never touches the eimi API directly:\n\n```ts\n// In your frontend:\nconst call = new VoiceCall({\n  fetchJoinUrl: async () => {\n    const res = await fetch(\"/api/create-voice-call\", { method: \"POST\" });\n    if (!res.ok) throw new Error(\"Could not start call\");\n    return res.json(); // { joinUrl, callId, sessionToken }\n  },\n  agentName: \"YourAgent\",\n});\n```\n\n```js\n// On your server (/api/create-voice-call):\n// Sign the request with your Key ID + Key Secret using HMAC-SHA256.\n// See the Authentication section below for the exact signing scheme.\n```\n\nYou don't need to register any Allowed Origins when using the server-side path,\nbecause the HMAC signature — not the browser Origin — is what authenticates\nthe request.\n\n---\n\n## Authentication — choosing the right method\n\nThe SDK supports three auth patterns. Pick the one that matches your deployment.\n\n### Option A — `fetchJoinUrl` (full control)\n\nSupply your own async function that returns `{ joinUrl, callId?, sessionToken? }`.\nUse this when your backend already has an endpoint that creates the Ultravox call\nsession and you want the SDK to stay out of the request entirely.\n\n```ts\nimport { VoiceCall } from \"@aihumanity/voice-sdk\";\n\nconst call = new VoiceCall({\n  fetchJoinUrl: async () => {\n    const res = await fetch(\"/api/create-call\", { method: \"POST\" });\n    if (!res.ok) throw new Error(\"Could not start call\");\n    return res.json(); // { joinUrl, callId, sessionToken? }\n  },\n  agentName: \"DavidChiu\",\n});\n```\n\nThis is the **recommended approach for production web apps**. Your server holds\nthe credentials; the browser never sees them.\n\n> **`sessionToken`** — When your backend returns a short-lived, call-scoped JWT\n> alongside `joinUrl` / `callId`, include it in the response object. The SDK\n> forwards it to `pollEmotion(callId, sessionToken)` so emotion polling can\n> authenticate without a long-lived secret in the browser.\n\n---\n\n### Option B — `publicKey` (browser-direct, no backend)\n\nUse your **Key ID** from the developer portal directly in browser code. The\nserver validates requests using the browser's `Origin` header against your\nregistered Allowed Origins list — see [Getting your credentials](#getting-your-credentials)\nfor the signup and origin registration steps.\n\n```ts\nconst call = new VoiceCall({\n  apiUrl:    \"https://api.eimi.ai\",\n  publicKey: \"YOUR_KEY_ID\",   // Key ID from developer portal — safe to commit\n  agentName: \"YourAgent\",\n  username:  \"visitor\",\n});\n```\n\nThe SDK sends `X-Public-Key: <publicKey>` and POSTs to\n`${apiUrl}/v1/voice/joinurl`. Override the path with `joinUrlPath` if needed.\n\n> Requests from origins not in your Allowed Origins list are rejected with 403.\n> Add `http://localhost:PORT` while developing locally.\n\n---\n\n### Option C — `fetchJoinUrl` with HMAC backend proxy\n\nKeep your Key ID and Key Secret on your server. Your backend endpoint signs the\njoin request; the browser calls your endpoint via `fetchJoinUrl`.\n\n```ts\n// Frontend — no credentials in the browser at all:\nconst call = new VoiceCall({\n  fetchJoinUrl: async () => {\n    const res = await fetch(\"/api/create-voice-call\", { method: \"POST\" });\n    if (!res.ok) throw new Error(\"Could not start call\");\n    return res.json(); // { joinUrl, callId, sessionToken }\n  },\n  agentName: \"YourAgent\",\n});\n```\n\nYour server endpoint signs requests to `POST /v1/voice/joinurl` using\nHMAC-SHA256:\n\n```js\n// Server-side signing (Node example):\nconst crypto    = require(\"crypto\");\nconst timestamp = Date.now().toString();\nconst method    = \"POST\";\nconst path      = \"/v1/voice/joinurl\";\nconst canonical = `${timestamp}\\n${method}\\n${path}`;\nconst signature = crypto\n  .createHmac(\"sha256\", YOUR_KEY_SECRET)\n  .update(canonical)\n  .digest(\"base64\");\n\nconst response = await fetch(`https://api.eimi.ai${path}`, {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\":    \"application/json\",\n    \"X-SDK-Key-Id\":    YOUR_KEY_ID,\n    \"X-SDK-Timestamp\": timestamp,\n    \"X-SDK-Signature\": signature,\n  },\n  body: JSON.stringify({ agentName: \"YourAgent\", username: req.user.id }),\n});\nreturn response.json(); // forward { joinUrl, callId, sessionToken } to the browser\n```\n\n`YOUR_KEY_ID` and `YOUR_KEY_SECRET` come from the developer portal. The secret\nnever leaves your server.\n\n> The `authToken` option (Bearer JWT) also maps to this server-side path but is\n> intended for internal operator use. External developers should use `fetchJoinUrl`\n> with HMAC signing as shown above.\n\n---\n\n## Quick start (vanilla TypeScript / JavaScript)\n\n```ts\nimport { VoiceCall, CallStatus } from \"@aihumanity/voice-sdk\";\n\n// Option A — recommended for production\nconst call = new VoiceCall({\n  fetchJoinUrl: async () => {\n    const res = await fetch(\"/.netlify/functions/create-call\", { method: \"POST\" });\n    if (!res.ok) throw new Error(\"Could not create call session.\");\n    return res.json(); // { joinUrl, callId, sessionToken }\n  },\n  // Poll server-side emotion every 15 s using the call-scoped session token.\n  pollEmotion: async (callId, sessionToken) => {\n    const params = new URLSearchParams({ callId });\n    if (sessionToken) params.set(\"sessionToken\", sessionToken);\n    const res = await fetch(`/.netlify/functions/get-emotion?${params}`);\n    if (!res.ok) return null;\n    const data = await res.json();\n    return data?.emotion ?? null;\n  },\n  emotionPollIntervalMs: 15_000,\n  agentName: \"DavidChiu\",\n});\n\ncall.on(\"status\",     (s) => console.log(\"call status:\", s));\ncall.on(\"transcript\", (t) => console.log(t.speaker, t.text));\ncall.on(\"emotion\",    (e) => console.log(\"emotion:\", e.label));\ncall.on(\"error\",      (err) => console.error(err));\n\ndocument.querySelector(\"#start\")!.addEventListener(\"click\", () => call.start());\ndocument.querySelector(\"#stop\")!.addEventListener(\"click\",  () => call.end());\n```\n\n### How the join URL is fetched\n\nThe SDK resolves credentials in this order:\n\n1. **`fetchJoinUrl`** — calls your function; skips all built-in request logic.\n2. **`publicKey`** — POSTs to `${apiUrl}/v1/voice/joinurl` with `X-Public-Key`.\n3. **`authToken`** — POSTs to `${apiUrl}/ultravox/secure/joinurl` with `Authorization: Bearer`.\n\nThe backend response must contain at least `joinUrl`. Optional fields:\n\n```jsonc\n{\n  \"joinUrl\":      \"https://...\",          // required\n  \"callId\":       \"uuid\",                 // forwarded to pollEmotion\n  \"sessionToken\": \"eyJ...\",              // short-lived JWT for emotion polling\n  \"emotion\":      { \"dataConnectionEnabled\": true, ... }\n}\n```\n\nOverride the default path for options B or C with `joinUrlPath`:\n\n```ts\nnew VoiceCall({ publicKey: \"pk_...\", joinUrlPath: \"/v1/voice/joinurl\", ... })\n```\n\n### Session tokens and emotion polling\n\nWhen the backend returns a `sessionToken` alongside the join URL, the SDK stores\nit for the duration of the call. If you provide a `pollEmotion` callback, the SDK\npasses both `(callId, sessionToken)` so your function can authenticate the polling\nrequest without embedding a service credential in browser code:\n\n```ts\npollEmotion: async (callId, sessionToken) => {\n  const headers: Record<string, string> = {};\n  if (sessionToken) headers[\"Authorization\"] = `Bearer ${sessionToken}`;\n  const res = await fetch(`/api/calls/${callId}/emotion`, { headers });\n  if (!res.ok) return null;\n  const { emotion } = await res.json();\n  return emotion ?? null;\n},\n```\n\n## React\n\n```tsx\nimport { useVoiceCall, CallStatus } from \"@aihumanity/voice-sdk/react\";\n\n// Define stable callbacks outside the component so the hook doesn't re-run.\nasync function fetchJoinUrl() {\n  const res = await fetch(\"/api/create-call\", { method: \"POST\" });\n  if (!res.ok) throw new Error(\"Could not start call\");\n  return res.json(); // { joinUrl, callId, sessionToken }\n}\n\nasync function pollEmotion(callId: string, sessionToken?: string) {\n  const params = new URLSearchParams({ callId });\n  if (sessionToken) params.set(\"sessionToken\", sessionToken);\n  const res = await fetch(`/api/emotion?${params}`);\n  if (!res.ok) return null;\n  const data = await res.json();\n  return data?.emotion ?? null;\n}\n\nconst VOICE_OPTS = { fetchJoinUrl, pollEmotion, emotionPollIntervalMs: 15_000 };\n\nfunction TalkButton() {\n  const {\n    status, isLive, isBusy, transcripts, lastEmotion,\n    micMuted, error, start, end, toggleMicMute,\n  } = useVoiceCall(VOICE_OPTS);\n\n  return (\n    <div>\n      <button onClick={isLive || isBusy ? end : start}>\n        {isLive ? \"End\" : isBusy ? \"Connecting…\" : \"Talk\"}\n      </button>\n      <button onClick={toggleMicMute} disabled={!isLive}>\n        {micMuted ? \"Unmute\" : \"Mute\"}\n      </button>\n      {error && <p style={{ color: \"tomato\" }}>{error.message}</p>}\n      {lastEmotion && <p>Vocal emotion: {lastEmotion}</p>}\n      <ul>\n        {transcripts.map((t, i) => (\n          <li key={i}><b>{t.speaker}:</b> {t.text}</li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n```\n\nStatus values map directly onto `CallStatus`:\n\n| `CallStatus`     | When you'll see it                                              |\n| ---------------- | --------------------------------------------------------------- |\n| `IDLE`           | Before `start()` and after the call has fully ended.           |\n| `CONNECTING`     | Fetching the join URL or running WebRTC handshake.             |\n| `CONNECTED`      | Call is live and the agent is waiting (no one is talking).     |\n| `LISTENING`      | Mic is open and capturing user audio.                          |\n| `THINKING`       | Agent is reasoning about the user's last utterance.            |\n| `SPEAKING`       | Agent is generating audio.                                     |\n| `DISCONNECTING`  | `end()` was called; teardown in progress.                      |\n| `DISCONNECTED`   | Terminal state from ultravox-client; SDK normalises back to `IDLE`. |\n\n## Floating widget\n\nMount a self-contained mic button + call panel anywhere:\n\n```ts\nimport { mountFloatingWidget } from \"@aihumanity/voice-sdk/widget\";\n\n// Option A — server-side proxy (recommended)\nmountFloatingWidget({\n  fetchJoinUrl: () =>\n    fetch(\"/api/create-call\", { method: \"POST\" }).then((r) => r.json()),\n  agentName: \"DavidChiu\",\n  persona: {\n    name: \"David Chiu\",\n    title: \"Founder & CEO · AIHumanity\",\n    initials: \"DC\",\n    intro: \"Have a real-time voice conversation with David — ask anything.\",\n  },\n});\n\n// Option B — browser-direct with a public key\nmountFloatingWidget({\n  apiUrl:    \"https://api.eimi.ai\",\n  publicKey: \"pk_live_abc123\",  // register your origin in the developer portal first\n  agentName: \"DavidChiu\",\n  persona:   { name: \"David Chiu\", initials: \"DC\" },\n});\n```\n\nOr via plain `<script>` (IIFE build):\n\n```html\n<script src=\"https://your.cdn/aihumanity-voice.iife.js\"></script>\n<script>\n  // Browser-direct with public key\n  AIHVoice.mountFloatingWidget({\n    apiUrl:    \"https://api.eimi.ai\",\n    publicKey: \"pk_live_abc123\",\n    agentName: \"DavidChiu\",\n    persona:   { name: \"David Chiu\", initials: \"DC\" },\n  });\n</script>\n```\n\nThe widget renders inside a Shadow DOM, so its CSS won't fight your site's.\n\n## Events reference\n\n| Event           | Payload                                  | Notes                                       |\n| --------------- | ---------------------------------------- | ------------------------------------------- |\n| `status`        | `CallStatus`                             | Coarse semantic status.                    |\n| `raw_status`    | `string`                                 | Underlying ultravox-client status string.  |\n| `transcript`    | `Transcript`                             | Fired per added/updated entry.             |\n| `transcripts`   | `Transcript[]`                           | Snapshot after each transcript update.    |\n| `emotion`       | `{ label: string, raw: unknown }`        | Emitted when emotion regex matches a data message. |\n| `data_message`  | `unknown`                                | Every `experimental_message` payload.      |\n| `mic_muted`     | `boolean`                                |                                             |\n| `speaker_muted` | `boolean`                                |                                             |\n| `contact_saved` | `void`                                   | Heuristic on agent transcript.             |\n| `warning`       | `string`                                 | E.g. emotion bridge not configured.        |\n| `error`         | `Error`                                  | Fatal during start/operation.              |\n| `ended`         | `void`                                   | Fires once the underlying session disconnects. |\n\n## API surface\n\n```ts\nclass VoiceCall {\n  constructor(options: VoiceCallOptions);\n\n  // Read-only state\n  readonly status: CallStatus;\n  readonly callId: string | null;\n  readonly transcripts: Transcript[];\n  readonly lastEmotion: string | null;\n  readonly contactSaved: boolean;\n  readonly isMicMuted: boolean;\n  readonly isSpeakerMuted: boolean;\n  readonly emotionMeta: ServerEmotionMeta | null;\n  readonly rawSession: UltravoxSession | null;\n\n  // Events\n  on<E>(event, listener): () => void;     // returns unsubscribe\n  off<E>(event, listener): void;\n  once<E>(event, listener): () => void;\n\n  // Control\n  start(): Promise<void>;\n  end(): Promise<void>;\n  muteMic(): void;          unmuteMic(): void;          toggleMicMute(): boolean;\n  muteSpeaker(): void;      unmuteSpeaker(): void;      toggleSpeakerMute(): boolean;\n  sendText(text: string, deferResponse?: boolean): void;\n  sendData(obj: unknown): void;\n  dispose(): void;\n}\n```\n\n## Building from source\n\n```bash\nnpm install\nnpm run build         # ESM + CJS + .d.ts (library mode)\nnpm run build:iife    # bundled <script> tag build\nnpm run build:all\nnpm run typecheck\n```\n\nThe `examples/demo.html` page loads `dist/aihumanity-voice.iife.js`, so run\n`npm run build:all` once before opening it. The `npm run demo` script does\nboth for you.\n\n## Publishing\n\nBefore publishing, verify the package still builds and the tarball contents are\nwhat npm should receive:\n\n```bash\nnpm whoami\nnpm pack --dry-run\n```\n\nPublish the scoped package publicly:\n\n```bash\nnpm publish --access public\n```\n\nIf npm returns `E403` with `Two-factor authentication or granular access token\nwith bypass 2fa enabled is required`, the package metadata is usually not the\nproblem. Use one of these auth paths:\n\n```bash\n# Interactive publish with a current 2FA code.\nnpm publish --access public --otp 123456\n\n# Token publish: configure a granular npm token with read/write package access\n# for @aihumanity and \"bypass 2FA\" enabled.\nnpm config set //registry.npmjs.org/:_authToken npm_xxx\nnpm publish --access public\n```\n\nNewer npm versions protect token reads, so `npm config get\n//registry.npmjs.org/:_authToken` may fail even when a token is configured. Use\n`npm config list --location=user` to confirm the token entry exists without\nprinting the secret.\n\n## Roadmap\n\n- Streaming partial-emotion confidences (instead of just last label).\n- Pluggable transcript renderers (Markdown, ReactMarkdown).\n- Server-side helper to mint short-lived per-user JWTs.\n- Unit tests for emotion-pattern matching and status mapping.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}