From 31720cf8d53ca5d24edc827473338a16d13f022b Mon Sep 17 00:00:00 2001 From: TheDesignFounder Date: Sun, 13 Sep 2026 17:45:14 -0700 Subject: [PATCH 1/2] feat: accept custom sprite animations and selectable export sizes --- README.md | 6 +++++- src/cli.ts | 26 ++++++++++++++++++++++---- src/client.ts | 11 ++++++++--- test/cli.test.mjs | 18 ++++++++++++++++++ 4 files changed, 53 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 959244e..a6f4ebe 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,11 @@ MIT. See LICENSE and NOTICE. ## Sprite-sheet beta -Eligible accounts can create walk, run, or idle sprite bundles. Request an integer `frame_count` from 7 to 100 (default 12). Frames 1–14 cost $0.14 each; additional frames cost $0.07 each. One credit is $0.17. The total request charge rounds up to one decimal credit; displayed balances round down without changing stored funds. Check `sprite_pricing` in capabilities and approve the total with `max_credits`. A bundle includes transparent frames, sheet, atlas, preview, and import instructions. Large requests may contain a sequence rather than one seamless loop; the atlas identifies the sampling mode. Insufficient distinct frames fail without padding or interpolation. Jobs may take several minutes. Hold credits at admission, charge after complete delivery, and restore the hold if generation fails or times out. Sprite requests have no customer cancellation. Keep the execution ID to resume status. +Sprite requests accept exactly one of `options.animation_prompt` (1–4000 characters) or an `options.action` preset (`walk`, `run`, `idle`). Custom prompts can describe characters, creatures, objects, effects or 360° turntables. `animation_mode` is `loop` or `once`; presets default to loop, custom prompts to once. For a turntable, request a stationary camera and rotating subject. Broad requests do not guarantee correct motion, unseen details or successful effect transparency. + +Request integer `frame_count` 7–100 (default 12) and `frame_size` 32, 64, 128, 256, 512, 720 or 1080 (default 512). These are square export canvases, not source video detail; enlargement is identified in the atlas. Aspect ratio and shared alignment are preserved with transparent padding. A bundle contains transparent PNG frames, sheet, atlas, preview and import instructions. Loops exclude the closing sample; one-time actions preserve beginning and end. Insufficient distinct frames, including duplicates caused by cleanup/downscaling, fail without padding or interpolation. Translucent effects can fail background removal; small exports are not automatically pixel art. + +Pricing is unchanged across sizes: frames 1–14 cost $0.14 each; additional frames $0.07 each. One credit is $0.17. Round the complete order upward once to a tenth of a credit. Check `sprite_pricing` in capabilities and approve the quote with `max_credits`. Credits are held during processing, settled after complete delivery and restored on failure/timeout. There is no customer cancellation. Keep the execution ID to resume status. Custom requests need the matching broad-animation server release; older servers reject them. New live generation quality, 100-frame duration and actual cost remain unverified. ```sh dreamlayer sprite character.png --action walk --frames 12 --max-credits 9.9 --out walk.zip diff --git a/src/cli.ts b/src/cli.ts index 9ca1b24..616d452 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -54,7 +54,10 @@ OPTIONS --out Where to write the image. Default: dreamlayer-.png --image Attach an image when answering a question that asks for one --aspect 1:1, 16:9, 9:16, 4:3, 3:4. Default 1:1 - --action Sprite animation: walk, run, idle + --action Sprite preset: walk, run, idle (walk if no custom prompt) + --animation-prompt Custom animation; cannot combine with --action + --animation-mode Default: loop for presets, once for custom + --frame-size Square export: 32, 64, 128, 256, 512 (default), 720, 1080 --frames Frame count: integer 7–100, default 12 --max-credits Maximum approved charge for the sprite job --json Machine-readable output on stdout @@ -69,7 +72,10 @@ Image operations cost one credit. Sprite pricing is listed in capabilities. A ne `; type Options = { - action: "walk" | "run" | "idle"; + action?: "walk" | "run" | "idle"; + animationPrompt?: string; + animationMode?: "loop" | "once"; + frameSize?: 32 | 64 | 128 | 256 | 512 | 720 | 1080; maxCredits: number; frameCount: number; out: string | null; @@ -85,7 +91,6 @@ class UsageError extends Error {} function parseOptions(argv: string[]): { positional: string[]; options: Options } { const positional: string[] = []; const options: Options = { - action: "walk", maxCredits: 1, frameCount: 12, out: null, @@ -107,6 +112,18 @@ function parseOptions(argv: string[]): { positional: string[]; options: Options const value = argv[++i]; if (value !== "walk" && value !== "run" && value !== "idle") throw new UsageError("--action must be walk, run, or idle"); options.action = value; + } else if (token === "--animation-prompt") { + const value = argv[++i]; + if (!value?.trim() || [...value].length > 4000) throw new UsageError("--animation-prompt needs 1–4000 characters"); + options.animationPrompt = value; + } else if (token === "--animation-mode") { + const value = argv[++i]; + if (value !== "loop" && value !== "once") throw new UsageError("--animation-mode must be loop or once"); + options.animationMode = value; + } else if (token === "--frame-size") { + const value = Number(argv[++i]); + if (![32, 64, 128, 256, 512, 720, 1080].includes(value)) throw new UsageError("--frame-size must be 32, 64, 128, 256, 512, 720 or 1080"); + options.frameSize = value as Options["frameSize"]; } else if (token === "--frames") { const value = Number(argv[++i]); if (!Number.isInteger(value) || value < 7 || value > 100) throw new UsageError("--frames must be an integer from 7 to 100"); @@ -317,6 +334,7 @@ async function main(argv: string[]): Promise { switch (command) { case "sprite": { + if (options.action && options.animationPrompt) throw new UsageError("Use either --action or --animation-prompt, not both"); const file = positional[0]; if (!file) throw new UsageError("sprite needs a reference image"); const api = client(); @@ -325,7 +343,7 @@ async function main(argv: string[]): Promise { if (!caps.sprite_pricing) throw new UsageError("The server does not support configurable sprite pricing yet"); const price = spriteCreditPrice(options.frameCount); if (options.maxCredits < price) throw new UsageError(`Sprite jobs require ${price} credits. Set --max-credits to approve that amount.`); - return run(api, { operation: "sprite_sheet", input_asset_id: await upload(api, file), options: { action: options.action, frame_count: options.frameCount }, max_credits: options.maxCredits }, options); + return run(api, { operation: "sprite_sheet", input_asset_id: await upload(api, file), options: { ...(options.animationPrompt ? { animation_prompt: options.animationPrompt } : { action: options.action ?? "walk" }), ...(options.animationMode ? { animation_mode: options.animationMode } : {}), ...(options.frameSize ? { frame_size: options.frameSize } : {}), frame_count: options.frameCount }, max_credits: options.maxCredits }, options); } case "generate": { const prompt = positional[0]; diff --git a/src/client.ts b/src/client.ts index 410f4a9..6c30966 100644 --- a/src/client.ts +++ b/src/client.ts @@ -71,7 +71,7 @@ export type ManagedExecuteInput = { aspect_ratio?: string; /** Requires the gateway build that added it. See ManagedOperation. */ operation?: ManagedOperation; - options?: { action: "walk" | "run" | "idle"; frame_count?: number }; + options?: { action?: "walk" | "run" | "idle"; animation_prompt?: string; animation_mode?: "loop" | "once"; frame_count?: number; frame_size?: 32 | 64 | 128 | 256 | 512 | 720 | 1080 }; max_credits?: number; }; @@ -83,8 +83,13 @@ export function spriteCreditPrice(frameCount: number): number { export function validateSpriteInput(input: ManagedExecuteInput): void { if (input.operation !== "sprite_sheet") return; - if (!input.options || !["walk", "run", "idle"].includes(input.options.action)) throw new Error("Sprite requests require options.action"); - const price = spriteCreditPrice(input.options.frame_count ?? 12); + const options = input.options; + if (!options || (options.action === undefined) === (options.animation_prompt === undefined)) throw new Error("Sprite requests require exactly one of options.action or options.animation_prompt"); + if (options.action !== undefined && !["walk", "run", "idle"].includes(options.action)) throw new Error("Invalid sprite preset"); + if (options.animation_prompt !== undefined && (typeof options.animation_prompt !== "string" || !options.animation_prompt.trim() || [...options.animation_prompt].length > 4000)) throw new Error("animation_prompt must contain 1–4000 characters"); + if (options.animation_mode !== undefined && !["loop", "once"].includes(options.animation_mode)) throw new Error("animation_mode must be loop or once"); + const price = spriteCreditPrice(options.frame_count ?? 12); + if (options.frame_size !== undefined && ![32, 64, 128, 256, 512, 720, 1080].includes(options.frame_size)) throw new Error("frame_size must be 32, 64, 128, 256, 512, 720 or 1080"); if (typeof input.max_credits !== "number" || !Number.isFinite(input.max_credits) || input.max_credits < price || input.max_credits > 100) throw new Error(`This sprite request requires ${price} credits. Supply a sufficient max_credits limit.`); } diff --git a/test/cli.test.mjs b/test/cli.test.mjs index d21f644..96a3a83 100644 --- a/test/cli.test.mjs +++ b/test/cli.test.mjs @@ -942,6 +942,24 @@ for (const [count, credits] of [[7,5.8],[14,11.6],[15,12],[99,46.6],[100,47]]) { } finally {api.close();} }); } +for (const size of [32,64,128,256,512,720,1080]) { + test(`custom sprite CLI forwards prompt, mode and ${size}px export`, async()=>{ + const api=await listen(fakeApi({capabilities:{api_version:'1',operations:['sprite_sheet'],sprite_pricing:{minimum_frames:7,maximum_frames:100}},events:[started,{event:'asset',data:{asset_id:'44444444-4444-4444-8444-444444444444',download_url:'ASSET'}},{event:'done',data:{status:'completed'}}]})); + const dir=await mkdtemp(path.join(tmpdir(),'sprite-custom-')); + const input=path.join(dir,'reference.png');await writeFile(input,PNG); + try { + const result=await runCli(['sprite',input,'--animation-prompt','Rotate this character 360 degrees','--animation-mode','loop','--frame-size',String(size),'--frames','7','--max-credits','5.8','--out',path.join(dir,'sheet.zip'),'--quiet'],{DREAMLAYER_API_URL:api.url}); + assert.equal(result.code,0,result.stderr); + assert.deepEqual(api.calls.find(c=>c.url==='/v1/execute').body.options,{animation_prompt:'Rotate this character 360 degrees',animation_mode:'loop',frame_size:size,frame_count:7}); + } finally {api.close();} + }); +} +for (const args of [['--action','walk','--animation-prompt','spin'],['--frame-size','33'],['--animation-prompt',' '],['--animation-mode','maybe']]) { + test(`invalid sprite options rejected before upload: ${args}`,async()=>{ + const api=await listen(fakeApi({events:[]})); + try {const result=await runCli(['sprite','missing.png',...args,'--max-credits','100'],{DREAMLAYER_API_URL:api.url});assert.notEqual(result.code,0);assert.equal(api.calls.length,0);}finally{api.close();} + }); +} for (const count of [6,101,7.5]) { test(`sprite CLI rejects ${count} frames before a network call`,async()=>{ const api=await listen(fakeApi({events:[]})); From fd19c7e141f75f3e6616878fa7856739dc4baf5f Mon Sep 17 00:00:00 2001 From: TheDesignFounder Date: Sun, 13 Sep 2026 18:44:31 -0700 Subject: [PATCH 2/2] docs: describe sprite output without internal processing terminology --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a6f4ebe..1a5c097 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ MIT. See LICENSE and NOTICE. Sprite requests accept exactly one of `options.animation_prompt` (1–4000 characters) or an `options.action` preset (`walk`, `run`, `idle`). Custom prompts can describe characters, creatures, objects, effects or 360° turntables. `animation_mode` is `loop` or `once`; presets default to loop, custom prompts to once. For a turntable, request a stationary camera and rotating subject. Broad requests do not guarantee correct motion, unseen details or successful effect transparency. -Request integer `frame_count` 7–100 (default 12) and `frame_size` 32, 64, 128, 256, 512, 720 or 1080 (default 512). These are square export canvases, not source video detail; enlargement is identified in the atlas. Aspect ratio and shared alignment are preserved with transparent padding. A bundle contains transparent PNG frames, sheet, atlas, preview and import instructions. Loops exclude the closing sample; one-time actions preserve beginning and end. Insufficient distinct frames, including duplicates caused by cleanup/downscaling, fail without padding or interpolation. Translucent effects can fail background removal; small exports are not automatically pixel art. +Request integer `frame_count` 7–100 (default 12) and `frame_size` 32, 64, 128, 256, 512, 720 or 1080 (default 512). These are square export canvases; a larger export does not guarantee additional detail. Aspect ratio and shared alignment are preserved with transparent padding. A bundle contains transparent PNG frames, sheet, atlas, preview and import instructions, including each frame's playback duration. Choose a repeating loop or a one-time action with a beginning and ending. If the requested number of distinct frames cannot be delivered, the job fails and held credits are returned. Translucent effects can lose detail or fail; small exports are not automatically pixel art. Pricing is unchanged across sizes: frames 1–14 cost $0.14 each; additional frames $0.07 each. One credit is $0.17. Round the complete order upward once to a tenth of a credit. Check `sprite_pricing` in capabilities and approve the quote with `max_credits`. Credits are held during processing, settled after complete delivery and restored on failure/timeout. There is no customer cancellation. Keep the execution ID to resume status. Custom requests need the matching broad-animation server release; older servers reject them. New live generation quality, 100-frame duration and actual cost remain unverified.