index.mjs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. // lib/index.mjs — fmode-mv 主入口
  2. //
  3. // 版本号三处必须一致:package.json / SKILL.md frontmatter / 本文件 VERSION。
  4. // 用 `fmode-mv version --check` 可自动校验。
  5. export const VERSION = '0.1.0';
  6. export const NAME = 'fmode-mv';
  7. export const SKILL = 'skill-mv';
  8. import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
  9. import { dirname, join, resolve } from 'node:path';
  10. import { fileURLToPath } from 'node:url';
  11. export const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
  12. // 重新导出各模块,脚本统一从 index.mjs 取用
  13. export * from './config.mjs';
  14. export * from './api.mjs';
  15. export * from './providers.mjs';
  16. export * from './ffmpeg.mjs';
  17. // ==================== 工作区约定 ====================
  18. //
  19. // work/ —— 本次生产的中间产物(不入库)
  20. // song.json 作曲填词结果
  21. // storyboard.json 分镜脚本
  22. // audio/ 音乐、人声、伴奏、配音
  23. // frames/ 关键帧图片
  24. // clips/ 生成的视频片段
  25. // lipsync/ 唇形驱动结果
  26. // subtitles.srt 字幕
  27. // out/ —— 最终成片
  28. export const WORK = process.env.FMODE_MV_WORK ? resolve(process.env.FMODE_MV_WORK) : join(ROOT, 'work');
  29. export const OUT = process.env.FMODE_MV_OUT ? resolve(process.env.FMODE_MV_OUT) : join(ROOT, 'out');
  30. export const paths = {
  31. work: WORK,
  32. out: OUT,
  33. audio: join(WORK, 'audio'),
  34. frames: join(WORK, 'frames'),
  35. clips: join(WORK, 'clips'),
  36. lipsync: join(WORK, 'lipsync'),
  37. song: join(WORK, 'song.json'),
  38. storyboard: join(WORK, 'storyboard.json'),
  39. script: join(WORK, 'script.json'),
  40. subtitles: join(WORK, 'subtitles.srt'),
  41. };
  42. export function ensureWork() {
  43. for (const d of [paths.work, paths.out, paths.audio, paths.frames, paths.clips, paths.lipsync]) mkdirSync(d, { recursive: true });
  44. return paths.work;
  45. }
  46. // ==================== JSON 读写 ====================
  47. export function writeJSON(file, data) {
  48. mkdirSync(dirname(file), { recursive: true });
  49. writeFileSync(file, JSON.stringify(data, null, 2) + '\n', 'utf8');
  50. return file;
  51. }
  52. export function readJSON(file, fallback = null) {
  53. try {
  54. if (!existsSync(file)) return fallback;
  55. return JSON.parse(readFileSync(file, 'utf8'));
  56. } catch (e) {
  57. throw new Error(`读取 JSON 失败 ${file}: ${e.message}`);
  58. }
  59. }
  60. // ==================== 日志 ====================
  61. const C = {
  62. reset: '\x1b[0m',
  63. dim: '\x1b[2m',
  64. red: '\x1b[31m',
  65. green: '\x1b[32m',
  66. yellow: '\x1b[33m',
  67. blue: '\x1b[34m',
  68. cyan: '\x1b[36m',
  69. bold: '\x1b[1m',
  70. };
  71. const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
  72. const paint = (c, s) => (useColor ? `${c}${s}${C.reset}` : String(s));
  73. export const log = {
  74. step: (s) => console.log(`\n${paint(C.bold + C.blue, '▶ ' + s)}`),
  75. ok: (s) => console.log(` ${paint(C.green, '✓')} ${s}`),
  76. warn: (s) => console.log(` ${paint(C.yellow, '!')} ${s}`),
  77. err: (s) => console.error(` ${paint(C.red, '✗')} ${s}`),
  78. info: (s) => console.log(` ${s}`),
  79. dim: (s) => console.log(paint(C.dim, ` ${s}`)),
  80. raw: (s) => console.log(s),
  81. key: (k, v) => console.log(` ${paint(C.cyan, k.padEnd(14))} ${v}`),
  82. };
  83. /** 打印未开通通道的标准提示(统一格式,保证「不静默失败」) */
  84. export function reportUnavailable(kind, err) {
  85. log.err(`通道「${kind}」不可用:${String(err.message).split('\n')[0]}`);
  86. if (err.hint) {
  87. console.log('');
  88. console.log(paint(C.yellow, ' 可操作方案:'));
  89. for (const line of String(err.hint).split('\n')) console.log(` ${line}`);
  90. }
  91. console.log('');
  92. console.log(paint(C.dim, ` 用 \`fmode-mv providers\` 查看全部通道现状。`));
  93. }
  94. // ==================== 参数解析 ====================
  95. /**
  96. * 极简参数解析:支持 --k v / --k=v / --flag / 位置参数。
  97. * @returns {{_:string[], flags:object}}
  98. */
  99. export function parseArgs(argv = process.argv.slice(2)) {
  100. const _ = [];
  101. const flags = {};
  102. for (let i = 0; i < argv.length; i++) {
  103. const a = argv[i];
  104. if (a.startsWith('--')) {
  105. const eq = a.indexOf('=');
  106. if (eq > -1) {
  107. flags[a.slice(2, eq)] = a.slice(eq + 1);
  108. } else {
  109. const key = a.slice(2);
  110. const next = argv[i + 1];
  111. if (next === undefined || next.startsWith('--')) flags[key] = true;
  112. else {
  113. flags[key] = next;
  114. i++;
  115. }
  116. }
  117. } else if (a.startsWith('-') && a.length > 1) {
  118. flags[a.slice(1)] = true;
  119. } else {
  120. _.push(a);
  121. }
  122. }
  123. return { _, flags };
  124. }
  125. /** 三处版本一致性校验 */
  126. export function checkVersionConsistency() {
  127. const issues = [];
  128. const pkgPath = join(ROOT, 'package.json');
  129. const skillPath = join(ROOT, 'SKILL.md');
  130. let pkgVer = null;
  131. let skillVer = null;
  132. try {
  133. pkgVer = JSON.parse(readFileSync(pkgPath, 'utf8')).version;
  134. } catch (e) {
  135. issues.push(`package.json 读取失败:${e.message}`);
  136. }
  137. try {
  138. const txt = readFileSync(skillPath, 'utf8');
  139. const m = txt.match(/^---([\s\S]*?)---/);
  140. const fm = m ? m[1] : '';
  141. skillVer = (fm.match(/^version:\s*"?([^"\n]+)"?/m) || [])[1]?.trim() || null;
  142. } catch (e) {
  143. issues.push(`SKILL.md 读取失败:${e.message}`);
  144. }
  145. const ok = pkgVer === VERSION && skillVer === VERSION;
  146. if (pkgVer !== VERSION) issues.push(`package.json=${pkgVer} ≠ index.mjs=${VERSION}`);
  147. if (skillVer !== VERSION) issues.push(`SKILL.md=${skillVer} ≠ index.mjs=${VERSION}`);
  148. return { ok, version: VERSION, pkgVersion: pkgVer, skillVersion: skillVer, issues };
  149. }
  150. export default { VERSION, NAME, SKILL, paths, checkVersionConsistency };