@spatius/avatarkit 1.3.1-beta.3 → 1.3.1-beta.5

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
@@ -5,6 +5,30 @@ 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-beta.5] - 2026-07-31
9
+
10
+ ### Changed
11
+ - Internal playback and telemetry changes only; no changes to the public API or runtime behavior.
12
+
13
+ ## [1.3.1-beta.4] - 2026-07-30
14
+
15
+ ### Added
16
+ - 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.opusUplinkEnabled` (default `false`) opts the direct-mode uplink into Opus compression (roughly 1/8 the upload, at the cost of client-side encoding), and `AudioFormat.opusBitrate` (default `48000`) tunes the target bitrate. Only effective in direct mode.
17
+
18
+ ### Changed
19
+ - When `opusUplinkEnabled` is `true` but the configured `sampleRate` is not Opus-compatible, `initialize` now logs a warning and automatically falls back to a raw PCM uplink instead of silently failing to build the encoder.
20
+ - `initialize` with a missing `appId` now fails fast (throws) with an updated message pointing to https://app.spatius.ai/ to obtain an app ID.
21
+
22
+ ### Fixed
23
+ - 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.
24
+ - 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.
25
+ - 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.
26
+
27
+ ## [1.3.1-beta.3] - 2026-07-18
28
+
29
+ ### Changed
30
+ - Internal playback telemetry only; no changes to the public API or runtime behavior.
31
+
8
32
  ## [1.3.1-beta.2] - 2026-07-17
9
33
 
10
34
  ### Fixed
@@ -0,0 +1,525 @@
1
+ import { n as __exportAll } from "./rolldown-runtime-B-1-B7_t.js";
2
+ import { t as AvatarSDK } from "./AvatarSDK-BRDgRJ_Z.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-C61wOfFi.js";
4
+ import { n as APP_CONFIG, r as getFlameCdnBase, t as errorToMessage } from "./error-utils-Ct04L0Ek.js";
5
+ import { t as PwaCacheManager } from "./pwa-cache-manager-0wrNgZ7C.js";
6
+ //#region core/AvatarDownloader.ts
7
+ var AvatarDownloader_exports = /* @__PURE__ */ __exportAll({ AvatarDownloader: () => AvatarDownloader });
8
+ /**
9
+ * Get cache information for a resource
10
+ * Detects browser cache, CDN cache, and PWA (Service Worker) cache
11
+ */
12
+ function getCacheInfo(url, response) {
13
+ const resourceTiming = performance.getEntriesByName(url, "resource")[0];
14
+ const transferSize = resourceTiming?.transferSize ?? 0;
15
+ const deliveryType = resourceTiming?.deliveryType;
16
+ const hasServiceWorker = typeof navigator !== "undefined" && navigator.serviceWorker?.controller !== null;
17
+ const isPwaCache = transferSize === 0 && hasServiceWorker && (deliveryType === "cache" || deliveryType === "serviceworker") && resourceTiming?.duration !== void 0 && resourceTiming.duration > 0;
18
+ const isBrowserCache = transferSize === 0 && !isPwaCache && (!hasServiceWorker || deliveryType === "cache") && resourceTiming?.duration !== void 0 && resourceTiming.duration > 0;
19
+ const cfCacheStatus = response.headers.get("cf-cache-status");
20
+ const xCache = response.headers.get("x-cache");
21
+ const xSwiftCacheTime = response.headers.get("x-swift-cache-time");
22
+ const cdnCacheStatus = cfCacheStatus || xCache || xSwiftCacheTime;
23
+ let isCdnCache = false;
24
+ if (cdnCacheStatus) {
25
+ const statusLower = cdnCacheStatus.toLowerCase();
26
+ if (statusLower === "hit" || statusLower === "hits" || statusLower.includes("hit")) {
27
+ if (!statusLower.includes("miss") && !statusLower.includes("dynamic") && !statusLower.includes("bypass")) isCdnCache = true;
28
+ }
29
+ }
30
+ let cacheHit = false;
31
+ let cacheType = "none";
32
+ if (isPwaCache) {
33
+ cacheHit = true;
34
+ cacheType = "pwa";
35
+ } else if (isBrowserCache) {
36
+ cacheHit = true;
37
+ cacheType = "browser";
38
+ } else if (isCdnCache) {
39
+ cacheHit = true;
40
+ cacheType = "cdn";
41
+ }
42
+ return {
43
+ cacheHit,
44
+ cacheType,
45
+ transferSize,
46
+ cdnCacheStatus: cdnCacheStatus || void 0
47
+ };
48
+ }
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
61
+ */
62
+ async function downloadResource(url, options) {
63
+ const { signal, characterId, resourceType, maxRetries = 3 } = options || {};
64
+ if (signal?.aborted) throw new Error("Download cancelled");
65
+ try {
66
+ let cached = null;
67
+ let pwaCacheSubtype = void 0;
68
+ if (characterId) {
69
+ cached = await PwaCacheManager.getCharacterResource(characterId, url);
70
+ if (cached) pwaCacheSubtype = "character";
71
+ } else if (resourceType === "template") {
72
+ cached = await PwaCacheManager.getTemplateResource(url);
73
+ if (cached) pwaCacheSubtype = "template";
74
+ }
75
+ if (cached) {
76
+ const response = new Response(cached);
77
+ await new Promise((resolve) => setTimeout(resolve, 0));
78
+ const cacheInfo = getCacheInfo(url, response);
79
+ cacheInfo.cacheHit = true;
80
+ cacheInfo.cacheType = "pwa";
81
+ cacheInfo.pwaCacheSubtype = pwaCacheSubtype;
82
+ return {
83
+ data: cached,
84
+ cacheInfo
85
+ };
86
+ }
87
+ let lastError = null;
88
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
89
+ if (signal?.aborted) throw new Error("Download cancelled");
90
+ try {
91
+ const response = await fetch(url, { signal });
92
+ if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
93
+ const arrayBuffer = await response.arrayBuffer();
94
+ const contentLength = response.headers.get("content-length");
95
+ if (contentLength) {
96
+ const expectedSize = parseInt(contentLength, 10);
97
+ if (!isNaN(expectedSize) && arrayBuffer.byteLength < expectedSize) throw new Error(`Download incomplete: received ${arrayBuffer.byteLength} bytes, expected ${expectedSize} bytes`);
98
+ }
99
+ if (characterId) PwaCacheManager.putCharacterResource(characterId, url, arrayBuffer).catch((err) => {
100
+ logger.warn(`[downloadResource] Failed to cache character resource:`, err);
101
+ });
102
+ else if (resourceType === "template") PwaCacheManager.putTemplateResource(url, arrayBuffer).catch((err) => {
103
+ logger.warn(`[downloadResource] Failed to cache template resource:`, err);
104
+ });
105
+ await new Promise((resolve) => setTimeout(resolve, 0));
106
+ return {
107
+ data: arrayBuffer,
108
+ cacheInfo: getCacheInfo(url, response)
109
+ };
110
+ } catch (err) {
111
+ if (err instanceof Error && (err.name === "AbortError" || err.message === "Download cancelled")) throw err;
112
+ lastError = err instanceof Error ? err : new Error(String(err));
113
+ if (attempt < maxRetries) logger.warn(`[downloadResource] Attempt ${attempt}/${maxRetries} failed for ${url}, retrying immediately...`);
114
+ }
115
+ }
116
+ throw lastError || /* @__PURE__ */ new Error(`Failed to download ${url} after ${maxRetries} attempts`);
117
+ } catch (err) {
118
+ if (err instanceof Error && (err.name === "AbortError" || err.message === "Download cancelled")) throw err;
119
+ const msg = errorToMessage(err);
120
+ throw new Error(`[downloadResource] ${url} → ${msg}`);
121
+ }
122
+ }
123
+ var AvatarDownloader = class {
124
+ baseAssetsPath;
125
+ constructor(baseAssetsPath = "/") {
126
+ this.baseAssetsPath = baseAssetsPath;
127
+ }
128
+ /**
129
+ * Load unified template model (single gzip-compressed file)
130
+ * Includes PWA cache, retry, integrity check, and telemetry
131
+ * @internal
132
+ */
133
+ async loadUnifiedTemplate() {
134
+ await PwaCacheManager.checkTemplateCacheVersion();
135
+ const startTime = Date.now();
136
+ const cdnBase = getFlameCdnBase(AvatarSDK.configuration?.region || "us-west");
137
+ const { unifiedModelPath } = APP_CONFIG.flame;
138
+ const url = `${cdnBase}/${unifiedModelPath}`;
139
+ logger.log(`📥 Loading unified template from: ${url}`);
140
+ const cached = await PwaCacheManager.getTemplateResource(url);
141
+ if (cached) {
142
+ const duration = Date.now() - startTime;
143
+ logger.log(`✅ Unified template loaded from cache (${(cached.byteLength / 1024 / 1024).toFixed(1)} MB)`);
144
+ logMetric("template_resources_load_measure", duration, {
145
+ file_count: 1,
146
+ cache_hit: true,
147
+ cache_type: "pwa"
148
+ });
149
+ return { unifiedModel: cached };
150
+ }
151
+ const maxRetries = 3;
152
+ 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...`);
174
+ }
175
+ throw lastError || /* @__PURE__ */ new Error(`Failed to download unified template after ${maxRetries} attempts`);
176
+ }
177
+ /**
178
+ * Load camera settings from CharacterMeta (optional)
179
+ * @internal
180
+ */
181
+ async loadCameraSettings(characterMeta, options) {
182
+ const { signal } = options || {};
183
+ const cameraUrl = characterMeta.camera?.resource?.remote;
184
+ if (!cameraUrl) {
185
+ logger.log("ℹ️ No camera resource URL provided");
186
+ return;
187
+ }
188
+ if (signal?.aborted) throw new Error("Load cancelled");
189
+ try {
190
+ logger.log(`📥 Loading camera info from: ${cameraUrl}`);
191
+ const { data: arrayBuffer } = await downloadResource(cameraUrl, {
192
+ signal,
193
+ characterId: characterMeta.characterId ?? void 0,
194
+ resourceType: "character"
195
+ });
196
+ const text = new TextDecoder().decode(arrayBuffer);
197
+ const cameraSettings = JSON.parse(text);
198
+ logger.log("✅ Camera info loaded:", cameraSettings);
199
+ return cameraSettings;
200
+ } catch (error) {
201
+ logger.warn("⚠️ Failed to load camera info, using default:", error);
202
+ return;
203
+ }
204
+ }
205
+ /**
206
+ * Load character data from CharacterMeta (iOS compatible)
207
+ * @internal
208
+ */
209
+ async loadCharacterData(characterMeta, options) {
210
+ const { progressCallback = null, signal, useCompressedModel = false } = options || {};
211
+ if (signal?.aborted) throw new Error("Download cancelled");
212
+ const totalStartTime = Date.now();
213
+ const shapeUrl = characterMeta.models?.shape?.resource?.remote;
214
+ const pointCloudUrl = useCompressedModel ? characterMeta.models?.gsStandard?.xrResource?.remote ?? characterMeta.models?.gsStandard?.resource?.remote : characterMeta.models?.gsStandard?.resource?.remote;
215
+ const idleAnimationUrl = characterMeta.animations?.frameIdle?.resource?.remote;
216
+ const monoAnimationUrl = characterMeta.animations?.frameMono?.resource?.remote;
217
+ if (!shapeUrl || !pointCloudUrl) throw new Error("Missing required resources: shape or gsStandard (point cloud)");
218
+ const filesToLoad = [{
219
+ key: "shape",
220
+ url: shapeUrl,
221
+ filename: "shape.pb"
222
+ }, {
223
+ key: "pointCloud",
224
+ url: pointCloudUrl,
225
+ filename: "point_cloud.ply"
226
+ }];
227
+ if (idleAnimationUrl) filesToLoad.push({
228
+ key: "idleAnimation",
229
+ url: idleAnimationUrl,
230
+ filename: "idle.pb",
231
+ optional: true
232
+ });
233
+ if (monoAnimationUrl) filesToLoad.push({
234
+ key: "monoAnimation",
235
+ url: monoAnimationUrl,
236
+ filename: "mono.pb",
237
+ optional: true
238
+ });
239
+ let loadedFiles = 0;
240
+ const totalFiles = filesToLoad.length;
241
+ const updateProgress = (filename, loaded) => {
242
+ if (progressCallback) {
243
+ if (loaded) loadedFiles++;
244
+ progressCallback({
245
+ stage: "character",
246
+ filename,
247
+ loaded: loadedFiles,
248
+ total: totalFiles,
249
+ progress: loadedFiles / totalFiles
250
+ });
251
+ }
252
+ };
253
+ const characterData = {};
254
+ const cacheInfos = [];
255
+ const parallelStartTime = Date.now();
256
+ const downloadPromises = filesToLoad.map(async ({ key, url, filename, optional }) => {
257
+ updateProgress(filename, false);
258
+ try {
259
+ const { data: arrayBuffer, cacheInfo } = await downloadResource(url, {
260
+ signal,
261
+ characterId: characterMeta.characterId ?? void 0,
262
+ resourceType: "character"
263
+ });
264
+ if (key === "shape") characterData.shape = arrayBuffer;
265
+ else if (key === "pointCloud") characterData.pointCloud = arrayBuffer;
266
+ else if (key === "idleAnimation") characterData.idleAnimation = arrayBuffer;
267
+ else if (key === "monoAnimation") characterData.monoAnimation = arrayBuffer;
268
+ cacheInfos.push(cacheInfo);
269
+ updateProgress(filename, true);
270
+ return {
271
+ key,
272
+ success: true,
273
+ size: arrayBuffer.byteLength
274
+ };
275
+ } catch (error) {
276
+ if (error instanceof Error && (error.name === "AbortError" || error.message === "Download cancelled")) {
277
+ logEvent("download_avatar_assets_cancelled", "info", { avatar_id: characterMeta.characterId ?? "unknown" });
278
+ throw error;
279
+ }
280
+ if (!optional) {
281
+ const errorMessage = error instanceof Error ? error.message : String(error);
282
+ logEvent("download_avatar_assets_failed", "error", {
283
+ avatar_id: characterMeta.characterId ?? "unknown",
284
+ description: `Failed to download required resource: ${filename}`,
285
+ resource: key,
286
+ url,
287
+ error: errorMessage
288
+ });
289
+ throw error;
290
+ }
291
+ logger.warn(`⚠️ Optional resource ${filename} failed to load:`, error);
292
+ updateProgress(filename, true);
293
+ return {
294
+ key,
295
+ success: false,
296
+ size: 0
297
+ };
298
+ }
299
+ });
300
+ await Promise.all(downloadPromises);
301
+ const parallelDuration = Date.now() - parallelStartTime;
302
+ const totalDuration = Date.now() - totalStartTime;
303
+ if (!characterData.shape || !characterData.pointCloud) {
304
+ const reason = "Failed to load character data";
305
+ logEvent("download_avatar_assets_failed", "error", {
306
+ avatar_id: characterMeta.characterId ?? "unknown",
307
+ description: reason
308
+ });
309
+ throw new Error(reason);
310
+ }
311
+ const cacheHit = cacheInfos.length > 0 && cacheInfos.every((info) => info.cacheHit);
312
+ const cacheType = cacheInfos[0]?.cacheType || "none";
313
+ const totalSize = Object.values(characterData).reduce((sum, buffer) => {
314
+ return sum + (buffer ? buffer.byteLength : 0);
315
+ }, 0);
316
+ logMetric("download_avatar_assets_latency", totalDuration, {
317
+ resolution: "default",
318
+ use_compressed_model: useCompressedModel,
319
+ file_count: filesToLoad.length,
320
+ cache_hit: cacheHit,
321
+ cache_type: cacheType
322
+ }, {
323
+ avatar_id: characterMeta.characterId ?? "unknown",
324
+ parallel_duration: parallelDuration,
325
+ total_size: totalSize
326
+ });
327
+ return characterData;
328
+ }
329
+ /**
330
+ * Preload all resources (template + character data + camera info + settings)
331
+ * @internal
332
+ */
333
+ async preloadResources(characterMeta, options) {
334
+ const { progressCallback = null, signal, useCompressedModel = false } = options || {};
335
+ if (signal?.aborted) throw new Error("Preload cancelled");
336
+ const [characterData, preloadCameraSettings] = await Promise.all([this.loadCharacterData(characterMeta, {
337
+ signal,
338
+ useCompressedModel,
339
+ progressCallback: (info) => {
340
+ if (progressCallback) progressCallback({
341
+ ...info,
342
+ stage: `character-${info.stage}`
343
+ });
344
+ }
345
+ }), this.loadCameraSettings(characterMeta, { signal })]);
346
+ return {
347
+ characterData,
348
+ preloadCameraSettings,
349
+ characterSettings: characterMeta.characterSettings
350
+ };
351
+ }
352
+ /**
353
+ * Get AvatarKit SDK API Client (region-templated endpoint composed by AvatarSDK.getEnvironmentConfig).
354
+ * Used for: character details and resource URLs (public endpoints, no auth required)
355
+ * Note: This endpoint does not require authentication, so we don't add X-App-Id or Authorization headers
356
+ * to avoid CORS preflight requests for simple GET requests
357
+ */
358
+ getSdkApiClient() {
359
+ return { async request(url, options = {}) {
360
+ const baseUrl = AvatarSDK.getEnvironmentConfig().sdkApiBaseUrl;
361
+ const fullUrl = baseUrl + url;
362
+ const headers = {};
363
+ const method = options.method || "GET";
364
+ if (method !== "GET" && options.body) headers["Content-Type"] = "application/json";
365
+ const operation = url.split("?")[0].replace(/\/v2\/avatar\/[^/?]+/, "/v2/avatar/{id}");
366
+ let serverAddress;
367
+ try {
368
+ serverAddress = new URL(baseUrl).host;
369
+ } catch {}
370
+ const startMs = performance.now();
371
+ try {
372
+ const response = await fetch(fullUrl, {
373
+ method,
374
+ headers: {
375
+ ...headers,
376
+ ...options.headers
377
+ },
378
+ body: options.body ? JSON.stringify(options.body) : void 0,
379
+ signal: options.signal
380
+ });
381
+ recordHttpClientDuration({
382
+ operation,
383
+ method,
384
+ durationMs: Math.round(performance.now() - startMs),
385
+ statusCode: response.status,
386
+ serverAddress
387
+ });
388
+ if (!response.ok) {
389
+ let serverMessage = "";
390
+ try {
391
+ const body = await response.json();
392
+ if (body?.errors && Array.isArray(body.errors) && body.errors.length > 0) {
393
+ const e = body.errors[0];
394
+ serverMessage = e.detail || e.title || e.message || JSON.stringify(e);
395
+ } else serverMessage = body?.message || body?.error || JSON.stringify(body);
396
+ } catch {
397
+ serverMessage = response.statusText;
398
+ }
399
+ let error;
400
+ if (response.status === 404) {
401
+ const urlMatch = url.match(/\/v2\/(?:character|avatar)\/([^/?]+)/);
402
+ const extractedCharacterId = urlMatch ? urlMatch[1] : "unknown";
403
+ const callerTraceId = (options.headers || {})["x-sp-trace-id"];
404
+ logEvent("avatar_id_unrecognized", "error", {
405
+ avatar_id: extractedCharacterId,
406
+ description: `HTTP 404: ${serverMessage}`,
407
+ ...callerTraceId ? { trace_id: callerTraceId } : {}
408
+ });
409
+ error = new AvatarError(`HTTP 404: ${serverMessage}`, ErrorCode.avatarIDUnrecognized);
410
+ } else error = new AvatarError(`HTTP ${response.status}: ${serverMessage}`, ErrorCode.failedToFetchAvatarMetadata);
411
+ throw error;
412
+ }
413
+ try {
414
+ return await response.json();
415
+ } catch {
416
+ throw new AvatarError("Avatar data is invalid. Please contact Spatius support.", ErrorCode.invalidAvatarMetadata);
417
+ }
418
+ } catch (err) {
419
+ if (err instanceof AvatarError) throw err;
420
+ recordHttpClientDuration({
421
+ operation,
422
+ method,
423
+ durationMs: Math.round(performance.now() - startMs),
424
+ serverAddress
425
+ });
426
+ throw new AvatarError("Failed to load avatar due to network issues, please check your connection and try again.", ErrorCode.failedToFetchAvatarMetadata);
427
+ }
428
+ } };
429
+ }
430
+ /**
431
+ * Map the new `/v2/avatar/{id}` `AvatarAsset` payload onto the internal
432
+ * `CharacterMeta` shape used by the existing download / render pipeline.
433
+ *
434
+ * The backend (grpc-gateway) serialises proto fields as camelCase JSON, so the
435
+ * runtime object is loosely shaped like the generated `AvatarAsset`. We:
436
+ * - lift `models.gs` into `models.gsStandard` (downloader/asset-count read gsStandard)
437
+ * - rename `animations.frameFallback` → `animations.frameMono`
438
+ * - fold the inline `camera` / `transform` into `characterSettings` so the
439
+ * renderer's `resolveCameraConfig` reads structured values and no camera
440
+ * resource is downloaded (top-level `camera` is intentionally left unset)
441
+ * @internal
442
+ */
443
+ mapAvatarAssetToCharacterMeta(asset, avatarId) {
444
+ return {
445
+ characterId: avatarId,
446
+ version: asset.version ?? "",
447
+ compatibilityFlags: asset.compatibilityFlags ?? [],
448
+ updatedAt: asset.updatedAt,
449
+ models: {
450
+ shape: asset.models?.shape,
451
+ gsStandard: asset.models?.gs
452
+ },
453
+ animations: {
454
+ frameIdle: asset.animations?.frameIdle,
455
+ frameMono: asset.animations?.frameFallback
456
+ },
457
+ customAnimations: asset.animations?.customAnimations ?? [],
458
+ characterSettings: {
459
+ ...asset.camera ? { camera: { ...asset.camera } } : {},
460
+ ...asset.transform ? { transform: { ...asset.transform } } : {}
461
+ }
462
+ };
463
+ }
464
+ /**
465
+ * Get single avatar by ID from AvatarKit SDK API (v2 driven-ingress avatar API).
466
+ * Domain: composed from region as api.${region}.spatius.ai
467
+ * Auth: Public endpoint, no authentication required
468
+ * Fetches the new `AvatarAsset` payload from `/v2/avatar/{id}` and maps it onto
469
+ * the internal `CharacterMeta` shape consumed by the download / render pipeline:
470
+ * - `models.gs` → `models.gsStandard`
471
+ * - `animations.frameFallback` → `animations.frameMono`
472
+ * - inline `camera` / `transform` → `characterSettings.{camera,transform}`
473
+ * (so the renderer reads structured values directly and no camera resource is downloaded)
474
+ * @internal
475
+ */
476
+ async getCharacterById(characterId, options) {
477
+ const { signal } = options || {};
478
+ const startTime = Date.now();
479
+ const traceId = generateTraceId();
480
+ try {
481
+ if (signal?.aborted) throw new Error("Request cancelled");
482
+ const response = await this.getSdkApiClient().request(`/v2/avatar/${characterId}`, {
483
+ method: "GET",
484
+ headers: { "x-sp-trace-id": traceId },
485
+ signal
486
+ });
487
+ if (response?.errors && Array.isArray(response.errors) && response.errors.length > 0) {
488
+ const firstError = response.errors[0];
489
+ throw new AvatarError(`${firstError.code || firstError.status || "SERVER_ERROR"}: ${firstError.detail || firstError.title || firstError.message || "Unknown server error"}`, ErrorCode.failedToFetchAvatarMetadata);
490
+ }
491
+ logMetric("fetch_avatar_metadata_latency", Date.now() - startTime, {}, {
492
+ avatar_id: characterId,
493
+ trace_id: traceId
494
+ });
495
+ return this.mapAvatarAssetToCharacterMeta(response, characterId);
496
+ } catch (error) {
497
+ if (error instanceof Error && (error.name === "AbortError" || error.message === "Request cancelled")) {
498
+ logEvent("fetch_avatar_metadata_cancelled", "info", {
499
+ avatar_id: characterId ?? "unknown",
500
+ trace_id: traceId
501
+ });
502
+ throw error;
503
+ }
504
+ logger.error("Failed to fetch character:", error);
505
+ if (error instanceof AvatarError) {
506
+ logEvent("fetch_avatar_metadata_failed", "error", {
507
+ avatar_id: characterId ?? "unknown",
508
+ description: error.message,
509
+ trace_id: traceId
510
+ });
511
+ throw error;
512
+ }
513
+ const errorMessage = error && typeof error === "object" && "message" in error ? String(error.message) : "Failed to fetch character";
514
+ const dataMessage = error && typeof error === "object" && "data" in error && typeof error.data === "object" && error.data?.message ? String(error.data.message) : null;
515
+ logEvent("fetch_avatar_metadata_failed", "error", {
516
+ avatar_id: characterId ?? "unknown",
517
+ description: dataMessage || errorMessage,
518
+ trace_id: traceId
519
+ });
520
+ throw new AvatarError("Failed to load avatar due to network issues, please check your connection and try again.", ErrorCode.failedToFetchAvatarMetadata);
521
+ }
522
+ }
523
+ };
524
+ //#endregion
525
+ export { AvatarDownloader_exports as n, AvatarDownloader as t };