diff --git a/.github/workflows/real-yolo-smoke.yml b/.github/workflows/real-yolo-smoke.yml index cf59746..89b80b2 100644 --- a/.github/workflows/real-yolo-smoke.yml +++ b/.github/workflows/real-yolo-smoke.yml @@ -15,7 +15,7 @@ permissions: contents: read jobs: - wheat-sample: + all-crop-samples: runs-on: ubuntu-latest timeout-minutes: 20 steps: @@ -27,5 +27,5 @@ jobs: run: python -m pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu - name: Install Ultralytics run: python -m pip install ultralytics - - name: Run a real sample prediction on CPU + - name: Run real crop checkpoints on CPU run: python inference/tests/smoke_real_model.py diff --git a/docs/TESTING.md b/docs/TESTING.md index 5860648..76abd04 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -6,7 +6,7 @@ | Vue build | `cd frontend && npm ci && npm run build` | The frontend bundle compiles | Browser integration or API responses | | Spring Boot checks | `cd backend && mvn -Dtest=DeepSeekControllerTest,FileControllerTest package` | Backend compilation, selected input/control tests | Database integration or paid AI connectivity | | Documentation links | `python scripts/check_docs_links.py` | Relative Markdown links resolve | External link availability | -| **Real YOLO smoke test** | GitHub Actions: `Real YOLO CPU smoke test` | Loads an actual wheat checkpoint and predicts one bundled JPG on a GitHub-hosted CPU | Diagnostic accuracy, full application workflow, multi-user concurrency, camera/video streaming, other weights | +| **Real YOLO smoke test** | GitHub Actions: `Real YOLO CPU smoke test` | Loads all nine crop checkpoints and predicts one corresponding bundled JPG per crop on a GitHub-hosted CPU | Diagnostic accuracy, full application workflow, multi-user concurrency, camera/video streaming, disease classification accuracy | ## Run the real inference smoke test @@ -19,7 +19,7 @@ gh run list -R FuTseYi/CropLens --workflow real-yolo-smoke.yml --limit 1 Alternatively, use **Actions → Real YOLO CPU smoke test → Run workflow** in the GitHub repository. -The optional test installs CPU PyTorch/Ultralytics packages and may download substantial dependencies. It is intentionally not run on every PR or on ordinary documentation updates. Read the run logs for the actual pass/fail status. A passing smoke test indicates only that **one checkpoint can load and process one repository sample**; it says nothing about inference accuracy or deployment readiness. +The optional test covers nine weight files and matching sample folders. It installs CPU PyTorch/Ultralytics packages and may download substantial dependencies. It is intentionally not run on every PR or on ordinary documentation updates. Read the run logs for the actual pass/fail status. A passing smoke test indicates only that **each tested checkpoint can load and process one matching repository sample**; it says nothing about inference accuracy or deployment readiness. ## Before claiming production readiness diff --git a/inference/tests/smoke_real_model.py b/inference/tests/smoke_real_model.py index ce49adc..ec2203b 100644 --- a/inference/tests/smoke_real_model.py +++ b/inference/tests/smoke_real_model.py @@ -1,39 +1,56 @@ -"""Execute one real CPU-only crop model prediction without Flask or a database. +"""CPU-only smoke test: load and run each bundled crop model on one real JPG. -Unlike the standard mock-based unit tests, this must load a checked-in -Ultralytics checkpoint and decode an actual sample image. It does not -evaluate diagnostic accuracy or full frontend/backend integration. +No Flask, database or browser is needed. This checks basic compatibility, +not prediction accuracy, safety or reliable crop-disease diagnosis. """ +import gc from pathlib import Path + from ultralytics import YOLO +CROPS = ( + ("apple", "苹果"), + ("corn", "玉米"), + ("cotton", "棉花"), + ("grape", "葡萄"), + ("potato", "马铃薯"), + ("rice", "水稻"), + ("strawberry", "草莓"), + ("tomato", "番茄"), + ("wheat", "小麦"), +) + + def main(): root = Path(__file__).resolve().parents[2] - weights = root / "inference" / "weights" / "wheat_best.pt" - images = sorted((root / "assets" / "samples" / "小麦").glob("*.jpg")) - if not weights.is_file() or not images: - raise SystemExit("Required wheat checkpoint/sample not found") - image = images[0] - - print("Model:", weights.relative_to(root)) - print("Image:", image.relative_to(root)) - model = YOLO(str(weights)) - results = model.predict( - source=str(image), - device="cpu", - imgsz=640, - conf=0.25, - save=False, - verbose=False, - ) - assert len(results) == 1, "Expected exactly one image prediction" - result = results[0] - assert result.boxes is not None, "Expected a detection result structure" - assert len(result.orig_shape) == 2 and all(n > 0 for n in result.orig_shape) - print("Real inference succeeded; image shape:", result.orig_shape) - print("Detections:", len(result.boxes)) - print("This is a smoke test, not an accuracy or clinical evaluation.") + tested = 0 + for crop, chinese_folder in CROPS: + weights = root / "inference" / "weights" / (crop + "_best.pt") + samples = sorted((root / "assets" / "samples" / chinese_folder).glob("*.jpg")) + if not weights.is_file() or not samples: + raise SystemExit("Missing checkpoint or sample for {}".format(crop)) + + image = samples[0] + print("Checking {} with {} ...".format(crop, image.relative_to(root)), flush=True) + model = YOLO(str(weights)) + results = model.predict( + source=str(image), device="cpu", imgsz=640, + conf=0.25, save=False, verbose=False, + ) + assert len(results) == 1, "Expected one image result for {}".format(crop) + result = results[0] + assert result.boxes is not None, "Missing detection result for {}".format(crop) + assert len(result.orig_shape) == 2 and all(n > 0 for n in result.orig_shape) + print("OK: {} — image {} — {} detections".format( + crop, result.orig_shape, len(result.boxes)), flush=True) + tested += 1 + del result, results, model + gc.collect() + + assert tested == len(CROPS) + print("All {} crop checkpoints completed CPU inference.".format(tested)) + print("This is a smoke test only, NOT a validation of model accuracy.") if __name__ == "__main__":