Tao Finetune Anomalygennext
NVIDIAOfficialCreated by NVIDIA Corporation
- Skill
- Workflow
- Data and AI
- Apache-2.0
- Medium risk
- Has scripts
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a structured workflow for fine-tuning AnomalyGenNext models on custom datasets. It ensures data integrity by validating dataset structures, image formats, and binary masks before training. The process includes converting base checkpoints to the required DCP format, generating a canonical training recipe, and executing the fine-tuning process within the specified AnomalyGenNext 1.1 container environment.
What it does
- Validates user datasets, including defect specifications and image/mask pairings.
- Converts Cosmos3-Nano checkpoints into the required DCP format.
- Generates a canonical fine-tuning recipe from user templates.
- Enforces strict validation of binary masks and image file extensions.
- Executes LoRA fine-tuning using the AnomalyGenNext 1.1 runtime.
- Verifies training success through iteration-zero and scored checkpoint validation.
When to use it
- When you need to fine-tune an AnomalyGenNext model on a custom texture and defect dataset.
- When AnomalyGenNext task weights do not yet exist for your specific use case.
- When you need to ensure your dataset and recipe meet the strict input requirements of the AnomalyGenNext 1.1 container.
- When you need to convert a Hugging Face format checkpoint for use in the AnomalyGenNext training pipeline.
Advantages
- Ensures compatibility with the AnomalyGenNext 1.1 runtime through automated validation.
- Prevents training failures by checking mask and image validity before GPU resources are consumed.
- Standardizes the training process with canonical recipes and verified checkpoint layouts.
- Provides a clear path for model acceptance based on objective performance metrics.
Usage examples
- “Prepare a fine-tuning recipe for my new texture dataset located at /data/my_dataset.”
- The agent runs the recipe preparation script, validates the dataset structure and masks, and outputs a canonical recipe file ready for training.
- “Convert my Hugging Face Cosmos3-Nano checkpoint for use with AnomalyGenNext.”
- The agent executes the conversion script to transform the checkpoint into the required DCP format, ensuring it is ready for the fine-tuning workflow.
- “Train an AnomalyGenNext model using my validated canonical recipe.”
- The agent launches the training workflow, mounts the necessary checkpoints and data, and monitors the process until a successful handoff is achieved.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/nvidia/tao-finetune-anomalygennext/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip tao-finetune-anomalygennext.zip -d .agents/skills
Documentation by NVIDIA Corporation
Fine-tune AnomalyGenNext
This leaf prepares a user-owned dataset and optional recipe for task-specific
AnomalyGenNext LoRA training. The pinned image is declared in
references/skill_info.yaml; do not replace it with the older 1.0 release.
Read references/input-contract.md when validating a dataset or adapting a
recipe, and references/container-runtime.md before submission.
Inputs
DATASET/
defect_spec.jsonl
TEXTURE/
clean_image/*
anomaly_image/DEFECT/*
mask/DEFECT/*
VALIDATION/testcase.jsonlAlso supply a DCP-format Cosmos3-Nano base checkpoint directory containing
checkpoint.json, model/.metadata, and at least one model/*.distcp shard,
Wan2.2_VAE.pth, and
a complete checkpoint root containing the required Qwen tokenizer model assets
under hf/ plus facebook/dinov2-large/ for validation. The hf/ directory
name is part of the upstream image's fixed checkpoint layout. The validation
JSONL must contain image_filename, mask_filename, and anomaly_type; each trained
TEXTURE+DEFECT needs at least three rows. A separately stored defect spec is
accepted with --defect-spec.
If the official Cosmos3-Nano checkpoint is not already staged in DCP format, run the converter shipped in the pinned AnomalyGenNext image and write its output to a mounted, persistent directory:
python -m cosmos_framework.scripts.convert_model_to_dcp \
-o /models/Cosmos3-Nano-dcp \
--checkpoint-path Cosmos3-NanoThe registered Cosmos3-Nano name resolves the official checkpoint. To
convert an already-staged Hugging Face checkpoint instead, replace it with
that directory's absolute container path. Use the converter output as
--base-checkpoint; preparation verifies checkpoint.json,
model/.metadata, and the generated model/*.distcp shards before GPU work.
Dataset and validation images and masks must use .jpg, .jpeg, or .png,
matching the extensions supported by the AnomalyGenNext 1.1 runtime loader.
Every mask pixel must be binary 0 or 255; palette or grayscale masks with
intermediate values are rejected before training. Each mask must contain both
values, so empty/all-zero and full/all-255 masks are also rejected.
An optional user recipe.yaml is a template. Custom training settings remain,
while dataset, checkpoint, validation, type order, and iteration-zero validation
are replaced by validated identities. Without a template, the packaged recipe
uses the established 5000-step defaults.
Prepare
After the common launch review, run the prepare_recipe action:
scripts/prepare_finetune_recipe.py \
--dataset-root /data/my_dataset \
--validation-testcase /data/validation/testcase.jsonl \
--base-checkpoint /models/Cosmos3-Nano \
--vae-path /models/Wan2.2_VAE.pth \
--checkpoint-root /models/anomalygen-checkpoints \
--dataset-name my_dataset \
--recipe-template /data/recipe.yaml \
--output /results/canonical_recipe.yamlThe action freezes absolute validation paths, validates the DCP base-checkpoint
shape, rejects image extensions the runtime cannot decode, validates
anomaly/mask pairing and binary masks, checks type agreement with
defect_spec.jsonl, refuses output reuse, and emits a recipe plus metadata.
validation_iter must be a multiple of save_iter; max_iter must reach a
post-baseline validation.
Train and accept
Invoke tao-launch-workflow, review the platform, image, mounts, GPU shape,
runtime, and exact recipe, then submit the train action. Bind the entire
checkpoint root read-only at the fixed container path declared in
skill_info.yaml; the upstream trainer resolves the Qwen tokenizer model
assets at checkpoints/hf and DINOv2 at
checkpoints/facebook/dinov2-large beneath its image repository. Mounting only
the DINOv2 directory or setting HF_HOME does not satisfy that contract.
scripts/finetune_anomalygennext.sh \
--recipe /results/canonical_recipe.yaml \
--results-dir /new/training_results \
--checkpoint-root /workspace/paidf-anomalygen/checkpoints \
--num-gpus 1The wrapper uses the upstream trainer already inside the pinned container. It
refuses existing publication outputs and, unless --resume is explicit, an
existing runtime directory. Accept success only when training_handoff.json
is COMPLETE: iteration zero and a later scored checkpoint must exist,
best_checkpoint.txt must select the maximum Average.nn_score, and the score
must improve strictly. The handoff hashes the copied adapter and recipe.
Read references/nn-score-contract.md before changing metric or acceptance
settings. A 1000-step run is the smallest quality smoke; shorter runs prove
wiring only.
Files
- ATTRIBUTION.mdAdded by AutoGineer951 B
- BENCHMARK.md6.9 KiB
- LICENSELicence file added by AutoGineer11.2 KiB
- SKILL.md5.1 KiB
- assets/default_recipe.yaml593 B
- config/skillspector-baseline.yaml1.6 KiB
- evals/evals.json1.0 KiB
- references/container-runtime.md2.3 KiB
- references/input-contract.md2.8 KiB
- references/nn-score-contract.md837 B
- references/skill_info.yaml2.5 KiB
- scripts/finetune_anomalygennext.shScript3.4 KiB
- scripts/prepare_finetune_recipe.pyScript12.8 KiB
- scripts/tests/test_nn_improvement.pyScript3.1 KiB
- scripts/tests/test_prepare_finetune_recipe.pyScript8.9 KiB
- scripts/validate_nn_improvement.pyScript6.4 KiB
- skill-card.md5.2 KiB
- skill.oms.sig7.2 KiB
scripts/prepare_finetune_recipe.py
#!/usr/bin/env python3
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Validate AnomalyGenNext fine-tuning inputs and freeze a canonical recipe."""
from __future__ import annotations
import argparse
import hashlib
import json
import re
from collections import Counter
from pathlib import Path
from typing import Any
import yaml
from PIL import Image
IMAGES = {".jpeg", ".jpg", ".png"}
UNSUPPORTED_IMAGE_EXTS = {".bmp", ".tif", ".tiff", ".webp"}
SAFE_NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
DEFAULT_RECIPE = Path(__file__).resolve().parents[1] / "assets" / "default_recipe.yaml"
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _images(path: Path) -> list[Path]:
files = sorted(item for item in path.iterdir() if item.is_file())
unsupported = [item for item in files
if item.suffix.lower() in UNSUPPORTED_IMAGE_EXTS]
if unsupported:
raise ValueError(
f"unsupported AnomalyGenNext image extension: {unsupported[0]}; "
"use .jpg, .jpeg, or .png"
)
return [item for item in files if item.suffix.lower() in IMAGES]
def _validate_extension(path: Path, label: str) -> None:
if path.suffix.lower() not in IMAGES:
raise ValueError(
f"unsupported AnomalyGenNext {label} extension: {path}; "
"use .jpg, .jpeg, or .png"
)
def _validate_mask(mask: Path) -> None:
try:
with Image.open(mask) as image:
values = {value for value, count in enumerate(image.convert("L").histogram()) if count}
except OSError as error:
raise ValueError(f"cannot read AnomalyGenNext mask: {mask}") from error
if values != {0, 255}:
raise ValueError(
f"mask must contain exactly binary pixel values 0 and 255: {mask}; "
f"found {sorted(values)}"
)
def _pairs(root: Path) -> list[list[str]]:
result = []
for texture in sorted(item for item in root.iterdir() if item.is_dir()):
anomaly_root = texture / "anomaly_image"
if not anomaly_root.is_dir():
continue
if not (texture / "clean_image").is_dir() or not _images(texture / "clean_image"):
raise ValueError(f"texture has no clean images: {texture.name}")
for defect in sorted(item for item in anomaly_root.iterdir() if item.is_dir()):
anomalies = _images(defect)
if not anomalies:
continue
mask_root = texture / "mask" / defect.name
if not mask_root.is_dir():
raise ValueError(f"missing mask directory: {mask_root}")
for image in anomalies:
choices = (mask_root / f"{image.stem}_mask{image.suffix}",
mask_root / f"{image.stem}_mask.png", mask_root / image.name)
mask = next((path for path in choices if path.is_file()), None)
if mask is None:
raise ValueError(f"anomaly lacks matching mask: {image}")
_validate_mask(mask)
result.append([texture.name, defect.name])
if not result:
raise ValueError(f"no TEXTURE/anomaly_image/DEFECT inputs under {root}")
return result
def _declared_types(path: Path) -> set[str]:
result = set()
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
if not line.strip():
continue
row = json.loads(line)
name = str(row.get("defect_type") or row.get("anomaly_type") or "")
if not name:
raise ValueError(f"missing defect_type at {path}:{number}")
if row.get("spatial_dependency") == "text" and not row.get("roi_prompt_defect_location"):
raise ValueError(f"text-routed {name} lacks roi_prompt_defect_location")
result.add(name)
return result
def _validation_path(raw: str, testcase: Path, dataset: Path, name: str) -> Path:
path = Path(raw)
choices = [path] if path.is_absolute() else [testcase.parent / path, dataset / path]
parts = path.parts
if not path.is_absolute() and len(parts) > 2 and parts[:2] == ("datasets", name):
choices.append(dataset / Path(*parts[2:]))
for choice in choices:
if choice.is_file():
return choice.resolve()
raise ValueError(f"cannot resolve validation path: {raw}")
def _validation(path: Path, dataset: Path, name: str) -> tuple[list[dict[str, Any]], Counter]:
rows, counts = [], Counter()
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
if not line.strip():
continue
row = json.loads(line)
anomaly = str(row.get("anomaly_type") or "")
if not anomaly:
raise ValueError(f"missing anomaly_type at {path}:{number}")
for key in ("image_filename", "mask_filename"):
if not row.get(key):
raise ValueError(f"missing {key} at {path}:{number}")
resolved = _validation_path(str(row[key]), path, dataset, name)
_validate_extension(resolved, "mask" if key == "mask_filename" else "image")
row[key] = str(resolved)
_validate_mask(Path(row["mask_filename"]))
rows.append(row)
counts[anomaly] += 1
if not rows:
raise ValueError("validation testcase is empty")
return rows, counts
def _validate_checkpoint_tree(root: Path) -> None:
root = root.expanduser().resolve()
qwen_assets = root / "hf"
dinov2 = root / "facebook" / "dinov2-large"
if not qwen_assets.is_dir():
raise ValueError(
f"checkpoint root lacks required Qwen tokenizer assets under hf/: {qwen_assets}"
)
if not (dinov2 / "config.json").is_file():
raise ValueError(f"checkpoint root lacks DINOv2 config: {dinov2}")
if not ((dinov2 / "model.safetensors").is_file()
or (dinov2 / "pytorch_model.bin").is_file()):
raise ValueError(f"checkpoint root lacks DINOv2 weights: {dinov2}")
def _validate_base_checkpoint(root: Path) -> None:
root = root.expanduser().resolve()
model = root / "model"
if (not (root / "checkpoint.json").is_file()
or not (model / ".metadata").is_file()
or not any(path.is_file() for path in model.glob("*.distcp"))):
raise ValueError(
"Cosmos3-Nano base checkpoint must be a DCP directory containing "
f"checkpoint.json, model/.metadata, and model/*.distcp: {root}. "
"Convert it with cosmos_framework.scripts.convert_model_to_dcp in the pinned "
"AnomalyGenNext container; see references/container-runtime.md."
)
def prepare(args: argparse.Namespace) -> dict[str, Any]:
required = (("dataset root", args.dataset_root, True),
("validation testcase", args.validation_testcase, False),
("base checkpoint", args.base_checkpoint, True),
("VAE", args.vae_path, False),
("checkpoint root", args.checkpoint_root, True))
for label, path, directory in required:
if not (path.is_dir() if directory else path.is_file()):
raise ValueError(f"{label} is missing: {path}")
_validate_base_checkpoint(args.base_checkpoint)
_validate_checkpoint_tree(args.checkpoint_root)
if not SAFE_NAME.fullmatch(args.dataset_name) or not SAFE_NAME.fullmatch(args.job_name):
raise ValueError("dataset and job names may contain only letters, numbers, dot, dash, underscore")
recipe = yaml.safe_load(DEFAULT_RECIPE.read_text())
if args.recipe_template:
template = yaml.safe_load(args.recipe_template.read_text())
if not isinstance(template, dict):
raise ValueError("recipe template must be a YAML mapping")
recipe.update(template)
discovered = _pairs(args.dataset_root.resolve())
pairs = recipe.get("anomaly_types", discovered)
if not isinstance(pairs, list) or not pairs or any(
not isinstance(pair, list) or len(pair) != 2 or not all(isinstance(v, str) and v for v in pair)
for pair in pairs):
raise ValueError("anomaly_types must be nonempty [texture, defect] pairs")
if len({tuple(pair) for pair in pairs}) != len(pairs):
raise ValueError("anomaly_types contains duplicates")
missing = sorted(set(map(tuple, pairs)) - set(map(tuple, discovered)))
if missing:
raise ValueError(f"recipe types absent from dataset: {missing}")
types = [f"{texture}+{defect}" for texture, defect in pairs]
defect_spec = (args.defect_spec or args.dataset_root / "defect_spec.jsonl").resolve()
if not defect_spec.is_file():
raise ValueError(f"defect spec is missing: {defect_spec}")
undefined = sorted(set(types) - _declared_types(defect_spec))
if undefined:
raise ValueError(f"recipe types absent from defect spec: {undefined}")
rows, counts = _validation(args.validation_testcase.resolve(), args.dataset_root.resolve(),
args.dataset_name)
insufficient = {name: counts[name] for name in types if counts[name] < args.min_validation_per_type}
extra = sorted(set(counts) - set(types))
if insufficient or extra:
raise ValueError(f"validation coverage mismatch: insufficient={insufficient}, extra={extra}")
if args.max_iter is not None:
recipe["max_iter"] = args.max_iter
if args.validation_iter is not None:
recipe["validation_iter"] = args.validation_iter
if args.save_iter is not None:
recipe["save_iter"] = args.save_iter
for key in ("max_iter", "validation_iter", "save_iter"):
if not isinstance(recipe.get(key), int) or isinstance(recipe[key], bool) or recipe[key] <= 0:
raise ValueError(f"{key} must be a positive integer")
if recipe["validation_iter"] % recipe["save_iter"] or recipe["max_iter"] < recipe["validation_iter"]:
raise ValueError("validation/save intervals must align and max_iter must reach validation")
normalized = args.output.with_suffix(".validation.jsonl")
metadata = args.output.with_suffix(".metadata.json")
if any(path.exists() for path in (args.output, normalized, metadata)):
raise FileExistsError("refusing to overwrite recipe outputs")
normalized.parent.mkdir(parents=True, exist_ok=True)
normalized.write_text("".join(json.dumps(row, sort_keys=True) + "\n" for row in rows))
recipe.update({"task_type": "texture_ft", "experiment": "anomalygen_texture_ft",
"job_name": args.job_name, "dataset_name": args.dataset_name,
"anomaly_types": pairs, "dataset_path": str(args.dataset_root.resolve()),
"testcase_jsonl": str(normalized.resolve()), "model_size": "nano",
"base_checkpoint_path": str(args.base_checkpoint.resolve()),
"vae_path": str(args.vae_path.resolve()), "run_validation_on_start": True,
"early_stop_metric": "nn"})
args.output.write_text(yaml.safe_dump(recipe, sort_keys=False))
report = {"status": "COMPLETE", "recipe": str(args.output.resolve()),
"recipe_sha256": _sha256(args.output), "validation_testcase": str(normalized.resolve()),
"dataset_root": str(args.dataset_root.resolve()), "defect_spec": str(defect_spec),
"base_checkpoint": str(args.base_checkpoint.resolve()),
"base_checkpoint_format": "dcp",
"checkpoint_root": str(args.checkpoint_root.resolve()),
"anomaly_types": types,
"validation_counts": {name: counts[name] for name in types},
"metric": "Average.nn_score", "direction": "max"}
metadata.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n")
return report
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--dataset-root", type=Path, required=True)
parser.add_argument("--validation-testcase", type=Path, required=True)
parser.add_argument("--base-checkpoint", type=Path, required=True)
parser.add_argument("--vae-path", type=Path, required=True)
parser.add_argument("--checkpoint-root", type=Path, required=True)
parser.add_argument("--dataset-name", required=True)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--defect-spec", type=Path)
parser.add_argument("--recipe-template", type=Path)
parser.add_argument("--job-name", default="anomalygen_texture_ft")
parser.add_argument("--min-validation-per-type", type=int, default=3)
parser.add_argument("--max-iter", type=int)
parser.add_argument("--validation-iter", type=int)
parser.add_argument("--save-iter", type=int)
args = parser.parse_args()
print(json.dumps(prepare(args), sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Declares allowed tools, Needs Python
The author says: Requires the AnomalyGenNext 1.1 container, Pillow, and its model checkpoints.
Security
What our scanner noticed· Medium risk
- Large base64-encoded blobs
- Scripts that your agent may run
This skill includes scripts that your agent may run. Read them before you use it.
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- NVIDIA Corporation
- Publisher
- NVIDIA (Official)
- Source
- NVIDIA/skills · skills/tao-finetune-anomalygennext · commit fce3cca
- Licence
- Apache License 2.0 (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE-APACHE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by NVIDIA Corporation or NVIDIA.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download tao-finetune-anomalygennext.zip