{"record":{"id":"e058681eebe0a12b","repo":"unslothai/unsloth","slug":"controlnet-spec-id-is-for-join-entry-fam","errorCode":null,"errorMessage":"ControlNet '{spec_id}' is for {', '.join(entry.families)}, not the loaded '{family}' model; pick a ControlNet built for this family.","messagePattern":"ControlNet '(.+?)' is for (.+?), not the loaded '(.+?)' model; pick a ControlNet built for this family\\.","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"studio/backend/core/inference/diffusion_controlnet.py","lineNumber":171,"sourceCode":"\ndef resolve_controlnet(spec_id: str, *, family: Optional[str] = None) -> ResolvedControlNet:\n    \"\"\"Resolve a ControlNet id to a loadable repo id / local dir.\n\n    Accepts a catalog/local id or a bare HF repo id (``owner/name``); the backend loads it with\n    ``from_pretrained``. Raises on an unknown id (caller maps to 400).\n\n    ``family`` enforces compatibility: a ControlNet is architecture-specific, so an entry tagged\n    for another family is rejected here rather than loaded through the wrong pipeline later.\n    \"\"\"\n    entry = _catalog_by_id().get(spec_id)\n    if entry is None:\n        # A curated entry named by its full repo id must still hit the family gate below, not slip through the bare-repo fallback.\n        entry = next((e for e in _CURATED if e.repo_id and e.repo_id == spec_id), None)\n    if entry is not None:\n        # A direct API call could send an entry for another family; reject it before any download.\n        fam = (family or \"\").strip().lower()\n        if entry.families and fam and fam not in {f.lower() for f in entry.families}:\n            raise ValueError(\n                f\"ControlNet '{spec_id}' is for {', '.join(entry.families)}, not the loaded \"\n                f\"'{family}' model; pick a ControlNet built for this family.\"\n            )\n        if entry.source == \"local\":\n            path = entry.local_path or \"\"\n            if not path or not Path(path).is_dir():\n                raise FileNotFoundError(f\"ControlNet '{spec_id}' is no longer present on disk\")\n            return ResolvedControlNet(spec_id, path, is_local = True)\n        if not entry.repo_id:\n            raise ValueError(f\"ControlNet '{spec_id}' has no repo\")\n        return ResolvedControlNet(spec_id, entry.repo_id, is_local = False)\n\n    # A bare HF repo id (owner/name). STRICT shape so a filesystem-looking id can never reach from_pretrained.\n    if re.fullmatch(r\"[A-Za-z0-9][A-Za-z0-9_.-]*/[A-Za-z0-9][A-Za-z0-9_.-]*\", spec_id):\n        return ResolvedControlNet(spec_id, spec_id, is_local = False)\n\n    raise FileNotFoundError(\n        f\"unknown ControlNet '{spec_id}': not a local model, catalog entry, or HF repo id\"","sourceCodeStart":153,"sourceCodeEnd":189,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/core/inference/diffusion_controlnet.py#L153-L189","documentation":"ControlNets are architecture-specific: resolve_controlnet() looks the spec_id up in the curated catalog (including full-repo-id matches) and rejects an entry whose families list does not contain the loaded model's family, before any download happens. The family check also catches full-repo-id lookups so a curated entry named by repo id cannot bypass the gate via the bare-repo fallback.","triggerScenarios":"Passing a ControlNet id tagged for another family — e.g. an SDXL ControlNet ('xinsir ... sdxl ...') while a FLUX model is loaded, or a Flux Union CN while SDXL is loaded. The loaded family is passed in as family=state.family.name.","commonSituations":"Switching loaded models without re-picking the ControlNet; curated lists mixing SD/SDXL/Flux entries with similar names; automated pipelines pairing a fixed CN repo with a rotating set of base models.","solutions":["Pick a ControlNet whose catalog entry lists the loaded family (check entry.families).","After switching the loaded model, re-resolve the ControlNet for the new family before generating.","Expose family info in your picker UI so CN options filter by the loaded model."],"exampleFix":"# before: SDXL CN with a Flux model loaded\nengine.generate(prompt=p, controlnet=(\"xinsir-controlnet-v1.1-sdxl-canny\", img, \"canny\", 0.8, 0.0, 1.0))\n# after: Flux-family CN\nengine.generate(prompt=p, controlnet=(\"instantx/flux-union-controlnet\", img, \"canny\", 0.8, 0.0, 1.0))","handlingStrategy":"validation","validationCode":"from core.inference import diffusion_controlnet\n\ndef cn_matches_family(spec_id: str, family: str) -> bool:\n    entry = diffusion_controlnet._catalog_by_id().get(spec_id)\n    if entry is None:\n        return False\n    fam = (family or \"\").strip().lower()\n    return not entry.families or not fam or fam in {f.lower() for f in entry.families}","typeGuard":"def controlnet_suitable_for(spec_id: str, family: str) -> bool:\n    return cn_matches_family(spec_id, family)","tryCatchPattern":"try:\n    out = engine.generate(prompt=p, controlnet=(cn_id, img, t, s, gs, ge))\nexcept ValueError as e:\n    if \"is for\" in str(e) and \"pick a ControlNet built for this family\" in str(e):\n        cn_id = default_cn_for(family=state.family.name)  # re-pick from catalog by family\n        out = engine.generate(prompt=p, controlnet=(cn_id, img, t, s, gs, ge))\n    else:\n        raise","preventionTips":["Filter the CN catalog by entry.families against the loaded family before showing options.","Clear or re-resolve the selected ControlNet whenever the loaded model changes.","Never hardcode one CN id across a pipeline that rotates base models."],"tags":["diffusion","controlnet","model-family","validation","catalog"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}