@agentmedia/schema 0.2.1 → 0.3.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/src/video.ts CHANGED
@@ -221,18 +221,54 @@ export const SubtitleSchema = z.object({
221
221
 
222
222
  export type SubtitleInput = z.infer<typeof SubtitleSchema>;
223
223
 
224
- export const ProductReviewSchema = z.object({
225
- product_url: z.string().url(),
224
+ export const SaasReviewSchema = z.preprocess(flattenGroupedParams, z.object({
225
+ script: z.string().min(50).max(3000).optional(),
226
+ prompt: z.string().min(1).max(1000).optional(),
227
+ product_url: z.string().url().optional(),
226
228
  angle: z.enum(REVIEW_ANGLES).optional(),
227
- tone: z.enum(TONES).optional(),
228
229
  actor_slug: z.string().min(1).max(100).optional(),
230
+ persona_slug: z.string().min(1).max(100).optional(),
231
+ face_photo_url: z.string().url().optional(),
232
+ voice: z.string().max(100).optional(),
229
233
  target_duration: z.number().refine(
230
234
  (v) => (DURATIONS as readonly number[]).includes(v),
231
235
  { message: 'target_duration must be 5, 10, or 15' },
232
236
  ).optional(),
233
- });
237
+ style: z.string().max(50).optional(),
238
+ tone: z.enum(TONES).optional(),
239
+ voice_speed: z.number().min(0.7).max(1.5).optional(),
240
+ music: z.enum(MUSIC_GENRES).optional(),
241
+ cta: z.string().max(100).optional(),
242
+ aspect_ratio: z.enum(ASPECT_RATIOS).optional(),
243
+ template: z.string().max(50).optional(),
244
+ composition_mode: z.literal('pip').optional(),
245
+ pip_options: PipOptionsSchema.optional(),
246
+ allow_broll: z.boolean().optional(),
247
+ broll_model: z.enum(BROLL_MODELS).optional(),
248
+ broll_images: z.array(z.string().url()).max(10).optional(),
249
+ dub_language: z.string().max(10).optional(),
250
+ webhook_url: z.string().url().optional(),
251
+ scenes: z.array(
252
+ z.object({
253
+ type: z.enum(SCENE_TYPES).optional(),
254
+ }).passthrough(),
255
+ ).max(30).optional(),
256
+ }).refine(
257
+ (data) => data.script !== undefined || data.prompt !== undefined || data.product_url !== undefined,
258
+ { message: 'script, prompt, or product_url is required' },
259
+ ));
260
+
261
+ export type SaasReviewInput = z.infer<typeof SaasReviewSchema>;
234
262
 
235
- export type ProductReviewInput = z.infer<typeof ProductReviewSchema>;
263
+ /**
264
+ * @deprecated Use SaasReviewSchema. Kept for API/SDK compatibility.
265
+ */
266
+ export const ProductReviewSchema = SaasReviewSchema;
267
+
268
+ /**
269
+ * @deprecated Use SaasReviewInput. Kept for API/SDK compatibility.
270
+ */
271
+ export type ProductReviewInput = SaasReviewInput;
236
272
 
237
273
  export const SHOW_YOUR_APP_WORDS_PER_SECOND = 3;
238
274
  export const SHOW_YOUR_APP_DURATIONS = [5, 10, 15] as const;
@@ -261,3 +297,123 @@ export const ShowYourAppSchema = z.object({
261
297
  });
262
298
 
263
299
  export type ShowYourAppInput = z.infer<typeof ShowYourAppSchema>;
300
+
301
+ // ── Laptop UGC ──────────────────────────────────────────────────────────────
302
+
303
+ export const LAPTOP_UGC_WORDS_PER_SECOND = 3;
304
+ export const LAPTOP_UGC_DURATIONS = [15, 20] as const;
305
+ export const LAPTOP_UGC_MAX_RECORDING_BYTES = 10 * 1024 * 1024; // 10 MB
306
+ export const LAPTOP_UGC_MAX_RECORDING_SECONDS = 10;
307
+ export const LAPTOP_UGC_RECORDING_MIME_TYPES = ['video/mp4', 'video/quicktime'] as const;
308
+ export const LAPTOP_UGC_MAX_IMAGE_BYTES = 5 * 1024 * 1024; // 5 MB
309
+ export const LAPTOP_UGC_IMAGE_MIME_TYPES = ['image/png', 'image/jpeg', 'image/webp'] as const;
310
+
311
+ export const LaptopUgcSchema = z.object({
312
+ app_screen_recording_url: z.string().url().optional().describe('Public URL of a screen recording of your app (mp4 or mov). Max 10 MB, max 10 seconds. Provide this OR app_screen_image_url, not both.'),
313
+ app_screen_image_url: z.string().url().optional().describe('Public URL of a static screenshot of your app (png, jpeg, or webp). Max 5 MB. Provide this OR app_screen_recording_url, not both.'),
314
+ actor_slug: z.string().min(1).max(100).optional().describe('Actor slug from GET /v1/actors. Random actor if omitted.'),
315
+ script: z.string().min(5).max(3000).describe('Full voiceover script. Auto-split at sentence boundary into two halves for the two face shots.'),
316
+ duration: z.number().refine(
317
+ (v) => (LAPTOP_UGC_DURATIONS as readonly number[]).includes(v),
318
+ { message: 'duration must be 15 or 20' },
319
+ ).optional().default(20),
320
+ subtitle_style: z.enum(['hormozi', 'none']).optional().default('hormozi'),
321
+ webhook_url: z.string().url().optional(),
322
+ }).superRefine((val, ctx) => {
323
+ // Exactly one of recording / image must be provided
324
+ const hasRecording = !!val.app_screen_recording_url;
325
+ const hasImage = !!val.app_screen_image_url;
326
+ if (hasRecording && hasImage) {
327
+ ctx.addIssue({
328
+ code: z.ZodIssueCode.custom,
329
+ path: ['app_screen_recording_url'],
330
+ message: 'Provide app_screen_recording_url OR app_screen_image_url, not both',
331
+ });
332
+ }
333
+ if (!hasRecording && !hasImage) {
334
+ ctx.addIssue({
335
+ code: z.ZodIssueCode.custom,
336
+ path: ['app_screen_recording_url'],
337
+ message: 'Either app_screen_recording_url or app_screen_image_url is required',
338
+ });
339
+ }
340
+ // Word-count cap
341
+ const duration = val.duration ?? 20;
342
+ const maxWords = duration * LAPTOP_UGC_WORDS_PER_SECOND;
343
+ const wordCount = val.script.trim().split(/\s+/).filter(Boolean).length;
344
+ if (wordCount > maxWords) {
345
+ ctx.addIssue({
346
+ code: z.ZodIssueCode.custom,
347
+ path: ['script'],
348
+ message: `Script has ${wordCount} words; max ${maxWords} for ${duration}s at ${LAPTOP_UGC_WORDS_PER_SECOND} words/sec`,
349
+ });
350
+ }
351
+ });
352
+
353
+ export type LaptopUgcInput = z.infer<typeof LaptopUgcSchema>;
354
+
355
+ export const PRODUCT_ACTING_WORDS_PER_SECOND = 3;
356
+ export const PRODUCT_ACTING_DURATIONS = [5, 10, 15] as const;
357
+ export const PRODUCT_ACTING_TEMPLATES = [
358
+ 'product-in-hand',
359
+ 'mirror-selfie',
360
+ 'bathroom-reaction',
361
+ 'kitchen-counter',
362
+ 'car-selfie',
363
+ 'couch-review',
364
+ 'expert-interview',
365
+ 'product-closeup',
366
+ ] as const;
367
+ export const PRODUCT_ACTING_STYLES = [
368
+ 'raw-selfie',
369
+ 'shocked',
370
+ 'angry',
371
+ 'excited',
372
+ 'dramatic',
373
+ 'weird-hook',
374
+ 'casual-demo',
375
+ 'honest-review',
376
+ ] as const;
377
+
378
+ export const ProductActingSchema = z.object({
379
+ product_image_url: z.string().url().describe('Public URL of the product image to place in the actor scene. PNG, JPEG, or WebP recommended.'),
380
+ actor_slug: z.string().min(1).max(100).describe('Actor slug from GET /v1/actors. This actor is used as the creator reference.'),
381
+ actor_variant_id: z.string().uuid().optional().describe('Optional actor variant UUID for a specific outfit, framing, expression, or background.'),
382
+ product_name: z.string().min(1).max(120).optional().describe('Optional product name used for prompt/script context.'),
383
+ product_description: z.string().min(1).max(1000).optional().describe('Product context used to generate a short script when script is omitted. Required when script is omitted.'),
384
+ script: z.string().min(5).max(3000).optional().describe('Exact words the actor should say. If omitted, the API generates a short script from product_description.'),
385
+ template: z.enum(PRODUCT_ACTING_TEMPLATES).optional().default('product-in-hand').describe('UGC scenario template for the generated frame and motion.'),
386
+ acting_style: z.enum(PRODUCT_ACTING_STYLES).optional().default('raw-selfie').describe('Acting energy and delivery style.'),
387
+ visual_style: z.string().max(200).optional().describe('Extra camera, pose, environment, or framing direction.'),
388
+ duration: z.number().refine(
389
+ (v) => (PRODUCT_ACTING_DURATIONS as readonly number[]).includes(v),
390
+ { message: 'duration must be 5, 10, or 15' },
391
+ ).optional().default(5).describe('Video duration in seconds. Valid values: 5, 10, or 15.'),
392
+ subtitles: z.boolean().optional().default(true).describe('Whether to burn word-level synced subtitles into the final video.'),
393
+ subtitle_style: z.enum(['hormozi', 'none']).optional().default('hormozi').describe('Subtitle style. Use none to disable subtitle rendering.'),
394
+ webhook_url: z.string().url().optional().describe('HTTPS URL to receive a callback when the job completes or fails.'),
395
+ }).superRefine((val, ctx) => {
396
+ if (!val.script && !val.product_description) {
397
+ ctx.addIssue({
398
+ code: z.ZodIssueCode.custom,
399
+ path: ['product_description'],
400
+ message: 'Either script or product_description is required',
401
+ });
402
+ return;
403
+ }
404
+
405
+ if (!val.script) return;
406
+
407
+ const duration = val.duration ?? 5;
408
+ const maxWords = duration * PRODUCT_ACTING_WORDS_PER_SECOND;
409
+ const wordCount = val.script.trim().split(/\s+/).filter(Boolean).length;
410
+ if (wordCount > maxWords) {
411
+ ctx.addIssue({
412
+ code: z.ZodIssueCode.custom,
413
+ path: ['script'],
414
+ message: `Script has ${wordCount} words; max ${maxWords} for ${duration}s at ${PRODUCT_ACTING_WORDS_PER_SECOND} words/sec`,
415
+ });
416
+ }
417
+ });
418
+
419
+ export type ProductActingInput = z.infer<typeof ProductActingSchema>;