@spatius/avatarkit 1.3.1-beta.6 → 1.3.1

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.
Files changed (22) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/{AvatarDownloader-Dfi7wtgQ.js → AvatarDownloader-DyhenPKd.js} +98 -43
  3. package/dist/{AvatarSDK-CnZqa_X4.js → AvatarSDK-DgPvHU4L.js} +162 -2560
  4. package/dist/{OpusCodec-D9DcUV65.js → OpusCodec-CXU7ysrU.js} +43 -9
  5. package/dist/{OpusDecoderProxy-Ar_dFXRB.js → OpusDecoderProxy-Cq8Z_MZY.js} +19 -6
  6. package/dist/{OpusEncoderProxy-9YIZ7nIt.js → OpusEncoderProxy-CzY-VckE.js} +42 -7
  7. package/dist/{StreamingAudioPlayer-D4q7c1U0.js → StreamingAudioPlayer-CIJmBp3z.js} +2 -2
  8. package/dist/assets/{AvatarDownloader-D5GePpJ2.js → AvatarDownloader-G5FO1bVl.js} +96 -41
  9. package/dist/assets/{AvatarSDK-DR8dNylh.js → AvatarSDK-C_Anluwp.js} +149 -12
  10. package/dist/assets/{OpusDecoderWorker.worker-B7R93nev.js → OpusDecoderWorker.worker-C2T0afkj.js} +21 -5
  11. package/dist/assets/{OpusEncoderWorker.worker-BYWIV5xu.js → OpusEncoderWorker.worker-DLYO9vPK.js} +53 -11
  12. package/dist/assets/{logger-D8rDVim7.js → logger-YPooEAbA.js} +451 -132
  13. package/dist/core/AvatarController.d.ts +10 -0
  14. package/dist/{error-utils-CqKOrHrY.js → error-utils-BogEllAd.js} +1 -1
  15. package/dist/index.js +279 -46
  16. package/dist/internal-telemetry.d.ts +45 -20
  17. package/dist/internal-telemetry.js +27 -2
  18. package/dist/{logger-fAU8oahT.js → logger-BOpQ7u0w.js} +468 -132
  19. package/dist/otel-trace-Ct4T48nC.js +2565 -0
  20. package/dist/{pwa-cache-manager-CwXaM3xR.js → pwa-cache-manager-C7NbU52Q.js} +1 -1
  21. package/dist/types/index.d.ts +30 -4
  22. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.3.1] - 2026-08-07
9
+
10
+ Rolls up everything from the 1.3.1 beta line.
11
+
12
+ ### Added
13
+ - `DrivingServiceMode.rtc`. Use it **only** when driving the avatar through the companion RTC SDK, `@spatius/avatarkit-rtc`; on `direct` / `backend` nothing changes. It is a telemetry dimension and unlocks no API — declaring it lets RTC-driven sessions be told apart from plain `backend` traffic on the dashboards.
14
+ - Opus audio support. `AudioFormat.inputAudioFormat` (`'pcm'` default, or `'opus'`) declares the format the host feeds into the SDK via `send`/`yieldAudioData`; `'opus'` input is decoded back to PCM16 for local rendering and forwarded upstream as-is in direct mode. `AudioFormat.opusBitrate` (default `48000`) tunes the target bitrate. Only effective in direct mode.
15
+
16
+ ### Changed
17
+ - **The direct-mode uplink now compresses audio to Opus by default** (`AudioFormat.opusUplinkEnabled`, previously off). It cuts the upload to roughly 1/8 at the cost of client-side encoding, which matters most on the mobile networks where stalls actually happen. Pass `opusUplinkEnabled: false` to keep the raw PCM uplink. Unchanged for host mode (no uplink) and for `'opus'` input (already compressed).
18
+ - The `region` configuration now defaults to automatic selection: when left unset, the SDK picks the closest serving region at initialization. Passing an explicit `region` continues to force that region, unchanged. If automatic selection can't be reached, the SDK falls back to a default region and continues initializing.
19
+ - Opus uplink encoding streams: each second of audio is sent as soon as it is encoded, instead of waiting for the whole chunk to finish. When a host hands over a long utterance in one call, the first packet used to wait out the entire encode; playback now starts noticeably sooner.
20
+ - If the configured `sampleRate` is not one of Opus's supported rates, `initialize` logs a warning and falls back to a raw PCM uplink instead of silently failing to build the encoder.
21
+ - `initialize` with a missing `appId` now fails fast (throws) with an updated message pointing to https://app.spatius.ai/ to obtain an app ID.
22
+
23
+ ### Fixed
24
+ - Fixed playback failing to start when the first audio arrived slightly after the animation frames (both host and direct modes) — playback now begins correctly instead of stalling.
25
+ - Fixed an intermittent burst of harsh noise at the start of Opus-input playback: on a fast start the first audio could be played before the Opus decoder finished initializing. Opus input is now always decoded before playback, so no raw Opus is ever played as PCM.
26
+ - Fixed the idle animation freezing (and a delayed start) in direct mode with Opus input when animation frames arrived before the decoded audio: playback now waits for both audio and animation to be ready, and the avatar keeps looping idle smoothly until the round actually begins.
27
+ - Fixed playback getting stuck when audio was sent while the connection was still being established and the session later reconnected: the audio clock could freeze, leaving the avatar frozen on a single frame instead of animating in sync with the audio.
28
+ - Fixed the avatar not returning to idle after a playback round finished in that same send-then-connect scenario; it now correctly transitions back to idle when playback ends.
29
+
30
+ ## [1.3.1-beta.7] - 2026-08-02
31
+
32
+ ### Changed
33
+ - Internal telemetry changes only; no changes to the public API or runtime behavior.
34
+
8
35
  ## [1.3.1-beta.6] - 2026-08-01
9
36
 
10
37
  ### Changed
@@ -1,8 +1,8 @@
1
1
  import { n as __exportAll } from "./rolldown-runtime-B-1-B7_t.js";
2
- import { t as AvatarSDK } from "./AvatarSDK-CnZqa_X4.js";
3
- import { Et as ErrorCode, c as logEvent, gt as generateTraceId, l as logMetric, t as logger, vt as AvatarError, y as recordHttpClientDuration } from "./logger-fAU8oahT.js";
4
- import { n as APP_CONFIG, r as getFlameCdnBase, t as errorToMessage } from "./error-utils-CqKOrHrY.js";
5
- import { t as PwaCacheManager } from "./pwa-cache-manager-CwXaM3xR.js";
2
+ import { t as AvatarSDK } from "./AvatarSDK-DgPvHU4L.js";
3
+ import { S as recordHttpClientDuration, c as logEvent, kt as ErrorCode, l as logMetric, t as logger, v as hostOf, xt as AvatarError, yt as generateTraceId } from "./logger-BOpQ7u0w.js";
4
+ import { n as APP_CONFIG, r as getFlameCdnBase, t as errorToMessage } from "./error-utils-BogEllAd.js";
5
+ import { t as PwaCacheManager } from "./pwa-cache-manager-C7NbU52Q.js";
6
6
  //#region core/AvatarDownloader.ts
7
7
  var AvatarDownloader_exports = /* @__PURE__ */ __exportAll({ AvatarDownloader: () => AvatarDownloader });
8
8
  /**
@@ -47,20 +47,46 @@ function getCacheInfo(url, response) {
47
47
  };
48
48
  }
49
49
  /**
50
- * Simple download helper with CORS proxy support for development
51
- * Static resources don't need appId and token headers
52
- *
53
- * Features:
54
- * - Supports AbortSignal for cancellation
55
- * - Automatic retry (default 3 times, no delay)
56
- * - PWA cache integration
57
- *
58
- * @param url 资源 URL
59
- * @param options 下载选项
60
- * @returns ArrayBuffer and cache information
50
+ * 把整次角色加载的资产下载汇总成**一条** Transaction。
51
+ *
52
+ * 成败取「最差的一片」:任一片 transport error 即整次记 transport error;否则取
53
+ * 状态码最大的那个(4xx/5xx 会盖过 200)。这样后端按状态码切出的成功率,含义是
54
+ * 「这次加载的资产是否全部拿到」——与用户实际体验一致,一片挂了角色就出不来。
55
+ *
56
+ * duration 传整体耗时而非单片,与 `download_avatar_assets_latency` 同口径。
61
57
  */
58
+ function recordAssetTransaction(outcomes, durationMs, operation = ASSET_OPERATION) {
59
+ const real = outcomes.filter((o) => !o.skipped);
60
+ if (real.length === 0) {
61
+ recordHttpClientDuration({
62
+ operation,
63
+ method: "GET",
64
+ durationMs,
65
+ statusCode: 200,
66
+ cacheHit: true
67
+ });
68
+ return;
69
+ }
70
+ recordHttpClientDuration({
71
+ operation,
72
+ method: "GET",
73
+ durationMs,
74
+ statusCode: real.some((o) => o.statusCode === void 0) ? void 0 : real.reduce((max, o) => Math.max(max, o.statusCode ?? 0), 0),
75
+ serverAddress: real[0].host,
76
+ cacheHit: false
77
+ });
78
+ }
79
+ /**
80
+ * Transaction 的 `operation` 取值。必须低基数:资产 URL 带 avatar id 与文件名,
81
+ * 直接当维度会让时间序列随角色数无限增长。要定位「哪个文件挂了」用
82
+ * `download_avatar_assets_failed` log,它带 resource/url/error。
83
+ */
84
+ var ASSET_OPERATION = "/assets/character";
85
+ var TEMPLATE_OPERATION = "/assets/template";
62
86
  async function downloadResource(url, options) {
63
- const { signal, characterId, resourceType, maxRetries = 3 } = options || {};
87
+ const { signal, characterId, resourceType, maxRetries = 3, outcomes } = options || {};
88
+ const outcomeSlotIndex = outcomes ? outcomes.length : -1;
89
+ if (outcomes) outcomes.push({ host: hostOf(url) });
64
90
  if (signal?.aborted) throw new Error("Download cancelled");
65
91
  try {
66
92
  let cached = null;
@@ -79,6 +105,10 @@ async function downloadResource(url, options) {
79
105
  cacheInfo.cacheHit = true;
80
106
  cacheInfo.cacheType = "pwa";
81
107
  cacheInfo.pwaCacheSubtype = pwaCacheSubtype;
108
+ if (outcomes && outcomeSlotIndex >= 0) outcomes[outcomeSlotIndex] = {
109
+ host: "",
110
+ skipped: true
111
+ };
82
112
  return {
83
113
  data: cached,
84
114
  cacheInfo
@@ -87,8 +117,11 @@ async function downloadResource(url, options) {
87
117
  let lastError = null;
88
118
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
89
119
  if (signal?.aborted) throw new Error("Download cancelled");
120
+ const outcomeSlot = { host: hostOf(url) };
121
+ if (outcomes) outcomes[outcomeSlotIndex] = outcomeSlot;
90
122
  try {
91
123
  const response = await fetch(url, { signal });
124
+ outcomeSlot.statusCode = response.status;
92
125
  if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
93
126
  const arrayBuffer = await response.arrayBuffer();
94
127
  const contentLength = response.headers.get("content-length");
@@ -146,32 +179,41 @@ var AvatarDownloader = class {
146
179
  cache_hit: true,
147
180
  cache_type: "pwa"
148
181
  });
182
+ recordAssetTransaction([], duration, TEMPLATE_OPERATION);
149
183
  return { unifiedModel: cached };
150
184
  }
151
185
  const maxRetries = 3;
152
186
  let lastError = null;
153
- for (let attempt = 1; attempt <= maxRetries; attempt++) try {
154
- const response = await fetch(url);
155
- if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
156
- let buffer;
157
- if (APP_CONFIG.flame.unifiedModelPath.endsWith(".gz")) {
158
- const decompressedStream = response.body.pipeThrough(new DecompressionStream("gzip"));
159
- buffer = await new Response(decompressedStream).arrayBuffer();
160
- } else buffer = await response.arrayBuffer();
161
- logger.log(`✅ Unified template loaded (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB)`);
162
- PwaCacheManager.putTemplateResource(url, buffer).catch((err) => {
163
- logger.warn(`[loadUnifiedTemplate] Failed to cache:`, err);
164
- });
165
- logMetric("template_resources_load_measure", Date.now() - startTime, {
166
- file_count: 1,
167
- cache_hit: false,
168
- cache_type: "none"
169
- });
170
- return { unifiedModel: buffer };
171
- } catch (err) {
172
- lastError = err instanceof Error ? err : new Error(String(err));
173
- if (attempt < maxRetries) logger.warn(`[loadUnifiedTemplate] Attempt ${attempt}/${maxRetries} failed, retrying...`);
187
+ const templateOutcomes = [];
188
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
189
+ const outcomeSlot = { host: hostOf(url) };
190
+ templateOutcomes[0] = outcomeSlot;
191
+ try {
192
+ const response = await fetch(url);
193
+ outcomeSlot.statusCode = response.status;
194
+ if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
195
+ let buffer;
196
+ if (APP_CONFIG.flame.unifiedModelPath.endsWith(".gz")) {
197
+ const decompressedStream = response.body.pipeThrough(new DecompressionStream("gzip"));
198
+ buffer = await new Response(decompressedStream).arrayBuffer();
199
+ } else buffer = await response.arrayBuffer();
200
+ logger.log(`✅ Unified template loaded (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB)`);
201
+ PwaCacheManager.putTemplateResource(url, buffer).catch((err) => {
202
+ logger.warn(`[loadUnifiedTemplate] Failed to cache:`, err);
203
+ });
204
+ logMetric("template_resources_load_measure", Date.now() - startTime, {
205
+ file_count: 1,
206
+ cache_hit: false,
207
+ cache_type: "none"
208
+ });
209
+ recordAssetTransaction(templateOutcomes, Date.now() - startTime, TEMPLATE_OPERATION);
210
+ return { unifiedModel: buffer };
211
+ } catch (err) {
212
+ lastError = err instanceof Error ? err : new Error(String(err));
213
+ if (attempt < maxRetries) logger.warn(`[loadUnifiedTemplate] Attempt ${attempt}/${maxRetries} failed, retrying...`);
214
+ }
174
215
  }
216
+ recordAssetTransaction(templateOutcomes, Date.now() - startTime, TEMPLATE_OPERATION);
175
217
  throw lastError || /* @__PURE__ */ new Error(`Failed to download unified template after ${maxRetries} attempts`);
176
218
  }
177
219
  /**
@@ -252,6 +294,8 @@ var AvatarDownloader = class {
252
294
  };
253
295
  const characterData = {};
254
296
  const cacheInfos = [];
297
+ /** 各片的 HTTP 结果,收尾时汇总成一条 Transaction(见 recordAssetTransaction)。 */
298
+ const httpOutcomes = [];
255
299
  const parallelStartTime = Date.now();
256
300
  const downloadPromises = filesToLoad.map(async ({ key, url, filename, optional }) => {
257
301
  updateProgress(filename, false);
@@ -259,7 +303,8 @@ var AvatarDownloader = class {
259
303
  const { data: arrayBuffer, cacheInfo } = await downloadResource(url, {
260
304
  signal,
261
305
  characterId: characterMeta.characterId ?? void 0,
262
- resourceType: "character"
306
+ resourceType: "character",
307
+ outcomes: httpOutcomes
263
308
  });
264
309
  if (key === "shape") characterData.shape = arrayBuffer;
265
310
  else if (key === "pointCloud") characterData.pointCloud = arrayBuffer;
@@ -297,7 +342,11 @@ var AvatarDownloader = class {
297
342
  };
298
343
  }
299
344
  });
300
- await Promise.all(downloadPromises);
345
+ try {
346
+ await Promise.all(downloadPromises);
347
+ } finally {
348
+ recordAssetTransaction(httpOutcomes, Date.now() - totalStartTime);
349
+ }
301
350
  const parallelDuration = Date.now() - parallelStartTime;
302
351
  const totalDuration = Date.now() - totalStartTime;
303
352
  if (!characterData.shape || !characterData.pointCloud) {
@@ -324,7 +373,11 @@ var AvatarDownloader = class {
324
373
  parallel_duration: parallelDuration,
325
374
  total_size: totalSize
326
375
  });
327
- return characterData;
376
+ return {
377
+ data: characterData,
378
+ cacheHit,
379
+ cacheType
380
+ };
328
381
  }
329
382
  /**
330
383
  * Preload all resources (template + character data + camera info + settings)
@@ -333,7 +386,7 @@ var AvatarDownloader = class {
333
386
  async preloadResources(characterMeta, options) {
334
387
  const { progressCallback = null, signal, useCompressedModel = false } = options || {};
335
388
  if (signal?.aborted) throw new Error("Preload cancelled");
336
- const [characterData, preloadCameraSettings] = await Promise.all([this.loadCharacterData(characterMeta, {
389
+ const [characterResult, preloadCameraSettings] = await Promise.all([this.loadCharacterData(characterMeta, {
337
390
  signal,
338
391
  useCompressedModel,
339
392
  progressCallback: (info) => {
@@ -344,9 +397,11 @@ var AvatarDownloader = class {
344
397
  }
345
398
  }), this.loadCameraSettings(characterMeta, { signal })]);
346
399
  return {
347
- characterData,
400
+ characterData: characterResult.data,
348
401
  preloadCameraSettings,
349
- characterSettings: characterMeta.characterSettings
402
+ characterSettings: characterMeta.characterSettings,
403
+ cacheHit: characterResult.cacheHit,
404
+ cacheType: characterResult.cacheType
350
405
  };
351
406
  }
352
407
  /**