feat(video-from-script): 批量生产元数据增强 — 选题/转发文案/草稿命名/导出/草稿箱改名
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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 函数
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user