// lib/index.mjs — fmode-mv 主入口 // // 版本号三处必须一致:package.json / SKILL.md frontmatter / 本文件 VERSION。 // 用 `fmode-mv version --check` 可自动校验。 export const VERSION = '0.2.0'; export const NAME = 'fmode-mv'; export const SKILL = 'skill-mv'; import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; export const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); // 重新导出各模块,脚本统一从 index.mjs 取用 export * from './config.mjs'; export * from './api.mjs'; export * from './providers.mjs'; export * from './ffmpeg.mjs'; export * from './jimeng.mjs'; export * from './sharpness.mjs'; export * from './prompts.mjs'; // ==================== 工作区约定 ==================== // // work/ —— 本次生产的中间产物(不入库) // song.json 作曲填词结果 // storyboard.json 分镜脚本 // audio/ 音乐、人声、伴奏、配音 // frames/ 关键帧图片 // clips/ 生成的视频片段 // lipsync/ 唇形驱动结果 // subtitles.srt 字幕 // out/ —— 最终成片 export const WORK = process.env.FMODE_MV_WORK ? resolve(process.env.FMODE_MV_WORK) : join(ROOT, 'work'); export const OUT = process.env.FMODE_MV_OUT ? resolve(process.env.FMODE_MV_OUT) : join(ROOT, 'out'); export const paths = { work: WORK, out: OUT, audio: join(WORK, 'audio'), frames: join(WORK, 'frames'), clips: join(WORK, 'clips'), lipsync: join(WORK, 'lipsync'), song: join(WORK, 'song.json'), storyboard: join(WORK, 'storyboard.json'), script: join(WORK, 'script.json'), subtitles: join(WORK, 'subtitles.srt'), }; export function ensureWork() { for (const d of [paths.work, paths.out, paths.audio, paths.frames, paths.clips, paths.lipsync]) mkdirSync(d, { recursive: true }); return paths.work; } // ==================== JSON 读写 ==================== export function writeJSON(file, data) { mkdirSync(dirname(file), { recursive: true }); writeFileSync(file, JSON.stringify(data, null, 2) + '\n', 'utf8'); return file; } export function readJSON(file, fallback = null) { try { if (!existsSync(file)) return fallback; return JSON.parse(readFileSync(file, 'utf8')); } catch (e) { throw new Error(`读取 JSON 失败 ${file}: ${e.message}`); } } // ==================== 日志 ==================== const C = { reset: '\x1b[0m', dim: '\x1b[2m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m', bold: '\x1b[1m', }; const useColor = process.stdout.isTTY && !process.env.NO_COLOR; const paint = (c, s) => (useColor ? `${c}${s}${C.reset}` : String(s)); export const log = { step: (s) => console.log(`\n${paint(C.bold + C.blue, '▶ ' + s)}`), ok: (s) => console.log(` ${paint(C.green, '✓')} ${s}`), warn: (s) => console.log(` ${paint(C.yellow, '!')} ${s}`), err: (s) => console.error(` ${paint(C.red, '✗')} ${s}`), info: (s) => console.log(` ${s}`), dim: (s) => console.log(paint(C.dim, ` ${s}`)), raw: (s) => console.log(s), key: (k, v) => console.log(` ${paint(C.cyan, k.padEnd(14))} ${v}`), }; /** 打印未开通通道的标准提示(统一格式,保证「不静默失败」) */ export function reportUnavailable(kind, err) { log.err(`通道「${kind}」不可用:${String(err.message).split('\n')[0]}`); if (err.hint) { console.log(''); console.log(paint(C.yellow, ' 可操作方案:')); for (const line of String(err.hint).split('\n')) console.log(` ${line}`); } console.log(''); console.log(paint(C.dim, ` 用 \`fmode-mv providers\` 查看全部通道现状。`)); } // ==================== 参数解析 ==================== /** * 极简参数解析:支持 --k v / --k=v / --flag / 位置参数。 * @returns {{_:string[], flags:object}} */ export function parseArgs(argv = process.argv.slice(2)) { const _ = []; const flags = {}; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a.startsWith('--')) { const eq = a.indexOf('='); if (eq > -1) { flags[a.slice(2, eq)] = a.slice(eq + 1); } else { const key = a.slice(2); const next = argv[i + 1]; if (next === undefined || next.startsWith('--')) flags[key] = true; else { flags[key] = next; i++; } } } else if (a.startsWith('-') && a.length > 1) { flags[a.slice(1)] = true; } else { _.push(a); } } return { _, flags }; } /** 三处版本一致性校验 */ export function checkVersionConsistency() { const issues = []; const pkgPath = join(ROOT, 'package.json'); const skillPath = join(ROOT, 'SKILL.md'); let pkgVer = null; let skillVer = null; try { pkgVer = JSON.parse(readFileSync(pkgPath, 'utf8')).version; } catch (e) { issues.push(`package.json 读取失败:${e.message}`); } try { const txt = readFileSync(skillPath, 'utf8'); const m = txt.match(/^---([\s\S]*?)---/); const fm = m ? m[1] : ''; skillVer = (fm.match(/^version:\s*"?([^"\n]+)"?/m) || [])[1]?.trim() || null; } catch (e) { issues.push(`SKILL.md 读取失败:${e.message}`); } const ok = pkgVer === VERSION && skillVer === VERSION; if (pkgVer !== VERSION) issues.push(`package.json=${pkgVer} ≠ index.mjs=${VERSION}`); if (skillVer !== VERSION) issues.push(`SKILL.md=${skillVer} ≠ index.mjs=${VERSION}`); return { ok, version: VERSION, pkgVersion: pkgVer, skillVersion: skillVer, issues }; } export default { VERSION, NAME, SKILL, paths, checkVersionConsistency };