{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://raw.githubusercontent.com/api-evangelist/ludo-ai/main/json-schema/ludo-ai-animate-sprite-keyframes-payload-schema.json", "title": "AnimateSpriteKeyframesPayload", "description": "Payload for generating an animated spritesheet that interpolates through up to three fixed keyframes (initial / middle / final). Runs on hydra (default), forge or forge-pixel - the models supporting a middle keyframe. Input images can be provided in base64 or URL. At least one of initial_image or middle_image must be provided.", "x-generated": "2026-10-02", "x-method": "derived", "x-generator": "derive-json-schema.py", "x-source": "openapi/ludo-ai-openapi.json#/components/schemas/AnimateSpriteKeyframesPayload", "type": "object", "properties": { "motion_prompt": { "type": "string", "description": "Optional text description of the desired animation, e.g. \"walking\", \"attack slash\". When omitted, the motion is derived purely from the keyframes. When provided, describe exactly and unambiguously what the character should do. Specific or complex actions are fine; there is no need to simplify them. Do not over-describe, though: too much detail can harm the animation, and an action the model already knows should be named, not decomposed into its steps (\"walking\", never \"move the left foot forward, then the right foot\"). Do not restate what the keyframes already show (the character, its appearance and equipment, the art style, the background, the lighting, the camera), and leave frame count and timing to the frames and duration fields. Negative phrasing (\"no background\", \"do not move the camera\", \"without a weapon\") works only on hydra. On forge and forge-pixel it backfires: naming something you do not want makes it more likely to appear, so \"without a weapon\" tends to produce a weapon. On those models never phrase anything negatively; state only what should happen, and control everything else through the keyframes and the other fields. Keep it well under 100 words: in longer prompts details start competing and some get dropped." }, "initial_image": { "type": "string", "description": "The url OR base64 of the first keyframe. Optional when a middle_image is provided." }, "middle_image": { "type": "string", "description": "The url OR base64 of the middle keyframe the animation passes through between the initial and final frames. Consecutive keyframes must differ - two identical keyframes in a row produce no motion across that span." }, "final_image": { "type": "string", "description": "The url OR base64 of the final keyframe. Requires an initial_image or middle_image to anchor the animation." }, "model": { "type": "string", "description": "Model to use. Available models:\n- \"hydra\" (Hydra): 3 credits/s, min charge 9 credits · Most capable all-around model, generates audio\n- \"forge\" (Forge): 1.5 credits/s, min charge 4 credits · Sharper detail, simpler motion. Best for simple body shapes and short actions; may need a few tries\n- \"forge-pixel\" (Forge Pixel): 1.5 credits/s, min charge 4 credits · Best for low-res pixel art animations\nforge-pixel expects real pixel art - a perfect pixel grid at its natural resolution; for other art use hydra or forge. Tiling image types stay seamless only on forge and forge-pixel.", "enum": [ "hydra", "forge", "forge-pixel" ], "default": "hydra" }, "loop": { "type": "boolean", "default": true, "description": "Trim the animation at the beginning or end to create a seamless loop. Not guaranteed to produce a perfect loop." }, "crop": { "type": "boolean", "default": true, "description": "Crop sprite frames to fit content. Results in smaller spritesheets but inconsistent frame sizes across different animations." }, "frames": { "type": "number", "format": "integer", "description": "Maximum number of frames in the output spritesheet. Allow about one second of duration per 16 frames - above that the model may not be able to produce them all (64 frames needs a duration of at least 4). The result can have fewer frames when the duration is short or bad frames were removed.", "default": 36, "enum": [ 4, 9, 16, 25, 36, 49, 64 ] }, "frame_size": { "type": "number", "format": "integer", "description": "Size of each frame in pixels (width and height). It sizes the exported frames only - it does not change how the sprite is generated. 0 is maximum resolution. -1 is AI 1.5x upscaling. -9 is True Size: frames keep the size and position of the input frame, untrimmed, so the spritesheet lines up exactly with the input image - use it (not margin_ratio_mode \"none\") whenever the output must align with your input; it works with any margin mode. Exporting larger than the input image adds no detail: do not pick a size above the input's own resolution, and 0 or -1 gain nothing on inputs under 512 px.", "default": 0, "enum": [ 32, 64, 96, 128, 192, 256, 384, 0, -1, -9 ] }, "margin_ratio": { "type": "number", "format": "float", "description": "Deprecated: prefer margin_ratio_horizontal / margin_ratio_vertical. Amount of padding around the sprite as a ratio (0.0 to 1.0). Sets both axes to this value. A per-axis value, when also given, overrides this for that axis. Supplying any margin value selects margin_ratio_mode \"manual\" unless margin_ratio_mode is set explicitly.", "deprecated": true }, "margin_ratio_horizontal": { "type": "number", "format": "float", "description": "Horizontal padding around the sprite as a ratio (0.0 to 1.0). Every bit of margin is resolution taken from the sprite, so keep it as tight as the motion allows - about 0.15 - and go higher only for motion that extends sideways (sword slashes, punches) or if the sprite gets cut off. Supplying a value selects margin_ratio_mode \"manual\" unless margin_ratio_mode is set explicitly; overrides the legacy margin_ratio on this axis." }, "margin_ratio_vertical": { "type": "number", "format": "float", "description": "Vertical padding around the sprite as a ratio (0.0 to 1.0). Every bit of margin is resolution taken from the sprite, so keep it as tight as the motion allows - about 0.15 - and go higher only for motion that extends up or down (jumps) or if the sprite gets cut off. Supplying a value selects margin_ratio_mode \"manual\" unless margin_ratio_mode is set explicitly; overrides the legacy margin_ratio on this axis." }, "margin_ratio_mode": { "type": "string", "description": "How the sprite is framed for generation. The model always generates on a canvas of the same size, so any margin is canvas the sprite does not fill: the more margin, the lower the resolution and detail of the sprite. \"auto\" (default, recommended) trims and centers the sprite and picks a margin suited to the motion. \"manual\" uses margin_ratio_horizontal / margin_ratio_vertical; keep them as tight as the motion allows (about 0.15) and raise only the axis the motion needs. \"none\" animates the input exactly as framed, with no trimming and no margin added, so the input must already have suitable margins: a sprite touching the edges gets its limbs, weapons and effects cut off as soon as it moves, and an image that is mostly empty space yields a small, low-detail sprite. \"none\" is the way to keep a deliberately off-center sprite (e.g. a character at the left edge firing a beam to the right), but results can be unpredictable. Do not use \"none\" to make the spritesheet line up with the input image - use frame_size -9 (True Size) for that; it works with any margin mode. Rules: omit margin_ratio_mode and send margin_ratio_horizontal / margin_ratio_vertical to get \"manual\" automatically; \"manual\" with no margin value fails with HTTP 400; \"auto\" or \"none\" sent together with a margin value fails with HTTP 400 (the value would be ignored).", "enum": [ "auto", "manual", "none" ], "default": "auto" }, "image_type": { "type": "string", "description": "Type of sprite being animated. Affects generation parameters and styling. Tiling types (horizontal / vertical tiles, scrolling backgrounds, parallax layers, textures) stay seamless only on forge and forge-pixel, and only if the input image itself already wraps seamlessly on that axis; otherwise the animation shows a visible seam when tiled.", "default": "sprite", "enum": [ "sprite", "sprite-vfx", "item-icon", "ui_asset", "logo", "sprite-tiling-horizontal", "sprite-tiling-vertical", "parallax_layer", "tile", "texture", "portrait", "card-art" ] }, "duration": { "type": "number", "format": "float", "default": 3, "description": "Duration in seconds. Available values depend on the model:\n- hydra: 3, 3.5, 4, 4.5, 5\n- forge: 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5\n- forge-pixel: 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5" }, "augment_prompt": { "type": "boolean", "default": true, "description": "Rewrites your prompt behind the scenes into the form the model works best with. Leave it on (the default). Turning it off does not give you more control - it usually gives worse results. Do not disable it unless you really know what you are doing and have tested your prompts extensively." }, "gif": { "type": "boolean", "default": false, "description": "When true, generates an animated GIF from the spritesheet and returns it in gif_url. Disabled by default to reduce response time." }, "individual_frames": { "type": "boolean", "default": false, "description": "When true, extracts each frame from the spritesheet as an individual image and returns the URLs in individual_frame_urls." }, "spritesheet_with_background": { "type": "boolean", "default": false, "description": "When true, also returns the spritesheet with background intact (before background removal). Useful for manually fixing background removal issues. The with-background spritesheet URL will be in spritesheet_with_background_url." }, "request_id": { "type": "string", "description": "Optional client-provided identifier, unique per request. Re-sending the same request_id returns the existing job instead of generating again. Also usable as the request_id filter of listGenerations." }, "async": { "type": "boolean", "default": true, "description": "Defaults to true: the call returns 202 immediately with a job id to poll with getApiJob. Set false for a synchronous response (deprecated but supported): the call then blocks until the result is ready." } } }