honeydo 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +90 -0
- package/README.zh-CN.md +88 -0
- package/package.json +54 -0
- package/packages/cli/dist/index.d.ts +2 -0
- package/packages/cli/dist/index.js +171 -0
- package/packages/cli/dist/index.js.map +1 -0
- package/packages/doubao/dist/cli.d.ts +38 -0
- package/packages/doubao/dist/cli.d.ts.map +1 -0
- package/packages/doubao/dist/cli.js +206 -0
- package/packages/gcli/dist/cli.d.ts +465 -0
- package/packages/gcli/dist/cli.js +2017 -0
- package/packages/gcli/dist/cli.js.map +1 -0
- package/packages/lmedia/dist/index.d.ts +1 -0
- package/packages/lmedia/dist/index.js +1594 -0
- package/packages/lmedia/python/edit.py +107 -0
- package/packages/lmedia/python/esrgan_path.py +16 -0
- package/packages/lmedia/python/gen.py +131 -0
- package/packages/lmedia/python/serve.py +352 -0
- package/packages/lmedia/python/sfx.py +527 -0
- package/packages/lmedia/python/teacache.py +255 -0
- package/packages/lmedia/python/upscale.py +41 -0
- package/packages/minimax/dist/cli.d.ts +51 -0
- package/packages/minimax/dist/cli.js +307 -0
- package/packages/minimax/dist/cli.js.map +1 -0
- package/packages/minimax/dist/client.d.ts +20 -0
- package/packages/minimax/dist/client.js +55 -0
- package/packages/minimax/dist/client.js.map +1 -0
- package/packages/minimax/dist/tts.d.ts +33 -0
- package/packages/minimax/dist/tts.js +64 -0
- package/packages/minimax/dist/tts.js.map +1 -0
- package/packages/minimax/dist/validate.d.ts +29 -0
- package/packages/minimax/dist/validate.js +122 -0
- package/packages/minimax/dist/validate.js.map +1 -0
- package/packages/minimax/dist/voice-clone.d.ts +17 -0
- package/packages/minimax/dist/voice-clone.js +47 -0
- package/packages/minimax/dist/voice-clone.js.map +1 -0
- package/packages/minimax/dist/voices.d.ts +17 -0
- package/packages/minimax/dist/voices.js +20 -0
- package/packages/minimax/dist/voices.js.map +1 -0
- package/packages/qwen/dist/index.d.ts +1 -0
- package/packages/qwen/dist/index.js +311 -0
|
@@ -0,0 +1,1594 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import * as fs5 from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { spawn, spawnSync, execFileSync } from 'child_process';
|
|
8
|
+
import * as net from 'net';
|
|
9
|
+
import { setTimeout } from 'timers/promises';
|
|
10
|
+
import { createHash } from 'crypto';
|
|
11
|
+
|
|
12
|
+
var HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
function rootCandidate() {
|
|
14
|
+
const home = os.homedir();
|
|
15
|
+
let root = process.env.LMEDIA_RUNTIME ?? "";
|
|
16
|
+
if (!root) {
|
|
17
|
+
const link = path.join(home, ".lmedia", "runtime");
|
|
18
|
+
if (fs5.existsSync(link)) root = fs5.realpathSync(link);
|
|
19
|
+
}
|
|
20
|
+
if (!root) root = path.join(home, "ml", "lb-local-gen");
|
|
21
|
+
return root;
|
|
22
|
+
}
|
|
23
|
+
function pythonDriverDir() {
|
|
24
|
+
const candidates = [
|
|
25
|
+
path.resolve(HERE, "..", "python"),
|
|
26
|
+
// dist/index.js → <pkg>/python
|
|
27
|
+
path.resolve(HERE, "..", "..", "python")
|
|
28
|
+
// src/lib/runtime.ts(tsx dev)→ <pkg>/python
|
|
29
|
+
];
|
|
30
|
+
for (const c of candidates) {
|
|
31
|
+
if (fs5.existsSync(path.join(c, "sfx.py"))) return c;
|
|
32
|
+
}
|
|
33
|
+
return candidates[0];
|
|
34
|
+
}
|
|
35
|
+
function resolveVideoRuntime() {
|
|
36
|
+
const root = rootCandidate();
|
|
37
|
+
return {
|
|
38
|
+
root,
|
|
39
|
+
venvVideo: path.join(root, ".venv-video"),
|
|
40
|
+
pythonVideo: path.join(root, ".venv-video", "bin", "python"),
|
|
41
|
+
mmh3turbo: path.join(root, ".venv-video", "bin", "mmh3turbo"),
|
|
42
|
+
weightsDir: path.join(os.homedir(), ".cache", "mmh3turbo")
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function resolveAudioRuntime() {
|
|
46
|
+
const root = rootCandidate();
|
|
47
|
+
return {
|
|
48
|
+
root,
|
|
49
|
+
pythonAudio: path.join(root, ".venv-audio", "bin", "python"),
|
|
50
|
+
pythonDir: pythonDriverDir()
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function hfHubCache() {
|
|
54
|
+
if (process.env.HF_HUB_CACHE) return process.env.HF_HUB_CACHE;
|
|
55
|
+
if (process.env.HF_HOME) return path.join(process.env.HF_HOME, "hub");
|
|
56
|
+
return path.join(os.homedir(), ".cache", "huggingface", "hub");
|
|
57
|
+
}
|
|
58
|
+
function hfSnapshot(repo) {
|
|
59
|
+
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("."));
|
|
62
|
+
return snaps.length ? path.join(dir, snaps[0]) : null;
|
|
63
|
+
}
|
|
64
|
+
function resolveRuntime() {
|
|
65
|
+
const root = rootCandidate();
|
|
66
|
+
if (!fs5.existsSync(root)) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`\u8FD0\u884C\u65F6\u672A\u627E\u5230: ${root}
|
|
69
|
+
\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
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const hf = path.join(os.homedir(), ".cache", "huggingface", "hub");
|
|
73
|
+
const snapOf = (repo) => {
|
|
74
|
+
const modelDir = path.join(hf, `models--${repo.replace("/", "--")}`);
|
|
75
|
+
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);
|
|
78
|
+
if (snaps.length === 0) throw new Error(`\u6A21\u578B\u5FEB\u7167\u4E3A\u7A7A: ${repo}`);
|
|
79
|
+
const refFile = path.join(modelDir, "refs", "main");
|
|
80
|
+
if (fs5.existsSync(refFile)) {
|
|
81
|
+
const ref = fs5.readFileSync(refFile, "utf-8").trim();
|
|
82
|
+
if (snaps.includes(ref)) return path.join(dir, ref);
|
|
83
|
+
}
|
|
84
|
+
return path.join(dir, snaps[0]);
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
root,
|
|
88
|
+
pythonGen: path.join(root, ".venv-train", "bin", "python"),
|
|
89
|
+
pythonFast: path.join(root, ".venv", "bin", "python"),
|
|
90
|
+
snapshot: snapOf("Qwen/Qwen-Image-2512"),
|
|
91
|
+
snapshotEdit: snapOf("Qwen/Qwen-Image-Edit-2511"),
|
|
92
|
+
pythonDir: pythonDriverDir()
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
var REGISTRY_DIR = path.join(os.homedir(), ".config", "limg");
|
|
96
|
+
var REGISTRY_PATH = path.join(REGISTRY_DIR, "loras.json");
|
|
97
|
+
function seed() {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
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));
|
|
104
|
+
}
|
|
105
|
+
return JSON.parse(fs5.readFileSync(REGISTRY_PATH, "utf-8"));
|
|
106
|
+
}
|
|
107
|
+
function findLora(name) {
|
|
108
|
+
return loadRegistry().find((l) => l.name === name);
|
|
109
|
+
}
|
|
110
|
+
function addLora(entry) {
|
|
111
|
+
const list = loadRegistry().filter((l) => l.name !== entry.name);
|
|
112
|
+
list.push(entry);
|
|
113
|
+
fs5.writeFileSync(REGISTRY_PATH, JSON.stringify(list, null, 2));
|
|
114
|
+
}
|
|
115
|
+
function runPython(python, script, payload, opts = {}) {
|
|
116
|
+
return new Promise((resolve7, reject) => {
|
|
117
|
+
const child = spawn(python, [script, JSON.stringify(payload)], {
|
|
118
|
+
env: { ...process.env, HF_HUB_OFFLINE: process.env.HF_HUB_OFFLINE ?? "1" }
|
|
119
|
+
});
|
|
120
|
+
let stdoutBuf = "";
|
|
121
|
+
let stderrTail = "";
|
|
122
|
+
child.stdout.on("data", (d) => {
|
|
123
|
+
const text = d.toString();
|
|
124
|
+
if (!opts.quiet) process.stderr.write(text);
|
|
125
|
+
stdoutBuf += text;
|
|
126
|
+
});
|
|
127
|
+
child.stderr.on("data", (d) => {
|
|
128
|
+
const text = d.toString();
|
|
129
|
+
if (!opts.quiet) process.stderr.write(text);
|
|
130
|
+
stderrTail = (stderrTail + text).slice(-2e3);
|
|
131
|
+
});
|
|
132
|
+
child.on("close", (code) => {
|
|
133
|
+
if (code !== 0) return reject(new Error(`python \u9000\u51FA\u7801 ${code}
|
|
134
|
+
${stderrTail}`));
|
|
135
|
+
const lines = stdoutBuf.trim().split("\n").filter(Boolean);
|
|
136
|
+
const lastLine = lines[lines.length - 1] ?? "";
|
|
137
|
+
try {
|
|
138
|
+
resolve7(JSON.parse(lastLine));
|
|
139
|
+
} catch {
|
|
140
|
+
reject(new Error(`\u65E0\u6CD5\u89E3\u6790 python \u8F93\u51FA: ${lastLine}
|
|
141
|
+
${stderrTail}`));
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
var SERVE_DIR = path.join(os.homedir(), ".lmedia", "serve");
|
|
147
|
+
var sockPath = (m) => path.join(SERVE_DIR, `${m}.sock`);
|
|
148
|
+
var statusPath = (m) => path.join(SERVE_DIR, `${m}.json`);
|
|
149
|
+
var logPath = (m) => path.join(SERVE_DIR, `${m}.log`);
|
|
150
|
+
var DEFAULT_IDLE_TIMEOUT_SEC = 1800;
|
|
151
|
+
function otherMode(m) {
|
|
152
|
+
return m === "gen" ? "edit" : "gen";
|
|
153
|
+
}
|
|
154
|
+
function pidAlive(pid) {
|
|
155
|
+
try {
|
|
156
|
+
process.kill(pid, 0);
|
|
157
|
+
return true;
|
|
158
|
+
} catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function readStatus(m) {
|
|
163
|
+
try {
|
|
164
|
+
return JSON.parse(fs5.readFileSync(statusPath(m), "utf-8"));
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function requestJob(mode, payload) {
|
|
170
|
+
return new Promise((resolve7, reject) => {
|
|
171
|
+
const sock = net.connect({ path: sockPath(mode) });
|
|
172
|
+
sock.setNoDelay(true);
|
|
173
|
+
let buf = "";
|
|
174
|
+
let settled = false;
|
|
175
|
+
const fail2 = (err) => {
|
|
176
|
+
if (settled) return;
|
|
177
|
+
settled = true;
|
|
178
|
+
sock.destroy();
|
|
179
|
+
reject(err);
|
|
180
|
+
};
|
|
181
|
+
const gone = () => fail2(new Error("daemon \u8FDE\u63A5\u4E2D\u65AD\uFF08\u4EFB\u52A1\u7ED3\u679C\u672A\u77E5\uFF0C\u4E0D\u81EA\u52A8\u91CD\u8DD1\uFF1B\u4EA7\u7269\u6587\u4EF6\u53EF\u80FD\u5DF2\u843D\u76D8\uFF09"));
|
|
182
|
+
sock.setTimeout(5e3, () => {
|
|
183
|
+
if (!sock.writableEnded && buf === "") {
|
|
184
|
+
settled = true;
|
|
185
|
+
sock.destroy();
|
|
186
|
+
resolve7(null);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
sock.on("connect", () => {
|
|
190
|
+
sock.setTimeout(0);
|
|
191
|
+
sock.write(`${JSON.stringify(payload)}
|
|
192
|
+
`);
|
|
193
|
+
});
|
|
194
|
+
sock.on("error", (e) => {
|
|
195
|
+
if (settled) return;
|
|
196
|
+
if (e.code === "ENOENT" || e.code === "ECONNREFUSED") {
|
|
197
|
+
settled = true;
|
|
198
|
+
sock.destroy();
|
|
199
|
+
resolve7(null);
|
|
200
|
+
} else fail2(new Error(`daemon \u8FDE\u63A5\u9519\u8BEF: ${e.message}`));
|
|
201
|
+
});
|
|
202
|
+
sock.on("data", (d) => {
|
|
203
|
+
buf += d.toString();
|
|
204
|
+
let idx;
|
|
205
|
+
while ((idx = buf.indexOf("\n")) >= 0) {
|
|
206
|
+
const line = buf.slice(0, idx);
|
|
207
|
+
buf = buf.slice(idx + 1);
|
|
208
|
+
if (!line) continue;
|
|
209
|
+
let frame;
|
|
210
|
+
try {
|
|
211
|
+
frame = JSON.parse(line);
|
|
212
|
+
} catch {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (frame.t === "log") {
|
|
216
|
+
process.stderr.write(String(frame.line ?? "") + (String(frame.line ?? "").endsWith("\n") ? "" : "\n"));
|
|
217
|
+
} else if (frame.t === "queued") {
|
|
218
|
+
process.stderr.write(`\u6392\u961F\u4E2D\uFF08\u524D\u9762\u8FD8\u6709 ${frame.position} \u4E2A\u4EFB\u52A1\uFF09
|
|
219
|
+
`);
|
|
220
|
+
} else if (frame.t === "done") {
|
|
221
|
+
settled = true;
|
|
222
|
+
sock.end();
|
|
223
|
+
if (frame.ok) resolve7(frame.result);
|
|
224
|
+
else fail2(new Error(`daemon \u4EFB\u52A1\u5931\u8D25: ${frame.error}
|
|
225
|
+
${frame.tb ?? ""}`));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
sock.on("close", () => gone());
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
async function pingDaemon(mode) {
|
|
233
|
+
const r = await requestJob(mode, { kind: "ping" });
|
|
234
|
+
return r ?? null;
|
|
235
|
+
}
|
|
236
|
+
async function spawnDaemon(mode, opts = {}) {
|
|
237
|
+
const alive = await pingDaemon(mode);
|
|
238
|
+
if (alive) return { pid: alive.pid };
|
|
239
|
+
const rt = resolveRuntime();
|
|
240
|
+
fs5.mkdirSync(SERVE_DIR, { recursive: true });
|
|
241
|
+
const idle = String(opts.idleTimeoutSec ?? DEFAULT_IDLE_TIMEOUT_SEC);
|
|
242
|
+
const args = [
|
|
243
|
+
path.join(rt.pythonDir, "serve.py"),
|
|
244
|
+
"--mode",
|
|
245
|
+
mode,
|
|
246
|
+
"--socket",
|
|
247
|
+
sockPath(mode),
|
|
248
|
+
"--status",
|
|
249
|
+
statusPath(mode),
|
|
250
|
+
"--log",
|
|
251
|
+
logPath(mode),
|
|
252
|
+
"--snapshot",
|
|
253
|
+
mode === "gen" ? rt.snapshot : rt.snapshotEdit,
|
|
254
|
+
"--idle-timeout",
|
|
255
|
+
idle
|
|
256
|
+
];
|
|
257
|
+
const logFd = fs5.openSync(logPath(mode), "a");
|
|
258
|
+
const child = spawn(rt.pythonGen, args, {
|
|
259
|
+
detached: true,
|
|
260
|
+
stdio: ["ignore", logFd, logFd],
|
|
261
|
+
env: { ...process.env, HF_HUB_OFFLINE: process.env.HF_HUB_OFFLINE ?? "1" }
|
|
262
|
+
});
|
|
263
|
+
child.unref();
|
|
264
|
+
const deadline = Date.now() + 2e4;
|
|
265
|
+
while (Date.now() < deadline) {
|
|
266
|
+
if (fs5.existsSync(sockPath(mode))) return { pid: child.pid ?? -1 };
|
|
267
|
+
await setTimeout(200);
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
async function stopDaemon(mode) {
|
|
272
|
+
const st = await pingDaemon(mode);
|
|
273
|
+
if (!st) {
|
|
274
|
+
const had = fs5.existsSync(sockPath(mode)) || fs5.existsSync(statusPath(mode));
|
|
275
|
+
for (const p of [sockPath(mode), statusPath(mode)]) {
|
|
276
|
+
try {
|
|
277
|
+
fs5.rmSync(p);
|
|
278
|
+
} catch {
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return had ? "not-running-cleaned" : "not-running";
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
process.kill(st.pid, "SIGTERM");
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
const deadline = Date.now() + 15e3;
|
|
288
|
+
while (Date.now() < deadline && pidAlive(st.pid)) await setTimeout(300);
|
|
289
|
+
if (pidAlive(st.pid)) {
|
|
290
|
+
try {
|
|
291
|
+
process.kill(st.pid, "SIGKILL");
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
await setTimeout(500);
|
|
295
|
+
for (const p of [sockPath(mode), statusPath(mode)]) {
|
|
296
|
+
try {
|
|
297
|
+
fs5.rmSync(p);
|
|
298
|
+
} catch {
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return "killed";
|
|
302
|
+
}
|
|
303
|
+
return "stopped";
|
|
304
|
+
}
|
|
305
|
+
async function waitGpuIdle() {
|
|
306
|
+
if (process.env.LMEDIA_NO_GPU_WAIT === "1") return;
|
|
307
|
+
for (; ; ) {
|
|
308
|
+
const busy = await pingDaemon("gen") ?? await pingDaemon("edit");
|
|
309
|
+
if (!busy || !busy.busy && busy.queue === 0) return;
|
|
310
|
+
process.stderr.write(`\u7B49\u5F85 ${busy.mode} daemon \u7A7A\u95F2\uFF08GPU \u4E32\u884C\uFF1BLMEDIA_NO_GPU_WAIT=1 \u53EF\u8DF3\u8FC7\uFF09\u2026
|
|
311
|
+
`);
|
|
312
|
+
await setTimeout(5e3);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function parseMode(v) {
|
|
316
|
+
if (v !== "gen" && v !== "edit") {
|
|
317
|
+
console.error("--mode \u5FC5\u987B\u662F gen \u6216 edit");
|
|
318
|
+
process.exit(2);
|
|
319
|
+
}
|
|
320
|
+
return v;
|
|
321
|
+
}
|
|
322
|
+
function registerStart(serve) {
|
|
323
|
+
serve.command("start").description("\u542F\u52A8\u5E38\u9A7B\u63A8\u7406 daemon\uFF08\u6A21\u578B\u52A0\u8F7D\u4E00\u6B21\uFF0C\u540E\u7EED\u4EFB\u52A1\u514D ~2min \u51B7\u52A0\u8F7D\uFF1B\u7A7A\u95F2\u81EA\u52A8\u9000\u51FA\uFF09").requiredOption("--mode <m>", "\u7BA1\u7EBF\uFF1Agen\uFF08Qwen-Image-2512\uFF09| edit\uFF08Qwen-Image-Edit-2511\uFF09").option("--idle-timeout <sec>", "\u7A7A\u95F2\u81EA\u9000\u79D2\u6570", String(DEFAULT_IDLE_TIMEOUT_SEC)).option("--wait", "\u963B\u585E\u7B49\u6A21\u578B\u52A0\u8F7D\u5B8C\u6210\uFF08\u9ED8\u8BA4\u7ACB\u5373\u8FD4\u56DE\uFF0C\u671F\u95F4\u4EFB\u52A1\u81EA\u52A8\u6392\u961F\uFF09").option("--swap", "\u53E6\u4E00\u6A21\u5F0F daemon \u5728\u8FD0\u884C\u65F6\u5148\u505C\u5B83\u518D\u542F\u52A8\uFF08\u53CC\u9A7B ~110GB \u8D85\u672C\u673A\u5185\u5B58\uFF09").action(async (opts) => {
|
|
324
|
+
const mode = parseMode(opts.mode);
|
|
325
|
+
const idleTimeoutSec = parseInt(opts.idleTimeout, 10);
|
|
326
|
+
if (!Number.isFinite(idleTimeoutSec) || idleTimeoutSec < 5) {
|
|
327
|
+
console.error("--idle-timeout \u9700 \u22655 \u79D2");
|
|
328
|
+
process.exit(2);
|
|
329
|
+
}
|
|
330
|
+
const alive = await pingDaemon(mode);
|
|
331
|
+
if (alive) {
|
|
332
|
+
console.log(`${mode} daemon \u5DF2\u5728\u8FD0\u884C\uFF08pid ${alive.pid}\uFF0C${alive.state}\uFF0Cjobs ${alive.jobs}\uFF09`);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const other = await pingDaemon(otherMode(mode));
|
|
336
|
+
if (other) {
|
|
337
|
+
if (!opts.swap) {
|
|
338
|
+
console.error(
|
|
339
|
+
`\u2717 ${other.mode} daemon \u6B63\u5728\u8FD0\u884C\uFF08pid ${other.pid}\uFF09\u3002\u53CC\u7BA1\u7EBF\u9A7B\u7559 ~110GB \u8D85\u51FA 128GB \u673A\u5668\uFF1B\u786E\u8BA4\u5207\u6362\u8BF7\u52A0 --swap`
|
|
340
|
+
);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
console.log(`--swap\uFF1A\u5148\u505C ${other.mode} daemon\uFF08pid ${other.pid}\uFF09\u2026`);
|
|
344
|
+
const r2 = await stopDaemon(other.mode);
|
|
345
|
+
console.log(` \u5DF2${r2 === "killed" ? "\u5F3A\u5236\u7EC8\u6B62" : "\u505C\u6B62"}`);
|
|
346
|
+
}
|
|
347
|
+
const r = await spawnDaemon(mode, { idleTimeoutSec });
|
|
348
|
+
if (!r) {
|
|
349
|
+
console.error(`\u2717 daemon \u542F\u52A8\u5931\u8D25\uFF0820s \u5185\u672A bind socket\uFF09\u2014\u2014\u73B0\u573A\u65E5\u5FD7\uFF1Atail -50 ${logPath(mode)}`);
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
352
|
+
console.log(`\u2713 ${mode} daemon \u5DF2\u542F\u52A8\uFF08pid ${r.pid}\uFF09\uFF1B\u6A21\u578B\u52A0\u8F7D\u4E2D\uFF08~2 \u5206\u949F\uFF09\uFF0C\u671F\u95F4\u4EFB\u52A1\u81EA\u52A8\u6392\u961F`);
|
|
353
|
+
console.log(` \u65E5\u5FD7\uFF1Atail -f ${logPath(mode)}`);
|
|
354
|
+
if (!opts.wait) return;
|
|
355
|
+
const deadline = Date.now() + 3e5;
|
|
356
|
+
while (Date.now() < deadline) {
|
|
357
|
+
const st = await pingDaemon(mode);
|
|
358
|
+
if (st?.state === "ready") {
|
|
359
|
+
console.log(`\u2713 \u5C31\u7EEA\uFF08pid ${st.pid}\uFF0Cjobs ${st.jobs}\uFF09`);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
await setTimeout(2e3);
|
|
363
|
+
}
|
|
364
|
+
console.error(`\u2717 5 \u5206\u949F\u672A\u5C31\u7EEA\u2014\u2014\u770B\u65E5\u5FD7\uFF1Atail -50 ${logPath(mode)}`);
|
|
365
|
+
process.exit(1);
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
function registerStop(serve) {
|
|
369
|
+
serve.command("stop").description("\u505C\u6B62 daemon\uFF08TERM \u2192 15s \u2192 KILL\uFF1Bbusy \u65F6\u4F1A\u4E2D\u65AD\u5F53\u524D\u4EFB\u52A1\uFF09").option("--mode <m>", "gen | edit\uFF08\u7F3A\u7701\u4E24\u6A21\u5F0F\u90FD\u5904\u7406\uFF09").action(async (opts) => {
|
|
370
|
+
const modes = opts.mode ? [parseMode(opts.mode)] : ["gen", "edit"];
|
|
371
|
+
let any = false;
|
|
372
|
+
for (const m of modes) {
|
|
373
|
+
const st = await pingDaemon(m);
|
|
374
|
+
if (st?.busy) console.error(`\u26A0\uFE0F ${m} daemon \u6B63\u5728\u8DD1\u4EFB\u52A1\uFF0Cstop \u5C06\u4E2D\u65AD\u5B83\uFF08\u5BA2\u6237\u7AEF\u4F1A\u62A5\u9519\uFF09`);
|
|
375
|
+
const r = await stopDaemon(m);
|
|
376
|
+
if (r === "stopped") {
|
|
377
|
+
console.log(`\u2713 ${m} daemon \u5DF2\u505C\u6B62`);
|
|
378
|
+
any = true;
|
|
379
|
+
} else if (r === "killed") {
|
|
380
|
+
console.log(`\u2713 ${m} daemon \u5DF2 SIGKILL \u5F3A\u5236\u7EC8\u6B62\uFF08\u6B63\u5E38 TERM \u672A\u9000\uFF09`);
|
|
381
|
+
any = true;
|
|
382
|
+
} else if (r === "not-running-cleaned") console.log(`${m} daemon \u672A\u8FD0\u884C\uFF08\u5DF2\u6E05\u7406\u6B8B\u7559 socket/status\uFF09`);
|
|
383
|
+
else console.log(`${m} daemon \u672A\u8FD0\u884C`);
|
|
384
|
+
}
|
|
385
|
+
if (!any && modes.length === 1) process.exit(1);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
function registerStatus(serve) {
|
|
389
|
+
serve.command("status").description("\u67E5\u770B\u4E24\u6A21\u5F0F daemon \u72B6\u6001\uFF08\u8FD0\u884C\u4E2D / \u6B8B\u7559 / \u672A\u8FD0\u884C\uFF09").action(async () => {
|
|
390
|
+
let anyRunning = false;
|
|
391
|
+
for (const m of ["gen", "edit"]) {
|
|
392
|
+
const st = await pingDaemon(m);
|
|
393
|
+
if (st) {
|
|
394
|
+
anyRunning = true;
|
|
395
|
+
const idle = st.lastJobAt ? Math.round((Date.now() / 1e3 - st.lastJobAt) / 60) : "-";
|
|
396
|
+
console.log(
|
|
397
|
+
`\u2713 ${m}: pid ${st.pid}\uFF0C${st.state}\uFF0Cqueue ${st.queue}\uFF0Cjobs ${st.jobs}\uFF0C\u4E0A\u6B21\u4EFB\u52A1 ${idle}min \u524D`
|
|
398
|
+
);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const saved = readStatus(m);
|
|
402
|
+
if (saved) {
|
|
403
|
+
console.log(`\u2717 ${m}: \u6B8B\u7559\u72B6\u6001\uFF08pid ${saved.pid} \u5DF2\u9000\u51FA\uFF09\u2014\u2014 lmedia image serve stop --mode ${m} \u6E05\u7406`);
|
|
404
|
+
} else {
|
|
405
|
+
console.log(`\xB7 ${m}: \u672A\u8FD0\u884C\uFF08\u53EF\u9009\u52A0\u901F\uFF1Bgen/edit \u4F1A\u81EA\u52A8\u62C9\u8D77\uFF09`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
process.exit(anyRunning ? 0 : 1);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
function registerServe(image) {
|
|
412
|
+
const serve = image.command("serve").description("\u5E38\u9A7B\u63A8\u7406 daemon\uFF08\u6A21\u578B\u52A0\u8F7D\u4E00\u6B21\uFF0C\u4EFB\u52A1\u514D\u51B7\u52A0\u8F7D\uFF1B\u7A7A\u95F2 30min \u81EA\u52A8\u9000\u51FA\uFF09");
|
|
413
|
+
registerStart(serve);
|
|
414
|
+
registerStop(serve);
|
|
415
|
+
registerStatus(serve);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/commands/image.ts
|
|
419
|
+
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
|
+
function parseCache(v) {
|
|
421
|
+
if (v === void 0) return void 0;
|
|
422
|
+
return { thresh: v === true ? 0.3 : parseFloat(String(v)) };
|
|
423
|
+
}
|
|
424
|
+
async function runImageJob(rt, args) {
|
|
425
|
+
if (!args.noDaemon && process.env.LMEDIA_NO_DAEMON !== "1") {
|
|
426
|
+
for (const mode of args.daemonModes) {
|
|
427
|
+
const r = await requestJob(mode, { kind: args.kind, ...args.payload });
|
|
428
|
+
if (r !== null) return r;
|
|
429
|
+
}
|
|
430
|
+
if (args.kind === "gen" || args.kind === "edit") {
|
|
431
|
+
const mode = args.daemonModes[0];
|
|
432
|
+
if (!await pingDaemon(otherMode(mode))) {
|
|
433
|
+
process.stderr.write(
|
|
434
|
+
`\u27F3 \u62C9\u8D77 ${mode} daemon\uFF08\u9996\u6B21\u4EFB\u52A1\u542B ~2min \u6A21\u578B\u52A0\u8F7D\uFF1B\u7A7A\u95F2 ${DEFAULT_IDLE_TIMEOUT_SEC / 60}min \u81EA\u9000\uFF0C--no-daemon \u53EF\u7ED5\u8FC7\uFF09\u2026
|
|
435
|
+
`
|
|
436
|
+
);
|
|
437
|
+
await spawnDaemon(mode);
|
|
438
|
+
const r = await requestJob(mode, { kind: args.kind, ...args.payload });
|
|
439
|
+
if (r !== null) return r;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
await waitGpuIdle();
|
|
443
|
+
}
|
|
444
|
+
return runPython(rt.pythonGen, args.script, args.payload);
|
|
445
|
+
}
|
|
446
|
+
function registerGen(image) {
|
|
447
|
+
image.command("gen <prompt>").description("\u6587\u751F\u56FE\uFF1AQwen-Image-2512 bf16\uFF08\u672C\u5730\uFF0C\u96F6 API \u6210\u672C\uFF09").option("-o, --out <path>", "\u8F93\u51FA\u8DEF\u5F84", `lmedia-${Date.now()}.png`).option("--style <name>", "\u98CE\u683C LoRA \u540D\uFF08\u5982 lbwatercolor\uFF09").option("--char <name>", "\u89D2\u8272 LoRA \u540D\uFF08\u5982 pipi\uFF09").option("--style-weight <n>", "\u98CE\u683C LoRA \u6743\u91CD\uFF08\u8986\u76D6\u6CE8\u518C\u8868\u9ED8\u8BA4\uFF09").option("--char-weight <n>", "\u89D2\u8272 LoRA \u6743\u91CD\uFF08\u8986\u76D6\u6CE8\u518C\u8868\u9ED8\u8BA4\uFF09").option("--lora <name...>", "\u4EFB\u610F\u6CE8\u518C LoRA \u540D\uFF08\u53EF\u591A\u4E2A\uFF09").option("--width <px>", "\u5BBD", "1664").option("--height <px>", "\u9AD8", "928").option("--steps <n>", "\u6B65\u6570\uFF08\u9ED8\u8BA4 20\uFF1B--fast \u9ED8\u8BA4 8\uFF1B\u663E\u5F0F\u4F20\u5165\u4F18\u5148\uFF09").option("--seed <n>", "\u968F\u673A\u79CD\u5B50", "42").option("--cfg <n>", "true CFG \u5F3A\u5EA6\uFF08\u9ED8\u8BA4 4.0=\u5B98\u65B9\u914D\u65B9\uFF1B<=1 \u5173\u95ED\u5F15\u5BFC\uFF1B--fast \u9ED8\u8BA4 1.0\uFF09").option("--neg <text>", "\u8D1F\u5411\u63D0\u793A\u8BCD\uFF08\u9ED8\u8BA4\u5B98\u65B9\u4E2D\u6587\u6A21\u677F\uFF1B\u4F20\u7A7A\u4E32=\u65E0\u8D1F\u5411\uFF09").option("--fast [steps]", "Lightning \u84B8\u998F\u52A0\u901F\uFF1A\u6CE8\u5165 lightning2512 LoRA\uFF08\u9ED8\u8BA4 8 \u6B65\uFF0C\u53EF --fast 4\uFF09\uFF0Ccfg=1\uFF0C\u53EF\u4E0E --style/--char \u53E0\u52A0").option("--cache [thresh]", "TeaCache \u6B65\u7F13\u5B58\uFF08\u9608\u503C\u7F3A\u7701 0.3\uFF0C\u8D8A\u5927\u8D8A\u5FEB\u8D8A\u7CD9\uFF1B\u8D28\u91CF\u8FD1\u4F3C\u65E0\u635F\uFF09").option("--num <n>", "\u4E00\u6B21\u751F\u6210\u5F20\u6570\uFF081-4\uFF09\uFF0C\u8F93\u51FA out-1.png..out-N.png", "1").option("--upscale", "\u751F\u6210\u540E\u8D85\u5206\u5230 2730\xD71535").option("--no-daemon", "\u7ED5\u8FC7\u5E38\u9A7B daemon\uFF0C\u5F3A\u5236\u51B7\u8DEF\u5F84\uFF08\u6392\u969C\u7528\uFF09").action(async (promptIn, opts) => {
|
|
448
|
+
const rt = resolveRuntime();
|
|
449
|
+
let prompt = promptIn;
|
|
450
|
+
const loras = [];
|
|
451
|
+
const names = [opts.style, opts.char, ...opts.lora ?? []].filter(Boolean);
|
|
452
|
+
const weightOverride = {
|
|
453
|
+
[opts.style ?? ""]: opts.styleWeight ? parseFloat(opts.styleWeight) : void 0,
|
|
454
|
+
[opts.char ?? ""]: opts.charWeight ? parseFloat(opts.charWeight) : void 0
|
|
455
|
+
};
|
|
456
|
+
for (const name of names) {
|
|
457
|
+
const entry = findLora(name);
|
|
458
|
+
if (!entry) {
|
|
459
|
+
console.error(`\u672A\u6CE8\u518C\u7684 LoRA: ${name}\uFF08lmedia lora list \u67E5\u770B\uFF09`);
|
|
460
|
+
process.exit(2);
|
|
461
|
+
}
|
|
462
|
+
loras.push({ path: entry.path, scale: weightOverride[name] ?? entry.defaultWeight });
|
|
463
|
+
if (entry.trigger && !prompt.includes(entry.trigger)) {
|
|
464
|
+
prompt = `${entry.trigger}\uFF0C${prompt}`;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const fast = opts.fast !== void 0;
|
|
468
|
+
const num2 = parseInt(opts.num ?? "1", 10);
|
|
469
|
+
if (!Number.isFinite(num2) || num2 < 1 || num2 > 4) {
|
|
470
|
+
console.error("--num \u53D6\u503C 1-4");
|
|
471
|
+
process.exit(2);
|
|
472
|
+
}
|
|
473
|
+
const steps = opts.steps !== void 0 ? parseInt(opts.steps, 10) : fast ? opts.fast === true ? 8 : parseInt(opts.fast, 10) : 20;
|
|
474
|
+
const trueCfg = opts.cfg !== void 0 ? parseFloat(opts.cfg) : fast ? 1 : 4;
|
|
475
|
+
if (fast && !names.includes("lightning2512")) {
|
|
476
|
+
const lt = findLora("lightning2512");
|
|
477
|
+
if (!lt || !fs5.existsSync(lt.path)) {
|
|
478
|
+
console.error("--fast \u9700\u8981 lightning2512 LoRA\uFF08\u4E0B\u8F7D\u540E: lmedia lora add lightning2512 <path> --kind speed\uFF09");
|
|
479
|
+
process.exit(2);
|
|
480
|
+
}
|
|
481
|
+
loras.unshift({ path: lt.path, scale: 1 });
|
|
482
|
+
}
|
|
483
|
+
let neg;
|
|
484
|
+
if (trueCfg > 1) {
|
|
485
|
+
if (opts.neg !== void 0 && opts.neg !== "") neg = opts.neg;
|
|
486
|
+
else if (opts.neg === void 0) neg = DEFAULT_NEG;
|
|
487
|
+
}
|
|
488
|
+
const result = await runImageJob(rt, {
|
|
489
|
+
kind: "gen",
|
|
490
|
+
daemonModes: ["gen"],
|
|
491
|
+
script: path.join(rt.pythonDir, "gen.py"),
|
|
492
|
+
noDaemon: opts.daemon === false,
|
|
493
|
+
payload: {
|
|
494
|
+
prompt,
|
|
495
|
+
out: path.resolve(opts.out),
|
|
496
|
+
snapshot: rt.snapshot,
|
|
497
|
+
width: parseInt(opts.width, 10),
|
|
498
|
+
height: parseInt(opts.height, 10),
|
|
499
|
+
steps,
|
|
500
|
+
trueCfg,
|
|
501
|
+
neg,
|
|
502
|
+
num: num2,
|
|
503
|
+
seed: parseInt(opts.seed, 10),
|
|
504
|
+
loras,
|
|
505
|
+
lightningSched: fast || void 0,
|
|
506
|
+
teaCache: parseCache(opts.cache),
|
|
507
|
+
upscaleTo: opts.upscale ? [2730, 1535] : void 0
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
console.log(JSON.stringify(result, null, 2));
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
function registerEdit(image) {
|
|
514
|
+
image.command("edit <prompt>").description("\u53C2\u8003\u56FE\u7F16\u8F91\uFF1AQwen-Image-Edit-2511\uFF081+ \u5F20\u53C2\u8003\u56FE\u9501\u89D2\u8272/\u98CE\u683C\uFF09").requiredOption("--ref <path...>", "\u53C2\u8003\u56FE\uFF08\u53EF\u591A\u5F20\uFF09").option("-o, --out <path>", "\u8F93\u51FA\u8DEF\u5F84", `lmedia-edit-${Date.now()}.png`).option("--width <px>", "\u5BBD", "1664").option("--height <px>", "\u9AD8", "928").option("--steps <n>", "\u6B65\u6570\uFF08\u9ED8\u8BA4 20\uFF1B\u5B98\u65B9 2511 \u914D\u65B9 40\uFF0C\u8D28\u91CF\u4F18\u5148\u53EF\u4E0A\u8C03\uFF1B--fast \u9ED8\u8BA4 8\uFF1B\u663E\u5F0F\u4F20\u5165\u4F18\u5148\uFF09").option("--seed <n>", "\u968F\u673A\u79CD\u5B50", "42").option("--cfg <n>", "true CFG \u5F3A\u5EA6\uFF08\u9ED8\u8BA4 4.0=\u5B98\u65B9 Edit-2511 \u914D\u65B9\uFF1B--fast \u9ED8\u8BA4 1.0\uFF09").option("--neg <text>", '\u8D1F\u5411\u63D0\u793A\u8BCD\uFF08\u5B98\u65B9\u7F16\u8F91\u914D\u65B9\u9ED8\u8BA4 " "\uFF0C\u51E0\u4E4E\u65E0\u7EA6\u675F\uFF1Bcfg<=1 \u65F6\u4E0D\u4E0B\u53D1\uFF09').option("--fast [steps]", "Lightning \u84B8\u998F\u52A0\u901F\uFF1A\u6CE8\u5165 lightningedit2511 LoRA\uFF08\u9ED8\u8BA4 8 \u6B65\uFF0C\u53EF --fast 4\uFF09\uFF0Ccfg=1 + \u84B8\u998F\u8C03\u5EA6\u5668\uFF0C\u7EA6 1/7 \u8017\u65F6").option("--cache [thresh]", "TeaCache \u6B65\u7F13\u5B58\uFF08\u9608\u503C\u7F3A\u7701 0.3\uFF0C\u8D8A\u5927\u8D8A\u5FEB\u8D8A\u7CD9\uFF1B\u8D28\u91CF\u8FD1\u4F3C\u65E0\u635F\uFF0C\u51FA\u7248\u6863\u5019\u9009\uFF09").option("--no-daemon", "\u7ED5\u8FC7\u5E38\u9A7B daemon\uFF0C\u5F3A\u5236\u51B7\u8DEF\u5F84\uFF08\u6392\u969C\u7528\uFF09").action(async (prompt, opts) => {
|
|
515
|
+
const rt = resolveRuntime();
|
|
516
|
+
const fast = opts.fast !== void 0;
|
|
517
|
+
const fastSteps = opts.fast === true ? 8 : parseInt(opts.fast, 10);
|
|
518
|
+
const trueCfg = opts.cfg !== void 0 ? parseFloat(opts.cfg) : fast ? 1 : 4;
|
|
519
|
+
const steps = opts.steps !== void 0 ? parseInt(opts.steps, 10) : fast ? fastSteps : 20;
|
|
520
|
+
const loras = [];
|
|
521
|
+
if (fast) {
|
|
522
|
+
const wantX4 = fastSteps <= 4;
|
|
523
|
+
const lt = (wantX4 ? findLora("lightningedit2511x4") : void 0) ?? findLora("lightningedit2511");
|
|
524
|
+
if (!lt || !fs5.existsSync(lt.path)) {
|
|
525
|
+
console.error("--fast \u9700\u8981 lightningedit2511 LoRA\uFF08\u4E0B\u8F7D\u540E: lmedia lora add lightningedit2511 <path> --kind speed\uFF09");
|
|
526
|
+
process.exit(2);
|
|
527
|
+
}
|
|
528
|
+
loras.push({ path: lt.path, scale: 1 });
|
|
529
|
+
}
|
|
530
|
+
const result = await runImageJob(rt, {
|
|
531
|
+
kind: "edit",
|
|
532
|
+
daemonModes: ["edit"],
|
|
533
|
+
script: path.join(rt.pythonDir, "edit.py"),
|
|
534
|
+
noDaemon: opts.daemon === false,
|
|
535
|
+
payload: {
|
|
536
|
+
prompt,
|
|
537
|
+
out: path.resolve(opts.out),
|
|
538
|
+
snapshotEdit: rt.snapshotEdit,
|
|
539
|
+
refs: opts.ref.map((r) => path.resolve(r)),
|
|
540
|
+
width: parseInt(opts.width, 10),
|
|
541
|
+
height: parseInt(opts.height, 10),
|
|
542
|
+
steps,
|
|
543
|
+
trueCfg,
|
|
544
|
+
neg: trueCfg > 1 ? opts.neg ?? " " : void 0,
|
|
545
|
+
seed: parseInt(opts.seed, 10),
|
|
546
|
+
loras,
|
|
547
|
+
lightningSched: fast || void 0,
|
|
548
|
+
teaCache: parseCache(opts.cache)
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
console.log(JSON.stringify(result, null, 2));
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
function registerUpscale(image) {
|
|
555
|
+
image.command("upscale <input> <output>").description("Real-ESRGAN x2 \u8D85\u5206\uFF08MPS\uFF0C\u79D2\u7EA7\uFF09").option("--size <WxH>", "\u6700\u7EC8\u5C3A\u5BF8\uFF08\u5982 2730x1535\uFF09").option("--no-daemon", "\u7ED5\u8FC7\u5E38\u9A7B daemon\uFF0C\u5F3A\u5236\u51B7\u8DEF\u5F84\uFF08\u6392\u969C\u7528\uFF09").action(async (input, output, opts) => {
|
|
556
|
+
const rt = resolveRuntime();
|
|
557
|
+
const size = opts.size?.split("x").map((s) => parseInt(s, 10));
|
|
558
|
+
const result = await runImageJob(rt, {
|
|
559
|
+
kind: "upscale",
|
|
560
|
+
daemonModes: ["gen", "edit"],
|
|
561
|
+
// 哪种 daemon 活着搭哪个便车;都不在直接冷路径(spandrel 秒级)
|
|
562
|
+
script: path.join(rt.pythonDir, "upscale.py"),
|
|
563
|
+
noDaemon: opts.daemon === false,
|
|
564
|
+
payload: {
|
|
565
|
+
in: path.resolve(input),
|
|
566
|
+
out: path.resolve(output),
|
|
567
|
+
finalSize: size
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
console.log(JSON.stringify(result, null, 2));
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
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");
|
|
575
|
+
registerGen(image);
|
|
576
|
+
registerEdit(image);
|
|
577
|
+
registerUpscale(image);
|
|
578
|
+
registerServe(image);
|
|
579
|
+
}
|
|
580
|
+
function hasCommand(cmd) {
|
|
581
|
+
return spawnSync("which", [cmd], { stdio: "ignore" }).status === 0;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/commands/video.ts
|
|
585
|
+
var RES_PRESETS = [
|
|
586
|
+
"256p",
|
|
587
|
+
"352p",
|
|
588
|
+
"480p",
|
|
589
|
+
"576p",
|
|
590
|
+
"704p",
|
|
591
|
+
"720p",
|
|
592
|
+
"768p",
|
|
593
|
+
"square",
|
|
594
|
+
"portrait",
|
|
595
|
+
"vertical"
|
|
596
|
+
// 竖版/方版(绘本竖页用 portrait)
|
|
597
|
+
];
|
|
598
|
+
function runStep(cmd, args) {
|
|
599
|
+
const r = spawnSync(cmd, args, { stdio: "inherit" });
|
|
600
|
+
if (r.status !== 0) {
|
|
601
|
+
console.error(`\u6B65\u9AA4\u5931\u8D25: ${cmd} ${args.join(" ")}\uFF08\u9000\u51FA\u7801 ${r.status}\uFF09`);
|
|
602
|
+
process.exit(1);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
function patchGgufFilename(venvVideo) {
|
|
606
|
+
const libDir = path.join(venvVideo, "lib");
|
|
607
|
+
if (!fs5.existsSync(libDir)) return;
|
|
608
|
+
const pyDir = fs5.readdirSync(libDir).find((d) => d.startsWith("python"));
|
|
609
|
+
if (!pyDir) return;
|
|
610
|
+
const w = path.join(libDir, pyDir, "site-packages", "mmh3turbo", "weights.py");
|
|
611
|
+
if (!fs5.existsSync(w)) return;
|
|
612
|
+
const src = fs5.readFileSync(w, "utf8");
|
|
613
|
+
const OLD = "MiniMax-H3-Qwen3VL-32B-TextEncoder-Q2_K.gguf";
|
|
614
|
+
const NEW = "qwen3vl-32B-MiniMax-H3-Q2_K.gguf";
|
|
615
|
+
if (src.includes(OLD)) {
|
|
616
|
+
fs5.writeFileSync(w, src.replaceAll(OLD, NEW));
|
|
617
|
+
console.error(`\u5DF2\u4FEE\u8865 mmh3turbo \u4E0A\u6E38 GGUF \u6587\u4EF6\u540D\u6F02\u79FB\uFF08${OLD} \u2192 ${NEW}\uFF09`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
var MIRROR = "https://hf-mirror.com";
|
|
621
|
+
var MIRROR_FILES = [
|
|
622
|
+
{ repo: "yunfengwang/mmh3turbo-bundles", dest: "~/.cache/mmh3turbo/dit.bin", bytes: 20967495552 },
|
|
623
|
+
{ repo: "yunfengwang/mmh3turbo-bundles", dest: "~/.cache/mmh3turbo/dit.idx" },
|
|
624
|
+
// 尺寸运行时探测
|
|
625
|
+
{ repo: "yunfengwang/mmh3turbo-bundles", dest: "~/.cache/mmh3turbo/qwen3vl_4bit.safetensors", bytes: 15239339391 },
|
|
626
|
+
{ repo: "realrebelai/MiniMax-H3_GGUFs", dest: "~/.cache/huggingface/hub/models--realrebelai--MiniMax-H3_GGUFs/blobs/qwen3vl-32B-MiniMax-H3-Q2_K.gguf", bytes: 8487968160 },
|
|
627
|
+
{ repo: "Comfy-Org/MiniMax-H3", dest: "~/.cache/huggingface/hub/models--Comfy-Org--MiniMax-H3/blobs/minimax_h3_video_vae_fp16.safetensors", bytes: 5207808496 },
|
|
628
|
+
{ repo: "Comfy-Org/MiniMax-H3", dest: "~/.cache/huggingface/hub/models--Comfy-Org--MiniMax-H3/blobs/minimax_h3_audio_vae_fp32.safetensors", bytes: 605254808 }
|
|
629
|
+
];
|
|
630
|
+
function remoteSize(url) {
|
|
631
|
+
const r = spawnSync("curl", ["-sIL", "--max-time", "20", url], { encoding: "utf8" });
|
|
632
|
+
const m = (r.stdout ?? "").match(/content-length:\s*(\d+)/i);
|
|
633
|
+
return m ? parseInt(m[1], 10) : 0;
|
|
634
|
+
}
|
|
635
|
+
function mirrorProvision() {
|
|
636
|
+
for (const f of MIRROR_FILES) {
|
|
637
|
+
const dest = f.dest.replace(/^~/, os.homedir());
|
|
638
|
+
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) {
|
|
641
|
+
console.error(`\u5DF2\u5C31\u4F4D: ${dest}`);
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
const expect = f.bytes ?? remoteSize(url);
|
|
645
|
+
console.error(`\u4E0B\u8F7D ${path.basename(dest)}\uFF08${(expect / 1e9).toFixed(1)} GB\uFF09\u2192 ${dest}`);
|
|
646
|
+
for (let attempt = 1; ; attempt++) {
|
|
647
|
+
const r = spawnSync(
|
|
648
|
+
"curl",
|
|
649
|
+
["-L", "-C", "-", "--retry", "8", "--retry-delay", "3", "--connect-timeout", "20", "-o", `${dest}.part`, url],
|
|
650
|
+
{ stdio: "inherit" }
|
|
651
|
+
);
|
|
652
|
+
if (r.status === 0) break;
|
|
653
|
+
if (attempt >= 5) {
|
|
654
|
+
console.error(`\u4E0B\u8F7D\u5931\u8D25\uFF08\u5DF2\u8BD5 ${attempt} \u8F6E\uFF09: ${url}`);
|
|
655
|
+
process.exit(1);
|
|
656
|
+
}
|
|
657
|
+
console.error(`\u4E0B\u8F7D\u4E2D\u65AD\uFF0C5 \u79D2\u540E\u7EED\u4F20\uFF08\u7B2C ${attempt} \u8F6E\uFF09\u2026`);
|
|
658
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5e3);
|
|
659
|
+
}
|
|
660
|
+
fs5.renameSync(`${dest}.part`, dest);
|
|
661
|
+
const got = fs5.statSync(dest).size;
|
|
662
|
+
if (expect && got !== expect) {
|
|
663
|
+
console.error(`\u5927\u5C0F\u6821\u9A8C\u5931\u8D25: ${dest} got=${got} expect=${expect}`);
|
|
664
|
+
process.exit(1);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
console.log("H3 \u6743\u91CD\u955C\u50CF\u9884\u7F6E\u5B8C\u6210\uFF08~/.cache/mmh3turbo + HF blobs\uFF0Cmmh3turbo \u5168\u90E8\u672C\u5730\u547D\u4E2D\uFF0C\u4E0D\u518D\u8D70\u7F51\u7EDC\uFF09");
|
|
668
|
+
}
|
|
669
|
+
function registerSetup(video) {
|
|
670
|
+
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
|
+
const rt = resolveVideoRuntime();
|
|
672
|
+
fs5.mkdirSync(rt.root, { recursive: true });
|
|
673
|
+
if (!hasCommand("ffmpeg")) {
|
|
674
|
+
console.error("\u26A0\uFE0F \u672A\u627E\u5230 ffmpeg\uFF08mp4 \u5C01\u88C5\u5FC5\u9700\uFF09\uFF1Abrew install ffmpeg");
|
|
675
|
+
}
|
|
676
|
+
if (!fs5.existsSync(rt.pythonVideo)) {
|
|
677
|
+
console.error(`\u521B\u5EFA venv: ${rt.venvVideo}`);
|
|
678
|
+
if (hasCommand("uv")) runStep("uv", ["venv", "--python", "3.12", rt.venvVideo]);
|
|
679
|
+
else runStep("python3", ["-m", "venv", rt.venvVideo]);
|
|
680
|
+
}
|
|
681
|
+
console.error("\u5B89\u88C5/\u5347\u7EA7 mmh3turbo \u2026");
|
|
682
|
+
if (hasCommand("uv")) {
|
|
683
|
+
runStep("uv", ["pip", "install", "--python", rt.pythonVideo, "--upgrade", "mmh3turbo"]);
|
|
684
|
+
} else {
|
|
685
|
+
runStep(rt.pythonVideo, ["-m", "pip", "install", "--upgrade", "mmh3turbo"]);
|
|
686
|
+
}
|
|
687
|
+
patchGgufFilename(rt.venvVideo);
|
|
688
|
+
if (opts.mirror) {
|
|
689
|
+
mirrorProvision();
|
|
690
|
+
} else {
|
|
691
|
+
console.log(`\u89C6\u9891\u8FD0\u884C\u65F6\u5C31\u7EEA: ${rt.mmh3turbo}`);
|
|
692
|
+
console.log("\u9996\u6B21 lmedia video gen \u4F1A\u81EA\u52A8\u4E0B\u8F7D H3 \u6743\u91CD\uFF08~33GB\uFF0C\u9700\u80FD\u76F4\u8FDE huggingface.co\uFF09\uFF1B\u56FD\u5185\u63A8\u8350\u6539\u7528: lmedia video setup --mirror");
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
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) => {
|
|
698
|
+
const res = opts.res;
|
|
699
|
+
const seconds = parseFloat(opts.seconds);
|
|
700
|
+
const steps = parseInt(opts.steps, 10);
|
|
701
|
+
const seed2 = parseInt(opts.seed, 10);
|
|
702
|
+
if (!RES_PRESETS.includes(res)) {
|
|
703
|
+
console.error(`--res \u4EC5\u652F\u6301 ${RES_PRESETS.join("|")}\uFF08\u5F53\u524D ${res}\uFF09`);
|
|
704
|
+
process.exit(2);
|
|
705
|
+
}
|
|
706
|
+
if (!Number.isFinite(seconds) || seconds < 1 || seconds > 15) {
|
|
707
|
+
console.error(`--seconds \u9700\u4E3A 1-15 \u7684\u6570\u5B57\uFF08\u5F53\u524D ${opts.seconds}\uFF09`);
|
|
708
|
+
process.exit(2);
|
|
709
|
+
}
|
|
710
|
+
if (!Number.isInteger(steps) || steps < 1 || steps > 50) {
|
|
711
|
+
console.error(`--steps \u9700\u4E3A 1-50 \u7684\u6574\u6570\uFF08\u5F53\u524D ${opts.steps}\uFF09`);
|
|
712
|
+
process.exit(2);
|
|
713
|
+
}
|
|
714
|
+
if (!Number.isInteger(seed2)) {
|
|
715
|
+
console.error(`--seed \u9700\u4E3A\u6574\u6570\uFF08\u5F53\u524D ${opts.seed}\uFF09`);
|
|
716
|
+
process.exit(2);
|
|
717
|
+
}
|
|
718
|
+
if (opts.firstFrame && !fs5.existsSync(opts.firstFrame)) {
|
|
719
|
+
console.error(`\u9996\u5E27\u56FE\u4E0D\u5B58\u5728: ${opts.firstFrame}`);
|
|
720
|
+
process.exit(2);
|
|
721
|
+
}
|
|
722
|
+
const rt = resolveVideoRuntime();
|
|
723
|
+
if (!fs5.existsSync(rt.mmh3turbo)) {
|
|
724
|
+
console.error(`mmh3turbo \u672A\u5B89\u88C5\uFF08${rt.venvVideo}\uFF09\u3002\u5148\u8FD0\u884C: lmedia video setup`);
|
|
725
|
+
process.exit(1);
|
|
726
|
+
}
|
|
727
|
+
if (!hasCommand("ffmpeg")) {
|
|
728
|
+
console.error("\u672A\u627E\u5230 ffmpeg\uFF08mp4 \u5C01\u88C5\u5FC5\u9700\uFF09\uFF1Abrew install ffmpeg \u540E\u91CD\u8BD5");
|
|
729
|
+
process.exit(1);
|
|
730
|
+
}
|
|
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`);
|
|
733
|
+
}
|
|
734
|
+
const runDir = path.join(os.homedir(), ".lmedia", "video-runs", `${Date.now()}`);
|
|
735
|
+
fs5.mkdirSync(runDir, { recursive: true });
|
|
736
|
+
const args = [
|
|
737
|
+
prompt,
|
|
738
|
+
"-r",
|
|
739
|
+
res,
|
|
740
|
+
"--seconds",
|
|
741
|
+
String(seconds),
|
|
742
|
+
"--steps",
|
|
743
|
+
String(steps),
|
|
744
|
+
"--seed",
|
|
745
|
+
String(seed2),
|
|
746
|
+
"-o",
|
|
747
|
+
runDir
|
|
748
|
+
];
|
|
749
|
+
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}`);
|
|
751
|
+
const t0 = Date.now();
|
|
752
|
+
const env = { ...process.env };
|
|
753
|
+
const weightsReady = fs5.existsSync(path.join(rt.weightsDir, "dit.bin"));
|
|
754
|
+
if (!weightsReady && env.HF_HUB_OFFLINE === "1") {
|
|
755
|
+
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
|
+
env.HF_HUB_OFFLINE = "0";
|
|
757
|
+
}
|
|
758
|
+
const code = await new Promise((resolve7) => {
|
|
759
|
+
const child = spawn(rt.mmh3turbo, args, { env });
|
|
760
|
+
child.stdout.on("data", (d) => process.stderr.write(d));
|
|
761
|
+
child.stderr.on("data", (d) => process.stderr.write(d));
|
|
762
|
+
child.on("error", (e) => {
|
|
763
|
+
console.error(`\u542F\u52A8 mmh3turbo \u5931\u8D25: ${e.message}`);
|
|
764
|
+
process.exit(1);
|
|
765
|
+
});
|
|
766
|
+
child.on("close", (c) => resolve7(c ?? -1));
|
|
767
|
+
});
|
|
768
|
+
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`);
|
|
770
|
+
process.exit(1);
|
|
771
|
+
}
|
|
772
|
+
const produced = path.join(runDir, "video.mp4");
|
|
773
|
+
if (!fs5.existsSync(produced)) {
|
|
774
|
+
console.error(`\u672A\u627E\u5230\u4EA7\u7269 ${produced}\uFF08\u8FD0\u884C\u76EE\u5F55\u5185\u5BB9\u4FDD\u7559\u4F9B\u6392\u67E5: ${runDir}\uFF09`);
|
|
775
|
+
process.exit(1);
|
|
776
|
+
}
|
|
777
|
+
const out = path.resolve(opts.out);
|
|
778
|
+
fs5.mkdirSync(path.dirname(out), { recursive: true });
|
|
779
|
+
try {
|
|
780
|
+
fs5.renameSync(produced, out);
|
|
781
|
+
} catch {
|
|
782
|
+
fs5.copyFileSync(produced, out);
|
|
783
|
+
fs5.unlinkSync(produced);
|
|
784
|
+
}
|
|
785
|
+
console.log(
|
|
786
|
+
JSON.stringify(
|
|
787
|
+
{
|
|
788
|
+
out,
|
|
789
|
+
seconds: Number(((Date.now() - t0) / 1e3).toFixed(1)),
|
|
790
|
+
runDir,
|
|
791
|
+
engine: "mmh3turbo",
|
|
792
|
+
res,
|
|
793
|
+
duration: seconds,
|
|
794
|
+
steps,
|
|
795
|
+
seed: seed2,
|
|
796
|
+
...opts.firstFrame ? { firstFrame: path.resolve(opts.firstFrame) } : {}
|
|
797
|
+
},
|
|
798
|
+
null,
|
|
799
|
+
2
|
|
800
|
+
)
|
|
801
|
+
);
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
function registerListRes(video) {
|
|
805
|
+
video.command("list-res").description("\u5217\u51FA mmh3turbo \u652F\u6301\u7684\u5206\u8FA8\u7387\u6863\u4F4D\uFF08\u900F\u4F20\uFF09").action(() => {
|
|
806
|
+
const rt = resolveVideoRuntime();
|
|
807
|
+
if (!fs5.existsSync(rt.mmh3turbo)) {
|
|
808
|
+
console.error("mmh3turbo \u672A\u5B89\u88C5\u3002\u5148\u8FD0\u884C: lmedia video setup");
|
|
809
|
+
process.exit(1);
|
|
810
|
+
}
|
|
811
|
+
const r = spawnSync(rt.mmh3turbo, ["--list-res"], { encoding: "utf8" });
|
|
812
|
+
if (r.status !== 0) {
|
|
813
|
+
console.error(`mmh3turbo --list-res \u9000\u51FA\u7801 ${r.status}`);
|
|
814
|
+
process.exit(1);
|
|
815
|
+
}
|
|
816
|
+
console.log((r.stdout ?? "").trim());
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
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");
|
|
821
|
+
registerSetup(video);
|
|
822
|
+
registerGen2(video);
|
|
823
|
+
registerListRes(video);
|
|
824
|
+
}
|
|
825
|
+
function registerLora(program2) {
|
|
826
|
+
const lora = program2.command("lora").description("LoRA \u6CE8\u518C\u8868\u7BA1\u7406");
|
|
827
|
+
lora.command("list").description("\u5217\u51FA\u5DF2\u6CE8\u518C LoRA").action(() => {
|
|
828
|
+
for (const l of loadRegistry()) {
|
|
829
|
+
const exists = fs5.existsSync(l.path) ? "\u2713" : "\u2717 \u6587\u4EF6\u7F3A\u5931";
|
|
830
|
+
console.log(
|
|
831
|
+
`${l.name} [${l.kind}] weight=${l.defaultWeight} trigger="${l.trigger}" ${exists}
|
|
832
|
+
${l.path}${l.note ? `
|
|
833
|
+
${l.note}` : ""}`
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
lora.command("add <name> <path>").description("\u6CE8\u518C\u65B0 LoRA").requiredOption("--kind <kind>", "style|character|speed").option("--trigger <text>", "\u89E6\u53D1\u8BCD", "").option("--weight <n>", "\u9ED8\u8BA4\u6743\u91CD", "1.0").option("--note <text>", "\u5907\u6CE8").action(
|
|
838
|
+
(name, p, opts) => {
|
|
839
|
+
addLora({
|
|
840
|
+
name,
|
|
841
|
+
path: path.resolve(p),
|
|
842
|
+
trigger: opts.trigger,
|
|
843
|
+
defaultWeight: parseFloat(opts.weight),
|
|
844
|
+
kind: opts.kind,
|
|
845
|
+
note: opts.note
|
|
846
|
+
});
|
|
847
|
+
console.log(`\u5DF2\u6CE8\u518C ${name}`);
|
|
848
|
+
}
|
|
849
|
+
);
|
|
850
|
+
}
|
|
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
|
+
var ManifestCorruptError = class extends Error {
|
|
861
|
+
};
|
|
862
|
+
var InvalidArgsError = class extends Error {
|
|
863
|
+
};
|
|
864
|
+
var SfxEnvError = class extends Error {
|
|
865
|
+
};
|
|
866
|
+
var KEY_RE = /^[a-z0-9-]+$/;
|
|
867
|
+
function defaultLibraryRoot() {
|
|
868
|
+
return path.join(os.homedir(), ".config", "limg", "sfx-library");
|
|
869
|
+
}
|
|
870
|
+
function resolveLibraryRoot(flag) {
|
|
871
|
+
if (flag && flag.trim()) return path.resolve(flag.trim());
|
|
872
|
+
const env = process.env.LMEDIA_SFX_LIB;
|
|
873
|
+
if (env && env.trim()) return path.resolve(env.trim());
|
|
874
|
+
return defaultLibraryRoot();
|
|
875
|
+
}
|
|
876
|
+
function manifestPath(root) {
|
|
877
|
+
return path.join(root, "index.json");
|
|
878
|
+
}
|
|
879
|
+
function emptyManifest() {
|
|
880
|
+
return { version: 1, generatedAt: (/* @__PURE__ */ new Date()).toISOString(), items: [] };
|
|
881
|
+
}
|
|
882
|
+
function readManifest(root) {
|
|
883
|
+
const p = manifestPath(root);
|
|
884
|
+
if (!fs5.existsSync(p)) return emptyManifest();
|
|
885
|
+
const raw = fs5.readFileSync(p, "utf-8");
|
|
886
|
+
let parsed;
|
|
887
|
+
try {
|
|
888
|
+
parsed = JSON.parse(raw);
|
|
889
|
+
} catch {
|
|
890
|
+
throw new ManifestCorruptError(
|
|
891
|
+
`\u97F3\u6548\u5E93\u6E05\u5355\u635F\u574F\uFF0C\u65E0\u6CD5\u89E3\u6790: ${p}
|
|
892
|
+
\u8BF7\u624B\u5DE5\u4FEE\u590D\u6216\u79FB\u8D70\u8BE5\u6587\u4EF6\uFF08\u4E0D\u4F1A\u81EA\u52A8\u91CD\u5EFA\u4EE5\u514D\u4E22\u8D26\uFF09\u3002`
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
const m = parsed;
|
|
896
|
+
if (!m || typeof m !== "object" || !Array.isArray(m.items)) {
|
|
897
|
+
throw new ManifestCorruptError(
|
|
898
|
+
`\u97F3\u6548\u5E93\u6E05\u5355\u7ED3\u6784\u4E0D\u7B26\u5408\u9884\u671F\uFF08\u7F3A items \u6570\u7EC4\uFF09: ${p}
|
|
899
|
+
\u8BF7\u624B\u5DE5\u4FEE\u590D\uFF08\u4E0D\u4F1A\u81EA\u52A8\u91CD\u5EFA\u4EE5\u514D\u4E22\u8D26\uFF09\u3002`
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
return { version: 1, generatedAt: m.generatedAt ?? "", items: m.items };
|
|
903
|
+
}
|
|
904
|
+
function writeManifest(root, manifest) {
|
|
905
|
+
fs5.mkdirSync(root, { recursive: true });
|
|
906
|
+
const p = manifestPath(root);
|
|
907
|
+
const tmp = `${p}.${process.pid}.tmp`;
|
|
908
|
+
fs5.writeFileSync(tmp, `${JSON.stringify(manifest, null, 1)}
|
|
909
|
+
`, "utf-8");
|
|
910
|
+
fs5.renameSync(tmp, p);
|
|
911
|
+
}
|
|
912
|
+
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));
|
|
916
|
+
if (created) writeManifest(root, emptyManifest());
|
|
917
|
+
return { root: path.resolve(root), manifest: manifestPath(root), created };
|
|
918
|
+
}
|
|
919
|
+
function listEntries(root, filter = {}) {
|
|
920
|
+
return readManifest(root).items.filter(
|
|
921
|
+
(i) => (!filter.type || i.type === filter.type) && (!filter.status || i.status === filter.status)
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
function sha256File(file) {
|
|
925
|
+
return createHash("sha256").update(fs5.readFileSync(file)).digest("hex");
|
|
926
|
+
}
|
|
927
|
+
function assertFfmpeg() {
|
|
928
|
+
for (const cmd of ["ffmpeg", "ffprobe"]) {
|
|
929
|
+
if (spawnSync("which", [cmd], { stdio: "ignore" }).status !== 0) {
|
|
930
|
+
throw new SfxEnvError(`\u7F3A\u5C11 ${cmd}\uFF08brew install ffmpeg\uFF09\uFF0C\u97F3\u6548\u5E93\u5165\u5E93\u9700\u8981\u8F6C\u7801/\u63A2\u9488`);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
function probeAudio(file) {
|
|
935
|
+
let stdout;
|
|
936
|
+
try {
|
|
937
|
+
stdout = execFileSync(
|
|
938
|
+
"ffprobe",
|
|
939
|
+
["-v", "error", "-show_entries", "format=duration:stream=sample_rate", "-of", "json", file],
|
|
940
|
+
{ encoding: "utf-8" }
|
|
941
|
+
);
|
|
942
|
+
} catch {
|
|
943
|
+
throw new SfxEnvError(`ffprobe \u65E0\u6CD5\u89E3\u6790\u97F3\u9891: ${file}`);
|
|
944
|
+
}
|
|
945
|
+
const j = JSON.parse(stdout);
|
|
946
|
+
return {
|
|
947
|
+
duration: parseFloat(j.format?.duration ?? "0"),
|
|
948
|
+
sampleRate: parseInt(j.streams?.[0]?.sample_rate ?? "0", 10)
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
function probeAudioDurationSync(file) {
|
|
952
|
+
try {
|
|
953
|
+
return probeAudio(file).duration;
|
|
954
|
+
} catch {
|
|
955
|
+
return -1;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
function transcode(file, dst) {
|
|
959
|
+
try {
|
|
960
|
+
execFileSync(
|
|
961
|
+
"ffmpeg",
|
|
962
|
+
["-y", "-v", "error", "-i", file, "-ar", "44100", "-ac", "1", "-b:a", "160k", dst],
|
|
963
|
+
{ stdio: ["ignore", "ignore", "pipe"], encoding: "utf-8" }
|
|
964
|
+
);
|
|
965
|
+
} catch (e) {
|
|
966
|
+
throw new SfxEnvError(`\u8F6C\u7801\u5931\u8D25: ${file} \u2192 ${dst}
|
|
967
|
+
${(e.stderr ?? "").slice(-400)}`);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
function addEntry(root, opts) {
|
|
971
|
+
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
|
+
const type = opts.type ?? "sfx";
|
|
973
|
+
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}`);
|
|
975
|
+
const hash = sha256File(opts.file);
|
|
976
|
+
const dup = readManifest(root).items.find((i) => i.content_hash === hash);
|
|
977
|
+
if (dup) return { item: dup, duplicated: true };
|
|
978
|
+
assertFfmpeg();
|
|
979
|
+
initLibrary(root);
|
|
980
|
+
const dst = path.join(root, type, `${opts.key}.mp3`);
|
|
981
|
+
transcode(opts.file, dst);
|
|
982
|
+
let probe;
|
|
983
|
+
try {
|
|
984
|
+
probe = probeAudio(dst);
|
|
985
|
+
} catch (e) {
|
|
986
|
+
fs5.rmSync(dst, { force: true });
|
|
987
|
+
throw e;
|
|
988
|
+
}
|
|
989
|
+
const item = {
|
|
990
|
+
key: opts.key,
|
|
991
|
+
library: opts.library?.trim() || "default",
|
|
992
|
+
type,
|
|
993
|
+
path: dst,
|
|
994
|
+
duration: Math.round(probe.duration * 1e3) / 1e3,
|
|
995
|
+
sample_rate: probe.sampleRate,
|
|
996
|
+
tags: opts.tags ?? [],
|
|
997
|
+
title: opts.title ?? "",
|
|
998
|
+
note: opts.note ?? "",
|
|
999
|
+
status: "ready",
|
|
1000
|
+
content_hash: hash,
|
|
1001
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1002
|
+
};
|
|
1003
|
+
const next = readManifest(root);
|
|
1004
|
+
const idx = next.items.findIndex((i) => i.type === type && i.key === item.key);
|
|
1005
|
+
if (idx >= 0) next.items[idx] = item;
|
|
1006
|
+
else next.items.push(item);
|
|
1007
|
+
writeManifest(root, next);
|
|
1008
|
+
return { item, duplicated: false };
|
|
1009
|
+
}
|
|
1010
|
+
function removeEntry(root, key, type) {
|
|
1011
|
+
const manifest = readManifest(root);
|
|
1012
|
+
const idx = manifest.items.findIndex((i) => i.key === key && (!type || i.type === type));
|
|
1013
|
+
if (idx < 0) return null;
|
|
1014
|
+
const [removed] = manifest.items.splice(idx, 1);
|
|
1015
|
+
writeManifest(root, manifest);
|
|
1016
|
+
return removed;
|
|
1017
|
+
}
|
|
1018
|
+
function verifyLibrary(root) {
|
|
1019
|
+
const items = readManifest(root).items;
|
|
1020
|
+
const problems = [];
|
|
1021
|
+
for (const item of items) {
|
|
1022
|
+
const issues = [];
|
|
1023
|
+
if (!fs5.existsSync(item.path)) {
|
|
1024
|
+
issues.push(`\u6587\u4EF6\u7F3A\u5931: ${item.path}`);
|
|
1025
|
+
} else {
|
|
1026
|
+
const dur = probeAudioDurationSync(item.path);
|
|
1027
|
+
if (dur < 0) issues.push(`\u65E0\u6CD5\u8BFB\u53D6\u65F6\u957F: ${item.path}`);
|
|
1028
|
+
else if (Math.abs(dur - item.duration) > 0.1) {
|
|
1029
|
+
issues.push(`\u65F6\u957F\u6F02\u79FB: \u6E05\u5355 ${item.duration.toFixed(2)}s / \u5B9E\u9645 ${dur.toFixed(2)}s`);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
if (item.status !== "ready") issues.push(`\u72B6\u6001\u5F02\u5E38: ${item.status}`);
|
|
1033
|
+
if (issues.length) problems.push({ key: item.key, type: item.type, issues });
|
|
1034
|
+
}
|
|
1035
|
+
return { root: path.resolve(root), items, problems };
|
|
1036
|
+
}
|
|
1037
|
+
function num(v, name) {
|
|
1038
|
+
const n = parseFloat(v);
|
|
1039
|
+
if (!Number.isFinite(n)) throw new InvalidArgsError(`${name} \u9700\u4E3A\u6570\u5B57: ${v}`);
|
|
1040
|
+
return n;
|
|
1041
|
+
}
|
|
1042
|
+
function inRange(n, name, min, max) {
|
|
1043
|
+
if (n < min || n > max) throw new InvalidArgsError(`${name} \u53D6\u503C ${min} - ${max}: ${n}`);
|
|
1044
|
+
return n;
|
|
1045
|
+
}
|
|
1046
|
+
function parseInteger(v, name, min, max, def) {
|
|
1047
|
+
if (v === void 0 || v === "") return def;
|
|
1048
|
+
const n = Number(v);
|
|
1049
|
+
if (!Number.isInteger(n)) throw new InvalidArgsError(`${name} \u9700\u4E3A\u6574\u6570: ${v}`);
|
|
1050
|
+
return inRange(n, name, min, max);
|
|
1051
|
+
}
|
|
1052
|
+
function parseFloatIn(v, name, min, max, def) {
|
|
1053
|
+
if (v === void 0 || v === "") return def;
|
|
1054
|
+
return inRange(num(v, name), name, min, max);
|
|
1055
|
+
}
|
|
1056
|
+
var parseRolls = (v) => parseInteger(v, "--rolls", 1, 10, 3);
|
|
1057
|
+
function parseThreshDb(v, def) {
|
|
1058
|
+
if (v === void 0 || v === "") return def;
|
|
1059
|
+
if (!/^-\d+dB$/.test(v)) throw new InvalidArgsError(`--thresh \u9700\u5F62\u5982 -35dB: ${v}`);
|
|
1060
|
+
return parseFloat(v.slice(0, -2));
|
|
1061
|
+
}
|
|
1062
|
+
var parsePad = (v) => parseFloatIn(v, "--pad", 0.01, 2, 0.15);
|
|
1063
|
+
var parseCap = (v) => parseFloatIn(v, "--cap", 0.5, 10, 3.5);
|
|
1064
|
+
var parseTarget = (v) => parseFloatIn(v, "--target", -60, -0.5, -6);
|
|
1065
|
+
var parseLoudness = (v) => parseFloatIn(v, "--loudness", -40, -5, -23);
|
|
1066
|
+
function parseMinDur(v) {
|
|
1067
|
+
return parseFloatIn(v, "--min-dur", 0.05, 3600, 0.4);
|
|
1068
|
+
}
|
|
1069
|
+
function parseMaxDur(v, minDur = 0.4) {
|
|
1070
|
+
const max = parseFloatIn(v, "--max-dur", 0.1, 3600, 4);
|
|
1071
|
+
if (minDur >= max) throw new InvalidArgsError(`--min-dur\uFF08${minDur}\uFF09\u9700\u5C0F\u4E8E --max-dur\uFF08${max}\uFF09`);
|
|
1072
|
+
return max;
|
|
1073
|
+
}
|
|
1074
|
+
function assertMutuallyExclusive(a, b, nameA, nameB) {
|
|
1075
|
+
if (a !== void 0 && b !== void 0) {
|
|
1076
|
+
throw new InvalidArgsError(`${nameA} \u4E0E ${nameB} \u4E92\u65A5\uFF0C\u53EA\u7ED9\u5176\u4E00`);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
function assertFilesExist(files, flagName = "\u8F93\u5165\u6587\u4EF6") {
|
|
1080
|
+
const missing = files.filter((f) => !fs5.existsSync(f));
|
|
1081
|
+
if (missing.length) {
|
|
1082
|
+
throw new InvalidArgsError(`${flagName}\u4E0D\u5B58\u5728: ${missing.join(", ")}`);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
function validateKey(key) {
|
|
1086
|
+
if (!KEY_RE.test(key)) throw new InvalidArgsError(`key \u4EC5\u5141\u8BB8\u5C0F\u5199\u5B57\u6BCD/\u6570\u5B57/\u77ED\u6A2A\u7EBF: ${key}`);
|
|
1087
|
+
return key;
|
|
1088
|
+
}
|
|
1089
|
+
function parseBatchManifest(text, keysFilter) {
|
|
1090
|
+
if (!text.trim()) {
|
|
1091
|
+
throw new ManifestCorruptError('\u6279\u6E05\u5355\u4E3A\u7A7A\uFF08\u9700 [{"key":"bear","prompt":"..."}]\uFF09');
|
|
1092
|
+
}
|
|
1093
|
+
let parsed;
|
|
1094
|
+
try {
|
|
1095
|
+
parsed = JSON.parse(text);
|
|
1096
|
+
} catch (e) {
|
|
1097
|
+
throw new ManifestCorruptError(`\u6279\u6E05\u5355\u89E3\u6790\u5931\u8D25\uFF08\u975E\u6CD5 JSON\uFF09: ${e.message}`);
|
|
1098
|
+
}
|
|
1099
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
1100
|
+
throw new ManifestCorruptError('\u6279\u6E05\u5355\u9700\u4E3A\u975E\u7A7A\u6570\u7EC4 [{"key":"bear","prompt":"..."}]');
|
|
1101
|
+
}
|
|
1102
|
+
const items = [];
|
|
1103
|
+
for (const raw of parsed) {
|
|
1104
|
+
const it = raw;
|
|
1105
|
+
if (!it || typeof it !== "object" || typeof it.key !== "string" || typeof it.prompt !== "string" || !it.prompt.trim()) {
|
|
1106
|
+
throw new ManifestCorruptError("\u6279\u6E05\u5355\u6761\u76EE\u9700\u542B\u975E\u7A7A key \u4E0E prompt \u5B57\u7B26\u4E32");
|
|
1107
|
+
}
|
|
1108
|
+
items.push({ key: it.key, prompt: it.prompt });
|
|
1109
|
+
}
|
|
1110
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1111
|
+
for (const it of items) {
|
|
1112
|
+
validateKey(it.key);
|
|
1113
|
+
if (seen.has(it.key)) throw new InvalidArgsError(`\u6279\u6E05\u5355 key \u91CD\u590D: ${it.key}`);
|
|
1114
|
+
seen.add(it.key);
|
|
1115
|
+
}
|
|
1116
|
+
if (!keysFilter?.length) return items;
|
|
1117
|
+
const unknown = keysFilter.filter((k) => !seen.has(k));
|
|
1118
|
+
if (unknown.length) throw new InvalidArgsError(`--keys \u542B\u6E05\u5355\u5916 key: ${unknown.join(", ")}`);
|
|
1119
|
+
const wanted = new Set(keysFilter);
|
|
1120
|
+
const picked = items.filter((it) => wanted.has(it.key));
|
|
1121
|
+
if (!picked.length) throw new InvalidArgsError("--keys \u8FC7\u6EE4\u540E\u65E0\u53EF\u7528\u6761\u76EE");
|
|
1122
|
+
return picked;
|
|
1123
|
+
}
|
|
1124
|
+
function parseAbGroups(text) {
|
|
1125
|
+
if (!text.trim()) throw new ManifestCorruptError('A/B groups \u4E3A\u7A7A\uFF08\u9700 [{"name":"...","candidates":["a.wav"]}]\uFF09');
|
|
1126
|
+
let parsed;
|
|
1127
|
+
try {
|
|
1128
|
+
parsed = JSON.parse(text);
|
|
1129
|
+
} catch (e) {
|
|
1130
|
+
throw new ManifestCorruptError(`A/B groups \u89E3\u6790\u5931\u8D25\uFF08\u975E\u6CD5 JSON\uFF09: ${e.message}`);
|
|
1131
|
+
}
|
|
1132
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
1133
|
+
throw new ManifestCorruptError('A/B groups \u9700\u4E3A\u975E\u7A7A\u6570\u7EC4 [{"name":"...","candidates":[...]}]');
|
|
1134
|
+
}
|
|
1135
|
+
return parsed.map((raw, i) => {
|
|
1136
|
+
const g = raw;
|
|
1137
|
+
if (!g || typeof g !== "object" || !Array.isArray(g.candidates) || g.candidates.some((c) => typeof c !== "string" || !c.trim())) {
|
|
1138
|
+
throw new ManifestCorruptError(`A/B groups \u7B2C ${i + 1} \u7EC4\u9700\u542B candidates \u8DEF\u5F84\u6570\u7EC4`);
|
|
1139
|
+
}
|
|
1140
|
+
return { name: typeof g.name === "string" && g.name.trim() ? g.name : `\u7EC4${i + 1}`, candidates: g.candidates };
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// src/commands/sfx.ts
|
|
1145
|
+
var MODEL = "mispeech/Dasheng-AudioGen";
|
|
1146
|
+
var TOKENIZER = "mispeech/dashengtokenizer";
|
|
1147
|
+
function fail(e) {
|
|
1148
|
+
if (e instanceof InvalidArgsError) {
|
|
1149
|
+
console.error(`\u2717 ${e.message}`);
|
|
1150
|
+
process.exit(2);
|
|
1151
|
+
}
|
|
1152
|
+
console.error(`\u2717 ${e instanceof Error ? e.message : String(e)}`);
|
|
1153
|
+
process.exit(1);
|
|
1154
|
+
}
|
|
1155
|
+
function requireAudioRuntime() {
|
|
1156
|
+
const rt = resolveAudioRuntime();
|
|
1157
|
+
if (!fs5.existsSync(rt.pythonAudio)) {
|
|
1158
|
+
throw new SfxEnvError(
|
|
1159
|
+
`\u97F3\u6548 venv \u672A\u627E\u5230: ${rt.pythonAudio}
|
|
1160
|
+
\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`
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
return rt;
|
|
1164
|
+
}
|
|
1165
|
+
function requireFfmpeg() {
|
|
1166
|
+
if (!hasCommand("ffmpeg") || !hasCommand("ffprobe")) {
|
|
1167
|
+
throw new SfxEnvError("\u7F3A\u5C11 ffmpeg/ffprobe\uFF08brew install ffmpeg\uFF09\uFF0C\u97F3\u9891\u540E\u5904\u7406\u4F9D\u8D56");
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
async function runSfxOp(rt, payload) {
|
|
1171
|
+
try {
|
|
1172
|
+
return await runPython(rt.pythonAudio, path.join(rt.pythonDir, "sfx.py"), payload);
|
|
1173
|
+
} catch (e) {
|
|
1174
|
+
throw new SfxEnvError(e.message);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
var n1 = (v) => v === void 0 ? "?" : v.toFixed(1);
|
|
1178
|
+
var pad = (s, w) => s.length >= w ? s : s + " ".repeat(w - s.length);
|
|
1179
|
+
var padL = (s, w) => s.length >= w ? s : " ".repeat(w - s.length) + s;
|
|
1180
|
+
function registerGen3(sfx) {
|
|
1181
|
+
sfx.command("gen <prompt>").description("\u6587\u751F\u97F3\u6548\u3002prompt \u7528\u82F1\u6587\u63CF\u8FF0\u58F0\u97F3\u573A\u666F\uFF08\u975E\u62DF\u58F0\u8BCD\uFF09").option("-o, --out <file>", "\u8F93\u51FA wav \u8DEF\u5F84", "sfx.wav").option("--rolls <n>", "\u57FA\u7840\u751F\u6210\u6B21\u6570\uFF08\u5168\u5E9F\u81EA\u52A8\u52A0\u63B7\u22642\uFF09", "3").option("--keep-rolls", "\u4FDD\u7559\u5168\u90E8\u63B7\u6B21\u5230 <out>.rolls/ \u76EE\u5F55").option("--full", "\u8DF3\u8FC7\u526A\u88C1\uFF0C\u4FDD\u7559 10s \u539F\u59CB\u751F\u6210").action(async (prompt, opts) => {
|
|
1182
|
+
try {
|
|
1183
|
+
const rolls = parseRolls(opts.rolls);
|
|
1184
|
+
const rt = requireAudioRuntime();
|
|
1185
|
+
requireFfmpeg();
|
|
1186
|
+
const out = path.resolve(opts.out ?? "sfx.wav");
|
|
1187
|
+
fs5.mkdirSync(path.dirname(out), { recursive: true });
|
|
1188
|
+
console.error(`\u{1F3AC} sfx gen\uFF08${rolls} \u63B7 + \u8D28\u91CF\u95E8 + \u526A\u88C1\uFF09\u2026`);
|
|
1189
|
+
const res = await runSfxOp(rt, {
|
|
1190
|
+
prompt,
|
|
1191
|
+
out,
|
|
1192
|
+
rolls,
|
|
1193
|
+
keepRolls: !!opts.keepRolls,
|
|
1194
|
+
trim: !opts.full
|
|
1195
|
+
});
|
|
1196
|
+
const r = res;
|
|
1197
|
+
if (r.out) {
|
|
1198
|
+
console.error(`${r.gatePassed ? "\u2713" : "\u26A0\uFE0F \u65E0\u5408\u683C\u63B7\uFF08\u53D6\u5CF0\u503C\u6700\u9AD8\uFF0C\u5EFA\u8BAE\u91CD\u8DD1\uFF09"} best=r${r.bestRoll} peak=${r.peak}dB snr=${r.snr}dB \u2192 ${r.out} (${r.dur}s)`);
|
|
1199
|
+
}
|
|
1200
|
+
} catch (e) {
|
|
1201
|
+
fail(e);
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
function registerBatch(sfx) {
|
|
1206
|
+
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
|
+
try {
|
|
1208
|
+
const rolls = parseRolls(opts.rolls);
|
|
1209
|
+
const text = opts.manifest === "-" ? fs5.readFileSync(0, "utf-8") : (() => {
|
|
1210
|
+
if (!fs5.existsSync(opts.manifest)) {
|
|
1211
|
+
throw new InvalidArgsError(`\u6279\u6E05\u5355\u4E0D\u5B58\u5728: ${opts.manifest}`);
|
|
1212
|
+
}
|
|
1213
|
+
return fs5.readFileSync(opts.manifest, "utf-8");
|
|
1214
|
+
})();
|
|
1215
|
+
const items = parseBatchManifest(
|
|
1216
|
+
text,
|
|
1217
|
+
opts.keys ? opts.keys.split(",").map((k) => k.trim()).filter(Boolean) : void 0
|
|
1218
|
+
);
|
|
1219
|
+
const rt = requireAudioRuntime();
|
|
1220
|
+
requireFfmpeg();
|
|
1221
|
+
const outDir = path.resolve(opts.out);
|
|
1222
|
+
fs5.mkdirSync(outDir, { recursive: true });
|
|
1223
|
+
console.error(`\u{1F3AC} sfx batch\uFF08${items.length} key \xD7 ${rolls} \u63B7\uFF09\u2192 ${outDir}`);
|
|
1224
|
+
const res = await runSfxOp(rt, {
|
|
1225
|
+
op: "batch",
|
|
1226
|
+
items,
|
|
1227
|
+
outDir,
|
|
1228
|
+
rolls,
|
|
1229
|
+
keepRolls: !!opts.keepRolls,
|
|
1230
|
+
trim: !opts.full
|
|
1231
|
+
});
|
|
1232
|
+
const nPass = res.items.filter((i) => i.anyPass).length;
|
|
1233
|
+
console.error(`\u2713 \u5B8C\u6210 ${nPass}/${res.items.length} key \u6709\u5408\u683C\u751F\u6210\uFF08${res.genSec}s\uFF09\u2192 ${path.join(res.dir, "report.json")}`);
|
|
1234
|
+
console.log(JSON.stringify(res, null, 2));
|
|
1235
|
+
} catch (e) {
|
|
1236
|
+
fail(e);
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
function registerTrim(sfx) {
|
|
1241
|
+
sfx.command("trim <files...>").description("\u53BB\u9996\u5C3E\u9759\u97F3\uFF08\u4E24\u7EA7\u9608\u503C\uFF09+ \u5185\u90E8\u7C07\u622A\u65AD \u2192 <name>.trim.wav + <name>.short.wav + <name>.ops.json").option("--thresh <v>", "\u9759\u97F3\u9608\u503C\uFF08\u5F62\u5982 -35dB\uFF09", "-35dB").option("--pad <sec>", "\u526A\u88C1\u524D\u540E\u4FDD\u7559\uFF08\u79D2\uFF09", "0.15").action(async (files, opts) => {
|
|
1242
|
+
try {
|
|
1243
|
+
assertFilesExist(files);
|
|
1244
|
+
const thresh = parseThreshDb(opts.thresh, -35);
|
|
1245
|
+
const padSec = parsePad(opts.pad);
|
|
1246
|
+
requireFfmpeg();
|
|
1247
|
+
const rt = requireAudioRuntime();
|
|
1248
|
+
console.error(`\xB7 \u526A\u88C1 ${files.length} \u4E2A\u6587\u4EF6\uFF08thresh ${opts.thresh} / pad ${padSec}s\uFF09`);
|
|
1249
|
+
const res = await runSfxOp(rt, {
|
|
1250
|
+
op: "trim",
|
|
1251
|
+
files,
|
|
1252
|
+
threshDb: thresh,
|
|
1253
|
+
pad: padSec
|
|
1254
|
+
});
|
|
1255
|
+
for (const it of res.items) {
|
|
1256
|
+
console.log(`\u2713 ${it.in} \u2192 ${it.trim} (dur ${n1(it.durIn)}\u2192${n1(it.durTrim)}, peak ${n1(it.peakIn)}\u2192${n1(it.peakOut)})`);
|
|
1257
|
+
console.error(`\xB7 short: ${it.short}\uFF08${n1(it.durShort)}s\uFF09`);
|
|
1258
|
+
}
|
|
1259
|
+
} catch (e) {
|
|
1260
|
+
fail(e);
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
function registerRecut(sfx) {
|
|
1265
|
+
sfx.command("recut <files...>").description("\u7075\u654F\u91CD\u526A\uFF08-40dB/0.45s \u95F4\u9699 + \u786C\u5E3D\uFF09+ \u6BB5\u5185\u5F52\u4E00 \u2192 \u8986\u5199 <name>.short.wav").option("--thresh <v>", "\u9759\u97F3\u9608\u503C\uFF08\u5F62\u5982 -40dB\uFF09", "-40dB").option("--cap <sec>", "\u6210\u54C1\u65F6\u957F\u786C\u5E3D\uFF080.5-10s\uFF09", "3.5").action(async (files, opts) => {
|
|
1266
|
+
try {
|
|
1267
|
+
assertFilesExist(files);
|
|
1268
|
+
const thresh = parseThreshDb(opts.thresh, -40);
|
|
1269
|
+
const cap = parseCap(opts.cap);
|
|
1270
|
+
requireFfmpeg();
|
|
1271
|
+
const rt = requireAudioRuntime();
|
|
1272
|
+
console.error(`\xB7 \u91CD\u526A ${files.length} \u4E2A\u6587\u4EF6\uFF08thresh ${opts.thresh} / cap ${cap}s\uFF09`);
|
|
1273
|
+
const res = await runSfxOp(rt, {
|
|
1274
|
+
op: "recut",
|
|
1275
|
+
files,
|
|
1276
|
+
threshDb: thresh,
|
|
1277
|
+
cap
|
|
1278
|
+
});
|
|
1279
|
+
for (const it of res.items) {
|
|
1280
|
+
console.log(`\u2713 ${it.in} \u2192 ${it.short} (dur ${n1(it.durIn)}\u2192${n1(it.durOut)}, peak ${n1(it.peakIn)}\u2192${n1(it.peakOut)})`);
|
|
1281
|
+
}
|
|
1282
|
+
} catch (e) {
|
|
1283
|
+
fail(e);
|
|
1284
|
+
}
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
function registerNormalize(sfx) {
|
|
1288
|
+
sfx.command("normalize <files...>").description("\u54CD\u5EA6\u5F52\u4E00\uFF1A\u9ED8\u8BA4\u4E24\u904D\u5CF0\u503C\u5F52\u4E00 -6dBFS\uFF1B--loudness \u8D70 loudnorm \u4E24\u904D\uFF08ambient\uFF09").option("--target <dbfs>", "\u5CF0\u503C\u76EE\u6807 dBFS\uFF08-60 - -0.5\uFF0C\u7F3A\u7701 -6\uFF09").option("--loudness <lufs>", "\u7EFC\u5408\u54CD\u5EA6\u76EE\u6807 LUFS\uFF08-40 - -5\uFF0C\u4E0E --target \u4E92\u65A5\uFF09").option("--out-dir <dir>", "\u8F93\u51FA\u76EE\u5F55\uFF08\u7F3A\u7701\u539F\u5730\u8986\u5199\uFF09").action(async (files, opts) => {
|
|
1289
|
+
try {
|
|
1290
|
+
assertFilesExist(files);
|
|
1291
|
+
assertMutuallyExclusive(opts.target, opts.loudness, "--target", "--loudness");
|
|
1292
|
+
const loudness = opts.loudness !== void 0 ? parseLoudness(opts.loudness) : void 0;
|
|
1293
|
+
const target = opts.target !== void 0 ? parseTarget(opts.target) : -6;
|
|
1294
|
+
requireFfmpeg();
|
|
1295
|
+
const rt = requireAudioRuntime();
|
|
1296
|
+
const outDir = opts.outDir ? path.resolve(opts.outDir) : void 0;
|
|
1297
|
+
console.error(`\xB7 \u5F52\u4E00 ${files.length} \u4E2A\u6587\u4EF6\uFF08${loudness !== void 0 ? `loudnorm ${loudness} LUFS` : `\u5CF0\u503C ${target} dBFS`}\uFF09`);
|
|
1298
|
+
const res = await runSfxOp(rt, {
|
|
1299
|
+
op: "normalize",
|
|
1300
|
+
files,
|
|
1301
|
+
target,
|
|
1302
|
+
loudness,
|
|
1303
|
+
outDir
|
|
1304
|
+
});
|
|
1305
|
+
for (const it of res.items) {
|
|
1306
|
+
if (it.skipped) {
|
|
1307
|
+
console.log(`\xB7 ${it.in} \u2192 ${it.out}\uFF08${it.reason}\uFF09`);
|
|
1308
|
+
} else if (it.mode === "loudness") {
|
|
1309
|
+
console.log(`\u2713 ${it.in} \u2192 ${it.out} (dur ${n1(it.durIn)}\u2192${n1(it.durOut)}, lufs ${n1(it.lufsIn)}\u2192${n1(it.lufsOut)})`);
|
|
1310
|
+
} else {
|
|
1311
|
+
console.log(`\u2713 ${it.in} \u2192 ${it.out} (dur ${n1(it.durIn)}\u2192${n1(it.durOut)}, peak ${n1(it.peakIn)}\u2192${n1(it.peakOut)})`);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
} catch (e) {
|
|
1315
|
+
fail(e);
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
function registerAccept(sfx) {
|
|
1320
|
+
sfx.command("accept <files...>").description("\u91CF\u5316\u9A8C\u6536\uFF1A\u65F6\u957F/\u5CF0\u503C/\u5747\u503C\u62A5\u8868\uFF08\u4EFB\u4E00 flag \u547D\u4E2D exit 1\uFF0C\u4EBA\u5DE5\u7EC8\u5BA1\u524D\u7F6E\uFF09").option("--max-dur <sec>", "\u8FC7\u957F\u9608\u503C\uFF08\u79D2\uFF09", "4.0").option("--min-dur <sec>", "\u8FC7\u77ED\u9608\u503C\uFF08\u79D2\uFF09", "0.4").action(async (files, opts) => {
|
|
1321
|
+
try {
|
|
1322
|
+
assertFilesExist(files);
|
|
1323
|
+
const minDur = parseMinDur(opts.minDur);
|
|
1324
|
+
const maxDur = parseMaxDur(opts.maxDur, minDur);
|
|
1325
|
+
requireFfmpeg();
|
|
1326
|
+
const rt = requireAudioRuntime();
|
|
1327
|
+
const res = await runSfxOp(rt, {
|
|
1328
|
+
op: "accept",
|
|
1329
|
+
files,
|
|
1330
|
+
maxDur,
|
|
1331
|
+
minDur
|
|
1332
|
+
});
|
|
1333
|
+
console.error(`${pad("\u6587\u4EF6", 42)}${padL("\u65F6\u957F", 7)}${padL("\u5CF0\u503C", 10)}${padL("\u5747\u503C", 10)} flag`);
|
|
1334
|
+
for (const it of res.items) {
|
|
1335
|
+
const name = path.basename(it.path);
|
|
1336
|
+
const flagText = it.pass ? it.flags.join(",") : `\u2717 ${it.flags.join(",")}`;
|
|
1337
|
+
console.log(
|
|
1338
|
+
`${pad(name, 42)}${padL(`${n1(it.dur)}s`, 7)}${padL(`${n1(it.peak)}dB`, 10)}${padL(`${n1(it.mean)}dB`, 10)} ${pad(flagText, 16)} status=${it.pass ? "pass" : "fail"}`
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
const nPass = res.items.filter((i) => i.pass).length;
|
|
1342
|
+
console.log(`${nPass}/${res.items.length} \u901A\u8FC7`);
|
|
1343
|
+
if (nPass !== res.items.length) process.exit(1);
|
|
1344
|
+
} catch (e) {
|
|
1345
|
+
fail(e);
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
function registerAb(sfx) {
|
|
1350
|
+
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
|
+
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"));
|
|
1354
|
+
const candidates = groups.flatMap((g) => g.candidates);
|
|
1355
|
+
assertFilesExist(candidates, "\u5019\u9009\u97F3\u9891");
|
|
1356
|
+
const rt = requireAudioRuntime();
|
|
1357
|
+
const out = path.resolve(opts.out);
|
|
1358
|
+
fs5.mkdirSync(path.dirname(out), { recursive: true });
|
|
1359
|
+
const res = await runSfxOp(rt, { op: "abpage", groups, out });
|
|
1360
|
+
console.error(`\xB7 \u5019\u9009\u97F3\u9891\u5DF2\u62F7\u8D1D\u5230 ${path.join(path.dirname(out), "ab_files")}`);
|
|
1361
|
+
console.log(`\u2713 ${res.out}\uFF08${res.groups} \u7EC4 / ${res.candidates} \u5019\u9009\uFF09`);
|
|
1362
|
+
} catch (e) {
|
|
1363
|
+
fail(e);
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
function registerSetup2(sfx) {
|
|
1368
|
+
sfx.command("setup").description("\u5E42\u7B49\u521B\u5EFA\u97F3\u6548 venv\uFF08uv venv .venv-audio + torch/transformers<5 \u7B49\uFF09").action(async () => {
|
|
1369
|
+
try {
|
|
1370
|
+
const rt = resolveAudioRuntime();
|
|
1371
|
+
if (spawnSync("which", ["uv"], { stdio: "ignore" }).status !== 0) {
|
|
1372
|
+
throw new SfxEnvError("\u7F3A\u5C11 uv\uFF08curl -LsSf https://astral.sh/uv/install.sh | sh\uFF09\uFF0Csfx setup \u4F9D\u8D56");
|
|
1373
|
+
}
|
|
1374
|
+
const deps = ["torch", "torchaudio", "transformers<5", "einops", "soundfile"];
|
|
1375
|
+
if (fs5.existsSync(rt.pythonAudio)) {
|
|
1376
|
+
const ok = spawnSync(rt.pythonAudio, ["-c", "import torch, torchaudio, transformers, einops, soundfile"], { stdio: "ignore" }).status === 0;
|
|
1377
|
+
if (ok) {
|
|
1378
|
+
console.error(`\u2713 \u97F3\u6548 venv \u5DF2\u5C31\u7EEA: ${rt.pythonAudio}`);
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
console.error("\xB7 venv \u5DF2\u5B58\u5728\u4F46\u4F9D\u8D56\u4E0D\u5168 \u2192 \u8865\u88C5");
|
|
1382
|
+
} else {
|
|
1383
|
+
console.error(`\xB7 \u521B\u5EFA venv: ${rt.root}/.venv-audio\uFF08python 3.11\uFF09`);
|
|
1384
|
+
const r2 = spawnSync("uv", ["venv", path.join(rt.root, ".venv-audio"), "--python", "3.11"], { stdio: "inherit" });
|
|
1385
|
+
if (r2.status !== 0) throw new SfxEnvError("uv venv \u5931\u8D25");
|
|
1386
|
+
}
|
|
1387
|
+
console.error(`\xB7 \u5B89\u88C5\u4F9D\u8D56: ${deps.map((d) => d.includes("<") ? `"${d}"` : d).join(" ")}`);
|
|
1388
|
+
const r = spawnSync("uv", ["pip", "install", "-p", rt.pythonAudio, ...deps], { stdio: "inherit" });
|
|
1389
|
+
if (r.status !== 0) throw new SfxEnvError("\u4F9D\u8D56\u5B89\u88C5\u5931\u8D25\uFF08uv pip install\uFF09");
|
|
1390
|
+
console.error(`\u2713 \u97F3\u6548 venv \u5C31\u7EEA: ${rt.pythonAudio}`);
|
|
1391
|
+
} catch (e) {
|
|
1392
|
+
fail(e);
|
|
1393
|
+
}
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
function registerSfxDoctor(sfx) {
|
|
1397
|
+
sfx.command("doctor").description("\u97F3\u6548\u6808\u81EA\u68C0\uFF08venv / \u4E3B\u6A21\u578B\u7F13\u5B58 / \u5206\u8BCD\u5668\u7F13\u5B58\uFF09\uFF0C\u72EC\u7ACB\u9000\u51FA\u7801 0/1").action(async () => {
|
|
1398
|
+
try {
|
|
1399
|
+
const rt = resolveAudioRuntime();
|
|
1400
|
+
const checks = [
|
|
1401
|
+
[
|
|
1402
|
+
"\u97F3\u6548 venv",
|
|
1403
|
+
fs5.existsSync(rt.pythonAudio),
|
|
1404
|
+
`\u4FEE\u590D: lmedia sfx setup\uFF08\u5EFA ${rt.root}/.venv-audio\uFF09`
|
|
1405
|
+
],
|
|
1406
|
+
[
|
|
1407
|
+
`\u4E3B\u6A21\u578B\u7F13\u5B58 ${MODEL}`,
|
|
1408
|
+
!!hfSnapshot(MODEL),
|
|
1409
|
+
`\u4FEE\u590D: HF_HUB_OFFLINE=0 huggingface-cli download ${MODEL}\uFF08\u6216\u4EFB\u4E00 transformers \u52A0\u8F7D\u89E6\u53D1\u4E0B\u8F7D\uFF09`
|
|
1410
|
+
],
|
|
1411
|
+
[
|
|
1412
|
+
`\u5206\u8BCD\u5668\u7F13\u5B58 ${TOKENIZER}`,
|
|
1413
|
+
!!hfSnapshot(TOKENIZER),
|
|
1414
|
+
`\u4FEE\u590D: HF_HUB_OFFLINE=0 huggingface-cli download ${TOKENIZER}`
|
|
1415
|
+
]
|
|
1416
|
+
];
|
|
1417
|
+
for (const [name, ok, fix] of checks) {
|
|
1418
|
+
if (ok) console.log(`\u2713 ${name}`);
|
|
1419
|
+
else console.log(`\u2717 ${name} \u7F3A\u5931 \u2192 ${fix}`);
|
|
1420
|
+
}
|
|
1421
|
+
const bad = checks.filter(([, ok]) => !ok).length;
|
|
1422
|
+
if (bad === 0) {
|
|
1423
|
+
console.log("\u97F3\u6548\u6A21\u6001\u5C31\u7EEA\uFF08sfx gen/batch \u53EF\u7528\uFF09");
|
|
1424
|
+
} else {
|
|
1425
|
+
console.error(`\u97F3\u6548\u6808\u7F3A\u5931 ${bad} \u9879\uFF08\u89C1 \u2717 \u4FEE\u590D\u6307\u5F15\uFF09`);
|
|
1426
|
+
process.exit(1);
|
|
1427
|
+
}
|
|
1428
|
+
} catch (e) {
|
|
1429
|
+
fail(e);
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
function registerLib(sfx) {
|
|
1434
|
+
const lib = sfx.command("lib").description("SSOT \u97F3\u6548\u5E93\uFF1A\u5165\u5E93\uFF0844.1k mono 160k mp3\uFF09/ \u5217\u8868 / \u79FB\u9664 / \u5BF9\u8D26");
|
|
1435
|
+
lib.command("init").description("\u521B\u5EFA\u5E93\u6839 + \u76EE\u5F55\u9AA8\u67B6 + \u7A7A\u6E05\u5355\uFF08\u5E42\u7B49\uFF09").option("--lib <dir>", "\u5E93\u6839\uFF08\u7F3A\u7701 ~/.config/limg/sfx-library\uFF0C\u53EF\u88AB LMEDIA_SFX_LIB \u8986\u76D6\uFF09").action(async (opts) => {
|
|
1436
|
+
try {
|
|
1437
|
+
const root = resolveLibraryRoot(opts.lib);
|
|
1438
|
+
const r = initLibrary(root);
|
|
1439
|
+
console.log(`${r.created ? "\u2713 \u5DF2\u521B\u5EFA" : "\xB7 \u5DF2\u5B58\u5728"}\u97F3\u6548\u5E93: ${r.root}\uFF08\u6E05\u5355 ${r.manifest}\uFF09`);
|
|
1440
|
+
} catch (e) {
|
|
1441
|
+
fail(e);
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1444
|
+
lib.command("add <file>").description("\u8F6C\u7801\u5165\u5E93 + \u6E05\u5355\u843D\u8D26\uFF08\u540C\u5185\u5BB9\u91CD\u590D\u767B\u8BB0\u5E42\u7B49\u8DF3\u8FC7\uFF09").requiredOption("--key <k>", "\u97F3\u6548 key\uFF08\u5C0F\u5199\u5B57\u6BCD/\u6570\u5B57/\u77ED\u6A2A\u7EBF\uFF09").option("--type <t>", "sfx | ambient", "sfx").option("--library <name>", "\u5E93\u540D\uFF08\u843D\u5165\u8BB0\u5F55 library \u5B57\u6BB5\uFF09", "default").option("--title <t>", "\u6807\u9898").option("--note <n>", "\u5907\u6CE8").option("--tags <list>", "\u6807\u7B7E\uFF0C\u9017\u53F7\u5206\u9694\uFF08\u5982 animal,cute\uFF09").option("--lib <dir>", "\u5E93\u6839").action(async (file, opts) => {
|
|
1445
|
+
try {
|
|
1446
|
+
if (opts.type !== "sfx" && opts.type !== "ambient") {
|
|
1447
|
+
throw new InvalidArgsError(`--type \u4EC5\u652F\u6301 sfx|ambient: ${opts.type}`);
|
|
1448
|
+
}
|
|
1449
|
+
if (opts.library !== void 0 && !opts.library.trim()) {
|
|
1450
|
+
throw new InvalidArgsError("--library \u9700\u4E3A\u975E\u7A7A\u5E93\u540D");
|
|
1451
|
+
}
|
|
1452
|
+
assertFilesExist([file], "\u8F93\u5165\u97F3\u9891");
|
|
1453
|
+
const root = resolveLibraryRoot(opts.lib);
|
|
1454
|
+
const { item, duplicated } = addEntry(root, {
|
|
1455
|
+
file: path.resolve(file),
|
|
1456
|
+
key: opts.key,
|
|
1457
|
+
type: opts.type,
|
|
1458
|
+
library: opts.library,
|
|
1459
|
+
title: opts.title,
|
|
1460
|
+
note: opts.note,
|
|
1461
|
+
tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()).filter(Boolean) : []
|
|
1462
|
+
});
|
|
1463
|
+
if (duplicated) {
|
|
1464
|
+
console.log(`\xB7 \u5DF2\u6709\u540C\u5185\u5BB9\u6761\u76EE: ${item.key}\uFF08content_hash \u76F8\u540C\uFF0C\u5E42\u7B49\u8DF3\u8FC7\uFF09`);
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
console.log(`\u2713 \u5165\u5E93 ${item.key} \u2192 ${item.path}\uFF08${item.duration.toFixed(2)}s / ${item.sample_rate}Hz / [${item.tags.join(", ")}]\uFF09`);
|
|
1468
|
+
} catch (e) {
|
|
1469
|
+
fail(e);
|
|
1470
|
+
}
|
|
1471
|
+
});
|
|
1472
|
+
lib.command("list").description("\u8868\u683C\u5217\u51FA\u5E93\u5185\u6761\u76EE\uFF08stdout \u884C\u6570 == \u8FC7\u6EE4\u540E\u6E05\u5355\u6761\u6570\uFF09").option("--type <t>", "\u6309\u7C7B\u578B\u8FC7\u6EE4\uFF08sfx|ambient\uFF09").option("--status <s>", "\u6309\u72B6\u6001\u8FC7\u6EE4\uFF08ready\uFF09").option("--lib <dir>", "\u5E93\u6839").action(async (opts) => {
|
|
1473
|
+
try {
|
|
1474
|
+
const root = resolveLibraryRoot(opts.lib);
|
|
1475
|
+
const items = listEntries(root, { type: opts.type, status: opts.status });
|
|
1476
|
+
console.error(`${pad("key", 20)}${pad("type", 9)}${padL("dur", 8)} ${pad("tags", 18)}${pad("status", 7)}path`);
|
|
1477
|
+
for (const i of items) {
|
|
1478
|
+
console.log(
|
|
1479
|
+
`${pad(i.key, 20)}${pad(i.type, 9)}${padL(i.duration.toFixed(2), 8)} ${pad(`[${i.tags.join(",")}]`, 18)}${pad(i.status, 7)}${i.path}`
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1482
|
+
console.error(`\u5171 ${items.length} \u6761`);
|
|
1483
|
+
} catch (e) {
|
|
1484
|
+
fail(e);
|
|
1485
|
+
}
|
|
1486
|
+
});
|
|
1487
|
+
lib.command("remove <key>").description("\u79FB\u9664\u6E05\u5355\u8BB0\u5F55\uFF08\u4E0D\u5220\u97F3\u9891\u6587\u4EF6\uFF09").option("--type <t>", "\u6309\u7C7B\u578B\u9650\u5B9A\uFF08sfx|ambient\uFF09").option("--lib <dir>", "\u5E93\u6839").action(async (key, opts) => {
|
|
1488
|
+
try {
|
|
1489
|
+
const root = resolveLibraryRoot(opts.lib);
|
|
1490
|
+
const removed = removeEntry(root, key, opts.type);
|
|
1491
|
+
if (!removed) {
|
|
1492
|
+
console.error(`\u2717 \u5E93\u4E2D\u65E0\u6B64\u8BB0\u5F55\uFF08\u6309 type \u8FC7\u6EE4\u540E\uFF09: ${key}\uFF08lmedia sfx lib list \u67E5\u770B\uFF09`);
|
|
1493
|
+
process.exit(1);
|
|
1494
|
+
}
|
|
1495
|
+
console.log(`\u2713 \u5DF2\u79FB\u9664\u8BB0\u5F55 ${key}\uFF08\u97F3\u9891\u6587\u4EF6\u4FDD\u7559: ${removed.path}\uFF09`);
|
|
1496
|
+
} catch (e) {
|
|
1497
|
+
fail(e);
|
|
1498
|
+
}
|
|
1499
|
+
});
|
|
1500
|
+
lib.command("verify").description("\u5BF9\u8D26\uFF1A\u8BB0\u5F55 \u2194 \u6587\u4EF6 \u2194 \u65F6\u957F\uFF08\xB10.1s\uFF09\u2194 status\uFF0C\u6F02\u79FB exit 1").option("--lib <dir>", "\u5E93\u6839").action(async (opts) => {
|
|
1501
|
+
try {
|
|
1502
|
+
const root = resolveLibraryRoot(opts.lib);
|
|
1503
|
+
const { items, problems } = verifyLibrary(root);
|
|
1504
|
+
for (const item of items) {
|
|
1505
|
+
const p = problems.find((x) => x.key === item.key && x.type === item.type);
|
|
1506
|
+
if (!p) console.log(`\u2713 ${item.key}\uFF08${item.type}\uFF09`);
|
|
1507
|
+
else console.log(`\u2717 ${item.key}\uFF08${item.type}\uFF09: ${p.issues.join("; ")}`);
|
|
1508
|
+
}
|
|
1509
|
+
console.error(`\u5171 ${items.length} \u6761\uFF0C\u6F02\u79FB ${problems.length} \u6761`);
|
|
1510
|
+
if (problems.length) process.exit(1);
|
|
1511
|
+
} catch (e) {
|
|
1512
|
+
fail(e);
|
|
1513
|
+
}
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
function registerSfx(program2) {
|
|
1517
|
+
const sfx = program2.command("sfx").description("\u97F3\u6548\u6A21\u6001\uFF1ADasheng \u672C\u5730\u751F\u6210\uFF08gen/batch\uFF09+ \u526A\u88C1/\u5F52\u4E00/\u9A8C\u6536/\u5165\u5E93\u4EA7\u7EBF\uFF08trim/recut/normalize/accept/ab/lib\uFF09");
|
|
1518
|
+
registerGen3(sfx);
|
|
1519
|
+
registerBatch(sfx);
|
|
1520
|
+
registerTrim(sfx);
|
|
1521
|
+
registerRecut(sfx);
|
|
1522
|
+
registerNormalize(sfx);
|
|
1523
|
+
registerAccept(sfx);
|
|
1524
|
+
registerAb(sfx);
|
|
1525
|
+
registerSetup2(sfx);
|
|
1526
|
+
registerSfxDoctor(sfx);
|
|
1527
|
+
registerLib(sfx);
|
|
1528
|
+
}
|
|
1529
|
+
function sfxDoctorChecks() {
|
|
1530
|
+
const rt = resolveAudioRuntime();
|
|
1531
|
+
return [
|
|
1532
|
+
[`[sfx] .venv-audio\uFF08Dasheng-AudioGen\uFF09`, fs5.existsSync(rt.pythonAudio)],
|
|
1533
|
+
[`[sfx] \u4E3B\u6A21\u578B\u7F13\u5B58 ${MODEL}`, !!hfSnapshot(MODEL)],
|
|
1534
|
+
[`[sfx] \u5206\u8BCD\u5668\u7F13\u5B58 ${TOKENIZER}`, !!hfSnapshot(TOKENIZER)]
|
|
1535
|
+
];
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// src/commands/doctor.ts
|
|
1539
|
+
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 () => {
|
|
1541
|
+
const vrt = resolveVideoRuntime();
|
|
1542
|
+
const videoChecks = [
|
|
1543
|
+
["[video] .venv-video\uFF08mmh3turbo\uFF09", fs5.existsSync(vrt.mmh3turbo)],
|
|
1544
|
+
["[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"))]
|
|
1546
|
+
];
|
|
1547
|
+
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}`);
|
|
1559
|
+
for (const l of loadRegistry()) {
|
|
1560
|
+
console.log(`${fs5.existsSync(l.path) ? "\u2713" : "\u2717"} LoRA: ${l.name}`);
|
|
1561
|
+
}
|
|
1562
|
+
imageOk = checks.every(([, ok]) => ok);
|
|
1563
|
+
} catch (e) {
|
|
1564
|
+
imageOk = false;
|
|
1565
|
+
console.error(e.message);
|
|
1566
|
+
}
|
|
1567
|
+
for (const m of ["gen", "edit"]) {
|
|
1568
|
+
const st = await pingDaemon(m);
|
|
1569
|
+
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"}`
|
|
1571
|
+
);
|
|
1572
|
+
}
|
|
1573
|
+
for (const [name, ok] of videoChecks) console.log(`${ok ? "\u2713" : "\u2717"} ${name}`);
|
|
1574
|
+
const videoOk = videoChecks[0][1] && videoChecks[1][1];
|
|
1575
|
+
const sfxChecks = sfxDoctorChecks();
|
|
1576
|
+
for (const [name, ok] of sfxChecks) console.log(`${ok ? "\u2713" : "\u2717"} ${name}`);
|
|
1577
|
+
const sfxOk = sfxChecks.every(([, ok]) => ok);
|
|
1578
|
+
console.log(
|
|
1579
|
+
`
|
|
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"}`
|
|
1581
|
+
);
|
|
1582
|
+
if (!imageOk) process.exit(1);
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// src/index.ts
|
|
1587
|
+
var program = new Command();
|
|
1588
|
+
program.name("lmedia").description("\u672C\u5730\u5A92\u4F53\u751F\u4EA7 CLI \u2014 \u56FE\u50CF/\u89C6\u9891\uFF08\u672C\u5730\u63A8\u7406\uFF0C\u96F6 API \u6210\u672C\uFF09+ LoRA \u6CE8\u518C\u8868\u3002Apple Silicon").version("0.1.0");
|
|
1589
|
+
registerImage(program);
|
|
1590
|
+
registerVideo(program);
|
|
1591
|
+
registerLora(program);
|
|
1592
|
+
registerDoctor(program);
|
|
1593
|
+
registerSfx(program);
|
|
1594
|
+
program.parse();
|