From 8787d369d3d832aff43f7ee58f2d28f8022b98ec Mon Sep 17 00:00:00 2001 From: lc Date: Thu, 14 May 2026 23:16:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(video-from-script):=20=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E7=94=9F=E4=BA=A7=E5=85=83=E6=95=B0=E6=8D=AE=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=20=E2=80=94=20=E9=80=89=E9=A2=98/=E8=BD=AC=E5=8F=91=E6=96=87?= =?UTF-8?q?=E6=A1=88/=E8=8D=89=E7=A8=BF=E5=91=BD=E5=90=8D/=E5=AF=BC?= =?UTF-8?q?=E5=87=BA/=E8=8D=89=E7=A8=BF=E7=AE=B1=E6=94=B9=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - batch-pipeline.js: 新增 mark 元数据字段(topicA/B, draftName, forwardCopy, hashtags) - batch-pipeline.js: 新增 export 命令导出 CSV/XLSX 最终表格 - batch-pipeline.js: 新增 rename-drafts 命令批量重命名剪映草稿(Mac 直接 mv 文件夹) - batch-pipeline.js: 完善 displayTitle 向后兼容旧 topic 字段 - lib/phase-tts: 增强 TTS 生成稳定性 - lib/phase-videos: 视频生成优化 - lib/video-poll-utils: 提取轮询重试共享工具 - CLAUDE.md: 补充批量生产选题/转发文案/草稿命名/导出/草稿箱改名文档 - 执黑先行 account.json: 配置更新 Co-Authored-By: Claude Opus 4.7 --- .claude/skills/config.json | 36 +- .../scripts/batch-pipeline.js | 370 +++++++++++++++++- .../scripts/gemini-image-generator.js | 2 +- .../scripts/lib/phase-tts.js | 40 +- .../scripts/lib/phase-videos.js | 4 +- .../scripts/lib/video-poll-utils.js | 25 ++ .../video-from-script/scripts/pnpm-lock.yaml | 88 +++++ CLAUDE.md | 98 ++++- accounts/执黑先行/account.json | 3 +- 9 files changed, 617 insertions(+), 49 deletions(-) diff --git a/.claude/skills/config.json b/.claude/skills/config.json index e76ef90..4c742f6 100644 --- a/.claude/skills/config.json +++ b/.claude/skills/config.json @@ -1,6 +1,6 @@ { "jianyingDraftPath": "/Users/lc/Movies/JianyingPro/User Data/Projects/com.lveditor.draft", - "capcutMateDir": "/Users/lc/capcut-mate", + "capcutMateDir": "C:/Users/45070/capcut-mate", "capcutMateApiBase": "http://capcut.muyetools.cn/openapi/capcut-mate/v1", "imgbbApiKey": "deprecated", "geminiApiBaseUrl": "https://yunwu.ai", @@ -13,9 +13,6 @@ "grokModel": "grok-video-3", "veoApiBaseUrl": "https://yunwu.ai", "veoApiKey": "sk-m5inhwXqrbcBL6NNKOe7kTdhX8M31azvAvDvtSPGS71rRzd8", - "gptImageApiBaseUrl": "https://yunwu.ai", - "gptImageApiKey": "sk-m5inhwXqrbcBL6NNKOe7kTdhX8M31azvAvDvtSPGS71rRzd8", - "gptImageModel": "gpt-image-2", "veoModel": "veo3-fast-frames", "veoEnhancePrompt": true, "veoEnableUpsample": true, @@ -32,10 +29,29 @@ "ttsApiBaseUrl": "https://dashscope.aliyuncs.com/api/v1", "ttsApiKey": "sk-1c503705b0f844a6b4f2386f6c1cc35b", "ttsModel": "cosyvoice-v3.5-plus", - "ttsVoice": "斯内普", + "ttsVoice": "cosyvoice-v3.5-plus-bailian-fa8787c0f70b4ba2a907c35511e6a6f6", "ttsLanguage": "Chinese", - "ttsVoices": { - "斯内普": "cosyvoice-v3.5-plus-bailian-fa8787c0f70b4ba2a907c35511e6a6f6", - "布拉德": "cosyvoice-v3.5-plus-bailian-574be4b7013a4e1f924de08fa8b9bdef" - } -} + "ttsVoices": [ + { + "name": "斯内普", + "id": "cosyvoice-v3.5-plus-bailian-fa8787c0f70b4ba2a907c35511e6a6f6", + "model": "cosyvoice-v3.5-plus", + "instruction": "用沉稳有力的男性声音朗读,语速适中,语气坚定有力,像是一个有经历有力量的人在平静地讲述生活的方向", + "style": "沉稳有力男声" + }, + { + "name": "斯内普v3plus", + "id": "cosyvoice-v3-plus-bailian-155c1d86a5564d4ca981147d79e309b1", + "model": "cosyvoice-v3-plus", + "instruction": "用沉稳有力的男性声音朗读,语速适中,语气坚定有力,像是一个有经历有力量的人在平静地讲述生活的方向", + "style": "沉稳有力男声(v3-plus模型)" + }, + { + "name": "六沉", + "id": "cosyvoice-v3.5-plus-bailian-91eb3b18acc64c96976a63a64bc6c169", + "model": "cosyvoice-v3.5-plus", + "instruction": "音量由正常对话迅速增强至高喊,性格直率,情绪易激动且外露", + "style": "直率激动,由低到高" + } + ] +} \ No newline at end of file diff --git a/.claude/skills/video-from-script/scripts/batch-pipeline.js b/.claude/skills/video-from-script/scripts/batch-pipeline.js index a28ed83..1fe4a42 100644 --- a/.claude/skills/video-from-script/scripts/batch-pipeline.js +++ b/.claude/skills/video-from-script/scripts/batch-pipeline.js @@ -35,6 +35,15 @@ function parseArgs(argv) { else if (argv[i] === '--status' && argv[i + 1]) args.status = argv[++i] else if (argv[i] === '--manifest-path' && argv[i + 1]) args.manifestPath = argv[++i] else if (argv[i] === '--error' && argv[i + 1]) args.error = argv[++i] + else if (argv[i] === '--topic' && argv[i + 1]) args.topic = argv[++i] + else if (argv[i] === '--topic-a' && argv[i + 1]) args.topicA = argv[++i] + else if (argv[i] === '--topic-b' && argv[i + 1]) args.topicB = argv[++i] + else if (argv[i] === '--draft-name' && argv[i + 1]) args.draftName = argv[++i] + else if (argv[i] === '--forward-copy' && argv[i + 1]) args.forwardCopy = argv[++i] + else if (argv[i] === '--hashtags' && argv[i + 1]) args.hashtags = argv[++i] + else if (argv[i] === '--format' && argv[i + 1]) args.format = argv[++i] + else if (argv[i] === '--draft-dir' && argv[i + 1]) args.draftDir = argv[++i] + else if (argv[i] === '--with-script') args.withScript = true else if (!args.command) args.command = argv[i] } return args @@ -88,13 +97,14 @@ function cmdInit(args) { for (let i = 0; i < rows.length; i++) { const row = rows[i] const script = extractField(row, ['脚本', 'script', '文案', '旁白']) - const title = extractField(row, ['选题', '标题', 'title', 'name']) || `视频${i + 1}` + const title = extractField(row, ['选题', '标题', 'title', 'name']) || '' const account = extractField(row, ['账号', 'account']) || defaultAccount const mode = extractField(row, ['模式', 'mode']) || defaultMode const voiceName = extractField(row, ['音色', 'voice']) || defaultVoice + const forwardRaw = extractField(row, ['转发文案带话题', '转发文案', 'forwardCopy', '分享文案']) if (!script || !script.trim()) { - console.warn(` ⚠ 第 ${i + 2} 行(${title})脚本为空,跳过`) + console.warn(` ⚠ 第 ${i + 2} 行(${title || '无标题'})脚本为空,跳过`) continue } @@ -104,12 +114,20 @@ function cmdInit(args) { // 解析音色名称 → ID const resolvedVoice = voiceName ? resolveVoice(voiceName) : '' + // 解析转发文案带话题:分离出 hashtags 和转发文案 + const { forwardCopy, hashtags } = parseForwardField(forwardRaw) + items.push({ row: i + 1, - title, + title: title || '', // 原 Excel 选题(可为空,由 AI 后续填充) account: account || defaultAccount, mode: mode || defaultMode, voice: resolvedVoice, + forwardCopy: forwardCopy || '', + hashtags: hashtags || '', + topicA: '', // 方案A: 双句封面 ≤12字 + topicB: '', // 方案B: ≤4字极致精简 + draftName: '', // 草稿名称: 账号_月日_序号_方案B scriptFile: `scripts/row_${String(i + 1).padStart(3, '0')}.txt`, status: 'pending', manifestPath: null, @@ -167,38 +185,40 @@ function cmdStatus(args) { list.push(item) } + const displayTitle = (it) => it.topicA || it.topic || it.title || '' + if (grouped.completed.length > 0) { console.log(` ✅ 完成 (${grouped.completed.length}):`) for (const it of grouped.completed) { - console.log(` #${it.row} ${it.title} → ${it.manifestPath || ''}`) + console.log(` #${it.row} ${displayTitle(it)} → ${it.manifestPath || ''}`) } } if (grouped.failed.length > 0) { console.log(` ❌ 失败 (${grouped.failed.length}):`) for (const it of grouped.failed) { - console.log(` #${it.row} ${it.title} — ${it.error || '未知错误'}`) + console.log(` #${it.row} ${displayTitle(it)} — ${it.error || '未知错误'}`) } } if (grouped.processing.length > 0) { console.log(` 🔄 进行中 (${grouped.processing.length}):`) for (const it of grouped.processing) { - console.log(` #${it.row} ${it.title}`) + console.log(` #${it.row} ${displayTitle(it)}`) } } if (grouped.pending.length > 0) { console.log(` ⏳ 待处理 (${grouped.pending.length}):`) for (const it of grouped.pending) { - console.log(` #${it.row} ${it.title} (账号: ${it.account || '未指定'}, 模式: ${it.mode}, 音色: ${it.voice || '账号默认'})`) + console.log(` #${it.row} ${displayTitle(it)} (账号: ${it.account || '未指定'}, 模式: ${it.mode}, 音色: ${it.voice || '账号默认'})`) } } // 输出下一个待处理的行号(方便 AI agent 消费) const next = batch.items.find(it => it.status === 'pending') if (next) { - console.log(`\n ▶ 下一条: #${next.row} (账号: ${next.account}, 模式: ${next.mode}, 音色: ${next.voice || '账号默认'})`) + console.log(`\n ▶ 下一条: #${next.row} ${displayTitle(next)} (账号: ${next.account}, 模式: ${next.mode}, 音色: ${next.voice || '账号默认'})`) console.log(` 脚本文件: ${path.resolve(batchDir, next.scriptFile)}`) } @@ -231,9 +251,18 @@ function cmdMark(args) { if (args.error) item.error = args.error if (args.status !== 'failed') item.error = null + // 可选元数据更新 + if (args.topic) item.topicA = args.topic // 向后兼容 --topic + if (args.topicA) item.topicA = args.topicA + if (args.topicB) item.topicB = args.topicB + if (args.draftName) item.draftName = args.draftName + if (args.forwardCopy) item.forwardCopy = args.forwardCopy + if (args.hashtags) item.hashtags = args.hashtags + batch.stats = calcStats(batch.items) writeJson(manifestPath, batch) - console.log(`#${item.row} ${item.title}: ${oldStatus} → ${args.status}`) + const label = item.topicA || item.title || '' + console.log(`#${item.row} ${label}: ${oldStatus} → ${args.status}`) } // ============================================================================ @@ -273,15 +302,283 @@ function cmdNext(args) { return } - console.log(JSON.stringify({ + const result = { done: false, row: item.row, - title: item.title, + title: item.title || '', account: item.account, mode: item.mode, voice: item.voice || '', + forwardCopy: item.forwardCopy || '', + hashtags: item.hashtags || '', + topicA: item.topicA || '', + topicB: item.topicB || '', + draftName: item.draftName || '', scriptFile: path.resolve(batchDir, item.scriptFile), - })) + } + + // --with-script:附带脚本内容,方便 AI 直接基于脚本生成选题/转发文案 + if (args.withScript) { + try { + result.script = fs.readFileSync(path.resolve(batchDir, item.scriptFile), 'utf-8') + } catch { + result.script = '' + } + } + + console.log(JSON.stringify(result)) +} + +// ============================================================================ +// export: 输出最终表格(含草稿名称列) +// ============================================================================ + +function cmdExport(args) { + const manifestPath = path.resolve(args.file) + const batch = readJson(manifestPath) + const batchDir = path.dirname(manifestPath) + + // 构建导出行 + const rows = [] + for (const item of batch.items) { + // 读取脚本文件 + let script = '' + try { + script = fs.readFileSync(path.resolve(batchDir, item.scriptFile), 'utf-8').trim() + } catch {} + + // 重组转发文案带话题(避免 hashtags 重复) + const htags = (item.hashtags || '').trim() + let forwardBody = (item.forwardCopy || '').trim() + // 如果 forwardCopy 已包含 hashtags,则剥离避免重复 + if (htags && forwardBody.endsWith(htags)) { + forwardBody = forwardBody.slice(0, -htags.length).trim() + } + const forwardFull = [forwardBody, htags].filter(Boolean).join('') + + // 选题列:topicA(方案A)> 旧字段 topic > 原 title + const topicDisplay = item.topicA || item.topic || item.title || '' + + rows.push({ + row: item.row, + 选题: topicDisplay, + 脚本: script, + 账号: item.account, + 模式: item.mode, + 音色: item.voice || '', + 转发文案带话题: forwardFull, + 草稿名称: item.draftName || '', + }) + } + + // 按 row 排序 + rows.sort((a, b) => a.row - b.row) + + const format = args.format || 'csv' + const dateStr = formatDate(new Date()) + const baseName = path.basename(manifestPath, '.json') + + if (format === 'xlsx') { + exportXlsx(manifestPath, rows) + } else { + exportCsv(manifestPath, rows) + } +} + +function exportCsv(manifestPath, rows) { + const outPath = manifestPath.replace('.json', '_export.csv') + const headers = ['选题', '脚本', '账号', '模式', '音色', '转发文案带话题', '草稿名称'] + + const lines = [headers.join(',')] + for (const r of rows) { + const vals = headers.map(h => { + const v = String(r[h] || '') + // CSV 转义:含逗号、引号、换行的字段用引号包裹 + if (v.includes(',') || v.includes('"') || v.includes('\n')) { + return `"${v.replace(/"/g, '""')}"` + } + return v + }) + lines.push(vals.join(',')) + } + + fs.writeFileSync(outPath, lines.join('\n'), 'utf-8') + console.log(`表格已导出: ${outPath}`) + console.log(` 共 ${rows.length} 条记录`) + + // 同时打印到控制台 + console.log() + printTable(rows, headers) +} + +function exportXlsx(manifestPath, rows) { + try { + const XLSX = require('xlsx') + const headers = ['选题', '脚本', '账号', '模式', '音色', '转发文案带话题', '草稿名称'] + const data = rows.map(r => headers.map(h => r[h] || '')) + data.unshift(headers) + + const ws = XLSX.utils.aoa_to_sheet(data) + const wb = XLSX.utils.book_new() + XLSX.utils.book_append_sheet(wb, ws, '视频清单') + + const outPath = manifestPath.replace('.json', '_export.xlsx') + XLSX.writeFile(wb, outPath) + console.log(`表格已导出: ${outPath}`) + console.log(` 共 ${rows.length} 条记录`) + + console.log() + printTable(rows, headers) + } catch (err) { + if (err.code === 'MODULE_NOT_FOUND') { + console.warn('xlsx 模块未安装,改用 CSV 格式') + exportCsv(manifestPath, rows) + } else { + throw err + } + } +} + +function printTable(rows, headers) { + // 计算每列最大宽度 + const widths = headers.map(h => { + const maxData = rows.reduce((m, r) => Math.max(m, String(r[h] || '').length), 0) + return Math.min(Math.max(maxData, h.length), 40) // 单列最长 40 字符 + }) + + // 分隔线 + const sep = '|-' + widths.map(w => '-'.repeat(w)).join('-|-') + '-|' + + // 表头 + const headerLine = '| ' + headers.map((h, i) => pad(h, widths[i])).join(' | ') + ' |' + + console.log(sep) + console.log(headerLine) + console.log(sep) + for (const r of rows) { + const line = '| ' + headers.map((h, i) => pad(String(r[h] || ''), widths[i])).join(' | ') + ' |' + console.log(line) + } + console.log(sep) +} + +function pad(s, width) { + // 中文字符占 2 个显示宽度 + let displayLen = 0 + for (const ch of s) { + displayLen += /[一-鿿＀-￯]/.test(ch) ? 2 : 1 + } + const padding = Math.max(0, width - displayLen) + return s + ' '.repeat(padding) +} + +// ============================================================================ +// rename-drafts: 批量重命名剪映草稿箱文件夹 +// Mac 版剪映草稿显示名 = 文件夹名,直接 mv 即可 +// ============================================================================ + +function cmdRenameDrafts(args) { + const manifestPath = path.resolve(args.file) + const batch = readJson(manifestPath) + const batchDir = path.dirname(manifestPath) + + // 草稿目录:默认 Mac 版 JianyingPro 路径 + const homeDir = require('os').homedir() + const draftDir = args.draftDir || path.join(homeDir, 'Movies', 'JianyingPro', 'User Data', 'Projects', 'com.lveditor.draft') + + if (!fs.existsSync(draftDir)) { + console.error(`草稿目录不存在: ${draftDir}`) + process.exit(1) + } + + // 构建脚本指纹 → draftName 映射 + const itemLookup = [] + for (const it of batch.items) { + if (!it.draftName) continue + const sp = path.resolve(batchDir, it.scriptFile) + let s = '' + try { s = fs.readFileSync(sp, 'utf-8').trim().replace(/\s+/g, '') } catch {} + itemLookup.push({ row: it.row, draftName: it.draftName, sig: s.slice(0, 30) }) + } + + // 扫描草稿目录 + const allDrafts = fs.readdirSync(draftDir) + .filter(d => fs.statSync(path.join(draftDir, d)).isDirectory()) + + // 匹配 + const matches = [] + for (const folderId of allDrafts) { + const cp = path.join(draftDir, folderId, 'draft_content.json') + if (!fs.existsSync(cp)) continue + let content + try { + const raw = fs.readFileSync(cp, 'utf-8') + if (!raw.startsWith('{')) continue + content = JSON.parse(raw) + } catch { continue } + + const texts = (content.materials?.texts || []) + .filter(t => t.type === 'subtitle') + .map(t => { try { const c = JSON.parse(t.content); return c.text || '' } catch { return '' } }) + const fullText = texts.join('').replace(/\s+/g, '') + if (fullText.length < 50) continue + + for (const item of itemLookup) { + if (fullText.includes(item.sig)) { + matches.push({ folderId, item, dateStr: folderId.slice(0, 14) }) + break + } + } + } + + // 按 row 分组,按日期排序(同 row 多个草稿用 _v2 区分) + const rowGroups = {} + for (const m of matches) { + if (!rowGroups[m.item.row]) rowGroups[m.item.row] = [] + rowGroups[m.item.row].push(m) + } + + // 收集所有目标名称,用于跳过已改名的 + const targetNames = new Set() + for (const [row, group] of Object.entries(rowGroups)) { + for (let i = 0; i < group.length; i++) { + targetNames.add(i === 0 ? group[i].item.draftName : group[i].item.draftName + '_v' + (i + 1)) + } + } + + let renamed = 0 + for (const [row, group] of Object.entries(rowGroups)) { + group.sort((a, b) => a.dateStr.localeCompare(b.dateStr)) + for (let i = 0; i < group.length; i++) { + const m = group[i] + let newName = m.item.draftName + if (i > 0) newName = m.item.draftName + '_v' + (i + 1) + + // 跳过已是目标名称或已被其他行匹配占用的 + if (m.folderId === newName || (targetNames.has(m.folderId) && m.folderId !== newName)) { + continue + } + + const oldPath = path.join(draftDir, m.folderId) + const newPath = path.join(draftDir, newName) + if (oldPath === newPath) continue + + try { + fs.renameSync(oldPath, newPath) + console.log('#' + row + ' ' + m.folderId + ' → ' + newName) + renamed++ + } catch (e) { + console.log('#' + row + ' ' + m.folderId + ' FAILED: ' + e.message) + } + } + } + + const unmatched = itemLookup.filter(it => !matches.some(m => m.item.row === it.row)) + console.log('\n改名: ' + renamed + ' 匹配: ' + matches.length + ' 未匹配: ' + unmatched.length) + if (unmatched.length > 0) { + console.log('未匹配:') + for (const u of unmatched) console.log(' #' + u.row + ' ' + u.draftName) + } } // ============================================================================ @@ -385,6 +682,30 @@ function parseCsv(filePath) { return rows } +/** + * 解析「转发文案带话题」字段 + * 输入: "孩子只要长大,就会开始清算父母。#反派人格#执黑先行" + * 输出: { forwardCopy: "孩子只要长大,就会开始清算父母。", hashtags: "#反派人格#执黑先行" } + */ +function parseForwardField(raw) { + if (!raw || !raw.trim()) return { forwardCopy: '', hashtags: '' } + + // 提取所有 #xxx 格式的话题 + const hashtagRe = /#[^\s#]+/g + const hashtagMatches = raw.match(hashtagRe) || [] + const hashtags = hashtagMatches.join('') + + // 移除所有话题后剩余的是转发文案 + let forwardCopy = raw + for (const tag of hashtagMatches) { + forwardCopy = forwardCopy.replace(tag, '') + } + // 清理多余空白和标点周围的空格 + forwardCopy = forwardCopy.replace(/\s+/g, ' ').trim() + + return { forwardCopy, hashtags } +} + function parseCsvLine(line) { const result = [] let current = '' @@ -420,7 +741,7 @@ function main() { cmdStatus(args) } else if (command === 'mark') { if (!args.file || !args.row || !args.status) { - console.error('用法: batch-pipeline.js mark --file --row --status [--manifest-path ] [--error ]') + console.error('用法: batch-pipeline.js mark --file --row --status [--manifest-path ] [--error ] [--topic-a <方案A>] [--topic-b <方案B>] [--draft-name <草稿名称>] [--forward-copy <转发文案>] [--hashtags <话题>]') process.exit(1) } cmdMark(args) @@ -436,6 +757,18 @@ function main() { process.exit(1) } cmdNext(args) + } else if (command === 'export') { + if (!args.file) { + console.error('用法: batch-pipeline.js export --file [--format csv|xlsx]') + process.exit(1) + } + cmdExport(args) + } else if (command === 'rename-drafts') { + if (!args.file) { + console.error('用法: batch-pipeline.js rename-drafts --file [--draft-dir <草稿箱路径>]') + process.exit(1) + } + cmdRenameDrafts(args) } else { console.log('批量视频生产编排器') console.log('') @@ -443,16 +776,19 @@ function main() { console.log(' batch-pipeline.js init --file [--account <账号>] [--mode ] [--voice <音色>]') console.log(' batch-pipeline.js status --file ') console.log(' batch-pipeline.js next --file ') - console.log(' batch-pipeline.js mark --file <...> --row --status [--manifest-path ] [--error ]') + console.log(' batch-pipeline.js mark --file <...> --row --status [--manifest-path ] [--error ] [--topic-a <方案A>] [--topic-b <方案B>] [--draft-name <草稿名称>] [--forward-copy <转发文案>] [--hashtags <话题>]') console.log(' batch-pipeline.js retry-failed --file ') + console.log(' batch-pipeline.js export --file [--format csv|xlsx]') + console.log(' batch-pipeline.js rename-drafts --file [--draft-dir <路径>]') console.log('') console.log('Excel 格式:') - console.log(' 选题 | 脚本 | 账号 | 模式') - console.log(' 选题/标题/title — 标题(可选)') + console.log(' 选题 | 脚本 | 账号 | 模式 | 音色 | 转发文案带话题') + console.log(' 选题/标题/title — 标题(可选,留空则由 AI 根据脚本自动生成)') console.log(' 脚本/文案/旁白 — 口播文案(必填)') console.log(' 账号/account — 账号ID(可选,可由 --account 指定默认值)') console.log(' 模式/mode — single|framePair(可选,可由 --mode 指定默认值)') console.log(' 音色/voice — 音色名称或ID(可选,可由 --voice 指定默认值)') + console.log(' 转发文案带话题/转发文案/forwardCopy — 转发文案+#话题(可选,留空则由 AI 生成)') } } @@ -460,4 +796,4 @@ if (require.main === module) { main() } -module.exports = { cmdInit, cmdStatus, cmdMark, cmdRetryFailed, cmdNext } +module.exports = { cmdInit, cmdStatus, cmdMark, cmdRetryFailed, cmdNext, cmdExport, cmdRenameDrafts } diff --git a/.claude/skills/video-from-script/scripts/gemini-image-generator.js b/.claude/skills/video-from-script/scripts/gemini-image-generator.js index dfe2ac0..6be4ad3 100644 --- a/.claude/skills/video-from-script/scripts/gemini-image-generator.js +++ b/.claude/skills/video-from-script/scripts/gemini-image-generator.js @@ -64,7 +64,7 @@ const Config = { // 超时设置(毫秒) timeout: { - default: 180000, // 默认2分钟 + default: 300000, // 默认5分钟 max: 300000 // 最大5分钟 } } diff --git a/.claude/skills/video-from-script/scripts/lib/phase-tts.js b/.claude/skills/video-from-script/scripts/lib/phase-tts.js index 8ddf1f5..808bb34 100644 --- a/.claude/skills/video-from-script/scripts/lib/phase-tts.js +++ b/.claude/skills/video-from-script/scripts/lib/phase-tts.js @@ -119,14 +119,32 @@ async function phaseTts(manifest, manifestPath, options = {}) { const segInput = rawSegments[j] const segId = `${item.id}_${j + 1}` - try { - const { filePath, duration: realDuration } = await synthesize(segInput.text, { - outputDir: audioDir, - id: segId, - voice: manifest.ttsVoice || undefined, - instruction: manifest.ttsInstruction || undefined, - rate: ttsRate, - }) + // 带重试的合成(最多3次,指数退避) + let synthResult = null + let lastErr = null + for (let retry = 0; retry < 3; retry++) { + try { + synthResult = await synthesize(segInput.text, { + outputDir: audioDir, + id: segId, + voice: manifest.ttsVoice || undefined, + model: manifest.ttsModel || undefined, + instruction: manifest.ttsInstruction || undefined, + rate: ttsRate, + }) + break + } catch (e) { + lastErr = e + if (retry < 2) { + const delay = Math.pow(2, retry) * 3000 + log('tts', `[${idx}/${items.length}] 段${j + 1} 重试 ${retry + 1}/3, ${delay / 1000}s 后重试...`) + await new Promise(r => setTimeout(r, delay)) + } + } + } + + if (synthResult) { + const { filePath, duration: realDuration } = synthResult const segment = { id: segId, @@ -140,8 +158,8 @@ async function phaseTts(manifest, manifestPath, options = {}) { globalOffset += realDuration log('tts', `[${idx}/${items.length}] 段${j + 1}: 估算${segInput.estimatedDuration.toFixed(2)}s → 实测${realDuration.toFixed(2)}s | ${segInput.text.slice(0, 15)}...`) - } catch (err) { - log('tts', `[${idx}/${items.length}] 段${j + 1} 合成失败: ${err.message}`) + } else { + log('tts', `[${idx}/${items.length}] 段${j + 1} 合成失败(重试3次后): ${lastErr?.message || '未知错误'}`) segments.push({ id: segId, text: segInput.text, @@ -149,7 +167,7 @@ async function phaseTts(manifest, manifestPath, options = {}) { estimatedDuration: segInput.estimatedDuration, duration: 0, startOffset: globalOffset, - error: err.message, + error: lastErr?.message || '未知错误', }) globalOffset += segInput.estimatedDuration } diff --git a/.claude/skills/video-from-script/scripts/lib/phase-videos.js b/.claude/skills/video-from-script/scripts/lib/phase-videos.js index c1ec7f8..06ba39a 100644 --- a/.claude/skills/video-from-script/scripts/lib/phase-videos.js +++ b/.claude/skills/video-from-script/scripts/lib/phase-videos.js @@ -118,8 +118,8 @@ async function phaseVideos(manifest, manifestPath, options) { if (needSubmit.length === 0) { log('videos', '全部通过恢复完成'); return } - // Phase 2: 提交新任务(并发 3) - const concurrency = 3 + // Phase 2: 提交新任务(并发 5,Kling 最大并发) + const concurrency = 5 log('videos', `提交 ${needSubmit.length} 个新任务(并发: ${concurrency})...`) const submitted = [] diff --git a/.claude/skills/video-from-script/scripts/lib/video-poll-utils.js b/.claude/skills/video-from-script/scripts/lib/video-poll-utils.js index 5a1f8a4..b2815a1 100644 --- a/.claude/skills/video-from-script/scripts/lib/video-poll-utils.js +++ b/.claude/skills/video-from-script/scripts/lib/video-poll-utils.js @@ -5,6 +5,11 @@ * 两层重试:轮询级(同一 taskId,处理网络瞬断)→ 任务级(创建新 task + 优化提示词) */ +const path = require('path') +const fs = require('fs') +const https = require('https') +const http = require('http') + const TRANSIENT_RE = /timeout|ECONNRESET|ETIMEDOUT|network|socket/i const POLL_RETRIES = 2 // 同一 task 轮询重试次数 @@ -15,6 +20,26 @@ function isTransientError(err) { return TRANSIENT_RE.test(err.message || '') } +async function download(url, outputPath) { + const protocol = url.startsWith('https') ? https : http + return new Promise((resolve, reject) => { + const file = fs.createWriteStream(outputPath) + protocol.get(url, (response) => { + if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) { + file.close() + fs.unlinkSync(outputPath) + return download(response.headers.location, outputPath).then(resolve).catch(reject) + } + response.pipe(file) + file.on('finish', () => { file.close(); resolve(outputPath) }) + }).on('error', (err) => { + file.close() + if (fs.existsSync(outputPath)) fs.unlinkSync(outputPath) + reject(err) + }) + }) +} + /** * 创建 pollWithRetry 函数 * diff --git a/.claude/skills/video-from-script/scripts/pnpm-lock.yaml b/.claude/skills/video-from-script/scripts/pnpm-lock.yaml index 560ec94..5d5a025 100644 --- a/.claude/skills/video-from-script/scripts/pnpm-lock.yaml +++ b/.claude/skills/video-from-script/scripts/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: sharp: specifier: ^0.34.5 version: 0.34.5 + xlsx: + specifier: ^0.18.5 + version: 0.18.5 packages: @@ -53,89 +56,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -164,6 +183,10 @@ packages: resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} engines: {node: '>= 10.0.0'} + adler-32@1.3.1: + resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} + engines: {node: '>=0.8'} + agentkeepalive@3.5.3: resolution: {integrity: sha512-yqXL+k5rr8+ZRpOAntkaaRgWgE5o8ESAj5DyRmVTCSoZxXmqemb9Dd7T4i5UzwuERdLAJUy6XzR9zFVuf0kzkw==} engines: {node: '>= 4.0.0'} @@ -195,6 +218,14 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + cfb@1.2.2: + resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} + engines: {node: '>=0.8'} + + codepage@1.15.0: + resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} + engines: {node: '>=0.8'} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -209,6 +240,11 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + dateformat@2.2.0: resolution: {integrity: sha512-GODcnWq3YGoTnygPfi02ygEiRxqUxpJwuRHjdhJYuxpcZmDq4rjBiXYmbCCzStxo176ixfLT6i4NPwQooRySnw==} @@ -294,6 +330,10 @@ packages: formstream@1.5.2: resolution: {integrity: sha512-NASf0lgxC1AyKNXQIrXTEYkiX99LhCEXTkiGObXAkpBui86a4u8FjH1o2bGb3PpqI3kafC+yw4zWeK6l6VHTgg==} + frac@1.1.2: + resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} + engines: {node: '>=0.8'} + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -482,6 +522,10 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + ssf@0.11.2: + resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} + engines: {node: '>=0.8'} + statuses@1.5.0: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} @@ -536,9 +580,22 @@ packages: resolution: {integrity: sha512-iCRnKVvGxOQdsKhcQId2PXV1vV3J/sDPXKA4Oe9+Eti2nb2ESEsYHRYls/UjoUW3bIc5ZDO8dTH50A/5iVN+bw==} engines: {node: '>=0.10.0'} + wmf@1.0.2: + resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} + engines: {node: '>=0.8'} + + word@0.3.0: + resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} + engines: {node: '>=0.8'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + xlsx@0.18.5: + resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} + engines: {node: '>=0.8'} + hasBin: true + xml2js@0.6.2: resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} engines: {node: '>=4.0.0'} @@ -656,6 +713,8 @@ snapshots: address@1.2.2: {} + adler-32@1.3.1: {} + agentkeepalive@3.5.3: dependencies: humanize-ms: 1.2.1 @@ -717,6 +776,13 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + cfb@1.2.2: + dependencies: + adler-32: 1.3.1 + crc-32: 1.2.2 + + codepage@1.15.0: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -727,6 +793,8 @@ snapshots: core-util-is@1.0.3: {} + crc-32@1.2.2: {} + dateformat@2.2.0: {} debug@4.4.3: @@ -797,6 +865,8 @@ snapshots: node-hex: 1.0.1 pause-stream: 0.0.11 + frac@1.1.2: {} + function-bind@1.1.2: {} get-intrinsic@1.3.0: @@ -1008,6 +1078,10 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + ssf@0.11.2: + dependencies: + frac: 1.1.2 + statuses@1.5.0: {} stream-http@2.8.2: @@ -1072,8 +1146,22 @@ snapshots: dependencies: semver: 5.7.2 + wmf@1.0.2: {} + + word@0.3.0: {} + wrappy@1.0.2: {} + xlsx@0.18.5: + dependencies: + adler-32: 1.3.1 + cfb: 1.2.2 + codepage: 1.15.0 + crc-32: 1.2.2 + ssf: 0.11.2 + wmf: 1.0.2 + word: 0.3.0 + xml2js@0.6.2: dependencies: sax: 1.6.0 diff --git a/CLAUDE.md b/CLAUDE.md index 557d707..4759f18 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,7 +39,49 @@ 用户给一个 Excel/CSV,每行一条视频,Agent 逐条 spawn Worker 子 Agent 执行完整 pipeline。 -**Excel 格式:** `选题 | 脚本 | 账号 | 模式 | 音色`(账号/模式/音色可选,可由 CLI 参数指定默认值) +**Excel 格式:** `选题 | 脚本 | 账号 | 模式 | 音色 | 转发文案带话题`(账号/模式/音色/转发文案可选,可由 CLI 参数指定默认值) + +## 选题 & 转发文案自动生成 + +如果 Excel 中**选题**或**转发文案带话题**列为空,Orchestrator 必须在生成视频前自动填充: + +1. `batch-pipeline.js next --file --with-script` 获取脚本内容 +2. 根据脚本内容生成: + + **选题(封面标题)— 两种方案:** + - **方案A**(封面双句):≤12字,上下句中间用「·」分隔,每半句 5-7 字。语不惊人死不休,首要目标是点击欲望。示例:「闭嘴做事·才是狠人」「你的烦恼·就是缺钱」 + - **方案B**(封面短标题):≤4字,极致精简,制造悬念缺口。用于草稿命名。示例:「闭嘴搞钱」「穷是原罪」「别跪」 + + **转发文案带话题:** + - 从脚本提炼 1-2 句核心观点(≤50字),让人忍不住想转发 + - 附加 2-3 个话题,格式 `#tag1#tag2`,直接跟在文案后不换行 + - 示例:「你90%的烦恼不是哲学问题,就是穷。别矫情了,爬起来搞钱。#搞钱逻辑 #人性真相」 + + **草稿名称:** + - 格式:`账号名_月日_序号_方案B名称` + - 月日为视频完成日期(MMDD),序号为 Excel 行号(两位数补零) + - 示例:`执黑先行_0513_01_别跪` + +3. 通过 `mark` 命令回写元数据,再 spawn Worker 生产视频: + +```bash +node .claude/skills/video-from-script/scripts/batch-pipeline.js mark \ + --file --row --status processing \ + --topic-a "闭嘴做事·才是狠人" --topic-b "闭嘴搞钱" \ + --draft-name "执黑先行_0513_01_闭嘴搞钱" \ + --forward-copy "嘴巴是用来吃饭的,不是用来树敌的" \ + --hashtags "#反派人格#强者思维" +``` + +**话题库参考(根据脚本内容选用,也可自创):** + +| 领域 | 常用话题 | +|------|---------| +| 人性/心理 | `#人性解码` `#反派人格` `#认知升级` `#暗黑心理学` `#人性真相` | +| 权谋/博弈 | `#执黑先行` `#权力游戏` `#博弈论` `#厚黑学` `#权谋智慧` | +| 成长/逆袭 | `#强者思维` `#底层逆袭` `#认知觉醒` `#阶层跨越` | +| 情感/关系 | `#亲密关系` `#情感操控` `#两性博弈` `#关系本质` | +| 商业/赚钱 | `#商业思维` `#搞钱逻辑` `#财富密码` `#赚钱思维` | **CLI 命令:** @@ -50,24 +92,66 @@ node .claude/skills/video-from-script/scripts/batch-pipeline.js init --file --status [--manifest-path ] [--error ] +# 4. 标记状态(含元数据回写) +node .claude/skills/video-from-script/scripts/batch-pipeline.js mark \ + --file ... --row --status \ + [--manifest-path ] [--error ] \ + [--topic-a <方案A>] [--topic-b <方案B>] [--draft-name <草稿名称>] \ + [--forward-copy <转发文案>] [--hashtags <话题>] # 5. 重跑失败项 node .claude/skills/video-from-script/scripts/batch-pipeline.js retry-failed --file output/batch_XXX/batch-manifest.json + +# 6. 导出最终表格(批次完成后) +node .claude/skills/video-from-script/scripts/batch-pipeline.js export --file output/batch_XXX/batch-manifest.json [--format csv|xlsx] + +# 7. 批量重命名剪映草稿箱(Mac 版:文件夹名 = 显示名,直接 mv) +node .claude/skills/video-from-script/scripts/batch-pipeline.js rename-drafts --file output/batch_XXX/batch-manifest.json ``` **执行策略:Orchestrator-Worker** -- **Orchestrator(主 Agent)**:读 batch-manifest 元数据,逐条 spawn Worker 子 Agent,收集结果 -- **Worker(子 Agent)**:独立上下文,处理单条视频的完整流程(分镜 → 生图 → 生视频 → TTS → 成片)。Worker 调用 `pipeline.js init` 时通过 `--tts-voice` 传入音色 +- **Orchestrator(主 Agent)**:读 batch-manifest 元数据,逐条处理每条视频 +- **每条视频分两步**: + 1. **预处理**:`next --with-script` 获取脚本 → 生成选题/转发文案/话题 → `mark --status processing` 回写元数据 + 2. **生产**:spawn Worker 子 Agent,处理完整视频流程(分镜 → 生图 → 生视频 → TTS → 成片) +- **Worker(子 Agent)**:独立上下文,处理单条视频的完整流程。Worker 调用 `pipeline.js init` 时通过 `--tts-voice` 传入音色 - Orchestrator 上下文只存 batch-manifest 元数据,不读脚本正文 - 脚本正文通过文件路径传给 Worker,Worker 自行 Read - 批量模式下人工确认环节自动跳过(`confirm --all`) +## 批量完成后 — 导出表格 + +批次全部完成后,执行导出输出最终表格: + +```bash +node .claude/skills/video-from-script/scripts/batch-pipeline.js export --file output/batch_XXX/batch-manifest.json +``` + +输出 CSV 表格,列:`选题 | 脚本 | 账号 | 模式 | 音色 | 转发文案带话题 | 草稿名称` + +- **选题**列 = 方案A(封面双句) +- **草稿名称**列 = `账号名_月日_序号_方案B` +- CSV 文件路径:`batch-manifest_export.csv`(与 manifest 同目录) +- 导出后询问用户是否打包草稿到桌面 + +## 草稿箱改名 + +Mac 版剪映(JianyingPro)草稿箱显示名称 = `com.lveditor.draft/` 下的**文件夹名**。 + +**直接 `mv` 重命名文件夹即可**,不要改内部 JSON 文件(`draft_content.json`、`draft_meta_info.json` 等)。 + +草稿路径:`~/Movies/JianyingPro/User Data/Projects/com.lveditor.draft/` + +```bash +# 改名示例 +mv "~/Movies/JianyingPro/User Data/Projects/com.lveditor.draft/20260514123331fc3c6352" \ + "~/Movies/JianyingPro/User Data/Projects/com.lveditor.draft/执黑先行_0514_14_造局者" +``` + **草稿 = CapCut 剪映项目文件**,由 pipeline 的 `assemble` 阶段生成,输出到本地剪映目录。 # 参考文档 diff --git a/accounts/执黑先行/account.json b/accounts/执黑先行/account.json index fa763cd..3178d44 100644 --- a/accounts/执黑先行/account.json +++ b/accounts/执黑先行/account.json @@ -13,7 +13,8 @@ } }, "ttsVoice": "斯内普", - "ttsInstruction": "中低音,胸腔共鸣。带一点气声,像在你耳边说话,不是在台上演讲。咬字松弛,不播音,像个真实的人。越重要的话说得越轻、越慢。冷静但不冷漠,是看透之后的平静。偶尔一句冷幽默,面无表情说出来。让人觉得他什么都见过,但还是愿意坐下来跟你聊。很有磁性,性张力的男性声音,像个黑帮老大。但是你特别想跟他做朋友,很亲切。", + "ttsRate": 1.3, + "ttsInstruction": "用沉稳有力的男性声音朗读,语速适中,语气坚定有力,像是一个有经历有力量的人在平静地讲述生活的方向", "storyboardPrompt": "prompts/分镜.md", "imageStylePrompt": "prompts/图片提示词.md", "videoStylePrompt": "prompts/视频提示词.md",