Files
video-create/.claude/skills/video-from-script/scripts/kling-image-generator.js
sion123 8301c7b780 feat(video-from-script): 集成可灵图像生成器并优化审查流程
- 新增 kling-image-generator.js 作为可灵 API 图像生成模块,支持文生图和图生图
- 在 pipeline.js 中集成可灵生图,支持在 images 阶段使用 kling 模型
- 更新 SKILL.md 文档:简化用户审查流程,支持"自行选图"模式
- 实现模型降级链(gemini → kling → mj → gemini),增强生图容错性
- 扩展 `--image-model` 参数支持,允许在重试时切换模型
2026-04-30 00:49:33 +08:00

211 lines
7.2 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 Image Generator - 可灵文生图 / 图生图
*
* 配置来源: config.json 的 kelingApiKey + kelingSecretAccessKey + kelingApiBaseUrl
*
* 用法:
* node kling-image-generator.js "a cute cat" -o ./output
* node kling-image-generator.js "portrait" -r http://img.com/ref.jpg
*/
const fs = require('fs')
const path = require('path')
const https = require('https')
const http = require('http')
const crypto = require('crypto')
// ============================================================================
// 配置
// ============================================================================
const configPath = path.join(__dirname, '..', '..', 'config.json')
const cfg = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, 'utf-8')) : {}
const Config = {
apiBase: (cfg.kelingApiBaseUrl || 'https://api-beijing.klingai.com').replace(/\/+$/, ''),
ak: (cfg.kelingApiKey || '').trim(),
sk: (cfg.kelingSecretAccessKey || '').trim(),
model: cfg.kelingImageModel || 'kling-v2-1',
pollInterval: 5000,
maxPollTime: 300000,
}
// ============================================================================
// JWT 鉴权
// ============================================================================
function base64url(buf) {
return Buffer.from(buf).toString('base64')
.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
}
function getToken() {
if (!Config.ak || !Config.sk) throw new Error('未配置可灵 API 凭证config.json 需要 kelingApiKey + kelingSecretAccessKey')
const header = base64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))
const now = Math.floor(Date.now() / 1000)
const payload = base64url(JSON.stringify({ iss: Config.ak, exp: now + 1800, nbf: now - 5 }))
const sig = base64url(crypto.createHmac('sha256', Config.sk).update(`${header}.${payload}`).digest())
return `${header}.${payload}.${sig}`
}
// ============================================================================
// 图片下载
// ============================================================================
async function download(url, outputPath) {
const protocol = url.startsWith('https') ? https : http
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(outputPath)
protocol.get(url, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
file.close()
fs.unlinkSync(outputPath)
return download(res.headers.location, outputPath).then(resolve).catch(reject)
}
res.pipe(file)
file.on('finish', () => { file.close(); resolve(outputPath) })
}).on('error', (err) => {
file.close()
if (fs.existsSync(outputPath)) fs.unlinkSync(outputPath)
reject(err)
})
})
}
// ============================================================================
// 可灵图片 API
// ============================================================================
const KlingImageApi = {
async submit(prompt, options = {}) {
const { referenceImageUrl = '', aspectRatio = '' } = options
const token = getToken()
const body = { model_name: Config.model, prompt, n: 1 }
if (referenceImageUrl) body.image = referenceImageUrl
if (aspectRatio) body.aspect_ratio = aspectRatio
console.log(`\n📡 提交可灵图片任务`)
console.log(` 模型: ${Config.model}`)
console.log(` 提示词: ${prompt.substring(0, 80)}...`)
if (referenceImageUrl) console.log(` 参考图: ${referenceImageUrl.substring(0, 60)}...`)
const res = await fetch(`${Config.apiBase}/v1/images/generations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify(body),
})
const json = await res.json()
if (json.code !== undefined && json.code !== 0 && json.code !== 200) {
throw new Error(`可灵图片 API 错误 (code=${json.code}): ${json.message || JSON.stringify(json)}`)
}
const taskId = (json.data || {}).task_id
if (!taskId) throw new Error(`可灵图片提交失败: ${JSON.stringify(json)}`)
console.log(` 任务 ID: ${taskId}`)
return taskId
},
async poll(taskId) {
const startTime = Date.now()
console.log(`\n⏳ 等待可灵生成...`)
while (Date.now() - startTime < Config.maxPollTime) {
const token = getToken()
const res = await fetch(`${Config.apiBase}/v1/images/generations/${taskId}`, {
headers: { 'Authorization': `Bearer ${token}` },
})
const json = await res.json()
const data = json.data || {}
const status = data.task_status
if (status === 'succeed') {
const images = data.task_result?.images || []
if (images.length > 0) {
console.log(`\n✅ 可灵图片生成完成!`)
return { success: true, imageUrl: images[0].url }
}
throw new Error('可灵返回成功但无图片')
}
if (status === 'failed') {
throw new Error(`可灵图片生成失败: ${data.task_status_msg || '未知原因'}`)
}
process.stdout.write(` 状态: ${status || 'processing'}\r`)
await new Promise(r => setTimeout(r, Config.pollInterval))
}
throw new Error(`可灵图片生成超时 (${Config.maxPollTime / 1000}s)`)
},
}
// ============================================================================
// 核心流程
// ============================================================================
async function generate(prompt, options = {}) {
const { outputDir = './output', referenceImageUrl = '' } = options
fs.mkdirSync(outputDir, { recursive: true })
const taskId = await KlingImageApi.submit(prompt, { referenceImageUrl })
const result = await KlingImageApi.poll(taskId)
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
const filePath = path.join(outputDir, `${timestamp}_kling.png`)
await download(result.imageUrl, filePath)
console.log(` 下载完成: ${filePath}`)
return { success: true, taskId, imageUrl: result.imageUrl, savedFiles: [filePath] }
}
// ============================================================================
// CLI
// ============================================================================
async function main() {
const args = process.argv.slice(2)
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
console.log(`
用法: node kling-image-generator.js <prompt> [options]
选项:
-o, --output <dir> 输出目录 (默认: ./output)
-r, --ref <url> 参考图 URL
-h, --help 帮助
`)
return
}
const options = { outputDir: './output', referenceImageUrl: '' }
const params = []
let i = 0
while (i < args.length) {
const arg = args[i]
if (arg === '-o' || arg === '--output') { options.outputDir = args[++i] }
else if (arg === '-r' || arg === '--ref') { options.referenceImageUrl = args[++i] }
else { params.push(arg) }
i++
}
const prompt = params.join(' ')
if (!prompt) { console.error('请提供提示词'); process.exit(1) }
await generate(prompt, options)
}
module.exports = { generate, KlingImageApi }
if (require.main === module) {
main().catch(err => {
console.error(`\n❌ 错误: ${err.message}`)
process.exit(1)
})
}