init: video-create project with skills and accounts

This commit is contained in:
2026-04-29 21:04:43 +08:00
commit dadddc7aec
64 changed files with 14715 additions and 0 deletions

View File

@@ -0,0 +1,327 @@
#!/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);
});
}