| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292 |
- // scripts/jimeng.mjs — 即梦通道直调工具
- //
- // 用途:把即梦的能力暴露到命令行,既可单独验证通道,也可做 KB 里那些脚本能做的事
- // (超清、局部重绘、动作模仿、数字人…),而不必再复制 KB 的硬编码脚本。
- //
- // 所有调用都会:调用前打印预估费用 → 调用后打印实扣/耗时/产物路径与字节数。
- import { existsSync, mkdirSync } from 'node:fs';
- import { join, resolve } from 'node:path';
- import {
- parseArgs, log, humanSize, fileBytes, CostLedger,
- JIMENG, ENDPOINTS, jimengAvailable, maskToken, jimengToken,
- VIDEO_FRAMES, secondsToFrames, framesToSeconds, estimateCost,
- uploadToCdn, submit, poll, fetchWork, downloadTo, runTask,
- } from '../lib/index.mjs';
- import { getProvider } from '../lib/providers.mjs';
- function printEndpoints() {
- log.raw('');
- log.raw(' 即梦端点清单(¥ 为单价;实测 2026-10-13)');
- log.raw(' ' + '─'.repeat(66));
- const rows = Object.entries(ENDPOINTS);
- const w = Math.max(...rows.map(([k]) => k.length));
- for (const [k, v] of rows) {
- log.raw(` ${k.padEnd(w)} ${v.path.padEnd(28)} ¥${v.cost}/${v.unit} ${v.label}`);
- }
- log.raw('');
- log.raw(` 通道基址 : ${JIMENG.base}`);
- log.raw(` 作品记录 : ${JIMENG.parseBase}/classes/${JIMENG.workClass}/{workId}`);
- log.raw(` 七牛公司 : ${JIMENG.cdnCompany}(可用 FMODE_CDN_COMPANY 覆盖)`);
- log.raw(` 视频帧数 : [${VIDEO_FRAMES.min}, ${VIDEO_FRAMES.max}] 帧 = ${framesToSeconds(VIDEO_FRAMES.min)}~${framesToSeconds(VIDEO_FRAMES.max)} 秒`);
- log.raw('');
- }
- function printStatus() {
- const t = jimengToken();
- log.raw('');
- log.raw(' 即梦通道状态');
- log.raw(' ' + '─'.repeat(66));
- log.key('sessionToken', t ? `${maskToken(t)}(已加载)` : '未找到 ← 需设置 FMODE_SESSION_TOKEN 或 /opt/data/fmode-identity.json');
- log.key('通道基址', JIMENG.base);
- log.key('可用性', jimengAvailable() ? '✅ 凭据就绪' : '⚠️ 缺凭据');
- log.raw('');
- }
- export async function main(argv = process.argv.slice(2)) {
- const { _, flags } = parseArgs(argv);
- const action = _[0] || (flags.status ? 'status' : '');
- if (flags.help || !action) {
- console.log(`
- fmode-mv jimeng — 即梦通道直调(✅ 实测可用)
- 用法:
- fmode-mv jimeng --status # 通道状态(不产生费用)
- fmode-mv jimeng --endpoints # 端点清单 + 费用表
- fmode-mv jimeng --work <workId> # 查某个 workId 的结果直链
- fmode-mv jimeng --test # 真跑一张最小尺寸文生图(¥0.20)
- # 文生图
- fmode-mv jimeng text2img --prompt "..." [--width 1024 --height 1024] [--model v3|v31|v4] [--out f.png]
- # 图生图(多图参考,自动压缩+上传七牛)
- fmode-mv jimeng img2img --prompt "..." --ref a.png --ref b.png [--width --height --scale 0.6] [--out f.png]
- # 图生视频
- fmode-mv jimeng img2video --first a.png [--last b.png] --prompt "..." [--seconds 5] [--resolution 720p|1080p|pro]
- # 智能超清
- fmode-mv jimeng upscale --ref a.png [--resolution 4k|8k] [--out f.png]
- # 动作模仿(模板视频需公网直链)
- fmode-mv jimeng actor --ref a.png --video-url "https://..." [--version 2] [--out f.mp4]
- # 数字人(三步流程)
- fmode-mv jimeng digital-human --ref a.png --audio a.mp3 [--prompt "动作描述"] [--out f.mp4]
- # 传图到公网(拿直链)
- fmode-mv jimeng upload --ref a.png
- 选项:
- --prompt "..." 提示词
- --ref <file> 输入图(可重复传多张)
- --first/--last 图生视频的首帧 / 尾帧
- --audio <file> 数字人音频(mp3/wav)
- --video-url <url> 动作模仿的模板视频公网直链
- --seconds <n> 视频时长(会吸附到 5~10 秒,接口限 121~241 帧)
- --resolution ... 720p(默认)/1080p/pro;超清用 4k/8k
- --out <file> 输出文件
- --width/--height 图像尺寸(默认 1024×1024)
- --scale <0-1> 图生图文本影响程度(默认 0.6)
- --dry-run 只打印将发送的请求,不调用(零费用)
- ⚠️ 凭据:sessionToken 只从 FMODE_SESSION_TOKEN 或 /opt/data/fmode-identity.json 运行时读取,
- 本仓不写入、不提交任何 token。
- `);
- return 0;
- }
- const ledger = new CostLedger('jimeng 直调');
- const outDir = String(flags.out ? resolve(flags.out, '..') : join(resolve('.'), 'out', 'jimeng'));
- const outFile = flags.out ? resolve(String(flags.out)) : null;
- const provider = getProvider('jimeng');
- // ---------- 只读动作 ----------
- if (action === 'status' || flags.status) {
- printStatus();
- return jimengAvailable() ? 0 : 1;
- }
- if (action === 'endpoints' || flags.endpoints) {
- printEndpoints();
- return 0;
- }
- if (flags.work) {
- const workId = String(flags.work);
- const rec = await fetchWork(workId);
- log.raw('');
- log.key('workId', workId);
- log.key('images', rec.images.length ? rec.images.join('\n ') : '(无)');
- log.key('videos', rec.videos.length ? rec.videos.join('\n ') : '(无)');
- log.key('实扣', rec.usage?.cost != null ? `¥${rec.usage.cost}` : '未记录');
- return 0;
- }
- if (action === 'upload') {
- const refs = toArray(flags.ref);
- if (!refs.length) throw new Error('需要 --ref <file>');
- for (const f of refs) {
- const url = await uploadToCdn(f);
- log.ok(`${f} → ${url}`);
- }
- return 0;
- }
- // ---------- 需要凭据 ----------
- if (!jimengAvailable()) {
- log.err('未找到 sessionToken');
- log.dim('设置 FMODE_SESSION_TOKEN,或在 /opt/data/fmode-identity.json 写入 session_token');
- return 1;
- }
- if (flags['dry-run']) {
- log.raw('');
- log.raw(` [dry-run] action=${action}`);
- log.raw(` ${JSON.stringify({ ...flags, ref: toArray(flags.ref) }, null, 2)}`);
- log.raw(' 未调用通道,无费用。');
- return 0;
- }
- if (action === 'test') {
- log.step('即梦通道自检:真跑一张 1024×1024 文生图(¥0.20)');
- const f = await provider.text2img({
- prompt: 'a single red apple resting on a wooden table, soft window daylight, photorealistic, shallow depth of field',
- width: 1024,
- height: 1024,
- outFile: outFile || join(outDir, 'selftest.png'),
- ledger,
- });
- log.ok(`→ ${f}(${humanSize(fileBytes(f))})`);
- log.raw(ledger.summary());
- return 0;
- }
- if (action === 'text2img') {
- const f = await provider.text2img({
- prompt: String(flags.prompt || ''),
- width: Number(flags.width) || 1024,
- height: Number(flags.height) || 1024,
- model: String(flags.model || 'v3'),
- outFile: outFile || join(outDir, `text2img-${Date.now()}.png`),
- ledger,
- });
- log.ok(`→ ${f}(${humanSize(fileBytes(f))})`);
- log.raw(ledger.summary());
- return 0;
- }
- if (action === 'img2img') {
- const refs = toArray(flags.ref);
- if (!refs.length) throw new Error('需要 --ref <file>(可多张)');
- const f = await provider.img2img({
- prompt: String(flags.prompt || ''),
- refImagePaths: refs,
- width: Number(flags.width) || 2560,
- height: Number(flags.height) || 1440,
- scale: flags.scale != null ? Number(flags.scale) : 0.6,
- outFile: outFile || join(outDir, `img2img-${Date.now()}.png`),
- ledger,
- });
- log.ok(`→ ${f}(${humanSize(fileBytes(f))})`);
- log.raw(ledger.summary());
- return 0;
- }
- if (action === 'img2video') {
- const first = String(flags.first || toArray(flags.ref)[0] || '');
- if (!first) throw new Error('需要 --first <file>');
- const seconds = Number(flags.seconds) || 5;
- const frames = secondsToFrames(seconds);
- log.raw('');
- log.raw(` 时长 ${seconds}s → 吸附为 ${framesToSeconds(frames)}s(${frames} 帧);接口限 ${VIDEO_FRAMES.min}~${VIDEO_FRAMES.max} 帧`);
- const r = await provider.img2video({
- prompt: String(flags.prompt || ''),
- firstImagePath: first,
- lastImagePath: flags.last ? String(flags.last) : undefined,
- seconds,
- resolution: String(flags.resolution || '720p'),
- method: flags.last ? '3' : '2',
- outFile: outFile || join(outDir, `clip-${Date.now()}.mp4`),
- ledger,
- });
- log.ok(`→ ${r.files[0]}(${humanSize(fileBytes(r.files[0]))},method=${r.method}${r.degraded ? ' 已降级' : ''},workId=${r.workId})`);
- log.raw(ledger.summary());
- return 0;
- }
- if (action === 'upscale') {
- const refs = toArray(flags.ref);
- if (!refs.length) throw new Error('需要 --ref <file>');
- const f = await provider.upscale({
- imagePath: refs[0],
- resolution: String(flags.resolution || '4k'),
- outFile: outFile || join(outDir, `upscale-${Date.now()}.png`),
- ledger,
- });
- log.ok(`→ ${f}(${humanSize(fileBytes(f))})`);
- log.raw(ledger.summary());
- return 0;
- }
- if (action === 'actor') {
- const refs = toArray(flags.ref);
- if (!refs.length) throw new Error('需要 --ref <file>');
- if (!flags['video-url']) throw new Error('需要 --video-url <公网直链 mp4>');
- const f = await provider.actor({
- imagePath: refs[0],
- templateVideoUrl: String(flags['video-url']),
- version: Number(flags.version) || 2,
- outFile: outFile || join(outDir, `actor-${Date.now()}.mp4`),
- ledger,
- });
- log.ok(`→ ${f}(${humanSize(fileBytes(f))})`);
- log.raw(ledger.summary());
- return 0;
- }
- if (action === 'digital-human' || action === 'digitalhuman') {
- const refs = toArray(flags.ref);
- if (!refs.length) throw new Error('需要 --ref <file>(人物图)');
- if (!flags.audio) throw new Error('需要 --audio <file>(配音音频)');
- const f = await provider.digitalHuman({
- imagePath: refs[0],
- audioPath: String(flags.audio),
- prompt: String(flags.prompt || ''),
- outFile: outFile || join(outDir, `digital-human-${Date.now()}.mp4`),
- ledger,
- });
- log.ok(`→ ${f}(${humanSize(fileBytes(f))})`);
- log.raw(ledger.summary());
- return 0;
- }
- // 兜底:允许直接按端点名提交(高级用法)
- if (ENDPOINTS[action]) {
- const body = {};
- if (flags.prompt) body.prompt = String(flags.prompt);
- if (flags.width || flags.height) body.sizeDate = { width: Number(flags.width) || 1024, height: Number(flags.height) || 1024 };
- log.raw(` 直接提交端点 ${action}(${ENDPOINTS[action].label})`);
- const est = estimateCost(action);
- ledger.printEstimate(action, '', est);
- const r = await runTask(action, body, {
- kind: ENDPOINTS[action].unit === '秒' ? 'video' : 'image',
- outDir,
- prefix: action,
- estimateCost: est,
- });
- log.ok(`→ ${r.files.join(', ')}(workId=${r.workId})`);
- log.raw(ledger.summary());
- return 0;
- }
- log.err(`未知动作:${action}`);
- log.dim('可用:status / endpoints / --work / --test / text2img / img2img / img2video / upscale / actor / digital-human / upload');
- return 1;
- }
- function toArray(v) {
- if (v == null || v === true) return [];
- return Array.isArray(v) ? v.map(String) : [String(v)];
- }
- export { submit, poll, fetchWork, downloadTo, mkdirSync, existsSync };
|