Files
video-create/.claude/skills/video-from-script/scripts/lib/pipeline-utils.js
lc 2232be4eee feat: 视频阶段诊断增强 + 新账号 product_viral_factory + 执黑先行提示词更新
- phase-videos.js: 增加 item 不符合条件时的逐项诊断日志,明确 confirmed 校验
- pipeline-utils.js: saveManifest 先直写,EPERM 时回退 tmp+rename
- 执黑先行: 分镜/图片/视频提示词完善
- 新增 product_viral_factory 账号(PPT产品宣传片方向)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 17:07:06 +08:00

242 lines
7.5 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) {
try {
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8')
} catch (err) {
if (err.code === 'EPERM') {
// rename/tmp fallback on EPERM
const tmp = manifestPath + '.tmp'
fs.writeFileSync(tmp, JSON.stringify(manifest, null, 2), 'utf-8')
try { fs.renameSync(tmp, manifestPath) } catch (_) {}
}
}
}
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,
}