@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 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.58] - 2026-01-12
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
- - **Transition Duration Control API** - Added `setTransitionDuration(isStart: boolean, durationMs: number)` method to `AvatarView` to configure transition animation durations:
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, e as errorToMessage, l as logEvent, a as logger } from "./index-wQJM-Vug.js";
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 startTransitionDurationMs;
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
- setTransitionDuration(isStart: boolean, durationMs: number): void;
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-Cka1VDtX.js");
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.58");
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 || isAudioContextSuspended && this.isPlaying || hasScheduledButNoActive && this.isPlaying;
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
- const clamp01 = (x2) => Math.max(0, Math.min(1, x2));
13940
- function createBezierEasing(x1, y1, x2, y2) {
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], getT("global")),
13988
- rotation: lerpArrays(from.rotation || [0, 0, 0], to2.rotation || [0, 0, 0], getT("global")),
13989
- neckPose: lerpArrays(from.neckPose || [0, 0, 0], to2.neckPose || [0, 0, 0], getT("neck")),
13990
- jawPose: lerpArrays(from.jawPose || [0, 0, 0], to2.jawPose || [0, 0, 0], getT("jaw")),
13991
- eyePose: lerpArrays(from.eyePose || [0, 0, 0, 0, 0, 0], to2.eyePose || [0, 0, 0, 0, 0, 0], getT("eye")),
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 ((fromEyelid == null ? void 0 : fromEyelid.length) && (toEyelid == null ? void 0 : toEyelid.length))
13996
- return lerpArrays(fromEyelid, toEyelid, getT("eye"));
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 || [], getT("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] = bezierLerp(from, to2, progress);
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, "startTransitionDurationMs", 400);
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, durationMs) {
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
- durationMs,
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 currentTransitionDurationMs = state === "transitioningToSpeaking" ? this.startTransitionDurationMs : this.endTransitionDurationMs;
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.startTransitionDurationMs + 100) {
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, this.startTransitionDurationMs);
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, this.endTransitionDurationMs);
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
- logger as a,
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
- logEvent as l,
14798
+ logger as l,
14815
14799
  extractResourceUrls as m
14816
14800
  };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { b, c, f, d, j, g, C, i, D, E, k, h, L, R, S, m } from "./index-wQJM-Vug.js";
1
+ import { b, c, f, d, j, g, C, i, D, E, k, h, L, R, S, m } from "./index-B0MJ_hCJ.js";
2
2
  export {
3
3
  b as Avatar,
4
4
  c as AvatarController,
@@ -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.58",
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
+ }