Image Segmentation
PyTorch
English
painting-vision-robotics-kit
semantic-segmentation
robotics
edge-ai
construction-ai
autonomous-painting
wall-painting-robot
paint-coverage-estimation
building-facade
drywall
skirting-detection
window-detection
lidar
depth-validation
segformer
deeplabv3
Eval Results (legacy)
Upload release_check.py with huggingface_hub
Browse files- release_check.py +125 -0
release_check.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Is this release deployable? Checks the checkpoint and the code set before anything is published.
|
| 3 |
+
|
| 4 |
+
Two failures this exists to stop (both happened):
|
| 5 |
+
|
| 6 |
+
* **A checkpoint the robot cannot use.** Without a condition head the safety gate
|
| 7 |
+
rejects every frame ("surface condition not assessed"); without a completion
|
| 8 |
+
threshold sign-off is disabled; without temperatures the confidence thresholds
|
| 9 |
+
mean nothing; without ``condition_operating_point`` damage flags run on
|
| 10 |
+
uncalibrated defaults that flag nearly every real photo.
|
| 11 |
+
* **Code that does not import.** A hand-kept file list drifted behind ``predict.py``'s
|
| 12 |
+
imports. ``release_files()`` takes every tracked top-level module instead, and
|
| 13 |
+
``missing_imports()`` proves each published module's local imports are published too.
|
| 14 |
+
|
| 15 |
+
Usage::
|
| 16 |
+
|
| 17 |
+
python3 release_check.py --checkpoint artifacts/candidates/ft4-cal.pt # exit 1 = not deployable
|
| 18 |
+
"""
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
import argparse
|
| 22 |
+
import ast
|
| 23 |
+
import json
|
| 24 |
+
import subprocess
|
| 25 |
+
import sys
|
| 26 |
+
from pathlib import Path
|
| 27 |
+
|
| 28 |
+
PROJECT = Path(__file__).resolve().parent
|
| 29 |
+
# Data-collection and one-off scripts that are not part of the runtime kit.
|
| 30 |
+
EXCLUDE = {"fetch_construction_data.py", "fetch_surface_eval.py"}
|
| 31 |
+
EXTRA = ["requirements.txt", "metadata_template.csv", "console/index.html", "console/app.js", "console/style.css"]
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def tracked_files(project=PROJECT):
|
| 35 |
+
out = subprocess.run(["git", "ls-files"], cwd=project, capture_output=True, text=True, check=True).stdout
|
| 36 |
+
return set(out.split())
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def release_files(project=PROJECT):
|
| 40 |
+
"""Every tracked top-level module (and its tests), minus data scripts, plus runtime assets."""
|
| 41 |
+
tracked = tracked_files(project)
|
| 42 |
+
modules = sorted(f for f in tracked if f.endswith(".py") and "/" not in f and f not in EXCLUDE)
|
| 43 |
+
return modules + [f for f in EXTRA if f in tracked]
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def missing_imports(files, project=PROJECT):
|
| 47 |
+
"""{published module: [local modules it imports that are not published]}."""
|
| 48 |
+
local = {p.stem for p in project.glob("*.py")}
|
| 49 |
+
published = {Path(f).stem for f in files if f.endswith(".py")}
|
| 50 |
+
gaps = {}
|
| 51 |
+
for name in files:
|
| 52 |
+
if not name.endswith(".py"):
|
| 53 |
+
continue
|
| 54 |
+
tree = ast.parse((project / name).read_text(), filename=name)
|
| 55 |
+
imported = set()
|
| 56 |
+
for node in ast.walk(tree):
|
| 57 |
+
if isinstance(node, ast.Import):
|
| 58 |
+
imported.update(alias.name.split(".")[0] for alias in node.names)
|
| 59 |
+
elif isinstance(node, ast.ImportFrom) and node.module and not node.level:
|
| 60 |
+
imported.add(node.module.split(".")[0])
|
| 61 |
+
missing = sorted((imported & local) - published)
|
| 62 |
+
if missing:
|
| 63 |
+
gaps[name] = missing
|
| 64 |
+
return gaps
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def check_checkpoint(checkpoint):
|
| 68 |
+
"""``{"errors": [...], "warnings": [...]}`` for a loaded checkpoint dict."""
|
| 69 |
+
from label_schema import CLASSES
|
| 70 |
+
errors, warnings = [], []
|
| 71 |
+
if tuple(checkpoint.get("classes") or ()) != tuple(CLASSES):
|
| 72 |
+
errors.append("class schema does not match label_schema.CLASSES")
|
| 73 |
+
if not checkpoint.get("condition_classes"):
|
| 74 |
+
errors.append("no surface-condition head: the gate rejects every frame (condition not assessed)")
|
| 75 |
+
if checkpoint.get("temperature") is None:
|
| 76 |
+
errors.append("no temperature calibration: run train.py --calibrate-checkpoint")
|
| 77 |
+
if checkpoint.get("completion_threshold") is None:
|
| 78 |
+
errors.append("no completion threshold: sign-off is disabled (train.py --calibrate-checkpoint)")
|
| 79 |
+
point = checkpoint.get("condition_operating_point")
|
| 80 |
+
if checkpoint.get("condition_classes") and not point:
|
| 81 |
+
errors.append("no condition_operating_point: damage flags use uncalibrated defaults "
|
| 82 |
+
"(calibrate_condition.py --write)")
|
| 83 |
+
elif point and point.get("false_alarm_set") != "real":
|
| 84 |
+
warnings.append("condition operating point was calibrated without real sound walls")
|
| 85 |
+
if "wallpaper" not in (checkpoint.get("material_classes") or []):
|
| 86 |
+
warnings.append("no wallpaper material class: wallpaper rests on the pattern cue alone")
|
| 87 |
+
return {"errors": errors, "warnings": warnings}
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def run(checkpoint_path=None, project=PROJECT):
|
| 91 |
+
report = {"files": release_files(project)}
|
| 92 |
+
report["missing_imports"] = missing_imports(report["files"], project)
|
| 93 |
+
report["errors"] = [f"{m} imports unpublished {', '.join(g)}" for m, g in report["missing_imports"].items()]
|
| 94 |
+
report["warnings"] = []
|
| 95 |
+
if checkpoint_path:
|
| 96 |
+
import torch
|
| 97 |
+
checkpoint = torch.load(checkpoint_path, map_location="cpu", weights_only=True)
|
| 98 |
+
result = check_checkpoint(checkpoint)
|
| 99 |
+
report["errors"] += result["errors"]
|
| 100 |
+
report["warnings"] += result["warnings"]
|
| 101 |
+
report["checkpoint"] = str(checkpoint_path)
|
| 102 |
+
report["deployable"] = not report["errors"]
|
| 103 |
+
return report
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def main():
|
| 107 |
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 108 |
+
ap.add_argument("--checkpoint", type=Path)
|
| 109 |
+
ap.add_argument("--json", action="store_true")
|
| 110 |
+
args = ap.parse_args()
|
| 111 |
+
report = run(args.checkpoint)
|
| 112 |
+
if args.json:
|
| 113 |
+
print(json.dumps(report, indent=2))
|
| 114 |
+
else:
|
| 115 |
+
print(f"{len(report['files'])} files in the release")
|
| 116 |
+
for e in report["errors"]:
|
| 117 |
+
print("ERROR ", e)
|
| 118 |
+
for w in report["warnings"]:
|
| 119 |
+
print("warning", w)
|
| 120 |
+
print("deployable" if report["deployable"] else "NOT deployable")
|
| 121 |
+
sys.exit(0 if report["deployable"] else 1)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
if __name__ == "__main__":
|
| 125 |
+
main()
|