Files
video-create/.claude/skills/video-from-script/scripts/lib/pipeline-utils.js
sion123 0e3f0f7d0f feat(video-pipeline): 增强参考图自动上传与视频生成重试机制
- 在 `init-manifest` 阶段添加输入文件清理日志和 WARNING 提示
- `getReferences` 改为异步并自动将本地参考图上传至 OSS,减少手动操作
- `phase-videos` 支持 `pending`/`failed` 状态 item 的自动重试,自动清理旧视频引用
- 优化 `phase-assemble` 中字幕与配音开关的逻辑,根据实际内容动态判断
2026-05-03 02:03:17 +08:00

235 lines
7.2 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.
/**
* 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'))
}
// ============================================================================
// 参考图解析
// ============================================================================
async function getReferences(manifest, accountConfig) {
const result = { localPaths: [], urls: [] }
const accountId = accountConfig.id || manifest.account || ''
// 自动上传本地文件到 OSS 的辅助函数
const ensureUrl = async (localPath, label) => {
try {
const { uploadFile } = require('../oss-upload')
const { url } = await uploadFile(localPath)
result.urls.push(url)
log('images', `参考图已自动上传 OSS: ${label}`)
return url
} catch (err) {
log('images', `参考图上传 OSS 失败: ${label} (${err.message}),仅本地使用`)
return null
}
}
// 优先读 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)
// 有本地文件但没有 OSS URL → 自动上传,杜绝 agent 只用本地路径
if (!ref.url) {
ref.url = await ensureUrl(localPath, path.basename(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 的顶层 references 读取
const topRefs = accountConfig.references || []
for (const ref of topRefs) {
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 (!ref.url) {
ref.url = await ensureUrl(localPath, ref.file)
}
}
}
}
if (result.localPaths.length > 0 || result.urls.length > 0) return result
// Fallback 2: 扫描 account 的 references 目录(自动上传 OSS
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) {
const localPath = path.join(refDir, f)
result.localPaths.push(localPath)
await ensureUrl(localPath, f)
}
if (files.length > 0) {
log('images', `从 references 目录兜底扫描到 ${files.length} 个参考图(已自动上传 OSS`)
}
}
}
if (result.localPaths.length === 0 && result.urls.length === 0) {
log('images', '无参考图,将使用纯文生图模式')
}
return result
}
// ============================================================================
// 文件操作
// ============================================================================
function ensureDir(dir) {
fs.mkdirSync(dir, { recursive: true })
}
function slugify(text) {
// 限制中文字符最多5个,其他字符(英文数字)最多10个
let chineseChars = []
let otherChars = []
for (const char of text) {
if (/\p{Script=Han}/u.test(char)) {
// 中文字符
if (chineseChars.length < 5) {
chineseChars.push(char)
}
} else if (/\w/u.test(char)) {
// 英文、数字
if (otherChars.length < 10) {
otherChars.push(char)
}
}
}
const result = chineseChars.concat(otherChars).join('')
return result || 'untitled'
}
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))
}
// ============================================================================
// 文本切分
// ============================================================================
function splitTextIntoSentences(text) {
// 在句号、感叹号、分号、逗号处断句——它们是口播语音的天然呼吸点。
const sentenceEnders = /[。!;,]/
const sentences = []
let current = ''
for (const char of text) {
current += char
if (sentenceEnders.test(char)) {
sentences.push(current.trim().replace(/[。!;,:?、——…]/g, ''))
current = ''
}
}
if (current.trim()) {
sentences.push(current.trim().replace(/[。!;,:?、——…]/g, ''))
}
return sentences
}
// ============================================================================
// Exports
// ============================================================================
module.exports = {
SCRIPTS_DIR,
SKILLS_DIR,
PROJECT_ROOT,
CONFIG_PATH,
ACCOUNTS_DIR,
loadConfig,
loadManifest,
saveManifest,
loadAccountConfig,
getReferences,
ensureDir,
slugify,
renameGeneratedFile,
splitTextIntoSentences,
log,
getManifestDir,
}