#!/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); });