@spatialwalk/avatarkit 1.0.0-beta.76 → 1.0.0-beta.78
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/CHANGELOG.md +14 -0
- package/dist/{StreamingAudioPlayer-CuNtk6eL.js → StreamingAudioPlayer-BNj8LpDw.js} +1 -1
- package/dist/core/AvatarManager.d.ts +12 -3
- package/dist/core/AvatarView.d.ts +33 -0
- package/dist/{index-ZN-iK3b8.js → index-DEJMvfST.js} +478 -190
- package/dist/index.js +1 -1
- package/dist/internal/constants.d.ts +102 -0
- package/dist/internal/index.d.ts +7 -0
- package/dist/types/index.d.ts +1 -2
- package/dist/utils/animation-interpolation.d.ts +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.0.0-beta.78] - 2026-02-04
|
|
9
|
+
|
|
10
|
+
### 🐛 Bugfixes
|
|
11
|
+
- **Telemetry Accuracy** - Fixed environment field reporting issue in analytics events
|
|
12
|
+
|
|
13
|
+
### ✨ New Features
|
|
14
|
+
- **Performance Monitoring** - Added loading performance metrics for avatar resources
|
|
15
|
+
- `fetch_avatar_latency` - Total loading time
|
|
16
|
+
- `fetch_avatar_metadata_latency` - Metadata fetch time
|
|
17
|
+
- `download_avatar_assets_latency` - Asset download time
|
|
18
|
+
|
|
19
|
+
### 🔧 Improvements
|
|
20
|
+
- **Internal Architecture** - Unified configuration constants across platforms for better maintainability
|
|
21
|
+
|
|
8
22
|
## [1.0.0-beta.74] - 2026-01-22
|
|
9
23
|
|
|
10
24
|
### 🔧 Improvements
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
2
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
-
import { A as APP_CONFIG, l as logger, e as errorToMessage, a as logEvent } from "./index-
|
|
4
|
+
import { A as APP_CONFIG, l as logger, e as errorToMessage, a as logEvent } from "./index-DEJMvfST.js";
|
|
5
5
|
class StreamingAudioPlayer {
|
|
6
6
|
// Mark if AudioContext is being resumed, avoid concurrent resume requests
|
|
7
7
|
constructor(options) {
|
|
@@ -4,9 +4,12 @@ export declare class AvatarManager {
|
|
|
4
4
|
private static _instance;
|
|
5
5
|
private avatarDownloader;
|
|
6
6
|
private avatarCache;
|
|
7
|
-
|
|
7
|
+
/** 下载队列:FIFO 顺序 */
|
|
8
8
|
private downloadQueue;
|
|
9
|
-
|
|
9
|
+
/** 当前正在执行的任务 */
|
|
10
|
+
private currentTask;
|
|
11
|
+
/** 任务索引:快速查找 id 对应的任务 */
|
|
12
|
+
private taskIndex;
|
|
10
13
|
/**
|
|
11
14
|
* Access via global singleton
|
|
12
15
|
*/
|
|
@@ -18,6 +21,12 @@ export declare class AvatarManager {
|
|
|
18
21
|
* @returns Promise<Avatar>
|
|
19
22
|
*/
|
|
20
23
|
load(id: string, onProgress?: (progress: LoadProgressInfo) => void): Promise<Avatar>;
|
|
24
|
+
/**
|
|
25
|
+
* Cancel a pending or running download task
|
|
26
|
+
* @param id Avatar ID to cancel
|
|
27
|
+
* @returns true if task was found and cancelled
|
|
28
|
+
*/
|
|
29
|
+
cancelLoad(id: string): boolean;
|
|
21
30
|
/**
|
|
22
31
|
* Get cached avatar
|
|
23
32
|
* @param id Avatar ID
|
|
@@ -30,7 +39,7 @@ export declare class AvatarManager {
|
|
|
30
39
|
*/
|
|
31
40
|
clear(id: string): void;
|
|
32
41
|
/**
|
|
33
|
-
* Clear all avatar cache and
|
|
42
|
+
* Clear all avatar cache and cancel all tasks
|
|
34
43
|
*/
|
|
35
44
|
clearAll(): void;
|
|
36
45
|
}
|
|
@@ -30,6 +30,7 @@ export declare class AvatarView {
|
|
|
30
30
|
private characterHandle;
|
|
31
31
|
private characterId;
|
|
32
32
|
private isPureRenderingMode;
|
|
33
|
+
private _renderingEnabled;
|
|
33
34
|
private avatarActiveTimer;
|
|
34
35
|
private readonly AVATAR_ACTIVE_INTERVAL;
|
|
35
36
|
/**
|
|
@@ -61,6 +62,38 @@ export declare class AvatarView {
|
|
|
61
62
|
frameCount: number;
|
|
62
63
|
useLinear?: boolean;
|
|
63
64
|
}): Promise<KeyframeData[]>;
|
|
65
|
+
/**
|
|
66
|
+
* Pause rendering loop
|
|
67
|
+
*
|
|
68
|
+
* When called:
|
|
69
|
+
* - Rendering loop stops (no GPU/canvas updates)
|
|
70
|
+
* - Audio playback continues normally
|
|
71
|
+
* - Animation state machine continues running
|
|
72
|
+
*
|
|
73
|
+
* Use `resumeRendering()` to resume rendering.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* // Stop rendering to save GPU resources (audio continues)
|
|
77
|
+
* avatarView.pauseRendering()
|
|
78
|
+
*/
|
|
79
|
+
pauseRendering(): void;
|
|
80
|
+
/**
|
|
81
|
+
* Resume rendering loop
|
|
82
|
+
*
|
|
83
|
+
* When called:
|
|
84
|
+
* - Rendering loop resumes from current state
|
|
85
|
+
* - If in Idle state, immediately renders current frame to restore display
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* // Resume rendering
|
|
89
|
+
* avatarView.resumeRendering()
|
|
90
|
+
*/
|
|
91
|
+
resumeRendering(): void;
|
|
92
|
+
/**
|
|
93
|
+
* Check if rendering is currently enabled
|
|
94
|
+
* @returns true if rendering is enabled, false if paused
|
|
95
|
+
*/
|
|
96
|
+
isRenderingEnabled(): boolean;
|
|
64
97
|
/**
|
|
65
98
|
* Get or set avatar transform in canvas
|
|
66
99
|
*
|
|
@@ -1400,6 +1400,32 @@ function base64FromBytes$1(arr) {
|
|
|
1400
1400
|
function isSet$1(value) {
|
|
1401
1401
|
return value !== null && value !== void 0;
|
|
1402
1402
|
}
|
|
1403
|
+
function convertProtoFlameToWasmParams(protoFlame) {
|
|
1404
|
+
var _a;
|
|
1405
|
+
return {
|
|
1406
|
+
translation: protoFlame.translation || [0, 0, 0],
|
|
1407
|
+
rotation: protoFlame.rotation || [0, 0, 0],
|
|
1408
|
+
neck_pose: protoFlame.neckPose || [0, 0, 0],
|
|
1409
|
+
jaw_pose: protoFlame.jawPose || [0, 0, 0],
|
|
1410
|
+
eyes_pose: protoFlame.eyePose || [0, 0, 0, 0, 0, 0],
|
|
1411
|
+
eyelid: protoFlame.eyeLid || [0, 0],
|
|
1412
|
+
expr_params: protoFlame.expression || [],
|
|
1413
|
+
shape_params: [],
|
|
1414
|
+
// Realtime doesn't provide shape params, use default
|
|
1415
|
+
has_eyelid: (((_a = protoFlame.eyeLid) == null ? void 0 : _a.length) || 0) > 0
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
function convertWasmParamsToProtoFlame(wasmParams) {
|
|
1419
|
+
return {
|
|
1420
|
+
translation: wasmParams.translation || [0, 0, 0],
|
|
1421
|
+
rotation: wasmParams.rotation || [0, 0, 0],
|
|
1422
|
+
neckPose: wasmParams.neck_pose || [0, 0, 0],
|
|
1423
|
+
jawPose: wasmParams.jaw_pose || [0, 0, 0],
|
|
1424
|
+
eyePose: wasmParams.eyes_pose || [0, 0, 0, 0, 0, 0],
|
|
1425
|
+
eyeLid: wasmParams.eyelid || [0, 0],
|
|
1426
|
+
expression: wasmParams.expr_params || []
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1403
1429
|
const POSTHOG_HOST_INTL = "https://i.spatialwalk.ai";
|
|
1404
1430
|
const POSTHOG_API_KEY_INTL = "phc_IFTLa6Z6VhTaNvsxB7klvG2JeNwcSpnnwz8YvZRC96Q";
|
|
1405
1431
|
function getPostHogConfig(_environment) {
|
|
@@ -1452,32 +1478,6 @@ const APP_CONFIG = {
|
|
|
1452
1478
|
}
|
|
1453
1479
|
}
|
|
1454
1480
|
};
|
|
1455
|
-
function convertProtoFlameToWasmParams(protoFlame) {
|
|
1456
|
-
var _a;
|
|
1457
|
-
return {
|
|
1458
|
-
translation: protoFlame.translation || [0, 0, 0],
|
|
1459
|
-
rotation: protoFlame.rotation || [0, 0, 0],
|
|
1460
|
-
neck_pose: protoFlame.neckPose || [0, 0, 0],
|
|
1461
|
-
jaw_pose: protoFlame.jawPose || [0, 0, 0],
|
|
1462
|
-
eyes_pose: protoFlame.eyePose || [0, 0, 0, 0, 0, 0],
|
|
1463
|
-
eyelid: protoFlame.eyeLid || [0, 0],
|
|
1464
|
-
expr_params: protoFlame.expression || [],
|
|
1465
|
-
shape_params: [],
|
|
1466
|
-
// Realtime doesn't provide shape params, use default
|
|
1467
|
-
has_eyelid: (((_a = protoFlame.eyeLid) == null ? void 0 : _a.length) || 0) > 0
|
|
1468
|
-
};
|
|
1469
|
-
}
|
|
1470
|
-
function convertWasmParamsToProtoFlame(wasmParams) {
|
|
1471
|
-
return {
|
|
1472
|
-
translation: wasmParams.translation || [0, 0, 0],
|
|
1473
|
-
rotation: wasmParams.rotation || [0, 0, 0],
|
|
1474
|
-
neckPose: wasmParams.neck_pose || [0, 0, 0],
|
|
1475
|
-
jawPose: wasmParams.jaw_pose || [0, 0, 0],
|
|
1476
|
-
eyePose: wasmParams.eyes_pose || [0, 0, 0, 0, 0, 0],
|
|
1477
|
-
eyeLid: wasmParams.eyelid || [0, 0],
|
|
1478
|
-
expression: wasmParams.expr_params || []
|
|
1479
|
-
};
|
|
1480
|
-
}
|
|
1481
1481
|
var t = "undefined" != typeof window ? window : void 0, i = "undefined" != typeof globalThis ? globalThis : t;
|
|
1482
1482
|
"undefined" == typeof self && (i.self = i), "undefined" == typeof File && (i.File = function() {
|
|
1483
1483
|
});
|
|
@@ -7294,7 +7294,6 @@ function extractResourceUrls(meta) {
|
|
|
7294
7294
|
var Environment = /* @__PURE__ */ ((Environment2) => {
|
|
7295
7295
|
Environment2["cn"] = "cn";
|
|
7296
7296
|
Environment2["intl"] = "intl";
|
|
7297
|
-
Environment2["test"] = "test";
|
|
7298
7297
|
return Environment2;
|
|
7299
7298
|
})(Environment || {});
|
|
7300
7299
|
var DrivingServiceMode = /* @__PURE__ */ ((DrivingServiceMode2) => {
|
|
@@ -7944,7 +7943,8 @@ function getCommonFields() {
|
|
|
7944
7943
|
return {
|
|
7945
7944
|
platform: "Web",
|
|
7946
7945
|
sdk_version: sdkVersion,
|
|
7947
|
-
|
|
7946
|
+
env: currentEnvironment || "unknown",
|
|
7947
|
+
// environment 缩写为 env
|
|
7948
7948
|
app_id: logContext.app_id || "",
|
|
7949
7949
|
user_id: logContext.user_id || "",
|
|
7950
7950
|
// 没有就传空字符串
|
|
@@ -8139,7 +8139,6 @@ function cleanupPostHog() {
|
|
|
8139
8139
|
}
|
|
8140
8140
|
function logEvent(event, level = "info", contents = {}) {
|
|
8141
8141
|
const context = {
|
|
8142
|
-
environment: currentEnvironment || "unknown",
|
|
8143
8142
|
sessionToken: idManager.getSessionToken() ?? "",
|
|
8144
8143
|
userId: idManager.getUserId() ?? "",
|
|
8145
8144
|
...contents
|
|
@@ -8244,7 +8243,7 @@ const _AnimationPlayer = class _AnimationPlayer {
|
|
|
8244
8243
|
if (this.streamingPlayer) {
|
|
8245
8244
|
return;
|
|
8246
8245
|
}
|
|
8247
|
-
const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-
|
|
8246
|
+
const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-BNj8LpDw.js");
|
|
8248
8247
|
const { AvatarSDK: AvatarSDK2 } = await Promise.resolve().then(() => AvatarSDK$1);
|
|
8249
8248
|
const audioFormat = AvatarSDK2.getAudioFormat();
|
|
8250
8249
|
this.streamingPlayer = new StreamingAudioPlayer({
|
|
@@ -8443,10 +8442,42 @@ const _AnimationPlayer = class _AnimationPlayer {
|
|
|
8443
8442
|
};
|
|
8444
8443
|
__publicField(_AnimationPlayer, "audioUnlocked", false);
|
|
8445
8444
|
let AnimationPlayer = _AnimationPlayer;
|
|
8445
|
+
const FLAME_FRAME_RATE = 25;
|
|
8446
|
+
const START_TRANSITION_DURATION_S = 0.2;
|
|
8447
|
+
const END_TRANSITION_DURATION_S = 1.6;
|
|
8448
|
+
const START_TRANSITION_DURATION_MS = START_TRANSITION_DURATION_S * 1e3;
|
|
8449
|
+
const END_TRANSITION_DURATION_MS = END_TRANSITION_DURATION_S * 1e3;
|
|
8450
|
+
const AUDIO_SAMPLE_RATE = 16e3;
|
|
8451
|
+
const AUDIO_CHANNELS = 1;
|
|
8452
|
+
const AUDIO_BYTES_PER_SAMPLE = 2;
|
|
8453
|
+
const AUDIO_BYTES_PER_SECOND = AUDIO_SAMPLE_RATE * AUDIO_CHANNELS * AUDIO_BYTES_PER_SAMPLE;
|
|
8454
|
+
const BEZIER_CURVES$1 = {
|
|
8455
|
+
/** 下颌: 快速启动,平滑停止 */
|
|
8456
|
+
jaw: [0.2, 0.8, 0.3, 1],
|
|
8457
|
+
/** 表情: 平滑 S 曲线 */
|
|
8458
|
+
expression: [0.4, 0, 0.2, 1],
|
|
8459
|
+
/** 眼部: 柔和 S 曲线 */
|
|
8460
|
+
eye: [0.3, 0, 0.1, 1],
|
|
8461
|
+
/** 颈部: 慢启动,惯性停止 */
|
|
8462
|
+
neck: [0.1, 0.2, 0.2, 1],
|
|
8463
|
+
/** 全局: 标准 ease-in-out */
|
|
8464
|
+
global: [0.42, 0, 0.58, 1]
|
|
8465
|
+
};
|
|
8466
|
+
const TIME_SCALE = {
|
|
8467
|
+
/** 下颌: 40% 时间完成 (1/2.5) */
|
|
8468
|
+
jaw: 2.5,
|
|
8469
|
+
/** 表情: 62.5% 时间完成 (1/1.6) */
|
|
8470
|
+
expression: 1.6,
|
|
8471
|
+
/** 眼部: 77% 时间完成 (1/1.3) */
|
|
8472
|
+
eye: 1.3,
|
|
8473
|
+
/** 颈部: 100% 时间完成 */
|
|
8474
|
+
neck: 1,
|
|
8475
|
+
/** 全局: 100% 时间完成 */
|
|
8476
|
+
global: 1
|
|
8477
|
+
};
|
|
8446
8478
|
const DEFAULT_SDK_CONFIG = {
|
|
8447
8479
|
[Environment.cn]: "https://api.open.spatialwalk.top",
|
|
8448
|
-
[Environment.intl]: "https://api.intl.spatialwalk.cloud"
|
|
8449
|
-
[Environment.test]: "https://api-test.spatialwalk.top"
|
|
8480
|
+
[Environment.intl]: "https://api.intl.spatialwalk.cloud"
|
|
8450
8481
|
};
|
|
8451
8482
|
const configCache = {
|
|
8452
8483
|
config: null,
|
|
@@ -8484,9 +8515,6 @@ async function fetchSdkConfig(version) {
|
|
|
8484
8515
|
} else if (data.endpoints.intl) {
|
|
8485
8516
|
config[Environment.intl] = `https://${data.endpoints.intl}`;
|
|
8486
8517
|
}
|
|
8487
|
-
if (data.endpoints.test) {
|
|
8488
|
-
config[Environment.test] = `https://${data.endpoints.test}`;
|
|
8489
|
-
}
|
|
8490
8518
|
configCache.config = config;
|
|
8491
8519
|
logger.log(`[SdkConfigLoader] SDK config fetched successfully:`, config);
|
|
8492
8520
|
} catch (error) {
|
|
@@ -9941,7 +9969,7 @@ class AvatarSDK {
|
|
|
9941
9969
|
}
|
|
9942
9970
|
__publicField(AvatarSDK, "_isInitialized", false);
|
|
9943
9971
|
__publicField(AvatarSDK, "_configuration", null);
|
|
9944
|
-
__publicField(AvatarSDK, "_version", "1.0.0-beta.
|
|
9972
|
+
__publicField(AvatarSDK, "_version", "1.0.0-beta.78");
|
|
9945
9973
|
__publicField(AvatarSDK, "_avatarCore", null);
|
|
9946
9974
|
__publicField(AvatarSDK, "_dynamicSdkConfig", null);
|
|
9947
9975
|
const AvatarSDK$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
@@ -11819,7 +11847,7 @@ class AvatarController {
|
|
|
11819
11847
|
recvFirstFlameTimestamp: 0,
|
|
11820
11848
|
didRecvFirstFlame: false
|
|
11821
11849
|
});
|
|
11822
|
-
__publicField(this, "audioBytesPerSecond",
|
|
11850
|
+
__publicField(this, "audioBytesPerSecond", AUDIO_BYTES_PER_SECOND);
|
|
11823
11851
|
this.avatar = avatar;
|
|
11824
11852
|
this.playbackMode = (options == null ? void 0 : options.playbackMode) ?? DrivingServiceMode.sdk;
|
|
11825
11853
|
if (this.playbackMode === DrivingServiceMode.sdk) {
|
|
@@ -12726,7 +12754,7 @@ class AvatarController {
|
|
|
12726
12754
|
if (this.playbackLoopId) {
|
|
12727
12755
|
return;
|
|
12728
12756
|
}
|
|
12729
|
-
const fps =
|
|
12757
|
+
const fps = FLAME_FRAME_RATE;
|
|
12730
12758
|
const playLoop = async () => {
|
|
12731
12759
|
if (!this.isPlaying || this.currentState === AvatarState.paused || !this.animationPlayer) {
|
|
12732
12760
|
this.playbackLoopId = null;
|
|
@@ -13296,47 +13324,71 @@ function getCacheInfo(url, response) {
|
|
|
13296
13324
|
};
|
|
13297
13325
|
}
|
|
13298
13326
|
async function downloadResource(url, options) {
|
|
13327
|
+
const { signal, characterId, resourceType, maxRetries = 3 } = options || {};
|
|
13328
|
+
if (signal == null ? void 0 : signal.aborted) {
|
|
13329
|
+
throw new Error("Download cancelled");
|
|
13330
|
+
}
|
|
13299
13331
|
try {
|
|
13300
13332
|
let cached = null;
|
|
13301
13333
|
let pwaCacheSubtype = void 0;
|
|
13302
|
-
if (
|
|
13303
|
-
cached = await PwaCacheManager.getCharacterResource(
|
|
13334
|
+
if (characterId) {
|
|
13335
|
+
cached = await PwaCacheManager.getCharacterResource(characterId, url);
|
|
13304
13336
|
if (cached) {
|
|
13305
13337
|
pwaCacheSubtype = "character";
|
|
13306
13338
|
}
|
|
13307
|
-
} else if (
|
|
13339
|
+
} else if (resourceType === "template") {
|
|
13308
13340
|
cached = await PwaCacheManager.getTemplateResource(url);
|
|
13309
13341
|
if (cached) {
|
|
13310
13342
|
pwaCacheSubtype = "template";
|
|
13311
13343
|
}
|
|
13312
13344
|
}
|
|
13313
13345
|
if (cached) {
|
|
13314
|
-
const
|
|
13346
|
+
const response = new Response(cached);
|
|
13315
13347
|
await new Promise((resolve2) => setTimeout(resolve2, 0));
|
|
13316
|
-
const
|
|
13317
|
-
|
|
13318
|
-
|
|
13319
|
-
|
|
13320
|
-
return { data: cached, cacheInfo
|
|
13321
|
-
}
|
|
13322
|
-
|
|
13323
|
-
|
|
13324
|
-
|
|
13325
|
-
|
|
13326
|
-
|
|
13327
|
-
|
|
13328
|
-
|
|
13329
|
-
|
|
13330
|
-
|
|
13331
|
-
|
|
13332
|
-
|
|
13333
|
-
|
|
13334
|
-
|
|
13348
|
+
const cacheInfo = getCacheInfo(url, response);
|
|
13349
|
+
cacheInfo.cacheHit = true;
|
|
13350
|
+
cacheInfo.cacheType = "pwa";
|
|
13351
|
+
cacheInfo.pwaCacheSubtype = pwaCacheSubtype;
|
|
13352
|
+
return { data: cached, cacheInfo };
|
|
13353
|
+
}
|
|
13354
|
+
let lastError = null;
|
|
13355
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
13356
|
+
if (signal == null ? void 0 : signal.aborted) {
|
|
13357
|
+
throw new Error("Download cancelled");
|
|
13358
|
+
}
|
|
13359
|
+
try {
|
|
13360
|
+
const response = await fetch(url, { signal });
|
|
13361
|
+
if (!response.ok) {
|
|
13362
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
13363
|
+
}
|
|
13364
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
13365
|
+
if (characterId) {
|
|
13366
|
+
PwaCacheManager.putCharacterResource(characterId, url, arrayBuffer).catch((err) => {
|
|
13367
|
+
logger.warn(`[downloadResource] Failed to cache character resource:`, err);
|
|
13368
|
+
});
|
|
13369
|
+
} else if (resourceType === "template") {
|
|
13370
|
+
PwaCacheManager.putTemplateResource(url, arrayBuffer).catch((err) => {
|
|
13371
|
+
logger.warn(`[downloadResource] Failed to cache template resource:`, err);
|
|
13372
|
+
});
|
|
13373
|
+
}
|
|
13374
|
+
await new Promise((resolve2) => setTimeout(resolve2, 0));
|
|
13375
|
+
const cacheInfo = getCacheInfo(url, response);
|
|
13376
|
+
return { data: arrayBuffer, cacheInfo };
|
|
13377
|
+
} catch (err) {
|
|
13378
|
+
if (err instanceof Error && (err.name === "AbortError" || err.message === "Download cancelled")) {
|
|
13379
|
+
throw err;
|
|
13380
|
+
}
|
|
13381
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
13382
|
+
if (attempt < maxRetries) {
|
|
13383
|
+
logger.warn(`[downloadResource] Attempt ${attempt}/${maxRetries} failed for ${url}, retrying immediately...`);
|
|
13384
|
+
}
|
|
13385
|
+
}
|
|
13335
13386
|
}
|
|
13336
|
-
|
|
13337
|
-
const cacheInfo = getCacheInfo(url, response);
|
|
13338
|
-
return { data: arrayBuffer, cacheInfo };
|
|
13387
|
+
throw lastError || new Error(`Failed to download ${url} after ${maxRetries} attempts`);
|
|
13339
13388
|
} catch (err) {
|
|
13389
|
+
if (err instanceof Error && (err.name === "AbortError" || err.message === "Download cancelled")) {
|
|
13390
|
+
throw err;
|
|
13391
|
+
}
|
|
13340
13392
|
const msg = errorToMessage(err);
|
|
13341
13393
|
throw new Error(`[downloadResource] ${url} → ${msg}`);
|
|
13342
13394
|
}
|
|
@@ -13482,16 +13534,21 @@ class AvatarDownloader {
|
|
|
13482
13534
|
* Load camera settings from CharacterMeta (optional)
|
|
13483
13535
|
* @internal
|
|
13484
13536
|
*/
|
|
13485
|
-
async loadCameraSettings(characterMeta) {
|
|
13537
|
+
async loadCameraSettings(characterMeta, options) {
|
|
13486
13538
|
var _a, _b;
|
|
13539
|
+
const { signal } = options || {};
|
|
13487
13540
|
const cameraUrl = (_b = (_a = characterMeta.camera) == null ? void 0 : _a.resource) == null ? void 0 : _b.remote;
|
|
13488
13541
|
if (!cameraUrl) {
|
|
13489
13542
|
logger.log("ℹ️ No camera resource URL provided");
|
|
13490
13543
|
return void 0;
|
|
13491
13544
|
}
|
|
13545
|
+
if (signal == null ? void 0 : signal.aborted) {
|
|
13546
|
+
throw new Error("Load cancelled");
|
|
13547
|
+
}
|
|
13492
13548
|
try {
|
|
13493
13549
|
logger.log(`📥 Loading camera info from: ${cameraUrl}`);
|
|
13494
13550
|
const { data: arrayBuffer } = await downloadResource(cameraUrl, {
|
|
13551
|
+
signal,
|
|
13495
13552
|
characterId: characterMeta.characterId ?? void 0,
|
|
13496
13553
|
resourceType: "character"
|
|
13497
13554
|
});
|
|
@@ -13510,7 +13567,10 @@ class AvatarDownloader {
|
|
|
13510
13567
|
*/
|
|
13511
13568
|
async loadCharacterData(characterMeta, options) {
|
|
13512
13569
|
var _a, _b, _c, _d, _e2, _f, _g, _h, _i2, _j;
|
|
13513
|
-
const { progressCallback = null } = options || {};
|
|
13570
|
+
const { progressCallback = null, signal } = options || {};
|
|
13571
|
+
if (signal == null ? void 0 : signal.aborted) {
|
|
13572
|
+
throw new Error("Download cancelled");
|
|
13573
|
+
}
|
|
13514
13574
|
const totalStartTime = Date.now();
|
|
13515
13575
|
const shapeUrl = (_c = (_b = (_a = characterMeta.models) == null ? void 0 : _a.shape) == null ? void 0 : _b.resource) == null ? void 0 : _c.remote;
|
|
13516
13576
|
const pointCloudUrl = (_f = (_e2 = (_d = characterMeta.models) == null ? void 0 : _d.gsStandard) == null ? void 0 : _e2.resource) == null ? void 0 : _f.remote;
|
|
@@ -13553,6 +13613,7 @@ class AvatarDownloader {
|
|
|
13553
13613
|
updateProgress(filename, false);
|
|
13554
13614
|
try {
|
|
13555
13615
|
const { data: arrayBuffer, cacheInfo } = await downloadResource(url, {
|
|
13616
|
+
signal,
|
|
13556
13617
|
characterId: characterMeta.characterId ?? void 0,
|
|
13557
13618
|
resourceType: "character"
|
|
13558
13619
|
});
|
|
@@ -13567,6 +13628,9 @@ class AvatarDownloader {
|
|
|
13567
13628
|
updateProgress(filename, true);
|
|
13568
13629
|
return { key, success: true, size: arrayBuffer.byteLength };
|
|
13569
13630
|
} catch (error) {
|
|
13631
|
+
if (error instanceof Error && (error.name === "AbortError" || error.message === "Download cancelled")) {
|
|
13632
|
+
throw error;
|
|
13633
|
+
}
|
|
13570
13634
|
if (!optional) {
|
|
13571
13635
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
13572
13636
|
logEvent("download_avatar_assets_failed", "error", {
|
|
@@ -13599,9 +13663,12 @@ class AvatarDownloader {
|
|
|
13599
13663
|
const totalSize = Object.values(characterData).reduce((sum, buffer) => {
|
|
13600
13664
|
return sum + (buffer ? buffer.byteLength : 0);
|
|
13601
13665
|
}, 0);
|
|
13602
|
-
logEvent("
|
|
13666
|
+
logEvent("download_avatar_assets_latency", "info", {
|
|
13667
|
+
resolution: "default",
|
|
13668
|
+
// 目前写死 default
|
|
13603
13669
|
avatar_id: characterMeta.characterId ?? "unknown",
|
|
13604
|
-
|
|
13670
|
+
duration: totalDuration,
|
|
13671
|
+
// 保留其他有用的字段
|
|
13605
13672
|
parallel_duration: parallelDuration,
|
|
13606
13673
|
total_size: totalSize,
|
|
13607
13674
|
file_count: filesToLoad.length,
|
|
@@ -13615,9 +13682,13 @@ class AvatarDownloader {
|
|
|
13615
13682
|
* @internal
|
|
13616
13683
|
*/
|
|
13617
13684
|
async preloadResources(characterMeta, options) {
|
|
13618
|
-
const { progressCallback = null } = options || {};
|
|
13685
|
+
const { progressCallback = null, signal } = options || {};
|
|
13686
|
+
if (signal == null ? void 0 : signal.aborted) {
|
|
13687
|
+
throw new Error("Preload cancelled");
|
|
13688
|
+
}
|
|
13619
13689
|
const [characterData, preloadCameraSettings] = await Promise.all([
|
|
13620
13690
|
this.loadCharacterData(characterMeta, {
|
|
13691
|
+
signal,
|
|
13621
13692
|
progressCallback: (info) => {
|
|
13622
13693
|
if (progressCallback) {
|
|
13623
13694
|
progressCallback({
|
|
@@ -13627,7 +13698,7 @@ class AvatarDownloader {
|
|
|
13627
13698
|
}
|
|
13628
13699
|
}
|
|
13629
13700
|
}),
|
|
13630
|
-
this.loadCameraSettings(characterMeta)
|
|
13701
|
+
this.loadCameraSettings(characterMeta, { signal })
|
|
13631
13702
|
]);
|
|
13632
13703
|
return {
|
|
13633
13704
|
characterData,
|
|
@@ -13661,7 +13732,8 @@ class AvatarDownloader {
|
|
|
13661
13732
|
...headers,
|
|
13662
13733
|
...options.headers
|
|
13663
13734
|
},
|
|
13664
|
-
body: options.body ? JSON.stringify(options.body) : void 0
|
|
13735
|
+
body: options.body ? JSON.stringify(options.body) : void 0,
|
|
13736
|
+
signal: options.signal
|
|
13665
13737
|
});
|
|
13666
13738
|
if (!response.ok) {
|
|
13667
13739
|
let error;
|
|
@@ -13694,16 +13766,21 @@ class AvatarDownloader {
|
|
|
13694
13766
|
* Returns CharacterMeta with nested resource structure
|
|
13695
13767
|
* @internal
|
|
13696
13768
|
*/
|
|
13697
|
-
async getCharacterById(characterId) {
|
|
13769
|
+
async getCharacterById(characterId, options) {
|
|
13698
13770
|
var _a;
|
|
13771
|
+
const { signal } = options || {};
|
|
13699
13772
|
const startTime = Date.now();
|
|
13700
13773
|
try {
|
|
13774
|
+
if (signal == null ? void 0 : signal.aborted) {
|
|
13775
|
+
throw new Error("Request cancelled");
|
|
13776
|
+
}
|
|
13701
13777
|
const client = this.getSdkApiClient();
|
|
13702
13778
|
const response = await client.request(`/v2/character/${characterId}`, {
|
|
13703
|
-
method: "GET"
|
|
13779
|
+
method: "GET",
|
|
13780
|
+
signal
|
|
13704
13781
|
});
|
|
13705
13782
|
const duration = Date.now() - startTime;
|
|
13706
|
-
logEvent("
|
|
13783
|
+
logEvent("fetch_avatar_metadata_latency", "info", {
|
|
13707
13784
|
avatar_id: characterId,
|
|
13708
13785
|
duration
|
|
13709
13786
|
});
|
|
@@ -13735,10 +13812,12 @@ const _AvatarManager = class _AvatarManager {
|
|
|
13735
13812
|
constructor() {
|
|
13736
13813
|
__publicField(this, "avatarDownloader", null);
|
|
13737
13814
|
__publicField(this, "avatarCache", /* @__PURE__ */ new Map());
|
|
13738
|
-
|
|
13739
|
-
// 下载队列:确保资源下载串行执行
|
|
13815
|
+
/** 下载队列:FIFO 顺序 */
|
|
13740
13816
|
__publicField(this, "downloadQueue", []);
|
|
13741
|
-
|
|
13817
|
+
/** 当前正在执行的任务 */
|
|
13818
|
+
__publicField(this, "currentTask", null);
|
|
13819
|
+
/** 任务索引:快速查找 id 对应的任务 */
|
|
13820
|
+
__publicField(this, "taskIndex", /* @__PURE__ */ new Map());
|
|
13742
13821
|
}
|
|
13743
13822
|
/**
|
|
13744
13823
|
* Access via global singleton
|
|
@@ -13756,28 +13835,139 @@ const _AvatarManager = class _AvatarManager {
|
|
|
13756
13835
|
* @returns Promise<Avatar>
|
|
13757
13836
|
*/
|
|
13758
13837
|
async load(id, onProgress) {
|
|
13759
|
-
const loadingPromise = this.loadingPromises.get(id);
|
|
13760
|
-
if (loadingPromise) {
|
|
13761
|
-
logger.log(`[AvatarManager] Avatar ${id} is already loading, reusing promise`);
|
|
13762
|
-
return loadingPromise;
|
|
13763
|
-
}
|
|
13764
13838
|
if (!AvatarSDK.isInitialized) {
|
|
13765
13839
|
throw new Error("AvatarSDK not initialized. Please call AvatarSDK.initialize() first.");
|
|
13766
13840
|
}
|
|
13767
13841
|
if (!this.avatarDownloader) {
|
|
13768
13842
|
this.avatarDownloader = new AvatarDownloader();
|
|
13769
13843
|
}
|
|
13770
|
-
|
|
13771
|
-
|
|
13772
|
-
|
|
13844
|
+
const existingTask = this.taskIndex.get(id);
|
|
13845
|
+
if (existingTask && existingTask.status !== "completed" && existingTask.status !== "failed" && existingTask.status !== "cancelled") {
|
|
13846
|
+
logger.log(`[AvatarManager] Avatar ${id} already in queue/loading, adding callback to existing task`);
|
|
13847
|
+
onProgress == null ? void 0 : onProgress({ type: LoadProgress.downloading, progress: 0 });
|
|
13848
|
+
return new Promise((resolve2, reject) => {
|
|
13849
|
+
existingTask.requests.push({ onProgress, resolve: resolve2, reject });
|
|
13850
|
+
});
|
|
13851
|
+
}
|
|
13852
|
+
logger.log(`[AvatarManager] Queuing new avatar download for id: ${id}`);
|
|
13853
|
+
let taskResolve;
|
|
13854
|
+
let taskReject;
|
|
13855
|
+
const taskPromise = new Promise((resolve2, reject) => {
|
|
13856
|
+
taskResolve = resolve2;
|
|
13857
|
+
taskReject = reject;
|
|
13858
|
+
});
|
|
13859
|
+
const task = {
|
|
13860
|
+
id,
|
|
13861
|
+
status: "queued",
|
|
13862
|
+
requests: [{ onProgress, resolve: taskResolve, reject: taskReject }],
|
|
13863
|
+
abortController: new AbortController(),
|
|
13864
|
+
promise: taskPromise,
|
|
13865
|
+
_resolve: taskResolve,
|
|
13866
|
+
_reject: taskReject
|
|
13867
|
+
};
|
|
13868
|
+
onProgress == null ? void 0 : onProgress({ type: LoadProgress.downloading, progress: 0 });
|
|
13869
|
+
this.downloadQueue.push(task);
|
|
13870
|
+
this.taskIndex.set(id, task);
|
|
13871
|
+
this.processQueue();
|
|
13872
|
+
return taskPromise;
|
|
13873
|
+
}
|
|
13874
|
+
/**
|
|
13875
|
+
* Cancel a pending or running download task
|
|
13876
|
+
* @param id Avatar ID to cancel
|
|
13877
|
+
* @returns true if task was found and cancelled
|
|
13878
|
+
*/
|
|
13879
|
+
cancelLoad(id) {
|
|
13880
|
+
const task = this.taskIndex.get(id);
|
|
13881
|
+
if (!task) {
|
|
13882
|
+
logger.log(`[AvatarManager] No task found for id: ${id}`);
|
|
13883
|
+
return false;
|
|
13884
|
+
}
|
|
13885
|
+
if (task.status === "completed" || task.status === "failed" || task.status === "cancelled") {
|
|
13886
|
+
logger.log(`[AvatarManager] Task ${id} already finished with status: ${task.status}`);
|
|
13887
|
+
return false;
|
|
13888
|
+
}
|
|
13889
|
+
logger.log(`[AvatarManager] Cancelling task for id: ${id}`);
|
|
13890
|
+
task.abortController.abort();
|
|
13891
|
+
task.status = "cancelled";
|
|
13892
|
+
const cancelError = new Error(`Download cancelled for avatar: ${id}`);
|
|
13893
|
+
this.notifyAllRequests(task, { type: LoadProgress.failed, error: cancelError });
|
|
13894
|
+
task.requests.forEach((req) => req.reject(cancelError));
|
|
13895
|
+
this.taskIndex.delete(id);
|
|
13896
|
+
const queueIndex = this.downloadQueue.indexOf(task);
|
|
13897
|
+
if (queueIndex !== -1) {
|
|
13898
|
+
this.downloadQueue.splice(queueIndex, 1);
|
|
13899
|
+
}
|
|
13900
|
+
if (this.currentTask === task) {
|
|
13901
|
+
this.currentTask = null;
|
|
13902
|
+
this.processQueue();
|
|
13903
|
+
}
|
|
13904
|
+
return true;
|
|
13905
|
+
}
|
|
13906
|
+
/**
|
|
13907
|
+
* Process download queue (FIFO)
|
|
13908
|
+
* @internal
|
|
13909
|
+
*/
|
|
13910
|
+
async processQueue() {
|
|
13911
|
+
var _a, _b;
|
|
13912
|
+
if (this.currentTask || this.downloadQueue.length === 0) {
|
|
13913
|
+
return;
|
|
13914
|
+
}
|
|
13915
|
+
const task = this.downloadQueue.shift();
|
|
13916
|
+
this.currentTask = task;
|
|
13917
|
+
logger.log(`[AvatarManager] Processing task: ${task.id} (${this.downloadQueue.length} items remaining)`);
|
|
13918
|
+
try {
|
|
13919
|
+
const avatar = await this.executeTask(task);
|
|
13920
|
+
if (task.status === "cancelled") {
|
|
13921
|
+
return;
|
|
13922
|
+
}
|
|
13923
|
+
task.status = "completed";
|
|
13924
|
+
this.notifyAllRequests(task, { type: LoadProgress.completed });
|
|
13925
|
+
task.requests.forEach((req) => req.resolve(avatar));
|
|
13926
|
+
(_a = task._resolve) == null ? void 0 : _a.call(task, avatar);
|
|
13927
|
+
} catch (error) {
|
|
13928
|
+
if (task.status === "cancelled") {
|
|
13929
|
+
return;
|
|
13930
|
+
}
|
|
13931
|
+
task.status = "failed";
|
|
13932
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
13933
|
+
this.notifyAllRequests(task, { type: LoadProgress.failed, error: err });
|
|
13934
|
+
task.requests.forEach((req) => req.reject(err));
|
|
13935
|
+
(_b = task._reject) == null ? void 0 : _b.call(task, err);
|
|
13936
|
+
} finally {
|
|
13937
|
+
if (task.status !== "cancelled") {
|
|
13938
|
+
this.taskIndex.delete(task.id);
|
|
13939
|
+
}
|
|
13940
|
+
this.currentTask = null;
|
|
13941
|
+
this.processQueue();
|
|
13942
|
+
}
|
|
13943
|
+
}
|
|
13944
|
+
/**
|
|
13945
|
+
* Execute a single download task
|
|
13946
|
+
* @internal
|
|
13947
|
+
*/
|
|
13948
|
+
async executeTask(task) {
|
|
13949
|
+
const { id, abortController } = task;
|
|
13950
|
+
const signal = abortController.signal;
|
|
13951
|
+
const startTime = Date.now();
|
|
13952
|
+
if (signal.aborted) {
|
|
13953
|
+
throw new Error("Task cancelled");
|
|
13954
|
+
}
|
|
13955
|
+
task.status = "fetching-meta";
|
|
13956
|
+
logger.log(`[AvatarManager] Fetching metadata for avatar ${id}...`);
|
|
13957
|
+
const characterMeta = await this.avatarDownloader.getCharacterById(id, { signal });
|
|
13958
|
+
task.characterMeta = characterMeta;
|
|
13959
|
+
if (signal.aborted) {
|
|
13960
|
+
throw new Error("Task cancelled");
|
|
13961
|
+
}
|
|
13773
13962
|
const cached = this.avatarCache.get(id);
|
|
13774
13963
|
if (cached) {
|
|
13775
13964
|
const cachedMeta = cached.getCharacterMeta();
|
|
13776
13965
|
const cachedVersion = cachedMeta.version;
|
|
13777
|
-
const newVersion =
|
|
13966
|
+
const newVersion = characterMeta.version;
|
|
13778
13967
|
if (cachedVersion === newVersion) {
|
|
13779
|
-
logger.log(`[AvatarManager] Avatar ${id} found in cache with same version (${cachedVersion}),
|
|
13780
|
-
cached.updateCharacterMeta(
|
|
13968
|
+
logger.log(`[AvatarManager] Avatar ${id} found in cache with same version (${cachedVersion}), returning cached`);
|
|
13969
|
+
cached.updateCharacterMeta(characterMeta);
|
|
13970
|
+
this.notifyAllRequests(task, { type: LoadProgress.downloading, progress: 100 });
|
|
13781
13971
|
return cached;
|
|
13782
13972
|
} else {
|
|
13783
13973
|
logger.log(`[AvatarManager] Avatar ${id} version mismatch: cached=${cachedVersion}, new=${newVersion}, reloading...`);
|
|
@@ -13786,77 +13976,71 @@ const _AvatarManager = class _AvatarManager {
|
|
|
13786
13976
|
await PwaCacheManager2.clearCharacterCache(id);
|
|
13787
13977
|
}
|
|
13788
13978
|
}
|
|
13789
|
-
|
|
13790
|
-
const
|
|
13791
|
-
|
|
13792
|
-
|
|
13793
|
-
|
|
13794
|
-
|
|
13795
|
-
|
|
13796
|
-
|
|
13797
|
-
|
|
13979
|
+
const totalAssets = this.countTotalAssets(characterMeta);
|
|
13980
|
+
const metaProgress = Math.round(1 / (1 + totalAssets) * 100);
|
|
13981
|
+
this.notifyAllRequests(task, { type: LoadProgress.downloading, progress: metaProgress });
|
|
13982
|
+
if (signal.aborted) {
|
|
13983
|
+
throw new Error("Task cancelled");
|
|
13984
|
+
}
|
|
13985
|
+
task.status = "downloading";
|
|
13986
|
+
logger.log("[AvatarManager] Downloading resources...");
|
|
13987
|
+
let downloadedCount = 0;
|
|
13988
|
+
const resources = await this.avatarDownloader.preloadResources(characterMeta, {
|
|
13989
|
+
signal,
|
|
13990
|
+
progressCallback: (info) => {
|
|
13991
|
+
if (info.loaded > downloadedCount) {
|
|
13992
|
+
downloadedCount = info.loaded;
|
|
13993
|
+
const progress = Math.round((1 + downloadedCount) / (1 + totalAssets) * 100);
|
|
13994
|
+
this.notifyAllRequests(task, { type: LoadProgress.downloading, progress });
|
|
13995
|
+
}
|
|
13996
|
+
}
|
|
13798
13997
|
});
|
|
13799
|
-
|
|
13800
|
-
|
|
13801
|
-
|
|
13998
|
+
if (signal.aborted) {
|
|
13999
|
+
throw new Error("Task cancelled");
|
|
14000
|
+
}
|
|
14001
|
+
logger.log("[AvatarManager] Creating Avatar instance...");
|
|
14002
|
+
const avatar = new Avatar(id, characterMeta, resources);
|
|
14003
|
+
this.avatarCache.set(id, avatar);
|
|
14004
|
+
logger.log("[AvatarManager] Avatar loaded successfully");
|
|
14005
|
+
const totalDuration = Date.now() - startTime;
|
|
14006
|
+
logEvent("fetch_avatar_latency", "info", {
|
|
14007
|
+
resolution: "default",
|
|
14008
|
+
// 目前写死 default
|
|
14009
|
+
avatar_id: id,
|
|
14010
|
+
duration: totalDuration
|
|
13802
14011
|
});
|
|
13803
|
-
|
|
13804
|
-
|
|
14012
|
+
logEvent("character_load", "info", {
|
|
14013
|
+
avatar_id: id,
|
|
14014
|
+
event: "load_success"
|
|
14015
|
+
});
|
|
14016
|
+
return avatar;
|
|
13805
14017
|
}
|
|
13806
14018
|
/**
|
|
13807
|
-
*
|
|
14019
|
+
* 计算需要下载的资源总数
|
|
13808
14020
|
* @internal
|
|
13809
14021
|
*/
|
|
13810
|
-
|
|
13811
|
-
|
|
13812
|
-
|
|
13813
|
-
|
|
13814
|
-
|
|
13815
|
-
|
|
13816
|
-
|
|
13817
|
-
|
|
13818
|
-
const avatar = await this.doLoad(item.id, item.characterMeta, item.onProgress);
|
|
13819
|
-
item.resolve(avatar);
|
|
13820
|
-
} catch (error) {
|
|
13821
|
-
item.reject(error instanceof Error ? error : new Error(String(error)));
|
|
13822
|
-
} finally {
|
|
13823
|
-
this.isDownloading = false;
|
|
13824
|
-
this.processDownloadQueue();
|
|
13825
|
-
}
|
|
14022
|
+
countTotalAssets(meta) {
|
|
14023
|
+
var _a, _b, _c, _d, _e2, _f, _g, _h, _i2, _j, _k;
|
|
14024
|
+
let count = 0;
|
|
14025
|
+
if ((_c = (_b = (_a = meta.models) == null ? void 0 : _a.shape) == null ? void 0 : _b.resource) == null ? void 0 : _c.remote) count++;
|
|
14026
|
+
if ((_f = (_e2 = (_d = meta.models) == null ? void 0 : _d.gsStandard) == null ? void 0 : _e2.resource) == null ? void 0 : _f.remote) count++;
|
|
14027
|
+
if ((_i2 = (_h = (_g = meta.animations) == null ? void 0 : _g.frameIdle) == null ? void 0 : _h.resource) == null ? void 0 : _i2.remote) count++;
|
|
14028
|
+
if ((_k = (_j = meta.camera) == null ? void 0 : _j.resource) == null ? void 0 : _k.remote) count++;
|
|
14029
|
+
return count;
|
|
13826
14030
|
}
|
|
13827
14031
|
/**
|
|
13828
|
-
*
|
|
14032
|
+
* 通知所有请求者进度更新
|
|
13829
14033
|
* @internal
|
|
13830
14034
|
*/
|
|
13831
|
-
|
|
13832
|
-
|
|
13833
|
-
|
|
13834
|
-
|
|
13835
|
-
|
|
13836
|
-
|
|
13837
|
-
|
|
13838
|
-
|
|
13839
|
-
|
|
13840
|
-
progress: Math.round(externalProgress)
|
|
13841
|
-
});
|
|
13842
|
-
}
|
|
13843
|
-
});
|
|
13844
|
-
logger.log("[AvatarManager] Step 2: Creating Avatar instance...");
|
|
13845
|
-
const avatar = new Avatar(id, characterMeta, resources);
|
|
13846
|
-
this.avatarCache.set(id, avatar);
|
|
13847
|
-
logger.log("[AvatarManager] Avatar loaded successfully");
|
|
13848
|
-
onProgress == null ? void 0 : onProgress({ type: LoadProgress.completed });
|
|
13849
|
-
logEvent("character_load", "info", {
|
|
13850
|
-
avatar_id: id,
|
|
13851
|
-
event: "load_success"
|
|
13852
|
-
});
|
|
13853
|
-
return avatar;
|
|
13854
|
-
} catch (error) {
|
|
13855
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
13856
|
-
logger.error("Failed to load avatar:", message);
|
|
13857
|
-
onProgress == null ? void 0 : onProgress({ type: LoadProgress.failed, error });
|
|
13858
|
-
throw error;
|
|
13859
|
-
}
|
|
14035
|
+
notifyAllRequests(task, progress) {
|
|
14036
|
+
task.requests.forEach((req) => {
|
|
14037
|
+
var _a;
|
|
14038
|
+
try {
|
|
14039
|
+
(_a = req.onProgress) == null ? void 0 : _a.call(req, progress);
|
|
14040
|
+
} catch (err) {
|
|
14041
|
+
logger.warn(`[AvatarManager] Error in progress callback:`, err);
|
|
14042
|
+
}
|
|
14043
|
+
});
|
|
13860
14044
|
}
|
|
13861
14045
|
/**
|
|
13862
14046
|
* Get cached avatar
|
|
@@ -13871,17 +14055,53 @@ const _AvatarManager = class _AvatarManager {
|
|
|
13871
14055
|
* @param id Avatar ID
|
|
13872
14056
|
*/
|
|
13873
14057
|
clear(id) {
|
|
14058
|
+
this.cancelLoad(id);
|
|
13874
14059
|
const removed = this.avatarCache.delete(id);
|
|
13875
14060
|
if (removed) {
|
|
13876
14061
|
logger.log(`[AvatarManager] Cleared avatar cache for id: ${id}`);
|
|
13877
14062
|
}
|
|
13878
14063
|
}
|
|
13879
14064
|
/**
|
|
13880
|
-
* Clear all avatar cache and
|
|
14065
|
+
* Clear all avatar cache and cancel all tasks
|
|
13881
14066
|
*/
|
|
13882
14067
|
clearAll() {
|
|
14068
|
+
for (const task of this.taskIndex.values()) {
|
|
14069
|
+
if (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
|
|
14070
|
+
this.cancelLoad(task.id);
|
|
14071
|
+
}
|
|
14072
|
+
}
|
|
13883
14073
|
this.avatarCache.clear();
|
|
13884
|
-
|
|
14074
|
+
this.downloadQueue = [];
|
|
14075
|
+
this.taskIndex.clear();
|
|
14076
|
+
this.currentTask = null;
|
|
14077
|
+
logger.log("[AvatarManager] Cleared all avatar cache and cancelled all tasks");
|
|
14078
|
+
}
|
|
14079
|
+
/**
|
|
14080
|
+
* Get the number of pending tasks in the queue
|
|
14081
|
+
* @internal For testing purposes
|
|
14082
|
+
*/
|
|
14083
|
+
get pendingTaskCount() {
|
|
14084
|
+
return this.downloadQueue.length + (this.currentTask ? 1 : 0);
|
|
14085
|
+
}
|
|
14086
|
+
/**
|
|
14087
|
+
* Check if a task is in progress for the given ID
|
|
14088
|
+
* @internal For testing purposes
|
|
14089
|
+
*/
|
|
14090
|
+
isLoading(id) {
|
|
14091
|
+
const task = this.taskIndex.get(id);
|
|
14092
|
+
return task !== void 0 && task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled";
|
|
14093
|
+
}
|
|
14094
|
+
/**
|
|
14095
|
+
* @internal For testing - get loadingPromises map (compatibility)
|
|
14096
|
+
*/
|
|
14097
|
+
get loadingPromises() {
|
|
14098
|
+
const map = /* @__PURE__ */ new Map();
|
|
14099
|
+
for (const [id, task] of this.taskIndex) {
|
|
14100
|
+
if (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
|
|
14101
|
+
map.set(id, task.promise);
|
|
14102
|
+
}
|
|
14103
|
+
}
|
|
14104
|
+
return map;
|
|
13885
14105
|
}
|
|
13886
14106
|
};
|
|
13887
14107
|
__publicField(_AvatarManager, "_instance", null);
|
|
@@ -15556,7 +15776,7 @@ function linearLerp(from, to2, progress) {
|
|
|
15556
15776
|
expression: lerpArrays(from.expression || [], to2.expression || [], progress)
|
|
15557
15777
|
};
|
|
15558
15778
|
}
|
|
15559
|
-
function generateTransitionFramesLinear(from, to2, durationMs, fps =
|
|
15779
|
+
function generateTransitionFramesLinear(from, to2, durationMs, fps = FLAME_FRAME_RATE) {
|
|
15560
15780
|
const steps = Math.max(1, Math.floor(durationMs / 1e3 * fps));
|
|
15561
15781
|
const frames = Array.from({ length: steps });
|
|
15562
15782
|
if (steps === 1) {
|
|
@@ -15599,27 +15819,11 @@ function createBezierEasing(x1, y1, x2, y2) {
|
|
|
15599
15819
|
};
|
|
15600
15820
|
}
|
|
15601
15821
|
const BEZIER_CURVES = {
|
|
15602
|
-
|
|
15603
|
-
|
|
15604
|
-
|
|
15605
|
-
|
|
15606
|
-
|
|
15607
|
-
eye: createBezierEasing(0.3, 0, 0.1, 1),
|
|
15608
|
-
// neck: slow start, inertial stop
|
|
15609
|
-
neck: createBezierEasing(0.1, 0.2, 0.2, 1),
|
|
15610
|
-
// global: standard ease-in-out
|
|
15611
|
-
global: createBezierEasing(0.42, 0, 0.58, 1)
|
|
15612
|
-
};
|
|
15613
|
-
const TIME_SCALE = {
|
|
15614
|
-
jaw: 2.5,
|
|
15615
|
-
// 40% time to complete
|
|
15616
|
-
expression: 1.6,
|
|
15617
|
-
// 62.5% time to complete
|
|
15618
|
-
eye: 1.3,
|
|
15619
|
-
// 77% time to complete
|
|
15620
|
-
neck: 1,
|
|
15621
|
-
// 100% time to complete
|
|
15622
|
-
global: 1
|
|
15822
|
+
jaw: createBezierEasing(...BEZIER_CURVES$1.jaw),
|
|
15823
|
+
expression: createBezierEasing(...BEZIER_CURVES$1.expression),
|
|
15824
|
+
eye: createBezierEasing(...BEZIER_CURVES$1.eye),
|
|
15825
|
+
neck: createBezierEasing(...BEZIER_CURVES$1.neck),
|
|
15826
|
+
global: createBezierEasing(...BEZIER_CURVES$1.global)
|
|
15623
15827
|
};
|
|
15624
15828
|
function bezierLerp(from, to2, progress) {
|
|
15625
15829
|
const getT = (key) => {
|
|
@@ -15642,7 +15846,7 @@ function bezierLerp(from, to2, progress) {
|
|
|
15642
15846
|
expression: lerpArrays(from.expression || [], to2.expression || [], getT("expression"))
|
|
15643
15847
|
};
|
|
15644
15848
|
}
|
|
15645
|
-
function generateTransitionFrames(from, to2, durationMs, fps =
|
|
15849
|
+
function generateTransitionFrames(from, to2, durationMs, fps = FLAME_FRAME_RATE) {
|
|
15646
15850
|
const steps = Math.max(1, Math.floor(durationMs / 1e3 * fps));
|
|
15647
15851
|
const frames = Array.from({ length: steps });
|
|
15648
15852
|
if (steps === 1) {
|
|
@@ -15692,9 +15896,9 @@ class AvatarView {
|
|
|
15692
15896
|
// Transition animation data
|
|
15693
15897
|
__publicField(this, "transitionKeyframes", []);
|
|
15694
15898
|
__publicField(this, "transitionStartTime", 0);
|
|
15695
|
-
__publicField(this, "startTransitionDurationMs",
|
|
15899
|
+
__publicField(this, "startTransitionDurationMs", START_TRANSITION_DURATION_MS);
|
|
15696
15900
|
// Idle -> Speaking 过渡时长
|
|
15697
|
-
__publicField(this, "endTransitionDurationMs",
|
|
15901
|
+
__publicField(this, "endTransitionDurationMs", END_TRANSITION_DURATION_MS);
|
|
15698
15902
|
// Speaking -> Idle 过渡时长
|
|
15699
15903
|
__publicField(this, "cachedIdleFirstFrame", null);
|
|
15700
15904
|
__publicField(this, "idleCurrentFrameIndex", 0);
|
|
@@ -15706,6 +15910,8 @@ class AvatarView {
|
|
|
15706
15910
|
// Unique ID for this character instance
|
|
15707
15911
|
// 纯渲染模式标志(阻止 idle 循环渲染)
|
|
15708
15912
|
__publicField(this, "isPureRenderingMode", false);
|
|
15913
|
+
// 渲染开关标志(控制是否进行渲染循环)
|
|
15914
|
+
__publicField(this, "_renderingEnabled", true);
|
|
15709
15915
|
// avatar_active 埋点相关
|
|
15710
15916
|
__publicField(this, "avatarActiveTimer", null);
|
|
15711
15917
|
__publicField(this, "AVATAR_ACTIVE_INTERVAL", 6e5);
|
|
@@ -15777,12 +15983,12 @@ class AvatarView {
|
|
|
15777
15983
|
aligned.from,
|
|
15778
15984
|
aligned.to,
|
|
15779
15985
|
durationMs,
|
|
15780
|
-
|
|
15986
|
+
FLAME_FRAME_RATE
|
|
15781
15987
|
) : generateTransitionFrames(
|
|
15782
15988
|
aligned.from,
|
|
15783
15989
|
aligned.to,
|
|
15784
15990
|
durationMs,
|
|
15785
|
-
|
|
15991
|
+
FLAME_FRAME_RATE
|
|
15786
15992
|
);
|
|
15787
15993
|
if (keyframes.length < 2) {
|
|
15788
15994
|
keyframes = [aligned.from, aligned.to];
|
|
@@ -16111,7 +16317,7 @@ class AvatarView {
|
|
|
16111
16317
|
}
|
|
16112
16318
|
this.idleCurrentFrameIndex = 0;
|
|
16113
16319
|
let lastTime = 0;
|
|
16114
|
-
const targetFPS =
|
|
16320
|
+
const targetFPS = FLAME_FRAME_RATE;
|
|
16115
16321
|
const frameInterval = 1e3 / targetFPS;
|
|
16116
16322
|
this.initFPS();
|
|
16117
16323
|
const renderFrame = async (currentTime) => {
|
|
@@ -16133,6 +16339,10 @@ class AvatarView {
|
|
|
16133
16339
|
this.idleAnimationLoopId = requestAnimationFrame(renderFrame);
|
|
16134
16340
|
return;
|
|
16135
16341
|
}
|
|
16342
|
+
if (!this._renderingEnabled) {
|
|
16343
|
+
this.idleAnimationLoopId = requestAnimationFrame(renderFrame);
|
|
16344
|
+
return;
|
|
16345
|
+
}
|
|
16136
16346
|
const avatarCore = AvatarSDK.getAvatarCore();
|
|
16137
16347
|
if (!avatarCore) {
|
|
16138
16348
|
return;
|
|
@@ -16146,8 +16356,7 @@ class AvatarView {
|
|
|
16146
16356
|
if (this.isPureRenderingMode) {
|
|
16147
16357
|
return;
|
|
16148
16358
|
}
|
|
16149
|
-
this.
|
|
16150
|
-
this.renderSystem.renderFrame();
|
|
16359
|
+
this.doRender(splatData);
|
|
16151
16360
|
}
|
|
16152
16361
|
this.idleAnimationLoopId = requestAnimationFrame(renderFrame);
|
|
16153
16362
|
} catch (error) {
|
|
@@ -16168,7 +16377,7 @@ class AvatarView {
|
|
|
16168
16377
|
this.stopRealtimeAnimationLoop();
|
|
16169
16378
|
}
|
|
16170
16379
|
let lastTime = 0;
|
|
16171
|
-
const targetFPS =
|
|
16380
|
+
const targetFPS = FLAME_FRAME_RATE;
|
|
16172
16381
|
const frameInterval = 1e3 / targetFPS;
|
|
16173
16382
|
this.initFPS();
|
|
16174
16383
|
const renderFrame = async (currentTime) => {
|
|
@@ -16216,8 +16425,7 @@ class AvatarView {
|
|
|
16216
16425
|
if (avatarCore) {
|
|
16217
16426
|
const sd = await avatarCore.computeFrameFlatFromParams(wasmParams, this.characterHandle ?? void 0);
|
|
16218
16427
|
if (sd) {
|
|
16219
|
-
this.
|
|
16220
|
-
this.renderSystem.renderFrame();
|
|
16428
|
+
this.doRender(sd);
|
|
16221
16429
|
}
|
|
16222
16430
|
}
|
|
16223
16431
|
if (progress >= 1) {
|
|
@@ -16295,6 +16503,18 @@ class AvatarView {
|
|
|
16295
16503
|
this.stopIdleAnimationLoop();
|
|
16296
16504
|
this.stopRealtimeAnimationLoop();
|
|
16297
16505
|
}
|
|
16506
|
+
/**
|
|
16507
|
+
* Unified render method - all rendering goes through here
|
|
16508
|
+
* This is the single point of control for renderingEnabled flag
|
|
16509
|
+
* @internal
|
|
16510
|
+
*/
|
|
16511
|
+
doRender(splatData) {
|
|
16512
|
+
if (!this.renderSystem || !this._renderingEnabled) {
|
|
16513
|
+
return;
|
|
16514
|
+
}
|
|
16515
|
+
this.renderSystem.loadSplatsFromPackedData(splatData);
|
|
16516
|
+
this.renderSystem.renderFrame();
|
|
16517
|
+
}
|
|
16298
16518
|
/**
|
|
16299
16519
|
* Render realtime frame (called by playback layer callback)
|
|
16300
16520
|
* @internal
|
|
@@ -16303,13 +16523,12 @@ class AvatarView {
|
|
|
16303
16523
|
if (!this.renderSystem || this.renderingState !== "speaking") {
|
|
16304
16524
|
return;
|
|
16305
16525
|
}
|
|
16306
|
-
this.renderSystem.loadSplatsFromPackedData(splatData);
|
|
16307
|
-
this.renderSystem.renderFrame();
|
|
16308
16526
|
this.lastRenderedFrameIndex = frameIndex;
|
|
16309
16527
|
if (frameIndex >= 0 && frameIndex < this.currentKeyframes.length) {
|
|
16310
16528
|
this.lastRealtimeProtoFrame = this.currentKeyframes[frameIndex];
|
|
16311
16529
|
this.currentPlayingFrame = this.lastRealtimeProtoFrame;
|
|
16312
16530
|
}
|
|
16531
|
+
this.doRender(splatData);
|
|
16313
16532
|
}
|
|
16314
16533
|
/**
|
|
16315
16534
|
* State transition method
|
|
@@ -16676,8 +16895,7 @@ class AvatarView {
|
|
|
16676
16895
|
this.characterHandle ?? void 0
|
|
16677
16896
|
);
|
|
16678
16897
|
if (splatData) {
|
|
16679
|
-
this.
|
|
16680
|
-
this.renderSystem.renderFrame();
|
|
16898
|
+
this.doRender(splatData);
|
|
16681
16899
|
}
|
|
16682
16900
|
} catch (error) {
|
|
16683
16901
|
logger.error("[AvatarView] Failed to render flame:", error instanceof Error ? error.message : String(error));
|
|
@@ -16714,7 +16932,7 @@ class AvatarView {
|
|
|
16714
16932
|
const aligned = this.alignFlamePair(fromFrame, toFrame);
|
|
16715
16933
|
const alignedFrom = aligned.from;
|
|
16716
16934
|
const alignedTo = aligned.to;
|
|
16717
|
-
const fps =
|
|
16935
|
+
const fps = FLAME_FRAME_RATE;
|
|
16718
16936
|
const durationMs = frameCount / fps * 1e3;
|
|
16719
16937
|
const transitionFrames = useLinear ? generateTransitionFramesLinear(alignedFrom, alignedTo, durationMs, fps) : generateTransitionFrames(alignedFrom, alignedTo, durationMs, fps);
|
|
16720
16938
|
transitionFrames[0] = alignedFrom;
|
|
@@ -16746,6 +16964,76 @@ class AvatarView {
|
|
|
16746
16964
|
this.renderSystem.handleResize();
|
|
16747
16965
|
}
|
|
16748
16966
|
}
|
|
16967
|
+
/**
|
|
16968
|
+
* Pause rendering loop
|
|
16969
|
+
*
|
|
16970
|
+
* When called:
|
|
16971
|
+
* - Rendering loop stops (no GPU/canvas updates)
|
|
16972
|
+
* - Audio playback continues normally
|
|
16973
|
+
* - Animation state machine continues running
|
|
16974
|
+
*
|
|
16975
|
+
* Use `resumeRendering()` to resume rendering.
|
|
16976
|
+
*
|
|
16977
|
+
* @example
|
|
16978
|
+
* // Stop rendering to save GPU resources (audio continues)
|
|
16979
|
+
* avatarView.pauseRendering()
|
|
16980
|
+
*/
|
|
16981
|
+
pauseRendering() {
|
|
16982
|
+
if (!this._renderingEnabled) {
|
|
16983
|
+
return;
|
|
16984
|
+
}
|
|
16985
|
+
this._renderingEnabled = false;
|
|
16986
|
+
logger.log("[AvatarView] Rendering paused");
|
|
16987
|
+
}
|
|
16988
|
+
/**
|
|
16989
|
+
* Resume rendering loop
|
|
16990
|
+
*
|
|
16991
|
+
* When called:
|
|
16992
|
+
* - Rendering loop resumes from current state
|
|
16993
|
+
* - If in Idle state, immediately renders current frame to restore display
|
|
16994
|
+
*
|
|
16995
|
+
* @example
|
|
16996
|
+
* // Resume rendering
|
|
16997
|
+
* avatarView.resumeRendering()
|
|
16998
|
+
*/
|
|
16999
|
+
resumeRendering() {
|
|
17000
|
+
if (this._renderingEnabled) {
|
|
17001
|
+
return;
|
|
17002
|
+
}
|
|
17003
|
+
this._renderingEnabled = true;
|
|
17004
|
+
logger.log("[AvatarView] Rendering resumed");
|
|
17005
|
+
if (this.isInitialized && this.renderSystem && this.renderingState === "idle") {
|
|
17006
|
+
this.renderCurrentIdleFrame();
|
|
17007
|
+
}
|
|
17008
|
+
}
|
|
17009
|
+
/**
|
|
17010
|
+
* Check if rendering is currently enabled
|
|
17011
|
+
* @returns true if rendering is enabled, false if paused
|
|
17012
|
+
*/
|
|
17013
|
+
isRenderingEnabled() {
|
|
17014
|
+
return this._renderingEnabled;
|
|
17015
|
+
}
|
|
17016
|
+
/**
|
|
17017
|
+
* Render current idle frame immediately
|
|
17018
|
+
* @internal
|
|
17019
|
+
*/
|
|
17020
|
+
async renderCurrentIdleFrame() {
|
|
17021
|
+
const avatarCore = AvatarSDK.getAvatarCore();
|
|
17022
|
+
if (!avatarCore || !this.renderSystem) {
|
|
17023
|
+
return;
|
|
17024
|
+
}
|
|
17025
|
+
try {
|
|
17026
|
+
const splatData = await avatarCore.computeCompleteFrameFlat(
|
|
17027
|
+
{ frameIndex: this.idleCurrentFrameIndex },
|
|
17028
|
+
this.characterHandle ?? void 0
|
|
17029
|
+
);
|
|
17030
|
+
if (splatData) {
|
|
17031
|
+
this.doRender(splatData);
|
|
17032
|
+
}
|
|
17033
|
+
} catch (error) {
|
|
17034
|
+
logger.warn("[AvatarView] Failed to render current idle frame:", error);
|
|
17035
|
+
}
|
|
17036
|
+
}
|
|
16749
17037
|
/**
|
|
16750
17038
|
* 获取渲染性能统计
|
|
16751
17039
|
* @returns 渲染性能统计数据,如果渲染系统未初始化则返回 null
|
|
@@ -16786,7 +17074,7 @@ class AvatarView {
|
|
|
16786
17074
|
const { x: x2, y: y2, scale } = value;
|
|
16787
17075
|
logger.log(`[AvatarView] Setting transform: x=${x2}, y=${y2}, scale=${scale}`);
|
|
16788
17076
|
this.renderSystem.setTransform(x2, y2, scale);
|
|
16789
|
-
if (this.isInitialized && this.renderSystem) {
|
|
17077
|
+
if (this.isInitialized && this.renderSystem && this._renderingEnabled) {
|
|
16790
17078
|
this.renderSystem.renderFrame();
|
|
16791
17079
|
}
|
|
16792
17080
|
}
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Avatar SDK 内部共享常量
|
|
3
|
+
* 跨平台统一配置,确保行为一致
|
|
4
|
+
*
|
|
5
|
+
* 对应:
|
|
6
|
+
* - Android: AvatarPlayer.kt 中的 companion object
|
|
7
|
+
* - iOS: (待实现)
|
|
8
|
+
*
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
/** 帧间隔 (毫秒) */
|
|
12
|
+
export declare const FLAME_FRAME_INTERVAL_MS: number;
|
|
13
|
+
/** 开始过渡时长 (秒): idle -> speaking */
|
|
14
|
+
export declare const START_TRANSITION_DURATION_S = 0.2;
|
|
15
|
+
/** 结束过渡时长 (秒): speaking -> idle */
|
|
16
|
+
export declare const END_TRANSITION_DURATION_S = 1.6;
|
|
17
|
+
/** 开始过渡时长 (毫秒) */
|
|
18
|
+
export declare const START_TRANSITION_DURATION_MS: number;
|
|
19
|
+
/** 结束过渡时长 (毫秒) */
|
|
20
|
+
export declare const END_TRANSITION_DURATION_MS: number;
|
|
21
|
+
/** 默认音频采样率 (Hz) */
|
|
22
|
+
export declare const AUDIO_SAMPLE_RATE = 16000;
|
|
23
|
+
/** 音频声道数 (mono) */
|
|
24
|
+
export declare const AUDIO_CHANNELS = 1;
|
|
25
|
+
/** 每样本字节数 (16bit PCM) */
|
|
26
|
+
export declare const AUDIO_BYTES_PER_SAMPLE = 2;
|
|
27
|
+
/** 每秒音频字节数 */
|
|
28
|
+
export declare const AUDIO_BYTES_PER_SECOND: number;
|
|
29
|
+
/**
|
|
30
|
+
* 各部位插值曲线控制点
|
|
31
|
+
* 格式: [x1, y1, x2, y2] 对应 cubic-bezier(x1, y1, x2, y2)
|
|
32
|
+
*
|
|
33
|
+
* 与 Android FlameFrameData.kt 中的 BEZIER_CURVES 完全一致
|
|
34
|
+
*/
|
|
35
|
+
export declare const BEZIER_CURVES: {
|
|
36
|
+
/** 下颌: 快速启动,平滑停止 */
|
|
37
|
+
readonly jaw: readonly [0.2, 0.8, 0.3, 1];
|
|
38
|
+
/** 表情: 平滑 S 曲线 */
|
|
39
|
+
readonly expression: readonly [0.4, 0, 0.2, 1];
|
|
40
|
+
/** 眼部: 柔和 S 曲线 */
|
|
41
|
+
readonly eye: readonly [0.3, 0, 0.1, 1];
|
|
42
|
+
/** 颈部: 慢启动,惯性停止 */
|
|
43
|
+
readonly neck: readonly [0.1, 0.2, 0.2, 1];
|
|
44
|
+
/** 全局: 标准 ease-in-out */
|
|
45
|
+
readonly global: readonly [0.42, 0, 0.58, 1];
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* 各部位时间缩放因子
|
|
49
|
+
* 控制各部位完成过渡所需的相对时间
|
|
50
|
+
*
|
|
51
|
+
* 与 Android FlameFrameData.kt 中的 TIME_SCALE 完全一致
|
|
52
|
+
*/
|
|
53
|
+
export declare const TIME_SCALE: {
|
|
54
|
+
/** 下颌: 40% 时间完成 (1/2.5) */
|
|
55
|
+
readonly jaw: 2.5;
|
|
56
|
+
/** 表情: 62.5% 时间完成 (1/1.6) */
|
|
57
|
+
readonly expression: 1.6;
|
|
58
|
+
/** 眼部: 77% 时间完成 (1/1.3) */
|
|
59
|
+
readonly eye: 1.3;
|
|
60
|
+
/** 颈部: 100% 时间完成 */
|
|
61
|
+
readonly neck: 1;
|
|
62
|
+
/** 全局: 100% 时间完成 */
|
|
63
|
+
readonly global: 1;
|
|
64
|
+
};
|
|
65
|
+
export type BezierCurveKey = keyof typeof BEZIER_CURVES;
|
|
66
|
+
export type TimeScaleKey = keyof typeof TIME_SCALE;
|
|
67
|
+
export declare const AvatarConstants: {
|
|
68
|
+
readonly FLAME_FRAME_RATE: 25;
|
|
69
|
+
readonly FLAME_FRAME_INTERVAL_MS: number;
|
|
70
|
+
readonly START_TRANSITION_DURATION_S: 0.2;
|
|
71
|
+
readonly END_TRANSITION_DURATION_S: 1.6;
|
|
72
|
+
readonly START_TRANSITION_DURATION_MS: number;
|
|
73
|
+
readonly END_TRANSITION_DURATION_MS: number;
|
|
74
|
+
readonly AUDIO_SAMPLE_RATE: 16000;
|
|
75
|
+
readonly AUDIO_CHANNELS: 1;
|
|
76
|
+
readonly AUDIO_BYTES_PER_SAMPLE: 2;
|
|
77
|
+
readonly AUDIO_BYTES_PER_SECOND: number;
|
|
78
|
+
readonly BEZIER_CURVES: {
|
|
79
|
+
/** 下颌: 快速启动,平滑停止 */
|
|
80
|
+
readonly jaw: readonly [0.2, 0.8, 0.3, 1];
|
|
81
|
+
/** 表情: 平滑 S 曲线 */
|
|
82
|
+
readonly expression: readonly [0.4, 0, 0.2, 1];
|
|
83
|
+
/** 眼部: 柔和 S 曲线 */
|
|
84
|
+
readonly eye: readonly [0.3, 0, 0.1, 1];
|
|
85
|
+
/** 颈部: 慢启动,惯性停止 */
|
|
86
|
+
readonly neck: readonly [0.1, 0.2, 0.2, 1];
|
|
87
|
+
/** 全局: 标准 ease-in-out */
|
|
88
|
+
readonly global: readonly [0.42, 0, 0.58, 1];
|
|
89
|
+
};
|
|
90
|
+
readonly TIME_SCALE: {
|
|
91
|
+
/** 下颌: 40% 时间完成 (1/2.5) */
|
|
92
|
+
readonly jaw: 2.5;
|
|
93
|
+
/** 表情: 62.5% 时间完成 (1/1.6) */
|
|
94
|
+
readonly expression: 1.6;
|
|
95
|
+
/** 眼部: 77% 时间完成 (1/1.3) */
|
|
96
|
+
readonly eye: 1.3;
|
|
97
|
+
/** 颈部: 100% 时间完成 */
|
|
98
|
+
readonly neck: 1;
|
|
99
|
+
/** 全局: 100% 时间完成 */
|
|
100
|
+
readonly global: 1;
|
|
101
|
+
};
|
|
102
|
+
};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Animation Interpolation Utilities (SDK)
|
|
3
3
|
* Layered interpolation with different timing for each facial component
|
|
4
|
-
* Aligned with app implementation and iOS behavior
|
|
4
|
+
* Aligned with app implementation and iOS/Android behavior
|
|
5
|
+
*
|
|
6
|
+
* 贝塞尔曲线和时间缩放配置与 Android FlameFrameData.kt 保持一致
|
|
5
7
|
*/
|
|
6
8
|
export {};
|
package/package.json
CHANGED