codeccat-train 0.1.0__py3-none-any.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_train/__init__.py +1 -0
- codeccat_train/__main__.py +242 -0
- codeccat_train/evaluate.py +123 -0
- codeccat_train/l3.py +80 -0
- codeccat_train/residual.py +72 -0
- codeccat_train-0.1.0.dist-info/METADATA +128 -0
- codeccat_train-0.1.0.dist-info/RECORD +10 -0
- codeccat_train-0.1.0.dist-info/WHEEL +4 -0
- codeccat_train-0.1.0.dist-info/entry_points.txt +2 -0
- codeccat_train-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Offline training and native-model export; never imported by CodecCat inference."""
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Offline CodecCat trainer: safe dataset reading, group isolation, CCM1 export.
|
|
2
|
+
|
|
3
|
+
No dependency on codeccat's public runtime or internal legacy R21 training.
|
|
4
|
+
Users can install codeccat-train only in their training environment.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
import argparse,codecs,hashlib,json,struct,sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import numpy as np
|
|
10
|
+
from sklearn.feature_extraction.text import TfidfVectorizer
|
|
11
|
+
from sklearn.linear_model import SGDClassifier
|
|
12
|
+
from sklearn.metrics import accuracy_score
|
|
13
|
+
|
|
14
|
+
def read_manifest(manifest:Path,require_validation:bool=True):
|
|
15
|
+
root=manifest.resolve().parent
|
|
16
|
+
result=[];digests={};groups={}
|
|
17
|
+
for number,line in enumerate(manifest.read_text(encoding="utf-8").splitlines(),1):
|
|
18
|
+
if not line.strip():continue
|
|
19
|
+
try:record=json.loads(line)
|
|
20
|
+
except json.JSONDecodeError as ex:raise ValueError(f"line {number}: invalid JSON") from ex
|
|
21
|
+
for key in ("path","encoding","group","split"):
|
|
22
|
+
if not isinstance(record.get(key),str) or not record[key]:
|
|
23
|
+
raise ValueError(f"line {number}: missing {key}")
|
|
24
|
+
if record["split"] not in ("train","validation"):
|
|
25
|
+
raise ValueError(f"line {number}: split must be train or validation")
|
|
26
|
+
rel=Path(record["path"])
|
|
27
|
+
resolved=(root/rel).resolve()
|
|
28
|
+
if rel.is_absolute() or not resolved.is_relative_to(root):
|
|
29
|
+
raise ValueError(f"line {number}: path escapes manifest directory")
|
|
30
|
+
raw=resolved.read_bytes()
|
|
31
|
+
if not raw:raise ValueError(f"line {number}: empty input")
|
|
32
|
+
# Optional per-file attestations protect reproducible training against
|
|
33
|
+
# silent input changes after a manifest was generated.
|
|
34
|
+
for key,payload in (("sha256",raw),("prefix_sha256",raw[:4096])):
|
|
35
|
+
expected=record.get(key)
|
|
36
|
+
if expected is not None:
|
|
37
|
+
actual=hashlib.sha256(payload).hexdigest()
|
|
38
|
+
if not isinstance(expected,str) or expected!=actual:
|
|
39
|
+
raise ValueError(f"line {number}: {key} mismatch")
|
|
40
|
+
try:codecs.lookup(record["encoding"])
|
|
41
|
+
except LookupError as ex:raise ValueError(f"line {number}: unknown codec") from ex
|
|
42
|
+
try:raw.decode(record["encoding"],"strict")
|
|
43
|
+
except UnicodeError as ex:raise ValueError(f"line {number}: label does not decode bytes") from ex
|
|
44
|
+
sha=hashlib.sha256(raw).hexdigest()
|
|
45
|
+
grp=record["group"];split=record["split"]
|
|
46
|
+
# Register and validate groups before deduplicating identical content.
|
|
47
|
+
# A duplicate can introduce a group that later appears in another split.
|
|
48
|
+
if grp in groups and groups[grp]!=split:
|
|
49
|
+
raise ValueError(f"line {number}: source-group leakage")
|
|
50
|
+
groups[grp]=split
|
|
51
|
+
if sha in digests:
|
|
52
|
+
old=digests[sha]
|
|
53
|
+
if old!=(record["encoding"],split):
|
|
54
|
+
raise ValueError(f"line {number}: duplicate bytes cross labels/splits")
|
|
55
|
+
continue
|
|
56
|
+
digests[sha]=(record["encoding"],split)
|
|
57
|
+
result.append((raw,record["encoding"],grp,split,sha))
|
|
58
|
+
train=[r for r in result if r[3]=="train"]
|
|
59
|
+
val=[r for r in result if r[3]=="validation"]
|
|
60
|
+
if not train or (require_validation and not val):
|
|
61
|
+
raise ValueError("both train and validation records required")
|
|
62
|
+
if len({r[1] for r in train})<2:raise ValueError("need >=2 training codec labels")
|
|
63
|
+
if any(r[1] not in {t[1] for t in train} for r in val):
|
|
64
|
+
raise ValueError("validation codec label absent from training")
|
|
65
|
+
return train,val
|
|
66
|
+
|
|
67
|
+
def fingerprint_dataset(rows):
|
|
68
|
+
"""Fingerprint accepted labeled bytes and assignments without storing texts."""
|
|
69
|
+
source_rows = [
|
|
70
|
+
{"sha256": row[4], "encoding": row[1], "group": row[2], "split": row[3]}
|
|
71
|
+
for row in rows
|
|
72
|
+
]
|
|
73
|
+
return hashlib.sha256(json.dumps(
|
|
74
|
+
source_rows, sort_keys=True, separators=(",", ":"), ensure_ascii=False
|
|
75
|
+
).encode("utf-8")).hexdigest()
|
|
76
|
+
|
|
77
|
+
def historical_stable23_fingerprint(rows):
|
|
78
|
+
"""Match the original TrainableDetector fingerprint on 4KB corpus prefixes.
|
|
79
|
+
|
|
80
|
+
This fingerprint is an audit value, not a trust or license certificate.
|
|
81
|
+
"""
|
|
82
|
+
digest=hashlib.sha256()
|
|
83
|
+
for row in rows:
|
|
84
|
+
raw,encoding,group=row[:3]
|
|
85
|
+
prefix_hash=hashlib.sha256(raw[:4096]).hexdigest()
|
|
86
|
+
digest.update(prefix_hash.encode()+b"\0"+encoding.encode()+b"\0"+group.encode()+b"\0")
|
|
87
|
+
return digest.hexdigest()
|
|
88
|
+
|
|
89
|
+
def write_string(parts,s):
|
|
90
|
+
payload=s.encode("utf-8")
|
|
91
|
+
if not 0<len(payload)<=256:raise ValueError("invalid model string length")
|
|
92
|
+
parts.extend((struct.pack("<H",len(payload)),payload))
|
|
93
|
+
|
|
94
|
+
def export_ccm(path,vectorizer,classifier,precision="f64"):
|
|
95
|
+
if precision not in ("f64", "f32"):
|
|
96
|
+
raise ValueError("unsupported weight precision")
|
|
97
|
+
vocab=vectorizer.vocabulary_
|
|
98
|
+
width=len(vectorizer.idf_)
|
|
99
|
+
classes=[str(x) for x in classifier.classes_]
|
|
100
|
+
rows=1 if len(classes)==2 else len(classes)
|
|
101
|
+
if len(vocab)!=width or classifier.coef_.shape!=(rows,width):
|
|
102
|
+
raise ValueError("model tensor dimensions invalid")
|
|
103
|
+
if classifier.intercept_.shape!=(rows,):
|
|
104
|
+
raise ValueError("model bias dimensions invalid")
|
|
105
|
+
if width>200_000 or len(classes)>256:raise ValueError("model exceeds native limit")
|
|
106
|
+
low,high=vectorizer.ngram_range
|
|
107
|
+
flags=int(vectorizer.binary)|2*int(vectorizer.sublinear_tf)
|
|
108
|
+
schema=b"CCM2" if precision=="f32" else b"CCM1"
|
|
109
|
+
parts=[struct.pack("<4sIIBBBB",schema,width,len(classes),low,high,flags,0)]
|
|
110
|
+
for c in classes:write_string(parts,c)
|
|
111
|
+
for gram,index in sorted(vocab.items(),key=lambda item:item[1]):
|
|
112
|
+
parts.append(struct.pack("<I",index))
|
|
113
|
+
write_string(parts,gram)
|
|
114
|
+
for tensor in (vectorizer.idf_, classifier.intercept_):
|
|
115
|
+
parts.append(np.asarray(tensor,dtype="<f8").tobytes(order="C"))
|
|
116
|
+
weights_dtype="<f4" if precision=="f32" else "<f8"
|
|
117
|
+
parts.append(np.asarray(classifier.coef_,dtype=weights_dtype).tobytes(order="C"))
|
|
118
|
+
data=b"".join(parts)
|
|
119
|
+
data+=hashlib.sha256(data).digest()
|
|
120
|
+
path.parent.mkdir(parents=True,exist_ok=True)
|
|
121
|
+
path.write_bytes(data)
|
|
122
|
+
return hashlib.sha256(data).hexdigest(),len(data)
|
|
123
|
+
|
|
124
|
+
def fit_l2(training_rows,seed:int):
|
|
125
|
+
"""Stable23-equivalent L2 fitting on supplied rows; no official corpus implied."""
|
|
126
|
+
strings=[r[0][:4096].decode("latin1") for r in training_rows]
|
|
127
|
+
labels=[r[1] for r in training_rows]
|
|
128
|
+
vec=TfidfVectorizer(analyzer="char",ngram_range=(2,3),
|
|
129
|
+
min_df=2,max_features=45000,sublinear_tf=True)
|
|
130
|
+
x=vec.fit_transform(strings)
|
|
131
|
+
model=SGDClassifier(loss="modified_huber",alpha=.0001,
|
|
132
|
+
random_state=seed,max_iter=1500)
|
|
133
|
+
model.fit(x,labels)
|
|
134
|
+
return vec,model
|
|
135
|
+
|
|
136
|
+
def fit(manifest:Path,output:Path,report:Path,seed:int,precision:str="f64",residual_focus:bool=False,train_only:bool=False):
|
|
137
|
+
train,validation=read_manifest(manifest,require_validation=not train_only)
|
|
138
|
+
if train_only and validation:
|
|
139
|
+
raise ValueError("--train-only requires a manifest containing train rows only")
|
|
140
|
+
original_train_n=len(train)
|
|
141
|
+
source_fingerprint=fingerprint_dataset(train + validation)
|
|
142
|
+
removed={}
|
|
143
|
+
if residual_focus:
|
|
144
|
+
from .residual import residual_subset
|
|
145
|
+
train,removed=residual_subset(train)
|
|
146
|
+
effective_fingerprint=fingerprint_dataset(train)
|
|
147
|
+
vec,model=fit_l2(train,seed)
|
|
148
|
+
accuracy=None
|
|
149
|
+
if validation:
|
|
150
|
+
xval=vec.transform([r[0][:4096].decode("latin1") for r in validation])
|
|
151
|
+
pred=model.predict(xval)
|
|
152
|
+
accuracy=float(accuracy_score([r[1] for r in validation],pred))
|
|
153
|
+
digest,size=export_ccm(output,vec,model,precision=precision)
|
|
154
|
+
result={
|
|
155
|
+
"schema":"codeccat-offline-train-v1",
|
|
156
|
+
"feature_profile":"stable23-equivalent",
|
|
157
|
+
"model_schema":"CCM2" if precision=="f32" else "CCM1",
|
|
158
|
+
"weight_precision":precision,
|
|
159
|
+
"seed":seed,
|
|
160
|
+
"training_n":len(train),"validation_n":len(validation),
|
|
161
|
+
"training_original_n":original_train_n,
|
|
162
|
+
"residual_focus_applied":residual_focus,
|
|
163
|
+
"training_removed_by_label":removed,
|
|
164
|
+
"training_groups":len({r[2] for r in train}),
|
|
165
|
+
"validation_groups":len({r[2] for r in validation}),
|
|
166
|
+
"classes":list(model.classes_),
|
|
167
|
+
"validation_codec_label_accuracy":accuracy,
|
|
168
|
+
"train_only":train_only,
|
|
169
|
+
"note":"Validation checks exact encoding LABEL, not full public Runtime text-level detection and not independent external generalization.",
|
|
170
|
+
"manifest_sha256":hashlib.sha256(manifest.read_bytes()).hexdigest(),
|
|
171
|
+
"dataset_content_fingerprint_sha256":source_fingerprint,
|
|
172
|
+
"effective_training_fingerprint_sha256":effective_fingerprint,
|
|
173
|
+
"historical_stable23_fingerprint":historical_stable23_fingerprint(train),
|
|
174
|
+
"dataset_fingerprint_scope":"SHA256 of ordered accepted bytes SHA256, encoding, group and split; no raw text",
|
|
175
|
+
"model_sha256":digest,
|
|
176
|
+
"model_bytes":size,
|
|
177
|
+
"training_tool_version":"0.1.0",
|
|
178
|
+
"python_version":sys.version.split()[0],
|
|
179
|
+
"numpy_version":np.__version__,
|
|
180
|
+
"sklearn_version":__import__("sklearn").__version__,
|
|
181
|
+
"training_config":{"min_df":2,"max_features":45000,"ngram_range":[2,3],
|
|
182
|
+
"sublinear_tf":True,"loss":"modified_huber",
|
|
183
|
+
"alpha":0.0001,"max_iter":1500,
|
|
184
|
+
"byte_limit_per_document":4096}
|
|
185
|
+
}
|
|
186
|
+
report.parent.mkdir(parents=True,exist_ok=True)
|
|
187
|
+
report.write_text(json.dumps(result,indent=2,ensure_ascii=False)+"\n",encoding="utf-8")
|
|
188
|
+
return result
|
|
189
|
+
|
|
190
|
+
def main():
|
|
191
|
+
import sys
|
|
192
|
+
args=sys.argv[1:]
|
|
193
|
+
if args and args[0]=="fit-l3":
|
|
194
|
+
from .l3 import save_l3
|
|
195
|
+
p=argparse.ArgumentParser(description="Fit optional Unicode trigram L3 from the unfiltered training corpus")
|
|
196
|
+
p.add_argument("--manifest",type=Path,required=True)
|
|
197
|
+
p.add_argument("--output",type=Path,required=True)
|
|
198
|
+
p.add_argument("--report",type=Path,required=True)
|
|
199
|
+
p.add_argument("--max-features",type=int,default=30000)
|
|
200
|
+
a=p.parse_args(args[1:])
|
|
201
|
+
train,validation=read_manifest(a.manifest,require_validation=False)
|
|
202
|
+
if validation:
|
|
203
|
+
raise ValueError("fit-l3 requires a train-only manifest; no validation rows may be used")
|
|
204
|
+
result=save_l3(train,a.output,max_features=a.max_features)
|
|
205
|
+
result["input_manifest_sha256"]=hashlib.sha256(a.manifest.read_bytes()).hexdigest()
|
|
206
|
+
result["input_content_fingerprint_sha256"]=fingerprint_dataset(train)
|
|
207
|
+
result["input_n"]=len(train)
|
|
208
|
+
a.report.parent.mkdir(parents=True,exist_ok=True)
|
|
209
|
+
a.report.write_text(json.dumps(result,sort_keys=True,indent=2)+"\n",encoding="utf-8")
|
|
210
|
+
print("CODECCAT_L3_TRAIN_RESULT",json.dumps(result,sort_keys=True),flush=True)
|
|
211
|
+
return
|
|
212
|
+
if args and args[0]=="evaluate":
|
|
213
|
+
from .evaluate import evaluate
|
|
214
|
+
p=argparse.ArgumentParser(description="Compare a custom model against the bundled Stable model on labeled bytes")
|
|
215
|
+
p.add_argument("--manifest",type=Path,required=True)
|
|
216
|
+
p.add_argument("--model",type=Path,required=True)
|
|
217
|
+
p.add_argument("--report",type=Path,required=True)
|
|
218
|
+
p.add_argument("--baseline-model",type=Path,default=None)
|
|
219
|
+
p.add_argument("--split",choices=("test","validation"),default="test")
|
|
220
|
+
a=p.parse_args(args[1:])
|
|
221
|
+
print("CODECCAT_MODEL_COMPARISON",
|
|
222
|
+
json.dumps(evaluate(a.manifest,a.model,a.report,a.split,a.baseline_model),sort_keys=True),
|
|
223
|
+
flush=True)
|
|
224
|
+
return
|
|
225
|
+
# Preserve the original CLI without subcommands for existing users.
|
|
226
|
+
if args and args[0]=="fit":
|
|
227
|
+
args=args[1:]
|
|
228
|
+
p=argparse.ArgumentParser(description="Offline-only CodecCat training; exports numeric CCM1/CCM2 for Native Runtime")
|
|
229
|
+
p.add_argument("--manifest",type=Path,required=True)
|
|
230
|
+
p.add_argument("--output",type=Path,required=True)
|
|
231
|
+
p.add_argument("--report",type=Path,required=True)
|
|
232
|
+
p.add_argument("--seed",type=int,default=20261015)
|
|
233
|
+
p.add_argument("--train-only",action="store_true",
|
|
234
|
+
help="fit every labeled row; requires train-only manifest and reports no validation accuracy")
|
|
235
|
+
p.add_argument("--residual-focus",action="store_true",
|
|
236
|
+
help="replay historical Stable23 residual sample filtering (not official release approval)")
|
|
237
|
+
p.add_argument("--precision",choices=("f64","f32"),default="f64",
|
|
238
|
+
help="f64 exports CCM1 (default); f32 exports compact CCM2 weights")
|
|
239
|
+
a=p.parse_args(args)
|
|
240
|
+
print("CODECCAT_TRAIN_RESULT",json.dumps(fit(a.manifest,a.output,a.report,a.seed,precision=a.precision,residual_focus=a.residual_focus,train_only=a.train_only)),flush=True)
|
|
241
|
+
|
|
242
|
+
if __name__=="__main__":main()
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Compare a user-trained numeric model with the published CodecCat default.
|
|
2
|
+
|
|
3
|
+
Explicitly evaluates bytes using the public Runtime, never raw scikit-learn
|
|
4
|
+
labels. No source bytes or decoded text are copied into the report.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
import codecs
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def evaluate(manifest: Path, candidate: Path, report: Path,
|
|
15
|
+
split: str = "test", baseline: Path | None = None) -> dict:
|
|
16
|
+
# Optional dependency only for evaluation; offline fitting remains independent.
|
|
17
|
+
try:
|
|
18
|
+
from codeccat import CodecCat
|
|
19
|
+
except ImportError as ex:
|
|
20
|
+
raise RuntimeError(
|
|
21
|
+
"Evaluation needs the public Runtime: install 'codeccat' "
|
|
22
|
+
"in the evaluation environment"
|
|
23
|
+
) from ex
|
|
24
|
+
|
|
25
|
+
if split not in ("test", "validation"):
|
|
26
|
+
raise ValueError("evaluation split must be 'test' or 'validation'")
|
|
27
|
+
base = manifest.resolve().parent
|
|
28
|
+
records = []
|
|
29
|
+
selected_hashes = set()
|
|
30
|
+
# Audit the full manifest, not just the requested evaluation split.
|
|
31
|
+
# Otherwise an example can occur in train and test while appearing unique
|
|
32
|
+
# in the filtered evaluation subset.
|
|
33
|
+
seen_content = {}
|
|
34
|
+
group_splits = {}
|
|
35
|
+
for index, line in enumerate(manifest.read_text(encoding="utf-8").splitlines(), 1):
|
|
36
|
+
if not line.strip():
|
|
37
|
+
continue
|
|
38
|
+
try:
|
|
39
|
+
r = json.loads(line)
|
|
40
|
+
except json.JSONDecodeError as ex:
|
|
41
|
+
raise ValueError(f"manifest line {index}: invalid JSON") from ex
|
|
42
|
+
for key in ("path", "encoding", "group", "split"):
|
|
43
|
+
if not isinstance(r.get(key), str) or not r[key]:
|
|
44
|
+
raise ValueError(f"line {index}: missing {key}")
|
|
45
|
+
record_split = r["split"]
|
|
46
|
+
if record_split not in ("train", "validation", "test"):
|
|
47
|
+
raise ValueError(f"line {index}: unsupported split")
|
|
48
|
+
rel = Path(r["path"])
|
|
49
|
+
path = (base / rel).resolve()
|
|
50
|
+
if rel.is_absolute() or not path.is_relative_to(base):
|
|
51
|
+
raise ValueError(f"line {index}: path escapes manifest directory")
|
|
52
|
+
raw = path.read_bytes()
|
|
53
|
+
if not raw:
|
|
54
|
+
raise ValueError(f"line {index}: empty input")
|
|
55
|
+
try:
|
|
56
|
+
canonical = codecs.lookup(r["encoding"]).name
|
|
57
|
+
truth = raw.decode(r["encoding"], "strict")
|
|
58
|
+
except (UnicodeError, LookupError) as ex:
|
|
59
|
+
raise ValueError(f"line {index}: invalid encoding or ground truth") from ex
|
|
60
|
+
group = r["group"]
|
|
61
|
+
previous_split = group_splits.setdefault(group, record_split)
|
|
62
|
+
if previous_split != record_split:
|
|
63
|
+
raise ValueError(f"line {index}: source-group leakage across splits")
|
|
64
|
+
digest = hashlib.sha256(raw).hexdigest()
|
|
65
|
+
previous = seen_content.setdefault(digest, (canonical, record_split))
|
|
66
|
+
if previous[1] != record_split:
|
|
67
|
+
raise ValueError(f"line {index}: duplicated bytes across splits")
|
|
68
|
+
if previous[0] != canonical:
|
|
69
|
+
raise ValueError(f"line {index}: conflicting labels for identical bytes")
|
|
70
|
+
if record_split == split:
|
|
71
|
+
if digest in selected_hashes:
|
|
72
|
+
raise ValueError(f"line {index}: duplicated evaluation content")
|
|
73
|
+
selected_hashes.add(digest)
|
|
74
|
+
records.append((raw, canonical, truth))
|
|
75
|
+
if not records:
|
|
76
|
+
raise ValueError(f"manifest has no '{split}' examples")
|
|
77
|
+
|
|
78
|
+
new=CodecCat.from_files(candidate)
|
|
79
|
+
old=CodecCat.from_files(baseline) if baseline else CodecCat()
|
|
80
|
+
both={"candidate":{"n":0,"accepted":0,"correct_text":0,"correct_label":0},
|
|
81
|
+
"baseline":{"n":0,"accepted":0,"correct_text":0,"correct_label":0}}
|
|
82
|
+
by_codec=defaultdict(lambda:{
|
|
83
|
+
"n":0,"candidate_correct_text":0,"baseline_correct_text":0,
|
|
84
|
+
"helps":0,"harms":0})
|
|
85
|
+
helps=harms=ties_correct=ties_wrong=0
|
|
86
|
+
for raw,canonical,truth in records:
|
|
87
|
+
a=new.detect(raw);b=old.detect(raw)
|
|
88
|
+
a_ok=a.text==truth;b_ok=b.text==truth
|
|
89
|
+
group=by_codec[canonical]
|
|
90
|
+
group["n"]+=1
|
|
91
|
+
group["candidate_correct_text"]+=int(a_ok)
|
|
92
|
+
group["baseline_correct_text"]+=int(b_ok)
|
|
93
|
+
group["helps"]+=int(a_ok and not b_ok)
|
|
94
|
+
group["harms"]+=int(b_ok and not a_ok)
|
|
95
|
+
for name,det,ok in (("candidate",a,a_ok),("baseline",b,b_ok)):
|
|
96
|
+
val=both[name]
|
|
97
|
+
val["n"]+=1
|
|
98
|
+
val["accepted"]+=int(det.accepted)
|
|
99
|
+
val["correct_text"]+=int(ok)
|
|
100
|
+
val["correct_label"]+=int(det.encoding is not None
|
|
101
|
+
and codecs.lookup(det.encoding).name==canonical)
|
|
102
|
+
helps+=int(a_ok and not b_ok)
|
|
103
|
+
harms+=int(b_ok and not a_ok)
|
|
104
|
+
ties_correct+=int(a_ok and b_ok)
|
|
105
|
+
ties_wrong+=int(not a_ok and not b_ok)
|
|
106
|
+
|
|
107
|
+
output={
|
|
108
|
+
"schema":"codeccat-model-comparison-v1",
|
|
109
|
+
"evaluation_split":split,
|
|
110
|
+
"warning":"Only a held-out source-disjoint test manifest is independent. Exact cross-split byte and group reuse is rejected; this tool cannot prove absence of near-duplicate or external training-source overlap.",
|
|
111
|
+
"manifest_sha256":hashlib.sha256(manifest.read_bytes()).hexdigest(),
|
|
112
|
+
"candidate_model_sha256":hashlib.sha256(candidate.read_bytes()).hexdigest(),
|
|
113
|
+
"candidate_fingerprint":new.model_fingerprint,
|
|
114
|
+
"baseline_model_sha256":hashlib.sha256(baseline.read_bytes()).hexdigest() if baseline else None,
|
|
115
|
+
"baseline_fingerprint":old.model_fingerprint,
|
|
116
|
+
"results":both,
|
|
117
|
+
"by_codec":dict(sorted(by_codec.items())),
|
|
118
|
+
"paired":{"helps":helps,"harms":harms,"both_correct":ties_correct,
|
|
119
|
+
"both_wrong":ties_wrong},
|
|
120
|
+
}
|
|
121
|
+
report.parent.mkdir(parents=True,exist_ok=True)
|
|
122
|
+
report.write_text(json.dumps(output,indent=2,sort_keys=True)+"\n",encoding="utf-8")
|
|
123
|
+
return output
|
codeccat_train/l3.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Offline L3 unicode-trigram fitting for optional model research.
|
|
2
|
+
|
|
3
|
+
Matches historical LanguageCoherence.fit behavior without importing CodecCat
|
|
4
|
+
Runtime, legacy training code, or third-party corpus into this distribution.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import codecs
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
from collections import Counter
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
SCHEMA = "vnext-l3-unicode-trigram-v1"
|
|
16
|
+
_SPACES = re.compile(r"\s+")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _trigrams(text: str) -> Counter[str]:
|
|
20
|
+
text = _SPACES.sub(" ", text[:4096].casefold())
|
|
21
|
+
return Counter(text[i:i + 3] for i in range(max(0, len(text)-2)))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _decode(data: bytes, encoding: str) -> str | None:
|
|
25
|
+
try:
|
|
26
|
+
decoder = codecs.getincrementaldecoder(encoding)("strict")
|
|
27
|
+
return decoder.decode(data[:4096], final=len(data) < 4096)
|
|
28
|
+
except (UnicodeError, LookupError, ValueError):
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def fit_l3(training_rows, max_features: int = 30000) -> dict:
|
|
33
|
+
if max_features < 100:
|
|
34
|
+
raise ValueError("max_features must be >= 100")
|
|
35
|
+
counts = Counter()
|
|
36
|
+
fingerprint = hashlib.sha256()
|
|
37
|
+
found = 0
|
|
38
|
+
for row in training_rows:
|
|
39
|
+
raw, codec, group = row[:3]
|
|
40
|
+
if not isinstance(raw, bytes) or not isinstance(codec, str) or not isinstance(group, str) or not codec or not group:
|
|
41
|
+
raise ValueError("expected (bytes, encoding, source_group)")
|
|
42
|
+
# Historical corpus() passed a truncated prefix to L3 training.
|
|
43
|
+
# Reproduce that stage when source files themselves exceed 4096 B.
|
|
44
|
+
raw = raw[:4096]
|
|
45
|
+
text = _decode(raw, codec)
|
|
46
|
+
if text is None:
|
|
47
|
+
continue
|
|
48
|
+
counts.update(_trigrams(text))
|
|
49
|
+
fingerprint.update(hashlib.sha256(raw).digest())
|
|
50
|
+
fingerprint.update(codec.encode() + b"\x00" + group.encode() + b"\x00")
|
|
51
|
+
found += 1
|
|
52
|
+
if not found or not counts:
|
|
53
|
+
raise ValueError("no decodable training text")
|
|
54
|
+
grams = dict(sorted(counts.most_common(max_features), key=lambda x: x[0]))
|
|
55
|
+
return {
|
|
56
|
+
"schema": SCHEMA,
|
|
57
|
+
"grams": grams,
|
|
58
|
+
"total": sum(grams.values()),
|
|
59
|
+
"fingerprint": fingerprint.hexdigest(),
|
|
60
|
+
"alpha": 0.5,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def save_l3(training_rows, destination: Path, max_features: int = 30000) -> dict:
|
|
65
|
+
fitted = fit_l3(training_rows, max_features=max_features)
|
|
66
|
+
# Historical LanguageCoherence.save used json.dumps with these options
|
|
67
|
+
# and wrote no trailing newline.
|
|
68
|
+
payload = json.dumps(fitted, sort_keys=True, ensure_ascii=False)
|
|
69
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
destination.write_text(payload, encoding="utf-8")
|
|
71
|
+
return {
|
|
72
|
+
"schema": SCHEMA,
|
|
73
|
+
"model_sha256": hashlib.sha256(payload.encode("utf-8")).hexdigest(),
|
|
74
|
+
"model_bytes": len(payload.encode("utf-8")),
|
|
75
|
+
"training_fingerprint": fitted["fingerprint"],
|
|
76
|
+
"training_grams": len(fitted["grams"]),
|
|
77
|
+
"training_total_trigram_count": fitted["total"],
|
|
78
|
+
"max_features": max_features,
|
|
79
|
+
"optional_model_enabled_by_default": False,
|
|
80
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Stable23 residual-sample selection for offline replay.
|
|
2
|
+
|
|
3
|
+
This is the *data-selection* stage used by the historical bundled-model
|
|
4
|
+
recipe. It is separate from the fitting algorithm and does not download data.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
from collections import Counter
|
|
8
|
+
|
|
9
|
+
_BOMS = (
|
|
10
|
+
(b"\x00\x00\xfe\xff", "utf-32-be"),
|
|
11
|
+
(b"\xff\xfe\x00\x00", "utf-32-le"),
|
|
12
|
+
(b"\xef\xbb\xbf", "utf-8-sig"),
|
|
13
|
+
(b"\xfe\xff", "utf-16-be"),
|
|
14
|
+
(b"\xff\xfe", "utf-16-le"),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def stable_structural_guess(data: bytes, known: set[str]) -> str | None:
|
|
19
|
+
# Match the former Stable23 selection rule, not the full Runtime gate.
|
|
20
|
+
final = len(data) < 4096
|
|
21
|
+
if not final and data and any(
|
|
22
|
+
sig.startswith(data) and len(data) < len(sig) for sig, _ in _BOMS
|
|
23
|
+
):
|
|
24
|
+
return None
|
|
25
|
+
for signature, encoding in _BOMS:
|
|
26
|
+
if data.startswith(signature):
|
|
27
|
+
family = "-".join(encoding.split("-")[:2])
|
|
28
|
+
preferred = (
|
|
29
|
+
family if family in ("utf-16", "utf-32") and family in known
|
|
30
|
+
else encoding
|
|
31
|
+
)
|
|
32
|
+
return preferred if preferred in known else None
|
|
33
|
+
if len(data) < 32:
|
|
34
|
+
return None
|
|
35
|
+
pairs = min(len(data), 4096) // 2
|
|
36
|
+
even = data[: pairs * 2 : 2].count(0)
|
|
37
|
+
odd = data[1 : pairs * 2 : 2].count(0)
|
|
38
|
+
if max(even, odd) >= max(12, int(pairs * 0.30)) and max(
|
|
39
|
+
even, odd
|
|
40
|
+
) >= 4 * max(1, min(even, odd)):
|
|
41
|
+
predicted = "utf-16-be" if even > odd else "utf-16-le"
|
|
42
|
+
if predicted in known:
|
|
43
|
+
return predicted
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def residual_subset(rows, min_anchors: int = 2, max_drop_fraction: float = 0.35):
|
|
48
|
+
"""Return selected input rows and per-class drop counts, preserving order.
|
|
49
|
+
|
|
50
|
+
A row is any sequence starting with (bytes, encoding, group); remaining
|
|
51
|
+
metadata is preserved. Filtering applies to training samples only.
|
|
52
|
+
"""
|
|
53
|
+
labels = {row[1] for row in rows}
|
|
54
|
+
per_label = Counter(row[1] for row in rows)
|
|
55
|
+
drop_limit = {
|
|
56
|
+
label: min(per_label[label] - min_anchors,
|
|
57
|
+
int(per_label[label] * max_drop_fraction))
|
|
58
|
+
for label in per_label
|
|
59
|
+
}
|
|
60
|
+
covered = Counter()
|
|
61
|
+
dropped = Counter()
|
|
62
|
+
chosen = []
|
|
63
|
+
for row in rows:
|
|
64
|
+
data, label = row[:2]
|
|
65
|
+
if stable_structural_guess(data, labels) == label and covered[label] < drop_limit[label]:
|
|
66
|
+
covered[label] += 1
|
|
67
|
+
dropped[label] += 1
|
|
68
|
+
else:
|
|
69
|
+
chosen.append(row)
|
|
70
|
+
if {row[1] for row in chosen} != labels:
|
|
71
|
+
raise ValueError("residual selection removed a training codec class")
|
|
72
|
+
return chosen, dict(dropped)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: codeccat-train
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Offline trainer and evaluator for CodecCat native encoding models
|
|
5
|
+
Project-URL: Repository, https://github.com/Johnny-Kao/CodecCat
|
|
6
|
+
Project-URL: Issues, https://github.com/Johnny-Kao/CodecCat/issues
|
|
7
|
+
Project-URL: Documentation, https://github.com/Johnny-Kao/CodecCat/blob/main/native-prototype/training/docs/GETTING_STARTED.md
|
|
8
|
+
Author: Johnny Kao
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Text Processing :: General
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: numpy>=2.0
|
|
23
|
+
Requires-Dist: scikit-learn<2,>=1.4
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# codeccat-train — independent offline training toolkit
|
|
27
|
+
|
|
28
|
+
Independent Python distribution for training and exporting CodecCat native
|
|
29
|
+
`.ccm` models. **Not a runtime dependency** of `codeccat`. The train
|
|
30
|
+
environment requires NumPy and scikit-learn; production inference does not.
|
|
31
|
+
|
|
32
|
+
**Release candidate: `codeccat-train 0.1.0` (independent package).** The core offline Trainer and end-to-end published-Runtime model-loading workflow are implemented and verified. This repository branch contains a stable release candidate; **PyPI availability is not implied until a separate verified publishing run succeeds**. Historical official L2/L3 reproduction establishes *reproducibility*, not a better model or permission to publish third-party training texts.
|
|
33
|
+
|
|
34
|
+
The same Trainer is used for both workflows: private community-supplied datasets
|
|
35
|
+
remain the user's responsibility; the project-maintained dataset is governed by
|
|
36
|
+
a separate rights and default-model acceptance decision. No user dataset or
|
|
37
|
+
generated model is automatically submitted to the CodecCat project.
|
|
38
|
+
|
|
39
|
+
From a checkout (works before and after the first PyPI publication):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
python -m pip install ./native-prototype/training
|
|
43
|
+
python -m codeccat_train fit --manifest dataset/manifest.jsonl --output custom.ccm --report training-report.json
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Dataset manifest format (one JSON object per line, paths relative to the
|
|
47
|
+
manifest file):
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{"path":"samples/a.bin","encoding":"utf-8","split":"train","group":"document-a"}
|
|
51
|
+
{"path":"samples/b.bin","encoding":"shift_jis","split":"validation","group":"document-b"}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The trainer checks strict decodability, rejects path traversal, conflicting
|
|
55
|
+
duplicate contents and train/validation source-group overlap. Supply varied,
|
|
56
|
+
lawfully obtained text samples with independently known encoding labels.
|
|
57
|
+
The training report stores both `manifest_sha256` (the manifest file bytes) and
|
|
58
|
+
`dataset_content_fingerprint_sha256` (a deterministic digest of accepted
|
|
59
|
+
input-file hashes, codec labels, group IDs and split assignments). Unlike a
|
|
60
|
+
manifest hash alone, the second value changes when an input file's content
|
|
61
|
+
changes at the same path. This does **not** prove dataset licensing or
|
|
62
|
+
independent holdout status, and does not expose raw training text.
|
|
63
|
+
|
|
64
|
+
The generated report describes model provenance and validation; it is **not**
|
|
65
|
+
a guarantee of accuracy on arbitrary real-world text.
|
|
66
|
+
|
|
67
|
+
See the [CodecCat repository](https://github.com/Johnny-Kao/CodecCat)
|
|
68
|
+
for the native runtime and [rights and provenance policy](https://github.com/Johnny-Kao/CodecCat/blob/main/RIGHTS_AND_PROVENANCE.md).
|
|
69
|
+
|
|
70
|
+
## Verification and release boundary
|
|
71
|
+
|
|
72
|
+
- **Implemented and CI-verified:** grouped source-manifest ingestion and isolation,
|
|
73
|
+
deterministic same-environment CCM1/CCM2 training, native Runtime loading,
|
|
74
|
+
source-content fingerprints, candidate-vs-Stable Unicode comparison,
|
|
75
|
+
optional L3 training, exact pinned official historical L2/L3 replay.
|
|
76
|
+
- **Platform evidence:** training and Runtime interoperability checked on Linux
|
|
77
|
+
x64/ARM64 and macOS ARM64; custom-model Runtime portability checked on
|
|
78
|
+
macOS Intel and Windows x64.
|
|
79
|
+
- **Stable-candidate release boundary:** the independent `0.1.0` source/Wheel and release CI must pass before tagging. It is not available via PyPI until its own Trusted Publisher is configured and a separate upload succeeds. Historical training provenance is documented with residual risk, not universally cleared.
|
|
80
|
+
- **Not an automatic Runtime update:** installing or training with this tool
|
|
81
|
+
does not alter `codeccat` or the bundled Stable model. A candidate default
|
|
82
|
+
requires a separately approved evaluation and release.
|
|
83
|
+
|
|
84
|
+
CI evidence: [Trainer E2E](https://github.com/Johnny-Kao/CodecCat/actions/runs/42240463787),
|
|
85
|
+
[macOS ARM64 native trainer](https://github.com/Johnny-Kao/CodecCat/actions/runs/42237800042),
|
|
86
|
+
and [model submission standards](../../docs/MODEL_SUBMISSIONS.md).
|
|
87
|
+
|
|
88
|
+
## Evaluate a trained model
|
|
89
|
+
|
|
90
|
+
Install the public `codeccat==0.1.0` Runtime only in the evaluation environment, then compare against the official bundled model:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
python -m codeccat_train evaluate --model custom.ccm \
|
|
94
|
+
--manifest dataset/test-manifest.jsonl --split test --report comparison.json
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This compares decoded Unicode text (primary) and exact codec labels (secondary), with per-codec results and paired improves/regressions. `--split validation` can be used for development, but **do not call it an independent holdout**. The command does not replace or update the official model.
|
|
98
|
+
|
|
99
|
+
## Optional L3 and historical reproducibility
|
|
100
|
+
|
|
101
|
+
For the optional L3 Unicode-trigram artifact, the **same offline Trainer**
|
|
102
|
+
supports a train-only manifest. Unlike the L2 `--residual-focus` replay,
|
|
103
|
+
L3 uses every eligible unfiltered input record:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
codeccat-train fit-l3 --manifest dataset/train-only.jsonl \
|
|
107
|
+
--output language-l3.json --report l3-report.json
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
L3 stays disabled by default. The pinned historical L2 CCM2 and L3 JSON
|
|
111
|
+
have both been reproduced byte-for-byte in GitHub Actions; see
|
|
112
|
+
[official source and reproducibility status](../../datasets/official/README.md).
|
|
113
|
+
The manual strict replay gate fails when required hashes, source
|
|
114
|
+
fingerprints or numeric identity evidence differ. This is **not** rights
|
|
115
|
+
clearance or authorization to replace a production model.
|
|
116
|
+
|
|
117
|
+
See [quick start](docs/GETTING_STARTED.md). The legacy `python -m codeccat_train --manifest ...` training command remains supported.
|
|
118
|
+
|
|
119
|
+
## Compact CCM2 export
|
|
120
|
+
|
|
121
|
+
The same trainer can export `CCM2` float32 weights instead of the default `CCM1` float64 weights:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
codeccat-train fit --manifest dataset/manifest.jsonl --output custom-f32.ccm \
|
|
125
|
+
--report train-f32.json --precision f32
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`--precision f64` remains the default for compatibility. CCM2 stores IDF and bias as float64 while compressing the weight matrix to float32. A smaller model does not imply better accuracy; compare both models on independently held-out bytes before choosing one. This is a format option for custom models, **not** reproduction of the published official CCM2 model.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
codeccat_train/__init__.py,sha256=cC_9nJupVDVttZPpBDhWAp2K8OlqlpQPDfyY1dqgRn8,86
|
|
2
|
+
codeccat_train/__main__.py,sha256=s6Bg2_6XFO6n6jW2Nw1vM9m6UuDaB6ZUyONZWu_bh2g,12676
|
|
3
|
+
codeccat_train/evaluate.py,sha256=AhBUF7reBWKSGsMRtiBw8pWIZGXmgCmo2aB0kpZXzK8,5814
|
|
4
|
+
codeccat_train/l3.py,sha256=46BXqOvcF6rBsCu7YMxhnbXWWTB4_QtyrK_6g4EY_Sk,3015
|
|
5
|
+
codeccat_train/residual.py,sha256=vaShT0o4rhIvhHO_t9CF5oDIbsR90sPW6pQWSA_61ns,2601
|
|
6
|
+
codeccat_train-0.1.0.dist-info/METADATA,sha256=SAqpYmdiU-a2JjVyPuIFTbfSthxb6id8QyU-MEMUFLk,7112
|
|
7
|
+
codeccat_train-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
8
|
+
codeccat_train-0.1.0.dist-info/entry_points.txt,sha256=REQ1SgJWObuvHb_uvBN6xoB0UI639_W2xb_-zxHSdY4,64
|
|
9
|
+
codeccat_train-0.1.0.dist-info/licenses/LICENSE,sha256=pFFx-WIl6v-GV7UQSH09uaQMNzjX4gPmjOGhtYi64ks,1067
|
|
10
|
+
codeccat_train-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Johnny Kao
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|