Files
video-create/.claude/skills/klingai-1.1.0/scripts/image.mjs

328 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Kling AI image generation — text-to-image, image-to-image, 4K, series, subject
* Node.js 18+, zero external deps
*/
import { submitTask, queryTask, pollTask, downloadFile } from './shared/task.mjs';
import { resolve, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseArgs, getTokenOrExit, readMediaAsValue, resolveAllowedOutputDir } from './shared/args.mjs';
const API_GEN = '/v1/images/generations';
const API_OMNI = '/v1/images/omni-image';
function normalizeModelName(v) {
return String(v || '').trim();
}
function normalizeAliasKey(v) {
return String(v || '').trim().toLowerCase().replace(/[\s_]+/g, '-');
}
function getImageModelAliasTarget(v) {
const key = normalizeAliasKey(v);
const aliasMap = new Map([
['omni3', 'kling-v3-omni'],
['omni-3', 'kling-v3-omni'],
['omni-v3', 'kling-v3-omni'],
['v3-omni', 'kling-v3-omni'],
['o3', 'kling-v3-omni'],
['O3', 'kling-v3-omni'],
['kling-image-o3', 'kling-v3-omni'],
['kling-o3', 'kling-v3-omni'],
['omni1', 'kling-image-o1'],
['omni-1', 'kling-image-o1'],
['o1', 'kling-image-o1'],
['kling-o1', 'kling-image-o1'],
]);
return aliasMap.get(key) || '';
}
function validateModelAliasInput(rawModel) {
if (!rawModel) return;
const model = normalizeModelName(rawModel).toLowerCase();
const target = getImageModelAliasTarget(rawModel);
if (!target || model === target) return;
throw new Error(
`Invalid --model alias / --model 使用了别名: ${rawModel}\n`
+ `Use canonical name / 请改用标准名: ${target}\n`
+ 'Alias mapping / 别名映射: omni3 | omni v3 | o3 -> kling-v3-omni; image o1/omni1 -> kling-image-o1',
);
}
function validateModelForRoute(apiPath, args) {
validateModelAliasInput(args.model);
const model = normalizeModelName(args.model);
if (!model) return;
// We only validate what we can be sure about from public enums.
// - omni-image: only kling-v3-omni / kling-image-o1
// - generations: must not use omni-only models
if (apiPath === API_OMNI) {
const allowed = new Set(['kling-v3-omni', 'kling-image-o1']);
if (!allowed.has(model)) {
throw new Error(
`Invalid --model for omni-image / omni-image 不支持该模型: ${model}\n`
+ `Allowed / 允许: kling-v3-omni, kling-image-o1`,
);
}
} else {
const forbidden = new Set(['kling-v3-omni', 'kling-image-o1', 'kling-video-o1']);
if (forbidden.has(model)) {
throw new Error(
`Invalid --model for generations / generations 不支持该模型: ${model}\n`
+ `Hint / 提示: remove --model or use kling-v3`,
);
}
}
}
function parseImageInputs(rawImageArg) {
if (!rawImageArg) return [];
const parts = String(rawImageArg).split(',').map(s => s.trim());
if (parts.some(p => !p)) {
throw new Error(
'Invalid --image list / --image 列表中存在空值;请移除空项并确保每个 image 非空。',
);
}
return parts;
}
function parseElementIds(rawElementIdsArg) {
if (!rawElementIdsArg) return [];
const parts = String(rawElementIdsArg).split(',').map(s => s.trim());
if (parts.some(p => !p)) {
throw new Error(
'Invalid --element_ids list / --element_ids 列表中存在空值;请移除空项并确保每个 element_id 非空。',
);
}
return parts;
}
function validateOmniRefCount(imageInputs, elementIds) {
const totalRefs = imageInputs.length + elementIds.length;
if (totalRefs > 10) {
throw new Error(
`Too many refs for omni-image / omni-image 参考图与主体总数超限: max 10 (current ${totalRefs})`,
);
}
}
function printHelp() {
console.log(`Kling AI image generation
Usage:
node kling.mjs image --prompt <text> [options] # Text/image-to-image
node kling.mjs image --prompt "..." [--resolution 4k] # 4K / series / subject → Omni
node kling.mjs image --model kling-v3-omni --prompt "..." # explicit Omni model → omni-image (t2i / i2i)
node kling.mjs image --task_id <id> [--download] # Query/download
Submit (common):
--prompt Image description (required). Omni: <<<image_1>>> / <<<element_1>>>
--resolution 1k / 2k / 4k (4k uses Omni)
--aspect_ratio Aspect ratio (default: 16:9 basic, auto for Omni)
--n Number of images 1-9
--output_dir Output dir (default: ./output)
--no-wait Submit only, do not wait
--wait Wait for completion (default)
Basic API:
--negative_prompt Negative prompt
--model Model (default: kling-v3)
Omni (4K/series/subject):
--model kling-v3-omni / kling-image-o1
--result_type single / series (default: single)
--series_amount Series count 2-9 (when result_type=series)
--image Reference image path or URL, comma-separated for multiple
--element_ids Subject IDs, comma-separated
(omni refs) image count + element count <= 10
Query/download:
--task_id Task ID
--download Download if task succeeded
Env:
credentials file ~/.config/kling/.credentials (access_key_id, secret_access_key)
KLING_TOKEN Session-only Bearer (optional override)
KLING_MEDIA_ROOTS Comma-separated extra dirs for --image / --output_dir (default: cwd only)
KLING_ALLOW_ABSOLUTE_PATHS=1 Allow any local path (e.g. WSL downloads)`);
}
function useOmniApi(args) {
// Match video.mjs chooseApiPath: explicit Omni image models → omni-image (incl. plain text-to-image).
const m = normalizeModelName(args.model).toLowerCase();
if (m === 'kling-v3-omni' || m === 'kling-image-o1') return true;
if (args.element_ids) return true;
if (args.result_type === 'series') return true;
if ((args.resolution || '').toLowerCase() === '4k') return true;
if ((args.aspect_ratio || '').toLowerCase() === 'auto') return true;
if (args.image && args.image.includes(',')) return true;
return false;
}
async function queryTaskAnyPath(taskId, token) {
for (const apiPath of [API_OMNI, API_GEN]) {
try {
const data = await queryTask(apiPath, taskId, token);
if (data && (data.task_status === 'succeed' || data.task_status === 'failed' || data.task_status === 'processing' || data.task_status === 'submitted')) {
return { apiPath, data };
}
} catch (_) { /* try next */ }
}
throw new Error(`Task not found / 未找到任务: ${taskId}`);
}
function collectImageUrls(taskResult) {
const urls = [];
const append = (list) => {
if (!Array.isArray(list)) return;
for (const item of list) {
if (item?.url) urls.push(item.url);
}
};
append(taskResult?.images);
append(taskResult?.series_images);
if (urls.length === 0 && taskResult?.url) urls.push(taskResult.url);
return urls;
}
async function pollAndDownloadImages(apiPath, taskId, outputDir, opts = {}) {
const data = await pollTask(apiPath, taskId, opts);
const urls = collectImageUrls(data?.task_result || {});
if (urls.length === 0) {
throw new Error('Task succeeded but missing image urls / 任务成功但未返回图片 URL');
}
const outPaths = [];
for (let i = 0; i < urls.length; i++) {
const outPath = join(outputDir, urls.length === 1 ? `${taskId}.png` : `${taskId}_${i}.png`);
await downloadFile(urls[i], outPath);
outPaths.push(outPath);
}
return outPaths;
}
export async function main() {
const args = parseArgs(process.argv);
if (args.help) { printHelp(); return; }
validateModelAliasInput(args.model);
const token = await getTokenOrExit();
const outputDir = resolveAllowedOutputDir(args.output_dir || './output');
const queryHint = `node kling.mjs image --task_id`;
if (args.task_id && !args.prompt) {
try {
const { apiPath, data } = await queryTaskAnyPath(args.task_id, token);
console.log(`Task ID / 任务 ID: ${args.task_id}`);
console.log(`Status / 状态: ${data?.task_status || 'unknown'}`);
const result = data?.task_result || {};
const imageUrls = collectImageUrls(result);
imageUrls.forEach((url, i) => {
console.log(`Image / 图片[${i}]: ${url}`);
});
if (args.download && imageUrls.length > 0) {
const { mkdir } = await import('node:fs/promises');
await mkdir(outputDir, { recursive: true });
for (let i = 0; i < imageUrls.length; i++) {
const outPath = join(outputDir, imageUrls.length === 1 ? `${args.task_id}.png` : `${args.task_id}_${i}.png`);
await downloadFile(imageUrls[i], outPath);
}
}
} catch (e) {
console.error(`Error / 错误: ${e.message}`);
process.exit(1);
}
return;
}
if (!args.prompt) {
console.error('Error / 错误: --prompt or --task_id required');
console.error('Use --help / 使用 --help 查看帮助');
process.exit(1);
}
const apiPath = useOmniApi(args) ? API_OMNI : API_GEN;
const imageInputs = parseImageInputs(args.image);
const elementIds = parseElementIds(args.element_ids);
try {
validateModelForRoute(apiPath, args);
if (apiPath === API_GEN) {
const payload = {
model_name: args.model || 'kling-v3',
prompt: args.prompt,
negative_prompt: args.negative_prompt || '',
n: parseInt(args.n || '1', 10),
aspect_ratio: args.aspect_ratio || '16:9',
resolution: args.resolution || '1k',
callback_url: '',
};
if (imageInputs.length > 0) {
payload.image = await readMediaAsValue(imageInputs[0]);
}
const result = await submitTask(API_GEN, payload, token);
console.log(`\nTask ID / 任务 ID: ${result.taskId}`);
console.log(`Query / 查询: ${queryHint} ${result.taskId} [--download]`);
if (args.wait !== false) {
console.log();
const outPaths = await pollAndDownloadImages(API_GEN, result.taskId, outputDir, { token });
console.log(`\n✓ Done / 完成: ${outPaths.length} image(s)`);
outPaths.forEach((p) => console.log(` - ${p}`));
}
return;
}
const payload = {
model_name: args.model || 'kling-v3-omni',
prompt: args.prompt,
resolution: (args.resolution || '1k').toLowerCase(),
aspect_ratio: (args.aspect_ratio || 'auto').toLowerCase(),
result_type: args.result_type || 'single',
callback_url: '',
};
if (payload.result_type === 'series') {
if (imageInputs.length === 0) {
throw new Error(
'Invalid --result_type series without --image / 组图仅支持 i2i请提供 --imaget2i 不支持 series。',
);
}
payload.series_amount = parseInt(args.series_amount || '4', 10);
} else {
payload.n = parseInt(args.n || '1', 10);
}
validateOmniRefCount(imageInputs, elementIds);
if (imageInputs.length > 0) {
payload.image_list = [];
for (const img of imageInputs) {
payload.image_list.push({ image: await readMediaAsValue(img) });
}
}
if (elementIds.length > 0) {
payload.element_list = elementIds.map(id => ({ element_id: id }));
}
const result = await submitTask(API_OMNI, payload, token);
console.log(`\nTask ID / 任务 ID: ${result.taskId}`);
console.log(`Query / 查询: ${queryHint} ${result.taskId} [--download]`);
if (args.wait !== false) {
console.log();
const outPaths = await pollAndDownloadImages(API_OMNI, result.taskId, outputDir, { token });
console.log(`\n✓ Done / 完成: ${outPaths.length} image(s)`);
outPaths.forEach((p) => console.log(` - ${p}`));
}
} catch (e) {
console.error(`Error / 错误: ${e.message}`);
process.exit(1);
}
}
const __filename = fileURLToPath(import.meta.url);
if (process.argv[1] && resolve(__filename) === resolve(process.argv[1])) {
main().catch((e) => {
console.error(`Error / 错误: ${e?.message || e}`);
process.exit(1);
});
}