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

ania-avatar-react

v1.16.1

Published

React component library for ANIA Avatar with TTS, STT, and chatbot integration

Readme

ania-avatar-react

React library for animated avatars with TTS (7 providers including browser-side Piper ONNX and the on-device neural pt-BR Robs Voice beta), STT, lip sync, action frames, chatbot/LLM integration, and file attachments.

Installation

npm install ania-avatar-react

Peer dependencies: react >= 17.0.0, react-dom >= 17.0.0

Required: Load AniaPlayer before using. Pin a specific version and add a Subresource Integrity hash so a compromised or MITM'd CDN cannot run tampered JavaScript in your page:

<script
  src="https://cdn.example.com/[email protected]"
  integrity="sha384-REPLACE_WITH_REAL_HASH_OF_THE_PINNED_FILE"
  crossorigin="anonymous"
></script>

Generate the hash for the exact file you pin, e.g. curl -s https://cdn.example.com/[email protected] | openssl dgst -sha384 -binary | openssl base64 -A, and prefix the result with sha384-. The browser will refuse to execute the script if the delivered bytes don't match.


Quick Start

import { AvatarChatbot } from 'ania-avatar-react';

<AvatarChatbot
  avatarUrl="https://example.com/avatar.ania"
  avatarPassword="secret"
  webhookUrl="https://n8n.example.com/webhook/chat"
  assistantName="Luna"
  userName="User"
  enableTTS={true}
  enableSTT={true}
  enableAttachments={true}
/>

Avatar file types — only MARKETPLACE plays in the browser

Not every .ania works on the web. This is the single most common integration failure, and the password is never the cause.

| Export type | Header | Password | Frames | Web player | |-------------|--------|----------|--------|------------| | MARKETPLACE | ANIA3.0, zeroed HMAC/salt/IV | none | plain base64 WEBP | ✅ works | | Password-exported (no license block) | ANIA1.0 / ANIA2.0 | required | plain base64 WEBP after decrypt | ✅ works | | PERSONAL / licensed export | ANIA1.0 + license.type: "PERSONAL" | required | AES-encrypted, per frame | ❌ desktop only |

A PERSONAL export decrypts fine in the browser — the JSON, the animation ranges and the frame count all come out right — but every frame is still ciphertext. Only the desktop AniaPlayer can unlock them: it calls the license server with a hardware id + client IP to fetch the per-file decrypt key. A browser has neither, so the key never arrives.

Symptom if you ship one anyway (older versions of this library): a blank canvas plus an endless console loop from the player bundle, one line per frame, on every animation tick:

aniaplayer.min.js: Erro ao renderizar frame 171: Error: Erro ao carregar frame 171
aniaplayer.min.js: Erro ao renderizar frame 172: Error: Erro ao carregar frame 172
...

Since 1.11.5 the library detects this before the player starts and fails once with an explicit message (avatar.error.encryptedFrames) instead — surfaced in the widget's error state and via onError. The cached copy of the bad file is dropped too, so re-uploading a MARKETPLACE file at the same URL takes effect immediately.

Fix: re-export the avatar as MARKETPLACE in the Ania studio/desktop app and point avatarUrl at that file (no avatarPassword needed). There is no client-side workaround — the key does not exist in the browser.

Check a file yourself before mounting the widget:

import { decryptAniaFile, inspectAvatarFrames } from 'ania-avatar-react';

const buf = await (await fetch(url)).arrayBuffer();
const data = await decryptAniaFile(buf, password ?? '');
const info = inspectAvatarFrames(data);
// { playable: false, reason: 'encrypted-frames', licenseType: 'PERSONAL',
//   frameCount: 537, frameFormat: null }
if (!info.playable) throw new Error('Use a MARKETPLACE export');

Components

<AvatarChatbot />

Full chatbot with avatar, TTS, STT, file uploads, and webhook integration.

Avatar Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | avatarUrl | string | - | URL to .ania or .json avatar file. Must be a MARKETPLACE export — see Avatar file types | | avatarPassword | string | - | Password for encrypted .ania files | | avatarData | object | - | Direct avatar data (alternative to URL) | | authToken | string | - | Bearer token for avatar URL fetch | | position | 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right' | 'bottom-right' | Screen position | | width | number | 400 | Avatar width (px) | | height | number | 300 | Avatar height (px) | | transparent | boolean | false | Transparent avatar background | | theme | 'dark' | 'light' | 'blue' | 'purple' | 'dark' | Color theme | | startMinimized | boolean | false | Start in minimized state | | preserveQuality | boolean | true | Maintain original resolution | | draggable | boolean | true | Allow drag when minimized | | mobileMinimizedSize | number | 60 | Minimized size on mobile (px) | | mobileBreakpoint | number | 768 | Mobile breakpoint (px) | | avatarMaxHeightVh | number | 34 | Share of viewport height the avatar stage may take once the chat is open. See below. |

Animation Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | idleSpeed | number | 1 | Idle speed multiplier, relative to the .ania's own frame rate. Only values inside the fpsClamp window change anything (≈0.96–1.2 on 25 fps footage); a value asking for more than twice the window's ceiling is a pre-1.13 divisor and is ignored, with a console warning. | | talkSpeed | number | 1 | Talk speed multiplier. Same rules as idleSpeed. | | fpsClamp | boolean \| {min,max} | {min:24,max:30} | Frame-rate window playback is held to, lip-sync sweep included. false hands the raw multiplier to the runtime. | | autoCalculateSpeed | boolean | true | Deprecated and ignored since 1.13.0. Use fpsClamp. | | showSpeedControls | boolean | false | Show speed adjustment sliders |

Chat Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | webhookUrl | string | - | Webhook URL for chat messages | | webhookApiKey | string | - | API key (sent as Bearer + X-API-Key) | | webhookHeaders | Record<string, string> | {} | Custom headers for webhook | | autoGreeting | boolean | true | Auto greeting on load | | assistantName | string | 'Assistant' | Name shown on bot messages | | userName | string | 'You' | Name shown on user messages | | enableAttachments | boolean | false | Enable file/image uploads | | transparentChat | boolean | false | Transparent chat background | | locale | string | 'pt-BR' | Locale for built-in UI strings (Localization) | | messagesOverride | Record<string, string \| string[]> | - | Override individual built-in strings (Localization) |

TTS Props (Text-to-Speech)

| Prop | Type | Default | Description | |------|------|---------|-------------| | enableTTS | boolean | true | Enable text-to-speech | | ttsProvider | 'browser' | 'tiktok' | 'elevenlabs' | 'google' | 'azure' | 'piper' | 'robsvoice' | 'browser' | TTS provider | | ttsVoice | string | 'auto' | Voice name (browser TTS) | | ttsVoiceId | string | - | Voice ID (cloud providers) | | ttsGender | 'auto' | 'male' | 'female' | 'auto' | Preferred voice gender | | ttsLang | string | 'pt-BR' | Language code | | ttsRate | number | 1 | Speech rate (0.5-2) | | ttsPitch | number | 1 | Speech pitch (0.5-2) | | ttsApiKey | string | - | API key for cloud TTS | | ttsApiUrl | string | - | Custom TTS API endpoint | | ttsModel | string | - | Model ID (ElevenLabs, etc) | | talkStartDelay | number | 0 | Delay before talk animation (ms) | | postTalkDelay | number | 1500 | Delay after speech ends (ms) | | minTalkDuration | number | 800 | Minimum talk state duration (ms) | | minIdleDuration | number | 400 | Minimum idle state duration (ms) |

STT Props (Speech-to-Text)

| Prop | Type | Default | Description | |------|------|---------|-------------| | enableSTT | boolean | false | Enable speech-to-text | | sttProvider | 'browser' | 'google' | 'browser' | STT provider | | sttLang | string | 'pt-BR' | Recognition language | | sttContinuous | boolean | false | Continuous listening (always on) | | sttInterimResults | boolean | true | Show interim transcripts | | sttAutoSend | boolean | true | Auto-send when phrase ends | | sttApiKey | string | - | Google Cloud STT API key | | sttApiUrl | string | - | Custom STT API endpoint |

Piper TTS Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | piperModelUrl | string | - | URL to Piper ONNX model file | | piperModelConfigUrl | string | - | URL to model config JSON | | piperPitch | number | 1 | Pitch (0.75-1.3) | | piperSpeed | number | 1 | Speed (0.75-1.3) |

Robs Voice TTS Props (beta)

Robs Voice is an on-device neural pt-BR voice (BETA): two ONNX graphs (Matcha acoustic + Vocos vocoder) driven by a rule-based, deterministic pt-BR text frontend that runs entirely in the browser (no key, no network after the model download). The voice artifact is self-describing — a robsvoice.json manifest carries the audio params and the inline phoneme→id map, so the runtime never derives the map (byte-parity with training, MAP_VERSION = 1).

Set ttsProvider="robsvoice" and point it at a voice artifact — either a base directory (holding acoustic.onnx, vocoder.onnx, robsvoice.json) or the three explicit URLs:

| Prop | Type | Default | Description | |------|------|---------|-------------| | robsVoiceUrl | string | - | Base dir with acoustic.onnx + vocoder.onnx + robsvoice.json | | robsAcousticUrl | string | - | Explicit acoustic model URL (alternative to robsVoiceUrl) | | robsVocoderUrl | string | - | Explicit vocoder model URL | | robsManifestUrl | string | - | Explicit robsvoice.json manifest URL |

<AvatarChatbot
  enableTTS
  ttsProvider="robsvoice"
  robsVoiceUrl="https://cdn.example.com/voices/blueheart"
/>

Also available as the built-in plugin tts-robsvoice (config keys robsVoiceUrl / robsAcousticUrl / robsVocoderUrl / robsManifestUrl). onnxruntime-web is loaded lazily on first synthesis and results are LRU-cached.

Lip Sync Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | lipSyncEnabled | boolean | false | Enable real-time lip sync | | lipSyncAutoFetch | boolean | true | Look the avatar up on the server and apply the best published config | | lipSyncServerUrl | string | null | API origin for the published configs. null = the default ANIA API | | lipSyncConfigId | string | null | null | Pin one published config by id (skips the automatic pick) | | lipSyncMaxCandidates | number | 5 | How many published configs to download and compare | | onLipSyncConfig | function | - | ({ source, configId, configName, isActive, score, candidates, keyframes }) once a config is applied | | lipSyncIntensity | number | 0.6 | Sync intensity (0-1) | | lipSyncResponsiveness | number | 0.5 | Response speed (0.05-1) | | lipSyncSustainStyle | 'hold' | 'wiggle' | null | null | Mouth behaviour during stable speech. null = use server config (or 'wiggle'). | | lipSyncWiggleSpeed | number | null | null | Wiggle amplitude (1-6). null = use server config (or 5). |

Where the keyframes come from. Creators tune lip sync in the desktop player and publish the .json to the ANIA server, keyed by the avatar's contentHash. With lipSyncEnabled, the widget resolves that hash (from the .ania, or by hashing its frames) and fetches the avatar's published config — no host wiring needed.

An avatar accepts up to 10 published configs, and in practice several are drafts: 2 keyframes, or only the first slice of the talk range. The desktop shows a list and a human picks; on a customer's site there is nobody to ask, so the widget downloads the candidates and scores them — talk-range coverage first, then mouth amplitude, then keyframe density, with the owner's "active" flag as a tiebreak (never enough to rescue a half-covered config). Configs whose keyframes fall outside this file's talk range, or that never open the mouth, are thrown away.

Fetching stays off the mount path: the avatar starts with whatever the .ania itself carries, and the server config swaps in when it arrives (8s timeout, failures ignored). To see what won:

<AvatarChatbot
  avatarUrl="https://…/avatar.ania"
  lipSyncEnabled
  onLipSyncConfig={(info) => console.log(info.configName, info.score)}
/>

Pin a specific config once you have validated it:

<AvatarChatbot lipSyncEnabled lipSyncConfigId="7f3c…" avatarUrl="…" />

The picker is exported too, for a host that wants to run it itself: listLipSyncConfigs, fetchBestLipSyncConfig, fetchLipSyncConfigById, scoreLipSyncConfig, parseLipSyncConfig, computeContentHash.

Action Frame Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | actions | ActionConfig[] | - | Action configurations | | enableActionHotkeys | boolean | true | Enable keyboard shortcuts | | initialAction | string | - | Action ID to play on load | | initialActionLoop | boolean | false | Loop initial action |

Plugin Props (new in 1.4)

| Prop | Type | Default | Description | |------|------|---------|-------------| | plugins | Plugin[] | - | Custom plugins (TTS/STT/action/integration), registered on top of the built-ins | | activeTtsPlugin | string | - | Force the active TTS provider by plugin id | | activeSttPlugin | string | - | Force the active STT provider by plugin id | | onPluginsReady | (registry: PluginRegistry) => void | - | Receives the registry once ready |

Wake Word Props (new in 1.4)

| Prop | Type | Default | Description | |------|------|---------|-------------| | wakeWordEnabled | boolean | false | Enable on-device wake-word detection | | wakeWordModelUrl | string | - | URL to the openWakeWord ONNX model (required) | | wakeWordThreshold | number | 0.5 | Detection threshold (0-1) | | wakeWordWasmPaths | string | - | Path the onnxruntime-web .wasm files are served from | | onWake | () => void | - | Fired on detection. Without it, the chat opens + warms TTS |

Wake word requires the optional peer dependency onnxruntime-web. If it is not installed the feature degrades gracefully (no wake word, no crash).

External Control Props (new in 1.4)

| Prop | Type | Default | Description | |------|------|---------|-------------| | enablePostMessageControl | boolean | false | Install an origin-allowlisted window.postMessage command listener | | postMessageOrigins | string[] | - | Allowed origins. Use ['*'] to allow all (unsafe) |

A host page drives the embedded avatar with:

iframeOrWindow.postMessage({ source: 'ania', cmd: 'action 1' }, targetOrigin);

NO-AI Flow Engine Props (new in 1.6)

A deterministic, no-LLM bubble/balloon conversation flow. The avatar speaks each step's prompt and the user answers by tapping clickable bubbles. No model is called until the user explicitly escalates. Free-text input keeps working alongside the flow, and omitting flow/flowUrl leaves behavior identical to 1.5.

| Prop | Type | Default | Description | |------|------|---------|-------------| | flow | FlowDef | - | The flow definition (see schema below). When set, option bubbles render and the avatar speaks prompts | | flowUrl | string | - | URL to lazily fetch a flow JSON from (ignored when flow is given) | | appId | string | - | Opaque app/tenant id forwarded to the capture/escalation callbacks | | onFlowCapture | ({ sessionId, appId, key, value, collected }) => void | - | Fired on every captured answer (stream to a CRM) | | onFlowEscalate | ({ collected, contact, sessionId, transcript }) => void | - | Fired when the flow escalates. contact = { name, phone, email }. Defaults to forwarding a name-aware escalation message to the webhook | | initialContext | object | - | Known-user fields (e.g. { name, email } from auth) pre-seeded into collected — the chat greets a signed-in user by name and skips inputs it already has | | persist | boolean | true | Persist { sessionId, collected, currentNodeId } to localStorage so a returning visitor (same browser) is remembered (30-day TTL) | | persistKey | string | ania-flow-<appId\|flowId> | Override the localStorage key | | flowConsentKey | string | - | LGPD: gate persistence on a collected key — nothing is stored until collected[flowConsentKey] is truthy; decline/reset clears it |

TYPED-INPUT nodes (free-text lead capture, new in 1.7)

A node may carry an input spec to capture a TYPED value (name, phone/WhatsApp, email, …) instead of clickable bubbles. The avatar still SPEAKS the prompt; the typed answer is silent and goes ONLY into collected (never sent to the AI webhook). The chatbot renders a labeled field + submit button (and a "Pular" bubble when optionalSkip:true); Enter submits.

lead_name: {
  id: 'lead_name',
  prompt: 'Como posso te chamar?',         // spoken (TTS) + shown
  input: {
    key: 'name',                           // collected.name = the typed value
    type: 'text',                          // text | email | tel | number | textarea
    placeholder: 'Seu nome',               // i18n key or literal
    required: true,                        // default true
    next: 'lead_phone'                     // advance here on a valid submit
  }
},
lead_phone: {
  id: 'lead_phone',
  prompt: 'Qual o seu WhatsApp com DDD?',
  input: {
    key: 'phone', type: 'tel',
    validate: 'phone',                     // 'email' | 'phone' | 'cep' | <regex source>
    errorMsg: 'Informe um telefone válido.', // inline error (i18n key or literal)
    next: 'lead_email'
  }
},
lead_email: {
  id: 'lead_email',
  prompt: 'E qual o seu melhor e-mail? (opcional)',
  input: {
    key: 'email', type: 'email', validate: 'email',
    required: false, optionalSkip: true,   // render a "Pular" bubble → advance, no capture
    next: 'lead_done'
  }
}

This name → phone → email chain streams each field to the CRM via onFlowCapture and, on escalation, hands the AI the contact so it greets the user personally.

Personalization, known users & returning visitors (new in 1.7)
  • {var} interpolation: node prompts AND option labels substitute {name} or {{name}} from collected (after i18n). Once collected.name is captured (or seeded), "Prazer, {name}!" is spoken and shown as "Prazer, João!". A missing var resolves to empty cleanly (no raw {name}), so author greetings to read naturally without a name: "Bem-vindo de volta, {name}!" → "Bem-vindo de volta!".
  • Known users: pass initialContext={{ name, email }} from your auth session and the chat already knows the signed-in user (greets by name, skips known inputs). Force a re-ask on a specific input with alwaysAsk: true.
  • Returning visitors: with persist (default on), the same browser restores collected next visit. Gate it for LGPD with flowConsentKey. Call clearPersistedFlow() (or reset()) to forget the visitor.

Flow definition schema (FlowDef):

{
  id: 'support-flow',
  version: '1.0.0',
  startNode: 'welcome',
  nodes: {
    welcome: {
      id: 'welcome',
      prompt: 'How can I help?',     // string OR i18n key; spoken via TTS on enter
      speak: true,                   // default true; false = render silently, no TTS
      collectKey: 'intent',          // optional: store the picked option.value here
      options: [
        // label = bubble text (NEVER spoken); value recorded into `collected`
        { label: 'Buy',   value: 'buy',   next: 'buy_what' },
        { label: 'Support', value: 'support', next: 'support_area',
          capture: { area: 'support' } },          // merge object into collected
        { label: 'Agent', value: 'human', escalate: true } // hand off to the AI
      ]
    },
    // ... a `terminal: true` option (or node) ends the flow
  }
}

capture may be an object (merged verbatim) or a string key (stores option.value). Drive it imperatively with the flow <nodeId> command verb. Use the headless useFlowEngine hook (or the pure flowReducer export) to test flows without React. A demo flow + sanity test live under examples/.

<AvatarChatbot
  avatarUrl="..."
  webhookUrl="https://.../webhook"   // used only when the user escalates
  flow={myFlow}
  appId="acme"
  onFlowCapture={({ key, value, collected }) => crm.update(collected)}
/>

Callbacks

| Prop | Type | Description | |------|------|-------------| | onLoad | (player) => void | Avatar loaded | | onTalkStart | () => void | Started talking | | onTalkEnd | () => void | Stopped talking | | onClose | () => void | Widget closed | | onToggleMinimize | (isMinimized: boolean) => void | Minimize state changed |


Sizing the avatar against the chat — avatarMaxHeightVh

width/height say how big the avatar should be. avatarMaxHeightVh says how much of the screen's height it may take once the chat is open, and the smaller of the two wins:

actual stage height = min(height, avatarMaxHeightVh% of the viewport height)

So on a tall screen the number never comes into play — height is smaller and height wins. It only binds when the window is too short for the size asked for, which is the case this exists for: a 400px avatar in a 930px-tall browser window is 43% of the screen, and with the input bar and a flow's options also needing room, the conversation is left with a sliver.

Finding your number. Open the chat, then in the browser console:

// the avatar stage is the canvas's parent
document.querySelector('canvas').parentElement.style.maxHeight = '22dvh';

Try values until it looks right on your shortest realistic window, then pass that number as the prop. There is no need to redeploy to experiment.

<AvatarChatbot avatarUrl={url} height={400} avatarMaxHeightVh={22} />

Guidance: 34 (default) suits a chat that is mostly free text. Drop to 20-25 when a flow with several option chips is the main path, because those chips are competing for the same vertical space. Values are clamped to 12-80; below 12 the face stops reading as a face.

<AniaAvatar />

Standalone avatar without chat UI. Use for custom implementations.

import { AniaAvatar } from 'ania-avatar-react';

<AniaAvatar
  avatarUrl="/avatar.ania"
  avatarPassword="secret"
  width={300}
  height={300}
  onLoad={(player) => {
    player.animationController.setTalkingState(true);
  }}
>
  {/* Custom overlay content */}
</AniaAvatar>

Props: Same avatar/animation/callback props as AvatarChatbot.


Hooks

useChatbot(options)

Manage webhook chat with authentication and attachments.

import { useChatbot } from 'ania-avatar-react';

const { messages, sendMessage, isLoading, error, clearMessages } = useChatbot({
  webhookUrl: 'https://n8n.example.com/webhook/chat',
  webhookApiKey: 'your-api-key',
  webhookHeaders: { 'X-Session': 'abc123' },
  onResponse: (msg, rawData) => console.log('Bot:', msg.content),
  onError: (err, friendlyMsg) => console.error(err),
  formatRequest: (text, meta) => ({ message: text, ...meta }),
  parseResponse: (data) => data.output || data.message
});

// Send message
await sendMessage('Hello!');

// Send with attachments
await sendMessage('Check this', {
  attachments: [{
    name: 'photo.jpg',
    type: 'image/jpeg',
    size: 12345,
    data: 'data:image/jpeg;base64,...'
  }]
});

// Clear history
clearMessages();

Options: | Option | Type | Description | |--------|------|-------------| | webhookUrl | string | Webhook endpoint | | webhookApiKey | string | API key for auth | | webhookHeaders | Record<string, string> | Custom headers | | onResponse | (msg, data) => void | Response callback | | onError | (err, friendly) => void | Error callback | | formatRequest | (text, meta) => any | Custom request format | | parseResponse | (data) => string \| object | Custom response parser |

Returns: | Field | Type | Description | |-------|------|-------------| | messages | ChatMessage[] | All messages | | sendMessage | (text, meta?) => Promise | Send message | | isLoading | boolean | Request in progress | | error | string \| null | Last error | | clearMessages | () => void | Clear history |


useTTSDetection(options)

Text-to-speech with automatic talk state detection.

import { useTTSDetection } from 'ania-avatar-react';

const { isTalking, speak, cancel } = useTTSDetection({
  pauseThreshold: 350,
  idleTransitionDelay: 1500,
  onTalkStart: () => player.animationController.setTalkingState(true),
  onTalkEnd: () => player.animationController.setTalkingState(false),
  ttsProvider: 'browser',
  ttsConfig: {
    ttsLang: 'pt-BR',
    ttsRate: 1.0,
    ttsPitch: 1.0
  }
});

// Speak text
speak('Hello world!', {
  lang: 'en-US',
  rate: 1.0,
  pitch: 1.0,
  cancelPrevious: true
});

// Stop speaking
cancel();

useSpeechRecognition(options)

Speech-to-text with continuous mode support.

import { useSpeechRecognition } from 'ania-avatar-react';

const {
  isListening,
  transcript,
  interimTranscript,
  startListening,
  stopListening,
  clearTranscript
} = useSpeechRecognition({
  sttProvider: 'browser',
  sttLang: 'pt-BR',
  sttContinuous: true,
  sttInterimResults: true,
  onTranscriptChange: (text, isFinal) => console.log(text),
  onFinalTranscript: (text) => sendMessage(text),
  onEnd: () => startListening(), // Auto-restart for continuous
  onError: (err) => console.error(err)
});

// Start/stop
await startListening();
stopListening();
clearTranscript();

useAniaAvatarRef()

Direct avatar player control.

import { useAniaAvatarRef } from 'ania-avatar-react';

const { ref, setTalking, play, pause } = useAniaAvatarRef();

<AniaAvatar ref={ref} avatarUrl="/avatar.ania" />

// Control
setTalking(true);
setTalking(false);
play();
pause();

// Run a desktop-style command line (new in 1.4)
runCommand('action 1');
runCommand('tts Hello there', { speak: mySpeakFn });

usePlugins(options) (new in 1.4)

Create/adopt a PluginRegistry, register the built-ins, and layer custom plugins.

import { usePlugins } from 'ania-avatar-react';

const { registry, setActive, resolveEngine, getByKind } = usePlugins({
  plugins: [myCustomTtsPlugin],
  activeTts: 'my-tts',
});

// Resolve the active engine for a subsystem
const ttsEngine = await resolveEngine('tts', { config: { ttsApiKey } });
await ttsEngine.speak('Hello');

// Switch active provider at runtime
setActive('tts', 'tts-piper');

useWakeWord(options) (new in 1.4)

On-device wake-word detection (openWakeWord on onnxruntime-web).

import { useWakeWord } from 'ania-avatar-react';

const { isListening, isLoaded, score, error } = useWakeWord({
  enabled: true,
  modelUrl: '/models/hey_ania.onnx',
  threshold: 0.5,
  wasmPaths: '/',                 // where ort .wasm files are served
  onWake: () => console.log('wake!'),
});

Plugin Architecture (new in 1.4)

A Plugin is a plain object mirroring the desktop PluginBase:

{
  id: string;
  name: string;
  version?: string;
  description?: string;
  kind: 'tts' | 'stt' | 'wakeword' | 'action' | 'integration';
  enabled?: boolean;
  init?(ctx): void | Promise<void>;
  start?(): void;
  stop?(): void;
  createEngine?(ctx): TTSEngine | STTEngine | WakeWordEngine;  // tts/stt/wakeword
  createHandler?(ctx): any;                                    // action/integration
  settingsSchema?: object[];
}

The built-in providers (tts-browser, tts-tiktok, tts-elevenlabs, tts-google, tts-azure, tts-piper, tts-robsvoice, stt-browser, stt-google, action-audio) are registered automatically; custom plugins are layered on top and can override a built-in by reusing its id.

import { PluginRegistry, registerBuiltins } from 'ania-avatar-react';

const registry = new PluginRegistry();
registerBuiltins(registry);

registry.register({
  id: 'my-tts', name: 'My TTS', version: '1.0.0', kind: 'tts',
  createEngine: () => ({ async speak(text) { /* ... */ } }),
});
registry.setActive('tts', 'my-tts');

const engine = await registry.resolveEngine('tts');
await engine.speak('Hello from my plugin');

Command / External-Control API (new in 1.4)

executeCommand(line, ctx) ports the desktop socket command set to the browser.

import { executeCommand } from 'ania-avatar-react';

executeCommand('action 1', ctx);
executeCommand('speed 1.5 2', ctx);
executeCommand('tts Hello there', ctx);

Commands: show, hide, toggle, action <id|index>, actions, info, speed <idle> [talk], sensitivity <0..1>, mute, unmute, tts <text>, ask <text> (alias provider), flow <nodeId> (jump the NO-AI flow), wake, stop, help.

Drive the avatar from a host page (postMessage)

<AvatarChatbot
  avatarUrl="/avatar.ania"
  enablePostMessageControl
  postMessageOrigins={['https://my-host-app.com']}
/>
// from the host page
avatarIframe.contentWindow.postMessage(
  { source: 'ania', cmd: 'action 1' },
  'https://my-host-app.com'
);

The library replies with { source: 'ania-reply', cmd, result }.


Avatar Configurator (dev tool)

The library ships its own configuration UI. Render <AvatarConfigurator> to tune the avatar live — position, size, theme, animation speeds/delays, TTS provider/voice/rate/pitch, STT, chat names/greeting — and export the tuned props as JSX or JSON with one click. It's a developer tool: gate it behind a dev/staging flag (e.g. ?config), not an end-user surface.

Screenshot placeholder — panel (collapsible Avatar / Layout / Animation / TTS / STT / Chat sections) on the left, the live avatar it controls on the right.

import { AvatarConfigurator } from 'ania-avatar-react';

Only props that DIFFER from their defaults are exported, so the copied snippet stays minimal. The last config is persisted to localStorage (namespaced key, with a Reset button). Importing it is optional and tree-shakeable — apps that never reference it ship none of its code, and it is SSR-safe.

(a) Batteries-included — the panel renders the avatar

Fastest way to eyeball a config: the configurator renders the <AvatarChatbot> itself next to the controls.

<AvatarConfigurator avatarUrl="https://example.com/avatar.ania" />

Any AvatarChatbot prop can be passed inline as a starting value (panel-owned props seed the fields; everything else — plugins, flow, callbacks — flows through to the previewed avatar untouched).

(b) Controlled — your app renders its own avatar

Drive the config from your own state and render the avatar yourself:

import { useState } from 'react';
import { AvatarConfigurator, AvatarChatbot } from 'ania-avatar-react';

function Tuner() {
  const [config, setConfig] = useState({ avatarUrl: 'https://example.com/avatar.ania' });
  return (
    <div style={{ display: 'flex', gap: 24 }}>
      <AvatarConfigurator value={config} onChange={setConfig} />
      <AvatarChatbot {...config} />
    </div>
  );
}

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | value | Partial<AvatarChatbotProps> | - | Controlled config (with onChange) | | onChange | (config) => void | - | Controlled setter, fired on every edit | | defaultValue | Partial<AvatarChatbotProps> | - | Uncontrolled initial config | | storageKey | string | 'ania-avatar-configurator' | localStorage namespace | | persist | boolean | true | Persist config to localStorage | | showPreview | boolean | !controlled | Render the avatar preview beside the panel | | exportComponentName | string | 'AvatarChatbot' | Name used in the exported JSX | | onExport | (props) => void | - | Fired with the export-ready prop map on change | | layout | 'row' | 'column' | 'row' | Preview layout |

Any other AvatarChatbot prop passed inline seeds the config (if the panel owns it) or passes through to the preview.

The pure serializers are exported too, so you can build your own export UI or snapshot configs in tests without mounting the component:

import { configuratorToJSX, configuratorToJSON, configuratorExportProps } from 'ania-avatar-react';

configuratorToJSX({ avatarUrl: '/a.ania', theme: 'blue' }); // '<AvatarChatbot\n  avatarUrl="/a.ania"\n  theme="blue"\n/>'

A build-free playground lives in examples/configurator/.


Localization (i18n)

The library ships its own lightweight, dependency-free locale table for every user-facing built-in string (greetings, waiting messages, the Enable Sound / speed-slider labels, control titles, loading/error text). No i18next or any other runtime is required — it's a tiny synchronous resolver with English fallback. ~190 languages are bundled (en + pt-BR are hand-authored; the rest are machine-translated).

Pick a language

<AvatarChatbot
  avatarUrl="/avatar.ania"
  webhookUrl="https://n8n.example.com/webhook/chat"
  locale="en"        // 'es', 'fr', 'ja', 'de', 'ar', ... (BCP-47)
/>

locale defaults to 'pt-BR' (the library's original wording, so existing apps are unaffected). Unknown codes fall back to the base language (pt → pt-BR, es-MX → es) and finally to English — a string is never rendered as a raw key. <AniaAvatar> accepts the same locale prop.

Override individual strings

Supply your own copy for any key without forking the component. Scalar keys take a string; the list keys (greetings, waiting) take a string[]:

<AvatarChatbot
  locale="en"
  messagesOverride={{
    "chat.enableSound": "Turn on sound",
    "avatar.loading": "Summoning your avatar…",
    greetings: ["Hey! What's up?", "Hi there!"]
  }}
/>

Helper exports

The resolver is also exported for use outside the components:

import {
  getString, getStringList, createTranslator,
  availableLocales, hasLocale, DEFAULT_LOCALE, FALLBACK_LOCALE
} from 'ania-avatar-react';

getString('chat.enableSound', 'es');                       // 'Activar sonido'
getString('avatar.error.loadFailed', 'en', { vars: { error: 'timeout' } });
getStringList('greetings', 'ja');                          // string[]
availableLocales();                                        // ['af','am',...,'zu']

const tr = createTranslator('fr');
tr.t('chat.input.placeholder');                            // 'Tapez votre message...'

String keys: chat.input.placeholder, chat.input.listening, chat.enableSound, chat.stt.transcribing, chat.stt.micActive, chat.stt.heardError, chat.stt.micAccessError, chat.speed.idle, chat.speed.talk, avatar.loading, avatar.title.{maximize,minimize,close, clickToMaximize,clickToMinimize}, avatar.error.{loadFailed,playerNotLoaded, passwordRequired,noSource}. List keys: greetings, waiting.


Cache Utilities

Manage avatar cache in IndexedDB.

import {
  getCachedAvatar,
  setCachedAvatar,
  deleteCachedAvatar,
  clearAvatarCache,
  getCacheStats
} from 'ania-avatar-react';

// Get from cache
const data = await getCachedAvatar('https://example.com/avatar.ania');

// Save to cache
await setCachedAvatar(url, avatarData, isEncrypted);

// Delete
await deleteCachedAvatar(url);
await clearAvatarCache();

// Stats
const { count, size, sizeFormatted } = await getCacheStats();
// { count: 3, size: 15728640, sizeFormatted: '15 MB' }

Examples

Basic Chatbot

<AvatarChatbot
  avatarUrl="/avatars/assistant.ania"
  avatarPassword="123"
  webhookUrl="https://api.example.com/chat"
  position="bottom-right"
  theme="dark"
  assistantName="Luna"
  userName="User"
  enableTTS={true}
  enableSTT={true}
  enableAttachments={true}
/>

n8n with Authentication

<AvatarChatbot
  avatarUrl="/avatar.ania"
  avatarPassword="secret"
  webhookUrl="https://n8n.example.com/webhook/abc123"
  webhookApiKey={process.env.REACT_APP_N8N_API_KEY}
  webhookHeaders={{
    'X-Workflow-Id': 'my-workflow',
    'X-Session-Id': sessionId
  }}
  enableAttachments={true}
/>

Google Cloud TTS

<AvatarChatbot
  avatarUrl="/avatar.ania"
  webhookUrl="/api/chat"
  enableTTS={true}
  ttsProvider="google"
  ttsApiKey={process.env.REACT_APP_GOOGLE_TTS_KEY}
  ttsVoiceId="pt-BR-Wavenet-B"
  ttsLang="pt-BR"
  ttsRate={1.0}
  ttsPitch={1.0}
/>

ElevenLabs TTS

<AvatarChatbot
  avatarUrl="/avatar.ania"
  webhookUrl="/api/chat"
  enableTTS={true}
  ttsProvider="elevenlabs"
  ttsApiKey={process.env.REACT_APP_ELEVENLABS_KEY}
  ttsVoiceId="21m00Tcm4TlvDq8ikWAM"
  ttsModel="eleven_monolingual_v1"
/>

Browser TTS (Windows Voice)

<AvatarChatbot
  avatarUrl="/avatar.ania"
  webhookUrl="/api/chat"
  enableTTS={true}
  ttsProvider="browser"
  ttsVoice="Microsoft Daniel - Portuguese (Brazil)"
  ttsLang="pt-BR"
  ttsRate={1.0}
  ttsPitch={0.95}
/>

Continuous STT (Always Listening)

<AvatarChatbot
  avatarUrl="/avatar.ania"
  webhookUrl="/api/chat"
  enableSTT={true}
  sttContinuous={true}
  sttAutoSend={true}
  sttLang="pt-BR"
/>

File Uploads to n8n

<AvatarChatbot
  avatarUrl="/avatar.ania"
  webhookUrl="/api/chat"
  enableAttachments={true}
/>

// Webhook receives:
// {
//   message: "Check this image",
//   attachments: [{
//     name: "photo.jpg",
//     type: "image/jpeg",
//     size: 12345,
//     data: "data:image/jpeg;base64,..."
//   }]
// }

Custom Implementation

import { AniaAvatar, useTTSDetection, useChatbot } from 'ania-avatar-react';

function CustomChat() {
  const [player, setPlayer] = useState(null);

  const { messages, sendMessage, isLoading } = useChatbot({
    webhookUrl: '/api/chat',
    onResponse: (msg) => speak(msg.content)
  });

  const { isTalking, speak } = useTTSDetection({
    onTalkStart: () => player?.animationController.setTalkingState(true),
    onTalkEnd: () => player?.animationController.setTalkingState(false)
  });

  return (
    <div>
      <AniaAvatar
        avatarUrl="/avatar.ania"
        onLoad={setPlayer}
        width={200}
        height={200}
      />
      <div>
        {messages.map(m => <div key={m.id}>{m.content}</div>)}
      </div>
      <input onKeyPress={e => e.key === 'Enter' && sendMessage(e.target.value)} />
    </div>
  );
}

TypeScript

Full TypeScript support. Import types:

import type {
  AniaAvatarProps,
  AvatarChatbotProps,
  ChatMessage,
  ChatAttachment,
  UseChatbotOptions,
  UseChatbotResult,
  UseTTSDetectionOptions,
  UseTTSDetectionResult,
  UseSpeechRecognitionOptions,
  UseSpeechRecognitionResult,
  UseAniaAvatarRefResult,
  SpeakOptions,
  CacheStats,
  // new in 1.4
  Plugin,
  PluginKind,
  PluginContext,
  TTSEngine,
  STTEngine,
  WakeWordEngineLike,
  UsePluginsOptions,
  UsePluginsResult,
  UseWakeWordOptions,
  UseWakeWordResult,
  CommandContext,
  CommandResult,
  CommandDescriptor
} from 'ania-avatar-react';

Browser Support

| Feature | Chrome | Firefox | Safari | Edge | |---------|--------|---------|--------|------| | Avatar | ✅ | ✅ | ✅ | ✅ | | Browser TTS | ✅ | ✅ | ✅ | ✅ | | Browser STT | ✅ | ❌ | ❌ | ✅ | | Cloud TTS/STT | ✅ | ✅ | ✅ | ✅ |

Note: Browser STT (Web Speech API) requires Chrome or Edge.


Security

This library is compiled with maximum obfuscation:

  • Control flow flattening
  • Dead code injection
  • Debug protection
  • String encryption (base64 + RC4)
  • Self-defending code

License

MIT