@docyrus/animaguru 0.17.0 → 0.18.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
@@ -221,16 +221,50 @@ animaguru assets generate PROJECT_ID --type video --model fal/minimax-h3-max \
221
221
 
222
222
  `--end-reference` can be used without `--reference`; the video then ends on that image. `--soundtrack` takes a music, speech or SFX asset's `file` SHA and replaces the generated sound; the video must be at least 2 seconds. Text-to-video takes `--aspect-ratio` 21:9, 16:9 (default), 4:3, 1:1, 3:4 or 9:16. Extensions take a video asset's SHA as `--source` (1.6–60 seconds, up to 50 MB), bill `--seconds` of new footage, offer 2K, and keep the source's shape unless `--aspect-ratio` crops it. `--extend-output continuation` returns only a 1.625-second overlap and the new footage. Extensions take up to three comma-separated `--audio-references` (2–15 seconds each, 15 seconds in total); refer to them in the prompt as Audio 1, Audio 2 and so on. `--prompt-expansion` is `disabled` (default), `balanced` or `quality`; for extensions, any value other than `disabled` turns expansion on. Each model's `videoProfiles` entry in `catalog` lists its workflows, resolutions and options. The server needs `AI_PROVIDER_KEY_FAL`.
223
223
 
224
+ CLI 0.18.0 adds MiniMax H3 Max's eight other workflows (`fal/minimax-h3-max`, not Turbo):
225
+
226
+ ```sh
227
+ # Keep Marie the same across scenes: reference images, a clip and audio, named in the prompt.
228
+ animaguru assets generate PROJECT_ID --type video --model fal/minimax-h3-max \
229
+ --workflow reference-to-video --name "Marie at the market" \
230
+ --prompt "Image 1 is Marie. She walks through the market in Video 1's style, humming Audio 1" \
231
+ --references CHARACTER_SHEET_SHA,MARKET_SHA --reference-videos CLIP_SHA --audio-references SONG_SHA \
232
+ --seconds 6.5 --resolution 768p --dry-run
233
+ # Make the face in an image say saved speech; the video lasts as long as the speech.
234
+ animaguru assets generate PROJECT_ID --type video --model fal/minimax-h3-max \
235
+ --workflow lip-sync --name "Marie explains" --reference PORTRAIT_SHA --soundtrack SPEECH_SHA \
236
+ --transcribe --resolution 768p --dry-run
237
+ # Orbit the camera half way around a still scene.
238
+ animaguru assets generate PROJECT_ID --type video --model fal/minimax-h3-max \
239
+ --workflow camera-controls --name "Product reveal" --reference PRODUCT_SHA \
240
+ --camera "0,0,10,1;1,180,10,1.2" --seconds 6 --dry-run
241
+ # Film in a preset look.
242
+ animaguru assets generate PROJECT_ID --type video --model fal/minimax-h3-max \
243
+ --workflow style --style vhs --damage light --name "Home video" \
244
+ --prompt "A birthday party in a backyard, 1989" --aspect-ratio 4:3 --dry-run
245
+ ```
246
+
247
+ - `reference-to-video` keeps the subjects or look of up to nine `--references` images, three `--reference-videos` (2–15 seconds each, 15 in total) and three `--audio-references`, twelve files in all. Name them in the prompt as Image 1, Video 1, Audio 1 and so on. It can also open on `--reference`, end on `--end-reference`, and pass `--middle-reference` at `--middle-seconds`; a middle image needs both of the others and 480p or 768p. `--aspect-ratio auto` (default) follows the inputs.
248
+ - `lip-sync` makes the face in `--reference` (an image between 2:5 and 5:2) say `--soundtrack`, speech of 5 seconds to 15 minutes. It takes no prompt and goes up to 2K. `--transcribe` transcribes the speech to guide the mouth. Past 15 seconds it costs 1.2× per second.
249
+ - `camera-controls` moves the camera around `--reference` along `--camera` keyframes `time,azimuth,elevation,distance`. `time` runs from 0 at the start to 1 at the end, and the angles are in degrees (elevation −90 to 90). Use 2–12 keyframes, separated by semicolons. Without a prompt the scene stays still and only the camera moves.
250
+ - `insert-video` adds a new scene of `--seconds` (5–13) to `--source`, starting `--insert-at` seconds in and handing back to the source at `--resume-at`. The prompt, `--references` and `--reference-videos` guide the scene. It renders at 480p or 768p, and `--no-color-match` turns off matching the source's colors.
251
+ - `recast` puts the people in `--references` (one photo each, up to four, left to right) in place of the people in `--source` (5–30 seconds), at 768p or 1080p. Pass `--subject-consent` only after each person in the photos agreed to appear. `--enhance-realism` blends them in more naturally.
252
+ - `relight` relights `--source` (up to 15 seconds) from `--lighting`, an image of a sphere lit the way you want. It takes no prompt, keeps the source's shape unless `--aspect-ratio` crops it, and goes up to 2K.
253
+ - `3d-to-video` turns `--source`, a 3D render up to 15 seconds long, into realistic footage. It follows up to eight `--references`, or draws its own (`--generated-references`, 1–8, default 2). It takes no seed and costs at least 5 seconds.
254
+ - `style` films in a preset look: `vhs` (with `--damage light|medium|heavy`), `retro-toon-70s`, `low-poly`, `hand-drawn` or `16bit-pixel`. It renders at 768p for 5–15 whole seconds, from the prompt with `--aspect-ratio`, or from `--reference` as the first frame.
255
+
256
+ H3 takes fractions of a second (`--seconds 6.5`). Lip-sync, recast, relight and 3d-to-video last as long as their speech or video, and `--seconds` doesn't apply to them. Reference images, clips and audio past fal's included allowance add to the cost; so do the references 3d-to-video draws. Their `--dry-run` asks the server, which measures the saved files. `--resolution` and `--mode` default to the first the workflow offers.
257
+
224
258
  Images have a full background by default; `--transparent` requests a cutout. CLI 0.17.0 adds `--references`, `--mask` and the 16:9 and 9:16 shapes. `--aspect-ratio` is 1:1 (default, 1024×1024), 3:2 (1536×1024), 2:3 (1024×1536), 16:9 (1536×864) or 9:16 (864×1536). `--references` takes up to four comma-separated blob SHAs of this project's images (including attached uploads) or sprite art, and the new image follows them: generate a character sheet once, then reference it in every scene to keep the character consistent. Refer to them in the prompt as Image 1, Image 2 and so on, and say what to take from each. Each reference adds a little to the estimate. `--mask` edits only part of Image 1, the first reference: give up to eight rectangles as `x,y,width,height` fractions of its size from the top left, separated by semicolons (`0,0,1,0.4` is the top 40%). The rest of the image is kept, and the result keeps Image 1's shape, which must be one of the five above. Gemini speech's `--prompt` is the verbatim transcript, and `--delivery` controls how it sounds without being spoken. Gemini speech supports both `gemini-3.8-flash-tts` and `gemini-3.8-flash-lite-tts`. Music uses Lyria. Google audio requires the server's `AI_PROVIDER_KEY_GOOGLE` secret; no provider keys are stored in the CLI.
225
259
 
226
- Preview a validated request and its estimated credits offline with `--dry-run`, without signing in or spending credits:
260
+ Preview a validated request and its estimated credits with `--dry-run`, without spending credits; most dry runs work offline without signing in:
227
261
 
228
262
  ```sh
229
263
  animaguru assets generate PROJECT_ID --type music --name "Theme" \
230
264
  --prompt "A gentle instrumental theme" --dry-run
231
265
  ```
232
266
 
233
- Dry-run estimates use the CLI's bundled rates and do not check server configuration, remaining credits or reference ownership; the server performs those checks before actual generation. Omit `--dry-run` and pass `--yes` to submit. Actual provider usage determines the charge. Asset requests can also be supplied as JSON using the web API's shared schema:
267
+ Dry-run estimates use the CLI's bundled rates and do not check server configuration, remaining credits or reference ownership; the server performs those checks before actual generation. The exceptions are H3 Max's lip-sync, recast, relight, 3d-to-video, reference-to-video and insert-video: their prices depend on the saved files' lengths and shapes, so their dry runs are signed-in server estimates. Omit `--dry-run` and pass `--yes` to submit. Actual provider usage determines the charge. Asset requests can also be supplied as JSON using the web API's shared schema:
234
268
 
235
269
  ```json
236
270
  {"type":"speech","name":"Welcome","prompt":"Welcome, traveler.","model":"gemini-3.8-flash-lite-tts","voice":"Kore","delivery":"Warm and welcoming"}
package/dist/animaguru.js CHANGED
@@ -19761,7 +19761,7 @@ var require_ignore2 = __commonJS({
19761
19761
  // cli/package.json
19762
19762
  var package_default = {
19763
19763
  name: "@docyrus/animaguru",
19764
- version: "0.17.0",
19764
+ version: "0.18.0",
19765
19765
  description: "Agent-friendly AnimaGuru CLI built with CrustJS",
19766
19766
  type: "module",
19767
19767
  bin: {
@@ -25339,7 +25339,12 @@ var SPRITE_IMAGE_MODEL_IDS = SPRITE_IMAGE_MODELS.map((m) => m.id);
25339
25339
  function spriteImageModel(id2) {
25340
25340
  return SPRITE_IMAGE_MODELS.find((m) => m.id === id2) ?? SPRITE_IMAGE_MODELS[0];
25341
25341
  }
25342
- var VIDEO_WORKFLOWS = ["image-to-video", "text-to-video", "extend-video"];
25342
+ var VIDEO_WORKFLOWS = ["image-to-video", "text-to-video", "extend-video", "reference-to-video", "lip-sync", "camera-controls", "insert-video", "recast", "relight", "3d-to-video", "style"];
25343
+ var STUDIO_VIDEO_WORKFLOWS = ["image-to-video", "text-to-video", "extend-video"];
25344
+ var MEDIA_LENGTH_WORKFLOWS = ["lip-sync", "recast", "relight", "3d-to-video"];
25345
+ var lengthFromMedia = (workflow) => MEDIA_LENGTH_WORKFLOWS.includes(workflow);
25346
+ var VIDEO_STYLES = ["vhs", "retro-toon-70s", "low-poly", "hand-drawn", "16bit-pixel"];
25347
+ var VHS_DAMAGE_LEVELS = ["light", "medium", "heavy"];
25343
25348
  var VIDEO_RESOLUTIONS = ["480p", "768p", "1080p", "2k"];
25344
25349
  var VIDEO_ASPECT_RATIOS = ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16"];
25345
25350
  var PROMPT_EXPANSION_MODES = ["disabled", "balanced", "quality"];
@@ -25347,6 +25352,15 @@ var EXTEND_OUTPUTS = ["extended", "continuation"];
25347
25352
  var SOUNDTRACK_MAX_BYTES = 15 * 1024 * 1024;
25348
25353
  var EXTEND_SOURCE_MAX_BYTES = 50 * 1024 * 1024;
25349
25354
  var MAX_AUDIO_REFERENCES = 3;
25355
+ var MAX_VIDEO_REFERENCE_IMAGES = 9;
25356
+ var MAX_REFERENCE_VIDEOS = 3;
25357
+ var MAX_REFERENCE_FILES = 12;
25358
+ var MAX_RECAST_PEOPLE = 4;
25359
+ var MAX_3D_REFERENCES = 8;
25360
+ var MAX_CAMERA_KEYFRAMES = 12;
25361
+ var MAX_CAMERA_TURNS = 32;
25362
+ var H3_MAX = "minimax/h3-max";
25363
+ var h3Endpoints = (route) => ({ "image-to-video": `${route}/image-to-video`, "text-to-video": `${route}/text-to-video`, "extend-video": `${route}/extend-video` });
25350
25364
  var SPRITE_VIDEO_MODELS = [
25351
25365
  {
25352
25366
  id: "pruna/p-video-2-pro",
@@ -25375,9 +25389,27 @@ var SPRITE_VIDEO_MODELS = [
25375
25389
  label: "MiniMax H3 Max (fal.ai)",
25376
25390
  provider: "fal",
25377
25391
  workflows: VIDEO_WORKFLOWS,
25378
- endpoints: { "image-to-video": "minimax/h3-max/image-to-video", "text-to-video": "minimax/h3-max/text-to-video", "extend-video": "minimax/h3-max/extend-video" },
25392
+ endpoints: {
25393
+ ...h3Endpoints(H3_MAX),
25394
+ "reference-to-video": `${H3_MAX}/reference-to-video`,
25395
+ "lip-sync": `${H3_MAX}/lip-sync/image-to-video`,
25396
+ "camera-controls": `${H3_MAX}/camera-controls`,
25397
+ "insert-video": `${H3_MAX}/insert-video`,
25398
+ recast: `${H3_MAX}/recast`,
25399
+ relight: `${H3_MAX}/relight`,
25400
+ "3d-to-video": `${H3_MAX}/3d-to-video`,
25401
+ style: `${H3_MAX}/styles`
25402
+ },
25379
25403
  resolutions: ["480p", "768p", "1080p"],
25380
- extendResolutions: VIDEO_RESOLUTIONS,
25404
+ workflowOptions: {
25405
+ "extend-video": { resolutions: VIDEO_RESOLUTIONS },
25406
+ "lip-sync": { resolutions: VIDEO_RESOLUTIONS, seconds: { min: 5, max: 900, fractional: true } },
25407
+ "insert-video": { resolutions: ["480p", "768p"], seconds: { min: 5, max: 13, fractional: true }, creditsPerSecond: { "480p": 5, "768p": 6 } },
25408
+ recast: { resolutions: ["768p", "1080p"], seconds: { min: 5, max: 30, fractional: true }, creditsPerSecond: { "768p": 30, "1080p": 45 } },
25409
+ relight: { resolutions: VIDEO_RESOLUTIONS, seconds: { min: 2.33, max: 15, fractional: true } },
25410
+ "3d-to-video": { seconds: { min: 1, max: 15, fractional: true } },
25411
+ style: { resolutions: ["768p"], seconds: { min: 5, max: 15 } }
25412
+ },
25381
25413
  modes: ["quality"],
25382
25414
  lastFrame: true,
25383
25415
  endFrameOnly: true,
@@ -25385,16 +25417,16 @@ var SPRITE_VIDEO_MODELS = [
25385
25417
  promptExpansion: PROMPT_EXPANSION_MODES,
25386
25418
  audio: true,
25387
25419
  estimatedCreditsPerSecond: { "480p": { quality: 5 }, "768p": { quality: 8 }, "1080p": { quality: 16 }, "2k": { quality: 32 } },
25388
- seconds: { min: 1, max: 15 }
25420
+ seconds: { min: 1, max: 15, fractional: true }
25389
25421
  },
25390
25422
  {
25391
25423
  id: "fal/minimax-h3-max-turbo",
25392
25424
  label: "MiniMax H3 Max Turbo (fal.ai)",
25393
25425
  provider: "fal",
25394
- workflows: VIDEO_WORKFLOWS,
25395
- endpoints: { "image-to-video": "minimax/h3-max-turbo/image-to-video", "text-to-video": "minimax/h3-max-turbo/text-to-video", "extend-video": "minimax/h3-max-turbo/extend-video" },
25426
+ workflows: STUDIO_VIDEO_WORKFLOWS,
25427
+ endpoints: h3Endpoints("minimax/h3-max-turbo"),
25396
25428
  resolutions: ["480p", "768p", "1080p"],
25397
- extendResolutions: VIDEO_RESOLUTIONS,
25429
+ workflowOptions: { "extend-video": { resolutions: VIDEO_RESOLUTIONS } },
25398
25430
  modes: ["speed"],
25399
25431
  lastFrame: true,
25400
25432
  endFrameOnly: true,
@@ -25402,7 +25434,7 @@ var SPRITE_VIDEO_MODELS = [
25402
25434
  promptExpansion: PROMPT_EXPANSION_MODES,
25403
25435
  audio: true,
25404
25436
  estimatedCreditsPerSecond: { "480p": { speed: 2.5 }, "768p": { speed: 4 }, "1080p": { speed: 8 }, "2k": { speed: 16 } },
25405
- seconds: { min: 1, max: 15 }
25437
+ seconds: { min: 1, max: 15, fractional: true }
25406
25438
  }
25407
25439
  ];
25408
25440
  var SPRITE_VIDEO_MODEL_IDS = SPRITE_VIDEO_MODELS.map((m) => m.id);
@@ -25411,12 +25443,31 @@ function spriteVideoModel(id2) {
25411
25443
  return SPRITE_VIDEO_MODELS.find((m) => m.id === id2) ?? SPRITE_VIDEO_MODELS[0];
25412
25444
  }
25413
25445
  function videoResolutions(model, workflow = "image-to-video") {
25414
- return workflow === "extend-video" ? model.extendResolutions ?? model.resolutions : model.resolutions;
25415
- }
25416
- function spriteVideoEstimate(model, p) {
25446
+ return model.workflowOptions?.[workflow]?.resolutions ?? model.resolutions;
25447
+ }
25448
+ function videoSeconds(model, workflow = "image-to-video") {
25449
+ return model.workflowOptions?.[workflow]?.seconds ?? model.seconds;
25450
+ }
25451
+ var INCLUDED_REFERENCE_TOKENS = 4096;
25452
+ var imageReferenceTokens = (size) => size && size[0] > 0 && size[1] > 0 ? 32 * Math.ceil(32 * Math.max(...size) / Math.min(...size)) : 1824;
25453
+ var clipReferenceTokens = (seconds2, resolution) => Math.ceil(Math.max(0, seconds2) * (resolution === "480p" ? 2700 : 6900));
25454
+ var audioReferenceTokens = (seconds2) => Math.ceil(Math.max(0, seconds2) * 80);
25455
+ var DEFAULT_GENERATED_REFERENCES = 2;
25456
+ function billedVideoSeconds(model, workflow, seconds2) {
25457
+ const s = videoSeconds(model, workflow).fractional ? seconds2 : Math.round(seconds2);
25458
+ if (workflow === "3d-to-video") return Math.max(5, s);
25459
+ return workflow === "lip-sync" && s > 15 ? s * 1.2 : s;
25460
+ }
25461
+ function videoCreditsPerSecond(model, p) {
25462
+ const override = model.workflowOptions?.[p.workflow ?? "image-to-video"]?.creditsPerSecond?.[p.resolution];
25463
+ if (override !== void 0) return override;
25417
25464
  const byMode = model.estimatedCreditsPerSecond[p.resolution] ?? model.estimatedCreditsPerSecond[model.resolutions[0]];
25418
- const perSecond = byMode[p.mode] ?? byMode[model.modes[0]] ?? 0;
25419
- return Math.max(1, Math.ceil(perSecond * Math.round(p.seconds) * UNITS_PER_CREDIT));
25465
+ return byMode[p.mode] ?? byMode[model.modes[0]] ?? 0;
25466
+ }
25467
+ function spriteVideoEstimate(model, p, extras2 = {}) {
25468
+ const seconds2 = billedVideoSeconds(model, p.workflow ?? "image-to-video", p.seconds);
25469
+ const beyond = Math.max(0, (extras2.referenceTokens ?? 0) - INCLUDED_REFERENCE_TOKENS) * 2e-3 + (extras2.generatedReferences ?? 0) * 3;
25470
+ return Math.max(1, Math.ceil((videoCreditsPerSecond(model, p) * seconds2 + beyond) * UNITS_PER_CREDIT));
25420
25471
  }
25421
25472
 
25422
25473
  // node_modules/zod/v4/classic/external.js
@@ -46347,55 +46398,214 @@ var MUSIC_MODELS = [{ id: "lyria-3.5", label: "Lyria 3.5" }];
46347
46398
  var SPEECH_VOICES = ["Kore", "Puck", "Charon", "Fenrir", "Aoede", "Leda", "Orus", "Zephyr"];
46348
46399
  var base = { name: external_exports.string().trim().min(1).max(80), prompt: external_exports.string().trim().min(3).max(4e3) };
46349
46400
  var blob = external_exports.string().regex(/^[a-f0-9]{64}$/);
46401
+ var cameraKeyframe = external_exports.object({
46402
+ /** When, from 0 at the start of the clip to 1 at its end. */
46403
+ time: external_exports.number().min(0).max(1),
46404
+ /** Degrees around the subject; signed full turns are kept. */
46405
+ azimuth: external_exports.number().min(-360 * MAX_CAMERA_TURNS).max(360 * MAX_CAMERA_TURNS),
46406
+ /** Degrees above (positive) or below the subject. */
46407
+ elevation: external_exports.number().min(-90).max(90),
46408
+ /** Distance from the subject in scene units. */
46409
+ distance: external_exports.number().gt(0).max(100)
46410
+ });
46350
46411
  var videoAsset = external_exports.object({
46351
- ...base,
46412
+ name: base.name,
46413
+ /** Lip-sync and relight take no prompt, and camera-controls, insert-video, recast and 3d-to-video may leave it out. */
46414
+ prompt: external_exports.string().trim().max(4e3).default(""),
46352
46415
  type: external_exports.literal("video"),
46353
46416
  model: external_exports.enum(SPRITE_VIDEO_MODEL_IDS).default(DEFAULT_SPRITE_VIDEO_MODEL),
46354
46417
  workflow: external_exports.enum(VIDEO_WORKFLOWS).default("image-to-video"),
46355
- /** First frame. */
46418
+ /** First frame; for lip-sync the face that speaks. */
46356
46419
  reference: blob.optional(),
46357
46420
  /** Last frame. */
46358
46421
  endReference: blob.optional(),
46359
- /** The saved clip to extend. */
46422
+ /** A frame reference-to-video reaches at middleSeconds, between its first and last frames. */
46423
+ middleReference: blob.optional(),
46424
+ middleSeconds: external_exports.number().gt(0).lt(15).optional(),
46425
+ /** The saved clip to extend, insert a scene into, recast, relight, or turn from a 3D render into footage. */
46360
46426
  source: blob.optional(),
46361
- /** Saved audio pinned as the soundtrack (at least two seconds of video). */
46427
+ /** Saved audio pinned as the soundtrack (at least two seconds of video), or the speech a lip-sync follows. */
46362
46428
  soundtrack: blob.optional(),
46363
- /** Saved audio guiding an extension's new sound; name each clip's role in the prompt. */
46429
+ /** Saved audio guiding an extension's new sound or a reference-to-video clip; name each clip's role in the prompt. */
46364
46430
  audioReferences: external_exports.array(blob).max(MAX_AUDIO_REFERENCES).default([]),
46365
- seconds: external_exports.number().int().min(1).max(15).default(5),
46366
- resolution: external_exports.enum(VIDEO_RESOLUTIONS).default("480p"),
46367
- mode: external_exports.enum(["cost", "speed", "quality"]).default("cost"),
46368
- /** A clip written from text (default 16:9), or an extension cropped to a new canvas ("auto" keeps the source's). */
46431
+ /** Images whose subjects or look the clip keeps, named Image 1, Image 2… in the prompt; for recast, one photo per new person. */
46432
+ references: external_exports.array(blob).max(MAX_VIDEO_REFERENCE_IMAGES).optional(),
46433
+ /** Saved clips guiding motion, named Video 1, Video 2… in the prompt. */
46434
+ referenceVideos: external_exports.array(blob).max(MAX_REFERENCE_VIDEOS).optional(),
46435
+ /** The camera's path around the reference image: 2–12 keyframes. */
46436
+ camera: external_exports.array(cameraKeyframe).min(2).max(MAX_CAMERA_KEYFRAMES).optional(),
46437
+ /** Seconds into the source where the inserted scene starts, and where the source resumes after it. */
46438
+ insertAt: external_exports.number().min(1.625).max(60).optional(),
46439
+ resumeAt: external_exports.number().gt(1.625).max(60).optional(),
46440
+ /** Match the inserted scene's colors to the source (fal's default). */
46441
+ colorMatch: external_exports.boolean().optional(),
46442
+ /** Transcribe the speech to guide a lip-sync. */
46443
+ transcribe: external_exports.boolean().optional(),
46444
+ /** Recast: more natural skin and blending of the new people. */
46445
+ enhanceRealism: external_exports.boolean().optional(),
46446
+ /** Recast: the user confirmed that each person in the photos agreed to appear in the video. */
46447
+ subjectConsent: external_exports.literal(true).optional(),
46448
+ /** Relight: a sphere render showing the light to apply. */
46449
+ lighting: blob.optional(),
46450
+ /** 3d-to-video without reference images: how many it may draw itself (default 2). */
46451
+ generatedReferences: external_exports.number().int().min(1).max(8).optional(),
46452
+ style: external_exports.enum(VIDEO_STYLES).optional(),
46453
+ /** VHS tape wear. */
46454
+ damage: external_exports.enum(VHS_DAMAGE_LEVELS).optional(),
46455
+ /** Defaults to 5, within the workflow's range; lip-sync, recast, relight and 3d-to-video take their input's length. */
46456
+ seconds: external_exports.number().min(0.5).max(900).optional(),
46457
+ /** Defaults to the workflow's first resolution. */
46458
+ resolution: external_exports.enum(VIDEO_RESOLUTIONS).optional(),
46459
+ /** Defaults to the model's first mode. */
46460
+ mode: external_exports.enum(["cost", "speed", "quality"]).optional(),
46461
+ /** A clip written from text (default 16:9), or one cropped to a new canvas ("auto" keeps its inputs' shape). */
46369
46462
  aspectRatio: external_exports.enum(["auto", ...VIDEO_ASPECT_RATIOS]).optional(),
46370
46463
  promptExpansion: external_exports.enum(PROMPT_EXPANSION_MODES).default("disabled"),
46371
46464
  seed: external_exports.number().int().min(0).max(2 ** 32 - 1).optional(),
46372
46465
  /** An extension returns the whole clip, or only the new footage after a short overlap. */
46373
46466
  extendOutput: external_exports.enum(EXTEND_OUTPUTS).default("extended")
46374
- }).superRefine((p, ctx) => {
46467
+ });
46468
+ var ALL = VIDEO_WORKFLOWS;
46469
+ var TAKES = {
46470
+ reference: ["image-to-video", "reference-to-video", "lip-sync", "camera-controls", "style"],
46471
+ endReference: ["image-to-video", "reference-to-video"],
46472
+ middleReference: ["reference-to-video"],
46473
+ middleSeconds: ["reference-to-video"],
46474
+ source: ["extend-video", "insert-video", "recast", "relight", "3d-to-video"],
46475
+ soundtrack: ["image-to-video", "text-to-video", "lip-sync"],
46476
+ audioReferences: ["extend-video", "reference-to-video"],
46477
+ references: ["reference-to-video", "insert-video", "recast", "3d-to-video"],
46478
+ referenceVideos: ["reference-to-video", "insert-video"],
46479
+ camera: ["camera-controls"],
46480
+ insertAt: ["insert-video"],
46481
+ resumeAt: ["insert-video"],
46482
+ colorMatch: ["insert-video"],
46483
+ transcribe: ["lip-sync"],
46484
+ enhanceRealism: ["recast"],
46485
+ subjectConsent: ["recast"],
46486
+ lighting: ["relight"],
46487
+ generatedReferences: ["3d-to-video"],
46488
+ style: ["style"],
46489
+ damage: ["style"],
46490
+ aspectRatio: ["text-to-video", "extend-video", "reference-to-video", "relight", "style"],
46491
+ promptExpansion: ["image-to-video", "text-to-video", "extend-video", "reference-to-video", "camera-controls", "insert-video"],
46492
+ seed: ALL.filter((w) => w !== "3d-to-video"),
46493
+ extendOutput: ["extend-video"]
46494
+ };
46495
+ var NEEDS = {
46496
+ "lip-sync": ["reference", "soundtrack"],
46497
+ "camera-controls": ["reference", "camera"],
46498
+ "insert-video": ["source", "insertAt", "resumeAt"],
46499
+ recast: ["source", "references", "subjectConsent"],
46500
+ relight: ["source", "lighting"],
46501
+ "3d-to-video": ["source"],
46502
+ style: ["style"]
46503
+ };
46504
+ var LABELS = {
46505
+ reference: "a starting image",
46506
+ endReference: "an ending image",
46507
+ middleReference: "a middle image",
46508
+ middleSeconds: "middleSeconds",
46509
+ source: "a saved video",
46510
+ soundtrack: "a soundtrack",
46511
+ audioReferences: "audio references",
46512
+ references: "reference images",
46513
+ referenceVideos: "reference videos",
46514
+ camera: "camera keyframes",
46515
+ insertAt: "insertAt",
46516
+ resumeAt: "resumeAt",
46517
+ colorMatch: "colorMatch",
46518
+ transcribe: "transcribe",
46519
+ enhanceRealism: "enhanceRealism",
46520
+ lighting: "a lighting image",
46521
+ subjectConsent: "subjectConsent: true, confirming that each person in the photos agreed to appear in the video",
46522
+ generatedReferences: "generatedReferences",
46523
+ style: "a style",
46524
+ damage: "damage",
46525
+ aspectRatio: "an aspect ratio",
46526
+ promptExpansion: "prompt rewriting",
46527
+ seed: "a seed",
46528
+ extendOutput: "extendOutput"
46529
+ };
46530
+ var STUDIO_MESSAGES = {
46531
+ reference: "Only image-to-video starts from an image.",
46532
+ endReference: "Only image-to-video ends on an image.",
46533
+ source: "Only an extension starts from a saved video.",
46534
+ audioReferences: "Only an extension takes audio references.",
46535
+ extendOutput: "Only an extension can return the new footage alone."
46536
+ };
46537
+ var PROMPT = { "camera-controls": "optional", "insert-video": "optional", recast: "optional", "3d-to-video": "optional", "lip-sync": "none", relight: "none" };
46538
+ var given = (value) => value !== void 0 && !(Array.isArray(value) && !value.length);
46539
+ function videoChecks(p, ctx) {
46375
46540
  const model = spriteVideoModel(p.model);
46376
46541
  const issue2 = (path, message) => ctx.addIssue({ code: "custom", message, path: [path] });
46377
46542
  if (!model.workflows.includes(p.workflow)) return issue2("workflow", "Choose a way of making video supported by the video model.");
46378
- const extend2 = p.workflow === "extend-video";
46379
- if (p.seconds < model.seconds.min || p.seconds > model.seconds.max) issue2("seconds", "Choose a duration supported by the video model.");
46380
- if (!videoResolutions(model, p.workflow).includes(p.resolution)) issue2("resolution", "Choose a resolution supported by the video model.");
46381
- if (!model.modes.includes(p.mode)) issue2("mode", "Choose a mode supported by the video model.");
46382
- if (p.workflow === "image-to-video") {
46543
+ const w = p.workflow;
46544
+ const extend2 = w === "extend-video";
46545
+ const range = videoSeconds(model, w);
46546
+ const seconds2 = p.seconds ?? defaultSeconds(model, w);
46547
+ const resolution = p.resolution ?? videoResolutions(model, w)[0];
46548
+ const prompt = PROMPT[w];
46549
+ if (prompt === "none" && p.prompt) issue2("prompt", `${w} takes no prompt; leave it out.`);
46550
+ if (!prompt && p.prompt.length < 3) issue2("prompt", "Describe the video in at least 3 characters.");
46551
+ if ((w === "recast" || w === "3d-to-video") && p.prompt.length > 2e3) issue2("prompt", `${w} takes a prompt of at most 2,000 characters.`);
46552
+ if (!lengthFromMedia(w) && (seconds2 < range.min || seconds2 > range.max || !range.fractional && !Number.isInteger(seconds2))) issue2("seconds", "Choose a duration supported by the video model.");
46553
+ if (!videoResolutions(model, w).includes(resolution)) issue2("resolution", "Choose a resolution supported by the video model.");
46554
+ if (p.mode && !model.modes.includes(p.mode)) issue2("mode", "Choose a mode supported by the video model.");
46555
+ for (const [field, workflows] of Object.entries(TAKES)) {
46556
+ if (!given(p[field]) || workflows.includes(w)) continue;
46557
+ if (field === "promptExpansion" && p.promptExpansion === "disabled" || field === "extendOutput" && p.extendOutput === "extended") continue;
46558
+ const studio = w === "image-to-video" || w === "text-to-video" || extend2;
46559
+ if (field === "aspectRatio" && w === "image-to-video") issue2(field, "A video made from an image keeps the image's shape.");
46560
+ else if (field === "soundtrack" && extend2) issue2(field, "An extension keeps its source's soundtrack; add audio references instead.");
46561
+ else if (studio && STUDIO_MESSAGES[field]) issue2(field, STUDIO_MESSAGES[field]);
46562
+ else issue2(field, `${w} doesn't take ${LABELS[field] ?? field}.`);
46563
+ }
46564
+ for (const field of NEEDS[w] ?? []) if (!given(p[field])) issue2(field, `${w} needs ${LABELS[field] ?? field}.`);
46565
+ if (w === "image-to-video") {
46383
46566
  if (!p.reference && !(p.endReference && model.endFrameOnly)) issue2("reference", "Choose a starting image.");
46384
46567
  if (p.endReference && !model.lastFrame) issue2("endReference", "This video model can't end on a chosen image.");
46385
- } else {
46386
- if (p.reference) issue2("reference", "Only image-to-video starts from an image.");
46387
- if (p.endReference) issue2("endReference", "Only image-to-video ends on an image.");
46388
- }
46389
- if (extend2 !== Boolean(p.source)) issue2("source", extend2 ? "Choose a saved video to extend." : "Only an extension starts from a saved video.");
46390
- if (p.aspectRatio && p.workflow === "image-to-video") issue2("aspectRatio", "A video made from an image keeps the image's shape.");
46391
- if (p.aspectRatio === "auto" && p.workflow === "text-to-video") issue2("aspectRatio", "Choose an aspect ratio for a video written from text.");
46392
- if (p.soundtrack && (!model.audio || extend2)) issue2("soundtrack", extend2 ? "An extension keeps its source's soundtrack; add audio references instead." : "This video model can't use a soundtrack.");
46393
- if (p.soundtrack && p.seconds < 2) issue2("seconds", "A video with a soundtrack must be at least 2 seconds long.");
46394
- if (p.audioReferences.length && (!model.audio || !extend2)) issue2("audioReferences", "Only an extension takes audio references.");
46568
+ }
46569
+ if (extend2 && !p.source) issue2("source", "Choose a saved video to extend.");
46570
+ if (p.aspectRatio === "auto" && (w === "text-to-video" || w === "style")) issue2("aspectRatio", "Choose an aspect ratio for a video written from text.");
46571
+ if (w === "style" && p.aspectRatio && p.reference) issue2("aspectRatio", "A styled video made from an image keeps the image's shape.");
46572
+ if (p.damage && p.style !== "vhs") issue2("damage", "Only the VHS style takes tape damage.");
46573
+ if (p.soundtrack && !model.audio) issue2("soundtrack", "This video model can't use a soundtrack.");
46574
+ if (p.soundtrack && w !== "lip-sync" && seconds2 < 2) issue2("seconds", "A video with a soundtrack must be at least 2 seconds long.");
46575
+ if (p.audioReferences.length && !model.audio) issue2("audioReferences", "Only an extension takes audio references.");
46395
46576
  if (p.promptExpansion !== "disabled" && !model.promptExpansion?.includes(p.promptExpansion)) issue2("promptExpansion", "This video model doesn't rewrite prompts.");
46396
46577
  if (p.seed !== void 0 && !model.seed) issue2("seed", "This video model doesn't take a seed.");
46397
- if (p.extendOutput !== "extended" && !extend2) issue2("extendOutput", "Only an extension can return the new footage alone.");
46398
- });
46578
+ if (p.seed !== void 0 && p.seed >= 2 ** 31 && (w === "lip-sync" || w === "insert-video")) issue2("seed", `${w} takes a seed below 2,147,483,648.`);
46579
+ const references = p.references ?? [];
46580
+ const unique = (shas2) => new Set(shas2).size === shas2.length;
46581
+ if (!unique(references) || !unique(p.referenceVideos ?? []) || !unique(p.audioReferences)) issue2("references", "Choose each reference once.");
46582
+ if (w === "reference-to-video") {
46583
+ if (references.length + (p.referenceVideos?.length ?? 0) + p.audioReferences.length > MAX_REFERENCE_FILES) issue2("references", `reference-to-video takes at most ${MAX_REFERENCE_FILES} reference files in all.`);
46584
+ if (!references.length && !p.referenceVideos?.length && !p.audioReferences.length && !p.reference && !p.endReference) issue2("references", "reference-to-video needs reference images, videos or audio, or a first or last frame.");
46585
+ if (Boolean(p.middleReference) !== (p.middleSeconds !== void 0)) issue2("middleSeconds", "A middle image needs middleSeconds, and middleSeconds a middle image.");
46586
+ if (p.middleReference && (!p.reference || !p.endReference)) issue2("middleReference", "A middle image needs the starting and ending images too.");
46587
+ if (p.middleReference && !["480p", "768p"].includes(resolution)) issue2("middleReference", "A middle image needs 480p or 768p.");
46588
+ if (p.middleSeconds !== void 0 && p.middleSeconds >= seconds2) issue2("middleSeconds", "Put the middle image before the end of the clip.");
46589
+ }
46590
+ if (w === "recast" && references.length > MAX_RECAST_PEOPLE) issue2("references", `recast takes at most ${MAX_RECAST_PEOPLE} photos, one per person.`);
46591
+ if (w === "3d-to-video" && references.length > MAX_3D_REFERENCES) issue2("references", `3d-to-video takes at most ${MAX_3D_REFERENCES} reference images.`);
46592
+ if (w === "3d-to-video" && references.length && p.generatedReferences !== void 0) issue2("generatedReferences", "3d-to-video only draws its own references when you give none.");
46593
+ if (w === "insert-video" && p.insertAt !== void 0 && p.resumeAt !== void 0 && p.resumeAt < p.insertAt) issue2("resumeAt", "Resume the source at or after the point where the new scene starts.");
46594
+ if (p.camera) {
46595
+ const times = p.camera.map((k) => k.time);
46596
+ if (times.some((t, i2) => i2 && t <= times[i2 - 1])) issue2("camera", "Order the camera keyframes by time, each later than the one before.");
46597
+ const travel = p.camera.slice(1).reduce((sum, k, i2) => sum + Math.abs(k.azimuth - p.camera[i2].azimuth), 0);
46598
+ if (travel > 360 * MAX_CAMERA_TURNS) issue2("camera", `The camera may travel at most ${MAX_CAMERA_TURNS} full turns around the subject.`);
46599
+ }
46600
+ }
46601
+ var defaultSeconds = (model, workflow) => {
46602
+ const r = videoSeconds(model, workflow);
46603
+ return Math.min(r.max, Math.max(r.min, 5));
46604
+ };
46605
+ var videoDefaults = (p) => {
46606
+ const model = spriteVideoModel(p.model);
46607
+ return { ...p, seconds: p.seconds ?? defaultSeconds(model, p.workflow), resolution: p.resolution ?? videoResolutions(model, p.workflow)[0], mode: p.mode ?? model.modes[0] };
46608
+ };
46399
46609
  var IMAGE_ASPECT_RATIOS = ["1:1", "3:2", "2:3", "16:9", "9:16"];
46400
46610
  var MAX_IMAGE_REFERENCES = 4;
46401
46611
  var MAX_MASK_REGIONS = 8;
@@ -46416,7 +46626,7 @@ var imageChecks = (p, ctx) => {
46416
46626
  };
46417
46627
  var assetRequest = external_exports.discriminatedUnion("type", [
46418
46628
  imageAsset.superRefine(imageChecks),
46419
- videoAsset,
46629
+ videoAsset.superRefine(videoChecks).transform(videoDefaults),
46420
46630
  external_exports.object({ ...base, type: external_exports.literal("music"), model: external_exports.literal("lyria-3.5").default("lyria-3.5") }),
46421
46631
  external_exports.object({ ...base, type: external_exports.literal("speech"), model: external_exports.enum(["gemini-3.8-flash-tts", "gemini-3.8-flash-lite-tts", ...ELEVENLABS_SPEECH_MODEL_IDS]).default("gemini-3.8-flash-lite-tts"), voice: external_exports.string().trim().max(80).default(""), delivery: external_exports.string().trim().max(600).default("") }).superRefine((p, ctx) => {
46422
46632
  if (p.voice && (isElevenLabsSpeech(p.model) ? !/^[a-zA-Z0-9]{20}$/.test(p.voice) : !SPEECH_VOICES.includes(p.voice))) ctx.addIssue({ code: "custom", message: "Choose a voice supported by the speech provider.", path: ["voice"] });
@@ -46432,9 +46642,18 @@ var uploadedMedia = external_exports.object({ name: base.name, prompt: external_
46432
46642
  var savedAssetOptions = external_exports.union([assetRequest, libraryAudio, uploadedMedia]);
46433
46643
  var speechCharacters = (text2) => Array.from(text2).length;
46434
46644
  var imageEstimate = (p) => Math.ceil((130 + 40 * p.references.length) * spriteImageModel(p.model).relCost);
46645
+ function videoExtras(p, facts = {}) {
46646
+ const w = p.workflow;
46647
+ if (w !== "reference-to-video" && w !== "insert-video" && w !== "3d-to-video") return {};
46648
+ const images = (p.references ?? []).reduce((sum, sha2) => sum + imageReferenceTokens(facts.size?.(sha2)), 0);
46649
+ if (w === "3d-to-video") return { referenceTokens: images + clipReferenceTokens(p.seconds, p.resolution), generatedReferences: p.references?.length ? 0 : p.generatedReferences ?? DEFAULT_GENERATED_REFERENCES };
46650
+ const clips = Math.min(15, (p.referenceVideos ?? []).reduce((sum, sha2) => sum + Math.min(p.seconds, facts.seconds?.(sha2) ?? p.seconds), 0));
46651
+ const audio = Math.min(15, p.audioReferences.reduce((sum, sha2) => sum + (facts.seconds?.(sha2) ?? 15), 0));
46652
+ return { referenceTokens: images + clipReferenceTokens(clips, p.resolution) + (w === "reference-to-video" ? audioReferenceTokens(audio) : 0) };
46653
+ }
46435
46654
  function assetEstimate(p, now = Date.now()) {
46436
46655
  if (p.type === "image") return imageEstimate(p);
46437
- if (p.type === "video") return spriteVideoEstimate(spriteVideoModel(p.model), p);
46656
+ if (p.type === "video") return spriteVideoEstimate(spriteVideoModel(p.model), p, videoExtras(p));
46438
46657
  if (p.type === "music") return 80;
46439
46658
  const units = (credits) => Math.max(1, Math.ceil(credits * UNITS_PER_CREDIT));
46440
46659
  if (p.type === "sfx") return units(Math.max(0, p.seconds) * 0.2);
@@ -47608,7 +47827,42 @@ async function changeVideoSubtitles(client, id2, options) {
47608
47827
  import { mkdir as mkdir8, readFile as readFile9, stat as stat4, writeFile as writeFile8 } from "node:fs/promises";
47609
47828
  import { dirname as dirname7, join as join10, resolve as resolve10 } from "node:path";
47610
47829
  import { setTimeout as delay3 } from "node:timers/promises";
47611
- var extras = { image: ["aspect-ratio", "transparent", "references", "mask"], video: ["workflow", "reference", "end-reference", "source", "soundtrack", "audio-references", "seconds", "resolution", "mode", "aspect-ratio", "prompt-expansion", "seed", "extend-output"], music: [], speech: ["voice", "delivery"], sfx: ["seconds", "loop", "prompt-influence"] };
47830
+ var extras = {
47831
+ image: ["aspect-ratio", "transparent", "references", "mask"],
47832
+ video: [
47833
+ "workflow",
47834
+ "reference",
47835
+ "end-reference",
47836
+ "source",
47837
+ "soundtrack",
47838
+ "audio-references",
47839
+ "seconds",
47840
+ "resolution",
47841
+ "mode",
47842
+ "aspect-ratio",
47843
+ "prompt-expansion",
47844
+ "seed",
47845
+ "extend-output",
47846
+ "references",
47847
+ "reference-videos",
47848
+ "middle-reference",
47849
+ "middle-seconds",
47850
+ "camera",
47851
+ "insert-at",
47852
+ "resume-at",
47853
+ "color-match",
47854
+ "transcribe",
47855
+ "enhance-realism",
47856
+ "subject-consent",
47857
+ "lighting",
47858
+ "generated-references",
47859
+ "style",
47860
+ "damage"
47861
+ ],
47862
+ music: [],
47863
+ speech: ["voice", "delivery"],
47864
+ sfx: ["seconds", "loop", "prompt-influence"]
47865
+ };
47612
47866
  var list2 = (items) => items.length < 2 ? items.join("") : `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
47613
47867
  function accepted(type) {
47614
47868
  if (type === "music") return "Music accepts only --name, --prompt and --model; the model chooses the length, so fit it during the edit.";
@@ -47628,17 +47882,21 @@ var regions = (value) => value?.split(";").filter((region) => region.trim()).map
47628
47882
  if (rest.length || [x2, y, width, height].some((n) => !Number.isFinite(n))) throw new Error(`--mask regions are x,y,width,height in fractions of Image 1, separated by semicolons; "${region.trim()}" is not.`);
47629
47883
  return { x: x2, y, width, height };
47630
47884
  });
47885
+ var keyframes = (value) => value?.split(";").filter((frame) => frame.trim()).map((frame) => {
47886
+ const [time3, azimuth, elevation, distance, ...rest] = frame.split(",").map((n) => n.trim() ? Number(n) : NaN);
47887
+ if (rest.length || [time3, azimuth, elevation, distance].some((n) => !Number.isFinite(n))) throw new Error(`--camera keyframes are time,azimuth,elevation,distance (time 0\u20131, angles in degrees), separated by semicolons; "${frame.trim()}" is not.`);
47888
+ return { time: time3, azimuth, elevation, distance };
47889
+ });
47631
47890
  function assetFromFlags(flags) {
47632
47891
  if (flags.type === "image" && flags.model) throw new Error("Image models are managed by staff. Omit --model.");
47633
47892
  for (const name2 of Object.values(extras).flat()) {
47634
47893
  const value = flags[name2];
47635
47894
  if (value !== void 0 && value !== false && !extras[flags.type]?.includes(name2)) throw new Error(`--${name2} is not an option for ${flags.type} assets. ${accepted(flags.type)}`.trim());
47636
47895
  }
47896
+ if (flags.type !== "video" && flags.prompt === void 0) throw new Error(`--prompt is required for ${flags.type} assets.`);
47637
47897
  const base2 = { type: flags.type, name: flags.name, prompt: flags.prompt, model: flags.model };
47638
47898
  if (flags.type === "image") return parseAsset({ ...base2, aspectRatio: flags["aspect-ratio"], transparent: flags.transparent, references: shas(flags.references), mask: regions(flags.mask) });
47639
47899
  if (flags.type === "video") {
47640
- const model = spriteVideoModel(flags.model);
47641
- const workflow = flags.workflow ?? "image-to-video";
47642
47900
  return parseAsset({
47643
47901
  ...base2,
47644
47902
  workflow: flags.workflow,
@@ -47647,9 +47905,24 @@ function assetFromFlags(flags) {
47647
47905
  source: flags.source,
47648
47906
  soundtrack: flags.soundtrack,
47649
47907
  audioReferences: shas(flags["audio-references"]),
47650
- seconds: flags.seconds ?? Math.max(5, model.seconds.min),
47651
- resolution: flags.resolution ?? videoResolutions(model, workflow)[0],
47652
- mode: flags.mode ?? model.modes[0],
47908
+ references: shas(flags.references),
47909
+ referenceVideos: shas(flags["reference-videos"]),
47910
+ middleReference: flags["middle-reference"],
47911
+ middleSeconds: flags["middle-seconds"],
47912
+ camera: keyframes(flags.camera),
47913
+ insertAt: flags["insert-at"],
47914
+ resumeAt: flags["resume-at"],
47915
+ colorMatch: flags["color-match"],
47916
+ transcribe: flags.transcribe,
47917
+ enhanceRealism: flags["enhance-realism"],
47918
+ subjectConsent: flags["subject-consent"] || void 0,
47919
+ lighting: flags.lighting,
47920
+ generatedReferences: flags["generated-references"],
47921
+ style: flags.style,
47922
+ damage: flags.damage,
47923
+ seconds: flags.seconds,
47924
+ resolution: flags.resolution,
47925
+ mode: flags.mode,
47653
47926
  aspectRatio: flags["aspect-ratio"],
47654
47927
  promptExpansion: flags["prompt-expansion"],
47655
47928
  seed: flags.seed,
@@ -47703,8 +47976,13 @@ async function assetsFromBatchFile(file2, model) {
47703
47976
  ${errors.join("\n")}`);
47704
47977
  return requests;
47705
47978
  }
47979
+ var measured = (body) => body.type === "video" && (lengthFromMedia(body.workflow) || ["reference-to-video", "insert-video", "3d-to-video"].includes(body.workflow));
47980
+ async function estimateAsset(client, project, body) {
47981
+ if (measured(body)) return client.request(`${assetsPath(project)}/estimate`, { method: "POST", body });
47982
+ return { dryRun: true, request: body, estimatedCredits: toCredits(assetEstimate(body)) };
47983
+ }
47706
47984
  async function submitAsset(client, project, body, options) {
47707
- if (options["dry-run"]) return { dryRun: true, request: body, estimatedCredits: toCredits(assetEstimate(body)) };
47985
+ if (options["dry-run"]) return estimateAsset(client, project, body);
47708
47986
  if (!options.yes) throw new Error("Pass --yes to authorize spending credits, or --dry-run to validate and estimate without generation.");
47709
47987
  return client.request(`/api/v1/sprites/${encodeURIComponent(project)}/assets`, { method: "POST", body });
47710
47988
  }
@@ -47800,8 +48078,12 @@ async function generateAsset(client, project, body, options, signal) {
47800
48078
  var sumCredits = (amounts) => toCredits(amounts.reduce((total, credits) => total + (typeof credits === "number" ? toUnits(credits) : 0), 0));
47801
48079
  async function submitAssetBatch(client, project, requests, options, signal) {
47802
48080
  if (options["dry-run"]) {
47803
- const items2 = requests.map((request2) => ({ name: request2.name, type: request2.type, model: request2.model, estimatedCredits: toCredits(assetEstimate(request2)) }));
47804
- return { dryRun: true, items: items2, estimatedCredits: toCredits(requests.reduce((total2, request2) => total2 + assetEstimate(request2), 0)) };
48081
+ const items2 = [];
48082
+ for (const request2 of requests) {
48083
+ const { request: checked3, estimatedCredits } = await estimateAsset(client, project, request2);
48084
+ items2.push({ name: checked3.name, type: checked3.type, model: checked3.model, estimatedCredits });
48085
+ }
48086
+ return { dryRun: true, items: items2, estimatedCredits: sumCredits(items2.map((item) => item.estimatedCredits)) };
47805
48087
  }
47806
48088
  if (!options.yes) throw new Error("Pass --yes to authorize spending credits, or --dry-run to validate and estimate without generation.");
47807
48089
  const waiting = Boolean(options.wait || options.download);
@@ -49567,21 +49849,35 @@ Confirm code: ${device.user_code}`);
49567
49849
  })))).command("assets", (assets) => assets.add(defineCommand("list", { description: "List saved assets, their blob hashes, and configured generation models" }, (command) => command.flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" } }).args(id).action(async ({ args, flags, stdout, signal }) => out(stdout, await new Client(flags.url, signal).request(`${projectPath(args.id)}/assets`))))).add(defineCommand("generate", { description: "Generate an image, video, music, speech or sound-effect asset; --yes authorizes credits" }, (command) => command.flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" } }).args(id).flags(
49568
49850
  { name: "type", type: "string", required: true, choices: ASSET_TYPES },
49569
49851
  { name: "name", type: "string", required: true },
49570
- { name: "prompt", type: "string", required: true, description: "Description to generate, or verbatim transcript for speech" },
49852
+ { name: "prompt", type: "string", description: "Description to generate, or verbatim transcript for speech; video lip-sync and relight take none, and camera-controls, insert-video, recast and 3d-to-video may leave it out" },
49571
49853
  { name: "model", type: "string", choices: assetModelIds, description: "Model for the chosen asset type" },
49572
- { name: "aspect-ratio", type: "string", choices: [.../* @__PURE__ */ new Set([...IMAGE_ASPECT_RATIOS, "auto", ...VIDEO_ASPECT_RATIOS])], description: "Image: 1:1 (default), 3:2, 2:3, 16:9 or 9:16; a masked edit keeps Image 1's shape. Video from text: 21:9\u20139:16 (default 16:9). Extension: auto (default, the source's) or a ratio to crop to" },
49854
+ { name: "aspect-ratio", type: "string", choices: [.../* @__PURE__ */ new Set([...IMAGE_ASPECT_RATIOS, "auto", ...VIDEO_ASPECT_RATIOS])], description: "Image: 1:1 (default), 3:2, 2:3, 16:9 or 9:16; a masked edit keeps Image 1's shape. Video from text or a style without --reference: 21:9\u20139:16 (default 16:9). Extension, reference-to-video and relight: auto (default, the inputs' shape) or a ratio to crop to" },
49573
49855
  { name: "transparent", type: "boolean", description: "Image: transparent background" },
49574
- { name: "references", type: "string", description: "Image: up to 4 comma-separated blob SHAs of this project's images or sprite art to follow, such as a character sheet; refer to them as Image 1, Image 2\u2026 in the prompt" },
49856
+ { name: "references", type: "string", description: "Comma-separated blob SHAs of this project's images or sprite art, named Image 1, Image 2\u2026 in the prompt. Image: up to 4 to follow, such as a character sheet. Video: subjects or looks to keep (reference-to-video and insert-video up to 9, 3d-to-video up to 8), or recast's new people, one photo each (up to 4)" },
49575
49857
  { name: "mask", type: "string", description: "Image: edit only these rectangles of Image 1 (the first --references image), as x,y,width,height fractions of its size from the top left, up to 8 separated by semicolons; the rest is kept" },
49576
- { name: "workflow", type: "string", choices: VIDEO_WORKFLOWS, description: "Video: image-to-video (default), text-to-video or extend-video, as the model's videoProfiles allow" },
49577
- { name: "reference", type: "string", description: "Video: starting image blob SHA from this project's assets or sprites" },
49858
+ { name: "workflow", type: "string", choices: VIDEO_WORKFLOWS, description: "Video: image-to-video (default), text-to-video or extend-video; H3 Max also reference-to-video, lip-sync, camera-controls, insert-video, recast, relight, 3d-to-video and style, as the model's videoProfiles allow" },
49859
+ { name: "reference", type: "string", description: "Video: starting image blob SHA from this project's assets or sprites; the speaking face for lip-sync, the image the camera moves around for camera-controls" },
49578
49860
  { name: "end-reference", type: "string", description: "Video: ending image blob SHA; models with endFrameOnly accept it without --reference" },
49579
- { name: "source", type: "string", description: "Extension: blob SHA of a video asset in this project to extend" },
49580
- { name: "soundtrack", type: "string", description: "Video from an image or text: blob SHA of a music, speech or SFX asset pinned as the soundtrack (2 s or longer)" },
49581
- { name: "audio-references", type: "string", description: "Extension: up to 3 comma-separated audio asset blob SHAs (2\u201315 s each, 15 s total) guiding the new sound; refer to them as Audio 1, Audio 2\u2026 in the prompt" },
49582
- { name: "seconds", type: "number", description: "Video or SFX duration in seconds (default 5); extensions add this many seconds; SFX supports 0.5\u201330" },
49583
- { name: "resolution", type: "string", choices: VIDEO_RESOLUTIONS, description: "Video resolution supported by the selected model and workflow (2k for extensions)" },
49584
- { name: "mode", type: "string", choices: ["cost", "speed", "quality"], description: "Video generation mode supported by the selected model" },
49861
+ { name: "middle-reference", type: "string", description: "Reference-to-video: image blob SHA reached at --middle-seconds; needs --reference and --end-reference, 480p or 768p" },
49862
+ { name: "middle-seconds", type: "number", description: "Reference-to-video: when the middle image is reached, in seconds" },
49863
+ { name: "source", type: "string", description: "Blob SHA of a video asset in this project: the clip to extend, insert a scene into, recast or relight, or the 3D render 3d-to-video turns into footage" },
49864
+ { name: "soundtrack", type: "string", description: "Video from an image or text: blob SHA of a music, speech or SFX asset pinned as the soundtrack (2 s or longer). Lip-sync: the speech to say (5 s\u201315 min), which sets the length" },
49865
+ { name: "audio-references", type: "string", description: "Extension or reference-to-video: up to 3 comma-separated audio asset blob SHAs (2\u201315 s each, 15 s total) guiding the sound; refer to them as Audio 1, Audio 2\u2026 in the prompt" },
49866
+ { name: "reference-videos", type: "string", description: "Reference-to-video or insert-video: up to 3 comma-separated video asset blob SHAs (2\u201315 s each, 15 s total) guiding motion, named Video 1, Video 2\u2026 in the prompt" },
49867
+ { name: "camera", type: "string", description: 'Camera-controls: 2\u201312 keyframes time,azimuth,elevation,distance (time 0\u20131, angles in degrees, elevation \u221290\u201390) separated by semicolons, e.g. "0,0,0,1;1,90,15,1.2"' },
49868
+ { name: "insert-at", type: "number", description: "Insert-video: seconds into --source where the new scene starts (1.625\u201360)" },
49869
+ { name: "resume-at", type: "number", description: "Insert-video: seconds into --source where it resumes after the scene" },
49870
+ { name: "color-match", type: "boolean", description: "Insert-video: match the scene's colors to the source (default on; --no-color-match turns it off)" },
49871
+ { name: "transcribe", type: "boolean", description: "Lip-sync: transcribe the speech to guide the mouth" },
49872
+ { name: "enhance-realism", type: "boolean", description: "Recast: more natural skin and blending of the new people" },
49873
+ { name: "subject-consent", type: "boolean", description: "Recast: confirm that each person in the --references photos agreed to appear in the video" },
49874
+ { name: "lighting", type: "string", description: "Relight: blob SHA of an image of a sphere lit as the video should be" },
49875
+ { name: "generated-references", type: "number", description: "3d-to-video without --references: how many reference images it may draw, 1\u20138 (default 2)" },
49876
+ { name: "style", type: "string", choices: VIDEO_STYLES, description: "Style workflow: the preset look, at 768p for 5\u201315 whole seconds" },
49877
+ { name: "damage", type: "string", choices: VHS_DAMAGE_LEVELS, description: "VHS style: tape damage (fal's default medium)" },
49878
+ { name: "seconds", type: "number", description: "Video or SFX duration in seconds (default 5); H3 takes fractions (6.5); extensions add this many seconds, insert-video's scene is 5\u201313; lip-sync, recast, relight and 3d-to-video take their input's length; SFX supports 0.5\u201330" },
49879
+ { name: "resolution", type: "string", choices: VIDEO_RESOLUTIONS, description: "Video resolution supported by the selected model and workflow (default its first)" },
49880
+ { name: "mode", type: "string", choices: ["cost", "speed", "quality"], description: "Video generation mode supported by the selected model (default its first)" },
49585
49881
  { name: "prompt-expansion", type: "string", choices: PROMPT_EXPANSION_MODES, description: "Video: how much the model may rewrite the prompt (default disabled); extensions treat any other value as on" },
49586
49882
  { name: "seed", type: "number", description: "Video: seed for repeatable results, where the model takes one" },
49587
49883
  { name: "extend-output", type: "string", choices: EXTEND_OUTPUTS, description: "Extension: extended (default, source plus new footage) or continuation (a 1.625 s overlap, then the new footage)" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docyrus/animaguru",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Agent-friendly AnimaGuru CLI built with CrustJS",
5
5
  "type": "module",
6
6
  "bin": {
@@ -61,7 +61,7 @@ animaguru assets generate PROJECT_ID --type speech --name "Welcome" \
61
61
  --prompt "Welcome, traveler." --voice Kore --delivery "Warm and welcoming" --yes
62
62
  ```
63
63
 
64
- Images use the staff-managed route automatically. Use `--model` to select a model for video, music, speech or sound effects; discover the choices with `catalog` and the server's configured subset with `assets list`. Asset defaults are per type, independently of sprite project defaults. Image backgrounds are retained unless `--transparent` is supplied. CLI 0.17.0 adds `--references`, `--mask` and the 16:9 and 9:16 image shapes. `--references SHA,SHA` (up to four) makes an image follow this project's saved images, uploads or sprite art. To keep a recurring character consistent, generate a character sheet once and reference its `file` SHA in every later image; refer to references in the prompt as Image 1, Image 2 and so on, and say what to take from each. `--aspect-ratio` is 1:1, 3:2, 2:3, 16:9 or 9:16; use 16:9 or 9:16 for video scenes. `--mask x,y,width,height[;…]` (up to eight rectangles, in fractions of Image 1 from the top left) edits only those parts of Image 1, the first reference, and keeps the rest and Image 1's shape. Look at the image before choosing the rectangles. Videos require an image asset's `file` SHA from `assets list`, or a character's `art` SHA from `projects get`, in the same project. Validate video options against the selected model's `videoProfiles` in `catalog`; omitted video settings use five seconds and that provider's first resolution/mode. MiniMax H3 Max and H3 Max Turbo (`fal/minimax-h3-max`, `fal/minimax-h3-max-turbo`) also take `--workflow text-to-video` (with `--aspect-ratio`) and `--workflow extend-video` (with `--source VIDEO_SHA`, 2K resolution, `--extend-output`, and up to three comma-separated `--audio-references`). From images, they accept `--end-reference` with or without `--reference` and a `--soundtrack AUDIO_SHA`. All workflows take `--prompt-expansion` and `--seed`. These need CLI 0.8.0 or newer. For Gemini speech, `--prompt` is spoken verbatim and `--delivery` is a separate instruction. Music uses Lyria; the Gemini TTS models are for speech.
64
+ Images use the staff-managed route automatically. Use `--model` to select a model for video, music, speech or sound effects; discover the choices with `catalog` and the server's configured subset with `assets list`. Asset defaults are per type, independently of sprite project defaults. Image backgrounds are retained unless `--transparent` is supplied. CLI 0.17.0 adds `--references`, `--mask` and the 16:9 and 9:16 image shapes. `--references SHA,SHA` (up to four) makes an image follow this project's saved images, uploads or sprite art. To keep a recurring character consistent, generate a character sheet once and reference its `file` SHA in every later image; refer to references in the prompt as Image 1, Image 2 and so on, and say what to take from each. `--aspect-ratio` is 1:1, 3:2, 2:3, 16:9 or 9:16; use 16:9 or 9:16 for video scenes. `--mask x,y,width,height[;…]` (up to eight rectangles, in fractions of Image 1 from the top left) edits only those parts of Image 1, the first reference, and keeps the rest and Image 1's shape. Look at the image before choosing the rectangles. Videos require an image asset's `file` SHA from `assets list`, or a character's `art` SHA from `projects get`, in the same project. Validate video options against the selected model's `videoProfiles` in `catalog`; omitted video settings use five seconds and that provider's first resolution/mode. MiniMax H3 Max and H3 Max Turbo (`fal/minimax-h3-max`, `fal/minimax-h3-max-turbo`) also take `--workflow text-to-video` (with `--aspect-ratio`) and `--workflow extend-video` (with `--source VIDEO_SHA`, 2K resolution, `--extend-output`, and up to three comma-separated `--audio-references`). From images, they accept `--end-reference` with or without `--reference` and a `--soundtrack AUDIO_SHA`. All workflows take `--prompt-expansion` and `--seed`. These need CLI 0.8.0 or newer. CLI 0.18.0 adds H3 Max's (not Turbo's) `--workflow reference-to-video` (keep a character across clips from up to nine `--references`, three `--reference-videos` and three `--audio-references`, named Image 1, Video 1, Audio 1 in the prompt; optional `--reference`, `--end-reference`, `--middle-reference` with `--middle-seconds`), `lip-sync` (the face in `--reference` says `--soundtrack` speech of 5 s–15 min; no `--prompt`; `--transcribe`), `camera-controls` (`--camera "time,azimuth,elevation,distance;…"`, 2–12 keyframes around `--reference`; without a prompt only the camera moves), `insert-video` (a 5–13 s scene inside `--source` from `--insert-at` to `--resume-at`, guided by `--references`/`--reference-videos`), `recast` (people in `--references` replace those in `--source`; pass `--subject-consent` only after each person agreed), `relight` (`--source` relit from a `--lighting` sphere image; no prompt), `3d-to-video` (a 3D render `--source` into footage, from `--references` or `--generated-references`) and `style` (`--style vhs|retro-toon-70s|low-poly|hand-drawn|16bit-pixel`, `--damage` for VHS, 768p, 5–15 whole seconds). Lip-sync, recast, relight and 3d-to-video take their input's length; their dry runs, like reference-to-video's and insert-video's, are server estimates because the price depends on the files. H3 takes fractional `--seconds` (6.5). For Gemini speech, `--prompt` is spoken verbatim and `--delivery` is a separate instruction. Music uses Lyria; the Gemini TTS models are for speech.
65
65
 
66
66
  For ElevenLabs speech, choose `eleven_v4` or `eleven_v4_turbo`, pass an ElevenLabs voice ID with `--voice` (George is the default), and put audio tags in `--prompt` rather than using `--delivery`. For sound effects, use `--type sfx`; the only model is `eleven_text_to_sound_v2`. SFX supports `--seconds` from 0.5 to 30 (default 5), `--loop`, and `--prompt-influence` from 0 to 1 (default 0.3). These additions require CLI 0.5.0 or newer; check `catalog` before using them with an installed CLI.
67
67