Skip to content

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.modelscope that source is treated as non-existent. Also set manual_url so the manual-download page can be shown on failure.


How to declare (either one; model_artifacts() wins) ​

json
{
  "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 ​

python
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 ​

FieldRequiredDescription
idYesArtifact id (the host API uses it to locate the artifact)
nameRecommendedDisplay name
categoryRecommendedasr / mt / tts — decides which settings page it is attached to
repoSee belowCanonical id (usually the HF side); decides the local directory data/models/<org>--<name>
est_bytesRecommendedEstimated size (used by the progress bar and the disk pre-check); shows "unknown size" when missing
sourcesNoAllowed 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
reposNoReal 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_urlRecommended from 0.2.4Model-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.
homepageNoBackward-compatible alias for manual_url; manual_url wins when both are present.
ready_globsYesReadiness: every one of these globs must match among the direct child files of the target directory
allow_patternsNoDownload 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_cudaNofalse = 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
dirNoCustom on-disk directory name (relative to <data dir>/models). Only a single safe path segment is accepted (/ \ .. : forbidden); an illegal value is ignored automatically
partsNoUse when one artifact is assembled from several repos, ≤ 8 entries. See below

Common repos example (ModelScope and HF often use different org names):

json
"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) ​

  1. Open every repos URL and confirm the repository/page exists and its file list contains the ready_globs files.
  2. ModelScope and HF often use different org/repo names. Never copy the HF id. If there is no ModelScope repository, remove modelscope from sources instead of keeping a declaration that is guaranteed to 404.
  3. manual_url must open a page where the user can see the files (usually https://huggingface.co/<org>/<repo>/tree/main, or the corresponding ModelScope files page).
  4. Put exact final file names in ready_globs, not a vague *.gguf; this is what the manual-download panel shows to the user.
  5. Every part of a multi-repo parts artifact needs its own real repo; part-level repos is not inherited from the artifact.

On failure the model card shows "Can't download? Manual download" with manual_url, ready_globs and 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:

json
"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:

  1. 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.
  2. 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-mirror seamlessly. 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").

json
{
  "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 same repo twice merges the allow_patterns.
  • When a part does not declare sources it inherits the artifact-level sources; repos is not inherited (it is an override for a specific repo).
  • Files from all parts land in the same directory (dir → canonical repo name → 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_globs is still evaluated only against the direct child files of that directory.

Two traps ​

  1. Write allow_patterns for 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-level allow_patterns / ready_globs).
  2. 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_patterns has narrowed into non-overlapping sets.

Host API ​

python
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) -> None

inspect_model_artifact() returns:

python
{
  "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_dir to locate files (model_dir is empty while not ready). Do not build <org>--<name> yourself — it cannot be derived with multi-repo parts / a custom dir.

python
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 ​

python
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 calls ensure_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:

python
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 arts

The 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 clear
  • GpuReleaseRow: unified VRAM release and acceleration-framework status

Put only inference hyper-parameters in config_schema.


python
@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.


TransBox — empowering borderless cross-language communication for everyone