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.
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:
| Sample | What happens |
|---|---|
calendar | The user asks to move a meeting; the agent thinks, then confirms. |
quick-answer | A short question and a short answer. |
long-answer | A longer answer, for a speaking state that has to hold. |
barge-in | The 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.
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
}
voicepicks how the turn sounds:userfor a microphone-like level,agentfor a steadier, speech-synthesis-like one. A turn withoutvoiceis silent.bargeIn: truefires the interruption as the turn begins.lineis optional caption text.onFramereports 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.