@docyrus/animaguru 0.7.1 → 0.8.1

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
@@ -80,7 +80,7 @@ CLI 0.7.1 can select a cloud workspace for project listing, project creation, an
80
80
  animaguru workspaces list
81
81
  animaguru workspaces use team_0123456789abcdef0123456789abcdef
82
82
  animaguru projects list
83
- animaguru projects create --name "Shared cast" --style pixel
83
+ animaguru projects create --name "Shared cast"
84
84
  animaguru credits
85
85
  animaguru workspaces use personal
86
86
  ```
@@ -89,10 +89,12 @@ Use a team ID returned by `workspaces list`. The choice is saved per server in t
89
89
 
90
90
  ## Agent workflow
91
91
 
92
+ Remote projects are neutral containers. `projects create` has no `--style` flag. Choose a style when generating a sprite; local film setup keeps its separate style interview.
93
+
92
94
  ```sh
93
- animaguru projects create --name "Forest cast" --style pixel
95
+ animaguru projects create --name "Forest cast"
94
96
  # Read the returned project.id and use it below.
95
- animaguru generate PROJECT_ID --name "Knight" --description "A blue knight with a red scarf" --actions idle,walk --yes
97
+ animaguru generate PROJECT_ID --name "Knight" --description "A blue knight with a red scarf" --style pixel --actions idle,walk --yes
96
98
  animaguru jobs wait PROJECT_ID JOB_ID --timeout 600
97
99
  animaguru jobs list PROJECT_ID
98
100
  animaguru projects get PROJECT_ID
@@ -123,7 +125,7 @@ animaguru models PROJECT_ID --image-model gpt-image-2.5 --video-model pruna/p-vi
123
125
  # Either selection can be changed independently:
124
126
  animaguru models PROJECT_ID --video-model heygen/heygen-video-1
125
127
  # Or choose them when creating a project:
126
- animaguru projects create --name "Forest cast" --style pixel \
128
+ animaguru projects create --name "Forest cast" \
127
129
  --image-model gpt-image-2.5 --video-model heygen/heygen-video-1
128
130
  ```
129
131
 
@@ -136,7 +138,7 @@ These flags are available in CLI 0.2.0 and later. Rerun the macOS installer to u
136
138
  CLI 0.4.0 adds standalone assets alongside the existing sprite commands. Use any project, or create an empty one first. `assets list` returns saved asset ids, image blob hashes for video references, and the models configured on that server.
137
139
 
138
140
  ```sh
139
- animaguru projects create --name "Adventure assets" --style hd-cartoon
141
+ animaguru projects create --name "Adventure assets"
140
142
  animaguru assets list PROJECT_ID
141
143
 
142
144
  animaguru assets generate PROJECT_ID --type image --name "Forest" \
@@ -177,6 +179,27 @@ animaguru assets generate PROJECT_ID --type sfx --name "Magic spell" \
177
179
 
178
180
  Replace `--dry-run` with `--yes` to generate within your authorized credit budget. ElevenLabs accepts a voice ID in `--voice` and defaults to George when omitted. Use audio tags in the transcript; `--delivery` is only for Gemini and is rejected for ElevenLabs. SFX has a single model, `eleven_text_to_sound_v2`; `--seconds` accepts 0.5–30 (default 5), `--loop` requests a seamless loop, and `--prompt-influence` accepts 0–1 (default 0.3). Both return MP3. `catalog` includes `sfxModels` and `elevenLabsVoices`. The server needs `AI_PROVIDER_KEY_ELEVENLABS`; provider keys never belong in CLI credentials.
179
181
 
182
+ CLI 0.8.0 adds MiniMax H3 Max Turbo and the H3 workflows. MiniMax H3 Max and H3 Max Turbo on fal.ai (`fal/minimax-h3-max`, `fal/minimax-h3-max-turbo`) also write videos from text and extend saved videos. Choose with `--workflow`:
183
+
184
+ ```sh
185
+ # End on a pose, pin saved audio as the soundtrack, and keep a repeatable seed.
186
+ animaguru assets generate PROJECT_ID --type video --model fal/minimax-h3-max-turbo \
187
+ --name "Hero lands" --prompt "The hero leaps and lands in a crouch" \
188
+ --reference START_IMAGE_SHA --end-reference END_IMAGE_SHA --soundtrack AUDIO_SHA \
189
+ --prompt-expansion balanced --seed 42 --resolution 1080p --dry-run
190
+ # Write a scene from text alone.
191
+ animaguru assets generate PROJECT_ID --type video --model fal/minimax-h3-max \
192
+ --workflow text-to-video --name "Harbor" --prompt "A foggy harbor at dawn" \
193
+ --aspect-ratio 21:9 --seconds 8 --resolution 768p --dry-run
194
+ # Add six seconds to a saved video at 2K, guided by saved audio.
195
+ animaguru assets generate PROJECT_ID --type video --model fal/minimax-h3-max \
196
+ --workflow extend-video --source VIDEO_SHA --name "Harbor, continued" \
197
+ --prompt "Audio 1 for the gulls. The fog lifts and a ship sails out" \
198
+ --audio-references AUDIO_SHA --seconds 6 --resolution 2k --dry-run
199
+ ```
200
+
201
+ `--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`.
202
+
180
203
  Images have a full background by default; `--transparent` requests a cutout. 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.
181
204
 
182
205
  Preview a validated request and its estimated credits offline with `--dry-run`, without signing in or spending credits:
package/dist/animaguru.js CHANGED
@@ -542,7 +542,7 @@ var init_dist = __esm({
542
542
  // cli/package.json
543
543
  var package_default = {
544
544
  name: "@docyrus/animaguru",
545
- version: "0.7.1",
545
+ version: "0.8.1",
546
546
  description: "Agent-friendly AnimaGuru CLI built with CrustJS",
547
547
  type: "module",
548
548
  bin: {
@@ -6111,11 +6111,20 @@ var SPRITE_IMAGE_MODEL_IDS = SPRITE_IMAGE_MODELS.map((m) => m.id);
6111
6111
  function spriteImageModel(id2) {
6112
6112
  return SPRITE_IMAGE_MODELS.find((m) => m.id === id2) ?? SPRITE_IMAGE_MODELS[0];
6113
6113
  }
6114
+ var VIDEO_WORKFLOWS = ["image-to-video", "text-to-video", "extend-video"];
6115
+ var VIDEO_RESOLUTIONS = ["480p", "768p", "1080p", "2k"];
6116
+ var VIDEO_ASPECT_RATIOS = ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16"];
6117
+ var PROMPT_EXPANSION_MODES = ["disabled", "balanced", "quality"];
6118
+ var EXTEND_OUTPUTS = ["extended", "continuation"];
6119
+ var SOUNDTRACK_MAX_BYTES = 15 * 1024 * 1024;
6120
+ var EXTEND_SOURCE_MAX_BYTES = 50 * 1024 * 1024;
6121
+ var MAX_AUDIO_REFERENCES = 3;
6114
6122
  var SPRITE_VIDEO_MODELS = [
6115
6123
  {
6116
6124
  id: "pruna/p-video-2-pro",
6117
6125
  label: "Pruna P-Video 2 Pro",
6118
6126
  provider: "pruna",
6127
+ workflows: ["image-to-video"],
6119
6128
  resolutions: ["480p", "768p"],
6120
6129
  modes: ["cost", "speed", "quality"],
6121
6130
  lastFrame: true,
@@ -6126,6 +6135,7 @@ var SPRITE_VIDEO_MODELS = [
6126
6135
  id: "heygen/heygen-video-1",
6127
6136
  label: "HeyGen Video 1",
6128
6137
  provider: "heygen",
6138
+ workflows: ["image-to-video"],
6129
6139
  resolutions: ["480p", "768p"],
6130
6140
  modes: ["cost"],
6131
6141
  lastFrame: false,
@@ -6136,13 +6146,40 @@ var SPRITE_VIDEO_MODELS = [
6136
6146
  id: "fal/minimax-h3-max",
6137
6147
  label: "MiniMax H3 Max (fal.ai)",
6138
6148
  provider: "fal",
6139
- endpoint: "minimax/h3-max/image-to-video",
6149
+ workflows: VIDEO_WORKFLOWS,
6150
+ 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" },
6140
6151
  resolutions: ["480p", "768p", "1080p"],
6152
+ extendResolutions: VIDEO_RESOLUTIONS,
6141
6153
  modes: ["quality"],
6142
6154
  lastFrame: true,
6143
- // Regular rates, not the temporary promotion ending 2026-10-15.
6144
- // https://fal.ai/models/minimax/h3-max/image-to-video
6145
- usdPerSecond: { "480p": { quality: 0.05 }, "768p": { quality: 0.08 }, "1080p": { quality: 0.16 } },
6155
+ endFrameOnly: true,
6156
+ seed: true,
6157
+ promptExpansion: PROMPT_EXPANSION_MODES,
6158
+ audio: true,
6159
+ // Regular rates, not the temporary promotion ending 2026-10-15; the same for every workflow,
6160
+ // 2K extensions only. Extensions bill reference input beyond 4,096 tokens a request too
6161
+ // ($0.02 per 1,000), which fal doesn't report back, so it isn't charged.
6162
+ // https://fal.ai/models/minimax/h3-max/image-to-video, …/text-to-video, …/extend-video
6163
+ usdPerSecond: { "480p": { quality: 0.05 }, "768p": { quality: 0.08 }, "1080p": { quality: 0.16 }, "2k": { quality: 0.32 } },
6164
+ seconds: { min: 1, max: 15 }
6165
+ },
6166
+ {
6167
+ id: "fal/minimax-h3-max-turbo",
6168
+ label: "MiniMax H3 Max Turbo (fal.ai)",
6169
+ provider: "fal",
6170
+ workflows: VIDEO_WORKFLOWS,
6171
+ 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" },
6172
+ resolutions: ["480p", "768p", "1080p"],
6173
+ extendResolutions: VIDEO_RESOLUTIONS,
6174
+ modes: ["speed"],
6175
+ lastFrame: true,
6176
+ endFrameOnly: true,
6177
+ seed: true,
6178
+ promptExpansion: PROMPT_EXPANSION_MODES,
6179
+ audio: true,
6180
+ // Regular rates, not the temporary promotion ending 2026-10-15; extensions bill only the new footage.
6181
+ // https://fal.ai/models/minimax/h3-max-turbo/image-to-video, …/text-to-video, …/extend-video
6182
+ usdPerSecond: { "480p": { speed: 0.025 }, "768p": { speed: 0.04 }, "1080p": { speed: 0.08 }, "2k": { speed: 0.16 } },
6146
6183
  seconds: { min: 1, max: 15 }
6147
6184
  }
6148
6185
  ];
@@ -6151,6 +6188,9 @@ var DEFAULT_SPRITE_VIDEO_MODEL = "pruna/p-video-2-pro";
6151
6188
  function spriteVideoModel(id2) {
6152
6189
  return SPRITE_VIDEO_MODELS.find((m) => m.id === id2) ?? SPRITE_VIDEO_MODELS[0];
6153
6190
  }
6191
+ function videoResolutions(model, workflow = "image-to-video") {
6192
+ return workflow === "extend-video" ? model.extendResolutions ?? model.resolutions : model.resolutions;
6193
+ }
6154
6194
  function spriteVideoCostUsd(model, p) {
6155
6195
  const byMode = model.usdPerSecond[p.resolution] ?? model.usdPerSecond[model.resolutions[0]];
6156
6196
  const perSecond = byMode[p.mode] ?? byMode[model.modes[0]] ?? 0;
@@ -26811,7 +26851,7 @@ var VIDEO_WORKFLOW_IDS = [
26811
26851
  "ambient-loop",
26812
26852
  "visual-essay"
26813
26853
  ];
26814
- var VIDEO_WORKFLOWS = VIDEO_WORKFLOW_IDS.map((id2, index) => ({
26854
+ var VIDEO_WORKFLOWS2 = VIDEO_WORKFLOW_IDS.map((id2, index) => ({
26815
26855
  id: id2,
26816
26856
  category: index < 8 ? "advertisement" : "general-video",
26817
26857
  name: animation_styles_default.workflows[id2].name,
@@ -26840,10 +26880,10 @@ function videoTemplateCatalog() {
26840
26880
  name: animation_styles_default.categories[id2].name,
26841
26881
  description: animation_styles_default.categories[id2].description,
26842
26882
  templates: VIDEO_TEMPLATES.filter((template) => template.categories.some((category) => category === id2)),
26843
- workflows: VIDEO_WORKFLOWS.filter((workflow) => workflow.category === id2)
26883
+ workflows: VIDEO_WORKFLOWS2.filter((workflow) => workflow.category === id2)
26844
26884
  })),
26845
26885
  templates: VIDEO_TEMPLATES,
26846
- workflows: VIDEO_WORKFLOWS
26886
+ workflows: VIDEO_WORKFLOWS2
26847
26887
  };
26848
26888
  }
26849
26889
 
@@ -27076,14 +27116,59 @@ var isElevenLabsSpeech = (model) => ELEVENLABS_SPEECH_MODEL_IDS.includes(model);
27076
27116
  var MUSIC_MODELS = [{ id: "lyria-3.5", label: "Lyria 3.5" }];
27077
27117
  var SPEECH_VOICES = ["Kore", "Puck", "Charon", "Fenrir", "Aoede", "Leda", "Orus", "Zephyr"];
27078
27118
  var base = { name: external_exports.string().trim().min(1).max(80), prompt: external_exports.string().trim().min(3).max(4e3) };
27119
+ var blob = external_exports.string().regex(/^[a-f0-9]{64}$/);
27120
+ var videoAsset = external_exports.object({
27121
+ ...base,
27122
+ type: external_exports.literal("video"),
27123
+ model: external_exports.enum(SPRITE_VIDEO_MODEL_IDS).default(DEFAULT_SPRITE_VIDEO_MODEL),
27124
+ workflow: external_exports.enum(VIDEO_WORKFLOWS).default("image-to-video"),
27125
+ /** First frame. */
27126
+ reference: blob.optional(),
27127
+ /** Last frame. */
27128
+ endReference: blob.optional(),
27129
+ /** The saved clip to extend. */
27130
+ source: blob.optional(),
27131
+ /** Saved audio pinned as the soundtrack (at least two seconds of video). */
27132
+ soundtrack: blob.optional(),
27133
+ /** Saved audio guiding an extension's new sound; name each clip's role in the prompt. */
27134
+ audioReferences: external_exports.array(blob).max(MAX_AUDIO_REFERENCES).default([]),
27135
+ seconds: external_exports.number().int().min(1).max(15).default(5),
27136
+ resolution: external_exports.enum(VIDEO_RESOLUTIONS).default("480p"),
27137
+ mode: external_exports.enum(["cost", "speed", "quality"]).default("cost"),
27138
+ /** A clip written from text (default 16:9), or an extension cropped to a new canvas ("auto" keeps the source's). */
27139
+ aspectRatio: external_exports.enum(["auto", ...VIDEO_ASPECT_RATIOS]).optional(),
27140
+ promptExpansion: external_exports.enum(PROMPT_EXPANSION_MODES).default("disabled"),
27141
+ seed: external_exports.number().int().min(0).max(2 ** 32 - 1).optional(),
27142
+ /** An extension returns the whole clip, or only the new footage after a short overlap. */
27143
+ extendOutput: external_exports.enum(EXTEND_OUTPUTS).default("extended")
27144
+ }).superRefine((p, ctx) => {
27145
+ const model = spriteVideoModel(p.model);
27146
+ const issue2 = (path, message) => ctx.addIssue({ code: "custom", message, path: [path] });
27147
+ if (!model.workflows.includes(p.workflow)) return issue2("workflow", "Choose a way of making video supported by the video model.");
27148
+ const extend2 = p.workflow === "extend-video";
27149
+ if (p.seconds < model.seconds.min || p.seconds > model.seconds.max) issue2("seconds", "Choose a duration supported by the video model.");
27150
+ if (!videoResolutions(model, p.workflow).includes(p.resolution)) issue2("resolution", "Choose a resolution supported by the video model.");
27151
+ if (!model.modes.includes(p.mode)) issue2("mode", "Choose a mode supported by the video model.");
27152
+ if (p.workflow === "image-to-video") {
27153
+ if (!p.reference && !(p.endReference && model.endFrameOnly)) issue2("reference", "Choose a starting image.");
27154
+ if (p.endReference && !model.lastFrame) issue2("endReference", "This video model can't end on a chosen image.");
27155
+ } else {
27156
+ if (p.reference) issue2("reference", "Only image-to-video starts from an image.");
27157
+ if (p.endReference) issue2("endReference", "Only image-to-video ends on an image.");
27158
+ }
27159
+ if (extend2 !== Boolean(p.source)) issue2("source", extend2 ? "Choose a saved video to extend." : "Only an extension starts from a saved video.");
27160
+ if (p.aspectRatio && p.workflow === "image-to-video") issue2("aspectRatio", "A video made from an image keeps the image's shape.");
27161
+ if (p.aspectRatio === "auto" && p.workflow === "text-to-video") issue2("aspectRatio", "Choose an aspect ratio for a video written from text.");
27162
+ 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.");
27163
+ if (p.soundtrack && p.seconds < 2) issue2("seconds", "A video with a soundtrack must be at least 2 seconds long.");
27164
+ if (p.audioReferences.length && (!model.audio || !extend2)) issue2("audioReferences", "Only an extension takes audio references.");
27165
+ if (p.promptExpansion !== "disabled" && !model.promptExpansion?.includes(p.promptExpansion)) issue2("promptExpansion", "This video model doesn't rewrite prompts.");
27166
+ if (p.seed !== void 0 && !model.seed) issue2("seed", "This video model doesn't take a seed.");
27167
+ if (p.extendOutput !== "extended" && !extend2) issue2("extendOutput", "Only an extension can return the new footage alone.");
27168
+ });
27079
27169
  var assetRequest = external_exports.discriminatedUnion("type", [
27080
27170
  external_exports.object({ ...base, type: external_exports.literal("image"), model: external_exports.enum(SPRITE_IMAGE_MODEL_IDS).default(DEFAULT_SPRITE_IMAGE_MODEL), aspectRatio: external_exports.enum(["1:1", "3:2", "2:3"]).default("1:1"), transparent: external_exports.boolean().default(false) }),
27081
- external_exports.object({ ...base, type: external_exports.literal("video"), model: external_exports.enum(SPRITE_VIDEO_MODEL_IDS).default(DEFAULT_SPRITE_VIDEO_MODEL), reference: external_exports.string().regex(/^[a-f0-9]{64}$/), seconds: external_exports.number().int().min(1).max(15).default(5), resolution: external_exports.enum(["480p", "768p", "1080p"]).default("480p"), mode: external_exports.enum(["cost", "speed", "quality"]).default("cost") }).superRefine((p, ctx) => {
27082
- const model = spriteVideoModel(p.model);
27083
- if (p.seconds < model.seconds.min || p.seconds > model.seconds.max) ctx.addIssue({ code: "custom", message: "Choose a duration supported by the video model.", path: ["seconds"] });
27084
- if (!model.resolutions.includes(p.resolution)) ctx.addIssue({ code: "custom", message: "Choose a resolution supported by the video model.", path: ["resolution"] });
27085
- if (!model.modes.includes(p.mode)) ctx.addIssue({ code: "custom", message: "Choose a mode supported by the video model.", path: ["mode"] });
27086
- }),
27171
+ videoAsset,
27087
27172
  external_exports.object({ ...base, type: external_exports.literal("music"), model: external_exports.literal("lyria-3.5").default("lyria-3.5") }),
27088
27173
  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) => {
27089
27174
  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"] });
@@ -27310,25 +27395,46 @@ async function submitVideo(client, options) {
27310
27395
  // cli/src/assets.ts
27311
27396
  import { readFile as readFile7, writeFile as writeFile7 } from "node:fs/promises";
27312
27397
  import { resolve as resolve8 } from "node:path";
27313
- var extras = { image: ["aspect-ratio", "transparent"], video: ["reference", "seconds", "resolution", "mode"], music: [], speech: ["voice", "delivery"], sfx: ["seconds", "loop", "prompt-influence"] };
27398
+ var extras = { image: ["aspect-ratio", "transparent"], 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"] };
27399
+ function parseAsset(input2) {
27400
+ const parsed = assetRequest.safeParse(input2);
27401
+ if (parsed.success) return parsed.data;
27402
+ throw new Error(parsed.error.issues.map((issue2) => `${issue2.path.length ? `${issue2.path.join(".")}: ` : ""}${issue2.message}`).join("\n"));
27403
+ }
27314
27404
  function assetFromFlags(flags) {
27315
27405
  for (const name of Object.values(extras).flat()) {
27316
27406
  const value = flags[name];
27317
27407
  if (value !== void 0 && value !== false && !extras[flags.type]?.includes(name)) throw new Error(`--${name} is not an option for ${flags.type} assets.`);
27318
27408
  }
27319
27409
  const base2 = { type: flags.type, name: flags.name, prompt: flags.prompt, model: flags.model };
27320
- if (flags.type === "image") return assetRequest.parse({ ...base2, aspectRatio: flags["aspect-ratio"], transparent: flags.transparent });
27410
+ if (flags.type === "image") return parseAsset({ ...base2, aspectRatio: flags["aspect-ratio"], transparent: flags.transparent });
27321
27411
  if (flags.type === "video") {
27322
27412
  const model = spriteVideoModel(flags.model);
27323
- return assetRequest.parse({ ...base2, reference: flags.reference, seconds: flags.seconds ?? Math.max(5, model.seconds.min), resolution: flags.resolution ?? model.resolutions[0], mode: flags.mode ?? model.modes[0] });
27413
+ const workflow = flags.workflow ?? "image-to-video";
27414
+ return parseAsset({
27415
+ ...base2,
27416
+ workflow: flags.workflow,
27417
+ reference: flags.reference,
27418
+ endReference: flags["end-reference"],
27419
+ source: flags.source,
27420
+ soundtrack: flags.soundtrack,
27421
+ audioReferences: flags["audio-references"]?.split(",").map((sha) => sha.trim()).filter(Boolean),
27422
+ seconds: flags.seconds ?? Math.max(5, model.seconds.min),
27423
+ resolution: flags.resolution ?? videoResolutions(model, workflow)[0],
27424
+ mode: flags.mode ?? model.modes[0],
27425
+ aspectRatio: flags["aspect-ratio"],
27426
+ promptExpansion: flags["prompt-expansion"],
27427
+ seed: flags.seed,
27428
+ extendOutput: flags["extend-output"]
27429
+ });
27324
27430
  }
27325
- if (flags.type === "sfx") return assetRequest.parse({ ...base2, seconds: flags.seconds, loop: flags.loop, promptInfluence: flags["prompt-influence"] });
27326
- return assetRequest.parse({ ...base2, ...flags.type === "speech" ? { voice: flags.voice, delivery: flags.delivery } : {} });
27431
+ if (flags.type === "sfx") return parseAsset({ ...base2, seconds: flags.seconds, loop: flags.loop, promptInfluence: flags["prompt-influence"] });
27432
+ return parseAsset({ ...base2, ...flags.type === "speech" ? { voice: flags.voice, delivery: flags.delivery } : {} });
27327
27433
  }
27328
27434
  async function assetFromFile(file2, model) {
27329
27435
  const input2 = JSON.parse(await readFile7(file2, "utf8"));
27330
27436
  if (!input2 || typeof input2 !== "object" || Array.isArray(input2)) throw new Error("The asset file must contain a JSON object.");
27331
- return assetRequest.parse({ ...input2, ...model ? { model } : {} });
27437
+ return parseAsset({ ...input2, ...model ? { model } : {} });
27332
27438
  }
27333
27439
  async function submitAsset(client, project, body, options) {
27334
27440
  if (options["dry-run"]) return { dryRun: true, request: body, estimatedCredits: assetEstimate(body) };
@@ -27457,10 +27563,10 @@ Confirm code: ${device.user_code}`);
27457
27563
  }))).add(defineCommand("catalog", { description: "List styles, kinds, actions, asset types, models, video settings and speech voices" }, (command) => command.action(({ stdout }) => out(stdout, { styles: SPRITE_STYLES, kinds: SPRITE_KINDS, actions: ALL_SPRITE_ACTIONS, imageModels: SPRITE_IMAGE_MODEL_IDS, videoModels: SPRITE_VIDEO_MODEL_IDS, assetTypes: ASSET_TYPES, musicModels: MUSIC_MODELS.map((m) => m.id), speechModels: SPEECH_MODELS.map((m) => m.id), sfxModels: SFX_MODELS.map((m) => m.id), elevenLabsVoices: ELEVENLABS_VOICES, voices: SPEECH_VOICES, videoProfiles: SPRITE_VIDEO_MODELS })))).command("projects", (projects) => projects.add(defineCommand("list", { description: "List projects in the selected workspace" }, (command) => command.flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" }, description: "Server origin" }).action(async ({ flags, stdout, signal }) => {
27458
27564
  const client = new Client(flags.url, signal);
27459
27565
  out(stdout, await client.request(`/api/v1/projects${await workspaceQuery(client)}`));
27460
- }))).add(defineCommand("create", { description: "Create an empty sprite project; generate artwork with jobs submit" }, (command) => command.flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" }, description: "Server origin" }).flags(...modelFlags).flags({ name: "name", type: "string", required: true }, { name: "style", type: "string", required: true, choices: SPRITE_STYLES }).action(async ({ flags, stdout, signal }) => {
27566
+ }))).add(defineCommand("create", { description: "Create an empty sprite project; generate artwork with jobs submit" }, (command) => command.flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" }, description: "Server origin" }).flags(...modelFlags).flags({ name: "name", type: "string", required: true }).action(async ({ flags, stdout, signal }) => {
27461
27567
  const client = new Client(flags.url, signal);
27462
27568
  const selected = await selectedWorkspace(client.origin);
27463
- out(stdout, await client.request("/api/v1/sprites", { method: "POST", body: { owner: selected, name: flags.name, style: flags.style, ...selectedModels(flags) } }));
27569
+ out(stdout, await client.request("/api/v1/sprites", { method: "POST", body: { owner: selected, name: flags.name, ...selectedModels(flags) } }));
27464
27570
  }))).add(defineCommand("get", { description: "Read project, subjects, animations, estimates and job state" }, (command) => command.flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" }, description: "Server origin" }).args(id).action(async ({ args, flags, stdout, signal }) => out(stdout, await new Client(flags.url, signal).request(projectPath(args.id)))))).add(defineCommand("rename", { description: "Rename a project" }, (command) => command.flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" }, description: "Server origin" }).args(id).flags({ name: "name", type: "string", required: true }).action(async ({ args, flags, stdout, signal }) => out(stdout, await new Client(flags.url, signal).request(`/api/v1/projects/${encodeURIComponent(args.id)}`, { method: "PATCH", body: { name: flags.name } }))))).add(defineCommand("delete", { description: "Delete a project; requires --yes" }, (command) => command.flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" }, description: "Server origin" }).args(id).flags({ name: "yes", type: "boolean", required: true }).action(async ({ args, flags, stdout, signal }) => {
27465
27571
  if (!flags.yes) throw new Error("Pass --yes to delete this project.");
27466
27572
  out(stdout, await new Client(flags.url, signal).request(`/api/v1/projects/${encodeURIComponent(args.id)}`, { method: "DELETE" }));
@@ -27469,12 +27575,20 @@ Confirm code: ${device.user_code}`);
27469
27575
  { name: "name", type: "string", required: true },
27470
27576
  { name: "prompt", type: "string", required: true, description: "Description to generate, or verbatim transcript for speech" },
27471
27577
  { name: "model", type: "string", choices: assetModelIds, description: "Model for the chosen asset type" },
27472
- { name: "aspect-ratio", type: "string", choices: ["1:1", "3:2", "2:3"], description: "Image aspect ratio (default 1:1)" },
27578
+ { name: "aspect-ratio", type: "string", choices: [.../* @__PURE__ */ new Set(["1:1", "3:2", "2:3", "auto", ...VIDEO_ASPECT_RATIOS])], description: "Image: 1:1 (default), 3:2 or 2:3. Video from text: 21:9\u20139:16 (default 16:9). Extension: auto (default, the source's) or a ratio to crop to" },
27473
27579
  { name: "transparent", type: "boolean", description: "Image: transparent background" },
27580
+ { 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" },
27474
27581
  { name: "reference", type: "string", description: "Video: starting image blob SHA from this project's assets or sprites" },
27475
- { name: "seconds", type: "number", description: "Video or SFX duration in seconds (default 5); SFX supports 0.5\u201330" },
27476
- { name: "resolution", type: "string", choices: ["480p", "768p", "1080p"], description: "Video resolution supported by the selected model" },
27582
+ { name: "end-reference", type: "string", description: "Video: ending image blob SHA; models with endFrameOnly accept it without --reference" },
27583
+ { name: "source", type: "string", description: "Extension: blob SHA of a video asset in this project to extend" },
27584
+ { 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)" },
27585
+ { 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" },
27586
+ { name: "seconds", type: "number", description: "Video or SFX duration in seconds (default 5); extensions add this many seconds; SFX supports 0.5\u201330" },
27587
+ { name: "resolution", type: "string", choices: VIDEO_RESOLUTIONS, description: "Video resolution supported by the selected model and workflow (2k for extensions)" },
27477
27588
  { name: "mode", type: "string", choices: ["cost", "speed", "quality"], description: "Video generation mode supported by the selected model" },
27589
+ { 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" },
27590
+ { name: "seed", type: "number", description: "Video: seed for repeatable results, where the model takes one" },
27591
+ { 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)" },
27478
27592
  { name: "voice", type: "string", description: "Gemini voice name or ElevenLabs voice ID (defaults: Kore / George)" },
27479
27593
  { name: "delivery", type: "string", description: "Gemini speech delivery instructions; ElevenLabs uses audio tags in the transcript" },
27480
27594
  { name: "loop", type: "boolean", description: "SFX: create a seamless loop" },
@@ -27502,9 +27616,9 @@ Confirm code: ${device.user_code}`);
27502
27616
  const body = selectedModels(flags);
27503
27617
  if (!Object.keys(body).length) throw new Error("Pass --image-model or --video-model.");
27504
27618
  out(stdout, await new Client(flags.url, signal).request(`${projectPath(args.id)}/models`, { method: "PUT", body }));
27505
- }))).add(defineCommand("generate", { description: "Draw a subject; --yes authorizes generation costs" }, (command) => command.flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" }, description: "Server origin" }).args(id).flags(...modelFlags).flags({ name: "name", type: "string", required: true }, { name: "description", type: "string", required: true }, { name: "kind", type: "string", choices: SPRITE_KINDS, default: "character" }, { name: "actions", type: "string", description: "Comma-separated built-in actions to animate after drawing" }, { name: "yes", type: "boolean", required: true }).action(async ({ args, flags, stdout, signal }) => {
27619
+ }))).add(defineCommand("generate", { description: "Draw a subject; --yes authorizes generation costs" }, (command) => command.flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" }, description: "Server origin" }).args(id).flags(...modelFlags).flags({ name: "name", type: "string", required: true }, { name: "description", type: "string", required: true }, { name: "style", type: "string", choices: SPRITE_STYLES, description: "Artwork style for this sprite" }, { name: "kind", type: "string", choices: SPRITE_KINDS, default: "character" }, { name: "actions", type: "string", description: "Comma-separated built-in actions to animate after drawing" }, { name: "yes", type: "boolean", required: true }).action(async ({ args, flags, stdout, signal }) => {
27506
27620
  if (!flags.yes) throw new Error("Pass --yes to authorize spending credits.");
27507
- const body = spriteJobBody.parse({ kind: "character", name: flags.name, description: flags.description, subjectKind: flags.kind, actions: actions(flags.actions), ...selectedModels(flags) });
27621
+ const body = spriteJobBody.parse({ kind: "character", name: flags.name, description: flags.description, ...flags.style ? { style: flags.style } : {}, subjectKind: flags.kind, actions: actions(flags.actions), ...selectedModels(flags) });
27508
27622
  out(stdout, await new Client(flags.url, signal).request(`${projectPath(args.id)}/jobs`, { method: "POST", body }));
27509
27623
  }))).add(defineCommand("schema", { description: "Print the Crust command schema for agent discovery" }, (command) => command.action(async ({ stdout }) => out(stdout, await createCli().snapshot())))).command("files", (files) => files.add(defineCommand("import", { description: "Import a PNG, JPEG or WebP reference; --yes authorizes redraw costs when configured" }, (command) => command.flags({ name: "url", type: "string", default: "https://animaguru.com", env: { name: "ANIMAGURU_URL" } }).args(id).flags(...modelFlags).flags({ name: "file", type: "string", required: true }, { name: "name", type: "string", required: true }, { name: "description", type: "string" }, { name: "yes", type: "boolean", required: true }).action(async ({ args, flags, stdout, signal }) => {
27510
27624
  if (!flags.yes) throw new Error("Pass --yes to authorize reference redraw costs.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docyrus/animaguru",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "description": "Agent-friendly AnimaGuru CLI built with CrustJS",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,9 +24,9 @@ Share the [animation-style gallery](https://animaguru.com/animation-styles), or
24
24
  Reuse the user's chosen project. Otherwise inspect `animaguru projects list` before creating one. Read `animaguru projects get PROJECT_ID` for subjects, character identifiers, generation estimates, and current job state. Use identifiers returned by the service rather than guessing slugs.
25
25
 
26
26
  ```sh
27
- animaguru projects create --name "Forest cast" --style pixel
27
+ animaguru projects create --name "Forest cast"
28
28
  animaguru generate PROJECT_ID --name "Knight" \
29
- --description "A blue knight with a red scarf" --actions idle,walk --yes
29
+ --description "A blue knight with a red scarf" --style pixel --actions idle,walk --yes
30
30
  ```
31
31
 
32
32
  Read the project ID from `projects create` and job IDs from submission responses. Supported actions depend on the subject kind. Use `--kind` for a different subject kind, based on the catalog.
@@ -63,7 +63,7 @@ Import reference artwork with `animaguru files import PROJECT_ID --file knight.p
63
63
 
64
64
  ## Standalone images, video, music and speech
65
65
 
66
- CLI 0.4.0 adds `assets generate`, `assets submit`, `assets list`, and `assets download`. Use `assets list PROJECT_ID` to discover saved assets and configured provider models. New asset projects can be created with `projects create --name NAME --style hd-cartoon`; that style does not constrain standalone prompts.
66
+ CLI 0.4.0 adds `assets generate`, `assets submit`, `assets list`, and `assets download`. Use `assets list PROJECT_ID` to discover saved assets and configured provider models. Create neutral asset projects with `projects create --name NAME`. Projects have no art style; choose style when generating an individual sprite.
67
67
 
68
68
  ```sh
69
69
  animaguru assets generate PROJECT_ID --type image --name "Forest" \
@@ -77,7 +77,7 @@ animaguru assets generate PROJECT_ID --type speech --name "Welcome" \
77
77
  --prompt "Welcome, traveler." --voice Kore --delivery "Warm and welcoming" --yes
78
78
  ```
79
79
 
80
- Use `--model` to select a model for any asset type; 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. 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. For Gemini speech, `--prompt` is spoken verbatim and `--delivery` is a separate instruction. Music uses Lyria; the Gemini TTS models are for speech.
80
+ Use `--model` to select a model for any asset type; 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. 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.
81
81
 
82
82
  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.
83
83
 
@@ -24,7 +24,7 @@ Read [visual and motion guidance](references/visual-style.md) when designing sce
24
24
 
25
25
  Use image tools for bitmap artwork, cutouts and textures, and PixiJS for composition, typography, masks and motion. Treat people, products, objects, landscapes and abstract forms as equally valid cutout subjects. Choose coherent edge treatment, material, lighting and movement so the film feels intentionally assembled.
26
26
 
27
- Use the user's selected generation models and budget. Otherwise inspect the current AnimaGuru catalog and configured providers to select models suited to the assets and motion. MiniMax H3 Max (`fal/minimax-h3-max`) is one option for animated photographic assets when available; cutout motion can also be built directly in PixiJS from still layers. Validate supported settings before submission.
27
+ Use the user's selected generation models and budget. Otherwise inspect the current AnimaGuru catalog and configured providers to select models suited to the assets and motion. MiniMax H3 Max (`fal/minimax-h3-max`) or its faster, cheaper Turbo (`fal/minimax-h3-max-turbo`) is one option for animated photographic assets when available; cutout motion can also be built directly in PixiJS from still layers. Validate supported settings before submission.
28
28
 
29
29
  Deliver the film described in the brief with consistent cutout visuals and intentional animation. Keep story development responsive to the project's purpose rather than borrowing the narrative of an example film.
30
30
 
@@ -1,4 +1,4 @@
1
- import { readFile, cp, mkdir } from "node:fs/promises";
1
+ import { readFile, cp, mkdir, stat } from "node:fs/promises";
2
2
  import { createReadStream } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { listAnimations, projectContext, publicFile, readBranding } from "./project-context.mjs";
@@ -50,9 +50,19 @@ void mountPlayer({config,Film,chapters,branding:${JSON.stringify(context.brandin
50
50
  }
51
51
  const file = await publicFile(context, match[2]);
52
52
  const type = ({svg:"image/svg+xml",png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",webp:"image/webp",mp3:"audio/mpeg",wav:"audio/wav",m4a:"audio/mp4",woff2:"font/woff2",woff:"font/woff",ttf:"font/ttf",mp4:"video/mp4",webm:"video/webm"})[file.split(".").at(-1).toLowerCase()] ?? "application/octet-stream";
53
- response.writeHead(200, { "Content-Type": type, "Cache-Control": "no-store" });
53
+ // Media elements seek with byte ranges; without them Chrome cannot load or seek video.
54
+ const { size } = await stat(file);
55
+ const range = /^bytes=(\d*)-(\d*)$/.exec(request.headers.range ?? "");
56
+ let start = 0, end = size - 1;
57
+ if (range && (range[1] || range[2])) {
58
+ start = range[1] ? Number(range[1]) : Math.max(0, size - Number(range[2]));
59
+ if (range[1] && range[2]) end = Math.min(Number(range[2]), size - 1);
60
+ if (start > end) { response.writeHead(416, { "Content-Range": `bytes */${size}` }).end(); return; }
61
+ }
62
+ const partial = Boolean(range && (range[1] || range[2]));
63
+ response.writeHead(partial ? 206 : 200, { "Content-Type": type, "Cache-Control": "no-store", "Accept-Ranges": "bytes", "Content-Length": String(end - start + 1), ...(partial ? { "Content-Range": `bytes ${start}-${end}/${size}` } : {}) });
54
64
  if (request.method === "HEAD") { response.end(); return; }
55
- const stream = createReadStream(file); response.once("close", () => stream.destroy()); stream.pipe(response);
65
+ const stream = createReadStream(file, { start, end }); response.once("close", () => stream.destroy()); stream.pipe(response);
56
66
  })().catch((error) => { if (!response.headersSent) response.writeHead(404).end(error.message); });
57
67
  });
58
68
  },
@@ -110,10 +110,11 @@ export async function mountPlayer({ config, Film, chapters, branding, exportEndp
110
110
  if (request !== playbackRequest) { audio?.pause(); return; }
111
111
  last = performance.now(); playing = true; update();
112
112
  }
113
- function toggleSound() {
113
+ function setMuted(value: boolean) {
114
114
  if (!audio) return;
115
- muted = !muted; audio.muted = muted; update();
115
+ muted = value; audio.muted = muted; update();
116
116
  }
117
+ function toggleSound() { setMuted(!muted); }
117
118
  function setCaptions(value: boolean) { film.captions = value; void render(time); update(); }
118
119
  async function fullscreen() {
119
120
  try { if (document.fullscreenElement) await document.exitFullscreen(); else await theater.requestFullscreen(); }
@@ -206,12 +207,12 @@ export async function mountPlayer({ config, Film, chapters, branding, exportEndp
206
207
  }
207
208
  await render(time); update();
208
209
  const api = {
209
- ready: true, width: config.width, height: config.height, duration: config.duration, fps: config.fps, language: config.language, canvas: app.canvas,
210
+ ready: true, width: config.width, height: config.height, duration: config.duration, fps: config.fps, language: config.language, languages: [{ code: config.language, name: config.language }], canvas: app.canvas,
210
211
  get state() { return { time, playing, muted, captions: film.captions, audio: { configured: Boolean(audio), readyState: audio?.readyState ?? 0, error: audio?.error?.code ?? null } }; },
211
212
  resources: film.resources,
212
213
  get texts() { return film.texts; },
213
214
  get cues() { return film.cues; },
214
- seek, play, pause, setCaptions, saveFrame,
215
+ seek, play, pause, setMuted, setCaptions, saveFrame,
215
216
  async renderFrame(frame: number, fps = config.fps) { await seek(frame / fps); },
216
217
  };
217
218
  Object.assign(window, { animaguru: api });