feat(video-pipeline): 添加关键字氛围词花字叠加和Oss URL回写功能
- 新增 Q17 关键字氛围词问答项,支持关闭/默认/自定义花字效果 - 在 manifest 和 account.json 中添加 keyword 和 keywordStyle 字段 - 实现关键字氛围词在画面中央的叠加逻辑,支持动画、阴影、文字效果等配置 - 在 assemble 流程中增加 `keywords` 步骤,自动读取账号配置生成花字 - 修复音频上传后未回写 OSS URL 到 manifest 的问题,避免重复上传
This commit is contained in:
@@ -288,7 +288,7 @@ async function assemble(args) {
|
||||
|
||||
const steps = []
|
||||
if (mode === 'images') steps.push('upload')
|
||||
steps.push('draft', 'materials', 'audio_oss', 'voiceover', 'audio', 'subtitles', 'effects', 'filter', 'save', 'sync')
|
||||
steps.push('draft', 'materials', 'audio_oss', 'voiceover', 'audio', 'subtitles', 'keywords', 'effects', 'filter', 'save', 'sync')
|
||||
const totalSteps = steps.length
|
||||
let step = 0
|
||||
|
||||
@@ -380,6 +380,25 @@ async function assemble(args) {
|
||||
try {
|
||||
audioUrls = await batchUploadAudio(inputDir, items)
|
||||
console.log(` 成功: ${Object.keys(audioUrls).length} 段音频\n`)
|
||||
// 回写 OSS URL 到 manifest,避免重复上传
|
||||
if (Object.keys(audioUrls).length > 0 && manifestFile) {
|
||||
let changed = false
|
||||
for (const item of manifest.items) {
|
||||
if (item.audio && audioUrls[item.audio]) {
|
||||
item.audio = audioUrls[item.audio]
|
||||
changed = true
|
||||
}
|
||||
if (item.segments) {
|
||||
for (const seg of item.segments) {
|
||||
if (seg.audio && audioUrls[seg.audio]) {
|
||||
seg.audio = audioUrls[seg.audio]
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (changed) saveManifest(manifestFile, manifest)
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(` OSS 上传失败,将尝试本地路径: ${err.message}\n`)
|
||||
}
|
||||
@@ -415,6 +434,15 @@ async function assemble(args) {
|
||||
console.log(' 跳过')
|
||||
}
|
||||
|
||||
// -- 添加关键字氛围词 --
|
||||
step++; console.log(`[${step}/${totalSteps}] 添加关键字氛围词...`)
|
||||
const keywordStyle = loadKeywordStyle(manifest)
|
||||
if (Object.keys(keywordStyle).length > 0 && items.some(i => i.keyword)) {
|
||||
await addKeywordOverlays(draftUrl, items, timeline, keywordStyle)
|
||||
} else {
|
||||
console.log(' 跳过(无关键字或未配置 keywordStyle)')
|
||||
}
|
||||
|
||||
// -- 添加特效 --
|
||||
step++; console.log(`[${step}/${totalSteps}] 添加特效...`)
|
||||
if (effectsStr) {
|
||||
@@ -796,6 +824,75 @@ function loadSubtitleStyle(manifest) {
|
||||
} catch { return {} }
|
||||
}
|
||||
|
||||
function loadKeywordStyle(manifest) {
|
||||
const account = manifest.account
|
||||
if (!account) return {}
|
||||
const scriptDir = __dirname
|
||||
const accountFile = path.join(scriptDir, '..', '..', '..', 'accounts', account, 'account.json')
|
||||
if (!fs.existsSync(accountFile)) return {}
|
||||
try {
|
||||
const accountData = JSON.parse(fs.readFileSync(accountFile, 'utf-8'))
|
||||
return accountData.capcut?.keywordStyle || {}
|
||||
} catch { return {} }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 添加关键字氛围词叠加(画面中央大字)
|
||||
// ============================================================================
|
||||
|
||||
async function addKeywordOverlays(draftUrl, items, timeline, style = {}) {
|
||||
const keywordItems = items.filter(item => item.keyword)
|
||||
if (keywordItems.length === 0) {
|
||||
console.log(' 无关键字,跳过')
|
||||
return
|
||||
}
|
||||
|
||||
const captions = []
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i]
|
||||
if (!item.keyword) continue
|
||||
const tl = timeline[i]
|
||||
|
||||
const cap = {
|
||||
start: tl.start,
|
||||
end: tl.end,
|
||||
text: item.keyword,
|
||||
}
|
||||
if (style.inAnimation) cap.in_animation = style.inAnimation
|
||||
if (style.outAnimation) cap.out_animation = style.outAnimation
|
||||
if (style.inAnimDuration) cap.in_animation_duration = style.inAnimDuration
|
||||
if (style.outAnimDuration) cap.out_animation_duration = style.outAnimDuration
|
||||
|
||||
captions.push(cap)
|
||||
}
|
||||
|
||||
if (captions.length === 0) return
|
||||
|
||||
await api('add_captions', {
|
||||
draft_url: draftUrl,
|
||||
captions: JSON.stringify(captions),
|
||||
font: style.font || null,
|
||||
font_size: style.fontSize || 60,
|
||||
text_color: style.color || '#FFFFFF',
|
||||
alignment: 1,
|
||||
bold: style.bold || false,
|
||||
has_shadow: style.hasShadow || false,
|
||||
shadow_info: style.shadowAlpha ? {
|
||||
shadow_alpha: style.shadowAlpha,
|
||||
shadow_color: style.shadowColor || '#000000',
|
||||
shadow_diffuse: 15,
|
||||
shadow_distance: 5,
|
||||
shadow_angle: -45,
|
||||
} : undefined,
|
||||
alpha: style.alpha || 1,
|
||||
scale_x: 1, scale_y: 1,
|
||||
transform_x: 0,
|
||||
transform_y: style.transformY || 0,
|
||||
text_effect: style.textEffect || null,
|
||||
})
|
||||
console.log(` 已添加 ${captions.length} 个关键字氛围词 (效果: ${style.textEffect || '无'})`)
|
||||
}
|
||||
|
||||
function loadTransitions(manifest) {
|
||||
const account = manifest.account
|
||||
if (!account) return null
|
||||
|
||||
@@ -79,6 +79,7 @@ function initManifest(options) {
|
||||
}
|
||||
if (raw.directorRef) item.directorRef = raw.directorRef
|
||||
if (raw.videoPrompt) item.videoPrompt = raw.videoPrompt
|
||||
if (raw.keyword) item.keyword = raw.keyword
|
||||
if (resolvedMode === 'framePair') item.lastFramePrompt = raw.lastFramePrompt
|
||||
return item
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user