realtime-avatar 0.4.0 → 0.5.0

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/dist/express.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { P as ProxyConfig } from './types-9leGOgVv.js';
2
- import './types-C5zypiVY.js';
1
+ import { P as ProxyConfig } from './types-B45GgjrV.js';
2
+ import './types-DLxz6uFE.js';
3
3
 
4
4
  type Expressish = {
5
5
  method: string;
package/dist/express.js CHANGED
@@ -43,7 +43,7 @@ var RealtimeAvatarHttpError = class extends RealtimeAvatarError {
43
43
 
44
44
  // ../http-client/src/client.ts
45
45
  var DEFAULT_BASE_URL = "https://realtimeavatar.ai/api/v1";
46
- var SDK_VERSION = "0.4.0";
46
+ var SDK_VERSION = "0.5.0";
47
47
  var RealtimeAvatar = class {
48
48
  #apiKey;
49
49
  #baseUrl;
@@ -60,7 +60,7 @@ var RealtimeAvatar = class {
60
60
  if (!options.apiKey) throw new RealtimeAvatarError("apiKey is required");
61
61
  this.#apiKey = options.apiKey;
62
62
  this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
63
- this.#fetch = options.fetch ?? globalThis.fetch;
63
+ this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
64
64
  this.#timeoutMs = options.timeoutMs ?? 6e4;
65
65
  this.#maxRetries = Math.max(0, options.maxRetries ?? 2);
66
66
  this.#userAgent = [`realtime-avatar-sdk/${SDK_VERSION}`, runtimeTag(), options.userAgent].filter(Boolean).join(" ");
@@ -197,9 +197,91 @@ var RealtimeAvatar = class {
197
197
  await this.#json(await this.#request("PATCH", `/avatars/${avatarId}`, { json: patch }))
198
198
  );
199
199
  }
200
+ /**
201
+ * Re-shoot the character — swap in new footage as her resting loop.
202
+ *
203
+ * ASYNCHRONOUS, and that is the whole design. This returns as soon as the swap is
204
+ * accepted; the avatar keeps serving its CURRENT loop, cache and clips the entire time,
205
+ * and cuts over to the new generation in one step once the replacement is prepared.
206
+ * A call minted a second after this returns is a normal call on the old footage.
207
+ *
208
+ * Two consequences worth designing for:
209
+ *
210
+ * - **The clip library empties and refills.** Old takes are footage of the old source and
211
+ * cannot splice against the new loop, so they are dropped and re-rendered. Between the
212
+ * cutover and the last re-render she rests on the new loop with less variety — never
213
+ * broken, just plainer. Do not gate your UI on the library being full.
214
+ * - **A failed swap does not fail the avatar.** She keeps serving, `status` stays `ready`,
215
+ * and the reason lands on `error`. So poll `getAvatar` and read `error` — a non-null
216
+ * `error` on a `ready` avatar is the swap that did not take, not an unhealthy character.
217
+ *
218
+ * The frame this rests on comes from the new footage: pass `anchorTimeMs` when frame 0 of
219
+ * the take is mid-blink. Video-sourced avatars only — a portrait-anchored one is a 422.
220
+ */
221
+ async swapSource(avatarId, input) {
222
+ const json2 = { sourceAssetId: input.sourceAssetId };
223
+ if (input.anchorTimeMs !== void 0) json2.anchorTimeMs = input.anchorTimeMs;
224
+ return toAvatar(await this.#json(await this.#request("PATCH", `/avatars/${avatarId}`, { json: json2 })));
225
+ }
226
+ /**
227
+ * Re-point the anchor at a different frame of the loop she ALREADY has — same footage,
228
+ * different rest pose. `swapSource` replaces the footage; this only moves the frame.
229
+ *
230
+ * Re-renders the clip library the same way, with the same degradation window, and is
231
+ * clamped server-side to the loop's last extractable frame (read the avatar back to see
232
+ * what was actually cut). Video-sourced avatars only.
233
+ */
234
+ async retimeAnchor(avatarId, anchorTimeMs) {
235
+ return toAvatar(
236
+ await this.#json(await this.#request("PATCH", `/avatars/${avatarId}`, { json: { anchorTimeMs } }))
237
+ );
238
+ }
200
239
  async deleteAvatar(avatarId) {
201
240
  await this.#json(await this.#request("DELETE", `/avatars/${avatarId}`));
202
241
  }
242
+ // A clip envelope missing `revision` would silently drop `expectedRevision` from the
243
+ // next declare — CAS degrades to unconditional with zero signal — so it throws instead.
244
+ #clipEnvelope(out) {
245
+ if (typeof out.revision !== "number" || !Array.isArray(out.data)) {
246
+ throw new RealtimeAvatarError("clip library response did not match the contract");
247
+ }
248
+ return out;
249
+ }
250
+ /**
251
+ * Declare the avatar's full desired clip library — a declaration, not a delta. The
252
+ * platform reconciles it against what exists: unchanged clips are `kept` (still
253
+ * serving), new or changed ones are `queued` to render, and clips you dropped are
254
+ * `retired`. The 202 is acceptance, not readiness — poll `listClips` until no row is
255
+ * `queued` or `generating`. A rejected upload settles `failed`, which is terminal, so
256
+ * waiting for all-`ready` waits forever. While a re-render is in flight the previous
257
+ * take keeps serving, so a declaration never blanks a live avatar.
258
+ *
259
+ * `expectedRevision` is compare-and-set: pass the `revision` you last read and a
260
+ * concurrent writer surfaces as a 409 instead of a lost update. Omit it to declare
261
+ * unconditionally.
262
+ *
263
+ * At most 12 clips: one `idle`, up to two `listen`, the rest `gesture`. An uploaded
264
+ * clip (`source: { assetId }`) must start AND end on the avatar's rest pose — pose
265
+ * validation rejects it otherwise (`status: "failed"`, the verdict in `poseCheck`),
266
+ * and the rest of the library is untouched.
267
+ */
268
+ async setClipLibrary(avatarId, library) {
269
+ const body = { clips: library.clips };
270
+ if (library.expectedRevision !== void 0) body.expectedRevision = library.expectedRevision;
271
+ return this.#clipEnvelope(
272
+ await this.#json(
273
+ await this.#request("PUT", `/avatars/${avatarId}/clips`, { json: body })
274
+ )
275
+ );
276
+ }
277
+ /** The avatar's clip library: every non-retired clip, plus revision, anchor and eligibility. */
278
+ async listClips(avatarId) {
279
+ return this.#clipEnvelope(
280
+ await this.#json(
281
+ await this.#request("GET", `/avatars/${avatarId}/clips`)
282
+ )
283
+ );
284
+ }
203
285
  /**
204
286
  * Reconcile an avatar's clip set after it changes.
205
287
  *
@@ -210,6 +292,10 @@ var RealtimeAvatar = class {
210
292
  * **At most 32 URLs per call.** This is the whole set for the avatar, not a delta, and the
211
293
  * endpoint rejects an oversize list rather than truncating it — so a library that outgrows
212
294
  * 32 needs the set trimmed, not split across two calls.
295
+ *
296
+ * @deprecated The externally-hosted clip tier this serves is sunsetting. Declare the
297
+ * library with {@link setClipLibrary} instead — the platform renders and hosts the
298
+ * clips, and pose-validates uploads against the avatar's rest pose.
213
299
  */
214
300
  async syncClips(avatarId, clipUrls) {
215
301
  const out = await this.#json(
@@ -385,7 +471,11 @@ function toAvatar(raw) {
385
471
  displayName: String(a.displayName ?? ""),
386
472
  sourceKind: a.sourceKind === "video" ? "video" : "image",
387
473
  status: a.status ?? "draft",
388
- defaultVoiceId: a.defaultVoiceId ? String(a.defaultVoiceId) : null
474
+ defaultVoiceId: a.defaultVoiceId ? String(a.defaultVoiceId) : null,
475
+ sourceAssetId: a.sourceAssetId ? String(a.sourceAssetId) : null,
476
+ // Carried because it is the ONLY channel a failed source swap has: she stays `ready`
477
+ // and serving, and this says why the re-shoot did not take.
478
+ error: a.error ? String(a.error) : null
389
479
  };
390
480
  }
391
481
  function toAsset(raw) {
package/dist/hono.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { P as ProxyConfig } from './types-9leGOgVv.js';
2
- import './types-C5zypiVY.js';
1
+ import { P as ProxyConfig } from './types-B45GgjrV.js';
2
+ import './types-DLxz6uFE.js';
3
3
 
4
4
  /**
5
5
  * Hono (and anything else built on Fetch handlers — Workers, Bun, Deno).
package/dist/hono.js CHANGED
@@ -43,7 +43,7 @@ var RealtimeAvatarHttpError = class extends RealtimeAvatarError {
43
43
 
44
44
  // ../http-client/src/client.ts
45
45
  var DEFAULT_BASE_URL = "https://realtimeavatar.ai/api/v1";
46
- var SDK_VERSION = "0.4.0";
46
+ var SDK_VERSION = "0.5.0";
47
47
  var RealtimeAvatar = class {
48
48
  #apiKey;
49
49
  #baseUrl;
@@ -60,7 +60,7 @@ var RealtimeAvatar = class {
60
60
  if (!options.apiKey) throw new RealtimeAvatarError("apiKey is required");
61
61
  this.#apiKey = options.apiKey;
62
62
  this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
63
- this.#fetch = options.fetch ?? globalThis.fetch;
63
+ this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
64
64
  this.#timeoutMs = options.timeoutMs ?? 6e4;
65
65
  this.#maxRetries = Math.max(0, options.maxRetries ?? 2);
66
66
  this.#userAgent = [`realtime-avatar-sdk/${SDK_VERSION}`, runtimeTag(), options.userAgent].filter(Boolean).join(" ");
@@ -197,9 +197,91 @@ var RealtimeAvatar = class {
197
197
  await this.#json(await this.#request("PATCH", `/avatars/${avatarId}`, { json: patch }))
198
198
  );
199
199
  }
200
+ /**
201
+ * Re-shoot the character — swap in new footage as her resting loop.
202
+ *
203
+ * ASYNCHRONOUS, and that is the whole design. This returns as soon as the swap is
204
+ * accepted; the avatar keeps serving its CURRENT loop, cache and clips the entire time,
205
+ * and cuts over to the new generation in one step once the replacement is prepared.
206
+ * A call minted a second after this returns is a normal call on the old footage.
207
+ *
208
+ * Two consequences worth designing for:
209
+ *
210
+ * - **The clip library empties and refills.** Old takes are footage of the old source and
211
+ * cannot splice against the new loop, so they are dropped and re-rendered. Between the
212
+ * cutover and the last re-render she rests on the new loop with less variety — never
213
+ * broken, just plainer. Do not gate your UI on the library being full.
214
+ * - **A failed swap does not fail the avatar.** She keeps serving, `status` stays `ready`,
215
+ * and the reason lands on `error`. So poll `getAvatar` and read `error` — a non-null
216
+ * `error` on a `ready` avatar is the swap that did not take, not an unhealthy character.
217
+ *
218
+ * The frame this rests on comes from the new footage: pass `anchorTimeMs` when frame 0 of
219
+ * the take is mid-blink. Video-sourced avatars only — a portrait-anchored one is a 422.
220
+ */
221
+ async swapSource(avatarId, input) {
222
+ const json2 = { sourceAssetId: input.sourceAssetId };
223
+ if (input.anchorTimeMs !== void 0) json2.anchorTimeMs = input.anchorTimeMs;
224
+ return toAvatar(await this.#json(await this.#request("PATCH", `/avatars/${avatarId}`, { json: json2 })));
225
+ }
226
+ /**
227
+ * Re-point the anchor at a different frame of the loop she ALREADY has — same footage,
228
+ * different rest pose. `swapSource` replaces the footage; this only moves the frame.
229
+ *
230
+ * Re-renders the clip library the same way, with the same degradation window, and is
231
+ * clamped server-side to the loop's last extractable frame (read the avatar back to see
232
+ * what was actually cut). Video-sourced avatars only.
233
+ */
234
+ async retimeAnchor(avatarId, anchorTimeMs) {
235
+ return toAvatar(
236
+ await this.#json(await this.#request("PATCH", `/avatars/${avatarId}`, { json: { anchorTimeMs } }))
237
+ );
238
+ }
200
239
  async deleteAvatar(avatarId) {
201
240
  await this.#json(await this.#request("DELETE", `/avatars/${avatarId}`));
202
241
  }
242
+ // A clip envelope missing `revision` would silently drop `expectedRevision` from the
243
+ // next declare — CAS degrades to unconditional with zero signal — so it throws instead.
244
+ #clipEnvelope(out) {
245
+ if (typeof out.revision !== "number" || !Array.isArray(out.data)) {
246
+ throw new RealtimeAvatarError("clip library response did not match the contract");
247
+ }
248
+ return out;
249
+ }
250
+ /**
251
+ * Declare the avatar's full desired clip library — a declaration, not a delta. The
252
+ * platform reconciles it against what exists: unchanged clips are `kept` (still
253
+ * serving), new or changed ones are `queued` to render, and clips you dropped are
254
+ * `retired`. The 202 is acceptance, not readiness — poll `listClips` until no row is
255
+ * `queued` or `generating`. A rejected upload settles `failed`, which is terminal, so
256
+ * waiting for all-`ready` waits forever. While a re-render is in flight the previous
257
+ * take keeps serving, so a declaration never blanks a live avatar.
258
+ *
259
+ * `expectedRevision` is compare-and-set: pass the `revision` you last read and a
260
+ * concurrent writer surfaces as a 409 instead of a lost update. Omit it to declare
261
+ * unconditionally.
262
+ *
263
+ * At most 12 clips: one `idle`, up to two `listen`, the rest `gesture`. An uploaded
264
+ * clip (`source: { assetId }`) must start AND end on the avatar's rest pose — pose
265
+ * validation rejects it otherwise (`status: "failed"`, the verdict in `poseCheck`),
266
+ * and the rest of the library is untouched.
267
+ */
268
+ async setClipLibrary(avatarId, library) {
269
+ const body = { clips: library.clips };
270
+ if (library.expectedRevision !== void 0) body.expectedRevision = library.expectedRevision;
271
+ return this.#clipEnvelope(
272
+ await this.#json(
273
+ await this.#request("PUT", `/avatars/${avatarId}/clips`, { json: body })
274
+ )
275
+ );
276
+ }
277
+ /** The avatar's clip library: every non-retired clip, plus revision, anchor and eligibility. */
278
+ async listClips(avatarId) {
279
+ return this.#clipEnvelope(
280
+ await this.#json(
281
+ await this.#request("GET", `/avatars/${avatarId}/clips`)
282
+ )
283
+ );
284
+ }
203
285
  /**
204
286
  * Reconcile an avatar's clip set after it changes.
205
287
  *
@@ -210,6 +292,10 @@ var RealtimeAvatar = class {
210
292
  * **At most 32 URLs per call.** This is the whole set for the avatar, not a delta, and the
211
293
  * endpoint rejects an oversize list rather than truncating it — so a library that outgrows
212
294
  * 32 needs the set trimmed, not split across two calls.
295
+ *
296
+ * @deprecated The externally-hosted clip tier this serves is sunsetting. Declare the
297
+ * library with {@link setClipLibrary} instead — the platform renders and hosts the
298
+ * clips, and pose-validates uploads against the avatar's rest pose.
213
299
  */
214
300
  async syncClips(avatarId, clipUrls) {
215
301
  const out = await this.#json(
@@ -385,7 +471,11 @@ function toAvatar(raw) {
385
471
  displayName: String(a.displayName ?? ""),
386
472
  sourceKind: a.sourceKind === "video" ? "video" : "image",
387
473
  status: a.status ?? "draft",
388
- defaultVoiceId: a.defaultVoiceId ? String(a.defaultVoiceId) : null
474
+ defaultVoiceId: a.defaultVoiceId ? String(a.defaultVoiceId) : null,
475
+ sourceAssetId: a.sourceAssetId ? String(a.sourceAssetId) : null,
476
+ // Carried because it is the ONLY channel a failed source swap has: she stays `ready`
477
+ // and serving, and this says why the re-shoot did not take.
478
+ error: a.error ? String(a.error) : null
389
479
  };
390
480
  }
391
481
  function toAsset(raw) {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as CallPolicy, C as CallMode, S as StartCallResult, E as EndCallOptions, A as Avatar, b as AvatarUpdate, c as ClipSyncResult, d as AssetKind, e as Asset, L as ListSessionsOptions, U as UsageSessionPage, f as UsageSession, g as CreditBalance, T as TranscriptPayload } from './types-C5zypiVY.js';
2
- export { h as CallConnection, i as CallQueued, j as ContextMessage, k as EndCallReason, V as VideoPolicy, l as VideoState, m as isQueued } from './types-C5zypiVY.js';
1
+ import { a as CallPolicy, C as CallMode, S as StartCallResult, E as EndCallOptions, A as Avatar, b as AvatarUpdate, c as AvatarSourceSwap, d as ClipDeclaration, e as ClipLibraryUpdate, f as ClipLibrary, g as ClipSyncResult, h as AssetKind, i as Asset, L as ListSessionsOptions, U as UsageSessionPage, j as UsageSession, k as CreditBalance, T as TranscriptPayload } from './types-DLxz6uFE.js';
2
+ export { l as AvatarClip, m as CallConnection, n as CallQueued, o as ClipLibraryPlan, p as ClipSource, q as ContextMessage, r as EndCallReason, V as VideoPolicy, s as VideoState, t as isQueued } from './types-DLxz6uFE.js';
3
3
 
4
4
  interface RealtimeAvatarOptions {
5
5
  /** `tic_live_…` or `tic_test_…`. Server-side only — never ship this to a browser. */
@@ -98,7 +98,62 @@ declare class RealtimeAvatar {
98
98
  getAvatar(avatarId: string): Promise<Avatar>;
99
99
  /** Re-point what an avatar already is. `defaultVoiceId: null` clears the default voice. */
100
100
  updateAvatar(avatarId: string, patch: AvatarUpdate): Promise<Avatar>;
101
+ /**
102
+ * Re-shoot the character — swap in new footage as her resting loop.
103
+ *
104
+ * ASYNCHRONOUS, and that is the whole design. This returns as soon as the swap is
105
+ * accepted; the avatar keeps serving its CURRENT loop, cache and clips the entire time,
106
+ * and cuts over to the new generation in one step once the replacement is prepared.
107
+ * A call minted a second after this returns is a normal call on the old footage.
108
+ *
109
+ * Two consequences worth designing for:
110
+ *
111
+ * - **The clip library empties and refills.** Old takes are footage of the old source and
112
+ * cannot splice against the new loop, so they are dropped and re-rendered. Between the
113
+ * cutover and the last re-render she rests on the new loop with less variety — never
114
+ * broken, just plainer. Do not gate your UI on the library being full.
115
+ * - **A failed swap does not fail the avatar.** She keeps serving, `status` stays `ready`,
116
+ * and the reason lands on `error`. So poll `getAvatar` and read `error` — a non-null
117
+ * `error` on a `ready` avatar is the swap that did not take, not an unhealthy character.
118
+ *
119
+ * The frame this rests on comes from the new footage: pass `anchorTimeMs` when frame 0 of
120
+ * the take is mid-blink. Video-sourced avatars only — a portrait-anchored one is a 422.
121
+ */
122
+ swapSource(avatarId: string, input: AvatarSourceSwap): Promise<Avatar>;
123
+ /**
124
+ * Re-point the anchor at a different frame of the loop she ALREADY has — same footage,
125
+ * different rest pose. `swapSource` replaces the footage; this only moves the frame.
126
+ *
127
+ * Re-renders the clip library the same way, with the same degradation window, and is
128
+ * clamped server-side to the loop's last extractable frame (read the avatar back to see
129
+ * what was actually cut). Video-sourced avatars only.
130
+ */
131
+ retimeAnchor(avatarId: string, anchorTimeMs: number): Promise<Avatar>;
101
132
  deleteAvatar(avatarId: string): Promise<void>;
133
+ /**
134
+ * Declare the avatar's full desired clip library — a declaration, not a delta. The
135
+ * platform reconciles it against what exists: unchanged clips are `kept` (still
136
+ * serving), new or changed ones are `queued` to render, and clips you dropped are
137
+ * `retired`. The 202 is acceptance, not readiness — poll `listClips` until no row is
138
+ * `queued` or `generating`. A rejected upload settles `failed`, which is terminal, so
139
+ * waiting for all-`ready` waits forever. While a re-render is in flight the previous
140
+ * take keeps serving, so a declaration never blanks a live avatar.
141
+ *
142
+ * `expectedRevision` is compare-and-set: pass the `revision` you last read and a
143
+ * concurrent writer surfaces as a 409 instead of a lost update. Omit it to declare
144
+ * unconditionally.
145
+ *
146
+ * At most 12 clips: one `idle`, up to two `listen`, the rest `gesture`. An uploaded
147
+ * clip (`source: { assetId }`) must start AND end on the avatar's rest pose — pose
148
+ * validation rejects it otherwise (`status: "failed"`, the verdict in `poseCheck`),
149
+ * and the rest of the library is untouched.
150
+ */
151
+ setClipLibrary(avatarId: string, library: {
152
+ clips: readonly ClipDeclaration[];
153
+ expectedRevision?: number;
154
+ }): Promise<ClipLibraryUpdate>;
155
+ /** The avatar's clip library: every non-retired clip, plus revision, anchor and eligibility. */
156
+ listClips(avatarId: string): Promise<ClipLibrary>;
102
157
  /**
103
158
  * Reconcile an avatar's clip set after it changes.
104
159
  *
@@ -109,6 +164,10 @@ declare class RealtimeAvatar {
109
164
  * **At most 32 URLs per call.** This is the whole set for the avatar, not a delta, and the
110
165
  * endpoint rejects an oversize list rather than truncating it — so a library that outgrows
111
166
  * 32 needs the set trimmed, not split across two calls.
167
+ *
168
+ * @deprecated The externally-hosted clip tier this serves is sunsetting. Declare the
169
+ * library with {@link setClipLibrary} instead — the platform renders and hosts the
170
+ * clips, and pose-validates uploads against the avatar's rest pose.
112
171
  */
113
172
  syncClips(avatarId: string, clipUrls: readonly string[]): Promise<ClipSyncResult>;
114
173
  /** Hand us a URL and we stream it into storage. Prefer this for anything large. */
@@ -189,4 +248,4 @@ declare function verifyTranscript(rawBody: Uint8Array | string, headers: Headers
189
248
  toleranceSeconds?: number;
190
249
  }): Promise<TranscriptPayload>;
191
250
 
192
- export { Asset, AssetKind, Avatar, AvatarUpdate, CallMode, CallPolicy, ClipSyncResult, CreditBalance, EndCallOptions, ListSessionsOptions, RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, type RealtimeAvatarOptions, type StartCallOptions, StartCallResult, TranscriptPayload, UsageSession, UsageSessionPage, verifyTranscript };
251
+ export { Asset, AssetKind, Avatar, AvatarSourceSwap, AvatarUpdate, CallMode, CallPolicy, ClipDeclaration, ClipLibrary, ClipLibraryUpdate, ClipSyncResult, CreditBalance, EndCallOptions, ListSessionsOptions, RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, type RealtimeAvatarOptions, type StartCallOptions, StartCallResult, TranscriptPayload, UsageSession, UsageSessionPage, verifyTranscript };
package/dist/index.js CHANGED
@@ -43,7 +43,7 @@ var RealtimeAvatarHttpError = class extends RealtimeAvatarError {
43
43
 
44
44
  // ../http-client/src/client.ts
45
45
  var DEFAULT_BASE_URL = "https://realtimeavatar.ai/api/v1";
46
- var SDK_VERSION = "0.4.0";
46
+ var SDK_VERSION = "0.5.0";
47
47
  var RealtimeAvatar = class {
48
48
  #apiKey;
49
49
  #baseUrl;
@@ -60,7 +60,7 @@ var RealtimeAvatar = class {
60
60
  if (!options.apiKey) throw new RealtimeAvatarError("apiKey is required");
61
61
  this.#apiKey = options.apiKey;
62
62
  this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
63
- this.#fetch = options.fetch ?? globalThis.fetch;
63
+ this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
64
64
  this.#timeoutMs = options.timeoutMs ?? 6e4;
65
65
  this.#maxRetries = Math.max(0, options.maxRetries ?? 2);
66
66
  this.#userAgent = [`realtime-avatar-sdk/${SDK_VERSION}`, runtimeTag(), options.userAgent].filter(Boolean).join(" ");
@@ -197,9 +197,91 @@ var RealtimeAvatar = class {
197
197
  await this.#json(await this.#request("PATCH", `/avatars/${avatarId}`, { json: patch }))
198
198
  );
199
199
  }
200
+ /**
201
+ * Re-shoot the character — swap in new footage as her resting loop.
202
+ *
203
+ * ASYNCHRONOUS, and that is the whole design. This returns as soon as the swap is
204
+ * accepted; the avatar keeps serving its CURRENT loop, cache and clips the entire time,
205
+ * and cuts over to the new generation in one step once the replacement is prepared.
206
+ * A call minted a second after this returns is a normal call on the old footage.
207
+ *
208
+ * Two consequences worth designing for:
209
+ *
210
+ * - **The clip library empties and refills.** Old takes are footage of the old source and
211
+ * cannot splice against the new loop, so they are dropped and re-rendered. Between the
212
+ * cutover and the last re-render she rests on the new loop with less variety — never
213
+ * broken, just plainer. Do not gate your UI on the library being full.
214
+ * - **A failed swap does not fail the avatar.** She keeps serving, `status` stays `ready`,
215
+ * and the reason lands on `error`. So poll `getAvatar` and read `error` — a non-null
216
+ * `error` on a `ready` avatar is the swap that did not take, not an unhealthy character.
217
+ *
218
+ * The frame this rests on comes from the new footage: pass `anchorTimeMs` when frame 0 of
219
+ * the take is mid-blink. Video-sourced avatars only — a portrait-anchored one is a 422.
220
+ */
221
+ async swapSource(avatarId, input) {
222
+ const json = { sourceAssetId: input.sourceAssetId };
223
+ if (input.anchorTimeMs !== void 0) json.anchorTimeMs = input.anchorTimeMs;
224
+ return toAvatar(await this.#json(await this.#request("PATCH", `/avatars/${avatarId}`, { json })));
225
+ }
226
+ /**
227
+ * Re-point the anchor at a different frame of the loop she ALREADY has — same footage,
228
+ * different rest pose. `swapSource` replaces the footage; this only moves the frame.
229
+ *
230
+ * Re-renders the clip library the same way, with the same degradation window, and is
231
+ * clamped server-side to the loop's last extractable frame (read the avatar back to see
232
+ * what was actually cut). Video-sourced avatars only.
233
+ */
234
+ async retimeAnchor(avatarId, anchorTimeMs) {
235
+ return toAvatar(
236
+ await this.#json(await this.#request("PATCH", `/avatars/${avatarId}`, { json: { anchorTimeMs } }))
237
+ );
238
+ }
200
239
  async deleteAvatar(avatarId) {
201
240
  await this.#json(await this.#request("DELETE", `/avatars/${avatarId}`));
202
241
  }
242
+ // A clip envelope missing `revision` would silently drop `expectedRevision` from the
243
+ // next declare — CAS degrades to unconditional with zero signal — so it throws instead.
244
+ #clipEnvelope(out) {
245
+ if (typeof out.revision !== "number" || !Array.isArray(out.data)) {
246
+ throw new RealtimeAvatarError("clip library response did not match the contract");
247
+ }
248
+ return out;
249
+ }
250
+ /**
251
+ * Declare the avatar's full desired clip library — a declaration, not a delta. The
252
+ * platform reconciles it against what exists: unchanged clips are `kept` (still
253
+ * serving), new or changed ones are `queued` to render, and clips you dropped are
254
+ * `retired`. The 202 is acceptance, not readiness — poll `listClips` until no row is
255
+ * `queued` or `generating`. A rejected upload settles `failed`, which is terminal, so
256
+ * waiting for all-`ready` waits forever. While a re-render is in flight the previous
257
+ * take keeps serving, so a declaration never blanks a live avatar.
258
+ *
259
+ * `expectedRevision` is compare-and-set: pass the `revision` you last read and a
260
+ * concurrent writer surfaces as a 409 instead of a lost update. Omit it to declare
261
+ * unconditionally.
262
+ *
263
+ * At most 12 clips: one `idle`, up to two `listen`, the rest `gesture`. An uploaded
264
+ * clip (`source: { assetId }`) must start AND end on the avatar's rest pose — pose
265
+ * validation rejects it otherwise (`status: "failed"`, the verdict in `poseCheck`),
266
+ * and the rest of the library is untouched.
267
+ */
268
+ async setClipLibrary(avatarId, library) {
269
+ const body = { clips: library.clips };
270
+ if (library.expectedRevision !== void 0) body.expectedRevision = library.expectedRevision;
271
+ return this.#clipEnvelope(
272
+ await this.#json(
273
+ await this.#request("PUT", `/avatars/${avatarId}/clips`, { json: body })
274
+ )
275
+ );
276
+ }
277
+ /** The avatar's clip library: every non-retired clip, plus revision, anchor and eligibility. */
278
+ async listClips(avatarId) {
279
+ return this.#clipEnvelope(
280
+ await this.#json(
281
+ await this.#request("GET", `/avatars/${avatarId}/clips`)
282
+ )
283
+ );
284
+ }
203
285
  /**
204
286
  * Reconcile an avatar's clip set after it changes.
205
287
  *
@@ -210,6 +292,10 @@ var RealtimeAvatar = class {
210
292
  * **At most 32 URLs per call.** This is the whole set for the avatar, not a delta, and the
211
293
  * endpoint rejects an oversize list rather than truncating it — so a library that outgrows
212
294
  * 32 needs the set trimmed, not split across two calls.
295
+ *
296
+ * @deprecated The externally-hosted clip tier this serves is sunsetting. Declare the
297
+ * library with {@link setClipLibrary} instead — the platform renders and hosts the
298
+ * clips, and pose-validates uploads against the avatar's rest pose.
213
299
  */
214
300
  async syncClips(avatarId, clipUrls) {
215
301
  const out = await this.#json(
@@ -385,7 +471,11 @@ function toAvatar(raw) {
385
471
  displayName: String(a.displayName ?? ""),
386
472
  sourceKind: a.sourceKind === "video" ? "video" : "image",
387
473
  status: a.status ?? "draft",
388
- defaultVoiceId: a.defaultVoiceId ? String(a.defaultVoiceId) : null
474
+ defaultVoiceId: a.defaultVoiceId ? String(a.defaultVoiceId) : null,
475
+ sourceAssetId: a.sourceAssetId ? String(a.sourceAssetId) : null,
476
+ // Carried because it is the ONLY channel a failed source swap has: she stays `ready`
477
+ // and serving, and this says why the re-shoot did not take.
478
+ error: a.error ? String(a.error) : null
389
479
  };
390
480
  }
391
481
  function toAsset(raw) {
package/dist/nextjs.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { P as ProxyConfig } from './types-9leGOgVv.js';
2
- import './types-C5zypiVY.js';
1
+ import { P as ProxyConfig } from './types-B45GgjrV.js';
2
+ import './types-DLxz6uFE.js';
3
3
 
4
4
  /**
5
5
  * App Router. Mount at `app/api/realtime-avatar/[...path]/route.ts`:
package/dist/nextjs.js CHANGED
@@ -43,7 +43,7 @@ var RealtimeAvatarHttpError = class extends RealtimeAvatarError {
43
43
 
44
44
  // ../http-client/src/client.ts
45
45
  var DEFAULT_BASE_URL = "https://realtimeavatar.ai/api/v1";
46
- var SDK_VERSION = "0.4.0";
46
+ var SDK_VERSION = "0.5.0";
47
47
  var RealtimeAvatar = class {
48
48
  #apiKey;
49
49
  #baseUrl;
@@ -60,7 +60,7 @@ var RealtimeAvatar = class {
60
60
  if (!options.apiKey) throw new RealtimeAvatarError("apiKey is required");
61
61
  this.#apiKey = options.apiKey;
62
62
  this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
63
- this.#fetch = options.fetch ?? globalThis.fetch;
63
+ this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
64
64
  this.#timeoutMs = options.timeoutMs ?? 6e4;
65
65
  this.#maxRetries = Math.max(0, options.maxRetries ?? 2);
66
66
  this.#userAgent = [`realtime-avatar-sdk/${SDK_VERSION}`, runtimeTag(), options.userAgent].filter(Boolean).join(" ");
@@ -197,9 +197,91 @@ var RealtimeAvatar = class {
197
197
  await this.#json(await this.#request("PATCH", `/avatars/${avatarId}`, { json: patch }))
198
198
  );
199
199
  }
200
+ /**
201
+ * Re-shoot the character — swap in new footage as her resting loop.
202
+ *
203
+ * ASYNCHRONOUS, and that is the whole design. This returns as soon as the swap is
204
+ * accepted; the avatar keeps serving its CURRENT loop, cache and clips the entire time,
205
+ * and cuts over to the new generation in one step once the replacement is prepared.
206
+ * A call minted a second after this returns is a normal call on the old footage.
207
+ *
208
+ * Two consequences worth designing for:
209
+ *
210
+ * - **The clip library empties and refills.** Old takes are footage of the old source and
211
+ * cannot splice against the new loop, so they are dropped and re-rendered. Between the
212
+ * cutover and the last re-render she rests on the new loop with less variety — never
213
+ * broken, just plainer. Do not gate your UI on the library being full.
214
+ * - **A failed swap does not fail the avatar.** She keeps serving, `status` stays `ready`,
215
+ * and the reason lands on `error`. So poll `getAvatar` and read `error` — a non-null
216
+ * `error` on a `ready` avatar is the swap that did not take, not an unhealthy character.
217
+ *
218
+ * The frame this rests on comes from the new footage: pass `anchorTimeMs` when frame 0 of
219
+ * the take is mid-blink. Video-sourced avatars only — a portrait-anchored one is a 422.
220
+ */
221
+ async swapSource(avatarId, input) {
222
+ const json2 = { sourceAssetId: input.sourceAssetId };
223
+ if (input.anchorTimeMs !== void 0) json2.anchorTimeMs = input.anchorTimeMs;
224
+ return toAvatar(await this.#json(await this.#request("PATCH", `/avatars/${avatarId}`, { json: json2 })));
225
+ }
226
+ /**
227
+ * Re-point the anchor at a different frame of the loop she ALREADY has — same footage,
228
+ * different rest pose. `swapSource` replaces the footage; this only moves the frame.
229
+ *
230
+ * Re-renders the clip library the same way, with the same degradation window, and is
231
+ * clamped server-side to the loop's last extractable frame (read the avatar back to see
232
+ * what was actually cut). Video-sourced avatars only.
233
+ */
234
+ async retimeAnchor(avatarId, anchorTimeMs) {
235
+ return toAvatar(
236
+ await this.#json(await this.#request("PATCH", `/avatars/${avatarId}`, { json: { anchorTimeMs } }))
237
+ );
238
+ }
200
239
  async deleteAvatar(avatarId) {
201
240
  await this.#json(await this.#request("DELETE", `/avatars/${avatarId}`));
202
241
  }
242
+ // A clip envelope missing `revision` would silently drop `expectedRevision` from the
243
+ // next declare — CAS degrades to unconditional with zero signal — so it throws instead.
244
+ #clipEnvelope(out) {
245
+ if (typeof out.revision !== "number" || !Array.isArray(out.data)) {
246
+ throw new RealtimeAvatarError("clip library response did not match the contract");
247
+ }
248
+ return out;
249
+ }
250
+ /**
251
+ * Declare the avatar's full desired clip library — a declaration, not a delta. The
252
+ * platform reconciles it against what exists: unchanged clips are `kept` (still
253
+ * serving), new or changed ones are `queued` to render, and clips you dropped are
254
+ * `retired`. The 202 is acceptance, not readiness — poll `listClips` until no row is
255
+ * `queued` or `generating`. A rejected upload settles `failed`, which is terminal, so
256
+ * waiting for all-`ready` waits forever. While a re-render is in flight the previous
257
+ * take keeps serving, so a declaration never blanks a live avatar.
258
+ *
259
+ * `expectedRevision` is compare-and-set: pass the `revision` you last read and a
260
+ * concurrent writer surfaces as a 409 instead of a lost update. Omit it to declare
261
+ * unconditionally.
262
+ *
263
+ * At most 12 clips: one `idle`, up to two `listen`, the rest `gesture`. An uploaded
264
+ * clip (`source: { assetId }`) must start AND end on the avatar's rest pose — pose
265
+ * validation rejects it otherwise (`status: "failed"`, the verdict in `poseCheck`),
266
+ * and the rest of the library is untouched.
267
+ */
268
+ async setClipLibrary(avatarId, library) {
269
+ const body = { clips: library.clips };
270
+ if (library.expectedRevision !== void 0) body.expectedRevision = library.expectedRevision;
271
+ return this.#clipEnvelope(
272
+ await this.#json(
273
+ await this.#request("PUT", `/avatars/${avatarId}/clips`, { json: body })
274
+ )
275
+ );
276
+ }
277
+ /** The avatar's clip library: every non-retired clip, plus revision, anchor and eligibility. */
278
+ async listClips(avatarId) {
279
+ return this.#clipEnvelope(
280
+ await this.#json(
281
+ await this.#request("GET", `/avatars/${avatarId}/clips`)
282
+ )
283
+ );
284
+ }
203
285
  /**
204
286
  * Reconcile an avatar's clip set after it changes.
205
287
  *
@@ -210,6 +292,10 @@ var RealtimeAvatar = class {
210
292
  * **At most 32 URLs per call.** This is the whole set for the avatar, not a delta, and the
211
293
  * endpoint rejects an oversize list rather than truncating it — so a library that outgrows
212
294
  * 32 needs the set trimmed, not split across two calls.
295
+ *
296
+ * @deprecated The externally-hosted clip tier this serves is sunsetting. Declare the
297
+ * library with {@link setClipLibrary} instead — the platform renders and hosts the
298
+ * clips, and pose-validates uploads against the avatar's rest pose.
213
299
  */
214
300
  async syncClips(avatarId, clipUrls) {
215
301
  const out = await this.#json(
@@ -385,7 +471,11 @@ function toAvatar(raw) {
385
471
  displayName: String(a.displayName ?? ""),
386
472
  sourceKind: a.sourceKind === "video" ? "video" : "image",
387
473
  status: a.status ?? "draft",
388
- defaultVoiceId: a.defaultVoiceId ? String(a.defaultVoiceId) : null
474
+ defaultVoiceId: a.defaultVoiceId ? String(a.defaultVoiceId) : null,
475
+ sourceAssetId: a.sourceAssetId ? String(a.sourceAssetId) : null,
476
+ // Carried because it is the ONLY channel a failed source swap has: she stays `ready`
477
+ // and serving, and this says why the re-shoot did not take.
478
+ error: a.error ? String(a.error) : null
389
479
  };
390
480
  }
391
481
  function toAsset(raw) {
@@ -405,7 +405,8 @@ function useLiveKitAvatarGrant(input) {
405
405
  );
406
406
  useEffect(() => {
407
407
  if (!autoRetryBusy || !active || !tabVisible || state.status !== "busy" || !state.busy) return;
408
- const retryMs = Math.max(state.busy.recommended_retry_ms, 250);
408
+ const hinted = state.busy.recommended_retry_ms;
409
+ const retryMs = Math.max(Number.isFinite(hinted) ? hinted : 5e3, 250);
409
410
  const timer = window.setTimeout(refresh, retryMs);
410
411
  return () => window.clearTimeout(timer);
411
412
  }, [active, autoRetryBusy, tabVisible, refresh, state.busy, state.status]);
package/dist/react.js CHANGED
@@ -364,7 +364,8 @@ function useLiveKitAvatarGrant(input) {
364
364
  );
365
365
  useEffect(() => {
366
366
  if (!autoRetryBusy || !active || !tabVisible || state.status !== "busy" || !state.busy) return;
367
- const retryMs = Math.max(state.busy.recommended_retry_ms, 250);
367
+ const hinted = state.busy.recommended_retry_ms;
368
+ const retryMs = Math.max(Number.isFinite(hinted) ? hinted : 5e3, 250);
368
369
  const timer = window.setTimeout(refresh, retryMs);
369
370
  return () => window.clearTimeout(timer);
370
371
  }, [active, autoRetryBusy, tabVisible, refresh, state.busy, state.status]);
package/dist/server.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export { RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, RealtimeAvatarOptions, StartCallOptions, verifyTranscript } from './index.js';
2
- export { e as Asset, d as AssetKind, A as Avatar, b as AvatarUpdate, h as CallConnection, C as CallMode, a as CallPolicy, i as CallQueued, c as ClipSyncResult, j as ContextMessage, g as CreditBalance, E as EndCallOptions, k as EndCallReason, L as ListSessionsOptions, S as StartCallResult, T as TranscriptPayload, f as UsageSession, U as UsageSessionPage, V as VideoPolicy, l as VideoState, m as isQueued } from './types-C5zypiVY.js';
2
+ export { i as Asset, h as AssetKind, A as Avatar, l as AvatarClip, c as AvatarSourceSwap, b as AvatarUpdate, m as CallConnection, C as CallMode, a as CallPolicy, n as CallQueued, d as ClipDeclaration, f as ClipLibrary, o as ClipLibraryPlan, e as ClipLibraryUpdate, p as ClipSource, g as ClipSyncResult, q as ContextMessage, k as CreditBalance, E as EndCallOptions, r as EndCallReason, L as ListSessionsOptions, S as StartCallResult, T as TranscriptPayload, j as UsageSession, U as UsageSessionPage, V as VideoPolicy, s as VideoState, t as isQueued } from './types-DLxz6uFE.js';
package/dist/server.js CHANGED
@@ -43,7 +43,7 @@ var RealtimeAvatarHttpError = class extends RealtimeAvatarError {
43
43
 
44
44
  // ../http-client/src/client.ts
45
45
  var DEFAULT_BASE_URL = "https://realtimeavatar.ai/api/v1";
46
- var SDK_VERSION = "0.4.0";
46
+ var SDK_VERSION = "0.5.0";
47
47
  var RealtimeAvatar = class {
48
48
  #apiKey;
49
49
  #baseUrl;
@@ -60,7 +60,7 @@ var RealtimeAvatar = class {
60
60
  if (!options.apiKey) throw new RealtimeAvatarError("apiKey is required");
61
61
  this.#apiKey = options.apiKey;
62
62
  this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
63
- this.#fetch = options.fetch ?? globalThis.fetch;
63
+ this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
64
64
  this.#timeoutMs = options.timeoutMs ?? 6e4;
65
65
  this.#maxRetries = Math.max(0, options.maxRetries ?? 2);
66
66
  this.#userAgent = [`realtime-avatar-sdk/${SDK_VERSION}`, runtimeTag(), options.userAgent].filter(Boolean).join(" ");
@@ -197,9 +197,91 @@ var RealtimeAvatar = class {
197
197
  await this.#json(await this.#request("PATCH", `/avatars/${avatarId}`, { json: patch }))
198
198
  );
199
199
  }
200
+ /**
201
+ * Re-shoot the character — swap in new footage as her resting loop.
202
+ *
203
+ * ASYNCHRONOUS, and that is the whole design. This returns as soon as the swap is
204
+ * accepted; the avatar keeps serving its CURRENT loop, cache and clips the entire time,
205
+ * and cuts over to the new generation in one step once the replacement is prepared.
206
+ * A call minted a second after this returns is a normal call on the old footage.
207
+ *
208
+ * Two consequences worth designing for:
209
+ *
210
+ * - **The clip library empties and refills.** Old takes are footage of the old source and
211
+ * cannot splice against the new loop, so they are dropped and re-rendered. Between the
212
+ * cutover and the last re-render she rests on the new loop with less variety — never
213
+ * broken, just plainer. Do not gate your UI on the library being full.
214
+ * - **A failed swap does not fail the avatar.** She keeps serving, `status` stays `ready`,
215
+ * and the reason lands on `error`. So poll `getAvatar` and read `error` — a non-null
216
+ * `error` on a `ready` avatar is the swap that did not take, not an unhealthy character.
217
+ *
218
+ * The frame this rests on comes from the new footage: pass `anchorTimeMs` when frame 0 of
219
+ * the take is mid-blink. Video-sourced avatars only — a portrait-anchored one is a 422.
220
+ */
221
+ async swapSource(avatarId, input) {
222
+ const json = { sourceAssetId: input.sourceAssetId };
223
+ if (input.anchorTimeMs !== void 0) json.anchorTimeMs = input.anchorTimeMs;
224
+ return toAvatar(await this.#json(await this.#request("PATCH", `/avatars/${avatarId}`, { json })));
225
+ }
226
+ /**
227
+ * Re-point the anchor at a different frame of the loop she ALREADY has — same footage,
228
+ * different rest pose. `swapSource` replaces the footage; this only moves the frame.
229
+ *
230
+ * Re-renders the clip library the same way, with the same degradation window, and is
231
+ * clamped server-side to the loop's last extractable frame (read the avatar back to see
232
+ * what was actually cut). Video-sourced avatars only.
233
+ */
234
+ async retimeAnchor(avatarId, anchorTimeMs) {
235
+ return toAvatar(
236
+ await this.#json(await this.#request("PATCH", `/avatars/${avatarId}`, { json: { anchorTimeMs } }))
237
+ );
238
+ }
200
239
  async deleteAvatar(avatarId) {
201
240
  await this.#json(await this.#request("DELETE", `/avatars/${avatarId}`));
202
241
  }
242
+ // A clip envelope missing `revision` would silently drop `expectedRevision` from the
243
+ // next declare — CAS degrades to unconditional with zero signal — so it throws instead.
244
+ #clipEnvelope(out) {
245
+ if (typeof out.revision !== "number" || !Array.isArray(out.data)) {
246
+ throw new RealtimeAvatarError("clip library response did not match the contract");
247
+ }
248
+ return out;
249
+ }
250
+ /**
251
+ * Declare the avatar's full desired clip library — a declaration, not a delta. The
252
+ * platform reconciles it against what exists: unchanged clips are `kept` (still
253
+ * serving), new or changed ones are `queued` to render, and clips you dropped are
254
+ * `retired`. The 202 is acceptance, not readiness — poll `listClips` until no row is
255
+ * `queued` or `generating`. A rejected upload settles `failed`, which is terminal, so
256
+ * waiting for all-`ready` waits forever. While a re-render is in flight the previous
257
+ * take keeps serving, so a declaration never blanks a live avatar.
258
+ *
259
+ * `expectedRevision` is compare-and-set: pass the `revision` you last read and a
260
+ * concurrent writer surfaces as a 409 instead of a lost update. Omit it to declare
261
+ * unconditionally.
262
+ *
263
+ * At most 12 clips: one `idle`, up to two `listen`, the rest `gesture`. An uploaded
264
+ * clip (`source: { assetId }`) must start AND end on the avatar's rest pose — pose
265
+ * validation rejects it otherwise (`status: "failed"`, the verdict in `poseCheck`),
266
+ * and the rest of the library is untouched.
267
+ */
268
+ async setClipLibrary(avatarId, library) {
269
+ const body = { clips: library.clips };
270
+ if (library.expectedRevision !== void 0) body.expectedRevision = library.expectedRevision;
271
+ return this.#clipEnvelope(
272
+ await this.#json(
273
+ await this.#request("PUT", `/avatars/${avatarId}/clips`, { json: body })
274
+ )
275
+ );
276
+ }
277
+ /** The avatar's clip library: every non-retired clip, plus revision, anchor and eligibility. */
278
+ async listClips(avatarId) {
279
+ return this.#clipEnvelope(
280
+ await this.#json(
281
+ await this.#request("GET", `/avatars/${avatarId}/clips`)
282
+ )
283
+ );
284
+ }
203
285
  /**
204
286
  * Reconcile an avatar's clip set after it changes.
205
287
  *
@@ -210,6 +292,10 @@ var RealtimeAvatar = class {
210
292
  * **At most 32 URLs per call.** This is the whole set for the avatar, not a delta, and the
211
293
  * endpoint rejects an oversize list rather than truncating it — so a library that outgrows
212
294
  * 32 needs the set trimmed, not split across two calls.
295
+ *
296
+ * @deprecated The externally-hosted clip tier this serves is sunsetting. Declare the
297
+ * library with {@link setClipLibrary} instead — the platform renders and hosts the
298
+ * clips, and pose-validates uploads against the avatar's rest pose.
213
299
  */
214
300
  async syncClips(avatarId, clipUrls) {
215
301
  const out = await this.#json(
@@ -385,7 +471,11 @@ function toAvatar(raw) {
385
471
  displayName: String(a.displayName ?? ""),
386
472
  sourceKind: a.sourceKind === "video" ? "video" : "image",
387
473
  status: a.status ?? "draft",
388
- defaultVoiceId: a.defaultVoiceId ? String(a.defaultVoiceId) : null
474
+ defaultVoiceId: a.defaultVoiceId ? String(a.defaultVoiceId) : null,
475
+ sourceAssetId: a.sourceAssetId ? String(a.sourceAssetId) : null,
476
+ // Carried because it is the ONLY channel a failed source swap has: she stays `ready`
477
+ // and serving, and this says why the re-shoot did not take.
478
+ error: a.error ? String(a.error) : null
389
479
  };
390
480
  }
391
481
  function toAsset(raw) {
@@ -1,5 +1,5 @@
1
- import { P as ProxyConfig } from './types-9leGOgVv.js';
2
- import './types-C5zypiVY.js';
1
+ import { P as ProxyConfig } from './types-B45GgjrV.js';
2
+ import './types-DLxz6uFE.js';
3
3
 
4
4
  /**
5
5
  * TanStack Start. Mount at `routes/api/realtime-avatar/$.ts` — the trailing `$` is Start's
@@ -43,7 +43,7 @@ var RealtimeAvatarHttpError = class extends RealtimeAvatarError {
43
43
 
44
44
  // ../http-client/src/client.ts
45
45
  var DEFAULT_BASE_URL = "https://realtimeavatar.ai/api/v1";
46
- var SDK_VERSION = "0.4.0";
46
+ var SDK_VERSION = "0.5.0";
47
47
  var RealtimeAvatar = class {
48
48
  #apiKey;
49
49
  #baseUrl;
@@ -60,7 +60,7 @@ var RealtimeAvatar = class {
60
60
  if (!options.apiKey) throw new RealtimeAvatarError("apiKey is required");
61
61
  this.#apiKey = options.apiKey;
62
62
  this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
63
- this.#fetch = options.fetch ?? globalThis.fetch;
63
+ this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
64
64
  this.#timeoutMs = options.timeoutMs ?? 6e4;
65
65
  this.#maxRetries = Math.max(0, options.maxRetries ?? 2);
66
66
  this.#userAgent = [`realtime-avatar-sdk/${SDK_VERSION}`, runtimeTag(), options.userAgent].filter(Boolean).join(" ");
@@ -197,9 +197,91 @@ var RealtimeAvatar = class {
197
197
  await this.#json(await this.#request("PATCH", `/avatars/${avatarId}`, { json: patch }))
198
198
  );
199
199
  }
200
+ /**
201
+ * Re-shoot the character — swap in new footage as her resting loop.
202
+ *
203
+ * ASYNCHRONOUS, and that is the whole design. This returns as soon as the swap is
204
+ * accepted; the avatar keeps serving its CURRENT loop, cache and clips the entire time,
205
+ * and cuts over to the new generation in one step once the replacement is prepared.
206
+ * A call minted a second after this returns is a normal call on the old footage.
207
+ *
208
+ * Two consequences worth designing for:
209
+ *
210
+ * - **The clip library empties and refills.** Old takes are footage of the old source and
211
+ * cannot splice against the new loop, so they are dropped and re-rendered. Between the
212
+ * cutover and the last re-render she rests on the new loop with less variety — never
213
+ * broken, just plainer. Do not gate your UI on the library being full.
214
+ * - **A failed swap does not fail the avatar.** She keeps serving, `status` stays `ready`,
215
+ * and the reason lands on `error`. So poll `getAvatar` and read `error` — a non-null
216
+ * `error` on a `ready` avatar is the swap that did not take, not an unhealthy character.
217
+ *
218
+ * The frame this rests on comes from the new footage: pass `anchorTimeMs` when frame 0 of
219
+ * the take is mid-blink. Video-sourced avatars only — a portrait-anchored one is a 422.
220
+ */
221
+ async swapSource(avatarId, input) {
222
+ const json2 = { sourceAssetId: input.sourceAssetId };
223
+ if (input.anchorTimeMs !== void 0) json2.anchorTimeMs = input.anchorTimeMs;
224
+ return toAvatar(await this.#json(await this.#request("PATCH", `/avatars/${avatarId}`, { json: json2 })));
225
+ }
226
+ /**
227
+ * Re-point the anchor at a different frame of the loop she ALREADY has — same footage,
228
+ * different rest pose. `swapSource` replaces the footage; this only moves the frame.
229
+ *
230
+ * Re-renders the clip library the same way, with the same degradation window, and is
231
+ * clamped server-side to the loop's last extractable frame (read the avatar back to see
232
+ * what was actually cut). Video-sourced avatars only.
233
+ */
234
+ async retimeAnchor(avatarId, anchorTimeMs) {
235
+ return toAvatar(
236
+ await this.#json(await this.#request("PATCH", `/avatars/${avatarId}`, { json: { anchorTimeMs } }))
237
+ );
238
+ }
200
239
  async deleteAvatar(avatarId) {
201
240
  await this.#json(await this.#request("DELETE", `/avatars/${avatarId}`));
202
241
  }
242
+ // A clip envelope missing `revision` would silently drop `expectedRevision` from the
243
+ // next declare — CAS degrades to unconditional with zero signal — so it throws instead.
244
+ #clipEnvelope(out) {
245
+ if (typeof out.revision !== "number" || !Array.isArray(out.data)) {
246
+ throw new RealtimeAvatarError("clip library response did not match the contract");
247
+ }
248
+ return out;
249
+ }
250
+ /**
251
+ * Declare the avatar's full desired clip library — a declaration, not a delta. The
252
+ * platform reconciles it against what exists: unchanged clips are `kept` (still
253
+ * serving), new or changed ones are `queued` to render, and clips you dropped are
254
+ * `retired`. The 202 is acceptance, not readiness — poll `listClips` until no row is
255
+ * `queued` or `generating`. A rejected upload settles `failed`, which is terminal, so
256
+ * waiting for all-`ready` waits forever. While a re-render is in flight the previous
257
+ * take keeps serving, so a declaration never blanks a live avatar.
258
+ *
259
+ * `expectedRevision` is compare-and-set: pass the `revision` you last read and a
260
+ * concurrent writer surfaces as a 409 instead of a lost update. Omit it to declare
261
+ * unconditionally.
262
+ *
263
+ * At most 12 clips: one `idle`, up to two `listen`, the rest `gesture`. An uploaded
264
+ * clip (`source: { assetId }`) must start AND end on the avatar's rest pose — pose
265
+ * validation rejects it otherwise (`status: "failed"`, the verdict in `poseCheck`),
266
+ * and the rest of the library is untouched.
267
+ */
268
+ async setClipLibrary(avatarId, library) {
269
+ const body = { clips: library.clips };
270
+ if (library.expectedRevision !== void 0) body.expectedRevision = library.expectedRevision;
271
+ return this.#clipEnvelope(
272
+ await this.#json(
273
+ await this.#request("PUT", `/avatars/${avatarId}/clips`, { json: body })
274
+ )
275
+ );
276
+ }
277
+ /** The avatar's clip library: every non-retired clip, plus revision, anchor and eligibility. */
278
+ async listClips(avatarId) {
279
+ return this.#clipEnvelope(
280
+ await this.#json(
281
+ await this.#request("GET", `/avatars/${avatarId}/clips`)
282
+ )
283
+ );
284
+ }
203
285
  /**
204
286
  * Reconcile an avatar's clip set after it changes.
205
287
  *
@@ -210,6 +292,10 @@ var RealtimeAvatar = class {
210
292
  * **At most 32 URLs per call.** This is the whole set for the avatar, not a delta, and the
211
293
  * endpoint rejects an oversize list rather than truncating it — so a library that outgrows
212
294
  * 32 needs the set trimmed, not split across two calls.
295
+ *
296
+ * @deprecated The externally-hosted clip tier this serves is sunsetting. Declare the
297
+ * library with {@link setClipLibrary} instead — the platform renders and hosts the
298
+ * clips, and pose-validates uploads against the avatar's rest pose.
213
299
  */
214
300
  async syncClips(avatarId, clipUrls) {
215
301
  const out = await this.#json(
@@ -385,7 +471,11 @@ function toAvatar(raw) {
385
471
  displayName: String(a.displayName ?? ""),
386
472
  sourceKind: a.sourceKind === "video" ? "video" : "image",
387
473
  status: a.status ?? "draft",
388
- defaultVoiceId: a.defaultVoiceId ? String(a.defaultVoiceId) : null
474
+ defaultVoiceId: a.defaultVoiceId ? String(a.defaultVoiceId) : null,
475
+ sourceAssetId: a.sourceAssetId ? String(a.sourceAssetId) : null,
476
+ // Carried because it is the ONLY channel a failed source swap has: she stays `ready`
477
+ // and serving, and this says why the re-shoot did not take.
478
+ error: a.error ? String(a.error) : null
389
479
  };
390
480
  }
391
481
  function toAsset(raw) {
@@ -1,4 +1,4 @@
1
- import { C as CallMode, a as CallPolicy } from './types-C5zypiVY.js';
1
+ import { C as CallMode, a as CallPolicy } from './types-DLxz6uFE.js';
2
2
 
3
3
  /** The three things a caller can ask the proxy to do. Gate on these, not on URLs. */
4
4
  type ProxyOperation = "connect" | "end" | "avatars" | "credits";
@@ -132,6 +132,8 @@ interface components {
132
132
  cooldown_seconds?: number;
133
133
  /** @enum {string} */
134
134
  renderer?: "editor" | "generative";
135
+ /** @enum {string} */
136
+ objects?: "editor" | "generative";
135
137
  };
136
138
  };
137
139
  transcript_webhook?: {
@@ -142,6 +144,7 @@ interface components {
142
144
  client_metadata?: {
143
145
  [key: string]: string;
144
146
  };
147
+ fast_endpointing?: boolean;
145
148
  };
146
149
  LiveKitSessionGrant: {
147
150
  /**
@@ -311,6 +314,42 @@ interface components {
311
314
  stylePreset?: "cinematic-founder" | "editorial-companion" | "warm-anime" | "luxury-realism" | "soft-3d" | "noir-avatar";
312
315
  /** Format: uri */
313
316
  portraitUrl?: string;
317
+ sourceAssetId?: string;
318
+ anchorTimeMs?: number;
319
+ };
320
+ CreateApiKeyRequest: {
321
+ name: string;
322
+ /**
323
+ * @default test
324
+ * @enum {string}
325
+ */
326
+ environment: "live" | "test";
327
+ /**
328
+ * @default [
329
+ * "realtime:write",
330
+ * "credits:read",
331
+ * "avatars:read"
332
+ * ]
333
+ */
334
+ scopes: ("*" | "api_keys:write" | "credits:read" | "avatars:read" | "avatars:write" | "realtime:write" | "usage:read" | "usage:write")[];
335
+ spendLimitCreditMicros?: number | null;
336
+ expiresAt?: string | null;
337
+ };
338
+ CreateApiKeyResponse: {
339
+ id: string;
340
+ keyId: string;
341
+ tenantId: string;
342
+ name: string;
343
+ /** @enum {string} */
344
+ environment: "live" | "test";
345
+ redactedKey: string;
346
+ scopes: ("*" | "api_keys:write" | "credits:read" | "avatars:read" | "avatars:write" | "realtime:write" | "usage:read" | "usage:write")[];
347
+ /** @enum {string} */
348
+ status: "active" | "revoked" | "expired";
349
+ spendLimitCreditMicros: number | null;
350
+ createdAt: string;
351
+ expiresAt: string | null;
352
+ apiKey: string;
314
353
  };
315
354
  Asset: {
316
355
  id: string;
@@ -326,6 +365,9 @@ interface components {
326
365
  createdAt: string;
327
366
  updatedAt: string;
328
367
  };
368
+ AssetList: {
369
+ data: components["schemas"]["Asset"][];
370
+ };
329
371
  CreateRemoteAssetRequest: {
330
372
  /** @enum {string} */
331
373
  kind: "image" | "video" | "audio";
@@ -377,9 +419,111 @@ interface components {
377
419
  status: "queued" | "generating" | "ready" | "failed";
378
420
  url: string | null;
379
421
  whenHint: string | null;
422
+ /** @enum {string} */
423
+ source: "generated" | "uploaded";
424
+ motionPrompt: string | null;
425
+ durationSeconds: number | null;
426
+ anchorVersion: number;
427
+ poseCheck: {
428
+ verdict: {
429
+ sameSubject: boolean;
430
+ firstFrameMatchesAnchor: boolean;
431
+ lastFrameMatchesAnchor: boolean;
432
+ framingComparable: boolean;
433
+ confidence: number;
434
+ };
435
+ issues: string[];
436
+ firstFrameUrl: string | null;
437
+ lastFrameUrl: string | null;
438
+ trimStartMs: number | null;
439
+ trimEndMs: number | null;
440
+ } | null;
441
+ error: {
442
+ code: string;
443
+ message: string;
444
+ } | null;
445
+ createdAt: string;
446
+ updatedAt: string;
447
+ }[];
448
+ avatarId: string;
449
+ revision: number;
450
+ anchorVersion: number;
451
+ anchor: {
452
+ /** Format: uri */
453
+ url: string;
454
+ /** @enum {string} */
455
+ source: "portrait" | "source_frame";
456
+ timeMs: number | null;
457
+ } | null;
458
+ clipLibraryEligible: boolean;
459
+ };
460
+ PutAvatarClipsRequest: {
461
+ expectedRevision?: number;
462
+ clips: {
463
+ clipId: string;
464
+ /** @enum {string} */
465
+ role: "idle" | "listen" | "gesture";
466
+ whenHint?: string;
467
+ source: {
468
+ motionPrompt: string;
469
+ } | {
470
+ assetId: string;
471
+ };
472
+ durationSeconds?: number;
473
+ reroll?: boolean;
474
+ }[];
475
+ };
476
+ PutAvatarClipsResponse: {
477
+ data: {
478
+ clipId: string;
479
+ /** @enum {string} */
480
+ role: "idle" | "listen" | "gesture";
481
+ /** @enum {string} */
482
+ status: "queued" | "generating" | "ready" | "failed";
483
+ url: string | null;
484
+ whenHint: string | null;
485
+ /** @enum {string} */
486
+ source: "generated" | "uploaded";
487
+ motionPrompt: string | null;
488
+ durationSeconds: number | null;
489
+ anchorVersion: number;
490
+ poseCheck: {
491
+ verdict: {
492
+ sameSubject: boolean;
493
+ firstFrameMatchesAnchor: boolean;
494
+ lastFrameMatchesAnchor: boolean;
495
+ framingComparable: boolean;
496
+ confidence: number;
497
+ };
498
+ issues: string[];
499
+ firstFrameUrl: string | null;
500
+ lastFrameUrl: string | null;
501
+ trimStartMs: number | null;
502
+ trimEndMs: number | null;
503
+ } | null;
504
+ error: {
505
+ code: string;
506
+ message: string;
507
+ } | null;
380
508
  createdAt: string;
381
509
  updatedAt: string;
382
510
  }[];
511
+ avatarId: string;
512
+ revision: number;
513
+ anchorVersion: number;
514
+ anchor: {
515
+ /** Format: uri */
516
+ url: string;
517
+ /** @enum {string} */
518
+ source: "portrait" | "source_frame";
519
+ timeMs: number | null;
520
+ } | null;
521
+ clipLibraryEligible: boolean;
522
+ plan: {
523
+ kept: string[];
524
+ queued: string[];
525
+ retired: string[];
526
+ };
383
527
  };
384
528
  SyncAvatarClipsRequest: {
385
529
  clipUrls: string[];
@@ -392,10 +536,6 @@ interface components {
392
536
  OkResponse: {
393
537
  ok: boolean;
394
538
  };
395
- HealthResponse: {
396
- /** @enum {string} */
397
- status: "healthy";
398
- };
399
539
  };
400
540
  responses: never;
401
541
  parameters: never;
@@ -703,13 +843,25 @@ type Asset = Pick<Wire["Asset"], "id" | "kind"> & {
703
843
  */
704
844
  status: Wire["Asset"]["status"] | (string & {});
705
845
  };
706
- type Avatar = Pick<Wire["Avatar"], "id" | "displayName" | "sourceKind" | "status" | "defaultVoiceId">;
846
+ type Avatar = Pick<Wire["Avatar"], "id" | "displayName" | "sourceKind" | "status" | "defaultVoiceId" | "sourceAssetId" | "error">;
707
847
  /**
708
848
  * The patch `updateAvatar` sends — the two fields an integrator re-points after creation.
709
849
  * The contract's `UpdateAvatarRequest` carries more (llm, persona, art direction…); that is
710
850
  * dashboard machinery this surface deliberately does not model.
851
+ *
852
+ * `sourceAssetId` and `anchorTimeMs` are deliberately NOT here: the platform refuses them
853
+ * alongside any other field, so folding them in would let this type spell a request that can
854
+ * only ever 422. They get their own methods — `swapSource` and `retimeAnchor`.
711
855
  */
712
856
  type AvatarUpdate = Pick<Wire["UpdateAvatarRequest"], "displayName" | "defaultVoiceId">;
857
+ /**
858
+ * Re-shoot the character: `sourceAssetId` becomes the avatar's new resting loop, and the
859
+ * whole clip library re-renders against an anchor re-derived from that footage.
860
+ *
861
+ * `anchorTimeMs` picks the anchor frame OF THE NEW SOURCE (default 0) — frame 0 of a real
862
+ * take is sometimes mid-blink. It is the only field that may accompany a source swap.
863
+ */
864
+ type AvatarSourceSwap = Required<Pick<Wire["UpdateAvatarRequest"], "sourceAssetId">> & Pick<Wire["UpdateAvatarRequest"], "anchorTimeMs">;
713
865
  type UsageSessionsResponse = Wire["ListUsageSessionsResponse"];
714
866
  /**
715
867
  * One billable session — when it ran, how long it was billable for, what it cost.
@@ -746,8 +898,39 @@ interface ListSessionsOptions {
746
898
  cursor?: string;
747
899
  }
748
900
  type CreditBalance = Pick<Wire["CreditBalance"], "balanceCreditMicros" | "reservedCreditMicros">;
749
- /** Result of reconciling an avatar's clip set to the cache tier. */
901
+ /**
902
+ * Result of reconciling an avatar's clip set to the cache tier.
903
+ * @deprecated The externally-hosted clip tier is sunsetting. Declare the library with
904
+ * `setClipLibrary` instead — the platform renders and hosts the clips for you.
905
+ */
750
906
  type ClipSyncResult = Wire["SyncAvatarClipsResponse"];
907
+ /**
908
+ * One desired clip in a library declaration: a `clipId` you choose, a role, and exactly ONE
909
+ * source — a `motionPrompt` the platform renders from the avatar's rest-pose anchor, or the
910
+ * `assetId` of a video you uploaded. `durationSeconds` (4–8, default 5) applies to generated
911
+ * clips only; `reroll` is a write-only re-render nudge and is never echoed back by a read.
912
+ */
913
+ type ClipDeclaration = Wire["PutAvatarClipsRequest"]["clips"][number];
914
+ /** The two ways a declared clip gets its pixels. Exactly one — an object carrying both is rejected. */
915
+ type ClipSource = Wire["PutAvatarClipsRequest"]["clips"][number]["source"];
916
+ /**
917
+ * One clip row as the platform reports it. `status` is JOB state, not serveability: `url` is
918
+ * the SERVING asset and may stay non-null while a re-render is `generating` or has `failed` —
919
+ * the previous take keeps serving. An uploaded clip that fails pose validation carries the
920
+ * structured verdict in `poseCheck` and the reason in `error`.
921
+ */
922
+ type AvatarClip = Wire["ListAvatarClipsResponse"]["data"][number];
923
+ /**
924
+ * The library as `listClips` reports it: every non-retired clip plus the avatar-level facts —
925
+ * the declaration `revision` (0 = still creation-owned), the anchor generation, and the
926
+ * rest-pose `anchor` the clips splice against (null only while a video-sourced avatar's
927
+ * anchor frame has not been derived yet).
928
+ */
929
+ type ClipLibrary = Wire["ListAvatarClipsResponse"];
930
+ /** How a declaration reconciled against the previous library — by `clipId`. */
931
+ type ClipLibraryPlan = Wire["PutAvatarClipsResponse"]["plan"];
932
+ /** The accepted declaration's answer: the post-apply library plus how it reconciled. */
933
+ type ClipLibraryUpdate = Wire["PutAvatarClipsResponse"];
751
934
  /**
752
935
  * DERIVATION: hand-written, because the contract does not describe it.
753
936
  *
@@ -795,4 +978,4 @@ interface TranscriptPayload {
795
978
  client_metadata: Record<string, string>;
796
979
  }
797
980
 
798
- export { type Avatar as A, type CallMode as C, type EndCallOptions as E, type ListSessionsOptions as L, type StartCallResult as S, type TranscriptPayload as T, type UsageSessionPage as U, type VideoPolicy as V, type CallPolicy as a, type AvatarUpdate as b, type ClipSyncResult as c, type AssetKind as d, type Asset as e, type UsageSession as f, type CreditBalance as g, type CallConnection as h, type CallQueued as i, type ContextMessage as j, type EndCallReason as k, type VideoState as l, isQueued as m };
981
+ export { type Avatar as A, type CallMode as C, type EndCallOptions as E, type ListSessionsOptions as L, type StartCallResult as S, type TranscriptPayload as T, type UsageSessionPage as U, type VideoPolicy as V, type CallPolicy as a, type AvatarUpdate as b, type AvatarSourceSwap as c, type ClipDeclaration as d, type ClipLibraryUpdate as e, type ClipLibrary as f, type ClipSyncResult as g, type AssetKind as h, type Asset as i, type UsageSession as j, type CreditBalance as k, type AvatarClip as l, type CallConnection as m, type CallQueued as n, type ClipLibraryPlan as o, type ClipSource as p, type ContextMessage as q, type EndCallReason as r, type VideoState as s, isQueued as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "realtime-avatar",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Realtime Avatar SDK. `realtime-avatar` and the route adapters hold your API key and are server-only; `realtime-avatar/react`, `/browser` and `/tools` never can — importing a server entry into a browser build throws at bundle time.",
5
5
  "license": "MIT",
6
6
  "author": "The Influence Company",