@laplace.live/persona-sdkはプラグインAPIの型付きクライアントであり、そのワイヤースキーマの正となる定義でもあります。このページは型のリファレンスです。サーバーの有効化、キー、メソッド一覧についてはプラグインAPIを参照してください
アイソモーフィックです——Node 22+、Bun、ブラウザ、OBSブラウザソースで動きます——実行時の依存はワイヤースキーマを支えるzodだけです
npm install @laplace.live/persona-sdk以下の表はSDK自身の宣言をそのまま反映しています。すべてパッケージのエントリーポイントからエクスポートされているので、import type { … } from '@laplace.live/persona-sdk'の1行ですべて取得できます
何が作れるか
Persona Consoleは、このSDKで何ができるかを示す公式のリファレンス実装です。アプリ全体のリモコンで——シーン、レイヤーとそれを埋めるインベントリ、表情、モーション、ホットキー、トラッキング、設定、そしてリアルタイムのパラメータインジェクター——それらがブラウザのタブ1枚に収まり、配信中にスマートフォンから操作する前提でレイアウトされています
注目すべきは、デスクトップ側のコードをまったく取り込んでいないことです。プラグインAPIがその契約のすべてであり、つまりコンソールにできることは、同じSDKで書いたものなら何にでもできます
真似する価値のあるパターンがいくつかあります:
- 形式ではなくケイパビリティで判定する。インスタンス単位の各セクションは、まず呼び出して
unsupported-for-formatを処理するのではなく、InstanceRuntime.capabilitiesを読んで自ら無効化します - 再接続はキャッシュの破棄として扱う。ソケットが切れている間に見逃したイベントは再送されないので、遅延取得したデータは「少し古い」のではなく無効です——再接続時に捨てて、必要なものを読み直してください
- 終端的なクローズコードを正しく扱う。
4001と4002は再接続するなという意味です。それ以外は再試行する価値があります - 購読はSDKに任せる。
persona.on(…)は再接続後に購読を張り直すので、呼び出し側で管理する必要はありません
HTTPSで配信されるページは127.0.0.1にしかソケットを開けません。別のマシンのPersonaを操作するものは、平文のHTTPで配信するか、その前段に置いたTLSプロキシへ接続する必要があります——コンソールの説明を参照してください
LLMプロンプト
下のプロンプトをAIアシスタントに貼り付けるか、プロジェクトのCLAUDE.mdやAGENTS.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 }),
});WebSocketLikeはcreateWebSocketが返すべきソケットの形です。グローバルのWebSocketもwsのクライアントも構造的にこれを満たすので、型アサーションは不要です
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_VERSION | 1 | ワイヤー形式に破壊的変更があると増える。サーバーはhelloで自身のバージョンを報告する |
DEFAULT_API_HOST | 127.0.0.1 | ユーザーがローカルネットワークからのアクセスを有効にしない限り、サーバーはここにのみバインドする |
DEFAULT_API_PORT | 25034 | 変更しない限り、アプリはこのポートで待ち受ける |
CLOSE_KEY_REVOKED | 4001 | 終端的なクローズコード——キーが失効した |
CLOSE_FORCE_DISCONNECTED | 4002 | 終端的なクローズコード——ユーザーがこのセッションを切断した |
CLOSE_SERVER_STOPPING | 1001 | サーバーが停止中。終端的ではない——復帰したら再接続する |
INJECT_LEASE_TTL_MS | 1000 | 注入したパラメータが最後の書き込みからどれだけで元に戻るか |
INJECT_HEARTBEAT_MS | 100 | SDKが保持中のリースを再送する間隔 |
STORAGE_KEY_MAX_LENGTH | 128 | ストレージのキー名の最大長 |
STORAGE_VALUE_MAX_LENGTH | 65536 | シリアライズ後の値1つあたりの長さの上限 |
STORAGE_KEYS_MAX | 256 | APIキー1つが保持できるキーの数 |
SPEECH_URL_MAX_LENGTH | 8000000 | speech.playのurlの上限。base64のWAVで約40秒分に相当 |
API_ERROR_CODES、EVENT_NAMES、INPUT_NAMES、APP_CAPABILITIES、FPS_LIMIT_PRESETS、EFFECTS_QUALITY_LEVELSはas constの配列として提供されるので、ピッカーがアプリの受け付ける値を正確に列挙できます。isApiErrorCode、isEventName、isAppCapabilityはそれらの型ガードで、injectTargetKeyは注入対象の正規化されたキーを構築します
personaWsUrlはホスト、ポート、secureフラグ——またはユーザーが入力した1行のアドレス——からソケットのURLを組み立てます。host、host:port、角括弧の有無を問わないIPv6、そしてそのまま通す完全なws(s):// URLに対応します。トークンはここには含まれず、PersonaClientが自分で付け足します。parsePortとisValidPortは対になる入力検証です
モデルとアセット
モデルとアセットは同じ形のエントリを共有します。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.listはModelRef——kindをModelFormatに絞ったContentRef——を返します。asset.listはAssetRefを返し、こちらはkindをAssetKindに絞ったうえでexistsが加わります。ファイルがディスクから消えるとfalseになります
Prop
Type
Prop
Type
CatalogItem
ピッカーの裏側にある、転送方法に依存しないコンテンツのメタデータです。ローカルのレジストリエントリもリモートのカタログ行も、これにマッピングされます。中身はContentRefからoriginを除いたもの——リモートの行はインストール前にこのフィールドに答えられません——に、カタログだけが知っている情報を足したものです。downloadはまだディスク上にないエントリを取得する手段で、これがなければそのエントリはすでにローカルにあります
Prop
Type
配置
スクリーン空間はピクセル単位で、原点はステージの中心です。ワールド空間はメートル単位です。パネルには角度で表示されますが、ワイヤー形式での回転はすべてラジアンです
Prop
Type
Prop
Type
Place2DとPlace3Dはオブジェクト版です——フィールドは同じで、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の配列として提供します。順序はパネルと同じです
iblAssetIdとmodelAssetIdは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
rainとsnowは形こそ他のエフェクトと同じですが、振る舞いは異なります。ポストチェーンの一段ではなく、シーンの中のジオメトリです。雨と雪を参照してください
カスタムエフェクト
ユーザーが書いたエフェクトはこのフラットなレジストリに収まりません。何がインストールされているかは実行時にしか分からないからです。effectsと並ぶenvironment.customEffectsにリストとして入り、パラメータは組み込みのスライダーと同じくscene.patchから動かせます
カスタムエフェクトは一切信頼しないでください
カスタムエフェクトはPersonaの内部で動く任意のJavaScriptであって、サンドボックスに入れられたシェーダーではありません——いったん読み込まれたら、何をするかを縛るものは何もありません。これはパワーユーザー向けのテクニカルプレビューです。どのカスタムエフェクトも信頼できないものとして扱い、自分で書いたもの、または1行ずつ読んで理解したものだけを動かしてください。プラグインがユーザーをエフェクトのインストールへ誘導すべきではありません。信頼の境界を参照してください
Prop
Type
このマシンに存在しないエフェクトを指す項目は破棄されず保持されるので、そのエフェクトがないマシンへシーンを持っていって、そのまま持ち帰ることができます
ランタイム状態
instance.listはステージ上の各アイテムに実際に何ができるかを報告するので、クライアントはエラーで気付くのではなく、コントロールをあらかじめ無効化できます
Prop
Type
type InstanceCapability =
| "motions"
| "expressions"
| "placement-2d"
| "placement-3d"
| "mtoon"
| "idle-clips"
| "pose"
| "live2d-params"
| "speech";speechは形式ではなく描画エンジンによって決まるので、ランタイム側のケイパビリティ一覧にしか現れず、formatからは判断できません。アプリ自身のケイパビリティは別の一覧で、helloとapp.infoが報告します——機能の有効・無効はこれで判定し、versionの比較に頼らないでください
type AppCapability = "storage" | "speech" | "shortcuts";Prop
Type
Prop
Type
Prop
Type
Prop
Type
ホットキー
Prop
Type
Prop
Type
HotkeyConfigはregisteredを除いた同じ形——つまり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は書き込み可能な部分集合で、window、ui、performanceがいずれも省略可能です。トラッキングとポーズは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は設定済みのソースの一覧で、顔も身体も含みます。複数を同時に走らせられ、モデルインスタンスはSceneModelItemのfaceSourceIdとposeSourceIdを通じてidでソースに紐づきます
Prop
Type
tracking.source、pose.source、pose.portは複数ソース以前のフィールドで、いまは各チャンネルの最初のソースを映しているだけです。古いクライアントのために残されています。新しいコードはsourcesを読んでください
ストレージ
storage.*の値は任意のJSONです。上限は定数を、使い方はプラグインストレージを参照してください
type JsonValue =
string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };注入
Prop
Type
type InjectTargetType = "input" | "live2d-param" | "vrm-expression";InjectTargetはvalueとweightを除いた同じ形です。inputを対象とする場合、idはINPUT_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のブレンドシェイプ名の頭を大文字にしたもので、ARKitJawOpen・ARKitMouthSmileLeft・ARKitEyeBlinkRightなどです。前置するのは、VTSの語彙にすでにJawOpen・CheekPuff・TongueOutがあり、.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
ApiErrorCodeはErrorMessageが運ぶcodeの値の閉じた集合です——それぞれの意味はエラーを参照してください。PersonaApiErrorはcallが投げるクラスで、まさにこのcodeを持ちます
最終更新日:2026年9月3日