cang-ying/engines/kling-api-adapter.js

115 lines
3.7 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.

/**
* 光湖视频AI系统 · Kling 可灵 API 适配器
* D136+ · 铸渊 ICE-GL-ZY001 · 2026-06-21
*
* 接入可灵AI视频生成API。价格: kling-v2.6-pro $0.049/s (约1.8元/5秒)
* 非Seedance——用于低成本验证提示词和流程
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const { loadVideoAiEnv } = require('./env-loader');
loadVideoAiEnv();
// API配置
// 仅从苍耳本机环境读取;密钥绝不写入仓库。
const API_KEY = process.env.KLING_API_KEY || '';
const BASE_URL = 'api.klingapi.com';
const POLL_INTERVAL = 3000; // 可灵更快3秒轮询
const MAX_POLL = 60; // 最多3分钟
// 输出路径
const OUT_DIR = (() => {
const jzao = '/Volumes/JZAO/铸渊-ICE-GL-ZY001/OUT-输出/视频/zai-fu-fei-xiu-xian/ep01';
return fs.existsSync(jzao) ? jzao : path.resolve(__dirname, '../outputs/shots');
})();
function apiRequest(method, path_, body = null) {
if (!API_KEY) {
return Promise.reject(new Error('KLING_API_KEY 未设置。请仅在苍耳本机的仓库外密钥文件或环境变量中配置。'));
}
return new Promise((resolve, reject) => {
const options = {
hostname: BASE_URL,
path: path_,
method,
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try { resolve(JSON.parse(data)); }
catch (e) { resolve({ raw: data }); }
});
});
req.on('error', reject);
if (body) req.write(JSON.stringify(body));
req.end();
});
}
function downloadFile(url, dest) {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(dest);
const proto = url.startsWith('https') ? https : require('http');
proto.get(url, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400) {
return downloadFile(res.headers.location, dest).then(resolve).catch(reject);
}
res.pipe(file);
file.on('finish', () => { file.close(); resolve(dest); });
}).on('error', reject);
});
}
async function generateKlingVideo({ prompt, duration = 5, outputPath, model = 'kling-v2.6-pro' }) {
console.log('[Kling] 提交文生视频...');
// 提交
const submit = await apiRequest('POST', '/v1/videos/text2video', {
model,
prompt,
duration: Math.min(duration, 10),
aspect_ratio: '16:9',
mode: 'standard',
});
if (!submit.task_id) {
throw new Error(`提交失败: ${JSON.stringify(submit)}`);
}
console.log(`[Kling] 任务: ${submit.task_id}`);
// 轮询
for (let i = 1; i <= MAX_POLL; i++) {
await new Promise(r => setTimeout(r, POLL_INTERVAL));
const status = await apiRequest('GET', `/v1/videos/${submit.task_id}`);
if (status.status === 'completed') {
const videoUrl = status.video_url || status.output?.video_url;
if (!videoUrl) throw new Error('任务完成但无视频URL');
const out = outputPath || path.join(OUT_DIR, `kling-${submit.task_id}.mp4`);
fs.mkdirSync(path.dirname(out), { recursive: true });
console.log(`[Kling] 下载: ${videoUrl.substring(0,60)}...`);
await downloadFile(videoUrl, out);
console.log(`[Kling] ✅ ${path.basename(out)}`);
return { videoPath: out, taskId: submit.task_id };
}
if (status.status === 'failed') {
throw new Error(`生成失败: ${status.error || JSON.stringify(status)}`);
}
if (i % 10 === 0) console.log(`[Kling] 生成中... (${i}/${MAX_POLL})`);
}
throw new Error('超时');
}
module.exports = { generateKlingVideo };