@spatialwalk/avatarkit 1.0.0-beta.58 → 1.0.0-beta.59
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 +8 -5
- package/dist/{StreamingAudioPlayer-Cka1VDtX.js → StreamingAudioPlayer-BbUBoPqm.js} +85 -5
- package/dist/audio/StreamingAudioPlayer.d.ts +3 -0
- package/dist/avatar_core_wasm.wasm +0 -0
- package/dist/config/sdk-config-loader.d.ts +2 -0
- package/dist/core/AvatarView.d.ts +5 -3
- package/dist/{index-wQJM-Vug.js → index-B0MJ_hCJ.js} +99 -115
- package/dist/index.js +1 -1
- package/dist/utils/animation-interpolation.d.ts +0 -8
- package/package.json +12 -13
package/CHANGELOG.md
CHANGED
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [1.0.0-beta.
|
|
5
|
+
## [1.0.0-beta.59] - 2026-01-14
|
|
6
|
+
|
|
7
|
+
### 🐛 Bugfixes
|
|
8
|
+
- **AudioContext Auto-Resume** - Fixed issue where audio playback would get stuck after external applications request recording permissions. AudioContext now automatically resumes when suspended during playback
|
|
9
|
+
- **PostHog Instance Initialization** - Fixed PostHog named instance initialization to avoid conflicts with external PostHog instances
|
|
10
|
+
|
|
11
|
+
## [1.0.0-beta.58] - 2026-01-14
|
|
6
12
|
|
|
7
13
|
### ✨ New Features
|
|
8
|
-
- **
|
|
9
|
-
- `isStart: true` configures the start transition duration (Idle -> Speaking), default 400ms
|
|
10
|
-
- `isStart: false` configures the end transition duration (Speaking -> Idle), default 1600ms
|
|
11
|
-
- Allows external applications to customize transition animation timing
|
|
14
|
+
- **Pure Rendering Mode APIs** - Added `renderFlame()` and `generateTransitionFromIdle()` methods to `AvatarView` for external-controlled rendering without audio synchronization
|
|
12
15
|
|
|
13
16
|
## [1.0.0-beta.57] - 2026-01-09
|
|
14
17
|
|
|
@@ -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,
|
|
4
|
+
import { A as APP_CONFIG, l as logger, e as errorToMessage, a as logEvent } from "./index-B0MJ_hCJ.js";
|
|
5
5
|
class StreamingAudioPlayer {
|
|
6
6
|
constructor(options) {
|
|
7
7
|
__publicField(this, "audioContext", null);
|
|
@@ -27,6 +27,8 @@ class StreamingAudioPlayer {
|
|
|
27
27
|
__publicField(this, "gainNode", null);
|
|
28
28
|
__publicField(this, "volume", 1);
|
|
29
29
|
__publicField(this, "onEndedCallback");
|
|
30
|
+
__publicField(this, "stateChangeHandler");
|
|
31
|
+
__publicField(this, "isResuming", false);
|
|
30
32
|
this.sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
31
33
|
this.sampleRate = (options == null ? void 0 : options.sampleRate) ?? APP_CONFIG.audio.sampleRate;
|
|
32
34
|
this.channelCount = (options == null ? void 0 : options.channelCount) ?? 1;
|
|
@@ -46,6 +48,15 @@ class StreamingAudioPlayer {
|
|
|
46
48
|
if (this.audioContext.state === "suspended") {
|
|
47
49
|
await this.audioContext.resume();
|
|
48
50
|
}
|
|
51
|
+
this.stateChangeHandler = (event) => {
|
|
52
|
+
const context = event.target;
|
|
53
|
+
if (context.state === "suspended" && this.isPlaying && !this.isPaused) {
|
|
54
|
+
this.ensureAudioContextRunning().catch((err) => {
|
|
55
|
+
logger.errorWithError("[StreamingAudioPlayer] Failed to auto-resume AudioContext after external suspend:", err);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
this.audioContext.addEventListener("statechange", this.stateChangeHandler);
|
|
49
60
|
this.log("AudioContext initialized", {
|
|
50
61
|
sessionId: this.sessionId,
|
|
51
62
|
sampleRate: this.audioContext.sampleRate,
|
|
@@ -61,11 +72,64 @@ class StreamingAudioPlayer {
|
|
|
61
72
|
throw error instanceof Error ? error : new Error(message);
|
|
62
73
|
}
|
|
63
74
|
}
|
|
75
|
+
async ensureAudioContextRunning() {
|
|
76
|
+
if (!this.audioContext) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const state = this.audioContext.state;
|
|
80
|
+
if (state === "running") {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (state === "closed") {
|
|
84
|
+
this.log("AudioContext is closed, cannot resume", {
|
|
85
|
+
sessionId: this.sessionId,
|
|
86
|
+
state
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (state === "suspended" && this.isPlaying && !this.isPaused) {
|
|
91
|
+
if (this.isResuming) {
|
|
92
|
+
this.log("AudioContext resume already in progress, skipping duplicate request", {
|
|
93
|
+
sessionId: this.sessionId,
|
|
94
|
+
state
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
this.isResuming = true;
|
|
99
|
+
try {
|
|
100
|
+
this.log("AudioContext is suspended during playback, resuming...", {
|
|
101
|
+
sessionId: this.sessionId,
|
|
102
|
+
state,
|
|
103
|
+
isPlaying: this.isPlaying,
|
|
104
|
+
isPaused: this.isPaused
|
|
105
|
+
});
|
|
106
|
+
await this.audioContext.resume();
|
|
107
|
+
this.log("AudioContext resumed successfully", {
|
|
108
|
+
sessionId: this.sessionId,
|
|
109
|
+
state: this.audioContext.state
|
|
110
|
+
});
|
|
111
|
+
} catch (err) {
|
|
112
|
+
logger.errorWithError("[StreamingAudioPlayer] Failed to resume AudioContext:", err);
|
|
113
|
+
logEvent("character_player", "error", {
|
|
114
|
+
sessionId: this.sessionId,
|
|
115
|
+
event: "audio_context_resume_failed",
|
|
116
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
117
|
+
});
|
|
118
|
+
} finally {
|
|
119
|
+
this.isResuming = false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
64
123
|
addChunk(pcmData, isLast = false) {
|
|
65
124
|
if (!this.audioContext) {
|
|
66
125
|
logger.error("AudioContext not initialized");
|
|
67
126
|
return;
|
|
68
127
|
}
|
|
128
|
+
if (this.isPlaying && !this.isPaused && this.audioContext.state === "suspended") {
|
|
129
|
+
this.ensureAudioContextRunning().catch((err) => {
|
|
130
|
+
logger.errorWithError("[StreamingAudioPlayer] Failed to ensure AudioContext running in addChunk:", err);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
69
133
|
this.audioChunks.push({ data: pcmData, isLast });
|
|
70
134
|
this.log(`Added chunk ${this.audioChunks.length}`, {
|
|
71
135
|
size: pcmData.length,
|
|
@@ -83,7 +147,9 @@ class StreamingAudioPlayer {
|
|
|
83
147
|
}
|
|
84
148
|
if (!this.isPlaying && this.autoStartEnabled && this.audioChunks.length > 0) {
|
|
85
149
|
this.log("[StreamingAudioPlayer] Auto-starting playback from addChunk");
|
|
86
|
-
this.startPlayback()
|
|
150
|
+
this.startPlayback().catch((err) => {
|
|
151
|
+
logger.errorWithError("[StreamingAudioPlayer] Failed to start playback from addChunk:", err);
|
|
152
|
+
});
|
|
87
153
|
} else if (this.isPlaying && !this.isPaused) {
|
|
88
154
|
this.log("[StreamingAudioPlayer] Already playing, scheduling next chunk");
|
|
89
155
|
this.scheduleNextChunk();
|
|
@@ -107,7 +173,7 @@ class StreamingAudioPlayer {
|
|
|
107
173
|
this.addChunk(chunk.data, chunk.isLast);
|
|
108
174
|
}
|
|
109
175
|
}
|
|
110
|
-
startPlayback() {
|
|
176
|
+
async startPlayback() {
|
|
111
177
|
if (!this.audioContext) {
|
|
112
178
|
this.log("[StreamingAudioPlayer] Cannot start playback: AudioContext not initialized");
|
|
113
179
|
return;
|
|
@@ -116,6 +182,7 @@ class StreamingAudioPlayer {
|
|
|
116
182
|
this.log("[StreamingAudioPlayer] Cannot start playback: Already playing");
|
|
117
183
|
return;
|
|
118
184
|
}
|
|
185
|
+
await this.ensureAudioContextRunning();
|
|
119
186
|
this.isPlaying = true;
|
|
120
187
|
this.sessionStartTime = this.audioContext.currentTime;
|
|
121
188
|
this.scheduledTime = this.sessionStartTime;
|
|
@@ -126,7 +193,8 @@ class StreamingAudioPlayer {
|
|
|
126
193
|
sessionStartTime: this.sessionStartTime,
|
|
127
194
|
bufferedChunks: this.audioChunks.length,
|
|
128
195
|
scheduledChunks: this.scheduledChunks,
|
|
129
|
-
activeSources: this.activeSources.size
|
|
196
|
+
activeSources: this.activeSources.size,
|
|
197
|
+
audioContextState: this.audioContext.state
|
|
130
198
|
});
|
|
131
199
|
this.scheduleAllChunks();
|
|
132
200
|
}
|
|
@@ -144,6 +212,11 @@ class StreamingAudioPlayer {
|
|
|
144
212
|
this.log("[StreamingAudioPlayer] Cannot schedule chunk: Not playing or paused");
|
|
145
213
|
return;
|
|
146
214
|
}
|
|
215
|
+
if (this.audioContext.state === "suspended") {
|
|
216
|
+
this.ensureAudioContextRunning().catch((err) => {
|
|
217
|
+
logger.errorWithError("[StreamingAudioPlayer] Failed to ensure AudioContext running in scheduleNextChunk:", err);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
147
220
|
const chunkIndex = this.scheduledChunks;
|
|
148
221
|
if (chunkIndex >= this.audioChunks.length) {
|
|
149
222
|
this.log(`[StreamingAudioPlayer] No more chunks to schedule (chunkIndex: ${chunkIndex}, totalChunks: ${this.audioChunks.length})`);
|
|
@@ -345,6 +418,7 @@ class StreamingAudioPlayer {
|
|
|
345
418
|
}
|
|
346
419
|
this.isPlaying = false;
|
|
347
420
|
this.isPaused = false;
|
|
421
|
+
this.isResuming = false;
|
|
348
422
|
this.sessionStartTime = 0;
|
|
349
423
|
this.scheduledTime = 0;
|
|
350
424
|
for (const source of this.activeSources) {
|
|
@@ -373,7 +447,9 @@ class StreamingAudioPlayer {
|
|
|
373
447
|
return;
|
|
374
448
|
}
|
|
375
449
|
this.autoStartEnabled = true;
|
|
376
|
-
this.startPlayback()
|
|
450
|
+
this.startPlayback().catch((err) => {
|
|
451
|
+
logger.errorWithError("[StreamingAudioPlayer] Failed to start playback from play():", err);
|
|
452
|
+
});
|
|
377
453
|
}
|
|
378
454
|
markEnded() {
|
|
379
455
|
var _a;
|
|
@@ -389,6 +465,10 @@ class StreamingAudioPlayer {
|
|
|
389
465
|
}
|
|
390
466
|
dispose() {
|
|
391
467
|
this.stop();
|
|
468
|
+
if (this.audioContext && this.stateChangeHandler) {
|
|
469
|
+
this.audioContext.removeEventListener("statechange", this.stateChangeHandler);
|
|
470
|
+
this.stateChangeHandler = void 0;
|
|
471
|
+
}
|
|
392
472
|
if (this.audioContext) {
|
|
393
473
|
this.audioContext.close();
|
|
394
474
|
this.audioContext = null;
|
|
@@ -27,8 +27,11 @@ export declare class StreamingAudioPlayer {
|
|
|
27
27
|
private gainNode;
|
|
28
28
|
private volume;
|
|
29
29
|
private onEndedCallback?;
|
|
30
|
+
private stateChangeHandler?;
|
|
31
|
+
private isResuming;
|
|
30
32
|
constructor(options?: StreamingAudioPlayerOptions);
|
|
31
33
|
initialize(): Promise<void>;
|
|
34
|
+
private ensureAudioContextRunning;
|
|
32
35
|
addChunk(pcmData: Uint8Array, isLast?: boolean): void;
|
|
33
36
|
startNewSession(audioChunks: Array<{
|
|
34
37
|
data: Uint8Array;
|
|
File without changes
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Environment } from '../types';
|
|
2
2
|
|
|
3
|
+
export declare const DEFAULT_SDK_CONFIG: Partial<Record<Environment, string>>;
|
|
4
|
+
|
|
3
5
|
export declare function fetchSdkConfig(version: string): Promise<Partial<Record<Environment, string>>>;
|
|
4
6
|
|
|
5
7
|
export declare function clearSdkConfigCache(): void;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Flame } from '../generated/driveningress/v1/driveningress';
|
|
1
2
|
import { Avatar } from './Avatar';
|
|
2
3
|
import { AvatarController } from './AvatarController';
|
|
3
4
|
export declare class AvatarView {
|
|
@@ -21,12 +22,12 @@ export declare class AvatarView {
|
|
|
21
22
|
private currentFPS;
|
|
22
23
|
private transitionKeyframes;
|
|
23
24
|
private transitionStartTime;
|
|
24
|
-
private
|
|
25
|
-
private endTransitionDurationMs;
|
|
25
|
+
private readonly transitionDurationMs;
|
|
26
26
|
private cachedIdleFirstFrame;
|
|
27
27
|
private idleCurrentFrameIndex;
|
|
28
28
|
private characterHandle;
|
|
29
29
|
private characterId;
|
|
30
|
+
private isPureRenderingMode;
|
|
30
31
|
private alignFlamePair;
|
|
31
32
|
private generateAndAlignTransitionFrames;
|
|
32
33
|
private getCachedIdleFirstFrame;
|
|
@@ -57,7 +58,8 @@ export declare class AvatarView {
|
|
|
57
58
|
private startRealtimeRendering;
|
|
58
59
|
private stopRealtimeRendering;
|
|
59
60
|
dispose(): void;
|
|
60
|
-
|
|
61
|
+
renderFlame(flame: Flame, enableIdleRendering?: boolean): Promise<void>;
|
|
62
|
+
generateTransitionFromIdle(toFlame: Flame, frameCount: number): Promise<Flame[]>;
|
|
61
63
|
private rerenderCurrentFrameWithNewCamera;
|
|
62
64
|
private handleResize;
|
|
63
65
|
get transform(): {
|
|
@@ -7397,19 +7397,7 @@ let isInitialized = false;
|
|
|
7397
7397
|
const SDK_POSTHOG_INSTANCE_NAME = "spatialwalk-posthog";
|
|
7398
7398
|
let sdkPosthogInstance = null;
|
|
7399
7399
|
const eventQueue = [];
|
|
7400
|
-
const FILTERED_HOSTNAMES = ["localhost", "127.0.0.1", "0.0.0.0"];
|
|
7401
|
-
function shouldFilterHostname() {
|
|
7402
|
-
if (typeof window === "undefined") {
|
|
7403
|
-
return false;
|
|
7404
|
-
}
|
|
7405
|
-
const hostname = window.location.hostname;
|
|
7406
|
-
return FILTERED_HOSTNAMES.includes(hostname);
|
|
7407
|
-
}
|
|
7408
7400
|
function initializePostHog(environment, version) {
|
|
7409
|
-
if (shouldFilterHostname()) {
|
|
7410
|
-
logger.log(`[PostHog] Tracking disabled due to filtered hostname: ${window.location.hostname}`);
|
|
7411
|
-
return;
|
|
7412
|
-
}
|
|
7413
7401
|
const { host, apiKey, disableCompression } = getPostHogConfig(environment);
|
|
7414
7402
|
if (!apiKey) {
|
|
7415
7403
|
logger.warn(`[PostHog] API Key not configured for environment: ${environment}, tracking disabled`);
|
|
@@ -7425,7 +7413,6 @@ function initializePostHog(environment, version) {
|
|
|
7425
7413
|
const logContext = idManager.getLogContext();
|
|
7426
7414
|
Vo.init(apiKey, {
|
|
7427
7415
|
api_host: host,
|
|
7428
|
-
name: SDK_POSTHOG_INSTANCE_NAME,
|
|
7429
7416
|
person_profiles: "identified_only",
|
|
7430
7417
|
capture_pageview: false,
|
|
7431
7418
|
capture_pageleave: false,
|
|
@@ -7455,7 +7442,7 @@ function initializePostHog(environment, version) {
|
|
|
7455
7442
|
}
|
|
7456
7443
|
flushEventQueue();
|
|
7457
7444
|
}
|
|
7458
|
-
});
|
|
7445
|
+
}, SDK_POSTHOG_INSTANCE_NAME);
|
|
7459
7446
|
} catch (error) {
|
|
7460
7447
|
logger.warn("[PostHog] Failed to initialize:", error instanceof Error ? error.message : String(error));
|
|
7461
7448
|
}
|
|
@@ -7505,9 +7492,6 @@ function flushEventQueue() {
|
|
|
7505
7492
|
}, 0);
|
|
7506
7493
|
}
|
|
7507
7494
|
function trackEvent(event, level = "info", contents = {}) {
|
|
7508
|
-
if (shouldFilterHostname()) {
|
|
7509
|
-
return;
|
|
7510
|
-
}
|
|
7511
7495
|
const instance = getSdkPosthogInstance();
|
|
7512
7496
|
if (!instance) {
|
|
7513
7497
|
eventQueue.push({ event, level, contents });
|
|
@@ -7639,7 +7623,7 @@ const _AnimationPlayer = class _AnimationPlayer {
|
|
|
7639
7623
|
if (this.streamingPlayer) {
|
|
7640
7624
|
return;
|
|
7641
7625
|
}
|
|
7642
|
-
const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-
|
|
7626
|
+
const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-BbUBoPqm.js");
|
|
7643
7627
|
const { AvatarSDK: AvatarSDK2 } = await Promise.resolve().then(() => AvatarSDK$1);
|
|
7644
7628
|
const audioFormat = AvatarSDK2.getAudioFormat();
|
|
7645
7629
|
this.streamingPlayer = new StreamingAudioPlayer({
|
|
@@ -7786,6 +7770,10 @@ const _AnimationPlayer = class _AnimationPlayer {
|
|
|
7786
7770
|
};
|
|
7787
7771
|
__publicField(_AnimationPlayer, "audioUnlocked", false);
|
|
7788
7772
|
let AnimationPlayer = _AnimationPlayer;
|
|
7773
|
+
const DEFAULT_SDK_CONFIG = {
|
|
7774
|
+
[Environment.cn]: "https://api.open.spatialwalk.top",
|
|
7775
|
+
[Environment.intl]: "https://api.intl.spatialwalk.cloud"
|
|
7776
|
+
};
|
|
7789
7777
|
const configCache = {
|
|
7790
7778
|
config: null,
|
|
7791
7779
|
promise: null
|
|
@@ -7824,10 +7812,7 @@ async function fetchSdkConfig(version) {
|
|
|
7824
7812
|
configCache.config = config;
|
|
7825
7813
|
logger.log(`[SdkConfigLoader] SDK config fetched successfully:`, config);
|
|
7826
7814
|
} catch (error) {
|
|
7827
|
-
configCache.config = {
|
|
7828
|
-
[Environment.cn]: "https://api.open.spatialwalk.top",
|
|
7829
|
-
[Environment.intl]: "https://api.intl.spatialwalk.cloud"
|
|
7830
|
-
};
|
|
7815
|
+
configCache.config = { ...DEFAULT_SDK_CONFIG };
|
|
7831
7816
|
logger.log("[SdkConfigLoader] Using default SDK config:", configCache.config);
|
|
7832
7817
|
} finally {
|
|
7833
7818
|
configCache.promise = null;
|
|
@@ -8887,6 +8872,7 @@ class AvatarSDK {
|
|
|
8887
8872
|
idManager.setAppId(appId);
|
|
8888
8873
|
logger.log(`[AvatarSDK] Client ID: ${idManager.getClientId()}`);
|
|
8889
8874
|
initializePostHog(configuration.environment, this._version);
|
|
8875
|
+
this._dynamicSdkConfig = { ...DEFAULT_SDK_CONFIG };
|
|
8890
8876
|
this._fetchSdkConfig().catch((err) => {
|
|
8891
8877
|
const message = err instanceof Error ? err.message : String(err);
|
|
8892
8878
|
logger.warn("Failed to fetch SDK config from remote, using defaults:", message);
|
|
@@ -9062,7 +9048,7 @@ class AvatarSDK {
|
|
|
9062
9048
|
}
|
|
9063
9049
|
__publicField(AvatarSDK, "_isInitialized", false);
|
|
9064
9050
|
__publicField(AvatarSDK, "_configuration", null);
|
|
9065
|
-
__publicField(AvatarSDK, "_version", "1.0.0-beta.
|
|
9051
|
+
__publicField(AvatarSDK, "_version", "1.0.0-beta.59");
|
|
9066
9052
|
__publicField(AvatarSDK, "_avatarCore", null);
|
|
9067
9053
|
__publicField(AvatarSDK, "_dynamicSdkConfig", null);
|
|
9068
9054
|
const AvatarSDK$1 = Object.freeze(Object.defineProperty({
|
|
@@ -11419,7 +11405,6 @@ class AvatarController {
|
|
|
11419
11405
|
return true;
|
|
11420
11406
|
}
|
|
11421
11407
|
const audioContextState = ((_c = streamingPlayer.audioContext) == null ? void 0 : _c.state) || "unknown";
|
|
11422
|
-
const isAudioContextSuspended = audioContextState === "suspended";
|
|
11423
11408
|
const activeSourcesCount = ((_d = streamingPlayer.activeSources) == null ? void 0 : _d.size) || 0;
|
|
11424
11409
|
const scheduledChunks = streamingPlayer.scheduledChunks || 0;
|
|
11425
11410
|
const hasScheduledButNoActive = scheduledChunks > 0 && activeSourcesCount === 0;
|
|
@@ -11434,7 +11419,7 @@ class AvatarController {
|
|
|
11434
11419
|
state.audioTimeStuckCount = 0;
|
|
11435
11420
|
}
|
|
11436
11421
|
state.lastAudioTime = audioTime;
|
|
11437
|
-
const shouldReport = state.audioTimeZeroCount > this.MAX_AUDIO_TIME_ZERO_COUNT || state.audioTimeStuckCount > this.MAX_AUDIO_TIME_STUCK_COUNT ||
|
|
11422
|
+
const shouldReport = state.audioTimeZeroCount > this.MAX_AUDIO_TIME_ZERO_COUNT || state.audioTimeStuckCount > this.MAX_AUDIO_TIME_STUCK_COUNT || hasScheduledButNoActive && this.isPlaying;
|
|
11438
11423
|
if (shouldReport && isNotPaused) {
|
|
11439
11424
|
state.reported = true;
|
|
11440
11425
|
logEvent("character_player", "error", {
|
|
@@ -11442,7 +11427,6 @@ class AvatarController {
|
|
|
11442
11427
|
avatar_id: this.avatar.id,
|
|
11443
11428
|
conversationId: ((_e2 = this.networkLayer) == null ? void 0 : _e2.getCurrentConversationId()) || void 0,
|
|
11444
11429
|
audioContextState,
|
|
11445
|
-
isAudioContextSuspended,
|
|
11446
11430
|
audioTime,
|
|
11447
11431
|
audioTimeZero: audioTime === 0,
|
|
11448
11432
|
audioTimeZeroCount: state.audioTimeZeroCount,
|
|
@@ -13936,82 +13920,31 @@ function lerpArrays(from, to2, progress) {
|
|
|
13936
13920
|
}
|
|
13937
13921
|
return result2;
|
|
13938
13922
|
}
|
|
13939
|
-
|
|
13940
|
-
|
|
13941
|
-
const cx = 3 * x1;
|
|
13942
|
-
const bx = 3 * (x2 - x1) - cx;
|
|
13943
|
-
const ax = 1 - cx - bx;
|
|
13944
|
-
const cy = 3 * y1;
|
|
13945
|
-
const by = 3 * (y2 - y1) - cy;
|
|
13946
|
-
const ay = 1 - cy - by;
|
|
13947
|
-
const sampleCurveX = (t2) => ((ax * t2 + bx) * t2 + cx) * t2;
|
|
13948
|
-
const sampleCurveY = (t2) => ((ay * t2 + by) * t2 + cy) * t2;
|
|
13949
|
-
const sampleCurveDerivativeX = (t2) => (3 * ax * t2 + 2 * bx) * t2 + cx;
|
|
13950
|
-
const solveCurveX = (x3) => {
|
|
13951
|
-
let t2 = x3;
|
|
13952
|
-
for (let i2 = 0; i2 < 8; i2++) {
|
|
13953
|
-
const error = sampleCurveX(t2) - x3;
|
|
13954
|
-
if (Math.abs(error) < 1e-6) break;
|
|
13955
|
-
const d2 = sampleCurveDerivativeX(t2);
|
|
13956
|
-
if (Math.abs(d2) < 1e-6) break;
|
|
13957
|
-
t2 -= error / d2;
|
|
13958
|
-
}
|
|
13959
|
-
return t2;
|
|
13960
|
-
};
|
|
13961
|
-
return (x3) => {
|
|
13962
|
-
if (x3 <= 0) return 0;
|
|
13963
|
-
if (x3 >= 1) return 1;
|
|
13964
|
-
return sampleCurveY(solveCurveX(x3));
|
|
13965
|
-
};
|
|
13966
|
-
}
|
|
13967
|
-
const BEZIER_CURVES = {
|
|
13968
|
-
jaw: createBezierEasing(0.2, 0.8, 0.3, 1),
|
|
13969
|
-
expression: createBezierEasing(0.4, 0, 0.2, 1),
|
|
13970
|
-
eye: createBezierEasing(0.3, 0, 0.1, 1),
|
|
13971
|
-
neck: createBezierEasing(0.1, 0.2, 0.2, 1),
|
|
13972
|
-
global: createBezierEasing(0.42, 0, 0.58, 1)
|
|
13973
|
-
};
|
|
13974
|
-
const TIME_SCALE = {
|
|
13975
|
-
jaw: 2.5,
|
|
13976
|
-
expression: 1.6,
|
|
13977
|
-
eye: 1.3,
|
|
13978
|
-
neck: 1,
|
|
13979
|
-
global: 1
|
|
13980
|
-
};
|
|
13981
|
-
function bezierLerp(from, to2, progress) {
|
|
13982
|
-
const getT = (key) => {
|
|
13983
|
-
const scaledProgress = clamp01(progress * TIME_SCALE[key]);
|
|
13984
|
-
return BEZIER_CURVES[key](scaledProgress);
|
|
13985
|
-
};
|
|
13923
|
+
function linearLerp(from, to2, progress) {
|
|
13924
|
+
const easedProgress = 0.5 - Math.cos(progress * Math.PI) * 0.5;
|
|
13986
13925
|
return {
|
|
13987
|
-
translation: lerpArrays(from.translation || [0, 0, 0], to2.translation || [0, 0, 0],
|
|
13988
|
-
rotation: lerpArrays(from.rotation || [0, 0, 0], to2.rotation || [0, 0, 0],
|
|
13989
|
-
neckPose: lerpArrays(from.neckPose || [0, 0, 0], to2.neckPose || [0, 0, 0],
|
|
13990
|
-
jawPose: lerpArrays(from.jawPose || [0, 0, 0], to2.jawPose || [0, 0, 0],
|
|
13991
|
-
eyePose: lerpArrays(from.eyePose || [0, 0, 0, 0, 0, 0], to2.eyePose || [0, 0, 0, 0, 0, 0],
|
|
13926
|
+
translation: lerpArrays(from.translation || [0, 0, 0], to2.translation || [0, 0, 0], easedProgress),
|
|
13927
|
+
rotation: lerpArrays(from.rotation || [0, 0, 0], to2.rotation || [0, 0, 0], easedProgress),
|
|
13928
|
+
neckPose: lerpArrays(from.neckPose || [0, 0, 0], to2.neckPose || [0, 0, 0], easedProgress),
|
|
13929
|
+
jawPose: lerpArrays(from.jawPose || [0, 0, 0], to2.jawPose || [0, 0, 0], easedProgress),
|
|
13930
|
+
eyePose: lerpArrays(from.eyePose || [0, 0, 0, 0, 0, 0], to2.eyePose || [0, 0, 0, 0, 0, 0], easedProgress),
|
|
13992
13931
|
eyeLid: (() => {
|
|
13993
13932
|
const fromEyelid = from.eyeLid;
|
|
13994
13933
|
const toEyelid = to2.eyeLid;
|
|
13995
|
-
if (
|
|
13996
|
-
return lerpArrays(fromEyelid, toEyelid,
|
|
13934
|
+
if (fromEyelid && fromEyelid.length > 0 && toEyelid && toEyelid.length > 0)
|
|
13935
|
+
return lerpArrays(fromEyelid, toEyelid, easedProgress);
|
|
13997
13936
|
return fromEyelid || toEyelid || [];
|
|
13998
13937
|
})(),
|
|
13999
|
-
expression: lerpArrays(from.expression || [], to2.expression || [],
|
|
13938
|
+
expression: lerpArrays(from.expression || [], to2.expression || [], easedProgress)
|
|
14000
13939
|
};
|
|
14001
13940
|
}
|
|
14002
13941
|
function generateTransitionFrames(from, to2, durationMs, fps = 25) {
|
|
14003
13942
|
const steps = Math.max(1, Math.floor(durationMs / 1e3 * fps));
|
|
14004
13943
|
const frames = Array.from({ length: steps });
|
|
14005
|
-
if (steps === 1) {
|
|
14006
|
-
frames[0] = to2;
|
|
14007
|
-
return frames;
|
|
14008
|
-
}
|
|
14009
13944
|
for (let i2 = 0; i2 < steps; i2++) {
|
|
14010
13945
|
const progress = i2 / (steps - 1);
|
|
14011
|
-
frames[i2] =
|
|
13946
|
+
frames[i2] = linearLerp(from, to2, progress);
|
|
14012
13947
|
}
|
|
14013
|
-
frames[0] = from;
|
|
14014
|
-
frames[frames.length - 1] = to2;
|
|
14015
13948
|
return frames;
|
|
14016
13949
|
}
|
|
14017
13950
|
class AvatarView {
|
|
@@ -14036,12 +13969,12 @@ class AvatarView {
|
|
|
14036
13969
|
__publicField(this, "currentFPS", 0);
|
|
14037
13970
|
__publicField(this, "transitionKeyframes", []);
|
|
14038
13971
|
__publicField(this, "transitionStartTime", 0);
|
|
14039
|
-
__publicField(this, "
|
|
14040
|
-
__publicField(this, "endTransitionDurationMs", 1600);
|
|
13972
|
+
__publicField(this, "transitionDurationMs", 400);
|
|
14041
13973
|
__publicField(this, "cachedIdleFirstFrame", null);
|
|
14042
13974
|
__publicField(this, "idleCurrentFrameIndex", 0);
|
|
14043
13975
|
__publicField(this, "characterHandle", null);
|
|
14044
13976
|
__publicField(this, "characterId");
|
|
13977
|
+
__publicField(this, "isPureRenderingMode", false);
|
|
14045
13978
|
this.avatar = avatar;
|
|
14046
13979
|
this.characterId = `${avatar.id}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
14047
13980
|
if (!AvatarSDK.configuration) {
|
|
@@ -14091,12 +14024,12 @@ class AvatarView {
|
|
|
14091
14024
|
toFixed.expression = ensureLen(toFixed.expression, exprLen);
|
|
14092
14025
|
return { from: fromFixed, to: toFixed };
|
|
14093
14026
|
}
|
|
14094
|
-
generateAndAlignTransitionFrames(from, to2
|
|
14027
|
+
generateAndAlignTransitionFrames(from, to2) {
|
|
14095
14028
|
const aligned = this.alignFlamePair(from, to2);
|
|
14096
14029
|
let keyframes = generateTransitionFrames(
|
|
14097
14030
|
aligned.from,
|
|
14098
14031
|
aligned.to,
|
|
14099
|
-
|
|
14032
|
+
this.transitionDurationMs,
|
|
14100
14033
|
APP_CONFIG.animation.fps
|
|
14101
14034
|
);
|
|
14102
14035
|
if (keyframes.length < 2) {
|
|
@@ -14387,6 +14320,10 @@ class AvatarView {
|
|
|
14387
14320
|
return;
|
|
14388
14321
|
}
|
|
14389
14322
|
lastTime = currentTime;
|
|
14323
|
+
if (this.isPureRenderingMode) {
|
|
14324
|
+
this.idleAnimationLoopId = requestAnimationFrame(renderFrame);
|
|
14325
|
+
return;
|
|
14326
|
+
}
|
|
14390
14327
|
const avatarCore = AvatarSDK.getAvatarCore();
|
|
14391
14328
|
if (!avatarCore) {
|
|
14392
14329
|
return;
|
|
@@ -14397,6 +14334,9 @@ class AvatarView {
|
|
|
14397
14334
|
if (this.renderingState !== "idle") {
|
|
14398
14335
|
return;
|
|
14399
14336
|
}
|
|
14337
|
+
if (this.isPureRenderingMode) {
|
|
14338
|
+
return;
|
|
14339
|
+
}
|
|
14400
14340
|
this.renderSystem.loadSplatsFromPackedData(splatData);
|
|
14401
14341
|
this.renderSystem.renderFrame();
|
|
14402
14342
|
}
|
|
@@ -14446,8 +14386,7 @@ class AvatarView {
|
|
|
14446
14386
|
return;
|
|
14447
14387
|
}
|
|
14448
14388
|
const elapsed = performance.now() - this.transitionStartTime;
|
|
14449
|
-
const
|
|
14450
|
-
const progress = Math.min(1, Math.max(0, elapsed / currentTransitionDurationMs));
|
|
14389
|
+
const progress = Math.min(1, Math.max(0, elapsed / this.transitionDurationMs));
|
|
14451
14390
|
const steps = this.transitionKeyframes.length;
|
|
14452
14391
|
const idx = Math.min(steps - 1, Math.floor(progress * (steps - 1)));
|
|
14453
14392
|
const currentFrame = this.transitionKeyframes[idx];
|
|
@@ -14473,7 +14412,7 @@ class AvatarView {
|
|
|
14473
14412
|
return;
|
|
14474
14413
|
}
|
|
14475
14414
|
}
|
|
14476
|
-
if (state === "transitioningToSpeaking" && this.transitionStartTime > 0 && this.transitionKeyframes.length > 0 && elapsed >= this.
|
|
14415
|
+
if (state === "transitioningToSpeaking" && this.transitionStartTime > 0 && this.transitionKeyframes.length > 0 && elapsed >= this.transitionDurationMs + 100) {
|
|
14477
14416
|
this.setState("speaking");
|
|
14478
14417
|
this.transitionKeyframes = [];
|
|
14479
14418
|
this.avatarController.onTransitionComplete();
|
|
@@ -14603,7 +14542,7 @@ class AvatarView {
|
|
|
14603
14542
|
await this.getCachedIdleFirstFrame();
|
|
14604
14543
|
const firstSpeaking = keyframes[0];
|
|
14605
14544
|
const firstSpeakingWithPostProcessing = this.avatarController.applyPostProcessingToFlame(firstSpeaking);
|
|
14606
|
-
this.transitionKeyframes = this.generateAndAlignTransitionFrames(idleFrameProto, firstSpeakingWithPostProcessing
|
|
14545
|
+
this.transitionKeyframes = this.generateAndAlignTransitionFrames(idleFrameProto, firstSpeakingWithPostProcessing);
|
|
14607
14546
|
this.transitionStartTime = performance.now();
|
|
14608
14547
|
if (this.transitionKeyframes.length === 0) {
|
|
14609
14548
|
this.setState("speaking");
|
|
@@ -14656,7 +14595,7 @@ class AvatarView {
|
|
|
14656
14595
|
const lastSpeaking = this.avatarController.applyPostProcessingToFlame(lastSpeakingRaw);
|
|
14657
14596
|
const idleFirstProto = await this.getCachedIdleFirstFrame();
|
|
14658
14597
|
if (idleFirstProto) {
|
|
14659
|
-
this.transitionKeyframes = this.generateAndAlignTransitionFrames(lastSpeaking, idleFirstProto
|
|
14598
|
+
this.transitionKeyframes = this.generateAndAlignTransitionFrames(lastSpeaking, idleFirstProto);
|
|
14660
14599
|
this.transitionStartTime = performance.now();
|
|
14661
14600
|
if (this.transitionKeyframes.length > 0 && this.renderingState === "transitioningToIdle") {
|
|
14662
14601
|
if (APP_CONFIG.debug)
|
|
@@ -14720,22 +14659,6 @@ class AvatarView {
|
|
|
14720
14659
|
if (APP_CONFIG.debug)
|
|
14721
14660
|
logger.log("[AvatarView] Avatar view disposed successfully");
|
|
14722
14661
|
}
|
|
14723
|
-
setTransitionDuration(isStart, durationMs) {
|
|
14724
|
-
if (durationMs <= 0) {
|
|
14725
|
-
throw new Error("Transition duration must be a positive number");
|
|
14726
|
-
}
|
|
14727
|
-
if (isStart) {
|
|
14728
|
-
this.startTransitionDurationMs = durationMs;
|
|
14729
|
-
if (APP_CONFIG.debug) {
|
|
14730
|
-
logger.log("[AvatarView] Start transition duration updated:", durationMs);
|
|
14731
|
-
}
|
|
14732
|
-
} else {
|
|
14733
|
-
this.endTransitionDurationMs = durationMs;
|
|
14734
|
-
if (APP_CONFIG.debug) {
|
|
14735
|
-
logger.log("[AvatarView] End transition duration updated:", durationMs);
|
|
14736
|
-
}
|
|
14737
|
-
}
|
|
14738
|
-
}
|
|
14739
14662
|
getCameraConfig() {
|
|
14740
14663
|
return this.cameraConfig;
|
|
14741
14664
|
}
|
|
@@ -14752,6 +14675,67 @@ class AvatarView {
|
|
|
14752
14675
|
}
|
|
14753
14676
|
}
|
|
14754
14677
|
}
|
|
14678
|
+
async renderFlame(flame, enableIdleRendering) {
|
|
14679
|
+
if (!this.isInitialized || !this.renderSystem) {
|
|
14680
|
+
throw new Error("AvatarView not initialized");
|
|
14681
|
+
}
|
|
14682
|
+
if (enableIdleRendering === true) {
|
|
14683
|
+
this.isPureRenderingMode = false;
|
|
14684
|
+
return;
|
|
14685
|
+
}
|
|
14686
|
+
this.isPureRenderingMode = true;
|
|
14687
|
+
try {
|
|
14688
|
+
const processedFlame = this.avatarController.applyPostProcessingToFlame(flame);
|
|
14689
|
+
const wasmParams = convertProtoFlameToWasmParams(processedFlame);
|
|
14690
|
+
const avatarCore = AvatarSDK.getAvatarCore();
|
|
14691
|
+
if (!avatarCore) {
|
|
14692
|
+
throw new Error("AvatarCore not available");
|
|
14693
|
+
}
|
|
14694
|
+
const splatData = await avatarCore.computeFrameFlatFromParams(
|
|
14695
|
+
wasmParams,
|
|
14696
|
+
this.characterHandle ?? void 0
|
|
14697
|
+
);
|
|
14698
|
+
if (splatData) {
|
|
14699
|
+
this.renderSystem.loadSplatsFromPackedData(splatData);
|
|
14700
|
+
this.renderSystem.renderFrame();
|
|
14701
|
+
}
|
|
14702
|
+
} catch (error) {
|
|
14703
|
+
logger.error("[AvatarView] Failed to render flame:", error instanceof Error ? error.message : String(error));
|
|
14704
|
+
throw error;
|
|
14705
|
+
}
|
|
14706
|
+
}
|
|
14707
|
+
async generateTransitionFromIdle(toFlame, frameCount) {
|
|
14708
|
+
if (!this.isInitialized) {
|
|
14709
|
+
throw new Error("AvatarView not initialized");
|
|
14710
|
+
}
|
|
14711
|
+
if (frameCount <= 0) {
|
|
14712
|
+
throw new Error("Frame count must be greater than 0");
|
|
14713
|
+
}
|
|
14714
|
+
const avatarCore = AvatarSDK.getAvatarCore();
|
|
14715
|
+
if (!avatarCore) {
|
|
14716
|
+
throw new Error("AvatarCore not available");
|
|
14717
|
+
}
|
|
14718
|
+
try {
|
|
14719
|
+
const idleParams = await avatarCore.getCurrentFrameParams(this.idleCurrentFrameIndex, this.characterId);
|
|
14720
|
+
const idleFrameProto = convertWasmParamsToProtoFlame(idleParams);
|
|
14721
|
+
const toFlameWithPostProcessing = this.avatarController.applyPostProcessingToFlame(toFlame);
|
|
14722
|
+
const aligned = this.alignFlamePair(idleFrameProto, toFlameWithPostProcessing);
|
|
14723
|
+
const fps = APP_CONFIG.animation.fps;
|
|
14724
|
+
const durationMs = frameCount / fps * 1e3;
|
|
14725
|
+
const transitionFrames = generateTransitionFrames(
|
|
14726
|
+
aligned.from,
|
|
14727
|
+
aligned.to,
|
|
14728
|
+
durationMs,
|
|
14729
|
+
fps
|
|
14730
|
+
);
|
|
14731
|
+
transitionFrames[0] = aligned.from;
|
|
14732
|
+
transitionFrames[transitionFrames.length - 1] = aligned.to;
|
|
14733
|
+
return transitionFrames;
|
|
14734
|
+
} catch (error) {
|
|
14735
|
+
logger.error("[AvatarView] Failed to generate transition from idle:", error instanceof Error ? error.message : String(error));
|
|
14736
|
+
throw error;
|
|
14737
|
+
}
|
|
14738
|
+
}
|
|
14755
14739
|
async rerenderCurrentFrameWithNewCamera() {
|
|
14756
14740
|
if (this.avatarController.state !== AvatarState.paused || this.renderingState !== "speaking" || !this.renderSystem) {
|
|
14757
14741
|
return;
|
|
@@ -14800,7 +14784,7 @@ export {
|
|
|
14800
14784
|
LogLevel$1 as L,
|
|
14801
14785
|
ResourceType as R,
|
|
14802
14786
|
SPAvatarError as S,
|
|
14803
|
-
|
|
14787
|
+
logEvent as a,
|
|
14804
14788
|
Avatar as b,
|
|
14805
14789
|
AvatarController as c,
|
|
14806
14790
|
AvatarSDK as d,
|
|
@@ -14811,6 +14795,6 @@ export {
|
|
|
14811
14795
|
ConversationState as i,
|
|
14812
14796
|
AvatarState as j,
|
|
14813
14797
|
ErrorCode as k,
|
|
14814
|
-
|
|
14798
|
+
logger as l,
|
|
14815
14799
|
extractResourceUrls as m
|
|
14816
14800
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,4 @@
|
|
|
1
1
|
import { Flame } from '../generated/driveningress/v1/driveningress';
|
|
2
|
-
export interface TransitionConfig {
|
|
3
|
-
duration: number;
|
|
4
|
-
delay: number;
|
|
5
|
-
easing: (t: number) => number;
|
|
6
|
-
}
|
|
7
|
-
export declare const TRANSITION_LAYERS: Record<string, TransitionConfig>;
|
|
8
|
-
|
|
9
|
-
export declare function bezierLerp(from: Flame, to: Flame, progress: number): Flame;
|
|
10
2
|
|
|
11
3
|
export declare function linearLerp(from: Flame, to: Flame, progress: number): Flame;
|
|
12
4
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spatialwalk/avatarkit",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.0-beta.
|
|
5
|
-
"packageManager": "pnpm@10.18.2",
|
|
4
|
+
"version": "1.0.0-beta.59",
|
|
6
5
|
"description": "SPAvatar SDK - 3D Gaussian Splatting Avatar Rendering SDK",
|
|
7
6
|
"author": "SPAvatar Team",
|
|
8
7
|
"license": "MIT",
|
|
@@ -32,16 +31,6 @@
|
|
|
32
31
|
"CHANGELOG.md",
|
|
33
32
|
"dist"
|
|
34
33
|
],
|
|
35
|
-
"scripts": {
|
|
36
|
-
"build": "SDK_BUILD=true vite build --mode library",
|
|
37
|
-
"dev": "vite build --mode library --watch",
|
|
38
|
-
"clean": "rm -rf dist",
|
|
39
|
-
"typecheck": "tsc --noEmit",
|
|
40
|
-
"test": "cd tests && pnpm test",
|
|
41
|
-
"test:watch": "cd tests && pnpm run test:watch",
|
|
42
|
-
"test:e2e": "cd tests && pnpm run test:e2e",
|
|
43
|
-
"test:perf": "cd tests && pnpm run test:perf"
|
|
44
|
-
},
|
|
45
34
|
"peerDependencies": {
|
|
46
35
|
"@webgpu/types": "*"
|
|
47
36
|
},
|
|
@@ -58,5 +47,15 @@
|
|
|
58
47
|
"typescript": "^5.0.0",
|
|
59
48
|
"vite": "^5.0.0",
|
|
60
49
|
"vite-plugin-dts": "^4.5.4"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "SDK_BUILD=true vite build --mode library",
|
|
53
|
+
"dev": "vite build --mode library --watch",
|
|
54
|
+
"clean": "rm -rf dist",
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
56
|
+
"test": "cd tests && pnpm test",
|
|
57
|
+
"test:watch": "cd tests && pnpm run test:watch",
|
|
58
|
+
"test:e2e": "cd tests && pnpm run test:e2e",
|
|
59
|
+
"test:perf": "cd tests && pnpm run test:perf"
|
|
61
60
|
}
|
|
62
|
-
}
|
|
61
|
+
}
|