@hmcs/sdk 1.0.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/vrm.js ADDED
@@ -0,0 +1,466 @@
1
+ import { host } from './host.js';
2
+ import { EventSource } from 'eventsource';
3
+ import { entities } from './entities.js';
4
+
5
+ /**
6
+ * Helper functions for building {@link VrmaRepeat} values.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * await vrm.playVrma({
11
+ * asset: "vrma:idle-maid",
12
+ * repeat: repeat.forever(),
13
+ * });
14
+ *
15
+ * await vrm.playVrma({
16
+ * asset: "vrma:grabbed",
17
+ * repeat: repeat.count(3),
18
+ * });
19
+ * ```
20
+ */
21
+ var repeat;
22
+ (function (repeat) {
23
+ /**
24
+ * Repeat the animation forever.
25
+ */
26
+ function forever() {
27
+ return { type: "forever" };
28
+ }
29
+ repeat.forever = forever;
30
+ /**
31
+ * Play the animation once (no repeat).
32
+ */
33
+ function never() {
34
+ return { type: "never" };
35
+ }
36
+ repeat.never = never;
37
+ /**
38
+ * Repeat the animation a fixed number of times.
39
+ *
40
+ * @param n Positive integer repeat count.
41
+ * @throws {RangeError} If `n` is not a positive integer.
42
+ */
43
+ function count(n) {
44
+ if (!Number.isInteger(n) || !Number.isFinite(n) || n <= 0) {
45
+ throw new RangeError("repeat.count(n) requires a positive integer");
46
+ }
47
+ return { type: "count", count: n };
48
+ }
49
+ repeat.count = count;
50
+ })(repeat || (repeat = {}));
51
+ class VrmEventSource {
52
+ eventSource;
53
+ constructor(eventSource) {
54
+ this.eventSource = eventSource;
55
+ }
56
+ /**
57
+ * Registers an event listener for the specified event type.
58
+ */
59
+ on(event, callback) {
60
+ this.eventSource.addEventListener(event, e => {
61
+ callback(JSON.parse(e.data));
62
+ });
63
+ }
64
+ /**
65
+ * Closes the EventSource connection.
66
+ */
67
+ close() {
68
+ this.eventSource.close();
69
+ }
70
+ [Symbol.dispose]() {
71
+ this.eventSource.close();
72
+ }
73
+ }
74
+ class Vrm {
75
+ entity;
76
+ constructor(entity) {
77
+ this.entity = entity;
78
+ }
79
+ /**
80
+ * Returns an EventSource for receiving events related to this VRM entity.
81
+ */
82
+ events() {
83
+ const url = host.createUrl(`vrm/${this.entity}/events`);
84
+ return new VrmEventSource(new EventSource(url));
85
+ }
86
+ /**
87
+ * Returns the current state of the VRM.
88
+ */
89
+ async state() {
90
+ const response = await this.fetch("state");
91
+ const json = await response.json();
92
+ return json.state;
93
+ }
94
+ /**
95
+ * Sets the state of the VRM.
96
+ *
97
+ * @param state The new state to set.
98
+ */
99
+ async setState(state) {
100
+ await this.put("state", { state });
101
+ }
102
+ /**
103
+ * Returns the persona of the VRM.
104
+ *
105
+ * @example
106
+ * ```typescript
107
+ * const vrm = await Vrm.findByName("MyAvatar");
108
+ * const persona = await vrm.persona();
109
+ * console.log(persona.profile);
110
+ * ```
111
+ */
112
+ async persona() {
113
+ const response = await this.fetch("persona");
114
+ return await response.json();
115
+ }
116
+ /**
117
+ * Sets the persona of the VRM.
118
+ *
119
+ * @param persona The persona data to set.
120
+ *
121
+ * @example
122
+ * ```typescript
123
+ * const vrm = await Vrm.findByName("MyAvatar");
124
+ * await vrm.setPersona({
125
+ * profile: "A cheerful assistant",
126
+ * ocean: { openness: 0.8, extraversion: 0.7 },
127
+ * metadata: {},
128
+ * });
129
+ * ```
130
+ */
131
+ async setPersona(persona) {
132
+ await this.put("persona", persona);
133
+ }
134
+ /**
135
+ * Returns the name of the VRM avatar.
136
+ */
137
+ async name() {
138
+ return await entities.name(this.entity);
139
+ }
140
+ /**
141
+ * Finds the entity ID of a bone by its name.
142
+ */
143
+ async findBoneEntity(bone) {
144
+ const response = await host.get(host.createUrl(`vrm/${this.entity}/bone/${bone}`));
145
+ return Number(await response.json());
146
+ }
147
+ /**
148
+ * Despawns this VRM entity.
149
+ */
150
+ async despawn() {
151
+ await host.deleteMethod(host.createUrl(`vrm/${this.entity}`));
152
+ }
153
+ /**
154
+ * Gets the current position of this VRM in both screen and world coordinates.
155
+ *
156
+ * @example
157
+ * ```ts
158
+ * const vrm = await Vrm.findByName("MyCharacter");
159
+ * const pos = await vrm.position();
160
+ * console.log(`Screen: (${pos.globalViewport?.[0]}, ${pos.globalViewport?.[1]})`);
161
+ * console.log(`World: (${pos.world[0]}, ${pos.world[1]}, ${pos.world[2]})`);
162
+ * ```
163
+ */
164
+ async position() {
165
+ const response = await this.fetch("position");
166
+ return await response.json();
167
+ }
168
+ /**
169
+ * Gets all expressions and their current weights, including metadata
170
+ * such as binary status and override settings.
171
+ *
172
+ * @example
173
+ * ```typescript
174
+ * const vrm = await Vrm.findByName("MyAvatar");
175
+ * const { expressions } = await vrm.expressions();
176
+ * for (const expr of expressions) {
177
+ * console.log(`${expr.name}: ${expr.weight}`);
178
+ * }
179
+ * ```
180
+ */
181
+ async expressions() {
182
+ const response = await this.fetch("expressions");
183
+ return await response.json();
184
+ }
185
+ /**
186
+ * Sets expression weights, replacing all previous overrides.
187
+ * Expressions not included will return to VRMA animation control.
188
+ *
189
+ * @param weights A record of expression names to weight values (0.0-1.0).
190
+ *
191
+ * @example
192
+ * ```typescript
193
+ * const vrm = await Vrm.findByName("MyAvatar");
194
+ * await vrm.setExpressions({ happy: 1.0, blink: 0.5 });
195
+ * ```
196
+ */
197
+ async setExpressions(weights) {
198
+ await this.put("expressions", { weights });
199
+ }
200
+ /**
201
+ * Modifies specific expression weights without affecting others (partial update).
202
+ * Existing overrides not mentioned remain unchanged.
203
+ *
204
+ * @param weights A record of expression names to weight values (0.0-1.0).
205
+ *
206
+ * @example
207
+ * ```typescript
208
+ * const vrm = await Vrm.findByName("MyAvatar");
209
+ * // Only modifies "happy", leaves other overrides intact
210
+ * await vrm.modifyExpressions({ happy: 1.0 });
211
+ * ```
212
+ */
213
+ async modifyExpressions(weights) {
214
+ await this.patch("expressions", { weights });
215
+ }
216
+ /**
217
+ * Clears all expression overrides, returning control to VRMA animation.
218
+ *
219
+ * @example
220
+ * ```typescript
221
+ * const vrm = await Vrm.findByName("MyAvatar");
222
+ * await vrm.clearExpressions();
223
+ * ```
224
+ */
225
+ async clearExpressions() {
226
+ await this.delete("expressions");
227
+ }
228
+ /**
229
+ * Modifies mouth expression weights for lip-sync.
230
+ * Unspecified mouth expressions are reset to 0.0.
231
+ * Non-mouth expression overrides are preserved.
232
+ *
233
+ * @param weights A record of mouth expression names to weight values (0.0-1.0).
234
+ *
235
+ * @example
236
+ * ```typescript
237
+ * const vrm = await Vrm.findByName("MyAvatar");
238
+ * await vrm.modifyMouth({ aa: 0.8, oh: 0.2 });
239
+ * ```
240
+ */
241
+ async modifyMouth(weights) {
242
+ await this.patch("expressions/mouth", { weights });
243
+ }
244
+ /**
245
+ * Gets all spring bone chains.
246
+ */
247
+ async springBones() {
248
+ const response = await this.fetch("spring-bones");
249
+ return await response.json();
250
+ }
251
+ /**
252
+ * Gets a single spring bone chain by entity ID.
253
+ *
254
+ * @param chainId The chain entity ID.
255
+ */
256
+ async springBone(chainId) {
257
+ const response = await host.get(host.createUrl(`vrm/${this.entity}/spring-bones/${chainId}`));
258
+ return await response.json();
259
+ }
260
+ /**
261
+ * Updates spring bone properties for a chain.
262
+ *
263
+ * @param chainId The chain entity ID.
264
+ * @param props Partial properties to update.
265
+ */
266
+ async setSpringBone(chainId, props) {
267
+ await host.put(host.createUrl(`vrm/${this.entity}/spring-bones/${chainId}`), props);
268
+ }
269
+ /**
270
+ * Gets all VRMA animations for this VRM.
271
+ */
272
+ async listVrma() {
273
+ const response = await host.get(host.createUrl(`vrm/${this.entity}/vrma`));
274
+ return await response.json();
275
+ }
276
+ /**
277
+ * Plays a VRMA animation.
278
+ *
279
+ * @param options Play request options including asset, repeat, transition.
280
+ */
281
+ async playVrma(options) {
282
+ await this.post("vrma/play", options);
283
+ }
284
+ /**
285
+ * Stops a VRMA animation.
286
+ *
287
+ * @param asset The asset ID of the VRMA animation to stop.
288
+ */
289
+ async stopVrma(asset) {
290
+ await this.post("vrma/stop", { asset });
291
+ }
292
+ /**
293
+ * Gets the state of a VRMA animation.
294
+ *
295
+ * @param asset The asset ID of the VRMA animation to query.
296
+ */
297
+ async vrmaState(asset) {
298
+ const response = await host.get(host.createUrl(`vrm/${this.entity}/vrma/state`, { asset }));
299
+ return await response.json();
300
+ }
301
+ /**
302
+ * Sets the playback speed of a VRMA animation.
303
+ *
304
+ * @param asset The asset ID of the VRMA animation.
305
+ * @param speed The playback speed.
306
+ */
307
+ async setVrmaSpeed(asset, speed) {
308
+ await host.put(host.createUrl(`vrm/${this.entity}/vrma/speed`), { asset, speed });
309
+ }
310
+ /**
311
+ * Speaks using pre-generated audio with a timeline of expression keyframes.
312
+ * This allows any TTS engine to be used — the engine receives WAV audio and
313
+ * frame-synchronized lip-sync data.
314
+ *
315
+ * @param audio - WAV audio data as ArrayBuffer or Uint8Array.
316
+ * @param keyframes - Timeline keyframes specifying expression targets and durations.
317
+ * @param options - Optional settings (e.g. waitForCompletion).
318
+ *
319
+ * @example
320
+ * ```typescript
321
+ * const vrm = await Vrm.findByName("MyAvatar");
322
+ * const wavData = await fetchWavFromTTS("Hello world");
323
+ * await vrm.speakWithTimeline(wavData, [
324
+ * { duration: 0.1, targets: { aa: 1.0 } },
325
+ * { duration: 0.05 },
326
+ * { duration: 0.12, targets: { oh: 1.0, happy: 0.5 } },
327
+ * ]);
328
+ * ```
329
+ */
330
+ async speakWithTimeline(audio, keyframes, options) {
331
+ const bytes = audio instanceof Uint8Array ? audio : new Uint8Array(audio);
332
+ let binary = '';
333
+ for (let i = 0; i < bytes.length; i++) {
334
+ binary += String.fromCharCode(bytes[i]);
335
+ }
336
+ const base64Audio = btoa(binary);
337
+ await this.post("speech/timeline", {
338
+ audio: base64Audio,
339
+ keyframes,
340
+ ...options,
341
+ });
342
+ }
343
+ /**
344
+ * Looks at the mouse cursor.
345
+ */
346
+ async lookAtCursor() {
347
+ await this.put("look/cursor");
348
+ }
349
+ /**
350
+ * Sets the VRM's look-at target to a specific entity.
351
+ *
352
+ * @param target The entity ID to look at.
353
+ */
354
+ async lookAtTarget(target) {
355
+ await this.put(`look/target/${target}`);
356
+ }
357
+ /**
358
+ * Disables the VRM's look-at functionality.
359
+ */
360
+ async unlook() {
361
+ await this.delete("look");
362
+ }
363
+ /**
364
+ * Spawns a new VRM instance from the given mod asset ID.
365
+ */
366
+ static async spawn(asset, options) {
367
+ const response = await host.post(host.createUrl("vrm"), {
368
+ asset,
369
+ transform: options?.transform,
370
+ persona: options?.persona,
371
+ });
372
+ return new Vrm(Number(await response.text()));
373
+ }
374
+ /**
375
+ * Finds a VRM instance by its name.
376
+ *
377
+ * @param vrmName VRM avatar name
378
+ */
379
+ static async findByName(vrmName) {
380
+ const response = await host.get(host.createUrl("vrm", { name: vrmName }));
381
+ const entities = await response.json();
382
+ if (entities.length === 0) {
383
+ throw new Error(`VRM not found: ${vrmName}`);
384
+ }
385
+ return new Vrm(entities[0]);
386
+ }
387
+ /**
388
+ * Waits for a VRM instance to be spawned and initialized by its name.
389
+ *
390
+ * @param vrmName VRM avatar name
391
+ */
392
+ static async waitLoadByName(vrmName) {
393
+ const response = await host.get(host.createUrl("vrm/wait-load", {
394
+ name: vrmName,
395
+ }));
396
+ return new Vrm(Number(await response.json()));
397
+ }
398
+ /**
399
+ * Returns entity IDs of all currently loaded VRM instances.
400
+ *
401
+ * @example
402
+ * ```typescript
403
+ * const entities = await Vrm.findAllEntities();
404
+ * console.log(`Found ${entities.length} VRM entities`);
405
+ * ```
406
+ */
407
+ static async findAllEntities() {
408
+ const response = await host.get(host.createUrl("vrm"));
409
+ return await response.json();
410
+ }
411
+ /**
412
+ * Returns detailed snapshot of all VRM instances.
413
+ *
414
+ * @example
415
+ * ```typescript
416
+ * const snapshots = await Vrm.findAllDetailed();
417
+ * for (const s of snapshots) {
418
+ * console.log(`${s.name}: ${s.state} at (${s.globalViewport?.[0]}, ${s.globalViewport?.[1]})`);
419
+ * }
420
+ * ```
421
+ */
422
+ static async findAllDetailed() {
423
+ const response = await host.get(host.createUrl("vrm/snapshot"));
424
+ return await response.json();
425
+ }
426
+ static streamMetadata(f) {
427
+ const es = new EventSource(host.createUrl("vrm/stream"));
428
+ es.addEventListener("message", (e) => {
429
+ f(JSON.parse(e.data));
430
+ });
431
+ return es;
432
+ }
433
+ /**
434
+ * Streams all currently existing VRM instances and any VRM instances that will be created in the future.
435
+ * @param f
436
+ */
437
+ static stream(f) {
438
+ return Vrm.streamMetadata(metadata => {
439
+ f(new Vrm(metadata.entity));
440
+ });
441
+ }
442
+ /**
443
+ * Returns all VRM instances that are currently loaded.
444
+ */
445
+ static async findAll() {
446
+ const entities = await Vrm.findAllEntities();
447
+ return entities.map(entity => new Vrm(entity));
448
+ }
449
+ async fetch(path) {
450
+ return await host.get(host.createUrl(`vrm/${this.entity}/${path}`));
451
+ }
452
+ async post(path, body) {
453
+ return await host.post(host.createUrl(`vrm/${this.entity}/${path}`), body);
454
+ }
455
+ async put(path, body) {
456
+ await host.put(host.createUrl(`vrm/${this.entity}/${path}`), body);
457
+ }
458
+ async patch(path, body) {
459
+ await host.patch(host.createUrl(`vrm/${this.entity}/${path}`), body);
460
+ }
461
+ async delete(path) {
462
+ await host.deleteMethod(host.createUrl(`vrm/${this.entity}/${path}`));
463
+ }
464
+ }
465
+
466
+ export { Vrm, VrmEventSource, repeat };