#!/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
指定工作目录(默认 ./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);
});