realtime-avatar 0.15.0 → 0.16.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/README.md CHANGED
@@ -4,7 +4,7 @@ A live character your users can talk to — voice, or voice and video. She liste
4
4
  speaks, so you can interrupt her mid-sentence and she stops, the way a person stops.
5
5
 
6
6
  ```bash
7
- npm install --save-exact realtime-avatar@0.15.0
7
+ npm install --save-exact realtime-avatar@0.16.0
8
8
  ```
9
9
 
10
10
  ```ts
@@ -25,6 +25,61 @@ return call.raw; // relay to the browser byte-for-byte
25
25
 
26
26
  That is the whole server half. The client joins with the payload and renders her.
27
27
 
28
+ ## Optional call recordings
29
+
30
+ Your server decides whether to record after your application obtains consent:
31
+
32
+ ```ts
33
+ const call = await rta.startCall({ avatarId, recording: "audio_video" });
34
+ if (!isQueued(call) && call.recording) {
35
+ await saveRecordingId(call.sessionId, call.recording.recordingId);
36
+ }
37
+
38
+ // Later, in your authenticated admin backend:
39
+ const recording = await rta.getRecording(recordingId);
40
+ if (recording.status === "ready") {
41
+ const { url, expiresAt } = await rta.getRecordingAccess(recordingId);
42
+ // Return this short-lived access to the authorized viewer.
43
+ }
44
+ ```
45
+
46
+ Omitted or `"off"` disables recording. `"audio"` records published user and avatar audio;
47
+ `"video"` records published video without audio; `"audio_video"` keeps both on one media timeline.
48
+ Recording does not enable the microphone, camera, or screen sharing. Only tracks that participants
49
+ authorize and publish can be recorded. Camera controls remain a future SDK feature.
50
+
51
+ Recordings may finish processing after a call ends. Use `listRecordings({ sessionId })` to find
52
+ them, or refresh `getRecording(recordingId)` while processing. These methods and
53
+ `getRecordingAccess` require a server key with `recordings:read`.
54
+
55
+ Save `recordingId`, not a playback URL. Files are retained until `retainedUntil` (30 days by
56
+ default); each URL expires at `expiresAt` (up to one hour, capped by retention). Obtain fresh
57
+ access before replaying or seeking after expiry. An expired URL does not delete the file.
58
+ Treat the URL as private: anyone who has it can play that file until it expires.
59
+
60
+ Transcript delivery remains the signed `transcript` webhook configured on `startCall`. Join the
61
+ transcript and recordings by `sessionId`; keep your own script revision with that call. Transcript
62
+ timestamps describe conversation turns and do not by themselves establish frame-accurate media
63
+ alignment for lip-sync analysis.
64
+
65
+ Client-safe Zod schemas and derived types are available from `realtime-avatar/recording`.
66
+ They are the same executable contract used by the service, verified against the published digest.
67
+
68
+ ```mermaid
69
+ ---
70
+ title: Recording ownership and private playback
71
+ ---
72
+ flowchart LR
73
+ App[Application server: consent and recording policy] --> RTA[RTA: call and recording lifecycle]
74
+ RTA --> Media[Media provider: record published tracks]
75
+ Media --> Storage[Private media storage]
76
+ RTA --> Metadata[Recording ID and session ID]
77
+ Admin[Authorized admin backend] --> RTA
78
+ RTA --> Access[Temporary playback URL]
79
+ Access --> Player[Video or audio player]
80
+ Storage --> Player
81
+ ```
82
+
28
83
  New here? The [quickstart](https://realtimeavatar.ai/docs/quickstart) goes from an API key to a
29
84
  working call, and a [sandbox key](https://realtimeavatar.ai/signup) is free with no card. Mount
30
85
  the server half on your framework:
@@ -82,9 +137,14 @@ all), and the `video` policy types are deliberately not one-to-one with the wire
82
137
 
83
138
  ```ts
84
139
  // calls
85
- rta.startCall({ avatarId, mode?, instructions?, context?, maxSeconds?, video?, transcript?, metadata? })
140
+ rta.startCall({ avatarId, mode?, instructions?, context?, maxSeconds?, video?, recording?, transcript?, metadata? })
86
141
  rta.endCall(sessionId, { reason? }) // free an abandoned call's slot; idempotent, never throws
87
142
 
143
+ // optional recordings; server only, requires recordings:read
144
+ rta.listRecordings({ sessionId, limit?, cursor? })
145
+ rta.getRecording(recordingId)
146
+ rta.getRecordingAccess(recordingId)
147
+
88
148
  // avatars
89
149
  rta.createAvatarFromImage({ displayName, imageUrl, motionPrompt?, voice? }) // the only lane
90
150
  rta.createAvatarFromVideo({ displayName, videoUrl, voice? }) // DEPRECATED — closed, 422
@@ -141,6 +201,7 @@ Browser — these never can:
141
201
  | `realtime-avatar/react-native` | The same surface for Expo / React Native |
142
202
  | `realtime-avatar/browser` | `enableMicrophone`, `attachRemoteAudio`, `prepareAvatarRoom` — no React |
143
203
  | `realtime-avatar/tools` | `attachAvatarTools` — the browser tool plane |
204
+ | `realtime-avatar/recording` | Client-safe Zod recording schemas and derived types |
144
205
 
145
206
  Every adapter takes the same two hooks: `authorize` gates the request, `session` decides the
146
207
  call. Policy — `instructions`, `maxSeconds`, `voice`, `video` — is decided in `session`, on
@@ -1,4 +1,4 @@
1
- import { RealtimeAvatar, isQueued, RealtimeAvatarHttpError } from './chunk-PKKTFUA7.js';
1
+ import { RealtimeAvatar, isQueued, RealtimeAvatarHttpError } from './chunk-V26PE2M3.js';
2
2
 
3
3
  // ../proxy/src/config.ts
4
4
  var json = (body, status = 200) => new Response(JSON.stringify(body), {
@@ -214,6 +214,44 @@ var schema26 = z.strictObject({
214
214
  });
215
215
  var clipLibraryResponseSchema = schema24;
216
216
  var clipLibraryUpdateSchema = schema26;
217
+ var RECORDED_MEDIA_MODES = ["audio", "video", "audio_video"];
218
+ var recordingModeSchema = z.enum(["off", ...RECORDED_MEDIA_MODES]).describe("Server-owned recording policy; omitted means off. Audio includes user and avatar audio.");
219
+ var RECORDING_STATUSES = ["pending", "recording", "processing", "ready", "failed", "expired"];
220
+ z.enum(RECORDING_STATUSES);
221
+ var recordingMetadataSchema = z.object({
222
+ sessionId: z.string().min(1),
223
+ recordingId: z.string().min(1),
224
+ mode: recordingModeSchema.exclude(["off"]),
225
+ createdAt: z.string().datetime({ offset: true }),
226
+ retainedUntil: z.string().datetime({ offset: true })
227
+ });
228
+ var recordingArtifactSchema = z.discriminatedUnion("status", [
229
+ recordingMetadataSchema.extend({ status: z.enum(["pending", "recording", "processing", "expired"]) }).strict(),
230
+ recordingMetadataSchema.extend({
231
+ status: z.literal("ready"),
232
+ mediaType: z.enum(["audio/mp4", "video/mp4"]),
233
+ sizeBytes: z.number().int().positive(),
234
+ durationMs: z.number().int().nonnegative().nullable()
235
+ }).strict(),
236
+ recordingMetadataSchema.extend({
237
+ status: z.literal("failed"),
238
+ errorCode: z.enum(["recording_failed", "recording_unavailable"])
239
+ }).strict()
240
+ ]);
241
+ var listRecordingsQuerySchema = z.object({
242
+ sessionId: z.string().min(1).optional(),
243
+ limit: z.coerce.number().int().min(1).max(100).default(50),
244
+ cursor: z.string().min(1).optional()
245
+ }).strict();
246
+ var listRecordingsResponseSchema = z.object({
247
+ data: z.array(recordingArtifactSchema),
248
+ nextCursor: z.string().nullable()
249
+ }).strict();
250
+ var recordingAccessResponseSchema = z.object({
251
+ recordingId: z.string().min(1),
252
+ url: z.string().url(),
253
+ expiresAt: z.string().datetime({ offset: true })
254
+ }).strict();
217
255
 
218
256
  // ../http-client/src/retry.ts
219
257
  var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 500, 502, 503, 504]);
@@ -233,10 +271,332 @@ function isTransient(cause) {
233
271
  const name = cause?.name;
234
272
  return name === "TimeoutError" || name === "TypeError" || name === "FetchError";
235
273
  }
274
+ var DEFAULT_AVATAR_ID = "maria";
275
+ var DEFAULT_BACKGROUND_ID = "plain_white";
276
+ var sessionModeSchema = z.enum(["avatar", "voice"]);
277
+ var DEFAULT_SESSION_MODE = "avatar";
278
+ var DEFAULT_MAX_SESSION_SECONDS = 1800;
279
+ var avatarSourceKindSchema = z.enum(["portrait", "source_video"]);
280
+ var liveKitSttModeSchema = z.enum(["server", "off"]);
281
+ var renderBackendSchema = z.string();
282
+ var sessionLiveEditSchema = z.object({
283
+ rules: z.string().min(1).max(2e3),
284
+ cooldown_seconds: z.number().int().min(5).max(600).optional(),
285
+ // Which machinery runs the re-edit. Absent ⇒ the server default ("editor"). A deploy
286
+ // that cannot provide the requested renderer serves the editor lane and logs it,
287
+ // rather than failing a call that would otherwise have connected fine.
288
+ renderer: z.enum(["editor", "generative"]).optional()
289
+ }).strict();
290
+ var sessionSupportEditsSchema = z.object({
291
+ instruction: z.string().min(1).max(1e3),
292
+ reference_url: z.string().url().optional(),
293
+ live_edit: sessionLiveEditSchema.optional()
294
+ }).strict();
295
+ var LLM_PROVIDERS = ["local", "gemini", "openai"];
296
+ var llmProviderSchema = z.enum(LLM_PROVIDERS);
297
+ var llmConfigSchema = z.object({
298
+ backend: llmProviderSchema.optional(),
299
+ model: z.string().max(200).nullable().optional()
300
+ }).strict();
301
+ var llmSelectionSchema = z.object({
302
+ provider: llmProviderSchema,
303
+ model: z.string().max(200).nullable().optional()
304
+ }).strict();
305
+ var liveKitInitialContextMessageSchema = z.object({
306
+ role: z.enum(["system", "user", "assistant"]),
307
+ content: z.string().min(1).max(4e3)
308
+ }).strict();
309
+ var CARTESIA_TTS_MODELS = [
310
+ "cartesia/sonic-2",
311
+ "cartesia/sonic-2-latest",
312
+ "cartesia/sonic-3",
313
+ "cartesia/sonic-3-latest",
314
+ "cartesia/sonic-turbo",
315
+ "cartesia/sonic-turbo-latest"
316
+ ];
317
+ var cartesiaTtsModelSchema = z.enum(CARTESIA_TTS_MODELS);
318
+ var cartesiaVoiceSpecSchema = z.object({
319
+ provider: z.literal("cartesia"),
320
+ model: cartesiaTtsModelSchema.default("cartesia/sonic-3"),
321
+ voice_id: z.string().min(1).max(120),
322
+ speed: z.number().min(0.5).max(2).nullable().optional(),
323
+ emotion: z.string().min(1).max(80).nullable().optional(),
324
+ language: z.string().min(2).max(16).nullable().optional()
325
+ }).strict();
326
+ var FISH_TTS_MODELS = ["speech-1.6", "s1", "s2-pro", "speech-1.5", "s1-mini"];
327
+ var fishTtsModelSchema = z.enum(FISH_TTS_MODELS);
328
+ var breezeVoiceSpecSchema = z.object({
329
+ provider: z.literal("breezeblue"),
330
+ model: z.string().min(1).max(80).default("bluebell-v1-en"),
331
+ voice_id: z.string().min(1).max(120),
332
+ guidance_scale: z.number().min(1).max(10).nullable().optional(),
333
+ instructions: z.string().min(1).max(1e3).nullable().optional(),
334
+ language: z.string().min(2).max(16).nullable().optional()
335
+ }).strict();
336
+ var fishVoiceSpecSchema = z.object({
337
+ provider: z.literal("fish"),
338
+ model: fishTtsModelSchema.default("speech-1.6"),
339
+ voice_id: z.string().min(1).max(120),
340
+ speed: z.number().min(0.5).max(2).nullable().optional(),
341
+ emotion: z.string().min(1).max(80).nullable().optional(),
342
+ language: z.string().min(2).max(16).nullable().optional()
343
+ }).strict();
344
+ var voiceSpecSchema = z.discriminatedUnion("provider", [
345
+ cartesiaVoiceSpecSchema,
346
+ breezeVoiceSpecSchema,
347
+ fishVoiceSpecSchema
348
+ ]);
349
+ var nullableUrlSchema = z.string().url().nullable();
350
+ var clipTriggerSchema = z.enum([
351
+ "idle",
352
+ "listen",
353
+ "think",
354
+ "directive"
355
+ ]);
356
+ var sessionClipSchema = z.object({
357
+ clip_id: z.string().regex(/^[A-Za-z0-9_-]{1,64}$/, "clip_id must be a slug").refine((id) => id !== "primary", "'primary' is reserved for the avatar's source video"),
358
+ source_video_url: z.string().url().optional(),
359
+ video_cache_id: z.string().min(8).max(160).optional(),
360
+ max_seconds: z.number().min(1).max(10).optional(),
361
+ trigger: clipTriggerSchema.optional(),
362
+ loop: z.boolean().optional(),
363
+ weight: z.number().min(0).max(100).optional(),
364
+ crossfade_ms: z.number().int().min(0).max(1e3).optional(),
365
+ trim_start_ms: z.number().int().min(0).max(2e3).optional(),
366
+ trim_end_ms: z.number().int().min(0).max(2e3).optional(),
367
+ // The cue the character reads to decide this clip. `when` is the public name and the
368
+ // one the docs use; `hint` is the name the wire first shipped under and still accepts.
369
+ // Both are listed because this object is `.strict()` — omitting `when` would make the
370
+ // public name a validation error. Send one, never both.
371
+ when: z.string().min(1).max(120).optional(),
372
+ hint: z.string().min(1).max(120).optional()
373
+ }).strict().refine((clip) => clip.source_video_url || clip.video_cache_id, {
374
+ message: "a clip needs source_video_url or video_cache_id"
375
+ }).refine((clip) => !(clip.when && clip.hint), {
376
+ message: "set `when` or `hint`, not both \u2014 they are the same field",
377
+ path: ["when"]
378
+ });
379
+ var sessionChoreographySchema = z.object({
380
+ idle_dwell_min_seconds: z.number().min(1).max(60).optional(),
381
+ idle_dwell_max_seconds: z.number().min(1).max(120).optional(),
382
+ special_weight: z.number().min(0).max(100).optional(),
383
+ start_grace_seconds: z.number().min(0).max(60).optional(),
384
+ crossfade_ms: z.number().int().min(0).max(1e3).optional(),
385
+ crossfade_easing: z.enum(["linear", "smooth", "ease_out"]).optional(),
386
+ wrap_crossfade_ms: z.number().int().min(0).max(1e3).optional()
387
+ }).strict().refine(
388
+ (c) => c.idle_dwell_min_seconds === void 0 || c.idle_dwell_max_seconds === void 0 || c.idle_dwell_min_seconds <= c.idle_dwell_max_seconds,
389
+ { message: "idle_dwell_min_seconds must be <= idle_dwell_max_seconds" }
390
+ );
391
+ var sessionBehaviorSchema = z.object({
392
+ gestures_enabled: z.boolean().optional()
393
+ }).strict();
394
+ var transcriptWebhookSchema = z.object({
395
+ url: z.string().url().max(500),
396
+ secret: z.string().min(16).max(200)
397
+ }).strict();
398
+ var clientMetadataSchema = z.record(z.string().min(1).max(64), z.string().max(200)).refine((value) => Object.keys(value).length <= 16, {
399
+ message: "client_metadata carries at most 16 entries"
400
+ });
401
+ var MAX_SESSION_INSTRUCTIONS_CHARS = 8e3;
402
+ z.object({
403
+ avatar_id: z.string().min(1).max(160).default(DEFAULT_AVATAR_ID),
404
+ background_id: z.string().min(1).max(160).default(DEFAULT_BACKGROUND_ID),
405
+ mode: sessionModeSchema.default(DEFAULT_SESSION_MODE),
406
+ create_room: z.boolean().default(true),
407
+ dispatch_agent: z.boolean().default(true),
408
+ instructions: z.string().min(1).max(MAX_SESSION_INSTRUCTIONS_CHARS).optional(),
409
+ initial_context: z.array(liveKitInitialContextMessageSchema).max(32).default([]),
410
+ initial_say: z.string().min(1).max(1e3).optional(),
411
+ llm: llmConfigSchema.nullable().optional(),
412
+ max_session_seconds: z.number().int().min(1).max(DEFAULT_MAX_SESSION_SECONDS).optional(),
413
+ participant_identity: z.string().min(1).max(160).optional(),
414
+ participant_name: z.string().max(160).optional(),
415
+ queue_ticket_id: z.string().min(1).max(160).optional(),
416
+ portrait_url: nullableUrlSchema.optional(),
417
+ room_name: z.string().min(1).max(160).optional(),
418
+ source_kind: avatarSourceKindSchema.default("portrait"),
419
+ source_video_url: nullableUrlSchema.optional(),
420
+ stt_mode: liveKitSttModeSchema.default("server"),
421
+ video_cache_id: z.string().min(1).max(240).nullable().optional(),
422
+ voice: voiceSpecSchema.nullable().optional(),
423
+ voice_id: z.string().min(1).max(240).nullable().optional(),
424
+ // Deliberately UNBOUNDED. This object is `.strict()`, so it rejects rather than trims:
425
+ // a count cap here does not mean "use fewer clips", it means "there is no call". How
426
+ // many a session actually warms is decided where the clips are loaded, and loading
427
+ // fewer is always safe — so the wire must not hold a number too.
428
+ clip_library: z.array(sessionClipSchema).optional(),
429
+ choreography: sessionChoreographySchema.optional(),
430
+ behavior: sessionBehaviorSchema.optional(),
431
+ expression_profile: z.string().min(1).max(40).optional(),
432
+ render_backend: renderBackendSchema.optional(),
433
+ support_edits: sessionSupportEditsSchema.optional(),
434
+ transcript_webhook: transcriptWebhookSchema.optional(),
435
+ client_metadata: clientMetadataSchema.optional()
436
+ }).strict().superRefine((value, ctx) => {
437
+ if (value.support_edits && value.render_backend === "generative") {
438
+ ctx.addIssue({
439
+ code: "custom",
440
+ message: "support_edits needs a source video to edit; it cannot be combined with render_backend='generative'",
441
+ path: ["support_edits"]
442
+ });
443
+ }
444
+ if (value.support_edits && value.mode === "voice") {
445
+ ctx.addIssue({
446
+ code: "custom",
447
+ message: "support_edits needs a video session; it cannot be combined with mode='voice'",
448
+ path: ["support_edits"]
449
+ });
450
+ }
451
+ if (value.source_kind === "portrait") {
452
+ if (value.source_video_url || value.video_cache_id) {
453
+ ctx.addIssue({
454
+ code: "custom",
455
+ message: "source_video_url/video_cache_id require source_kind='source_video'",
456
+ path: ["source_kind"]
457
+ });
458
+ }
459
+ return;
460
+ }
461
+ if (value.portrait_url) {
462
+ ctx.addIssue({
463
+ code: "custom",
464
+ message: "portrait_url cannot be combined with source_kind='source_video'",
465
+ path: ["portrait_url"]
466
+ });
467
+ }
468
+ if (!value.source_video_url && !value.video_cache_id) {
469
+ ctx.addIssue({
470
+ code: "custom",
471
+ message: "source_kind='source_video' requires source_video_url or video_cache_id",
472
+ path: ["source_video_url"]
473
+ });
474
+ }
475
+ });
476
+ z.object({
477
+ avatarId: z.string().min(1).max(160),
478
+ backgroundId: z.string().min(1).max(160).default(DEFAULT_BACKGROUND_ID),
479
+ mode: sessionModeSchema.default(DEFAULT_SESSION_MODE),
480
+ createRoom: z.boolean().default(true),
481
+ dispatchAgent: z.boolean().default(true),
482
+ instructions: z.string().min(1).max(MAX_SESSION_INSTRUCTIONS_CHARS).optional(),
483
+ initialContext: z.array(liveKitInitialContextMessageSchema).max(32).default([]),
484
+ initialSay: z.string().min(1).max(1e3).optional(),
485
+ llm: llmSelectionSchema.nullable().optional(),
486
+ maxSessionSeconds: z.number().int().min(1).max(DEFAULT_MAX_SESSION_SECONDS).optional(),
487
+ participantIdentity: z.string().min(1).max(160).optional(),
488
+ participantName: z.string().max(160).optional(),
489
+ queueTicketId: z.string().min(1).max(160).optional(),
490
+ roomName: z.string().min(1).max(160).optional(),
491
+ sttMode: liveKitSttModeSchema.default("server"),
492
+ voice: voiceSpecSchema.nullable().optional(),
493
+ voiceId: z.string().min(1).max(240).nullable().optional(),
494
+ // Unbounded, for the same reason as `clip_library` on the wire schema above.
495
+ clipLibrary: z.array(sessionClipSchema).optional(),
496
+ behavior: sessionBehaviorSchema.optional(),
497
+ renderBackend: renderBackendSchema.optional(),
498
+ supportEdits: sessionSupportEditsSchema.optional(),
499
+ transcriptWebhook: transcriptWebhookSchema.optional(),
500
+ clientMetadata: clientMetadataSchema.optional()
501
+ }).strict();
502
+ var liveKitSessionGrantSchema = z.object({
503
+ recording: recordingArtifactSchema.optional(),
504
+ status: z.literal("ready").default("ready"),
505
+ session_id: z.string().min(1),
506
+ room_name: z.string().min(1),
507
+ livekit_url: z.string().min(1),
508
+ participant_token: z.string().min(1),
509
+ participant_identity: z.string().min(1),
510
+ reservation_expires_at: z.string().datetime({ offset: true }),
511
+ stt_mode: liveKitSttModeSchema.default("server"),
512
+ room_created: z.boolean().default(false),
513
+ dispatch_created: z.boolean().default(false),
514
+ join_timeout_seconds: z.number().int().nonnegative().default(0),
515
+ idle_timeout_seconds: z.number().int().nonnegative().default(0),
516
+ max_session_seconds: z.number().int().nonnegative().default(0)
517
+ }).passthrough();
518
+ var sessionEndReasonSchema = z.enum([
519
+ "user_ended",
520
+ "session_cap",
521
+ "idle",
522
+ "disconnected",
523
+ "out_of_credits",
524
+ "agent_ended",
525
+ "failed"
526
+ ]);
527
+ var approachingEndReasonSchema = z.enum(["session_cap", "idle"]);
528
+ var sessionClockFrameSchema = z.object({
529
+ kind: z.literal("session_clock"),
530
+ started_at_unix_ms: z.number().int().nonnegative(),
531
+ max_session_seconds: z.number().int().nonnegative(),
532
+ idle_timeout_seconds: z.number().int().nonnegative()
533
+ }).strict();
534
+ var endingFrameSchema = z.object({ kind: z.literal("ending"), reason: approachingEndReasonSchema }).strict();
535
+ var closingTurnDoneFrameSchema = z.object({ kind: z.literal("closing_turn_done"), turn_id: z.string().min(1) }).strict();
536
+ var endedFrameSchema = z.object({ kind: z.literal("ended"), reason: sessionEndReasonSchema }).strict();
537
+ var behaviorStateFrameSchema = z.object({
538
+ kind: z.literal("behavior_state"),
539
+ state: z.string().min(1).max(32),
540
+ clip_id: z.string().min(1).max(64).optional(),
541
+ trigger: clipTriggerSchema.optional(),
542
+ loop: z.boolean().optional(),
543
+ prev_clip_id: z.string().min(1).max(64).optional()
544
+ }).strip();
545
+ var clipAckFrameSchema = z.object({
546
+ kind: z.literal("clip_ack"),
547
+ request_id: z.string().max(64),
548
+ accepted: z.boolean(),
549
+ reason: z.string().max(64)
550
+ }).strip();
551
+ z.discriminatedUnion("kind", [
552
+ sessionClockFrameSchema,
553
+ endingFrameSchema,
554
+ closingTurnDoneFrameSchema,
555
+ endedFrameSchema,
556
+ behaviorStateFrameSchema,
557
+ clipAckFrameSchema
558
+ ]);
559
+ var liveKitCapacitySnapshotSchema = z.object({
560
+ // Placement identity + per-worker session ceiling. These are always present on the
561
+ // wire (the platform serializes them and they come back on the grant, so a customer
562
+ // already sees them) but are typed OPTIONAL here on purpose: this SDK is the defensive
563
+ // READER of that wire, so a consumer must tolerate a response variant that omits them
564
+ // rather than hard-fail parse. `max_sessions_per_gpu` is how many sessions one waking
565
+ // worker serves — a queue-depth estimate input a consumer reads off the busy response.
566
+ capacity_pool: z.string().min(1).optional(),
567
+ agent_name: z.string().min(1).optional(),
568
+ max_sessions: z.number().int().nonnegative(),
569
+ max_sessions_per_gpu: z.number().int().positive().optional(),
570
+ worker_count: z.number().int().nonnegative(),
571
+ active_sessions: z.number().int().nonnegative(),
572
+ reserved_sessions: z.number().int().nonnegative(),
573
+ observed_worker_active_sessions: z.number().int().nonnegative(),
574
+ available_sessions: z.number().int().nonnegative(),
575
+ queue_size: z.number().int().nonnegative(),
576
+ admission_open: z.boolean(),
577
+ recommended_retry_ms: z.number().int().nonnegative(),
578
+ load: z.number().min(0).max(1)
579
+ }).passthrough();
580
+ z.object({
581
+ message: z.string().min(1),
582
+ capacity: liveKitCapacitySnapshotSchema,
583
+ queue_size: z.number().int().nonnegative(),
584
+ queue_ticket_id: z.string().min(1).optional(),
585
+ queue_position: z.number().int().positive().optional(),
586
+ recommended_retry_ms: z.number().int().nonnegative()
587
+ }).strict();
588
+ z.enum([
589
+ "page_hide",
590
+ "disconnected",
591
+ "superseded",
592
+ "unmount",
593
+ "manual",
594
+ "idle_timeout"
595
+ ]);
236
596
 
237
597
  // ../http-client/src/client.ts
238
598
  var DEFAULT_BASE_URL = "https://realtimeavatar.ai/api/v1";
239
- var SDK_VERSION = "0.15.0";
599
+ var SDK_VERSION = "0.16.0";
240
600
  var RealtimeAvatar = class {
241
601
  #apiKey;
242
602
  #baseUrl;
@@ -281,6 +641,7 @@ var RealtimeAvatar = class {
281
641
  if (options.maxSeconds !== void 0) body.max_session_seconds = Math.floor(options.maxSeconds);
282
642
  if (options.voice !== void 0) body.voice = options.voice;
283
643
  if (options.metadata !== void 0) body.client_metadata = options.metadata;
644
+ if (options.recording !== void 0) body.recording = recordingModeSchema.parse(options.recording);
284
645
  if (options.clientTools) body.capabilities = ["client_tools"];
285
646
  if (options.transcript !== void 0) {
286
647
  body.transcript_webhook = { url: options.transcript.url, secret: options.transcript.secret };
@@ -299,20 +660,23 @@ var RealtimeAvatar = class {
299
660
  };
300
661
  }
301
662
  }
302
- const grant = await this.#json(response);
663
+ const raw = await this.#json(response);
664
+ const grant = liveKitSessionGrantSchema.parse(raw);
665
+ if (!isRecord(raw)) throw new RealtimeAvatarError("Invalid session grant");
303
666
  return {
304
667
  status: "ready",
305
- sessionId: String(grant.session_id),
306
- roomName: String(grant.room_name),
307
- livekitUrl: String(grant.livekit_url),
308
- participantToken: String(grant.participant_token),
309
- participantIdentity: String(grant.participant_identity),
310
- maxSessionSeconds: Number(grant.max_session_seconds ?? 0),
311
- idleTimeoutSeconds: Number(grant.idle_timeout_seconds ?? 0),
312
- reservationExpiresAt: String(grant.reservation_expires_at),
668
+ ...grant.recording === void 0 ? {} : { recording: grant.recording },
669
+ sessionId: grant.session_id,
670
+ roomName: grant.room_name,
671
+ livekitUrl: grant.livekit_url,
672
+ participantToken: grant.participant_token,
673
+ participantIdentity: grant.participant_identity,
674
+ maxSessionSeconds: grant.max_session_seconds,
675
+ idleTimeoutSeconds: grant.idle_timeout_seconds,
676
+ reservationExpiresAt: grant.reservation_expires_at,
313
677
  // The parsed fields above are for YOUR logic. Relay `raw` to the client untouched:
314
678
  // the browser SDK validates the grant strictly and rejects an added or renamed key.
315
- raw: grant
679
+ raw
316
680
  };
317
681
  }
318
682
  /**
@@ -632,6 +996,24 @@ var RealtimeAvatar = class {
632
996
  cursor = page.nextCursor ?? void 0;
633
997
  } while (cursor);
634
998
  }
999
+ /** Recording metadata for this account, optionally limited to one call. Requires recordings:read. */
1000
+ async listRecordings(options = {}) {
1001
+ const parsed = listRecordingsQuerySchema.parse(options);
1002
+ const query = new URLSearchParams();
1003
+ if (parsed.sessionId !== void 0) query.set("sessionId", parsed.sessionId);
1004
+ if (options.limit !== void 0) query.set("limit", String(parsed.limit));
1005
+ if (parsed.cursor !== void 0) query.set("cursor", parsed.cursor);
1006
+ const suffix = query.size ? `?${query}` : "";
1007
+ return listRecordingsResponseSchema.parse(await this.#json(await this.#request("GET", `/recordings${suffix}`)));
1008
+ }
1009
+ /** Current recording status. Final media can become ready after the call has ended. */
1010
+ async getRecording(recordingId) {
1011
+ return recordingArtifactSchema.parse(await this.#json(await this.#request("GET", `/recordings/${encodeURIComponent(recordingId)}`)));
1012
+ }
1013
+ /** Renewable playback access. Keep recordingId in storage; URLs expire at expiresAt. */
1014
+ async getRecordingAccess(recordingId) {
1015
+ return recordingAccessResponseSchema.parse(await this.#json(await this.#request("GET", `/recordings/${encodeURIComponent(recordingId)}/access`)));
1016
+ }
635
1017
  async creditBalance() {
636
1018
  return await this.#json(await this.#request("GET", "/credits/balance"));
637
1019
  }
@@ -811,4 +1193,4 @@ function isQueued(result) {
811
1193
  return "queued" in result;
812
1194
  }
813
1195
 
814
- export { RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, clipLibraryDeclarationSchema, isQueued, verifyTranscript };
1196
+ export { RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, clipLibraryDeclarationSchema, isQueued, listRecordingsResponseSchema, recordingAccessResponseSchema, recordingArtifactSchema, recordingModeSchema, verifyTranscript };
package/dist/express.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { P as ProxyConfig } from './types-CJcTowDB.js';
2
- import './types-B9GTrpx0.js';
1
+ import { P as ProxyConfig } from './types-Bphj69NC.js';
2
+ import './types-VOv0vsFs.js';
3
+ import 'zod';
3
4
 
4
5
  type Expressish = {
5
6
  method: string;
package/dist/express.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createProxyHandler } from './chunk-LZMJJX7C.js';
2
- import './chunk-PKKTFUA7.js';
1
+ import { createProxyHandler } from './chunk-2TNFET3Q.js';
2
+ import './chunk-V26PE2M3.js';
3
3
 
4
4
  // ../proxy/src/express.ts
5
5
  function realtimeAvatarExpress(config) {
package/dist/hono.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { P as ProxyConfig } from './types-CJcTowDB.js';
2
- import './types-B9GTrpx0.js';
1
+ import { P as ProxyConfig } from './types-Bphj69NC.js';
2
+ import './types-VOv0vsFs.js';
3
+ import 'zod';
3
4
 
4
5
  /**
5
6
  * Hono (and anything else built on Fetch handlers — Workers, Bun, Deno).
package/dist/hono.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createProxyHandler } from './chunk-LZMJJX7C.js';
2
- import './chunk-PKKTFUA7.js';
1
+ import { createProxyHandler } from './chunk-2TNFET3Q.js';
2
+ import './chunk-V26PE2M3.js';
3
3
 
4
4
  // ../proxy/src/hono.ts
5
5
  function realtimeAvatarHono(config) {
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 AvatarSourceSwap, d as ClipLibraryDeclaration, e as ClipLibraryUpdate, L as LoopRedirect, f as ClipLibrary, g as AssetKind, h as Asset, i as ListSessionsOptions, U as UsageSessionPage, j as UsageSession, k as CreditBalance, T as TranscriptPayload } from './types-B9GTrpx0.js';
2
- export { l as AvatarClip, m as CallConnection, n as CallQueued, o as ClipDeclaration, p as ClipLibraryPlan, q as ClipSource, r as ContextMessage, s as EndCallReason, V as VideoPolicy, t as isQueued } from './types-B9GTrpx0.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 ClipLibraryDeclaration, e as ClipLibraryUpdate, L as LoopRedirect, f as ClipLibrary, g as AssetKind, h as Asset, i as ListSessionsOptions, U as UsageSessionPage, j as UsageSession, k as ListRecordingsQuery, l as ListRecordingsResponse, R as RecordingArtifact, m as RecordingAccessResponse, n as CreditBalance, T as TranscriptPayload } from './types-VOv0vsFs.js';
2
+ export { o as AvatarClip, p as CallConnection, q as CallQueued, r as ClipDeclaration, s as ClipLibraryPlan, t as ClipSource, u as ContextMessage, v as EndCallReason, w as RecordingArtifactStatus, x as RecordingMode, V as VideoPolicy, y as isQueued, z as listRecordingsResponseSchema, B as recordingAccessResponseSchema, D as recordingArtifactSchema, F as recordingModeSchema } from './types-VOv0vsFs.js';
3
3
  import { z } from 'zod';
4
4
 
5
5
  interface RealtimeAvatarOptions {
@@ -265,6 +265,12 @@ declare class RealtimeAvatar {
265
265
  * caller writing a monthly report should not have to hold all of them to start writing.
266
266
  */
267
267
  iterateSessions(options?: ListSessionsOptions): AsyncGenerator<UsageSession>;
268
+ /** Recording metadata for this account, optionally limited to one call. Requires recordings:read. */
269
+ listRecordings(options?: ListRecordingsQuery): Promise<ListRecordingsResponse>;
270
+ /** Current recording status. Final media can become ready after the call has ended. */
271
+ getRecording(recordingId: string): Promise<RecordingArtifact>;
272
+ /** Renewable playback access. Keep recordingId in storage; URLs expire at expiresAt. */
273
+ getRecordingAccess(recordingId: string): Promise<RecordingAccessResponse>;
268
274
  creditBalance(): Promise<CreditBalance>;
269
275
  }
270
276
 
@@ -356,4 +362,4 @@ declare const clipLibraryDeclarationSchema: z.ZodObject<{
356
362
  }, z.core.$strict>>>>;
357
363
  }, z.core.$strict>;
358
364
 
359
- export { Asset, AssetKind, Avatar, AvatarSourceSwap, AvatarUpdate, CallMode, CallPolicy, ClipLibrary, ClipLibraryDeclaration, ClipLibraryUpdate, CreditBalance, EndCallOptions, ListSessionsOptions, LoopRedirect, RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, type RealtimeAvatarOptions, type StartCallOptions, StartCallResult, TranscriptPayload, UsageSession, UsageSessionPage, clipLibraryDeclarationSchema, verifyTranscript };
365
+ export { Asset, AssetKind, Avatar, AvatarSourceSwap, AvatarUpdate, CallMode, CallPolicy, ClipLibrary, ClipLibraryDeclaration, ClipLibraryUpdate, CreditBalance, EndCallOptions, ListRecordingsQuery, ListRecordingsResponse, ListSessionsOptions, LoopRedirect, RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, type RealtimeAvatarOptions, RecordingAccessResponse, RecordingArtifact, type StartCallOptions, StartCallResult, TranscriptPayload, UsageSession, UsageSessionPage, clipLibraryDeclarationSchema, verifyTranscript };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, clipLibraryDeclarationSchema, isQueued, verifyTranscript } from './chunk-PKKTFUA7.js';
1
+ export { RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, clipLibraryDeclarationSchema, isQueued, listRecordingsResponseSchema, recordingAccessResponseSchema, recordingArtifactSchema, recordingModeSchema, verifyTranscript } from './chunk-V26PE2M3.js';
package/dist/nextjs.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { P as ProxyConfig } from './types-CJcTowDB.js';
2
- import './types-B9GTrpx0.js';
1
+ import { P as ProxyConfig } from './types-Bphj69NC.js';
2
+ import './types-VOv0vsFs.js';
3
+ import 'zod';
3
4
 
4
5
  /**
5
6
  * App Router. Mount at `app/api/realtime-avatar/[...path]/route.ts`:
package/dist/nextjs.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createProxyHandler } from './chunk-LZMJJX7C.js';
2
- import './chunk-PKKTFUA7.js';
1
+ import { createProxyHandler } from './chunk-2TNFET3Q.js';
2
+ import './chunk-V26PE2M3.js';
3
3
 
4
4
  // ../proxy/src/nextjs.ts
5
5
  function createRealtimeAvatarRoute(config) {