dsh-h3-image 0.2.7 → 0.2.9

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/README.md CHANGED
@@ -45,13 +45,17 @@
45
45
 
46
46
  ### 安装(DSH)
47
47
 
48
- 在 DSH 里用 `plugin_manager` 安装,两种来源任选:
48
+ 图形界面:设置 → 插件 → 安装,输入 `dsh-h3-image`,装完启用(启用后重启一次 DSH 让工具生效)。
49
+
50
+ 也可以让 agent 用 `plugin_manager` 安装,两种来源任选:
49
51
 
50
52
  ```
51
53
  install_bundle → dsh-h3-image # 从 npm 装最新发布版
52
54
  install_bundle → /path/to/dsh-h3-image # 或本地仓库/解包目录
53
55
  ```
54
56
 
57
+ 装到旧版多半是 npm 镜像同步滞后(如 npmmirror 比官方源慢几分钟到几小时),切回官方源 `registry.npmjs.org` 重装即可。
58
+
55
59
  然后在 profile 中启用该 bundle,跑一次 `h3_status`——机器还缺什么它会逐项告诉你。
56
60
 
57
61
  ### 开发
@@ -108,13 +112,17 @@ This plugin's **code** is MIT. It does not ship, re-host or re-distribute the mo
108
112
 
109
113
  ### Install (DSH)
110
114
 
111
- Install with `plugin_manager`, from either source:
115
+ From the GUI: Settings → Plugins → Install, type `dsh-h3-image`, then enable it (restart DSH once so the tools load).
116
+
117
+ Or let the agent install it with `plugin_manager`, from either source:
112
118
 
113
119
  ```
114
120
  install_bundle → dsh-h3-image # latest release from npm
115
121
  install_bundle → /path/to/dsh-h3-image # or a local checkout / unpacked dir
116
122
  ```
117
123
 
124
+ If you end up on an older version, the npm mirror you picked is probably behind the official registry (npmmirror lags by minutes to hours); reinstall from `registry.npmjs.org`.
125
+
118
126
  Then enable the bundle in your profile and run `h3_status` — it tells you exactly what the machine still needs.
119
127
 
120
128
  ### Development
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env python3
2
+ """Weight-staging driver for dsh-h3-image (bundled asset, not engine code).
3
+
4
+ Why this exists: the pinned engine's fetch_h3_image.py hardcodes
5
+ https://huggingface.co and has no mirror switch, and patching the pinned
6
+ engine is out of scope (design D1). So instead of touching the engine, this
7
+ driver places the EXACT pinned file set (CORE_FILES + the transformer shards,
8
+ read live from the engine script — never re-listed or re-hashed here) into
9
+ the engine's own layout ahead of time. The engine's fetch contract then does
10
+ the rest: a file that already exists with the right size and sha256 is
11
+ returned without any network access, so the following engine run is instant
12
+ for the main weights while still converting shard by shard as usual.
13
+
14
+ Endpoint honesty: every endpoint is tried DIRECT first (proxies explicitly
15
+ disabled — a proxy download would silently spend the user's proxy data). Only
16
+ after every endpoint failed direct does one final round run through the system
17
+ proxy, announced by its own line before it starts. Redirects (301/302/303/
18
+ 307/308) are followed by hand because urllib does not follow 308, which is
19
+ exactly what a mirror answers for every file; the Range header is kept across
20
+ hops so a resumed download stays resumed through a 308.
21
+
22
+ File-list truth: imported from the engine script (--engine-script), so a pin
23
+ bump needs no change here.
24
+
25
+ Progress contract (parsed by lib/jobs.js, all flushed):
26
+ [stage n/N] fetching <name> via <host>
27
+ [stage n/N] placed <name>
28
+ [stage n/N] skipped <name> (already present)
29
+ [stage n/N] switched endpoint: <host>
30
+ stage: retrying via system proxy (may count against proxy data)
31
+ STAGE OK (terminal, stdout)
32
+ STAGE FAIL <rel> (terminal, stderr, exit 1)
33
+
34
+ Resumability: a `.part` survives a failed attempt and resumes with a Range
35
+ request; a server that ignores Range restarts the file cleanly (200 →
36
+ truncate). A part that fails sha256 is deleted, never renamed into place.
37
+
38
+ Usage:
39
+ python stage_weights.py --dir MODEL_DIR \
40
+ --engine-script .../scripts/fetch_h3_image.py \
41
+ --endpoints hf,https://mirror.example
42
+ """
43
+
44
+ from __future__ import annotations
45
+
46
+ import argparse
47
+ import hashlib
48
+ import sys
49
+ import urllib.error
50
+ import urllib.parse
51
+ import urllib.request
52
+ from pathlib import Path
53
+
54
+ TIMEOUT = 60 # seconds without data on any single read — same as the engine fetch
55
+ REDIRECT_CODES = (301, 302, 303, 307, 308)
56
+ HF = "https://huggingface.co"
57
+
58
+
59
+ class _NoRedirect(urllib.request.HTTPRedirectHandler):
60
+ """Neutralise urllib's built-in redirect handling (it ignores 308); the
61
+ loop in open_following() follows every redirect code itself."""
62
+
63
+ def redirect_request(self, req, fp, code, msg, headers, newurl): # noqa: D102
64
+ return None
65
+
66
+
67
+ def sha256_of(path: Path) -> str:
68
+ h = hashlib.sha256()
69
+ with path.open("rb") as fh:
70
+ for chunk in iter(lambda: fh.read(1 << 22), b""):
71
+ h.update(chunk)
72
+ return h.hexdigest()
73
+
74
+
75
+ def build_opener(direct: bool):
76
+ """direct=True → ProxyHandler({}) (never the user's proxy); direct=False
77
+ → the default proxy handler, which honours the system proxy settings."""
78
+ if direct:
79
+ return urllib.request.build_opener(_NoRedirect(), urllib.request.ProxyHandler({}))
80
+ return urllib.request.build_opener(_NoRedirect())
81
+
82
+
83
+ def open_following(opener, url: str, headers: dict, max_redirects: int = 10):
84
+ """GET url, following 301/302/303/307/308 by hand. Returns the live
85
+ response; the caller streams from it and it closes with the caller's
86
+ `with` block."""
87
+ for _ in range(max_redirects + 1):
88
+ req = urllib.request.Request(url, headers=headers)
89
+ try:
90
+ return opener.open(req, timeout=TIMEOUT)
91
+ except urllib.error.HTTPError as err:
92
+ location = err.headers.get("Location") if err.headers is not None else None
93
+ err.close()
94
+ if err.code in REDIRECT_CODES and location:
95
+ url = urllib.parse.urljoin(url, location)
96
+ continue
97
+ raise
98
+ raise RuntimeError(f"more than {max_redirects} redirects for {url}")
99
+
100
+
101
+ def attempt(opener, url: str, dest: Path, part: Path, size: int, sha, name: str,
102
+ index: int, total: int, first: bool) -> None:
103
+ """One endpoint, one file: stream into .part (resuming when the server
104
+ honours Range), then verify and rename. Any raise = this endpoint failed
105
+ for this file; the partial .part stays on disk for the next attempt."""
106
+ host = urllib.parse.urlsplit(url).hostname or url
107
+ if not first:
108
+ print(f"[stage {index}/{total}] switched endpoint: {host}", flush=True)
109
+ print(f"[stage {index}/{total}] fetching {name} via {host}", flush=True)
110
+ have = part.stat().st_size if part.exists() else 0
111
+ if have > size:
112
+ part.unlink()
113
+ have = 0
114
+ headers = {"Range": f"bytes={have}-"} if have else {}
115
+ with open_following(opener, url, headers) as resp:
116
+ resume = bool(have) and getattr(resp, "status", 200) == 206
117
+ with part.open("ab" if resume else "wb") as out:
118
+ while True:
119
+ block = resp.read(1 << 22)
120
+ if not block:
121
+ break
122
+ out.write(block)
123
+ got = part.stat().st_size if part.exists() else 0
124
+ if got != size:
125
+ if got > size:
126
+ part.unlink(missing_ok=True) # a corrupt oversize part can never resume
127
+ raise RuntimeError(f"expected {size} bytes, got {got}")
128
+ if sha is not None and sha256_of(part) != sha:
129
+ part.unlink(missing_ok=True) # wrong content: never stage it, never resume it
130
+ raise RuntimeError("sha256 mismatch")
131
+ part.rename(dest)
132
+ print(f"[stage {index}/{total}] placed {name}", flush=True)
133
+
134
+
135
+ def stage_file(*, name: str, rel: str, dest: Path, size: int, sha, endpoints,
136
+ middle: str, index: int, total: int) -> bool:
137
+ """Place one pinned file. Returns True on placed/skipped, False after every
138
+ endpoint failed in both the direct and the system-proxy round."""
139
+ dest.parent.mkdir(parents=True, exist_ok=True)
140
+ if dest.exists() and dest.stat().st_size == size and (sha is None or sha256_of(dest) == sha):
141
+ print(f"[stage {index}/{total}] skipped {name} (already present)", flush=True)
142
+ return True
143
+ part = dest.with_name(dest.name + ".part")
144
+ if part.exists() and part.stat().st_size == size:
145
+ # A part that is already complete goes straight to verification
146
+ # (same shortcut the engine fetch takes), skipping the network.
147
+ if sha is None or sha256_of(part) == sha:
148
+ part.rename(dest)
149
+ print(f"[stage {index}/{total}] placed {name}", flush=True)
150
+ return True
151
+ part.unlink()
152
+
153
+ url_of = lambda endpoint: f"{endpoint.rstrip('/')}/{middle}/{rel}" # noqa: E731
154
+ last_error = None
155
+ attempts = 0
156
+ direct = build_opener(direct=True)
157
+ for endpoint in endpoints:
158
+ try:
159
+ attempt(direct, url_of(endpoint), dest, part, size, sha, name, index, total, first=attempts == 0)
160
+ return True
161
+ except Exception as error: # noqa: BLE001 — any failure means: next endpoint
162
+ last_error = error
163
+ attempts += 1
164
+ print("stage: retrying via system proxy (may count against proxy data)", flush=True)
165
+ proxied = build_opener(direct=False)
166
+ for endpoint in endpoints:
167
+ try:
168
+ attempt(proxied, url_of(endpoint), dest, part, size, sha, name, index, total, first=attempts == 0)
169
+ return True
170
+ except Exception as error: # noqa: BLE001
171
+ last_error = error
172
+ attempts += 1
173
+ print(f"stage: {name}: {last_error}", file=sys.stderr, flush=True)
174
+ return False
175
+
176
+
177
+ def main(argv=None) -> int:
178
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
179
+ ap.add_argument("--dir", type=Path, required=True, help="model root (the engine fetch's --dir)")
180
+ ap.add_argument("--engine-script", type=Path, required=True, help="absolute path to the engine's fetch_h3_image.py")
181
+ ap.add_argument("--endpoints", required=True, help="comma-separated priority list; 'hf' or https://host[/prefix] per entry")
182
+ args = ap.parse_args(argv)
183
+
184
+ # The engine script's own directory goes on sys.path so `import
185
+ # fetch_h3_image` (and its sibling import of fetch_ph3_image) resolves.
186
+ # Importing it only reads constants — no network, no side effects.
187
+ sys.path.insert(0, str(args.engine_script.resolve().parent))
188
+ import fetch_h3_image as engine # noqa: E402
189
+
190
+ middle = f"{engine.REPO}/resolve/{engine.REVISION}"
191
+
192
+ endpoints = []
193
+ for raw in args.endpoints.split(","):
194
+ raw = raw.strip().rstrip("/")
195
+ if raw:
196
+ endpoints.append(HF if raw == "hf" else raw)
197
+ if not endpoints:
198
+ print("stage: --endpoints resolved to nothing", file=sys.stderr)
199
+ return 2
200
+
201
+ # Layout truth = the engine's own fetch_h3_image.main: CORE_FILES go to
202
+ # root/<rel> (root/source/<rel> for transformer/), shards to
203
+ # root/source/transformer/<name> via the engine's SHARD_FILE template.
204
+ shard_file = getattr(engine, "SHARD_FILE", "transformer/{name}")
205
+ items = []
206
+ for rel, (size, sha) in engine.CORE_FILES.items():
207
+ dest = args.dir / rel if not rel.startswith("transformer/") else args.dir / "source" / rel
208
+ items.append((rel, rel, dest, size, sha))
209
+ for shard_name in sorted(engine.SHARDS):
210
+ size, sha = engine.SHARDS[shard_name]
211
+ rel = shard_file.format(name=shard_name)
212
+ items.append((shard_name, rel, args.dir / "source" / rel, size, sha))
213
+
214
+ total = len(items)
215
+ for index, (name, rel, dest, size, sha) in enumerate(items, 1):
216
+ ok = stage_file(name=name, rel=rel, dest=dest, size=size, sha=sha,
217
+ endpoints=endpoints, middle=middle, index=index, total=total)
218
+ if not ok:
219
+ print(f"STAGE FAIL {rel}", file=sys.stderr, flush=True)
220
+ return 1
221
+ print("STAGE OK", flush=True)
222
+ return 0
223
+
224
+
225
+ if __name__ == "__main__":
226
+ raise SystemExit(main())
package/lib/checks.js CHANGED
@@ -313,6 +313,18 @@ export function checkDisk({ freeGB, modelDirName }) {
313
313
  // Full status run (used by the h3_status tool).
314
314
  // ---------------------------------------------------------------------------
315
315
 
316
+ /**
317
+ * Mirror normalization (single truth for the config surface):
318
+ * "hf" | undefined | anything invalid → "hf"
319
+ * "hf-mirror" (legacy literal) → "https://hf-mirror.com"
320
+ * any other https:// URL → kept as-is (custom endpoint)
321
+ */
322
+ export function normalizeMirror(value) {
323
+ if (value === "hf-mirror") return "https://hf-mirror.com";
324
+ if (typeof value === "string" && value.startsWith("https://") && !/\s/.test(value)) return value;
325
+ return "hf";
326
+ }
327
+
316
328
  export function resolveConfig(config = {}) {
317
329
  const home = homedir();
318
330
  // With an explicit model_dir, sibling defaults hang off its parent (the
@@ -337,7 +349,11 @@ export function resolveConfig(config = {}) {
337
349
  venv_dir: config.venv_dir ?? join(root, "venv"),
338
350
  text_dir,
339
351
  text_config,
340
- mirror: config.mirror === "hf-mirror" ? "hf-mirror" : "hf",
352
+ // Mirror: "hf" (default) or a custom endpoint. The legacy literal
353
+ // "hf-mirror" normalizes to its URL; any https:// URL is a valid custom
354
+ // endpoint (staged main weights + TE HF_ENDPOINT both use it). Anything
355
+ // else falls back to "hf" — a typo must never disable the default path.
356
+ mirror: normalizeMirror(config.mirror),
341
357
  // Reuse-probe scan roots (absolute or $HOME-relative). Default is the
342
358
  // one known asset location; an explicit [] forces a true bare-metal
343
359
  // simulation (no candidates → full download + verification path).
package/lib/index.js CHANGED
@@ -106,7 +106,8 @@ function formatSetup(value) {
106
106
  lines.push(`h3_setup poll [${value.kind}/${value.state}] job ${value.job_id}`);
107
107
  const p = value.progress ?? {};
108
108
  if (value.kind === "weights") {
109
- lines.push(`shards ${p.shard_done ?? 0}/${p.shard_total ?? 12} converted${p.current ? ` (fetching #${p.current})` : ""}; text encoder ${p.te_done ?? 0}/${p.te_total ?? 9}`);
109
+ const staged = p.stage_total ? `staged ${p.stage_done}/${p.stage_total}; ` : "";
110
+ lines.push(`${staged}shards ${p.shard_done ?? 0}/${p.shard_total ?? 12} converted${p.current ? ` (fetching #${p.current})` : ""}; text encoder ${p.te_done ?? 0}/${p.te_total ?? 9}`);
110
111
  } else if (p.stage) {
111
112
  lines.push(`stage: ${p.stage}`);
112
113
  }
package/lib/jobs.js CHANGED
@@ -144,6 +144,7 @@ export async function tailLog(jDir, id, maxBytes = 2000, readFileP = readFile) {
144
144
  * `PACK OK -> ...` (engine fetch, terminal)
145
145
  * `[te n/9] fetched <name>` / `[te n/9] reused <name>` (our TE driver)
146
146
  * `TE OK` (TE driver, terminal)
147
+ * `[stage n/N] placed <name>` (our staging driver)
147
148
  * `== <label> ==` (supervisor stage marker)
148
149
  */
149
150
  export function parseProgress(logTail) {
@@ -161,6 +162,13 @@ export function parseProgress(logTail) {
161
162
  out.te_total = Math.max(out.te_total, +m[2]);
162
163
  out.te_done = Math.max(out.te_done, +m[1]);
163
164
  }
165
+ // Staging only reports a total when a mirror run actually staged files;
166
+ // absent lines leave stage_total undefined so the renderer can skip it.
167
+ const stageLines = [...logTail.matchAll(/^\[stage (\d+)\/(\d+)\] (placed|skipped)/gm)];
168
+ for (const m of stageLines) {
169
+ out.stage_total = Math.max(out.stage_total ?? 0, +m[2]);
170
+ out.stage_done = Math.max(out.stage_done ?? 0, +m[1]);
171
+ }
164
172
  out.pack_ok = /^PACK OK/m.test(logTail);
165
173
  out.te_ok = /^TE OK/m.test(logTail);
166
174
  return out;
package/lib/setup.js CHANGED
@@ -23,11 +23,14 @@
23
23
  * symlink, never by copy — only on an exact digest match. No match → download
24
24
  * from the pinned revision. A size match alone never authorizes reuse.
25
25
  *
26
- * Mirror honesty (documented in README): `mirror: "hf-mirror"` redirects the
27
- * text-encoder download (hf_hub_download honors HF_ENDPOINT). The main
28
- * weights come from the engine's own fetch script whose URL is hardcoded —
29
- * patching the pinned engine is out of scope (design D1), so the mirror
30
- * option never claims to cover the main weights.
26
+ * Mirror honesty (documented in README): a non-default `mirror` covers BOTH
27
+ * download faces without touching the pinned engine (design D1). The main
28
+ * weights are staged ahead of the engine fetch by assets/stage_weights.py,
29
+ * which places the exact pinned file set into the engine's own layout from
30
+ * the chosen endpoint; the engine fetch then skips every placed file (its
31
+ * contract: present + right size + right sha256 → no network). The
32
+ * text-encoder download honours HF_ENDPOINT (hf_hub_download). The default
33
+ * `hf` carries no stage step and no env override at all.
31
34
  */
32
35
  import { execFile } from "node:child_process";
33
36
  import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
@@ -192,9 +195,39 @@ function teDriverPath() {
192
195
  return join(import.meta.dirname, "..", "assets", "te_setup.py");
193
196
  }
194
197
 
195
- /** Engine fetch chain for the weights job (resumable by the engine itself). */
198
+ function stageDriverPath() {
199
+ return join(import.meta.dirname, "..", "assets", "stage_weights.py");
200
+ }
201
+
202
+ /** Legacy literal kept working for old profile configs; anything else that
203
+ * is not "hf" is treated as a custom endpoint URL. */
204
+ const HF_MIRROR_URL = "https://hf-mirror.com";
205
+
206
+ /**
207
+ * Engine fetch chain for the weights job (resumable by the engine itself).
208
+ * `mirror` "hf" (default) = plain engine fetch + TE, zero env. Any other
209
+ * value (resolveConfig normalizes "hf-mirror" to its URL) = a custom
210
+ * endpoint: a `stage` command runs FIRST and places every pinned main-weight
211
+ * file from `<mirror>,hf` (mirror first — a user who configured one means
212
+ * it; hf as fallback), so the engine fetch that follows skips them all;
213
+ * the TE command points HF_ENDPOINT at the same endpoint.
214
+ */
196
215
  export function buildWeightsCommands({ resolved, venvBin, mirror }) {
197
- return [
216
+ const endpoint = mirror === "hf-mirror" ? HF_MIRROR_URL : mirror;
217
+ const commands = [];
218
+ if (mirror !== "hf") {
219
+ commands.push({
220
+ label: "stage",
221
+ argv: [
222
+ venvBin,
223
+ stageDriverPath(),
224
+ "--dir", resolved.model_dir,
225
+ "--engine-script", join(resolved.engine_dir, "scripts", "fetch_h3_image.py"),
226
+ "--endpoints", `${endpoint},hf`,
227
+ ],
228
+ });
229
+ }
230
+ commands.push(
198
231
  {
199
232
  label: "weights-fetch",
200
233
  argv: [venvBin, join(resolved.engine_dir, "scripts", "fetch_h3_image.py"), "--dir", resolved.model_dir, "--accepted"],
@@ -217,11 +250,13 @@ export function buildWeightsCommands({ resolved, venvBin, mirror }) {
217
250
  "--meta-files", UPSTREAM_META.files.join(","),
218
251
  "--candidates-file", join(resolved.model_dir, "h3-jobs", "reuse-candidates.txt"),
219
252
  ],
220
- // hf-mirror only ever touches THIS command (hf_hub_download honours
221
- // HF_ENDPOINT); the default carries no env override at all.
222
- ...(mirror === "hf-mirror" ? { env: { HF_ENDPOINT: "https://hf-mirror.com" } } : {}),
253
+ // A custom endpoint only ever touches THIS command's env
254
+ // (hf_hub_download honours HF_ENDPOINT); the default carries no env
255
+ // override at all.
256
+ ...(mirror === "hf" ? {} : { env: { HF_ENDPOINT: endpoint } }),
223
257
  },
224
- ];
258
+ );
259
+ return commands;
225
260
  }
226
261
 
227
262
  /** Smoke chain: one 1 MP image into the job area, metrics beside it. */
package/locale/en.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
- "title": "H3 Image (local MLX)",
4
- "description": "Unofficial third-party plugin, not affiliated with MiniMax. On Apple Silicon, fully local text-to-image with the open-source MiniMax H3-Image (MLX engine). A one-time setup wizard downloads a fixed version of the engine and weights to your machine (the MiniMax community license and its region terms are shown before any download; you accept it yourself), then generation runs offline — no keys, no accounts, no network access. Long setup steps are background jobs that survive restarts and resume where they stopped. Requires Apple Silicon with 48 GB+ unified memory (32 GB is not guaranteed) and ~60 GB free disk during setup; Intel Macs and other platforms are declined up front. Current version (0.2.7): read-only environment check, setup wizard, and local generation (the community license must be explicitly accepted by you before the first image)."
3
+ "title": "H3 Image (dsh-h3-image)",
4
+ "description": "Unofficial third-party plugin, not affiliated with MiniMax. On Apple Silicon, fully local text-to-image with the open-source MiniMax H3-Image (MLX engine). A one-time setup wizard downloads a fixed version of the engine and weights to your machine (the MiniMax community license and its region terms are shown before any download; you accept it yourself), then generation runs offline — no keys, no accounts, no network access. Long setup steps are background jobs that survive restarts and resume where they stopped. Requires Apple Silicon with 48 GB+ unified memory (32 GB is not guaranteed) and ~60 GB free disk during setup; Intel Macs and other platforms are declined up front. Current version (0.2.9): read-only environment check, setup wizard, and local generation (the community license must be explicitly accepted by you before the first image)."
5
5
  }
6
6
  }
package/locale/zh.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
- "title": "H3 本地生图(MLX)",
4
- "description": "非官方第三方插件,与 MiniMax 无关联。在 Apple Silicon 上用开源 MiniMax H3-Image(MLX 引擎)做完全本地文生图。安装向导一次性把固定版本的引擎与权重下载到本机(下载前展示 MiniMax 社区许可及地域条款,由你本人确认接受);之后生图全程离线——不用密钥、不用账号、不联网。安装中的长步骤是后台任务,DSH 重启不丢、断点续传。需 Apple Silicon、48GB 及以上统一内存(32GB 不保证)、安装期约 60GB 可用磁盘;Intel Mac 与其他平台在检查阶段即明确拒绝。当前版本(0.2.7):只读环境体检 + 安装向导 + 本地产图(社区许可需你本人确认接受后才会出图)。"
3
+ "title": "H3 本地生图 (dsh-h3-image)",
4
+ "description": "非官方第三方插件,与 MiniMax 无关联。在 Apple Silicon 上用开源 MiniMax H3-Image(MLX 引擎)做完全本地文生图。安装向导一次性把固定版本的引擎与权重下载到本机(下载前展示 MiniMax 社区许可及地域条款,由你本人确认接受);之后生图全程离线——不用密钥、不用账号、不联网。安装中的长步骤是后台任务,DSH 重启不丢、断点续传。需 Apple Silicon、48GB 及以上统一内存(32GB 不保证)、安装期约 60GB 可用磁盘;Intel Mac 与其他平台在检查阶段即明确拒绝。当前版本(0.2.9):只读环境体检 + 安装向导 + 本地产图(社区许可需你本人确认接受后才会出图)。"
5
5
  }
6
6
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-h3-image",
3
- "version": "0.2.7",
4
- "description": "在 Apple Silicon 上用开源 MiniMax H3-Image(MLX 引擎)做完全本地文生图的 DeepSeek Harness 插件:一键安装向导 + 零出网本地生图。Fully local MiniMax H3-Image text-to-image on Apple Silicon MLX — a DeepSeek Harness plugin with a one-time setup wizard and zero-egress generation.",
3
+ "version": "0.2.9",
4
+ "description": "在 Apple Silicon 上用开源 MiniMax H3-Image(MLX 引擎)做完全本地文生图的 DeepSeek Harness 插件:一键安装向导 + 零出网本地生图。Fully local MiniMax H3-Image text-to-image on Apple Silicon MLX — a DeepSeek Harness plugin with a one-time setup wizard and no network access at generation.",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
7
7
  "scripts": {