File size: 7,891 Bytes
66ee87e
 
6012dcc
66ee87e
 
6012dcc
66ee87e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6012dcc
66ee87e
 
 
 
 
 
6012dcc
 
66ee87e
 
 
 
 
6012dcc
66ee87e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
"""Which models DecisionLab compares.

First the operator's Hugging Face models, in this order (rulings 2026-09-28 and 2026-09-30):
  LightDec_Arthur  Falconsai/LightDec_Arthur  (Arthur; code ships with DecisionLab)
  LightDec_V2      Falconsai/LightDec_V2      (FalconDec; its modeling code must be on the allowlist)
  Enterprise Reflux Laya V2.1  yasserrmd/enterprise-reflux-laya-v21  (a Laya fine-tune; loaded with laya 0.3.20)
  Laya             convaiinnovations/laya
Then every model folder found in the models directory, named "... (local)".

The models directory (MODELS_DIR, default /models, mounted from ./models on the host) is scanned once at
start-up. Two kinds of folder are recognised, in folder-name order:
  - FalconDec (LightDec and variants): a falcondec_config.json; named from that config.
  - Arthur: a config.json with Arthur's architecture keys plus model.safetensors; named "Arthur <tier>".
    Arthur folders hold data only: the code that runs them ships with DecisionLab (app/arthur.py).
Laya is always last and still comes from the Hugging Face Hub.

Pure Python, no torch: discovery and its settings can be tested anywhere.
"""
from __future__ import annotations

import hashlib
import json
import re
from pathlib import Path
from typing import Mapping

VARIANTS = ("fp16", "int8")
CONFIG_FILE = "falcondec_config.json"
DEFAULT_MODELS_DIR = "/models"
PALETTE = ("c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7")   # colour slots; CSS defines each


def _variant(env: Mapping[str, str], name: str) -> str:
    v = env.get(name) or "fp16"
    if v not in VARIANTS:
        raise ValueError(f"{name} must be fp16 or int8 (got '{v}').")
    return v


def _slug(folder: str) -> str:
    return re.sub(r"[^a-z0-9]+", "_", folder.lower()).strip("_") or "model"


def _display_name(folder: Path) -> str:
    try:
        cfg = json.loads((folder / CONFIG_FILE).read_text(encoding="utf-8"))
        name, version = cfg.get("name"), cfg.get("version")
        if name:
            return f"{name} {version}" if version else str(name)
    except (OSError, ValueError, AttributeError):
        pass
    return folder.name


ARTHUR_KEYS = frozenset({"tier", "d", "heads", "e", "buckets", "recursions", "interact", "mlp", "layout", "temperatures"})


def _arthur_config(folder: Path) -> dict | None:
    """The folder's Arthur config, or None if it is not an Arthur model folder."""
    if not (folder / "config.json").is_file() or not (folder / "model.safetensors").is_file():
        return None
    try:
        cfg = json.loads((folder / "config.json").read_text(encoding="utf-8"))
    except (OSError, ValueError):
        return None
    return cfg if isinstance(cfg, dict) and ARTHUR_KEYS <= set(cfg) else None


def _kind(folder: Path) -> str | None:
    if (folder / CONFIG_FILE).is_file():
        return "lightdec"
    if _arthur_config(folder) is not None:
        return "arthur"
    return None


def discover(models_dir: str) -> list[tuple[Path, str]]:
    """(folder, kind) for every recognised model folder in models_dir, sorted by folder name. Missing dir -> []."""
    root = Path(models_dir)
    if not root.is_dir():
        return []
    found = ((p, _kind(p)) for p in root.iterdir() if p.is_dir())
    return sorted(((p, k) for p, k in found if k), key=lambda pk: pk[0].name)


def model_specs(env: Mapping[str, str]) -> list[dict]:
    """One dict per model, in the order the UI shows them: the four Hub models, then the models folder."""
    variant = _variant(env, "LIGHTDEC_VARIANT")
    specs = [
        {"key": "lightdec_arthur", "name": "LightDec_Arthur", "kind": "arthur", "side": PALETTE[0], "source": "hub",
         "repo": env.get("LIGHTDEC_ARTHUR_REPO") or "Falconsai/LightDec_Arthur"},
        {"key": "lightdec_v2", "name": "LightDec_V2", "kind": "lightdec", "side": PALETTE[1], "source": "hub",
         "repo": env.get("LIGHTDEC_V2_REPO") or "Falconsai/LightDec_V2", "revision": None, "variant": variant},
        {"key": "enterprise_reflux_laya_v21", "name": "Enterprise Reflux Laya V2.1", "kind": "laya", "side": PALETTE[2],
         "repo": env.get("REFLUX_LAYA_REPO") or "yasserrmd/enterprise-reflux-laya-v21", "fallback_repo": ""},
        {"key": "laya", "name": "Laya", "kind": "laya", "side": "laya",
         "repo": env.get("LAYA_REPO") or "convaiinnovations/laya",
         "fallback_repo": env.get("LAYA_FALLBACK_REPO", "")},
    ]
    used = {s["key"] for s in specs}
    for i, (folder, kind) in enumerate(discover(env.get("MODELS_DIR") or DEFAULT_MODELS_DIR), start=3):
        key, n = _slug(folder.name), 2
        while key in used:
            key, n = f"{_slug(folder.name)}_{n}", n + 1
        used.add(key)
        spec = {"key": key, "kind": kind, "side": PALETTE[i % len(PALETTE)], "source": "local", "path": str(folder),
                "path_env": "MODELS_DIR"}
        if kind == "arthur":
            cfg = _arthur_config(folder)
            name = f"Arthur {cfg.get('tier', folder.name)}" + (" (pretrained)" if cfg.get("pretrain") else "")
        else:
            name = _display_name(folder)
            spec["variant"] = variant
        spec["name"] = f"{name} (local)"
        specs.append(spec)
    return specs


def load_order(env: Mapping[str, str]) -> list[str]:
    """Which models load at start-up, in order. Default: comparison order."""
    known = [s["key"] for s in model_specs(env)]
    raw = env.get("LOAD_ORDER")
    wanted = [k.strip() for k in raw.split(",")] if raw else known
    return [k for k in wanted if k in known]


def resolve_local_dir(path: str, variant: str, label: str, path_env: str) -> Path:
    """The folder load_falcondec should read, or a FileNotFoundError that says how to fix it."""
    root = Path(path)
    if not root.is_dir():
        raise FileNotFoundError(f"{label} folder not found at {path}. It was in the models folder at start-up; "
                                "put it back or restart DecisionLab.")
    folder = root / "compact-int8" if variant == "int8" else root
    if not (folder / CONFIG_FILE).is_file():
        raise FileNotFoundError(f"No {CONFIG_FILE} in {folder}. This model has no int8 copy; set LIGHTDEC_VARIANT to fp16.")
    return folder


# ----------------------------------------------------------------------------- modeling-code trust (DL-SA-002)
# falcondec_modeling.py is executed as Python when a model loads. Only files whose sha256 (line endings
# normalised to \n) is known are run. KNOWN: the FalconDec modeling file shipped with LightDec v1.0.2 and
# LightDec_V2_Long v1.0.0 (identical). More hashes can be trusted via TRUSTED_MODELING_SHA256 (comma-separated).
MODELING_FILE = "falcondec_modeling.py"
KNOWN_MODELING_SHA256 = frozenset({"cc211c2d50a1e6946ed01860abb77673bb15f33d1022cd0d9c8739e166ec6b93"})


def modeling_sha256(path: Path) -> str:
    return hashlib.sha256(Path(path).read_bytes().replace(b"\r\n", b"\n")).hexdigest()


def trusted_modeling(folder: Path, env: Mapping[str, str]) -> Path:
    """The modeling file to execute from folder, or PermissionError if its code is not on the allowlist."""
    f = Path(folder) / MODELING_FILE
    if not f.is_file():
        raise FileNotFoundError(f"No {MODELING_FILE} in {folder}.")
    extra = {h.strip().lower() for h in (env.get("TRUSTED_MODELING_SHA256") or "").split(",") if h.strip()}
    digest = modeling_sha256(f)
    if digest not in KNOWN_MODELING_SHA256 | extra:
        raise PermissionError(f"Refusing to run {f}: its sha256 {digest} is not a known FalconDec modeling file. "
                              "If you trust it, add the hash to TRUSTED_MODELING_SHA256 in .env.")
    return f


def warmup_enabled(env: Mapping[str, str]) -> bool:
    """Run each model once after loading? Off with DLAB_WARMUP=0 (the ZeroGPU Space: no real GPU outside
    @spaces.GPU, so a warm-up there would fail)."""
    return env.get("DLAB_WARMUP", "1") != "0"