feat(video-pipeline): 增强参考图自动上传与视频生成重试机制

- 在 `init-manifest` 阶段添加输入文件清理日志和 WARNING 提示
- `getReferences` 改为异步并自动将本地参考图上传至 OSS,减少手动操作
- `phase-videos` 支持 `pending`/`failed` 状态 item 的自动重试,自动清理旧视频引用
- 优化 `phase-assemble` 中字幕与配音开关的逻辑,根据实际内容动态判断
This commit is contained in:
2026-05-03 02:03:17 +08:00
parent 6e8d2b8baa
commit 0e3f0f7d0f
10 changed files with 713 additions and 12 deletions

View File

@@ -27,6 +27,8 @@ function initManifest(options) {
process.exit(1)
}
rawItems = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
console.log(` 已读取并清理临时文件: ${path.basename(filePath)}`)
fs.unlinkSync(filePath)
} else if (itemsJson) {
rawItems = JSON.parse(itemsJson)
} else {
@@ -57,7 +59,7 @@ function initManifest(options) {
}
}
// 从 account.json 继承参考图(顶层 references
// 从 account.json 继承参考图(顶层 references,仅使用带签名 OSS URL 的条目
const accountRefs = accountConfig.references || []
const references = accountRefs.map(ref => {
const entry = {}
@@ -65,6 +67,10 @@ function initManifest(options) {
if (ref.url) entry.url = ref.url
return entry
})
const refsWithoutUrl = references.filter(r => !r.url)
if (refsWithoutUrl.length > 0) {
console.log(`${refsWithoutUrl.length} 个参考图缺少 OSS URLimages 阶段会自动上传`)
}
// 构建 items
const items = rawItems.map((raw, i) => {

View File

@@ -23,13 +23,17 @@ async function phaseAssemble(manifest, manifestPath, options) {
}
}
// 检测是否有配音和字幕
const hasAudio = manifest.items.some(it => it.audio)
const hasSubtitles = manifest.items.some(it => it.script && it.script.trim() && it.script !== '[无配音]')
const assembleArgs = {
input: dir,
manifest: manifestPath,
mode,
format: manifest.format || accountConfig.defaultFormat || '9:16',
subtitles: 'true',
voiceover: manifest.items.some(it => it.audio) ? 'true' : 'false',
subtitles: hasSubtitles ? 'true' : 'false',
voiceover: hasAudio ? 'true' : 'false',
animation: capcutConfig.animation || '渐显+放大',
}
@@ -37,7 +41,7 @@ async function phaseAssemble(manifest, manifestPath, options) {
if (capcutConfig.effects) assembleArgs.effects = capcutConfig.effects.join(',')
if (capcutConfig.filter) assembleArgs.filter = capcutConfig.filter
log('assemble', `模式: ${mode}, 字幕: true, 配音: ${assembleArgs.voiceover}, 动画: ${assembleArgs.animation}`)
log('assemble', `模式: ${mode}, 字幕: ${assembleArgs.subtitles}, 配音: ${assembleArgs.voiceover}, 动画: ${assembleArgs.animation}`)
try {
const { assemble } = require('../capcut_assemble')

View File

@@ -29,7 +29,7 @@ async function phaseImages(manifest, manifestPath, options) {
log('images', '首尾帧模式不支持 MJ自动降级为 Gemini')
model = 'gemini'
}
const refs = getReferences(manifest, accountConfig)
const refs = await getReferences(manifest, accountConfig)
log('images', `${items.length} 张, 模型: ${model}, 画幅: ${ratio}, 参考图: ${refs.localPaths.length}本地/${refs.urls.length}URL, 并发: 全并行`)

View File

@@ -17,9 +17,28 @@ async function phaseVideos(manifest, manifestPath, options) {
const accountConfig = options.accountConfig || {}
const videoModel = manifest.videoModel || accountConfig.videoModel || 'veo3-fast-frames'
const items = manifest.items.filter(it =>
it.status === 'done' && it.confirmed !== false && it.url && it.videoPrompt && !it.video
)
// 筛选需要生视频的 item
// done — 正常流程,图片已确认且已上传
// pending / failed — 重试场景agent 只需将 item 设为 pending 即可触发再生
// 前提:有 url图片已上传+ videoPrompt且 confirmed 未被显式拒绝
const videoCandidates = manifest.items.filter(it => {
if (it.confirmed === false) return false
if (!it.url || !it.videoPrompt) return false
if (['done', 'pending', 'failed'].includes(it.status)) return true
return false
})
// 对重试 item 自动清理旧视频引用,无需 agent 手动删除
const items = []
for (const it of videoCandidates) {
if (it.video) {
if (it.status === 'done') continue // 已有视频且完成,跳过
delete it.video // pending/failed 但有旧 video → 清理重来
delete it.videoUrl
delete it.videoDuration
delete it.videoTaskId
}
items.push(it)
}
if (items.length === 0) { log('videos', '无待处理 item跳过'); return }
// 选择生成器

View File

@@ -42,10 +42,24 @@ function loadAccountConfig(accountId) {
// 参考图解析
// ============================================================================
function getReferences(manifest, accountConfig) {
async function getReferences(manifest, accountConfig) {
const result = { localPaths: [], urls: [] }
const accountId = accountConfig.id || manifest.account || ''
// 自动上传本地文件到 OSS 的辅助函数
const ensureUrl = async (localPath, label) => {
try {
const { uploadFile } = require('../oss-upload')
const { url } = await uploadFile(localPath)
result.urls.push(url)
log('images', `参考图已自动上传 OSS: ${label}`)
return url
} catch (err) {
log('images', `参考图上传 OSS 失败: ${label} (${err.message}),仅本地使用`)
return null
}
}
// 优先读 manifest.referencesagent 创建时写入)
const refs = manifest.references || []
if (refs.length > 0) {
@@ -55,6 +69,10 @@ function getReferences(manifest, accountConfig) {
const localPath = path.isAbsolute(ref.file) ? ref.file : path.resolve(ref.file)
if (fs.existsSync(localPath)) {
result.localPaths.push(localPath)
// 有本地文件但没有 OSS URL → 自动上传,杜绝 agent 只用本地路径
if (!ref.url) {
ref.url = await ensureUrl(localPath, path.basename(localPath))
}
} else {
log('images', `参考图不存在: ${ref.file}`)
}
@@ -72,12 +90,15 @@ function getReferences(manifest, accountConfig) {
const localPath = path.join(ACCOUNTS_DIR, accountId, 'references', ref.file)
if (fs.existsSync(localPath)) {
result.localPaths.push(localPath)
if (!ref.url) {
ref.url = await ensureUrl(localPath, ref.file)
}
}
}
}
if (result.localPaths.length > 0 || result.urls.length > 0) return result
// Fallback 2: 扫描 account 的 references 目录
// Fallback 2: 扫描 account 的 references 目录(自动上传 OSS
if (accountId) {
const refDir = path.join(ACCOUNTS_DIR, accountId, 'references')
if (fs.existsSync(refDir)) {
@@ -85,10 +106,12 @@ function getReferences(manifest, accountConfig) {
/\.(png|jpg|jpeg|webp)$/i.test(f)
)
for (const f of files) {
result.localPaths.push(path.join(refDir, f))
const localPath = path.join(refDir, f)
result.localPaths.push(localPath)
await ensureUrl(localPath, f)
}
if (files.length > 0) {
log('images', `从 references 目录兜底扫描到 ${files.length} 个参考图`)
log('images', `从 references 目录兜底扫描到 ${files.length} 个参考图(已自动上传 OSS`)
}
}
}