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 +10 -2
- package/assets/stage_weights.py +226 -0
- package/lib/checks.js +17 -1
- package/lib/index.js +2 -1
- package/lib/jobs.js +8 -0
- package/lib/setup.js +46 -11
- package/locale/en.json +2 -2
- package/locale/zh.json +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -45,13 +45,17 @@
|
|
|
45
45
|
|
|
46
46
|
### 安装(DSH)
|
|
47
47
|
|
|
48
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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):
|
|
27
|
-
*
|
|
28
|
-
* weights
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
221
|
-
// HF_ENDPOINT); the default carries no env
|
|
222
|
-
|
|
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 (
|
|
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.
|
|
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
|
|
4
|
-
"description": "非官方第三方插件,与 MiniMax 无关联。在 Apple Silicon 上用开源 MiniMax H3-Image(MLX 引擎)做完全本地文生图。安装向导一次性把固定版本的引擎与权重下载到本机(下载前展示 MiniMax 社区许可及地域条款,由你本人确认接受);之后生图全程离线——不用密钥、不用账号、不联网。安装中的长步骤是后台任务,DSH 重启不丢、断点续传。需 Apple Silicon、48GB 及以上统一内存(32GB 不保证)、安装期约 60GB 可用磁盘;Intel Mac 与其他平台在检查阶段即明确拒绝。当前版本(0.2.
|
|
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.
|
|
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
|
|
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": {
|