| from __future__ import annotations |
|
|
| import hashlib |
| from pathlib import Path |
| from typing import Any |
|
|
| from PIL import Image, ImageFilter, ImageStat |
|
|
| from adam.models import Job |
|
|
|
|
| IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".bmp"} |
|
|
|
|
| def _candidate_images(job: Job, limit: int = 64) -> list[Path]: |
| found: list[Path] = [] |
| if job.preview_path: |
| preview = Path(job.preview_path) |
| if preview.is_file(): |
| found.append(preview) |
| output = Path(job.output_folder).expanduser() if job.output_folder else None |
| if output is not None and output.is_dir(): |
| try: |
| for path in output.rglob("*"): |
| if len(found) >= limit: |
| break |
| if not path.is_file() or path.suffix.casefold() not in IMAGE_EXTENSIONS: |
| continue |
| lowered = str(path.relative_to(output)).casefold() |
| if any(token in lowered for token in ("preview", "sample", "generation", "epoch")) and path not in found: |
| found.append(path) |
| except OSError: |
| pass |
| return found |
|
|
|
|
| def evaluate_job_output(job: Job) -> dict[str, Any]: |
| """Evaluate technical sample health without claiming to judge artistic quality.""" |
| if not any(step.tool_id.endswith("_trainer") for step in job.plan.steps): |
| return {} |
| paths = _candidate_images(job) |
| hashes: list[str] = [] |
| sharpness: list[float] = [] |
| corrupted: list[str] = [] |
| for path in paths: |
| try: |
| with Image.open(path) as source: |
| image = source.convert("RGB") |
| thumb = image.resize((32, 32)).convert("L") |
| hashes.append(hashlib.sha1(thumb.tobytes()).hexdigest()) |
| edges = image.resize((256, 256)).convert("L").filter(ImageFilter.FIND_EDGES) |
| sharpness.append(float(ImageStat.Stat(edges).var[0])) |
| except (OSError, ValueError): |
| corrupted.append(str(path)) |
| valid = len(hashes) |
| unique = len(set(hashes)) |
| duplicate_count = valid - unique |
| if valid < 4: |
| status = "NEEDS SAMPLES" |
| summary = f"Only {valid} readable training preview(s) were available. Generate a fixed sample set for a meaningful comparison." |
| elif corrupted or duplicate_count / max(valid, 1) >= 0.25: |
| status = "NEEDS REVIEW" |
| summary = f"Reviewed {valid} samples; found {duplicate_count} exact-looking duplicate(s) and {len(corrupted)} unreadable file(s)." |
| else: |
| status = "TECHNICALLY HEALTHY" |
| summary = f"Reviewed {valid} samples with no obvious corruption or exact duplicate collapse. Subject and artistic quality still need human review." |
| return { |
| "agent": "NOVA", |
| "status": status, |
| "summary": summary, |
| "sample_count": valid, |
| "unique_count": unique, |
| "duplicate_count": duplicate_count, |
| "corrupted_count": len(corrupted), |
| "average_edge_variance": round(sum(sharpness) / len(sharpness), 2) if sharpness else None, |
| "sample_paths": [str(path) for path in paths], |
| } |
|
|