honeydo 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
- import * as fs5 from 'fs';
3
+ import * as fs7 from 'fs';
4
4
  import * as path from 'path';
5
5
  import * as os from 'os';
6
6
  import { fileURLToPath } from 'url';
@@ -10,12 +10,16 @@ import { setTimeout } from 'timers/promises';
10
10
  import { createHash } from 'crypto';
11
11
 
12
12
  var HERE = path.dirname(fileURLToPath(import.meta.url));
13
- function rootCandidate() {
13
+ var IMAGE_SNAPSHOT_REPOS = ["Qwen/Qwen-Image-2512", "Qwen/Qwen-Image-Edit-2511"];
14
+ function imageHubDir() {
15
+ return path.join(os.homedir(), ".cache", "huggingface", "hub");
16
+ }
17
+ function resolveRuntimeRoot() {
14
18
  const home = os.homedir();
15
19
  let root = process.env.LMEDIA_RUNTIME ?? "";
16
20
  if (!root) {
17
21
  const link = path.join(home, ".lmedia", "runtime");
18
- if (fs5.existsSync(link)) root = fs5.realpathSync(link);
22
+ if (fs7.existsSync(link)) root = fs7.realpathSync(link);
19
23
  }
20
24
  if (!root) root = path.join(home, "ml", "lb-local-gen");
21
25
  return root;
@@ -28,22 +32,23 @@ function pythonDriverDir() {
28
32
  // src/lib/runtime.ts(tsx dev)→ <pkg>/python
29
33
  ];
30
34
  for (const c of candidates) {
31
- if (fs5.existsSync(path.join(c, "sfx.py"))) return c;
35
+ if (fs7.existsSync(path.join(c, "sfx.py"))) return c;
32
36
  }
33
37
  return candidates[0];
34
38
  }
35
39
  function resolveVideoRuntime() {
36
- const root = rootCandidate();
40
+ const root = resolveRuntimeRoot();
37
41
  return {
38
42
  root,
39
43
  venvVideo: path.join(root, ".venv-video"),
40
44
  pythonVideo: path.join(root, ".venv-video", "bin", "python"),
41
45
  mmh3turbo: path.join(root, ".venv-video", "bin", "mmh3turbo"),
42
- weightsDir: path.join(os.homedir(), ".cache", "mmh3turbo")
46
+ weightsDir: process.env.LMEDIA_WEIGHTS_DIR ?? path.join(os.homedir(), ".cache", "mmh3turbo"),
47
+ pythonDir: pythonDriverDir()
43
48
  };
44
49
  }
45
50
  function resolveAudioRuntime() {
46
- const root = rootCandidate();
51
+ const root = resolveRuntimeRoot();
47
52
  return {
48
53
  root,
49
54
  pythonAudio: path.join(root, ".venv-audio", "bin", "python"),
@@ -57,28 +62,28 @@ function hfHubCache() {
57
62
  }
58
63
  function hfSnapshot(repo) {
59
64
  const dir = path.join(hfHubCache(), `models--${repo.replace("/", "--")}`, "snapshots");
60
- if (!fs5.existsSync(dir)) return null;
61
- const snaps = fs5.readdirSync(dir).filter((s) => !s.startsWith("."));
65
+ if (!fs7.existsSync(dir)) return null;
66
+ const snaps = fs7.readdirSync(dir).filter((s) => !s.startsWith("."));
62
67
  return snaps.length ? path.join(dir, snaps[0]) : null;
63
68
  }
64
69
  function resolveRuntime() {
65
- const root = rootCandidate();
66
- if (!fs5.existsSync(root)) {
70
+ const root = resolveRuntimeRoot();
71
+ if (!fs7.existsSync(root)) {
67
72
  throw new Error(
68
73
  `\u8FD0\u884C\u65F6\u672A\u627E\u5230: ${root}
69
74
  \u8BF7\u8BBE\u7F6E LMEDIA_RUNTIME \u6307\u5411\u672C\u5730\u751F\u6210\u6808\u76EE\u5F55\uFF08\u542B .venv/.venv-train\uFF09\uFF0C\u6216\u521B\u5EFA\u8F6F\u94FE: mkdir -p ~/.lmedia && ln -s <\u6808\u76EE\u5F55> ~/.lmedia/runtime`
70
75
  );
71
76
  }
72
- const hf = path.join(os.homedir(), ".cache", "huggingface", "hub");
77
+ const hf = imageHubDir();
73
78
  const snapOf = (repo) => {
74
79
  const modelDir = path.join(hf, `models--${repo.replace("/", "--")}`);
75
80
  const dir = path.join(modelDir, "snapshots");
76
- if (!fs5.existsSync(dir)) throw new Error(`\u6A21\u578B\u672A\u4E0B\u8F7D: ${repo}\uFF08\u5148\u8FD0\u884C lmedia doctor \u5B89\u88C5\u6307\u5F15\uFF09`);
77
- const snaps = fs5.readdirSync(dir);
81
+ if (!fs7.existsSync(dir)) throw new Error(`\u6A21\u578B\u672A\u4E0B\u8F7D: ${repo}\uFF08\u5148\u8FD0\u884C lmedia doctor \u5B89\u88C5\u6307\u5F15\uFF09`);
82
+ const snaps = fs7.readdirSync(dir);
78
83
  if (snaps.length === 0) throw new Error(`\u6A21\u578B\u5FEB\u7167\u4E3A\u7A7A: ${repo}`);
79
84
  const refFile = path.join(modelDir, "refs", "main");
80
- if (fs5.existsSync(refFile)) {
81
- const ref = fs5.readFileSync(refFile, "utf-8").trim();
85
+ if (fs7.existsSync(refFile)) {
86
+ const ref = fs7.readFileSync(refFile, "utf-8").trim();
82
87
  if (snaps.includes(ref)) return path.join(dir, ref);
83
88
  }
84
89
  return path.join(dir, snaps[0]);
@@ -87,8 +92,8 @@ function resolveRuntime() {
87
92
  root,
88
93
  pythonGen: path.join(root, ".venv-train", "bin", "python"),
89
94
  pythonFast: path.join(root, ".venv", "bin", "python"),
90
- snapshot: snapOf("Qwen/Qwen-Image-2512"),
91
- snapshotEdit: snapOf("Qwen/Qwen-Image-Edit-2511"),
95
+ snapshot: snapOf(IMAGE_SNAPSHOT_REPOS[0]),
96
+ snapshotEdit: snapOf(IMAGE_SNAPSHOT_REPOS[1]),
92
97
  pythonDir: pythonDriverDir()
93
98
  };
94
99
  }
@@ -98,11 +103,11 @@ function seed() {
98
103
  return [];
99
104
  }
100
105
  function loadRegistry() {
101
- if (!fs5.existsSync(REGISTRY_PATH)) {
102
- fs5.mkdirSync(REGISTRY_DIR, { recursive: true });
103
- fs5.writeFileSync(REGISTRY_PATH, JSON.stringify(seed(), null, 2));
106
+ if (!fs7.existsSync(REGISTRY_PATH)) {
107
+ fs7.mkdirSync(REGISTRY_DIR, { recursive: true });
108
+ fs7.writeFileSync(REGISTRY_PATH, JSON.stringify(seed(), null, 2));
104
109
  }
105
- return JSON.parse(fs5.readFileSync(REGISTRY_PATH, "utf-8"));
110
+ return JSON.parse(fs7.readFileSync(REGISTRY_PATH, "utf-8"));
106
111
  }
107
112
  function findLora(name) {
108
113
  return loadRegistry().find((l) => l.name === name);
@@ -110,10 +115,10 @@ function findLora(name) {
110
115
  function addLora(entry) {
111
116
  const list = loadRegistry().filter((l) => l.name !== entry.name);
112
117
  list.push(entry);
113
- fs5.writeFileSync(REGISTRY_PATH, JSON.stringify(list, null, 2));
118
+ fs7.writeFileSync(REGISTRY_PATH, JSON.stringify(list, null, 2));
114
119
  }
115
120
  function runPython(python, script, payload, opts = {}) {
116
- return new Promise((resolve7, reject) => {
121
+ return new Promise((resolve8, reject) => {
117
122
  const child = spawn(python, [script, JSON.stringify(payload)], {
118
123
  env: { ...process.env, HF_HUB_OFFLINE: process.env.HF_HUB_OFFLINE ?? "1" }
119
124
  });
@@ -135,7 +140,7 @@ ${stderrTail}`));
135
140
  const lines = stdoutBuf.trim().split("\n").filter(Boolean);
136
141
  const lastLine = lines[lines.length - 1] ?? "";
137
142
  try {
138
- resolve7(JSON.parse(lastLine));
143
+ resolve8(JSON.parse(lastLine));
139
144
  } catch {
140
145
  reject(new Error(`\u65E0\u6CD5\u89E3\u6790 python \u8F93\u51FA: ${lastLine}
141
146
  ${stderrTail}`));
@@ -161,13 +166,13 @@ function pidAlive(pid) {
161
166
  }
162
167
  function readStatus(m) {
163
168
  try {
164
- return JSON.parse(fs5.readFileSync(statusPath(m), "utf-8"));
169
+ return JSON.parse(fs7.readFileSync(statusPath(m), "utf-8"));
165
170
  } catch {
166
171
  return null;
167
172
  }
168
173
  }
169
174
  function requestJob(mode, payload) {
170
- return new Promise((resolve7, reject) => {
175
+ return new Promise((resolve8, reject) => {
171
176
  const sock = net.connect({ path: sockPath(mode) });
172
177
  sock.setNoDelay(true);
173
178
  let buf = "";
@@ -183,7 +188,7 @@ function requestJob(mode, payload) {
183
188
  if (!sock.writableEnded && buf === "") {
184
189
  settled = true;
185
190
  sock.destroy();
186
- resolve7(null);
191
+ resolve8(null);
187
192
  }
188
193
  });
189
194
  sock.on("connect", () => {
@@ -196,7 +201,7 @@ function requestJob(mode, payload) {
196
201
  if (e.code === "ENOENT" || e.code === "ECONNREFUSED") {
197
202
  settled = true;
198
203
  sock.destroy();
199
- resolve7(null);
204
+ resolve8(null);
200
205
  } else fail2(new Error(`daemon \u8FDE\u63A5\u9519\u8BEF: ${e.message}`));
201
206
  });
202
207
  sock.on("data", (d) => {
@@ -220,7 +225,7 @@ function requestJob(mode, payload) {
220
225
  } else if (frame.t === "done") {
221
226
  settled = true;
222
227
  sock.end();
223
- if (frame.ok) resolve7(frame.result);
228
+ if (frame.ok) resolve8(frame.result);
224
229
  else fail2(new Error(`daemon \u4EFB\u52A1\u5931\u8D25: ${frame.error}
225
230
  ${frame.tb ?? ""}`));
226
231
  }
@@ -237,7 +242,7 @@ async function spawnDaemon(mode, opts = {}) {
237
242
  const alive = await pingDaemon(mode);
238
243
  if (alive) return { pid: alive.pid };
239
244
  const rt = resolveRuntime();
240
- fs5.mkdirSync(SERVE_DIR, { recursive: true });
245
+ fs7.mkdirSync(SERVE_DIR, { recursive: true });
241
246
  const idle = String(opts.idleTimeoutSec ?? DEFAULT_IDLE_TIMEOUT_SEC);
242
247
  const args = [
243
248
  path.join(rt.pythonDir, "serve.py"),
@@ -254,7 +259,7 @@ async function spawnDaemon(mode, opts = {}) {
254
259
  "--idle-timeout",
255
260
  idle
256
261
  ];
257
- const logFd = fs5.openSync(logPath(mode), "a");
262
+ const logFd = fs7.openSync(logPath(mode), "a");
258
263
  const child = spawn(rt.pythonGen, args, {
259
264
  detached: true,
260
265
  stdio: ["ignore", logFd, logFd],
@@ -263,7 +268,7 @@ async function spawnDaemon(mode, opts = {}) {
263
268
  child.unref();
264
269
  const deadline = Date.now() + 2e4;
265
270
  while (Date.now() < deadline) {
266
- if (fs5.existsSync(sockPath(mode))) return { pid: child.pid ?? -1 };
271
+ if (fs7.existsSync(sockPath(mode))) return { pid: child.pid ?? -1 };
267
272
  await setTimeout(200);
268
273
  }
269
274
  return null;
@@ -271,10 +276,10 @@ async function spawnDaemon(mode, opts = {}) {
271
276
  async function stopDaemon(mode) {
272
277
  const st = await pingDaemon(mode);
273
278
  if (!st) {
274
- const had = fs5.existsSync(sockPath(mode)) || fs5.existsSync(statusPath(mode));
279
+ const had = fs7.existsSync(sockPath(mode)) || fs7.existsSync(statusPath(mode));
275
280
  for (const p of [sockPath(mode), statusPath(mode)]) {
276
281
  try {
277
- fs5.rmSync(p);
282
+ fs7.rmSync(p);
278
283
  } catch {
279
284
  }
280
285
  }
@@ -294,7 +299,7 @@ async function stopDaemon(mode) {
294
299
  await setTimeout(500);
295
300
  for (const p of [sockPath(mode), statusPath(mode)]) {
296
301
  try {
297
- fs5.rmSync(p);
302
+ fs7.rmSync(p);
298
303
  } catch {
299
304
  }
300
305
  }
@@ -312,6 +317,121 @@ async function waitGpuIdle() {
312
317
  await setTimeout(5e3);
313
318
  }
314
319
  }
320
+ var ESRGAN_CACHE_PATH = path.join(os.homedir(), ".cache", "lmedia", "RealESRGAN_x2.pth");
321
+ var LEGACY_TMP_PATH = "/tmp/RealESRGAN_x2.pth";
322
+ function resolveEsrganPath() {
323
+ const env = process.env.LMEDIA_REALESRGAN_PATH;
324
+ if (env && fs7.existsSync(env)) return env;
325
+ if (fs7.existsSync(ESRGAN_CACHE_PATH)) return ESRGAN_CACHE_PATH;
326
+ if (fs7.existsSync(LEGACY_TMP_PATH)) return LEGACY_TMP_PATH;
327
+ return process.env.LMEDIA_REALESRGAN_PATH ?? ESRGAN_CACHE_PATH;
328
+ }
329
+
330
+ // src/lib/image-state.ts
331
+ var IMAGE_STATE_ENV = "LMEDIA_IMAGE_STATE";
332
+ var IMAGE_GATE_BYPASS_ENV = "LMEDIA_NO_IMAGE_GATE";
333
+ function imageStatePath() {
334
+ const env = process.env[IMAGE_STATE_ENV];
335
+ if (env && env.trim()) return path.resolve(env.trim());
336
+ return path.join(os.homedir(), ".config", "limg", "image-state.json");
337
+ }
338
+ var ImageDisabledError = class extends Error {
339
+ };
340
+ function readImageGate() {
341
+ const file = imageStatePath();
342
+ if (!fs7.existsSync(file)) return { state: "enabled", file, present: false };
343
+ let parsed;
344
+ try {
345
+ parsed = JSON.parse(fs7.readFileSync(file, "utf-8"));
346
+ } catch (e) {
347
+ return { state: "corrupt", file, error: `\u4E0D\u662F\u5408\u6CD5 JSON\uFF08${e.message}\uFF09` };
348
+ }
349
+ const s = parsed;
350
+ if (!s || typeof s !== "object" || Array.isArray(s)) {
351
+ return { state: "corrupt", file, error: "\u6839\u4E0D\u662F\u5BF9\u8C61" };
352
+ }
353
+ if (typeof s.enabled !== "boolean") {
354
+ return { state: "corrupt", file, error: "\u7F3A\u5C11 enabled \u5E03\u5C14\u5B57\u6BB5" };
355
+ }
356
+ return s.enabled === false ? { state: "disabled", file, present: true, disabledAt: s.disabledAt ?? "", reason: s.reason } : { state: "enabled", file, present: true, reason: s.reason };
357
+ }
358
+ function writeImageState(enabled, opts = {}) {
359
+ const file = imageStatePath();
360
+ const state = enabled ? { enabled: true } : { enabled: false, disabledAt: (/* @__PURE__ */ new Date()).toISOString(), ...opts.reason ? { reason: opts.reason } : {} };
361
+ fs7.mkdirSync(path.dirname(file), { recursive: true });
362
+ const tmp = `${file}.${process.pid}.tmp`;
363
+ fs7.writeFileSync(tmp, `${JSON.stringify(state, null, 2)}
364
+ `, "utf-8");
365
+ fs7.renameSync(tmp, file);
366
+ return file;
367
+ }
368
+ function gateAdvice(g) {
369
+ const head = g.state === "disabled" ? `\u56FE\u50CF\u80FD\u529B\u5DF2\u7981\u7528${g.disabledAt ? `\uFF08${g.disabledAt}\uFF09` : ""}${g.reason ? ` \u539F\u56E0: ${g.reason}` : ""}` : `\u56FE\u50CF\u80FD\u529B\u72B6\u6001\u6587\u4EF6\u635F\u574F\uFF0C\u62D2\u7EDD\u6267\u884C\u56FE\u50CF\u547D\u4EE4\uFF08\u4E0D\u731C\uFF09: ${g.file}
370
+ \u539F\u56E0: ${g.error}`;
371
+ return `${head}
372
+ \u6062\u590D: lmedia image enable${g.state === "corrupt" ? `\uFF08\u7528\u542F\u7528\u72B6\u6001\u8986\u76D6\u91CD\u5199\uFF09
373
+ \u6216 rm ${g.file}\uFF08\u5220\u9664\u540E\u56DE\u5230\u9ED8\u8BA4\u542F\u7528\uFF09` : ""}
374
+ \u67E5\u770B: lmedia image status
375
+ \u4E91\u7AEF\u66FF\u4EE3: honeydo image gen "<prompt>" --engine doubao
376
+ \uFF08\u4E34\u65F6\u7ED5\u8FC7: ${IMAGE_GATE_BYPASS_ENV}=1\uFF09`;
377
+ }
378
+ function assertImageEnabled(what) {
379
+ if (process.env[IMAGE_GATE_BYPASS_ENV] === "1") {
380
+ const g2 = readImageGate();
381
+ if (g2.state !== "enabled") {
382
+ console.error(`\u26A0\uFE0F ${IMAGE_GATE_BYPASS_ENV}=1\uFF1A\u5DF2\u7ED5\u8FC7\u56FE\u50CF\u80FD\u529B gate\uFF08\u5F53\u524D\u4E3A${g2.state === "disabled" ? "\u7981\u7528" : "\u635F\u574F"}\u6001\uFF09`);
383
+ }
384
+ return;
385
+ }
386
+ const g = readImageGate();
387
+ if (g.state === "enabled") return;
388
+ throw new ImageDisabledError(`${gateAdvice(g)}
389
+ \uFF08\u88AB\u62E6\u622A\u7684\u547D\u4EE4: ${what}\uFF09`);
390
+ }
391
+ var VENV_FIX = "\u56FE\u50CF venv \u672A\u5C31\u7EEA\uFF1A\u6309 packages/lmedia/USAGE.md\u300C\u56FE\u50CF\u300D\u5C0F\u8282\u91CD\u5EFA .venv / .venv-train";
392
+ var snapDir = (repo) => path.join(imageHubDir(), `models--${repo.replace("/", "--")}`, "snapshots");
393
+ function hasSnapshot(repo) {
394
+ try {
395
+ const d = snapDir(repo);
396
+ return fs7.existsSync(d) && fs7.readdirSync(d).length > 0;
397
+ } catch {
398
+ return false;
399
+ }
400
+ }
401
+ function snapshotFix(repo) {
402
+ return `HF_HUB_OFFLINE=0 hf download ${repo}\uFF08~54GB\uFF1B\u7F51\u7EDC\u5DEE\u53EF HF_ENDPOINT=https://hf-mirror.com\uFF09`;
403
+ }
404
+ function imagePreflight() {
405
+ const root = resolveRuntimeRoot();
406
+ return [
407
+ {
408
+ name: "runtime root",
409
+ ok: fs7.existsSync(root),
410
+ fix: `\u8BBE LMEDIA_RUNTIME=${root} \u6307\u5411\u672C\u5730\u751F\u6210\u6808\uFF0C\u6216 ln -s <\u6808\u76EE\u5F55> ~/.lmedia/runtime`
411
+ },
412
+ {
413
+ name: "pythonGen (.venv-train)",
414
+ ok: fs7.existsSync(path.join(root, ".venv-train", "bin", "python")),
415
+ fix: VENV_FIX
416
+ },
417
+ {
418
+ name: "pythonFast (.venv)",
419
+ ok: fs7.existsSync(path.join(root, ".venv", "bin", "python")),
420
+ fix: VENV_FIX
421
+ },
422
+ // 标签取 repo 短名(Qwen-Image-2512),与 doctor 既有输出逐字一致,不破坏外部匹配
423
+ ...IMAGE_SNAPSHOT_REPOS.map((repo) => ({
424
+ name: `[image] ${repo.split("/")[1]} \u5FEB\u7167`,
425
+ ok: hasSnapshot(repo),
426
+ fix: snapshotFix(repo)
427
+ })),
428
+ {
429
+ name: "[image] Real-ESRGAN \u6743\u91CD",
430
+ ok: fs7.existsSync(resolveEsrganPath()),
431
+ fix: "\u7F3A RealESRGAN_x2.pth\uFF08~64MB\uFF09\uFF1A\u89C1 USAGE.md\u300C\u56FE\u50CF\u300D\u5C0F\u8282\uFF0C\u6216 LMEDIA_REALESRGAN_PATH \u6307\u5B9A"
432
+ }
433
+ ];
434
+ }
315
435
  function parseMode(v) {
316
436
  if (v !== "gen" && v !== "edit") {
317
437
  console.error("--mode \u5FC5\u987B\u662F gen \u6216 edit");
@@ -415,13 +535,119 @@ function registerServe(image) {
415
535
  registerStatus(serve);
416
536
  }
417
537
 
538
+ // src/commands/image-capability.ts
539
+ var MODES = ["gen", "edit"];
540
+ var isBusy = (st) => st.busy || st.queue > 0;
541
+ function registerDisable(image) {
542
+ image.command("disable").description("\u7981\u7528\u56FE\u50CF\u80FD\u529B\uFF1Agen/edit/upscale/serve start \u5168\u90E8\u62E6\u4E0B\uFF08enable/status/serve stop \u59CB\u7EC8\u53EF\u7528\uFF09").option("--reason <text>", "\u8BB0\u5F55\u7981\u7528\u539F\u56E0\uFF08\u5199\u8FDB\u72B6\u6001\u6587\u4EF6\uFF09").action(async (opts) => {
543
+ const g = readImageGate();
544
+ if (g.state === "disabled") {
545
+ console.log(`\xB7 \u5DF2\u662F\u7981\u7528\u6001\uFF08\u672A\u6539\u5199\u72B6\u6001\u6587\u4EF6\uFF09`);
546
+ }
547
+ let stopped = false;
548
+ for (const m of MODES) {
549
+ const st = await pingDaemon(m);
550
+ if (!st) {
551
+ console.log(`\xB7 ${m} daemon \u672A\u8FD0\u884C`);
552
+ continue;
553
+ }
554
+ if (isBusy(st)) {
555
+ console.error(
556
+ `\u2717 ${m} daemon \u6B63\u5FD9\uFF08pid ${st.pid}\uFF0Cjobs ${st.jobs}\uFF09\u2014\u2014\u672A\u5199\u72B6\u6001\u3002
557
+ \u7B49\u4EFB\u52A1\u7ED3\u675F\uFF0C\u6216 lmedia image serve stop --mode ${m} \u4E2D\u65AD\u5F53\u524D\u4EFB\u52A1\u540E\u91CD\u8BD5\u3002`
558
+ );
559
+ process.exit(1);
560
+ }
561
+ const r = await stopDaemon(m);
562
+ console.log(`\u2713 \u5DF2\u505C\u6B62 ${m} daemon\uFF08pid ${st.pid}${r === "killed" ? "\uFF0CSIGKILL" : ""}\uFF09\u2014\u2014\u91CA\u653E\u5E38\u9A7B\u5185\u5B58\u4E0E\u6587\u4EF6\u53E5\u67C4`);
563
+ stopped = true;
564
+ }
565
+ if (g.state === "corrupt") {
566
+ console.error(`\u26A0\uFE0F \u72B6\u6001\u6587\u4EF6\u635F\u574F\uFF0C\u5DF2\u7528\u65B0\u72B6\u6001\u8986\u76D6: ${g.file}\uFF08${g.error}\uFF09`);
567
+ }
568
+ if (g.state !== "disabled") {
569
+ const file = writeImageState(false, { reason: opts.reason });
570
+ console.log(`\u2713 \u56FE\u50CF\u80FD\u529B\u5DF2\u7981\u7528\uFF08${file}\uFF09`);
571
+ }
572
+ console.log(
573
+ ` \u6062\u590D: lmedia image enable
574
+ \u4E91\u7AEF\u66FF\u4EE3: honeydo image gen "<prompt>" --engine doubao
575
+ ` + (stopped ? "" : ` \u63D0\u793A: \u5220\u9664\u6743\u91CD\u524D\u8BF7\u786E\u8BA4 lmedia image serve status \u65E0 daemon \u5728\u8DD1\uFF08\u6301\u6709\u6587\u4EF6\u53E5\u67C4\u65F6 rm \u4E0D\u91CA\u653E\u7A7A\u95F4\uFF09
576
+ `) + ` \u968F\u540E\u53EF\u624B\u5DE5\u6E05\u7406\uFF08\u5F53\u524D\u5728\u4F4D\u60C5\u51B5\u89C1 lmedia image status\uFF09`
577
+ );
578
+ });
579
+ }
580
+ function registerEnable(image) {
581
+ image.command("enable").description("\u542F\u7528\u56FE\u50CF\u80FD\u529B\uFF08\u672C\u5730\u6743\u91CD/venv \u82E5\u5DF2\u6E05\u7406\u4F1A\u5217\u51FA\u7F3A\u5931\u9879\u4E0E\u91CD\u65B0\u4E0B\u8F7D\u6307\u5F15\uFF09").action(() => {
582
+ const g = readImageGate();
583
+ if (g.state === "corrupt") {
584
+ console.error(`\u26A0\uFE0F \u72B6\u6001\u6587\u4EF6\u635F\u574F\uFF0C\u5DF2\u7528\u65B0\u72B6\u6001\u8986\u76D6: ${g.file}\uFF08${g.error}\uFF09`);
585
+ }
586
+ const file = writeImageState(true);
587
+ console.log(`\u2713 \u56FE\u50CF\u80FD\u529B\u5DF2\u542F\u7528\uFF08${file}\uFF09`);
588
+ const missing = imagePreflight().filter((i) => !i.ok);
589
+ if (!missing.length) {
590
+ console.log(` \u672C\u5730\u56FE\u50CF\u8D44\u4EA7\u9F50\u5907\uFF0C\u53EF\u76F4\u63A5\u4F7F\u7528\u3002`);
591
+ return;
592
+ }
593
+ console.error(`\u26A0\uFE0F \u4F46\u56FE\u50CF\u6808\u4ECD\u7F3A ${missing.length} \u9879\uFF0C\u6682\u4E0D\u53EF\u7528\uFF1A`);
594
+ for (const m of missing) console.error(` \u2717 ${m.name} \u2192 ${m.fix}`);
595
+ console.error(` \u78C1\u76D8\u9700\u9884\u7559\u7EA6 108GB\uFF08\u4E24\u4E2A\u5FEB\u7167\uFF09\uFF1B\u5C31\u7EEA\u540E lmedia doctor \u590D\u6838`);
596
+ process.exit(1);
597
+ });
598
+ }
599
+ function registerStatus2(image) {
600
+ image.command("status").description("\u67E5\u770B\u56FE\u50CF\u80FD\u529B\u5F00\u5173\u72B6\u6001\u4E0E\u672C\u5730\u4F4D\u5728\u4F4D\u60C5\u51B5\uFF080=\u53EF\u8DD1\u56FE\u50CF\u547D\u4EE4\uFF1B\u4E0E image serve status \u770B\u7684\u662F\u4E0D\u540C\u4E1C\u897F\uFF09").action(async () => {
601
+ const g = readImageGate();
602
+ const label = g.state === "disabled" ? "\u5DF2\u7981\u7528" : g.state === "corrupt" ? "\u72B6\u6001\u6587\u4EF6\u635F\u574F" : "\u5DF2\u542F\u7528";
603
+ const when = g.state === "disabled" && g.disabledAt ? `\uFF08${g.disabledAt}\uFF09` : "";
604
+ const why = g.state !== "corrupt" && g.reason ? ` \xB7 ${g.reason}` : "";
605
+ console.log(`\u56FE\u50CF\u80FD\u529B: ${label}${when}${why}`);
606
+ console.log(` \u72B6\u6001\u6587\u4EF6: ${g.file}${g.state === "enabled" && !g.present ? "\uFF08\u4E0D\u5B58\u5728=\u9ED8\u8BA4\u542F\u7528\uFF09" : ""}`);
607
+ if (g.state !== "enabled") console.log(gateAdvice(g).split("\n").map((l) => ` ${l}`).join("\n"));
608
+ const items = imagePreflight();
609
+ for (const i of items) {
610
+ console.log(` ${i.ok ? "\u2713" : "\u2717"} ${i.name}${i.ok ? "" : ` \u2192 ${i.fix}`}`);
611
+ }
612
+ for (const m of MODES) {
613
+ const st = await pingDaemon(m);
614
+ console.log(
615
+ ` ${st ? g.state === "disabled" ? "\u2717" : "\u2713" : "\xB7"} daemon ${m}` + (st ? `: ${st.state}\uFF08pid ${st.pid}\uFF0Cjobs ${st.jobs}\uFF09${g.state === "disabled" ? "\u2014\u2014\u80FD\u529B\u5DF2\u7981\u7528\u5374\u4ECD\u5728\u8DD1\uFF0C\u8BF7 lmedia image serve stop" : ""}` : ": \u672A\u8FD0\u884C")
616
+ );
617
+ }
618
+ const usable = g.state === "enabled" && items.every((i) => i.ok);
619
+ process.exit(usable ? 0 : 1);
620
+ });
621
+ }
622
+ function registerImageCapability(image) {
623
+ registerDisable(image);
624
+ registerEnable(image);
625
+ registerStatus2(image);
626
+ }
627
+
418
628
  // src/commands/image.ts
629
+ var IMAGE_GATED = /* @__PURE__ */ new Set(["image gen", "image edit", "image upscale", "image serve start"]);
630
+ function commandPath(cmd) {
631
+ const parts = [];
632
+ for (let c = cmd; c && c.parent; c = c.parent) parts.unshift(c.name());
633
+ return parts.join(" ");
634
+ }
635
+ function gateImage(what) {
636
+ try {
637
+ assertImageEnabled(what);
638
+ } catch (e) {
639
+ if (!(e instanceof ImageDisabledError)) throw e;
640
+ console.error(`\u2717 ${e.message}`);
641
+ process.exit(1);
642
+ }
643
+ }
419
644
  var DEFAULT_NEG = "\u4F4E\u5206\u8FA8\u7387\uFF0C\u4F4E\u753B\u8D28\uFF0C\u80A2\u4F53\u7578\u5F62\uFF0C\u624B\u6307\u7578\u5F62\uFF0C\u753B\u9762\u8FC7\u9971\u548C\uFF0C\u8721\u50CF\u611F\uFF0C\u4EBA\u8138\u65E0\u7EC6\u8282\uFF0C\u8FC7\u5EA6\u5149\u6ED1\uFF0C\u753B\u9762\u5177\u6709AI\u611F\u3002\u6784\u56FE\u6DF7\u4E71\u3002\u6587\u5B57\u6A21\u7CCA\uFF0C\u626D\u66F2\u3002";
420
645
  function parseCache(v) {
421
646
  if (v === void 0) return void 0;
422
647
  return { thresh: v === true ? 0.3 : parseFloat(String(v)) };
423
648
  }
424
649
  async function runImageJob(rt, args) {
650
+ gateImage(`image ${args.kind}`);
425
651
  if (!args.noDaemon && process.env.LMEDIA_NO_DAEMON !== "1") {
426
652
  for (const mode of args.daemonModes) {
427
653
  const r = await requestJob(mode, { kind: args.kind, ...args.payload });
@@ -474,7 +700,7 @@ function registerGen(image) {
474
700
  const trueCfg = opts.cfg !== void 0 ? parseFloat(opts.cfg) : fast ? 1 : 4;
475
701
  if (fast && !names.includes("lightning2512")) {
476
702
  const lt = findLora("lightning2512");
477
- if (!lt || !fs5.existsSync(lt.path)) {
703
+ if (!lt || !fs7.existsSync(lt.path)) {
478
704
  console.error("--fast \u9700\u8981 lightning2512 LoRA\uFF08\u4E0B\u8F7D\u540E: lmedia lora add lightning2512 <path> --kind speed\uFF09");
479
705
  process.exit(2);
480
706
  }
@@ -521,7 +747,7 @@ function registerEdit(image) {
521
747
  if (fast) {
522
748
  const wantX4 = fastSteps <= 4;
523
749
  const lt = (wantX4 ? findLora("lightningedit2511x4") : void 0) ?? findLora("lightningedit2511");
524
- if (!lt || !fs5.existsSync(lt.path)) {
750
+ if (!lt || !fs7.existsSync(lt.path)) {
525
751
  console.error("--fast \u9700\u8981 lightningedit2511 LoRA\uFF08\u4E0B\u8F7D\u540E: lmedia lora add lightningedit2511 <path> --kind speed\uFF09");
526
752
  process.exit(2);
527
753
  }
@@ -571,11 +797,16 @@ function registerUpscale(image) {
571
797
  });
572
798
  }
573
799
  function registerImage(program2) {
574
- const image = program2.command("image").description("\u56FE\u50CF\u751F\u4EA7\u80FD\u529B\uFF08\u6587\u751F\u56FE/\u53C2\u8003\u56FE\u7F16\u8F91/\u8D85\u5206/\u5E38\u9A7B daemon\uFF09");
800
+ const image = program2.command("image").description("\u56FE\u50CF\u751F\u4EA7\u80FD\u529B\uFF08\u6587\u751F\u56FE/\u53C2\u8003\u56FE\u7F16\u8F91/\u8D85\u5206/\u5E38\u9A7B daemon\uFF1B\u5F00\u5173\u89C1 image disable|enable|status\uFF09");
801
+ image.hook("preAction", (_thisCommand, actionCommand) => {
802
+ const p = commandPath(actionCommand);
803
+ if (IMAGE_GATED.has(p)) gateImage(p);
804
+ });
575
805
  registerGen(image);
576
806
  registerEdit(image);
577
807
  registerUpscale(image);
578
808
  registerServe(image);
809
+ registerImageCapability(image);
579
810
  }
580
811
  function hasCommand(cmd) {
581
812
  return spawnSync("which", [cmd], { stdio: "ignore" }).status === 0;
@@ -590,11 +821,43 @@ var RES_PRESETS = [
590
821
  "704p",
591
822
  "720p",
592
823
  "768p",
824
+ "1080p",
593
825
  "square",
594
826
  "portrait",
595
827
  "vertical"
596
828
  // 竖版/方版(绘本竖页用 portrait)
597
829
  ];
830
+ var TURBO_BUNDLE = "dit-turbo";
831
+ var TURBO_LORA_FILE = "lightx2v_fl2v_turbo_8step_v1.0_768p_bf16.safetensors";
832
+ var TURBO_LORA_URL = `https://hf-mirror.com/lightx2v/Minimax-h3-Turbo/resolve/main/${TURBO_LORA_FILE}`;
833
+ var FAST_STEPS = 8;
834
+ var FAST_SHIFT_VIDEO = 6;
835
+ var PERSON_RE = new RegExp(
836
+ "\u4EBA\u50CF|\u4EBA\u7269|\u5973\u5B69|\u7537\u5B69|\u5973\u4EBA|\u7537\u4EBA|\u5973\u751F|\u7537\u751F|\u7238\u7238|\u5988\u5988|\u5B9D\u5B9D|\u5B69\u5B50|\u5C0F\u5B69|\u513F\u5B50|\u5973\u513F|\u59D0\u59D0|\u5F1F\u5F1F|\u59B9\u59B9|\u54E5\u54E5|\u7237\u7237|\u5976\u5976|\u53D4\u53D4|\u963F\u59E8|\u5BB6\u5EAD|portrait|person|child|baby|girl|boy|woman|man|people|face",
837
+ "i"
838
+ );
839
+ var LOWRES_PRESETS = /* @__PURE__ */ new Set(["256p", "352p", "480p", "576p"]);
840
+ function qualityHints(prompt, o) {
841
+ const hints = [];
842
+ if (!PERSON_RE.test(prompt)) return hints;
843
+ if (o.fast) {
844
+ console.error("\u63D0\u793A: \u4EBA\u7269\u89C6\u9891\u7528 --fast\uFF08\u84B8\u998F\u6863\uFF09\u4F1A\u51FA\u73B0\u7EB9\u7406\u6CB9\u753B\u611F\u4E0E\u8EAB\u4EFD\u6F02\u79FB\u2014\u2014\u8D28\u91CF\u4F18\u5148\u8BF7\u53BB\u6389 --fast \u7528\u57FA\u5EA7 12 \u6B65\uFF0C\u5E76\u628A\u6784\u56FE\u88C1\u5230\u4EBA\u7269\u7279\u5199\uFF08\u4EBA\u8138\u5360\u753B\u9762 1/4 \u4EE5\u4E0A\uFF09\u3002\u914D\u65B9: lmedia video recipes");
845
+ hints.push("person+fast: \u84B8\u998F\u6863\u7EB9\u7406\u8F6F\u5316\u4E14\u65F6\u57DF\u6F02\u79FB\u5927\uFF1B\u4EBA\u7269\u8D28\u91CF\u4F18\u5148\u7528\u57FA\u5EA7 12 \u6B65 + \u4E3B\u4F53\u7279\u5199\u6784\u56FE\uFF08lmedia video recipes\uFF09");
846
+ }
847
+ if (LOWRES_PRESETS.has(o.res)) {
848
+ console.error(`\u63D0\u793A: \u4EBA\u7269\u89C6\u9891\u5206\u8FA8\u7387\u4E0D\u8DB3\uFF08${o.res} \u77ED\u8FB9 < 704\uFF0C\u4EBA\u8138\u50CF\u7D20\u592A\u5C11\u5FC5\u7CCA\uFF09\u2014\u2014\u5EFA\u8BAE -r 720p/square/portrait \u5E76\u8BA9\u4EBA\u7269\u5360\u753B\u9762\u4E3B\u4F53\u3002\u914D\u65B9: lmedia video recipes`);
849
+ hints.push(`person+${o.res}: \u4EBA\u8138\u50CF\u7D20\u4E0D\u8DB3\uFF1B\u7528 720p/square/portrait + \u4E3B\u4F53\u7279\u5199\u6784\u56FE`);
850
+ }
851
+ if (o.firstFrame && !o.lastFrame) {
852
+ console.error("\u63D0\u793A: \u56FE\u751F\u89C6\u9891\u4EC5\u951A\u5B9A\u9996\u5E27\uFF0C\u4EBA\u7269\u52A8\u4F5C\u5927\u65F6\u7ED3\u5C3E\u8EAB\u4EFD\u4F1A\u6F02\u79FB\uFF08\u5B9E\u6D4B\u7ED3\u5C3E\u4EBA\u8138\u76F8\u4F3C\u5EA6\u53EF\u8DCC\u5230 0.3\uFF09\u2014\u2014\u540C\u4E00\u5F20\u56FE\u518D\u52A0 --last-frame \u53CC\u951A\u5B9A\u53EF\u56DE\u73AF\u6291\u5236\uFF08\u5B9E\u6D4B\u62C9\u56DE 0.97\uFF09\u3002\u914D\u65B9: lmedia video recipes");
853
+ hints.push("first-frame only: \u7ED3\u5C3E\u8EAB\u4EFD\u4F1A\u6F02\u79FB\uFF1B\u540C\u56FE --last-frame \u53CC\u951A\u5B9A\u53EF\u5C06\u7ED3\u5C3E\u4EBA\u8138\u76F8\u4F3C\u5EA6 0.3\u21920.97");
854
+ }
855
+ if (o.seconds > 5) {
856
+ console.error("\u63D0\u793A: \u4EBA\u7269\u89C6\u9891\u6F02\u79FB\u968F\u65F6\u957F\u7D2F\u79EF\uFF0C\u5EFA\u8BAE \u22645s\uFF08\u8981\u957F\u955C\u5934\u8BF7\u5206\u591A\u6BB5 + --last-frame \u8854\u63A5\uFF09\u3002");
857
+ hints.push("person+long: \u6F02\u79FB\u968F\u65F6\u957F\u7D2F\u79EF\uFF0C\u5EFA\u8BAE \u22645s \u6216\u5206\u6BB5");
858
+ }
859
+ return hints;
860
+ }
598
861
  function runStep(cmd, args) {
599
862
  const r = spawnSync(cmd, args, { stdio: "inherit" });
600
863
  if (r.status !== 0) {
@@ -604,16 +867,16 @@ function runStep(cmd, args) {
604
867
  }
605
868
  function patchGgufFilename(venvVideo) {
606
869
  const libDir = path.join(venvVideo, "lib");
607
- if (!fs5.existsSync(libDir)) return;
608
- const pyDir = fs5.readdirSync(libDir).find((d) => d.startsWith("python"));
870
+ if (!fs7.existsSync(libDir)) return;
871
+ const pyDir = fs7.readdirSync(libDir).find((d) => d.startsWith("python"));
609
872
  if (!pyDir) return;
610
873
  const w = path.join(libDir, pyDir, "site-packages", "mmh3turbo", "weights.py");
611
- if (!fs5.existsSync(w)) return;
612
- const src = fs5.readFileSync(w, "utf8");
874
+ if (!fs7.existsSync(w)) return;
875
+ const src = fs7.readFileSync(w, "utf8");
613
876
  const OLD = "MiniMax-H3-Qwen3VL-32B-TextEncoder-Q2_K.gguf";
614
877
  const NEW = "qwen3vl-32B-MiniMax-H3-Q2_K.gguf";
615
878
  if (src.includes(OLD)) {
616
- fs5.writeFileSync(w, src.replaceAll(OLD, NEW));
879
+ fs7.writeFileSync(w, src.replaceAll(OLD, NEW));
617
880
  console.error(`\u5DF2\u4FEE\u8865 mmh3turbo \u4E0A\u6E38 GGUF \u6587\u4EF6\u540D\u6F02\u79FB\uFF08${OLD} \u2192 ${NEW}\uFF09`);
618
881
  }
619
882
  }
@@ -636,8 +899,8 @@ function mirrorProvision() {
636
899
  for (const f of MIRROR_FILES) {
637
900
  const dest = f.dest.replace(/^~/, os.homedir());
638
901
  const url = f.repo === "Comfy-Org/MiniMax-H3" ? `${MIRROR}/${f.repo}/resolve/main/vae/${path.basename(dest)}` : `${MIRROR}/${f.repo}/resolve/main/${path.basename(dest)}`;
639
- fs5.mkdirSync(path.dirname(dest), { recursive: true });
640
- if (fs5.existsSync(dest) && fs5.statSync(dest).size === f.bytes) {
902
+ fs7.mkdirSync(path.dirname(dest), { recursive: true });
903
+ if (fs7.existsSync(dest) && fs7.statSync(dest).size === f.bytes) {
641
904
  console.error(`\u5DF2\u5C31\u4F4D: ${dest}`);
642
905
  continue;
643
906
  }
@@ -657,8 +920,8 @@ function mirrorProvision() {
657
920
  console.error(`\u4E0B\u8F7D\u4E2D\u65AD\uFF0C5 \u79D2\u540E\u7EED\u4F20\uFF08\u7B2C ${attempt} \u8F6E\uFF09\u2026`);
658
921
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5e3);
659
922
  }
660
- fs5.renameSync(`${dest}.part`, dest);
661
- const got = fs5.statSync(dest).size;
923
+ fs7.renameSync(`${dest}.part`, dest);
924
+ const got = fs7.statSync(dest).size;
662
925
  if (expect && got !== expect) {
663
926
  console.error(`\u5927\u5C0F\u6821\u9A8C\u5931\u8D25: ${dest} got=${got} expect=${expect}`);
664
927
  process.exit(1);
@@ -669,11 +932,11 @@ function mirrorProvision() {
669
932
  function registerSetup(video) {
670
933
  video.command("setup").description("\u521B\u5EFA .venv-video \u5E76\u5B89\u88C5 mmh3turbo\uFF08MiniMax-H3 MLX \u5F15\u64CE\uFF0CApple Silicon\uFF09").option("--mirror", "\u7528 hf-mirror.com + curl \u9884\u7F6E H3 \u6743\u91CD\uFF08~50GB\uFF0C\u56FD\u5185\u63A8\u8350\uFF1Bhuggingface_hub \u5BA2\u6237\u7AEF\u4E0E\u955C\u50CF\u91CD\u5B9A\u5411\u4E0D\u517C\u5BB9\u65F6\u7684\u6B63\u89E3\uFF09").action((opts) => {
671
934
  const rt = resolveVideoRuntime();
672
- fs5.mkdirSync(rt.root, { recursive: true });
935
+ fs7.mkdirSync(rt.root, { recursive: true });
673
936
  if (!hasCommand("ffmpeg")) {
674
937
  console.error("\u26A0\uFE0F \u672A\u627E\u5230 ffmpeg\uFF08mp4 \u5C01\u88C5\u5FC5\u9700\uFF09\uFF1Abrew install ffmpeg");
675
938
  }
676
- if (!fs5.existsSync(rt.pythonVideo)) {
939
+ if (!fs7.existsSync(rt.pythonVideo)) {
677
940
  console.error(`\u521B\u5EFA venv: ${rt.venvVideo}`);
678
941
  if (hasCommand("uv")) runStep("uv", ["venv", "--python", "3.12", rt.venvVideo]);
679
942
  else runStep("python3", ["-m", "venv", rt.venvVideo]);
@@ -694,10 +957,21 @@ function registerSetup(video) {
694
957
  });
695
958
  }
696
959
  function registerGen2(video) {
697
- video.command("gen <prompt>").description("\u6587\u751F\u89C6\u9891/\u9996\u5E27\u56FE\u751F\u89C6\u9891\uFF1AMiniMax-H3 \u672C\u5730\uFF08\u9ED8\u8BA4 480p / 5s / 12 \u6B65\uFF0Cmp4 \u542B\u7ACB\u4F53\u58F0\u97F3\u8F68\uFF09").option("-o, --out <path>", "\u8F93\u51FA mp4 \u8DEF\u5F84", `lmedia-video-${Date.now()}.mp4`).option("-r, --res <preset>", `\u5206\u8FA8\u7387\u6863\uFF1A${RES_PRESETS.join("|")}\uFF08lmedia video list-res \u770B\u5B9E\u9645\u753B\u5E03\uFF09`, "480p").option("--seconds <s>", "\u7247\u6BB5\u65F6\u957F\u79D2\uFF081-15\uFF09", "5").option("--steps <n>", "\u53BB\u566A\u6B65\u6570\uFF0812 \u6B65\u4E0E 20 \u6B65\u89C6\u89C9\u4E0D\u53EF\u533A\u5206\uFF0C4/8 \u6B65\u660E\u663E\u6389\u8D28\uFF09", "12").option("--seed <n>", "\u968F\u673A\u79CD\u5B50", "42").option("--first-frame <path>", "\u9996\u5E27\u56FE\uFF08\u56FE\u751F\u89C6\u9891\uFF1B\u63A8\u8350\u8854\u63A5 lmedia image gen \u4EA7\u7269\uFF09").action(async (prompt, opts) => {
960
+ video.command("gen <prompt>").description("\u6587\u751F\u89C6\u9891/\u9996\u5E27\u56FE\u751F\u89C6\u9891\uFF1AMiniMax-H3 \u672C\u5730\uFF08\u9ED8\u8BA4 480p / 5s / 12 \u6B65\uFF0Cmp4 \u542B\u7ACB\u4F53\u58F0\u97F3\u8F68\uFF1B\u751F\u4EA7\u914D\u65B9\u89C1 lmedia video recipes\uFF09").option("-o, --out <path>", "\u8F93\u51FA mp4 \u8DEF\u5F84", `lmedia-video-${Date.now()}.mp4`).option("-r, --res <preset>", `\u5206\u8FA8\u7387\u6863\uFF1A${RES_PRESETS.join("|")}\uFF08lmedia video list-res \u770B\u5B9E\u9645\u753B\u5E03\uFF09`, "480p").option("--seconds <s>", "\u7247\u6BB5\u65F6\u957F\u79D2\uFF081-15\uFF09", "5").option("--steps <n>", `\u53BB\u566A\u6B65\u6570\uFF08\u57FA\u5EA7 12 \u6B65\u224820 \u6B65\uFF1B--fast \u9ED8\u8BA4\u53D6 bundle meta \u7684\u63A8\u8350\u6B65\u6570\uFF0C\u7F3A\u5931\u6309 ${FAST_STEPS}\uFF0C4-8 \u4E3A\u84B8\u998F\u6709\u6548\u533A\u95F4\uFF09`).option("--seed <n>", "\u968F\u673A\u79CD\u5B50", "42").option("--first-frame <path>", "\u9996\u5E27\u56FE\uFF08\u56FE\u751F\u89C6\u9891\uFF1B\u63A8\u8350\u8854\u63A5 lmedia image gen \u4EA7\u7269\uFF09").option("--last-frame <path>", "\u5C3E\u5E27\u56FE\uFF08\u9996\u5C3E\u951A\u5B9A\u63D2\u503C\uFF09").option("--fast", `Lightning \u84B8\u998F\u52A0\u901F\u6863\uFF08dit-turbo bundle + ${FAST_STEPS} \u6B65 + shift ${FAST_SHIFT_VIDEO}\uFF1B\u9996\u6B21\u5148 lmedia video turbo-merge\uFF09\u3002\u98CE\u666F/\u9759\u7269/\u7ED8\u672C\u7528\uFF1B\u4EBA\u7269\u89C6\u9891\u614E\u7528\uFF08\u6CB9\u753B\u611F+\u6F02\u79FB\uFF0C\u89C1 lmedia video recipes\uFF09`, false).action(async (prompt, opts) => {
961
+ const rt = resolveVideoRuntime();
962
+ const turboPrefix = path.join(rt.weightsDir, TURBO_BUNDLE);
963
+ const turboMeta = (() => {
964
+ if (!opts.fast) return void 0;
965
+ try {
966
+ return JSON.parse(fs7.readFileSync(`${turboPrefix}.idx`, "utf8"))["__meta__"];
967
+ } catch {
968
+ return void 0;
969
+ }
970
+ })();
698
971
  const res = opts.res;
699
972
  const seconds = parseFloat(opts.seconds);
700
- const steps = parseInt(opts.steps, 10);
973
+ const fast = !!opts.fast;
974
+ const steps = opts.steps ? parseInt(opts.steps, 10) : fast ? Number(turboMeta?.turbo_nfe) || FAST_STEPS : 12;
701
975
  const seed2 = parseInt(opts.seed, 10);
702
976
  if (!RES_PRESETS.includes(res)) {
703
977
  console.error(`--res \u4EC5\u652F\u6301 ${RES_PRESETS.join("|")}\uFF08\u5F53\u524D ${res}\uFF09`);
@@ -711,16 +985,35 @@ function registerGen2(video) {
711
985
  console.error(`--steps \u9700\u4E3A 1-50 \u7684\u6574\u6570\uFF08\u5F53\u524D ${opts.steps}\uFF09`);
712
986
  process.exit(2);
713
987
  }
988
+ if (fast && steps > 8) {
989
+ console.error(`\u63D0\u793A: --fast \u84B8\u998F\u6863\u6B65\u6570 >8 \u65E0\u6536\u76CA\u4E14\u53EF\u80FD\u8FC7\u9510\u5316\uFF08\u84B8\u998F\u914D\u65B9\u63A8\u8350 ${FAST_STEPS}\uFF0C4-8 \u6709\u6548\uFF09`);
990
+ }
714
991
  if (!Number.isInteger(seed2)) {
715
992
  console.error(`--seed \u9700\u4E3A\u6574\u6570\uFF08\u5F53\u524D ${opts.seed}\uFF09`);
716
993
  process.exit(2);
717
994
  }
718
- if (opts.firstFrame && !fs5.existsSync(opts.firstFrame)) {
719
- console.error(`\u9996\u5E27\u56FE\u4E0D\u5B58\u5728: ${opts.firstFrame}`);
720
- process.exit(2);
995
+ for (const [label, p] of [["\u9996\u5E27\u56FE", opts.firstFrame], ["\u5C3E\u5E27\u56FE", opts.lastFrame]]) {
996
+ if (p && !fs7.existsSync(p)) {
997
+ console.error(`${label}\u4E0D\u5B58\u5728: ${p}`);
998
+ process.exit(2);
999
+ }
721
1000
  }
722
- const rt = resolveVideoRuntime();
723
- if (!fs5.existsSync(rt.mmh3turbo)) {
1001
+ const hints = qualityHints(prompt, {
1002
+ fast,
1003
+ seconds,
1004
+ res,
1005
+ firstFrame: opts.firstFrame,
1006
+ lastFrame: opts.lastFrame
1007
+ });
1008
+ const turboReady = fs7.existsSync(`${turboPrefix}.bin`);
1009
+ if (fast && !opts.steps && turboReady && !turboMeta?.turbo_nfe) {
1010
+ console.error(`\u63D0\u793A: \u52A0\u901F\u6863 bundle \u672A\u8BB0\u5F55\u63A8\u8350\u6B65\u6570\uFF08turbo_nfe\uFF09\uFF0C\u672C\u6B21\u6309\u515C\u5E95 ${FAST_STEPS} \u6B65\u8DD1\u2014\u2014\u82E5\u5408\u5E76\u7684\u662F 4 \u6B65\u7248 LoRA\uFF0C\u8BF7\u663E\u5F0F --steps 4\uFF08\u6216 turbo-merge --nfe 4 \u91CD\u5199 meta\uFF09`);
1011
+ hints.push(`turbo bundle \u7F3A turbo_nfe\uFF0C\u6B65\u6570\u6309\u515C\u5E95 ${FAST_STEPS}\uFF1B4 \u6B65\u7248 LoRA \u9700\u663E\u5F0F --steps 4`);
1012
+ }
1013
+ if (/[一-鿿]/.test(prompt) && prompt.length < 20) {
1014
+ console.error(`\u63D0\u793A: \u4E2D\u6587 prompt \u5EFA\u8BAE 30-50 \u5B57\uFF08\u5F53\u524D ${prompt.length} \u5B57\uFF09\uFF0C\u592A\u77ED\u6613\u88AB seed \u4E3B\u5BFC\uFF08\u6362 prompt \u753B\u9762\u4E0D\u53D8\uFF09`);
1015
+ }
1016
+ if (!fs7.existsSync(rt.mmh3turbo)) {
724
1017
  console.error(`mmh3turbo \u672A\u5B89\u88C5\uFF08${rt.venvVideo}\uFF09\u3002\u5148\u8FD0\u884C: lmedia video setup`);
725
1018
  process.exit(1);
726
1019
  }
@@ -728,11 +1021,12 @@ function registerGen2(video) {
728
1021
  console.error("\u672A\u627E\u5230 ffmpeg\uFF08mp4 \u5C01\u88C5\u5FC5\u9700\uFF09\uFF1Abrew install ffmpeg \u540E\u91CD\u8BD5");
729
1022
  process.exit(1);
730
1023
  }
731
- if (/[一-鿿]/.test(prompt) && prompt.length < 20) {
732
- console.error(`\u63D0\u793A: \u4E2D\u6587 prompt \u5EFA\u8BAE 30-50 \u5B57\uFF08\u5F53\u524D ${prompt.length} \u5B57\uFF09\uFF0C\u592A\u77ED\u6613\u88AB seed \u4E3B\u5BFC\uFF08\u6362 prompt \u753B\u9762\u4E0D\u53D8\uFF09`);
1024
+ if (fast && !fs7.existsSync(`${turboPrefix}.bin`)) {
1025
+ console.error(`\u52A0\u901F\u6863 bundle \u672A\u5C31\u7EEA\uFF08${turboPrefix}.bin\uFF09\u3002\u5148\u8FD0\u884C: lmedia video turbo-merge`);
1026
+ process.exit(1);
733
1027
  }
734
1028
  const runDir = path.join(os.homedir(), ".lmedia", "video-runs", `${Date.now()}`);
735
- fs5.mkdirSync(runDir, { recursive: true });
1029
+ fs7.mkdirSync(runDir, { recursive: true });
736
1030
  const args = [
737
1031
  prompt,
738
1032
  "-r",
@@ -747,40 +1041,54 @@ function registerGen2(video) {
747
1041
  runDir
748
1042
  ];
749
1043
  if (opts.firstFrame) args.push("--first-frame", path.resolve(opts.firstFrame));
750
- console.error(`mmh3turbo \xB7 ${res} \xB7 ${seconds}s \xB7 ${steps} \u6B65 \xB7 seed ${seed2} \u2192 ${runDir}`);
1044
+ if (opts.lastFrame) args.push("--last-frame", path.resolve(opts.lastFrame));
1045
+ let engineCmd = rt.mmh3turbo;
1046
+ let engineArgs = args;
1047
+ if (fast) {
1048
+ engineCmd = rt.pythonVideo;
1049
+ engineArgs = [
1050
+ path.join(rt.pythonDir, "video_gen.py"),
1051
+ "--shift-video",
1052
+ String(FAST_SHIFT_VIDEO),
1053
+ "--weights",
1054
+ turboPrefix,
1055
+ ...args
1056
+ ];
1057
+ }
1058
+ console.error(`mmh3turbo${fast ? "(turbo)" : ""} \xB7 ${res} \xB7 ${seconds}s \xB7 ${steps} \u6B65${fast ? ` \xB7 shift ${FAST_SHIFT_VIDEO}` : ""} \xB7 seed ${seed2} \u2192 ${runDir}`);
751
1059
  const t0 = Date.now();
752
1060
  const env = { ...process.env };
753
- const weightsReady = fs5.existsSync(path.join(rt.weightsDir, "dit.bin"));
1061
+ const weightsReady = fs7.existsSync(path.join(rt.weightsDir, "dit.bin")) || fast && fs7.existsSync(`${turboPrefix}.bin`);
754
1062
  if (!weightsReady && env.HF_HUB_OFFLINE === "1") {
755
1063
  console.error("\u63D0\u793A: \u68C0\u6D4B\u5230 HF_HUB_OFFLINE=1 \u4E14 H3 \u6743\u91CD\u672A\u5C31\u7EEA\uFF0C\u672C\u6B21\u4E34\u65F6\u5173\u95ED\u4EE5\u62C9\u53D6\u6743\u91CD\uFF08\u56FD\u5185\u63A8\u8350 lmedia video setup --mirror\uFF09");
756
1064
  env.HF_HUB_OFFLINE = "0";
757
1065
  }
758
- const code = await new Promise((resolve7) => {
759
- const child = spawn(rt.mmh3turbo, args, { env });
1066
+ const code = await new Promise((resolve8) => {
1067
+ const child = spawn(engineCmd, engineArgs, { env });
760
1068
  child.stdout.on("data", (d) => process.stderr.write(d));
761
1069
  child.stderr.on("data", (d) => process.stderr.write(d));
762
1070
  child.on("error", (e) => {
763
- console.error(`\u542F\u52A8 mmh3turbo \u5931\u8D25: ${e.message}`);
1071
+ console.error(`\u542F\u52A8\u5F15\u64CE\u5931\u8D25: ${e.message}`);
764
1072
  process.exit(1);
765
1073
  });
766
- child.on("close", (c) => resolve7(c ?? -1));
1074
+ child.on("close", (c) => resolve8(c ?? -1));
767
1075
  });
768
1076
  if (code !== 0) {
769
- console.error(`mmh3turbo \u9000\u51FA\u7801 ${code}\uFF08\u8FD0\u884C\u76EE\u5F55\u4FDD\u7559\u4F9B\u6392\u67E5: ${runDir}\uFF1B\u5E38\u89C1\u539F\u56E0: \u78C1\u76D8\u7A7A\u95F4\u4E0D\u8DB3\u3001\u6743\u91CD\u4E0B\u8F7D\u4E2D\u65AD\u2014\u2014\u91CD\u8DD1\u5373\u53EF\u7EED\u4F20\uFF09`);
1077
+ console.error(`\u5F15\u64CE\u9000\u51FA\u7801 ${code}\uFF08\u8FD0\u884C\u76EE\u5F55\u4FDD\u7559\u4F9B\u6392\u67E5: ${runDir}\uFF1B\u5E38\u89C1\u539F\u56E0: \u78C1\u76D8\u7A7A\u95F4\u4E0D\u8DB3\u3001\u6743\u91CD\u4E0B\u8F7D\u4E2D\u65AD\u2014\u2014\u91CD\u8DD1\u5373\u53EF\u7EED\u4F20\uFF09`);
770
1078
  process.exit(1);
771
1079
  }
772
1080
  const produced = path.join(runDir, "video.mp4");
773
- if (!fs5.existsSync(produced)) {
1081
+ if (!fs7.existsSync(produced)) {
774
1082
  console.error(`\u672A\u627E\u5230\u4EA7\u7269 ${produced}\uFF08\u8FD0\u884C\u76EE\u5F55\u5185\u5BB9\u4FDD\u7559\u4F9B\u6392\u67E5: ${runDir}\uFF09`);
775
1083
  process.exit(1);
776
1084
  }
777
1085
  const out = path.resolve(opts.out);
778
- fs5.mkdirSync(path.dirname(out), { recursive: true });
1086
+ fs7.mkdirSync(path.dirname(out), { recursive: true });
779
1087
  try {
780
- fs5.renameSync(produced, out);
1088
+ fs7.renameSync(produced, out);
781
1089
  } catch {
782
- fs5.copyFileSync(produced, out);
783
- fs5.unlinkSync(produced);
1090
+ fs7.copyFileSync(produced, out);
1091
+ fs7.unlinkSync(produced);
784
1092
  }
785
1093
  console.log(
786
1094
  JSON.stringify(
@@ -788,12 +1096,15 @@ function registerGen2(video) {
788
1096
  out,
789
1097
  seconds: Number(((Date.now() - t0) / 1e3).toFixed(1)),
790
1098
  runDir,
791
- engine: "mmh3turbo",
1099
+ engine: fast ? "mmh3turbo+turbo-lora" : "mmh3turbo",
792
1100
  res,
793
1101
  duration: seconds,
794
1102
  steps,
795
1103
  seed: seed2,
796
- ...opts.firstFrame ? { firstFrame: path.resolve(opts.firstFrame) } : {}
1104
+ ...fast ? { fast: true, shiftVideo: FAST_SHIFT_VIDEO, ...turboMeta?.turbo_lora ? { lora: turboMeta.turbo_lora } : {} } : {},
1105
+ ...hints.length ? { hints } : {},
1106
+ ...opts.firstFrame ? { firstFrame: path.resolve(opts.firstFrame) } : {},
1107
+ ...opts.lastFrame ? { lastFrame: path.resolve(opts.lastFrame) } : {}
797
1108
  },
798
1109
  null,
799
1110
  2
@@ -801,10 +1112,99 @@ function registerGen2(video) {
801
1112
  );
802
1113
  });
803
1114
  }
1115
+ function registerTurboMerge(video) {
1116
+ video.command("turbo-merge").description("\u628A few-step \u84B8\u998F LoRA \u5408\u5E76\u8FDB int8 DiT bundle\uFF08--fast \u52A0\u901F\u6863\u7684\u4E00\u6B21\u6027\u51C6\u5907\uFF0C~20GB \u8BFB\u5199\uFF09").option("--lora <path>", "\u84B8\u998F LoRA safetensors\uFF08lightx2v \u547D\u540D\uFF1B\u7F3A\u5931\u65F6\u81EA\u52A8\u4ECE\u955C\u50CF\u4E0B\u8F7D\uFF09").option("--bundle <prefix>", "\u6E90 int8 bundle \u524D\u7F00\uFF08dit.bin/.idx\uFF09").option("--out <prefix>", "\u8F93\u51FA bundle \u524D\u7F00").option("--scale <n>", "LoRA \u5F3A\u5EA6\uFF08\u84B8\u998F\u914D\u65B9\u56FA\u5B9A 1.0\uFF0C\u52FF\u8C03\uFF09", "1.0").option("--nfe <n>", "\u63A8\u8350\u53BB\u566A\u6B65\u6570\uFF08\u5199\u5165 bundle meta\uFF0C--fast \u9ED8\u8BA4\u6B65\u6570\uFF1B8 \u6B65\u7248=8\uFF09").option("--no-download", "LoRA \u7F3A\u5931\u65F6\u4E0D\u81EA\u52A8\u4E0B\u8F7D\uFF0C\u76F4\u63A5\u62A5\u9519").action((opts) => {
1117
+ const rt = resolveVideoRuntime();
1118
+ const bundle = opts.bundle ?? path.join(rt.weightsDir, "dit");
1119
+ const out = opts.out ?? path.join(rt.weightsDir, TURBO_BUNDLE);
1120
+ const loraDir = path.join(rt.weightsDir, "loras");
1121
+ const lora = opts.lora ?? path.join(loraDir, TURBO_LORA_FILE);
1122
+ if (!fs7.existsSync(`${bundle}.bin`) || !fs7.existsSync(`${bundle}.idx`)) {
1123
+ console.error(`\u6E90 int8 bundle \u4E0D\u5B58\u5728: ${bundle}.bin/.idx\u3002\u5148\u8FD0\u884C: lmedia video setup --mirror`);
1124
+ process.exit(1);
1125
+ }
1126
+ if (!fs7.existsSync(rt.pythonVideo)) {
1127
+ console.error(`\u89C6\u9891 venv \u4E0D\u5B58\u5728: ${rt.venvVideo}\u3002\u5148\u8FD0\u884C: lmedia video setup`);
1128
+ process.exit(1);
1129
+ }
1130
+ if (!fs7.existsSync(lora)) {
1131
+ if (!opts.download) {
1132
+ console.error(`LoRA \u4E0D\u5B58\u5728\u4E14\u5DF2\u7981\u7528\u81EA\u52A8\u4E0B\u8F7D: ${lora}`);
1133
+ process.exit(1);
1134
+ }
1135
+ fs7.mkdirSync(loraDir, { recursive: true });
1136
+ console.error(`\u4E0B\u8F7D\u84B8\u998F LoRA\uFF08~1.4GB\uFF09\u2192 ${lora}`);
1137
+ const r2 = spawnSync("curl", ["-L", "-C", "-", "--retry", "8", "--retry-delay", "5", "-o", `${lora}.part`, TURBO_LORA_URL], { stdio: "inherit" });
1138
+ if (r2.status !== 0 || !fs7.existsSync(`${lora}.part`) || fs7.statSync(`${lora}.part`).size < 1e8) {
1139
+ console.error(`LoRA \u4E0B\u8F7D\u5931\u8D25\uFF08\u53EF\u624B\u52A8\u4E0B\u8F7D\u540E\u653E\u81F3 ${lora}\uFF09: ${TURBO_LORA_URL}`);
1140
+ process.exit(1);
1141
+ }
1142
+ fs7.renameSync(`${lora}.part`, lora);
1143
+ }
1144
+ console.error(`\u5408\u5E76 ${path.basename(lora)} \u2192 ${out}.bin/.idx\uFF08fp32 \u7CBE\u5EA6\u52A0 delta \u540E\u6309\u884C\u91CD\u91CF\u5316\uFF09`);
1145
+ const mergeArgs = [
1146
+ path.join(rt.pythonDir, "turbo_merge.py"),
1147
+ "--lora",
1148
+ lora,
1149
+ "--bundle",
1150
+ bundle,
1151
+ "--out",
1152
+ out,
1153
+ "--scale",
1154
+ opts.scale
1155
+ ];
1156
+ if (opts.nfe) mergeArgs.push("--nfe", opts.nfe);
1157
+ const r = spawnSync(rt.pythonVideo, mergeArgs, { stdio: "inherit" });
1158
+ if (r.status !== 0) {
1159
+ console.error(`turbo_merge \u9000\u51FA\u7801 ${r.status}`);
1160
+ process.exit(r.status ?? 1);
1161
+ }
1162
+ console.log(`\u5C31\u7EEA: lmedia video gen --fast ... \uFF08bundle: ${out}\uFF09`);
1163
+ });
1164
+ }
1165
+ function registerRecipes(video) {
1166
+ video.command("recipes").description("\u6253\u5370\u5B9E\u6D4B\u9A8C\u6536\u8FC7\u7684\u751F\u4EA7\u914D\u65B9\uFF08\u4EBA\u7269\u5FAE\u52A8/\u98CE\u666F\u7ED8\u672C/\u53CC\u951A\u5B9A\u4E00\u81F4\u6027\uFF09\uFF0C\u590D\u5236\u5373\u7528").action(() => {
1167
+ console.log(`lmedia video \u751F\u4EA7\u914D\u65B9\uFF08M4 Max \u5B9E\u6D4B\u9A8C\u6536\uFF0C2026-09\uFF09
1168
+
1169
+ \u2460 \u4EBA\u7269\u5FAE\u52A8\uFF08\u8D28\u91CF\u4F18\u5148\u2014\u2014\u4EBA\u7269\u89C6\u9891\u7684\u9ED8\u8BA4\u9009\u62E9\uFF09
1170
+ \u8981\u70B9: \u57FA\u5EA7 12 \u6B65\uFF08\u4E0D\u7528 --fast\uFF0C\u84B8\u998F\u6863\u4EBA\u8138\u4F1A\u6CB9\u753B\u5316\uFF09+ \u4E3B\u4F53\u7279\u5199\u6784\u56FE
1171
+ \uFF08\u5148\u88C1\u526A\uFF1A\u8BA9\u4EBA\u8138\u5360\u753B\u9762 1/4 \u4EE5\u4E0A\uFF0C\u8138\u8D8A\u5927\u50CF\u7D20\u8D8A\u8DB3\uFF09+ \u52A8\u4F5C\u6536\u655B\u7684 prompt
1172
+ + \u65F6\u957F \u22645s\uFF08\u6F02\u79FB\u968F\u65F6\u957F\u7D2F\u79EF\uFF09
1173
+
1174
+ # \u4ECE\u5927\u56FE\u88C1\u4EBA\u7269\u7279\u5199\uFF08\u793A\u4F8B\uFF1A\u4ECE 1808\xB2 \u6E90\u56FE\u53D6\u4E3B\u4F53\u65B9\u6846\uFF09
1175
+ ffmpeg -i photo.jpg -vf "crop=1150:1150:X:Y" closeup.jpg
1176
+ lmedia video gen "\u4EBA\u7269+\u8F7B\u5FAE\u52A8\u4F5C\u63CF\u8FF0\uFF0C\u52A8\u4F5C\u8F7B\u67D4\u3002Audio: ambience, no talking, no voices" \\
1177
+ -r square --seconds 4 --seed 11 --first-frame closeup.jpg -o clip.mp4
1178
+ # ~23min/\u6761\uFF08M4 Max square 768\xB2 4s\uFF09
1179
+
1180
+ \u2461 \u98CE\u666F/\u9759\u7269/\u7ED8\u672C\u5FAE\u52A8\uFF08\u5FEB\uFF09
1181
+ \u8981\u70B9: --fast \u84B8\u998F\u6863\uFF088 \u6B65 shift 6\uFF0C\u9996\u6B21\u5148 lmedia video turbo-merge\uFF09\uFF1B
1182
+ \u84B8\u998F 768p \u7CFB\u5206\u8FA8\u7387\u7EAA\u5F8B\uFF1A\u77ED\u8FB9 \u2265700\uFF0C352p/480p \u4EC5\u94FE\u8DEF\u9A8C\u8BC1
1183
+
1184
+ lmedia video gen "\u6C34\u5F69\u7ED8\u672C\u98CE\u683C\uFF1A\u5C0F\u871C\u8702\u7537\u5B69\u5728\u82B1\u56ED\u91CC\u6325\u624B\uFF0C\u82B1\u74E3\u98D8\u843D\uFF0C\u7EC6\u8282\u4E30\u5BCC" \\
1185
+ --fast -r square --seconds 4 --first-frame page.png -o clip.mp4
1186
+ # ~17min/\u6761\uFF1B\u4EBA\u7269\u614E\u7528\uFF08\u6CB9\u753B\u611F+\u6F02\u79FB\uFF09
1187
+
1188
+ \u2462 \u4EBA\u7269\u4E00\u81F4\u6027\uFF1A\u53CC\u951A\u5B9A\uFF08\u9996\u5C3E\u540C\u56FE\u56DE\u73AF\uFF09
1189
+ \u8981\u70B9: \u56FE\u751F\u89C6\u9891\u53EA\u951A\u9996\u5E27\u65F6\u7ED3\u5C3E\u8EAB\u4EFD\u5FC5\u6F02\uFF08\u5B9E\u6D4B\u4EBA\u8138\u76F8\u4F3C\u5EA6\u8DCC\u5230 0.3\uFF09\uFF1B
1190
+ \u540C\u4E00\u5F20\u56FE\u540C\u65F6\u5582 --first-frame \u548C --last-frame\uFF0C\u52A8\u4F5C\u300C\u8361\u51FA\u53BB\u518D\u8361\u56DE\u6765\u300D\uFF0C
1191
+ ArcFace \u5B9E\u6D4B\u7ED3\u5C3E\u76F8\u4F3C\u5EA6\u62C9\u56DE 0.97\u3002\u8D70\u8DEF\u4F4D\u79FB\u7C7B\u6784\u56FE\u4E0D\u9002\u7528\uFF08\u4F1A\u903C\u51FA\u56DE\u9000\u52A8\u4F5C\uFF09
1192
+
1193
+ lmedia video gen "\u4EBA\u7269+\u8F7B\u5FAE\u52A8\u4F5C\u63CF\u8FF0\uFF0C\u52A8\u4F5C\u8F7B\u67D4\u51E0\u4E4E\u4FDD\u6301\u539F\u59FF\u3002Audio: ..." \\
1194
+ -r square --seconds 4 --seed 11 \\
1195
+ --first-frame closeup.jpg --last-frame closeup.jpg -o clip.mp4
1196
+
1197
+ \u901A\u7528\u7EAA\u5F8B\uFF1A
1198
+ - \u6587\u751F\u89C6\u9891\u4E2D\u6587 prompt 30-50 \u5B57\uFF0C\u5199\u6E05\u52A8\u4F5C\u4E0E\u955C\u5934\uFF0CAudio: \u6BB5\u5199\u73AF\u5883\u97F3\u5E76\u52A0 no talking
1199
+ - \u57FA\u5EA7\u9ED8\u8BA4 12 \u6B65\u5373 20 \u6B65\u89C2\u611F\uFF1B\u4E0D\u8981\u4F4E\u4E8E 8 \u6B65\uFF08\u6389\u8D28\u660E\u663E\uFF09
1200
+ - \u9996\u5E27\u7167\u7247\u4F18\u9009\uFF1A\u6B63\u8138\u3001\u6E05\u6670\u3001\u4EBA\u8138\u5360\u6BD4\u5927\u2014\u2014\u9996\u5E27\u5C31\u662F\u8EAB\u4EFD\u7684\u57FA\u56E0
1201
+ - \u751F\u6210\u7ED3\u679C\u673A\u8BFB hints \u5B57\u6BB5\u4F1A\u5E26\u573A\u666F\u8D28\u91CF\u5EFA\u8BAE\uFF1B\u73AF\u5883\u81EA\u68C0 lmedia doctor`);
1202
+ });
1203
+ }
804
1204
  function registerListRes(video) {
805
1205
  video.command("list-res").description("\u5217\u51FA mmh3turbo \u652F\u6301\u7684\u5206\u8FA8\u7387\u6863\u4F4D\uFF08\u900F\u4F20\uFF09").action(() => {
806
1206
  const rt = resolveVideoRuntime();
807
- if (!fs5.existsSync(rt.mmh3turbo)) {
1207
+ if (!fs7.existsSync(rt.mmh3turbo)) {
808
1208
  console.error("mmh3turbo \u672A\u5B89\u88C5\u3002\u5148\u8FD0\u884C: lmedia video setup");
809
1209
  process.exit(1);
810
1210
  }
@@ -817,16 +1217,18 @@ function registerListRes(video) {
817
1217
  });
818
1218
  }
819
1219
  function registerVideo(program2) {
820
- const video = program2.command("video").description("\u89C6\u9891\u751F\u4EA7\u80FD\u529B\uFF08\u672C\u5730 MiniMax-H3 / mmh3turbo\uFF0C\u96F6 API \u6210\u672C\uFF09");
1220
+ const video = program2.command("video").description("\u89C6\u9891\u751F\u4EA7\u80FD\u529B\uFF08\u672C\u5730 MiniMax-H3 / mmh3turbo\uFF0C\u96F6 API \u6210\u672C\uFF1B--fast Lightning \u84B8\u998F\u52A0\u901F\u6863\uFF1B\u751F\u4EA7\u914D\u65B9 lmedia video recipes\uFF09");
821
1221
  registerSetup(video);
822
1222
  registerGen2(video);
1223
+ registerTurboMerge(video);
1224
+ registerRecipes(video);
823
1225
  registerListRes(video);
824
1226
  }
825
1227
  function registerLora(program2) {
826
1228
  const lora = program2.command("lora").description("LoRA \u6CE8\u518C\u8868\u7BA1\u7406");
827
1229
  lora.command("list").description("\u5217\u51FA\u5DF2\u6CE8\u518C LoRA").action(() => {
828
1230
  for (const l of loadRegistry()) {
829
- const exists = fs5.existsSync(l.path) ? "\u2713" : "\u2717 \u6587\u4EF6\u7F3A\u5931";
1231
+ const exists = fs7.existsSync(l.path) ? "\u2713" : "\u2717 \u6587\u4EF6\u7F3A\u5931";
830
1232
  console.log(
831
1233
  `${l.name} [${l.kind}] weight=${l.defaultWeight} trigger="${l.trigger}" ${exists}
832
1234
  ${l.path}${l.note ? `
@@ -848,15 +1250,6 @@ function registerLora(program2) {
848
1250
  }
849
1251
  );
850
1252
  }
851
- var ESRGAN_CACHE_PATH = path.join(os.homedir(), ".cache", "lmedia", "RealESRGAN_x2.pth");
852
- var LEGACY_TMP_PATH = "/tmp/RealESRGAN_x2.pth";
853
- function resolveEsrganPath() {
854
- const env = process.env.LMEDIA_REALESRGAN_PATH;
855
- if (env && fs5.existsSync(env)) return env;
856
- if (fs5.existsSync(ESRGAN_CACHE_PATH)) return ESRGAN_CACHE_PATH;
857
- if (fs5.existsSync(LEGACY_TMP_PATH)) return LEGACY_TMP_PATH;
858
- return process.env.LMEDIA_REALESRGAN_PATH ?? ESRGAN_CACHE_PATH;
859
- }
860
1253
  var ManifestCorruptError = class extends Error {
861
1254
  };
862
1255
  var InvalidArgsError = class extends Error {
@@ -881,8 +1274,8 @@ function emptyManifest() {
881
1274
  }
882
1275
  function readManifest(root) {
883
1276
  const p = manifestPath(root);
884
- if (!fs5.existsSync(p)) return emptyManifest();
885
- const raw = fs5.readFileSync(p, "utf-8");
1277
+ if (!fs7.existsSync(p)) return emptyManifest();
1278
+ const raw = fs7.readFileSync(p, "utf-8");
886
1279
  let parsed;
887
1280
  try {
888
1281
  parsed = JSON.parse(raw);
@@ -902,17 +1295,17 @@ function readManifest(root) {
902
1295
  return { version: 1, generatedAt: m.generatedAt ?? "", items: m.items };
903
1296
  }
904
1297
  function writeManifest(root, manifest) {
905
- fs5.mkdirSync(root, { recursive: true });
1298
+ fs7.mkdirSync(root, { recursive: true });
906
1299
  const p = manifestPath(root);
907
1300
  const tmp = `${p}.${process.pid}.tmp`;
908
- fs5.writeFileSync(tmp, `${JSON.stringify(manifest, null, 1)}
1301
+ fs7.writeFileSync(tmp, `${JSON.stringify(manifest, null, 1)}
909
1302
  `, "utf-8");
910
- fs5.renameSync(tmp, p);
1303
+ fs7.renameSync(tmp, p);
911
1304
  }
912
1305
  function initLibrary(root) {
913
- fs5.mkdirSync(path.join(root, "sfx"), { recursive: true });
914
- fs5.mkdirSync(path.join(root, "ambient"), { recursive: true });
915
- const created = !fs5.existsSync(manifestPath(root));
1306
+ fs7.mkdirSync(path.join(root, "sfx"), { recursive: true });
1307
+ fs7.mkdirSync(path.join(root, "ambient"), { recursive: true });
1308
+ const created = !fs7.existsSync(manifestPath(root));
916
1309
  if (created) writeManifest(root, emptyManifest());
917
1310
  return { root: path.resolve(root), manifest: manifestPath(root), created };
918
1311
  }
@@ -922,7 +1315,7 @@ function listEntries(root, filter = {}) {
922
1315
  );
923
1316
  }
924
1317
  function sha256File(file) {
925
- return createHash("sha256").update(fs5.readFileSync(file)).digest("hex");
1318
+ return createHash("sha256").update(fs7.readFileSync(file)).digest("hex");
926
1319
  }
927
1320
  function assertFfmpeg() {
928
1321
  for (const cmd of ["ffmpeg", "ffprobe"]) {
@@ -971,7 +1364,7 @@ function addEntry(root, opts) {
971
1364
  if (!KEY_RE.test(opts.key)) throw new InvalidArgsError(`key \u4EC5\u5141\u8BB8\u5C0F\u5199\u5B57\u6BCD/\u6570\u5B57/\u77ED\u6A2A\u7EBF: ${opts.key}`);
972
1365
  const type = opts.type ?? "sfx";
973
1366
  if (type !== "sfx" && type !== "ambient") throw new InvalidArgsError(`type \u4EC5\u652F\u6301 sfx|ambient: ${type}`);
974
- if (!fs5.existsSync(opts.file)) throw new InvalidArgsError(`\u8F93\u5165\u97F3\u9891\u4E0D\u5B58\u5728: ${opts.file}`);
1367
+ if (!fs7.existsSync(opts.file)) throw new InvalidArgsError(`\u8F93\u5165\u97F3\u9891\u4E0D\u5B58\u5728: ${opts.file}`);
975
1368
  const hash = sha256File(opts.file);
976
1369
  const dup = readManifest(root).items.find((i) => i.content_hash === hash);
977
1370
  if (dup) return { item: dup, duplicated: true };
@@ -983,7 +1376,7 @@ function addEntry(root, opts) {
983
1376
  try {
984
1377
  probe = probeAudio(dst);
985
1378
  } catch (e) {
986
- fs5.rmSync(dst, { force: true });
1379
+ fs7.rmSync(dst, { force: true });
987
1380
  throw e;
988
1381
  }
989
1382
  const item = {
@@ -1020,7 +1413,7 @@ function verifyLibrary(root) {
1020
1413
  const problems = [];
1021
1414
  for (const item of items) {
1022
1415
  const issues = [];
1023
- if (!fs5.existsSync(item.path)) {
1416
+ if (!fs7.existsSync(item.path)) {
1024
1417
  issues.push(`\u6587\u4EF6\u7F3A\u5931: ${item.path}`);
1025
1418
  } else {
1026
1419
  const dur = probeAudioDurationSync(item.path);
@@ -1077,7 +1470,7 @@ function assertMutuallyExclusive(a, b, nameA, nameB) {
1077
1470
  }
1078
1471
  }
1079
1472
  function assertFilesExist(files, flagName = "\u8F93\u5165\u6587\u4EF6") {
1080
- const missing = files.filter((f) => !fs5.existsSync(f));
1473
+ const missing = files.filter((f) => !fs7.existsSync(f));
1081
1474
  if (missing.length) {
1082
1475
  throw new InvalidArgsError(`${flagName}\u4E0D\u5B58\u5728: ${missing.join(", ")}`);
1083
1476
  }
@@ -1154,7 +1547,7 @@ function fail(e) {
1154
1547
  }
1155
1548
  function requireAudioRuntime() {
1156
1549
  const rt = resolveAudioRuntime();
1157
- if (!fs5.existsSync(rt.pythonAudio)) {
1550
+ if (!fs7.existsSync(rt.pythonAudio)) {
1158
1551
  throw new SfxEnvError(
1159
1552
  `\u97F3\u6548 venv \u672A\u627E\u5230: ${rt.pythonAudio}
1160
1553
  \u4FEE\u590D: lmedia sfx setup\uFF08\u6216 cd ${rt.root} && uv venv .venv-audio --python 3.11 && uv pip install -p .venv-audio/bin/python torch torchaudio "transformers<5" einops soundfile\uFF09`
@@ -1184,7 +1577,7 @@ function registerGen3(sfx) {
1184
1577
  const rt = requireAudioRuntime();
1185
1578
  requireFfmpeg();
1186
1579
  const out = path.resolve(opts.out ?? "sfx.wav");
1187
- fs5.mkdirSync(path.dirname(out), { recursive: true });
1580
+ fs7.mkdirSync(path.dirname(out), { recursive: true });
1188
1581
  console.error(`\u{1F3AC} sfx gen\uFF08${rolls} \u63B7 + \u8D28\u91CF\u95E8 + \u526A\u88C1\uFF09\u2026`);
1189
1582
  const res = await runSfxOp(rt, {
1190
1583
  prompt,
@@ -1206,11 +1599,11 @@ function registerBatch(sfx) {
1206
1599
  sfx.command("batch").description("\u6E05\u5355\u9A71\u52A8\u6279\u91CF\u751F\u6210\uFF1A\u9010 key \u591A\u63B7 + \u8D28\u91CF\u95E8\u9009\u4F18 \u2192 <dir>/<key>.best.wav + report.json").requiredOption("-m, --manifest <file>", "\u6279\u6E05\u5355 JSON \u8DEF\u5F84\uFF08- \u8BFB stdin\uFF09").requiredOption("-o, --out <dir>", "\u8F93\u51FA\u76EE\u5F55").option("--rolls <n>", "\u6BCF key \u57FA\u7840\u751F\u6210\u6B21\u6570\uFF081-10\uFF0C\u5168\u5E9F\u81EA\u52A8\u52A0\u63B7\u22642\uFF09", "3").option("--keep-rolls", "\u4FDD\u7559\u5168\u90E8\u63B7\u6B21\u5230\u8F93\u51FA\u76EE\u5F55\uFF08<key>.r<N>.wav\uFF09").option("--full", "\u8DF3\u8FC7\u526A\u88C1\uFF0C\u4FDD\u7559 10s \u539F\u59CB\u751F\u6210").option("--keys <list>", "\u53EA\u8DD1\u5B50\u96C6\uFF0C\u9017\u53F7\u5206\u9694\uFF08\u5982 bear,frog\uFF09").action(async (opts) => {
1207
1600
  try {
1208
1601
  const rolls = parseRolls(opts.rolls);
1209
- const text = opts.manifest === "-" ? fs5.readFileSync(0, "utf-8") : (() => {
1210
- if (!fs5.existsSync(opts.manifest)) {
1602
+ const text = opts.manifest === "-" ? fs7.readFileSync(0, "utf-8") : (() => {
1603
+ if (!fs7.existsSync(opts.manifest)) {
1211
1604
  throw new InvalidArgsError(`\u6279\u6E05\u5355\u4E0D\u5B58\u5728: ${opts.manifest}`);
1212
1605
  }
1213
- return fs5.readFileSync(opts.manifest, "utf-8");
1606
+ return fs7.readFileSync(opts.manifest, "utf-8");
1214
1607
  })();
1215
1608
  const items = parseBatchManifest(
1216
1609
  text,
@@ -1219,7 +1612,7 @@ function registerBatch(sfx) {
1219
1612
  const rt = requireAudioRuntime();
1220
1613
  requireFfmpeg();
1221
1614
  const outDir = path.resolve(opts.out);
1222
- fs5.mkdirSync(outDir, { recursive: true });
1615
+ fs7.mkdirSync(outDir, { recursive: true });
1223
1616
  console.error(`\u{1F3AC} sfx batch\uFF08${items.length} key \xD7 ${rolls} \u63B7\uFF09\u2192 ${outDir}`);
1224
1617
  const res = await runSfxOp(rt, {
1225
1618
  op: "batch",
@@ -1349,13 +1742,13 @@ function registerAccept(sfx) {
1349
1742
  function registerAb(sfx) {
1350
1743
  sfx.command("ab").description("\u751F\u6210 A/B \u8BD5\u542C\u9875\uFF08\u72EC\u7ACB html\uFF0C\u5019\u9009\u97F3\u9891\u62F7\u8D1D\u5230 ab_files/ \u76F8\u5BF9\u8DEF\u5F84\u5F15\u7528\uFF09").requiredOption("-m, --groups <file>", 'groups JSON\uFF1A[{"name":"...","candidates":["a.wav","b.wav"]}]').requiredOption("-o, --out <file>", "\u8F93\u51FA html \u8DEF\u5F84").action(async (opts) => {
1351
1744
  try {
1352
- if (!fs5.existsSync(opts.groups)) throw new InvalidArgsError(`groups \u6587\u4EF6\u4E0D\u5B58\u5728: ${opts.groups}`);
1353
- const groups = parseAbGroups(fs5.readFileSync(opts.groups, "utf-8"));
1745
+ if (!fs7.existsSync(opts.groups)) throw new InvalidArgsError(`groups \u6587\u4EF6\u4E0D\u5B58\u5728: ${opts.groups}`);
1746
+ const groups = parseAbGroups(fs7.readFileSync(opts.groups, "utf-8"));
1354
1747
  const candidates = groups.flatMap((g) => g.candidates);
1355
1748
  assertFilesExist(candidates, "\u5019\u9009\u97F3\u9891");
1356
1749
  const rt = requireAudioRuntime();
1357
1750
  const out = path.resolve(opts.out);
1358
- fs5.mkdirSync(path.dirname(out), { recursive: true });
1751
+ fs7.mkdirSync(path.dirname(out), { recursive: true });
1359
1752
  const res = await runSfxOp(rt, { op: "abpage", groups, out });
1360
1753
  console.error(`\xB7 \u5019\u9009\u97F3\u9891\u5DF2\u62F7\u8D1D\u5230 ${path.join(path.dirname(out), "ab_files")}`);
1361
1754
  console.log(`\u2713 ${res.out}\uFF08${res.groups} \u7EC4 / ${res.candidates} \u5019\u9009\uFF09`);
@@ -1372,7 +1765,7 @@ function registerSetup2(sfx) {
1372
1765
  throw new SfxEnvError("\u7F3A\u5C11 uv\uFF08curl -LsSf https://astral.sh/uv/install.sh | sh\uFF09\uFF0Csfx setup \u4F9D\u8D56");
1373
1766
  }
1374
1767
  const deps = ["torch", "torchaudio", "transformers<5", "einops", "soundfile"];
1375
- if (fs5.existsSync(rt.pythonAudio)) {
1768
+ if (fs7.existsSync(rt.pythonAudio)) {
1376
1769
  const ok = spawnSync(rt.pythonAudio, ["-c", "import torch, torchaudio, transformers, einops, soundfile"], { stdio: "ignore" }).status === 0;
1377
1770
  if (ok) {
1378
1771
  console.error(`\u2713 \u97F3\u6548 venv \u5DF2\u5C31\u7EEA: ${rt.pythonAudio}`);
@@ -1400,7 +1793,7 @@ function registerSfxDoctor(sfx) {
1400
1793
  const checks = [
1401
1794
  [
1402
1795
  "\u97F3\u6548 venv",
1403
- fs5.existsSync(rt.pythonAudio),
1796
+ fs7.existsSync(rt.pythonAudio),
1404
1797
  `\u4FEE\u590D: lmedia sfx setup\uFF08\u5EFA ${rt.root}/.venv-audio\uFF09`
1405
1798
  ],
1406
1799
  [
@@ -1529,7 +1922,7 @@ function registerSfx(program2) {
1529
1922
  function sfxDoctorChecks() {
1530
1923
  const rt = resolveAudioRuntime();
1531
1924
  return [
1532
- [`[sfx] .venv-audio\uFF08Dasheng-AudioGen\uFF09`, fs5.existsSync(rt.pythonAudio)],
1925
+ [`[sfx] .venv-audio\uFF08Dasheng-AudioGen\uFF09`, fs7.existsSync(rt.pythonAudio)],
1533
1926
  [`[sfx] \u4E3B\u6A21\u578B\u7F13\u5B58 ${MODEL}`, !!hfSnapshot(MODEL)],
1534
1927
  [`[sfx] \u5206\u8BCD\u5668\u7F13\u5B58 ${TOKENIZER}`, !!hfSnapshot(TOKENIZER)]
1535
1928
  ];
@@ -1537,37 +1930,43 @@ function sfxDoctorChecks() {
1537
1930
 
1538
1931
  // src/commands/doctor.ts
1539
1932
  function registerDoctor(program2) {
1540
- program2.command("doctor").description("\u73AF\u5883\u81EA\u68C0\uFF1A\u8FD0\u884C\u65F6/\u6A21\u578B\u5FEB\u7167/LoRA \u6587\u4EF6\u5B8C\u6574\u6027").action(async () => {
1933
+ program2.command("doctor").description("\u73AF\u5883\u81EA\u68C0\uFF1A\u8FD0\u884C\u65F6/\u6A21\u578B\u5FEB\u7167/LoRA \u6587\u4EF6\u5B8C\u6574\u6027\uFF08\u80FD\u529B\u5F00\u5173\u89C1 lmedia image status\uFF09").action(async () => {
1541
1934
  const vrt = resolveVideoRuntime();
1542
1935
  const videoChecks = [
1543
- ["[video] .venv-video\uFF08mmh3turbo\uFF09", fs5.existsSync(vrt.mmh3turbo)],
1936
+ ["[video] .venv-video\uFF08mmh3turbo\uFF09", fs7.existsSync(vrt.mmh3turbo)],
1544
1937
  ["[video] ffmpeg", hasCommand("ffmpeg")],
1545
- ["[video] H3 \u6743\u91CD\uFF08~/.cache/mmh3turbo\uFF0C\u672A\u5C31\u7EEA\u65F6\u9996\u6B21\u751F\u6210\u81EA\u52A8\u4E0B\u8F7D\u6216 setup --mirror\uFF09", fs5.existsSync(path.join(vrt.weightsDir, "dit.bin"))]
1938
+ ["[video] H3 \u6743\u91CD\uFF08~/.cache/mmh3turbo\uFF0C\u672A\u5C31\u7EEA\u65F6\u9996\u6B21\u751F\u6210\u81EA\u52A8\u4E0B\u8F7D\u6216 setup --mirror\uFF09", fs7.existsSync(path.join(vrt.weightsDir, "dit.bin"))],
1939
+ ["[video] \u52A0\u901F\u6863 bundle dit-turbo\uFF08--fast \u7528\uFF1B\u7F3A\u5931\u8DD1 lmedia video turbo-merge\uFF09", fs7.existsSync(path.join(vrt.weightsDir, "dit-turbo.bin"))]
1546
1940
  ];
1941
+ const gate = readImageGate();
1547
1942
  let imageOk = true;
1548
- try {
1549
- const rt = resolveRuntime();
1550
- const checks = [
1551
- ["runtime root", fs5.existsSync(rt.root)],
1552
- ["pythonGen (.venv-train)", fs5.existsSync(rt.pythonGen)],
1553
- ["pythonFast (.venv)", fs5.existsSync(rt.pythonFast)],
1554
- ["[image] Qwen-Image-2512 \u5FEB\u7167", fs5.existsSync(rt.snapshot)],
1555
- ["[image] Qwen-Image-Edit-2511 \u5FEB\u7167", fs5.existsSync(rt.snapshotEdit)],
1556
- ["[image] Real-ESRGAN \u6743\u91CD", fs5.existsSync(resolveEsrganPath())]
1557
- ];
1558
- for (const [name, ok] of checks) console.log(`${ok ? "\u2713" : "\u2717"} ${name}`);
1943
+ if (gate.state === "corrupt") {
1944
+ console.log(`\u2717 [image] \u80FD\u529B\u72B6\u6001\u6587\u4EF6\u635F\u574F\uFF08\u65E0\u6CD5\u89E3\u6790\uFF09\u2192 ${gate.file}`);
1945
+ console.log(` \u539F\u56E0: ${gate.error}`);
1946
+ console.log(` \u4FEE\u590D: lmedia image enable\uFF08\u7528\u542F\u7528\u72B6\u6001\u8986\u76D6\u91CD\u5199\uFF09\uFF0C\u6216 rm ${gate.file}\uFF08\u5220\u9664\u540E\u56DE\u5230\u9ED8\u8BA4\u542F\u7528\uFF09`);
1947
+ imageOk = false;
1948
+ } else if (gate.state === "disabled") {
1949
+ console.log(
1950
+ `\xB7 [image] \u56FE\u50CF\u80FD\u529B\u5DF2\u7981\u7528\uFF08${gate.file}${gate.disabledAt ? `\uFF0C${gate.disabledAt}` : ""}${gate.reason ? `\uFF0C\u539F\u56E0: ${gate.reason}` : ""}\uFF09`
1951
+ );
1952
+ console.log(`\xB7 runtime/venv/\u6A21\u578B\u5FEB\u7167/LoRA \u68C0\u67E5\u5DF2\u8DF3\u8FC7\uFF1B\u6062\u590D: lmedia image enable`);
1953
+ } else {
1954
+ const items = imagePreflight();
1955
+ for (const it of items) console.log(`${it.ok ? "\u2713" : "\u2717"} ${it.name}${it.ok ? "" : ` \u2192 ${it.fix}`}`);
1559
1956
  for (const l of loadRegistry()) {
1560
- console.log(`${fs5.existsSync(l.path) ? "\u2713" : "\u2717"} LoRA: ${l.name}`);
1957
+ console.log(`${fs7.existsSync(l.path) ? "\u2713" : "\u2717"} LoRA: ${l.name}`);
1561
1958
  }
1562
- imageOk = checks.every(([, ok]) => ok);
1563
- } catch (e) {
1564
- imageOk = false;
1565
- console.error(e.message);
1959
+ imageOk = items.every((i) => i.ok);
1566
1960
  }
1567
1961
  for (const m of ["gen", "edit"]) {
1568
1962
  const st = await pingDaemon(m);
1963
+ if (st && gate.state === "disabled") {
1964
+ console.log(`\u2717 [image] daemon ${m} \u4ECD\u5728\u8FD0\u884C\uFF08pid ${st.pid}\uFF09\u2014\u2014\u56FE\u50CF\u80FD\u529B\u5DF2\u7981\u7528\uFF1B\u505C\u6B62: lmedia image serve stop --mode ${m}`);
1965
+ imageOk = false;
1966
+ continue;
1967
+ }
1569
1968
  console.log(
1570
- `${st ? "\u2713" : "\xB7"} [image] daemon ${m}${st ? `: ${st.state}\uFF08pid ${st.pid}\uFF0Cjobs ${st.jobs}\uFF09` : ": \u672A\u8FD0\u884C\uFF08\u53EF\u9009\uFF0Clmedia image serve start --mode " + m + "\uFF09"}`
1969
+ `${st ? "\u2713" : "\xB7"} [image] daemon ${m}${st ? `: ${st.state}\uFF08pid ${st.pid}\uFF0Cjobs ${st.jobs}\uFF09` : gate.state === "disabled" ? ": \u672A\u8FD0\u884C\uFF08\u80FD\u529B\u5DF2\u7981\u7528\uFF0Cstart \u88AB\u62E6\u622A\uFF09" : ": \u672A\u8FD0\u884C\uFF08\u53EF\u9009\uFF0Clmedia image serve start --mode " + m + "\uFF09"}`
1571
1970
  );
1572
1971
  }
1573
1972
  for (const [name, ok] of videoChecks) console.log(`${ok ? "\u2713" : "\u2717"} ${name}`);
@@ -1575,9 +1974,10 @@ function registerDoctor(program2) {
1575
1974
  const sfxChecks = sfxDoctorChecks();
1576
1975
  for (const [name, ok] of sfxChecks) console.log(`${ok ? "\u2713" : "\u2717"} ${name}`);
1577
1976
  const sfxOk = sfxChecks.every(([, ok]) => ok);
1977
+ const imageText = gate.state === "corrupt" ? "\u80FD\u529B\u72B6\u6001\u635F\u574F\uFF08\u4FEE\u590D\u6307\u5F15\u89C1\u4E0A\uFF09" : gate.state === "disabled" ? `\u5DF2\u7981\u7528${imageOk ? "" : "\uFF08\u4F46 daemon \u4ECD\u5728\u8FD0\u884C\uFF0C\u89C1\u4E0A\uFF09"}` : imageOk ? "\u5C31\u7EEA" : "\u672A\u5C31\u7EEA";
1578
1978
  console.log(
1579
1979
  `
1580
- \u56FE\u50CF\u6A21\u6001${imageOk ? "\u5C31\u7EEA" : "\u672A\u5C31\u7EEA"}\u3002\u89C6\u9891\u6A21\u6001\uFF08\u672C\u5730 MiniMax-H3 / mmh3turbo\uFF09\uFF1A${videoOk ? "\u5C31\u7EEA" : "\u672A\u5C31\u7EEA\uFF08lmedia video setup\uFF09"}\u3002\u97F3\u6548\u6A21\u6001\uFF1A${sfxOk ? "\u5C31\u7EEA" : "\u672A\u5C31\u7EEA\uFF08lmedia sfx doctor \u770B\u4FEE\u590D\u6307\u5F15\uFF09"}`
1980
+ \u56FE\u50CF\u6A21\u6001${imageText}\u3002\u89C6\u9891\u6A21\u6001\uFF08\u672C\u5730 MiniMax-H3 / mmh3turbo\uFF09\uFF1A${videoOk ? "\u5C31\u7EEA" : "\u672A\u5C31\u7EEA\uFF08lmedia video setup\uFF09"}\u3002\u97F3\u6548\u6A21\u6001\uFF1A${sfxOk ? "\u5C31\u7EEA" : "\u672A\u5C31\u7EEA\uFF08lmedia sfx doctor \u770B\u4FEE\u590D\u6307\u5F15\uFF09"}`
1581
1981
  );
1582
1982
  if (!imageOk) process.exit(1);
1583
1983
  });