diff --git a/visionstate/backend/tests/test_teach.py b/visionstate/backend/tests/test_teach.py index c5552a8..207b906 100644 --- a/visionstate/backend/tests/test_teach.py +++ b/visionstate/backend/tests/test_teach.py @@ -275,6 +275,37 @@ def test_an_index_built_before_an_own_label_existed_is_rebuilt(settings): # noq db.engine.dispose() +def test_a_taught_box_whose_image_is_gone_is_skipped(settings): # noqa: F811 + """A taught box whose stored image has disappeared (a partly restored data dir, an external + clean-up) must not take the whole sensor down: it is skipped, like a broken upload, and the + index is built from the boxes that are still there.""" + import asyncio + + from PIL import Image + + from visionstate.db import Database, Sample, Sensor + from visionstate.engine import Runtime + + settings.ensure_dirs() + db = Database(settings.db_path) + db.init() + rt = Runtime(settings, db) + rt.embedder = FakeEmbedder() + with db.session() as s: + row = Sensor(slug="drive", name="Drive", kind=KIND_OBJECTS, source_type="http", source="x", interval_s=5, + threshold=0.6, debounce=1, objects={"classes": ["dog"]}) # fmt: skip + s.add(row) + s.flush() + sid = row.id + kept = rt.add_object_example(sid, Image.new("RGB", (64, 64)), [0.1, 0.1, 0.5, 0.5], "dog", "dog", 0.9, "live") + gone = rt.add_object_example(sid, Image.new("RGB", (64, 64)), [0.5, 0.5, 0.9, 0.9], "dog", "dog", 0.9, "live") + with db.session() as s: + rt.storage.sample_path(sid, s.get(Sample, gone).filename).unlink() + index = asyncio.run(rt.taught_index(rt.load_sensor(sid))) # would raise FileNotFoundError before + assert index is not None and index.ids == [kept] + db.engine.dispose() + + # --- the API, with the real detector and backbone on CC0 photos ---------------------------- diff --git a/visionstate/backend/visionstate/engine/teaching.py b/visionstate/backend/visionstate/engine/teaching.py index b1c21dc..8ab4931 100644 --- a/visionstate/backend/visionstate/engine/teaching.py +++ b/visionstate/backend/visionstate/engine/teaching.py @@ -48,6 +48,12 @@ def _build_taught(self, cfg: SensorConfig, generation: int = 0) -> teach.TaughtI select(Sample).where(Sample.sensor_id == cfg.id, Sample.object_label.is_not(None)).order_by(Sample.id) ) examples = [x for x in rows if x.object_label is not None and teach.usable(x.object_label, parents)] + # A taught box whose image has vanished (a partly restored data dir, an external clean-up) + # must not take the whole sensor down: it is skipped, like a broken upload. + gone = [x.id for x in examples if not self.storage.sample_path(cfg.id, x.filename).exists()] + if gone: + log.warning("Sensor %s: skipping %d taught box(es) with no image on disk: %s", cfg.slug, len(gone), gone) + examples = [x for x in examples if x.id not in set(gone)] vectors = self._taught_vectors(cfg, examples) return teach.build( embedder.spec.id,