Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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; 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.

```sh
dreamlayer sprite character.png --action walk --frames 12 --max-credits 9.9 --out walk.zip
Expand Down
26 changes: 22 additions & 4 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ OPTIONS
--out <file> Where to write the image. Default: dreamlayer-<n>.png
--image <file> Attach an image when answering a question that asks for one
--aspect <ratio> 1:1, 16:9, 9:16, 4:3, 3:4. Default 1:1
--action <name> Sprite animation: walk, run, idle
--action <name> Sprite preset: walk, run, idle (walk if no custom prompt)
--animation-prompt <text> Custom animation; cannot combine with --action
--animation-mode <loop|once> Default: loop for presets, once for custom
--frame-size <px> Square export: 32, 64, 128, 256, 512 (default), 720, 1080
--frames <n> Frame count: integer 7–100, default 12
--max-credits <n> Maximum approved charge for the sprite job
--json Machine-readable output on stdout
Expand All @@ -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;
Expand All @@ -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,
Expand All @@ -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");
Expand Down Expand Up @@ -317,6 +334,7 @@ async function main(argv: string[]): Promise<number> {

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();
Expand All @@ -325,7 +343,7 @@ async function main(argv: string[]): Promise<number> {
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];
Expand Down
11 changes: 8 additions & 3 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand All @@ -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.`);
}

Expand Down
18 changes: 18 additions & 0 deletions test/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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:[]}));
Expand Down