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

@@ -0,0 +1,170 @@
/**
* Pipeline 共享工具函数与路径常量
*
* 所有 phase/command 模块共用:配置加载、文件操作、路径计算、日志
*/
const fs = require('fs')
const path = require('path')
// 路径常量(基于 lib/ 的父目录 scripts/
const SCRIPTS_DIR = path.join(__dirname, '..')
const SKILLS_DIR = path.join(SCRIPTS_DIR, '..')
const PROJECT_ROOT = path.join(SKILLS_DIR, '..', '..')
const CONFIG_PATH = path.join(SKILLS_DIR, 'config.json')
const ACCOUNTS_DIR = path.join(PROJECT_ROOT, 'accounts')
// ============================================================================
// 配置 & Manifest
// ============================================================================
function loadConfig() {
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'))
}
function loadManifest(manifestPath) {
return JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
}
function saveManifest(manifestPath, manifest) {
const tmp = manifestPath + '.tmp'
fs.writeFileSync(tmp, JSON.stringify(manifest, null, 2), 'utf-8')
fs.renameSync(tmp, manifestPath)
}
function loadAccountConfig(accountId) {
const accountPath = path.join(ACCOUNTS_DIR, accountId, 'account.json')
if (!fs.existsSync(accountPath)) throw new Error(`账号不存在: ${accountPath}`)
return JSON.parse(fs.readFileSync(accountPath, 'utf-8'))
}
// ============================================================================
// 参考图解析
// ============================================================================
function getReferences(manifest, accountConfig) {
const result = { localPaths: [], urls: [] }
const accountId = accountConfig.id || manifest.account || ''
// 优先读 manifest.referencesagent 创建时写入)
const refs = manifest.references || []
if (refs.length > 0) {
for (const ref of refs) {
if (ref.url) result.urls.push(ref.url)
if (ref.file) {
const localPath = path.isAbsolute(ref.file) ? ref.file : path.resolve(ref.file)
if (fs.existsSync(localPath)) {
result.localPaths.push(localPath)
} else {
log('images', `参考图不存在: ${ref.file}`)
}
}
}
if (result.localPaths.length > 0 || result.urls.length > 0) return result
log('images', 'manifest.references 全部无效,尝试 account fallback')
}
// Fallback 1: 从 account.json 的 styles.*.references 读取
const styles = accountConfig.styles || {}
for (const [, style] of Object.entries(styles)) {
for (const ref of (style.references || [])) {
if (ref.url) result.urls.push(ref.url)
if (ref.file && accountId) {
const localPath = path.join(ACCOUNTS_DIR, accountId, 'references', ref.file)
if (fs.existsSync(localPath)) {
result.localPaths.push(localPath)
}
}
}
}
if (result.localPaths.length > 0 || result.urls.length > 0) return result
// Fallback 2: 扫描 account 的 references 目录
if (accountId) {
const refDir = path.join(ACCOUNTS_DIR, accountId, 'references')
if (fs.existsSync(refDir)) {
const files = fs.readdirSync(refDir).filter(f =>
/\.(png|jpg|jpeg|webp)$/i.test(f)
)
for (const f of files) {
result.localPaths.push(path.join(refDir, f))
}
if (files.length > 0) {
log('images', `从 references 目录兜底扫描到 ${files.length} 个参考图`)
}
}
}
if (result.localPaths.length === 0 && result.urls.length === 0) {
log('images', '无参考图,将使用纯文生图模式')
}
return result
}
// ============================================================================
// 文件操作
// ============================================================================
function ensureDir(dir) {
fs.mkdirSync(dir, { recursive: true })
}
function slugify(text) {
return text
.replace(/[^\w一-鿿]/g, '_')
.replace(/_+/g, '_')
.replace(/^_|_$/g, '')
.substring(0, 20)
}
function renameGeneratedFile(oldRelPath, dir, seq, nameHint, suffix) {
if (!oldRelPath) return oldRelPath
const oldAbs = path.resolve(dir, oldRelPath)
if (!fs.existsSync(oldAbs)) return oldRelPath
const ext = path.extname(oldAbs)
const slug = nameHint ? slugify(nameHint) : ''
const tag = suffix ? `_${suffix}` : ''
const newName = slug
? `scene_${String(seq).padStart(2, '0')}_${slug}${tag}${ext}`
: `scene_${String(seq).padStart(2, '0')}${tag}${ext}`
const newAbs = path.join(path.dirname(oldAbs), newName)
if (oldAbs !== newAbs) {
try { fs.renameSync(oldAbs, newAbs) } catch (_) { return oldRelPath }
}
return path.relative(dir, newAbs).replace(/\\/g, '/')
}
// ============================================================================
// 日志 & 路径
// ============================================================================
function log(phase, msg) {
console.log(`[${phase}] ${msg}`)
}
function getManifestDir(manifestPath) {
return path.dirname(path.resolve(manifestPath))
}
// ============================================================================
// Exports
// ============================================================================
module.exports = {
SCRIPTS_DIR,
SKILLS_DIR,
PROJECT_ROOT,
CONFIG_PATH,
ACCOUNTS_DIR,
loadConfig,
loadManifest,
saveManifest,
loadAccountConfig,
getReferences,
ensureDir,
slugify,
renameGeneratedFile,
log,
getManifestDir,
}