fmode-mv.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  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 视频全流程技能(零依赖 · 纯 ESM · 即梦真实通道)
  11. 用法:
  12. fmode-mv <命令> [参数]
  13. 两条产线:
  14. song 产线A:AI 单曲 MV 端到端编排
  15. 主题 → 作曲填词 → AI音乐 → 音轨分离 → 人物资产 → 场景图 → 关键帧 → 图生视频 → 剪辑成片
  16. talk 产线B:AI 口播讲解视频端到端编排(VOC-SOP 五阶段)
  17. 对标 → 仿写 → 分镜 → 人物资产/场景 → 图生视频 → 口播/数字人 → 剪辑成片
  18. 分步命令(各自可单独跑,产物落 work/):
  19. lyrics 作曲填词:主题 → 歌词 + 曲风 prompt + 结构标记 [chat]
  20. storyboard 分镜脚本:歌词/口播稿 → 每镜头时间码/画面/prompt [chat]
  21. character-sheet ④ 人物资产多视图:定妆照→上半身→全身→多视图(锁定不换脸)[即梦]
  22. scene ⑤ 场景图(无人)→ 关键帧(有人,多图参考) [即梦]
  23. video ⑥ 图生视频(首尾帧/首帧,含降级) [即梦]
  24. keyframe 关键帧单跑(走旧 image 通道,兼容保留) [image]
  25. music AI 音乐生成 [music ⚠️未开通]
  26. separate 人声/伴奏分离(ffmpeg 兜底 + demucs 可选) [local]
  27. lipsync 唇形驱动 [lipsync ⚠️未开通]
  28. tts 口播配音 [tts ⚠️未开通]
  29. assemble 剪辑合成成片(卡点 + 字幕 + 混音) [local ffmpeg]
  30. jimeng 即梦通道直调(文生图/图生图/图生视频/超清/动作模仿/数字人)
  31. 诊断:
  32. providers 打印能力现状表(--deep 做真实探活)
  33. doctor 环境自检:node / ffmpeg / 凭据 / 各通道
  34. version 版本号(--check 校验三处一致)
  35. 通用参数:
  36. --out <dir> 指定工作目录(默认 ./work)
  37. --json 只输出 JSON(便于程序消费)
  38. --help, -h 显示帮助
  39. 示例:
  40. # 产线A:一句话到成片
  41. fmode-mv song "深夜末班地铁上的城市白领" --genre "city pop" --shots 8
  42. # 产线B:口播讲解(VOC-SOP)
  43. fmode-mv talk "三分钟讲清楚什么是多租户数据隔离" --duration 90
  44. # 单跑工序
  45. fmode-mv character-sheet --desc "25岁东亚女性,栗棕长发,白T+波点裙" --limit 1
  46. fmode-mv scene --from work/storyboard.json --character out/character/4-多视图.png
  47. fmode-mv video --from work/storyboard.json --limit 1 # 先跑 1 镜 ¥1.41
  48. fmode-mv jimeng --status # 看通道与凭据
  49. fmode-mv jimeng text2img --prompt "..." --width 1024 --height 1024
  50. ⚠️ 全部画面为纯 AI 生成,不引入任何实拍素材。
  51. 即梦通道需 sessionToken(FMODE_SESSION_TOKEN 或 /opt/data/fmode-identity.json),本仓不存任何凭据。
  52. `;
  53. const COMMANDS = {
  54. song: { file: 'pipeline-a.mjs', desc: '产线A:AI 单曲 MV 端到端' },
  55. talk: { file: 'pipeline-b.mjs', desc: '产线B:AI 口播讲解视频端到端' },
  56. lyrics: { file: 'lyrics.mjs', desc: '作曲填词' },
  57. storyboard: { file: 'storyboard.mjs', desc: '分镜脚本' },
  58. 'character-sheet': { file: 'character-sheet.mjs', desc: '人物资产多视图(工序④)' },
  59. 'char-sheet': { file: 'character-sheet.mjs', desc: '人物资产多视图(工序④,别名)' },
  60. scene: { file: 'scene.mjs', desc: '场景图 + 关键帧(工序⑤)' },
  61. video: { file: 'video.mjs', desc: '图生视频(工序⑥)' },
  62. keyframe: { file: 'keyframe.mjs', desc: '关键帧单跑(旧 image 通道)' },
  63. music: { file: 'music.mjs', desc: 'AI 音乐生成' },
  64. separate: { file: 'separate.mjs', desc: '人声/伴奏分离' },
  65. lipsync: { file: 'lipsync.mjs', desc: '唇形驱动' },
  66. tts: { file: 'tts.mjs', desc: '口播配音' },
  67. assemble: { file: 'assemble.mjs', desc: '剪辑合成成片' },
  68. jimeng: { file: 'jimeng.mjs', desc: '即梦通道直调' },
  69. };
  70. async function main() {
  71. const argv = process.argv.slice(2);
  72. const cmd = argv[0];
  73. if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {
  74. console.log(HELP);
  75. return 0;
  76. }
  77. if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
  78. const { VERSION, checkVersionConsistency } = await import(join(ROOT, 'lib', 'index.mjs'));
  79. if (argv.includes('--check')) {
  80. const r = checkVersionConsistency();
  81. console.log(`index.mjs : ${r.version}`);
  82. console.log(`package : ${r.pkgVersion}`);
  83. console.log(`SKILL.md : ${r.skillVersion}`);
  84. if (r.ok) {
  85. console.log('✓ 三处版本一致');
  86. return 0;
  87. }
  88. for (const i of r.issues) console.error(`✗ ${i}`);
  89. return 1;
  90. }
  91. console.log(VERSION);
  92. return 0;
  93. }
  94. if (cmd === 'providers') {
  95. const { probeAll, PROVIDERS, KINDS } = await import(join(ROOT, 'lib', 'providers.mjs'));
  96. const { loadConfig } = await import(join(ROOT, 'lib', 'config.mjs'));
  97. const deep = argv.includes('--deep');
  98. console.log(`\n网关: ${loadConfig().baseUrl}`);
  99. console.log(deep ? '模式: 深度探活(会真实提交)\n' : '模式: 轻量探活(不做真实提交)\n');
  100. const rows = await probeAll({ deep });
  101. const w = Math.max(...rows.map((r) => r.kind.length));
  102. for (const r of rows) {
  103. const mark = r.available === true ? '✅ 可用' : r.available === null ? '◐ 未知' : '⚠️ 未开通';
  104. console.log(`${r.kind.padEnd(w)} ${mark} ${r.label}`);
  105. if (r.model) console.log(`${' '.repeat(w)} 模型: ${r.model}`);
  106. console.log(`${' '.repeat(w)} 现状: ${r.detail}`);
  107. if (r.hint) console.log(`${' '.repeat(w)} 建议: ${String(r.hint).split('\n')[0]}`);
  108. console.log('');
  109. }
  110. const okCount = rows.filter((r) => r.available === true).length;
  111. console.log(`汇总: ${okCount}/${rows.length} 通道可用`);
  112. return 0;
  113. }
  114. if (cmd === 'doctor') {
  115. const { runDoctor } = await import(join(ROOT, 'lib', 'doctor.mjs'));
  116. return runDoctor(argv.slice(1));
  117. }
  118. const spec = COMMANDS[cmd];
  119. if (!spec) {
  120. console.error(`未知命令: ${cmd}\n`);
  121. console.log(HELP);
  122. return 1;
  123. }
  124. const mod = await import(join(ROOT, 'scripts', spec.file));
  125. if (typeof mod.main !== 'function') {
  126. console.error(`内部错误: scripts/${spec.file} 未导出 main()`);
  127. return 1;
  128. }
  129. try {
  130. return (await mod.main(argv.slice(1))) ?? 0;
  131. } catch (e) {
  132. const { reportUnavailable, log } = await import(join(ROOT, 'lib', 'index.mjs'));
  133. if (e?.name === 'ProviderUnavailableError') {
  134. reportUnavailable(spec.desc, e);
  135. return 3;
  136. }
  137. log.err(e?.message || String(e));
  138. if (process.env.DEBUG) console.error(e.stack);
  139. return 1;
  140. }
  141. }
  142. main()
  143. .then((code) => process.exit(Number(code) || 0))
  144. .catch((e) => {
  145. console.error('致命错误:', e?.stack || e?.message || e);
  146. process.exit(1);
  147. });