@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
@@ -95,6 +95,16 @@ export declare class AvatarController {
95
95
  * frames received). Reset per conversation alongside latencyMarks.
96
96
  */
97
97
  private traceActions;
98
+ /** 上行编码分段的接收钩子,构造时装上、dispose 时卸下(见 setOpusEncodeSegmentSink)。 */
99
+ private readonly onEncodeSegment;
100
+ /**
101
+ * 解码 span 的聚合状态:累计满 1 秒 PCM 收一段。
102
+ *
103
+ * 跨 chunk 累计——宿主流式喂时一块可能远不足 1 秒,按块收段会碎成几百个 span。
104
+ * `decodeSpanStart` 是上一段的收尾时刻(null = 用本次 decode 的发起时刻当起点)。
105
+ */
106
+ private decodeSpanStart;
107
+ private decodeSpanBytes;
98
108
  /**
99
109
  * Highest Ogg granule forwarded so far this round, for Opus input. The granule
100
110
  * accumulates across a stream, so a send's audio seconds is its own end granule
@@ -1,4 +1,4 @@
1
- import { mt as isDebugMode } from "./logger-fAU8oahT.js";
1
+ import { _t as isDebugMode } from "./logger-BOpQ7u0w.js";
2
2
  //#region config/app-config.ts
3
3
  var GLOBAL_FLAME_CDN_BASE = "https://cdn.spatialwalk.cloud/public";
4
4
  var CN_FLAME_CDN_BASE = "https://cdn.spatialwalk.top/public";
package/dist/index.js CHANGED
@@ -1,8 +1,10 @@
1
- import { a as avatarViewRegistry, c as Message, d as BinaryReader, f as BinaryWriter, i as buildTraceparent, l as MessageType, o as AudioFormat, r as startPlaybackTrace, s as EgressType, t as AvatarSDK, u as TransportCompression } from "./AvatarSDK-CnZqa_X4.js";
2
- import { At as RENDER_QUALITY_PARAMS, Ct as DEFAULT_REGION, Dt as FrameStarvationMode, Et as ErrorCode, Mt as TransitionType, Ot as LoadProgress, St as DEFAULT_OPUS_BITRATE, Tt as DrivingServiceMode, _t as AnimationType, b as recordMetric, bt as ConnectionState, c as logEvent, h as clockSync, ht as idManager, jt as RenderQuality, kt as LogLevel, l as logMetric, m as resolveMarks, p as monoTimestamp, t as logger, vt as AvatarError, wt as DEFAULT_REGION_REQUEST, xt as ConversationState, yt as AvatarState } from "./logger-fAU8oahT.js";
3
- import { n as APP_CONFIG } from "./error-utils-CqKOrHrY.js";
4
- import { r as OggMuxer, t as OggPageSplitter } from "./OpusCodec-D9DcUV65.js";
5
- import { t as AvatarDownloader } from "./AvatarDownloader-Dfi7wtgQ.js";
1
+ import { a as EgressType, c as TransportCompression, i as AudioFormat, l as BinaryReader, o as Message, r as avatarViewRegistry, s as MessageType, t as AvatarSDK, u as BinaryWriter } from "./AvatarSDK-DgPvHU4L.js";
2
+ import { At as FrameStarvationMode, C as recordMetric, Ct as ConnectionState, Dt as DEFAULT_REGION_REQUEST, Et as DEFAULT_REGION, Ft as TransitionType, Mt as LogLevel, Nt as RENDER_QUALITY_PARAMS, Ot as DrivingServiceMode, Pt as RenderQuality, St as AvatarState, Tt as DEFAULT_OPUS_BITRATE, bt as AnimationType, c as logEvent, g as clockSync, h as resolveMarks, jt as LoadProgress, kt as ErrorCode, l as logMetric, m as monoTimestamp, p as monoMarkFrom, t as logger, vt as idManager, wt as ConversationState, xt as AvatarError } from "./logger-BOpQ7u0w.js";
3
+ import { a as startPlaybackTrace, i as startLoadTrace, o as buildTraceparent } from "./otel-trace-Ct4T48nC.js";
4
+ import { n as APP_CONFIG } from "./error-utils-BogEllAd.js";
5
+ import { r as OggMuxer, t as OggPageSplitter } from "./OpusCodec-CXU7ysrU.js";
6
+ import { n as setOpusEncodeFrameSink } from "./OpusEncoderProxy-CzY-VckE.js";
7
+ import { t as AvatarDownloader } from "./AvatarDownloader-DyhenPKd.js";
6
8
  //#region core/Avatar.ts
7
9
  var Avatar = class {
8
10
  id;
@@ -309,8 +311,8 @@ var AnimationPlayer = class AnimationPlayer {
309
311
  */
310
312
  async createAndInitializeStreamingPlayer() {
311
313
  if (this.streamingPlayer) return;
312
- const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-D4q7c1U0.js");
313
- const { AvatarSDK } = await import("./AvatarSDK-CnZqa_X4.js").then((n) => n.n);
314
+ const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-CIJmBp3z.js");
315
+ const { AvatarSDK } = await import("./AvatarSDK-DgPvHU4L.js").then((n) => n.n);
314
316
  const audioFormat = AvatarSDK.getAudioFormat();
315
317
  this.streamingPlayer = new StreamingAudioPlayer({
316
318
  sampleRate: audioFormat.sampleRate,
@@ -930,9 +932,11 @@ var PendingAudioBuffer = class {
930
932
  };
931
933
  //#endregion
932
934
  //#region animation/AnimationWebSocketClient.ts
933
- /**
934
- * @internal
935
- */
935
+ var onOpusEncodeSegment = null;
936
+ /** @internal 由 AvatarController 在每轮播放开始时装上、结束时卸下。 */
937
+ function setOpusEncodeSegmentSink(fn) {
938
+ onOpusEncodeSegment = fn;
939
+ }
936
940
  var AnimationWebSocketClient = class extends EventEmitter {
937
941
  wsUrl;
938
942
  reconnectAttempts;
@@ -1194,26 +1198,84 @@ var AnimationWebSocketClient = class extends EventEmitter {
1194
1198
  * message so the server sees the stream close. Caller ensures socket/session
1195
1199
  * are ready.
1196
1200
  */
1201
+ /** 1 秒 PCM 的字节数(16-bit 单声道),用于把编码分段按音频秒数切开。 */
1202
+ get audioBytesPerSecond() {
1203
+ return (AvatarSDK.getAudioFormat().sampleRate || 16e3) * 2;
1204
+ }
1205
+ /**
1206
+ * 编码 span 的跨**调用**累计状态(不足 1 秒的余量留到下一次 encode 调用)。
1207
+ *
1208
+ * `encodeSpanReqId` 用来判轮次切换:这几个字段是实例级的,不跟着 controller 每轮
1209
+ * 重置的 traceActions 走,上一轮的余量留到下一轮会让段数虚增、时刻串到几秒前
1210
+ * (实测 12 秒音频报出 24 段,且落在 send_audio 之前 4 秒)。
1211
+ */
1212
+ encodeSpanReqId = null;
1213
+ encodeSpanStart = null;
1214
+ encodeSpanBytes = 0;
1215
+ /**
1216
+ * 把逐帧的编码时刻按**音频秒数**切成段,报给 trace。
1217
+ *
1218
+ * 与解码侧同一套口径:累计满 1 秒 PCM 收一段,边界用那一帧编完的真实时刻;
1219
+ * 尾巴不足 1 秒的余量跨调用累计(流式喂时一块往往远不足 1 秒)。
1220
+ */
1221
+ emitEncodeSegments(reqId, fallbackStart, frames, frameTs) {
1222
+ if (frames.length === 0) return;
1223
+ if (this.encodeSpanReqId !== reqId) {
1224
+ this.encodeSpanReqId = reqId;
1225
+ this.encodeSpanStart = null;
1226
+ this.encodeSpanBytes = 0;
1227
+ }
1228
+ const oneSecondBytes = this.audioBytesPerSecond;
1229
+ for (let i = 0; i < frames.length; i++) {
1230
+ this.encodeSpanStart ??= fallbackStart;
1231
+ this.encodeSpanBytes += frames[i];
1232
+ if (this.encodeSpanBytes < oneSecondBytes) continue;
1233
+ const endTs = monoMarkFrom(frameTs[i]);
1234
+ onOpusEncodeSegment?.({
1235
+ startTs: this.encodeSpanStart,
1236
+ endTs,
1237
+ pcmBytes: this.encodeSpanBytes
1238
+ });
1239
+ this.encodeSpanStart = endTs;
1240
+ this.encodeSpanBytes = 0;
1241
+ }
1242
+ }
1197
1243
  async encodeAndSendOpus(conversationId, audioData, end) {
1198
1244
  try {
1199
- const pages = await this.opusEncoder.encode(conversationId, audioData, end);
1200
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
1201
- for (let i = 0; i < pages.length; i++) {
1202
- const isLast = end && i === pages.length - 1;
1245
+ const encodeStartTs = monoTimestamp();
1246
+ let pendingFrames = [];
1247
+ let pendingTs = [];
1248
+ setOpusEncodeFrameSink((frames, ts) => {
1249
+ pendingFrames = frames;
1250
+ pendingTs = ts;
1251
+ });
1252
+ const sendPage = (page, isLast) => {
1253
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
1203
1254
  const message = {
1204
1255
  type: MessageType.MESSAGE_CLIENT_AUDIO_INPUT,
1205
1256
  clientAudioInput: {
1206
1257
  reqId: conversationId,
1207
- audio: pages[i],
1258
+ audio: page,
1208
1259
  end: isLast,
1209
1260
  traceContext: this.takeTraceContext(conversationId)
1210
1261
  }
1211
1262
  };
1212
1263
  const writer = new BinaryWriter();
1213
1264
  Message.encode(message, writer);
1214
- const bytes = writer.finish();
1215
- this.ws.send(bytes);
1216
- }
1265
+ this.ws.send(writer.finish());
1266
+ };
1267
+ let held = null;
1268
+ const pushPage = (page) => {
1269
+ if (held) sendPage(held, false);
1270
+ held = page;
1271
+ };
1272
+ await this.opusEncoder.encode(conversationId, audioData, end, (batch) => {
1273
+ for (const page of batch) pushPage(page);
1274
+ });
1275
+ setOpusEncodeFrameSink(null);
1276
+ this.emitEncodeSegments(conversationId, encodeStartTs, pendingFrames, pendingTs);
1277
+ if (held === null && end) held = /* @__PURE__ */ new Uint8Array(0);
1278
+ if (held) sendPage(held, end);
1217
1279
  } catch (error) {
1218
1280
  const message = error instanceof Error ? error.message : String(error);
1219
1281
  logger.error("[AnimationWebSocketClient] Failed to send Opus audio:", message);
@@ -1229,8 +1291,8 @@ var AnimationWebSocketClient = class extends EventEmitter {
1229
1291
  * Internal decision for the SDK's upstream audio format when the host feeds PCM:
1230
1292
  * whether to encode PCM→Opus (true) or send raw PCM (false).
1231
1293
  *
1232
- * Opus uplink is opt-in: without `opusUplinkEnabled: true` the uplink stays raw
1233
- * PCM, so the encode cost is only paid where it is wanted.
1294
+ * Opus uplink is opt-OUT: it is on unless the host passes
1295
+ * `opusUplinkEnabled: false`, which keeps the raw PCM uplink.
1234
1296
  *
1235
1297
  * Once opted in, the decision is still the SDK's: the backend (v2) supports Opus
1236
1298
  * unconditionally and the encoder has a two-tier fallback (worker → main-thread
@@ -1240,7 +1302,7 @@ var AnimationWebSocketClient = class extends EventEmitter {
1240
1302
  */
1241
1303
  shouldUseOpusUplink() {
1242
1304
  const { opusUplinkEnabled } = AvatarSDK.getAudioFormat();
1243
- if (opusUplinkEnabled !== true) return false;
1305
+ if (opusUplinkEnabled === false) return false;
1244
1306
  return true;
1245
1307
  }
1246
1308
  /**
@@ -1281,7 +1343,7 @@ var AnimationWebSocketClient = class extends EventEmitter {
1281
1343
  return;
1282
1344
  }
1283
1345
  try {
1284
- const { OpusEncoderProxy } = await import("./OpusEncoderProxy-9YIZ7nIt.js");
1346
+ const { OpusEncoderProxy } = await import("./OpusEncoderProxy-CzY-VckE.js").then((n) => n.t);
1285
1347
  const bitrate = opusBitrate ?? 48e3;
1286
1348
  this.opusEncoder = await OpusEncoderProxy.create(sampleRate, bitrate);
1287
1349
  this.uplinkIsOpus = true;
@@ -2056,7 +2118,7 @@ function resolveInputAudioFormat() {
2056
2118
  */
2057
2119
  function resolveUplinkAudioFormat() {
2058
2120
  const fmt = AvatarSDK.getAudioFormat();
2059
- return fmt.inputAudioFormat === "opus" || fmt.opusUplinkEnabled === true ? "opus" : "pcm";
2121
+ return fmt.inputAudioFormat === "opus" || fmt.opusUplinkEnabled !== false ? "opus" : "pcm";
2060
2122
  }
2061
2123
  var AvatarController = class {
2062
2124
  networkLayer;
@@ -2185,9 +2247,24 @@ var AvatarController = class {
2185
2247
  */
2186
2248
  traceActions = {
2187
2249
  sends: [],
2188
- recvs: []
2250
+ recvs: [],
2251
+ decodes: [],
2252
+ encodes: []
2253
+ };
2254
+ /** 上行编码分段的接收钩子,构造时装上、dispose 时卸下(见 setOpusEncodeSegmentSink)。 */
2255
+ onEncodeSegment = (seg) => {
2256
+ this.traceActions.encodes.push(seg);
2257
+ this.markAudioTapAt(seg.pcmBytes, seg.endTs);
2189
2258
  };
2190
2259
  /**
2260
+ * 解码 span 的聚合状态:累计满 1 秒 PCM 收一段。
2261
+ *
2262
+ * 跨 chunk 累计——宿主流式喂时一块可能远不足 1 秒,按块收段会碎成几百个 span。
2263
+ * `decodeSpanStart` 是上一段的收尾时刻(null = 用本次 decode 的发起时刻当起点)。
2264
+ */
2265
+ decodeSpanStart = null;
2266
+ decodeSpanBytes = 0;
2267
+ /**
2191
2268
  * Highest Ogg granule forwarded so far this round, for Opus input. The granule
2192
2269
  * accumulates across a stream, so a send's audio seconds is its own end granule
2193
2270
  * minus this. Reset per conversation. -1 = nothing forwarded yet.
@@ -2208,6 +2285,7 @@ var AvatarController = class {
2208
2285
  if (this.playbackMode === DrivingServiceMode.direct) this.networkLayer = new NetworkLayer(this);
2209
2286
  if (typeof document !== "undefined") document.addEventListener("visibilitychange", this.handleVisibilityChange);
2210
2287
  this.ensureInputOpusDecoder();
2288
+ setOpusEncodeSegmentSink(this.onEncodeSegment);
2211
2289
  }
2212
2290
  handleVisibilityChange = () => {
2213
2291
  if (document.visibilityState === "hidden") this.shouldReportPlaybackStats = false;
@@ -2442,9 +2520,11 @@ var AvatarController = class {
2442
2520
  recordMetric("playback_animation_frame_count", this.receivedAnimationFrames, labels);
2443
2521
  recordMetric("playback_avg_fps", Math.round(stats.avgFps), labels);
2444
2522
  recordMetric("playback_jank_frame_count", stats.jankFrameCount, labels);
2445
- recordMetric("playback_gpu_render_p50_ms", Math.round(stats.gpuRenderP50Ms), labels);
2446
- recordMetric("playback_gpu_render_p90_ms", Math.round(stats.gpuRenderP90Ms), labels);
2447
- recordMetric("playback_gpu_render_p95_ms", Math.round(stats.gpuRenderP95Ms), labels);
2523
+ if (stats.gpuRenderP50Ms > 0) {
2524
+ recordMetric("playback_gpu_render_p50_ms", Math.round(stats.gpuRenderP50Ms), labels);
2525
+ recordMetric("playback_gpu_render_p90_ms", Math.round(stats.gpuRenderP90Ms), labels);
2526
+ recordMetric("playback_gpu_render_p95_ms", Math.round(stats.gpuRenderP95Ms), labels);
2527
+ }
2448
2528
  recordMetric("playback_stall_count", stall.count, labels);
2449
2529
  recordMetric("playback_stall_max_ms", stall.max_ms, labels);
2450
2530
  recordMetric("playback_stall_total_ms", stall.total_ms, labels);
@@ -2455,9 +2535,14 @@ var AvatarController = class {
2455
2535
  const resolved = resolveMarks(latency);
2456
2536
  const tap0 = resolved["tap_0"];
2457
2537
  if (typeof tap0 === "number") {
2538
+ const latencyLabels = {
2539
+ ...labels,
2540
+ input_audio_format: resolveInputAudioFormat(),
2541
+ uplink_audio_format: resolveUplinkAudioFormat()
2542
+ };
2458
2543
  const emitDelta = (name, key) => {
2459
2544
  const t = resolved[key];
2460
- if (typeof t === "number") recordMetric(name, t - tap0, labels);
2545
+ if (typeof t === "number") recordMetric(name, t - tap0, latencyLabels);
2461
2546
  };
2462
2547
  emitDelta("playback_latency_tap_1_ms", "tap_1");
2463
2548
  emitDelta("playback_latency_tap_2_ms", "tap_2");
@@ -2495,6 +2580,14 @@ var AvatarController = class {
2495
2580
  actionMarks[`recv_start_${i}`] = r.startTs;
2496
2581
  actionMarks[`recv_end_${i}`] = r.endTs;
2497
2582
  });
2583
+ this.traceActions.decodes.forEach((d, i) => {
2584
+ actionMarks[`decode_start_${i}`] = d.startTs;
2585
+ actionMarks[`decode_end_${i}`] = d.endTs;
2586
+ });
2587
+ this.traceActions.encodes.forEach((e, i) => {
2588
+ actionMarks[`encode_start_${i}`] = e.startTs;
2589
+ actionMarks[`encode_end_${i}`] = e.endTs;
2590
+ });
2498
2591
  const rm = resolveMarks(actionMarks);
2499
2592
  const allEnds = [...this.traceActions.sends.map((_, i) => rm[`send_end_${i}`]), ...this.traceActions.recvs.map((_, i) => rm[`recv_end_${i}`])].filter((v) => typeof v === "number");
2500
2593
  const allStarts = [...this.traceActions.sends.map((_, i) => rm[`send_start_${i}`]), ...this.traceActions.recvs.map((_, i) => rm[`recv_start_${i}`])].filter((v) => typeof v === "number");
@@ -2522,6 +2615,44 @@ var AvatarController = class {
2522
2615
  });
2523
2616
  });
2524
2617
  arrival.end(arrivalEnd);
2618
+ const decodeMarks = this.traceActions.decodes;
2619
+ if (decodeMarks.length > 0) {
2620
+ const firstStart = rm[`decode_start_0`];
2621
+ if (typeof firstStart === "number") {
2622
+ const decodeGroup = t.group("decode", firstStart, { view: "decode" });
2623
+ let decodeEnd = firstStart;
2624
+ decodeMarks.forEach((d, i) => {
2625
+ const start = rm[`decode_start_${i}`];
2626
+ const end = rm[`decode_end_${i}`];
2627
+ if (typeof start !== "number" || typeof end !== "number") return;
2628
+ decodeEnd = Math.max(decodeEnd, end);
2629
+ decodeGroup.span(`decode_audio_${i + 1}`, start, floorEnd(start, end), {
2630
+ dur_ms: Math.round(end - start),
2631
+ audio_ms: Math.round(d.pcmBytes / this.audioBytesPerSecond * 1e3)
2632
+ });
2633
+ });
2634
+ decodeGroup.end(decodeEnd);
2635
+ }
2636
+ }
2637
+ const encodeMarks = this.traceActions.encodes;
2638
+ if (encodeMarks.length > 0) {
2639
+ const firstStart = rm[`encode_start_0`];
2640
+ if (typeof firstStart === "number") {
2641
+ const encodeGroup = t.group("encode", firstStart, { view: "encode" });
2642
+ let encodeEnd = firstStart;
2643
+ encodeMarks.forEach((e, i) => {
2644
+ const start = rm[`encode_start_${i}`];
2645
+ const end = rm[`encode_end_${i}`];
2646
+ if (typeof start !== "number" || typeof end !== "number") return;
2647
+ encodeEnd = Math.max(encodeEnd, end);
2648
+ encodeGroup.span(`encode_audio_${i + 1}`, start, floorEnd(start, end), {
2649
+ dur_ms: Math.round(end - start),
2650
+ audio_ms: Math.round(e.pcmBytes / this.audioBytesPerSecond * 1e3)
2651
+ });
2652
+ });
2653
+ encodeGroup.end(encodeEnd);
2654
+ }
2655
+ }
2525
2656
  const renderEnd = this.playbackEndedAt > tap0 ? this.playbackEndedAt : tap0 + durationMs;
2526
2657
  const emitPlaybackChain = (into, playLabel, stallLabel, chainStart, chainEnd, events) => {
2527
2658
  const stalls = events.filter((e) => e.startTs >= chainStart && e.startTs <= chainEnd).sort((a, b) => a.startTs - b.startTs);
@@ -2628,7 +2759,7 @@ var AvatarController = class {
2628
2759
  const { inputAudioFormat } = AvatarSDK.getAudioFormat();
2629
2760
  if (inputAudioFormat !== "opus" || this.inputOpusDecoderProxy) return;
2630
2761
  try {
2631
- const { OpusDecoderProxy } = await import("./OpusDecoderProxy-Ar_dFXRB.js");
2762
+ const { OpusDecoderProxy } = await import("./OpusDecoderProxy-Cq8Z_MZY.js");
2632
2763
  this.inputOpusDecoderProxy = await OpusDecoderProxy.create();
2633
2764
  this.inputDecodeChain = Promise.resolve();
2634
2765
  } catch (error) {
@@ -2647,15 +2778,28 @@ var AvatarController = class {
2647
2778
  if (!this.inputOpusDecoderProxy) {
2648
2779
  if (resolveInputAudioFormat() === "opus") {
2649
2780
  logger.error("[AvatarController] Opus input but decoder unavailable; dropping chunk to avoid playing raw Opus as PCM");
2650
- return /* @__PURE__ */ new Uint8Array(0);
2781
+ return {
2782
+ pcm: /* @__PURE__ */ new Uint8Array(0),
2783
+ segments: [],
2784
+ segmentTs: []
2785
+ };
2651
2786
  }
2652
- return new Uint8Array(data);
2787
+ const raw = new Uint8Array(data);
2788
+ return {
2789
+ pcm: raw,
2790
+ segments: raw.length > 0 ? [raw.length] : [],
2791
+ segmentTs: raw.length > 0 ? [performance.now()] : []
2792
+ };
2653
2793
  }
2654
2794
  try {
2655
2795
  return await this.inputOpusDecoderProxy.decode(data);
2656
2796
  } catch (error) {
2657
2797
  logger.error("[AvatarController] Failed to decode input Opus audio:", error instanceof Error ? error.message : String(error));
2658
- return /* @__PURE__ */ new Uint8Array(0);
2798
+ return {
2799
+ pcm: /* @__PURE__ */ new Uint8Array(0),
2800
+ segments: [],
2801
+ segmentTs: []
2802
+ };
2659
2803
  }
2660
2804
  }
2661
2805
  /**
@@ -2703,6 +2847,7 @@ var AvatarController = class {
2703
2847
  this.currentConversationId = this.generateAndLogNewConversationId();
2704
2848
  this.reqEnd = false;
2705
2849
  }
2850
+ this.markFirstAudioTap();
2706
2851
  const sendStartTs = monoTimestamp();
2707
2852
  const isOpusInput = resolveInputAudioFormat() === "opus";
2708
2853
  let sendAudioMs = isOpusInput ? 0 : audioData.byteLength / this.audioBytesPerSecond * 1e3;
@@ -2730,17 +2875,41 @@ var AvatarController = class {
2730
2875
  const oggEnd = end;
2731
2876
  this.inputDecodeChain = this.inputDecodeChain.then(async () => {
2732
2877
  await this.ensureInputOpusDecoder();
2733
- const pcm = await this.normalizeInputAudioToPcm(oggForDecode);
2878
+ const decodeStartTs = monoTimestamp();
2879
+ const { pcm, segments, segmentTs } = await this.normalizeInputAudioToPcm(oggForDecode);
2734
2880
  this.receivedAudioBytes += pcm.length;
2735
- this.markAudioTap(pcm.length);
2881
+ for (let i = 0; i < segments.length; i++) {
2882
+ this.decodeSpanBytes += segments[i];
2883
+ if (this.decodeSpanBytes < this.audioBytesPerSecond) continue;
2884
+ const endTs = monoMarkFrom(segmentTs[i]);
2885
+ this.traceActions.decodes.push({
2886
+ startTs: this.decodeSpanStart ?? decodeStartTs,
2887
+ endTs,
2888
+ pcmBytes: this.decodeSpanBytes
2889
+ });
2890
+ this.decodeSpanStart = endTs;
2891
+ this.decodeSpanBytes = 0;
2892
+ }
2893
+ if (this.decodeSpanBytes > 0) {
2894
+ const endTs = monoMarkFrom(segmentTs[segments.length - 1]);
2895
+ this.traceActions.decodes.push({
2896
+ startTs: this.decodeSpanStart ?? decodeStartTs,
2897
+ endTs,
2898
+ pcmBytes: this.decodeSpanBytes
2899
+ });
2900
+ this.decodeSpanStart = endTs;
2901
+ this.decodeSpanBytes = 0;
2902
+ }
2736
2903
  this.addAudioChunkToBuffer(pcm, oggEnd);
2737
2904
  }).catch((error) => {
2738
2905
  logger.error("[AvatarController] Opus decode→buffer chain error:", error instanceof Error ? error.message : String(error));
2739
2906
  });
2740
2907
  this.networkLayer.sendAudioData(this.currentConversationId, audioData, end);
2908
+ this.markAudioTap(sendAudioMs / 1e3 * this.audioBytesPerSecond);
2741
2909
  } else {
2742
2910
  this.receivedAudioBytes += audioData.byteLength;
2743
- this.markAudioTap(audioData.byteLength);
2911
+ if (resolveUplinkAudioFormat() === "opus") this.markFirstAudioTap();
2912
+ else this.markAudioTap(audioData.byteLength);
2744
2913
  this.addAudioChunkToBuffer(new Uint8Array(audioData), end);
2745
2914
  this.networkLayer.sendAudioData(this.currentConversationId, audioData, end);
2746
2915
  }
@@ -2852,7 +3021,7 @@ var AvatarController = class {
2852
3021
  const oggLast = isLast;
2853
3022
  this.inputDecodeChain = this.inputDecodeChain.then(async () => {
2854
3023
  await this.ensureInputOpusDecoder();
2855
- const pcm = await this.normalizeInputAudioToPcm(ogg);
3024
+ const { pcm } = await this.normalizeInputAudioToPcm(ogg);
2856
3025
  this.bufferHostPcm(pcm, oggLast);
2857
3026
  }).catch((error) => {
2858
3027
  logger.error("[AvatarController] Opus decode→buffer chain error:", error instanceof Error ? error.message : String(error));
@@ -2866,24 +3035,50 @@ var AvatarController = class {
2866
3035
  return this.currentConversationId;
2867
3036
  }
2868
3037
  /**
3038
+ * 记下 tap_0 —— 宿主送入首包音频的时刻,只抢零点、不参与分秒累计。
3039
+ *
3040
+ * 语义是「用户把第一块音频交给 SDK 的那一刻」,PCM 与 Opus 一视同仁:它是端到端
3041
+ * 延迟的零点,上行发送、本地解码都发生在它之后,本就该被算进后续各段 Δ 里。
3042
+ *
3043
+ * 分秒(tap_N≥1)在各路径的「发出去」那一刻推进,见 markAudioTap / markAudioTapAt
3044
+ * 的调用点。字节口径恒为 PCM(opus 输入由 granule 秒数折算),因为分母
3045
+ * audioBytesPerSecond 按真实采样率算,直接用 Ogg 压缩字节会让 tap_N 几乎不递增。
3046
+ * taps.set(0) 是 set-once,故这里先占、后面那次不会覆盖。
3047
+ * @internal
3048
+ */
3049
+ markFirstAudioTap() {
3050
+ const marks = this.latencyMarks;
3051
+ if (!marks.taps.has(0)) marks.taps.set(0, monoTimestamp());
3052
+ }
3053
+ /**
2869
3054
  * 记一次音频 tap:tap_0=首包送入,之后每累计满整秒记一个 tap_N。
2870
3055
  *
2871
3056
  * **两种模式都走这里**(direct 由 send() 调、backend 由 bufferHostPcm 调),
2872
3057
  * 所以只有这一份分秒逻辑。
2873
3058
  *
2874
- * @param pcmByteLength 本块**解码后 PCM** 的字节数。必须是 PCM:分母
2875
- * audioBytesPerSecond 按真实采样率算,若传 Opus 压缩字节(比 PCM 小 5-10 倍)
2876
- * 会让 tap_N 稀疏到几乎不递增。
3059
+ * @param pcmByteLength 本块的 **PCM 口径**字节数。分母 audioBytesPerSecond 按真实
3060
+ * 采样率算,直接传 Opus 压缩字节(比 PCM 小 5-10 倍)会让 tap_N 几乎不递增;
3061
+ * opus 输入不解码,故由 Ogg granule 的秒数折算成等价 PCM 字节再传进来。
2877
3062
  * @internal
2878
3063
  */
2879
3064
  markAudioTap(pcmByteLength) {
3065
+ this.markAudioTapAt(pcmByteLength, monoTimestamp());
3066
+ }
3067
+ /**
3068
+ * markAudioTap 的指定时刻版:这一秒音频「就绪」的时刻由调用方给,而不是取当下。
3069
+ *
3070
+ * 上行 Opus 的编码分段用它——那一秒的就绪时刻是它编完的时刻(回调触发时已过去
3071
+ * 一小会儿),取 monoTimestamp() 会把编码后的排队/回调延迟算进 tap。
3072
+ * @internal
3073
+ */
3074
+ markAudioTapAt(pcmByteLength, at) {
2880
3075
  const marks = this.latencyMarks;
2881
- if (!marks.taps.has(0)) marks.taps.set(0, monoTimestamp());
3076
+ if (!marks.taps.has(0)) marks.taps.set(0, at);
2882
3077
  marks.accumulatedBytes += pcmByteLength;
2883
3078
  const currentDuration = marks.accumulatedBytes / this.audioBytesPerSecond;
2884
3079
  let s = (marks.taps.size > 0 ? Math.max(...marks.taps.keys()) : 0) + 1;
2885
3080
  while (currentDuration >= s) {
2886
- marks.taps.set(s, monoTimestamp());
3081
+ marks.taps.set(s, at);
2887
3082
  s++;
2888
3083
  }
2889
3084
  }
@@ -3147,8 +3342,12 @@ var AvatarController = class {
3147
3342
  };
3148
3343
  this.traceActions = {
3149
3344
  sends: [],
3150
- recvs: []
3345
+ recvs: [],
3346
+ decodes: [],
3347
+ encodes: []
3151
3348
  };
3349
+ this.decodeSpanStart = null;
3350
+ this.decodeSpanBytes = 0;
3152
3351
  this.lastOpusGranule = -1;
3153
3352
  }
3154
3353
  /**
@@ -3408,7 +3607,7 @@ var AvatarController = class {
3408
3607
  const stateChanged = isOutOfBounds !== this.lastOutOfBoundsState;
3409
3608
  const timeSinceLastLog = now - this.lastSyncLogTime;
3410
3609
  if (stateChanged || timeSinceLastLog >= 1e3) {
3411
- logger.warn(`[PlaybackLoop] Frame index out of bounds! frameIndex: ${frameIndex}, maxAvailable: ${this.currentKeyframes.length - 1 + this.keyframesOffset}, using last frame`);
3610
+ logger.warn(`[AvatarKit] Animation is running behind the audio: playback has reached ${(frameIndex / 25).toFixed(1)}s but only ${((this.currentKeyframes.length + this.keyframesOffset) / 25).toFixed(1)}s of animation has arrived, so the last available frame is being held. Send audio to the SDK faster than real time — at least 2s of audio per second of wall clock, ideally more — so animation frames can stay ahead of playback.`);
3412
3611
  this.lastSyncLogTime = now;
3413
3612
  }
3414
3613
  this.lastOutOfBoundsState = isOutOfBounds;
@@ -3612,6 +3811,7 @@ var AvatarController = class {
3612
3811
  this.animationPlayer.dispose();
3613
3812
  this.animationPlayer = null;
3614
3813
  }
3814
+ setOpusEncodeSegmentSink(null);
3615
3815
  if (this.inputOpusDecoderProxy) {
3616
3816
  this.inputOpusDecoderProxy.destroy();
3617
3817
  this.inputOpusDecoderProxy = null;
@@ -3797,6 +3997,7 @@ var AvatarManager = class AvatarManager {
3797
3997
  if (task.status === "cancelled") return;
3798
3998
  task.status = "failed";
3799
3999
  const err = error instanceof Error ? error : new Error(String(error));
4000
+ task._loadTrace?.end(false, Date.now(), { error: err.message });
3800
4001
  this.notifyAllRequests(task, {
3801
4002
  type: LoadProgress.failed,
3802
4003
  error: err
@@ -3817,10 +4018,19 @@ var AvatarManager = class AvatarManager {
3817
4018
  const { id, abortController } = task;
3818
4019
  const signal = abortController.signal;
3819
4020
  const startTime = Date.now();
4021
+ const loadTrace = startLoadTrace(id, startTime, {
4022
+ use_compressed_model: task.useCompressedModel,
4023
+ source: "network"
4024
+ });
4025
+ task._loadTrace = loadTrace;
3820
4026
  if (signal.aborted) throw new Error("Task cancelled");
3821
4027
  task.status = "fetching-meta";
3822
4028
  logger.log(`[AvatarManager] Fetching metadata for avatar ${id}...`);
4029
+ const metaStart = Date.now();
4030
+ loadTrace.span("prepare", startTime, metaStart);
3823
4031
  const characterMeta = await this.avatarDownloader.getCharacterById(id, { signal });
4032
+ const metaEnd = Date.now();
4033
+ loadTrace.span("fetch_metadata", metaStart, metaEnd);
3824
4034
  task.characterMeta = characterMeta;
3825
4035
  if (signal.aborted) throw new Error("Task cancelled");
3826
4036
  const cached = this.avatarCache.get(id);
@@ -3837,11 +4047,19 @@ var AvatarManager = class AvatarManager {
3837
4047
  type: LoadProgress.downloading,
3838
4048
  progress: 1
3839
4049
  });
4050
+ const hitEnd = Date.now();
4051
+ loadTrace.span("prepare_assets", metaEnd, hitEnd, { cache_hit: true });
4052
+ loadTrace.end(true, hitEnd, { cache_hit: true });
4053
+ logMetric("fetch_avatar_latency", Date.now() - startTime, {
4054
+ resolution: "default",
4055
+ use_compressed_model: task.useCompressedModel,
4056
+ cache_hit: true
4057
+ }, { avatar_id: id });
3840
4058
  return cached;
3841
4059
  } else {
3842
4060
  logger.log(`[AvatarManager] Avatar ${id} cache invalid: updatedAt=${cachedUpdatedAt}->${newUpdatedAt}, modelType=${cachedModelType}->${requestedModelType}, reloading...`);
3843
4061
  this.avatarCache.delete(id);
3844
- const { PwaCacheManager } = await import("./pwa-cache-manager-CwXaM3xR.js").then((n) => n.n);
4062
+ const { PwaCacheManager } = await import("./pwa-cache-manager-C7NbU52Q.js").then((n) => n.n);
3845
4063
  await PwaCacheManager.clearCharacterCache(id);
3846
4064
  }
3847
4065
  }
@@ -3853,6 +4071,8 @@ var AvatarManager = class AvatarManager {
3853
4071
  });
3854
4072
  if (signal.aborted) throw new Error("Task cancelled");
3855
4073
  task.status = "downloading";
4074
+ const assetsStart = Date.now();
4075
+ loadTrace.span("prepare_assets", metaEnd, assetsStart);
3856
4076
  logger.log("[AvatarManager] Downloading resources...");
3857
4077
  let downloadedCount = 0;
3858
4078
  const resources = await this.avatarDownloader.preloadResources(characterMeta, {
@@ -3872,11 +4092,18 @@ var AvatarManager = class AvatarManager {
3872
4092
  if (signal.aborted) throw new Error("Task cancelled");
3873
4093
  logger.log("[AvatarManager] Creating Avatar instance...");
3874
4094
  const avatar = new Avatar(id, characterMeta, resources, requestedModelType, false);
4095
+ const loadEnd = Date.now();
4096
+ loadTrace.span("load_assets", assetsStart, loadEnd, {
4097
+ cache_hit: resources.cacheHit,
4098
+ cache_type: resources.cacheType
4099
+ });
4100
+ loadTrace.end(true, loadEnd, { cache_hit: resources.cacheHit });
3875
4101
  this.avatarCache.set(id, avatar);
3876
4102
  logger.log("[AvatarManager] Avatar loaded successfully");
3877
4103
  logMetric("fetch_avatar_latency", Date.now() - startTime, {
3878
4104
  resolution: "default",
3879
- use_compressed_model: task.useCompressedModel
4105
+ use_compressed_model: task.useCompressedModel,
4106
+ cache_hit: resources.cacheHit
3880
4107
  }, { avatar_id: id });
3881
4108
  return avatar;
3882
4109
  }
@@ -3911,7 +4138,13 @@ var AvatarManager = class AvatarManager {
3911
4138
  * @returns Avatar instance, or undefined if not in cache
3912
4139
  */
3913
4140
  retrieve(id) {
3914
- return this.avatarCache.get(id);
4141
+ const trace = startLoadTrace(id, Date.now(), { source: "cache" });
4142
+ const cached = this.avatarCache.get(id);
4143
+ trace.end(true, Date.now(), {
4144
+ result: cached ? "hit" : "miss",
4145
+ cache_hit: cached !== void 0
4146
+ });
4147
+ return cached;
3915
4148
  }
3916
4149
  /**
3917
4150
  * Clear cached avatar for specified ID
@@ -1,23 +1,4 @@
1
- /**
2
- * Internal telemetry surface for the companion RTC SDK.
3
- *
4
- * `@spatius/avatarkit-rtc` reports into the same OpenObserve backend and shares
5
- * this SDK's OTel providers, so its spans must sit on the same calibrated
6
- * timeline. Without that, a client whose wall clock is off puts RTC spans at a
7
- * time that does not line up with the server-side records they need to be read
8
- * against.
9
- *
10
- * Deliberately NOT re-exported from the package root: this is a bridge between
11
- * two first-party packages, not API for integrators. It exposes conversion only
12
- * — nothing here can alter the clock baseline.
13
- *
14
- * NOTE: nothing in this file may be tagged `@internal`. The dts plugin runs with
15
- * `stripInternal: true`, which erases such declarations from the emitted `.d.ts`
16
- * — the entry would still export both functions at runtime while shipping an
17
- * empty `export {}` type file, leaving the RTC package with no types.
18
- *
19
- * @packageDocumentation
20
- */
1
+ import { Attributes } from '@opentelemetry/api';
21
2
  /**
22
3
  * Convert a `performance.now()` reading taken earlier into a timeline
23
4
  * timestamp (epoch ms), preferring the server-calibrated baseline and falling
@@ -35,3 +16,47 @@ export declare function resolveMonoTimestamp(mono: number): number;
35
16
  * resolve either way; this only reports whether they are server-aligned yet.
36
17
  */
37
18
  export declare function isClockCalibrated(): boolean;
19
+ /**
20
+ * Handle for one round's trace.
21
+ *
22
+ * Declared here rather than re-exported from `utils/otel-trace`: the dts build
23
+ * excludes `utils/**`, so a type imported from there resolves to a file absent
24
+ * from the published package — which fails the whole module's type resolution,
25
+ * not just that one name.
26
+ *
27
+ * Structurally identical to the internal `PlaybackTrace`; the compiler checks
28
+ * the two match at `startPlaybackTrace`'s return below.
29
+ */
30
+ export interface PlaybackTrace {
31
+ /** Add one interval span under the round's root. */
32
+ span(name: string, startMs: number, endMs: number, attrs?: Attributes, isError?: boolean): void;
33
+ /** Open a container span; spans added to the returned handle nest under it. */
34
+ group(name: string, startMs: number, attrs?: Attributes): SpanGroup;
35
+ /** Close the round's root span. */
36
+ end(ok?: boolean, endTimeMs?: number, attrs?: Attributes): void;
37
+ }
38
+ /** Sub-handle returned by {@link PlaybackTrace.group}. */
39
+ export interface SpanGroup {
40
+ span(name: string, startMs: number, endMs: number, attrs?: Attributes, isError?: boolean): void;
41
+ end(endTimeMs?: number, attrs?: Attributes): void;
42
+ }
43
+ /**
44
+ * Open one round's trace: `driven.request` → `playback` → the caller's spans.
45
+ *
46
+ * Shared with the RTC SDK rather than reimplemented there, so both packages
47
+ * emit the same tree. The root's ids derive from the conversation id, which is
48
+ * what lets the backend's spans join a round without threading state through;
49
+ * reproducing that separately would mean two definitions of one wire contract.
50
+ */
51
+ export declare function startPlaybackTrace(conversationId: string, startTimeMs?: number, attrs?: Attributes): PlaybackTrace;
52
+ /**
53
+ * Record one measurement onto avatarkit's metric pipeline.
54
+ *
55
+ * Must go through here rather than the RTC SDK reaching for the global meter
56
+ * itself: a metrics export is skipped entirely unless that round carried real
57
+ * business data, and the flag saying so lives in this module. A histogram
58
+ * written directly through the global provider lands in the same instrument but
59
+ * leaves the flag clear, so the whole batch — RTC's readings included — is
60
+ * dropped before it is sent.
61
+ */
62
+ export declare function recordMetric(name: string, value: number, attributes?: Attributes): void;