@laplace.live/persona-sdk 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/address.d.ts +18 -0
- package/dist/client/address.js +56 -0
- package/dist/{client.d.ts → client/client.d.ts} +37 -15
- package/dist/{client.js → client/client.js} +67 -20
- package/dist/index.d.ts +14 -8
- package/dist/index.js +16 -8
- package/dist/values/effect-schema.d.ts +30 -0
- package/dist/values/effect-schema.js +111 -0
- package/dist/values/guards.d.ts +8 -0
- package/dist/values/guards.js +17 -0
- package/dist/values/labels.d.ts +6 -0
- package/dist/values/labels.js +9 -0
- package/dist/values/limits.d.ts +73 -0
- package/dist/values/limits.js +112 -0
- package/dist/values/locale.d.ts +9 -0
- package/dist/values/locale.js +21 -0
- package/dist/{envelope.d.ts → wire/envelope.d.ts} +10 -5
- package/dist/{envelope.js → wire/envelope.js} +5 -4
- package/dist/{events.d.ts → wire/events.d.ts} +10 -0
- package/dist/{events.js → wire/events.js} +2 -0
- package/dist/{methods.d.ts → wire/methods.d.ts} +126 -1
- package/dist/{protocol.d.ts → wire/protocol.d.ts} +4 -0
- package/dist/{protocol.js → wire/protocol.js} +4 -0
- package/dist/{schemas.d.ts → wire/schemas.d.ts} +39 -0
- package/dist/{schemas.js → wire/schemas.js} +46 -4
- package/dist/{types.d.ts → wire/types.d.ts} +71 -3
- package/dist/{types.js → wire/types.js} +19 -0
- package/package.json +2 -2
- /package/dist/{errors.d.ts → wire/errors.d.ts} +0 -0
- /package/dist/{errors.js → wire/errors.js} +0 -0
- /package/dist/{methods.js → wire/methods.js} +0 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export function clamp(v, min, max) {
|
|
2
|
+
return Math.min(max, Math.max(min, v));
|
|
3
|
+
}
|
|
4
|
+
/** Clamp to the unit interval. */
|
|
5
|
+
export function clamp01(v) {
|
|
6
|
+
return v < 0 ? 0 : v > 1 ? 1 : v;
|
|
7
|
+
}
|
|
8
|
+
export const DEFAULT_LIVE2D_PLACEMENT = { x: 0, y: 0, scale: 1, rotation: 0 };
|
|
9
|
+
export const DEFAULT_VRM_PLACEMENT = { x: 0, y: 0, z: 0, rotX: 0, rotY: 0, rotZ: 0, scale: 1 };
|
|
10
|
+
/** Scene colors heal to 6-digit hex. */
|
|
11
|
+
export const SCENE_COLOR_RE = /^#[0-9a-f]{6}$/i;
|
|
12
|
+
/** Drop the alpha byte a picker's hex input can produce (`#rrggbbaa` → `#rrggbb`). */
|
|
13
|
+
export function opaqueHex(hex) {
|
|
14
|
+
return /^#[0-9a-f]{8}$/i.test(hex) ? hex.slice(0, 7) : hex;
|
|
15
|
+
}
|
|
16
|
+
export const SCENE_LIGHT_INTENSITY_MAX = 2;
|
|
17
|
+
/**
|
|
18
|
+
* Point lights decay physically (1/d²), so reach rides on power the way Blender's
|
|
19
|
+
* does: 20 stays visible to ~10 m where 2 self-extinguishes by ~5. The other
|
|
20
|
+
* types have no falloff to overcome — 20 would just blow the stage out.
|
|
21
|
+
*/
|
|
22
|
+
export const SCENE_LIGHT_POINT_INTENSITY_MAX = 20;
|
|
23
|
+
/** Intensity ceiling for a light of `type` — one source for the slider and scene healing. */
|
|
24
|
+
export function sceneLightIntensityMax(type) {
|
|
25
|
+
return type === 'point' ? SCENE_LIGHT_POINT_INTENSITY_MAX : SCENE_LIGHT_INTENSITY_MAX;
|
|
26
|
+
}
|
|
27
|
+
// Live2D bounds are the zoom clamp (zoom.ts re-exports these); VRM scales a world-space root.
|
|
28
|
+
export const LIVE2D_SCALE_MIN = 0.1;
|
|
29
|
+
export const LIVE2D_SCALE_MAX = 24;
|
|
30
|
+
export const VRM_SCALE_MIN = 0.05;
|
|
31
|
+
export const VRM_SCALE_MAX = 10;
|
|
32
|
+
// Range only *cuts off* a point light — three's 1/d² decay has extinguished it
|
|
33
|
+
// by ~18 m even at max intensity, so a slider past 20 changes nothing visible.
|
|
34
|
+
export const SCENE_LIGHT_RANGE_MAX = 20;
|
|
35
|
+
/** Past this the 5-tap kernel spreads thin enough that its dither reads as noise. */
|
|
36
|
+
export const SCENE_LIGHT_SHADOW_RADIUS_MAX = 16;
|
|
37
|
+
/** The point disk's 32 fixed taps stay dense across a far wider penumbra than 5 dithered ones. */
|
|
38
|
+
export const SCENE_LIGHT_POINT_SHADOW_RADIUS_MAX = 32;
|
|
39
|
+
/** Shadow softness (PCF disk radius, in shadow-map texels) ceiling for a light of `type`. */
|
|
40
|
+
export function sceneLightShadowRadiusMax(type) {
|
|
41
|
+
return type === 'point' ? SCENE_LIGHT_POINT_SHADOW_RADIUS_MAX : SCENE_LIGHT_SHADOW_RADIUS_MAX;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Exp2 falloff is squared, so a stage measured in metres is fully socked in well
|
|
45
|
+
* before Unity's nominal 1 — this keeps the slider's useful band across its width.
|
|
46
|
+
*/
|
|
47
|
+
export const SCENE_FOG_DENSITY_MAX = 0.5;
|
|
48
|
+
/** ×4 is +2 stops — enough to rescue an AgX-dimmed avatar without turning the slider to mush. */
|
|
49
|
+
export const SCENE_EXPOSURE_MAX = 4;
|
|
50
|
+
export const MTOON_RIM_MAX = 2;
|
|
51
|
+
export const MTOON_OUTLINE_WIDTH_MAX = 2;
|
|
52
|
+
/** HDR headroom: past ×1 the point is pushing emissive parts over the bloom threshold. */
|
|
53
|
+
export const MTOON_EMISSIVE_MAX = 4;
|
|
54
|
+
export function defaultMToonTuning() {
|
|
55
|
+
return { shade: 1, shadingShift: 0, shadingToony: 0, giEqualization: 0, rim: 1, outlineWidth: 1, emissive: 1 };
|
|
56
|
+
}
|
|
57
|
+
// Ranges for toggle-style effect params live in effect-schema.ts (EFFECT_SPECS).
|
|
58
|
+
// Angles at which lightDirection reproduces (1,1,1).normalize() — the app's original hardcoded light.
|
|
59
|
+
export const DEFAULT_LIGHT_AZIMUTH_DEG = 45;
|
|
60
|
+
export const DEFAULT_LIGHT_ELEVATION_DEG = (Math.asin(1 / Math.sqrt(3)) * 180) / Math.PI;
|
|
61
|
+
/** A fresh light of `type`, at the defaults that read sensibly for that type. */
|
|
62
|
+
export function defaultSceneLightOf(type) {
|
|
63
|
+
return {
|
|
64
|
+
id: crypto.randomUUID(),
|
|
65
|
+
type,
|
|
66
|
+
color: '#ffffff',
|
|
67
|
+
intensity: 1,
|
|
68
|
+
azimuth: DEFAULT_LIGHT_AZIMUTH_DEG,
|
|
69
|
+
elevation: DEFAULT_LIGHT_ELEVATION_DEG,
|
|
70
|
+
x: 0,
|
|
71
|
+
y: 1.4,
|
|
72
|
+
z: 1,
|
|
73
|
+
range: 10,
|
|
74
|
+
// A point light shadow is six cube faces, so it stays opt-in where a
|
|
75
|
+
// directional light's single depth pass can be on by default.
|
|
76
|
+
shadowQuality: type === 'directional' ? 'high' : 'off',
|
|
77
|
+
shadowRadius: 6,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export const PLACE_2D_SCALE_MIN = 0.01;
|
|
81
|
+
export const PLACE_2D_SCALE_MAX = 50;
|
|
82
|
+
export const PLACE_3D_SCALE_MIN = 0.01;
|
|
83
|
+
export const PLACE_3D_SCALE_MAX = 100;
|
|
84
|
+
/** A prop is a mesh, so it only exists in the three.js scene; every other kind renders in both. */
|
|
85
|
+
export function objectSupportsSpace(kind, space) {
|
|
86
|
+
return kind === 'prop' ? space === '3d' : true;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The one model format a space's objects can ride: the renderers never mix, and
|
|
90
|
+
* the three canvas always composites over the Pixi one, so a cross-space pin
|
|
91
|
+
* would z-fight by construction.
|
|
92
|
+
*/
|
|
93
|
+
export function attachableParentFormat(space) {
|
|
94
|
+
return space === '2d' ? 'live2d' : 'vrm';
|
|
95
|
+
}
|
|
96
|
+
export const DEFAULT_HEAD_ANGLE = { multiplier: 1, smoothing: 15 };
|
|
97
|
+
export const ATTACH_MULTIPLIER_MIN = -2;
|
|
98
|
+
export const ATTACH_MULTIPLIER_MAX = 2;
|
|
99
|
+
export const ATTACH_SMOOTHING_MAX = 50;
|
|
100
|
+
// Warudo Attachable defaults and slider ceilings, verbatim.
|
|
101
|
+
export const DEFAULT_ELASTICITY = { stiffness: 2, damping: 3, maxSpeed: 2 };
|
|
102
|
+
export const ELASTICITY_STIFFNESS_MAX = 100;
|
|
103
|
+
export const ELASTICITY_DAMPING_MAX = 10;
|
|
104
|
+
export const ELASTICITY_MAX_SPEED_MAX = 100;
|
|
105
|
+
// Plugin storage (`storage.*`) — the wire schemas and the server enforce these.
|
|
106
|
+
export const STORAGE_KEY_MAX_LENGTH = 128;
|
|
107
|
+
/** Ceiling on one value's JSON-serialized length (UTF-16 units, `JSON.stringify(v).length`). */
|
|
108
|
+
export const STORAGE_VALUE_MAX_LENGTH = 64 * 1024;
|
|
109
|
+
/** Keys one API key may hold; a `storage.set` that would exceed it answers `invalid-state`. */
|
|
110
|
+
export const STORAGE_KEYS_MAX = 256;
|
|
111
|
+
/** `speech.play` URL ceiling — sized for a ~40 s WAV as a base64 `data:audio/*` payload. */
|
|
112
|
+
export const SPEECH_URL_MAX_LENGTH = 8_000_000;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Locales shipped in the compiled catalogs (`pnpm i18n`). */
|
|
2
|
+
export declare const LOCALES: readonly ["en", "ja", "zh-CN", "zh-TW"];
|
|
3
|
+
export type Locale = (typeof LOCALES)[number];
|
|
4
|
+
/** `ui.language` setting: an explicit locale, or follow the OS language. */
|
|
5
|
+
export type LanguageSetting = 'system' | Locale;
|
|
6
|
+
/** Native-language display names for the language picker (deliberately untranslated). */
|
|
7
|
+
export declare const LOCALE_LABELS: Record<Locale, string>;
|
|
8
|
+
/** Best supported locale for a BCP 47-ish tag: exact match first, then fuzzy per language. */
|
|
9
|
+
export declare function matchLocaleTag(tag: string): Locale;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Locales shipped in the compiled catalogs (`pnpm i18n`). */
|
|
2
|
+
export const LOCALES = ['en', 'ja', 'zh-CN', 'zh-TW'];
|
|
3
|
+
/** Native-language display names for the language picker (deliberately untranslated). */
|
|
4
|
+
export const LOCALE_LABELS = {
|
|
5
|
+
en: 'English',
|
|
6
|
+
ja: '日本語',
|
|
7
|
+
'zh-CN': '简体中文',
|
|
8
|
+
'zh-TW': '繁體中文',
|
|
9
|
+
};
|
|
10
|
+
/** Best supported locale for a BCP 47-ish tag: exact match first, then fuzzy per language. */
|
|
11
|
+
export function matchLocaleTag(tag) {
|
|
12
|
+
if (LOCALES.includes(tag))
|
|
13
|
+
return tag;
|
|
14
|
+
const t = tag.toLowerCase();
|
|
15
|
+
// Most systems/browsers report `zh-TW`/`zh-HK` with no `hant` subtag, so match the regions too.
|
|
16
|
+
if (t.startsWith('zh'))
|
|
17
|
+
return /hant|tw|hk|mo/.test(t) ? 'zh-TW' : 'zh-CN';
|
|
18
|
+
if (t.startsWith('ja'))
|
|
19
|
+
return 'ja';
|
|
20
|
+
return 'en';
|
|
21
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ApiErrorCode } from './errors.ts';
|
|
2
2
|
import type { EventData, EventName } from './events.ts';
|
|
3
3
|
import type { MethodName, MethodRequest } from './methods.ts';
|
|
4
|
+
import type { AppCapability } from './types.ts';
|
|
4
5
|
export interface RequestMessage<M extends MethodName = MethodName> {
|
|
5
6
|
kind: 'request';
|
|
6
7
|
/** Correlation id, echoed on the response. Non-empty, client-chosen. */
|
|
@@ -12,11 +13,7 @@ export interface RequestMessage<M extends MethodName = MethodName> {
|
|
|
12
13
|
export interface HelloMessage {
|
|
13
14
|
kind: 'hello';
|
|
14
15
|
protocol: number;
|
|
15
|
-
app:
|
|
16
|
-
name: string;
|
|
17
|
-
version: string;
|
|
18
|
-
platform: string;
|
|
19
|
-
};
|
|
16
|
+
app: AppInfo;
|
|
20
17
|
}
|
|
21
18
|
export interface ResponseMessage {
|
|
22
19
|
kind: 'response';
|
|
@@ -37,6 +34,14 @@ export interface EventMessage<E extends EventName = EventName> {
|
|
|
37
34
|
}
|
|
38
35
|
export type ServerMessage = HelloMessage | ResponseMessage | ErrorMessage | EventMessage;
|
|
39
36
|
export type ClientMessage = RequestMessage;
|
|
37
|
+
/** The desktop app's identity, as `hello` reports it. */
|
|
38
|
+
export interface AppInfo {
|
|
39
|
+
name: string;
|
|
40
|
+
version: string;
|
|
41
|
+
platform: string;
|
|
42
|
+
/** Features to gate on — never version-sniff. Empty when the server predates capability reporting. */
|
|
43
|
+
capabilities: AppCapability[];
|
|
44
|
+
}
|
|
40
45
|
/**
|
|
41
46
|
* Parse one inbound client frame. Returns the request, or an error code telling
|
|
42
47
|
* the server what to answer: `parse-error` for junk bytes, `invalid-request`
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
+
import { isRecord } from "../values/guards.js";
|
|
1
2
|
import { isApiErrorCode } from "./errors.js";
|
|
2
3
|
import { isEventName } from "./events.js";
|
|
3
|
-
|
|
4
|
-
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
5
|
-
}
|
|
4
|
+
import { isAppCapability } from "./types.js";
|
|
6
5
|
/**
|
|
7
6
|
* Parse one inbound client frame. Returns the request, or an error code telling
|
|
8
7
|
* the server what to answer: `parse-error` for junk bytes, `invalid-request`
|
|
@@ -48,7 +47,9 @@ export function parseServerMessage(raw) {
|
|
|
48
47
|
const { name, version, platform } = v.app;
|
|
49
48
|
if (typeof name !== 'string' || typeof version !== 'string' || typeof platform !== 'string')
|
|
50
49
|
return null;
|
|
51
|
-
|
|
50
|
+
// Tolerant both ways: absent on older servers, unknown names from newer ones dropped.
|
|
51
|
+
const capabilities = Array.isArray(v.app.capabilities) ? v.app.capabilities.filter(isAppCapability) : [];
|
|
52
|
+
return { kind: 'hello', protocol: v.protocol, app: { name, version, platform, capabilities } };
|
|
52
53
|
}
|
|
53
54
|
case 'response':
|
|
54
55
|
if (typeof v.id !== 'string' || v.id === '')
|
|
@@ -10,9 +10,11 @@ export interface EventMap {
|
|
|
10
10
|
modelId: string | null;
|
|
11
11
|
config: HotkeyState;
|
|
12
12
|
};
|
|
13
|
+
/** `sources` maps source id → status; optional so pre-multi-source servers still parse. */
|
|
13
14
|
'tracking.status': {
|
|
14
15
|
tracking: TrackingStatus;
|
|
15
16
|
pose: PoseStatus;
|
|
17
|
+
sources?: Record<string, TrackingStatus>;
|
|
16
18
|
};
|
|
17
19
|
/** A model instance finished loading on stage. */
|
|
18
20
|
'instance.loaded': {
|
|
@@ -31,6 +33,14 @@ export interface EventMap {
|
|
|
31
33
|
'motion.ended': {
|
|
32
34
|
instanceId: string;
|
|
33
35
|
};
|
|
36
|
+
/** A `speech.play` utterance began on this instance. */
|
|
37
|
+
'speech.started': {
|
|
38
|
+
instanceId: string;
|
|
39
|
+
};
|
|
40
|
+
/** The utterance finished, failed, was stopped, or was superseded — one `ended` per `started`. */
|
|
41
|
+
'speech.ended': {
|
|
42
|
+
instanceId: string;
|
|
43
|
+
};
|
|
34
44
|
/** True while a scene apply or pipeline warm-up is in flight. */
|
|
35
45
|
'scene.loading': {
|
|
36
46
|
loading: boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EventName } from './events.ts';
|
|
2
|
-
import type { AnchorOption, AssetKind, AssetRef, Attach, Expression, ExpressionPersistence, HotkeyConfig, HotkeyState, InjectEntry, InjectTarget, InstanceRuntime, ModelInfo, ModelRef, MotionGroup, MToonTuning, ObjectContent, ObjectLightOverride, ObjectSpace, Place2D, Place3D, PlayingMotion, PoseSourceId, PoseStatus, Scene, SceneItem, ScenePatch, SceneState, ScreenPlacement, Settings, SettingsPatch, TrackingSourceId, TrackingStatus, VrmPlacement } from './types.ts';
|
|
2
|
+
import type { AnchorOption, AppCapability, AssetKind, AssetRef, Attach, Expression, ExpressionPersistence, HotkeyConfig, HotkeyState, InjectEntry, InjectTarget, InstanceRuntime, JsonValue, ModelInfo, ModelRef, MotionGroup, MToonTuning, ObjectContent, ObjectLightOverride, ObjectSpace, Place2D, Place3D, PlayingMotion, PoseSourceId, PoseStatus, Scene, SceneItem, ScenePatch, SceneState, ScreenPlacement, Settings, SettingsPatch, TrackingSourceConfig, TrackingSourceId, TrackingSourceKind, TrackingStatus, VrmPlacement } from './types.ts';
|
|
3
3
|
/** Marker for methods that take no parameters; the client lets you omit the argument. */
|
|
4
4
|
export type EmptyRequest = Record<never, never>;
|
|
5
5
|
/** Marker for methods whose success response carries no data. */
|
|
@@ -73,6 +73,13 @@ export interface InstanceSetVisibleRequest {
|
|
|
73
73
|
visible: boolean;
|
|
74
74
|
}
|
|
75
75
|
export type InstanceSetVisibleResponse = EmptyResponse;
|
|
76
|
+
/** Omitted fields keep their binding; null stops tracking that channel for this instance. */
|
|
77
|
+
export interface InstanceSetTrackingSourcesRequest {
|
|
78
|
+
instanceId: string;
|
|
79
|
+
faceSourceId?: string | null;
|
|
80
|
+
poseSourceId?: string | null;
|
|
81
|
+
}
|
|
82
|
+
export type InstanceSetTrackingSourcesResponse = EmptyResponse;
|
|
76
83
|
export interface InstanceReorderRequest {
|
|
77
84
|
/** Full z-order within the scene; ids not listed keep their relative order at the end. */
|
|
78
85
|
orderedInstanceIds: string[];
|
|
@@ -206,6 +213,20 @@ export interface MotionPlayingRequest {
|
|
|
206
213
|
export interface MotionPlayingResponse {
|
|
207
214
|
playing: PlayingMotion | null;
|
|
208
215
|
}
|
|
216
|
+
/** Play a clip with lip-sync. One utterance per model: a new play supersedes the current one. */
|
|
217
|
+
export interface SpeechPlayRequest {
|
|
218
|
+
instanceId?: string;
|
|
219
|
+
/** `https:`/`http:` (local TTS bridges) or an inline `data:audio/*` payload. */
|
|
220
|
+
url: string;
|
|
221
|
+
/** 0..1; the backend's own default when omitted. */
|
|
222
|
+
volume?: number;
|
|
223
|
+
}
|
|
224
|
+
export type SpeechPlayResponse = EmptyResponse;
|
|
225
|
+
/** Stopping a silent model is a silent success. */
|
|
226
|
+
export interface SpeechStopRequest {
|
|
227
|
+
instanceId?: string;
|
|
228
|
+
}
|
|
229
|
+
export type SpeechStopResponse = EmptyResponse;
|
|
209
230
|
export interface HotkeyListRequest {
|
|
210
231
|
modelId?: string;
|
|
211
232
|
}
|
|
@@ -279,10 +300,37 @@ export interface PoseSetPortRequest {
|
|
|
279
300
|
port: number;
|
|
280
301
|
}
|
|
281
302
|
export type PoseSetPortResponse = EmptyResponse;
|
|
303
|
+
export interface TrackingAddSourceRequest {
|
|
304
|
+
kind: TrackingSourceKind;
|
|
305
|
+
name?: string;
|
|
306
|
+
/** Face kinds: pin to a device IP. */
|
|
307
|
+
phoneIp?: string | null;
|
|
308
|
+
/** vmc: the UDP port to listen on; defaults to the next free VMC port. */
|
|
309
|
+
port?: number;
|
|
310
|
+
}
|
|
311
|
+
export interface TrackingAddSourceResponse {
|
|
312
|
+
source: TrackingSourceConfig;
|
|
313
|
+
}
|
|
314
|
+
export interface TrackingUpdateSourceRequest {
|
|
315
|
+
id: string;
|
|
316
|
+
name?: string;
|
|
317
|
+
enabled?: boolean;
|
|
318
|
+
phoneIp?: string | null;
|
|
319
|
+
port?: number;
|
|
320
|
+
}
|
|
321
|
+
export interface TrackingUpdateSourceResponse {
|
|
322
|
+
source: TrackingSourceConfig;
|
|
323
|
+
}
|
|
324
|
+
export interface TrackingRemoveSourceRequest {
|
|
325
|
+
id: string;
|
|
326
|
+
}
|
|
327
|
+
export type TrackingRemoveSourceResponse = EmptyResponse;
|
|
282
328
|
export type TrackingStatusRequest = EmptyRequest;
|
|
283
329
|
export interface TrackingStatusResponse {
|
|
284
330
|
tracking: TrackingStatus;
|
|
285
331
|
pose: PoseStatus;
|
|
332
|
+
/** Per-source status by {@link TrackingSourceConfig.id}. */
|
|
333
|
+
sources: Record<string, TrackingStatus>;
|
|
286
334
|
}
|
|
287
335
|
export interface StageResetTransformRequest {
|
|
288
336
|
instanceId?: string;
|
|
@@ -298,6 +346,8 @@ export interface AppInfoResponse {
|
|
|
298
346
|
version: string;
|
|
299
347
|
platform: string;
|
|
300
348
|
protocol: number;
|
|
349
|
+
/** Same list `hello` carries; gate features on it, never on `version`. */
|
|
350
|
+
capabilities: AppCapability[];
|
|
301
351
|
}
|
|
302
352
|
export type AppLocalAddressesRequest = EmptyRequest;
|
|
303
353
|
export interface AppLocalAddressesResponse {
|
|
@@ -323,6 +373,28 @@ export interface SessionIdentifyRequest {
|
|
|
323
373
|
developer?: string;
|
|
324
374
|
}
|
|
325
375
|
export type SessionIdentifyResponse = EmptyResponse;
|
|
376
|
+
export interface StorageGetRequest {
|
|
377
|
+
key: string;
|
|
378
|
+
}
|
|
379
|
+
export interface StorageGetResponse {
|
|
380
|
+
/** Null when the key was never set (a stored null is indistinguishable by design). */
|
|
381
|
+
value: JsonValue | null;
|
|
382
|
+
}
|
|
383
|
+
export interface StorageSetRequest {
|
|
384
|
+
key: string;
|
|
385
|
+
value: JsonValue;
|
|
386
|
+
}
|
|
387
|
+
export type StorageSetResponse = EmptyResponse;
|
|
388
|
+
/** Deleting an absent key succeeds silently. */
|
|
389
|
+
export interface StorageDeleteRequest {
|
|
390
|
+
key: string;
|
|
391
|
+
}
|
|
392
|
+
export type StorageDeleteResponse = EmptyResponse;
|
|
393
|
+
export type StorageListRequest = EmptyRequest;
|
|
394
|
+
export interface StorageListResponse {
|
|
395
|
+
/** Sorted for stable output. */
|
|
396
|
+
keys: string[];
|
|
397
|
+
}
|
|
326
398
|
export interface EventsSubscribeRequest {
|
|
327
399
|
events: EventName[];
|
|
328
400
|
}
|
|
@@ -413,6 +485,11 @@ export interface MethodMap {
|
|
|
413
485
|
request: InstanceSetVisibleRequest;
|
|
414
486
|
response: InstanceSetVisibleResponse;
|
|
415
487
|
};
|
|
488
|
+
/** Bind this instance's face/pose channels to tracking sources (null stops tracking that channel). */
|
|
489
|
+
'instance.setTrackingSources': {
|
|
490
|
+
request: InstanceSetTrackingSourcesRequest;
|
|
491
|
+
response: InstanceSetTrackingSourcesResponse;
|
|
492
|
+
};
|
|
416
493
|
'instance.reorder': {
|
|
417
494
|
request: InstanceReorderRequest;
|
|
418
495
|
response: InstanceReorderResponse;
|
|
@@ -511,6 +588,18 @@ export interface MethodMap {
|
|
|
511
588
|
request: MotionPlayingRequest;
|
|
512
589
|
response: MotionPlayingResponse;
|
|
513
590
|
};
|
|
591
|
+
/**
|
|
592
|
+
* Lip-synced audio on models advertising the `speech` capability. The response means
|
|
593
|
+
* playback started (else `invalid-state`); `speech.started`/`speech.ended` bracket it.
|
|
594
|
+
*/
|
|
595
|
+
'speech.play': {
|
|
596
|
+
request: SpeechPlayRequest;
|
|
597
|
+
response: SpeechPlayResponse;
|
|
598
|
+
};
|
|
599
|
+
'speech.stop': {
|
|
600
|
+
request: SpeechStopRequest;
|
|
601
|
+
response: SpeechStopResponse;
|
|
602
|
+
};
|
|
514
603
|
'hotkey.list': {
|
|
515
604
|
request: HotkeyListRequest;
|
|
516
605
|
response: HotkeyListResponse;
|
|
@@ -551,18 +640,33 @@ export interface MethodMap {
|
|
|
551
640
|
request: TrackingSetEnabledRequest;
|
|
552
641
|
response: TrackingSetEnabledResponse;
|
|
553
642
|
};
|
|
643
|
+
/** Legacy single-source setter: rewrites the first face source's kind. Prefer the source CRUD below. */
|
|
554
644
|
'tracking.setSource': {
|
|
555
645
|
request: TrackingSetSourceRequest;
|
|
556
646
|
response: TrackingSetSourceResponse;
|
|
557
647
|
};
|
|
648
|
+
'tracking.addSource': {
|
|
649
|
+
request: TrackingAddSourceRequest;
|
|
650
|
+
response: TrackingAddSourceResponse;
|
|
651
|
+
};
|
|
652
|
+
'tracking.updateSource': {
|
|
653
|
+
request: TrackingUpdateSourceRequest;
|
|
654
|
+
response: TrackingUpdateSourceResponse;
|
|
655
|
+
};
|
|
656
|
+
'tracking.removeSource': {
|
|
657
|
+
request: TrackingRemoveSourceRequest;
|
|
658
|
+
response: TrackingRemoveSourceResponse;
|
|
659
|
+
};
|
|
558
660
|
'pose.setEnabled': {
|
|
559
661
|
request: PoseSetEnabledRequest;
|
|
560
662
|
response: PoseSetEnabledResponse;
|
|
561
663
|
};
|
|
664
|
+
/** Legacy: operates on the first vmc source. Prefer the tracking.* source CRUD. */
|
|
562
665
|
'pose.setSource': {
|
|
563
666
|
request: PoseSetSourceRequest;
|
|
564
667
|
response: PoseSetSourceResponse;
|
|
565
668
|
};
|
|
669
|
+
/** Legacy: sets the first vmc source's port. */
|
|
566
670
|
'pose.setPort': {
|
|
567
671
|
request: PoseSetPortRequest;
|
|
568
672
|
response: PoseSetPortResponse;
|
|
@@ -603,6 +707,27 @@ export interface MethodMap {
|
|
|
603
707
|
request: SessionIdentifyRequest;
|
|
604
708
|
response: SessionIdentifyResponse;
|
|
605
709
|
};
|
|
710
|
+
/**
|
|
711
|
+
* Durable key–value storage namespaced by the session's API key; revoking the key deletes
|
|
712
|
+
* it. Limits: `STORAGE_KEY_MAX_LENGTH`, `STORAGE_VALUE_MAX_LENGTH`, `STORAGE_KEYS_MAX`
|
|
713
|
+
* keys (over-quota answers `invalid-state`).
|
|
714
|
+
*/
|
|
715
|
+
'storage.get': {
|
|
716
|
+
request: StorageGetRequest;
|
|
717
|
+
response: StorageGetResponse;
|
|
718
|
+
};
|
|
719
|
+
'storage.set': {
|
|
720
|
+
request: StorageSetRequest;
|
|
721
|
+
response: StorageSetResponse;
|
|
722
|
+
};
|
|
723
|
+
'storage.delete': {
|
|
724
|
+
request: StorageDeleteRequest;
|
|
725
|
+
response: StorageDeleteResponse;
|
|
726
|
+
};
|
|
727
|
+
'storage.list': {
|
|
728
|
+
request: StorageListRequest;
|
|
729
|
+
response: StorageListResponse;
|
|
730
|
+
};
|
|
606
731
|
'events.subscribe': {
|
|
607
732
|
request: EventsSubscribeRequest;
|
|
608
733
|
response: EventsSubscribeResponse;
|
|
@@ -3,10 +3,14 @@ import type { InjectTarget } from './types.ts';
|
|
|
3
3
|
export declare const PROTOCOL_VERSION = 1;
|
|
4
4
|
/** Default port the Persona API server listens on (user-configurable in the app). */
|
|
5
5
|
export declare const DEFAULT_API_PORT = 25034;
|
|
6
|
+
/** Default host clients dial: the server binds loopback unless LAN is opted in. */
|
|
7
|
+
export declare const DEFAULT_API_HOST = "127.0.0.1";
|
|
6
8
|
/** Server close code: the session's API key was revoked. Terminal — the client must not redial. */
|
|
7
9
|
export declare const CLOSE_KEY_REVOKED = 4001;
|
|
8
10
|
/** Server close code: the user disconnected this session from Persona's settings. Terminal — the client must not redial. */
|
|
9
11
|
export declare const CLOSE_FORCE_DISCONNECTED = 4002;
|
|
12
|
+
/** Standard going-away code the server sends when it stops; reconnecting is expected once it returns. */
|
|
13
|
+
export declare const CLOSE_SERVER_STOPPING = 1001;
|
|
10
14
|
/** An injected parameter reverts this long after its last write — the lease, not a setting. */
|
|
11
15
|
export declare const INJECT_LEASE_TTL_MS = 1000;
|
|
12
16
|
/** How often {@link PersonaClient.driveParameter} re-sends held leases to keep them alive. */
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
export const PROTOCOL_VERSION = 1;
|
|
3
3
|
/** Default port the Persona API server listens on (user-configurable in the app). */
|
|
4
4
|
export const DEFAULT_API_PORT = 25034;
|
|
5
|
+
/** Default host clients dial: the server binds loopback unless LAN is opted in. */
|
|
6
|
+
export const DEFAULT_API_HOST = '127.0.0.1';
|
|
5
7
|
/** Server close code: the session's API key was revoked. Terminal — the client must not redial. */
|
|
6
8
|
export const CLOSE_KEY_REVOKED = 4001;
|
|
7
9
|
/** Server close code: the user disconnected this session from Persona's settings. Terminal — the client must not redial. */
|
|
8
10
|
export const CLOSE_FORCE_DISCONNECTED = 4002;
|
|
11
|
+
/** Standard going-away code the server sends when it stops; reconnecting is expected once it returns. */
|
|
12
|
+
export const CLOSE_SERVER_STOPPING = 1001;
|
|
9
13
|
/** An injected parameter reverts this long after its last write — the lease, not a setting. */
|
|
10
14
|
export const INJECT_LEASE_TTL_MS = 1000;
|
|
11
15
|
/** How often {@link PersonaClient.driveParameter} re-sends held leases to keep them alive. */
|
|
@@ -93,6 +93,27 @@ export declare const requestSchemas: {
|
|
|
93
93
|
'hotkey.trigger': z.ZodObject<{
|
|
94
94
|
hotkeyId: z.ZodString;
|
|
95
95
|
}, z.core.$strip>;
|
|
96
|
+
'tracking.addSource': z.ZodObject<{
|
|
97
|
+
kind: z.ZodEnum<{
|
|
98
|
+
"vts-ios": "vts-ios";
|
|
99
|
+
"vts-ios-native": "vts-ios-native";
|
|
100
|
+
ifacialmocap: "ifacialmocap";
|
|
101
|
+
vmc: "vmc";
|
|
102
|
+
}>;
|
|
103
|
+
name: z.ZodOptional<z.ZodString>;
|
|
104
|
+
phoneIp: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
105
|
+
port: z.ZodOptional<z.ZodNumber>;
|
|
106
|
+
}, z.core.$strip>;
|
|
107
|
+
'tracking.updateSource': z.ZodObject<{
|
|
108
|
+
id: z.ZodString;
|
|
109
|
+
name: z.ZodOptional<z.ZodString>;
|
|
110
|
+
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
111
|
+
phoneIp: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
112
|
+
port: z.ZodOptional<z.ZodNumber>;
|
|
113
|
+
}, z.core.$strip>;
|
|
114
|
+
'tracking.removeSource': z.ZodObject<{
|
|
115
|
+
id: z.ZodString;
|
|
116
|
+
}, z.core.$strip>;
|
|
96
117
|
'model.register': z.ZodObject<{
|
|
97
118
|
path: z.ZodString;
|
|
98
119
|
}, z.core.$strip>;
|
|
@@ -153,6 +174,24 @@ export declare const requestSchemas: {
|
|
|
153
174
|
version: z.ZodOptional<z.ZodString>;
|
|
154
175
|
developer: z.ZodOptional<z.ZodString>;
|
|
155
176
|
}, z.core.$strip>;
|
|
177
|
+
'storage.get': z.ZodObject<{
|
|
178
|
+
key: z.ZodString;
|
|
179
|
+
}, z.core.$strip>;
|
|
180
|
+
'storage.set': z.ZodObject<{
|
|
181
|
+
key: z.ZodString;
|
|
182
|
+
value: z.ZodJSONSchema;
|
|
183
|
+
}, z.core.$strip>;
|
|
184
|
+
'storage.delete': z.ZodObject<{
|
|
185
|
+
key: z.ZodString;
|
|
186
|
+
}, z.core.$strip>;
|
|
187
|
+
'speech.play': z.ZodObject<{
|
|
188
|
+
instanceId: z.ZodOptional<z.ZodString>;
|
|
189
|
+
url: z.ZodString;
|
|
190
|
+
volume: z.ZodOptional<z.ZodNumber>;
|
|
191
|
+
}, z.core.$strip>;
|
|
192
|
+
'speech.stop': z.ZodObject<{
|
|
193
|
+
instanceId: z.ZodOptional<z.ZodString>;
|
|
194
|
+
}, z.core.$strip>;
|
|
156
195
|
'events.subscribe': z.ZodObject<{
|
|
157
196
|
events: z.ZodArray<z.ZodCustom<keyof import("./events.ts").EventMap, keyof import("./events.ts").EventMap>>;
|
|
158
197
|
}, z.core.$strip>;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import * as z from 'zod';
|
|
2
|
+
import { isRecord } from "../values/guards.js";
|
|
3
|
+
import { SPEECH_URL_MAX_LENGTH, STORAGE_KEY_MAX_LENGTH, STORAGE_VALUE_MAX_LENGTH } from "../values/limits.js";
|
|
2
4
|
// Runtime validation for the request side of the wire. Schemas exist for the
|
|
3
5
|
// methods whose params the app's main process consumes directly; methods without
|
|
4
6
|
// one are stage-owned — the renderer validates and heals them (same rules as the
|
|
5
7
|
// panel). Fields marked z.custom are deliberately gated shallowly here because
|
|
6
8
|
// the app heals them into shape server-side.
|
|
7
|
-
function isRecord(v) {
|
|
8
|
-
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
9
|
-
}
|
|
10
9
|
const nonEmpty = z.string().min(1);
|
|
10
|
+
const port = z.number().int().min(1).max(65535);
|
|
11
11
|
export const InjectTargetSchema = z.object({
|
|
12
12
|
type: z.enum(['input', 'live2d-param', 'vrm-expression']),
|
|
13
13
|
id: nonEmpty,
|
|
@@ -32,6 +32,25 @@ export const SettingsPatchSchema = z.object({
|
|
|
32
32
|
});
|
|
33
33
|
/** Tolerates unknown names for version skew; the server filters them and reports what took. */
|
|
34
34
|
const lenientEventNameSchema = z.custom(v => typeof v === 'string', 'event names must be strings');
|
|
35
|
+
const storageKey = z.string().min(1).max(STORAGE_KEY_MAX_LENGTH);
|
|
36
|
+
/** JSON-serializable and bounded; the serialized-length cap is what the server persists by. */
|
|
37
|
+
const storageValue = z
|
|
38
|
+
.json()
|
|
39
|
+
.refine(v => JSON.stringify(v).length <= STORAGE_VALUE_MAX_LENGTH, `value exceeds ${STORAGE_VALUE_MAX_LENGTH} serialized chars`);
|
|
40
|
+
/** http(s) covers hosted clips and local TTS bridges; data: carries inline audio only. */
|
|
41
|
+
const speechUrl = z
|
|
42
|
+
.string()
|
|
43
|
+
.min(1)
|
|
44
|
+
.max(SPEECH_URL_MAX_LENGTH)
|
|
45
|
+
.refine(v => {
|
|
46
|
+
try {
|
|
47
|
+
const p = new URL(v).protocol;
|
|
48
|
+
return p === 'https:' || p === 'http:' || (p === 'data:' && v.slice(5, 11) === 'audio/');
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}, 'url must be http(s) or data:audio/*');
|
|
35
54
|
export const requestSchemas = {
|
|
36
55
|
'scene.get': z.object({ sceneId: nonEmpty.optional() }),
|
|
37
56
|
'scene.activate': z.object({ sceneId: nonEmpty }),
|
|
@@ -57,6 +76,20 @@ export const requestSchemas = {
|
|
|
57
76
|
config: z.custom(isRecord, 'config must be a hotkey config'),
|
|
58
77
|
}),
|
|
59
78
|
'hotkey.trigger': z.object({ hotkeyId: nonEmpty }),
|
|
79
|
+
'tracking.addSource': z.object({
|
|
80
|
+
kind: z.enum(['vts-ios', 'vts-ios-native', 'ifacialmocap', 'vmc']),
|
|
81
|
+
name: z.string().optional(),
|
|
82
|
+
phoneIp: nonEmpty.nullable().optional(),
|
|
83
|
+
port: port.optional(),
|
|
84
|
+
}),
|
|
85
|
+
'tracking.updateSource': z.object({
|
|
86
|
+
id: nonEmpty,
|
|
87
|
+
name: z.string().optional(),
|
|
88
|
+
enabled: z.boolean().optional(),
|
|
89
|
+
phoneIp: nonEmpty.nullable().optional(),
|
|
90
|
+
port: port.optional(),
|
|
91
|
+
}),
|
|
92
|
+
'tracking.removeSource': z.object({ id: nonEmpty }),
|
|
60
93
|
'model.register': z.object({ path: nonEmpty }),
|
|
61
94
|
'asset.register': z.object({ path: nonEmpty, want: z.enum(['image', 'video', 'prop', 'ibl', 'lut']).optional() }),
|
|
62
95
|
'settings.patch': z.object({ settings: SettingsPatchSchema }),
|
|
@@ -64,12 +97,21 @@ export const requestSchemas = {
|
|
|
64
97
|
'tracking.setSource': z.object({ source: z.enum(['vts-ios', 'vts-ios-native', 'ifacialmocap']) }),
|
|
65
98
|
'pose.setEnabled': z.object({ enabled: z.boolean() }),
|
|
66
99
|
'pose.setSource': z.object({ source: z.enum(['vmc']) }),
|
|
67
|
-
'pose.setPort': z.object({ port
|
|
100
|
+
'pose.setPort': z.object({ port }),
|
|
68
101
|
'session.identify': z.object({
|
|
69
102
|
name: z.string().trim().min(1).max(64),
|
|
70
103
|
version: z.string().max(32).optional(),
|
|
71
104
|
developer: z.string().max(64).optional(),
|
|
72
105
|
}),
|
|
106
|
+
'storage.get': z.object({ key: storageKey }),
|
|
107
|
+
'storage.set': z.object({ key: storageKey, value: storageValue }),
|
|
108
|
+
'storage.delete': z.object({ key: storageKey }),
|
|
109
|
+
'speech.play': z.object({
|
|
110
|
+
instanceId: nonEmpty.optional(),
|
|
111
|
+
url: speechUrl,
|
|
112
|
+
volume: z.number().min(0).max(1).optional(),
|
|
113
|
+
}),
|
|
114
|
+
'speech.stop': z.object({ instanceId: nonEmpty.optional() }),
|
|
73
115
|
'events.subscribe': z.object({ events: z.array(lenientEventNameSchema).min(1) }),
|
|
74
116
|
'events.unsubscribe': z.object({ events: z.array(lenientEventNameSchema).optional() }),
|
|
75
117
|
'param.inject': z.object({ entries: z.array(InjectEntrySchema).min(1) }),
|