| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 |
- #!/usr/bin/env node
- // bin/fmode-mv.js — fmode-mv CLI 入口
- //
- // 所有子命令都是薄封装:真正逻辑在 scripts/*.mjs 与 lib/*.mjs。
- import { fileURLToPath } from 'node:url';
- import { dirname, join } from 'node:path';
- const __dirname = dirname(fileURLToPath(import.meta.url));
- const ROOT = join(__dirname, '..');
- const HELP = `
- fmode-mv — 纯 AI 视频全流程技能(零依赖 · 纯 ESM · 即梦真实通道)
- 用法:
- fmode-mv <命令> [参数]
- 两条产线:
- song 产线A:AI 单曲 MV 端到端编排
- 主题 → 作曲填词 → AI音乐 → 音轨分离 → 人物资产 → 场景图 → 关键帧 → 图生视频 → 剪辑成片
- talk 产线B:AI 口播讲解视频端到端编排(VOC-SOP 五阶段)
- 对标 → 仿写 → 分镜 → 人物资产/场景 → 图生视频 → 口播/数字人 → 剪辑成片
- 分步命令(各自可单独跑,产物落 work/):
- lyrics 作曲填词:主题 → 歌词 + 曲风 prompt + 结构标记 [chat]
- storyboard 分镜脚本:歌词/口播稿 → 每镜头时间码/画面/prompt [chat]
- character-sheet ④ 人物资产多视图:定妆照→上半身→全身→多视图(锁定不换脸)[即梦]
- scene ⑤ 场景图(无人)→ 关键帧(有人,多图参考) [即梦]
- video ⑥ 图生视频(首尾帧/首帧,含降级) [即梦]
- keyframe 关键帧单跑(走旧 image 通道,兼容保留) [image]
- music AI 音乐生成 [music ⚠️未开通]
- separate 人声/伴奏分离(ffmpeg 兜底 + demucs 可选) [local]
- lipsync 唇形驱动 [lipsync ⚠️未开通]
- tts 口播配音 [tts ⚠️未开通]
- assemble 剪辑合成成片(卡点 + 字幕 + 混音) [local ffmpeg]
- jimeng 即梦通道直调(文生图/图生图/图生视频/超清/动作模仿/数字人)
- 诊断:
- providers 打印能力现状表(--deep 做真实探活)
- doctor 环境自检:node / ffmpeg / 凭据 / 各通道
- version 版本号(--check 校验三处一致)
- 通用参数:
- --out <dir> 指定工作目录(默认 ./work)
- --json 只输出 JSON(便于程序消费)
- --help, -h 显示帮助
- 示例:
- # 产线A:一句话到成片
- fmode-mv song "深夜末班地铁上的城市白领" --genre "city pop" --shots 8
- # 产线B:口播讲解(VOC-SOP)
- fmode-mv talk "三分钟讲清楚什么是多租户数据隔离" --duration 90
- # 单跑工序
- fmode-mv character-sheet --desc "25岁东亚女性,栗棕长发,白T+波点裙" --limit 1
- fmode-mv scene --from work/storyboard.json --character out/character/4-多视图.png
- fmode-mv video --from work/storyboard.json --limit 1 # 先跑 1 镜 ¥1.41
- fmode-mv jimeng --status # 看通道与凭据
- fmode-mv jimeng text2img --prompt "..." --width 1024 --height 1024
- ⚠️ 全部画面为纯 AI 生成,不引入任何实拍素材。
- 即梦通道需 sessionToken(FMODE_SESSION_TOKEN 或 /opt/data/fmode-identity.json),本仓不存任何凭据。
- `;
- const COMMANDS = {
- song: { file: 'pipeline-a.mjs', desc: '产线A:AI 单曲 MV 端到端' },
- talk: { file: 'pipeline-b.mjs', desc: '产线B:AI 口播讲解视频端到端' },
- lyrics: { file: 'lyrics.mjs', desc: '作曲填词' },
- storyboard: { file: 'storyboard.mjs', desc: '分镜脚本' },
- 'character-sheet': { file: 'character-sheet.mjs', desc: '人物资产多视图(工序④)' },
- 'char-sheet': { file: 'character-sheet.mjs', desc: '人物资产多视图(工序④,别名)' },
- scene: { file: 'scene.mjs', desc: '场景图 + 关键帧(工序⑤)' },
- video: { file: 'video.mjs', desc: '图生视频(工序⑥)' },
- keyframe: { file: 'keyframe.mjs', desc: '关键帧单跑(旧 image 通道)' },
- music: { file: 'music.mjs', desc: 'AI 音乐生成' },
- separate: { file: 'separate.mjs', desc: '人声/伴奏分离' },
- lipsync: { file: 'lipsync.mjs', desc: '唇形驱动' },
- tts: { file: 'tts.mjs', desc: '口播配音' },
- assemble: { file: 'assemble.mjs', desc: '剪辑合成成片' },
- jimeng: { file: 'jimeng.mjs', desc: '即梦通道直调' },
- };
- async function main() {
- const argv = process.argv.slice(2);
- const cmd = argv[0];
- if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {
- console.log(HELP);
- return 0;
- }
- if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
- const { VERSION, checkVersionConsistency } = await import(join(ROOT, 'lib', 'index.mjs'));
- if (argv.includes('--check')) {
- const r = checkVersionConsistency();
- console.log(`index.mjs : ${r.version}`);
- console.log(`package : ${r.pkgVersion}`);
- console.log(`SKILL.md : ${r.skillVersion}`);
- if (r.ok) {
- console.log('✓ 三处版本一致');
- return 0;
- }
- for (const i of r.issues) console.error(`✗ ${i}`);
- return 1;
- }
- console.log(VERSION);
- return 0;
- }
- if (cmd === 'providers') {
- const { probeAll, PROVIDERS, KINDS } = await import(join(ROOT, 'lib', 'providers.mjs'));
- const { loadConfig } = await import(join(ROOT, 'lib', 'config.mjs'));
- const deep = argv.includes('--deep');
- console.log(`\n网关: ${loadConfig().baseUrl}`);
- console.log(deep ? '模式: 深度探活(会真实提交)\n' : '模式: 轻量探活(不做真实提交)\n');
- const rows = await probeAll({ deep });
- const w = Math.max(...rows.map((r) => r.kind.length));
- for (const r of rows) {
- const mark = r.available === true ? '✅ 可用' : r.available === null ? '◐ 未知' : '⚠️ 未开通';
- console.log(`${r.kind.padEnd(w)} ${mark} ${r.label}`);
- if (r.model) console.log(`${' '.repeat(w)} 模型: ${r.model}`);
- console.log(`${' '.repeat(w)} 现状: ${r.detail}`);
- if (r.hint) console.log(`${' '.repeat(w)} 建议: ${String(r.hint).split('\n')[0]}`);
- console.log('');
- }
- const okCount = rows.filter((r) => r.available === true).length;
- console.log(`汇总: ${okCount}/${rows.length} 通道可用`);
- return 0;
- }
- if (cmd === 'doctor') {
- const { runDoctor } = await import(join(ROOT, 'lib', 'doctor.mjs'));
- return runDoctor(argv.slice(1));
- }
- const spec = COMMANDS[cmd];
- if (!spec) {
- console.error(`未知命令: ${cmd}\n`);
- console.log(HELP);
- return 1;
- }
- const mod = await import(join(ROOT, 'scripts', spec.file));
- if (typeof mod.main !== 'function') {
- console.error(`内部错误: scripts/${spec.file} 未导出 main()`);
- return 1;
- }
- try {
- return (await mod.main(argv.slice(1))) ?? 0;
- } catch (e) {
- const { reportUnavailable, log } = await import(join(ROOT, 'lib', 'index.mjs'));
- if (e?.name === 'ProviderUnavailableError') {
- reportUnavailable(spec.desc, e);
- return 3;
- }
- log.err(e?.message || String(e));
- if (process.env.DEBUG) console.error(e.stack);
- return 1;
- }
- }
- main()
- .then((code) => process.exit(Number(code) || 0))
- .catch((e) => {
- console.error('致命错误:', e?.stack || e?.message || e);
- process.exit(1);
- });
|