Files
video-create/.claude/skills/video-from-script/scripts/lib/cmd-init.js
sion123 86b9b7948d 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权限配置
2026-04-30 21:18:31 +08:00

139 lines
4.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Command: init — 从 account.json + AI 创意内容生成规范 manifest.json
*
* 校验 items → 继承账号配置 → 创建输出目录 → 写出 manifest
*/
const fs = require('fs')
const path = require('path')
const { loadAccountConfig, saveManifest, ensureDir, ACCOUNTS_DIR, SKILLS_DIR } = require('./pipeline-utils')
function initManifest(options) {
const { account: accountId, mode, items: itemsJson, itemsFile } = options
if (!accountId) {
console.error('错误: 必须指定 --account <账号ID>')
process.exit(1)
}
const accountConfig = loadAccountConfig(accountId)
// 解析 items
let rawItems
if (itemsFile) {
const filePath = path.resolve(itemsFile)
if (!fs.existsSync(filePath)) {
console.error(`错误: items 文件不存在: ${filePath}`)
process.exit(1)
}
rawItems = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
} else if (itemsJson) {
rawItems = JSON.parse(itemsJson)
} else {
console.error('错误: 必须指定 --items <JSON> 或 --items-file <path>')
process.exit(1)
}
if (!Array.isArray(rawItems) || rawItems.length === 0) {
console.error('错误: items 必须是非空数组')
process.exit(1)
}
// 校验必填字段
const requiredFields = ['shotDesc', 'narration', 'imagePrompt']
const resolvedMode = mode || 'single'
for (let i = 0; i < rawItems.length; i++) {
const item = rawItems[i]
for (const f of requiredFields) {
if (!item[f]) {
console.error(`错误: items[${i}] 缺少必填字段 "${f}"`)
process.exit(1)
}
}
if (resolvedMode === 'framePair' && !item.lastFramePrompt) {
console.error(`错误: 首尾帧模式 items[${i}] 缺少 "lastFramePrompt"imagePrompt 作为第一帧)`)
process.exit(1)
}
}
// 从 account.json 继承参考图
const styles = accountConfig.styles || {}
const firstStyleKey = Object.keys(styles)[0]
const styleRefs = firstStyleKey ? (styles[firstStyleKey].references || []) : []
const references = styleRefs.map(ref => {
const entry = {}
if (ref.file) entry.file = path.join(ACCOUNTS_DIR, accountId, 'references', ref.file)
if (ref.url) entry.url = ref.url
return entry
})
// 构建 items
const items = rawItems.map((raw, i) => {
const item = {
id: i + 1,
status: 'pending',
shotDesc: raw.shotDesc || '',
narration: raw.narration || raw.text || '',
duration: raw.duration || 5,
imagePrompt: raw.imagePrompt,
confirmed: false,
}
if (raw.directorRef) item.directorRef = raw.directorRef
if (raw.videoPrompt) item.videoPrompt = raw.videoPrompt
if (resolvedMode === 'framePair') item.lastFramePrompt = raw.lastFramePrompt
return item
})
// 组装 manifest
const manifest = {
account: accountId,
imageModel: accountConfig.imageModel || 'gemini',
videoModel: accountConfig.videoModel || 'veo3-fast-frames',
format: accountConfig.defaultFormat || '9:16',
mode: resolvedMode,
references,
items,
}
// 创建输出目录(自增序号)
const date = new Date()
const dateStr = [
date.getFullYear(),
String(date.getMonth() + 1).padStart(2, '0'),
String(date.getDate()).padStart(2, '0'),
].join('')
const prefix = `${accountConfig.name}_${dateStr}`
const outputBase = path.join(SKILLS_DIR, '..', '..', '..', 'output')
ensureDir(outputBase)
let seq = 1
while (fs.existsSync(path.join(outputBase, `${prefix}_${String(seq).padStart(3, '0')}`))) {
seq++
}
const dirName = `${prefix}_${String(seq).padStart(3, '0')}`
const outputDir = path.join(outputBase, dirName)
ensureDir(outputDir)
ensureDir(path.join(outputDir, 'images'))
ensureDir(path.join(outputDir, 'videos'))
ensureDir(path.join(outputDir, 'audio'))
const manifestPath = path.join(outputDir, 'manifest.json')
saveManifest(manifestPath, manifest)
console.log(`\nManifest 已创建: ${manifestPath}`)
console.log(` 账号: ${accountId} (${accountConfig.name})`)
console.log(` 模型: ${manifest.imageModel} + ${manifest.videoModel}`)
console.log(` 画幅: ${manifest.format}, 模式: ${manifest.mode}`)
console.log(` Items: ${items.length}`)
console.log(` 参考图: ${references.length}`)
if (items.some(it => !it.videoPrompt)) {
console.log(`${items.filter(it => !it.videoPrompt).length} 个 item 缺少 videoPrompt生视频阶段将跳过`)
}
console.log()
return manifestPath
}
module.exports = { initManifest }