| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172 |
- // 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 };
|