scripts/kie.mjs
scripts/kie.mjsBrowse 22 files
2,195 tokens
7,780 bytes
Token encoding: o200k_base
Snapshot 24fd22b
← Back to SKILL.md
1#!/usr/bin/env node2/**3 * scrollcraft asset generator: kie.ai unified jobs API.4 *5 * POST https://api.kie.ai/api/v1/jobs/createTask { model, input }6 * GET https://api.kie.ai/api/v1/jobs/recordInfo?taskId=...7 *8 * COMMANDS9 * still <prompt> <out.png> [--ar 16:9] [--ref a.png]10 * seedream/5-pro-text-to-image (or -image-to-image with --ref).11 * Photoreal by default. Stills are cheap; generate, look, reroll.12 *13 * shot <prompt> <in.png> <out.mp4> [--tail b.png] [--dur 5]14 * kling/v2-1-pro image-to-video. --tail pins the LAST frame, which is15 * the whole trick behind a seamless chain: leg N's tail is leg N+1's16 * head, so the cut between them is frame-identical and invisible.17 *18 * probe print account credit and exit.19 *20 * Env: KIE_AI_API_KEY, read from the project-root .env if not already set.21 */22 23import fs from "node:fs";24import path from "node:path";25 26const API = "https://api.kie.ai";27const UPLOAD = "https://kieai.redpandaai.co/api/file-base64-upload";28 29const MODELS = {30 still: "seedream/5-pro-text-to-image",31 stillEdit: "seedream/5-pro-image-to-image",32 shot: "kling/v2-1-pro",33};34 35// ---------------------------------------------------------------- key ----36function findEnv(start) {37 let dir = path.resolve(start);38 for (let i = 0; i < 8; i++) {39 const p = path.join(dir, ".env");40 if (fs.existsSync(p)) return p;41 const up = path.dirname(dir);42 if (up === dir) break;43 dir = up;44 }45 return null;46}47function loadKey() {48 if (process.env.KIE_AI_API_KEY) return process.env.KIE_AI_API_KEY;49 const envPath = findEnv(process.cwd());50 if (!envPath) throw new Error("KIE_AI_API_KEY not set and no .env found walking up from " + process.cwd());51 for (const line of fs.readFileSync(envPath, "utf8").split(/\r?\n/)) {52 const m = line.match(/^\s*KIE_AI_API_KEY\s*=\s*(.+?)\s*$/);53 if (m) return m[1].replace(/^["']|["']$/g, "");54 }55 throw new Error("KIE_AI_API_KEY not found in " + envPath);56}57const KEY = loadKey();58const H = { "Content-Type": "application/json", Authorization: `Bearer ${KEY}` };59 60// ------------------------------------------------------------- helpers ----61const sleep = (ms) => new Promise((r) => setTimeout(r, ms));62 63async function uploadLocal(file) {64 const abs = path.resolve(file);65 if (!fs.existsSync(abs)) throw new Error("input not found: " + abs);66 const ext = path.extname(abs).slice(1).toLowerCase();67 const mime = ext === "jpg" ? "image/jpeg" : `image/${ext}`;68 const dataUrl = `data:${mime};base64,${fs.readFileSync(abs).toString("base64")}`;69 const res = await fetch(UPLOAD, {70 method: "POST", headers: H,71 body: JSON.stringify({ base64Data: dataUrl, uploadPath: "scrollcraft", fileName: path.basename(abs) }),72 });73 const j = await res.json();74 const url = j?.data?.downloadUrl || j?.data?.fileUrl || j?.data?.url;75 if (!url) throw new Error("upload failed: " + JSON.stringify(j));76 return url;77}78 79// A local path becomes a hosted URL; an http(s) string passes straight through.80const asUrl = (v) => (/^https?:\/\//i.test(v) ? Promise.resolve(v) : uploadLocal(v));81 82async function createTask(model, input) {83 const res = await fetch(`${API}/api/v1/jobs/createTask`, {84 method: "POST", headers: H, body: JSON.stringify({ model, input }),85 });86 const j = await res.json();87 if (j.code !== 200 || !j?.data?.taskId) throw new Error(`createTask ${model}: ${JSON.stringify(j)}`);88 return j.data.taskId;89}90 91async function waitTask(taskId, { label = "job", timeoutMs = 15 * 60 * 1000 } = {}) {92 const t0 = Date.now();93 let delay = 4000;94 for (;;) {95 if (Date.now() - t0 > timeoutMs) throw new Error(`${label}: timed out after ${Math.round((Date.now() - t0) / 1000)}s`);96 const res = await fetch(`${API}/api/v1/jobs/recordInfo?taskId=${encodeURIComponent(taskId)}`, { headers: H });97 const j = await res.json();98 const d = j?.data || {};99 const state = d.state || d.status;100 if (state === "success") {101 let out = d.resultJson;102 if (typeof out === "string") { try { out = JSON.parse(out); } catch {} }103 const urls = out?.resultUrls || out?.result_urls || out?.urls || [];104 if (!urls.length) throw new Error(`${label}: success with no result url: ${JSON.stringify(d)}`);105 return urls;106 }107 if (state === "fail" || state === "failed") {108 throw new Error(`${label} failed: ${d.failMsg || d.failCode || JSON.stringify(d)}`);109 }110 process.stderr.write(` ${label}: ${state || "queued"} (${Math.round((Date.now() - t0) / 1000)}s)\n`);111 await sleep(delay);112 delay = Math.min(delay * 1.25, 15000);113 }114}115 116async function download(url, out) {117 fs.mkdirSync(path.dirname(path.resolve(out)), { recursive: true });118 const res = await fetch(url);119 if (!res.ok) throw new Error(`download ${res.status} ${url}`);120 fs.writeFileSync(path.resolve(out), Buffer.from(await res.arrayBuffer()));121 return out;122}123 124function flag(argv, name, dflt = null) {125 const i = argv.indexOf(name);126 return i > -1 && argv[i + 1] ? argv[i + 1] : dflt;127}128function flags(argv, name) {129 const out = [];130 argv.forEach((a, i) => { if (a === name && argv[i + 1]) out.push(argv[i + 1]); });131 return out;132}133 134// ---------------------------------------------------------------- main ----135const [cmd, ...rest] = process.argv.slice(2);136 137try {138 if (cmd === "probe") {139 const r = await fetch(`${API}/api/v1/chat/credit`, { headers: H });140 const j = await r.json();141 console.log("credit:", j.data);142 143 } else if (cmd === "still") {144 const [prompt, out] = rest;145 if (!prompt || !out) throw new Error('usage: kie.mjs still "<prompt>" <out.png> [--ar 16:9] [--ref a.png]');146 const ar = flag(rest, "--ar", "16:9");147 const refs = flags(rest, "--ref");148 let model = MODELS.still;149 // aspect_ratio, quality and output_format are all required by seedream;150 // omitting any one returns a bare "This field is required" that does not151 // name the field, so keep them explicit rather than relying on defaults.152 const input = {153 prompt,154 aspect_ratio: ar,155 quality: flag(rest, "--quality", "high"),156 output_format: "png",157 nsfw_checker: false,158 };159 if (refs.length) {160 model = MODELS.stillEdit;161 input.image_urls = await Promise.all(refs.map(asUrl));162 }163 const id = await createTask(model, input);164 const urls = await waitTask(id, { label: path.basename(out) });165 await download(urls[0], out);166 console.log(out);167 168 } else if (cmd === "shot") {169 const [prompt, head, out] = rest;170 if (!prompt || !head || !out) {171 throw new Error('usage: kie.mjs shot "<prompt>" <head.png> <out.mp4> [--tail b.png] [--dur 5]');172 }173 const dur = flag(rest, "--dur", "5");174 const tail = flag(rest, "--tail");175 const input = {176 prompt,177 image_url: await asUrl(head),178 duration: String(dur),179 // Camera-move clips are graded on smoothness, so the negative prompt180 // targets exactly what breaks a scrub: judder, warping, cuts.181 negative_prompt: "blur, distortion, low quality, warping, morphing, jitter, flicker, text, watermark, cut, scene change",182 cfg_scale: 0.5,183 };184 if (tail) input.tail_image_url = await asUrl(tail);185 const id = await createTask(MODELS.shot, input);186 const urls = await waitTask(id, { label: path.basename(out), timeoutMs: 20 * 60 * 1000 });187 await download(urls[0], out);188 console.log(out);189 190 } else {191 console.error(`scrollcraft asset generator192 193 node kie.mjs probe194 node kie.mjs still "<prompt>" <out.png> [--ar 16:9] [--ref ref.png]195 node kie.mjs shot "<prompt>" <head.png> <out.mp4> [--tail tail.png] [--dur 5]196`);197 process.exit(1);198 }199} catch (err) {200 console.error("ERROR:", err.message);201 process.exit(1);202}203 Referenced from SKILL.md
SKILL.mdView in source ↗
Source excerpt starting at line 143.SKILL.mdView in source ↗143**Optional upstream path — kie.ai** (vendored verbatim as144[scripts/kie.mjs](scripts/kie.mjs)): photoreal stills and camera-move clips.145Requires the `KIE_AI_API_KEY` environment variable (export it in your shell;
Source excerpt starting at line 232.232 scroll in steps, capture screenshots, and inspect them yourself.233- `scripts/kie.mjs` needs `KIE_AI_API_KEY` and paid credit; prefer234 `image_generate` or user assets when the budget is unclear.