@spatius/avatarkit 1.3.5-beta.1 → 1.3.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.
@@ -6,15 +6,6 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
8
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
9
- var __exportAll = (all, no_symbols) => {
10
- let target = {};
11
- for (var name in all) __defProp(target, name, {
12
- get: all[name],
13
- enumerable: true
14
- });
15
- if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
16
- return target;
17
- };
18
9
  var __copyProps = (to, from, except, desc) => {
19
10
  if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
20
11
  key = keys[i];
@@ -30,4 +21,4 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
21
  enumerable: true
31
22
  }) : target, mod));
32
23
  //#endregion
33
- export { __exportAll as n, __toESM as r, __commonJSMin as t };
24
+ export { __toESM as n, __commonJSMin as t };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@spatius/avatarkit",
3
3
  "type": "module",
4
- "version": "1.3.5-beta.1",
4
+ "version": "1.3.5",
5
5
  "packageManager": "pnpm@10.18.2",
6
6
  "description": "AvatarKit — real-time, audio-driven avatar rendering SDK for Web.",
7
7
  "homepage": "https://spatius.ai/",
@@ -1,616 +0,0 @@
1
- import { n as __exportAll } from "./rolldown-runtime-B-1-B7_t.js";
2
- import { Mt as ErrorCode, S as recordHttpClientDuration, St as generateTraceId, c as logEvent, l as logMetric, t as logger, v as hostOf, wt as AvatarError } from "./logger-BCrlTIt2.js";
3
- import { n as APP_CONFIG, r as getFlameCdnBase, t as errorToMessage } from "./error-utils-CX5P3vnW.js";
4
- import { t as AvatarSDK } from "./AvatarSDK-DCYDBMwd.js";
5
- import { t as PwaCacheManager } from "./pwa-cache-manager-C7su6HpW.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
- * 把整次角色加载的资产下载汇总成**一条** Transaction。
51
- *
52
- * 成败取「最差的一片」:任一片 transport error 即整次记 transport error;否则取
53
- * 状态码最大的那个(4xx/5xx 会盖过 200)。这样后端按状态码切出的成功率,含义是
54
- * 「这次加载的资产是否全部拿到」——与用户实际体验一致,一片挂了角色就出不来。
55
- *
56
- * duration 传整体耗时而非单片,与 `download_avatar_assets_latency` 同口径。
57
- */
58
- function recordAssetTransaction(outcomes, durationMs, operation = ASSET_OPERATION) {
59
- const real = outcomes.filter((o) => !o.skipped);
60
- if (real.length === 0) return;
61
- recordHttpClientDuration({
62
- operation,
63
- method: "GET",
64
- durationMs,
65
- statusCode: real.some((o) => o.statusCode === void 0) ? void 0 : real.reduce((max, o) => Math.max(max, o.statusCode ?? 0), 0),
66
- serverAddress: real[0].host
67
- });
68
- }
69
- /**
70
- * Transaction 的 `operation` 取值。必须低基数:资产 URL 带 avatar id 与文件名,
71
- * 直接当维度会让时间序列随角色数无限增长。要定位「哪个文件挂了」用
72
- * `download_avatar_assets_failed` log,它带 resource/url/error。
73
- */
74
- var ASSET_OPERATION = "/assets/character";
75
- var TEMPLATE_OPERATION = "/assets/template";
76
- async function downloadResource(url, options) {
77
- const { signal, characterId, resourceType, maxRetries = 3, outcomes } = options || {};
78
- const outcomeSlotIndex = outcomes ? outcomes.length : -1;
79
- if (outcomes) outcomes.push({ host: hostOf(url) });
80
- if (signal?.aborted) throw new Error("Download cancelled");
81
- try {
82
- let cached = null;
83
- let pwaCacheSubtype = void 0;
84
- if (characterId) {
85
- cached = await PwaCacheManager.getCharacterResource(characterId, url);
86
- if (cached) pwaCacheSubtype = "character";
87
- } else if (resourceType === "template") {
88
- cached = await PwaCacheManager.getTemplateResource(url);
89
- if (cached) pwaCacheSubtype = "template";
90
- }
91
- if (cached) {
92
- const response = new Response(cached);
93
- await new Promise((resolve) => setTimeout(resolve, 0));
94
- const cacheInfo = getCacheInfo(url, response);
95
- cacheInfo.cacheHit = true;
96
- cacheInfo.cacheType = "pwa";
97
- cacheInfo.pwaCacheSubtype = pwaCacheSubtype;
98
- if (outcomes && outcomeSlotIndex >= 0) outcomes[outcomeSlotIndex] = {
99
- host: hostOf(url),
100
- skipped: true
101
- };
102
- return {
103
- data: cached,
104
- cacheInfo
105
- };
106
- }
107
- let lastError = null;
108
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
109
- if (signal?.aborted) throw new Error("Download cancelled");
110
- const outcomeSlot = { host: hostOf(url) };
111
- if (outcomes) outcomes[outcomeSlotIndex] = outcomeSlot;
112
- try {
113
- const response = await fetch(url, { signal });
114
- outcomeSlot.statusCode = response.status;
115
- if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
116
- const arrayBuffer = await response.arrayBuffer();
117
- const contentLength = response.headers.get("content-length");
118
- if (contentLength) {
119
- const expectedSize = parseInt(contentLength, 10);
120
- if (!isNaN(expectedSize) && arrayBuffer.byteLength < expectedSize) throw new Error(`Download incomplete: received ${arrayBuffer.byteLength} bytes, expected ${expectedSize} bytes`);
121
- }
122
- if (characterId) PwaCacheManager.putCharacterResource(characterId, url, arrayBuffer).catch((err) => {
123
- logger.warn(`[downloadResource] Failed to cache character resource:`, err);
124
- });
125
- else if (resourceType === "template") PwaCacheManager.putTemplateResource(url, arrayBuffer).catch((err) => {
126
- logger.warn(`[downloadResource] Failed to cache template resource:`, err);
127
- });
128
- await new Promise((resolve) => setTimeout(resolve, 0));
129
- return {
130
- data: arrayBuffer,
131
- cacheInfo: getCacheInfo(url, response)
132
- };
133
- } catch (err) {
134
- if (err instanceof Error && (err.name === "AbortError" || err.message === "Download cancelled")) throw err;
135
- lastError = err instanceof Error ? err : new Error(String(err));
136
- if (attempt < maxRetries) logger.warn(`[downloadResource] Attempt ${attempt}/${maxRetries} failed for ${url}, retrying immediately...`);
137
- }
138
- }
139
- throw lastError || /* @__PURE__ */ new Error(`Failed to download ${url} after ${maxRetries} attempts`);
140
- } catch (err) {
141
- if (err instanceof Error && (err.name === "AbortError" || err.message === "Download cancelled")) throw err;
142
- const msg = errorToMessage(err);
143
- throw new Error(`[downloadResource] ${url} → ${msg}`);
144
- }
145
- }
146
- var AvatarDownloader = class {
147
- baseAssetsPath;
148
- constructor(baseAssetsPath = "/") {
149
- this.baseAssetsPath = baseAssetsPath;
150
- }
151
- /**
152
- * Load unified template model (single gzip-compressed file)
153
- * Includes PWA cache, retry, integrity check, and telemetry
154
- * @internal
155
- */
156
- async loadUnifiedTemplate() {
157
- await PwaCacheManager.checkTemplateCacheVersion();
158
- const startTime = Date.now();
159
- const cdnBase = getFlameCdnBase(AvatarSDK.configuration?.region || "us-west");
160
- const { unifiedModelPath } = APP_CONFIG.flame;
161
- const url = `${cdnBase}/${unifiedModelPath}`;
162
- logger.log(`📥 Loading unified template from: ${url}`);
163
- const cached = await PwaCacheManager.getTemplateResource(url);
164
- if (cached) {
165
- const duration = Date.now() - startTime;
166
- logger.log(`✅ Unified template loaded from cache (${(cached.byteLength / 1024 / 1024).toFixed(1)} MB)`);
167
- logMetric("template_resources_load_measure", duration, {
168
- file_count: 1,
169
- cache_hit: true,
170
- cache_type: "pwa",
171
- result: "success"
172
- });
173
- return { unifiedModel: cached };
174
- }
175
- const maxRetries = 3;
176
- let lastError = null;
177
- const templateOutcomes = [];
178
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
179
- const outcomeSlot = { host: hostOf(url) };
180
- templateOutcomes[0] = outcomeSlot;
181
- try {
182
- const response = await fetch(url);
183
- outcomeSlot.statusCode = response.status;
184
- if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
185
- let buffer;
186
- if (APP_CONFIG.flame.unifiedModelPath.endsWith(".gz")) {
187
- const decompressedStream = response.body.pipeThrough(new DecompressionStream("gzip"));
188
- buffer = await new Response(decompressedStream).arrayBuffer();
189
- } else buffer = await response.arrayBuffer();
190
- logger.log(`✅ Unified template loaded (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB)`);
191
- PwaCacheManager.putTemplateResource(url, buffer).catch((err) => {
192
- logger.warn(`[loadUnifiedTemplate] Failed to cache:`, err);
193
- });
194
- logMetric("template_resources_load_measure", Date.now() - startTime, {
195
- file_count: 1,
196
- cache_hit: false,
197
- cache_type: "none",
198
- result: "success"
199
- });
200
- recordAssetTransaction(templateOutcomes, Date.now() - startTime, TEMPLATE_OPERATION);
201
- return { unifiedModel: buffer };
202
- } catch (err) {
203
- lastError = err instanceof Error ? err : new Error(String(err));
204
- if (attempt < maxRetries) logger.warn(`[loadUnifiedTemplate] Attempt ${attempt}/${maxRetries} failed, retrying...`);
205
- }
206
- }
207
- recordAssetTransaction(templateOutcomes, Date.now() - startTime, TEMPLATE_OPERATION);
208
- const lastStatus = templateOutcomes[0]?.statusCode;
209
- logMetric("template_resources_load_measure", Date.now() - startTime, {
210
- file_count: 1,
211
- cache_hit: false,
212
- cache_type: "none",
213
- result: lastStatus !== void 0 && lastStatus >= 200 && lastStatus < 400 ? "decode_failed" : "download_failed"
214
- }, {
215
- description: lastError?.message ?? "unknown",
216
- ...lastStatus !== void 0 ? { last_status_code: lastStatus } : {}
217
- });
218
- throw lastError || /* @__PURE__ */ new Error(`Failed to download unified template after ${maxRetries} attempts`);
219
- }
220
- /**
221
- * Load camera settings from CharacterMeta (optional)
222
- * @internal
223
- */
224
- async loadCameraSettings(characterMeta, options) {
225
- const { signal } = options || {};
226
- const cameraUrl = characterMeta.camera?.resource?.remote;
227
- if (!cameraUrl) {
228
- logger.log("ℹ️ No camera resource URL provided");
229
- return;
230
- }
231
- if (signal?.aborted) throw new Error("Load cancelled");
232
- try {
233
- logger.log(`📥 Loading camera info from: ${cameraUrl}`);
234
- const { data: arrayBuffer } = await downloadResource(cameraUrl, {
235
- signal,
236
- characterId: characterMeta.characterId ?? void 0,
237
- resourceType: "character"
238
- });
239
- const text = new TextDecoder().decode(arrayBuffer);
240
- const cameraSettings = JSON.parse(text);
241
- logger.log("✅ Camera info loaded:", cameraSettings);
242
- return cameraSettings;
243
- } catch (error) {
244
- logger.warn("⚠️ Failed to load camera info, using default:", error);
245
- return;
246
- }
247
- }
248
- /**
249
- * Load character data from CharacterMeta (iOS compatible)
250
- * @internal
251
- */
252
- async loadCharacterData(characterMeta, options) {
253
- const { progressCallback = null, signal, useCompressedModel = false } = options || {};
254
- if (signal?.aborted) throw new Error("Download cancelled");
255
- const totalStartTime = Date.now();
256
- const shapeUrl = characterMeta.models?.shape?.resource?.remote;
257
- const pointCloudUrl = useCompressedModel ? characterMeta.models?.gsStandard?.xrResource?.remote ?? characterMeta.models?.gsStandard?.resource?.remote : characterMeta.models?.gsStandard?.resource?.remote;
258
- const idleAnimationUrl = characterMeta.animations?.frameIdle?.resource?.remote;
259
- const monoAnimationUrl = characterMeta.animations?.frameMono?.resource?.remote;
260
- if (!shapeUrl || !pointCloudUrl) throw new Error("Missing required resources: shape or gsStandard (point cloud)");
261
- const filesToLoad = [{
262
- key: "shape",
263
- url: shapeUrl,
264
- filename: "shape.pb"
265
- }, {
266
- key: "pointCloud",
267
- url: pointCloudUrl,
268
- filename: "point_cloud.ply"
269
- }];
270
- if (idleAnimationUrl) filesToLoad.push({
271
- key: "idleAnimation",
272
- url: idleAnimationUrl,
273
- filename: "idle.pb",
274
- optional: true
275
- });
276
- if (monoAnimationUrl) filesToLoad.push({
277
- key: "monoAnimation",
278
- url: monoAnimationUrl,
279
- filename: "mono.pb",
280
- optional: true
281
- });
282
- let loadedFiles = 0;
283
- const totalFiles = filesToLoad.length;
284
- const updateProgress = (filename, loaded) => {
285
- if (progressCallback) {
286
- if (loaded) loadedFiles++;
287
- progressCallback({
288
- stage: "character",
289
- filename,
290
- loaded: loadedFiles,
291
- total: totalFiles,
292
- progress: loadedFiles / totalFiles
293
- });
294
- }
295
- };
296
- const characterData = {};
297
- const cacheInfos = [];
298
- /** 各片的 HTTP 结果,收尾时汇总成一条 Transaction(见 recordAssetTransaction)。 */
299
- const httpOutcomes = [];
300
- const parallelStartTime = Date.now();
301
- const downloadPromises = filesToLoad.map(async ({ key, url, filename, optional }) => {
302
- updateProgress(filename, false);
303
- try {
304
- const { data: arrayBuffer, cacheInfo } = await downloadResource(url, {
305
- signal,
306
- characterId: characterMeta.characterId ?? void 0,
307
- resourceType: "character",
308
- outcomes: httpOutcomes
309
- });
310
- if (key === "shape") characterData.shape = arrayBuffer;
311
- else if (key === "pointCloud") characterData.pointCloud = arrayBuffer;
312
- else if (key === "idleAnimation") characterData.idleAnimation = arrayBuffer;
313
- else if (key === "monoAnimation") characterData.monoAnimation = arrayBuffer;
314
- cacheInfos.push(cacheInfo);
315
- updateProgress(filename, true);
316
- return {
317
- key,
318
- success: true,
319
- size: arrayBuffer.byteLength
320
- };
321
- } catch (error) {
322
- if (error instanceof Error && (error.name === "AbortError" || error.message === "Download cancelled")) {
323
- logEvent("download_avatar_assets_cancelled", "info", { avatar_id: characterMeta.characterId ?? "unknown" });
324
- throw error;
325
- }
326
- if (!optional) {
327
- const errorMessage = error instanceof Error ? error.message : String(error);
328
- logEvent("download_avatar_assets_failed", "error", {
329
- avatar_id: characterMeta.characterId ?? "unknown",
330
- description: `Failed to download required resource: ${filename}`,
331
- resource: key,
332
- url,
333
- error: errorMessage
334
- });
335
- throw error;
336
- }
337
- logger.warn(`⚠️ Optional resource ${filename} failed to load:`, error);
338
- updateProgress(filename, true);
339
- return {
340
- key,
341
- success: false,
342
- size: 0
343
- };
344
- }
345
- });
346
- try {
347
- await Promise.all(downloadPromises);
348
- } catch (error) {
349
- if (!(error instanceof Error && (error.name === "AbortError" || error.message === "Download cancelled"))) logMetric("download_avatar_assets_latency", Date.now() - totalStartTime, {
350
- resolution: "default",
351
- use_compressed_model: useCompressedModel,
352
- file_count: filesToLoad.length,
353
- cache_hit: false,
354
- cache_type: "none",
355
- result: "download_failed"
356
- }, {
357
- avatar_id: characterMeta.characterId ?? "unknown",
358
- description: error instanceof Error ? error.message : String(error)
359
- });
360
- throw error;
361
- } finally {
362
- recordAssetTransaction(httpOutcomes, Date.now() - totalStartTime);
363
- }
364
- const parallelDuration = Date.now() - parallelStartTime;
365
- const totalDuration = Date.now() - totalStartTime;
366
- if (!characterData.shape || !characterData.pointCloud) {
367
- const reason = "Failed to load character data";
368
- logMetric("download_avatar_assets_latency", totalDuration, {
369
- resolution: "default",
370
- use_compressed_model: useCompressedModel,
371
- file_count: filesToLoad.length,
372
- cache_hit: false,
373
- cache_type: "none",
374
- result: "incomplete_data"
375
- }, {
376
- avatar_id: characterMeta.characterId ?? "unknown",
377
- description: reason
378
- });
379
- logEvent("download_avatar_assets_failed", "error", {
380
- avatar_id: characterMeta.characterId ?? "unknown",
381
- description: reason
382
- });
383
- throw new Error(reason);
384
- }
385
- const cacheHit = cacheInfos.length > 0 && cacheInfos.every((info) => info.cacheHit);
386
- const cacheType = cacheInfos[0]?.cacheType || "none";
387
- const totalSize = Object.values(characterData).reduce((sum, buffer) => {
388
- return sum + (buffer ? buffer.byteLength : 0);
389
- }, 0);
390
- logMetric("download_avatar_assets_latency", totalDuration, {
391
- resolution: "default",
392
- use_compressed_model: useCompressedModel,
393
- file_count: filesToLoad.length,
394
- cache_hit: cacheHit,
395
- cache_type: cacheType,
396
- result: "success"
397
- }, {
398
- avatar_id: characterMeta.characterId ?? "unknown",
399
- parallel_duration: parallelDuration,
400
- total_size: totalSize
401
- });
402
- return {
403
- data: characterData,
404
- cacheHit,
405
- cacheType
406
- };
407
- }
408
- /**
409
- * Preload all resources (template + character data + camera info + settings)
410
- * @internal
411
- */
412
- async preloadResources(characterMeta, options) {
413
- const { progressCallback = null, signal, useCompressedModel = false } = options || {};
414
- if (signal?.aborted) throw new Error("Preload cancelled");
415
- const [characterResult, preloadCameraSettings] = await Promise.all([this.loadCharacterData(characterMeta, {
416
- signal,
417
- useCompressedModel,
418
- progressCallback: (info) => {
419
- if (progressCallback) progressCallback({
420
- ...info,
421
- stage: `character-${info.stage}`
422
- });
423
- }
424
- }), this.loadCameraSettings(characterMeta, { signal })]);
425
- return {
426
- characterData: characterResult.data,
427
- preloadCameraSettings,
428
- characterSettings: characterMeta.characterSettings,
429
- cacheHit: characterResult.cacheHit,
430
- cacheType: characterResult.cacheType
431
- };
432
- }
433
- /**
434
- * Get AvatarKit SDK API Client (region-templated endpoint composed by AvatarSDK.getEnvironmentConfig).
435
- * Used for: character details and resource URLs (public endpoints, no auth required)
436
- * Note: This endpoint does not require authentication, so we don't add X-App-Id or Authorization headers
437
- * to avoid CORS preflight requests for simple GET requests
438
- */
439
- getSdkApiClient() {
440
- return { async request(url, options = {}) {
441
- const baseUrl = AvatarSDK.getEnvironmentConfig().sdkApiBaseUrl;
442
- const fullUrl = baseUrl + url;
443
- const headers = {};
444
- const method = options.method || "GET";
445
- if (method !== "GET" && options.body) headers["Content-Type"] = "application/json";
446
- const operation = url.split("?")[0].replace(/\/v2\/avatar\/[^/?]+/, "/v2/avatar/{id}");
447
- let serverAddress;
448
- try {
449
- serverAddress = new URL(baseUrl).host;
450
- } catch {}
451
- const startMs = performance.now();
452
- try {
453
- const response = await fetch(fullUrl, {
454
- method,
455
- headers: {
456
- ...headers,
457
- ...options.headers
458
- },
459
- body: options.body ? JSON.stringify(options.body) : void 0,
460
- signal: options.signal
461
- });
462
- recordHttpClientDuration({
463
- operation,
464
- method,
465
- durationMs: Math.round(performance.now() - startMs),
466
- statusCode: response.status,
467
- serverAddress
468
- });
469
- if (!response.ok) {
470
- let serverMessage = "";
471
- try {
472
- const body = await response.json();
473
- if (body?.errors && Array.isArray(body.errors) && body.errors.length > 0) {
474
- const e = body.errors[0];
475
- serverMessage = e.detail || e.title || e.message || JSON.stringify(e);
476
- } else serverMessage = body?.message || body?.error || JSON.stringify(body);
477
- } catch {
478
- serverMessage = response.statusText;
479
- }
480
- let error;
481
- if (response.status === 404) {
482
- const urlMatch = url.match(/\/v2\/(?:character|avatar)\/([^/?]+)/);
483
- const extractedCharacterId = urlMatch ? urlMatch[1] : "unknown";
484
- const callerTraceId = (options.headers || {})["x-sp-trace-id"];
485
- logEvent("avatar_id_unrecognized", "error", {
486
- avatar_id: extractedCharacterId,
487
- description: `HTTP 404: ${serverMessage}`,
488
- ...callerTraceId ? { trace_id: callerTraceId } : {}
489
- });
490
- error = new AvatarError(`HTTP 404: ${serverMessage}`, ErrorCode.avatarIDUnrecognized);
491
- } else error = new AvatarError(`HTTP ${response.status}: ${serverMessage}`, ErrorCode.failedToFetchAvatarMetadata);
492
- throw error;
493
- }
494
- try {
495
- return await response.json();
496
- } catch {
497
- throw new AvatarError("Avatar data is invalid. Please contact Spatius support.", ErrorCode.invalidAvatarMetadata);
498
- }
499
- } catch (err) {
500
- if (err instanceof AvatarError) throw err;
501
- recordHttpClientDuration({
502
- operation,
503
- method,
504
- durationMs: Math.round(performance.now() - startMs),
505
- serverAddress
506
- });
507
- throw new AvatarError("Failed to load avatar due to network issues, please check your connection and try again.", ErrorCode.failedToFetchAvatarMetadata);
508
- }
509
- } };
510
- }
511
- /**
512
- * Map the new `/v2/avatar/{id}` `AvatarAsset` payload onto the internal
513
- * `CharacterMeta` shape used by the existing download / render pipeline.
514
- *
515
- * The backend (grpc-gateway) serialises proto fields as camelCase JSON, so the
516
- * runtime object is loosely shaped like the generated `AvatarAsset`. We:
517
- * - lift `models.gs` into `models.gsStandard` (downloader/asset-count read gsStandard)
518
- * - rename `animations.frameFallback` → `animations.frameMono`
519
- * - fold the inline `camera` / `transform` into `characterSettings` so the
520
- * renderer's `resolveCameraConfig` reads structured values and no camera
521
- * resource is downloaded (top-level `camera` is intentionally left unset)
522
- * @internal
523
- */
524
- mapAvatarAssetToCharacterMeta(asset, avatarId) {
525
- return {
526
- characterId: avatarId,
527
- version: asset.version ?? "",
528
- compatibilityFlags: asset.compatibilityFlags ?? [],
529
- updatedAt: asset.updatedAt,
530
- models: {
531
- shape: asset.models?.shape,
532
- gsStandard: asset.models?.gs
533
- },
534
- animations: {
535
- frameIdle: asset.animations?.frameIdle,
536
- frameMono: asset.animations?.frameFallback
537
- },
538
- customAnimations: asset.animations?.customAnimations ?? [],
539
- characterSettings: {
540
- ...asset.camera ? { camera: { ...asset.camera } } : {},
541
- ...asset.transform ? { transform: { ...asset.transform } } : {}
542
- }
543
- };
544
- }
545
- /**
546
- * Get single avatar by ID from AvatarKit SDK API (v2 driven-ingress avatar API).
547
- * Domain: composed from region as api.${region}.spatius.ai
548
- * Auth: Public endpoint, no authentication required
549
- * Fetches the new `AvatarAsset` payload from `/v2/avatar/{id}` and maps it onto
550
- * the internal `CharacterMeta` shape consumed by the download / render pipeline:
551
- * - `models.gs` → `models.gsStandard`
552
- * - `animations.frameFallback` → `animations.frameMono`
553
- * - inline `camera` / `transform` → `characterSettings.{camera,transform}`
554
- * (so the renderer reads structured values directly and no camera resource is downloaded)
555
- * @internal
556
- */
557
- async getCharacterById(characterId, options) {
558
- const { signal } = options || {};
559
- const startTime = Date.now();
560
- const traceId = generateTraceId();
561
- try {
562
- if (signal?.aborted) throw new Error("Request cancelled");
563
- const response = await this.getSdkApiClient().request(`/v2/avatar/${characterId}`, {
564
- method: "GET",
565
- headers: { "x-sp-trace-id": traceId },
566
- signal
567
- });
568
- if (response?.errors && Array.isArray(response.errors) && response.errors.length > 0) {
569
- const firstError = response.errors[0];
570
- throw new AvatarError(`${firstError.code || firstError.status || "SERVER_ERROR"}: ${firstError.detail || firstError.title || firstError.message || "Unknown server error"}`, ErrorCode.failedToFetchAvatarMetadata);
571
- }
572
- logMetric("fetch_avatar_metadata_latency", Date.now() - startTime, { result: "success" }, {
573
- avatar_id: characterId,
574
- trace_id: traceId
575
- });
576
- return this.mapAvatarAssetToCharacterMeta(response, characterId);
577
- } catch (error) {
578
- if (error instanceof Error && (error.name === "AbortError" || error.message === "Request cancelled")) {
579
- logEvent("fetch_avatar_metadata_cancelled", "info", {
580
- avatar_id: characterId ?? "unknown",
581
- trace_id: traceId
582
- });
583
- throw error;
584
- }
585
- logger.error("Failed to fetch character:", error);
586
- if (error instanceof AvatarError) {
587
- logMetric("fetch_avatar_metadata_latency", Date.now() - startTime, { result: error.code }, {
588
- avatar_id: characterId,
589
- trace_id: traceId,
590
- description: error.message
591
- });
592
- logEvent("fetch_avatar_metadata_failed", "error", {
593
- avatar_id: characterId ?? "unknown",
594
- description: error.message,
595
- trace_id: traceId
596
- });
597
- throw error;
598
- }
599
- const errorMessage = error && typeof error === "object" && "message" in error ? String(error.message) : "Failed to fetch character";
600
- const dataMessage = error && typeof error === "object" && "data" in error && typeof error.data === "object" && error.data?.message ? String(error.data.message) : null;
601
- logMetric("fetch_avatar_metadata_latency", Date.now() - startTime, { result: "network_error" }, {
602
- avatar_id: characterId,
603
- trace_id: traceId,
604
- description: dataMessage || errorMessage
605
- });
606
- logEvent("fetch_avatar_metadata_failed", "error", {
607
- avatar_id: characterId ?? "unknown",
608
- description: dataMessage || errorMessage,
609
- trace_id: traceId
610
- });
611
- throw new AvatarError("Failed to load avatar due to network issues, please check your connection and try again.", ErrorCode.failedToFetchAvatarMetadata);
612
- }
613
- }
614
- };
615
- //#endregion
616
- export { AvatarDownloader_exports as n, AvatarDownloader as t };