Sinua

Live visuals for voice AI.

One engine draws the same frame on the web, iOS, Android and React Native. Give it your agent's state and it shows what is happening: listening, thinking, speaking.

idleSimulated conversation

 

43 patterns in 6 families

Orbs for an agent's inner life, signals for sound, rings for progress, beacons for attention, cores for text that is on its way and characters that give your assistant a face. Point at one to set it moving; every frame below is drawn by the engine.

Wired to the conversation

Hand a view a voice source and it follows the agent on its own: it listens while the user talks, thinks between turns and speaks with the agent's audio. All four states work on every pattern, and a barge-in shows as a brief flash.

Where the voice comes from

  • Microphonethe device mic, with no service at all
  • LiveKitan agent in a LiveKit roombeta
  • OpenAI RealtimeWebRTCbeta
  • Gemini LiveWebSocketbeta
  • ElevenLabsConversational AIbeta

VoiceSourcethe agent's state and the audio level, many times a second

Your server hands the app a short-lived token; a raw API key is always refused. The vendor sources are beta: tested on simulated sessions, not yet against each live service.

  1. idle
  2. listening
  3. thinking
  4. speaking
  5. idle
Orb speaking
Signal waveform
Beacon broadcasting

 

Web
@sinua/voice/livekit/openai/gemini/elevenlabs/mic
iOS
SinuaVoiceSinuaLiveKitSinuaOpenAISinuaGeminiLiveSinuaElevenLabs
Android
sinua-coresinua-livekitsinua-openaisinua-geminisinua-elevenlabs

The same frame on every platform

One Rust engine is compiled into each platform, so a design is never ported four times. The web, iOS and Android builds each check the same 506 frozen frames in CI; React Native runs the iOS and Android binaries as they are.

All four are drawn here by the web build. How parity is tested

import { SinuaOrb } from "@sinua/web/components";

export function Visual() {
  return <SinuaOrb pattern="connecting" color={{ hue: 172, lightness: 0.5, mix: 1, mode: "fixed", saturation: 0.8 }} style={{ width: 160, height: 160 }} />;
}

In a real app

DevinFit, our own nutrition app, draws its Soul with Sinua on iPhone and Android. Its colour follows the day's nutrition, and it thinks while the AI answers. There's no voice in DevinFit: the same states run on app data.

  • An empty day in the logo's colours
  • The day's nutrition sets the colour
  • Thinking while the AI answers
{
  "fxSpec": "1.8",
  "object": "orb",
  "pattern": "glowing",
  "size": 64,
  "gradient": { "stops": ["#E19A43", "#3CBB4E"], "angle": 90, "strength": 1, "saturation": 0.8 },
  "params": { "depthTone": 0 }
}

The file is the one DevinFit ships, as it is. The code is the shape of it: the app also writes the day's colour into the file before drawing it.

Your numbers pick the state

Pass the app's values and let the file decide what they mean. Here a binding fills the rings, a rule switches to the goal state at 10,000 steps, and one call plays a short celebration on top. Push the heart rate past 150 for the other rule.

Steps
Heart rate
State

baseno rule holds

Play an effect
import { useEffect, useRef } from "react";
import type { FxHandle } from "@sinua/web";
import { SinuaView } from "@sinua/web/react";
import rings from "../spec/activity-rules.fxspec.json";

// Your numbers go in; the file decides what they mean. A binding fills the rings, a rule
// switches to goalReached at 10,000 steps, and one call plays a short celebration on top.
export function DailyRings({ steps, heartRate }: { steps: number; heartRate: number }) {
  const fx = useRef<FxHandle | null>(null);
  const last = useRef(steps);
  useEffect(() => {
    if (last.current < 10_000 && steps >= 10_000) fx.current?.trigger("celebrate");
    last.current = steps;
  }, [steps]);
  return <SinuaView spec={rings} inputs={{ steps, heartRate }} onReady={(h) => (fx.current = h)} style={{ width: 160, height: 160 }} />;
}

Your colours, your finish

Every pattern takes a colour or a ramp, and wears any of the materials: glow, noise and pulse. They are ordinary props, so the design lives in your code, or in one FX Spec file every platform reads the same way.

Pattern
Colour
Hue
Materials
import { SinuaOrb } from "@sinua/web/components";

export function Visual() {
  return <SinuaOrb pattern="glowing" color={{ hue: 265, lightness: 0.5, mix: 1, mode: "fixed", saturation: 0.8 }} glow={{ strength: 0.7 }} style={{ width: 160, height: 160 }} />;
}

Free runtime, separate Studio

The runtime is open source and free. The Studio, a design tool for it, is a separate subscription.

Packages

The engine and every renderer, Apache-2.0, shipped inside your app. Nothing to sign up for.

npm i @sinua/web@beta

Studio

Tune a visual live and export it as code or an FX Spec file. From $8.25 a month, with a 14-day free trial.