seedance 0.1.3

Generate video with ByteDance Seedance 2.0 from the terminal. Agent-friendly.
/// Machine-readable capability manifest.
pub fn run() {
    let name = env!("CARGO_PKG_NAME");
    let config_path = crate::config::config_path();

    let info = serde_json::json!({
        "name": name,
        "version": env!("CARGO_PKG_VERSION"),
        "description": env!("CARGO_PKG_DESCRIPTION"),
        "commands": {
            "generate": {
                "description": "Create a video generation task with Seedance 2.0 (alias: gen)",
                "aliases": ["gen"],
                "args": [],
                "options": [
                    {"name": "--prompt",            "short": "-p", "type": "string", "required": false, "description": "Text prompt (supports [Image N], [Video N], [Audio N], time codes like [0-4s])"},
                    {"name": "--image",             "short": "-i", "type": "string", "required": false, "multiple": true, "description": "Reference image path or URL (repeatable, max 9; role=reference_image)"},
                    {"name": "--first-frame",                      "type": "string", "required": false, "description": "Image used as first frame (role=first_frame)"},
                    {"name": "--last-frame",                       "type": "string", "required": false, "description": "Image used as last frame (role=last_frame, requires --first-frame)"},
                    {"name": "--video",             "short": "-v", "type": "string", "required": false, "multiple": true, "description": "Reference video URL (repeatable, max 3, local paths not supported)"},
                    {"name": "--audio",             "short": "-a", "type": "string", "required": false, "multiple": true, "description": "Reference audio path or URL (wav/mp3, repeatable, max 3; requires at least one image or video)"},
                    {"name": "--duration",          "short": "-d", "type": "integer", "required": false, "default": 5, "description": "Seconds [4,15] or -1 for auto"},
                    {"name": "--resolution",        "short": "-r", "type": "string", "required": false, "default": "720p", "values": ["480p", "720p"], "description": "Output resolution (Seedance 2.0 has no 1080p)"},
                    {"name": "--ratio",                            "type": "string", "required": false, "default": "adaptive", "values": ["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"], "description": "Aspect ratio"},
                    {"name": "--seed",                             "type": "integer", "required": false, "default": -1, "description": "Seed; -1 = random"},
                    {"name": "--audio-sync",                       "type": "bool", "required": false, "default": true, "description": "Generate synchronized audio"},
                    {"name": "--no-audio-sync",                    "type": "bool", "required": false, "default": false, "description": "Output silent video"},
                    {"name": "--watermark",                        "type": "bool", "required": false, "default": false, "description": "Add ModelArk watermark"},
                    {"name": "--fast",                             "type": "bool", "required": false, "default": false, "description": "Use Seedance 2.0 Fast"},
                    {"name": "--model",                            "type": "string", "required": false, "description": "Override model id"},
                    {"name": "--callback-url",                     "type": "string", "required": false, "description": "Webhook to notify on status change"},
                    {"name": "--safety-identifier",                "type": "string", "required": false, "description": "Hashed end-user id (<=64 ASCII chars)"},
                    {"name": "--wait",              "short": "-w", "type": "bool", "required": false, "description": "Block until the task finishes"},
                    {"name": "--output",            "short": "-o", "type": "path", "required": false, "description": "Output file path (implies --wait)"},
                    {"name": "--poll-interval",                    "type": "integer", "required": false, "default": 5, "description": "Seconds between polls while waiting"},
                    {"name": "--timeout",                          "type": "integer", "required": false, "default": 900, "description": "Max wait seconds (0 = unlimited)"},
                    {"name": "--api-key",                          "type": "string", "required": false, "description": "API key override (else SEEDANCE_API_KEY / ARK_API_KEY / config)"}
                ]
            },
            "status": {
                "description": "Retrieve a video generation task (alias: get)",
                "aliases": ["get"],
                "args": [{"name": "id", "kind": "positional", "type": "string", "required": true}],
                "options": [
                    {"name": "--api-key", "type": "string", "required": false}
                ]
            },
            "download": {
                "description": "Download the video for a completed task",
                "args": [{"name": "id", "kind": "positional", "type": "string", "required": true}],
                "options": [
                    {"name": "--output", "short": "-o", "type": "path", "required": false, "description": "Output file path (default: <id>.mp4)"},
                    {"name": "--api-key", "type": "string", "required": false}
                ]
            },
            "cancel": {
                "description": "Cancel a queued task (alias: rm)",
                "aliases": ["rm"],
                "args": [{"name": "id", "kind": "positional", "type": "string", "required": true}],
                "options": [
                    {"name": "--api-key", "type": "string", "required": false}
                ]
            },
            "character-sheet": {
                "description": "Build a 9-angle character reference sheet from a single photo via nanaban (Nano Banana Pro). Resulting PNG can be passed to `generate --image` to keep a specific person consistent across Seedance shots -- works around the single-face upload block.",
                "args": [
                    {"name": "input", "kind": "positional", "type": "string", "required": true, "description": "Path or URL of the subject photo"}
                ],
                "options": [
                    {"name": "--output", "short": "-o", "type": "path",   "required": false, "description": "Output PNG path (default: ~/Documents/seedance/character-sheet-<hash>.png)"},
                    {"name": "--style",                   "type": "string", "required": false, "description": "Extra styling hints to append to the grid prompt"},
                    {"name": "--angles",                  "type": "integer", "required": false, "default": 9, "values": [4, 9], "description": "9 (3x3) or 4 (2x2) cells"}
                ],
                "requires": ["nanaban"],
                "credit": "Community trick originated by @wtry1102 / @voxelplot Advanced Workflow #8"
            },
            "audio-to-video": {
                "description": "Wrap an audio file inside a silent mp4 so it can be fed as `--video`. Workaround for Seedance 2.0's reference-audio-mutates-lyrics quirk. Hard-caps output duration at 14.5s (BytePlus rejects > 15.2s). Uses ffmpeg.",
                "args": [
                    {"name": "input", "kind": "positional", "type": "path", "required": true, "description": "Input audio file (wav/mp3/m4a/etc)"}
                ],
                "options": [
                    {"name": "--output",     "short": "-o", "type": "path",    "required": false, "description": "Output mp4 path (default: <input>.silent.mp4)"},
                    {"name": "--background",                "type": "string",  "required": false, "default": "black", "values": ["black","white"]},
                    {"name": "--height",                    "type": "integer", "required": false, "default": 480, "values": [480, 720]},
                    {"name": "--upload",                    "type": "bool",    "required": false, "default": false, "description": "Also upload to tmpfiles.org and print the hosted URL (ready for --video)"}
                ],
                "requires": ["ffmpeg"],
                "credit": "@simeonnz via @MrDavids1"
            },
            "prep-face": {
                "description": "Apply the empirical face-filter-bypass recipe to a portrait so it can be used as --first-frame without triggering BytePlus's real-face detector. Default keeps colour; --bw swaps to grayscale. Uses ImageMagick. Discovered empirically 2026-04-16.",
                "args": [
                    {"name": "input", "kind": "positional", "type": "path", "required": true, "description": "Input portrait"}
                ],
                "options": [
                    {"name": "--output", "short": "-o", "type": "path",    "required": false, "description": "Output PNG path"},
                    {"name": "--bw",                    "type": "bool",    "required": false, "default": false, "description": "Use the pure B&W + grain variant instead of colour-with-grain"},
                    {"name": "--width",                 "type": "integer", "required": false, "default": 512, "description": "Output width (256-1024, 512 is proven passing value)"}
                ],
                "requires": ["imagemagick"],
                "recipes": {
                    "heavy-grain-color": "magick <in> -resize 512x -attenuate 1.4 +noise Gaussian -unsharp 0x1 -modulate 100,90,100 <out>",
                    "bw-grain":          "magick <in> -resize 512x -modulate 100,55,100 -attenuate 0.8 +noise Gaussian -unsharp 0x1 -colorspace Gray -separate -combine <out>"
                }
            },
            "upload": {
                "description": "Upload a local file to tmpfiles.org and print the direct-download HTTPS URL (ready for --video / --image). tmpfiles.org is BytePlus-fetcher-compatible; catbox.moe is blocklisted.",
                "args": [
                    {"name": "input", "kind": "positional", "type": "path", "required": true}
                ],
                "options": []
            },
            "models": {
                "description": "List available Seedance model ids (alias: ls)",
                "aliases": ["ls"],
                "args": [],
                "options": []
            },
            "doctor": {
                "description": "Check API key, base URL, and dependency health",
                "args": [],
                "options": []
            },
            "agent-info": {
                "description": "This manifest",
                "aliases": ["info"],
                "args": [],
                "options": []
            },
            "skill install": {"description": "Install skill file to agent platforms", "args": [], "options": []},
            "skill status":  {"description": "Check skill installation status",        "args": [], "options": []},
            "config show":   {"description": "Display effective merged configuration (api_key masked)", "args": [], "options": []},
            "config path":   {"description": "Show configuration file path",            "args": [], "options": []},
            "config set":    {
                "description": "Persist a setting in the config file (chmod 600)",
                "args": [
                    {"name": "key",   "kind": "positional", "type": "string", "required": true, "values": ["api-key","base-url","model"]},
                    {"name": "value", "kind": "positional", "type": "string", "required": true}
                ],
                "options": []
            },
            "config unset":  {
                "description": "Remove a setting from the config file",
                "args": [
                    {"name": "key", "kind": "positional", "type": "string", "required": true, "values": ["api-key","base-url","model"]}
                ],
                "options": []
            },
            "update": {
                "description": "Self-update binary from GitHub Releases",
                "args": [],
                "options": [{"name": "--check", "type": "bool", "required": false, "default": false}]
            }
        },
        "global_flags": {
            "--json":  {"description": "Force JSON output (auto-enabled when piped)", "type": "bool", "default": false},
            "--quiet": {"description": "Suppress informational output",               "type": "bool", "default": false}
        },
        "exit_codes": {
            "0": "Success",
            "1": "Transient error (IO, network, API) -- retry",
            "2": "Config error -- fix setup",
            "3": "Bad input -- fix arguments",
            "4": "Rate limited -- wait and retry"
        },
        "envelope": {
            "version": "1",
            "success": "{ version, status, data }",
            "error": "{ version, status, error: { code, message, suggestion } }"
        },
        "config": {
            "path": config_path.display().to_string(),
            "env_prefix": "SEEDANCE_",
            "fallback_env_keys": ["SEEDANCE_API_KEY", "ARK_API_KEY"]
        },
        "api": {
            "provider": "BytePlus ModelArk",
            "base_url_default": crate::config::DEFAULT_BASE_URL,
            "default_model": crate::config::DEFAULT_MODEL,
            "fast_model": crate::config::DEFAULT_MODEL_FAST,
            "reference_limits": {
                "images": "0-9",
                "videos": "0-3 URLs, total <=15s, local paths not supported by API",
                "audio": "0-3 (wav/mp3), total <=15s, must accompany at least one image or video"
            },
            "prompt_syntax": "Use [Image 1]..[Image N], [Video 1]..[Video 3], [Audio 1]..[Audio 3], and time codes like `[0-4s]: ...`"
        },
        "auto_json_when_piped": true,
        "companion_tools": {
            "nanaban": {
                "purpose": "Image generation. Used by `seedance character-sheet` to render a 9-angle reference grid (Nano Banana Pro / Gemini 3 Pro image).",
                "install": "npm i -g nanaban",
                "repo": "https://github.com/199-biotechnologies/nanaban",
                "required_for": ["character-sheet"]
            },
            "ffmpeg": {
                "purpose": "Media transcoding. Used by `seedance audio-to-video` to wrap audio in a silent mp4.",
                "install": "brew install ffmpeg (macOS) or apt install ffmpeg (linux)",
                "required_for": ["audio-to-video"]
            },
            "imagemagick": {
                "purpose": "Raster image processing. Used by `seedance prep-face` to apply the face-filter-bypass recipe.",
                "install": "brew install imagemagick (macOS) or apt install imagemagick (linux)",
                "required_for": ["prep-face"]
            }
        },
        "empirical_findings": {
            "video_url_allowed_schemes": ["http://", "https://", "asset://"],
            "video_url_rejected_examples": {
                "catbox.moe": "files.catbox.moe/* -- rejected with InvalidParameter: Invalid video_url",
                "litter.catbox.moe": "rejected with InvalidParameter: Invalid video_url",
                "tmpfiles.org_https": "WORKS -- https://tmpfiles.org/dl/<id>/<file> accepted and fetched",
                "transfer.sh": "SSL certificate expired as of 2026-04-16",
                "0x0.st": "uploads disabled",
                "files_api_file_id": "BytePlus Files API (/api/v3/files) exists but file IDs are rejected in video_url.url -- 'invalid url scheme'"
            },
            "video_url_real_duration_cap_secs": 15.2,
            "video_url_documented_duration_cap_secs": 15.0,
            "face_filter_passing_recipes": {
                "heavy_grain_color": "magick <in> -resize 512x -attenuate 1.4 +noise Gaussian -unsharp 0x1 -modulate 100,90,100 <out>",
                "bw_grain": "magick <in> -resize 512x -modulate 100,55,100 -attenuate 0.8 +noise Gaussian -unsharp 0x1 -colorspace Gray -separate -combine <out>"
            },
            "face_filter_failing_inputs": [
                "raw portrait photo",
                "Nano Banana Pro photographic re-render",
                "GPT Image photographic re-render",
                "9-panel character sheet (too many faces)",
                "oil-paint / paint-filter",
                "cinema-crush color grade",
                "cross-processed teal-orange grade",
                "mild low-res + grain (below threshold)"
            ],
            "identity_anchoring": "Subject description in the prompt IS the character identity. Seed only randomises micro-performance and framing. To get visibly different characters across takes you MUST vary age, hair, build, ethnicity, and clothing in the prompt text -- not just the seed.",
            "reference_video_face_filter": "BytePlus also applies the face filter to external --video references (InputVideoSensitiveContentDetected.PrivacyInformation). Face-free b-roll / logo cards are the only way to pass an external style reference through."
        },
        "agent_workflows": {
            "consistent_person_across_shots": [
                "1. seedance character-sheet <single_photo> -o sheet.png",
                "2. seedance generate --image sheet.png --prompt '...(subject from [Image 1]) ...' --wait",
                "(The grid acts as a multi-angle subject reference. Do NOT also use --first-frame with the same photo -- multi-mode conflict.)"
            ],
            "exact_music_or_dialogue_preserved": [
                "1. seedance audio-to-video song.mp3 --upload     # writes .silent.mp4 AND uploads to tmpfiles.org",
                "2. Copy the printed URL and pass as --video",
                "3. seedance generate --video <url> --prompt '...use [Video 1] as the voiceover verbatim...' --first-frame <prepped_face> --wait",
                "(Passing raw --audio lets Seedance rewrite lyrics/melody. Hard-cap on wrapped mp4 is 14.5s to stay under BytePlus's 15.2s real limit.)"
            ],
            "specific_person_face_across_shots": [
                "1. seedance prep-face photo.jpg -o face.png    # heavy-grain colour variant passes the filter",
                "2. seedance generate --first-frame face.png --prompt '... keep his face consistent with the first frame ...' --wait",
                "(Alternative: --bw flag produces a B&W-grain variant that also passes but output stays monochrome.)"
            ]
        }
    });
    println!("{}", serde_json::to_string_pretty(&info).unwrap());
}