fmode-mv.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. #!/usr/bin/env node
  2. // bin/fmode-mv.js — fmode-mv CLI 入口
  3. //
  4. // 所有子命令都是薄封装:真正逻辑在 scripts/*.mjs 与 lib/*.mjs。
  5. import { fileURLToPath } from 'node:url';
  6. import { dirname, join } from 'node:path';
  7. const __dirname = dirname(fileURLToPath(import.meta.url));
  8. const ROOT = join(__dirname, '..');
  9. const HELP = `
  10. fmode-mv — 纯 AI 音乐 MV 全流程技能(零依赖 · 纯 ESM)
  11. 用法:
  12. fmode-mv <命令> [参数]
  13. 两条产线:
  14. song 产线A:AI 单曲 MV 端到端编排
  15. 主题 → 作曲填词 → AI音乐 → 音轨分离 → 关键帧 → 图生视频 → 唇形 → 剪辑成片
  16. talk 产线B:AI 口播讲解视频端到端编排
  17. 口播稿 → TTS 配音 → 讲解画面 → 场景 B-roll → 剪辑成片
  18. 分步命令(各自可单独跑,产物落 work/):
  19. lyrics 作曲填词:主题 → 歌词 + 曲风 prompt + 结构标记 [chat]
  20. storyboard 分镜脚本:歌词/口播稿 → 每镜头时间码/画面/prompt [chat]
  21. music AI 音乐生成 [music]
  22. separate 人声/伴奏分离(ffmpeg 兜底 + demucs 可选) [local]
  23. keyframe 场景 & 人物关键帧 [image]
  24. video 图生视频(B-roll 空镜 / 人物演唱镜头) [video]
  25. lipsync 唇形驱动 [lipsync]
  26. tts 口播配音 [tts]
  27. assemble 剪辑合成成片(卡点 + 字幕 + 混音) [local ffmpeg]
  28. 诊断:
  29. providers 打印能力现状表(--deep 做真实探活)
  30. doctor 环境自检:node / ffmpeg / 凭据 / 各通道
  31. version 版本号(--check 校验三处一致)
  32. 通用参数:
  33. --out <dir> 指定工作目录(默认 ./work)
  34. --json 只输出 JSON(便于程序消费)
  35. --help, -h 显示帮助
  36. 示例:
  37. fmode-mv lyrics "离别与重逢" --genre "city pop" --style 温暖
  38. fmode-mv keyframe --from work/storyboard.json --shot 1
  39. fmode-mv song "城市夜晚的孤独" --title "夜行" --limit 6
  40. fmode-mv talk --script work/script.md
  41. fmode-mv providers --deep
  42. `;
  43. const COMMANDS = {
  44. song: { file: 'pipeline-a.mjs', desc: '产线A:AI 单曲 MV 端到端' },
  45. talk: { file: 'pipeline-b.mjs', desc: '产线B:AI 口播讲解视频端到端' },
  46. lyrics: { file: 'lyrics.mjs', desc: '作曲填词' },
  47. storyboard: { file: 'storyboard.mjs', desc: '分镜脚本' },
  48. music: { file: 'music.mjs', desc: 'AI 音乐生成' },
  49. separate: { file: 'separate.mjs', desc: '人声/伴奏分离' },
  50. keyframe: { file: 'keyframe.mjs', desc: '关键帧生成' },
  51. video: { file: 'video.mjs', desc: '图生视频' },
  52. lipsync: { file: 'lipsync.mjs', desc: '唇形驱动' },
  53. tts: { file: 'tts.mjs', desc: '口播配音' },
  54. assemble: { file: 'assemble.mjs', desc: '剪辑合成成片' },
  55. };
  56. async function main() {
  57. const argv = process.argv.slice(2);
  58. const cmd = argv[0];
  59. if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {
  60. console.log(HELP);
  61. return 0;
  62. }
  63. if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
  64. const { VERSION, checkVersionConsistency } = await import(join(ROOT, 'lib', 'index.mjs'));
  65. if (argv.includes('--check')) {
  66. const r = checkVersionConsistency();
  67. console.log(`index.mjs : ${r.version}`);
  68. console.log(`package : ${r.pkgVersion}`);
  69. console.log(`SKILL.md : ${r.skillVersion}`);
  70. if (r.ok) {
  71. console.log('✓ 三处版本一致');
  72. return 0;
  73. }
  74. for (const i of r.issues) console.error(`✗ ${i}`);
  75. return 1;
  76. }
  77. console.log(VERSION);
  78. return 0;
  79. }
  80. if (cmd === 'providers') {
  81. const { probeAll, PROVIDERS, KINDS } = await import(join(ROOT, 'lib', 'providers.mjs'));
  82. const { loadConfig } = await import(join(ROOT, 'lib', 'config.mjs'));
  83. const deep = argv.includes('--deep');
  84. console.log(`\n网关: ${loadConfig().baseUrl}`);
  85. console.log(deep ? '模式: 深度探活(会真实提交)\n' : '模式: 轻量探活(不做真实提交)\n');
  86. const rows = await probeAll({ deep });
  87. const w = Math.max(...rows.map((r) => r.kind.length));
  88. for (const r of rows) {
  89. const mark = r.available === true ? '✅ 可用' : r.available === null ? '◐ 未知' : '⚠️ 未开通';
  90. console.log(`${r.kind.padEnd(w)} ${mark} ${r.label}`);
  91. if (r.model) console.log(`${' '.repeat(w)} 模型: ${r.model}`);
  92. console.log(`${' '.repeat(w)} 现状: ${r.detail}`);
  93. if (r.hint) console.log(`${' '.repeat(w)} 建议: ${String(r.hint).split('\n')[0]}`);
  94. console.log('');
  95. }
  96. const okCount = rows.filter((r) => r.available === true).length;
  97. console.log(`汇总: ${okCount}/${rows.length} 通道可用`);
  98. return 0;
  99. }
  100. if (cmd === 'doctor') {
  101. const { runDoctor } = await import(join(ROOT, 'lib', 'doctor.mjs'));
  102. return runDoctor(argv.slice(1));
  103. }
  104. const spec = COMMANDS[cmd];
  105. if (!spec) {
  106. console.error(`未知命令: ${cmd}\n`);
  107. console.log(HELP);
  108. return 1;
  109. }
  110. const mod = await import(join(ROOT, 'scripts', spec.file));
  111. if (typeof mod.main !== 'function') {
  112. console.error(`内部错误: scripts/${spec.file} 未导出 main()`);
  113. return 1;
  114. }
  115. try {
  116. return (await mod.main(argv.slice(1))) ?? 0;
  117. } catch (e) {
  118. const { reportUnavailable, log } = await import(join(ROOT, 'lib', 'index.mjs'));
  119. if (e?.name === 'ProviderUnavailableError') {
  120. reportUnavailable(spec.desc, e);
  121. return 3;
  122. }
  123. log.err(e?.message || String(e));
  124. if (process.env.DEBUG) console.error(e.stack);
  125. return 1;
  126. }
  127. }
  128. main()
  129. .then((code) => process.exit(Number(code) || 0))
  130. .catch((e) => {
  131. console.error('致命错误:', e?.stack || e?.message || e);
  132. process.exit(1);
  133. });