00:00 / 00:00

Persona

SDK

@laplace.live/persona-sdkはプラグインAPIの型付きクライアントであり、そのワイヤースキーマの正となる定義でもあります。このページは型のリファレンスです。サーバーの有効化、キー、メソッド一覧についてはプラグインAPIを参照してください

アイソモーフィックです——Node 22+、Bun、ブラウザ、OBSブラウザソースで動きます——実行時の依存はワイヤースキーマを支えるzodだけです

npm install @laplace.live/persona-sdk

何が作れるか

Persona Consoleは、このSDKで何ができるかを示す公式のリファレンス実装です。アプリ全体のリモコンで——シーン、レイヤーとそれを埋めるインベントリ、表情、モーション、ホットキー、トラッキング、設定、そしてリアルタイムのパラメータインジェクター——それらがブラウザのタブ1枚に収まり、配信中にスマートフォンから操作する前提でレイアウトされています

注目すべきは、デスクトップ側のコードをまったく取り込んでいないことです。プラグインAPIがその契約のすべてであり、つまりコンソールにできることは、同じSDKで書いたものなら何にでもできます

真似する価値のあるパターンがいくつかあります:

  • 形式ではなくケイパビリティで判定する。インスタンス単位の各セクションは、まず呼び出してunsupported-for-formatを処理するのではなく、InstanceRuntime.capabilitiesを読んで自ら無効化します
  • 再接続はキャッシュの破棄として扱う。ソケットが切れている間に見逃したイベントは再送されないので、遅延取得したデータは「少し古い」のではなく無効です——再接続時に捨てて、必要なものを読み直してください
  • 終端的なクローズコードを正しく扱う40014002は再接続するなという意味です。それ以外は再試行する価値があります
  • 購読はSDKに任せるpersona.on(…)は再接続後に購読を張り直すので、呼び出し側で管理する必要はありません

LLMプロンプト

下のプロンプトをAIアシスタントに貼り付けるか、プロジェクトのCLAUDE.mdAGENTS.mdのようなルールファイルに保存してください。プラグインを書いてもらうのに必要な背景はこれでひととおり揃います。どのモデルにも読みやすいよう英語で書かれており、引用しているドキュメントURLはそのまま取得できます

You are helping me build a plugin for LAPLACE Persona, a desktop VTuber app
(https://laplace.live/persona). Persona exposes a local WebSocket server called the
Plugin API, and `@laplace.live/persona-sdk` is its typed client.

Before writing any code, fetch the reference documents listed at the end — they are
the single source of truth for method names, events, error codes and types. Do not
rely on memory and do not invent names.

## Setup

- Install `@laplace.live/persona-sdk`. Isomorphic: Node 22+, Bun, browsers and OBS
  browser sources. Its only runtime dependency is zod.
- The user enables the server in Persona under Advanced → Plugin API and creates an
  API key there. Tokens look like `sk-lp-v1-…`. Default endpoint: `ws://127.0.0.1:25034`.
- Everything — values and types — is exported from the package root. The one
  other entry point is `@laplace.live/persona-sdk/effects`, which types the
  [custom effect](/persona/custom-effects) contract; it is separate because it
  touches three, declared as an optional peer.

## Client

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

const persona = new PersonaClient({
  token: "sk-lp-v1-…", // required; everything else is optional
  clientInfo: { name: "My Plugin", version: "1.0.0" }, // display-only identity
});
await persona.connect();

const { scenes, activeSceneId } = await persona.call("scene.list");
await persona.call("scene.patch", { behavior: { lookAtCursor: true } });
persona.on("motion.started", (m) => console.log("playing", m.group));

const mouth = persona.driveParameter("MouthOpen", 0.8); // parameter lease
mouth.set(0.3);
mouth.release();
```

`call(method, params?)` is fully typed — the method name determines the request and
response shapes. Methods cover scenes, model instances, stage objects, expressions,
motions, speech, hotkeys, shortcuts, model and asset registration, settings,
tracking and pose input, per-key storage and parameter injection; events notify you
of scene, selection, expression, motion, speech, tracking and registry changes. The
exact method and event names, parameter shapes, error codes and limits live in the
reference below — look them up instead of guessing. Gate features on the
capabilities reported by `app.info` and `instance.list`, never on version strings.

## Reference

Fetch these before writing code. The documentation is served as raw Markdown —
append `.mdx` to any page URL (`.en.mdx` for English):

- https://laplace.live/persona/plugin-api.en.mdx — server setup, API keys, and every
  method, event, error code and limit
- https://laplace.live/persona/sdk.en.mdx — the SDK client: options, auth modes and
  every exported type
- https://laplace.live/llms.txt — the whole documentation set in one file

クライアント

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

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

const { scenes, activeSceneId } = await persona.call("scene.list");
await persona.call("scene.patch", { behavior: { lookAtCursor: true } });
persona.on("motion.started", (m) => console.log("playing", m.group));

PersonaClientは次のオプションで構築します。必須なのはtokenだけです

Prop

Type

ClientInfo

clientInfoは、自分のアプリがPersonaの設定の接続中のクライアントにどの名前で並ぶかを決めます。省略可能で、自己申告であり、表示専用です——認可には決して使われません——SDKが再接続のたびに申告し直します

const persona = new PersonaClient({
  token,
  clientInfo: { name: "My Overlay", version: "1.2.0", developer: "You" },
});

Prop

Type

認証

既定ではトークンは?token=クエリパラメータで渡されます。ブラウザではこれしかできません。Nodeではリクエストヘッダーを設定するソケットを渡すことで、トークンをAuthorization: Bearerヘッダーとして送れます——別途インストールしたwsパッケージを使います:

import WebSocket from "ws";

const persona = new PersonaClient({
  token,
  auth: "header",
  createWebSocket: (url, headers) => new WebSocket(url, { headers }),
});

WebSocketLikecreateWebSocketが返すべきソケットの形です。グローバルのWebSocketwsのクライアントも構造的にこれを満たすので、型アサーションは不要です

Prop

Type

パラメータリース

driveParameterはモデルのパラメータに値を押し当て続け、InjectionHandleを返します。ハートビートはSDKが担当し、リースは最後の書き込みからおよそ1秒で期限切れになります——つまりプロセスが落ちればパラメータは固まったままにならず、自動的に元へ戻ります

const mouth = persona.driveParameter("MouthOpen", 0.8);
mouth.set(0.3);
mouth.release();

1つのパラメータを同時に保持できるセッションは1つだけです。2つ目はconflictエラーを受け取ります

3つ目の引数にはweightに加えてonErrorも渡せます。バックグラウンドのハートビートが失敗したときに通知されるので、呼び出し側が更新のたびにawaitする必要はありません

Prop

Type

クローズ

onCloseは、サーバーまたはネットワーク由来のクローズをすべて報告します。元のソケットのクローズコードと理由に加えて、クライアントが再接続しないものを示すterminalが付きます。クライアント自身の処理(状態、保留中の呼び出し)が終わったあとで発火するので、その中でclose()を呼んでも安全です。自分から呼んだclose()は報告されません。これを指定した場合、終端的なクローズはonWarningを通らなくなります

Prop

Type

PersonaClientState

type PersonaClientState = "closed" | "connecting" | "open" | "reconnecting";

定数

エクスポート意味
PROTOCOL_VERSION1ワイヤー形式に破壊的変更があると増える。サーバーはhelloで自身のバージョンを報告する
DEFAULT_API_HOST127.0.0.1ユーザーがローカルネットワークからのアクセスを有効にしない限り、サーバーはここにのみバインドする
DEFAULT_API_PORT25034変更しない限り、アプリはこのポートで待ち受ける
CLOSE_KEY_REVOKED4001終端的なクローズコード——キーが失効した
CLOSE_FORCE_DISCONNECTED4002終端的なクローズコード——ユーザーがこのセッションを切断した
CLOSE_SERVER_STOPPING1001サーバーが停止中。終端的ではない——復帰したら再接続する
INJECT_LEASE_TTL_MS1000注入したパラメータが最後の書き込みからどれだけで元に戻るか
INJECT_HEARTBEAT_MS100SDKが保持中のリースを再送する間隔
STORAGE_KEY_MAX_LENGTH128ストレージのキー名の最大長
STORAGE_VALUE_MAX_LENGTH65536シリアライズ後の値1つあたりの長さの上限
STORAGE_KEYS_MAX256APIキー1つが保持できるキーの数
SPEECH_URL_MAX_LENGTH8000000speech.playurlの上限。base64のWAVで約40秒分に相当

API_ERROR_CODESEVENT_NAMESINPUT_NAMESAPP_CAPABILITIESFPS_LIMIT_PRESETSEFFECTS_QUALITY_LEVELSas constの配列として提供されるので、ピッカーがアプリの受け付ける値を正確に列挙できます。isApiErrorCodeisEventNameisAppCapabilityはそれらの型ガードで、injectTargetKeyは注入対象の正規化されたキーを構築します

personaWsUrlはホスト、ポート、secureフラグ——またはユーザーが入力した1行のアドレス——からソケットのURLを組み立てます。hosthost:port、角括弧の有無を問わないIPv6、そしてそのまま通す完全なws(s):// URLに対応します。トークンはここには含まれず、PersonaClientが自分で付け足します。parsePortisValidPortは対になる入力検証です

モデルとアセット

モデルとアセットは同じ形のエントリを共有します。kindは常にそのエントリが何かを、originは常にどこから来たかを表します——bundledはアプリに同梱されたもの、userはユーザーが登録したものです。その隣に、アプリのコンテンツマニフェストが宣言した作者クレジットが並びます

Prop

Type

type ModelFormat = "live2d" | "vrm";
type ContentOrigin = "bundled" | "user";
type AssetKind = "image" | "video" | "prop" | "ibl" | "lut" | "animation";
type InventoryKind = ModelFormat | "pngtuber" | AssetKind | "effect";

effectはファイルを指さない唯一の種類です。インベントリはこれを使って、内蔵エフェクトとインストール済みのカスタムエフェクトを並べます

model.listModelRef——kindModelFormatに絞ったContentRef——を返します。asset.listAssetRefを返し、こちらはkindAssetKindに絞ったうえでexistsが加わります。ファイルがディスクから消えるとfalseになります

Prop

Type

Prop

Type

CatalogItem

ピッカーの裏側にある、転送方法に依存しないコンテンツのメタデータです。ローカルのレジストリエントリもリモートのカタログ行も、これにマッピングされます。中身はContentRefからoriginを除いたもの——リモートの行はインストール前にこのフィールドに答えられません——に、カタログだけが知っている情報を足したものです。downloadはまだディスク上にないエントリを取得する手段で、これがなければそのエントリはすでにローカルにあります

Prop

Type

配置

スクリーン空間はピクセル単位で、原点はステージの中心です。ワールド空間はメートル単位です。パネルには角度で表示されますが、ワイヤー形式での回転はすべてラジアンです

Prop

Type

Prop

Type

Place2DPlace3Dはオブジェクト版です——フィールドは同じで、opacityが加わります

シーンアイテム

シーンのitems配列は、モデルとオブジェクトをz順で一緒に保持します

type SceneItem = SceneModelItem | SceneObjectItem;

Prop

Type

Prop

Type

Prop

Type

ObjectContent

オブジェクトが描画する内容で、kindで区別されます。captureを除き、現時点でいずれもUIから作成できます——レイヤーを参照してください

type ObjectContent =
  | { kind: "image"; assetId: string }
  | {
      kind: "video";
      assetId: string;
      loop: boolean;
      muted: boolean;
      volume: number;
    }
  | { kind: "prop"; assetId: string }
  | {
      kind: "web";
      url: string;
      width: number;
      height: number;
      fps: number;
      transparent: boolean;
      css: string;
      layer: WebLayer;
    }
  | { kind: "capture"; source: CaptureKind; sourceId: string; label: string };

type ObjectSpace = "2d" | "3d";
type WebLayer = "behind" | "front";
type CaptureKind = "display" | "window";

Prop

Type

ピン留め

Prop

Type

AttachAnchor

type AttachAnchor =
  | { kind: "root" }
  | { kind: "bone"; bone: string }
  | {
      kind: "artMesh";
      id: string;
      verts: [number, number, number];
      weights: [number, number, number];
    };

object.anchorsは親が提供するアンカーをAnchorOptionとして返します——アンカーと、その表示名の組です。VRMはモデルルートとヒューマノイドのボーンを、Live2Dはモデルルートとすべてのアートメッシュを返します

Prop

Type

Prop

Type

シーン

Prop

Type

Prop

Type

Prop

Type

背景と動作

Prop

Type

type BackgroundMode = "transparent" | "color" | "image";

Prop

Type

カメラ

Prop

Type

Prop

Type

ライティング

Prop

Type

type SceneLightType = "directional" | "point" | "ambient";
type ShadowQuality = "off" | "low" | "medium" | "high" | "ultra";

SHADOW_QUALITY_LEVELSはこれらの段階をas constの配列として提供します。順序はパネルと同じです

iblAssetIdmodelAssetIdは1つのスロットの両面です。シーンは環境マップを纏うか、3Dセットの中に立つかのどちらかで、両方は取れません。書き込みにはenvironmentWithAsset()を使ってください。この関数がもう一方を消し、前のセットに紐づいていたbakeFromModelとライトの上書きも一緒に落とします。ほかの2つのヘルパーはパネルのボタンに対応します。applyEnvironmentLighting()は2つのライトルートを有効にしてシーン自身のライトを理想の強さまで下げ、applyEnvironmentLook()EnvironmentLookをシーンに畳み込みます

Prop

Type

Prop

Type

Prop

Type

エフェクト

SceneEffectsは意図的にフラットです。「スイッチと数値」を持つエフェクトはすべてトップレベルに置かれているので、ツールから一律に走査できます。あるエフェクトがパネルのどこに現れるかは表示上の選択であって、構造ではありません

Prop

Type

レイヤーリストにどのエフェクトが行を持つかは、environment.effectLayers——このレジストリの「スイッチと数値」を持つキー、つまりToggleEffectKeyのリスト——が別に持っています。enabledとは別物で、enabledはオンかどうか、effectLayersは行があるかどうかです。エフェクトをオンにパッチすると行も自動で追加されるので、enabledだけ書けば足ります

type SceneToneMapping = "none" | "neutral" | "aces" | "agx";

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

rainsnowは形こそ他のエフェクトと同じですが、振る舞いは異なります。ポストチェーンの一段ではなく、シーンの中のジオメトリです。を参照してください

カスタムエフェクト

ユーザーが書いたエフェクトはこのフラットなレジストリに収まりません。何がインストールされているかは実行時にしか分からないからです。effectsと並ぶenvironment.customEffectsにリストとして入り、パラメータは組み込みのスライダーと同じくscene.patchから動かせます

Prop

Type

このマシンに存在しないエフェクトを指す項目は破棄されず保持されるので、そのエフェクトがないマシンへシーンを持っていって、そのまま持ち帰ることができます

ランタイム状態

instance.listはステージ上の各アイテムに実際に何ができるかを報告するので、クライアントはエラーで気付くのではなく、コントロールをあらかじめ無効化できます

Prop

Type

type InstanceCapability =
  | "motions"
  | "expressions"
  | "placement-2d"
  | "placement-3d"
  | "mtoon"
  | "idle-clips"
  | "pose"
  | "live2d-params"
  | "speech";

speechは形式ではなく描画エンジンによって決まるので、ランタイム側のケイパビリティ一覧にしか現れず、formatからは判断できません。アプリ自身のケイパビリティは別の一覧で、helloapp.infoが報告します——機能の有効・無効はこれで判定し、versionの比較に頼らないでください

type AppCapability = "storage" | "speech" | "shortcuts";

Prop

Type

Prop

Type

Prop

Type

Prop

Type

ホットキー

Prop

Type

Prop

Type

HotkeyConfigregisteredを除いた同じ形——つまりhotkey.setが受け付けるものです

グローバルショートカット

アプリ自身のショートカットは別ものです。モデルではなくアプリに属し、1つのショートカットが現在のシーンに対するアクションの並びを持ちます。shortcut.listが一覧を返し、shortcut.triggerが1つを実行し、shortcut.stateは一覧が変わるたびに全体を送り直します。shortcutsケイパビリティが必要で、使い方はプラグインAPIにあります

Prop

Type

type ShortcutActionKind =
  "effect-toggle" | "effect-params" | "camera-pose" | "layer-visibility";

actionKindsの型がこのユニオンではなくstring[]なのは意図的です。サーバーはSDKより新しい種類を送ってくることがあります。shortcutActionLabelは1つの種類に英語のラベルを与え、未知のものは汎用の名前にまとめます。shortcutLabelはショートカット全体のラベルを返します——titleがあればそれを、なければ各種類のラベルを+でつないだものです

設定

Prop

Type

SettingsPatchは書き込み可能な部分集合で、windowuiperformanceがいずれも省略可能です。トラッキングとポーズはtracking.*pose.*から変更します

type TrackingSourceId = "vts-ios" | "ifacialmocap";
type PoseSourceId = "vmc";
type TrackingSourceKind = TrackingSourceId | PoseSourceId;
type TrackingStatus = "off" | "waiting" | "tracking" | "no-face";
type PoseStatus = "off" | "waiting" | "tracking";
type EffectsQuality = "low" | "medium" | "high";

トラッキングソース

tracking.sourcesは設定済みのソースの一覧で、顔も身体も含みます。複数を同時に走らせられ、モデルインスタンスはSceneModelItemfaceSourceIdposeSourceIdを通じてidでソースに紐づきます

Prop

Type

tracking.sourcepose.sourcepose.portは複数ソース以前のフィールドで、いまは各チャンネルの最初のソースを映しているだけです。古いクライアントのために残されています。新しいコードはsourcesを読んでください

ストレージ

storage.*の値は任意のJSONです。上限は定数を、使い方はプラグインストレージを参照してください

type JsonValue =
  string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };

注入

Prop

Type

type InjectTargetType = "input" | "live2d-param" | "vrm-expression";

InjectTargetvalueweightを除いた同じ形です。inputを対象とする場合、idINPUT_NAMESから取ります——VTube Studio自身の語彙に、52個の生のARKitチャンネルを加えたもので、モデルの.vtube.jsonが参照している名前でもあります

VTSの語彙の21個:

FaceAngleX FaceAngleY FaceAngleZ FacePositionX FacePositionY FacePositionZ EyeOpenLeft EyeOpenRight EyeLeftX EyeLeftY EyeRightX EyeRightY Brows BrowLeftY BrowRightY MouthSmile MouthOpen MouthX CheekPuff JawOpen TongueOut

さらにARKitを前置した52個のチャンネルが続きます。名前はARKitのブレンドシェイプ名の頭を大文字にしたもので、ARKitJawOpenARKitMouthSmileLeftARKitEyeBlinkRightなどです。前置するのは、VTSの語彙にすでにJawOpenCheekPuffTongueOutがあり、.vtube.jsonの入力名の照合が大文字小文字を区別しないためです

この3つの名前がまさにARKIT_TWINSです。それぞれ対応するARKitチャンネルと同じ浮動小数点値で、どちらの綴りも受け付け、どちらもARKit側のidとして読まれます。各入力のレンジはINPUT_RANGESにあります——頭の角度は、ARKitチャンネルは0..1、それ以外は無次元です

これらの入力がLive2Dモデルのパラメータへどう届くかはパラメータの割り当てを参照してください

ワイヤーエンベロープ

SDKを使うなら不要です——プロトコルに直接つなぐ人のためのものです

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

ApiErrorCodeErrorMessageが運ぶcodeの値の閉じた集合です——それぞれの意味はエラーを参照してください。PersonaApiErrorcallが投げるクラスで、まさにこのcodeを持ちます

最終更新日:2026年9月3日

Tech otakus destroy the world