from __future__ import annotations from pathlib import Path from adam.models import ExecutionPlan, PlanStep from adam.studio import ( PreviewEvaluation, StudioStore, TrainingRecipe, caption_path, exact_duplicate_groups, image_files, ) from adam.training_assistant import estimate_plan def test_dataset_review_and_recipe_store_round_trip(tmp_path: Path) -> None: dataset = tmp_path / "dataset" dataset.mkdir() first = dataset / "first.png" second = dataset / "second.png" first.write_bytes(b"same image bytes") second.write_bytes(b"same image bytes") caption_path(first).write_text("subject portrait\n", encoding="utf-8") assert image_files(dataset) == [first, second] assert exact_duplicate_groups([first, second]) == [[str(first), str(second)]] store = StudioStore(tmp_path) store.set_decision(str(dataset), str(first), "keep") recipe = store.add_recipe( TrainingRecipe("Portrait recipe", "lora", 80, preview_prompt="portrait") ) store.add_evaluation( PreviewEvaluation("model-1", "checkpoint", "portrait", 42, 4) ) assert store.toggle_best("model-1") restored = StudioStore(tmp_path) review = restored.review(str(dataset)) assert review.decisions[str(first.resolve())] == "keep" assert restored.recipes[0].id == recipe.id assert restored.evaluations[0].seed == 42 assert "model-1" in restored.best_models def test_plan_estimate_is_labelled_and_bounded(tmp_path: Path) -> None: dataset = tmp_path / "images" dataset.mkdir() for index in range(3): (dataset / f"{index}.jpg").write_bytes(b"image") plan = ExecutionPlan( request="train", summary="Train", steps=[ PlanStep( "lora_trainer", "Train LoRA", "Training", {"dataset_dir": str(dataset), "epochs": 10}, ) ], ) estimates = estimate_plan(plan) assert len(estimates) == 3 assert all(item.level == "estimate" for item in estimates) assert "30 image-epochs" in estimates[0].message assert "Rough duration" in estimates[1].message assert "VRAM" in estimates[2].message def test_rejected_images_are_quarantined_and_recoverable(tmp_path: Path) -> None: dataset = tmp_path / "dataset" dataset.mkdir() image = dataset / "bad.png" image.write_bytes(b"bad image") caption_path(image).write_text("wrong subject", encoding="utf-8") store = StudioStore(tmp_path) store.set_decision(str(dataset), str(image), "reject") assert store.apply_rejections(str(dataset)) == 1 assert not image.exists() assert not caption_path(image).exists() assert store.restore_rejections(str(dataset)) == 1 assert image.is_file() assert caption_path(image).is_file() def test_dataset_review_can_keep_all_then_reject_one(tmp_path: Path) -> None: dataset = tmp_path / "dataset" dataset.mkdir() images = [dataset / "one.png", dataset / "two.png", dataset / "three.png"] for image in images: image.write_bytes(b"image") store = StudioStore(tmp_path) assert store.set_all_decisions(str(dataset), images, "keep") == 3 store.set_decision(str(dataset), str(images[1]), "reject") decisions = store.review(str(dataset)).decisions assert decisions[str(images[0].resolve())] == "keep" assert decisions[str(images[1].resolve())] == "reject" assert decisions[str(images[2].resolve())] == "keep" def test_dataset_review_applies_mixed_decisions_in_one_operation(tmp_path: Path) -> None: dataset = tmp_path / "dataset" dataset.mkdir() first = dataset / "first.png"; first.write_bytes(b"one") second = dataset / "second.png"; second.write_bytes(b"two") store = StudioStore(tmp_path) changed = store.apply_decisions( str(dataset), {str(first): "keep", str(second): "reject"} ) assert changed == 2 assert store.review(str(dataset)).decisions[str(first.resolve())] == "keep" assert store.review(str(dataset)).decisions[str(second.resolve())] == "reject"