feat(video-from-script): 重构工作流为子Agent分步执行并新增提示词模板系统

将视频制作工作流拆分为独立子步骤:分镜 → 图片提示词 → 生图 → 视频提示词 → 生视频 → 成片,每步由子Agent独立执行。引入prompts/目录统一管理提示词模板(分镜.md、图片提示词.md、视频提示词.md),通过account.json的storyboardPrompt/imageStylePrompt/videoStylePrompt字段引用。

变更内容:
- 新增confirmed机制和pipeline.js confirm命令,生图后必须人工确认才能继续
- manifest schema改用shotDesc/narration/duration/directorRef替代旧字段
- 文件命名规则从keyword改为slug(从shotDesc/narration派生)
- 删除旧的storyboard-rules.md和prompt-rules.md
- pipeline.js脚本拆分为lib/目录下的独立模块(cmd-init/cmd-confirm/cmd-validate/phase-*)
- 新增cmd-create-account支持一键创建带prompts目录的账号
- capcut_assemble支持narration字段替代text作为字幕源
- 新增.gitclaude/settings.json权限配置
This commit is contained in:
2026-04-30 21:18:31 +08:00
parent 7f955647fe
commit 86b9b7948d
32 changed files with 2826 additions and 1292 deletions

View File

@@ -132,7 +132,7 @@ function buildTimeline(items, defaultDurationUs) {
// 音频为主轴视频调速适配≤2x 加速,>2x 截断)
let offset = 0
return items.map(item => {
const audioDur = (item.duration != null) ? item.duration * US : 0
const audioDur = (item.audioDuration != null) ? item.audioDuration * US : (item.duration != null) ? item.duration * US : 0
const videoDur = (item.videoDuration != null) ? item.videoDuration * US : 0
// 无 TTS用视频时长或固定时长
if (audioDur <= 0) {
@@ -196,7 +196,7 @@ async function assemble(args) {
// 统一时间线:由 duration 驱动TTS 音频时长)或 fallback 到固定时长
const timeline = buildTimeline(items, defaultDurationUs)
const totalDurationUs = timeline.length > 0 ? timeline[timeline.length - 1].end : 0
const hasTTS = items.some(item => item.audio && item.duration != null)
const hasTTS = items.some(item => item.audio && (item.audioDuration != null || item.duration != null))
console.log(`\nCapCut 成片组装`)
console.log(` 模式: ${mode} 画幅: ${format} (${width}x${height})`)
@@ -280,7 +280,7 @@ async function assemble(args) {
if (manifestFile) {
try {
const m = JSON.parse(fs.readFileSync(manifestFile, 'utf-8'))
const mi = m.items.find(i => i.text === item.text)
const mi = m.items.find(i => i.id === item.id || i.narration === (item.narration || item.text) || i.text === (item.narration || item.text))
if (mi) { mi.videoUrl = url; fs.writeFileSync(manifestFile, JSON.stringify(m, null, 2)) }
} catch (_) {}
}
@@ -316,7 +316,7 @@ async function assemble(args) {
// -- 添加字幕 --
step++; console.log(`[${step}/${totalSteps}] 添加字幕...`)
if (subtitles === 'true' && items.some(i => i.text)) {
if (subtitles === 'true' && items.some(i => i.narration || i.text)) {
await addSubtitles(draftUrl, items, timeline, subtitleStyle)
} else {
console.log(' 跳过')
@@ -667,12 +667,12 @@ async function addSubtitles(draftUrl, items, timeline, style = {}) {
for (let i = 0; i < items.length; i++) {
const item = items[i]
const text = item.text || item.caption || ''
const text = item.narration || item.text || item.caption || ''
if (!text) continue
const tl = timeline[i]
const keyword = item.keyword || ''
const keywordColor = style.highlightColor || item.keywordColor || style.color || '#FFFFFF'
const keyword = ''
const keywordColor = style.highlightColor || style.color || '#FFFFFF'
const cap = {
start: tl.start,