codeccat 0.1.0__cp310-abi3-win_arm64.whl
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.
- codeccat/__init__.py +45 -0
- codeccat/__main__.py +4 -0
- codeccat/_codeccat_native.pyd +0 -0
- codeccat/api.py +31 -0
- codeccat/cli.py +59 -0
- codeccat/features.py +146 -0
- codeccat/legacy_portable.py +78 -0
- codeccat/model.py +68 -0
- codeccat/model_io.py +123 -0
- codeccat/models/default.ccm +0 -0
- codeccat/models/default.ccm.meta.json +1 -0
- codeccat/models/language-l3.json +1 -0
- codeccat/runtime.py +440 -0
- codeccat/usable.py +139 -0
- codeccat/vnext_bom.py +30 -0
- codeccat/vnext_decoding.py +143 -0
- codeccat/vnext_escape.py +31 -0
- codeccat/vnext_gate.py +49 -0
- codeccat/vnext_l3_language.py +158 -0
- codeccat/vnext_strong_l2.py +202 -0
- codeccat/vnext_unicode_structure.py +25 -0
- codeccat-0.1.0.dist-info/METADATA +100 -0
- codeccat-0.1.0.dist-info/RECORD +27 -0
- codeccat-0.1.0.dist-info/WHEEL +4 -0
- codeccat-0.1.0.dist-info/entry_points.txt +2 -0
- codeccat-0.1.0.dist-info/licenses/LICENSE +21 -0
- codeccat-0.1.0.dist-info/sboms/codeccat-native-prototype.cyclonedx.json +1097 -0
codeccat/__init__.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""CodecCat public API with lazily loaded opt-in legacy R21 compatibility.
|
|
2
|
+
|
|
3
|
+
Default import/detect/decode never imports NumPy. Legacy symbols preserve the
|
|
4
|
+
previous API when the user explicitly installs codeccat[legacy].
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from .usable import CodecCat, detect, decode
|
|
9
|
+
from .vnext_decoding import Detection
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"CodecCat", "detect", "decode", "Detection",
|
|
13
|
+
"DetectionResult", "Detector", "LinearModel", "RuntimeBundle",
|
|
14
|
+
"RuntimeStack", "load_bundle", "save_bundle",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
_LEGACY = {
|
|
18
|
+
"DetectionResult": "api",
|
|
19
|
+
"Detector": "api",
|
|
20
|
+
"LinearModel": "model",
|
|
21
|
+
"RuntimeBundle": "model",
|
|
22
|
+
"RuntimeStack": "model",
|
|
23
|
+
"load_bundle": "model_io",
|
|
24
|
+
"save_bundle": "model_io",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def __getattr__(name: str):
|
|
29
|
+
mod = _LEGACY.get(name)
|
|
30
|
+
if mod is None:
|
|
31
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
32
|
+
try:
|
|
33
|
+
from importlib import import_module
|
|
34
|
+
module = import_module(f".{mod}", __name__)
|
|
35
|
+
except ModuleNotFoundError as exc:
|
|
36
|
+
if exc.name == "numpy":
|
|
37
|
+
raise ImportError(
|
|
38
|
+
f"{name} is a legacy CodecCat API; "
|
|
39
|
+
"install optional compatibility support with "
|
|
40
|
+
"'pip install codeccat[legacy]'"
|
|
41
|
+
) from exc
|
|
42
|
+
raise
|
|
43
|
+
result = getattr(module, name)
|
|
44
|
+
globals()[name] = result
|
|
45
|
+
return result
|
codeccat/__main__.py
ADDED
|
Binary file
|
codeccat/api.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from .model import RuntimeBundle
|
|
6
|
+
from .runtime import Runtime
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class DetectionResult:
|
|
11
|
+
encoding: str | None
|
|
12
|
+
alternatives: tuple[str, ...]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Detector:
|
|
16
|
+
"""CodecCat runtime bound to one immutable fitted model bundle."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, bundle: RuntimeBundle):
|
|
19
|
+
self._runtime = Runtime(bundle)
|
|
20
|
+
|
|
21
|
+
def rank(self, data: bytes) -> tuple[str, ...]:
|
|
22
|
+
if not isinstance(data, (bytes, bytearray, memoryview)):
|
|
23
|
+
raise TypeError("CodecCat expects a bytes-like input")
|
|
24
|
+
return self._runtime.rank(bytes(data))
|
|
25
|
+
|
|
26
|
+
def detect(self, data: bytes) -> DetectionResult:
|
|
27
|
+
ranked = self.rank(data)
|
|
28
|
+
return DetectionResult(
|
|
29
|
+
encoding=ranked[0] if ranked else None,
|
|
30
|
+
alternatives=ranked,
|
|
31
|
+
)
|
codeccat/cli.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""CodecCat CLI: detect or strictly decode a local byte stream."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
MAX_BYTES = 16 * 1024 * 1024
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main(argv: list[str] | None = None) -> int:
|
|
12
|
+
p = argparse.ArgumentParser(description="Detect text encoding with CodecCat's bundled native model")
|
|
13
|
+
p.add_argument("file", nargs="?", default="-", help="input file or - for stdin")
|
|
14
|
+
p.add_argument("--decode", action="store_true", help="write strictly decoded text to stdout")
|
|
15
|
+
p.add_argument("--json", action="store_true", help="write structured detection metadata")
|
|
16
|
+
p.add_argument("--bom-policy", choices=("strip", "preserve"), default="strip")
|
|
17
|
+
args = p.parse_args(argv)
|
|
18
|
+
if args.json and args.decode:
|
|
19
|
+
p.error("--json and --decode are mutually exclusive")
|
|
20
|
+
try:
|
|
21
|
+
if args.file == "-":
|
|
22
|
+
raw = sys.stdin.buffer.read(MAX_BYTES + 1)
|
|
23
|
+
else:
|
|
24
|
+
with Path(args.file).open("rb") as stream:
|
|
25
|
+
raw = stream.read(MAX_BYTES + 1)
|
|
26
|
+
if len(raw) > MAX_BYTES:
|
|
27
|
+
p.error("input exceeds 16 MiB limit; never silently truncate a stream")
|
|
28
|
+
from .usable import detect
|
|
29
|
+
result = detect(raw, bom_policy=args.bom_policy)
|
|
30
|
+
except (OSError, RuntimeError, ValueError) as exc:
|
|
31
|
+
p.exit(2, f"codeccat: {exc}\n")
|
|
32
|
+
if args.decode:
|
|
33
|
+
if result.text is None:
|
|
34
|
+
print("codeccat: unable to strictly decode input", file=sys.stderr)
|
|
35
|
+
return 2
|
|
36
|
+
# Decoded data is emitted as UTF-8 bytes on all platforms.
|
|
37
|
+
# Windows terminal defaults (e.g., cp1252) must not corrupt text.
|
|
38
|
+
sys.stdout.buffer.write(result.text.encode("utf-8", "strict"))
|
|
39
|
+
elif args.json:
|
|
40
|
+
print(json.dumps({
|
|
41
|
+
"encoding": result.encoding,
|
|
42
|
+
"accepted": result.accepted,
|
|
43
|
+
"ambiguous": result.ambiguous,
|
|
44
|
+
"candidates": result.candidates,
|
|
45
|
+
"bom_present": result.bom_present,
|
|
46
|
+
"reason": result.reason,
|
|
47
|
+
}, ensure_ascii=False))
|
|
48
|
+
else:
|
|
49
|
+
if not result.accepted:
|
|
50
|
+
print("unknown")
|
|
51
|
+
return 2
|
|
52
|
+
print(result.encoding)
|
|
53
|
+
if result.ambiguous:
|
|
54
|
+
print("codeccat: encoding label is ambiguous", file=sys.stderr)
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
raise SystemExit(main())
|
codeccat/features.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
S3_THRESHOLD = 0.02
|
|
8
|
+
BASE_WIDTH = 518
|
|
9
|
+
EXTRA_WIDTH = 368
|
|
10
|
+
FULL_WIDTH = BASE_WIDTH + EXTRA_WIDTH
|
|
11
|
+
|
|
12
|
+
_REDUCE_IDX = np.asarray([32, 127, 128], dtype=np.intp)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def hmt768(data: bytes) -> bytes:
|
|
16
|
+
if len(data) <= 768:
|
|
17
|
+
return data
|
|
18
|
+
middle = len(data) // 2
|
|
19
|
+
return data[:256] + data[middle - 128 : middle + 128] + data[-256:]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def hmt768_array(data: bytes) -> np.ndarray:
|
|
23
|
+
return np.frombuffer(hmt768(data), dtype=np.uint8)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def strict_utf8(data: bytes) -> bool:
|
|
27
|
+
try:
|
|
28
|
+
data[:4096].decode("utf-8", "strict")
|
|
29
|
+
return True
|
|
30
|
+
except UnicodeDecodeError:
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def has_utf8_bom(data: bytes) -> bool:
|
|
35
|
+
return data.startswith(b"\xef\xbb\xbf")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def route(data: bytes) -> str:
|
|
39
|
+
sample = data[:4096]
|
|
40
|
+
if strict_utf8(sample):
|
|
41
|
+
return "U"
|
|
42
|
+
if b"\x00" in sample:
|
|
43
|
+
return "N"
|
|
44
|
+
array = np.frombuffer(sample, dtype=np.uint8)
|
|
45
|
+
high_ratio = float(np.mean(array >= 128)) if len(array) else 0.0
|
|
46
|
+
return "RL" if high_ratio <= S3_THRESHOLD else "RH"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def replacement_rate(data: bytes) -> float:
|
|
50
|
+
if not data:
|
|
51
|
+
return 0.0
|
|
52
|
+
text = data.decode("utf-8", errors="replace")
|
|
53
|
+
return text.count("\ufffd") / max(1, len(text))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def byte_signals(data: bytes) -> tuple[bool, bool, bool, float, float, int]:
|
|
57
|
+
sample = data[:4096]
|
|
58
|
+
array = np.frombuffer(sample, dtype=np.uint8)
|
|
59
|
+
strict = strict_utf8(data)
|
|
60
|
+
bom = has_utf8_bom(data)
|
|
61
|
+
if len(array):
|
|
62
|
+
high_count = int(np.count_nonzero(array >= 128))
|
|
63
|
+
nul_count = int(np.count_nonzero(array == 0))
|
|
64
|
+
high = high_count / len(array)
|
|
65
|
+
nul = nul_count / len(array)
|
|
66
|
+
ascii_only = high_count == 0
|
|
67
|
+
else:
|
|
68
|
+
high = 0.0
|
|
69
|
+
nul = 0.0
|
|
70
|
+
ascii_only = True
|
|
71
|
+
return bom, strict, ascii_only, high, nul, len(array)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def baseline_from_array(array: np.ndarray) -> np.ndarray:
|
|
75
|
+
n = max(1, len(array))
|
|
76
|
+
counts = np.bincount(array, minlength=256)
|
|
77
|
+
out = np.empty(BASE_WIDTH, dtype=np.float32)
|
|
78
|
+
out[:256] = counts
|
|
79
|
+
out[:256] *= 1.0 / n
|
|
80
|
+
|
|
81
|
+
if len(array) >= 2:
|
|
82
|
+
bins = array[:-1] + array[1:]
|
|
83
|
+
bigram_counts = np.bincount(bins, minlength=256)
|
|
84
|
+
out[256:512] = bigram_counts
|
|
85
|
+
out[256:512] *= 1.0 / (len(array) - 1)
|
|
86
|
+
else:
|
|
87
|
+
out[256:512] = 0.0
|
|
88
|
+
|
|
89
|
+
if len(array):
|
|
90
|
+
inv = 1.0 / len(array)
|
|
91
|
+
aggregate = np.add.reduceat(counts, _REDUCE_IDX)
|
|
92
|
+
printable = float(aggregate[0] * inv)
|
|
93
|
+
high = float(aggregate[2] * inv)
|
|
94
|
+
nul = float(counts[0] * inv)
|
|
95
|
+
lf = float(counts[10] * inv)
|
|
96
|
+
cr = float(counts[13] * inv)
|
|
97
|
+
else:
|
|
98
|
+
high = nul = printable = lf = cr = 0.0
|
|
99
|
+
|
|
100
|
+
out[512:] = (
|
|
101
|
+
math.log2(n + 1) / 16.0,
|
|
102
|
+
high,
|
|
103
|
+
nul,
|
|
104
|
+
printable,
|
|
105
|
+
lf,
|
|
106
|
+
cr,
|
|
107
|
+
)
|
|
108
|
+
return out
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def full_b_extra_from_array(array: np.ndarray, route_name: str) -> np.ndarray:
|
|
112
|
+
out = np.zeros(EXTRA_WIDTH, dtype=np.float32)
|
|
113
|
+
if route_name not in ("U", "RH"):
|
|
114
|
+
return out
|
|
115
|
+
|
|
116
|
+
if len(array) >= 2:
|
|
117
|
+
left = array[:-1]
|
|
118
|
+
right = array[1:]
|
|
119
|
+
x = left.astype(np.int16, copy=False)
|
|
120
|
+
y = right.astype(np.int16, copy=False)
|
|
121
|
+
diff = ((y - x) & 255).astype(np.intp, copy=False)
|
|
122
|
+
out[:256] = np.bincount(diff, minlength=256).astype(np.float32) / (len(array) - 1)
|
|
123
|
+
|
|
124
|
+
xb = (np.bitwise_xor(left, right) >> 2).astype(np.intp, copy=False)
|
|
125
|
+
out[256:320] = np.bincount(xb, minlength=64).astype(np.float32) / (len(array) - 1)
|
|
126
|
+
|
|
127
|
+
p = 320
|
|
128
|
+
for chunk in (array[:256], array[256:512], array[512:768]):
|
|
129
|
+
high = chunk[chunk >= 128]
|
|
130
|
+
if len(high):
|
|
131
|
+
idx = ((high.astype(np.uint16) - 128) >> 3).astype(np.intp, copy=False)
|
|
132
|
+
out[p : p + 16] = np.bincount(idx, minlength=16).astype(np.float32) / len(high)
|
|
133
|
+
p += 16
|
|
134
|
+
return out
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def feature_vector(data: bytes) -> np.ndarray:
|
|
138
|
+
return baseline_from_array(hmt768_array(data))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def full_b_vector(data: bytes, route_name: str | None = None) -> np.ndarray:
|
|
142
|
+
route_name = route_name or route(data)
|
|
143
|
+
array = hmt768_array(data)
|
|
144
|
+
baseline = baseline_from_array(array)
|
|
145
|
+
extra = full_b_extra_from_array(array, route_name)
|
|
146
|
+
return np.concatenate([baseline, extra])
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Optional legacy stable23/ordered13 numeric ZIP model adapter.
|
|
2
|
+
|
|
3
|
+
The original format stores JSON metadata plus compressed NumPy arrays, never
|
|
4
|
+
pickle. Loading it requires only the opt-in NumPy dependency. Inference still
|
|
5
|
+
uses the Rust-native L2 scorer; sklearn is not imported.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import io
|
|
10
|
+
import json
|
|
11
|
+
import zipfile
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
SCHEMA_23 = "vnext-l2-tfidf-ordered23-sgd-huber-v1"
|
|
15
|
+
SCHEMA_13 = "vnext-l2-tfidf-ordered13-sgd-huber-v2"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_legacy_zip(path: str | Path):
|
|
19
|
+
try:
|
|
20
|
+
import numpy as np
|
|
21
|
+
except ImportError as exc:
|
|
22
|
+
raise ImportError(
|
|
23
|
+
"Legacy CodecCat .zip models require optional NumPy support; "
|
|
24
|
+
"install 'codeccat[legacy]' or export your model to .ccm."
|
|
25
|
+
) from exc
|
|
26
|
+
from ._codeccat_native import NativeL2
|
|
27
|
+
from .vnext_strong_l2 import TrainableDetector
|
|
28
|
+
|
|
29
|
+
with zipfile.ZipFile(path, "r") as archive:
|
|
30
|
+
names = archive.namelist()
|
|
31
|
+
if sorted(names) != ["manifest.json", "weights.npz"]:
|
|
32
|
+
raise ValueError("unexpected or duplicated model archive entries")
|
|
33
|
+
manifest_info = archive.getinfo("manifest.json")
|
|
34
|
+
tensor_info = archive.getinfo("weights.npz")
|
|
35
|
+
if (manifest_info.file_size > 8_000_000 or
|
|
36
|
+
tensor_info.file_size > 200_000_000):
|
|
37
|
+
raise ValueError("legacy archive sections exceed maximum size")
|
|
38
|
+
manifest = json.loads(archive.read("manifest.json"))
|
|
39
|
+
schema = manifest.get("schema")
|
|
40
|
+
expected_range = {
|
|
41
|
+
SCHEMA_23: [2, 3],
|
|
42
|
+
SCHEMA_13: [1, 3],
|
|
43
|
+
}.get(schema)
|
|
44
|
+
if expected_range is None:
|
|
45
|
+
raise ValueError("unsupported legacy model schema")
|
|
46
|
+
if manifest.get("params") != {
|
|
47
|
+
"ngram_range": expected_range,
|
|
48
|
+
"sublinear_tf": True,
|
|
49
|
+
"lowercase": True,
|
|
50
|
+
"norm": "l2",
|
|
51
|
+
}:
|
|
52
|
+
raise ValueError("unsupported legacy feature transform")
|
|
53
|
+
|
|
54
|
+
with np.load(io.BytesIO(archive.read("weights.npz")),
|
|
55
|
+
allow_pickle=False) as tensors:
|
|
56
|
+
if set(tensors.files) != {"idf", "coef", "intercept"}:
|
|
57
|
+
raise ValueError("unexpected numeric tensor keys")
|
|
58
|
+
idf = tensors["idf"].copy()
|
|
59
|
+
coef = tensors["coef"].copy()
|
|
60
|
+
intercept = tensors["intercept"].copy()
|
|
61
|
+
|
|
62
|
+
vocab = manifest.get("vocabulary")
|
|
63
|
+
classes = manifest.get("classes")
|
|
64
|
+
fingerprint = manifest.get("fingerprint")
|
|
65
|
+
if (not isinstance(vocab, dict) or not vocab or
|
|
66
|
+
not isinstance(classes, list) or len(classes) < 2 or
|
|
67
|
+
not all(isinstance(c, str) for c in classes) or
|
|
68
|
+
not isinstance(fingerprint, str) or len(fingerprint) != 64 or
|
|
69
|
+
sorted(vocab.values()) != list(range(len(vocab))) or
|
|
70
|
+
idf.shape != (len(vocab),) or
|
|
71
|
+
coef.shape != (1 if len(classes) == 2 else len(classes), len(vocab)) or
|
|
72
|
+
intercept.shape != (coef.shape[0],) or
|
|
73
|
+
not np.isfinite(idf).all() or not np.isfinite(coef).all() or
|
|
74
|
+
not np.isfinite(intercept).all()):
|
|
75
|
+
raise ValueError("invalid legacy model metadata or weights")
|
|
76
|
+
native = NativeL2(vocab, idf.tolist(), coef.tolist(), intercept.tolist(),
|
|
77
|
+
expected_range[0], expected_range[1], False, True)
|
|
78
|
+
return TrainableDetector(native, fingerprint, classes=classes)
|
codeccat/model.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Mapping
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class LinearModel:
|
|
11
|
+
classes: tuple[str, ...]
|
|
12
|
+
weight: np.ndarray
|
|
13
|
+
bias: np.ndarray
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class RuntimeStack:
|
|
18
|
+
route_models: Mapping[str, LinearModel]
|
|
19
|
+
calibrator: LinearModel | None
|
|
20
|
+
triad: LinearModel | None
|
|
21
|
+
utf8_sig: LinearModel | None
|
|
22
|
+
utf8_gb: LinearModel | None
|
|
23
|
+
|
|
24
|
+
def __post_init__(self) -> None:
|
|
25
|
+
missing = {"U", "N", "RL", "RH"} - set(self.route_models)
|
|
26
|
+
if missing:
|
|
27
|
+
raise ValueError(f"missing route models: {sorted(missing)}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class RuntimeBundle:
|
|
32
|
+
baseline: RuntimeStack
|
|
33
|
+
full_b: RuntimeStack | None
|
|
34
|
+
rl_specialist: LinearModel | None
|
|
35
|
+
gate_thresholds: Mapping[str, float]
|
|
36
|
+
classes: tuple[str, ...]
|
|
37
|
+
families: tuple[str, ...]
|
|
38
|
+
r12_threshold: float = 0.65
|
|
39
|
+
|
|
40
|
+
def __post_init__(self) -> None:
|
|
41
|
+
if self.full_b is not None:
|
|
42
|
+
for route in ("U", "RH"):
|
|
43
|
+
if route not in self.full_b.route_models:
|
|
44
|
+
raise ValueError(f"full-B stack missing route: {route}")
|
|
45
|
+
for route in self.gate_thresholds:
|
|
46
|
+
if route not in ("U", "RH"):
|
|
47
|
+
raise ValueError(f"unsupported gate route: {route}")
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def route_models(self) -> Mapping[str, LinearModel]:
|
|
51
|
+
"""Compatibility alias for the baseline route models."""
|
|
52
|
+
return self.baseline.route_models
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def calibrator(self) -> LinearModel | None:
|
|
56
|
+
return self.baseline.calibrator
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def triad(self) -> LinearModel | None:
|
|
60
|
+
return self.baseline.triad
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def utf8_sig(self) -> LinearModel | None:
|
|
64
|
+
return self.baseline.utf8_sig
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def utf8_gb(self) -> LinearModel | None:
|
|
68
|
+
return self.baseline.utf8_gb
|
codeccat/model_io.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from .model import LinearModel, RuntimeBundle, RuntimeStack
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _put_model(arrays: dict[str, np.ndarray], meta: dict, prefix: str, model: LinearModel | None) -> None:
|
|
12
|
+
if model is None:
|
|
13
|
+
meta[prefix] = None
|
|
14
|
+
return
|
|
15
|
+
meta[prefix] = {"classes": list(model.classes)}
|
|
16
|
+
arrays[f"{prefix}.weight"] = np.asarray(model.weight, dtype=np.float64)
|
|
17
|
+
arrays[f"{prefix}.bias"] = np.asarray(model.bias, dtype=np.float64)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get_model(data, meta: dict, prefix: str) -> LinearModel | None:
|
|
21
|
+
info = meta[prefix]
|
|
22
|
+
if info is None:
|
|
23
|
+
return None
|
|
24
|
+
return LinearModel(
|
|
25
|
+
classes=tuple(info["classes"]),
|
|
26
|
+
weight=np.asarray(data[f"{prefix}.weight"], dtype=np.float64),
|
|
27
|
+
bias=np.asarray(data[f"{prefix}.bias"], dtype=np.float64),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _put_stack(arrays: dict[str, np.ndarray], meta: dict, prefix: str, stack: RuntimeStack | None) -> None:
|
|
32
|
+
if stack is None:
|
|
33
|
+
meta[prefix] = None
|
|
34
|
+
return
|
|
35
|
+
meta[prefix] = {"routes": list(stack.route_models)}
|
|
36
|
+
for route, model in stack.route_models.items():
|
|
37
|
+
_put_model(arrays, meta, f"{prefix}.route.{route}", model)
|
|
38
|
+
_put_model(arrays, meta, f"{prefix}.calibrator", stack.calibrator)
|
|
39
|
+
_put_model(arrays, meta, f"{prefix}.triad", stack.triad)
|
|
40
|
+
_put_model(arrays, meta, f"{prefix}.utf8_sig", stack.utf8_sig)
|
|
41
|
+
_put_model(arrays, meta, f"{prefix}.utf8_gb", stack.utf8_gb)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _get_stack(data, meta: dict, prefix: str) -> RuntimeStack | None:
|
|
45
|
+
info = meta[prefix]
|
|
46
|
+
if info is None:
|
|
47
|
+
return None
|
|
48
|
+
route_models = {
|
|
49
|
+
route: _get_model(data, meta, f"{prefix}.route.{route}")
|
|
50
|
+
for route in info["routes"]
|
|
51
|
+
}
|
|
52
|
+
if any(model is None for model in route_models.values()):
|
|
53
|
+
raise ValueError(f"{prefix} route model cannot be null")
|
|
54
|
+
return RuntimeStack(
|
|
55
|
+
route_models=route_models, # type: ignore[arg-type]
|
|
56
|
+
calibrator=_get_model(data, meta, f"{prefix}.calibrator"),
|
|
57
|
+
triad=_get_model(data, meta, f"{prefix}.triad"),
|
|
58
|
+
utf8_sig=_get_model(data, meta, f"{prefix}.utf8_sig"),
|
|
59
|
+
utf8_gb=_get_model(data, meta, f"{prefix}.utf8_gb"),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def save_bundle(path: str | Path, bundle: RuntimeBundle) -> None:
|
|
64
|
+
arrays: dict[str, np.ndarray] = {}
|
|
65
|
+
meta = {
|
|
66
|
+
"schema": 2,
|
|
67
|
+
"classes": list(bundle.classes),
|
|
68
|
+
"families": list(bundle.families),
|
|
69
|
+
"gate_thresholds": {k: float(v) for k, v in bundle.gate_thresholds.items()},
|
|
70
|
+
"r12_threshold": float(bundle.r12_threshold),
|
|
71
|
+
}
|
|
72
|
+
_put_stack(arrays, meta, "baseline", bundle.baseline)
|
|
73
|
+
_put_stack(arrays, meta, "full_b", bundle.full_b)
|
|
74
|
+
_put_model(arrays, meta, "rl_specialist", bundle.rl_specialist)
|
|
75
|
+
arrays["__manifest__"] = np.asarray(json.dumps(meta, sort_keys=True))
|
|
76
|
+
np.savez_compressed(Path(path), **arrays)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _load_schema1(data, meta: dict) -> RuntimeBundle:
|
|
80
|
+
route_models = {
|
|
81
|
+
route: _get_model(data, meta, f"route.{route}")
|
|
82
|
+
for route in meta["routes"]
|
|
83
|
+
}
|
|
84
|
+
if any(model is None for model in route_models.values()):
|
|
85
|
+
raise ValueError("route model cannot be null")
|
|
86
|
+
baseline = RuntimeStack(
|
|
87
|
+
route_models=route_models, # type: ignore[arg-type]
|
|
88
|
+
calibrator=_get_model(data, meta, "calibrator"),
|
|
89
|
+
triad=_get_model(data, meta, "triad"),
|
|
90
|
+
utf8_sig=_get_model(data, meta, "utf8_sig"),
|
|
91
|
+
utf8_gb=_get_model(data, meta, "utf8_gb"),
|
|
92
|
+
)
|
|
93
|
+
return RuntimeBundle(
|
|
94
|
+
baseline=baseline,
|
|
95
|
+
full_b=None,
|
|
96
|
+
rl_specialist=None,
|
|
97
|
+
gate_thresholds={},
|
|
98
|
+
classes=tuple(meta["classes"]),
|
|
99
|
+
families=tuple(meta["families"]),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def load_bundle(path: str | Path) -> RuntimeBundle:
|
|
104
|
+
with np.load(Path(path), allow_pickle=False) as data:
|
|
105
|
+
meta = json.loads(str(data["__manifest__"]))
|
|
106
|
+
schema = meta.get("schema")
|
|
107
|
+
if schema == 1:
|
|
108
|
+
return _load_schema1(data, meta)
|
|
109
|
+
if schema != 2:
|
|
110
|
+
raise ValueError(f"unsupported CodecCat model schema: {schema}")
|
|
111
|
+
|
|
112
|
+
baseline = _get_stack(data, meta, "baseline")
|
|
113
|
+
if baseline is None:
|
|
114
|
+
raise ValueError("baseline stack cannot be null")
|
|
115
|
+
return RuntimeBundle(
|
|
116
|
+
baseline=baseline,
|
|
117
|
+
full_b=_get_stack(data, meta, "full_b"),
|
|
118
|
+
rl_specialist=_get_model(data, meta, "rl_specialist"),
|
|
119
|
+
gate_thresholds={k: float(v) for k, v in meta.get("gate_thresholds", {}).items()},
|
|
120
|
+
classes=tuple(meta["classes"]),
|
|
121
|
+
families=tuple(meta["families"]),
|
|
122
|
+
r12_threshold=float(meta.get("r12_threshold", 0.65)),
|
|
123
|
+
)
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"model_sha256": "16073306b5027bf93acc2883f57fbda0cb0cf2d77b9a7bb62cf1b07fcbb9748b", "schema": "codeccat-native-model-provenance-v1", "training_fingerprint": "936eb0331a0d0ac447b0f6c6135bec7cd397962544d053cda3220358dacba85c"}
|