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.
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.
- idle
- listening
- thinking
- speaking
- idle
- 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.
baseno rule holds
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.
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@betaStudio
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.