@spatialwalk/avatarkit 1.0.0-beta.60 → 1.0.0-beta.62

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,6 +2,28 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.0.0-beta.62] - 2026-01-14
6
+
7
+ ### ✨ New Features
8
+ - **Bezier Curve Transition Animation** - Implemented Bezier curve easing functions for smoother transitions
9
+ - Added Bezier curve interpolation with different curves for different facial components (jaw, expression, eye, neck, global)
10
+ - Replaced linear interpolation with Bezier curve interpolation for more natural animation
11
+ - Split transition duration into start (200ms) and end (1600ms) for different transition types
12
+
13
+ ### 🔧 Improvements
14
+ - **Transition API Enhancement** - Updated `generateTransitionFromIdle()` to support both start and end transitions
15
+ - Added `transitionType` parameter: `'start'` for Idle -> Flame, `'end'` for Flame -> Idle
16
+ - Removed deprecated linear interpolation code and unused easing functions
17
+
18
+ ## [1.0.0-beta.61] - 2026-01-14
19
+
20
+ ### 🔧 Improvements
21
+ - **Telemetry Events Update** - Updated telemetry events with new parameters and structure
22
+ - `sdk_initialized`: Added `env` and `dsm` parameters
23
+ - `driving_service_latency`: Replaced `driving_latency`, now tracks `tap_0`, `tap_1`, `tap_2`, `tap_f` timestamps (Number type, milliseconds)
24
+ - `avatar_active`: New event for avatar view activity tracking with `env` and `dsm` parameters, reported on first render and every 10 minutes
25
+ - **Heartbeat Refactoring** - Removed `HeartbeatManager`, integrated heartbeat functionality into `NetworkLayer` (driving service heartbeat) and `AvatarView` (avatar active heartbeat)
26
+
5
27
  ## [1.0.0-beta.60] - 2026-01-14
6
28
 
7
29
  ### 🔧 Configuration Changes
@@ -240,11 +262,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
240
262
  - `LogLevel.warning` - Warning and error logs
241
263
  - `LogLevel.all` - All logs (default)
242
264
  - **Standardized CLS Logging Events** - All CLS logging events now use standardized field names for better querying
243
- - Standard events: `sdk_initialized`, `send_audio_failed`, `message_error`, `heartbeat_failed`, `service_restarted`, `session_token_invalid`, `session_token_expired`, `yield_animation_failed`, `fetch_avatar_metadata_failed`, `download_avatar_assets_failed`
265
+ - Standard events: `sdk_initialized`, `send_audio_failed`, `message_error`, `heartbeat_failed`, `service_restarted`, `session_token_invalid`, `session_token_expired`, `yield_animation_failed`, `fetch_avatar_metadata_failed`, `download_avatar_assets_failed`, `avatar_active`
244
266
  - Standard fields: `req_id`, `con_id`, `avatar_id`, `description`
245
- - **End-to-End Latency Tracking** - Added `driving_latency` event to track latency metrics for both SDK and Host modes
246
- - Tracks: `start`, `tap2`, `tap4`, `end`, `first_frame` timestamps
247
- - Automatically reported at key moments during audio/animation processing
267
+ - **End-to-End Latency Tracking** - Added `driving_service_latency` event to track latency metrics for both SDK and Host modes
268
+ - Tracks: `tap_0`, `tap_1`, `tap_2`, `tap_f` timestamps (Number type, milliseconds)
269
+ - Automatically reported once per conversation when first frame is received
248
270
 
249
271
  ### 🔧 Improvements
250
272
  - **CLS Configuration Alignment** - Updated CLS endpoints and Topic IDs to match iOS SDK configuration
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # SPAvatarSDK SDK
1
+ # AvatarKit SDK
2
2
 
3
3
  Real-time virtual avatar rendering SDK based on 3D Gaussian Splatting, supporting audio-driven animation rendering and high-quality 3D rendering.
4
4
 
@@ -97,11 +97,7 @@ avatarView.avatarController.yieldFramesData(animationDataArray, conversationId)
97
97
 
98
98
  ### Complete Examples
99
99
 
100
- Check the example code in the GitHub repository for complete usage flows for both modes.
101
-
102
- **Example Project:** [AvatarSDK-Web-Demo](https://github.com/spatialwalk/AvatarSDK-Web-Demo)
103
-
104
- This repository contains complete examples for Vanilla JS, Vue 3, and React, demonstrating:
100
+ This SDK supports two usage modes:
105
101
  - SDK mode: Real-time audio input with automatic animation data reception
106
102
  - Host mode: Custom data sources with manual audio/animation data management
107
103
 
@@ -652,6 +648,5 @@ Issues and Pull Requests are welcome!
652
648
  ## 📞 Support
653
649
 
654
650
  For questions, please contact:
655
- - Email: support@spavatar.com
651
+ - Email: code@spatialwalk.net
656
652
  - Documentation: https://docs.spatialreal.ai
657
- - GitHub: https://github.com/spavatar/sdk
@@ -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-D9nprBw2.js";
4
+ import { A as APP_CONFIG, l as logger, e as errorToMessage, a as logEvent } from "./index-Bhjn1nq3.js";
5
5
  class StreamingAudioPlayer {
6
6
  constructor(options) {
7
7
  __publicField(this, "audioContext", null);
@@ -76,5 +76,5 @@ export declare class AvatarController {
76
76
  private registerEventListener;
77
77
  protected emit(event: string, data?: any): void;
78
78
  private applyPostProcessingToParams;
79
- private reportDrivingLatency;
79
+ private reportDrivingServiceLatency;
80
80
  }
@@ -22,12 +22,15 @@ export declare class AvatarView {
22
22
  private currentFPS;
23
23
  private transitionKeyframes;
24
24
  private transitionStartTime;
25
- private readonly transitionDurationMs;
25
+ private readonly startTransitionDurationMs;
26
+ private readonly endTransitionDurationMs;
26
27
  private cachedIdleFirstFrame;
27
28
  private idleCurrentFrameIndex;
28
29
  private characterHandle;
29
30
  private characterId;
30
31
  private isPureRenderingMode;
32
+ private avatarActiveTimer;
33
+ private readonly AVATAR_ACTIVE_INTERVAL;
31
34
  private alignFlamePair;
32
35
  private generateAndAlignTransitionFrames;
33
36
  private getCachedIdleFirstFrame;
@@ -59,7 +62,7 @@ export declare class AvatarView {
59
62
  private stopRealtimeRendering;
60
63
  dispose(): void;
61
64
  renderFlame(flame: Flame, enableIdleRendering?: boolean): Promise<void>;
62
- generateTransitionFromIdle(toFlame: Flame, frameCount: number): Promise<Flame[]>;
65
+ generateTransitionFromIdle(toFlame: Flame, frameCount: number, transitionType?: 'start' | 'end'): Promise<Flame[]>;
63
66
  private rerenderCurrentFrameWithNewCamera;
64
67
  private handleResize;
65
68
  get transform(): {
@@ -72,4 +75,7 @@ export declare class AvatarView {
72
75
  y: number;
73
76
  scale: number;
74
77
  });
78
+ private reportAvatarActive;
79
+ private startAvatarActiveHeartbeat;
80
+ private stopAvatarActiveHeartbeat;
75
81
  }
@@ -7387,7 +7387,19 @@ let isInitialized = false;
7387
7387
  const SDK_POSTHOG_INSTANCE_NAME = "spatialwalk-posthog";
7388
7388
  let sdkPosthogInstance = null;
7389
7389
  const eventQueue = [];
7390
+ const FILTERED_HOSTNAMES = ["localhost", "127.0.0.1", "0.0.0.0"];
7391
+ function shouldFilterHostname() {
7392
+ if (typeof window === "undefined") {
7393
+ return false;
7394
+ }
7395
+ const hostname = window.location.hostname;
7396
+ return FILTERED_HOSTNAMES.includes(hostname);
7397
+ }
7390
7398
  function initializePostHog(environment, version) {
7399
+ if (shouldFilterHostname()) {
7400
+ logger.log(`[PostHog] Tracking disabled due to filtered hostname: ${window.location.hostname}`);
7401
+ return;
7402
+ }
7391
7403
  const { host, apiKey, disableCompression } = getPostHogConfig();
7392
7404
  if (isInitialized) {
7393
7405
  logger.log("[PostHog] Already initialized, skipping");
@@ -7478,6 +7490,9 @@ function flushEventQueue() {
7478
7490
  }, 0);
7479
7491
  }
7480
7492
  function trackEvent(event, level = "info", contents = {}) {
7493
+ if (shouldFilterHostname()) {
7494
+ return;
7495
+ }
7481
7496
  const instance = getSdkPosthogInstance();
7482
7497
  if (!instance) {
7483
7498
  eventQueue.push({ event, level, contents });
@@ -7609,7 +7624,7 @@ const _AnimationPlayer = class _AnimationPlayer {
7609
7624
  if (this.streamingPlayer) {
7610
7625
  return;
7611
7626
  }
7612
- const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-CH89JZHk.js");
7627
+ const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-BWsAt_s7.js");
7613
7628
  const { AvatarSDK: AvatarSDK2 } = await Promise.resolve().then(() => AvatarSDK$1);
7614
7629
  const audioFormat = AvatarSDK2.getAudioFormat();
7615
7630
  this.streamingPlayer = new StreamingAudioPlayer({
@@ -7828,95 +7843,6 @@ function isFirstUse() {
7828
7843
  return false;
7829
7844
  }
7830
7845
  }
7831
- class HeartbeatManager {
7832
- constructor() {
7833
- __publicField(this, "heartbeatTimer", null);
7834
- __publicField(this, "visibilityChangeHandler", null);
7835
- __publicField(this, "isInitialized", false);
7836
- __publicField(this, "currentEnvironment", null);
7837
- __publicField(this, "currentAppId", null);
7838
- __publicField(this, "failureCount", 0);
7839
- }
7840
- start(environment) {
7841
- if (this.isInitialized) {
7842
- return;
7843
- }
7844
- this.isInitialized = true;
7845
- this.currentEnvironment = environment;
7846
- this.currentAppId = idManager.getAppId();
7847
- this.startHeartbeatTimer();
7848
- this.setupVisibilityListener();
7849
- logger.log("[HeartbeatManager] Started");
7850
- }
7851
- stop() {
7852
- if (!this.isInitialized) {
7853
- return;
7854
- }
7855
- this.stopHeartbeatTimer();
7856
- this.removeVisibilityListener();
7857
- this.isInitialized = false;
7858
- this.currentEnvironment = null;
7859
- this.currentAppId = null;
7860
- logger.log("[HeartbeatManager] Stopped");
7861
- }
7862
- startHeartbeatTimer() {
7863
- if (this.heartbeatTimer !== null) {
7864
- return;
7865
- }
7866
- const HEARTBEAT_INTERVAL = 12e4;
7867
- this.heartbeatTimer = window.setInterval(() => {
7868
- if (this.isInitialized && typeof document !== "undefined" && document.visibilityState === "visible") {
7869
- this.reportHeartbeat();
7870
- }
7871
- }, HEARTBEAT_INTERVAL);
7872
- }
7873
- stopHeartbeatTimer() {
7874
- if (this.heartbeatTimer !== null) {
7875
- clearInterval(this.heartbeatTimer);
7876
- this.heartbeatTimer = null;
7877
- }
7878
- }
7879
- reportHeartbeat(additionalData) {
7880
- try {
7881
- trackEvent("sdk_heartbeat", "info", {
7882
- appId: this.currentAppId || idManager.getAppId(),
7883
- environment: this.currentEnvironment,
7884
- ...additionalData
7885
- });
7886
- this.failureCount = 0;
7887
- } catch (error) {
7888
- this.failureCount++;
7889
- logger.warn(`[HeartbeatManager] Heartbeat failed (count: ${this.failureCount})`);
7890
- if (this.failureCount >= 3) {
7891
- trackEvent("heartbeat_failed", "warning", {
7892
- con_id: idManager.getConnectionId() || "",
7893
- description: `Heartbeat failed ${this.failureCount} times`
7894
- });
7895
- this.failureCount = 0;
7896
- }
7897
- }
7898
- }
7899
- setupVisibilityListener() {
7900
- if (typeof window === "undefined" || typeof document === "undefined") {
7901
- return;
7902
- }
7903
- this.removeVisibilityListener();
7904
- this.visibilityChangeHandler = () => {
7905
- if (document.visibilityState === "visible" && this.isInitialized) {
7906
- this.reportHeartbeat({ resumed: true });
7907
- }
7908
- };
7909
- document.addEventListener("visibilitychange", this.visibilityChangeHandler);
7910
- }
7911
- removeVisibilityListener() {
7912
- if (typeof window === "undefined" || typeof document === "undefined" || !this.visibilityChangeHandler) {
7913
- return;
7914
- }
7915
- document.removeEventListener("visibilitychange", this.visibilityChangeHandler);
7916
- this.visibilityChangeHandler = null;
7917
- }
7918
- }
7919
- const heartbeatManager = new HeartbeatManager();
7920
7846
  class AvatarCoreMemoryManager {
7921
7847
  constructor(wasmModule) {
7922
7848
  __publicField(this, "module");
@@ -8869,14 +8795,16 @@ class AvatarSDK {
8869
8795
  await this.initializeWASMModule();
8870
8796
  await this.initializeTemplateResources();
8871
8797
  this._isInitialized = true;
8872
- logEvent("sdk_initialized", "info", {});
8798
+ logEvent("sdk_initialized", "info", {
8799
+ env: configuration.environment,
8800
+ dsm: configuration.drivingServiceMode || DrivingServiceMode.sdk
8801
+ });
8873
8802
  if (isFirstUse()) {
8874
8803
  logEvent("sdk_first_use", "info", {
8875
8804
  appId: idManager.getAppId(),
8876
8805
  environment: (_a = this._configuration) == null ? void 0 : _a.environment
8877
8806
  });
8878
8807
  }
8879
- heartbeatManager.start(this._configuration.environment);
8880
8808
  logger.log(`[AvatarSDK] Successfully initialized`);
8881
8809
  } catch (error) {
8882
8810
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -8983,7 +8911,6 @@ class AvatarSDK {
8983
8911
  return;
8984
8912
  }
8985
8913
  try {
8986
- heartbeatManager.stop();
8987
8914
  if (this._avatarCore) {
8988
8915
  this._avatarCore.release();
8989
8916
  this._avatarCore = null;
@@ -9034,7 +8961,7 @@ class AvatarSDK {
9034
8961
  }
9035
8962
  __publicField(AvatarSDK, "_isInitialized", false);
9036
8963
  __publicField(AvatarSDK, "_configuration", null);
9037
- __publicField(AvatarSDK, "_version", "1.0.0-beta.60");
8964
+ __publicField(AvatarSDK, "_version", "1.0.0-beta.62");
9038
8965
  __publicField(AvatarSDK, "_avatarCore", null);
9039
8966
  __publicField(AvatarSDK, "_dynamicSdkConfig", null);
9040
8967
  const AvatarSDK$1 = Object.freeze(Object.defineProperty({
@@ -10362,6 +10289,10 @@ class NetworkLayer {
10362
10289
  __publicField(this, "audioMetrics", this.createAudioMetrics());
10363
10290
  __publicField(this, "isFallbackMode", false);
10364
10291
  __publicField(this, "isConnecting", false);
10292
+ __publicField(this, "heartbeatTimer", null);
10293
+ __publicField(this, "visibilityChangeHandler", null);
10294
+ __publicField(this, "heartbeatFailureCount", 0);
10295
+ __publicField(this, "HEARTBEAT_INTERVAL", 12e4);
10365
10296
  this.dataController = dataController;
10366
10297
  const config = AvatarSDK.getEnvironmentConfig();
10367
10298
  this.wsClient = new AnimationWebSocketClient({
@@ -10372,6 +10303,7 @@ class NetworkLayer {
10372
10303
  clientId: idManager.getClientId()
10373
10304
  });
10374
10305
  this.setupWebSocketListeners();
10306
+ this.startHeartbeatCheck();
10375
10307
  }
10376
10308
  getAudioBytesPerSecond() {
10377
10309
  const audioFormat = AvatarSDK.getAudioFormat();
@@ -10435,28 +10367,16 @@ class NetworkLayer {
10435
10367
  });
10436
10368
  }
10437
10369
  const metrics = this.audioMetrics;
10438
- let shouldReportMetrics = false;
10439
10370
  if (metrics.startTimestamp === 0) {
10440
10371
  metrics.startTimestamp = Date.now();
10441
- metrics.cachedStartTimestamp = String(metrics.startTimestamp);
10442
- shouldReportMetrics = true;
10443
10372
  }
10444
10373
  metrics.accumulatedBytes += audioData.byteLength;
10445
10374
  const currentDuration = metrics.accumulatedBytes / this.getAudioBytesPerSecond();
10375
+ if (currentDuration >= 1 && metrics.tap1Timestamp === 0) {
10376
+ metrics.tap1Timestamp = Date.now();
10377
+ }
10446
10378
  if (currentDuration >= 2 && metrics.tap2Timestamp === 0) {
10447
10379
  metrics.tap2Timestamp = Date.now();
10448
- metrics.cachedTap2Timestamp = String(metrics.tap2Timestamp);
10449
- shouldReportMetrics = true;
10450
- }
10451
- if (currentDuration >= 4 && metrics.tap4Timestamp === 0) {
10452
- metrics.tap4Timestamp = Date.now();
10453
- metrics.cachedTap4Timestamp = String(metrics.tap4Timestamp);
10454
- shouldReportMetrics = true;
10455
- }
10456
- if (isLast && metrics.endTimestamp === 0) {
10457
- metrics.endTimestamp = Date.now();
10458
- metrics.cachedEndTimestamp = String(metrics.endTimestamp);
10459
- shouldReportMetrics = true;
10460
10380
  }
10461
10381
  if (audioData.byteLength === 0 && !isLast) {
10462
10382
  logger.warn("[NetworkLayer] Warning: sending empty audio data (size=0, end=false)");
@@ -10479,15 +10399,13 @@ class NetworkLayer {
10479
10399
  });
10480
10400
  return;
10481
10401
  }
10482
- if (shouldReportMetrics && this.currentConversationId) {
10483
- this.reportDrivingLatency(this.currentConversationId);
10484
- }
10485
10402
  }
10486
10403
  disconnect() {
10487
10404
  this.isFallbackMode = false;
10488
10405
  this.isConnecting = false;
10489
10406
  this.wsClient.removeAllListeners();
10490
10407
  this.wsClient.disconnect();
10408
+ this.stopHeartbeatCheck();
10491
10409
  idManager.clearConnectionId();
10492
10410
  this.currentConversationId = null;
10493
10411
  }
@@ -10582,9 +10500,9 @@ class NetworkLayer {
10582
10500
  if (!this.audioMetrics.didRecvFirstFlame) {
10583
10501
  this.audioMetrics.didRecvFirstFlame = true;
10584
10502
  this.audioMetrics.recvFirstFlameTimestamp = Date.now();
10585
- this.audioMetrics.cachedFirstFrameTimestamp = String(this.audioMetrics.recvFirstFlameTimestamp);
10586
- if (this.currentConversationId) {
10587
- this.reportDrivingLatency(this.currentConversationId);
10503
+ if (this.currentConversationId && !this.audioMetrics.didReportLatency) {
10504
+ this.reportDrivingServiceLatency(this.currentConversationId);
10505
+ this.audioMetrics.didReportLatency = true;
10588
10506
  }
10589
10507
  }
10590
10508
  } else {
@@ -10644,36 +10562,94 @@ class NetworkLayer {
10644
10562
  return {
10645
10563
  accumulatedBytes: 0,
10646
10564
  startTimestamp: 0,
10565
+ tap1Timestamp: 0,
10647
10566
  tap2Timestamp: 0,
10648
- tap4Timestamp: 0,
10649
- endTimestamp: 0,
10650
10567
  recvFirstFlameTimestamp: 0,
10651
10568
  didRecvFirstFlame: false,
10652
- cachedStartTimestamp: "",
10653
- cachedTap2Timestamp: "",
10654
- cachedTap4Timestamp: "",
10655
- cachedEndTimestamp: "",
10656
- cachedFirstFrameTimestamp: ""
10569
+ didReportLatency: false
10657
10570
  };
10658
10571
  }
10659
10572
  resetAudioMetrics() {
10660
10573
  this.audioMetrics = this.createAudioMetrics();
10661
10574
  }
10662
- reportDrivingLatency(conversationId) {
10575
+ reportDrivingServiceLatency(conversationId) {
10663
10576
  if (!conversationId) {
10664
10577
  return;
10665
10578
  }
10666
10579
  const metrics = this.audioMetrics;
10667
- logEvent("driving_latency", "info", {
10668
- driving_service_mode: "sdk",
10580
+ logEvent("driving_service_latency", "info", {
10669
10581
  req_id: conversationId,
10670
- start: metrics.cachedStartTimestamp || "",
10671
- tap2: metrics.cachedTap2Timestamp || "",
10672
- tap4: metrics.cachedTap4Timestamp || "",
10673
- end: metrics.cachedEndTimestamp || "",
10674
- first_frame: metrics.cachedFirstFrameTimestamp || ""
10582
+ dsm: "sdk",
10583
+ tap_0: metrics.startTimestamp || 0,
10584
+ tap_1: metrics.tap1Timestamp || 0,
10585
+ tap_2: metrics.tap2Timestamp || 0,
10586
+ tap_f: metrics.recvFirstFlameTimestamp || 0
10675
10587
  });
10676
10588
  }
10589
+ startHeartbeatCheck() {
10590
+ this.stopHeartbeatCheck();
10591
+ this.heartbeatTimer = window.setInterval(() => {
10592
+ if (typeof document !== "undefined" && document.visibilityState === "visible") {
10593
+ this.performHeartbeatCheck();
10594
+ }
10595
+ }, this.HEARTBEAT_INTERVAL);
10596
+ this.setupVisibilityListener();
10597
+ }
10598
+ stopHeartbeatCheck() {
10599
+ if (this.heartbeatTimer !== null) {
10600
+ clearInterval(this.heartbeatTimer);
10601
+ this.heartbeatTimer = null;
10602
+ }
10603
+ this.removeVisibilityListener();
10604
+ this.heartbeatFailureCount = 0;
10605
+ }
10606
+ performHeartbeatCheck() {
10607
+ try {
10608
+ const isConnected = this.wsClient.isConnected();
10609
+ if (isConnected) {
10610
+ this.heartbeatFailureCount = 0;
10611
+ } else {
10612
+ this.heartbeatFailureCount++;
10613
+ logger.warn(`[NetworkLayer] Driving service heartbeat failed (count: ${this.heartbeatFailureCount})`);
10614
+ if (this.heartbeatFailureCount >= 3) {
10615
+ logEvent("heartbeat_failed", "warning", {
10616
+ con_id: idManager.getConnectionId() || "",
10617
+ description: `Driving service heartbeat failed ${this.heartbeatFailureCount} times`
10618
+ });
10619
+ this.heartbeatFailureCount = 0;
10620
+ }
10621
+ }
10622
+ } catch (error) {
10623
+ this.heartbeatFailureCount++;
10624
+ logger.warn(`[NetworkLayer] Heartbeat check error (count: ${this.heartbeatFailureCount}):`, error instanceof Error ? error.message : String(error));
10625
+ if (this.heartbeatFailureCount >= 3) {
10626
+ logEvent("heartbeat_failed", "warning", {
10627
+ con_id: idManager.getConnectionId() || "",
10628
+ description: `Heartbeat check error: ${error instanceof Error ? error.message : String(error)}`
10629
+ });
10630
+ this.heartbeatFailureCount = 0;
10631
+ }
10632
+ }
10633
+ }
10634
+ setupVisibilityListener() {
10635
+ if (typeof window === "undefined" || typeof document === "undefined") {
10636
+ return;
10637
+ }
10638
+ this.removeVisibilityListener();
10639
+ this.visibilityChangeHandler = () => {
10640
+ if (document.visibilityState === "visible") {
10641
+ this.performHeartbeatCheck();
10642
+ }
10643
+ };
10644
+ document.addEventListener("visibilitychange", this.visibilityChangeHandler);
10645
+ }
10646
+ removeVisibilityListener() {
10647
+ if (typeof window === "undefined" || typeof document === "undefined" || !this.visibilityChangeHandler) {
10648
+ return;
10649
+ }
10650
+ document.removeEventListener("visibilitychange", this.visibilityChangeHandler);
10651
+ this.visibilityChangeHandler = null;
10652
+ }
10677
10653
  }
10678
10654
  class AvatarController {
10679
10655
  constructor(avatar, options) {
@@ -10717,16 +10693,11 @@ class AvatarController {
10717
10693
  __publicField(this, "hostModeMetrics", {
10718
10694
  accumulatedBytes: 0,
10719
10695
  startTimestamp: 0,
10696
+ tap1Timestamp: 0,
10720
10697
  tap2Timestamp: 0,
10721
- tap4Timestamp: 0,
10722
- endTimestamp: 0,
10723
10698
  recvFirstFlameTimestamp: 0,
10724
10699
  didRecvFirstFlame: false,
10725
- cachedStartTimestamp: "",
10726
- cachedTap2Timestamp: "",
10727
- cachedTap4Timestamp: "",
10728
- cachedEndTimestamp: "",
10729
- cachedFirstFrameTimestamp: ""
10700
+ didReportLatency: false
10730
10701
  });
10731
10702
  __publicField(this, "audioBytesPerSecond", 16e3 * 2);
10732
10703
  this.avatar = avatar;
@@ -10896,31 +10867,16 @@ class AvatarController {
10896
10867
  }
10897
10868
  if (this.playbackMode === DrivingServiceMode.host) {
10898
10869
  const metrics = this.hostModeMetrics;
10899
- let shouldReportMetrics = false;
10900
10870
  if (metrics.startTimestamp === 0) {
10901
10871
  metrics.startTimestamp = Date.now();
10902
- metrics.cachedStartTimestamp = String(metrics.startTimestamp);
10903
- shouldReportMetrics = true;
10904
10872
  }
10905
10873
  metrics.accumulatedBytes += data.length;
10906
10874
  const currentDuration = metrics.accumulatedBytes / this.audioBytesPerSecond;
10875
+ if (currentDuration >= 1 && metrics.tap1Timestamp === 0) {
10876
+ metrics.tap1Timestamp = Date.now();
10877
+ }
10907
10878
  if (currentDuration >= 2 && metrics.tap2Timestamp === 0) {
10908
10879
  metrics.tap2Timestamp = Date.now();
10909
- metrics.cachedTap2Timestamp = String(metrics.tap2Timestamp);
10910
- shouldReportMetrics = true;
10911
- }
10912
- if (currentDuration >= 4 && metrics.tap4Timestamp === 0) {
10913
- metrics.tap4Timestamp = Date.now();
10914
- metrics.cachedTap4Timestamp = String(metrics.tap4Timestamp);
10915
- shouldReportMetrics = true;
10916
- }
10917
- if (isLast && metrics.endTimestamp === 0) {
10918
- metrics.endTimestamp = Date.now();
10919
- metrics.cachedEndTimestamp = String(metrics.endTimestamp);
10920
- shouldReportMetrics = true;
10921
- }
10922
- if (shouldReportMetrics && this.currentConversationId) {
10923
- this.reportDrivingLatency(this.currentConversationId);
10924
10880
  }
10925
10881
  }
10926
10882
  if (this.isPlaying && ((_a = this.animationPlayer) == null ? void 0 : _a.isStreamingReady())) {
@@ -11018,9 +10974,9 @@ class AvatarController {
11018
10974
  if (this.playbackMode === DrivingServiceMode.host && !this.hostModeMetrics.didRecvFirstFlame) {
11019
10975
  this.hostModeMetrics.didRecvFirstFlame = true;
11020
10976
  this.hostModeMetrics.recvFirstFlameTimestamp = Date.now();
11021
- this.hostModeMetrics.cachedFirstFrameTimestamp = String(this.hostModeMetrics.recvFirstFlameTimestamp);
11022
- if (conversationId) {
11023
- this.reportDrivingLatency(conversationId);
10977
+ if (conversationId && !this.hostModeMetrics.didReportLatency) {
10978
+ this.reportDrivingServiceLatency(conversationId);
10979
+ this.hostModeMetrics.didReportLatency = true;
11024
10980
  }
11025
10981
  }
11026
10982
  } else {
@@ -11117,16 +11073,11 @@ class AvatarController {
11117
11073
  this.hostModeMetrics = {
11118
11074
  accumulatedBytes: 0,
11119
11075
  startTimestamp: 0,
11076
+ tap1Timestamp: 0,
11120
11077
  tap2Timestamp: 0,
11121
- tap4Timestamp: 0,
11122
- endTimestamp: 0,
11123
11078
  recvFirstFlameTimestamp: 0,
11124
11079
  didRecvFirstFlame: false,
11125
- cachedStartTimestamp: "",
11126
- cachedTap2Timestamp: "",
11127
- cachedTap4Timestamp: "",
11128
- cachedEndTimestamp: "",
11129
- cachedFirstFrameTimestamp: ""
11080
+ didReportLatency: false
11130
11081
  };
11131
11082
  }
11132
11083
  }
@@ -11142,16 +11093,11 @@ class AvatarController {
11142
11093
  this.hostModeMetrics = {
11143
11094
  accumulatedBytes: 0,
11144
11095
  startTimestamp: 0,
11096
+ tap1Timestamp: 0,
11145
11097
  tap2Timestamp: 0,
11146
- tap4Timestamp: 0,
11147
- endTimestamp: 0,
11148
11098
  recvFirstFlameTimestamp: 0,
11149
11099
  didRecvFirstFlame: false,
11150
- cachedStartTimestamp: "",
11151
- cachedTap2Timestamp: "",
11152
- cachedTap4Timestamp: "",
11153
- cachedEndTimestamp: "",
11154
- cachedFirstFrameTimestamp: ""
11100
+ didReportLatency: false
11155
11101
  };
11156
11102
  }
11157
11103
  }
@@ -11742,19 +11688,18 @@ class AvatarController {
11742
11688
  }
11743
11689
  return result2;
11744
11690
  }
11745
- reportDrivingLatency(conversationId) {
11691
+ reportDrivingServiceLatency(conversationId) {
11746
11692
  if (!conversationId || this.playbackMode !== DrivingServiceMode.host) {
11747
11693
  return;
11748
11694
  }
11749
11695
  const metrics = this.hostModeMetrics;
11750
- logEvent("driving_latency", "info", {
11751
- driving_service_mode: "host",
11696
+ logEvent("driving_service_latency", "info", {
11752
11697
  req_id: conversationId,
11753
- start: metrics.cachedStartTimestamp || "",
11754
- tap2: metrics.cachedTap2Timestamp || "",
11755
- tap4: metrics.cachedTap4Timestamp || "",
11756
- end: metrics.cachedEndTimestamp || "",
11757
- first_frame: metrics.cachedFirstFrameTimestamp || ""
11698
+ dsm: "host",
11699
+ tap_0: metrics.startTimestamp || 0,
11700
+ tap_1: metrics.tap1Timestamp || 0,
11701
+ tap_2: metrics.tap2Timestamp || 0,
11702
+ tap_f: metrics.recvFirstFlameTimestamp || 0
11758
11703
  });
11759
11704
  }
11760
11705
  }
@@ -13906,31 +13851,82 @@ function lerpArrays(from, to2, progress) {
13906
13851
  }
13907
13852
  return result2;
13908
13853
  }
13909
- function linearLerp(from, to2, progress) {
13910
- const easedProgress = 0.5 - Math.cos(progress * Math.PI) * 0.5;
13854
+ const clamp01 = (x2) => Math.max(0, Math.min(1, x2));
13855
+ function createBezierEasing(x1, y1, x2, y2) {
13856
+ const cx = 3 * x1;
13857
+ const bx = 3 * (x2 - x1) - cx;
13858
+ const ax = 1 - cx - bx;
13859
+ const cy = 3 * y1;
13860
+ const by = 3 * (y2 - y1) - cy;
13861
+ const ay = 1 - cy - by;
13862
+ const sampleCurveX = (t2) => ((ax * t2 + bx) * t2 + cx) * t2;
13863
+ const sampleCurveY = (t2) => ((ay * t2 + by) * t2 + cy) * t2;
13864
+ const sampleCurveDerivativeX = (t2) => (3 * ax * t2 + 2 * bx) * t2 + cx;
13865
+ const solveCurveX = (x3) => {
13866
+ let t2 = x3;
13867
+ for (let i2 = 0; i2 < 8; i2++) {
13868
+ const error = sampleCurveX(t2) - x3;
13869
+ if (Math.abs(error) < 1e-6) break;
13870
+ const d2 = sampleCurveDerivativeX(t2);
13871
+ if (Math.abs(d2) < 1e-6) break;
13872
+ t2 -= error / d2;
13873
+ }
13874
+ return t2;
13875
+ };
13876
+ return (x3) => {
13877
+ if (x3 <= 0) return 0;
13878
+ if (x3 >= 1) return 1;
13879
+ return sampleCurveY(solveCurveX(x3));
13880
+ };
13881
+ }
13882
+ const BEZIER_CURVES = {
13883
+ jaw: createBezierEasing(0.2, 0.8, 0.3, 1),
13884
+ expression: createBezierEasing(0.4, 0, 0.2, 1),
13885
+ eye: createBezierEasing(0.3, 0, 0.1, 1),
13886
+ neck: createBezierEasing(0.1, 0.2, 0.2, 1),
13887
+ global: createBezierEasing(0.42, 0, 0.58, 1)
13888
+ };
13889
+ const TIME_SCALE = {
13890
+ jaw: 2.5,
13891
+ expression: 1.6,
13892
+ eye: 1.3,
13893
+ neck: 1,
13894
+ global: 1
13895
+ };
13896
+ function bezierLerp(from, to2, progress) {
13897
+ const getT = (key) => {
13898
+ const scaledProgress = clamp01(progress * TIME_SCALE[key]);
13899
+ return BEZIER_CURVES[key](scaledProgress);
13900
+ };
13911
13901
  return {
13912
- translation: lerpArrays(from.translation || [0, 0, 0], to2.translation || [0, 0, 0], easedProgress),
13913
- rotation: lerpArrays(from.rotation || [0, 0, 0], to2.rotation || [0, 0, 0], easedProgress),
13914
- neckPose: lerpArrays(from.neckPose || [0, 0, 0], to2.neckPose || [0, 0, 0], easedProgress),
13915
- jawPose: lerpArrays(from.jawPose || [0, 0, 0], to2.jawPose || [0, 0, 0], easedProgress),
13916
- eyePose: lerpArrays(from.eyePose || [0, 0, 0, 0, 0, 0], to2.eyePose || [0, 0, 0, 0, 0, 0], easedProgress),
13902
+ translation: lerpArrays(from.translation || [0, 0, 0], to2.translation || [0, 0, 0], getT("global")),
13903
+ rotation: lerpArrays(from.rotation || [0, 0, 0], to2.rotation || [0, 0, 0], getT("global")),
13904
+ neckPose: lerpArrays(from.neckPose || [0, 0, 0], to2.neckPose || [0, 0, 0], getT("neck")),
13905
+ jawPose: lerpArrays(from.jawPose || [0, 0, 0], to2.jawPose || [0, 0, 0], getT("jaw")),
13906
+ eyePose: lerpArrays(from.eyePose || [0, 0, 0, 0, 0, 0], to2.eyePose || [0, 0, 0, 0, 0, 0], getT("eye")),
13917
13907
  eyeLid: (() => {
13918
13908
  const fromEyelid = from.eyeLid;
13919
13909
  const toEyelid = to2.eyeLid;
13920
- if (fromEyelid && fromEyelid.length > 0 && toEyelid && toEyelid.length > 0)
13921
- return lerpArrays(fromEyelid, toEyelid, easedProgress);
13910
+ if ((fromEyelid == null ? void 0 : fromEyelid.length) && (toEyelid == null ? void 0 : toEyelid.length))
13911
+ return lerpArrays(fromEyelid, toEyelid, getT("eye"));
13922
13912
  return fromEyelid || toEyelid || [];
13923
13913
  })(),
13924
- expression: lerpArrays(from.expression || [], to2.expression || [], easedProgress)
13914
+ expression: lerpArrays(from.expression || [], to2.expression || [], getT("expression"))
13925
13915
  };
13926
13916
  }
13927
13917
  function generateTransitionFrames(from, to2, durationMs, fps = 25) {
13928
13918
  const steps = Math.max(1, Math.floor(durationMs / 1e3 * fps));
13929
13919
  const frames = Array.from({ length: steps });
13920
+ if (steps === 1) {
13921
+ frames[0] = to2;
13922
+ return frames;
13923
+ }
13930
13924
  for (let i2 = 0; i2 < steps; i2++) {
13931
13925
  const progress = i2 / (steps - 1);
13932
- frames[i2] = linearLerp(from, to2, progress);
13926
+ frames[i2] = bezierLerp(from, to2, progress);
13933
13927
  }
13928
+ frames[0] = from;
13929
+ frames[frames.length - 1] = to2;
13934
13930
  return frames;
13935
13931
  }
13936
13932
  class AvatarView {
@@ -13955,12 +13951,15 @@ class AvatarView {
13955
13951
  __publicField(this, "currentFPS", 0);
13956
13952
  __publicField(this, "transitionKeyframes", []);
13957
13953
  __publicField(this, "transitionStartTime", 0);
13958
- __publicField(this, "transitionDurationMs", 400);
13954
+ __publicField(this, "startTransitionDurationMs", 200);
13955
+ __publicField(this, "endTransitionDurationMs", 1600);
13959
13956
  __publicField(this, "cachedIdleFirstFrame", null);
13960
13957
  __publicField(this, "idleCurrentFrameIndex", 0);
13961
13958
  __publicField(this, "characterHandle", null);
13962
13959
  __publicField(this, "characterId");
13963
13960
  __publicField(this, "isPureRenderingMode", false);
13961
+ __publicField(this, "avatarActiveTimer", null);
13962
+ __publicField(this, "AVATAR_ACTIVE_INTERVAL", 6e5);
13964
13963
  this.avatar = avatar;
13965
13964
  this.characterId = `${avatar.id}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
13966
13965
  if (!AvatarSDK.configuration) {
@@ -14010,12 +14009,12 @@ class AvatarView {
14010
14009
  toFixed.expression = ensureLen(toFixed.expression, exprLen);
14011
14010
  return { from: fromFixed, to: toFixed };
14012
14011
  }
14013
- generateAndAlignTransitionFrames(from, to2) {
14012
+ generateAndAlignTransitionFrames(from, to2, durationMs) {
14014
14013
  const aligned = this.alignFlamePair(from, to2);
14015
14014
  let keyframes = generateTransitionFrames(
14016
14015
  aligned.from,
14017
14016
  aligned.to,
14018
- this.transitionDurationMs,
14017
+ durationMs,
14019
14018
  APP_CONFIG.animation.fps
14020
14019
  );
14021
14020
  if (keyframes.length < 2) {
@@ -14260,6 +14259,8 @@ class AvatarView {
14260
14259
  if (APP_CONFIG.debug)
14261
14260
  logger.log("[AvatarView] First frame rendered successfully");
14262
14261
  (_a = this.onFirstRendering) == null ? void 0 : _a.call(this);
14262
+ this.reportAvatarActive();
14263
+ this.startAvatarActiveHeartbeat();
14263
14264
  } else {
14264
14265
  throw new Error("Failed to compute first frame splat data");
14265
14266
  }
@@ -14372,7 +14373,8 @@ class AvatarView {
14372
14373
  return;
14373
14374
  }
14374
14375
  const elapsed = performance.now() - this.transitionStartTime;
14375
- const progress = Math.min(1, Math.max(0, elapsed / this.transitionDurationMs));
14376
+ const currentTransitionDurationMs = state === "transitioningToSpeaking" ? this.startTransitionDurationMs : this.endTransitionDurationMs;
14377
+ const progress = Math.min(1, Math.max(0, elapsed / currentTransitionDurationMs));
14376
14378
  const steps = this.transitionKeyframes.length;
14377
14379
  const idx = Math.min(steps - 1, Math.floor(progress * (steps - 1)));
14378
14380
  const currentFrame = this.transitionKeyframes[idx];
@@ -14398,7 +14400,7 @@ class AvatarView {
14398
14400
  return;
14399
14401
  }
14400
14402
  }
14401
- if (state === "transitioningToSpeaking" && this.transitionStartTime > 0 && this.transitionKeyframes.length > 0 && elapsed >= this.transitionDurationMs + 100) {
14403
+ if (state === "transitioningToSpeaking" && this.transitionStartTime > 0 && this.transitionKeyframes.length > 0 && elapsed >= this.startTransitionDurationMs + 100) {
14402
14404
  this.setState("speaking");
14403
14405
  this.transitionKeyframes = [];
14404
14406
  this.avatarController.onTransitionComplete();
@@ -14528,7 +14530,7 @@ class AvatarView {
14528
14530
  await this.getCachedIdleFirstFrame();
14529
14531
  const firstSpeaking = keyframes[0];
14530
14532
  const firstSpeakingWithPostProcessing = this.avatarController.applyPostProcessingToFlame(firstSpeaking);
14531
- this.transitionKeyframes = this.generateAndAlignTransitionFrames(idleFrameProto, firstSpeakingWithPostProcessing);
14533
+ this.transitionKeyframes = this.generateAndAlignTransitionFrames(idleFrameProto, firstSpeakingWithPostProcessing, this.startTransitionDurationMs);
14532
14534
  this.transitionStartTime = performance.now();
14533
14535
  if (this.transitionKeyframes.length === 0) {
14534
14536
  this.setState("speaking");
@@ -14581,7 +14583,7 @@ class AvatarView {
14581
14583
  const lastSpeaking = this.avatarController.applyPostProcessingToFlame(lastSpeakingRaw);
14582
14584
  const idleFirstProto = await this.getCachedIdleFirstFrame();
14583
14585
  if (idleFirstProto) {
14584
- this.transitionKeyframes = this.generateAndAlignTransitionFrames(lastSpeaking, idleFirstProto);
14586
+ this.transitionKeyframes = this.generateAndAlignTransitionFrames(lastSpeaking, idleFirstProto, this.endTransitionDurationMs);
14585
14587
  this.transitionStartTime = performance.now();
14586
14588
  if (this.transitionKeyframes.length > 0 && this.renderingState === "transitioningToIdle") {
14587
14589
  if (APP_CONFIG.debug)
@@ -14615,6 +14617,7 @@ class AvatarView {
14615
14617
  this.avatarController.dispose();
14616
14618
  }
14617
14619
  this.stopAllAnimationLoops();
14620
+ this.stopAvatarActiveHeartbeat();
14618
14621
  this.setState("idle");
14619
14622
  this.cachedIdleFirstFrame = null;
14620
14623
  this.idleCurrentFrameIndex = 0;
@@ -14690,7 +14693,7 @@ class AvatarView {
14690
14693
  throw error;
14691
14694
  }
14692
14695
  }
14693
- async generateTransitionFromIdle(toFlame, frameCount) {
14696
+ async generateTransitionFromIdle(toFlame, frameCount, transitionType = "start") {
14694
14697
  if (!this.isInitialized) {
14695
14698
  throw new Error("AvatarView not initialized");
14696
14699
  }
@@ -14706,16 +14709,18 @@ class AvatarView {
14706
14709
  const idleFrameProto = convertWasmParamsToProtoFlame(idleParams);
14707
14710
  const toFlameWithPostProcessing = this.avatarController.applyPostProcessingToFlame(toFlame);
14708
14711
  const aligned = this.alignFlamePair(idleFrameProto, toFlameWithPostProcessing);
14712
+ const from = transitionType === "start" ? aligned.from : aligned.to;
14713
+ const to2 = transitionType === "start" ? aligned.to : aligned.from;
14709
14714
  const fps = APP_CONFIG.animation.fps;
14710
14715
  const durationMs = frameCount / fps * 1e3;
14711
14716
  const transitionFrames = generateTransitionFrames(
14712
- aligned.from,
14713
- aligned.to,
14717
+ from,
14718
+ to2,
14714
14719
  durationMs,
14715
14720
  fps
14716
14721
  );
14717
- transitionFrames[0] = aligned.from;
14718
- transitionFrames[transitionFrames.length - 1] = aligned.to;
14722
+ transitionFrames[0] = from;
14723
+ transitionFrames[transitionFrames.length - 1] = to2;
14719
14724
  return transitionFrames;
14720
14725
  } catch (error) {
14721
14726
  logger.error("[AvatarView] Failed to generate transition from idle:", error instanceof Error ? error.message : String(error));
@@ -14761,6 +14766,28 @@ class AvatarView {
14761
14766
  this.renderSystem.renderFrame();
14762
14767
  }
14763
14768
  }
14769
+ reportAvatarActive() {
14770
+ var _a, _b;
14771
+ logEvent("avatar_active", "info", {
14772
+ avatar_id: this.avatar.id,
14773
+ env: (_a = AvatarSDK.configuration) == null ? void 0 : _a.environment,
14774
+ dsm: ((_b = AvatarSDK.configuration) == null ? void 0 : _b.drivingServiceMode) || DrivingServiceMode.sdk
14775
+ });
14776
+ }
14777
+ startAvatarActiveHeartbeat() {
14778
+ this.stopAvatarActiveHeartbeat();
14779
+ this.avatarActiveTimer = window.setInterval(() => {
14780
+ if (this.isInitialized && typeof document !== "undefined" && document.visibilityState === "visible") {
14781
+ this.reportAvatarActive();
14782
+ }
14783
+ }, this.AVATAR_ACTIVE_INTERVAL);
14784
+ }
14785
+ stopAvatarActiveHeartbeat() {
14786
+ if (this.avatarActiveTimer !== null) {
14787
+ clearInterval(this.avatarActiveTimer);
14788
+ this.avatarActiveTimer = null;
14789
+ }
14790
+ }
14764
14791
  }
14765
14792
  export {
14766
14793
  APP_CONFIG as A,
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-D9nprBw2.js";
1
+ import { b, c, f, d, j, g, C, i, D, E, k, h, L, R, S, m } from "./index-Bhjn1nq3.js";
2
2
  export {
3
3
  b as Avatar,
4
4
  c as AvatarController,
@@ -1,6 +1,6 @@
1
1
  import { Flame } from '../generated/driveningress/v1/driveningress';
2
2
 
3
- export declare function linearLerp(from: Flame, to: Flame, progress: number): Flame;
3
+ export declare function bezierLerp(from: Flame, to: Flame, progress: number): Flame;
4
4
 
5
5
  export declare function generateTransitionFrames(from: Flame, to: Flame, durationMs: number, fps?: number): Flame[];
6
6
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@spatialwalk/avatarkit",
3
3
  "type": "module",
4
- "version": "1.0.0-beta.60",
4
+ "version": "1.0.0-beta.62",
5
5
  "description": "SPAvatar SDK - 3D Gaussian Splatting Avatar Rendering SDK",
6
6
  "author": "SPAvatar Team",
7
7
  "license": "MIT",
@@ -1,18 +0,0 @@
1
- import { Environment } from '../types';
2
- declare class HeartbeatManager {
3
- private heartbeatTimer;
4
- private visibilityChangeHandler;
5
- private isInitialized;
6
- private currentEnvironment;
7
- private currentAppId;
8
- private failureCount;
9
- start(environment: Environment): void;
10
- stop(): void;
11
- private startHeartbeatTimer;
12
- private stopHeartbeatTimer;
13
- private reportHeartbeat;
14
- private setupVisibilityListener;
15
- private removeVisibilityListener;
16
- }
17
- export declare const heartbeatManager: HeartbeatManager;
18
- export {};