Model artifacts (Model Artifact SPI)
Once a third-party local offline ASR / MT / TTS plugin declares model artifacts, the host takes over download / mirror switching / readiness checks / settings-page card / hardware rating — you do not write any download UI.
Do not hand-roll "model download path", "China mirror toggle" or "download button" inside
config_schema!Since 0.2.4, every declared source needs a real, verified repository address. ModelScope no longer reuses the Hugging Face repo id; without
repos.modelscopethat source is treated as non-existent. Also setmanual_urlso the manual-download page can be shown on failure.
How to declare (either one; model_artifacts() wins)
Option 1: mod.json (recommended)
{
"requires_cuda": false,
"model_artifacts": [
{
"id": "my-asr-weights",
"name": "My ASR 1.2B",
"category": "asr",
"repo": "org/my-asr-1.2b",
"repos": {
"huggingface": "org/my-asr-1.2b",
"hf-mirror": "org/my-asr-1.2b",
"modelscope": "RealOrg/My-ASR-1.2B"
},
"manual_url": "https://huggingface.co/org/my-asr-1.2b/tree/main",
"est_bytes": 1200000000,
"sources": ["huggingface", "modelscope"],
"ready_globs": ["*.safetensors", "*.bin"],
"allow_patterns": ["*.safetensors", "*.bin"],
"requires_cuda": false
}
]
}Option 2: Provider override
def model_artifacts(self) -> list[dict]: ...The returned dicts have exactly the same structure as above. It takes precedence over the manifest and suits picking a variant dynamically from config / what is already downloaded.
Full field table
| Field | Required | Description |
|---|---|---|
id | Yes | Artifact id (the host API uses it to locate the artifact) |
name | Recommended | Display name |
category | Recommended | asr / mt / tts — decides which settings page it is attached to |
repo | See below | Canonical id (usually the HF side); decides the local directory data/models/<org>--<name> |
est_bytes | Recommended | Estimated size (used by the progress bar and the disk pre-check); shows "unknown size" when missing |
sources | No | Allowed download sources, default ["huggingface", "modelscope"]. The host orders them by the user's mirror choice: global official → HF; hf-mirror → hf-mirror → HF official (the mirror may 308 large files back to HF); China → ModelScope (if declared) → hf-mirror. ★ If the model only exists on HF, declare ["huggingface", "hf-mirror"] — in China mode the host then goes straight to hf-mirror and never hits ModelScope |
repos | No | Real per-source repo id override, keys are huggingface / hf-mirror / modelscope. ModelScope must be declared explicitly; without repos.modelscope that source is non-existent, and the host will not guess using the HF id. |
manual_url | Recommended from 0.2.4 | Model-file page / official download page for this artifact. On failure, "Can't download? Manual download" in Settings opens it directly. It must be a real, verified page — never a guessed URL. |
homepage | No | Backward-compatible alias for manual_url; manual_url wins when both are present. |
ready_globs | Yes | Readiness: every one of these globs must match among the direct child files of the target directory |
allow_patterns | No | Download whitelist. When not given explicitly the host reuses ready_globs, so a multi-quant repo (a GGUF repo, tens of GB) is not pulled down in full |
requires_cuda | No | false = no hard dependency on NVIDIA (supports AMD ROCm / Vulkan / DirectML / multi-core CPU) → the host does not raise the restricted red box on AMD/Intel/pure CPU and rates it Good / Excellent instead |
dir | No | Custom on-disk directory name (relative to <data dir>/models). Only a single safe path segment is accepted (/ \ .. : forbidden); an illegal value is ignored automatically |
parts | No | Use when one artifact is assembled from several repos, ≤ 8 entries. See below |
Common repos example (ModelScope and HF often use different org names):
"repos": {
"huggingface": "tencent/Hy-MT2-1.8B-GGUF",
"hf-mirror": "tencent/Hy-MT2-1.8B-GGUF",
"modelscope": "Tencent-Hunyuan/Hy-MT2-1.8B-GGUF"
}Real-address checklist (verify before publishing)
- Open every
reposURL and confirm the repository/page exists and its file list contains theready_globsfiles. - ModelScope and HF often use different org/repo names. Never copy the HF id. If there is no ModelScope repository, remove
modelscopefromsourcesinstead of keeping a declaration that is guaranteed to 404. manual_urlmust open a page where the user can see the files (usuallyhttps://huggingface.co/<org>/<repo>/tree/main, or the corresponding ModelScope files page).- Put exact final file names in
ready_globs, not a vague*.gguf; this is what the manual-download panel shows to the user. - Every part of a multi-repo
partsartifact needs its own realrepo; part-levelreposis not inherited from the artifact.
On failure the model card shows "Can't download? Manual download" with
manual_url,ready_globsand the target directory. A guessed URL sends the user to a 404, so click it once before publishing.
Hugging Face-only weights (no ModelScope source): best practice
If your model is published only on Hugging Face (nobody mirrored it to ModelScope), declare it on the artifact or on each part:
"repo": "org/my-model-gguf",
"repos": {
"huggingface": "org/my-model-gguf",
"hf-mirror": "org/my-model-gguf"
},
"manual_url": "https://huggingface.co/org/my-model-gguf/tree/main",
"sources": ["huggingface", "hf-mirror"],
"ready_globs": ["my-model-Q4_K_M.gguf"]What the host guarantees:
- No blind wall-hitting: when a China-based user is on the default "China mirror" setting, the host follows the artifact's source declaration strictly. Seeing no
modelscope, it uses the hf-mirror China mirror directly to pull the Hugging Face assets at full speed — no 404 or timeout from probing a repository that does not exist on ModelScope. - Multi-node fallback with watchdog reset: if a model supports both ModelScope and hf-mirror and the preferred ModelScope node fails (network jitter or a stream cut by the server), the host falls back to
hf-mirrorseamlessly. The moment it switches, the host clears and resets the watchdog stall counter, giving the fallback mirror a full first-packet handshake window (96 s grace) and transfer budget — so the time burnt on the previous node can never starve the fallback.
Multi-repo assembly with parts + a custom dir
Problem: when the weights and the tokenizer / codec / vision tower live in different repos, one artifact = one repo cannot express that (the files never complete, and it stays stuck at "not ready").
{
"id": "qwen3-tts-1.7b-customvoice-q8",
"name": "Qwen3-TTS 1.7B CustomVoice (Q8_0)",
"category": "tts",
"dir": "qwen3-tts-1.7b-customvoice",
"est_bytes": 2480000000,
"sources": ["huggingface"],
"ready_globs": ["qwen3-tts-12hz-1.7b-customvoice-q8_0.gguf",
"qwen3-tts-tokenizer-12hz-q8_0.gguf"],
"parts": [
{ "repo": "cstr/qwen3-tts-1.7b-customvoice-GGUF",
"allow_patterns": ["*customvoice-q8_0.gguf"] },
{ "repo": "cstr/qwen3-tts-tokenizer-12hz-GGUF",
"allow_patterns": ["*tokenizer-12hz-q8_0.gguf"] }
]
}- Each part supports its own
repo/repos/sources/allow_patterns(applied per part), with a limit of 8 entries; writing the samerepotwice merges theallow_patterns. - When a part does not declare
sourcesit inherits the artifact-levelsources;reposis not inherited (it is an override for a specific repo). - Files from all parts land in the same directory (
dir→ canonicalreponame → first part's repo name); the host downloads the primary repo first, then fills in the remaining parts one by one; all of them must succeed for the artifact to be ready. - If any part fails the artifact as a whole is not ready; a retry skips files already on disk (resumable), and the progress text carries
1/2,2/2. ready_globsis still evaluated only against the direct child files of that directory.
Two traps
- Write
allow_patternsfor every part: a part without a whitelist is downloaded as the whole repo (the exception is the first part on the main path — it falls back to the artifact-levelallow_patterns/ready_globs). - Do not let different parts produce files with the same name (the later download silently overwrites the earlier one). Duplicate names should only occur in repos that
allow_patternshas narrowed into non-overlapping sets.
Host API
host.list_model_artifacts() -> list[dict]
host.inspect_model_artifact(plugin_id: str, artifact_id: str) -> dict
host.download_model_artifact(plugin_id: str, artifact_id: str) -> Noneinspect_model_artifact() returns:
{
"plugin_id": str,
"artifact_id": str,
"name": str,
"category": str,
"repo": str,
"est_bytes": int,
"is_downloaded": bool,
"model_dir": str, # directory where the weights were actually found; "" when not ready
"target_dir": str, # ★ definitive on-disk directory, always set whether downloaded or not
"size_str": str,
}Use
target_dirto locate files (model_diris empty while not ready). Do not build<org>--<name>yourself — it cannot be derived with multi-repoparts/ a customdir.
info = self.host.inspect_model_artifact(self.manifest.id, "my-asr-weights")
root = Path(info["target_dir"]) # where to read the files from
exe = root / "model.gguf"
ready = bool(info.get("is_downloaded"))The matching QML slots: listModelArtifacts() / inspectModelArtifact(pid, aid) / downloadModelArtifact(pid, aid).
Readiness check hook
def check_ready(self) -> tuple[bool, str, str]:
"""Returns (ok, issue_type, message).
issue_type: '' | 'missing_key' | 'model_not_ready' | 'unassigned'"""If not implemented, the host degrades to skipping this check.
Semantics:
check_ready()answers "can it be used" (is the runtime present, are the weights complete, is the key configured), not "is the service running right now". Local-service plugins must treat "service not started" as available (True), otherwise the user is refused when enabling a track (the host only callsensure_loaded()to start the service on paths that really need it). See Local-service engine contract.
Multi-capability models (variants)
When one model has several mutually exclusive variants (e.g. Qwen3-TTS base cloning / customvoice built-in voices / voicedesign text-described voices), declare them as multiple model_artifacts (each with its own ready_globs), and in model_artifacts() put the currently effective one first:
def model_artifacts(self) -> list[dict]:
arts = [...] # all variants
active = self._resolve_variant() # pick the effective one from config / what is downloaded
arts.sort(key=lambda a: 0 if a["id"] == active else 1)
return artsThe host model card shows and downloads the first artifact of the same category, so the user only needs the current variant: it is neither falsely reported as "not ready" nor forced to download every variant (for Qwen3-TTS, each variant is a complete LM, roughly 1.1 GB).
Measured reference: _VARIANTS / _ARTIFACT_BY_VARIANT in extensions/workshop_common_qwen3_tts.
Zero-code presence on the settings page
As long as a plugin declares model artifacts and its tags contain ["local"] or ["offline"]:
HardwareRatingBanner: automatically rates the compute capability from the model size and the GPU/CPU environment ("Excellent setup: RTX 4070 Ti (ample VRAM, blazing-fast millisecond offline translation)" / "Fast & light: pure CPU offline runs well" …)LocalModelStorageCard: global official / China mirror / hf-mirror node switching, proxy toggle and address, real byte progress, open storage path, custom relocation and redirection, one-click cache clearGpuReleaseRow: unified VRAM release and acceleration-framework status
Put only inference hyper-parameters in config_schema.
Other related properties
@property
def supports_concurrent_streams(self) -> bool:
"""Default True. Return False when the underlying engine is a single live handle, so the host degrades the other track."""
@property
def requires_hf_runtime(self) -> bool | None:
"""True=needs HF; False=pure API/cloud, the warm-up can be skipped; None=infer from manifest.is_local."""requires_hf_runtime can also be written in mod.json as "requires_hf": false. Third-party cloud plugins should declare false or tags: ["cloud"] to avoid a pointless transformers warm-up.
Search path (used by the host when resolving already-downloaded weights): plugin directory models/{artifact_id} → HF cache models--{org--name} → ModelScope cache.
Related
- Local-service engine contract
- Capabilities & read-only contract
- asr / mt / tts — the local offline pattern for each type
- Host runtimes — binary runtimes go this way, not through
model_artifacts
