feat: MiniMax TTS集成、口播批量剪辑流水线、执黑先行二号账号
- 新增 minimax-tts.js 和 minimax-voice-clone.js 脚本 - 新增口播批量剪辑流水线 (口播_assemble.js, 口播_pipeline.js) - 更新 video-from-script 各阶段脚本 (kling, images, tts, videos) - 新增执黑先行二号-风格延伸账号 - 更新执黑先行 account.json 配置 - 替换 ugc_product_seeding 参考图 - 更新 CLAUDE.md 和依赖配置 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -18,7 +18,23 @@ async function phaseImages(manifest, manifestPath, options) {
|
||||
((!it.status || it.status === 'pending' || it.status === 'generating') && it.imagePrompt) ||
|
||||
(it.status === 'done' && manifest.mode === 'framePair' && it.file && it.lastFramePrompt && !it.lastFrame)
|
||||
)
|
||||
if (items.length === 0) { log('images', '无待处理 item,跳过'); return }
|
||||
if (items.length === 0) {
|
||||
// 检查是否是真实的无待处理项(已有图片)还是配置错误
|
||||
const total = manifest.items.length
|
||||
const withImg = manifest.items.filter(it => it.url || it.file).length
|
||||
const withPrompt = manifest.items.filter(it => it.imagePrompt).length
|
||||
if (withImg === 0) {
|
||||
console.error("\n" + "=".repeat(60))
|
||||
console.error("❌ [images] 严重错误:没有任何待处理的图片生成项,且 manifest 中也没有已生成的图片!")
|
||||
console.error("=".repeat(60))
|
||||
console.error(` 总数: ${total} | 有imagePrompt: ${withPrompt} | 已有图片: ${withImg}`)
|
||||
if (withPrompt === 0) console.error(" 根因: 所有 item 都缺少 imagePrompt — 分镜文件可能有问题")
|
||||
console.error("=".repeat(60) + "\n")
|
||||
throw new Error(`图片阶段中断: ${total} 个 item 均无图片且无待处理项 (imagePrompt=${withPrompt}/${total})`)
|
||||
}
|
||||
log('images', '所有图片已生成,跳过')
|
||||
return
|
||||
}
|
||||
|
||||
const accountConfig = options.accountConfig || {}
|
||||
let model = options.imageModel || manifest.imageModel || accountConfig.imageModel || 'gemini'
|
||||
|
||||
@@ -87,7 +87,8 @@ async function phaseTts(manifest, manifestPath, options = {}) {
|
||||
const audioDir = path.join(dir, 'audio')
|
||||
ensureDir(audioDir)
|
||||
|
||||
const { synthesize } = require('../qwen-tts')
|
||||
const engine = manifest.ttsEngine || 'qwen'
|
||||
const { synthesize } = engine === 'minimax' ? require('../minimax-tts') : require('../qwen-tts')
|
||||
|
||||
const videoDur = manifest.estimatedVideoDuration || 6
|
||||
const ttsRate = manifest.ttsRate || 1.15
|
||||
@@ -95,7 +96,22 @@ async function phaseTts(manifest, manifestPath, options = {}) {
|
||||
const items = manifest.items.filter(it =>
|
||||
(it.script || it.text) && !it.audio
|
||||
)
|
||||
if (items.length === 0) { log('tts', '无待处理 item(已合成),跳过'); return }
|
||||
if (items.length === 0) {
|
||||
const total = manifest.items.length
|
||||
const withAudio = manifest.items.filter(it => it.audio).length
|
||||
const withScript = manifest.items.filter(it => it.script || it.text).length
|
||||
if (withAudio === 0) {
|
||||
console.error("\n" + "=".repeat(60))
|
||||
console.error("❌ [tts] 严重错误:没有任何待处理的配音项,且 manifest 中也没有已生成的音频!")
|
||||
console.error("=".repeat(60))
|
||||
console.error(` 总数: ${total} | 有script: ${withScript} | 已有audio: ${withAudio}`)
|
||||
if (withScript === 0) console.error(" 根因: 所有 item 都缺少 script 文本 — 分镜文件有问题")
|
||||
console.error("=".repeat(60) + "\n")
|
||||
throw new Error(`TTS 阶段中断: ${total} 个 item 均无音频且无待处理项 (script=${withScript}/${total})`)
|
||||
}
|
||||
log('tts', '所有音频已合成,跳过')
|
||||
return
|
||||
}
|
||||
|
||||
log('tts', `共 ${items.length} 段, 视频固定时长=${videoDur}s, TTS语速=${ttsRate}x`)
|
||||
|
||||
@@ -131,6 +147,9 @@ async function phaseTts(manifest, manifestPath, options = {}) {
|
||||
model: manifest.ttsModel || undefined,
|
||||
instruction: manifest.ttsInstruction || undefined,
|
||||
rate: ttsRate,
|
||||
emotion: manifest.ttsEmotion || undefined,
|
||||
languageBoost: manifest.ttsLanguageBoost || undefined,
|
||||
pitch: manifest.ttsPitch ?? undefined,
|
||||
})
|
||||
break
|
||||
} catch (e) {
|
||||
|
||||
@@ -49,23 +49,35 @@ async function phaseVideos(manifest, manifestPath, options) {
|
||||
})
|
||||
|
||||
if (videoCandidates.length === 0) {
|
||||
console.log("\n⚠️ [videos] 没有符合条件的 item 进入视频生成阶段")
|
||||
console.log(" manifest 中共有", manifest.items.length, "个 item,逐一诊断:")
|
||||
console.error("\n" + "=".repeat(60))
|
||||
console.error("❌ [videos] 严重错误:没有任何 item 可以进入视频生成阶段!")
|
||||
console.error("=".repeat(60))
|
||||
console.error(" manifest 中共有", manifest.items.length, "个 item,逐一诊断:")
|
||||
let reasonSummary = {}
|
||||
for (const it of manifest.items) {
|
||||
const reasons = []
|
||||
if (it.confirmed === false) reasons.push("confirmed=false")
|
||||
if (!it.url) reasons.push("缺少 url(图片未上传)")
|
||||
if (!it.videoPrompt) reasons.push("缺少 videoPrompt")
|
||||
if (it.confirmed === false) { reasons.push("confirmed=false(未确认)"); reasonSummary['confirmed=false'] = (reasonSummary['confirmed=false']||0)+1 }
|
||||
if (!it.url) { reasons.push("缺少 url(图片未上传)"); reasonSummary['no_url'] = (reasonSummary['no_url']||0)+1 }
|
||||
if (!it.videoPrompt) { reasons.push("缺少 videoPrompt"); reasonSummary['no_videoPrompt'] = (reasonSummary['no_videoPrompt']||0)+1 }
|
||||
if (it.status === 'done' && (it.video || it.videoUrl)) {
|
||||
reasons.push("视频已生成,已跳过")
|
||||
reasons.push("视频已生成")
|
||||
reasonSummary['already_done'] = (reasonSummary['already_done']||0)+1
|
||||
} else if (!["done","pending","failed"].includes(it.status)) {
|
||||
reasons.push("status=" + (it.status || "undefined") + "(不在 done/pending/failed 中)")
|
||||
reasonSummary['bad_status'] = (reasonSummary['bad_status']||0)+1
|
||||
}
|
||||
console.log(" - item", it.id || manifest.items.indexOf(it), ":", reasons.length > 0 ? reasons.join(", ") : "已满足全部条件(不应在此)")
|
||||
console.error(` - item ${it.id || manifest.items.indexOf(it)}: ${reasons.join(", ")}`)
|
||||
}
|
||||
console.log("\n 修复命令:")
|
||||
console.log(" node .claude/skills/video-from-script/scripts/pipeline.js confirm --manifest", manifestPath, "--all")
|
||||
console.log()
|
||||
console.error("\n 原因汇总:", JSON.stringify(reasonSummary))
|
||||
|
||||
let fixCmd = ""
|
||||
if (reasonSummary['confirmed=false']) fixCmd = `node ${path.join(__dirname, '..', 'pipeline.js')} confirm --manifest ${manifestPath} --all`
|
||||
else if (reasonSummary['no_url']) fixCmd = `node ${path.join(__dirname, '..', 'pipeline.js')} run --manifest ${manifestPath} --phase images,upload`
|
||||
else if (reasonSummary['bad_status']) fixCmd = `手动将 item status 改为 done/pending/failed 后重新运行 videos`
|
||||
|
||||
console.error("\n 修复命令: " + fixCmd)
|
||||
console.error("=".repeat(60) + "\n")
|
||||
throw new Error(`视频阶段中断: ${manifest.items.length} 个 item 均不符合视频生成条件 (${Object.entries(reasonSummary).map(([k,v])=>k+'×'+v).join(', ')})`)
|
||||
}
|
||||
|
||||
const items = []
|
||||
@@ -92,7 +104,10 @@ async function phaseVideos(manifest, manifestPath, options) {
|
||||
}
|
||||
items.push(it)
|
||||
}
|
||||
if (items.length === 0) { log('videos', '无待处理 item,跳过'); return }
|
||||
if (items.length === 0) {
|
||||
console.error("\n❌ [videos] 严重错误:视频候选列表非空但处理后无待处理项(磁盘恢复后全部跳过)")
|
||||
throw new Error('视频阶段中断: 处理后无待处理 item')
|
||||
}
|
||||
|
||||
let Api, pollFn
|
||||
const modelLower = videoModel.toLowerCase()
|
||||
@@ -159,9 +174,11 @@ async function phaseVideos(manifest, manifestPath, options) {
|
||||
if (needSubmit.length === 0) { log('videos', '全部通过恢复完成'); return }
|
||||
|
||||
// Phase 2+3: 分批提交+轮询(严格并发 ≤ 5,等一批完成再提交下一批)
|
||||
const concurrency = 5
|
||||
const concurrency = 1
|
||||
log('videos', `提交 ${needSubmit.length} 个新任务(并发: ${concurrency})...`)
|
||||
|
||||
let totalSubmitted = 0
|
||||
|
||||
for (let i = 0; i < needSubmit.length; i += concurrency) {
|
||||
const batch = needSubmit.slice(i, i + concurrency).filter(item => !item.videoTaskId)
|
||||
if (batch.length === 0) continue
|
||||
@@ -245,6 +262,34 @@ async function phaseVideos(manifest, manifestPath, options) {
|
||||
}
|
||||
saveManifest(manifestPath, manifest)
|
||||
}
|
||||
|
||||
// 最终检查:如果提交了视频任务但成功率极低,硬中断
|
||||
const successCount = manifest.items.filter(it => it.video || it.videoUrl).length
|
||||
const failCount = manifest.items.filter(it => it.status === 'failed').length
|
||||
const totalCandidates = videoCandidates.length
|
||||
|
||||
if (successCount === 0 && totalCandidates > 0) {
|
||||
console.error("\n" + "=".repeat(60))
|
||||
console.error("❌ [videos] 严重错误:所有视频生成任务全部失败!")
|
||||
console.error("=".repeat(60))
|
||||
console.error(` 候选 item: ${totalCandidates} | 成功: ${successCount} | 失败: ${failCount}`)
|
||||
const errSamples = manifest.items.filter(it => it.status === 'failed' && it.error).slice(0, 3)
|
||||
for (const it of errSamples) {
|
||||
console.error(` - item ${it.id}: ${it.error?.slice(0,100)}`)
|
||||
}
|
||||
const allErrors = errSamples.map(it => it.error || '').join()
|
||||
if (/负载已饱和|rate.?limit|quota|额度/.test(allErrors)) {
|
||||
console.error("\n 根因: Kling API 上游过载或额度不足,稍后重试")
|
||||
}
|
||||
console.error("=".repeat(60) + "\n")
|
||||
throw new Error(`视频阶段中断: 所有 ${totalCandidates} 个候选任务失败,成功 0 个`)
|
||||
}
|
||||
|
||||
if (successCount < totalCandidates * 0.3 && totalCandidates >= 5) {
|
||||
console.error("\n⚠️ [videos] 警告:视频生成成功率极低 (<30%)")
|
||||
console.error(` 候选: ${totalCandidates} | 成功: ${successCount} | 失败: ${failCount}`)
|
||||
console.error(" 已生成视频将继续使用,未生成的片段将以静态图片替代\n")
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { phaseVideos }
|
||||
|
||||
@@ -78,9 +78,11 @@ async function getReferences(manifest, accountConfig) {
|
||||
}
|
||||
|
||||
// 优先读 manifest.references(agent 创建时写入)
|
||||
// 兼容字符串格式("path/to/ref.jpg")和对象格式({file, url})
|
||||
const refs = manifest.references || []
|
||||
if (refs.length > 0) {
|
||||
for (const ref of refs) {
|
||||
for (const rawRef of refs) {
|
||||
const ref = typeof rawRef === 'string' ? { file: rawRef } : rawRef
|
||||
if (ref.url) result.urls.push(ref.url)
|
||||
if (ref.file) {
|
||||
const localPath = path.isAbsolute(ref.file) ? ref.file : path.resolve(ref.file)
|
||||
|
||||
Reference in New Issue
Block a user