00:00 / 00:00

Persona

Plugin API

Persona runs an embedded local WebSocket server that allows external applications to inspect and drive the application: switch scenes, add and arrange layers, toggle expressions, trigger motions, stream lip-synced audio, monitor tracking status, and inject raw model parameters directly.

The server is disabled by default. Enable it under Settings → Plugin API, create an API key, and configure the generated token in your plugin or integration client.

Server Configuration

SettingDescription
Enable APIStarts the WebSocket server. Requires a valid API key to authenticate incoming connections
PortDefault port is 25034
Allow Access on Local NetworkBinds beyond the loopback interface, allowing mobile devices or other computers on the LAN to connect

When local network access is enabled, Persona displays its accessible local IP addresses and port, making it easy to configure connections from remote devices or the Web Console.

If the configured port is already occupied by another process, the settings panel displays an inline error status rather than failing silently.

API Keys

Authentication is mandatory: unauthenticated connection attempts are immediately rejected. Clicking Create Key prompts for a descriptive name and reveals the full token once upon generation; the copy button on the key row allows copying the token to the clipboard at any time.

ActionDescription
Copy TokenCopy the full authentication token to the clipboard
Rename KeyUpdate the descriptive name of the key
Revoke KeyPermanently delete the key. All active sessions using it are terminated and instructed not to reconnect

Each key row displays real-time connection status: Connected, Never used, or a relative Last used … timestamp. When a client provides client information during handshake, the row records its reported name and version, keeping the key identifiable even when no client is currently connected.

Tokens are stored securely in the operating system credential vault (such as macOS Keychain or Windows Credential Manager). In environments where secure storage is unavailable, key generation fails with a descriptive error rather than writing plaintext credentials to disk.

Plugin Folders and Registration

Plugins can request Persona to register and load model or asset files. model.register and asset.register accept absolute paths only inside a user-configured plugin folder (configured via native dialog in settings); any other path is rejected with a forbidden-path error. Containment checks resolve symlinks on both sides.

With the asset-import capability, a client connected from the same computer (over loopback) can also call registry.pluginFolders (returning { folders }) to read the configured plugin folders, or asset.import { path, want? } to register a local file into the Inventory:

  • Files inside a plugin folder or already in the Inventory register immediately without prompting; files elsewhere prompt the user in the desktop Control Panel (Add File, naming the client and API key label, and hiding the folder name while Stream Mode is on). If the user declines or does not respond within 45 seconds (ASSET_IMPORT_PROMPT_MS), the call returns forbidden-path; a concurrent import while a prompt is open returns invalid-state. Non-loopback clients receive forbidden-path for both methods.
  • Both asset.register and asset.import wait for the stage to refresh its asset list before returning, so an immediate object.add can load the asset.

Registered asset IDs are content-addressed (a sha256(path) prefix), so they stay the same across restarts and never expose raw filesystem paths over the network. On ModelRef and AssetRef, origin: 'bundled' covers both content shipped with the app and content downloaded on demand from the Persona catalog on the desktop; catalog items that have not been downloaded are omitted, and the API has no download method.

asset.register takes a want field that asserts the asset kind; a mismatch returns invalid-params:

  • image, video, prop, ibl, lut
  • animation and cameraMotion, which share the .vmd extension and are told apart by their file headers
  • audio (WAV, MP3, OGG, FLAC, M4A or AAC files) and lyrics (.lrc files): asset inventory types, not ObjectContent, so registering them does not create a stage layer

A model reference carries its entry file's basename as entryFile. From model.list only, it also carries its .vtube.json basename as vtubeFile (null without one, and never a directory). The SDK's modelMatchesFile resolves a model-load hotkey's stored file against these names, so match hotkeys against model.list, not against scene item references.

Connected Clients

While the server is active, the settings panel lists all open client sessions, displaying their declared application name, version, developer identifier, the API key in use, and the remote client IP. Browser-based clients also display their page origin.

Clients that do not send an identification payload are listed as Unidentified client and retain full API access. Clicking the disconnect button on a session terminates the connection and directs the client not to reconnect.

Whenever a new connection is accepted, Persona posts a silent desktop notification detailing the client name, key, and origin IP. If operating system permissions prevent notifications (such as on unsigned developer builds or disabled notification privileges), a toast appears in the Control Panel instead while it is open. Notifications never render onto the stage canvas, preventing them from appearing in video captures.

The Web Console

See Web Console for these controls.

Stream Deck

The official Stream Deck Plugin is another first-party client built on the Plugin API, mapping model and scene switching, hotkey triggers, expression and motion playback, and avatar transformation adjustments directly to physical hardware keys.

MCP Server

@laplace.live/persona-mcp is the official Model Context Protocol server, exposing the Plugin API to AI assistants (such as Claude, Cursor, and Codex) through task-oriented tool definitions alongside generic call_api RPC execution and stage.capture visual feedback.

SDK

@laplace.live/persona-sdk is the official TypeScript/JavaScript client library for Node.js, Bun, modern browsers, and OBS browser sources.

import { PersonaClient } from "@laplace.live/persona-sdk";

const persona = new PersonaClient({ token: "sk-lp-v1-…" });
await persona.connect();

await persona.call("expression.toggle", { name: "Smile" });
persona.on("motion.started", (m) => console.log("playing", m.group));

All methods are governed by a shared, strongly-typed wire schema, providing compile-time parameter and return type inference. The SDK Reference details installation, client options, authentication, parameter lease management, and all exported types.

Events

Clients receive only the events they subscribe to. Following any reconnection, the SDK automatically restores all active subscriptions.

EventDescription
scene.changedScene lifecycle modification: creation, deletion, renaming, activation, or patch mutation
scene.loadingStart or completion of scene activation or render pipeline shader warm-up
settings.changedGlobal application configuration updated
instance.loadedA model instance has completed loading and stage placement
selection.changedActive editor selection changed from any input source
expression.changedThe active set of enabled facial expressions changed
expression.persistenceThe persistent expression toggle state changed on a model
expression.groupsA model's expression groups changed, from any editor or a renamed or deleted expression file (hosts with expression-groups)
motion.startedA motion clip started playback; includes the oneShot flag on hosts with motion-stop
motion.endedA motion clip finished playback
speech.startedSynthesized audio initiated via speech.play started playback
speech.endedSpeech playback finished, was stopped manually, or was superseded
hotkey.stateAn on-stage model's hotkey configuration or background-firing state updated; empty list when the model leaves the stage
automation.stateAutomation list, names, trigger kinds, enabled states, or background-firing states updated
tracking.statusAggregate face or pose tracking status, or any single source's state, updated (face, hand, and body tracking alike)
registry.changedLocal model or asset registry updated, a file went missing or returned, or a model's thumbnail changed; refetch lists for that kind and drop cached registry.thumbnail results
model.movementA model's Movement Config changed from any editor (hosts with model-movement)
model.versionsA model's version history changed (a version saved, named or switched to, history turned on or off), or Persona wrote one of its files; re-run model.listVersions (hosts with model-history)

Method Overview

Methods scoped to a specific model accept an optional instanceId parameter, defaulting to the Primary Model of the active scene when omitted.

NamespaceMethods
scene.*list get activate create duplicate rename delete patch inspect setLightFollowCamera
preset.*create update rename setTransition delete apply (requires the scene-presets capability)
instance.*list get add setModel remove setVisible setEffects setTrackingSources reorder setPrimary setPlacement attach setMToon setIdle setControllerMovement setLipSync info
binding.*get preview set release inputs override
controller.*state rename setDeadZone remove assign
selection.*get set
object.*add addMany rename setContent setText setSpace setPlacement attach setLightOverrides anchors
expression.*list active toggle getPersistence setPersistence, plus getGroups setGroups (requires the expression-groups capability)
motion.*list play playing stop
speech.*play stop
hotkey.*list set trigger
automation.*list run, plus get create update delete (requires automation-editing) and control status (requires automation-transport)
model.* / asset.*Respective list and register methods, asset.import and registry.pluginFolders (requires asset-import), registry.thumbnail, model.getMovement / model.setMovement for Live2D models, and model.listVersions setVersionHistory saveVersion nameVersion switchVersion restoreFiles compareVersions for Version History
fonts.*list (requires object-text capability)
settings.*get patch
tracking.* / pose.*tracking.addSource tracking.updateSource tracking.removeSource, setEnabled, setSource, pose.setPort, and tracking.status
lipSync.*state configure restart calibrate
stage.*resetTransform resetCamera spawn clearSpawned capture
app.* / session.*app.info app.localAddresses app.stats, and session.identify
storage.*get set delete list
param.*inject release

scene.create appends an empty scene and activates it immediately, just as creating a scene in the desktop app does. The response's activeSceneId is the new scene's ID: use it directly instead of comparing scene lists, which can race with other client sessions.

scene.patch updates the active scene's background, camera, lighting, environment and Scene Transitions. Scene items change separately, through instance.* and object.*.

Setting a light's enabled to false turns off its light and shadows but keeps its other settings. The field defaults to true; older host builds omit it and can't turn lights off individually.

Camera

vrmCamera sets the camera in one batch: orbit, fov (1 – 179°), roll (rotation around the view axis, in radians), projection (perspective or orthographic) and clipAssetId.

  • Orbit angles take any radian value, and distance can be negative. Mouse navigation keeps its narrower limits.
  • In orthographic projection, the view width is abs(distance); fov is kept but has no effect. A negative distance flips both viewport axes.
  • stage.resetCamera resets roll and projection to their defaults.
  • Older hosts don't report roll and projection, which then default to 0 and perspective. Check that the fields exist before showing their controls.

vrmCamera.follow makes the camera follow a layer: { instanceId, anchor, binding, damping }. It defaults to null and requires the camera-follow capability.

FieldDescription
instanceIdThe VRM model or 3D prop to follow. Pass { target: 'primary' } to follow whichever avatar is primary; following pauses while the primary is not a VRM
anchorOn a model, { kind: 'bone', bone } (default head) or { kind: 'root' }. A 3D object always follows its root
bindingHow the target's rotation turns the camera: world ignores it; yaw, yaw-pitch and full follow yaw, yaw and pitch, or all three axes; lazy turns only when the anchor leaves the frame sideways
dampingSmoothing time per axis in seconds, 0 – 10 (0 locks instantly): x, y, z for position and yaw, pitch, roll for rotation
  • orbit sets the framing for when the target stands at the origin. While following, the camera moves with placement, walking, motions and tracking without writing back to orbit.
  • Picking another layer or anchor glides across. Removing the followed layer, or moving it into 2D, resets follow to null.
  • A playing camera motion overrides following. stage.resetCamera reframes the target and keeps follow.

vrmCamera.aim turns the camera in place to keep a target in a chosen area of the screen: { instanceId, anchor, screenX, screenY, deadZone, softZone, bias, damping, lookahead }. It defaults to null and requires the camera-aim capability.

FieldDescription
screenX, screenYWhere to hold the target, as a normalized screen position (0 – 1 from the top-left)
deadZoneWidth and height, each 0 – 1. While the target stays inside, the camera holds still
softZoneNever narrower than the dead zone. Past it, the camera turns at full speed
biasShifts the soft zone
dampingPast the dead zone, the camera pans toward the target over damping.horizontal / vertical seconds
lookaheadlookahead.time (0 – 1 s, 0 for off) anticipates target motion from its velocity, smoothed over lookahead.smoothing (0 – 30), with an ignoreY toggle
  • The camera's position and distance stay put; only the view direction turns, heading about world Y and tilting about the camera's local X axis while keeping lens roll.
  • The camera holds still while gizmos are dragged or models load.
  • Aim clears in the same cases as follow, and camera motions override it.

vrmCamera.focus keeps the Depth of Field focus plane on a target layer and anchor instead of the look-at distance. It defaults to null and requires the camera-focus capability; its damping defaults to 0.2 s.

  • If the anchor moves behind the camera, focus falls back to the look-at distance.
  • Focus stays active during camera motion playback, and has no visible effect while Depth of Field is off.

vrmCamera.handheld sways the camera with procedural noise, for a natural handheld feel. It defaults to { enabled: false, intensity: 0.5, speed: 1 } and requires the camera-handheld capability; intensity ranges 0 – 1 and speed 0 – 2.

  • Turning it on or off, or changing the intensity, eases over about 1 second.
  • It is not saved to scene files, camera motion playback overrides it, and it pauses while viewport gizmos are dragged.

Objects and Text

object.add accepts place2d or place3d to place the prop in the same transaction that creates it, so it never renders a frame at the world origin. The values are clamped as in object.setPlacement.

object.addMany { objects } adds up to 100 objects in one scene edit, with a single save and a single undo step, and returns their instanceIds in request order. If any asset fails to resolve, the whole operation is rolled back.

With the object-text capability, ObjectContent can be a text layer: { kind: 'text', text, font, size, color, shear, letterSpacing, wordSpacing, lineHeight, align, verticalAlign, boxWidth, boxHeight, overflow, stroke, shadow, plate, sung, extrude, bevel, bevelResolution, roughness, metalness, emission, faceCamera }. One style applies to the whole layer.

FieldDescription
fontA system face named by postscriptName, falling back to family, weight, italic and stretch where that face is missing. null uses the default sans-serif font
sizeIn px. In 3D, text scales at 1000 px per meter, matching image quads
boxWidth, boxHeightIn px
letterSpacing, wordSpacingIn em
lineHeightA multiple
align, verticalAlignThe placement pins the alignment anchor, like a Blender text's origin: left-aligned text grows rightward
stroke, shadow, plate2D only: outline, shadow and backing plate. Their paint extents are in px
sung: { color, strokeColor }Paints what a lyrics clip has sung: the fill in both spaces, and the outline in 2D
extrude, bevel3D only, in em
bevelResolution3D only. Bevel segments
roughness, metalness, emission3D only. How the glyphs are lit
faceCamera3D only. Turns the text toward the view
  • In 3D, text renders as Blender-style extruded glyphs. Each space keeps the other space's fields untouched.
  • Every field applies live. object.setContent on a text layer edits it in place like a drag, with one undo step per typing pause.
  • fonts.list returns the desktop host's installed font families and faces, for remote font pickers.
  • Older hosts without this capability drop text layers.

object.setText { instanceId, text, transient? } changes only a text layer's text. It applies live, with one undo step per typing pause.

  • transient: true shows temporary text without writing to scenes.json or recording an undo step. The text stays until text: null is sent, the saved text is modified, or the layer leaves the stage (it is removed or another scene is activated). scene.inspect reports it as the object's liveText.
  • Calling it on a non-text object returns unsupported-for-format.
  • Text longer than TEXT_LENGTH_MAX is truncated.

With object-video-sound, object.setContent edits a video layer's loop, muted and volume in place like a drag, with one undo step per pause, instead of swapping the scene. The sound plays through the desktop host's configured audio output device. Older hosts swap the scene for each edit, producing separate undo steps, and play sound through the system default device.

With object-chroma-key, image, video and webpage layers accept chromaKey settings, { enabled, color, similarity, smoothness, spill }, which object.setContent edits in place with one undo step per pause. Older hosts drop this field during normalization.

instance.attach { instanceId, attach } attaches a Live2D instance to another Live2D model as an accessory item, matching object.attach. Passing null detaches the item in place.

FieldDescription
attach.depth{ kind: 'front' }, { kind: 'behind' } or { kind: 'artMesh', id }. The last places the item directly above that ArtMesh of the parent, falling back to front if the ID is missing
attach.split{ itemArtMesh, depth } splits the item at one of its own ArtMeshes and draws the part beneath it at a second depth
  • object.anchors returns an order field with each ArtMesh's draw order at the time of the query, which a depth control uses to order its options.
  • A circular attachment returns invalid-params, and calling it on a VRM instance returns unsupported-for-format.
  • With object-pin-depth, a 2D image, video or webpage pinned to a Live2D model also respects attach.depth, interleaving inside the parent model's stack. It renders whole, without split support. For 3D props, only front is supported for now.
  • object-pin-depth also stacks 2D webpages in scene order with post-processing effects, dropping the legacy layer field. Older hosts ignore the depth setting, stack pinned media by scene order, and composite 2D webpages wholly behind or in front of the stage using layer.

Stage

stage.resetTransform { instanceId? } resets a layer's placement. Without instanceId, it resets the layer selected in the app, or the primary model if nothing is selected. A loaded avatar eases home over about ⅓ second.

stage.capture returns a transparent PNG screenshot of the stage viewport as { dataUrl, width, height }. It requires the stage-capture capability. The longest edge is scaled down to at most maxEdge (64 – 2048, default 1024), never up.

  • Headless clients, such as AI agents using the MCP Server, rely on it to inspect what their scene changes did.
  • Capturing stalls the rendering pipeline for one frame, so call it on demand rather than polling.
  • renderer-unavailable means the stage window isn't initialized or has no frame buffer to copy. While the stage recovers from a crash, retry until the first frame renders.

Capabilities

Application-level and instance-level capability flags declare which features the running host build supports. Always inspect capabilities rather than parsing semantic version numbers.

Application capabilities are reported in the hello handshake response and in app.info:

CapabilityWhat It Adds
storagePlugin Storage
speechSpeech Playback
automationsAutomations
layer-effectsLayer Effects
scene-transitionsScene Transitions
light-areaArea lights
light-spotSpotlights
light-camera-followLights that follow the camera
light-shadow-filterPer-light shadow filtering, PCF or PCSS
environment-map-modelAn environment map and an environment model together
stage-spawnstage.spawn
idle-tracking-lostTracking-lost idle settings
motion-stopmotion.stop
stage-capturestage.capture
controllerscontroller.* and the controller.enabled setting
model-editingbinding.* and instance.setBreath
model-movementmodel.getMovement and model.setMovement
asset-inspectionscene.inspect and registry.thumbnail
object-pin-depthDepth for 2D media pinned to a Live2D model
object-textText layers and fonts.list
camera-followvrmCamera.follow
camera-aimvrmCamera.aim
camera-focusvrmCamera.focus
camera-handheldvrmCamera.handheld
object-video-soundIn-place video edits and sound routing
object-chroma-keyChroma key on image, video and webpage layers
model-movement-pivotA pivot for model movement
layer-effects-2dLayer effects on 2D images, videos, webpages and text
scene-presetsScene Presets
background-alphaAlpha in the background color
binding-calibrationPer-input binding calibration
binding-random-pulsesRandom pulse inputs and randomPulses schedules
expression-groupsExpression groups
expression-groups-hide-firstHide, Then Show switching on expression groups
model-historyModel version history
automation-editingautomation.get, create, update, and delete
automation-transportautomation.control and automation.status
asset-importasset.import and registry.pluginFolders

Names lead with what they extend (light-spot, object-text). Hosts also list each renamed capability's old name for older clients, and the SDK client folds old names into current ones.

Instance-level capabilities are returned in instance.list under capabilities; calling a method that an instance's format doesn't support returns unsupported-for-format. instance.list and instance.get also include loadError?: string | null explaining why the last load failed (null while loaded or still loading; omitted by older hosts).

Controllers

controllers exposes controller.* to read connected devices and saved profiles, rename profiles, set dead zones, remove profiles and start assignment. It also lets settings.patch set the controller.enabled master switch. These are controllers connected directly to the desktop host.

instance.setControllerMovement updates an instance's Controller Movement settings: enabled, slot (the saved profile number), moveSpeed, turnSpeed and smoothing. VRM models also accept walkEnabled, walkClip, walkSpeed and faceMovement; walkClip uses the same reference format as instance.setIdle. Out-of-range values are clamped, and calling the method on a non-avatar object returns unsupported-for-format.

Parameter Bindings

model-editing exposes instance.setBreath and binding.*, which reads, previews, writes and releases parameter bindings.

  • binding.override sets temporary input overrides.
  • binding.inputs returns { inputs, outputs }: inputs holds the raw tracking streams, and outputs the computed parameter readings after motions, expressions and physics. Outputs the model doesn't declare are left out.
  • Time24h and Time12h are built-in binding inputs with a 0 – 1 range. They sample the desktop's local wall clock once per rendered frame, independently of face tracking, for both Live2D and VRM bindings (see Time Parameter Bindings). TIME_INPUT_NAMES, TIME_INPUT_RANGES and isTimeInputName expose them in the SDK.

binding-calibration adds per-input calibration:

  • binding.get returns it: {} when uncalibrated, and omitted by older hosts without the capability.
  • binding.preview and binding.set accept connections, calibration, or both. Whichever side is omitted stays untouched both live and on disk.
  • Calibrating alone never writes bindings to <model>.persona.json, so .vtube.json bindings stay active.

binding-random-pulses adds the built-in random pulse inputs RandomPulse1 through RandomPulse8 (0..1, independent of face tracking) and per-model randomPulses schedules:

  • A binding whose inputs are all random pulses fires over whatever else moves its parameter (toward outRange[1], leaving it alone between firings unless its rest output differs from the parameter's default), so it can share that parameter with another binding (see Parameter Bindings).
  • binding.get returns randomPulses: {} when no pulse has a custom schedule, and omitted by older hosts without the capability. Each schedule is { intervalSeconds: [min, max], durationSeconds: [min, max], fadeInSeconds, fadeOutSeconds } (in seconds, 0.1 – 3600 for ranges and 0 – 3600 for fades); unconfigured pulses use DEFAULT_RANDOM_PULSE_SCHEDULE (0.5 – 4.5 s interval, 0.58 s duration, 0.29 s fade in and out, matching VTube Studio's auto-blink).
  • binding.preview and binding.set accept connections, calibration, randomPulses, or any combination. Whichever parts are omitted stay untouched both live and on disk, and saving randomPulses alone does not copy .vtube.json bindings into <model>.persona.json.

Whole-Model Movement

With model-movement, model.getMovement and model.setMovement read and edit a Live2D model's Whole-Model Movement settings, as defined by VTube Studio.

MethodDescription
model.getMovement { modelId? }Returns { movement }
model.setMovement { modelId?, movement }Merges a partial update { enabled, x, y, z }, clamping amounts to integers within ±50 (MODEL_MOVEMENT_MAX). Returns { movement, saved }, where saved tells whether the settings were written to disk. It is false for read-only or unwritable files: the change then stays in effect until Persona quits, and the file's next successful write saves it
  • modelId defaults to the active primary model. An unknown model returns not-found, and a VRM model returns unsupported-for-format.
  • The settings are stored per model, not per instance: in the model's .vtube.json when present, otherwise in .persona.json, so they stay interchangeable with VTube Studio.
  • Movement shifts the model's placement directly, without writing parameter values, and stacks with any FacePosition* parameter bindings.
  • Defaults match VTube Studio (enabled, 6/8/11). Every change fires the model.movement { modelId, movement } event.

model-movement-pivot adds pivot to movement: the point the model grows around as z changes. It is { kind: 'root' }, the model's center and the default, or a point on an ArtMesh, { kind: 'artMesh', id, verts, weights }.

  • The pivot is stored in the model's .persona.json, even when the model has a .vtube.json.
  • It is evaluated at the point's position in the model's rest pose, so head motion never moves the model.

Expression Groups

With expression-groups, expression.getGroups and expression.setGroups read and edit a model's expression groups, following VTube Studio's Expression Groups rules: within an enabled group, showing one member hides every other shown member so the group shows at most one expression at a time.

MethodDescription
expression.getGroups { modelId? }Returns { supported, groups, edited }, each group { id, name, enabled, hideFirst?, entries: [{ expression, fadeSeconds, inverted }] }. supported is false when the model has no config file Persona can write; edited is true when .persona.json holds its own group list on a model that also has a .vtube.json (omitted by older hosts)
expression.setGroups { modelId?, groups }Replaces all groups with lenient validation: missing or duplicate id values are regenerated, entries without an expression are dropped, and fadeSeconds is clamped to 0 – 2 s. Returns the saved { supported, groups, edited }
  • expression is the member's key: its .exp3.json basename on Live2D, its name on VRM, matched case-insensitively.
  • An expression counts as shown when it switches on, or when an inverted member switches off (an inverted member counts as shown while off, such as a "Hat Off" expression on a model that wears a hat by default). When a member is shown, every other shown member in each enabled group it belongs to is hidden (normal members switch off and inverted members switch on), each fading over its own fadeSeconds. Hiding one member never shows another, so a group can have no members shown; if a member belongs to multiple groups that conflict, the later group wins.
  • With expression-groups-hide-first, a group can set hideFirst: true (Hide, Then Show): when a member becomes shown, its fade-in waits until every other member hidden by that group has completely faded out (including inverted members fading in). When false or omitted, members Crossfade as in VTube Studio. Group-triggered hides, clearing all expressions, restores, reconciliations, and setting a weight to zero do not wait; older hosts without this capability drop hideFirst.
  • A member's fadeSeconds overrides the fade duration of whatever triggered the switch, including a hotkey's fadeSeconds. Disabled groups and their fade durations are ignored.
  • Groups apply regardless of how an expression is switched: expression.toggle, hotkeys, automations, clearing all expressions (after which each group reconciles and keeps its first shown member), and restoring remembered expressions. expression.changed reports the resulting active expressions.
  • expression.setGroups immediately reconciles the expressions shown on stage when enabling or disabling a group, or when adding, removing, or inverting a member. Renaming a group, changing fade times, or changing hideFirst does not trigger reconciliation, matching VTube Studio's editor.
  • Until .persona.json has its own group list, Persona imports .vtube.json's ExpressionGroups read-only (edited: false). Calling expression.setGroups (or editing groups in Persona) saves the full list to .persona.json, which takes precedence over .vtube.json from then on (edited: true when .vtube.json is also present) and never writes to .vtube.json. Renaming or deleting an .exp3.json in the Expression Editor updates matching entries in .vtube.json in place without .persona.json taking over, as well as in .persona.json if present.
  • Every change fires expression.groups { modelId, groups, edited } (edited is omitted by older hosts), including updates caused by renaming or deleting an expression file.

Version History

With model-history, clients can read and manage a model's saved versions, corresponding to the Versions section in a model's details on the desktop (see Version History). Every method accepts modelId (defaulting to the primary model) and returns invalid-state for bundled models. Versions are saved only when explicitly requested.

MethodDescription
model.setVersionHistory { enabled }Enables or disables version history and returns { enabled }. Enabling history should be a user decision; turning it on saves an initial version
model.listVersions { limit? }Returns { enabled, versions, more, unsavedFiles }: up to limit ModelVersion entries newest first (1 – MODEL_VERSIONS_LIST_MAX, default MODEL_VERSIONS_LIST_DEFAULT), whether older versions exist, and the file paths on disk that differ from the current version (ignoring which expressions are toggled on)
model.saveVersion { name?, notes? }Saves the current files as a version and returns { version } (or returns the current version if nothing changed and no name was provided)
model.nameVersion { versionId, name, notes? }Names or renames a version, or clears its name when name: null. Names are case-insensitively unique across versions; duplicate names return invalid-params
model.switchVersion { versionId, unsavedChanges? }Switches the model's files to the specified version while preserving active expressions, reloads the model, and returns { version, backup }. Switching does not create a new version
model.restoreFiles { versionId, paths, unsavedChanges? }Restores only the listed file paths and returns { backup }; restored files remain unsaved changes until the next save
model.compareVersions { from, to }Returns { files }: every path present on either side with its status (MODEL_VERSION_CHANGE_STATUSES). Either side may be MODEL_VERSION_CURRENT for the current files on disk; config files that differ only in active expressions are marked unchanged
  • A ModelVersion has { id, parentId, time, kind, message, name, notes, current }. kind is one of MODEL_VERSION_KINDS (first, saved, backup), or null for a commit created by another Git tool (in which case message is the first line of its commit message). current marks the version that the files on disk are based on.
  • When switching or restoring would overwrite files with unsaved changes, the call returns invalid-state unless unsavedChanges (MODEL_VERSION_UNSAVED_ACTIONS) is specified: 'save' first saves all unsaved changes as a backup version (returned in backup), while 'discard' discards the overwritten changes.
  • While version history is disabled, saving, switching, and restoring return invalid-state; listing, comparing, and naming can still read previously saved history.
  • If the model's files are missing, enabling history, saving, switching, and restoring return not-found. Enabling history on a folder that version history cannot read (such as a UNC share, or a folder whose name contains a backslash on macOS or Linux) returns invalid-state.
  • Unwritable files, or a repository left in an unfinished state by another Git tool (staged files, an in-progress merge, or detached HEAD), return invalid-state without modifying any files.
  • Names are up to 80 characters (MODEL_VERSION_NAME_MAX_LENGTH) and notes up to 2,000 (MODEL_VERSION_NOTES_MAX_LENGTH).
  • Every change fires model.versions { modelId }; file edits made by external applications do not fire this event. Older hosts do not support these methods.

Motions and Idle

motion.list retrieves motion groups configured on a model. For Live2D models, the host also indexes .motion3.json files in the model folder that .model3.json doesn't declare, creating a motion group named after each file. Motions recorded in the desktop Motions tab appear without a model reload.

motion.stop ends the running one-shot motion and eases the model back to idle rather than snapping (motion.ended fires at once, not after the ease). It requires the motion-stop capability. If only the idle animation is playing, the call does nothing and returns { stopped: false }.

motion.playing reports a oneShot flag, also sent in motion.started, that tells whether the clip is an overlay animation above the idle, the kind motion.stop ends. Older builds omit this field.

instance.setIdle updates idle configuration for an instance:

FieldDescription
idleAnimationMaster toggle for idle animation. false stops the VRM animation clip, or all Live2D idle motions (including the model default Idle group)
idleClipVRM only. Reference to the assigned idle animation clip
idleMotionLive2D only, requiring idle-motions capability. Motion file to loop underneath all animations; null loops one motion picked at random from the built-in Idle group. A file that no longer exists falls back to null
trackingLostBehaviorRequires idle-tracking-lost capability. hold freezes the last tracked pose; idle smoothly transitions to idle over about 0.3 seconds and returns when tracking recovers
trackingLostMotionRequires idle-tracking-lost capability, Live2D only. Motion played in place of idle when tracking is lost past the configured delay; null disables override
trackingLostDelayRequires idle-tracking-lost capability. Tracking loss timeout threshold in seconds, 0 – 60

Calling instance.get returns these idle settings alongside instance state.

Lighting

With the light-area capability, SceneLight accepts the area type: a rectangular light at { x, y, z }.

FieldDescription
azimuth, elevationOrientation, in degrees
rollIn-plane rotation, −180 – 180°, default 0
width, heightSize, 0.01 – 20 units each, default 1
  • Area lights illuminate PBR materials and MToon avatars; on MToon, the toon shader approximates them with four sample points. They don't affect Live2D or cast shadows.
  • range and shadow settings are preserved when the light changes type.

With the light-spot capability, SceneLight accepts the spot type: a spotlight at { x, y, z }.

FieldDescription
azimuth, elevationBeam direction. When both are zero, the beam points toward −Z; positive elevation tilts it downward
range0.1 – 20 units
angleHalf-angle of the cone, 1 – 89°, default 30
penumbraSoftens the cone edge, 0 – 1, default 0.3
  • Intensity ranges 0 – 20, as for point lights.
  • Spotlights illuminate PBR and MToon materials, support shadow quality and radius adjustments, and cast volumetric fog. Creating area lights and spotlights, or converting a light to either type, works only on hosts with the matching capability.

shadowFilter selects pcf (the default) or pcss filtering for each directional, point or spot light, and both can be mixed in one scene. Gate it on the light-shadow-filter capability: locally created lights can carry shadowFilter even on hosts that don't support it. Older scenes load with PCF.

FieldDescription
shadowQualityShadow map resolution; off disables shadows
shadowRadiusPCF edge softening, in shadow map texels. PCSS leaves the saved value untouched
shadowSourceSizeLight size for PCSS, which searches for blockers and widens penumbrae with distance. Degrees for directional lights, meters for point and spot lights; 0 – 10, default 0.1. 0 gives hard shadows

followCamera makes a light move with the camera. It defaults to false, requires the light-camera-follow capability, and ambient light ignores it. When true, the light's position and orientation are computed relative to the camera, so it moves with camera navigation and trajectory playback.

followCameraOptions chooses which parts of the camera transform to follow; each defaults to true:

FieldDescription
positionFollows the look-at target and pan
rotationFollows world-space rotation, including roll
distanceFollows the signed orbit distance
  • Modifying fov alone does not move the light.
  • A disabled component freezes at its value stored in followCameraReference: targetX, targetY, targetZ, distance and a world-space rotation quaternion. Without a reference, it uses the origin, no rotation and zero distance.

scene.setLightFollowCamera { lightId, followCamera, options? } turns camera following on or off for one light in the active scene, or merges new options, without moving or rotating the light in world space. Like followCamera, it requires light-camera-follow.

  • The desktop recalculates the light's coordinates from the displayed camera, including camera motion playback, and returns { light }, or light: null if the light was deleted.
  • scene.patch and scene loading apply the values as written, without this conversion.

SceneLight.volumetric toggles illuminated volumetric fog per point, spot, or area light (default false); other light types ignore it. Older builds used a single global toggle; scenes saved under that setting automatically enable volumetric on all supported lights upon loading.

SceneEnvironment includes optional volumetricLighting and clusteredLighting settings. Older builds omit them, so check that a field exists before offering its control.

volumetricLighting defines scene-wide fog parameters. It has no switch of its own: fog renders only when at least one active light has volumetric illumination enabled. Current hosts return the complete settings; edit them through scene.patch:

PropertyRangeDefault
density0 – 101
intensity0 – 101
range1 – 100 scene units from camera20
qualitylow, medium, highmedium
speed0 – 5 (0 pauses fog motion)0.2

Fog scatters around enabled lights, obeys depth occlusion, and remains transparent where unlit. While any active light has fog enabled, the point, spot and area lights built into 3D props and environment models scatter it too.

Fog requires a perspective camera; orthographic views preserve settings but skip rendering. Directional and ambient lights do not participate, and Live2D does not occlude fog.

clusteredLighting: true accelerates forward lighting for 1 – 64 bounded, non-shadowed point lights under WebGPU perspective cameras. It defaults to false. A point light without a bounded range, more lights than that, or an unsupported renderer or camera projection falls back to standard shading. It doesn't reduce shadow rendering cost or change which lights the fog samples.

In builds with environment-map-model, SceneEnvironment can specify both iblAssetId and modelAssetId:

  • bakeFromModel computes an environment probe from the 3D model, using the environment map as input; when false, the map illuminates the scene directly, falling back to the model's built-in sky if there is no map.
  • showSkybox controls only the background, which shows the map or the model's sky, whatever provides the lighting.
  • Clearing one asset preserves the other; environmentClearPatch() generates that patch. Older builds allow only one environment asset at a time, so clear the other one when setting a new one.

scene.patch also provides these optional panoramic controls. They exist only on supporting builds, so check that a field exists before showing its control:

FieldDescription
skyboxBlur0 – 1, default 0. Affects only the visible skybox background
skyboxIntensity0 – 2, default 1. Affects only the visible skybox background
iblRotationIn radians, default 0, and may go past a full turn. Rotates the map and its lighting environment together, including the sky used for model-generated lighting

Physics Simulation

SceneEnvironment.physics controls the global Physics Simulation: dynamic 3D props fall under gravity and collide with the stage floor and VRM avatars. It defaults to false, and older builds omit it. Simulated transforms are transient and never persisted, so object positions read through the API are always the initial placements; disabling physics immediately returns all props to those placements.

stage.spawn drops registered prop assets onto the stage as dynamic physics bodies. It requires the stage-spawn capability and returns { spawned, alive } on success, where alive counts every physics body now on the stage.

Spawned bodies exist only in renderer memory: they create no scene items or layer entries, cannot be undone and are not saved. Copies of one asset share geometry, textures and draw calls, so hundreds of them stay fast. A call returns invalid-state while physics is disabled. An unknown asset returns not-found, and an asset that isn't a prop returns invalid-params.

FieldDescription
assetIdRegistered prop asset ID
countQuantity to spawn, 1 – 200, default 1. Viewport limit is 500 active bodies; past it, the oldest are removed first
originSpawn center { x, y, z } in meters, default { x: 0, y: 3, z: 0 } (above avatar head)
spreadBounding spawn cube size in meters, 0 – 20, default 1
velocityInitial linear velocity { x, y, z } shared by every body, in m/s, each axis within ±50, default zero
scaleUniform scale multiplier, 0.01 – 100, default 1
ttlMsLifespan in milliseconds, 0 – 600000, default 30000. 0 persists until cleared
transition{ style, inMs, outMs }: custom Show and Hide animation, with entrance and exit times of 0 – 5000 ms each; omitted fields inherit from scene

Out-of-bounds parameters clamp to valid ranges. Spawned items despawn upon ttlMs expiration, scene change, or disabling physics. Calling stage.clearSpawned {} immediately purges all active bodies, returns { cleared }, and cancels spawns whose assets are still loading.

Scene Transitions

scene.activate blocks until the target scene completes loading and assumes stage rendering, without waiting for the transition animation to uncover the viewport. While the target loads, the outgoing scene keeps running and stays editable. Subsequent switch requests preempt pending operations, and the response reports the final active scene ID. The SDK defaults to a 120-second timeout, overridden by requestTimeoutMs.

Builds supporting scene-transitions report each scene's Scene Transition, the effect played when entering it, under Scene.transition. scene.patch { transition } replaces the configuration without initiating playback:

FieldDescription
typecut, fade, wipe, circle, image, or video
durationMsDuration in milliseconds, 100 – 10000
colorHexadecimal color for fade, wipe, or circle wipes
assetIdRegistered image or video asset ID, or null
switchPointVideo timestamp ratio (0.05 – 0.95) where scene cut occurs, chosen where video covers stage
autoFadeAutomatically fade video in and out, default off
fadeInMs / fadeOutMsFade durations in milliseconds, 0 – 10000, default 300, capped at the time before and after the switch point respectively; 0 disables that fade

All scene activations execute the transition defined on the target scene. Every transition except cut begins immediately and holds an opaque cover until the new scene is ready, extending beyond durationMs if assets load slowly. Video transitions play muted, rate-scaled to duration, pausing on switchPoint until the scene is prepared.

Scene Presets

Builds supporting scene-presets list the scene's saved scene presets under Scene.presets in display order, each as { id, name, transition, items }. Older hosts omit presets.

  • items is keyed by instance ID, and each value is { visible, place }.
  • place is { space: '2d', x, y, scale, rotation } for Live2D models and 2D objects, and { space: '3d', x, y, z, rotX, rotY, rotZ, scale } for everything else. Opacity is not part of a preset.
  • Layers added after a preset was saved have no entry and are left as they are when it is applied.

preset.* methods all act on the active scene:

MethodRequestDescription
preset.create{ name?, transition? }Saves the current layout as a new preset appended to the list; the name defaults to the next in sequence in the app's UI language (e.g. "Preset 1"), the transition to glide
preset.update{ presetId }Re-saves the preset from the current layout
preset.rename{ presetId, name }Renames the preset
preset.setTransition{ presetId, transition }Changes how the stage moves into the preset
preset.delete{ presetId }Deletes the preset, leaving layers where they are
preset.apply{ presetId }Moves the stage to the preset's layout; returns as soon as the transition starts, without waiting for layers to settle

create, update, rename, and setTransition return { preset }; delete and apply return {}.

transition sets how the stage moves into the preset, and visibility changes follow the same choice:

ValueDescription
glideMoved layers ease into place
itemEvery changed layer exits with the scene's itemTransition, then re-enters at its new place
cutEverything snaps into place
  • Saving mid-transition records where layers are about to settle. Calling apply again for the preset the stage is already heading to changes nothing.
  • A preset ID not found in the active scene returns not-found. Preset IDs are unique only within a scene, and duplicating a scene keeps them.
  • On the desktop, preset edits and applies count as scene undo steps, and every change is pushed through scene.changed.
  • Keyboards, controllers, and Stream Deck trigger presets through automations containing an Apply Preset action (or the Stream Deck Apply Preset key).

Background Opacity

Builds with the background-alpha capability let SceneBackground.color carry an alpha byte (#rrggbbaa), so a transparent stage is a color background at alpha 00 rather than a mode of its own (see Stage Background). Hosts still accept the old transparent mode (LEGACY_TRANSPARENT_BACKGROUND) and store it as that color at zero alpha.

Older hosts without the capability keep that mode and heal an alpha byte to their default color. The SDK client adapts to such a host:

  • It reads the host's transparent as an alpha-0 color.
  • It sends a scene.patch color background at alpha 00 as transparent.
  • It drops the alpha byte from any other background color.

Show and Hide

SceneEnvironment.itemTransition { style, durationMs } configures Show and Hide: how models, props, and spawned items animate when they enter or exit the stage. Older builds omit it. style accepts:

ValueDescription
glitchDithered dissolve with tears and burns (default)
fadeSmooth whole-layer alpha dissolve, replacing the legacy dither
popScale bounce
cutInstant transition

durationMs ranges 0 – 5000 ms, default 300 ms. With cut, or a durationMs of 0, layers appear and disappear instantly. Transitions are updated via scene.patch.

Layer Effects

Builds with layer-effects allow attaching dedicated Visual Effects to individual models or props before scene composition. Each scene item maintains independent effects and effectLayers definitions matching scene effect schemas, modified via instance.setEffects { instanceId, effects, effectLayers? }:

  • effects accepts partial configurations for color, levels, colorWheels, colorShift, selectColors, gradient, blur, bloom, diffusion, sketch, rim, outline, and dropShadow; omitted parameters keep their values
  • effectLayers completely replaces the ordered stack of added effect layers; enabled effects are always in the list

The response returns the sanitized configuration: values clamp within valid ranges, and unknown keys are discarded. Malformed payloads reject the entire call atomically.

Bloom (bloom) lighting follows Warudo's Camera Bloom (Beautify specification):

FieldRangeDefaultDescription
intensity0 – 100.1A plain gain; 0 adds no glow
threshold0 – 50.75A gamma-space per-channel cut, squared into linear light and capped at 1000
radius0 – 63Scaled with a 1080p frame height

Rim Light (rim) adds light following VTube Studio's backlight behavior: white rims clip to white and fade naturally into the subject's local color. It has no blendMode (legacy values are dropped). The default color is #ffeb74 and the default size is 0.5.

Outline (outline) uses normalized size, color, opacity, quality (low, medium, high), and roundness, a normalized closing radius that fills narrow gaps and rounds corners (0 follows the silhouette).

Sketch is a pencil drawing on paper after Jaume Sanchez's NPR sketch shader. It is available as environment.effects.sketch and as a layer effect:

FieldRangeDescription
paperColor—Paper color
lines0 – 2Line strength
lineWidth0.5 – 4Line width, in CSS pixels
shading0 – 2Shading
softness0 – 1Softness
saturation0 – 1Color saturation
opacity0 – 1Opacity
  • In scene scope, paperBackground: true fills transparent pixels with paper. Layer sketches always preserve source alpha, regardless of this field.
  • Older hosts omit sketch from their returned effects.

Fisheye is scene-only, at environment.effects.fisheye: the frame is redrawn as a fisheye lens sees it, bulging from the center. strength (0 – 1, default 0.5) sets the bend; 1 is a 150° fisheye across the frame diagonal. The corners hold still, so the frame stays filled and its edges crop. Older hosts omit fisheye from their returned effects.

Layer effects apply to avatar models and to props in 2D or 3D space. 2D images, videos, webpages, and text require the host's layer-effects-2d capability; their effects composite at the object's position in the layer stack (pinned or not) and fade with the object's opacity. Older hosts return unsupported-for-format for 2D props but preserve saved configurations. Saved configurations are likewise preserved when an object switches space.

Tracking Sources

Persona can run multiple tracking sources concurrently. Sources are listed in settings.tracking.sources and managed via tracking.addSource, tracking.updateSource, and tracking.removeSource.

instance.setTrackingSources maps an instance's face, pose, and hand channels (faceSourceId, poseSourceId, handSourceId) to configured sources. Setting a channel to null, or to an ID that doesn't exist, disables tracking for that channel. New instances bind to global default sources; a single source can drive multiple avatars simultaneously.

instance.setTrackingSources also configures handTrackingMode: arms drives VRM arms, wrists, and fingers from hand tracking; fingers articulates fingers only, preserving arm posing for VMC or mocopi body trackers.

Face and pose sources come in these kinds:

KindChannelDefault port
persona-iosFace19700
ifacialmocapFace49983
vts-iosFaceFixed by its protocol
vmcPose39539
mocopiPose12351
  • Face sources take an optional phoneIp filter.
  • A source added without a port takes the first free port from its kind's default.
  • Requests fail if ports collide, though multiple ifacialmocap sources, or multiple persona-ios sources, can share a single socket.
  • The deprecated vts-ios-native is rejected at the protocol layer.

mediapipe represents the Webcam Tracking engine. It runs as a singleton without port or IP settings, and takes these options:

FieldDescription
face, hands, bodyToggle each tracking task; each feeds its own channel
deviceIdThe camera to use; an empty string selects the default camera
mirrorMirrors the image
delegateCompute backend, CPU or GPU

By default, the image is mirrored, tracking runs on the CPU, face and hand tracking are on, and body tracking is off. tracking.addSource and tracking.updateSource accept partial options and leave the rest unchanged. Hand tracking also drives the hand inputs detailed in Parameter Injection.

tracking.status reports aggregated status for network face and pose trackers alongside each configured source's state, webcam included, under the sources map; the tracking.status event dispatches identical payloads whenever source states change.

Legacy single-source methods continue to operate on the first network source for the corresponding channel:

  • tracking.setSource changes its kind.
  • pose.setSource switches its protocol, creating the source if there is none, and returns its port.
  • pose.setPort updates its listening port.
  • tracking.setEnabled and pose.setEnabled remain global master toggles for network face and pose sources.

Webcam tracking follows only its own enabled setting, changed through tracking.updateSource. While it is off, its task toggles and avatar channel assignments are kept. Webcam inference requires the desktop Webcam Tracking Add-On; the API does not install add-ons, and until the add-on is installed on the desktop host, the mediapipe source's status remains waiting.

Microphone Lip-Sync

Lip-Sync drives avatar mouth shapes from the host microphone input. Device enumeration and audio capture run on the desktop host. Raw audio and calibration coefficients stay on the desktop: neither crosses the API or appears in public settings. Audio spectral analysis also drives audio inputs detailed in Parameter Injection.

MethodDescription
lipSync.stateInspect configuration, capture status (off, starting, listening, error), input devices, live volume and vowel samples, and active calibration progress
lipSync.configureUpdate partial configuration (enabled, deviceId, gain 0 – 30 dB, noiseGate −60 – 0 dBFS, smoothing 0 – 0.3 s) and return the complete saved configuration
lipSync.restartReinitialize capture on the next frame. It doesn't turn on a disabled microphone or wait for capture to start; poll lipSync.state to verify readiness
lipSync.calibrateAdvance calibration sequence and return updated progress

Configuration is mirrored under settings.lipSync and mutable via settings.patch.

Calibration runs on the selected desktop microphone and stays in sync with the desktop Control Panel, in these steps:

StepDescription
startCreates a draft
recordSamples a specific phoneme for two seconds: A, I, U, E, O, or background noise S
previewApplies the candidate profiles once all six sounds are recorded
save / cancelCommits or discards the changes
resetRestores the default profile for the current microphone

Recording returns immediately; poll lipSync.state to monitor progress. Its calibration reports the current phase (ready, recording, or verifying), the phoneme being recorded, and the phonemes already recorded. A step sent in the wrong phase returns invalid-state.

instance.setLipSync { instanceId?, mode } toggles microphone driving per model: always (default), when-untracked (active only when face tracking is lost), or off. Calling on non-avatar objects returns unsupported-for-format.

Speech Playback

speech.play plays an audio stream and animates avatar mouth shapes based on audio energy. It applies strictly to instances declaring the speech capability (currently loaded Live2D Models). Availability is governed dynamically by instance capabilities rather than file formats: 0.53 and earlier, for example, don't declare it under the legacy WebGL engine.

ParameterDescription
urlAudio resource URI: supports https:, http: (local TTS bridge), or inline data:audio/* payloads
volumeOptional volume scalar, 0..1
instanceIdTarget instance ID, defaulting to the primary model

A model can play only one speech stream at a time; new invocations immediately preempt existing playback. Successful return confirms that playback has begun; failures return invalid-state. Lifecycle is bounded by speech.started and speech.ended events, and playback can be terminated early via speech.stop.

Avatar Hotkeys

Model-embedded Hotkeys are inspected and triggered via three methods, each accepting an optional modelId defaulting to the primary model:

MethodDescription
hotkey.listList all configured hotkeys on the model
hotkey.setOverwrite hotkey configurations; the server heals them leniently
hotkey.triggerTrigger a hotkey action by identifier

Each stage model maintains its own active hotkey bindings regardless of whether it is primary, Live2D, or VRM. Pressing a physical key combination triggers matching bindings across all models simultaneously. Invoking hotkey.trigger on an unplaced model is silently ignored.

Hotkey objects support four optional properties derived from VTube Studio specifications:

FieldDescription
holdThe expression stays active only while the key is held down
fadeSecondsFade-in and fade-out time of the expression or motion, 0 – 2 s. null uses the file's own setting. An enabled expression group member's fade wins over it
deactivateAfterSecondsHow long until the expression dismisses itself, or the longest a Live2D motion plays, 0.1 – 3600 s. null disables the timeout
stopsOnLastFrameA Live2D motion pauses on its final frame until triggered again

Older hosts don't return these fields. Properties left out of hotkey.set keep their existing values, so older clients never wipe these settings.

Because hotkey.trigger is an instantaneous remote event without a release signal, hotkeys marked with hold operate as state toggles. Expressions toggled via hold or deactivateAfterSeconds are never persisted to disk.

Hotkey.accelerator is up to three +-joined tokens fired as a set: modifiers (MODIFIER_TOKENS: Control, Alt, Shift, Super), keys in Electron's names, the mouse buttons LeftMouseButton, RightMouseButton, and MiddleMouseButton, or one controller's Controller* buttons. Order doesn't affect matching, so compare combos with the SDK's comboKey, not as strings, and format them for display with acceleratorLabel(accelerator, app.platform) using the desktop's app.platform from hello.

HotkeyState.registered lists the hotkeys that currently fire while Persona is in the background: any global combo, including a single bare key, since the desktop reads key state directly rather than registering shortcuts with the OS. Older hosts list only the Control, Alt, or Super combos they registered with the OS.

Automations

Builds with automations support inspecting and executing Automations, which are multi-action sequences triggered by hotkeys, system events, or API calls; builds with automation-editing and automation-transport can also read and edit the full timeline definition and control timeline playback.

MethodDescription
automation.listRetrieve summary entries for all configured automations
automation.run { automationId }Execute an enabled automation workflow by identifier
automation.get { automationId }Return the full definition { automation } (AppAutomation, requires automation-editing)
automation.create { automation }Create an automation and return { automation } (requires automation-editing)
automation.update { automationId, automation }Replace an automation's timeline and triggers and return { automation } (requires automation-editing)
automation.delete { automationId }Delete an automation (requires automation-editing)
automation.control { automationId, command, positionMs? }Drive timeline transport (play, pause, stop, seek, prepare) and return { run } (requires automation-transport)
automation.status { automationId }Inspect the current run and step status { run } (requires automation-transport)

automation.list returns lightweight summary metadata: id, title, accelerator, registered (whether it currently fires while Persona is in the background; on older hosts, whether it is registered with the OS), enabled, and the actionKinds and triggerKinds arrays, providing enough metadata to build control surfaces without fetching the full timeline.

When title is null, SDK utilities such as automationLabel build a descriptive label from actionKinds. actionSceneIds lines up with actionKinds to give the target scene ID of each scene-switch action (null for other kinds), so clients can show the target scene's name.

Each automation runs as a timeline, and the order of actionKinds does not imply execution order. Action kinds use category-first names:

CategoryAction Kinds
Setup before the timeline runsscene-switch, model-load (matching AUTOMATION_SETUP_KINDS and isAutomationSetup)
Visual effectseffect-clip, effect-toggle, effect-params
Layer visibility and presetslayer-visibility, preset-apply (moves the active scene to one of its presets, skipped if missing)
Karaoke lyrics and textlyrics-play (renders an .lrc asset on a text layer over its duration), text-set
Camera controlscamera-pose, camera-reset, camera-motion-play, camera-motion-stop, camera-follow, camera-aim, camera-focus, camera-handheld
Audio playbackaudio-play, audio-control
Avatar and Stream Modeexpression-toggle, motion-play, expression-clear, model-load, model-position, stream-mode
  • The delay action has been removed; delays are represented by blank timeline intervals.
  • There is no separate audio API: audio files, playback and output devices remain local to the desktop, and clients run or play automations containing audio-play to trigger sound effects.
  • Deprecated action names sent by older hosts (defined in LEGACY_AUTOMATION_ACTION_KINDS, across actionKinds and Hotkey.action) are normalized by the SDK client on receipt, or through canonicalActionKind.
  • triggerKinds lists the event triggers bound besides the shortcut, currently scene, model-loaded, motion, face-tracking, microphone, and parameter.

With automation-editing, automation.get returns the full AppAutomation (durationMs, autoDuration, actions with id/startMs/durationMs/sourceInMs, triggers, cooldownMs, accelerator, global, and enabled):

  • automation.create and automation.update accept an AutomationDefinition (validated by AutomationDefinitionSchema, returning invalid-params when fields or timing are invalid).
  • Newly created automations start disabled (enabled: false), with no shortcut (accelerator: null, global: false), and fresh server-assigned IDs so nothing fires until the user enables it.
  • automation.update replaces the actions, triggers, duration, and cooldown and regenerates step IDs while preserving the saved shortcut (accelerator), global flag, and enabled switch.
  • Creating, updating, or deleting an automation via the API cannot be undone and clears the desktop timeline editor's undo history for that automation.

With automation-transport, automation.control and automation.status drive and inspect desktop timeline playback (AutomationRunStatus: { automationId, phase, positionMs, durationMs, error, steps }, or run: null before any run, preview, or prepare in the current session):

  • play plays the timeline to the end (even when enabled is false, continuing from the current playhead when paused or scrubbed), pause pauses playback, prepare preloads media and animation assets, seek moves the playhead to positionMs and previews Effect Clip, Camera Pose, Layer Visibility, Set Text, and Play Lyrics on stage, and stop stops playback and releases previewed state.
  • The session that last controlled an automation's transport owns it. If that client disconnects, a paused, previewing, or failed run is stopped so shortcuts and triggers work again; a playing or preparing run is stopped if it later pauses or fails.

Automations without a keyboard, mouse, or controller shortcut report accelerator: null and can be triggered freely via automation.run. Automations with enabled: false ignore all triggers, including automation.run (though automation.control with play still plays them). automation.run returns immediately upon dispatch, without waiting for the timeline to finish. Changes to automation lists, definitions, enabled states, or background-firing states broadcast an updated summary list via automation.state.

Settings

settings.get and settings.patch access and update a curated allowlist of global application settings (window pin, tray minimization, performance parameters, gamepad switches), rather than exposing the raw store.

Responses and settings.changed events include read-only window geometry:

  • window.stageSize: { width, height }: Current viewport dimensions in device-independent pixels, refreshed on every stage-window resize
  • window.stageReferenceSize: { width, height }: Initial stage size used to calibrate placement units and model fit. Its height defines the scale: one unpinned ScreenPlacement x/y unit renders as stageSize.height / stageReferenceSize.height device-independent pixels from the viewport center

The renderer fills the window across all aspect ratios. For 2D content and perspective 3D views, width resizing reveals or crops horizontally without refitting content or letterboxing; orthographic 3D views retain a width of abs(orbit.distance). Saved placements persist across resizes and relaunches. Clients rendering placement controls should follow the current stageSize aspect ratio. Older hosts may omit these dimensions, and physical desktop window coordinates remain private.

Performance settings expose showFps, fpsLimit, effectsQuality, and renderScale (optional on older hosts). Gamepad profiles are read-only under settings; use dedicated controller.* methods to update profiles. Responses and settings.changed events also expose read-only ui.streamMode so remote panels can mask sensitive webpage URLs (omitted on older host builds).

Plugin Storage

storage.* provides persistent key-value storage scoped per API Key. All sessions sharing an API key share the same storage namespace, and revoking the key purges associated data. Values support arbitrary JSON-serializable payloads.

MethodDescription
storage.getRetrieve the value for a key; returns value: null for unwritten keys
storage.setWrite a key-value pair
storage.deleteDelete a key; succeeding even if the key does not exist
storage.listList all stored keys in lexicographical order

Storage quotas enforce a maximum key length of 128 characters, a maximum serialized value size of 64 KB, and a ceiling of 256 keys per API key. Exceeding key count limits returns an invalid-state error.

Error Codes

When the server returns an error frame, persona.call rejects with a PersonaApiError whose code is in API_ERROR_CODES:

CodeDescription
parse-errorPayload is not valid JSON
invalid-requestMessage envelope structure is malformed
unknown-methodThe method is not implemented or unavailable in the host build
invalid-paramsParameters failed schema validation
not-foundReferenced target identifier could not be resolved
unsupported-for-formatThe target instance format does not support the requested operation (e.g. MToon methods on a Live2D model)
conflictParameter lease is currently held by another active session
renderer-unavailableStage cannot respond (no window, reloading, crashed, or timed out)
forbidden-pathTarget path is outside authorized plugin directories, the user declined the asset.import prompt, or a non-loopback client called asset.import or registry.pluginFolders
invalid-stateOperation disallowed in current state (e.g. deleting the sole scene, quota exceeded, audio failed to start, version history off)
internalUncaught internal host error

Error codes constitute an immutable protocol specification: clients discard error envelopes with unrecognized codes, so new codes are introduced only alongside major protocol revisions. New failures reuse the existing codes.

When the client or transport itself fails connect() or call(), it rejects with a PersonaClientError whose reason is in CLIENT_ERROR_REASONS:

  • connect() sees connect-timeout, connection-closed, or client-closed (or the platform's own error, such as TypeError, when the url cannot be dialed at all, rejecting the promise rather than throwing synchronously).
  • call() sees not-connected, request-timeout, connection-closed, or client-closed.

Disconnection

The client SDK automatically reconnects with exponential backoff upon unexpected network interruptions. The following WebSocket close codes indicate terminal conditions and inhibit automatic reconnection:

Close CodeConstantReason
4001CLOSE_KEY_REVOKEDAPI key was revoked
4002CLOSE_FORCE_DISCONNECTEDSession was manually terminated from the Control Panel

Upon receiving these codes, the client transitions to a terminal closed state. If configured, the onClose callback is invoked, bypassing onWarning; without onClose, the notice goes to onWarning. Standard code 1001 (Going Away) indicates normal host shutdown or restart, and the client continues reconnection attempts.

Revoking a key discards pending outbound frames immediately and closes all active connections using that key with code 4001. Pending RPC requests awaiting render pipeline confirmation are cancelled immediately. An action already forwarded to the stage may still complete, but its response is discarded.

Protocol Versioning

The wire protocol includes a major protocol version integer; if a mismatch occurs between client and server, the SDK logs a diagnostic warning. Subscribing to an event unsupported by an older Persona version does not cause a fatal error; the SDK automatically falls back to subscribing to events individually, keeping the supported ones and logging unsupported names.

The current protocol version is 4. Previous major versions streamlined and modernized protocol interfaces; plugins written for an older protocol need these migrations:

Protocol VersionChanges
2Removed Scene.behavior (cursor gaze tracking); excised from scene schema and scene.patch
3Removed stage.shockwave; invocations return unknown-method
4Renamed shortcut.list, shortcut.trigger, and shortcut.state to automation.list, automation.run, and automation.state; renamed shortcuts capability to automations, using automationId and automations in wire payloads

Last updated on November 4, 2026

Tech otakus destroy the world