@laplace.live/persona-sdk 0.18.1 → 1.1.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.
Files changed (48) hide show
  1. package/README.md +4 -2
  2. package/dist/client/client.d.ts +1 -1
  3. package/dist/index.d.ts +9 -1
  4. package/dist/index.js +9 -1
  5. package/dist/values/bindings.d.ts +43 -0
  6. package/dist/values/bindings.js +1 -0
  7. package/dist/values/controller.d.ts +109 -0
  8. package/dist/values/controller.js +167 -0
  9. package/dist/values/curve.d.ts +5 -8
  10. package/dist/values/curve.js +16 -7
  11. package/dist/values/edit-history.d.ts +21 -0
  12. package/dist/values/edit-history.js +62 -0
  13. package/dist/values/effect-schema.d.ts +72 -12
  14. package/dist/values/effect-schema.js +279 -67
  15. package/dist/values/gltf-extensions.d.ts +20 -0
  16. package/dist/values/gltf-extensions.js +68 -0
  17. package/dist/values/guards.d.ts +2 -0
  18. package/dist/values/guards.js +5 -0
  19. package/dist/values/hands.d.ts +53 -0
  20. package/dist/values/hands.js +89 -0
  21. package/dist/values/hotkeys.d.ts +6 -0
  22. package/dist/values/hotkeys.js +11 -0
  23. package/dist/values/labels.d.ts +2 -0
  24. package/dist/values/labels.js +5 -1
  25. package/dist/values/limits.d.ts +19 -0
  26. package/dist/values/limits.js +27 -0
  27. package/dist/values/lipsync.d.ts +106 -0
  28. package/dist/values/lipsync.js +120 -0
  29. package/dist/values/locale.d.ts +2 -0
  30. package/dist/values/model-info.d.ts +66 -0
  31. package/dist/values/model-info.js +1 -0
  32. package/dist/values/stage-info.d.ts +27 -0
  33. package/dist/values/stage-info.js +1 -0
  34. package/dist/values/vrm-bindings.d.ts +14 -0
  35. package/dist/values/vrm-bindings.js +20 -0
  36. package/dist/wire/events.d.ts +1 -1
  37. package/dist/wire/methods.d.ts +210 -5
  38. package/dist/wire/protocol.d.ts +1 -1
  39. package/dist/wire/protocol.js +1 -1
  40. package/dist/wire/schemas.d.ts +116 -2
  41. package/dist/wire/schemas.js +40 -0
  42. package/dist/wire/types.d.ts +294 -131
  43. package/dist/wire/types.js +90 -12
  44. package/package.json +3 -17
  45. package/dist/effects.d.ts +0 -79
  46. package/dist/effects.js +0 -6
  47. package/dist/values/custom-effect.d.ts +0 -44
  48. package/dist/values/custom-effect.js +0 -136
@@ -0,0 +1,53 @@
1
+ /** VTube Studio hand inputs, extended with nizima LIVE's wrist Y rotation. */
2
+ export declare const HAND_INPUT_NAMES: readonly ["HandLeftFound", "HandLeftPositionX", "HandLeftPositionY", "HandLeftPositionZ", "HandLeftAngleX", "HandLeftAngleY", "HandLeftAngleZ", "HandLeftOpen", "HandLeftFinger_1_Thumb", "HandLeftFinger_2_Index", "HandLeftFinger_3_Middle", "HandLeftFinger_4_Ring", "HandLeftFinger_5_Pinky", "HandRightFound", "HandRightPositionX", "HandRightPositionY", "HandRightPositionZ", "HandRightAngleX", "HandRightAngleY", "HandRightAngleZ", "HandRightOpen", "HandRightFinger_1_Thumb", "HandRightFinger_2_Index", "HandRightFinger_3_Middle", "HandRightFinger_4_Ring", "HandRightFinger_5_Pinky", "BothHandsFound", "HandDistance"];
3
+ export type HandInputName = (typeof HAND_INPUT_NAMES)[number];
4
+ export type HandInputs = Partial<Record<HandInputName, number>>;
5
+ /** Whether an input uses the canonical hand vocabulary. */
6
+ export declare function isHandInputName(input: string): input is HandInputName;
7
+ /** Names nizima uses for VTS's numbered fingers and both-hands detection. */
8
+ export declare const NIZIMA_HAND_ALIASES: {
9
+ readonly HandLeftFingerThumb: "HandLeftFinger_1_Thumb";
10
+ readonly HandLeftFingerIndex: "HandLeftFinger_2_Index";
11
+ readonly HandLeftFingerMiddle: "HandLeftFinger_3_Middle";
12
+ readonly HandLeftFingerRing: "HandLeftFinger_4_Ring";
13
+ readonly HandLeftFingerPinky: "HandLeftFinger_5_Pinky";
14
+ readonly HandRightFingerThumb: "HandRightFinger_1_Thumb";
15
+ readonly HandRightFingerIndex: "HandRightFinger_2_Index";
16
+ readonly HandRightFingerMiddle: "HandRightFinger_3_Middle";
17
+ readonly HandRightFingerRing: "HandRightFinger_4_Ring";
18
+ readonly HandRightFingerPinky: "HandRightFinger_5_Pinky";
19
+ readonly HandBothFound: "BothHandsFound";
20
+ };
21
+ /** Canonicalize a hand name; aliases share canonical units on the plugin API. */
22
+ export declare function canonicalHandInputName(input: string): HandInputName | null;
23
+ /** Angles are degrees; positions and distance use VTS's ten-unit scale. */
24
+ export declare const HAND_INPUT_RANGES: {
25
+ readonly HandLeftFound: readonly [0, 1];
26
+ readonly HandLeftPositionX: readonly [-10, 10];
27
+ readonly HandLeftPositionY: readonly [-10, 10];
28
+ readonly HandLeftPositionZ: readonly [-10, 10];
29
+ readonly HandLeftAngleX: readonly [-180, 180];
30
+ readonly HandLeftAngleY: readonly [-90, 90];
31
+ readonly HandLeftAngleZ: readonly [-180, 180];
32
+ readonly HandLeftOpen: readonly [0, 1];
33
+ readonly HandLeftFinger_1_Thumb: readonly [0, 1];
34
+ readonly HandLeftFinger_2_Index: readonly [0, 1];
35
+ readonly HandLeftFinger_3_Middle: readonly [0, 1];
36
+ readonly HandLeftFinger_4_Ring: readonly [0, 1];
37
+ readonly HandLeftFinger_5_Pinky: readonly [0, 1];
38
+ readonly HandRightFound: readonly [0, 1];
39
+ readonly HandRightPositionX: readonly [-10, 10];
40
+ readonly HandRightPositionY: readonly [-10, 10];
41
+ readonly HandRightPositionZ: readonly [-10, 10];
42
+ readonly HandRightAngleX: readonly [-180, 180];
43
+ readonly HandRightAngleY: readonly [-90, 90];
44
+ readonly HandRightAngleZ: readonly [-180, 180];
45
+ readonly HandRightOpen: readonly [0, 1];
46
+ readonly HandRightFinger_1_Thumb: readonly [0, 1];
47
+ readonly HandRightFinger_2_Index: readonly [0, 1];
48
+ readonly HandRightFinger_3_Middle: readonly [0, 1];
49
+ readonly HandRightFinger_4_Ring: readonly [0, 1];
50
+ readonly HandRightFinger_5_Pinky: readonly [0, 1];
51
+ readonly BothHandsFound: readonly [0, 1];
52
+ readonly HandDistance: readonly [0, 10];
53
+ };
@@ -0,0 +1,89 @@
1
+ /** VTube Studio hand inputs, extended with nizima LIVE's wrist Y rotation. */
2
+ export const HAND_INPUT_NAMES = [
3
+ 'HandLeftFound',
4
+ 'HandLeftPositionX',
5
+ 'HandLeftPositionY',
6
+ 'HandLeftPositionZ',
7
+ 'HandLeftAngleX',
8
+ 'HandLeftAngleY',
9
+ 'HandLeftAngleZ',
10
+ 'HandLeftOpen',
11
+ 'HandLeftFinger_1_Thumb',
12
+ 'HandLeftFinger_2_Index',
13
+ 'HandLeftFinger_3_Middle',
14
+ 'HandLeftFinger_4_Ring',
15
+ 'HandLeftFinger_5_Pinky',
16
+ 'HandRightFound',
17
+ 'HandRightPositionX',
18
+ 'HandRightPositionY',
19
+ 'HandRightPositionZ',
20
+ 'HandRightAngleX',
21
+ 'HandRightAngleY',
22
+ 'HandRightAngleZ',
23
+ 'HandRightOpen',
24
+ 'HandRightFinger_1_Thumb',
25
+ 'HandRightFinger_2_Index',
26
+ 'HandRightFinger_3_Middle',
27
+ 'HandRightFinger_4_Ring',
28
+ 'HandRightFinger_5_Pinky',
29
+ 'BothHandsFound',
30
+ 'HandDistance',
31
+ ];
32
+ const HAND_INPUT_SET = new Set(HAND_INPUT_NAMES);
33
+ /** Whether an input uses the canonical hand vocabulary. */
34
+ export function isHandInputName(input) {
35
+ return HAND_INPUT_SET.has(input);
36
+ }
37
+ /** Names nizima uses for VTS's numbered fingers and both-hands detection. */
38
+ export const NIZIMA_HAND_ALIASES = {
39
+ HandLeftFingerThumb: 'HandLeftFinger_1_Thumb',
40
+ HandLeftFingerIndex: 'HandLeftFinger_2_Index',
41
+ HandLeftFingerMiddle: 'HandLeftFinger_3_Middle',
42
+ HandLeftFingerRing: 'HandLeftFinger_4_Ring',
43
+ HandLeftFingerPinky: 'HandLeftFinger_5_Pinky',
44
+ HandRightFingerThumb: 'HandRightFinger_1_Thumb',
45
+ HandRightFingerIndex: 'HandRightFinger_2_Index',
46
+ HandRightFingerMiddle: 'HandRightFinger_3_Middle',
47
+ HandRightFingerRing: 'HandRightFinger_4_Ring',
48
+ HandRightFingerPinky: 'HandRightFinger_5_Pinky',
49
+ HandBothFound: 'BothHandsFound',
50
+ };
51
+ /** Canonicalize a hand name; aliases share canonical units on the plugin API. */
52
+ export function canonicalHandInputName(input) {
53
+ if (isHandInputName(input))
54
+ return input;
55
+ return Object.hasOwn(NIZIMA_HAND_ALIASES, input)
56
+ ? NIZIMA_HAND_ALIASES[input]
57
+ : null;
58
+ }
59
+ /** Angles are degrees; positions and distance use VTS's ten-unit scale. */
60
+ export const HAND_INPUT_RANGES = {
61
+ HandLeftFound: [0, 1],
62
+ HandLeftPositionX: [-10, 10],
63
+ HandLeftPositionY: [-10, 10],
64
+ HandLeftPositionZ: [-10, 10],
65
+ HandLeftAngleX: [-180, 180],
66
+ HandLeftAngleY: [-90, 90],
67
+ HandLeftAngleZ: [-180, 180],
68
+ HandLeftOpen: [0, 1],
69
+ HandLeftFinger_1_Thumb: [0, 1],
70
+ HandLeftFinger_2_Index: [0, 1],
71
+ HandLeftFinger_3_Middle: [0, 1],
72
+ HandLeftFinger_4_Ring: [0, 1],
73
+ HandLeftFinger_5_Pinky: [0, 1],
74
+ HandRightFound: [0, 1],
75
+ HandRightPositionX: [-10, 10],
76
+ HandRightPositionY: [-10, 10],
77
+ HandRightPositionZ: [-10, 10],
78
+ HandRightAngleX: [-180, 180],
79
+ HandRightAngleY: [-90, 90],
80
+ HandRightAngleZ: [-180, 180],
81
+ HandRightOpen: [0, 1],
82
+ HandRightFinger_1_Thumb: [0, 1],
83
+ HandRightFinger_2_Index: [0, 1],
84
+ HandRightFinger_3_Middle: [0, 1],
85
+ HandRightFinger_4_Ring: [0, 1],
86
+ HandRightFinger_5_Pinky: [0, 1],
87
+ BothHandsFound: [0, 1],
88
+ HandDistance: [0, 10],
89
+ };
@@ -1,2 +1,8 @@
1
1
  /** True when the combo carries a command modifier and so may be registered globally. */
2
2
  export declare function hasCommandModifier(accelerator: string | null): boolean;
3
+ /**
4
+ * True when `global` can apply: a command-modifier combo registers with the OS, and a controller
5
+ * combo is observed directly, so neither needs Persona focused. Classifies stored combos, which
6
+ * every writer canonicalizes; it does not validate one.
7
+ */
8
+ export declare function canRunInBackground(accelerator: string | null): boolean;
@@ -1,5 +1,6 @@
1
1
  // Hotkey rules every consumer must agree on: main decides OS registration by them, and a
2
2
  // client reading `Hotkey.global` off the wire needs the same rule to know the flag can apply.
3
+ import { isControllerTriggerName } from "./controller.js";
3
4
  // Shift is deliberately absent: Shift+letter is how text is typed, and a global
4
5
  // registration consumes the combo system-wide (VTS can allow it because it observes).
5
6
  const COMMAND_MODIFIERS = ['Control', 'Alt', 'Super'];
@@ -9,3 +10,13 @@ export function hasCommandModifier(accelerator) {
9
10
  return false;
10
11
  return accelerator.split('+').some(t => COMMAND_MODIFIERS.includes(t));
11
12
  }
13
+ /**
14
+ * True when `global` can apply: a command-modifier combo registers with the OS, and a controller
15
+ * combo is observed directly, so neither needs Persona focused. Classifies stored combos, which
16
+ * every writer canonicalizes; it does not validate one.
17
+ */
18
+ export function canRunInBackground(accelerator) {
19
+ if (accelerator === null)
20
+ return false;
21
+ return hasCommandModifier(accelerator) || accelerator.split('+').every(isControllerTriggerName);
22
+ }
@@ -1,4 +1,6 @@
1
1
  import type { ShortcutInfo } from '../wire/types.ts';
2
+ /** Last path segment, tolerating both `/` and `\` separators. */
3
+ export declare function fileBasename(path: string): string;
2
4
  /**
3
5
  * Display label for a model-relative motion/expression/animation file: the
4
6
  * basename with its format extension stripped. Cubism entries carry no `Name`,
@@ -1,10 +1,14 @@
1
+ /** Last path segment, tolerating both `/` and `\` separators. */
2
+ export function fileBasename(path) {
3
+ return path.split(/[/\\]/).at(-1) ?? path;
4
+ }
1
5
  /**
2
6
  * Display label for a model-relative motion/expression/animation file: the
3
7
  * basename with its format extension stripped. Cubism entries carry no `Name`,
4
8
  * so the basename is the only identifier there is.
5
9
  */
6
10
  export function motionLabel(file) {
7
- const base = file.split(/[\\/]/).pop() ?? file;
11
+ const base = fileBasename(file);
8
12
  return base.replace(/\.motion3\.json$|\.exp3\.json$|\.vrma$|\.json$/i, '') || base;
9
13
  }
10
14
  // English fallbacks; localizing clients keep their own map and use these for unknown kinds.
@@ -2,6 +2,8 @@ import type { AssetRef, Attach, EnvironmentLook, ModelFormat, MToonTuning, Objec
2
2
  export declare function clamp(v: number, min: number, max: number): number;
3
3
  /** Clamp to the unit interval. */
4
4
  export declare function clamp01(v: number): number;
5
+ /** Wrap an angle into (-π, π]. */
6
+ export declare function wrapAngle(a: number): number;
5
7
  export declare const DEFAULT_LIVE2D_PLACEMENT: ScreenPlacement;
6
8
  export declare const DEFAULT_VRM_PLACEMENT: VrmPlacement;
7
9
  /** Scene colors heal to 6-digit hex. */
@@ -142,3 +144,20 @@ export declare const STORAGE_VALUE_MAX_LENGTH: number;
142
144
  export declare const STORAGE_KEYS_MAX = 256;
143
145
  /** `speech.play` URL ceiling — sized for a ~40 s WAV as a base64 `data:audio/*` payload. */
144
146
  export declare const SPEECH_URL_MAX_LENGTH = 8000000;
147
+ /**
148
+ * Narrowest the panel's field column still reads at: the three-up axis fields divide the width,
149
+ * they never wrap. The window's own floor, and the detail column's beside a dragged outliner.
150
+ */
151
+ export declare const CONTROL_MIN_WIDTH = 360;
152
+ /** Narrowest the outliner column drags to: the scene picker and a layer row's affordances still fit. */
153
+ export declare const OUTLINER_MIN_WIDTH = 260;
154
+ /** Width the outliner column opens at before anyone drags it, and the one a double-click restores. */
155
+ export declare const OUTLINER_DEFAULT_WIDTH = 288;
156
+ /**
157
+ * Where the panel splits in two: below it the detail follows the outliner in one scroller, at or
158
+ * above it each column scrolls on its own. `OUTLINER_MIN_WIDTH + CONTROL_MIN_WIDTH` and the 1 px
159
+ * split must fit inside it, or no layout satisfies both floors and the group clips instead.
160
+ */
161
+ export declare const TWO_COLUMN_WIDTH = 672;
162
+ /** 3D-stage resolution multipliers offered by both performance pickers. */
163
+ export declare const RENDER_SCALE_PRESETS: readonly [1, 0.85, 0.75, 0.66, 0.5];
@@ -6,6 +6,16 @@ export function clamp(v, min, max) {
6
6
  export function clamp01(v) {
7
7
  return v < 0 ? 0 : v > 1 ? 1 : v;
8
8
  }
9
+ /** Wrap an angle into (-π, π]. */
10
+ export function wrapAngle(a) {
11
+ const TAU = Math.PI * 2;
12
+ let r = a % TAU;
13
+ if (r > Math.PI)
14
+ r -= TAU;
15
+ if (r <= -Math.PI)
16
+ r += TAU;
17
+ return r;
18
+ }
9
19
  export const DEFAULT_LIVE2D_PLACEMENT = { x: 0, y: 0, scale: 1, rotation: 0 };
10
20
  export const DEFAULT_VRM_PLACEMENT = { x: 0, y: 0, z: 0, rotX: 0, rotY: 0, rotZ: 0, scale: 1 };
11
21
  /** Scene colors heal to 6-digit hex. */
@@ -239,3 +249,20 @@ export const STORAGE_VALUE_MAX_LENGTH = 64 * 1024;
239
249
  export const STORAGE_KEYS_MAX = 256;
240
250
  /** `speech.play` URL ceiling — sized for a ~40 s WAV as a base64 `data:audio/*` payload. */
241
251
  export const SPEECH_URL_MAX_LENGTH = 8_000_000;
252
+ /**
253
+ * Narrowest the panel's field column still reads at: the three-up axis fields divide the width,
254
+ * they never wrap. The window's own floor, and the detail column's beside a dragged outliner.
255
+ */
256
+ export const CONTROL_MIN_WIDTH = 360;
257
+ /** Narrowest the outliner column drags to: the scene picker and a layer row's affordances still fit. */
258
+ export const OUTLINER_MIN_WIDTH = 260;
259
+ /** Width the outliner column opens at before anyone drags it, and the one a double-click restores. */
260
+ export const OUTLINER_DEFAULT_WIDTH = 288;
261
+ /**
262
+ * Where the panel splits in two: below it the detail follows the outliner in one scroller, at or
263
+ * above it each column scrolls on its own. `OUTLINER_MIN_WIDTH + CONTROL_MIN_WIDTH` and the 1 px
264
+ * split must fit inside it, or no layout satisfies both floors and the group clips instead.
265
+ */
266
+ export const TWO_COLUMN_WIDTH = 672;
267
+ /** 3D-stage resolution multipliers offered by both performance pickers. */
268
+ export const RENDER_SCALE_PRESETS = [1, 0.85, 0.75, 0.66, 0.5];
@@ -0,0 +1,106 @@
1
+ export declare const LIP_SYNC_VOWELS: readonly ["A", "I", "U", "E", "O"];
2
+ export type LipSyncVowel = (typeof LIP_SYNC_VOWELS)[number];
3
+ export declare const LIP_SYNC_PHONEMES: readonly ["A", "I", "U", "E", "O", "S"];
4
+ export type LipSyncPhoneme = (typeof LIP_SYNC_PHONEMES)[number];
5
+ export declare const LIP_SYNC_MODES: readonly ["off", "always", "when-untracked"];
6
+ export type LipSyncMode = (typeof LIP_SYNC_MODES)[number];
7
+ /** Public limits, shared by the settings healer, the wire schema, and the panel sliders. */
8
+ export declare const LIP_SYNC_GAIN_MAX = 30;
9
+ export declare const LIP_SYNC_NOISE_GATE_MIN = -60;
10
+ export declare const LIP_SYNC_SMOOTHING_MAX = 0.3;
11
+ /** Local microphone calibration; each sample is a 12-coefficient MFCC vector. */
12
+ export interface LipSyncProfile {
13
+ deviceId: string;
14
+ deviceLabel: string;
15
+ updatedAt: number;
16
+ samples: Record<LipSyncPhoneme, number[][]>;
17
+ }
18
+ /** Calibration steps that carry no phoneme; `record` is the sixth action and takes one. */
19
+ export declare const LIP_SYNC_CALIBRATION_ACTIONS: readonly ["start", "cancel", "preview", "save", "reset"];
20
+ export type LipSyncCalibrationCommand = {
21
+ action: (typeof LIP_SYNC_CALIBRATION_ACTIONS)[number];
22
+ } | {
23
+ action: 'record';
24
+ phoneme: LipSyncPhoneme;
25
+ };
26
+ export interface LipSyncCalibrationState {
27
+ phase: 'ready' | 'recording' | 'verifying';
28
+ phoneme: LipSyncPhoneme | null;
29
+ progress: number;
30
+ completed: LipSyncPhoneme[];
31
+ error: 'too-quiet' | 'insufficient-data' | null;
32
+ }
33
+ /** One desktop microphone, shared by the scene's participating models. */
34
+ export interface LipSyncConfig {
35
+ enabled: boolean;
36
+ /** Empty selects the system's default input. */
37
+ deviceId: string;
38
+ /** Input gain in dB, 0..30. */
39
+ gain: number;
40
+ /** Silence threshold in dBFS, -60..0. */
41
+ noiseGate: number;
42
+ /** Volume and vowel smoothing in seconds, 0..0.3. */
43
+ smoothing: number;
44
+ }
45
+ export declare const DEFAULT_LIP_SYNC_CONFIG: Readonly<LipSyncConfig>;
46
+ /** Analyzer output before model-specific mapping. */
47
+ export interface LipSyncSample {
48
+ /** Mouth-open envelope, 0..1. */
49
+ volume: number;
50
+ /** Smoothed vowel weights sum to at most one; the remainder represents non-vowel audio. */
51
+ vowels: Record<LipSyncVowel, number>;
52
+ }
53
+ export interface LipSyncState {
54
+ config: LipSyncConfig;
55
+ status: 'off' | 'starting' | 'listening' | 'error';
56
+ error: 'permission' | 'device' | 'analysis' | null;
57
+ devices: {
58
+ deviceId: string;
59
+ label: string;
60
+ }[];
61
+ sample: LipSyncSample | null;
62
+ microphone: {
63
+ deviceId: string;
64
+ label: string;
65
+ } | null;
66
+ profile: Omit<LipSyncProfile, 'samples'> | null;
67
+ calibration: LipSyncCalibrationState | null;
68
+ }
69
+ /** Drop incomplete or malformed calibrations rather than feeding them to the analyzer. */
70
+ export declare function healLipSyncProfile(raw: unknown): LipSyncProfile | null;
71
+ /** Heal persisted configuration, retaining supported values within their public limits. */
72
+ export declare function healLipSyncConfig(raw: unknown): LipSyncConfig;
73
+ /** Whether a persisted per-model mode is supported. */
74
+ export declare function isLipSyncMode(value: unknown): value is LipSyncMode;
75
+ export declare const VOICE_INPUT_NAMES: readonly ["VoiceVolume", "VoiceFrequency", "VoiceVolumePlusMouthOpen", "VoiceFrequencyPlusMouthSmile", "VoiceA", "VoiceI", "VoiceU", "VoiceE", "VoiceO", "VoiceSilence", "VoiceMouthOpen", "VoiceMouthSpread"];
76
+ export type VoiceInputName = (typeof VOICE_INPUT_NAMES)[number];
77
+ export declare const VOICE_INPUT_RANGES: {
78
+ readonly VoiceVolume: readonly [0, 1];
79
+ readonly VoiceFrequency: readonly [0, 1];
80
+ readonly VoiceVolumePlusMouthOpen: readonly [0, 2];
81
+ readonly VoiceFrequencyPlusMouthSmile: readonly [0, 1];
82
+ readonly VoiceA: readonly [0, 1];
83
+ readonly VoiceI: readonly [0, 1];
84
+ readonly VoiceU: readonly [0, 1];
85
+ readonly VoiceE: readonly [0, 1];
86
+ readonly VoiceO: readonly [0, 1];
87
+ readonly VoiceSilence: readonly [0, 1];
88
+ readonly VoiceMouthOpen: readonly [0, 1];
89
+ readonly VoiceMouthSpread: readonly [-1, 1];
90
+ };
91
+ /** Semantic import aliases; source analyzer amplitudes need not match Persona's. */
92
+ export declare const NIZIMA_VOICE_ALIASES: {
93
+ readonly LipSyncVolume: "VoiceVolume";
94
+ readonly LipSyncMouthOpen: "VoiceMouthOpen";
95
+ readonly LipSyncMouthSpread: "VoiceMouthSpread";
96
+ readonly LipSyncMouthA: "VoiceA";
97
+ readonly LipSyncMouthI: "VoiceI";
98
+ readonly LipSyncMouthU: "VoiceU";
99
+ readonly LipSyncMouthE: "VoiceE";
100
+ readonly LipSyncMouthO: "VoiceO";
101
+ };
102
+ /**
103
+ * VTS-compatible binding inputs. Frequency projects vowel weights onto its conventional
104
+ * mouth-form axis; composite sums retain over-range values for the authored binding to clamp.
105
+ */
106
+ export declare function voiceInputs(sample: LipSyncSample, faceInputs?: Readonly<Partial<Record<'MouthOpen' | 'MouthSmile', number>>>): Record<VoiceInputName, number>;
@@ -0,0 +1,120 @@
1
+ import { finiteOr, isFiniteNumber, isRecord, nonEmptyString } from "./guards.js";
2
+ import { clamp } from "./limits.js";
3
+ export const LIP_SYNC_VOWELS = ['A', 'I', 'U', 'E', 'O'];
4
+ export const LIP_SYNC_PHONEMES = [...LIP_SYNC_VOWELS, 'S'];
5
+ export const LIP_SYNC_MODES = ['off', 'always', 'when-untracked'];
6
+ /** Public limits, shared by the settings healer, the wire schema, and the panel sliders. */
7
+ export const LIP_SYNC_GAIN_MAX = 30;
8
+ export const LIP_SYNC_NOISE_GATE_MIN = -60;
9
+ export const LIP_SYNC_SMOOTHING_MAX = 0.3;
10
+ /** Calibration steps that carry no phoneme; `record` is the sixth action and takes one. */
11
+ export const LIP_SYNC_CALIBRATION_ACTIONS = ['start', 'cancel', 'preview', 'save', 'reset'];
12
+ export const DEFAULT_LIP_SYNC_CONFIG = {
13
+ enabled: false,
14
+ deviceId: '',
15
+ gain: 0,
16
+ noiseGate: -45,
17
+ smoothing: 0.08,
18
+ };
19
+ /** Drop incomplete or malformed calibrations rather than feeding them to the analyzer. */
20
+ export function healLipSyncProfile(raw) {
21
+ if (!isRecord(raw))
22
+ return null;
23
+ const deviceId = nonEmptyString(raw.deviceId);
24
+ if (deviceId === null ||
25
+ typeof raw.deviceLabel !== 'string' ||
26
+ !isFiniteNumber(raw.updatedAt) ||
27
+ raw.updatedAt < 0 ||
28
+ !isRecord(raw.samples)) {
29
+ return null;
30
+ }
31
+ const samples = { A: [], I: [], U: [], E: [], O: [], S: [] };
32
+ for (const phoneme of LIP_SYNC_PHONEMES) {
33
+ const vectors = raw.samples[phoneme];
34
+ if (!Array.isArray(vectors) || vectors.length < 12 || vectors.length > 40)
35
+ return null;
36
+ for (const vector of vectors) {
37
+ if (!Array.isArray(vector) || vector.length !== 12 || !vector.every(isFiniteNumber))
38
+ return null;
39
+ samples[phoneme].push([...vector]);
40
+ }
41
+ }
42
+ return { deviceId, deviceLabel: raw.deviceLabel, updatedAt: raw.updatedAt, samples };
43
+ }
44
+ /** Heal persisted configuration, retaining supported values within their public limits. */
45
+ export function healLipSyncConfig(raw) {
46
+ const value = isRecord(raw) ? raw : {};
47
+ return {
48
+ enabled: typeof value.enabled === 'boolean' ? value.enabled : DEFAULT_LIP_SYNC_CONFIG.enabled,
49
+ deviceId: typeof value.deviceId === 'string' ? value.deviceId : DEFAULT_LIP_SYNC_CONFIG.deviceId,
50
+ gain: clamp(finiteOr(value.gain, DEFAULT_LIP_SYNC_CONFIG.gain), 0, LIP_SYNC_GAIN_MAX),
51
+ noiseGate: clamp(finiteOr(value.noiseGate, DEFAULT_LIP_SYNC_CONFIG.noiseGate), LIP_SYNC_NOISE_GATE_MIN, 0),
52
+ smoothing: clamp(finiteOr(value.smoothing, DEFAULT_LIP_SYNC_CONFIG.smoothing), 0, LIP_SYNC_SMOOTHING_MAX),
53
+ };
54
+ }
55
+ /** Whether a persisted per-model mode is supported. */
56
+ export function isLipSyncMode(value) {
57
+ return LIP_SYNC_MODES.some(mode => mode === value);
58
+ }
59
+ export const VOICE_INPUT_NAMES = [
60
+ 'VoiceVolume',
61
+ 'VoiceFrequency',
62
+ 'VoiceVolumePlusMouthOpen',
63
+ 'VoiceFrequencyPlusMouthSmile',
64
+ 'VoiceA',
65
+ 'VoiceI',
66
+ 'VoiceU',
67
+ 'VoiceE',
68
+ 'VoiceO',
69
+ 'VoiceSilence',
70
+ 'VoiceMouthOpen',
71
+ 'VoiceMouthSpread',
72
+ ];
73
+ export const VOICE_INPUT_RANGES = {
74
+ VoiceVolume: [0, 1],
75
+ VoiceFrequency: [0, 1],
76
+ VoiceVolumePlusMouthOpen: [0, 2],
77
+ VoiceFrequencyPlusMouthSmile: [0, 1],
78
+ VoiceA: [0, 1],
79
+ VoiceI: [0, 1],
80
+ VoiceU: [0, 1],
81
+ VoiceE: [0, 1],
82
+ VoiceO: [0, 1],
83
+ VoiceSilence: [0, 1],
84
+ VoiceMouthOpen: [0, 1],
85
+ VoiceMouthSpread: [-1, 1],
86
+ };
87
+ /** Semantic import aliases; source analyzer amplitudes need not match Persona's. */
88
+ export const NIZIMA_VOICE_ALIASES = {
89
+ LipSyncVolume: 'VoiceVolume',
90
+ LipSyncMouthOpen: 'VoiceMouthOpen',
91
+ LipSyncMouthSpread: 'VoiceMouthSpread',
92
+ LipSyncMouthA: 'VoiceA',
93
+ LipSyncMouthI: 'VoiceI',
94
+ LipSyncMouthU: 'VoiceU',
95
+ LipSyncMouthE: 'VoiceE',
96
+ LipSyncMouthO: 'VoiceO',
97
+ };
98
+ /**
99
+ * VTS-compatible binding inputs. Frequency projects vowel weights onto its conventional
100
+ * mouth-form axis; composite sums retain over-range values for the authored binding to clamp.
101
+ */
102
+ export function voiceInputs(sample, faceInputs) {
103
+ const { volume, vowels } = sample;
104
+ // Vowel projection, not measured pitch: https://github.com/DenchiSoft/VTubeStudio/wiki/Lipsync
105
+ const frequency = 0.5 + (volume * (-0.9 * vowels.A - 0.7 * vowels.I - 0.6 * vowels.U + vowels.E + 0.9 * vowels.O)) / 2;
106
+ return {
107
+ VoiceVolume: volume,
108
+ VoiceFrequency: frequency,
109
+ VoiceVolumePlusMouthOpen: volume + (faceInputs?.MouthOpen ?? 0),
110
+ VoiceFrequencyPlusMouthSmile: faceInputs?.MouthSmile === undefined ? frequency : faceInputs.MouthSmile + (frequency - 0.5) * 2,
111
+ VoiceA: volume * vowels.A,
112
+ VoiceI: volume * vowels.I,
113
+ VoiceU: volume * vowels.U,
114
+ VoiceE: volume * vowels.E,
115
+ VoiceO: volume * vowels.O,
116
+ VoiceSilence: 1 - volume,
117
+ VoiceMouthOpen: volume * (vowels.A + 0.4 * vowels.I + 0.4 * vowels.U + 0.7 * vowels.E + vowels.O),
118
+ VoiceMouthSpread: volume * (vowels.A + vowels.I - vowels.U + vowels.E - vowels.O),
119
+ };
120
+ }
@@ -3,6 +3,8 @@ export declare const LOCALES: readonly ["en", "ja", "zh-CN", "zh-TW"];
3
3
  export type Locale = (typeof LOCALES)[number];
4
4
  /** `ui.language` setting: an explicit locale, or follow the OS language. */
5
5
  export type LanguageSetting = 'system' | Locale;
6
+ /** `ui.theme` setting: an explicit appearance, or follow the OS. */
7
+ export type ThemeSetting = 'system' | 'light' | 'dark';
6
8
  /** Native-language display names for the language picker (deliberately untranslated). */
7
9
  export declare const LOCALE_LABELS: Record<Locale, string>;
8
10
  /** Best supported locale for a BCP 47-ish tag: exact match first, then fuzzy per language. */
@@ -0,0 +1,66 @@
1
+ import type { ContentOrigin } from '../wire/types.ts';
2
+ /** Which VRM specification the file declares. */
3
+ export type VrmSpec = '0.x' | '1.0';
4
+ /** Everything the panel reports about a Live2D model on stage. */
5
+ export interface Live2DModelInfo {
6
+ format: 'live2d';
7
+ name: string;
8
+ origin: ContentOrigin;
9
+ file: string;
10
+ /** Config sidecars found next to the entry file (`*.vtube.json`, `*.persona.json`). */
11
+ configFiles: string[];
12
+ canvas: {
13
+ width: number;
14
+ height: number;
15
+ };
16
+ /** `csmMocVersion` of the model's own moc3, or null when the header couldn't be read. */
17
+ mocVersion: number | null;
18
+ /** Highest `csmMocVersion` the vendored Core can load (`csmGetLatestMocVersion()`). */
19
+ coreMocVersion: number;
20
+ /** Packed uint32 from `csmGetVersion()`; 0 when the Core global is missing. */
21
+ coreVersion: number;
22
+ params: number;
23
+ parts: number;
24
+ /** Pixel size of each loaded texture atlas. */
25
+ textureSizes: {
26
+ width: number;
27
+ height: number;
28
+ }[];
29
+ motionGroups: number;
30
+ motionTotal: number;
31
+ expressions: number;
32
+ /** Param ids the auto eye-blink drives (the engine falls back to standard params when undeclared). */
33
+ eyeBlinkParams: string[];
34
+ /** Param ids of the model's `LipSync` group; empty means lip sync cannot move the mouth. */
35
+ lipSyncParams: string[];
36
+ /** How many distinct raw ARKit channels the rig's bindings read; 52 is full perfect sync. */
37
+ arkitInputs: number;
38
+ /** Optional subsystems that loaded, e.g. `['physics', 'pose']`. */
39
+ extras: string[];
40
+ /** Wall-clock ms the stage spent loading the model. */
41
+ loadMs: number;
42
+ }
43
+ /** What the panel reports about a VRM on stage. */
44
+ export interface VrmModelInfo {
45
+ format: 'vrm';
46
+ name: string;
47
+ origin: ContentOrigin;
48
+ file: string;
49
+ spec: VrmSpec;
50
+ specVersion: string;
51
+ /** Every glTF extension the file declares, in file order; the rows split them by stage support. */
52
+ extensions: string[];
53
+ humanBones: number;
54
+ springGroups: number;
55
+ expressions: number;
56
+ /** How many of the 52 ARKit shapes the expressions cover; 52 enables perfect-sync tracking. */
57
+ arkitExpressions: number;
58
+ authors: string[];
59
+ licenseUrl: string | null;
60
+ /** VRM 1.0 `creditNotation === 'required'`; always false for 0.x. */
61
+ creditRequired: boolean;
62
+ /** Wall-clock ms the stage spent loading the model. */
63
+ loadMs: number;
64
+ }
65
+ /** Everything the panel reports about the model currently on stage. */
66
+ export type ModelInfo = Live2DModelInfo | VrmModelInfo;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ /** Decoded image/video pixels, or a web object's configured viewport pixels, before placement. */
2
+ export interface ObjectMediaSize {
3
+ width: number;
4
+ height: number;
5
+ }
6
+ /** One light baked into a prop asset, as the panel lists it. `index` is the override key. */
7
+ export interface BakedLightInfo {
8
+ index: number;
9
+ /** Node name from the asset; may be empty. */
10
+ name: string;
11
+ type: 'point' | 'spot' | 'directional';
12
+ /** The asset's own intensity after normalization, in scene-slider units. */
13
+ defaultIntensity: number;
14
+ }
15
+ /** Loaded asset metadata; absent dimensions/extensions mean the asset has not reported them yet. */
16
+ export interface SceneAssetInfo {
17
+ assetId: string | null;
18
+ lights: BakedLightInfo[];
19
+ extensions: string[] | null;
20
+ mediaSize: ObjectMediaSize | null;
21
+ }
22
+ /** Metadata for the active scene, tagged so a response from a previous scene can be discarded. */
23
+ export interface SceneInspection {
24
+ sceneId: string;
25
+ environment: Omit<SceneAssetInfo, 'mediaSize'>;
26
+ objects: Record<string, SceneAssetInfo>;
27
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ /** Where a VRM binding target lands; expressions are the model's own, the rest are pose offsets. */
2
+ export type VrmBindingGroup = 'head' | 'body' | 'gaze' | 'expressions';
3
+ export interface VrmPoseTarget {
4
+ id: string;
5
+ group: Exclude<VrmBindingGroup, 'expressions'>;
6
+ /** Symmetric range in degrees. */
7
+ range: number;
8
+ }
9
+ /** The generated VRM binding targets, in degrees; each is an offset layered over the current pose. */
10
+ export declare const VRM_POSE_TARGETS: readonly VrmPoseTarget[];
11
+ /** The binding target id for a VRM expression, keeping the model author's spelling. */
12
+ export declare function vrmExpressionTarget(name: string): string;
13
+ /** The expression behind a VRM binding target id; null for a pose target. */
14
+ export declare function vrmExpressionName(id: string): string | null;
@@ -0,0 +1,20 @@
1
+ /** The generated VRM binding targets, in degrees; each is an offset layered over the current pose. */
2
+ export const VRM_POSE_TARGETS = [
3
+ { id: 'HeadYaw', group: 'head', range: 30 },
4
+ { id: 'HeadPitch', group: 'head', range: 30 },
5
+ { id: 'HeadRoll', group: 'head', range: 30 },
6
+ { id: 'BodyYaw', group: 'body', range: 15 },
7
+ { id: 'BodyPitch', group: 'body', range: 15 },
8
+ { id: 'BodyRoll', group: 'body', range: 15 },
9
+ { id: 'GazeYaw', group: 'gaze', range: 90 },
10
+ { id: 'GazePitch', group: 'gaze', range: 90 },
11
+ ];
12
+ const VRM_EXPRESSION_PREFIX = 'Expression:';
13
+ /** The binding target id for a VRM expression, keeping the model author's spelling. */
14
+ export function vrmExpressionTarget(name) {
15
+ return `${VRM_EXPRESSION_PREFIX}${name}`;
16
+ }
17
+ /** The expression behind a VRM binding target id; null for a pose target. */
18
+ export function vrmExpressionName(id) {
19
+ return id.startsWith(VRM_EXPRESSION_PREFIX) ? id.slice(VRM_EXPRESSION_PREFIX.length) : null;
20
+ }
@@ -14,7 +14,7 @@ export interface EventMap {
14
14
  'shortcut.state': {
15
15
  shortcuts: ShortcutInfo[];
16
16
  };
17
- /** `sources` maps source id → status; optional so pre-multi-source servers still parse. */
17
+ /** Network channel aggregates; `sources` includes webcam status and is optional for older servers. */
18
18
  'tracking.status': {
19
19
  tracking: TrackingStatus;
20
20
  pose: PoseStatus;