Changelog
Every release of the Sinua packages. All of them share one version.
Every package (npm, Maven Central and SwiftPM) is released together under one version. A -beta.N version is a public prerelease: it is on npm's beta tag, and its API can still change.
0.1.0-beta.11
A hotfix: Android only. Web and iOS are the same as 0.1.0-beta.10; their packages are republished at this version so every platform keeps one version.
Size: native libraries unchanged from 0.1.0-beta.10 (Kotlin changes only).
Fixed
- Android: voice calls were very quiet, and on some phones the model didn't hear the user
(
OpenAIRealtimeVoiceSource,OpenAILiveVoiceSource,GeminiLiveVoiceSource,ElevenLabsVoiceSource). The sources never set up the app's audio: the call ran in the normal audio mode with no audio focus, so it played as a voice call through the earpiece or at call volume, and some devices record silence outside communication mode. Each source now enters communication mode, takes audio focus and routes to the loudspeaker for the call (a Bluetooth or wired headset still wins, and is followed when plugged in or out), then restores the app's audio when the call ends. Sinua's sources share one claim, so one source's disconnect never cuts another's audio. This is the Android twin of the iOSaudioSessionoption. - Android:
connect()with OpenAI Realtime or GPT-Live killed the app's process when the app didn't declareACCESS_NETWORK_STATE(WebRTC's network monitor reads it).sinua-openaiand the React Native library now declare it, as livekit-android does.
Added
- Android:
VoiceAudioSession(SPEAKERby default,RECEIVERfor the earpiece,UNMANAGEDwhen your app sets the audio up itself), theaudioSessionparameter on every Android source, and newGeminiLiveVoiceSource,ElevenLabsVoiceSourceandAndroidPcmAudioDeviceconstructors that take aContextfirst, like the OpenAI sources. The voice modules declareMODIFY_AUDIO_SETTINGS(a normal permission, no prompt).
Deprecated
- Android: the
GeminiLiveVoiceSource,ElevenLabsVoiceSourceandAndroidPcmAudioDeviceconstructors without aContext. They keep working but leave the app's audio alone (asUNMANAGED) and log a warning once. Pass aContextfirst. They're removed in 1.0.
0.1.0-beta.10
A hotfix: Android only. Web and iOS are the same as 0.1.0-beta.9; their packages are republished at this version so every platform keeps one version.
Size: unchanged from 0.1.0-beta.9 (ProGuard rules only).
Fixed
- Android: minified release builds no longer abort on
connect()with OpenAI Realtime or GPT-Live (dev.sinua:sinua-openai). The prefixed WebRTC binary looks its Java classes up by name through JNI, and 0.1.0-beta.9 shipped no ProGuard rules for them. An app with R8 on (isMinifyEnabled = true) stripped them, and the first peer connection aborted natively, with no Java exception.sinua-openainow ships consumer rules, the ones livekit-android ships for the same binary, and so does the React Native library for itsopenai/livekitvendors.- Apps can drop any keep rule they added for
livekit.org.webrtc/livekit.org.jni_zero. - A
-keep class livekit.org.webrtc.** { *; }alone was not enough: it misseslivekit.org.jni_zero.JniInit, whichJNI_OnLoadneeds. sinua-livekitwas not affected (livekit-android brings these rules). Web and iOS are unchanged.
- Apps can drop any keep rule they added for
- CI: a minified sample app with no app rules now builds WebRTC (
scripts/android-r8-jni-check.mjschecks that every class the.sonames survives R8) and opens a peer connection on the emulator, so this can't come back unnoticed.
0.1.0-beta.9
Size: web 437,572 B (gzip 326,269), Android arm64 .so 1,902,904 B, iOS .a 4,103,360 B; @sinua/voice min+gzip: voice 3,858, openai 8,961, gemini 6,829, elevenlabs 6,965, livekit 5,392, mic 1,202, tone 1,296 B (release builds). Down from beta.8: web −14.5%, .so −10.4%, .a −6.6%.
Added
-
FX Spec 1.14 (docs/fx-spec.md, v1.14), the minor 0.1.0-beta.9's cleanup ships under. It adds no key: its removals are errors at every version, and its one value change (
glowing's calmer zoom) reaches every version too. A"fxSpec": "1.14"file is now read strictly (unknown keys are errors), with its own identity lockspec/fx-spec-1.14-resolved.json;1.15and later read leniently. Every older lock is now held by a test, including 1.12 and 1.13, which had none. The 1.12 lock's recipe names forcustom-characterandremix-lattewere updated: the 1.14 cleanup edited those two examples. -
Transcripts (docs/audio-pipeline.md, Transcripts): live text for both speakers, for captions.
- API:
onTranscripton a source;SharedVoiceSource.onTranscript(Web) /listenTranscript(iOS, Android), which works beforeconnect();supportsTranscriptandtranscriptTiming;- the shape is
{ role, text, final, turnId, truncated?, startMs?, endMs? }.
- Modes: synced to the played audio by default, or raw with
syncToAudio: false. A barge-in ends the assistant's turntruncated, with only what was played. - Sources, on Web, iOS and Android, and on the React Native handle (
onTranscript):OpenAILiveVoiceSource(GPT-Live):segments, from GPT-Live'sstart_ms/end_ms;ElevenLabsVoiceSource:chars, character-exact from ElevenLabs' alignment;GeminiLiveVoiceSource:none, paced over the reply's audio. The Live setup now also asks foroutputAudioTranscription;OpenAIRealtimeVoiceSource:none, paced while audible; the assistant's text by default. The user's side needs the session's input transcription, which OpenAI bills per minute, so it is opt-in:transcribeUser: "<model>"(e.g.gpt-4o-mini-transcribe, on every platform and the React Native config) turns it on when something subscribes and the session has none;LiveKitVoiceSource:synced, the agent's ownlk.transcriptionstreams passed through;SimulatedVoiceSource.
- Your own source:
@sinua/voiceexportsTranscriptAssembler(andalignmentFragmentsfor per-character times); the rules live inspec/transcript-cases.jsonandspec/transcript-assembler-cases.json. - Privacy: display only. Sinua keeps nothing beyond the current turn and sends nothing anywhere.
- Measured on GPT-Live: synced is within 250 ms for most words in Turkish (median 82 ms, p95 191 ms); English is looser (median ~190 ms, p95 ~515 ms). Raw text arrives ~650 ms ahead of the audio. The other vendors are not measured live yet.
- API:
-
iOS: Apple privacy manifests. Every Swift target (
sinua-swift,-livekit,-openai) and the React Native pod ship aPrivacyInfo.xcprivacy, so an App Store upload no longer needs the app to declare Sinua's use for it (ITMS-91053). Declared: system boot time (35F9.1,systemUptimeas the animation clock) and file timestamps (C617.1, Rust's standard library reading the app binary's own metadata). No tracking, no collected data. A CI check keeps the manifests in step with the sources and the engine's symbols. Seedocs/publishing.md, Apple privacy manifests.
Changed
-
GPT-Live barge-in (
OpenAILiveVoiceSource, Web / iOS / Android): the 1 s window now runs from the user's latest words, not their first. Live sessions showed GPT-Live talking on for ~1 s after the user started, then switching straight to the new reply. The old rule missed those barge-ins, soonInterruptnever fired and the agent stayedspeakingacross replies. NowonInterruptand thespeaking→listeningchange come at such a cut-off. A short "mhm" under speech that goes on still doesn't count.spec/openai-live-cases.jsonhas the case. -
glowingno longer jumps between voice states at large sizes (every platform, every FX Spec version). Its voice-state profile zoomed the colour field 2.5× from listening to thinking (surfaceScale1.2 → 3.0) in under half a second, which read as a jump at video size (~560 px). Now thinking is 1.6 and speaking 1.4 (profileVersion4). Transitions measured at 560 px drop from 1.9–2.1× to 0.6–0.8× of the orb's own speaking motion, and thinking still looks closer in than listening. Apps that pinnedsurfaceScaleto hide the jump can drop the pin. Files of every FX Spec version see the new values (the identity locks were rewritten for these two values only). A new CI check (scripts/transitions/large.mjs) drives glowing, calibrating and progressing through their state changes in colour at 512 px. Goldensinua4.1.0 adds glowing under its thinking and speaking profiles (glowing-64-0.6-voice-thinking/-voice-speaking), which no case covered; every earlier case is unchanged.
Removed
1.14 takes out what the 1.14 review found unused or about to be replaced. None of it is used by DevinFit (glowing + colour / gradient + GPT-Live), so DevinFit needs no change.
characterSlots,nearestSlotand theCharacterSlottype (@sinua/core;characterSlotson iOS and Android). Migration: none; drag-to-dress comes back with the per-character wardrobe in a later version.docs/character-svg-guides.md. Migration: none; the SVG import was a Studio feature and is gone.- The liquid, particles and holographic materials. An FX Spec file that uses
materials.liquid,materials.particlesormaterials.holographic, or namesliquidorparticlesinperformance.lowPower.disable, is now rejected (an error naming the section), whatever itsfxSpecversion. Migration: delete the section. The typed components lose theirliquid,particlesandholographicprops;liquidSuitability(@sinua/core/dev) andparticleDefaultsare gone; theliquid*/particle*/holo*keys do nothing as raw overrides. The low-power host default is now glow off only. An FX Spec 1.8 or 1.9 file withstatesdrew particles in its voice states; it now resolves without them. Theliquid-orb,particles-orbandholo-orbexamples are gone. - The character wardrobe: cosmetics, loadouts and catalog packs. An FX Spec file with a
top-level
cosmeticsorwardrobeis now rejected, whatever itsfxSpecversion, and so is a recipe withslots,tags,cosmeticsor a part'srole. Gone with them:applyLoadout,cosmeticsFor,frameStill,loadCatalog,unloadCatalogand theLoadout/LoadoutApplied/CosmeticFittypes (@sinua/core);characterThumbnailand@sinua/web/catalog; the views'loadoutoption (web, React,<sinua-view>, the typed character components);SinuaLoadout,SinuaThumbnail,SinuaCatalogand theloadoutparameter (iOS, Android);loadout,catalogsand@sinua/react-native/catalog(React Native);FxSpecPlayer'sloadoutoption,setLoadoutandloadoutDiagnostics;StateTransition.wear(),wearingandWEAR_S;spec/catalog/catalog-1.json,spec/loadout-vectors.jsonanddocs/character-cosmetics.md. Migration: delete the keys and stop passing a loadout; a character-specific wardrobe is planned for a later version. Theparty-hatandwardrobe-beanexamples are gone. - Named palettes.
"palette": "sunset"andpalette.themeare now rejected, whatever the file's version, andspec/palettes.jsonis gone. Migration: write the colours out, by slot or by role ({ "primary": "#F1774B", "dark": { "primary": "#DD562C" } }); roles and thedarkvariant stay. Thethemed-cuppaexample is gone. - Eye styles.
params.eyeStyle(and the typed components'eyeStyleprop andEyeStyleenum), a recipe's eyestyle,irisandsclera, theirisrole and the built-ins'irisslot are gone; a file or recipe that still uses one is rejected. Migration: delete them; every character draws the shape eye it always drew by default. Theglossy-bean,pixel-beepanddot-humexamples are gone. - The built-in recipes lose their
tags,slotsandiriscolour, and therich-bean,rich-buzzy,custom-characterandremix-latteexamples theirslots: the frames are unchanged (goldensinuabyte-identical), but a file carrying one of these recipes resolves to a new registry key (recipe:<id>:<hash>). - The
concludingpattern (the crystallize mode: dots that snap into a crystal and let go). An FX Spec file whosepattern(base or astatesentry) isconcludingis now rejected, whatever itsfxSpecversion. Migration: pick another orb pattern (progressingorconfirmingread as "wrapping up"). The typed orb components loseconcludingand thedriftAmplitude/lineWidthprops only it read. Goldensinua4.0.0 drops its 12 cases; every other case is unchanged.
0.1.0-beta.8
Size: web 511,877 B (gzip 382,107), Android arm64 .so 2,124,360 B, iOS .a 4,393,832 B (release builds).
Breaking: Studio / dev APIs moved to @sinua/core/dev
The default @sinua/core entry now carries only what apps draw with; the dev tools moved to
a new entry, @sinua/core/dev. Change the import:
import { estimateCost, SimulatedVoiceSource } from "@sinua/core/dev"; // was "@sinua/core"- Moved:
parameterCatalog,checkOverrides,estimateCost,fxSpecCost,liquidSuitability,conversationAt,conversationSampleNames,conversationSample,SimulatedVoiceSource, and the JSON bridgesframeViaJson,frameWithOverridesViaJson,frameFromFxSpecViaJson. Their types stay exported from@sinua/core. @sinua/core/devre-exports the whole default API on the same (dev) wasm, so a tool that registers catalog packs or recipes can import everything from it and run one engine.- iOS and Android: the published artefacts leave out the Swift / Kotlin functions only
the Studios use:
estimateCost,fxSpecCost,liquidSuitability,parameterCatalogJson,checkOverrides(the Android library is ~100 KB smaller installed, ~42 KB in the download).packages/*/build.shkeeps them for development (tests, the Studios);SINUA_NATIVE_RELEASE=1builds the published variant. - Android install docs: leave JNA's
armeabi/mips/mips64loaders out of your app (packages/android/README.md), so Play doesn't offer it to devices without the engine. - The default bundle is about 48 KB smaller: the dev tools and the JSON frame bridges left it, and the transition and thumbnail frames now cross as packed arrays. Frames draw as fast as before.
Added
-
A new family,
character: voice-assistant characters with a face (FX Spec 1.11,docs/character.md), asSinuaCharacteron every platform (<sinua-character>on the Web). The characters:buzzy, a small space-hero assistant;hum, a vintage studio microphone whose grille lights with the voice and whose tally light shows when it listens;wisp, a helpful spirit whose sparkles gather, orbit and stream with the conversation;chirp, a songbird that sings its answers: the beak opens with the voice, notes rise and the wings flutter.- The head turns like a solid (
turn, ±25° by default, 0 = flat): the face slides round the head, the light stays put, near parts come forward. Idle looks corner to corner, listening turns to you, thinking looks up and away, speaking nods with the voice. - Shape eyes (no pupils, no brows); the gaze knows whose turn it is. It looks at you while listening, looks away while thinking, and blinks once at the end of your turn.
- The mouth is a voice line, two even waves that flow; the agent's level sets their height (amplitude only). Effects are expressions: success, error, celebrate.
- Options:
hue,mouth,accessories,look,turn,seed. Mute squints and fades instead of greying out. - Drawn entirely with fills: no painter changed.
-
FX Spec 1.11:
object: "character". A character takesparams.hue, notcolor/gradient. 1.8–1.10 files resolve exactly as before (spec/fx-spec-1.11-resolved.json). -
FX Spec 1.12:
recipe. A brand's own character travels in one FX Spec file: the file carries a character recipe and itspatternnames the recipe'sid. Every view draws it with no platform code (spec/examples/custom-character.fxspec.json). Recipe errors point into the recipe, and limits (64 KB, 48 parts, segments, counts, numbers) keep any file cheap to draw. Voice states come from a shared character language or a built-in's ("profile": "chirp"). 1.8–1.11 files resolve exactly as before (spec/fx-spec-1.12-resolved.json). -
Two new characters, made only from recipes:
cuppa, a coffee mug on its saucer whose steam rises while it listens and thickens with its voice, andbean, a coffee bean on little feet with aroma sparkles. The newsteampart draws soft wisps that rise and fade. -
A new character,
beep: a small tin robot with arms. A hand goes to the ear to listen and to the chin to think, and the arms beat with the voice; the antenna light shows listening and thinking. The newarmspart is a recipe part like the others, andarms: falseturns the arms off. -
Expressions: a character takes
expression(happy,surprised,thoughtful,sad,sleepy), as a view prop or an FX Spec 1.12 key in the base and instates. It shapes the eyes and the resting mouth (new "O" and frown mouths) while the voice state keeps the gaze and the talking mouth. Changes ease over 0.6 s. -
Recipe reference and schema:
spec/character-recipe-1.schema.jsondescribes every recipe key and part field (generated from the engine, so it can't drift),docs/character-recipe.mdlists them, anddocs/character-remix.mdwalks through a remix; the new exampleremix-latte.fxspec.jsonis Cuppa remixed into a latte glass. Example files point$schemaathttps://sinua.dev/schema/fx-spec-1.json(recipes:…/character-recipe-1.json). -
Remixing characters:
characterRecipe(id)gives a built-in's recipe as JSON, a start for your own (recipein an FX Spec 1.12 file).@sinua/snippetsaddsfitPath(any SVG path, arcs included, fitted into the recipe's 200-unit box) andsvgPaths. -
Palettes: a character takes
palette, a few of its colours by name ({ body: "#E63946" }), as a view prop or an FX Spec 1.12 key in the base and instates. Light and dark tones follow; a dark body lifts the eyes;huenever turns a given colour.characterRecipe(id)gives a built-in's recipe, whose palette names are the slots. Snippets print it as thepaletteprop. -
Tap to hop: a tap on a character plays the new
hopeffect. It hops, smiles, and glances toward the tap, then goes back to the voice state. It is on by default onSinuaCharacter(tap: falseturns it off) and silent; on other objectshopdoes nothing. Apps can play it themselves withtrigger("hop")/SinuaEffect.HOP. -
Character shapes from SVG paths: a recipe's body, patch or band can be a
path(the commandsM L H V C S Q T Z). Later subpaths are holes (a mug's handle). A concave body clips its inner layers exactly, and the outline keeps its width at sharp corners. -
stateAge, a runtime input every view now sets: seconds since the lifecycle state changed. -
State transitions: a catalog definition can mark a value as an arrival (
"transition": "arrive"). A state change then takes the new state's value at once instead of interpolating it (a character's turn blink). -
FX Spec 1.13:
cosmetics. A character can wear a hat, glasses or a badge, as data in the file:bodyandeyesparts on one of its slots (headTop,face,neck,chest), in the slot's units (docs/character-cosmetics.md). Cosmetics move with the pose, the hop and the head turn, takepaletteas<id>.<name>, and are left out at 20 px unlessaccessoriesis on. A cosmetic onheadTopzooms the character out about its feet so the hat fits.fitsandfitlimit or nudge it per character; one a character can't wear warns and isn't drawn. A recipe may carrycosmeticstoo. The slots are public, with each built-in'sheadTop.scaleset for hats (docs/character-recipe.md, Slots and cosmetics). Example:spec/examples/party-hat.fxspec.json. 1.8–1.12 files resolve exactly as before (spec/fx-spec-1.13-resolved.json). -
FX Spec 1.13: the richer look (
docs/character-recipe.md, The richer look). Characters can be airbrushed, rim-lit and grained on today's painter, with no blur:shadelayers (soft masses clipped to the body: core shadows, bounce light, highlights, a head's shadow on the body, a blush that turns with the face),rimlight, andgrain;- elliptical radial lights (
[cx, cy, rx, ry, angle]), stops with alpha, any number of stops, and"light": "none"overlay bodies; - new
grainandshadingprops onSinuaCharacter(every platform) and FX Specparams; under low power an FX Spec sheds them for characters that have them; - the paint contract gains two values in existing fields:
FillGradient.kind2 (elliptical) andFill.blend2 (grain, a shared noise tile); the Web, iOS, Android and React Native painters and the Studio's SVG export draw them. The FFI record and the packed transport are unchanged. - Examples:
spec/examples/rich-bean.fxspec.json,rich-buzzy.fxspec.json. The built-ins are unchanged.
-
FX Spec 1.13: named palettes (
docs/character.md, Named palettes and roles). One palette fits every character:- recipes map
primary/secondary/accentto their slots (roles); palettetakes role names, a built-in palette ("palette": "sunset";sunset,ocean,forest,candy,mono,night) and adarkvariant that the views pick in a dark theme (the Web, iOS, Android and React Native views passdark);- the
paletteprop on every view takes the same keys; - the Studio's character panel has a palette picker.
- recipes map
-
FX Spec 1.13: eye styles (
docs/character.md, Eye styles). Besides the shape eye:glossy: a lens or white sclera, an iris and pupil that follow the gaze, highlights that stay with the light, a lid line;pixel: the eye lit as a grid of glowing cells;dot: a soft glowing point.- A recipe sets its own on its
eyes/faceScreenpart (style,iris,sclera); the neweyeStyleprop onSinuaCharacter(every platform) andparams.eyeStyleoverride it. Blinks, the gaze and every expression work in each style; the built-ins keep the shape eye. The Studios have an Eye style picker.
-
FX Spec 1.13: wardrobes and loadouts (
docs/character-cosmetics.md, Let end users pick).- A file's
wardrobelists what an end user may pick (cosmetics, named palettes); a cosmetic may name itscategory. - A loadout (
{ "loadout": 1, "wear": [...], "palette": "...", "eyeStyle": "..." }) is one small value the app stores; the newloadoutoption / prop on every view applies it and eases a change (a hat pops in, colours blend, a new eye style swaps in a blink). A stale loadout only warns. - For picker screens:
cosmeticsFor(what fits, with a translatable reason),frameStilland the thumbnail helpers (characterThumbnailon the Web,SinuaThumbnailon iOS and Android),applyLoadout.SinuaThumbnail.imageneeds iOS 16 (SwiftUI'sImageRenderer); the package still supports iOS 15, whereframeStill's frame can be drawn yourself. - The Studio's character panel has a Wardrobe group (thumbnails, wear, Copy loadout).
- A file's
-
FX Spec 1.13: catalog packs (
docs/character-cosmetics.md, Catalog packs). Sinua's free catalog (14 cosmetics, among them headphones that glow while speaking and a seasonal winter hat, 6 palettes and 5 eye colours) loads as data:@sinua/web/catalog,SinuaCatalogon iOS and Android,SINUA_CATALOG+ thecatalogsprop on React Native. Files name items as"catalog:<id>"; a brand loads its own pack (from a file or a URL) under its own namespace.- Fit by capability: a slot is enough (a bow tie needs a
neck), andrequires/ recipetagscover the rest; a newframeslot draws round the whole character. - A loadout entry may carry a bounded nudge (
offset,scale,rotate). - The Studio's Wardrobe group lists the catalog beside the file's own items.
- Fit by capability: a slot is enough (a bow tie needs a
-
FX Spec 1.13: the
silhouettepattern (docs/fx-spec.md, v1.13: silhouettes), a head and shoulders of ~1,500 evenly spread dots (64 px; ~380 at 32 px, the outline alone at 20 px) that lives with the voice: waves come in from the outline while it listens, dots flicker along the eye line while it thinks, and waves spread from the mouth with the agent's level while it speaks. The dots never change between states.- Two built-ins,
humanandhelmet, or a file's own outline with its eye line and mouth ("silhouette": { "path", "eyes", "mouth" }). turnYawturns the head (the shoulders stay);hologramgives a hologram look (a bright rim, now and then a band sliding sideways),scanlinesandwireadd texture.- Medium cost; under low power (
performance.lowPower) it draws the first 800 dots, spread just as evenly.
- Two built-ins,
-
Palette slots are named by the part they paint, never by colour: every character's main colour is
body, andoutline,cheeks,shineandirismean the same everywhere (Buzzy'sshell/amber/cyanare nowbody/accent/glow; the full table is indocs/character.md, Palette).spec/character-slot-labels.jsongives every slot a label and a one-line description for colour pickers; the Studio shows them and keeps the light / dark tones behind Advanced.- Eye colour on every character: an
irisslot and role (palette: { "iris": "#7A4BD6" }) colours the glossy eye; a loadout'sirispicks one by name fromwardrobe.irisesor the catalog (catalog:eyes-hazel).
- Eye colour on every character: an
-
Imported characters: a recipe part may name its
role(head,ears,arms…), and a cosmetic'sbehind/abovedraws against it (a beanie behind a fox's ears). A recipe may now have 96 parts (was 48) and at most 4,096 path points in all.docs/character-svg-guides.mddescribes how to name an SVG's groups and guides so it becomes a character. -
A 1.13 file without
states(or missing some) now reacts to the voice: each voice state takes the voice profile. 1.8–1.12 files resolve as before. -
Resolving a file with a large recipe again (a picker's thumbnails, an imported character) no longer re-parses the recipe: the engine finds it by its text, guarded by a second hash.
-
Drag to dress:
characterSlotsgives a character's cosmetic slots where it draws them (frame units, following the pose), andnearestSlotpicks the one a dropped item snaps to. Thumbnails of a recipe file are about 3× faster: whether a character is heavy to draw is now measured once per character, not on every resolve. -
Transitions (part 2: the views): every view (Web, iOS, Android, React Native) moves a weight per state on a continuous clock, so a change mid-transition heads somewhere else without a kink and three quick changes blend. Voice-state changes take the profile's time per pair; a file's own
curveis kept, with the motion's velocity carried into it. The players pin their phase at speed changes (a state with its own speed no longer jumps the pose). From FX Spec 1.13 (and in views without a file), glowing and calibrating lay their dots out once for their densest state, so their sparser states look slightly more organic at rest; older files keep the old look. -
Android: JNA 5.19.1 (16 KB memory pages, required by Google Play for Android 15+); a CI check keeps every 64-bit
.so16 KB aligned, and the 32-bit libraries are aligned too. -
Transitions (part 1: the engine):
- Rate keys (
pulsePeriod,period,surfaceSpeed, …) can be accumulated by the view (ratesontransitionMix,<key>Cyclesoverrides), so a state change late in a session no longer makes a pattern flash or boil. Plain frames are unchanged. This is a bug fix for every file, with no version gate. voiceBlend(sides, weights, target, size): the voice states' weighted mix, for views whose transition clock keeps a weight per state.- Counts that change between voice states fade instead of swapping: the orb lattice's dots, the spectrum's bars, sonar's echoes. Plain frames are unchanged.
- Voice-state changes take the profile's time per pair when the file has no rule
(0.25 s into speaking … 0.9 s back to idle; profileVersion 3).
FxTransition.authoredsays whether the file wrote the curve. - The glow fades its faintest layers instead of dropping them, the speaker ring's shimmer grows from nothing, and a character's elbow eases where its two IK solutions tie (beep's resting arm moves by up to 4 units in the thinking pose).
- Rate keys (
-
Voice (from DevinFit's device test):
- iOS
OpenAILiveVoiceSource/OpenAIRealtimeVoiceSourceset up the audio session (.videoChaton the loudspeaker, Bluetooth;audioSession: .receiver/.unmanaged). Before, GPT-Live on iOS sent no mic audio and played nothing. - OpenAI sources on every platform leave
speakingafter an adaptive quiet tail: 0.7 s for a short reply, growing with the answer up to 1.7 s, so no pause inside an answer flips the state (it was ~300 ms); a barge-in stays instant. - Web:
headersandfetchoptions on the OpenAI sources; a failed request throws withstatusandbody(FatalConnectError/VoiceHttpError).
- iOS
Changed
parameterCatalog()/parameterCatalogJson()no longer carrydescriptiontexts: they are in the typed components' doc comments (your IDE shows them), inspec/parameters.jsonand on sinua.dev. The runtime embeds the catalog without them, minified (and the voice-state profile minified), about 10 KB less (gzip) in every app. Labels, ranges and defaults are unchanged.- Characters are now recipes (
spec/characters/*.json): a palette, a rig and a list of parts from the character part library, read by one schema-checked reader. The four characters draw exactly as before (golden byte-identical). A new look is a new recipe; a part can fade by voice state (show); each recipe declares slots for 1.13's cosmetics. Costs ~17 KB gzip in the wasm.
Removed
- SinuaEdge:
SinuaEdge/<sinua-edge>on every platform, FX Specobject: "edge"and its patternframing(the in-app screen-edge glow, new in beta.6). FX Spec files withobject: "edge"now fail with an error at/object, at every version: the one break of the "minor versions only add" rule, made once during the beta (docs/fx-spec.md, Versioning). Migration: remove the component or the file; Sinua has no replacement, so draw a screen-edge glow in the app itself. The box layout stays (playing, the voice message). Goldensinua2.0.0 drops the 15framingcases.
0.1.0-beta.7
Added
- OpenAI GPT-Live (
gpt-live-1):OpenAILiveVoiceSourceon the Web (@sinua/voice/openai), iOS (SinuaOpenAI) and Android (sinua-openai).- Your server opens the session (
POST /v1/live/sessionswith its key); the source posts{ "sdp": … }to yoursessionUrland takes OpenAI's 201 JSON back unchanged (or the bare SDP).sessionUrlcan't beapi.openai.com, and a credential for it is neversk-…. - State without turn events: the model's audio is
speaking, an open backend delegation isthinking, and user speech that stops the model is a barge-in. The same table on every platform:spec/openai-live-cases.json. disconnect()closes gracefully (session.close, then waits forsession.closed).expired/connection_lostreconnect with a new session.@sinua/voice/server:createOpenAILiveSessionandopenAILiveResponsefor your endpoint.- React Native doesn't have it yet. Beta: run against a live session on the Web; iOS and Android are tested against fakes and the shared table, and barge-in isn't tried live yet.
- Your server opens the session (
- iOS and Android typed components (
SinuaOrb,SinuaRing,SinuaSignal,SinuaBeacon,SinuaEdge,SinuaCore) takeeffect,labels,announce,hapticsand, on a spec,rules, likeSinuaView. Before, onlySinuaViewcould play a one-shot effect there. Web and React Native components already passed them through. @sinua/snippets:effectonbuildSnippetsandeffectLines(platform, effect), the lines that say how an app plays a one-shot effect (the Studio's Play effect adds them to its export);@sinua/design: asparkleicon.- WARP for OpenAI Realtime (
warp: true, off by default): a pre-negotiated event channel plusdcid. On iOS and Android it also turns on libwebrtc's DTLS 1.3 / SNAP / SPED field trials. AcallsUrlbackend must forwarddcid. On GPT-Live,warpsets only the native field trials (OpenAI documents nodcidthere); experimental.
Changed
- FX Spec 1.10: the shared voice-state profile no longer adds particles in listening /
thinking / speaking, so views are calmer. Overrides or a state's
materials.particlesturn them back on. 1.8 and 1.9 files resolve exactly as before (the identity locks hold them); plain views follow the new profile. The orbspeakingpattern sways within ±55° instead of turning edge-on. - One-shot effects on box-layout patterns (the screen-edge
framing, the voice-message barplaying) play in place: the rim or the bars turn green, red or gold and brighten, so a faint resting rim flashes too. There's no ring, tick or burst in the middle of the screen, and no shake that slides the rim away.spec/effect-vectors.jsongains their cases. - SimulatedVoiceSource: the user's turns are louder (0.40–0.78), so signal moves while listening.
- OpenAI Realtime: the credential rule now depends on where the credential goes. On OpenAI's
own host (
api.openai.com, the default) it must still be anek_…. On your own calls endpoint (callsURLon iOS,callsUrlon Android and, new, on the Web) it's your own short-lived token in any shape, but never a rawsk-…key. This is the "sideband" setup, where your backend opens the OpenAI session with its own key and handles tools and transcripts server-side. Beta.6 refused any non-ek_token even there. React Native doesn't pass a calls URL yet.
0.1.0-beta.6
Added
- Real state transitions on every platform. When a state change keeps the pattern (every
voice state, most FX Spec states), the view interpolates the parameters and the speed,
so the shape flows instead of two frames dissolving; the orb lattice trio (glowing /
calibrating / progressing) morphs point by point; anything else cross-fades. Views
without a spec now animate voice-state changes too (they used to jump). Engine:
transitionMix,frameTransitionWithOverrides,fxSpecTransition; callers:StateTransition(Web, iOS, Android) andFxSpecPlayer. - One-shot feedback effects on every platform (docs/fx-view.md, One-shot effects):
success(a green pulse, a ring, a tick),error(a shake, a red tint) andcelebrate(a particle burst), played once on top of any pattern.- Web:
trigger(name)on the handle and on<sinua-view>. iOS/Android: theeffect: SinuaEffectTrigger?parameter. React Native:effect={{ name, key }}. - Reduced motion keeps only the tint (and the tick in place).
- Effects are spoken ("Done", "Something went wrong", "Well done") unless
announceis false;labels["effect:<name>"]rewords them. - The engine draws them (
effectCode/effectAge/effectReducedruntime keys,effect_info), so every platform shows the same frames (spec/effect-vectors.json).
- Web:
- State-aware accessibility on every platform (docs/fx-view.md, Accessibility).
- A view's accessible name follows the state it shows ("Coach, listening").
- State changes are spoken, politely and rate-limited: a state is spoken after holding
1 s, at most once per 3 s. Web uses an
aria-liveregion, iOS VoiceOver announcements, Android TalkBack. - New view options:
labels(per-state words, for translation),announce(default on), nativehaptics(a light tap on "listening", opt-in). - The engine owns the words and the timing (
a11y_accessible_name,a11y_announce_step,spec/a11y-announce-vectors.json), so every platform says the same.
- FX Spec 1.9
accessibility:{ name, states: { key: words }, announce }. - The views apply FX Spec 1.9
rules: without a voice bound, the state the rules derive frominputswins over the app'sstate;rules: falseturns it off.fxSpecDeriveStateon the Web. - Voice button on every platform:
<sinua-voice-button>/ ReactSinuaVoiceButton(@sinua/web), SwiftUISinuaVoiceButton, ComposeSinuaVoiceButtonand React Native<SinuaVoiceButton>. It shows ready / connecting / listening / muted / error, derived from the source, around an engine-drawn ring that swells with the level.- Modes: toggle (connect, then mute / unmute) and push-to-talk. A long press or the ✕ ends the session.
- Accessible as a real button: the name follows the state, and there's an "End voice session" action.
- One state table (
spec/voice-button-cases.json) tests all platforms.
setMuted(muted)on every built-in voice source: silence goes out, the session stays up. AlsoonConnectionChange(connected), since an agent can beidlewhile connected. React Native:handle.setMuted,onConnectionChange,onMuteChange.SharedVoiceSource: one source, many listeners (a source holds one callback of each kind). Views bind a raw source through it, so a view and a voice button (or two views) share a source, and every view shows the source's mute.- Simulated conversations:
SimulatedVoiceSourceon Web (@sinua/core), iOS, Android and React Native (vendor: "simulated"). A script of turns plays like a real agent (state changes, a speech-like level and bands, the barge-in cue) with no microphone, audio or network. The curves come from the engine (conversationAt), so every platform plays the same frames. Four built-in looping samples:calendar,quick-answer,long-answer,barge-in. Controls: play, pause, seek, loop,onFramefor captions. See docs/audio-pipeline.md, Simulated conversations. - FX Spec 1.9: an optional
transitionsblock sets each state change's duration and curve (default,from->to,from->*,*->to); default 0.6 s, easeInOut. - Voice credentials, one contract on every platform (Web, iOS, Android, React Native).
Every source takes
credentialUrl(POSTs to your endpoint) or acredentialprovider, called again on every connect and reconnect, and reads one JSON shape:{ credential, expiresAt?, url? }. Gemini now resumes a dropped session with a fresh token, and ElevenLabs signs a new URL per connect. LiveKit also takes a credential URL or provider. Natively:CredentialSource(Swift.url/.provider/.value, Kotlinurl/provider/fixed) andinit(credentialUrl:)/withCredentialUrl(…); React Native:credentialUrl/credentialon every vendor. @sinua/voice/server:mintOpenAIRealtimeCredential,mintGeminiLiveCredential(model, voice and instructions locked into the token),signElevenLabsUrl,mintLiveKitCredentialandcredentialResponse. Server-only, no dependencies; runs on Node, Next.js, Workers, Deno and Supabase.npx @sinua/voice dev-proxy: serves those credentials on 127.0.0.1 from the keys in your.env, so you can try voice before you write a backend.examples/voice-server/: credential endpoint templates for Next.js, Express, Cloudflare Workers and Supabase Edge Functions.- Web:
pointer: true(<SinuaView pointer />,<sinua-view pointer>) turns on pointer and touch scatter: the dots near the pointer are pushed away from it, easing in and out. It was in the engine and the Studio, but no view fed it. Off by default; not applied under reduced motion or while paused. iOS and Android views follow; until then, passpointerX/Y/Radius/Strengthas overrides. - FX Spec 1.9: an optional
ruleslist derives the state from the app's inputs, e.g.{ "when": { "input": "steps", "gte": 10000 }, "state": "goalReached" }. Rules are tried in order and the first that holds wins;hysteresiskeeps a state from flickering at its threshold. Engine:fxSpecDeriveState(json, inputs, previous), a pure function (the caller keeps the previous state). Views call it next. - Ring
talking: a voice ring around an avatar ("who is speaking"). The ring thickens outward with the voice and never crosses the avatar; ripples travel out while speaking and in while listening, and an arc circles it while thinking. Parameters:innerRadius,avatarGap,thickness,reach,idleOpacity,flow,rippleCount,shimmer. The host view puts the image in the middle. - Signal
playing: a recorded voice message's waveform with its playback position, for chat bubbles. The app passes the clip's loudness asenvelope(up to 64 values; Sinua decodes no audio) and the position asprogress; bars before it are in full ink, the rest dimmed (unplayedOpacity).barCountresamples and keeps the peaks, andplayheaddraws the position line. It's the first box-layout pattern: it lays out insize × aspectbysize, from the newaspectruntime input (catalog:layout: "box"; engine:patternLayout(pattern)). Every view passes its box's ratio and fills the whole box, so it fits a chat bubble. SinuaEdge(new objectedge, patternframing): an in-app screen-edge glow. A colourful rim hugs the inside of the screen or container (rounded corners,cornerRadius), grows inward with the voice and never leaves the box; colour flows round it (hueSpread,flowSpeed) and a bright segment circles it while thinking (shimmer). Hidden at rest, faint while listening. Box layout, portrait or landscape (aspect0.125–8). Unlike the other objects it's colourful by default (saturation0.7); 0 gives the grey ink.SinuaAvatar(React, SwiftUI, Compose): an image in a circle with the ringtalkinground it, for call grids, agent lists and chat headers. The image spans2 × innerRadiusof the view, the same radius the ring draws round, so they can't drift apart; the ring is decorative and the image's label names the avatar.SinuaVoiceMessage(React, SwiftUI, Compose): signalplayingwith drag-to-seek. Passenvelopeandprogress; a drag or a tap reports the position under the finger throughonSeek, on the same row of bars the engine draws (playbackSeekProgress(aspect, x)). It's a slider for assistive tech (arrow keys on the Web, adjustable on iOS, set-progress on Android).- React
SinuaViewno longer forces a square (aspectRatio: 1) on a box-layout pattern;viewLayout(options)says which one a view is. @sinua/core:OrbStatenow liststalking,playingandframing(TypeScript callers got a type error for them), with a test that holds the union to the catalog;FxSpec.objectandParameterObject.idinclude"edge";ParameterPattern.layout.@sinua/snippets: code for a box-layout pattern uses its own box (framing180×390,playing220×44;snippetBox(pattern)); square patterns are unchanged.- Many small views at once:
- on the Web, every view on a page shares one
requestAnimationFrame; - a view whose short side is under 48 px / pt / dp draws at 30 fps by default (the
app's
maxFps, including 0 for display rate, or the spec'sperformance.maxFps, still wins); - on Android a view scrolled out of a clipping parent stops, as on the Web and iOS.
Measured on the Web bench (
apps/fx-bench-web?grid=50&px=32): 50 small orbs went from 869 to 459 ms of work a second, with no dropped frames. For a hundred or more, usepaused(one still frame).
- on the Web, every view on a page shares one
Changed
- Views bind a raw voice source through
SharedVoiceSource(each view keeps its own tracker), so a second view or a voice button on the same source no longer takes its callbacks. Subscribe onSharedVoiceSource.of(source)to listen yourself. - Android:
VoiceSourcegainsonError(it was on the vendor sources only), plus the optionalsetMuted/supportsMute/onConnectionChange/reportsConnection, all with defaults. - State changes default to 0.6 s easeInOut (were a 250 ms cubic ease-out cross-fade on
spec views, and a jump on plain ones). The views'
crossFadeoption is now optional and overrides every change's duration when set (0= a cut, as before); Swift/KotlincrossFade: Double? = nil. - Voice: session config belongs to the server.
OpenAIRealtimeVoiceSource'svoiceandinstructionsare ignored (they only ever applied to the removed client-side mint);GeminiLiveVoiceSource'sinstructionsis deprecated on every platform (a locked token wins). OpenAI'sgetCredential(Web, React Native) is a deprecated alias ofcredential: provider. - Orb
progressing: the day/night line is fixed on screen. The lit side grows from the left asprogressrises, and the sphere keeps spinning under it. It used to turn with the sphere, which read as a spinning two-tone ball, not progress. - Beacon
scanning:blipCountdefaults to 0 (was 3). Blips are the targets your app found (e.g. nearby devices), so pass the count; an empty scope reads as "still looking". Raising the count adds a blip and moves none. - Beacon
scanning: with many range rings (ringCount6–8) the scope no longer turns into a jumbled grid. The dots shrink to fit the gap between rings, sodotSizeis a maximum. The default 4 rings look the same.
Removed
- Breaking (voice, all platforms):
allowInsecureApiKeyand the raw-API-key paths behind it: OpenAI's on-device/in-browserclient_secretsmint, and Gemini's?key=/x-goog-api-keysocket. A raw key is now always refused before the mic opens. Usenpx @sinua/voice dev-proxylocally and@sinua/voice/serverin your backend. Also gone: iOS/AndroidOpenAIRealtimeSignaling.clientSecretRequest/clientSecret, andOpenAIRealtimeVoiceSource's nativemodel/voice/instructionsparameters (the backend sets them; Swift keeps a deprecated overload that ignores them).InsecureCredential.Refusedis replaced byCredentialError(Swift) /CredentialException(Kotlin).
Fixed
- React Native: a spec view's state changes follow the spec's
transitions(the native side always passed a 0.25 scrossFade), and a plain view'sstatereaches the native view, so<SinuaView pattern state="listening">gets the voice-state behaviour. The view also takesaudioStrengthfor the bound voice's pulse. - Parameter catalog:
ink'sspecPathis/ink, the top-level key FX Spec 1.8 reads; it said/params/ink, which the resolver rejects as an unknown key. A test now writes every pattern's every parameter at itsspecPathand resolves the file. - Web: a view that can't be seen (scrolled off screen, or in a background tab) no longer
draws a frame on every
update(). An app that feeds a live level into such a view painted each update anyway. The view now draws the latest state when it becomes visible again. A paused, visible view still draws its still frame at once.
0.1.0-beta.5
Fixed
- Android:
sinua-corenow ships consumer R8/ProGuard rules (JNA and the UniFFI bindings). In an app built with R8 (a minified release build), the first engine call threwUnsatisfiedLinkError: Can't obtain peer field ID for class com.sun.jna.Pointer: a plain app crashed on its first SinuaView. Apps no longer need their own rules for Sinua.
No change on iOS or the Web: npm and SwiftPM move to beta.5 in lockstep.
0.1.0-beta.4
Fixed
- Android:
SinuaView's frame loop now runs as an infinite animation (withInfiniteAnimationFrameNanos), so an app's Compose UI tests can go idle while a view animates on screen. Before,ComposeTestRule.waitForIdlefailed withComposeNotIdleExceptionon any screen showing a SinuaView. Drawing is unchanged.
No change on iOS or the Web: npm and SwiftPM move to beta.4 in lockstep.
0.1.0-beta.3
Changed
- iOS: the xcframework's static libraries no longer carry debug info (
strip -Sinpackages/ios/build.sh). The release zip drops from 23 MB to about 4 MB; the size an app gains is unchanged (checked byte for byte against beta.2).
Added
- CONTRIBUTING.md (maintainers only for now), SECURITY.md (private vulnerability reporting), CODEOWNERS.
No runtime or API change from beta.2.
0.1.0-beta.2
Added
- Android:
dev.sinua:sinua-core,sinua-viewand the voice modules on Maven Central, signed. Thedev.sinuanamespace is verified.
Fixed
- Release: a blank signing key (an unset CI secret) now stages unsigned instead of failing.
- The publishing docs: npm gives a package's first version
latestwhatever--tagsays.
No runtime or API change from beta.1: npm and SwiftPM move to beta.2 in lockstep.
0.1.0-beta.1
The first public beta: npm (@sinua/core, @sinua/web, @sinua/voice, beta tag) and
SwiftPM (sinua-swift, -livekit, -openai). The Android packages (dev.sinua:sinua-*
on Maven Central) follow with 0.1.0-beta.2, once the dev.sinua namespace is verified.
Added
- The runtime: one Rust engine (
core_engine) drawn on every platform. On the web it is WebAssembly:@sinua/core, and@sinua/webwith<sinua-view>and typed components. On iOS it is the SwiftUI viewSinuaViewin theSinuaproduct. On Android it issinua-viewfor Compose. - FX Spec 1.8: the file format the Studio exports and every runtime reads. It has 34 patterns, and each supports four voice states (idle, listening, thinking, speaking). 1.8 is the oldest version the runtime reads. A key added in a later minor is an error in a file that declares an older one, and is dropped rather than silently applied.
glowing:depthTone(0..1, default 1). At 0 every dot has the same tone; the sphere keeps its depth through dot size and opacity. At 1 it draws exactly as before.- Voice:
@sinua/voice,SinuaVoiceandsinua-coredrive the four states from an audio source. The adapters for Gemini Live, ElevenLabs, LiveKit and OpenAI Realtime are beta. On iOS the voice types are a separate product, so an app that only draws doesn't link microphone code. - Typed components generated from the parameter catalog on every platform, with the public API frozen by a snapshot.
Deprecated
@sinua/core's three JSON-bridge frame functions, each naming its direct twin.