feat(video-pipeline): 增强参考图自动上传与视频生成重试机制
- 在 `init-manifest` 阶段添加输入文件清理日志和 WARNING 提示 - `getReferences` 改为异步并自动将本地参考图上传至 OSS,减少手动操作 - `phase-videos` 支持 `pending`/`failed` 状态 item 的自动重试,自动清理旧视频引用 - 优化 `phase-assemble` 中字幕与配音开关的逻辑,根据实际内容动态判断
This commit is contained in:
@@ -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 URL,images 阶段会自动上传`)
|
||||
}
|
||||
|
||||
// 构建 items
|
||||
const items = rawItems.map((raw, i) => {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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, 并发: 全并行`)
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
// 选择生成器
|
||||
|
||||
@@ -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.references(agent 创建时写入)
|
||||
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)`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user