Sinua
Connect your appVoice

Simulated conversation

A believable voice conversation with no microphone, audio or network: for demos, previews, onboarding and tests.

SimulatedVoiceSource plays a scripted conversation: the agent's states change on a timeline, the level and bands move like speech, and a barge-in shows its cue. It is an ordinary voice source, so every view takes it unchanged, and it opens no microphone, plays no audio, asks for no permission and makes no network call.

Use it for a demo on your landing page, a preview in your design tool, an onboarding screen, or a test.

idle
A simulated conversation: no microphone, no audio, no network.
simulated-web.ts
import { mount } from "@sinua/web";
import { SimulatedVoiceSource } from "@sinua/core";

const canvas = document.querySelector<HTMLCanvasElement>("#orb")!;
const caption = document.querySelector<HTMLParagraphElement>("#caption")!;

// A believable conversation with no microphone and no network: for demos, previews and tests.
// Samples: "calendar", "quick-answer", "long-answer", "barge-in" (or pass your own script).
export const voice = new SimulatedVoiceSource("barge-in");
export const fx = mount(canvas, { pattern: "glowing", voice });

// Captions: the current line, as much of it as has been "said".
voice.onFrame((f) => {
  caption.textContent = f.line.slice(0, f.shown);
});

// Plays, and loops for the samples. No permission prompt: nothing is recorded.
await voice.connect();

The samples

Four ship with the engine, each 10–15 seconds and looping:

SampleWhat happens
calendarThe user asks to move a meeting; the agent thinks, then confirms.
quick-answerA short question and a short answer.
long-answerA longer answer, for a speaking state that has to hold.
barge-inThe user talks over the agent, and the view shows the barge-in cue.

Your own script

A script is the same JSON on every platform: a list of turns, each with a state and how long it lasts.

simulated-script-web.ts
import { SimulatedVoiceSource, type ConversationScript } from "@sinua/core";

// Your own conversation: the same JSON shape on every platform.
const script: ConversationScript = {
  name: "Booking",
  loop: true,
  turns: [
    { state: "idle", seconds: 1.5 },
    { state: "listening", seconds: 2.5, voice: "user", line: "A table for two at eight?" },
    { state: "thinking", seconds: 1.2 },
    { state: "speaking", seconds: 3, voice: "agent", line: "Booked for eight, by the window." },
    // The user talks over the agent: views show the barge-in cue.
    { state: "listening", seconds: 1.5, voice: "user", bargeIn: true, line: "Make it 8:30." },
  ],
};

export const voice = new SimulatedVoiceSource(script);

// A timeline of your own: play(), pause(), seek(t), loop, time, duration, turns.
export function scrubTo(seconds: number) {
  voice.seek(seconds); // seeking into a barge-in turn never fires the cue
}
  • voice picks how the turn sounds: user for a microphone-like level, agent for a steadier, speech-synthesis-like one. A turn without voice is silent.
  • bargeIn: true fires the interruption as the turn begins.
  • line is optional caption text. onFrame reports how much of it has been said (shown), so captions can type along.
  • A malformed script throws with the path of the first error (for example /turns/2/seconds); a turn has to last 0.1 to 60 seconds.

Controls

play(), pause(), seek(t), loop, time, duration and turns let you build a timeline of your own. Seeking into a barge-in turn never fires the cue. On the web and Android, autoTick: false stops the internal timer so you can call advance(dt) from your own loop, which is how you'd drive it in a test.

The conversation is computed by the engine, the same on every platform frame for frame, so a simulation on iOS and one on the web look alike.

On this page