feat(video-from-script): 将可灵生图切换为多图参考风格接口
将 Kling 图像生成器从单图生图 API 升级为多图参考生图端点,支持风格参考图片功能,并更新降级链顺序
This commit is contained in:
@@ -140,7 +140,7 @@ Step 3: 生图 → 审查
|
|||||||
|
|
||||||
生图模型
|
生图模型
|
||||||
- 支持模型:gemini / mj / kling
|
- 支持模型:gemini / mj / kling
|
||||||
- 降级链:gemini → kling → mj → gemini(循环)
|
- 降级链:gemini → mj → kling → gemini(循环)
|
||||||
- 触发:连续失败→ Agent 换下一个模型重跑失败项
|
- 触发:连续失败→ Agent 换下一个模型重跑失败项
|
||||||
- 操作:`pipeline.js run --manifest <path> --phase images --retry-failed --image-model <新模型>`
|
- 操作:`pipeline.js run --manifest <path> --phase images --retry-failed --image-model <新模型>`
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kling Image Generator - 可灵文生图 / 图生图
|
* Kling Image Generator - 可灵多图参考生图 (风格参考)
|
||||||
*
|
*
|
||||||
|
* 使用 /v1/images/multi-image2image 端点,支持 style_image 风格参考
|
||||||
* 配置来源: config.json 的 kelingApiKey + kelingSecretAccessKey + kelingApiBaseUrl
|
* 配置来源: config.json 的 kelingApiKey + kelingSecretAccessKey + kelingApiBaseUrl
|
||||||
*
|
*
|
||||||
* 用法:
|
* 用法:
|
||||||
* node kling-image-generator.js "a cute cat" -o ./output
|
* node kling-image-generator.js "a cute cat" -r http://img.com/style.jpg -o ./output
|
||||||
* node kling-image-generator.js "portrait" -r http://img.com/ref.jpg
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
@@ -75,24 +75,31 @@ async function download(url, outputPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 可灵图片 API
|
// 可灵多图参考生图 API (/v1/images/multi-image2image)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const KlingImageApi = {
|
const KlingImageApi = {
|
||||||
async submit(prompt, options = {}) {
|
async submit(prompt, options = {}) {
|
||||||
const { referenceImageUrl = '', aspectRatio = '' } = options
|
const { styleImageUrl = '', aspectRatio = '' } = options
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
|
|
||||||
const body = { model_name: Config.model, prompt, n: 1 }
|
const body = {
|
||||||
if (referenceImageUrl) body.image = referenceImageUrl
|
model_name: Config.model,
|
||||||
|
prompt,
|
||||||
|
n: 1,
|
||||||
|
}
|
||||||
|
if (styleImageUrl) {
|
||||||
|
body.subject_image_list = [{ subject_image: styleImageUrl }]
|
||||||
|
body.style_image = styleImageUrl
|
||||||
|
}
|
||||||
if (aspectRatio) body.aspect_ratio = aspectRatio
|
if (aspectRatio) body.aspect_ratio = aspectRatio
|
||||||
|
|
||||||
console.log(`\n📡 提交可灵图片任务`)
|
console.log(`\n📡 提交可灵图片任务`)
|
||||||
console.log(` 模型: ${Config.model}`)
|
console.log(` 模型: ${Config.model}`)
|
||||||
console.log(` 提示词: ${prompt.substring(0, 80)}...`)
|
console.log(` 提示词: ${prompt.substring(0, 80)}...`)
|
||||||
if (referenceImageUrl) console.log(` 参考图: ${referenceImageUrl.substring(0, 60)}...`)
|
if (styleImageUrl) console.log(` 风格图: ${styleImageUrl.substring(0, 60)}...`)
|
||||||
|
|
||||||
const res = await fetch(`${Config.apiBase}/v1/images/generations`, {
|
const res = await fetch(`${Config.apiBase}/v1/images/multi-image2image`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
|
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
@@ -116,7 +123,7 @@ const KlingImageApi = {
|
|||||||
|
|
||||||
while (Date.now() - startTime < Config.maxPollTime) {
|
while (Date.now() - startTime < Config.maxPollTime) {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
const res = await fetch(`${Config.apiBase}/v1/images/generations/${taskId}`, {
|
const res = await fetch(`${Config.apiBase}/v1/images/multi-image2image/${taskId}`, {
|
||||||
headers: { 'Authorization': `Bearer ${token}` },
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -150,11 +157,11 @@ const KlingImageApi = {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
async function generate(prompt, options = {}) {
|
async function generate(prompt, options = {}) {
|
||||||
const { outputDir = './output', referenceImageUrl = '' } = options
|
const { outputDir = './output', styleImageUrl = '', aspectRatio = '' } = options
|
||||||
|
|
||||||
fs.mkdirSync(outputDir, { recursive: true })
|
fs.mkdirSync(outputDir, { recursive: true })
|
||||||
|
|
||||||
const taskId = await KlingImageApi.submit(prompt, { referenceImageUrl })
|
const taskId = await KlingImageApi.submit(prompt, { styleImageUrl, aspectRatio })
|
||||||
const result = await KlingImageApi.poll(taskId)
|
const result = await KlingImageApi.poll(taskId)
|
||||||
|
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
||||||
@@ -177,20 +184,22 @@ async function main() {
|
|||||||
|
|
||||||
选项:
|
选项:
|
||||||
-o, --output <dir> 输出目录 (默认: ./output)
|
-o, --output <dir> 输出目录 (默认: ./output)
|
||||||
-r, --ref <url> 参考图 URL
|
-r, --ref <url> 风格参考图 URL
|
||||||
|
-a, --ar <ratio> 画幅比例 (如 9:16, 16:9)
|
||||||
-h, --help 帮助
|
-h, --help 帮助
|
||||||
`)
|
`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = { outputDir: './output', referenceImageUrl: '' }
|
const options = { outputDir: './output', styleImageUrl: '', aspectRatio: '' }
|
||||||
const params = []
|
const params = []
|
||||||
|
|
||||||
let i = 0
|
let i = 0
|
||||||
while (i < args.length) {
|
while (i < args.length) {
|
||||||
const arg = args[i]
|
const arg = args[i]
|
||||||
if (arg === '-o' || arg === '--output') { options.outputDir = args[++i] }
|
if (arg === '-o' || arg === '--output') { options.outputDir = args[++i] }
|
||||||
else if (arg === '-r' || arg === '--ref') { options.referenceImageUrl = args[++i] }
|
else if (arg === '-r' || arg === '--ref') { options.styleImageUrl = args[++i] }
|
||||||
|
else if (arg === '-a' || arg === '--ar') { options.aspectRatio = args[++i] }
|
||||||
else { params.push(arg) }
|
else { params.push(arg) }
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ async function phaseImages(manifest, manifestPath, options) {
|
|||||||
const { generate: klingGen } = require('./kling-image-generator')
|
const { generate: klingGen } = require('./kling-image-generator')
|
||||||
const klingOpts = { outputDir: imagesDir, aspectRatio: ratio }
|
const klingOpts = { outputDir: imagesDir, aspectRatio: ratio }
|
||||||
if (refs.urls.length > 0) {
|
if (refs.urls.length > 0) {
|
||||||
klingOpts.referenceImageUrl = refs.urls[0]
|
klingOpts.styleImageUrl = refs.urls[0]
|
||||||
}
|
}
|
||||||
log('images', `[${idx}/${items.length}] 可灵生图: ${item.imagePrompt.substring(0, 60)}...`)
|
log('images', `[${idx}/${items.length}] 可灵生图: ${item.imagePrompt.substring(0, 60)}...`)
|
||||||
result = await klingGen(item.imagePrompt, klingOpts)
|
result = await klingGen(item.imagePrompt, klingOpts)
|
||||||
@@ -272,7 +272,7 @@ async function phaseImages(manifest, manifestPath, options) {
|
|||||||
const { generate: klingGen } = require('./kling-image-generator')
|
const { generate: klingGen } = require('./kling-image-generator')
|
||||||
lastResult = await klingGen(item.lastFramePrompt, {
|
lastResult = await klingGen(item.lastFramePrompt, {
|
||||||
outputDir: imagesDir,
|
outputDir: imagesDir,
|
||||||
referenceImageUrl: item.url || '',
|
styleImageUrl: item.url || '',
|
||||||
aspectRatio: ratio,
|
aspectRatio: ratio,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user