@agentmedia/schema 0.2.2 → 0.4.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/.memory/cursor.json +3 -0
- package/.memory/memories.json +1 -0
- package/.memory/project.json +5 -0
- package/CLAUDE.md +7 -0
- package/LICENSE +199 -0
- package/README.md +2 -2
- package/dist/__tests__/character-pipeline.test.d.ts +2 -0
- package/dist/__tests__/character-pipeline.test.d.ts.map +1 -0
- package/dist/__tests__/character-pipeline.test.js +296 -0
- package/dist/__tests__/character-pipeline.test.js.map +1 -0
- package/dist/__tests__/parity.test.js +7 -0
- package/dist/__tests__/parity.test.js.map +1 -1
- package/dist/__tests__/text-to-video.test.d.ts +2 -0
- package/dist/__tests__/text-to-video.test.d.ts.map +1 -0
- package/dist/__tests__/text-to-video.test.js +67 -0
- package/dist/__tests__/text-to-video.test.js.map +1 -0
- package/dist/generators.d.ts +519 -7
- package/dist/generators.d.ts.map +1 -1
- package/dist/generators.js +24 -3
- package/dist/generators.js.map +1 -1
- package/dist/v2/character.d.ts +32 -0
- package/dist/v2/character.d.ts.map +1 -0
- package/dist/v2/character.js +29 -0
- package/dist/v2/character.js.map +1 -0
- package/dist/v2/generators.d.ts +69 -0
- package/dist/v2/generators.d.ts.map +1 -0
- package/dist/v2/generators.js +105 -0
- package/dist/v2/generators.js.map +1 -0
- package/dist/v2/index.d.ts +13 -0
- package/dist/v2/index.d.ts.map +1 -0
- package/dist/v2/index.js +14 -0
- package/dist/v2/index.js.map +1 -0
- package/dist/v2/selfie.d.ts +78 -0
- package/dist/v2/selfie.d.ts.map +1 -0
- package/dist/v2/selfie.js +87 -0
- package/dist/v2/selfie.js.map +1 -0
- package/dist/v2/subtitle.d.ts +31 -0
- package/dist/v2/subtitle.d.ts.map +1 -0
- package/dist/v2/subtitle.js +53 -0
- package/dist/v2/subtitle.js.map +1 -0
- package/dist/video.d.ts +628 -6
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +164 -4
- package/dist/video.js.map +1 -1
- package/package.json +36 -16
- package/scripts/generate-openapi.ts +87 -38
- package/scripts/generate-v2-docs.ts +328 -0
- package/src/__tests__/character-pipeline.test.ts +356 -0
- package/src/__tests__/parity.test.ts +8 -0
- package/src/__tests__/text-to-video.test.ts +79 -0
- package/src/generators.ts +29 -2
- package/src/v2/character.ts +39 -0
- package/src/v2/generators.ts +186 -0
- package/src/v2/index.ts +15 -0
- package/src/v2/selfie.ts +103 -0
- package/src/v2/subtitle.ts +62 -0
- package/src/video.ts +259 -5
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
|
|
225
|
-
|
|
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
|
+
));
|
|
234
260
|
|
|
235
|
-
export type
|
|
261
|
+
export type SaasReviewInput = z.infer<typeof SaasReviewSchema>;
|
|
262
|
+
|
|
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;
|
|
@@ -262,6 +298,60 @@ export const ShowYourAppSchema = z.object({
|
|
|
262
298
|
|
|
263
299
|
export type ShowYourAppInput = z.infer<typeof ShowYourAppSchema>;
|
|
264
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
|
+
|
|
265
355
|
export const PRODUCT_ACTING_WORDS_PER_SECOND = 3;
|
|
266
356
|
export const PRODUCT_ACTING_DURATIONS = [5, 10, 15] as const;
|
|
267
357
|
export const PRODUCT_ACTING_TEMPLATES = [
|
|
@@ -327,3 +417,167 @@ export const ProductActingSchema = z.object({
|
|
|
327
417
|
});
|
|
328
418
|
|
|
329
419
|
export type ProductActingInput = z.infer<typeof ProductActingSchema>;
|
|
420
|
+
|
|
421
|
+
// ── Character Video ────────────────────────────────────────────────────────
|
|
422
|
+
//
|
|
423
|
+
// 3-step pipeline:
|
|
424
|
+
// Step 1: Character reference sheet (PNG)
|
|
425
|
+
// — POST /v1/character/sheet-generate
|
|
426
|
+
// — actor_slug returns portrait_url (free), description calls
|
|
427
|
+
// gpt-image-2 (~$0.04)
|
|
428
|
+
// Step 2: Storyboard sheet (PNG)
|
|
429
|
+
// — POST /v1/character/storyboard-generate
|
|
430
|
+
// — gpt-image-2 paints numbered panels using the character
|
|
431
|
+
// sheet as a reference so panels stay on-character (~$0.04)
|
|
432
|
+
// Step 3: Final video (this generator)
|
|
433
|
+
// — Seedance 2.0 multimodal: image_urls = [character, storyboard]
|
|
434
|
+
// + short action_prompt + duration/ratio settings
|
|
435
|
+
//
|
|
436
|
+
// All three URLs must be public HTTPS so Seedance can fetch them.
|
|
437
|
+
|
|
438
|
+
export const CHARACTER_VIDEO_DURATIONS = [5, 10] as const;
|
|
439
|
+
export const CHARACTER_VIDEO_RATIOS = ['9:16', '16:9', '1:1'] as const;
|
|
440
|
+
|
|
441
|
+
export const CharacterVideoSchema = z.object({
|
|
442
|
+
character_sheet_url: z.string().url().describe(
|
|
443
|
+
'Public HTTPS URL of the character reference sheet PNG. Generate one via POST /v1/character/sheet-generate or upload your own.',
|
|
444
|
+
),
|
|
445
|
+
storyboard_url: z.string().url().describe(
|
|
446
|
+
'Public HTTPS URL of the storyboard panels PNG. Generate one via POST /v1/character/storyboard-generate or upload your own.',
|
|
447
|
+
),
|
|
448
|
+
action_prompt: z.string().min(1).max(2000).optional().describe(
|
|
449
|
+
'Optional scene/action description sent to Seedance as the primary scene driver (e.g. "Marco the chef in his Brooklyn kitchen at golden hour, takes a bite of fresh bread"). When omitted, the api-v2 server backstops it from the same session\'s storyboard job (script or beats) — without a real scene description Seedance picks the location arbitrarily.',
|
|
450
|
+
),
|
|
451
|
+
duration: z.number().refine(
|
|
452
|
+
(v) => (CHARACTER_VIDEO_DURATIONS as readonly number[]).includes(v),
|
|
453
|
+
{ message: 'duration must be 5 or 10' },
|
|
454
|
+
).optional().default(10).describe('Video duration in seconds. Valid values: 5 or 10. Max 10s — Seedance i2v quality degrades past 10s with multimodal inputs.'),
|
|
455
|
+
aspect_ratio: z.enum(CHARACTER_VIDEO_RATIOS).optional().default('9:16').describe('Output aspect ratio. Default 9:16.'),
|
|
456
|
+
generate_audio: z.boolean().optional().default(true).describe('Whether Seedance synthesizes synchronized audio (ambient sounds, breath, etc.).'),
|
|
457
|
+
session_id: z.string().uuid().optional().describe('Optional UUID linking the three pipeline steps (sheet → storyboard → video) into one wizard session. Same value across calls makes inserts idempotent and lets api-v2 backstop action_prompt from the storyboard step.'),
|
|
458
|
+
webhook_url: z.string().url().optional().describe('HTTPS URL to receive a callback when the job completes or fails.'),
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
export type CharacterVideoInput = z.infer<typeof CharacterVideoSchema>;
|
|
462
|
+
|
|
463
|
+
// ── Text-to-Video (pure prompt, no character / no storyboard) ──────────────
|
|
464
|
+
//
|
|
465
|
+
// Drives Seedance 2.0 text-to-video. The prompt IS the whole creative —
|
|
466
|
+
// style, subject, mood, composition all baked into one string. Used by
|
|
467
|
+
// the "Use prompts" batch-schedule mode and as a one-shot generator
|
|
468
|
+
// for Claude Code / MCP / CLI workflows where the agent already has a
|
|
469
|
+
// fully-formed video prompt.
|
|
470
|
+
|
|
471
|
+
export const TEXT_TO_VIDEO_DURATIONS = [5, 10, 15] as const;
|
|
472
|
+
export const TEXT_TO_VIDEO_RATIOS = ['9:16', '16:9', '1:1'] as const;
|
|
473
|
+
|
|
474
|
+
export const TextToVideoSchema = z.object({
|
|
475
|
+
prompt: z.string().min(20).max(1000).describe(
|
|
476
|
+
'The full video prompt. Style, subject, mood, composition, lighting, lens, motion — all in one string. Seedance reads this verbatim. Min 20 / max 1000 chars.',
|
|
477
|
+
),
|
|
478
|
+
duration: z.number().refine(
|
|
479
|
+
(v) => (TEXT_TO_VIDEO_DURATIONS as readonly number[]).includes(v),
|
|
480
|
+
{ message: 'duration must be 5, 10, or 15' },
|
|
481
|
+
).optional().default(10).describe('Video duration in seconds. Valid: 5, 10, or 15.'),
|
|
482
|
+
aspect_ratio: z.enum(TEXT_TO_VIDEO_RATIOS).optional().default('9:16').describe(
|
|
483
|
+
'Output aspect ratio. 9:16 portrait (TikTok / Reels / Shorts), 1:1 square (feed posts), 16:9 landscape (YouTube / X / LinkedIn). Default 9:16.',
|
|
484
|
+
),
|
|
485
|
+
generate_audio: z.boolean().optional().default(true).describe(
|
|
486
|
+
'Whether Seedance synthesizes synchronized audio. No extra charge.',
|
|
487
|
+
),
|
|
488
|
+
webhook_url: z.string().url().optional().describe(
|
|
489
|
+
'HTTPS URL to receive a callback when the job completes or fails.',
|
|
490
|
+
),
|
|
491
|
+
/** When set, on completion the webhook-provider fans the rendered MP4
|
|
492
|
+
* out to these Postiz integrations (X, LinkedIn, etc.). Use the IDs
|
|
493
|
+
* returned from GET /v1/integrations/postiz/accounts. */
|
|
494
|
+
postiz_integration_ids: z.array(z.string()).optional().describe(
|
|
495
|
+
'Postiz integration IDs to auto-publish to once the video is rendered. Get them from GET /v1/integrations/postiz/accounts.',
|
|
496
|
+
),
|
|
497
|
+
/** How to compose the social caption. 'static' = use `caption` verbatim. 'ai' = Claude writes one using `caption_guidance` as bias. */
|
|
498
|
+
caption_mode: z.enum(['ai', 'static']).optional().default('static').describe(
|
|
499
|
+
"Caption mode for auto-publish. 'static' (default) uses the literal `caption` string. 'ai' has Claude Opus generate one each time, biased by `caption_guidance`.",
|
|
500
|
+
),
|
|
501
|
+
caption: z.string().min(1).max(2000).optional().describe(
|
|
502
|
+
'Literal caption text for the social post. Only used when caption_mode is omitted or "static".',
|
|
503
|
+
),
|
|
504
|
+
caption_guidance: z.string().min(1).max(1000).optional().describe(
|
|
505
|
+
'Tone / hashtag / length hints for the AI caption writer. Only used when caption_mode is "ai".',
|
|
506
|
+
),
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
export type TextToVideoInput = z.infer<typeof TextToVideoSchema>;
|
|
510
|
+
|
|
511
|
+
// ── Step 1 of 3: Character Sheet generation ─────────────────────────────────
|
|
512
|
+
//
|
|
513
|
+
// POST /v1/character/sheet-generate. Async — returns 202 + job_id.
|
|
514
|
+
// Pricing: 12 credits ($0.12) for actor/reference (1 gpt-image-2 call),
|
|
515
|
+
// 20 credits ($0.20) for description-only (paint portrait + sheet).
|
|
516
|
+
|
|
517
|
+
export const CharacterSheetSchema = z.object({
|
|
518
|
+
description: z.string().min(3).max(400).optional().describe(
|
|
519
|
+
'Free-text description of the character (e.g. "Marco, a 35yo Italian chef with curly black hair, white uniform"). When provided alongside a reference image, it adds context. When provided alone (no actor_slug, no reference_image_url), the worker first paints a portrait via gpt-image-2 text-to-image, then turns it into the sheet.',
|
|
520
|
+
),
|
|
521
|
+
actor_slug: z.string().min(1).optional().describe(
|
|
522
|
+
'Slug of an actor from the agent-media library. The actor\'s portrait is used as the reference image. List actors via GET /v1/actors. Mutually exclusive with reference_image_url.',
|
|
523
|
+
),
|
|
524
|
+
reference_image_url: z.string().url().optional().describe(
|
|
525
|
+
'Public HTTPS URL of a portrait/reference image (PNG/JPEG/WebP, <5MB recommended). Mutually exclusive with actor_slug.',
|
|
526
|
+
),
|
|
527
|
+
session_id: z.string().uuid().optional().describe('Optional UUID linking the three pipeline steps. Same value across calls makes inserts idempotent.'),
|
|
528
|
+
}).refine(
|
|
529
|
+
(v) => Boolean(v.description) || Boolean(v.actor_slug) || Boolean(v.reference_image_url),
|
|
530
|
+
{ message: 'Provide at least one of: description, actor_slug, or reference_image_url' },
|
|
531
|
+
).refine(
|
|
532
|
+
(v) => !(v.actor_slug && v.reference_image_url),
|
|
533
|
+
{ message: 'actor_slug and reference_image_url are mutually exclusive' },
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
export type CharacterSheetInput = z.infer<typeof CharacterSheetSchema>;
|
|
537
|
+
|
|
538
|
+
// ── Step 2 of 3: Storyboard generation ──────────────────────────────────────
|
|
539
|
+
//
|
|
540
|
+
// POST /v1/character/storyboard-generate. Async — returns 202 + job_id.
|
|
541
|
+
// Pricing: 12 credits ($0.12).
|
|
542
|
+
// Provide EITHER `beats` (3-10 short strings, gpt-image-2 paints them as
|
|
543
|
+
// numbered panels) OR `script` (free-text up to 1500 chars; gpt-image-2
|
|
544
|
+
// splits it into 4-6 panels itself).
|
|
545
|
+
|
|
546
|
+
export const STORYBOARD_RATIOS = ['9:16', '16:9', '1:1'] as const;
|
|
547
|
+
|
|
548
|
+
export const CharacterStoryboardSchema = z.object({
|
|
549
|
+
character_sheet_url: z.string().url().describe(
|
|
550
|
+
'Public HTTPS URL of the character sheet PNG (from /v1/character/sheet-generate). Used as the visual reference so the character stays on-model across panels.',
|
|
551
|
+
),
|
|
552
|
+
beats: z.array(z.string().min(3).max(200)).min(3).max(10).optional().describe(
|
|
553
|
+
'Ordered list of 3-10 short beat descriptions, one per panel (e.g. ["Marco walks into kitchen", "pulls bread from oven", "takes a bite", "thumbs up"]). Mutually exclusive with `script`.',
|
|
554
|
+
),
|
|
555
|
+
script: z.string().min(10).max(1500).optional().describe(
|
|
556
|
+
'Free-text script (10-1500 chars). gpt-image-2 splits it into 4-6 sequential panels itself. Mutually exclusive with `beats`.',
|
|
557
|
+
),
|
|
558
|
+
ratio: z.enum(STORYBOARD_RATIOS).optional().default('9:16').describe('Storyboard sheet aspect ratio. Default 9:16.'),
|
|
559
|
+
session_id: z.string().uuid().optional().describe('Optional UUID linking the three pipeline steps.'),
|
|
560
|
+
}).refine(
|
|
561
|
+
(v) => Boolean(v.beats) !== Boolean(v.script),
|
|
562
|
+
{ message: 'Provide exactly one of: beats or script' },
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
export type CharacterStoryboardInput = z.infer<typeof CharacterStoryboardSchema>;
|
|
566
|
+
|
|
567
|
+
// ── Helper: AI-suggested beats ─────────────────────────────────────────────
|
|
568
|
+
//
|
|
569
|
+
// POST /v1/character/storyboard-suggest. Sync — returns immediately.
|
|
570
|
+
// No credits. Returns 3 distinct beat-sequence options for the wizard.
|
|
571
|
+
|
|
572
|
+
export const StoryboardSuggestSchema = z.object({
|
|
573
|
+
actor_slug: z.string().min(1).optional().describe('Library actor whose persona drives the suggestions. Mutually exclusive with character_description.'),
|
|
574
|
+
character_description: z.string().min(1).max(200).optional().describe('1-200 char description of the character. Mutually exclusive with actor_slug.'),
|
|
575
|
+
vibe: z.string().min(1).max(200).optional().describe('Optional vibe note that biases all 3 options (e.g. "wholesome", "chaotic", "cinematic").'),
|
|
576
|
+
duration: z.number().refine((v) => v === 5 || v === 10, { message: 'duration must be 5 or 10' }).optional().default(10),
|
|
577
|
+
n_panels: z.number().int().min(4).max(10).optional().default(6).describe('Panels per option (the storyboard sheet will be an n_panels-panel grid).'),
|
|
578
|
+
}).refine(
|
|
579
|
+
(v) => Boolean(v.actor_slug) !== Boolean(v.character_description),
|
|
580
|
+
{ message: 'Provide exactly one of: actor_slug or character_description' },
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
export type StoryboardSuggestInput = z.infer<typeof StoryboardSuggestSchema>;
|