feat(video-pipeline): 添加关键字氛围词花字叠加和Oss URL回写功能
- 新增 Q17 关键字氛围词问答项,支持关闭/默认/自定义花字效果 - 在 manifest 和 account.json 中添加 keyword 和 keywordStyle 字段 - 实现关键字氛围词在画面中央的叠加逻辑,支持动画、阴影、文字效果等配置 - 在 assemble 流程中增加 `keywords` 步骤,自动读取账号配置生成花字 - 修复音频上传后未回写 OSS URL 到 manifest 的问题,避免重复上传
This commit is contained in:
@@ -91,6 +91,23 @@ Phase 4: 技术配置(有默认值,可跳过)
|
||||
| 14 | 参考图文件? | 无 | 用户稍后放入 references/ 目录,Agent 上传 OSS 回写 URL |
|
||||
| 15 | TTS 音色? | config.json 全局 ttsVoice | account.json 的 ttsVoice,留空用全局默认 |
|
||||
| 16 | TTS 语气指令? | 无 | account.json 的 ttsInstruction,描述期望的语气风格 |
|
||||
| 17 | 关键字氛围词? | 开启(默认样式) | 画面中央大字叠加,增强冲击力。选项:关闭 / 默认样式 / 描述期望效果 |
|
||||
| | 期望的花字风格?(选填) | 按账号风格推荐 | 根据视觉基调推荐,见下方花字推荐表 |
|
||||
|
||||
**花字效果推荐表**(92 种免费效果,按风格分类):
|
||||
|
||||
| 账号风格 | 推荐花字 | 备选 |
|
||||
|---------|---------|------|
|
||||
| 暗黑/军事/权谋 | 暗黑潮酷黑色花字 | 潮酷 黑色紫光、简约白色黑边花字 |
|
||||
| 潮酷/街头/潮流 | 潮酷 白色橙边 | 超酷发光镂空花字、潮酷橙色 |
|
||||
| 科技/赛博 | 赛博朋克未来科技感立体发光花字 | 蓝色镂空发光花字、炫彩发光花字 |
|
||||
| 简约/商务/知识 | 简约白色黑边花字 | 简约白色花字、简约黑色描边立体花字 |
|
||||
| 清新/生活/治愈 | 小清新 白色 | 清新蓝色发光花字、梦幻粉色发光立体花字 |
|
||||
| 复古/港风 | 复古 红色 | 红色朦胧港风复古花字、书法墨环图案花字 |
|
||||
| 综艺/娱乐 | 综艺 白色黑边 | 立体综艺花字、综艺 橙色 |
|
||||
|
||||
> Agent 可调用 `get_text_effects` API 获取完整列表(92 种免费 + 更多 VIP)。
|
||||
> Q17 选"默认样式"时,Agent 根据账号风格从上表自动匹配。
|
||||
|
||||
**运动偏好 → 视频提示词映射**:
|
||||
|
||||
@@ -132,6 +149,7 @@ Phase 4: 技术配置(有默认值,可跳过)
|
||||
- 视频模型:{Q13}
|
||||
- TTS音色:{Q15}
|
||||
- TTS语气:{Q16}
|
||||
- 关键字氛围词:{Q17 开启/关闭,花字风格}
|
||||
|
||||
确认 "开始" → 创建账号
|
||||
修改 → 调整后重新输出
|
||||
@@ -159,6 +177,7 @@ Phase 4: 技术配置(有默认值,可跳过)
|
||||
2. **生成 account.json**
|
||||
- 从 `_template/account.json` 复制骨架
|
||||
- 填入 id、name、description、模型、画幅等
|
||||
- Q17 选关闭时删除 `keywordStyle` 节;选自定义花字时更新 `textEffect` 字段
|
||||
|
||||
3. **生成分镜.md**
|
||||
- 读取 `_template/prompts/通用分镜.md`
|
||||
|
||||
@@ -62,6 +62,7 @@ node scripts/pipeline.js validate --manifest <path>
|
||||
| `duration` | 计划视频时长(秒),来自分镜阶段 |
|
||||
| `imagePrompt` | 英文画面描述(给 Gemini/MJ),Step 2-A 生成 |
|
||||
| `directorRef` | 导演构图参考(tarantino / kitano / fincher),三层透传 |
|
||||
| `keyword` | 关键字氛围词(2-6 字),assemble 时以花字效果叠加在画面中央。可选 |
|
||||
| `confirmed` | 人工确认状态,默认 `false` |
|
||||
|
||||
### Agent 后续回写(Step 3-A 视频提示词)
|
||||
|
||||
@@ -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