dsh-h3-image 0.2.9 → 0.2.11
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 +4 -4
- package/assets/stage_weights.py +239 -17
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,10 +34,10 @@
|
|
|
34
34
|
| 安装期间磁盘 | 峰值约 60 GB |
|
|
35
35
|
| 装完后磁盘 | 约 53 GB(源分片转换完会自动删掉) |
|
|
36
36
|
| 单张图内存(1 MP) | 约 27.5 GB |
|
|
37
|
-
| 每个权重分片(下载 + 转换) |
|
|
37
|
+
| 每个权重分片(下载 + 转换) | 时间跟你的网络环境和机器有关,快慢差别很大;中断可续传 |
|
|
38
38
|
| 第一次出图 | 约 30 秒 |
|
|
39
39
|
|
|
40
|
-
中断的步骤可以接着来:下到一半断了从 `.part`
|
|
40
|
+
中断的步骤可以接着来:下到一半断了从 `.part` 继续,下完的分片不会重下。大文件分 4 段并发下载;服务器不支持分段时自动回单线程,行为不变。内存那行是引擎自己报的单张图占用。
|
|
41
41
|
|
|
42
42
|
### 许可——下载权重前必读
|
|
43
43
|
|
|
@@ -101,10 +101,10 @@ Measured on an M5 Ultra with 256 GB of unified memory, from a from-scratch insta
|
|
|
101
101
|
| Disk during setup | ~60 GB at peak |
|
|
102
102
|
| Disk after setup | ~53 GB (source shards are deleted once converted) |
|
|
103
103
|
| Memory per image (1 MP) | ~27.5 GB |
|
|
104
|
-
| Per weight shard (download + convert) |
|
|
104
|
+
| Per weight shard (download + convert) | Depends on your network and machine; slow and fast setups differ a lot. Interrupted downloads resume. |
|
|
105
105
|
| First image | ~30 s |
|
|
106
106
|
|
|
107
|
-
Interrupted steps resume: a killed download continues from its `.part`, and finished shards are never re-downloaded. The memory figure is the per-image footprint the engine reports itself.
|
|
107
|
+
Interrupted steps resume: a killed download continues from its `.part`, and finished shards are never re-downloaded. Large files download in 4 parallel segments; if the server doesn't support ranged segments, it falls back to a single connection automatically. The memory figure is the per-image footprint the engine reports itself.
|
|
108
108
|
|
|
109
109
|
### Licenses — read before downloading weights
|
|
110
110
|
|
package/assets/stage_weights.py
CHANGED
|
@@ -12,12 +12,17 @@ returned without any network access, so the following engine run is instant
|
|
|
12
12
|
for the main weights while still converting shard by shard as usual.
|
|
13
13
|
|
|
14
14
|
Endpoint honesty: every endpoint is tried DIRECT first (proxies explicitly
|
|
15
|
-
disabled — a proxy download would silently spend the user's proxy data).
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
disabled — a proxy download would silently spend the user's proxy data). The
|
|
16
|
+
direct round runs twice: mirrors are often flaky in bursts, and a second
|
|
17
|
+
direct round costs nothing next to silently spending proxy data. Only after
|
|
18
|
+
both direct rounds fail does one final round run through the system proxy,
|
|
19
|
+
announced by its own line before it starts. Redirects (301/302/303/307/308)
|
|
20
|
+
are followed by hand because urllib does not follow 308, which is exactly
|
|
21
|
+
what a mirror answers for every file; the Range header is kept across hops
|
|
22
|
+
so a resumed download stays resumed through a 308. When a redirect chain
|
|
23
|
+
lands on a different host than the endpoint asked for (a mirror bouncing
|
|
24
|
+
users back to the origin), a note line says so — a slow file behind such a
|
|
25
|
+
bounce is the mirror's doing, and the log should not hide it.
|
|
21
26
|
|
|
22
27
|
File-list truth: imported from the engine script (--engine-script), so a pin
|
|
23
28
|
bump needs no change here.
|
|
@@ -27,6 +32,9 @@ Progress contract (parsed by lib/jobs.js, all flushed):
|
|
|
27
32
|
[stage n/N] placed <name>
|
|
28
33
|
[stage n/N] skipped <name> (already present)
|
|
29
34
|
[stage n/N] switched endpoint: <host>
|
|
35
|
+
[stage n/N] note: <name> redirected to <host>
|
|
36
|
+
[stage n/N] seg <i>/4 <name> via <host> (segmented mode only)
|
|
37
|
+
stage: <name>: direct round failed (<err>); retrying direct once
|
|
30
38
|
stage: retrying via system proxy (may count against proxy data)
|
|
31
39
|
STAGE OK (terminal, stdout)
|
|
32
40
|
STAGE FAIL <rel> (terminal, stderr, exit 1)
|
|
@@ -35,10 +43,31 @@ Resumability: a `.part` survives a failed attempt and resumes with a Range
|
|
|
35
43
|
request; a server that ignores Range restarts the file cleanly (200 →
|
|
36
44
|
truncate). A part that fails sha256 is deleted, never renamed into place.
|
|
37
45
|
|
|
46
|
+
Segmented downloads (files at or above --seg-threshold, default 64 MB): the
|
|
47
|
+
file is split into SEG_COUNT=4 byte ranges fetched concurrently by a
|
|
48
|
+
ThreadPoolExecutor (max_workers=4), each segment streaming into its own
|
|
49
|
+
`<name>.part.<i>` with a `Range: bytes=start-end` request. Every segment
|
|
50
|
+
resumes independently: a segment `.part` of `have` bytes re-requests
|
|
51
|
+
`bytes=(start+have)-`; only a 206 appends, a segment `.part` longer than its
|
|
52
|
+
segment is deleted and re-fetched from zero. Once all segments are in, they
|
|
53
|
+
are stitched into the single `.part` in order (4 MB blocks), the segment
|
|
54
|
+
parts are deleted, and the existing verification path takes over unchanged:
|
|
55
|
+
total size == size and sha256 match → rename into place; anything else → the
|
|
56
|
+
`.part` (and any segment parts) are deleted, never renamed. Disk cost: while
|
|
57
|
+
stitching, the segment parts and the assembled `.part` coexist, so the
|
|
58
|
+
temporary footprint is roughly one extra copy of the file (about 2x its size
|
|
59
|
+
during the stitch, back to ~1x once the segments are unlinked). A server that
|
|
60
|
+
answers a segment with 200 instead of 206 does not honour Range: the whole
|
|
61
|
+
segmented attempt is abandoned, every segment part is deleted, and the file
|
|
62
|
+
falls back to the untouched single-stream path on that same endpoint. One
|
|
63
|
+
segmented attempt counts as one attempt against the endpoint-fallback ladder
|
|
64
|
+
(two direct rounds, then one system-proxy round).
|
|
65
|
+
|
|
38
66
|
Usage:
|
|
39
67
|
python stage_weights.py --dir MODEL_DIR \
|
|
40
68
|
--engine-script .../scripts/fetch_h3_image.py \
|
|
41
|
-
--endpoints hf,https://mirror.example
|
|
69
|
+
--endpoints hf,https://mirror.example \
|
|
70
|
+
[--seg-threshold BYTES]
|
|
42
71
|
"""
|
|
43
72
|
|
|
44
73
|
from __future__ import annotations
|
|
@@ -49,12 +78,21 @@ import sys
|
|
|
49
78
|
import urllib.error
|
|
50
79
|
import urllib.parse
|
|
51
80
|
import urllib.request
|
|
81
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
52
82
|
from pathlib import Path
|
|
53
83
|
|
|
54
84
|
TIMEOUT = 60 # seconds without data on any single read — same as the engine fetch
|
|
55
85
|
REDIRECT_CODES = (301, 302, 303, 307, 308)
|
|
56
86
|
HF = "https://huggingface.co"
|
|
57
87
|
|
|
88
|
+
# Files at or above this many bytes are fetched in parallel segments. The
|
|
89
|
+
# default is deliberately high: below it the extra connections and the stitch
|
|
90
|
+
# pass cost more than the wall-clock they save. Overridable with
|
|
91
|
+
# --seg-threshold (the L2 tests use a tiny value to exercise the path).
|
|
92
|
+
SEG_THRESHOLD = 64 * 1024 * 1024
|
|
93
|
+
SEG_COUNT = 4
|
|
94
|
+
STITCH_BLOCK = 1 << 22 # 4 MB — same block size the single-stream path streams with
|
|
95
|
+
|
|
58
96
|
|
|
59
97
|
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
60
98
|
"""Neutralise urllib's built-in redirect handling (it ignores 308); the
|
|
@@ -64,6 +102,43 @@ class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
|
64
102
|
return None
|
|
65
103
|
|
|
66
104
|
|
|
105
|
+
class _NoRangeError(RuntimeError):
|
|
106
|
+
"""A segmented request came back 200 instead of 206: this server does not
|
|
107
|
+
honour Range, so segmentation is off for the file and the single-stream
|
|
108
|
+
path takes over (never a reason to fail the whole endpoint)."""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def seg_part_of(dest: Path, i: int) -> Path:
|
|
112
|
+
return dest.with_name(dest.name + f".part.{i}")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def host_of(url: str) -> str:
|
|
116
|
+
return urllib.parse.urlsplit(url).hostname or url
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def clear_segment_parts(dest: Path) -> None:
|
|
120
|
+
"""Drop every segment part. Called after a successful stitch, after a
|
|
121
|
+
verification failure and on the 200 fallback — never after a plain network
|
|
122
|
+
failure, because those parts are what the next attempt resumes from."""
|
|
123
|
+
for i in range(SEG_COUNT):
|
|
124
|
+
seg_part_of(dest, i).unlink(missing_ok=True)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def segment_ranges(size: int, count: int = SEG_COUNT):
|
|
128
|
+
"""Contiguous [start, end) ranges covering [0, size), as even as possible.
|
|
129
|
+
Empty ranges are dropped; fewer than two ranges means there is nothing to
|
|
130
|
+
parallelise and the caller should stay on the single-stream path."""
|
|
131
|
+
base, extra = divmod(size, count)
|
|
132
|
+
ranges = []
|
|
133
|
+
start = 0
|
|
134
|
+
for i in range(count):
|
|
135
|
+
end = start + base + (1 if i < extra else 0)
|
|
136
|
+
if end > start:
|
|
137
|
+
ranges.append((start, end))
|
|
138
|
+
start = end
|
|
139
|
+
return ranges
|
|
140
|
+
|
|
141
|
+
|
|
67
142
|
def sha256_of(path: Path) -> str:
|
|
68
143
|
h = hashlib.sha256()
|
|
69
144
|
with path.open("rb") as fh:
|
|
@@ -113,6 +188,12 @@ def attempt(opener, url: str, dest: Path, part: Path, size: int, sha, name: str,
|
|
|
113
188
|
have = 0
|
|
114
189
|
headers = {"Range": f"bytes={have}-"} if have else {}
|
|
115
190
|
with open_following(opener, url, headers) as resp:
|
|
191
|
+
final_host = urllib.parse.urlsplit(resp.geturl()).hostname or host
|
|
192
|
+
if final_host != host:
|
|
193
|
+
# A mirror that bounces to another host (typically the origin) is
|
|
194
|
+
# still "via <mirror>" in the log above; say where the bytes
|
|
195
|
+
# actually come from so a slow/stalled transfer is attributed right.
|
|
196
|
+
print(f"[stage {index}/{total}] note: {name} redirected to {final_host}", flush=True)
|
|
116
197
|
resume = bool(have) and getattr(resp, "status", 200) == 206
|
|
117
198
|
with part.open("ab" if resume else "wb") as out:
|
|
118
199
|
while True:
|
|
@@ -132,8 +213,113 @@ def attempt(opener, url: str, dest: Path, part: Path, size: int, sha, name: str,
|
|
|
132
213
|
print(f"[stage {index}/{total}] placed {name}", flush=True)
|
|
133
214
|
|
|
134
215
|
|
|
216
|
+
def attempt_segment(opener, url: str, seg_part: Path, start: int, end: int,
|
|
217
|
+
name: str, i: int, index: int, total: int):
|
|
218
|
+
"""One endpoint, one byte range: stream into the segment's own .part
|
|
219
|
+
(resuming from its current size when the server honours Range). Returns
|
|
220
|
+
the final segment size. Raises _NoRangeError on a 200 (server ignores
|
|
221
|
+
Range → the caller falls back to single-stream) and RuntimeError on a
|
|
222
|
+
short/oversize transfer. A network failure leaves the partial segment part
|
|
223
|
+
on disk so the next attempt resumes it."""
|
|
224
|
+
host = urllib.parse.urlsplit(url).hostname or url
|
|
225
|
+
print(f"[stage {index}/{total}] seg {i}/{SEG_COUNT} {name} via {host}", flush=True)
|
|
226
|
+
seg_len = end - start
|
|
227
|
+
have = seg_part.stat().st_size if seg_part.exists() else 0
|
|
228
|
+
if have > seg_len:
|
|
229
|
+
# A segment part longer than its range can never be right (torn write
|
|
230
|
+
# or a stale part from a different layout): restart it from zero.
|
|
231
|
+
seg_part.unlink()
|
|
232
|
+
have = 0
|
|
233
|
+
if have == seg_len:
|
|
234
|
+
# Already complete from an earlier attempt — no request needed.
|
|
235
|
+
return have
|
|
236
|
+
# Closed range: the resume point is start+have, and the end stays pinned
|
|
237
|
+
# so a server that answers cannot overshoot the segment (an open-ended
|
|
238
|
+
# resume would pull the rest of the file into this segment).
|
|
239
|
+
headers = {"Range": f"bytes={start + have}-{end - 1}"}
|
|
240
|
+
with open_following(opener, url, headers) as resp:
|
|
241
|
+
if getattr(resp, "status", 200) != 206:
|
|
242
|
+
raise _NoRangeError(f"segment {i} of {name}: server answered {getattr(resp, 'status', 200)} to a Range request")
|
|
243
|
+
# 206 → append; the Range started exactly at `have`, so whatever is on
|
|
244
|
+
# disk stays and the missing tail streams in after it.
|
|
245
|
+
with seg_part.open("ab") as out:
|
|
246
|
+
while True:
|
|
247
|
+
block = resp.read(STITCH_BLOCK)
|
|
248
|
+
if not block:
|
|
249
|
+
break
|
|
250
|
+
out.write(block)
|
|
251
|
+
got = seg_part.stat().st_size if seg_part.exists() else 0
|
|
252
|
+
if got != seg_len:
|
|
253
|
+
if got > seg_len:
|
|
254
|
+
seg_part.unlink(missing_ok=True)
|
|
255
|
+
raise RuntimeError(f"segment {i} of {name}: expected {seg_len} bytes, got {got}")
|
|
256
|
+
return got
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def stitch_segments(dest: Path, part: Path, ranges) -> None:
|
|
260
|
+
"""Concatenate the segment parts into the single .part in order (blockwise,
|
|
261
|
+
so peak disk stays at ~2x the file), then drop the segment parts. The
|
|
262
|
+
caller's existing size/sha verification then judges the assembled part."""
|
|
263
|
+
with part.open("wb") as out:
|
|
264
|
+
for i, (_start, _end) in enumerate(ranges):
|
|
265
|
+
seg_part = seg_part_of(dest, i)
|
|
266
|
+
with seg_part.open("rb") as seg:
|
|
267
|
+
while True:
|
|
268
|
+
block = seg.read(STITCH_BLOCK)
|
|
269
|
+
if not block:
|
|
270
|
+
break
|
|
271
|
+
out.write(block)
|
|
272
|
+
clear_segment_parts(dest)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def attempt_segmented(opener, url: str, dest: Path, part: Path, size: int, name: str,
|
|
276
|
+
index: int, total: int) -> bool:
|
|
277
|
+
"""One endpoint, one whole-file segmented attempt: 4 ranges fetched
|
|
278
|
+
concurrently, stitched into the single .part. Returns True when the .part
|
|
279
|
+
is complete and ready for verification, False when the server does not
|
|
280
|
+
honour Range (parts cleaned, single-stream takes over). Other failures
|
|
281
|
+
raise with the segment parts kept for the next attempt's resume."""
|
|
282
|
+
host = urllib.parse.urlsplit(url).hostname or url
|
|
283
|
+
print(f"[stage {index}/{total}] fetching {name} via {host}", flush=True)
|
|
284
|
+
ranges = segment_ranges(size)
|
|
285
|
+
if len(ranges) < 2:
|
|
286
|
+
return False
|
|
287
|
+
# A previous single-stream attempt may have left a partial .part; the
|
|
288
|
+
# segment parts are the source of truth from here, so it would only be
|
|
289
|
+
# truncated by the stitch anyway.
|
|
290
|
+
part.unlink(missing_ok=True)
|
|
291
|
+
try:
|
|
292
|
+
with ThreadPoolExecutor(max_workers=SEG_COUNT) as pool:
|
|
293
|
+
futures = [
|
|
294
|
+
pool.submit(attempt_segment, opener, url, seg_part_of(dest, i), start, end,
|
|
295
|
+
name, i + 1, index, total)
|
|
296
|
+
for i, (start, end) in enumerate(ranges)
|
|
297
|
+
]
|
|
298
|
+
errors = []
|
|
299
|
+
for fut in as_completed(futures):
|
|
300
|
+
try:
|
|
301
|
+
fut.result()
|
|
302
|
+
except Exception as error: # noqa: BLE001 — collect, decide below
|
|
303
|
+
errors.append(error)
|
|
304
|
+
if errors:
|
|
305
|
+
# A 200 anywhere is a capability statement, not a transient
|
|
306
|
+
# failure: it must win over any other segment error so the
|
|
307
|
+
# single-stream fallback always triggers when Range is unsupported.
|
|
308
|
+
for error in errors:
|
|
309
|
+
if isinstance(error, _NoRangeError):
|
|
310
|
+
raise error
|
|
311
|
+
raise errors[0]
|
|
312
|
+
except _NoRangeError:
|
|
313
|
+
# Not this server's capability: clean slate, single-stream path from
|
|
314
|
+
# scratch — that path treats a 200 as a normal full-file download.
|
|
315
|
+
clear_segment_parts(dest)
|
|
316
|
+
return False
|
|
317
|
+
stitch_segments(dest, part, ranges)
|
|
318
|
+
return True
|
|
319
|
+
|
|
320
|
+
|
|
135
321
|
def stage_file(*, name: str, rel: str, dest: Path, size: int, sha, endpoints,
|
|
136
|
-
middle: str, index: int, total: int) -> bool:
|
|
322
|
+
middle: str, index: int, total: int, seg_threshold: int = SEG_THRESHOLD) -> bool:
|
|
137
323
|
"""Place one pinned file. Returns True on placed/skipped, False after every
|
|
138
324
|
endpoint failed in both the direct and the system-proxy round."""
|
|
139
325
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -150,22 +336,55 @@ def stage_file(*, name: str, rel: str, dest: Path, size: int, sha, endpoints,
|
|
|
150
336
|
return True
|
|
151
337
|
part.unlink()
|
|
152
338
|
|
|
339
|
+
def attempt_endpoint(opener, url, first):
|
|
340
|
+
"""One endpoint for this file. Big files try the segmented route
|
|
341
|
+
first; a server that will not do Range (or a file too small to split)
|
|
342
|
+
goes through the untouched single-stream attempt()."""
|
|
343
|
+
if size < seg_threshold:
|
|
344
|
+
attempt(opener, url, dest, part, size, sha, name, index, total, first=first)
|
|
345
|
+
return
|
|
346
|
+
if not first:
|
|
347
|
+
print(f"[stage {index}/{total}] switched endpoint: {host_of(url)}", flush=True)
|
|
348
|
+
if attempt_segmented(opener, url, dest, part, size, name, index, total):
|
|
349
|
+
# Stitched: the same verification the single-stream path applies.
|
|
350
|
+
got = part.stat().st_size if part.exists() else 0
|
|
351
|
+
if got != size:
|
|
352
|
+
part.unlink(missing_ok=True)
|
|
353
|
+
clear_segment_parts(dest)
|
|
354
|
+
raise RuntimeError(f"expected {size} bytes, got {got}")
|
|
355
|
+
if sha is not None and sha256_of(part) != sha:
|
|
356
|
+
part.unlink(missing_ok=True)
|
|
357
|
+
clear_segment_parts(dest)
|
|
358
|
+
raise RuntimeError("sha256 mismatch")
|
|
359
|
+
part.rename(dest)
|
|
360
|
+
print(f"[stage {index}/{total}] placed {name}", flush=True)
|
|
361
|
+
return
|
|
362
|
+
# 200 fallback: the switch (if any) was already announced above, so
|
|
363
|
+
# single-stream runs with first=True and just re-announces fetching.
|
|
364
|
+
attempt(opener, url, dest, part, size, sha, name, index, total, first=True)
|
|
365
|
+
|
|
153
366
|
url_of = lambda endpoint: f"{endpoint.rstrip('/')}/{middle}/{rel}" # noqa: E731
|
|
154
367
|
last_error = None
|
|
155
368
|
attempts = 0
|
|
156
369
|
direct = build_opener(direct=True)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
370
|
+
# Two direct rounds before touching the proxy: mirror flakiness is bursty
|
|
371
|
+
# (a file that times out once often succeeds seconds later), and a second
|
|
372
|
+
# direct round is free while proxy bytes are not.
|
|
373
|
+
for round_no in (1, 2):
|
|
374
|
+
for endpoint in endpoints:
|
|
375
|
+
try:
|
|
376
|
+
attempt_endpoint(direct, url_of(endpoint), first=attempts == 0)
|
|
377
|
+
return True
|
|
378
|
+
except Exception as error: # noqa: BLE001 — any failure means: next endpoint
|
|
379
|
+
last_error = error
|
|
380
|
+
attempts += 1
|
|
381
|
+
if round_no == 1:
|
|
382
|
+
print(f"stage: {name}: direct round failed ({last_error}); retrying direct once", flush=True)
|
|
164
383
|
print("stage: retrying via system proxy (may count against proxy data)", flush=True)
|
|
165
384
|
proxied = build_opener(direct=False)
|
|
166
385
|
for endpoint in endpoints:
|
|
167
386
|
try:
|
|
168
|
-
|
|
387
|
+
attempt_endpoint(proxied, url_of(endpoint), first=attempts == 0)
|
|
169
388
|
return True
|
|
170
389
|
except Exception as error: # noqa: BLE001
|
|
171
390
|
last_error = error
|
|
@@ -179,6 +398,8 @@ def main(argv=None) -> int:
|
|
|
179
398
|
ap.add_argument("--dir", type=Path, required=True, help="model root (the engine fetch's --dir)")
|
|
180
399
|
ap.add_argument("--engine-script", type=Path, required=True, help="absolute path to the engine's fetch_h3_image.py")
|
|
181
400
|
ap.add_argument("--endpoints", required=True, help="comma-separated priority list; 'hf' or https://host[/prefix] per entry")
|
|
401
|
+
ap.add_argument("--seg-threshold", type=int, default=SEG_THRESHOLD,
|
|
402
|
+
help=f"files at or above this many bytes download in {SEG_COUNT} concurrent Range segments (default {SEG_THRESHOLD})")
|
|
182
403
|
args = ap.parse_args(argv)
|
|
183
404
|
|
|
184
405
|
# The engine script's own directory goes on sys.path so `import
|
|
@@ -214,7 +435,8 @@ def main(argv=None) -> int:
|
|
|
214
435
|
total = len(items)
|
|
215
436
|
for index, (name, rel, dest, size, sha) in enumerate(items, 1):
|
|
216
437
|
ok = stage_file(name=name, rel=rel, dest=dest, size=size, sha=sha,
|
|
217
|
-
endpoints=endpoints, middle=middle, index=index, total=total
|
|
438
|
+
endpoints=endpoints, middle=middle, index=index, total=total,
|
|
439
|
+
seg_threshold=args.seg_threshold)
|
|
218
440
|
if not ok:
|
|
219
441
|
print(f"STAGE FAIL {rel}", file=sys.stderr, flush=True)
|
|
220
442
|
return 1
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
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.
|
|
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.11): 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
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.
|
|
4
|
+
"description": "非官方第三方插件,与 MiniMax 无关联。在 Apple Silicon 上用开源 MiniMax H3-Image(MLX 引擎)做完全本地文生图。安装向导一次性把固定版本的引擎与权重下载到本机(下载前展示 MiniMax 社区许可及地域条款,由你本人确认接受);之后生图全程离线——不用密钥、不用账号、不联网。安装中的长步骤是后台任务,DSH 重启不丢、断点续传。需 Apple Silicon、48GB 及以上统一内存(32GB 不保证)、安装期约 60GB 可用磁盘;Intel Mac 与其他平台在检查阶段即明确拒绝。当前版本(0.2.11):只读环境体检 + 安装向导 + 本地产图(社区许可需你本人确认接受后才会出图)。"
|
|
5
5
|
}
|
|
6
6
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-h3-image",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
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",
|