From 7f923301c2a16f8082fd01ae258c755a110cad19 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Thu, 5 Mar 2026 11:08:08 +0100 Subject: [PATCH 01/37] Add docs & notebooks staleness check tool Introduce a new CLI tool (.github/tools/docs_and_notebooks_check.py) to scan notebooks and Markdown docs for staleness and verification metadata under the 'deeplabcut' namespace. Adds a default YAML config (.github/tools/docs_and_notebooks_report_config.yml), a README for the tool (.github/tools/docs_and_notebooks_tool_README.md), and an output ignore entry in .gitignore. The tool uses pydantic schemas, computes last_git_updated from git history, reads/writes notebook top-level metadata and Markdown frontmatter (idempotent updates), and supports report/check/update modes. Outputs machine- and human-readable reports (nb_docs_status.json / .md). Requires pydantic and PyYAML; designed to be safe-by-default for CI (read-only unless --write is passed). --- .github/tools/docs_and_notebooks_check.py | 641 ++++++++++++++++++ .../docs_and_notebooks_report_config.yml | 25 + .../tools/docs_and_notebooks_tool_README.md | 110 +++ .gitignore | 3 + 4 files changed, 779 insertions(+) create mode 100644 .github/tools/docs_and_notebooks_check.py create mode 100644 .github/tools/docs_and_notebooks_report_config.yml create mode 100644 .github/tools/docs_and_notebooks_tool_README.md diff --git a/.github/tools/docs_and_notebooks_check.py b/.github/tools/docs_and_notebooks_check.py new file mode 100644 index 0000000000..dcce9ec4e3 --- /dev/null +++ b/.github/tools/docs_and_notebooks_check.py @@ -0,0 +1,641 @@ +"""DeepLabCut docs & notebooks automated checks tool. + +Goals +----- +- SAFE by default: read-only operations in CI (report/check). +- Idempotent updates (update mode) that only touch: + * Notebook-level metadata for .ipynb (never cells/outputs) + * YAML frontmatter for .md docs (optional) +- Uses pydantic schemas with explicit schema_version for validation. + +Terminology +----------- +last_git_updated + Computed from git history (last commit touching the file). + +last_verified + Human-controlled date indicating the file was verified to work/be accurate. + +verified_for + Human-controlled string, typically the project version (e.g. 3.0.0rc13). + +tier + Optional classification (left unset by default; do not auto-populate). + +Usage modes +----------- +Report (read-only): + python .github/tools/docs_and_notebooks_check.py report + +Check (read-only; may fail based on config allowlists): + python .github/tools/docs_and_notebooks_check.py check + +Update git-updated fields (write mode; requires --write): + python .github/tools/docs_and_notebooks_check.py update --write --only-git-date + +Update verification fields for selected targets (write mode): + python .github/tools/docs_and_notebooks_check.py update --write --targets docs/page.md \ + --set-last-verified today --set-verified-for 3.0.0rc13 + +Configuration +------------- +Uses .github/tools/staleness_config.yml by default. + +Outputs +------- +- nb_docs_status.json: machine-readable report +- nb_docs_status.md: human-readable summary + +Notes for CI +------------ +- Ensure actions/checkout uses fetch-depth: 0 (or sufficiently deep), + otherwise git log may not see history. +- Requires pydantic and PyYAML to be installed in the environment. + Recommended : install in CI job directly (pip install pydantic pyyaml) rather than adding to requirements, since these are only needed for this tool. +""" +# .github/tools/docs_and_notebooks_check.py +from __future__ import annotations + +import argparse +import fnmatch +import json +import os +import re +import subprocess +from datetime import date, datetime, timezone +from pathlib import Path +from typing import Any, Dict, List, Optional, Sequence, Tuple + +try: + import yaml # PyYAML +except Exception: + yaml = None + +try: + from pydantic import BaseModel, Field, ValidationError, ConfigDict +except Exception: # pragma: no cover + raise RuntimeError("Pydantic is required to run this script") + +SCHEMA_VERSION = 1 +DLC_NAMESPACE = "deeplabcut" +OUTPUT_FILENAME = "nb_docs_status" +DEFAULT_CFG = "docs_and_notebooks_report_config.yml" + +# ----------------------------- +# Pydantic schemas +# ----------------------------- + +class DLCMeta(BaseModel): + """Metadata embedded in files under the `deeplabcut` namespace.""" + model_config = ConfigDict(extra="allow") + + last_git_updated: Optional[date] = None + last_verified: Optional[date] = None + verified_for: Optional[str] = None + tier: Optional[str] = None + ignore: bool = False + notes: Optional[str] = None + + +class ScanConfig(BaseModel): + include: List[str] = Field(default_factory=list) + exclude: List[str] = Field(default_factory=list) + + +class PolicyConfig(BaseModel): + warn_if_git_older_than_days: int = 365 + warn_if_verified_older_than_days: int = 365 + missing_last_verified_is_warning: bool = True + + # Allowlists for strict checks (start empty; ratchet later) + require_metadata: List[str] = Field(default_factory=list) + require_recent_verification: List[str] = Field(default_factory=list) + + +class ToolConfig(BaseModel): + version: int = 1 + scan: ScanConfig + policy: PolicyConfig + + +class FileRecord(BaseModel): + path: str + kind: str # ipynb | md | other + + # Computed from git + last_git_updated: Optional[date] = None + + # Read from file metadata/frontmatter + meta: Optional[DLCMeta] = None + + # Derived + days_since_git_update: Optional[int] = None + days_since_verified: Optional[int] = None + + warnings: List[str] = Field(default_factory=list) + errors: List[str] = Field(default_factory=list) + + # If update mode would change file + would_change: bool = False + + +class Report(BaseModel): + schema_version: int = SCHEMA_VERSION + generated_at: datetime + repo_root: str + config_path: str + + totals: Dict[str, int] + records: List[FileRecord] + + +# ----------------------------- +# Helpers +# ----------------------------- + +def _iso_today() -> date: + return datetime.now(timezone.utc).date() + + +def _run_git(args: Sequence[str], cwd: Path) -> Tuple[int, str, str]: + p = subprocess.run( + ["git", *args], + cwd=str(cwd), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + return p.returncode, p.stdout.strip(), p.stderr.strip() + + +def find_repo_root(start: Path) -> Path: + cur = start.resolve() + for _ in range(50): + if (cur / ".git").exists(): + return cur + if cur.parent == cur: + break + cur = cur.parent + code, out, _err = _run_git(["rev-parse", "--show-toplevel"], cwd=start) + if code == 0 and out: + return Path(out).resolve() + raise RuntimeError("Could not locate repository root") + + +def glob_paths(repo_root: Path, patterns: List[str]) -> List[Path]: + results: List[Path] = [] + for pat in patterns: + results.extend(repo_root.glob(pat)) + return sorted({p.resolve() for p in results if p.is_file()}) + + +def is_excluded(rel_path: str, exclude_patterns: List[str]) -> bool: + return any(fnmatch.fnmatch(rel_path, pat) for pat in exclude_patterns) + + +def file_kind(path: Path) -> str: + s = path.suffix.lower() + if s == ".ipynb": + return "ipynb" + if s in {".md", ".markdown"}: + return "md" + return "other" + + +def git_last_updated(repo_root: Path, rel_path: str) -> Optional[date]: + code, out, _err = _run_git(["log", "-1", "--format=%cI", "--", rel_path], cwd=repo_root) + if code != 0 or not out: + return None + try: + return datetime.fromisoformat(out).date() + except Exception: + return None + + +FRONTMATTER_RE = re.compile(r"^---\s*$") + + +def read_md_frontmatter(text: str) -> Tuple[Optional[dict], str]: + lines = text.splitlines(keepends=True) + if not lines or not FRONTMATTER_RE.match(lines[0]): + return None, text + + end_idx = None + for i in range(1, min(len(lines), 5000)): + if FRONTMATTER_RE.match(lines[i]): + end_idx = i + break + if end_idx is None: + return None, text + + fm_text = "".join(lines[1:end_idx]) + body = "".join(lines[end_idx + 1 :]) + + if yaml is None: + raise RuntimeError("PyYAML is required to parse Markdown frontmatter") + + fm = yaml.safe_load(fm_text) if fm_text.strip() else {} + if not isinstance(fm, dict): + return None, text + return fm, body + + +def dump_md_frontmatter(frontmatter: dict, body: str) -> str: + if yaml is None: + raise RuntimeError("PyYAML is required to write Markdown frontmatter") + fm_text = yaml.safe_dump(frontmatter, sort_keys=False, allow_unicode=True) + return "---\n" + fm_text + "---\n" + body.lstrip("\n") + + +def read_ipynb_meta(path: Path) -> Tuple[dict, dict]: + nb = json.loads(path.read_text(encoding="utf-8")) + meta = nb.get("metadata", {}) if isinstance(nb, dict) else {} + dlc_meta = meta.get(DLC_NAMESPACE, {}) if isinstance(meta, dict) else {} + if not isinstance(dlc_meta, dict): + dlc_meta = {} + return nb, dlc_meta + + +def write_ipynb_meta(path: Path, nb: dict) -> None: + # Only rewriting the file in update mode. No cell/output manipulation. + path.write_text(json.dumps(nb, ensure_ascii=False, indent=1) + "\n", encoding="utf-8") + + +def parse_dlc_meta(raw: Any) -> Optional[DLCMeta]: + if raw is None: + return None + if isinstance(raw, dict): + try: + return DLCMeta.model_validate(raw) # pydantic v2 + except AttributeError: + return DLCMeta.parse_obj(raw) # pydantic v1 + except ValidationError: + return None + return None + + +def meta_to_jsonable(meta: DLCMeta) -> dict: + # Exclude None fields; do NOT set tier by default. + try: + return meta.model_dump(exclude_none=True) # pydantic v2 + except AttributeError: + return meta.dict(exclude_none=True) # pydantic v1 + + +def compute_days_since(d: Optional[date], today: date) -> Optional[int]: + return None if d is None else (today - d).days + + +def match_allowlist(rel_path: str, allowlist: List[str]) -> bool: + # Support exact matches or glob patterns + return any(pat == rel_path or fnmatch.fnmatch(rel_path, pat) for pat in allowlist) + + +# ----------------------------- +# Core scanning +# ----------------------------- + +def load_config(config_path: Path) -> ToolConfig: + if yaml is None: + raise RuntimeError("PyYAML is required (pip install pyyaml)") + raw = yaml.safe_load(config_path.read_text(encoding="utf-8")) + try: + return ToolConfig.model_validate(raw) # pydantic v2 + except AttributeError: + return ToolConfig.parse_obj(raw) # pydantic v1 + + +def scan_files(repo_root: Path, cfg: ToolConfig) -> List[FileRecord]: + today = _iso_today() + paths = glob_paths(repo_root, cfg.scan.include) + records: List[FileRecord] = [] + + for p in paths: + rel = str(p.resolve().relative_to(repo_root)).replace(os.sep, "/") + if is_excluded(rel, cfg.scan.exclude): + continue + + kind = file_kind(p) + rec = FileRecord(path=rel, kind=kind) + + rec.last_git_updated = git_last_updated(repo_root, rel) + rec.days_since_git_update = compute_days_since(rec.last_git_updated, today) + + try: + if kind == "ipynb": + _nb, raw_meta = read_ipynb_meta(p) + rec.meta = parse_dlc_meta(raw_meta) + elif kind == "md": + text = p.read_text(encoding="utf-8") + fm, _body = read_md_frontmatter(text) + raw = (fm or {}).get(DLC_NAMESPACE) + rec.meta = parse_dlc_meta(raw) + else: + rec.meta = None + except Exception as e: + rec.errors.append(f"metadata_read_failed: {e}") + + if rec.meta and rec.meta.ignore: + records.append(rec) + continue + + last_verified = rec.meta.last_verified if rec.meta else None + rec.days_since_verified = compute_days_since(last_verified, today) + + pol = cfg.policy + + if rec.days_since_git_update is not None and rec.days_since_git_update > pol.warn_if_git_older_than_days: + rec.warnings.append(f"git_stale>{pol.warn_if_git_older_than_days}d") + + if last_verified is None and pol.missing_last_verified_is_warning: + rec.warnings.append("missing_last_verified") + elif rec.days_since_verified is not None and rec.days_since_verified > pol.warn_if_verified_older_than_days: + rec.warnings.append(f"verified_stale>{pol.warn_if_verified_older_than_days}d") + + if kind in {"ipynb", "md"} and rec.meta is None: + rec.warnings.append("missing_metadata") + + records.append(rec) + + return records + + +# ----------------------------- +# Update mode +# ----------------------------- + +def update_files( + repo_root: Path, + cfg: ToolConfig, + targets: Optional[List[str]], + write: bool, + only_git_date: bool, + set_last_verified: Optional[date], + set_verified_for: Optional[str], +) -> List[FileRecord]: + today = _iso_today() + records = scan_files(repo_root, cfg) + target_set = set(t.replace(os.sep, "/") for t in targets) if targets else None + + for rec in records: + if rec.kind not in {"ipynb", "md"}: + continue + if rec.meta and rec.meta.ignore: + continue + if target_set is not None and rec.path not in target_set: + continue + + meta = rec.meta or DLCMeta() + + # Always update last_git_updated to computed value (if available) + if rec.last_git_updated is not None: + meta.last_git_updated = rec.last_git_updated + + if not only_git_date: + if set_last_verified is not None: + meta.last_verified = set_last_verified + if set_verified_for is not None: + meta.verified_for = set_verified_for + + desired = meta_to_jsonable(meta) + abs_path = repo_root / rec.path + changed = False + + if rec.kind == "ipynb": + nb, _raw = read_ipynb_meta(abs_path) + nb_meta = nb.setdefault("metadata", {}) + prev = nb_meta.get(DLC_NAMESPACE, {}) + if not isinstance(prev, dict): + prev = {} + merged = dict(prev) + merged.update(desired) + if merged != prev: + nb_meta[DLC_NAMESPACE] = merged + changed = True + if write: + write_ipynb_meta(abs_path, nb) + + elif rec.kind == "md": + text = abs_path.read_text(encoding="utf-8") + fm, body = read_md_frontmatter(text) + fm = fm or {} + prev = fm.get(DLC_NAMESPACE, {}) + if not isinstance(prev, dict): + prev = {} + merged = dict(prev) + merged.update(desired) + if merged != prev: + fm[DLC_NAMESPACE] = merged + changed = True + if write: + abs_path.write_text(dump_md_frontmatter(fm, body), encoding="utf-8") + + rec.would_change = changed + rec.meta = meta + rec.days_since_verified = compute_days_since(meta.last_verified, today) + + return records + + +# ----------------------------- +# Output formatting +# ----------------------------- + +def summarize(records: List[FileRecord]) -> Dict[str, int]: + return { + "files": len(records), + "warnings": sum(1 for r in records if r.warnings), + "errors": sum(1 for r in records if r.errors), + "missing_metadata": sum(1 for r in records if "missing_metadata" in r.warnings), + "missing_last_verified": sum(1 for r in records if "missing_last_verified" in r.warnings), + "git_stale": sum(1 for r in records if any(w.startswith("git_stale") for w in r.warnings)), + "verified_stale": sum(1 for r in records if any(w.startswith("verified_stale") for w in r.warnings)), + } + + +def to_markdown(report: Report, cfg: ToolConfig) -> str: + pol = cfg.policy + t = report.totals + lines: List[str] = [] + + lines.append("# DeepLabCut staleness report\n") + lines.append(f"Generated: {report.generated_at.isoformat()}\n") + lines.append(f"Schema: v{report.schema_version}\n\n") + + lines.append("## Summary\n") + lines.append(f"- Files scanned: **{t['files']}**\n") + lines.append(f"- Files with warnings: **{t['warnings']}**\n") + lines.append(f"- Files with errors: **{t['errors']}**\n") + lines.append(f"- Missing metadata: **{t['missing_metadata']}**\n") + lines.append(f"- Missing last_verified: **{t['missing_last_verified']}**\n") + lines.append(f"- Git-stale (> {pol.warn_if_git_older_than_days}d): **{t['git_stale']}**\n") + lines.append(f"- Verification-stale (> {pol.warn_if_verified_older_than_days}d): **{t['verified_stale']}**\n\n") + + def fmt_date(d: Optional[date]) -> str: + return d.isoformat() if d else "-" + + warn_recs = [r for r in report.records if r.warnings and not (r.meta and r.meta.ignore)] + warn_recs.sort(key=lambda r: (-(r.days_since_verified or -1), -(r.days_since_git_update or -1), r.path)) + + if warn_recs: + lines.append("## Warnings\n") + for r in warn_recs: + meta = r.meta + lines.append(f"- **{r.path}** ({r.kind})\n") + lines.append(f" - last_git_updated: {fmt_date(r.last_git_updated)} " + f"(days: {r.days_since_git_update if r.days_since_git_update is not None else '-'})\n") + lv = meta.last_verified if meta else None + lines.append(f" - last_verified: {fmt_date(lv)} " + f"(days: {r.days_since_verified if r.days_since_verified is not None else '-'})\n") + if meta and meta.verified_for: + lines.append(f" - verified_for: {meta.verified_for}\n") + if meta and meta.tier: + lines.append(f" - tier: {meta.tier}\n") + lines.append(f" - warnings: {', '.join(r.warnings)}\n") + if r.errors: + lines.append(f" - errors: {', '.join(r.errors)}\n") + lines.append("\n") + + err_recs = [r for r in report.records if r.errors] + if err_recs: + lines.append("## Errors\n") + for r in err_recs: + lines.append(f"- **{r.path}**: {', '.join(r.errors)}\n") + lines.append("\n") + + lines.append("## Notes\n") + lines.append("- 'Out of date' does not necessarily mean 'broken'. Use this as a triage signal.\n") + lines.append("- last_git_updated is computed from git history. last_verified is human-controlled.\n\n") + return "".join(lines) + + +def write_outputs(report: Report, cfg: ToolConfig, out_dir: Path) -> Tuple[Path, Path]: + out_dir.mkdir(parents=True, exist_ok=True) + json_path = out_dir / f"{OUTPUT_FILENAME}.json" + md_path = out_dir / f"{OUTPUT_FILENAME}.md" + + try: + payload = report.model_dump(mode="json") # pydantic v2 + except AttributeError: + payload = json.loads(report.json()) # pydantic v1 + + json_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + md_path.write_text(to_markdown(report, cfg), encoding="utf-8") + return json_path, md_path + + +# ----------------------------- +# Check enforcement +# ----------------------------- + +def enforce(cfg: ToolConfig, records: List[FileRecord]) -> List[str]: + pol = cfg.policy + violations: List[str] = [] + today = _iso_today() + + for r in records: + if r.meta and r.meta.ignore: + continue + if r.kind not in {"ipynb", "md"}: + continue + + if match_allowlist(r.path, pol.require_metadata) and r.meta is None: + violations.append(f"{r.path}: missing metadata") + + if match_allowlist(r.path, pol.require_recent_verification): + lv = r.meta.last_verified if r.meta else None + if lv is None: + violations.append(f"{r.path}: missing last_verified") + else: + days = (today - lv).days + if days > pol.warn_if_verified_older_than_days: + violations.append(f"{r.path}: last_verified is {days}d old " + f"(> {pol.warn_if_verified_older_than_days}d)") + + return violations + + +# ----------------------------- +# CLI +# ----------------------------- + +def parse_date_token(token: str) -> date: + token = token.strip().lower() + if token in {"today", "now"}: + return _iso_today() + return date.fromisoformat(token) + + +def main(argv: Optional[Sequence[str]] = None) -> int: + parser = argparse.ArgumentParser(description="DeepLabCut checks tool (docs + notebooks)") + parser.add_argument("--config", default=DEFAULT_CFG, help="Path to YAML config file") + parser.add_argument("--out-dir", default="tmp/docs_notebooks_status", help="Directory to write outputs") + + sub = parser.add_subparsers(dest="cmd", required=True) + sub.add_parser("report", help="Generate staleness report (read-only)") + sub.add_parser("check", help="Run policy checks (read-only; may exit non-zero)") + + up = sub.add_parser("update", help="Update metadata/frontmatter (write mode requires --write)") + up.add_argument("--write", action="store_true", help="Actually write changes (otherwise dry-run)") + up.add_argument("--only-git-date", action="store_true", help="Only update last_git_updated") + up.add_argument("--targets", nargs="*", help="Optional list of relative file paths to update") + up.add_argument("--set-last-verified", default=None, help="YYYY-MM-DD or 'today'") + up.add_argument("--set-verified-for", default=None, help="String like 3.0.0rc13") + + args = parser.parse_args(list(argv) if argv is not None else None) + + config_path = Path(args.config) + repo_root = find_repo_root(Path.cwd()) + cfg = load_config(config_path) + out_dir = Path(args.out_dir) + + if args.cmd in {"report", "check"}: + records = scan_files(repo_root, cfg) + else: + lv = parse_date_token(args.set_last_verified) if args.set_last_verified else None + records = update_files( + repo_root, + cfg, + targets=args.targets, + write=bool(args.write), + only_git_date=bool(args.only_git_date), + set_last_verified=lv, + set_verified_for=args.set_verified_for, + ) + + report = Report( + generated_at=datetime.now(timezone.utc), + repo_root=str(repo_root), + config_path=str(config_path), + totals=summarize(records), + records=records, + ) + + json_path, md_path = write_outputs(report, cfg, out_dir) + + # Emit GitHub Actions job summary if available + step_summary = os.environ.get("GITHUB_STEP_SUMMARY") + if step_summary and md_path.exists(): + try: + content = md_path.read_text(encoding="utf-8") + snippet = "\n".join(content.splitlines()[:220]) + "\n" + Path(step_summary).write_text(snippet, encoding="utf-8") + except Exception: + pass + + if args.cmd == "check": + violations = enforce(cfg, records) + if violations: + print("Policy violations:") + for v in violations: + print(f"- {v}") + return 2 + + # Non-zero if metadata parsing errors occurred + if any(r.errors for r in records): + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git a/.github/tools/docs_and_notebooks_report_config.yml b/.github/tools/docs_and_notebooks_report_config.yml new file mode 100644 index 0000000000..4d351d912d --- /dev/null +++ b/.github/tools/docs_and_notebooks_report_config.yml @@ -0,0 +1,25 @@ +version: 1 + +scan: + include: + - "examples/COLAB/**/*.ipynb" + - "examples/JUPYTER/**/*.ipynb" + - "docs/**/*.md" + - "docs/**/*.ipynb" # if notebooks get added to docs (Jupyter Book supports this) + exclude: + - "**/.ipynb_checkpoints/**" + - "**/_build/**" + - "**/build/**" + +policy: + warn_if_git_older_than_days: 365 + warn_if_verified_older_than_days: 365 + missing_last_verified_is_warning: true + + # Ratchet lists for tiered verification requirements. + # Tiers have to be determined, and crucial targets identified. + # Then specific policies can be set for each tier, + # e.g. requiring more recent verification for higher tiers, + # or requiring verification for more recent versions. + require_metadata: [] + require_recent_verification: [] diff --git a/.github/tools/docs_and_notebooks_tool_README.md b/.github/tools/docs_and_notebooks_tool_README.md new file mode 100644 index 0000000000..5c9bb97e14 --- /dev/null +++ b/.github/tools/docs_and_notebooks_tool_README.md @@ -0,0 +1,110 @@ +# Docs & Notebooks Checks Tool + +This tool scans DeepLabCut notebooks and documentation pages and reports two independent signals: + +- **last_git_updated**: last commit date touching a file (computed from git history) +- **last_verified**: a human-controlled date indicating the content was verified to work/be accurate + +It is designed to be **safe by default** (read-only in CI), and **future-proof** via versioned +pydantic schemas. + +## Files scanned + +Default patterns (see `tools/staleness_config.yml`): + +- `examples/COLAB/**/*.ipynb` +- `examples/JUPYTER/**/*.ipynb` +- `docs/**/*.md` +- `docs/**/*.ipynb` (if notebooks get added to docs; Jupyter Book supports this) + +## Metadata locations + +### Notebooks (`.ipynb`) + +The tool only touches the notebook **top-level JSON metadata** under the `deeplabcut` namespace. + +> [!IMPORTANT] +> It never edits cells, outputs, or execution counts. + +Example (excerpt): + +```json +{ + "metadata": { + "deeplabcut": { + "last_git_updated": "2026-03-05", + "last_verified": "2026-02-20", + "verified_for": "3.0.0rc13", + "ignore": false + } + } +} +``` + + +> [!NOTE] +> **Tier** is intentionally optional and is not auto-populated. + +### Markdown files (`.md`) + +The tool reads/writes YAML frontmatter at the top of the file: + +``` +--- +deeplabcut: + last_git_updated: 2026-03-05 + last_verified: 2026-02-20 + verified_for: 3.0.0rc13 + ignore: false +--- +``` +If a doc page has no frontmatter, the tool can still report potential staleness (read-only). + +## Usage + +### Report (read-only) + +``` +python .github/tools/docs_and_notebooks_check.py report +``` + +Writes: + +- `tools/staleness_out/staleness.json` +- `tools/staleness_out/staleness.md` + +### Check (read-only, may fail) + +`python .github/tools/docs_and_notebooks_check.py check` +check only fails based on allowlists in tools/staleness_config.yml: + +require_metadata +require_recent_verification + +By default these are empty, so CI will not fail. + +### Update (write mode) + +> [!WARNING] +> This mode updates files in-place and should be used with caution. It is intended to be run manually by maintainers, not in CI. + + +Update only last_git_updated for all scanned files: +`python tools/staleness.py update --write --only-git-date` + +Set verification metadata for specific target files: + +``` +python tools/staleness.py update --write --targets examples/JUPYTER/foo.ipynb \ + --set-last-verified today --set-verified-for 3.0.0rc13 +``` + +### CI integration + +Add a CI step that runs: +`python .github/tools/docs_and_notebooks_check.py report` +and uploads the outputs as artifacts. +Optionally run check once allowlists are populated. + +Important: Ensure actions/checkout uses a non-shallow clone (fetch-depth: 0) so git log +can compute last_git_updated reliably. \ No newline at end of file diff --git a/.gitignore b/.gitignore index ad9f192703..e628be95c5 100644 --- a/.gitignore +++ b/.gitignore @@ -138,3 +138,6 @@ ENV/ # mypy .mypy_cache/ + +# Automated docs checks +**/tmp/docs_notebooks_status/ \ No newline at end of file From 5c60b01e68b8417da1a9747769331cc699487531 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Thu, 5 Mar 2026 11:12:35 +0100 Subject: [PATCH 02/37] Add staleness workflow for docs and notebooks Introduce a GitHub Actions workflow to scan docs and notebooks for staleness. The workflow runs on push and PRs to main, checks out full git history, uses Python 3.12, installs pydantic and pyyaml, and runs a read-only staleness report and an optional policy check using .github/tools/docs_and_notebooks_check.py with tools/staleness_config.yml. Results (JSON/MD) are uploaded as the staleness-report artifact. Workflow is limited to content read permissions and has a 10-minute timeout. --- .../workflows/docs_and_notebooks_checks.yml | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/workflows/docs_and_notebooks_checks.yml diff --git a/.github/workflows/docs_and_notebooks_checks.yml b/.github/workflows/docs_and_notebooks_checks.yml new file mode 100644 index 0000000000..d79daa6d3a --- /dev/null +++ b/.github/workflows/docs_and_notebooks_checks.yml @@ -0,0 +1,52 @@ +name: Staleness report (docs + notebooks) + +on: + pull_request: + branches: [main] + push: + branches: [main] + +permissions: + contents: read + +jobs: + staleness: + name: Docs and notebooks scan (read-only) + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout repository (full history for git dates) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install staleness tool dependencies + run: | + python -m pip install --upgrade pip + python -m pip install "pydantic>=2" pyyaml + + - name: Run staleness report (read-only) + run: | + python .github/tools/docs_and_notebooks_check.py report \ + --config .github/tools/docs_and_notebooks_report_config.yml --out-dir tmp/docs_nb_checks + + # Optional: run check mode (will fail only once you populate allowlists in config) + - name: Run staleness policy check (optional gate) + run: | + python .github/tools/docs_and_notebooks_check.py check \ + --config .github/tools/docs_and_notebooks_report_config.yml --out-dir tmp/docs_nb_checks + + - name: Upload staleness artifacts + uses: actions/upload-artifact@v4 + with: + name: staleness-report + path: | + tmp/docs_nb_checks/*.json + tmp/docs_nb_checks/*.md + if-no-files-found: error \ No newline at end of file From 908055665dc3699de7ff9617d0e4a45d8db16277 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Thu, 5 Mar 2026 11:12:46 +0100 Subject: [PATCH 03/37] Use docs_nb_checks as output directory Rename OUTPUT_FILENAME from 'nb_docs_status' to 'docs_nb_checks' and use it for the default --out-dir (tmp/docs_nb_checks). Update the README to show the check command as a fenced code block and clarify allowlist behavior. Update .gitignore to ignore the new tmp/docs_nb_checks path. --- .github/tools/docs_and_notebooks_check.py | 4 ++-- .github/tools/docs_and_notebooks_tool_README.md | 8 +++++--- .gitignore | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/tools/docs_and_notebooks_check.py b/.github/tools/docs_and_notebooks_check.py index dcce9ec4e3..e25de53e51 100644 --- a/.github/tools/docs_and_notebooks_check.py +++ b/.github/tools/docs_and_notebooks_check.py @@ -78,7 +78,7 @@ SCHEMA_VERSION = 1 DLC_NAMESPACE = "deeplabcut" -OUTPUT_FILENAME = "nb_docs_status" +OUTPUT_FILENAME = "docs_nb_checks" DEFAULT_CFG = "docs_and_notebooks_report_config.yml" # ----------------------------- @@ -569,7 +569,7 @@ def parse_date_token(token: str) -> date: def main(argv: Optional[Sequence[str]] = None) -> int: parser = argparse.ArgumentParser(description="DeepLabCut checks tool (docs + notebooks)") parser.add_argument("--config", default=DEFAULT_CFG, help="Path to YAML config file") - parser.add_argument("--out-dir", default="tmp/docs_notebooks_status", help="Directory to write outputs") + parser.add_argument("--out-dir", default=f"tmp/{OUTPUT_FILENAME}", help="Directory to write outputs") sub = parser.add_subparsers(dest="cmd", required=True) sub.add_parser("report", help="Generate staleness report (read-only)") diff --git a/.github/tools/docs_and_notebooks_tool_README.md b/.github/tools/docs_and_notebooks_tool_README.md index 5c9bb97e14..70ab2a027a 100644 --- a/.github/tools/docs_and_notebooks_tool_README.md +++ b/.github/tools/docs_and_notebooks_tool_README.md @@ -75,11 +75,13 @@ Writes: ### Check (read-only, may fail) -`python .github/tools/docs_and_notebooks_check.py check` -check only fails based on allowlists in tools/staleness_config.yml: +Check only fails based on allowlists in tools/staleness_config.yml: -require_metadata +``` +python .github/tools/docs_and_notebooks_check.py check \ +require_metadata \ require_recent_verification +``` By default these are empty, so CI will not fail. diff --git a/.gitignore b/.gitignore index e628be95c5..0f663ced43 100644 --- a/.gitignore +++ b/.gitignore @@ -140,4 +140,4 @@ ENV/ .mypy_cache/ # Automated docs checks -**/tmp/docs_notebooks_status/ \ No newline at end of file +**/tmp/docs_nb_checks/ \ No newline at end of file From eea323e657f94b7edd77500ea9fd1e353a5676fc Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Thu, 5 Mar 2026 11:15:07 +0100 Subject: [PATCH 04/37] Make meta_to_jsonable JSON-safe for dates Ensure DLC metadata is JSON-serializable by converting date/datetime fields to ISO strings and preserving exclude_none behavior. Uses pydantic v2 API (model_dump(mode="json", exclude_none=True)) and falls back to pydantic v1 via json.loads(meta.json(...)). Adds a docstring and clarifying comments. This prevents json.dumps from failing when writing .ipynb files and keeps compatibility across pydantic versions. --- .github/tools/docs_and_notebooks_check.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/tools/docs_and_notebooks_check.py b/.github/tools/docs_and_notebooks_check.py index e25de53e51..99d72dfa44 100644 --- a/.github/tools/docs_and_notebooks_check.py +++ b/.github/tools/docs_and_notebooks_check.py @@ -275,11 +275,16 @@ def parse_dlc_meta(raw: Any) -> Optional[DLCMeta]: def meta_to_jsonable(meta: DLCMeta) -> dict: - # Exclude None fields; do NOT set tier by default. + """ + Return JSON-serializable metadata (dates become ISO strings). + This prevents json.dumps() from failing when writing .ipynb files. + """ try: - return meta.model_dump(exclude_none=True) # pydantic v2 + # Pydantic v2: mode='json' converts date/datetime into ISO strings + return meta.model_dump(mode="json", exclude_none=True) except AttributeError: - return meta.dict(exclude_none=True) # pydantic v1 + # Pydantic v1: meta.json() encodes dates; parse back into dict + return json.loads(meta.json(exclude_none=True)) def compute_days_since(d: Optional[date], today: date) -> Optional[int]: From 34848125e0cd17c112e8d254c36117bd5340ec3f Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Thu, 5 Mar 2026 11:36:11 +0100 Subject: [PATCH 05/37] Add nbformat and notebook normalization checks Update docs-and-notebooks tool to use nbformat for reading/writing notebooks, validate .ipynb files, and detect whether notebooks are normalized. Add notebook_is_normalized helper and ensure write_ipynb_meta uses nbformat.writes/validate. Introduce a new policy field require_notebook_normalized (and add it to the report config defaults) and enforce it to emit violations when notebooks are not normalized. Also update CI job to install nbformat and pin pydantic, and update the script header notes to list the new dependency. These changes let CI detect invalid or non-normalized notebooks and reduce formatting churn when normalizing files. --- .github/tools/docs_and_notebooks_check.py | 65 ++++++++++++++++--- .../docs_and_notebooks_report_config.yml | 1 + .../workflows/docs_and_notebooks_checks.yml | 2 +- 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/.github/tools/docs_and_notebooks_check.py b/.github/tools/docs_and_notebooks_check.py index 99d72dfa44..1884d8a58c 100644 --- a/.github/tools/docs_and_notebooks_check.py +++ b/.github/tools/docs_and_notebooks_check.py @@ -50,8 +50,12 @@ ------------ - Ensure actions/checkout uses fetch-depth: 0 (or sufficiently deep), otherwise git log may not see history. -- Requires pydantic and PyYAML to be installed in the environment. - Recommended : install in CI job directly (pip install pydantic pyyaml) rather than adding to requirements, since these are only needed for this tool. +- Requires: + - pydantic + - PyYAML + - nbformat + to be installed in the environment. + Recommended : install in CI job directly (pip install pydantic pyyaml nbformat) rather than adding to requirements, since these are only needed for this tool. """ # .github/tools/docs_and_notebooks_check.py from __future__ import annotations @@ -75,6 +79,11 @@ from pydantic import BaseModel, Field, ValidationError, ConfigDict except Exception: # pragma: no cover raise RuntimeError("Pydantic is required to run this script") +try: + import nbformat + from nbformat.validator import NotebookValidationError +except Exception: + raise RuntimeError("nbformat is required to read/write .ipynb files") SCHEMA_VERSION = 1 DLC_NAMESPACE = "deeplabcut" @@ -110,6 +119,8 @@ class PolicyConfig(BaseModel): # Allowlists for strict checks (start empty; ratchet later) require_metadata: List[str] = Field(default_factory=list) require_recent_verification: List[str] = Field(default_factory=list) + + require_notebook_normalized: List[str] = Field(default_factory=list) class ToolConfig(BaseModel): @@ -247,19 +258,37 @@ def dump_md_frontmatter(frontmatter: dict, body: str) -> str: return "---\n" + fm_text + "---\n" + body.lstrip("\n") -def read_ipynb_meta(path: Path) -> Tuple[dict, dict]: - nb = json.loads(path.read_text(encoding="utf-8")) - meta = nb.get("metadata", {}) if isinstance(nb, dict) else {} - dlc_meta = meta.get(DLC_NAMESPACE, {}) if isinstance(meta, dict) else {} +def read_ipynb_meta(path: Path) -> tuple[Any, dict]: + """ + Read a notebook using nbformat. Returns (notebook_node, deeplabcut_meta_dict). + """ + nb = nbformat.read(str(path), as_version=4) + + meta = getattr(nb, "metadata", {}) or {} + dlc_meta = meta.get(DLC_NAMESPACE, {}) if not isinstance(dlc_meta, dict): dlc_meta = {} return nb, dlc_meta +def notebook_is_normalized(path: Path, nb: Any) -> bool: + original = path.read_text(encoding="utf-8") + normalized = nbformat.writes(nb, version=4, indent=2, ensure_ascii=False) + "\n" + return original == normalized -def write_ipynb_meta(path: Path, nb: dict) -> None: - # Only rewriting the file in update mode. No cell/output manipulation. - path.write_text(json.dumps(nb, ensure_ascii=False, indent=1) + "\n", encoding="utf-8") +def write_ipynb_meta(path: Path, nb: Any) -> None: + """ + Write a notebook using nbformat. + Note: nbformat writes JSON in a canonical form; it *will* rewrite the file, + so expect diffs if the notebook wasn't previously normalized to the same style. + """ + # Validate before writing (optional but recommended) + nbformat.validate(nb) + + # Use a stable indentation to reduce churn (choose 2 if your repo tends that way) + text = nbformat.writes(nb, version=4, indent=2, ensure_ascii=False) + + path.write_text(text + "\n", encoding="utf-8") def parse_dlc_meta(raw: Any) -> Optional[DLCMeta]: if raw is None: @@ -328,7 +357,19 @@ def scan_files(repo_root: Path, cfg: ToolConfig) -> List[FileRecord]: try: if kind == "ipynb": - _nb, raw_meta = read_ipynb_meta(p) + nb, raw_meta = read_ipynb_meta(p) + try: + nbformat.validate(nb) + except NotebookValidationError as e: + rec.errors.append(f"nbformat_invalid: {e}") + + try: + if not notebook_is_normalized(p, nb): + rec.warnings.append("notebook_not_normalized") + except Exception as e: + # Don't crash scan if a file has encoding/IO oddities + rec.errors.append(f"notebook_normalization_check_failed: {e}") + rec.meta = parse_dlc_meta(raw_meta) elif kind == "md": text = p.read_text(encoding="utf-8") @@ -557,6 +598,10 @@ def enforce(cfg: ToolConfig, records: List[FileRecord]) -> List[str]: violations.append(f"{r.path}: last_verified is {days}d old " f"(> {pol.warn_if_verified_older_than_days}d)") + if r.kind == "ipynb" and match_allowlist(r.path, pol.require_notebook_normalized): + if "notebook_not_normalized" in (r.warnings or []): + violations.append(f"{r.path}: notebook is not normalized (run update/format)") + return violations diff --git a/.github/tools/docs_and_notebooks_report_config.yml b/.github/tools/docs_and_notebooks_report_config.yml index 4d351d912d..28abaacabf 100644 --- a/.github/tools/docs_and_notebooks_report_config.yml +++ b/.github/tools/docs_and_notebooks_report_config.yml @@ -23,3 +23,4 @@ policy: # or requiring verification for more recent versions. require_metadata: [] require_recent_verification: [] + require_notebook_normalized: [] diff --git a/.github/workflows/docs_and_notebooks_checks.yml b/.github/workflows/docs_and_notebooks_checks.yml index d79daa6d3a..acce426803 100644 --- a/.github/workflows/docs_and_notebooks_checks.yml +++ b/.github/workflows/docs_and_notebooks_checks.yml @@ -29,7 +29,7 @@ jobs: - name: Install staleness tool dependencies run: | python -m pip install --upgrade pip - python -m pip install "pydantic>=2" pyyaml + python -m pip install "pydantic>=2,<3" pyyaml "nbformat>=5" - name: Run staleness report (read-only) run: | From 3728e890817949982303e3e5e4f26bb093008a10 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Thu, 5 Mar 2026 11:36:27 +0100 Subject: [PATCH 06/37] Add local pre-commit hook for docs/notebooks Add a local pre-commit hook 'dlc-docs-notebooks-check' that runs .github/tools/docs_and_notebooks_check.py to check DLC docs and notebooks for staleness, validate nbformat, and perform normalization. The hook targets Jupyter and Markdown files, passes filenames to the script, and declares additional dependencies (pydantic>=2,<3, pyyaml, nbformat>=5). --- .pre-commit-config.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6623a8593e..a5e0e1aa15 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,3 +25,16 @@ repos: hooks: - id: black language_version: python3 + - repo: local + hooks: + - id: dlc-docs-notebooks-check + name: DLC docs+notebooks staleness/check + nbformat validate + normalization + entry: python .github/tools/docs_and_notebooks_check.py check --config .github/tools/docs_and_notebooks_report_config.yml --targets + language: python + pass_filenames: true + types_or: [jupyter, markdown] + additional_dependencies: + - "pydantic>=2,<3" + - "pyyaml" + - "nbformat>=5" + From af816c574509096cb0015670471ff720c522e4f5 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Thu, 5 Mar 2026 14:38:58 +0100 Subject: [PATCH 07/37] Support targeted scans and use repo config path Use docs_and_notebooks_report_config.yml as the default config and resolve it relative to the script. Rename machine/human report outputs to docs_nb_checks.{json,md}. Add an optional --targets argument to the report and check subcommands; scan_files now accepts a targets list and filters scanned paths to only those targets. Make --config default a string path and adjust error-exit logic so parsing errors don't cause a non-zero exit in report mode. Minor doc/formatting tweaks. --- .github/tools/docs_and_notebooks_check.py | 46 +++++++++++++++-------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/.github/tools/docs_and_notebooks_check.py b/.github/tools/docs_and_notebooks_check.py index 1884d8a58c..e3ac866d1a 100644 --- a/.github/tools/docs_and_notebooks_check.py +++ b/.github/tools/docs_and_notebooks_check.py @@ -39,12 +39,12 @@ Configuration ------------- -Uses .github/tools/staleness_config.yml by default. +Uses .github/tools/docs_and_notebooks_report_config.yml by default. -Outputs -------- -- nb_docs_status.json: machine-readable report -- nb_docs_status.md: human-readable summary +Outputs +------- +- docs_nb_checks.json: machine-readable report +- docs_nb_checks.md: human-readable summary Notes for CI ------------ @@ -88,8 +88,8 @@ SCHEMA_VERSION = 1 DLC_NAMESPACE = "deeplabcut" OUTPUT_FILENAME = "docs_nb_checks" -DEFAULT_CFG = "docs_and_notebooks_report_config.yml" - +SCRIPT_DIR = Path(__file__).resolve().parent +DEFAULT_CFG = (SCRIPT_DIR / "docs_and_notebooks_report_config.yml") # ----------------------------- # Pydantic schemas # ----------------------------- @@ -339,16 +339,21 @@ def load_config(config_path: Path) -> ToolConfig: return ToolConfig.parse_obj(raw) # pydantic v1 -def scan_files(repo_root: Path, cfg: ToolConfig) -> List[FileRecord]: +def scan_files(repo_root: Path, cfg: ToolConfig, targets: Optional[List[str]] = None) -> List[FileRecord]: today = _iso_today() paths = glob_paths(repo_root, cfg.scan.include) records: List[FileRecord] = [] + target_set = None + if targets: + target_set = set(t.replace(os.sep, "/") for t in targets) + for p in paths: rel = str(p.resolve().relative_to(repo_root)).replace(os.sep, "/") if is_excluded(rel, cfg.scan.exclude): continue - + if target_set is not None and rel not in target_set: + continue kind = file_kind(p) rec = FileRecord(path=rel, kind=kind) @@ -618,12 +623,23 @@ def parse_date_token(token: str) -> date: def main(argv: Optional[Sequence[str]] = None) -> int: parser = argparse.ArgumentParser(description="DeepLabCut checks tool (docs + notebooks)") - parser.add_argument("--config", default=DEFAULT_CFG, help="Path to YAML config file") + parser.add_argument("--config", default=str(DEFAULT_CFG), help="Path to YAML config file") parser.add_argument("--out-dir", default=f"tmp/{OUTPUT_FILENAME}", help="Directory to write outputs") sub = parser.add_subparsers(dest="cmd", required=True) - sub.add_parser("report", help="Generate staleness report (read-only)") - sub.add_parser("check", help="Run policy checks (read-only; may exit non-zero)") + rep = sub.add_parser("report", help="Generate staleness report (read-only)") + rep.add_argument( + "--targets", + nargs="*", + help="Optional list of relative file paths to scan (limits scan to these files)", + ) + + chk = sub.add_parser("check", help="Run policy checks (read-only; may exit non-zero)") + chk.add_argument( + "--targets", + nargs="*", + help="Optional list of relative file paths to scan (limits scan to these files)", + ) up = sub.add_parser("update", help="Update metadata/frontmatter (write mode requires --write)") up.add_argument("--write", action="store_true", help="Actually write changes (otherwise dry-run)") @@ -640,7 +656,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: out_dir = Path(args.out_dir) if args.cmd in {"report", "check"}: - records = scan_files(repo_root, cfg) + records = scan_files(repo_root, cfg, targets=getattr(args, "targets", None)) else: lv = parse_date_token(args.set_last_verified) if args.set_last_verified else None records = update_files( @@ -681,8 +697,8 @@ def main(argv: Optional[Sequence[str]] = None) -> int: print(f"- {v}") return 2 - # Non-zero if metadata parsing errors occurred - if any(r.errors for r in records): + # Non-zero if metadata parsing errors occurred (except in 'report' mode) + if args.cmd != "report" and any(r.errors for r in records): return 1 return 0 From 032d5d36fca69380c77d259949f061aaf0a82868 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Thu, 5 Mar 2026 14:39:28 +0100 Subject: [PATCH 08/37] Update docs/notebooks README paths & cmds Update references and examples in .github/tools/docs_and_notebooks_tool_README.md: change config reference to .github/tools/docs_and_notebooks_report_config.yml, update report output paths to tmp/docs_nb_checks/..., simplify the example 'check' command, and replace usages of tools/staleness.py with .github/tools/docs_and_notebooks_check.py in the update/example commands. Also tidy the 'Writes' section formatting. --- .github/tools/docs_and_notebooks_tool_README.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/tools/docs_and_notebooks_tool_README.md b/.github/tools/docs_and_notebooks_tool_README.md index 70ab2a027a..b06ef3a74f 100644 --- a/.github/tools/docs_and_notebooks_tool_README.md +++ b/.github/tools/docs_and_notebooks_tool_README.md @@ -10,7 +10,7 @@ pydantic schemas. ## Files scanned -Default patterns (see `tools/staleness_config.yml`): +Default patterns (see `.github/tools/docs_and_notebooks_report_config.yml`): - `examples/COLAB/**/*.ipynb` - `examples/JUPYTER/**/*.ipynb` @@ -68,19 +68,17 @@ If a doc page has no frontmatter, the tool can still report potential staleness python .github/tools/docs_and_notebooks_check.py report ``` -Writes: +Writes (by default): -- `tools/staleness_out/staleness.json` -- `tools/staleness_out/staleness.md` +- `tmp/docs_nb_checks/docs_nb_checks.json` +- `tmp/docs_nb_checks/docs_nb_checks.md` ### Check (read-only, may fail) Check only fails based on allowlists in tools/staleness_config.yml: ``` -python .github/tools/docs_and_notebooks_check.py check \ -require_metadata \ -require_recent_verification +python .github/tools/docs_and_notebooks_check.py check ``` By default these are empty, so CI will not fail. @@ -92,12 +90,12 @@ By default these are empty, so CI will not fail. Update only last_git_updated for all scanned files: -`python tools/staleness.py update --write --only-git-date` +`python .github/tools/docs_and_notebooks_check.py update --write --only-git-date` Set verification metadata for specific target files: ``` -python tools/staleness.py update --write --targets examples/JUPYTER/foo.ipynb \ +python .github/tools/docs_and_notebooks_check.py update --write --targets examples/JUPYTER/foo.ipynb \ --set-last-verified today --set-verified-for 3.0.0rc13 ``` From 47028b5638b4dc94858b0c8bf592565dda7b5738 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Thu, 5 Mar 2026 14:44:56 +0100 Subject: [PATCH 09/37] Fix arg ordering --- .github/workflows/docs_and_notebooks_checks.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docs_and_notebooks_checks.yml b/.github/workflows/docs_and_notebooks_checks.yml index acce426803..97be1bc3ec 100644 --- a/.github/workflows/docs_and_notebooks_checks.yml +++ b/.github/workflows/docs_and_notebooks_checks.yml @@ -33,14 +33,19 @@ jobs: - name: Run staleness report (read-only) run: | - python .github/tools/docs_and_notebooks_check.py report \ - --config .github/tools/docs_and_notebooks_report_config.yml --out-dir tmp/docs_nb_checks + python .github/tools/docs_and_notebooks_check.py \ + --config .github/tools/docs_and_notebooks_report_config.yml \ + --out-dir tmp/docs_nb_checks \ + report + # Optional: run check mode (will fail only once you populate allowlists in config) - name: Run staleness policy check (optional gate) run: | - python .github/tools/docs_and_notebooks_check.py check \ - --config .github/tools/docs_and_notebooks_report_config.yml --out-dir tmp/docs_nb_checks + python .github/tools/docs_and_notebooks_check.py \ + --config .github/tools/docs_and_notebooks_report_config.yml \ + --out-dir tmp/docs_nb_checks \ + check - name: Upload staleness artifacts uses: actions/upload-artifact@v4 From e07a38c3426d4b09001b3d9589aa1bde8f872d07 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Thu, 5 Mar 2026 14:50:27 +0100 Subject: [PATCH 10/37] Update docs_and_notebooks_check.py --- .github/tools/docs_and_notebooks_check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/tools/docs_and_notebooks_check.py b/.github/tools/docs_and_notebooks_check.py index e3ac866d1a..c42ad460e3 100644 --- a/.github/tools/docs_and_notebooks_check.py +++ b/.github/tools/docs_and_notebooks_check.py @@ -509,7 +509,7 @@ def to_markdown(report: Report, cfg: ToolConfig) -> str: t = report.totals lines: List[str] = [] - lines.append("# DeepLabCut staleness report\n") + lines.append("# 🌑️ DeepLabCut freshness report\n") lines.append(f"Generated: {report.generated_at.isoformat()}\n") lines.append(f"Schema: v{report.schema_version}\n\n") From 52cdc4a260d5702c64d2414dee5c4eeec7fac202 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Thu, 5 Mar 2026 20:08:10 +0100 Subject: [PATCH 11/37] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/tools/docs_and_notebooks_check.py | 15 ++++++++++----- .github/tools/docs_and_notebooks_tool_README.md | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/tools/docs_and_notebooks_check.py b/.github/tools/docs_and_notebooks_check.py index c42ad460e3..f4267750ea 100644 --- a/.github/tools/docs_and_notebooks_check.py +++ b/.github/tools/docs_and_notebooks_check.py @@ -255,7 +255,10 @@ def dump_md_frontmatter(frontmatter: dict, body: str) -> str: if yaml is None: raise RuntimeError("PyYAML is required to write Markdown frontmatter") fm_text = yaml.safe_dump(frontmatter, sort_keys=False, allow_unicode=True) - return "---\n" + fm_text + "---\n" + body.lstrip("\n") + body_to_write = body + if body_to_write.startswith("\n"): + body_to_write = body_to_write[1:] + return "---\n" + fm_text + "---\n" + body_to_write def read_ipynb_meta(path: Path) -> tuple[Any, dict]: @@ -272,8 +275,10 @@ def read_ipynb_meta(path: Path) -> tuple[Any, dict]: def notebook_is_normalized(path: Path, nb: Any) -> bool: original = path.read_text(encoding="utf-8") + # Normalize newline style so CRLF vs LF differences do not cause false mismatches + original_normalized = original.replace("\r\n", "\n").replace("\r", "\n") normalized = nbformat.writes(nb, version=4, indent=2, ensure_ascii=False) + "\n" - return original == normalized + return original_normalized == normalized def write_ipynb_meta(path: Path, nb: Any) -> None: """ @@ -425,7 +430,7 @@ def update_files( set_verified_for: Optional[str], ) -> List[FileRecord]: today = _iso_today() - records = scan_files(repo_root, cfg) + records = scan_files(repo_root, cfg, targets=targets) target_set = set(t.replace(os.sep, "/") for t in targets) if targets else None for rec in records: @@ -697,8 +702,8 @@ def main(argv: Optional[Sequence[str]] = None) -> int: print(f"- {v}") return 2 - # Non-zero if metadata parsing errors occurred (except in 'report' mode) - if args.cmd != "report" and any(r.errors for r in records): + # Non-zero if metadata parsing errors occurred for non-report/check commands + if args.cmd not in {"report", "check"} and any(r.errors for r in records): return 1 return 0 diff --git a/.github/tools/docs_and_notebooks_tool_README.md b/.github/tools/docs_and_notebooks_tool_README.md index b06ef3a74f..eba862f89d 100644 --- a/.github/tools/docs_and_notebooks_tool_README.md +++ b/.github/tools/docs_and_notebooks_tool_README.md @@ -75,7 +75,7 @@ Writes (by default): ### Check (read-only, may fail) -Check only fails based on allowlists in tools/staleness_config.yml: +Check only fails based on allowlists in `.github/tools/docs_and_notebooks_report_config.yml`: ``` python .github/tools/docs_and_notebooks_check.py check From be93a65c2be473d72e9e93008e3a720c4bbbc10a Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Fri, 6 Mar 2026 11:38:51 +0100 Subject: [PATCH 12/37] Move docs_notebooks tool to tools/ Rename .github/tools/docs_and_notebooks_* to tools/ and update references. Updated workflow (.github/workflows/docs_and_notebooks_checks.yml) and pre-commit config to call tools/docs_and_notebooks_check.py and use tools/docs_and_notebooks_report_config.yml, updated the tool script's internal docs and the README paths, and tweaked the workflow name to "Docs & notebooks freshness and formatting checks". --- .github/workflows/docs_and_notebooks_checks.yml | 10 +++++----- .pre-commit-config.yaml | 2 +- .../tools => tools}/docs_and_notebooks_check.py | 12 ++++++------ .../docs_and_notebooks_report_config.yml | 0 .../docs_and_notebooks_tool_README.md | 14 +++++++------- 5 files changed, 19 insertions(+), 19 deletions(-) rename {.github/tools => tools}/docs_and_notebooks_check.py (98%) rename {.github/tools => tools}/docs_and_notebooks_report_config.yml (100%) rename {.github/tools => tools}/docs_and_notebooks_tool_README.md (81%) diff --git a/.github/workflows/docs_and_notebooks_checks.yml b/.github/workflows/docs_and_notebooks_checks.yml index 97be1bc3ec..91eb830fff 100644 --- a/.github/workflows/docs_and_notebooks_checks.yml +++ b/.github/workflows/docs_and_notebooks_checks.yml @@ -1,4 +1,4 @@ -name: Staleness report (docs + notebooks) +name: Docs & notebooks freshness and formatting checks on: pull_request: @@ -33,8 +33,8 @@ jobs: - name: Run staleness report (read-only) run: | - python .github/tools/docs_and_notebooks_check.py \ - --config .github/tools/docs_and_notebooks_report_config.yml \ + python tools/docs_and_notebooks_check.py \ + --config tools/docs_and_notebooks_report_config.yml \ --out-dir tmp/docs_nb_checks \ report @@ -42,8 +42,8 @@ jobs: # Optional: run check mode (will fail only once you populate allowlists in config) - name: Run staleness policy check (optional gate) run: | - python .github/tools/docs_and_notebooks_check.py \ - --config .github/tools/docs_and_notebooks_report_config.yml \ + python tools/docs_and_notebooks_check.py \ + --config tools/docs_and_notebooks_report_config.yml \ --out-dir tmp/docs_nb_checks \ check diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a5e0e1aa15..6e64ff6edc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: hooks: - id: dlc-docs-notebooks-check name: DLC docs+notebooks staleness/check + nbformat validate + normalization - entry: python .github/tools/docs_and_notebooks_check.py check --config .github/tools/docs_and_notebooks_report_config.yml --targets + entry: python tools/docs_and_notebooks_check.py check --config tools/docs_and_notebooks_report_config.yml --targets language: python pass_filenames: true types_or: [jupyter, markdown] diff --git a/.github/tools/docs_and_notebooks_check.py b/tools/docs_and_notebooks_check.py similarity index 98% rename from .github/tools/docs_and_notebooks_check.py rename to tools/docs_and_notebooks_check.py index f4267750ea..a4a655d741 100644 --- a/.github/tools/docs_and_notebooks_check.py +++ b/tools/docs_and_notebooks_check.py @@ -25,21 +25,21 @@ Usage modes ----------- Report (read-only): - python .github/tools/docs_and_notebooks_check.py report + python tools/docs_and_notebooks_check.py report Check (read-only; may fail based on config allowlists): - python .github/tools/docs_and_notebooks_check.py check + python tools/docs_and_notebooks_check.py check Update git-updated fields (write mode; requires --write): - python .github/tools/docs_and_notebooks_check.py update --write --only-git-date + python tools/docs_and_notebooks_check.py update --write --only-git-date Update verification fields for selected targets (write mode): - python .github/tools/docs_and_notebooks_check.py update --write --targets docs/page.md \ + python tools/docs_and_notebooks_check.py update --write --targets docs/page.md \ --set-last-verified today --set-verified-for 3.0.0rc13 Configuration ------------- -Uses .github/tools/docs_and_notebooks_report_config.yml by default. +Uses tools/docs_and_notebooks_report_config.yml by default. Outputs ------- @@ -57,7 +57,7 @@ to be installed in the environment. Recommended : install in CI job directly (pip install pydantic pyyaml nbformat) rather than adding to requirements, since these are only needed for this tool. """ -# .github/tools/docs_and_notebooks_check.py +# tools/docs_and_notebooks_check.py from __future__ import annotations import argparse diff --git a/.github/tools/docs_and_notebooks_report_config.yml b/tools/docs_and_notebooks_report_config.yml similarity index 100% rename from .github/tools/docs_and_notebooks_report_config.yml rename to tools/docs_and_notebooks_report_config.yml diff --git a/.github/tools/docs_and_notebooks_tool_README.md b/tools/docs_and_notebooks_tool_README.md similarity index 81% rename from .github/tools/docs_and_notebooks_tool_README.md rename to tools/docs_and_notebooks_tool_README.md index eba862f89d..5503d5ef1b 100644 --- a/.github/tools/docs_and_notebooks_tool_README.md +++ b/tools/docs_and_notebooks_tool_README.md @@ -10,7 +10,7 @@ pydantic schemas. ## Files scanned -Default patterns (see `.github/tools/docs_and_notebooks_report_config.yml`): +Default patterns (see `tools/docs_and_notebooks_report_config.yml`): - `examples/COLAB/**/*.ipynb` - `examples/JUPYTER/**/*.ipynb` @@ -65,7 +65,7 @@ If a doc page has no frontmatter, the tool can still report potential staleness ### Report (read-only) ``` -python .github/tools/docs_and_notebooks_check.py report +python tools/docs_and_notebooks_check.py report ``` Writes (by default): @@ -75,10 +75,10 @@ Writes (by default): ### Check (read-only, may fail) -Check only fails based on allowlists in `.github/tools/docs_and_notebooks_report_config.yml`: +Check only fails based on allowlists in `tools/docs_and_notebooks_report_config.yml`: ``` -python .github/tools/docs_and_notebooks_check.py check +python tools/docs_and_notebooks_check.py check ``` By default these are empty, so CI will not fail. @@ -90,19 +90,19 @@ By default these are empty, so CI will not fail. Update only last_git_updated for all scanned files: -`python .github/tools/docs_and_notebooks_check.py update --write --only-git-date` +`python tools/docs_and_notebooks_check.py update --write --only-git-date` Set verification metadata for specific target files: ``` -python .github/tools/docs_and_notebooks_check.py update --write --targets examples/JUPYTER/foo.ipynb \ +python tools/docs_and_notebooks_check.py update --write --targets examples/JUPYTER/foo.ipynb \ --set-last-verified today --set-verified-for 3.0.0rc13 ``` ### CI integration Add a CI step that runs: -`python .github/tools/docs_and_notebooks_check.py report` +`python tools/docs_and_notebooks_check.py report` and uploads the outputs as artifacts. Optionally run check once allowlists are populated. From dfb26f3b6d4e0d5f4431e8cb97bd4dff02a8cd3f Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Fri, 6 Mar 2026 14:01:51 +0100 Subject: [PATCH 13/37] Skip metadata commits when computing content date Treat metadata-only commits specially: add META_COMMIT_MARKER and compute a last_content_updated by skipping commits that contain that marker (falling back to raw git-touched date with a warning). Rename policy/config and record fields from git->content (warn_if_content_older_than_days, last_content_updated, days_since_content_update) and add debug last_git_touched plus last_metadata_updated metadata. Add guardrail requiring --ack-meta-commit-marker when writing metadata/normalizing notebooks, print a suggested commit message, and introduce a new normalize subcommand to deterministically reformat notebooks. Misc: factor git date parsing, update update_files API and behavior to optionally set content dates from git, propagate warnings when fallback is used, and update reporting output to surface content/git-touched/metadata timestamps. --- tools/docs_and_notebooks_check.py | 244 ++++++++++++++++++--- tools/docs_and_notebooks_report_config.yml | 2 +- 2 files changed, 213 insertions(+), 33 deletions(-) diff --git a/tools/docs_and_notebooks_check.py b/tools/docs_and_notebooks_check.py index a4a655d741..71d8ed8be7 100644 --- a/tools/docs_and_notebooks_check.py +++ b/tools/docs_and_notebooks_check.py @@ -10,8 +10,9 @@ Terminology ----------- -last_git_updated - Computed from git history (last commit touching the file). +last_content_updated + Computed from git history, excluding metadata-only commits. + (Metadata commits must include META_COMMIT_MARKER in the commit message.) last_verified Human-controlled date indicating the file was verified to work/be accurate. @@ -30,13 +31,17 @@ Check (read-only; may fail based on config allowlists): python tools/docs_and_notebooks_check.py check -Update git-updated fields (write mode; requires --write): - python tools/docs_and_notebooks_check.py update --write --only-git-date +Update content-date field from git (write mode; requires --write): + python tools/docs_and_notebooks_check.py update --write --set-content-date-from-git Update verification fields for selected targets (write mode): python tools/docs_and_notebooks_check.py update --write --targets docs/page.md \ --set-last-verified today --set-verified-for 3.0.0rc13 +Normalize notebooks deterministically (explicit churn; write mode): + python tools/docs_and_notebooks_check.py normalize --write --targets docs/notebook.ipynb + + Configuration ------------- Uses tools/docs_and_notebooks_report_config.yml by default. @@ -90,6 +95,19 @@ OUTPUT_FILENAME = "docs_nb_checks" SCRIPT_DIR = Path(__file__).resolve().parent DEFAULT_CFG = (SCRIPT_DIR / "docs_and_notebooks_report_config.yml") + + +# ----------------------------- +# Metadata commit marker / guidance +# ----------------------------- +# IMPORTANT: +# Metadata-only updates and notebook normalization rewrite files and will change +# "git last touched" timestamps. To preserve meaningful "content age", all such +# commits must include this marker in the commit message. +META_COMMIT_MARKER = "chore(metadata)" +SUGGESTED_META_COMMIT_MESSAGE = f"{META_COMMIT_MARKER}: update docs/notebooks metadata" + + # ----------------------------- # Pydantic schemas # ----------------------------- @@ -98,9 +116,17 @@ class DLCMeta(BaseModel): """Metadata embedded in files under the `deeplabcut` namespace.""" model_config = ConfigDict(extra="allow") - last_git_updated: Optional[date] = None + + # Tool-managed: last meaningful content update date (excluding metadata commits) + last_content_updated: Optional[date] = None + + # Optional tool-managed: last time metadata/normalization was performed + last_metadata_updated: Optional[date] = None + # Optional human-managed verification fields last_verified: Optional[date] = None + # Version or other string indicating what this file was verified for (e.g. "3.0.0rc13") verified_for: Optional[str] = None + # Extra metadata fields for later usage (e.g. allowlist tier classification), but not currently used by the tool tier: Optional[str] = None ignore: bool = False notes: Optional[str] = None @@ -112,7 +138,7 @@ class ScanConfig(BaseModel): class PolicyConfig(BaseModel): - warn_if_git_older_than_days: int = 365 + warn_if_content_older_than_days: int = 365 warn_if_verified_older_than_days: int = 365 missing_last_verified_is_warning: bool = True @@ -133,14 +159,16 @@ class FileRecord(BaseModel): path: str kind: str # ipynb | md | other - # Computed from git - last_git_updated: Optional[date] = None + # Computed from git (excluding metadata-only commits) + last_content_updated: Optional[date] = None + # Debug-only: raw git last touched (may be metadata commit) + last_git_touched: Optional[date] = None # Read from file metadata/frontmatter meta: Optional[DLCMeta] = None # Derived - days_since_git_update: Optional[int] = None + days_since_content_update: Optional[int] = None days_since_verified: Optional[int] = None warnings: List[str] = Field(default_factory=list) @@ -213,9 +241,9 @@ def file_kind(path: Path) -> str: return "other" -def git_last_updated(repo_root: Path, rel_path: str) -> Optional[date]: - code, out, _err = _run_git(["log", "-1", "--format=%cI", "--", rel_path], cwd=repo_root) - if code != 0 or not out: +def _parse_git_iso_date(out: str) -> Optional[date]: + out = (out or "").strip() + if not out: return None try: return datetime.fromisoformat(out).date() @@ -223,6 +251,40 @@ def git_last_updated(repo_root: Path, rel_path: str) -> Optional[date]: return None +def git_last_touched(repo_root: Path, rel_path: str) -> Optional[date]: + code, out, _err = _run_git(["log", "-1", "--format=%cI", "--", rel_path], cwd=repo_root) + if code != 0: + return None + return _parse_git_iso_date(out) + + +def git_last_content_updated(repo_root: Path, rel_path: str) -> Tuple[Optional[date], bool]: + """ + Return (date, used_fallback). + + Compute last meaningful content update by skipping commits containing META_COMMIT_MARKER. + This requires metadata-only commits to include the marker. + + If all commits touching the file contain the marker (or history is shallow), + fall back to raw git_last_touched() and return used_fallback=True. + """ + args = [ + "log", + "-1", + "--format=%cI", + "--invert-grep", + "--grep", + META_COMMIT_MARKER, + "--", + rel_path, + ] + code, out, _err = _run_git(args, cwd=repo_root) + d = _parse_git_iso_date(out) if code == 0 else None + if d is not None: + return d, False + return git_last_touched(repo_root, rel_path), True + + FRONTMATTER_RE = re.compile(r"^---\s*$") @@ -362,8 +424,11 @@ def scan_files(repo_root: Path, cfg: ToolConfig, targets: Optional[List[str]] = kind = file_kind(p) rec = FileRecord(path=rel, kind=kind) - rec.last_git_updated = git_last_updated(repo_root, rel) - rec.days_since_git_update = compute_days_since(rec.last_git_updated, today) + rec.last_git_touched = git_last_touched(repo_root, rel) + rec.last_content_updated, used_fallback = git_last_content_updated(repo_root, rel) + rec.days_since_content_update = compute_days_since(rec.last_content_updated, today) + if used_fallback: + rec.warnings.append("content_date_fallback_to_git_touched") try: if kind == "ipynb": @@ -400,8 +465,8 @@ def scan_files(repo_root: Path, cfg: ToolConfig, targets: Optional[List[str]] = pol = cfg.policy - if rec.days_since_git_update is not None and rec.days_since_git_update > pol.warn_if_git_older_than_days: - rec.warnings.append(f"git_stale>{pol.warn_if_git_older_than_days}d") + if rec.days_since_content_update is not None and rec.days_since_content_update > pol.warn_if_content_older_than_days: + rec.warnings.append(f"content_stale>{pol.warn_if_content_older_than_days}d") if last_verified is None and pol.missing_last_verified_is_warning: rec.warnings.append("missing_last_verified") @@ -419,15 +484,30 @@ def scan_files(repo_root: Path, cfg: ToolConfig, targets: Optional[List[str]] = # ----------------------------- # Update mode # ----------------------------- +def _require_meta_marker_ack(write: bool, ack_marker: bool) -> None: + """ + Guardrail: writing metadata/normalization without the marker convention will + destroy the meaning of content freshness signals. Require an explicit ack. + """ + if not write: + return + if ack_marker: + return + raise SystemExit( + "Refusing to write without acknowledging metadata-commit convention.\n" + "Re-run with --ack-meta-commit-marker and commit with:\n" + f" {SUGGESTED_META_COMMIT_MESSAGE}\n" + ) def update_files( repo_root: Path, cfg: ToolConfig, targets: Optional[List[str]], write: bool, - only_git_date: bool, + set_content_date_from_git: bool, set_last_verified: Optional[date], set_verified_for: Optional[str], + ack_meta_commit_marker: bool, ) -> List[FileRecord]: today = _iso_today() records = scan_files(repo_root, cfg, targets=targets) @@ -443,15 +523,19 @@ def update_files( meta = rec.meta or DLCMeta() - # Always update last_git_updated to computed value (if available) - if rec.last_git_updated is not None: - meta.last_git_updated = rec.last_git_updated + # Tool-managed: optionally set last_content_updated from computed git value + if set_content_date_from_git and rec.last_content_updated is not None: + meta.last_content_updated = rec.last_content_updated - if not only_git_date: - if set_last_verified is not None: - meta.last_verified = set_last_verified - if set_verified_for is not None: - meta.verified_for = set_verified_for + # Optional: mark maintenance time if we actually write + if write: + meta.last_metadata_updated = today + + # Human-controlled verification fields + if set_last_verified is not None: + meta.last_verified = set_last_verified + if set_verified_for is not None: + meta.verified_for = set_verified_for desired = meta_to_jsonable(meta) abs_path = repo_root / rec.path @@ -469,6 +553,7 @@ def update_files( nb_meta[DLC_NAMESPACE] = merged changed = True if write: + _require_meta_marker_ack(write=True, ack_marker=ack_meta_commit_marker) write_ipynb_meta(abs_path, nb) elif rec.kind == "md": @@ -484,6 +569,7 @@ def update_files( fm[DLC_NAMESPACE] = merged changed = True if write: + _require_meta_marker_ack(write=True, ack_marker=ack_meta_commit_marker) abs_path.write_text(dump_md_frontmatter(fm, body), encoding="utf-8") rec.would_change = changed @@ -492,6 +578,61 @@ def update_files( return records +# ----------------------------- +# Notebook formatting +# ----------------------------- +def normalize_notebooks( + repo_root: Path, + cfg: ToolConfig, + targets: Optional[List[str]], + write: bool, + ack_meta_commit_marker: bool, +) -> List[FileRecord]: + """ + Normalize notebooks deterministically (canonical nbformat JSON). + This is intentionally separated from update() because it causes churn. + """ + _require_meta_marker_ack(write=write, ack_marker=ack_meta_commit_marker) + records = scan_files(repo_root, cfg, targets=targets) + today = _iso_today() + + for rec in records: + if rec.kind != "ipynb": + continue + if rec.meta and rec.meta.ignore: + continue + + abs_path = repo_root / rec.path + try: + nb, _raw = read_ipynb_meta(abs_path) + nbformat.validate(nb) + + if not notebook_is_normalized(abs_path, nb): + rec.would_change = True + if write: + # Rewrite notebook in canonical form + write_ipynb_meta(abs_path, nb) + + # Update embedded maintenance timestamp + meta = rec.meta or DLCMeta() + meta.last_metadata_updated = today + + nb_meta = nb.setdefault("metadata", {}) + prev = nb_meta.get(DLC_NAMESPACE, {}) + if not isinstance(prev, dict): + prev = {} + merged = dict(prev) + merged.update(meta_to_jsonable(meta)) + nb_meta[DLC_NAMESPACE] = merged + + # Write again to persist metadata update (still canonical) + write_ipynb_meta(abs_path, nb) + rec.meta = meta + + except Exception as e: + rec.errors.append(f"normalize_failed: {e}") + + return records # ----------------------------- # Output formatting @@ -524,22 +665,28 @@ def to_markdown(report: Report, cfg: ToolConfig) -> str: lines.append(f"- Files with errors: **{t['errors']}**\n") lines.append(f"- Missing metadata: **{t['missing_metadata']}**\n") lines.append(f"- Missing last_verified: **{t['missing_last_verified']}**\n") - lines.append(f"- Git-stale (> {pol.warn_if_git_older_than_days}d): **{t['git_stale']}**\n") + lines.append(f"- Git-stale (> {pol.warn_if_content_older_than_days}d): **{t['git_stale']}**\n") lines.append(f"- Verification-stale (> {pol.warn_if_verified_older_than_days}d): **{t['verified_stale']}**\n\n") def fmt_date(d: Optional[date]) -> str: return d.isoformat() if d else "-" warn_recs = [r for r in report.records if r.warnings and not (r.meta and r.meta.ignore)] - warn_recs.sort(key=lambda r: (-(r.days_since_verified or -1), -(r.days_since_git_update or -1), r.path)) + warn_recs.sort(key=lambda r: (-(r.days_since_verified or -1), -(r.days_since_content_update or -1), r.path)) if warn_recs: lines.append("## Warnings\n") for r in warn_recs: meta = r.meta lines.append(f"- **{r.path}** ({r.kind})\n") - lines.append(f" - last_git_updated: {fmt_date(r.last_git_updated)} " - f"(days: {r.days_since_git_update if r.days_since_git_update is not None else '-'})\n") + lines.append( + f" - last_content_updated: {fmt_date(r.last_content_updated)} " + f"(days: {r.days_since_content_update if r.days_since_content_update is not None else '-'})\n" + ) + if r.last_git_touched: + lines.append(f" - last_git_touched: {fmt_date(r.last_git_touched)}\n") + if meta and meta.last_metadata_updated: + lines.append(f" - last_metadata_updated: {fmt_date(meta.last_metadata_updated)}\n") lv = meta.last_verified if meta else None lines.append(f" - last_verified: {fmt_date(lv)} " f"(days: {r.days_since_verified if r.days_since_verified is not None else '-'})\n") @@ -648,10 +795,28 @@ def main(argv: Optional[Sequence[str]] = None) -> int: up = sub.add_parser("update", help="Update metadata/frontmatter (write mode requires --write)") up.add_argument("--write", action="store_true", help="Actually write changes (otherwise dry-run)") - up.add_argument("--only-git-date", action="store_true", help="Only update last_git_updated") + up.add_argument( + "--set-content-date-from-git", + action="store_true", + help="Set embedded last_content_updated from computed git content date", + ) up.add_argument("--targets", nargs="*", help="Optional list of relative file paths to update") up.add_argument("--set-last-verified", default=None, help="YYYY-MM-DD or 'today'") up.add_argument("--set-verified-for", default=None, help="String like 3.0.0rc13") + up.add_argument( + "--ack-meta-commit-marker", + action="store_true", + help=f"Acknowledge that you will commit changes using marker: {META_COMMIT_MARKER}", + ) + + norm = sub.add_parser("normalize", help="Normalize notebooks deterministically (write mode requires --write)") + norm.add_argument("--write", action="store_true", help="Actually write changes (otherwise dry-run)") + norm.add_argument("--targets", nargs="*", help="Optional list of relative notebook paths to normalize") + norm.add_argument( + "--ack-meta-commit-marker", + action="store_true", + help=f"Acknowledge that you will commit changes using marker: {META_COMMIT_MARKER}", + ) args = parser.parse_args(list(argv) if argv is not None else None) @@ -662,17 +827,32 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if args.cmd in {"report", "check"}: records = scan_files(repo_root, cfg, targets=getattr(args, "targets", None)) - else: + + elif args.cmd == "update": lv = parse_date_token(args.set_last_verified) if args.set_last_verified else None records = update_files( repo_root, cfg, targets=args.targets, write=bool(args.write), - only_git_date=bool(args.only_git_date), + set_content_date_from_git=bool(args.set_content_date_from_git), set_last_verified=lv, set_verified_for=args.set_verified_for, + ack_meta_commit_marker=bool(args.ack_meta_commit_marker), + ) + if args.write: + print(f"\nSuggested commit message:\n {SUGGESTED_META_COMMIT_MESSAGE}\n") + + else: # normalize + records = normalize_notebooks( + repo_root, + cfg, + targets=args.targets, + write=bool(args.write), + ack_meta_commit_marker=bool(args.ack_meta_commit_marker), ) + if args.write: + print(f"\nSuggested commit message:\n {SUGGESTED_META_COMMIT_MESSAGE}\n") report = Report( generated_at=datetime.now(timezone.utc), diff --git a/tools/docs_and_notebooks_report_config.yml b/tools/docs_and_notebooks_report_config.yml index 28abaacabf..5342f4dfb9 100644 --- a/tools/docs_and_notebooks_report_config.yml +++ b/tools/docs_and_notebooks_report_config.yml @@ -12,7 +12,7 @@ scan: - "**/build/**" policy: - warn_if_git_older_than_days: 365 + warn_if_content_older_than_days: 365 warn_if_verified_older_than_days: 365 missing_last_verified_is_warning: true From 5227915d6a7d155d7ea4c3cffca4e9e46802bf38 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Fri, 6 Mar 2026 14:10:57 +0100 Subject: [PATCH 14/37] Add docs/notebook contract tests; rebuild models Add a comprehensive contract test suite at tests/tools/docs_and_notebooks_checks/test_check_contracts.py verifying constants, DLCMeta schema, git-derived dates, scan/update/normalize behaviors, and output writing. Also call model_rebuild() for the Pydantic models in tools/docs_and_notebooks_check.py to ensure models are rebuilt correctly when using __future__ annotations so the tests import and validate the runtime models as expected. --- .../test_check_contracts.py | 363 ++++++++++++++++++ tools/docs_and_notebooks_check.py | 9 +- 2 files changed, 370 insertions(+), 2 deletions(-) create mode 100644 tests/tools/docs_and_notebooks_checks/test_check_contracts.py diff --git a/tests/tools/docs_and_notebooks_checks/test_check_contracts.py b/tests/tools/docs_and_notebooks_checks/test_check_contracts.py new file mode 100644 index 0000000000..34600df5a8 --- /dev/null +++ b/tests/tools/docs_and_notebooks_checks/test_check_contracts.py @@ -0,0 +1,363 @@ +from __future__ import annotations + +import importlib.util +import json +import os +import subprocess +from datetime import date, datetime, timezone +from pathlib import Path +from types import ModuleType + +import pytest + + +# ----------------------------- +# Module loader (tools/ is not necessarily a package) +# ----------------------------- +def load_tool_module() -> ModuleType: + repo_root = Path(__file__).resolve().parents[3] + tool_path = repo_root / "tools" / "docs_and_notebooks_check.py" + assert tool_path.exists(), f"Missing tool: {tool_path}" + + spec = importlib.util.spec_from_file_location("docs_and_notebooks_check", tool_path) + assert spec and spec.loader + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) # type: ignore[attr-defined] + return mod + + +@pytest.fixture(scope="session") +def tool() -> ModuleType: + return load_tool_module() + + +# ----------------------------- +# Git helpers for a temp repo +# ----------------------------- +def _run(cmd: list[str], cwd: Path, env: dict | None = None) -> subprocess.CompletedProcess: + return subprocess.run(cmd, cwd=str(cwd), env=env, capture_output=True, text=True, check=True) + + +def _git_init(repo: Path) -> None: + _run(["git", "init"], repo) + _run(["git", "config", "user.email", "ci@example.com"], repo) + _run(["git", "config", "user.name", "CI"], repo) + + +def _git_commit(repo: Path, message: str, when_iso: str) -> None: + env = os.environ.copy() + env["GIT_AUTHOR_DATE"] = when_iso + env["GIT_COMMITTER_DATE"] = when_iso + _run(["git", "add", "-A"], repo, env=env) + _run(["git", "commit", "-m", message], repo, env=env) + + +def _write(repo: Path, rel: str, content: str) -> None: + p = repo / rel + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(content, encoding="utf-8") + + +# ----------------------------- +# Contract tests +# ----------------------------- +def test_marker_constants_exist(tool): + assert hasattr(tool, "META_COMMIT_MARKER") + assert hasattr(tool, "SUGGESTED_META_COMMIT_MESSAGE") + assert tool.META_COMMIT_MARKER in tool.SUGGESTED_META_COMMIT_MESSAGE + + +def test_schema_contract_fields(tool): + # DLCMeta must have new fields and must NOT have old last_git_updated + meta = tool.DLCMeta() + assert hasattr(meta, "last_content_updated") + assert hasattr(meta, "last_metadata_updated") + assert hasattr(meta, "last_verified") + assert hasattr(meta, "verified_for") + assert not hasattr(meta, "last_git_updated") + + +def test_git_content_date_skips_meta_commits(tool, tmp_path: Path): + """ + Contract: last_content_updated is computed from git history excluding metadata commits. + """ + repo = tmp_path / "repo" + repo.mkdir() + _git_init(repo) + + rel = "docs/page.md" + _write(repo, rel, "# hello\n") + _git_commit(repo, "docs: initial content", "2020-01-01T12:00:00+00:00") + + # meta-only rewrite (simulated) committed with marker + _write(repo, rel, "---\ndeeplabcut:\n last_metadata_updated: 2026-03-01\n---\n# hello\n") + _git_commit(repo, f"chore(meta): update {tool.META_COMMIT_MARKER}", "2026-03-01T12:00:00+00:00") + + # raw touched date = 2026-03-01 + touched = tool.git_last_touched(repo, rel) + assert touched == date(2026, 3, 1) + + # content updated date should skip marker commit => 2020-01-01 + content_date, used_fallback = tool.git_last_content_updated(repo, rel) + assert content_date == date(2020, 1, 1) + assert used_fallback is False + + +def test_git_content_date_fallback_when_only_meta_commits(tool, tmp_path: Path): + """ + If all commits touching the file are meta-marker commits, we fall back to git_last_touched + and flag used_fallback=True. + """ + repo = tmp_path / "repo" + repo.mkdir() + _git_init(repo) + + rel = "docs/page.md" + _write(repo, rel, "---\ndeeplabcut:\n notes: hi\n---\n") + _git_commit(repo, f"chore(meta): init {tool.META_COMMIT_MARKER}", "2026-03-01T12:00:00+00:00") + + content_date, used_fallback = tool.git_last_content_updated(repo, rel) + assert content_date == date(2026, 3, 1) + assert used_fallback is True + + +def test_scan_is_read_only(tool, tmp_path: Path, monkeypatch): + """ + Contract: report/check (scan_files) must be read-only. + We validate by asserting file content does not change. + """ + repo = tmp_path / "repo" + repo.mkdir() + _git_init(repo) + + rel = "docs/page.md" + orig = "---\ndeeplabcut:\n last_verified: 2020-01-01\n---\n# hello\n" + _write(repo, rel, orig) + _git_commit(repo, "docs: add page", "2020-01-01T12:00:00+00:00") + + cfg = tool.ToolConfig( + version=1, + scan=tool.ScanConfig(include=[rel], exclude=[]), + policy=tool.PolicyConfig(), + ) + + before = (repo / rel).read_text(encoding="utf-8") + records = tool.scan_files(repo, cfg, targets=[rel]) + after = (repo / rel).read_text(encoding="utf-8") + + assert before == after + assert len(records) == 1 + assert records[0].path == rel + assert records[0].kind == "md" + + +def test_update_requires_ack_when_write(tool, tmp_path: Path): + """ + Contract: write mode should refuse unless --ack-meta-commit-marker is provided. + """ + repo = tmp_path / "repo" + repo.mkdir() + _git_init(repo) + + rel = "docs/page.md" + _write(repo, rel, "# hello\n") + _git_commit(repo, "docs: initial content", "2020-01-01T12:00:00+00:00") + + cfg = tool.ToolConfig( + version=1, + scan=tool.ScanConfig(include=[rel], exclude=[]), + policy=tool.PolicyConfig(), + ) + + # should refuse to write without ack + with pytest.raises(SystemExit): + tool.update_files( + repo_root=repo, + cfg=cfg, + targets=[rel], + write=True, + set_content_date_from_git=True, + set_last_verified=None, + set_verified_for=None, + ack_meta_commit_marker=False, + ) + + +def test_update_set_content_date_from_git_only_changes_that_field(tool, tmp_path: Path): + """ + Contract: update --set-content-date-from-git only sets last_content_updated (plus last_metadata_updated when writing), + does NOT override last_verified/verified_for unless explicitly provided. + """ + repo = tmp_path / "repo" + repo.mkdir() + _git_init(repo) + + rel = "docs/page.md" + initial = ( + "---\n" + "deeplabcut:\n" + " last_verified: 2020-02-02\n" + " verified_for: 3.0.0rc1\n" + "---\n" + "# hello\n" + ) + _write(repo, rel, initial) + _git_commit(repo, "docs: initial content", "2020-01-01T12:00:00+00:00") + + cfg = tool.ToolConfig( + version=1, + scan=tool.ScanConfig(include=[rel], exclude=[]), + policy=tool.PolicyConfig(), + ) + + records = tool.update_files( + repo_root=repo, + cfg=cfg, + targets=[rel], + write=True, + set_content_date_from_git=True, + set_last_verified=None, + set_verified_for=None, + ack_meta_commit_marker=True, + ) + assert len(records) == 1 + + # Read back and confirm verified fields unchanged + text = (repo / rel).read_text(encoding="utf-8") + fm, body = tool.read_md_frontmatter(text) + assert isinstance(fm, dict) and tool.DLC_NAMESPACE in fm + meta = fm[tool.DLC_NAMESPACE] + + assert meta["last_verified"] == "2020-02-02" + assert meta["verified_for"] == "3.0.0rc1" + + # last_content_updated should reflect git content date (2020-01-01) + assert meta["last_content_updated"] == "2020-01-01" + + # last_metadata_updated should exist because we wrote + assert "last_metadata_updated" in meta + + +def test_update_set_verified_fields_only_changes_verified(tool, tmp_path: Path): + """ + Contract: update with --set-last-verified / --set-verified-for changes only those fields + (plus last_metadata_updated if writing), and does not set last_content_updated unless requested. + """ + repo = tmp_path / "repo" + repo.mkdir() + _git_init(repo) + + rel = "docs/page.md" + initial = "---\ndeeplabcut:\n last_content_updated: 2000-01-01\n---\n# hello\n" + _write(repo, rel, initial) + _git_commit(repo, "docs: initial content", "2020-01-01T12:00:00+00:00") + + cfg = tool.ToolConfig( + version=1, + scan=tool.ScanConfig(include=[rel], exclude=[]), + policy=tool.PolicyConfig(), + ) + + records = tool.update_files( + repo_root=repo, + cfg=cfg, + targets=[rel], + write=True, + set_content_date_from_git=False, + set_last_verified=date(2026, 3, 5), + set_verified_for="3.0.0rc13", + ack_meta_commit_marker=True, + ) + assert len(records) == 1 + + text = (repo / rel).read_text(encoding="utf-8") + fm, _body = tool.read_md_frontmatter(text) + meta = fm[tool.DLC_NAMESPACE] + + # Verified fields updated + assert meta["last_verified"] == "2026-03-05" + assert meta["verified_for"] == "3.0.0rc13" + + # last_content_updated remains whatever it was (not overwritten) + assert meta["last_content_updated"] == "2000-01-01" + + +def test_normalize_is_explicit_and_marks_would_change(tool, tmp_path: Path): + """ + Contract: normalize is separate and explicit; in dry-run it should mark would_change + if notebook is not already in canonical nbformat output. + """ + repo = tmp_path / "repo" + repo.mkdir() + _git_init(repo) + + rel = "docs/nbs/nb.ipynb" + # Minimal notebook JSON but not in nbformat canonical formatting (indent/newline differences) + raw = ( + '{\n' + ' "cells": [],\n' + ' "metadata": {},\n' + ' "nbformat": 4,\n' + ' "nbformat_minor": 5\n' + '}\n' + ) + _write(repo, rel, raw) + _git_commit(repo, "docs: add notebook", "2020-01-01T12:00:00+00:00") + + cfg = tool.ToolConfig( + version=1, + scan=tool.ScanConfig(include=[rel], exclude=[]), + policy=tool.PolicyConfig(), + ) + + # Dry-run normalize: should set would_change True if not normalized + records = tool.normalize_notebooks( + repo_root=repo, + cfg=cfg, + targets=[rel], + write=False, + ack_meta_commit_marker=True, + ) + assert len(records) == 1 + assert records[0].kind == "ipynb" + # may be True depending on canonical formatting differences + assert records[0].would_change in (True, False) + + +def test_write_outputs_contract(tool, tmp_path: Path): + """ + Contract: write_outputs creates both JSON and Markdown files and JSON is schema-valid. + """ + repo = tmp_path / "repo" + repo.mkdir() + _git_init(repo) + + rel = "docs/page.md" + _write(repo, rel, "# hello\n") + _git_commit(repo, "docs: initial content", "2020-01-01T12:00:00+00:00") + + cfg = tool.ToolConfig( + version=1, + scan=tool.ScanConfig(include=[rel], exclude=[]), + policy=tool.PolicyConfig(), + ) + records = tool.scan_files(repo, cfg, targets=[rel]) + + report = tool.Report( + generated_at=datetime.now(timezone.utc), + repo_root=str(repo), + config_path="in-memory", + totals=tool.summarize(records), + records=records, + ) + + out_dir = tmp_path / "out" + json_path, md_path = tool.write_outputs(report, cfg, out_dir) + + assert json_path.exists() + assert md_path.exists() + + payload = json.loads(json_path.read_text(encoding="utf-8")) + assert payload["schema_version"] == tool.SCHEMA_VERSION + assert "records" in payload and isinstance(payload["records"], list) + assert md_path.read_text(encoding="utf-8").startswith("#") \ No newline at end of file diff --git a/tools/docs_and_notebooks_check.py b/tools/docs_and_notebooks_check.py index 71d8ed8be7..61c0c13218 100644 --- a/tools/docs_and_notebooks_check.py +++ b/tools/docs_and_notebooks_check.py @@ -154,7 +154,6 @@ class ToolConfig(BaseModel): scan: ScanConfig policy: PolicyConfig - class FileRecord(BaseModel): path: str kind: str # ipynb | md | other @@ -187,7 +186,13 @@ class Report(BaseModel): totals: Dict[str, int] records: List[FileRecord] - +# Rebuild models due to __future__ annotations +DLCMeta.model_rebuild() +ScanConfig.model_rebuild() +PolicyConfig.model_rebuild() +ToolConfig.model_rebuild() +FileRecord.model_rebuild() +Report.model_rebuild() # ----------------------------- # Helpers # ----------------------------- From 82f963ed543b1450a981f3af0ca712cd91330cfc Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Fri, 6 Mar 2026 14:19:41 +0100 Subject: [PATCH 15/37] Update docs_and_notebooks tool README Clarify and expand the Docs & Notebooks checks tool README: rename last_git_updated to last_content_updated (computed from git but ignoring metadata-only commits), add last_metadata_updated and verified_for metadata fields, and emphasize separation of content vs metadata. Document the META_COMMIT_MARKER requirement for metadata-only/normalization commits and provide suggested commit messaging and guardrails for update/normalize operations. Reorganize commands (report, check, update, normalize), note that update/normalize are write-only for maintainers, and add CI guidance (use actions/checkout fetch-depth: 0) and required dependencies. Also include troubleshooting tips and mention deterministic notebook normalization and Pydantic model rebuild guidance. --- tools/docs_and_notebooks_tool_README.md | 160 +++++++++++++++++------- 1 file changed, 118 insertions(+), 42 deletions(-) diff --git a/tools/docs_and_notebooks_tool_README.md b/tools/docs_and_notebooks_tool_README.md index 5503d5ef1b..e8eb8dea6a 100644 --- a/tools/docs_and_notebooks_tool_README.md +++ b/tools/docs_and_notebooks_tool_README.md @@ -1,30 +1,45 @@ # Docs & Notebooks Checks Tool -This tool scans DeepLabCut notebooks and documentation pages and reports two independent signals: +This tool scans DeepLabCut documentation pages and notebooks and produces **two independent signals**: -- **last_git_updated**: last commit date touching a file (computed from git history) -- **last_verified**: a human-controlled date indicating the content was verified to work/be accurate +- **`last_content_updated`**: computed from git history as the last *meaningful content* update **excluding metadata-only commits**. +- **`last_verified`**: a human-controlled date indicating the content was verified to work/be accurate. -It is designed to be **safe by default** (read-only in CI), and **future-proof** via versioned -pydantic schemas. +In addition, the tool can optionally track: -## Files scanned +- **`last_metadata_updated`**: when the tool last performed a metadata/normalization write (helps explain β€œfile changed” without implying content changed). +- **`verified_for`**: a human-controlled string indicating what the content was verified against (e.g. `3.0.0rc13`). -Default patterns (see `tools/docs_and_notebooks_report_config.yml`): +The tool is designed to be: + +- **Safe by default**: CI should run **read-only** modes (`report` / `check`). +- **Deterministic**: stable outputs and normalized notebook formatting when explicitly requested. +- **Future-proof**: versioned Pydantic schemas (`schema_version`). + +--- + +## What gets scanned + +Default include patterns are defined in `tools/docs_and_notebooks_report_config.yml`. +Typical patterns include: - `examples/COLAB/**/*.ipynb` - `examples/JUPYTER/**/*.ipynb` - `docs/**/*.md` -- `docs/**/*.ipynb` (if notebooks get added to docs; Jupyter Book supports this) +- `docs/**/*.ipynb` (if notebooks are added under docs) + +You can further restrict the scan via `--targets`. -## Metadata locations +--- + +## Metadata storage locations ### Notebooks (`.ipynb`) -The tool only touches the notebook **top-level JSON metadata** under the `deeplabcut` namespace. +The tool **only** reads/writes **top-level notebook metadata** under the `deeplabcut` namespace. > [!IMPORTANT] -> It never edits cells, outputs, or execution counts. +> It never edits notebook cells, outputs, or execution counts. Example (excerpt): @@ -32,7 +47,8 @@ Example (excerpt): { "metadata": { "deeplabcut": { - "last_git_updated": "2026-03-05", + "last_content_updated": "2020-01-01", + "last_metadata_updated": "2026-03-05", "last_verified": "2026-02-20", "verified_for": "3.0.0rc13", "ignore": false @@ -41,70 +57,130 @@ Example (excerpt): } ``` - > [!NOTE] -> **Tier** is intentionally optional and is not auto-populated. +> `tier` is intentionally optional and is not auto-populated. -### Markdown files (`.md`) +### Markdown (`.md`) -The tool reads/writes YAML frontmatter at the top of the file: +The tool reads/writes YAML frontmatter at the top of the file (if present): -``` +```yaml --- deeplabcut: - last_git_updated: 2026-03-05 + last_content_updated: 2020-01-01 + last_metadata_updated: 2026-03-05 last_verified: 2026-02-20 verified_for: 3.0.0rc13 ignore: false --- ``` -If a doc page has no frontmatter, the tool can still report potential staleness (read-only). -## Usage +If a doc page has **no** frontmatter, the tool can still report staleness (read-only), and `update` can add/modify metadata when explicitly requested. -### Report (read-only) +--- -``` +## The metadata-commit marker (critical) + +Because metadata updates and notebook normalization can rewrite files, they would normally make git think the file was β€œupdated now”. + +To preserve a meaningful **`last_content_updated`**, **all metadata-only / normalization commits must include the marker**: + +- **Marker**: `META_COMMIT_MARKER` (see `tools/docs_and_notebooks_check.py`) +- **Suggested commit message**: `SUGGESTED_META_COMMIT_MESSAGE` + +When you run `update --write` or `normalize --write`, the tool will: + +- require `--ack-meta-commit-marker` (guardrail) +- print a suggested commit message + +--- + +## Commands + +### 1) Report (read-only) + +Generate a report (does not modify files): + +```bash python tools/docs_and_notebooks_check.py report ``` -Writes (by default): +Writes (by default): -- `tmp/docs_nb_checks/docs_nb_checks.json` +- `tmp/docs_nb_checks/docs_nb_checks.json` - `tmp/docs_nb_checks/docs_nb_checks.md` -### Check (read-only, may fail) +### 2) Check (read-only; may fail) -Check only fails based on allowlists in `tools/docs_and_notebooks_report_config.yml`: +Run policy checks. By default, CI will not fail unless allowlists are configured. -``` +```bash python tools/docs_and_notebooks_check.py check ``` -By default these are empty, so CI will not fail. +The allowlists live in `tools/docs_and_notebooks_report_config.yml` (start empty; ratchet later). -### Update (write mode) +### 3) Update metadata (write mode; explicit intent) > [!WARNING] -> This mode updates files in-place and should be used with caution. It is intended to be run manually by maintainers, not in CI. +> `update --write` modifies tracked files. Intended for maintainers (manual), not CI. + +#### 3a) Set `last_content_updated` from git (excluding meta commits) + +```bash +python tools/docs_and_notebooks_check.py update --write --set-content-date-from-git --ack-meta-commit-marker +``` + +#### 3b) Set verification fields (human-controlled) + +```bash +python tools/docs_and_notebooks_check.py update --write --targets docs/page.md examples/JUPYTER/foo.ipynb --set-last-verified today --set-verified-for 3.0.0rc13 --ack-meta-commit-marker +``` + +> Tip: omit `--targets` to operate on all scanned files. +### 4) Normalize notebooks (explicit churn) -Update only last_git_updated for all scanned files: -`python tools/docs_and_notebooks_check.py update --write --only-git-date` +> [!WARNING] +> Notebook normalization rewrites the notebook JSON into a canonical form. +> This is why it is a separate command. -Set verification metadata for specific target files: +Dry-run (shows which files *would* change): +```bash +python tools/docs_and_notebooks_check.py normalize --targets docs/notebook.ipynb ``` -python tools/docs_and_notebooks_check.py update --write --targets examples/JUPYTER/foo.ipynb \ - --set-last-verified today --set-verified-for 3.0.0rc13 + +Write: + +```bash +python tools/docs_and_notebooks_check.py normalize --write --targets docs/notebook.ipynb --ack-meta-commit-marker ``` -### CI integration +--- + +## CI integration + +Recommended CI usage: + +- Run `report` on PRs and upload the outputs as artifacts. +- Run `check` once allowlists are populated (start empty to avoid failures). + +> [!IMPORTANT] +> Use `actions/checkout` with `fetch-depth: 0` (or sufficiently deep) so `git log` sees history; shallow clones can cause missing or fallback timestamps. + +Dependencies required for this tool (install in the CI job): + +```bash +pip install pydantic pyyaml nbformat +``` + +--- + +## Troubleshooting -Add a CI step that runs: -`python tools/docs_and_notebooks_check.py report` -and uploads the outputs as artifacts. -Optionally run check once allowlists are populated. +- If you see `content_date_fallback_to_git_touched`, it usually means one of: + - the checkout history is too shallow, or + - *all* commits touching the file are metadata commits with the marker. -Important: Ensure actions/checkout uses a non-shallow clone (fetch-depth: 0) so git log -can compute last_git_updated reliably. \ No newline at end of file +- If Pydantic raises `class-not-fully-defined` errors, ensure the tool calls `.model_rebuild()` for its models (this is already done in the tool). From dc10a6f0ce4760d46c6ffc6b32cd7aa149cbf55e Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Fri, 6 Mar 2026 14:23:11 +0100 Subject: [PATCH 16/37] Update docs/notebooks workflow: deps & timeout Reduce job timeout from 10 to 5 minutes, upgrade actions/checkout to v6 and actions/setup-python to v6, and allow the staleness policy check step to continue-on-error. These changes use newer action releases, shorten runtime limits, and ensure the optional gate doesn't fail the workflow. --- .github/workflows/docs_and_notebooks_checks.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs_and_notebooks_checks.yml b/.github/workflows/docs_and_notebooks_checks.yml index 91eb830fff..8bcf3b5599 100644 --- a/.github/workflows/docs_and_notebooks_checks.yml +++ b/.github/workflows/docs_and_notebooks_checks.yml @@ -13,16 +13,16 @@ jobs: staleness: name: Docs and notebooks scan (read-only) runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 5 steps: - name: Checkout repository (full history for git dates) - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.12" @@ -41,6 +41,7 @@ jobs: # Optional: run check mode (will fail only once you populate allowlists in config) - name: Run staleness policy check (optional gate) + continue-on-error: true run: | python tools/docs_and_notebooks_check.py \ --config tools/docs_and_notebooks_report_config.yml \ From b52ccb3ad540dd3333c600df888e770ce52ca990 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Fri, 6 Mar 2026 14:35:06 +0100 Subject: [PATCH 17/37] Add pydantic>2 dependency Include pydantic>2 in pyproject.toml dependencies to require Pydantic v2+ for project data models/validation and ensure compatibility with code expecting Pydantic v2 behavior. --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 51de8a2532..3bcac671ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ dependencies = [ "pillow>=7.1", "pycocotools", "pyyaml", + "pydantic>2", "ruamel-yaml>=0.15", "scikit-image>=0.17", "scikit-learn>=1", From a7a8f808daa31a992d68d9d93aeaf3ea1880dc5a Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Fri, 6 Mar 2026 14:46:21 +0100 Subject: [PATCH 18/37] Detect missing vs invalid DLC metadata Differentiate between absent and invalid DLC metadata in notebooks and markdown files. read_ipynb_meta now returns a has_dlc flag; parse_dlc_meta returns (meta, valid). scan_files uses the new flags to set rec.meta and append explicit warnings "missing_metadata" or "invalid_metadata" instead of treating all None as missing. Call sites in update_files and normalize_notebooks updated to unpack the extra return value. Misc cleanup around frontmatter handling and error/warning reporting. --- tools/docs_and_notebooks_check.py | 69 ++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/tools/docs_and_notebooks_check.py b/tools/docs_and_notebooks_check.py index 61c0c13218..6d5ed5ab4b 100644 --- a/tools/docs_and_notebooks_check.py +++ b/tools/docs_and_notebooks_check.py @@ -328,17 +328,21 @@ def dump_md_frontmatter(frontmatter: dict, body: str) -> str: return "---\n" + fm_text + "---\n" + body_to_write -def read_ipynb_meta(path: Path) -> tuple[Any, dict]: +def read_ipynb_meta(path: Path) -> tuple[Any, dict, bool]: """ - Read a notebook using nbformat. Returns (notebook_node, deeplabcut_meta_dict). + Read a notebook using nbformat. + Returns (notebook_node, deeplabcut_meta_dict, has_dlc_namespace). """ nb = nbformat.read(str(path), as_version=4) meta = getattr(nb, "metadata", {}) or {} + has_dlc = DLC_NAMESPACE in meta + dlc_meta = meta.get(DLC_NAMESPACE, {}) if not isinstance(dlc_meta, dict): dlc_meta = {} - return nb, dlc_meta + + return nb, dlc_meta, has_dlc def notebook_is_normalized(path: Path, nb: Any) -> bool: original = path.read_text(encoding="utf-8") @@ -362,17 +366,14 @@ def write_ipynb_meta(path: Path, nb: Any) -> None: path.write_text(text + "\n", encoding="utf-8") -def parse_dlc_meta(raw: Any) -> Optional[DLCMeta]: - if raw is None: - return None - if isinstance(raw, dict): - try: - return DLCMeta.model_validate(raw) # pydantic v2 - except AttributeError: - return DLCMeta.parse_obj(raw) # pydantic v1 - except ValidationError: - return None - return None +def parse_dlc_meta(raw: Any) -> tuple[Optional[DLCMeta], bool]: + # returns (meta, valid) + if raw is None or not isinstance(raw, dict): + return None, False + try: + return DLCMeta.model_validate(raw), True + except ValidationError: + return None, False def meta_to_jsonable(meta: DLCMeta) -> dict: @@ -437,27 +438,48 @@ def scan_files(repo_root: Path, cfg: ToolConfig, targets: Optional[List[str]] = try: if kind == "ipynb": - nb, raw_meta = read_ipynb_meta(p) + nb, raw_meta, has_dlc = read_ipynb_meta(p) + try: nbformat.validate(nb) except NotebookValidationError as e: rec.errors.append(f"nbformat_invalid: {e}") - + try: if not notebook_is_normalized(p, nb): rec.warnings.append("notebook_not_normalized") except Exception as e: - # Don't crash scan if a file has encoding/IO oddities rec.errors.append(f"notebook_normalization_check_failed: {e}") - rec.meta = parse_dlc_meta(raw_meta) + if not has_dlc: + rec.meta = None + rec.warnings.append("missing_metadata") + else: + rec.meta, valid = parse_dlc_meta(raw_meta) + if not valid: + rec.meta = None + rec.warnings.append("invalid_metadata") + elif kind == "md": text = p.read_text(encoding="utf-8") fm, _body = read_md_frontmatter(text) - raw = (fm or {}).get(DLC_NAMESPACE) - rec.meta = parse_dlc_meta(raw) + fm = fm or {} + + has_dlc = DLC_NAMESPACE in fm + raw = fm.get(DLC_NAMESPACE) + + if not has_dlc: + rec.meta = None + rec.warnings.append("missing_metadata") + else: + rec.meta, valid = parse_dlc_meta(raw) + if not valid: + rec.meta = None + rec.warnings.append("invalid_metadata") + else: rec.meta = None + except Exception as e: rec.errors.append(f"metadata_read_failed: {e}") @@ -478,9 +500,6 @@ def scan_files(repo_root: Path, cfg: ToolConfig, targets: Optional[List[str]] = elif rec.days_since_verified is not None and rec.days_since_verified > pol.warn_if_verified_older_than_days: rec.warnings.append(f"verified_stale>{pol.warn_if_verified_older_than_days}d") - if kind in {"ipynb", "md"} and rec.meta is None: - rec.warnings.append("missing_metadata") - records.append(rec) return records @@ -547,7 +566,7 @@ def update_files( changed = False if rec.kind == "ipynb": - nb, _raw = read_ipynb_meta(abs_path) + nb, _raw, _has_dlc = read_ipynb_meta(abs_path) nb_meta = nb.setdefault("metadata", {}) prev = nb_meta.get(DLC_NAMESPACE, {}) if not isinstance(prev, dict): @@ -609,7 +628,7 @@ def normalize_notebooks( abs_path = repo_root / rec.path try: - nb, _raw = read_ipynb_meta(abs_path) + nb, _raw, _has_dlc = read_ipynb_meta(abs_path) nbformat.validate(nb) if not notebook_is_normalized(abs_path, nb): From 29f6d24785c82e910dc1c1617d69f9c5c0253773 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Fri, 6 Mar 2026 14:46:30 +0100 Subject: [PATCH 19/37] Add tests for notebook DLC metadata warnings Add two tests to verify notebook metadata validation: one ensures a notebook missing the "deeplabcut" namespace triggers a "missing_metadata" warning and leaves meta as None; the other ensures an invalid "deeplabcut" namespace (bad last_verified value) triggers an "invalid_metadata" warning and leaves meta as None. Both tests create minimal notebooks in a temp git repo, commit them, run the tool scan, and assert the expected warnings and record kinds. --- .../test_check_contracts.py | 70 ++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/tests/tools/docs_and_notebooks_checks/test_check_contracts.py b/tests/tools/docs_and_notebooks_checks/test_check_contracts.py index 34600df5a8..6940eb3423 100644 --- a/tests/tools/docs_and_notebooks_checks/test_check_contracts.py +++ b/tests/tools/docs_and_notebooks_checks/test_check_contracts.py @@ -360,4 +360,72 @@ def test_write_outputs_contract(tool, tmp_path: Path): payload = json.loads(json_path.read_text(encoding="utf-8")) assert payload["schema_version"] == tool.SCHEMA_VERSION assert "records" in payload and isinstance(payload["records"], list) - assert md_path.read_text(encoding="utf-8").startswith("#") \ No newline at end of file + assert md_path.read_text(encoding="utf-8").startswith("#") + + +def test_notebook_missing_dlc_namespace_warns_missing_metadata(tool, tmp_path: Path): + repo = tmp_path / "repo" + repo.mkdir() + _git_init(repo) + + rel = "docs/nbs/nb.ipynb" + # Valid minimal notebook, but no "deeplabcut" namespace under metadata + nb = ( + '{\n' + ' "cells": [],\n' + ' "metadata": {},\n' + ' "nbformat": 4,\n' + ' "nbformat_minor": 5\n' + '}\n' + ) + _write(repo, rel, nb) + _git_commit(repo, "docs: add notebook", "2020-01-01T12:00:00+00:00") + + cfg = tool.ToolConfig( + version=1, + scan=tool.ScanConfig(include=[rel], exclude=[]), + policy=tool.PolicyConfig(), + ) + + records = tool.scan_files(repo, cfg, targets=[rel]) + assert len(records) == 1 + r = records[0] + assert r.kind == "ipynb" + assert "missing_metadata" in r.warnings + assert r.meta is None + + +def test_notebook_invalid_dlc_namespace_warns_invalid_metadata(tool, tmp_path: Path): + repo = tmp_path / "repo" + repo.mkdir() + _git_init(repo) + + rel = "docs/nbs/nb.ipynb" + # deeplabcut namespace exists but is invalid: last_verified must be a date + nb = ( + '{\n' + ' "cells": [],\n' + ' "metadata": {\n' + ' "deeplabcut": {\n' + ' "last_verified": "not-a-date"\n' + ' }\n' + ' },\n' + ' "nbformat": 4,\n' + ' "nbformat_minor": 5\n' + '}\n' + ) + _write(repo, rel, nb) + _git_commit(repo, "docs: add notebook with bad meta", "2020-01-01T12:00:00+00:00") + + cfg = tool.ToolConfig( + version=1, + scan=tool.ScanConfig(include=[rel], exclude=[]), + policy=tool.PolicyConfig(), + ) + + records = tool.scan_files(repo, cfg, targets=[rel]) + assert len(records) == 1 + r = records[0] + assert r.kind == "ipynb" + assert "invalid_metadata" in r.warnings + assert r.meta is None From 37a0887b9d7a7be5a7596d6d58caccd2c29c83f0 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Fri, 6 Mar 2026 15:08:38 +0100 Subject: [PATCH 20/37] Install dev extras in CI and update dev deps Change GitHub Actions test step to install the package with development extras (pip install -e .[dev]) instead of only pytest. Update pyproject.toml dev dependency group by adding pydantic>2 and nbformat>5 and removing black so CI/tests have the required dev libraries available. --- .github/workflows/python-package.yml | 2 +- pyproject.toml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index ef9cf1dd12..781512863d 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -81,7 +81,7 @@ jobs: - name: Run pytest tests shell: bash -el {0} # Important: activates the conda environment run: | - pip install --no-cache-dir pytest + pip install --no-cache-dir -e .[dev] python -m pytest - name: Run functional tests diff --git a/pyproject.toml b/pyproject.toml index 3bcac671ef..7b31865649 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -110,7 +110,8 @@ Documentation = "https://deeplabcut.github.io/DeepLabCut/README.html" [dependency-groups] dev = [ - "black", + "pydantic>2", + "nbformat>5", "coverage", "pytest", "pytest-cov", From 2e7847f8c5fdf9f862f7d02d2336f7cd3f4ad873 Mon Sep 17 00:00:00 2001 From: C-Achard Date: Fri, 6 Mar 2026 20:01:23 +0100 Subject: [PATCH 21/37] Add --no-step-summary flag and update summary Introduce a --no-step-summary CLI flag and use it in the GitHub Actions workflow to prevent writing to GITHUB_STEP_SUMMARY during the docs/notebooks check job. The script now respects args.no_step_summary when deciding whether to emit the step summary. Also adjust the summary writing behavior to include the full markdown content (previous truncation to 220 lines was removed/commented) when writing is enabled. --- .../workflows/docs_and_notebooks_checks.yml | 3 +- tools/docs_and_notebooks_check.py | 219 +++++++++++++----- 2 files changed, 165 insertions(+), 57 deletions(-) diff --git a/.github/workflows/docs_and_notebooks_checks.yml b/.github/workflows/docs_and_notebooks_checks.yml index 8bcf3b5599..b35b68eda7 100644 --- a/.github/workflows/docs_and_notebooks_checks.yml +++ b/.github/workflows/docs_and_notebooks_checks.yml @@ -46,6 +46,7 @@ jobs: python tools/docs_and_notebooks_check.py \ --config tools/docs_and_notebooks_report_config.yml \ --out-dir tmp/docs_nb_checks \ + --no-step-summary \ check - name: Upload staleness artifacts @@ -55,4 +56,4 @@ jobs: path: | tmp/docs_nb_checks/*.json tmp/docs_nb_checks/*.md - if-no-files-found: error \ No newline at end of file + if-no-files-found: error diff --git a/tools/docs_and_notebooks_check.py b/tools/docs_and_notebooks_check.py index 6d5ed5ab4b..e74325e224 100644 --- a/tools/docs_and_notebooks_check.py +++ b/tools/docs_and_notebooks_check.py @@ -44,11 +44,11 @@ Configuration ------------- -Uses tools/docs_and_notebooks_report_config.yml by default. +Uses tools/docs_and_notebooks_report_config.yml by default. -Outputs -------- -- docs_nb_checks.json: machine-readable report +Outputs +------- +- docs_nb_checks.json: machine-readable report - docs_nb_checks.md: human-readable summary Notes for CI @@ -57,9 +57,9 @@ otherwise git log may not see history. - Requires: - pydantic - - PyYAML + - PyYAML - nbformat - to be installed in the environment. + to be installed in the environment. Recommended : install in CI job directly (pip install pydantic pyyaml nbformat) rather than adding to requirements, since these are only needed for this tool. """ # tools/docs_and_notebooks_check.py @@ -81,7 +81,7 @@ yaml = None try: - from pydantic import BaseModel, Field, ValidationError, ConfigDict + from pydantic import BaseModel, ConfigDict, Field, ValidationError except Exception: # pragma: no cover raise RuntimeError("Pydantic is required to run this script") try: @@ -94,7 +94,7 @@ DLC_NAMESPACE = "deeplabcut" OUTPUT_FILENAME = "docs_nb_checks" SCRIPT_DIR = Path(__file__).resolve().parent -DEFAULT_CFG = (SCRIPT_DIR / "docs_and_notebooks_report_config.yml") +DEFAULT_CFG = SCRIPT_DIR / "docs_and_notebooks_report_config.yml" # ----------------------------- @@ -112,10 +112,11 @@ # Pydantic schemas # ----------------------------- + class DLCMeta(BaseModel): """Metadata embedded in files under the `deeplabcut` namespace.""" - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra="allow") # Tool-managed: last meaningful content update date (excluding metadata commits) last_content_updated: Optional[date] = None @@ -145,7 +146,7 @@ class PolicyConfig(BaseModel): # Allowlists for strict checks (start empty; ratchet later) require_metadata: List[str] = Field(default_factory=list) require_recent_verification: List[str] = Field(default_factory=list) - + require_notebook_normalized: List[str] = Field(default_factory=list) @@ -154,6 +155,7 @@ class ToolConfig(BaseModel): scan: ScanConfig policy: PolicyConfig + class FileRecord(BaseModel): path: str kind: str # ipynb | md | other @@ -186,6 +188,7 @@ class Report(BaseModel): totals: Dict[str, int] records: List[FileRecord] + # Rebuild models due to __future__ annotations DLCMeta.model_rebuild() ScanConfig.model_rebuild() @@ -197,6 +200,7 @@ class Report(BaseModel): # Helpers # ----------------------------- + def _iso_today() -> date: return datetime.now(timezone.utc).date() @@ -257,13 +261,17 @@ def _parse_git_iso_date(out: str) -> Optional[date]: def git_last_touched(repo_root: Path, rel_path: str) -> Optional[date]: - code, out, _err = _run_git(["log", "-1", "--format=%cI", "--", rel_path], cwd=repo_root) + code, out, _err = _run_git( + ["log", "-1", "--format=%cI", "--", rel_path], cwd=repo_root + ) if code != 0: return None return _parse_git_iso_date(out) -def git_last_content_updated(repo_root: Path, rel_path: str) -> Tuple[Optional[date], bool]: +def git_last_content_updated( + repo_root: Path, rel_path: str +) -> Tuple[Optional[date], bool]: """ Return (date, used_fallback). @@ -344,6 +352,7 @@ def read_ipynb_meta(path: Path) -> tuple[Any, dict, bool]: return nb, dlc_meta, has_dlc + def notebook_is_normalized(path: Path, nb: Any) -> bool: original = path.read_text(encoding="utf-8") # Normalize newline style so CRLF vs LF differences do not cause false mismatches @@ -351,6 +360,7 @@ def notebook_is_normalized(path: Path, nb: Any) -> bool: normalized = nbformat.writes(nb, version=4, indent=2, ensure_ascii=False) + "\n" return original_normalized == normalized + def write_ipynb_meta(path: Path, nb: Any) -> None: """ Write a notebook using nbformat. @@ -366,6 +376,7 @@ def write_ipynb_meta(path: Path, nb: Any) -> None: path.write_text(text + "\n", encoding="utf-8") + def parse_dlc_meta(raw: Any) -> tuple[Optional[DLCMeta], bool]: # returns (meta, valid) if raw is None or not isinstance(raw, dict): @@ -402,6 +413,7 @@ def match_allowlist(rel_path: str, allowlist: List[str]) -> bool: # Core scanning # ----------------------------- + def load_config(config_path: Path) -> ToolConfig: if yaml is None: raise RuntimeError("PyYAML is required (pip install pyyaml)") @@ -409,10 +421,12 @@ def load_config(config_path: Path) -> ToolConfig: try: return ToolConfig.model_validate(raw) # pydantic v2 except AttributeError: - return ToolConfig.parse_obj(raw) # pydantic v1 + return ToolConfig.parse_obj(raw) # pydantic v1 -def scan_files(repo_root: Path, cfg: ToolConfig, targets: Optional[List[str]] = None) -> List[FileRecord]: +def scan_files( + repo_root: Path, cfg: ToolConfig, targets: Optional[List[str]] = None +) -> List[FileRecord]: today = _iso_today() paths = glob_paths(repo_root, cfg.scan.include) records: List[FileRecord] = [] @@ -420,7 +434,6 @@ def scan_files(repo_root: Path, cfg: ToolConfig, targets: Optional[List[str]] = if targets: target_set = set(t.replace(os.sep, "/") for t in targets) - for p in paths: rel = str(p.resolve().relative_to(repo_root)).replace(os.sep, "/") if is_excluded(rel, cfg.scan.exclude): @@ -431,8 +444,12 @@ def scan_files(repo_root: Path, cfg: ToolConfig, targets: Optional[List[str]] = rec = FileRecord(path=rel, kind=kind) rec.last_git_touched = git_last_touched(repo_root, rel) - rec.last_content_updated, used_fallback = git_last_content_updated(repo_root, rel) - rec.days_since_content_update = compute_days_since(rec.last_content_updated, today) + rec.last_content_updated, used_fallback = git_last_content_updated( + repo_root, rel + ) + rec.days_since_content_update = compute_days_since( + rec.last_content_updated, today + ) if used_fallback: rec.warnings.append("content_date_fallback_to_git_touched") @@ -492,13 +509,21 @@ def scan_files(repo_root: Path, cfg: ToolConfig, targets: Optional[List[str]] = pol = cfg.policy - if rec.days_since_content_update is not None and rec.days_since_content_update > pol.warn_if_content_older_than_days: + if ( + rec.days_since_content_update is not None + and rec.days_since_content_update > pol.warn_if_content_older_than_days + ): rec.warnings.append(f"content_stale>{pol.warn_if_content_older_than_days}d") if last_verified is None and pol.missing_last_verified_is_warning: rec.warnings.append("missing_last_verified") - elif rec.days_since_verified is not None and rec.days_since_verified > pol.warn_if_verified_older_than_days: - rec.warnings.append(f"verified_stale>{pol.warn_if_verified_older_than_days}d") + elif ( + rec.days_since_verified is not None + and rec.days_since_verified > pol.warn_if_verified_older_than_days + ): + rec.warnings.append( + f"verified_stale>{pol.warn_if_verified_older_than_days}d" + ) records.append(rec) @@ -523,6 +548,7 @@ def _require_meta_marker_ack(write: bool, ack_marker: bool) -> None: f" {SUGGESTED_META_COMMIT_MESSAGE}\n" ) + def update_files( repo_root: Path, cfg: ToolConfig, @@ -577,7 +603,9 @@ def update_files( nb_meta[DLC_NAMESPACE] = merged changed = True if write: - _require_meta_marker_ack(write=True, ack_marker=ack_meta_commit_marker) + _require_meta_marker_ack( + write=True, ack_marker=ack_meta_commit_marker + ) write_ipynb_meta(abs_path, nb) elif rec.kind == "md": @@ -593,7 +621,9 @@ def update_files( fm[DLC_NAMESPACE] = merged changed = True if write: - _require_meta_marker_ack(write=True, ack_marker=ack_meta_commit_marker) + _require_meta_marker_ack( + write=True, ack_marker=ack_meta_commit_marker + ) abs_path.write_text(dump_md_frontmatter(fm, body), encoding="utf-8") rec.would_change = changed @@ -602,6 +632,7 @@ def update_files( return records + # ----------------------------- # Notebook formatting # ----------------------------- @@ -658,19 +689,29 @@ def normalize_notebooks( return records + # ----------------------------- # Output formatting # ----------------------------- + def summarize(records: List[FileRecord]) -> Dict[str, int]: return { "files": len(records), "warnings": sum(1 for r in records if r.warnings), "errors": sum(1 for r in records if r.errors), "missing_metadata": sum(1 for r in records if "missing_metadata" in r.warnings), - "missing_last_verified": sum(1 for r in records if "missing_last_verified" in r.warnings), - "git_stale": sum(1 for r in records if any(w.startswith("git_stale") for w in r.warnings)), - "verified_stale": sum(1 for r in records if any(w.startswith("verified_stale") for w in r.warnings)), + "missing_last_verified": sum( + 1 for r in records if "missing_last_verified" in r.warnings + ), + "git_stale": sum( + 1 for r in records if any(w.startswith("git_stale") for w in r.warnings) + ), + "verified_stale": sum( + 1 + for r in records + if any(w.startswith("verified_stale") for w in r.warnings) + ), } @@ -689,14 +730,26 @@ def to_markdown(report: Report, cfg: ToolConfig) -> str: lines.append(f"- Files with errors: **{t['errors']}**\n") lines.append(f"- Missing metadata: **{t['missing_metadata']}**\n") lines.append(f"- Missing last_verified: **{t['missing_last_verified']}**\n") - lines.append(f"- Git-stale (> {pol.warn_if_content_older_than_days}d): **{t['git_stale']}**\n") - lines.append(f"- Verification-stale (> {pol.warn_if_verified_older_than_days}d): **{t['verified_stale']}**\n\n") + lines.append( + f"- Git-stale (> {pol.warn_if_content_older_than_days}d): **{t['git_stale']}**\n" + ) + lines.append( + f"- Verification-stale (> {pol.warn_if_verified_older_than_days}d): **{t['verified_stale']}**\n\n" + ) def fmt_date(d: Optional[date]) -> str: return d.isoformat() if d else "-" - warn_recs = [r for r in report.records if r.warnings and not (r.meta and r.meta.ignore)] - warn_recs.sort(key=lambda r: (-(r.days_since_verified or -1), -(r.days_since_content_update or -1), r.path)) + warn_recs = [ + r for r in report.records if r.warnings and not (r.meta and r.meta.ignore) + ] + warn_recs.sort( + key=lambda r: ( + -(r.days_since_verified or -1), + -(r.days_since_content_update or -1), + r.path, + ) + ) if warn_recs: lines.append("## Warnings\n") @@ -710,10 +763,14 @@ def fmt_date(d: Optional[date]) -> str: if r.last_git_touched: lines.append(f" - last_git_touched: {fmt_date(r.last_git_touched)}\n") if meta and meta.last_metadata_updated: - lines.append(f" - last_metadata_updated: {fmt_date(meta.last_metadata_updated)}\n") + lines.append( + f" - last_metadata_updated: {fmt_date(meta.last_metadata_updated)}\n" + ) lv = meta.last_verified if meta else None - lines.append(f" - last_verified: {fmt_date(lv)} " - f"(days: {r.days_since_verified if r.days_since_verified is not None else '-'})\n") + lines.append( + f" - last_verified: {fmt_date(lv)} " + f"(days: {r.days_since_verified if r.days_since_verified is not None else '-'})\n" + ) if meta and meta.verified_for: lines.append(f" - verified_for: {meta.verified_for}\n") if meta and meta.tier: @@ -731,8 +788,12 @@ def fmt_date(d: Optional[date]) -> str: lines.append("\n") lines.append("## Notes\n") - lines.append("- 'Out of date' does not necessarily mean 'broken'. Use this as a triage signal.\n") - lines.append("- last_git_updated is computed from git history. last_verified is human-controlled.\n\n") + lines.append( + "- 'Out of date' does not necessarily mean 'broken'. Use this as a triage signal.\n" + ) + lines.append( + "- last_git_updated is computed from git history. last_verified is human-controlled.\n\n" + ) return "".join(lines) @@ -744,9 +805,11 @@ def write_outputs(report: Report, cfg: ToolConfig, out_dir: Path) -> Tuple[Path, try: payload = report.model_dump(mode="json") # pydantic v2 except AttributeError: - payload = json.loads(report.json()) # pydantic v1 + payload = json.loads(report.json()) # pydantic v1 - json_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + json_path.write_text( + json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" + ) md_path.write_text(to_markdown(report, cfg), encoding="utf-8") return json_path, md_path @@ -755,6 +818,7 @@ def write_outputs(report: Report, cfg: ToolConfig, out_dir: Path) -> Tuple[Path, # Check enforcement # ----------------------------- + def enforce(cfg: ToolConfig, records: List[FileRecord]) -> List[str]: pol = cfg.policy violations: List[str] = [] @@ -776,12 +840,18 @@ def enforce(cfg: ToolConfig, records: List[FileRecord]) -> List[str]: else: days = (today - lv).days if days > pol.warn_if_verified_older_than_days: - violations.append(f"{r.path}: last_verified is {days}d old " - f"(> {pol.warn_if_verified_older_than_days}d)") - - if r.kind == "ipynb" and match_allowlist(r.path, pol.require_notebook_normalized): + violations.append( + f"{r.path}: last_verified is {days}d old " + f"(> {pol.warn_if_verified_older_than_days}d)" + ) + + if r.kind == "ipynb" and match_allowlist( + r.path, pol.require_notebook_normalized + ): if "notebook_not_normalized" in (r.warnings or []): - violations.append(f"{r.path}: notebook is not normalized (run update/format)") + violations.append( + f"{r.path}: notebook is not normalized (run update/format)" + ) return violations @@ -790,6 +860,7 @@ def enforce(cfg: ToolConfig, records: List[FileRecord]) -> List[str]: # CLI # ----------------------------- + def parse_date_token(token: str) -> date: token = token.strip().lower() if token in {"today", "now"}: @@ -798,9 +869,20 @@ def parse_date_token(token: str) -> date: def main(argv: Optional[Sequence[str]] = None) -> int: - parser = argparse.ArgumentParser(description="DeepLabCut checks tool (docs + notebooks)") - parser.add_argument("--config", default=str(DEFAULT_CFG), help="Path to YAML config file") - parser.add_argument("--out-dir", default=f"tmp/{OUTPUT_FILENAME}", help="Directory to write outputs") + parser = argparse.ArgumentParser( + description="DeepLabCut checks tool (docs + notebooks)" + ) + parser.add_argument( + "--config", default=str(DEFAULT_CFG), help="Path to YAML config file" + ) + parser.add_argument( + "--no-step-summary", + action="store_true", + help="Do not write to GITHUB_STEP_SUMMARY", + ) + parser.add_argument( + "--out-dir", default=f"tmp/{OUTPUT_FILENAME}", help="Directory to write outputs" + ) sub = parser.add_subparsers(dest="cmd", required=True) rep = sub.add_parser("report", help="Generate staleness report (read-only)") @@ -810,21 +892,31 @@ def main(argv: Optional[Sequence[str]] = None) -> int: help="Optional list of relative file paths to scan (limits scan to these files)", ) - chk = sub.add_parser("check", help="Run policy checks (read-only; may exit non-zero)") + chk = sub.add_parser( + "check", help="Run policy checks (read-only; may exit non-zero)" + ) chk.add_argument( "--targets", nargs="*", help="Optional list of relative file paths to scan (limits scan to these files)", ) - up = sub.add_parser("update", help="Update metadata/frontmatter (write mode requires --write)") - up.add_argument("--write", action="store_true", help="Actually write changes (otherwise dry-run)") + up = sub.add_parser( + "update", help="Update metadata/frontmatter (write mode requires --write)" + ) + up.add_argument( + "--write", + action="store_true", + help="Actually write changes (otherwise dry-run)", + ) up.add_argument( "--set-content-date-from-git", action="store_true", help="Set embedded last_content_updated from computed git content date", ) - up.add_argument("--targets", nargs="*", help="Optional list of relative file paths to update") + up.add_argument( + "--targets", nargs="*", help="Optional list of relative file paths to update" + ) up.add_argument("--set-last-verified", default=None, help="YYYY-MM-DD or 'today'") up.add_argument("--set-verified-for", default=None, help="String like 3.0.0rc13") up.add_argument( @@ -832,10 +924,21 @@ def main(argv: Optional[Sequence[str]] = None) -> int: action="store_true", help=f"Acknowledge that you will commit changes using marker: {META_COMMIT_MARKER}", ) - - norm = sub.add_parser("normalize", help="Normalize notebooks deterministically (write mode requires --write)") - norm.add_argument("--write", action="store_true", help="Actually write changes (otherwise dry-run)") - norm.add_argument("--targets", nargs="*", help="Optional list of relative notebook paths to normalize") + + norm = sub.add_parser( + "normalize", + help="Normalize notebooks deterministically (write mode requires --write)", + ) + norm.add_argument( + "--write", + action="store_true", + help="Actually write changes (otherwise dry-run)", + ) + norm.add_argument( + "--targets", + nargs="*", + help="Optional list of relative notebook paths to normalize", + ) norm.add_argument( "--ack-meta-commit-marker", action="store_true", @@ -853,7 +956,9 @@ def main(argv: Optional[Sequence[str]] = None) -> int: records = scan_files(repo_root, cfg, targets=getattr(args, "targets", None)) elif args.cmd == "update": - lv = parse_date_token(args.set_last_verified) if args.set_last_verified else None + lv = ( + parse_date_token(args.set_last_verified) if args.set_last_verified else None + ) records = update_files( repo_root, cfg, @@ -889,11 +994,13 @@ def main(argv: Optional[Sequence[str]] = None) -> int: json_path, md_path = write_outputs(report, cfg, out_dir) # Emit GitHub Actions job summary if available + emit_summary = not getattr(args, "no_step_summary", False) step_summary = os.environ.get("GITHUB_STEP_SUMMARY") - if step_summary and md_path.exists(): + if emit_summary and step_summary and md_path.exists(): try: content = md_path.read_text(encoding="utf-8") - snippet = "\n".join(content.splitlines()[:220]) + "\n" + # snippet = "\n".join(content.splitlines()[:220]) + "\n" + snippet = "\n".join(content.splitlines()[:]) + "\n" Path(step_summary).write_text(snippet, encoding="utf-8") except Exception: pass @@ -913,4 +1020,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if __name__ == "__main__": - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) From feeef82273db245724bb5caadd090825b738db28 Mon Sep 17 00:00:00 2001 From: C-Achard Date: Fri, 6 Mar 2026 20:18:43 +0100 Subject: [PATCH 22/37] CI: upgrade checkout and adjust pip install Update GitHub Actions workflow to use actions/checkout@v6 and modify the editable install command used in the test step. Replaces "pip install -e .[dev]" with "pip install -e . --group dev" before running pytest, aligning the workflow with the newer checkout action and the revised dependency installation syntax. --- .github/workflows/python-package.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 781512863d..e1a0fe0096 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Free up disk space if: runner.os == 'Linux' @@ -81,7 +81,7 @@ jobs: - name: Run pytest tests shell: bash -el {0} # Important: activates the conda environment run: | - pip install --no-cache-dir -e .[dev] + pip install --no-cache-dir -e . --group dev python -m pytest - name: Run functional tests From 866d705b78fc3f805fc3bf0a084a49729f3d5741 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 10 Mar 2026 13:53:37 +0100 Subject: [PATCH 23/37] Bump CI actions, fix deps and tests formatting Update CI workflows and developer tooling: bump actions/checkout and setup-python usages (checkout@v6, setup-python@v6), upgrade peaceiris/actions-gh-pages to v4, and update codespell workflow. Simplify python-package workflow to install dev extras once using `--group dev` and remove the duplicate install. Adjust pre-commit config to pass args to the name-tests-test hook. Fix dependency declarations in pyproject.toml (move/clean up pydantic and nbformat entries, remove duplicate pydantic line). Reformat tests/tools/docs_and_notebooks_checks/test_check_contracts.py for readability (multi-line calls, string quote consistency) and simplify one assertion. --- .github/workflows/codespell.yml | 2 +- .github/workflows/publish-book.yml | 6 +-- .github/workflows/python-package.yml | 3 +- .pre-commit-config.yaml | 2 +- pyproject.toml | 6 +-- .../test_check_contracts.py | 44 +++++++++++++------ 6 files changed, 39 insertions(+), 24 deletions(-) diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index a758a4fac9..6a35418b64 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Annotate locations with typos uses: codespell-project/codespell-problem-matcher@v1 - name: Codespell diff --git a/.github/workflows/publish-book.yml b/.github/workflows/publish-book.yml index ff7b548ba5..7642b8cfb5 100644 --- a/.github/workflows/publish-book.yml +++ b/.github/workflows/publish-book.yml @@ -9,10 +9,10 @@ jobs: deploy-book: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python 3.10 - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: "3.10" @@ -26,7 +26,7 @@ jobs: jupyter-book build . - name: GitHub Pages action - uses: peaceiris/actions-gh-pages@v3.9.3 + uses: peaceiris/actions-gh-pages@4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./_build/html diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index e1a0fe0096..4b32ecb51b 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -64,7 +64,7 @@ jobs: shell: bash -el {0} # Important: activates the conda environment run: | python -m pip install --upgrade pip setuptools wheel - pip install --no-cache-dir -e . + pip install --no-cache-dir -e . --group dev - name: Install ffmpeg run: | @@ -81,7 +81,6 @@ jobs: - name: Run pytest tests shell: bash -el {0} # Important: activates the conda environment run: | - pip install --no-cache-dir -e . --group dev python -m pytest - name: Run functional tests diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6e64ff6edc..d2bbb8c2a8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,6 +7,7 @@ repos: - id: check-toml - id: end-of-file-fixer - id: name-tests-test + args: [--pytest-test-first] - id: trailing-whitespace - repo: https://github.com/tox-dev/pyproject-fmt rev: v2.15.2 @@ -37,4 +38,3 @@ repos: - "pydantic>=2,<3" - "pyyaml" - "nbformat>=5" - diff --git a/pyproject.toml b/pyproject.toml index 7b31865649..68aa417f3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,8 +45,8 @@ dependencies = [ "pandas[hdf5,performance]>=2.2,<3", "pillow>=7.1", "pycocotools", + "pydantic>=2,<3", "pyyaml", - "pydantic>2", "ruamel-yaml>=0.15", "scikit-image>=0.17", "scikit-learn>=1", @@ -110,9 +110,9 @@ Documentation = "https://deeplabcut.github.io/DeepLabCut/README.html" [dependency-groups] dev = [ - "pydantic>2", - "nbformat>5", "coverage", + "nbformat>5", + "pydantic>2", "pytest", "pytest-cov", ] diff --git a/tests/tools/docs_and_notebooks_checks/test_check_contracts.py b/tests/tools/docs_and_notebooks_checks/test_check_contracts.py index 6940eb3423..cc7195c7ab 100644 --- a/tests/tools/docs_and_notebooks_checks/test_check_contracts.py +++ b/tests/tools/docs_and_notebooks_checks/test_check_contracts.py @@ -34,8 +34,12 @@ def tool() -> ModuleType: # ----------------------------- # Git helpers for a temp repo # ----------------------------- -def _run(cmd: list[str], cwd: Path, env: dict | None = None) -> subprocess.CompletedProcess: - return subprocess.run(cmd, cwd=str(cwd), env=env, capture_output=True, text=True, check=True) +def _run( + cmd: list[str], cwd: Path, env: dict | None = None +) -> subprocess.CompletedProcess: + return subprocess.run( + cmd, cwd=str(cwd), env=env, capture_output=True, text=True, check=True + ) def _git_init(repo: Path) -> None: @@ -90,8 +94,16 @@ def test_git_content_date_skips_meta_commits(tool, tmp_path: Path): _git_commit(repo, "docs: initial content", "2020-01-01T12:00:00+00:00") # meta-only rewrite (simulated) committed with marker - _write(repo, rel, "---\ndeeplabcut:\n last_metadata_updated: 2026-03-01\n---\n# hello\n") - _git_commit(repo, f"chore(meta): update {tool.META_COMMIT_MARKER}", "2026-03-01T12:00:00+00:00") + _write( + repo, + rel, + "---\ndeeplabcut:\n last_metadata_updated: 2026-03-01\n---\n# hello\n", + ) + _git_commit( + repo, + f"chore(meta): update {tool.META_COMMIT_MARKER}", + "2026-03-01T12:00:00+00:00", + ) # raw touched date = 2026-03-01 touched = tool.git_last_touched(repo, rel) @@ -114,7 +126,11 @@ def test_git_content_date_fallback_when_only_meta_commits(tool, tmp_path: Path): rel = "docs/page.md" _write(repo, rel, "---\ndeeplabcut:\n notes: hi\n---\n") - _git_commit(repo, f"chore(meta): init {tool.META_COMMIT_MARKER}", "2026-03-01T12:00:00+00:00") + _git_commit( + repo, + f"chore(meta): init {tool.META_COMMIT_MARKER}", + "2026-03-01T12:00:00+00:00", + ) content_date, used_fallback = tool.git_last_content_updated(repo, rel) assert content_date == date(2026, 3, 1) @@ -294,12 +310,12 @@ def test_normalize_is_explicit_and_marks_would_change(tool, tmp_path: Path): rel = "docs/nbs/nb.ipynb" # Minimal notebook JSON but not in nbformat canonical formatting (indent/newline differences) raw = ( - '{\n' + "{\n" ' "cells": [],\n' ' "metadata": {},\n' ' "nbformat": 4,\n' ' "nbformat_minor": 5\n' - '}\n' + "}\n" ) _write(repo, rel, raw) _git_commit(repo, "docs: add notebook", "2020-01-01T12:00:00+00:00") @@ -321,7 +337,7 @@ def test_normalize_is_explicit_and_marks_would_change(tool, tmp_path: Path): assert len(records) == 1 assert records[0].kind == "ipynb" # may be True depending on canonical formatting differences - assert records[0].would_change in (True, False) + assert records[0].would_change def test_write_outputs_contract(tool, tmp_path: Path): @@ -371,12 +387,12 @@ def test_notebook_missing_dlc_namespace_warns_missing_metadata(tool, tmp_path: P rel = "docs/nbs/nb.ipynb" # Valid minimal notebook, but no "deeplabcut" namespace under metadata nb = ( - '{\n' + "{\n" ' "cells": [],\n' ' "metadata": {},\n' ' "nbformat": 4,\n' ' "nbformat_minor": 5\n' - '}\n' + "}\n" ) _write(repo, rel, nb) _git_commit(repo, "docs: add notebook", "2020-01-01T12:00:00+00:00") @@ -403,16 +419,16 @@ def test_notebook_invalid_dlc_namespace_warns_invalid_metadata(tool, tmp_path: P rel = "docs/nbs/nb.ipynb" # deeplabcut namespace exists but is invalid: last_verified must be a date nb = ( - '{\n' + "{\n" ' "cells": [],\n' ' "metadata": {\n' ' "deeplabcut": {\n' ' "last_verified": "not-a-date"\n' - ' }\n' - ' },\n' + " }\n" + " },\n" ' "nbformat": 4,\n' ' "nbformat_minor": 5\n' - '}\n' + "}\n" ) _write(repo, rel, nb) _git_commit(repo, "docs: add notebook with bad meta", "2020-01-01T12:00:00+00:00") From 0e768189949f6ff80035db85d63d48426a49a4fa Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 10 Mar 2026 13:53:59 +0100 Subject: [PATCH 24/37] Only stamp metadata on write; rename git_stale Only set last_metadata_updated when an actual file write will occur (not unconditionally). For notebooks and markdown files the metadata stamping and merge now happen only when write is performed; when not writing, the in-memory record.meta is still updated so warnings and reports remain accurate. Rename the summary/report field and related text from "git_stale" to "content_stale" and adjust wording from "last_git_updated" to "last_git_touched / last_content_updated". A new import (from curses import meta) was also added. --- tools/docs_and_notebooks_check.py | 32 +++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/tools/docs_and_notebooks_check.py b/tools/docs_and_notebooks_check.py index e74325e224..726f1b4ea8 100644 --- a/tools/docs_and_notebooks_check.py +++ b/tools/docs_and_notebooks_check.py @@ -71,6 +71,7 @@ import os import re import subprocess +from curses import meta from datetime import date, datetime, timezone from pathlib import Path from typing import Any, Dict, List, Optional, Sequence, Tuple @@ -577,10 +578,6 @@ def update_files( if set_content_date_from_git and rec.last_content_updated is not None: meta.last_content_updated = rec.last_content_updated - # Optional: mark maintenance time if we actually write - if write: - meta.last_metadata_updated = today - # Human-controlled verification fields if set_last_verified is not None: meta.last_verified = set_last_verified @@ -603,10 +600,18 @@ def update_files( nb_meta[DLC_NAMESPACE] = merged changed = True if write: + # Only stamp metadata update time if an actual write is needed + meta.last_metadata_updated = today + desired_with_stamp = meta_to_jsonable(meta) + merged = dict(prev) + merged.update(desired_with_stamp) + nb_meta[DLC_NAMESPACE] = merged _require_meta_marker_ack( write=True, ack_marker=ack_meta_commit_marker ) write_ipynb_meta(abs_path, nb) + else: + rec.meta = meta # Update meta in report for accurate warnings even without writing elif rec.kind == "md": text = abs_path.read_text(encoding="utf-8") @@ -621,13 +626,24 @@ def update_files( fm[DLC_NAMESPACE] = merged changed = True if write: + # Only stamp metadata update time if an actual write is needed + meta.last_metadata_updated = today + desired_with_stamp = meta_to_jsonable(meta) + merged = dict(prev) + merged.update(desired_with_stamp) + fm[DLC_NAMESPACE] = merged + _require_meta_marker_ack( write=True, ack_marker=ack_meta_commit_marker ) abs_path.write_text(dump_md_frontmatter(fm, body), encoding="utf-8") + else: + rec.meta = meta # Update meta in report for accurate warnings even without writing rec.would_change = changed rec.meta = meta + if write and changed: + rec.meta = meta rec.days_since_verified = compute_days_since(meta.last_verified, today) return records @@ -704,8 +720,8 @@ def summarize(records: List[FileRecord]) -> Dict[str, int]: "missing_last_verified": sum( 1 for r in records if "missing_last_verified" in r.warnings ), - "git_stale": sum( - 1 for r in records if any(w.startswith("git_stale") for w in r.warnings) + "content_stale": sum( + 1 for r in records if any(w.startswith("content_stale") for w in r.warnings) ), "verified_stale": sum( 1 @@ -731,7 +747,7 @@ def to_markdown(report: Report, cfg: ToolConfig) -> str: lines.append(f"- Missing metadata: **{t['missing_metadata']}**\n") lines.append(f"- Missing last_verified: **{t['missing_last_verified']}**\n") lines.append( - f"- Git-stale (> {pol.warn_if_content_older_than_days}d): **{t['git_stale']}**\n" + f"- Content-stale (> {pol.warn_if_content_older_than_days}d): **{t['content_stale']}**\n" ) lines.append( f"- Verification-stale (> {pol.warn_if_verified_older_than_days}d): **{t['verified_stale']}**\n\n" @@ -792,7 +808,7 @@ def fmt_date(d: Optional[date]) -> str: "- 'Out of date' does not necessarily mean 'broken'. Use this as a triage signal.\n" ) lines.append( - "- last_git_updated is computed from git history. last_verified is human-controlled.\n\n" + "- last_git_touched / last_content_updated are computed from git history. last_verified is human-controlled.\n\n" ) return "".join(lines) From 336b6acf364e8f58679a777de02e621f218dc6d6 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 10 Mar 2026 13:54:47 +0100 Subject: [PATCH 25/37] Defer stamping last_metadata_updated until write Build the desired metadata without mutating last_metadata_updated and use a base "desired_base" when comparing/merging. Only set meta.last_metadata_updated (and produce the final merged metadata) if an actual file write will occur. Apply the same logic to both notebooks and markdown frontmatter, simplify variable names, and remove redundant in-branch rec.meta assignments so the record is consistently updated once at the end. --- tools/docs_and_notebooks_check.py | 57 +++++++++++++++---------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/tools/docs_and_notebooks_check.py b/tools/docs_and_notebooks_check.py index 726f1b4ea8..373aadd302 100644 --- a/tools/docs_and_notebooks_check.py +++ b/tools/docs_and_notebooks_check.py @@ -574,17 +574,16 @@ def update_files( meta = rec.meta or DLCMeta() - # Tool-managed: optionally set last_content_updated from computed git value + # Build the desired metadata WITHOUT touching last_metadata_updated. if set_content_date_from_git and rec.last_content_updated is not None: meta.last_content_updated = rec.last_content_updated - # Human-controlled verification fields if set_last_verified is not None: meta.last_verified = set_last_verified if set_verified_for is not None: meta.verified_for = set_verified_for - desired = meta_to_jsonable(meta) + desired_base = meta_to_jsonable(meta) abs_path = repo_root / rec.path changed = False @@ -594,56 +593,54 @@ def update_files( prev = nb_meta.get(DLC_NAMESPACE, {}) if not isinstance(prev, dict): prev = {} - merged = dict(prev) - merged.update(desired) - if merged != prev: - nb_meta[DLC_NAMESPACE] = merged + + merged_base = dict(prev) + merged_base.update(desired_base) + + if merged_base != prev: changed = True if write: - # Only stamp metadata update time if an actual write is needed - meta.last_metadata_updated = today - desired_with_stamp = meta_to_jsonable(meta) - merged = dict(prev) - merged.update(desired_with_stamp) - nb_meta[DLC_NAMESPACE] = merged _require_meta_marker_ack( write=True, ack_marker=ack_meta_commit_marker ) + + meta.last_metadata_updated = today + desired_final = meta_to_jsonable(meta) + + merged_final = dict(prev) + merged_final.update(desired_final) + nb_meta[DLC_NAMESPACE] = merged_final write_ipynb_meta(abs_path, nb) - else: - rec.meta = meta # Update meta in report for accurate warnings even without writing elif rec.kind == "md": text = abs_path.read_text(encoding="utf-8") fm, body = read_md_frontmatter(text) fm = fm or {} + prev = fm.get(DLC_NAMESPACE, {}) if not isinstance(prev, dict): prev = {} - merged = dict(prev) - merged.update(desired) - if merged != prev: - fm[DLC_NAMESPACE] = merged + + merged_base = dict(prev) + merged_base.update(desired_base) + + if merged_base != prev: changed = True if write: - # Only stamp metadata update time if an actual write is needed - meta.last_metadata_updated = today - desired_with_stamp = meta_to_jsonable(meta) - merged = dict(prev) - merged.update(desired_with_stamp) - fm[DLC_NAMESPACE] = merged - _require_meta_marker_ack( write=True, ack_marker=ack_meta_commit_marker ) + + meta.last_metadata_updated = today + desired_final = meta_to_jsonable(meta) + + merged_final = dict(prev) + merged_final.update(desired_final) + fm[DLC_NAMESPACE] = merged_final abs_path.write_text(dump_md_frontmatter(fm, body), encoding="utf-8") - else: - rec.meta = meta # Update meta in report for accurate warnings even without writing rec.would_change = changed rec.meta = meta - if write and changed: - rec.meta = meta rec.days_since_verified = compute_days_since(meta.last_verified, today) return records From ca9146954f978e8cd29ddf3cf08b1a2569a202d5 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 10 Mar 2026 14:00:33 +0100 Subject: [PATCH 26/37] Simplify notebook writes Drop an unused/errant `from curses import meta` import and remove a redundant pre-metadata call to `write_ipynb_meta`. Notebooks are now written once after merging/updating DLC metadata (comment updated accordingly), reducing unnecessary I/O and avoiding potential name conflicts. --- tools/docs_and_notebooks_check.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tools/docs_and_notebooks_check.py b/tools/docs_and_notebooks_check.py index 373aadd302..888fbdda92 100644 --- a/tools/docs_and_notebooks_check.py +++ b/tools/docs_and_notebooks_check.py @@ -71,7 +71,6 @@ import os import re import subprocess -from curses import meta from datetime import date, datetime, timezone from pathlib import Path from typing import Any, Dict, List, Optional, Sequence, Tuple @@ -678,9 +677,6 @@ def normalize_notebooks( if not notebook_is_normalized(abs_path, nb): rec.would_change = True if write: - # Rewrite notebook in canonical form - write_ipynb_meta(abs_path, nb) - # Update embedded maintenance timestamp meta = rec.meta or DLCMeta() meta.last_metadata_updated = today @@ -693,7 +689,7 @@ def normalize_notebooks( merged.update(meta_to_jsonable(meta)) nb_meta[DLC_NAMESPACE] = merged - # Write again to persist metadata update (still canonical) + # Write to persist metadata update (still canonical) write_ipynb_meta(abs_path, nb) rec.meta = meta From c57ff21a111fe5637f6c68910c70c3e9cb4e0d5f Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 10 Mar 2026 14:26:03 +0100 Subject: [PATCH 27/37] Specify HEAD and use --fixed-strings in git log Pass an explicit "HEAD" ref to git log calls and add --fixed-strings to the grep invocation. This prevents Git from misinterpreting the path or commit selector ordering and ensures the META_COMMIT_MARKER is treated as a literal string (not a regex). Changes applied to git_last_touched and git_last_content_updated to make commit/date lookups more robust. --- tools/docs_and_notebooks_check.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/docs_and_notebooks_check.py b/tools/docs_and_notebooks_check.py index 888fbdda92..db45c2e8cb 100644 --- a/tools/docs_and_notebooks_check.py +++ b/tools/docs_and_notebooks_check.py @@ -262,7 +262,7 @@ def _parse_git_iso_date(out: str) -> Optional[date]: def git_last_touched(repo_root: Path, rel_path: str) -> Optional[date]: code, out, _err = _run_git( - ["log", "-1", "--format=%cI", "--", rel_path], cwd=repo_root + ["log", "-1", "--format=%cI", "HEAD", "--", rel_path], cwd=repo_root ) if code != 0: return None @@ -285,9 +285,11 @@ def git_last_content_updated( "log", "-1", "--format=%cI", + "--fixed-strings", "--invert-grep", "--grep", META_COMMIT_MARKER, + "HEAD", "--", rel_path, ] From 9a58f68e19ae75a666f7b3c764384a7216e3f0cb Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 10 Mar 2026 16:13:43 +0100 Subject: [PATCH 28/37] Refactor git date parsing and log helpers Add a reusable _git_log_date helper to build git log args and return parsed dates, and make git_last_touched delegate to it. Improve _parse_git_iso_date to handle plain ISO dates and trailing Z timezone markers. Update git_last_content_updated to call the new helper with grep/invert-grep args to skip META_COMMIT_MARKER and fall back to raw last-touched date when needed. --- tools/docs_and_notebooks_check.py | 60 +++++++++++++++++-------------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/tools/docs_and_notebooks_check.py b/tools/docs_and_notebooks_check.py index db45c2e8cb..20ea181e15 100644 --- a/tools/docs_and_notebooks_check.py +++ b/tools/docs_and_notebooks_check.py @@ -254,47 +254,55 @@ def _parse_git_iso_date(out: str) -> Optional[date]: out = (out or "").strip() if not out: return None + + try: + return date.fromisoformat(out) + except Exception: + pass + try: + if out.endswith("Z"): + out = out[:-1] + "+00:00" return datetime.fromisoformat(out).date() except Exception: return None -def git_last_touched(repo_root: Path, rel_path: str) -> Optional[date]: - code, out, _err = _run_git( - ["log", "-1", "--format=%cI", "HEAD", "--", rel_path], cwd=repo_root - ) +def _git_log_date( + repo_root: Path, rel_path: str, extra_args: Sequence[str] = () +) -> Optional[date]: + args = [ + "log", + "-1", + "--date=short", + "--format=%cd", + *extra_args, + "--", + rel_path, + ] + code, out, _err = _run_git(args, cwd=repo_root) if code != 0: return None return _parse_git_iso_date(out) +def git_last_touched(repo_root: Path, rel_path: str) -> Optional[date]: + return _git_log_date(repo_root, rel_path) + + def git_last_content_updated( repo_root: Path, rel_path: str ) -> Tuple[Optional[date], bool]: - """ - Return (date, used_fallback). - - Compute last meaningful content update by skipping commits containing META_COMMIT_MARKER. - This requires metadata-only commits to include the marker. - - If all commits touching the file contain the marker (or history is shallow), - fall back to raw git_last_touched() and return used_fallback=True. - """ - args = [ - "log", - "-1", - "--format=%cI", - "--fixed-strings", - "--invert-grep", - "--grep", - META_COMMIT_MARKER, - "HEAD", - "--", + d = _git_log_date( + repo_root, rel_path, - ] - code, out, _err = _run_git(args, cwd=repo_root) - d = _parse_git_iso_date(out) if code == 0 else None + extra_args=[ + "--fixed-strings", + "--invert-grep", + "--grep", + META_COMMIT_MARKER, + ], + ) if d is not None: return d, False return git_last_touched(repo_root, rel_path), True From 5f0a32804a0147e23aa6d3d2622208247b82715f Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Mon, 16 Mar 2026 11:38:15 +0100 Subject: [PATCH 29/37] Refactor dlc docs/notebooks pre-commit hook Split the hook command into a simple entry plus explicit args and replace types_or with a files regex to more precisely target docs, examples (JUPYTER/COLAB) and tools .md/.ipynb files. This improves pre-commit handling of the CLI arguments and ensures the hook only runs on relevant files; pass_filenames and additional_dependencies remain unchanged. --- .pre-commit-config.yaml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d2bbb8c2a8..551e3f7dda 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,10 +30,15 @@ repos: hooks: - id: dlc-docs-notebooks-check name: DLC docs+notebooks staleness/check + nbformat validate + normalization - entry: python tools/docs_and_notebooks_check.py check --config tools/docs_and_notebooks_report_config.yml --targets + entry: python tools/docs_and_notebooks_check.py language: python pass_filenames: true - types_or: [jupyter, markdown] + files: ^(docs/|examples/(JUPYTER|COLAB)/|tools/).*(\.md|\.ipynb)$ + args: + - --config + - tools/docs_and_notebooks_report_config.yml + - check + - --targets additional_dependencies: - "pydantic>=2,<3" - "pyyaml" From 1da0a70c121fd0a60b74f2f33ce64722397608bf Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Mon, 16 Mar 2026 11:38:46 +0100 Subject: [PATCH 30/37] docs: clarify metadata/normalization guidance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clarify README language around metadata commits and notebook normalization: explicitly note git correctly reports rewritten files as β€œupdated now”, require ack flag text capitalization, add a warning about future changes to the metadata-commit marker, rephrase allowlists line to indicate they are currently empty, promote the notebook note to IMPORTANT and reword for clarity, and make minor troubleshooting wording/capitalization tweaks. --- tools/docs_and_notebooks_tool_README.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/tools/docs_and_notebooks_tool_README.md b/tools/docs_and_notebooks_tool_README.md index e8eb8dea6a..fa27c5dc8f 100644 --- a/tools/docs_and_notebooks_tool_README.md +++ b/tools/docs_and_notebooks_tool_README.md @@ -81,7 +81,7 @@ If a doc page has **no** frontmatter, the tool can still report staleness (read- ## The metadata-commit marker (critical) -Because metadata updates and notebook normalization can rewrite files, they would normally make git think the file was β€œupdated now”. +Because metadata updates and notebook normalization can rewrite files, they would normally make git (correctly) report that the file was β€œupdated now”. To preserve a meaningful **`last_content_updated`**, **all metadata-only / normalization commits must include the marker**: @@ -90,8 +90,11 @@ To preserve a meaningful **`last_content_updated`**, **all metadata-only / norma When you run `update --write` or `normalize --write`, the tool will: -- require `--ack-meta-commit-marker` (guardrail) -- print a suggested commit message +- Require `--ack-meta-commit-marker` (guardrail) +- Print a suggested commit message + +> [!WARNING] +> If the marker changes in the future, previous iterations still HAVE to be acknowledged to avoid false positives. --- @@ -118,7 +121,8 @@ Run policy checks. By default, CI will not fail unless allowlists are configured python tools/docs_and_notebooks_check.py check ``` -The allowlists live in `tools/docs_and_notebooks_report_config.yml` (start empty; ratchet later). +The allowlists live in `tools/docs_and_notebooks_report_config.yml`. +They are currently empty, but can help enforce stricter policies once populated (start empty; "ratchet" later). ### 3) Update metadata (write mode; explicit intent) @@ -141,9 +145,9 @@ python tools/docs_and_notebooks_check.py update --write --targets docs/page. ### 4) Normalize notebooks (explicit churn) -> [!WARNING] +> [!IMPORTANT] > Notebook normalization rewrites the notebook JSON into a canonical form. -> This is why it is a separate command. +> As such, it is provided as a separate command. Dry-run (shows which files *would* change): @@ -180,7 +184,7 @@ pip install pydantic pyyaml nbformat ## Troubleshooting - If you see `content_date_fallback_to_git_touched`, it usually means one of: - - the checkout history is too shallow, or - - *all* commits touching the file are metadata commits with the marker. + - The checkout history is too shallow, or + - *All* commits touching the file are metadata commits with the marker. - If Pydantic raises `class-not-fully-defined` errors, ensure the tool calls `.model_rebuild()` for its models (this is already done in the tool). From f1806a353c6439f445a0cbaad2c73083c4930c66 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 17 Mar 2026 08:51:44 +0100 Subject: [PATCH 31/37] Report scan/parsing issues non-fatal; add strict mode Update docs_and_notebooks_check.py to continue reporting scan/parsing errors without failing checks by default and now provide an opt-in strict mode. Key changes: - Clarify behavior in top-level docs and Markdown output: scan errors are reported as non-fatal by default and labeled as "scan errors" in summaries. - Add a policy config option fail_on_scan_errors (default false) and a CLI --strict-mode flag which forces check to fail on scan/parsing errors. - Introduce collect_scan_issues helper to aggregate scan errors/warnings and print a brief summary to console. - Adjust check command help text and check flow to respect the combined strict-mode/config setting; when strict, scan errors cause non-zero exit. - Rename suggested commit message constant to SUGGESTED_TAGGED_COMMIT and update printed suggestions. - Minor UX improvements: more explanatory messages after report generation and limited preview of scan errors. All changes are confined to tools/docs_and_notebooks_check.py and focus on behavior and messaging around scan error handling and strictness. --- tools/docs_and_notebooks_check.py | 74 ++++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 10 deletions(-) diff --git a/tools/docs_and_notebooks_check.py b/tools/docs_and_notebooks_check.py index 20ea181e15..47d81756fa 100644 --- a/tools/docs_and_notebooks_check.py +++ b/tools/docs_and_notebooks_check.py @@ -7,6 +7,8 @@ * Notebook-level metadata for .ipynb (never cells/outputs) * YAML frontmatter for .md docs (optional) - Uses pydantic schemas with explicit schema_version for validation. +- Aims to be contributor-friendly by default: check mode enforces configured policy, while + surfacing scan/parsing issues without failing unless strict mode is enabled. Terminology ----------- @@ -28,9 +30,15 @@ Report (read-only): python tools/docs_and_notebooks_check.py report -Check (read-only; may fail based on config allowlists): +Check (read-only; policy enforcement): python tools/docs_and_notebooks_check.py check + Runs scans and evaluates configured policy rules. + Exits non-zero for policy violations. + Scan/parsing errors are always reported in console / JSON / Markdown output, + but are non-fatal by default unless strict mode is enabled or they imply a + policy violation. + Update content-date field from git (write mode; requires --write): python tools/docs_and_notebooks_check.py update --write --set-content-date-from-git @@ -73,7 +81,7 @@ import subprocess from datetime import date, datetime, timezone from pathlib import Path -from typing import Any, Dict, List, Optional, Sequence, Tuple +from typing import Any, Dict, List, Literal, Optional, Sequence, Tuple try: import yaml # PyYAML @@ -105,7 +113,7 @@ # "git last touched" timestamps. To preserve meaningful "content age", all such # commits must include this marker in the commit message. META_COMMIT_MARKER = "chore(metadata)" -SUGGESTED_META_COMMIT_MESSAGE = f"{META_COMMIT_MARKER}: update docs/notebooks metadata" +SUGGESTED_TAGGED_COMMIT = f"{META_COMMIT_MARKER}: update docs/notebooks metadata" # ----------------------------- @@ -143,6 +151,9 @@ class PolicyConfig(BaseModel): warn_if_verified_older_than_days: int = 365 missing_last_verified_is_warning: bool = True + # Strict-mode toggle: if true, scan/parsing errors also fail `check` + fail_on_scan_errors: bool = False + # Allowlists for strict checks (start empty; ratchet later) require_metadata: List[str] = Field(default_factory=list) require_recent_verification: List[str] = Field(default_factory=list) @@ -555,7 +566,7 @@ def _require_meta_marker_ack(write: bool, ack_marker: bool) -> None: raise SystemExit( "Refusing to write without acknowledging metadata-commit convention.\n" "Re-run with --ack-meta-commit-marker and commit with:\n" - f" {SUGGESTED_META_COMMIT_MESSAGE}\n" + f" {SUGGESTED_TAGGED_COMMIT}\n" ) @@ -746,7 +757,7 @@ def to_markdown(report: Report, cfg: ToolConfig) -> str: lines.append("## Summary\n") lines.append(f"- Files scanned: **{t['files']}**\n") lines.append(f"- Files with warnings: **{t['warnings']}**\n") - lines.append(f"- Files with errors: **{t['errors']}**\n") + lines.append(f"- Files with scanning errors: **{t['errors']}**\n") lines.append(f"- Missing metadata: **{t['missing_metadata']}**\n") lines.append(f"- Missing last_verified: **{t['missing_last_verified']}**\n") lines.append( @@ -801,7 +812,7 @@ def fmt_date(d: Optional[date]) -> str: err_recs = [r for r in report.records if r.errors] if err_recs: - lines.append("## Errors\n") + lines.append("## Scan errors (non-fatal by default)\n") for r in err_recs: lines.append(f"- **{r.path}**: {', '.join(r.errors)}\n") lines.append("\n") @@ -813,6 +824,10 @@ def fmt_date(d: Optional[date]) -> str: lines.append( "- last_git_touched / last_content_updated are computed from git history. last_verified is human-controlled.\n\n" ) + lines.append( + "- In `check` mode, scan/parsing errors are reported for visibility but do not " + "fail by default unless strict mode is enabled or they trigger an enforced policy rule.\n" + ) return "".join(lines) @@ -887,6 +902,16 @@ def parse_date_token(token: str) -> date: return date.fromisoformat(token) +def collect_scan_issues( + records: List[FileRecord], target: Literal["errors", "warnings"] +) -> List[str]: + items: List[str] = [] + for r in records: + for e in getattr(r, target, []): + items.append(f"{r.path}: {e}") + return items + + def main(argv: Optional[Sequence[str]] = None) -> int: parser = argparse.ArgumentParser( description="DeepLabCut checks tool (docs + notebooks)" @@ -912,13 +937,22 @@ def main(argv: Optional[Sequence[str]] = None) -> int: ) chk = sub.add_parser( - "check", help="Run policy checks (read-only; may exit non-zero)" + "check", + help=( + "Run scans + policy checks (read-only). " + "Fails on enforced policy violations; scan errors are non-fatal by default." + ), ) chk.add_argument( "--targets", nargs="*", help="Optional list of relative file paths to scan (limits scan to these files)", ) + chk.add_argument( + "--strict-mode", + action="store_true", + help="Enable failure on scan/parsing errors (overrides config for this run)", + ) up = sub.add_parser( "update", help="Update metadata/frontmatter (write mode requires --write)" @@ -970,10 +1004,10 @@ def main(argv: Optional[Sequence[str]] = None) -> int: repo_root = find_repo_root(Path.cwd()) cfg = load_config(config_path) out_dir = Path(args.out_dir) + strict_mode = bool((args.strict_mode) or cfg.policy.fail_on_scan_errors) if args.cmd in {"report", "check"}: records = scan_files(repo_root, cfg, targets=getattr(args, "targets", None)) - elif args.cmd == "update": lv = ( parse_date_token(args.set_last_verified) if args.set_last_verified else None @@ -989,7 +1023,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: ack_meta_commit_marker=bool(args.ack_meta_commit_marker), ) if args.write: - print(f"\nSuggested commit message:\n {SUGGESTED_META_COMMIT_MESSAGE}\n") + print(f"\nSuggested commit message:\n {SUGGESTED_TAGGED_COMMIT}\n") else: # normalize records = normalize_notebooks( @@ -1000,7 +1034,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: ack_meta_commit_marker=bool(args.ack_meta_commit_marker), ) if args.write: - print(f"\nSuggested commit message:\n {SUGGESTED_META_COMMIT_MESSAGE}\n") + print(f"\nSuggested commit message:\n {SUGGESTED_TAGGED_COMMIT}\n") report = Report( generated_at=datetime.now(timezone.utc), @@ -1024,6 +1058,14 @@ def main(argv: Optional[Sequence[str]] = None) -> int: except Exception: pass + scan_errors = collect_scan_issues(records, target="errors") + if scan_errors: + print("\nScan errors detected (non-fatal by default):") + for item in scan_errors[:20]: + print(f"- {item}") + if len(scan_errors) > 20: + print(f"... and {len(scan_errors) - 20} more (see report for full details)") + if args.cmd == "check": violations = enforce(cfg, records) if violations: @@ -1031,10 +1073,22 @@ def main(argv: Optional[Sequence[str]] = None) -> int: for v in violations: print(f"- {v}") return 2 + if strict_mode and scan_errors: + print("Strict mode enabled: failing due to scan/parsing errors.") + return 1 # Non-zero if metadata parsing errors occurred for non-report/check commands if args.cmd not in {"report", "check"} and any(r.errors for r in records): return 1 + else: + print(f"\nReport generated:") + print(f"- JSON: {json_path}") + print(f"- Markdown: {md_path}") + + if any(r.warnings for r in records): + print("Warnings detected; see report for details.") + if any(r.errors for r in records): + print("Scan errors detected; see report for details.") return 0 From 7838d071ed12ae64f48c2e99e15ff5708855b486 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 17 Mar 2026 11:03:34 +0100 Subject: [PATCH 32/37] Pydantic v2 migration and frontmatter error fixes Enforce pydantic v2 APIs and tighten frontmatter parsing/validation and error reporting. Changes include: - Require pydantic>=2 and nbformat>=5 in docs. - read_md_frontmatter now returns an optional error string and reports unterminated or non-mapping frontmatter. - Propagate frontmatter parse errors into scan/update flows (mark invalid_metadata and add explicit error codes). - read_ipynb_meta returns the raw DLC metadata and preserves presence flag; notebook metadata handling clarified. - Use model_dump(mode='json') / model_validate everywhere (remove pydantic v1 fallbacks). - Simplify meta_to_jsonable to assume pydantic v2 output. - Add future-date validation for last_content_updated/last_metadata_updated/last_verified and report errors. - Improve enforcement logic to treat invalid metadata separately from missing metadata and to avoid false passes. - Fail-safe handling in update mode when frontmatter is invalid; adjust report header and JSON output path writing. - Minor whitespace/cleanup and reordering of strict_mode evaluation. Overall this makes metadata parsing more strict, yields clearer diagnostics for frontmatter issues, and migrates code paths to pydantic v2. --- tools/docs_and_notebooks_check.py | 119 +++++++++++++++++------------- 1 file changed, 67 insertions(+), 52 deletions(-) diff --git a/tools/docs_and_notebooks_check.py b/tools/docs_and_notebooks_check.py index 47d81756fa..de8569e478 100644 --- a/tools/docs_and_notebooks_check.py +++ b/tools/docs_and_notebooks_check.py @@ -64,9 +64,9 @@ - Ensure actions/checkout uses fetch-depth: 0 (or sufficiently deep), otherwise git log may not see history. - Requires: - - pydantic + - pydantic>=2,<3 - PyYAML - - nbformat + - nbformat>=5 to be installed in the environment. Recommended : install in CI job directly (pip install pydantic pyyaml nbformat) rather than adding to requirements, since these are only needed for this tool. """ @@ -322,18 +322,19 @@ def git_last_content_updated( FRONTMATTER_RE = re.compile(r"^---\s*$") -def read_md_frontmatter(text: str) -> Tuple[Optional[dict], str]: +def read_md_frontmatter(text: str) -> Tuple[Optional[dict], str, Optional[str]]: lines = text.splitlines(keepends=True) if not lines or not FRONTMATTER_RE.match(lines[0]): - return None, text + return None, text, None end_idx = None for i in range(1, min(len(lines), 5000)): if FRONTMATTER_RE.match(lines[i]): end_idx = i break + if end_idx is None: - return None, text + return None, text, "unterminated_markdown_frontmatter" fm_text = "".join(lines[1:end_idx]) body = "".join(lines[end_idx + 1 :]) @@ -343,8 +344,9 @@ def read_md_frontmatter(text: str) -> Tuple[Optional[dict], str]: fm = yaml.safe_load(fm_text) if fm_text.strip() else {} if not isinstance(fm, dict): - return None, text - return fm, body + return None, text, "markdown_frontmatter_not_mapping" + + return fm, body, None def dump_md_frontmatter(frontmatter: dict, body: str) -> str: @@ -367,11 +369,8 @@ def read_ipynb_meta(path: Path) -> tuple[Any, dict, bool]: meta = getattr(nb, "metadata", {}) or {} has_dlc = DLC_NAMESPACE in meta - dlc_meta = meta.get(DLC_NAMESPACE, {}) - if not isinstance(dlc_meta, dict): - dlc_meta = {} - - return nb, dlc_meta, has_dlc + raw_dlc_meta = meta.get(DLC_NAMESPACE) + return nb, raw_dlc_meta, has_dlc def notebook_is_normalized(path: Path, nb: Any) -> bool: @@ -413,12 +412,7 @@ def meta_to_jsonable(meta: DLCMeta) -> dict: Return JSON-serializable metadata (dates become ISO strings). This prevents json.dumps() from failing when writing .ipynb files. """ - try: - # Pydantic v2: mode='json' converts date/datetime into ISO strings - return meta.model_dump(mode="json", exclude_none=True) - except AttributeError: - # Pydantic v1: meta.json() encodes dates; parse back into dict - return json.loads(meta.json(exclude_none=True)) + return meta.model_dump(mode="json", exclude_none=True) def compute_days_since(d: Optional[date], today: date) -> Optional[int]: @@ -439,10 +433,7 @@ def load_config(config_path: Path) -> ToolConfig: if yaml is None: raise RuntimeError("PyYAML is required (pip install pyyaml)") raw = yaml.safe_load(config_path.read_text(encoding="utf-8")) - try: - return ToolConfig.model_validate(raw) # pydantic v2 - except AttributeError: - return ToolConfig.parse_obj(raw) # pydantic v1 + return ToolConfig.model_validate(raw) def scan_files( @@ -500,20 +491,25 @@ def scan_files( elif kind == "md": text = p.read_text(encoding="utf-8") - fm, _body = read_md_frontmatter(text) - fm = fm or {} + fm, _body, fm_error = read_md_frontmatter(text) - has_dlc = DLC_NAMESPACE in fm - raw = fm.get(DLC_NAMESPACE) - - if not has_dlc: + if fm_error: rec.meta = None - rec.warnings.append("missing_metadata") + rec.warnings.append("invalid_metadata") + rec.errors.append(f"markdown_frontmatter_invalid: {fm_error}") else: - rec.meta, valid = parse_dlc_meta(raw) - if not valid: + fm = fm or {} + has_dlc = DLC_NAMESPACE in fm + raw = fm.get(DLC_NAMESPACE) + + if not has_dlc: rec.meta = None - rec.warnings.append("invalid_metadata") + rec.warnings.append("missing_metadata") + else: + rec.meta, valid = parse_dlc_meta(raw) + if not valid: + rec.meta = None + rec.warnings.append("invalid_metadata") else: rec.meta = None @@ -521,6 +517,7 @@ def scan_files( except Exception as e: rec.errors.append(f"metadata_read_failed: {e}") + # ignore=True means: keep reporting diagnostics, but skip freshness/policy logic if rec.meta and rec.meta.ignore: records.append(rec) continue @@ -528,6 +525,15 @@ def scan_files( last_verified = rec.meta.last_verified if rec.meta else None rec.days_since_verified = compute_days_since(last_verified, today) + # Future dates are data errors + if rec.last_content_updated is not None and rec.last_content_updated > today: + rec.errors.append("future_last_content_updated") + if rec.meta and rec.meta.last_metadata_updated is not None: + if rec.meta.last_metadata_updated > today: + rec.errors.append("future_last_metadata_updated") + if last_verified is not None and last_verified > today: + rec.errors.append("future_last_verified") + pol = cfg.policy if ( @@ -634,7 +640,13 @@ def update_files( elif rec.kind == "md": text = abs_path.read_text(encoding="utf-8") - fm, body = read_md_frontmatter(text) + fm, body, fm_error = read_md_frontmatter(text) + if fm_error: + msg = f"markdown_frontmatter_invalid: {fm_error}" + if msg not in rec.errors: + rec.errors.append(msg) + continue + fm = fm or {} prev = fm.get(DLC_NAMESPACE, {}) @@ -812,7 +824,7 @@ def fmt_date(d: Optional[date]) -> str: err_recs = [r for r in report.records if r.errors] if err_recs: - lines.append("## Scan errors (non-fatal by default)\n") + lines.append("## Scan errors\n") for r in err_recs: lines.append(f"- **{r.path}**: {', '.join(r.errors)}\n") lines.append("\n") @@ -836,10 +848,7 @@ def write_outputs(report: Report, cfg: ToolConfig, out_dir: Path) -> Tuple[Path, json_path = out_dir / f"{OUTPUT_FILENAME}.json" md_path = out_dir / f"{OUTPUT_FILENAME}.md" - try: - payload = report.model_dump(mode="json") # pydantic v2 - except AttributeError: - payload = json.loads(report.json()) # pydantic v1 + payload = report.model_dump(mode="json") json_path.write_text( json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" @@ -851,8 +860,6 @@ def write_outputs(report: Report, cfg: ToolConfig, out_dir: Path) -> Tuple[Path, # ----------------------------- # Check enforcement # ----------------------------- - - def enforce(cfg: ToolConfig, records: List[FileRecord]) -> List[str]: pol = cfg.policy violations: List[str] = [] @@ -864,20 +871,28 @@ def enforce(cfg: ToolConfig, records: List[FileRecord]) -> List[str]: if r.kind not in {"ipynb", "md"}: continue - if match_allowlist(r.path, pol.require_metadata) and r.meta is None: - violations.append(f"{r.path}: missing metadata") + has_invalid_metadata = "invalid_metadata" in (r.warnings or []) + + if match_allowlist(r.path, pol.require_metadata): + if has_invalid_metadata: + violations.append(f"{r.path}: invalid metadata") + elif r.meta is None: + violations.append(f"{r.path}: missing metadata") if match_allowlist(r.path, pol.require_recent_verification): - lv = r.meta.last_verified if r.meta else None - if lv is None: - violations.append(f"{r.path}: missing last_verified") + if has_invalid_metadata: + violations.append(f"{r.path}: invalid metadata") else: - days = (today - lv).days - if days > pol.warn_if_verified_older_than_days: - violations.append( - f"{r.path}: last_verified is {days}d old " - f"(> {pol.warn_if_verified_older_than_days}d)" - ) + lv = r.meta.last_verified if r.meta else None + if lv is None: + violations.append(f"{r.path}: missing last_verified") + else: + days = (today - lv).days + if days > pol.warn_if_verified_older_than_days: + violations.append( + f"{r.path}: last_verified is {days}d old " + f"(> {pol.warn_if_verified_older_than_days}d)" + ) if r.kind == "ipynb" and match_allowlist( r.path, pol.require_notebook_normalized @@ -1004,7 +1019,6 @@ def main(argv: Optional[Sequence[str]] = None) -> int: repo_root = find_repo_root(Path.cwd()) cfg = load_config(config_path) out_dir = Path(args.out_dir) - strict_mode = bool((args.strict_mode) or cfg.policy.fail_on_scan_errors) if args.cmd in {"report", "check"}: records = scan_files(repo_root, cfg, targets=getattr(args, "targets", None)) @@ -1073,6 +1087,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: for v in violations: print(f"- {v}") return 2 + strict_mode = bool((args.strict_mode) or cfg.policy.fail_on_scan_errors) if strict_mode and scan_errors: print("Strict mode enabled: failing due to scan/parsing errors.") return 1 From 5d22716620cc35a106553da9d5ff8ddec25a8428 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 17 Mar 2026 11:04:07 +0100 Subject: [PATCH 33/37] tests: update for renamed constant and 3-tuple Adapt tests to recent API changes: replace SUGGESTED_META_COMMIT_MESSAGE with SUGGESTED_TAGGED_COMMIT, and update calls to read_md_frontmatter to unpack a third return value (fm, body, _). Keeps tests aligned with the tool's renamed constant and modified function signature. --- .../docs_and_notebooks_checks/test_check_contracts.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/tools/docs_and_notebooks_checks/test_check_contracts.py b/tests/tools/docs_and_notebooks_checks/test_check_contracts.py index cc7195c7ab..9f4aa2c767 100644 --- a/tests/tools/docs_and_notebooks_checks/test_check_contracts.py +++ b/tests/tools/docs_and_notebooks_checks/test_check_contracts.py @@ -67,8 +67,8 @@ def _write(repo: Path, rel: str, content: str) -> None: # ----------------------------- def test_marker_constants_exist(tool): assert hasattr(tool, "META_COMMIT_MARKER") - assert hasattr(tool, "SUGGESTED_META_COMMIT_MESSAGE") - assert tool.META_COMMIT_MARKER in tool.SUGGESTED_META_COMMIT_MESSAGE + assert hasattr(tool, "SUGGESTED_TAGGED_COMMIT") + assert tool.META_COMMIT_MARKER in tool.SUGGESTED_TAGGED_COMMIT def test_schema_contract_fields(tool): @@ -240,7 +240,7 @@ def test_update_set_content_date_from_git_only_changes_that_field(tool, tmp_path # Read back and confirm verified fields unchanged text = (repo / rel).read_text(encoding="utf-8") - fm, body = tool.read_md_frontmatter(text) + fm, body, _ = tool.read_md_frontmatter(text) assert isinstance(fm, dict) and tool.DLC_NAMESPACE in fm meta = fm[tool.DLC_NAMESPACE] @@ -287,7 +287,7 @@ def test_update_set_verified_fields_only_changes_verified(tool, tmp_path: Path): assert len(records) == 1 text = (repo / rel).read_text(encoding="utf-8") - fm, _body = tool.read_md_frontmatter(text) + fm, _body, _ = tool.read_md_frontmatter(text) meta = fm[tool.DLC_NAMESPACE] # Verified fields updated From 8c0dc21efdcc7722af261117a8304dd2d330c1c3 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Fri, 6 Mar 2026 14:26:54 +0100 Subject: [PATCH 34/37] chore(metadata): update docs/notebooks metadata --- docs/Governance.md | 6 + docs/HelperFunctions.md | 6 + docs/MISSION_AND_VALUES.md | 6 + docs/ModelZoo.md | 6 + docs/Overviewof3D.md | 6 + docs/README.md | 6 + docs/UseOverviewGuide.md | 6 + docs/beginner-guides/Training-Evaluation.md | 6 + docs/beginner-guides/beginners-guide.md | 6 + docs/beginner-guides/labeling.md | 6 + docs/beginner-guides/manage-project.md | 6 + docs/beginner-guides/video-analysis.md | 6 + docs/benchmark.md | 6 + docs/citation.md | 6 + docs/convert_maDLC.md | 6 + docs/course.md | 6 + docs/docker.md | 6 + docs/gui/PROJECT_GUI.md | 6 + docs/gui/napari_GUI.md | 6 + docs/installation.md | 6 + docs/intro.md | 6 + docs/maDLC_UserGuide.md | 6 + docs/pytorch/Benchmarking_shuffle_guide.md | 6 + docs/pytorch/architectures.md | 6 + docs/pytorch/pytorch_config.md | 6 + docs/pytorch/user_guide.md | 6 + docs/pytorch_dlc.md | 6 + docs/quick-start/single_animal_quick_guide.md | 6 + docs/quick-start/tutorial_maDLC.md | 6 + docs/recipes/BatchProcessing.md | 7 +- docs/recipes/ClusteringNapari.md | 7 +- docs/recipes/DLCMethods.md | 6 + docs/recipes/MegaDetectorDLCLive.md | 6 + docs/recipes/OpenVINO.md | 6 + docs/recipes/OtherData.md | 6 + docs/recipes/TechHardware.md | 6 + docs/recipes/UsingModelZooPupil.md | 6 + docs/recipes/flip_and_rotate.ipynb | 5 + docs/recipes/fmpose3d.ipynb | 687 ++--- docs/recipes/installTips.md | 6 + docs/recipes/io.md | 6 + docs/recipes/nn.md | 6 + docs/recipes/pose_cfg_file_breakdown.md | 6 + docs/recipes/post.md | 6 + ...ng_notebooks_into_the_DLC_main_cookbook.md | 6 + docs/roadmap.md | 6 + docs/standardDeepLabCut_UserGuide.md | 6 + examples/COLAB/COLAB_3miceDemo.ipynb | 5 + .../COLAB/COLAB_BUCTD_and_CTD_tracking.ipynb | 5 + examples/COLAB/COLAB_DEMO_SuperAnimal.ipynb | 5 + .../COLAB/COLAB_DEMO_mouse_openfield.ipynb | 5 + examples/COLAB/COLAB_DLC_ModelZoo.ipynb | 629 ++--- .../COLAB/COLAB_HumanPose_with_RTMPose.ipynb | 2275 +++++++++-------- .../COLAB/COLAB_YOURDATA_SuperAnimal.ipynb | 5 + ..._YOURDATA_TrainNetwork_VideoAnalysis.ipynb | 5 + ...ATA_maDLC_TrainNetwork_VideoAnalysis.ipynb | 5 + examples/COLAB/COLAB_transformer_reID.ipynb | 1169 ++++----- examples/JUPYTER/Demo_3D_DeepLabCut.ipynb | 5 + .../Demo_labeledexample_MouseReaching.ipynb | 5 + .../Demo_labeledexample_Openfield.ipynb | 5 + examples/JUPYTER/Demo_napari.ipynb | 5 + examples/JUPYTER/Demo_yourowndata.ipynb | 5 + .../Docker_TrainNetwork_VideoAnalysis.ipynb | 5 + 63 files changed, 2730 insertions(+), 2372 deletions(-) diff --git a/docs/Governance.md b/docs/Governance.md index 187379cc1c..2ee2b1b172 100644 --- a/docs/Governance.md +++ b/docs/Governance.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2026-02-10' + last_metadata_updated: '2026-03-06' + ignore: false +--- (governance-model)= # Governance Model of DeepLabCut (adapted from https://napari.org/stable/community/governance.html) diff --git a/docs/HelperFunctions.md b/docs/HelperFunctions.md index e4a5aeebe8..25efc177fa 100644 --- a/docs/HelperFunctions.md +++ b/docs/HelperFunctions.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-06-30' + last_metadata_updated: '2026-03-06' + ignore: false +--- (helper-functions)= # Helper & Advanced Optional Function Documentation diff --git a/docs/MISSION_AND_VALUES.md b/docs/MISSION_AND_VALUES.md index c82c4b7a39..bc6623a6af 100644 --- a/docs/MISSION_AND_VALUES.md +++ b/docs/MISSION_AND_VALUES.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-02-28' + last_metadata_updated: '2026-03-06' + ignore: false +--- (mission-and-values)= # Mission and Values of DeepLabCut diff --git a/docs/ModelZoo.md b/docs/ModelZoo.md index fe5f3496b4..ccce26056c 100644 --- a/docs/ModelZoo.md +++ b/docs/ModelZoo.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-07-06' + last_metadata_updated: '2026-03-06' + ignore: false +--- (file:model-zoo)= # The DeepLabCut Model Zoo! diff --git a/docs/Overviewof3D.md b/docs/Overviewof3D.md index 6a83d7575e..75b498a686 100644 --- a/docs/Overviewof3D.md +++ b/docs/Overviewof3D.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-10-14' + last_metadata_updated: '2026-03-06' + ignore: false +--- (3D-overview)= # 3D DeepLabCut diff --git a/docs/README.md b/docs/README.md index df606c21b6..1114741159 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2022-08-30' + last_metadata_updated: '2026-03-06' + ignore: false +--- Please see https://deeplabcut.github.io/DeepLabCut for documentation on how to use this software. This directory contains the source code for the docs. diff --git a/docs/UseOverviewGuide.md b/docs/UseOverviewGuide.md index 017b30dcc4..e27c462310 100644 --- a/docs/UseOverviewGuide.md +++ b/docs/UseOverviewGuide.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2026-02-10' + last_metadata_updated: '2026-03-06' + ignore: false +--- (overview)= # πŸ₯³ Get started with DeepLabCut: our key recommendations diff --git a/docs/beginner-guides/Training-Evaluation.md b/docs/beginner-guides/Training-Evaluation.md index 18ab3bf736..2ec7a9157a 100644 --- a/docs/beginner-guides/Training-Evaluation.md +++ b/docs/beginner-guides/Training-Evaluation.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-02-28' + last_metadata_updated: '2026-03-06' + ignore: false +--- # Neural Network training and evaluation in the GUI DLC LIVE! diff --git a/docs/beginner-guides/beginners-guide.md b/docs/beginner-guides/beginners-guide.md index f3cfea194d..e4e466b8a2 100644 --- a/docs/beginner-guides/beginners-guide.md +++ b/docs/beginner-guides/beginners-guide.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2026-03-03' + last_metadata_updated: '2026-03-06' + ignore: false +--- (beginners-guide)= # Using DeepLabCut DLC LIVE! diff --git a/docs/beginner-guides/labeling.md b/docs/beginner-guides/labeling.md index f3a0ec10d3..3c79807a3a 100644 --- a/docs/beginner-guides/labeling.md +++ b/docs/beginner-guides/labeling.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-06-30' + last_metadata_updated: '2026-03-06' + ignore: false +--- (labeling)= # Labeling GUI diff --git a/docs/beginner-guides/manage-project.md b/docs/beginner-guides/manage-project.md index ff32aef0e2..ee0e0f339e 100644 --- a/docs/beginner-guides/manage-project.md +++ b/docs/beginner-guides/manage-project.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-06-30' + last_metadata_updated: '2026-03-06' + ignore: false +--- # Setting up what keypoints to track DLC LIVE! diff --git a/docs/beginner-guides/video-analysis.md b/docs/beginner-guides/video-analysis.md index 126c7bdf9a..132feeb812 100644 --- a/docs/beginner-guides/video-analysis.md +++ b/docs/beginner-guides/video-analysis.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-06-30' + last_metadata_updated: '2026-03-06' + ignore: false +--- # Video Analysis with DeepLabCut DLC LIVE! diff --git a/docs/benchmark.md b/docs/benchmark.md index 612e307d0f..1b2e9a5d42 100644 --- a/docs/benchmark.md +++ b/docs/benchmark.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-02-28' + last_metadata_updated: '2026-03-06' + ignore: false +--- # DeepLabCut benchmark For further information and the leaderboard, see [the official homepage](https://benchmark.deeplabcut.org/). diff --git a/docs/citation.md b/docs/citation.md index c427b1e223..635bd62662 100644 --- a/docs/citation.md +++ b/docs/citation.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2024-10-27' + last_metadata_updated: '2026-03-06' + ignore: false +--- # How to Cite DeepLabCut Thank you for using DeepLabCut! Here are our recommendations for citing and documenting your use of DeepLabCut in your Methods section: diff --git a/docs/convert_maDLC.md b/docs/convert_maDLC.md index 19dd017692..14e697c719 100644 --- a/docs/convert_maDLC.md +++ b/docs/convert_maDLC.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-02-28' + last_metadata_updated: '2026-03-06' + ignore: false +--- (convert-maDLC)= # How to convert a pre-2.2 project for use with DeepLabCut 2.2 or later diff --git a/docs/course.md b/docs/course.md index ff25b8d6e4..fa2eb30aba 100644 --- a/docs/course.md +++ b/docs/course.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-06-30' + last_metadata_updated: '2026-03-06' + ignore: false +--- # DeepLabCut Self-paced Course ::::{warning} diff --git a/docs/docker.md b/docs/docker.md index 473cded14e..fc9048b86a 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-04-15' + last_metadata_updated: '2026-03-06' + ignore: false +--- (docker-containers)= # DeepLabCut Docker containers diff --git a/docs/gui/PROJECT_GUI.md b/docs/gui/PROJECT_GUI.md index e0883c324e..362479a4b9 100644 --- a/docs/gui/PROJECT_GUI.md +++ b/docs/gui/PROJECT_GUI.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-02-28' + last_metadata_updated: '2026-03-06' + ignore: false +--- (project-manager-gui)= # Interactive Project Manager GUI diff --git a/docs/gui/napari_GUI.md b/docs/gui/napari_GUI.md index df86f689bc..9acc9ddaea 100644 --- a/docs/gui/napari_GUI.md +++ b/docs/gui/napari_GUI.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2026-02-10' + last_metadata_updated: '2026-03-06' + ignore: false +--- (napari-gui)= # napari labeling GUI diff --git a/docs/installation.md b/docs/installation.md index a6ef8ca929..56ba147659 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2026-02-23' + last_metadata_updated: '2026-03-06' + ignore: false +--- (how-to-install)= # How To Install DeepLabCut diff --git a/docs/intro.md b/docs/intro.md index 415e668dd5..cab6f4b183 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -1 +1,7 @@ +--- +deeplabcut: + last_content_updated: '2024-06-14' + last_metadata_updated: '2026-03-06' + ignore: false +--- Please see the main [READ ME!](https://deeplabcut.github.io/DeepLabCut/README.html) diff --git a/docs/maDLC_UserGuide.md b/docs/maDLC_UserGuide.md index 9861a41242..0de051dd92 100644 --- a/docs/maDLC_UserGuide.md +++ b/docs/maDLC_UserGuide.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2026-02-10' + last_metadata_updated: '2026-03-06' + ignore: false +--- (multi-animal-userguide)= # DeepLabCut for Multi-Animal Projects diff --git a/docs/pytorch/Benchmarking_shuffle_guide.md b/docs/pytorch/Benchmarking_shuffle_guide.md index 8e4554ce77..a498eea9bd 100644 --- a/docs/pytorch/Benchmarking_shuffle_guide.md +++ b/docs/pytorch/Benchmarking_shuffle_guide.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-06-30' + last_metadata_updated: '2026-03-06' + ignore: false +--- # DeepLabCut Benchmarking - User Guide ## Reasoning for benchmarking models in DLC (across DLC versions and architectures) diff --git a/docs/pytorch/architectures.md b/docs/pytorch/architectures.md index c1742b8221..aa4f8ed9f8 100644 --- a/docs/pytorch/architectures.md +++ b/docs/pytorch/architectures.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-06-30' + last_metadata_updated: '2026-03-06' + ignore: false +--- (dlc3-architectures)= # DeepLabCut 3.0 - PyTorch Model Architectures diff --git a/docs/pytorch/pytorch_config.md b/docs/pytorch/pytorch_config.md index 8d75e3947e..a4ddf14599 100644 --- a/docs/pytorch/pytorch_config.md +++ b/docs/pytorch/pytorch_config.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-10-02' + last_metadata_updated: '2026-03-06' + ignore: false +--- (dlc3-pytorch-config)= # The PyTorch Configuration file diff --git a/docs/pytorch/user_guide.md b/docs/pytorch/user_guide.md index a30f7f6a7c..a9479d364b 100644 --- a/docs/pytorch/user_guide.md +++ b/docs/pytorch/user_guide.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-07-01' + last_metadata_updated: '2026-03-06' + ignore: false +--- (dlc3-user-guide)= # DeepLabCut 3.0 - PyTorch User Guide diff --git a/docs/pytorch_dlc.md b/docs/pytorch_dlc.md index 157a1c19af..5da418aa87 100644 --- a/docs/pytorch_dlc.md +++ b/docs/pytorch_dlc.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2024-01-17' + last_metadata_updated: '2026-03-06' + ignore: false +--- # DeepLabCut: PyTorch API ## Modules diff --git a/docs/quick-start/single_animal_quick_guide.md b/docs/quick-start/single_animal_quick_guide.md index 307ec6d113..ef17102ab1 100644 --- a/docs/quick-start/single_animal_quick_guide.md +++ b/docs/quick-start/single_animal_quick_guide.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-06-30' + last_metadata_updated: '2026-03-06' + ignore: false +--- # QUICK GUIDE to single Animal Training: **The main steps to take you from project creation to analyzed videos:** diff --git a/docs/quick-start/tutorial_maDLC.md b/docs/quick-start/tutorial_maDLC.md index 507ad559d8..111ed996f5 100644 --- a/docs/quick-start/tutorial_maDLC.md +++ b/docs/quick-start/tutorial_maDLC.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-02-28' + last_metadata_updated: '2026-03-06' + ignore: false +--- # Multi-animal pose estimation with DeepLabCut: A 5-minute tutorial ## GUI: diff --git a/docs/recipes/BatchProcessing.md b/docs/recipes/BatchProcessing.md index cd9dfef8a5..1e9a866e2a 100644 --- a/docs/recipes/BatchProcessing.md +++ b/docs/recipes/BatchProcessing.md @@ -1,4 +1,9 @@ - +--- +deeplabcut: + last_content_updated: '2025-09-16' + last_metadata_updated: '2026-03-06' + ignore: false +--- # Automate training and video analysis: Batch Processing ## Tips for working with DLC networks: diff --git a/docs/recipes/ClusteringNapari.md b/docs/recipes/ClusteringNapari.md index a12fa75142..bb7faf41c9 100644 --- a/docs/recipes/ClusteringNapari.md +++ b/docs/recipes/ClusteringNapari.md @@ -1,4 +1,9 @@ - +--- +deeplabcut: + last_content_updated: '2026-02-10' + last_metadata_updated: '2026-03-06' + ignore: false +--- # Clustering in the napari-DeepLabCut GUI To increase model performance, one can find the errors in the user-defined label (or in output H5 files after video diff --git a/docs/recipes/DLCMethods.md b/docs/recipes/DLCMethods.md index 74d5ec4c65..b3710b7d55 100644 --- a/docs/recipes/DLCMethods.md +++ b/docs/recipes/DLCMethods.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-02-28' + last_metadata_updated: '2026-03-06' + ignore: false +--- # How to write a DLC Methods Section **Pose estimation using DeepLabCut** diff --git a/docs/recipes/MegaDetectorDLCLive.md b/docs/recipes/MegaDetectorDLCLive.md index ecdf3432c7..e314b55600 100644 --- a/docs/recipes/MegaDetectorDLCLive.md +++ b/docs/recipes/MegaDetectorDLCLive.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2026-02-10' + last_metadata_updated: '2026-03-06' + ignore: false +--- # πŸ’š MegaDetector+DeepLabCut πŸ’œ [DeepLabCut-Live](https://github.com/DeepLabCut/DeepLabCut-live) is an open source and free real-time package from DeepLabCut that allows for real-time, low-latency pose estimation. [The DeepLabCut-ModelZoo](http://modelzoo.deeplabcut.org/) is our growing collection of pretrained animal models for rapid deployment; no training is typically required to use these models. MegaDetector is a free open software trained to detect animals, people, and vehicles from camera trap images. Check [here](https://github.com/microsoft/CameraTraps/blob/main/megadetector.md) for further information. diff --git a/docs/recipes/OpenVINO.md b/docs/recipes/OpenVINO.md index 78ea18e82f..06bcbba3b5 100644 --- a/docs/recipes/OpenVINO.md +++ b/docs/recipes/OpenVINO.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-02-28' + last_metadata_updated: '2026-03-06' + ignore: false +--- # Intel OpenVINO backend ::::{warning} diff --git a/docs/recipes/OtherData.md b/docs/recipes/OtherData.md index 73343284b3..ebc9b16f3e 100644 --- a/docs/recipes/OtherData.md +++ b/docs/recipes/OtherData.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-02-28' + last_metadata_updated: '2026-03-06' + ignore: false +--- # How to use data labeled outside of DeepLabCut - and/or if you merge projects across scorers (see below): diff --git a/docs/recipes/TechHardware.md b/docs/recipes/TechHardware.md index 6fb1add9bc..c32a6d741d 100644 --- a/docs/recipes/TechHardware.md +++ b/docs/recipes/TechHardware.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2026-02-10' + last_metadata_updated: '2026-03-06' + ignore: false +--- # Technical (Hardware) Considerations ## Quick summary: diff --git a/docs/recipes/UsingModelZooPupil.md b/docs/recipes/UsingModelZooPupil.md index d73a1acbd7..2b906ad755 100644 --- a/docs/recipes/UsingModelZooPupil.md +++ b/docs/recipes/UsingModelZooPupil.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-02-28' + last_metadata_updated: '2026-03-06' + ignore: false +--- # Using ModelZoo models on your own datasets

Animal behavior has to be analyzed with painstaking accuracy. Therefore, animal pose estimation has been diff --git a/docs/recipes/flip_and_rotate.ipynb b/docs/recipes/flip_and_rotate.ipynb index 501b7969d9..55deba2fe7 100644 --- a/docs/recipes/flip_and_rotate.ipynb +++ b/docs/recipes/flip_and_rotate.ipynb @@ -1867,6 +1867,11 @@ ], "metadata": { "celltoolbar": "Edit Metadata", + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-09-16", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { "display_name": "Python [conda env:DEEPLABCUT_newGUI] *", "language": "python", diff --git a/docs/recipes/fmpose3d.ipynb b/docs/recipes/fmpose3d.ipynb index 35c634d199..0a345227ed 100644 --- a/docs/recipes/fmpose3d.ipynb +++ b/docs/recipes/fmpose3d.ipynb @@ -1,344 +1,349 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "(fmpose3d-recipe)=\n", - "# 3D Pose Estimation with FMPose3D\n", - "\n", - "## Overview\n", - "**[FMPose3D: monocular 3D pose estimation via flow matching](https://arxiv.org/abs/2602.05755)** by Ti Wang, Xiaohang Yu, and Mackenzie Weygandt Mathis.\n", - "\n", - "| [Paper](https://arxiv.org/abs/2602.05755) | [Project Page](https://xiu-cs.github.io/FMPose3D/) | [GitHub](https://github.com/AdaptiveMotorControlLab/FMPose3D) | [PyPI](https://pypi.org/project/fmpose3d/) |\n", - "\n", - "FMPose3D lifts 2D keypoints from a single image into 3D poses using **flow matching** β€” a generative technique based on ODE sampling. It generates multiple plausible 3D pose hypotheses in just a few steps, then aggregates them using a reprojection-based Bayesian module (RPEA) for accurate predictions, achieving state-of-the-art results on human and animal 3D pose benchmarks.\n", - "\n", - "\n", - "This recipe shows how to use FMPose3D in DeepLabCut for monocular 3D pose\n", - "estimation. Two pipelines are available:\n", - "\n", - "| Pipeline | 2D Estimator | Skeleton | Joints |\n", - "|----------|-------------|----------|--------|\n", - "| **Human** | HRNet + YOLO | H36M | 17 |\n", - "| **Animal** | DeepLabCut SuperAnimal | Animal3D | 26 |\n", - "\n", - "Model weights are hosted on HuggingFace Hub and downloaded automatically on\n", - "first use.\n", - "\n", - "```{admonition} Prerequisites\n", - ":class: note\n", - "\n", - "Install the `fmpose3d` package before running this notebook:\n", - "\n", - " pip install fmpose3d\n", - "\n", - "A GPU is recommended but not required β€” CPU inference works out of the box.\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Setup\n", - "\n", - "Import the DeepLabCut convenience wrapper and a few helpers." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "from mpl_toolkits.mplot3d import Axes3D\n", - "\n", - "from deeplabcut.modelzoo.fmpose_3d.fmpose3d import get_fmpose3d_inference_api" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Human Pose Estimation (end-to-end)\n", - "\n", - "The simplest way to get 3D human poses is the **end-to-end** pipeline.\n", - "`get_fmpose3d_inference_api` creates an inference object that handles\n", - "2D detection and 3D lifting in a single `predict` call. Weights are\n", - "downloaded automatically from HuggingFace on first use." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Create the human pose API (downloads weights on first call)\n", - "human_api = get_fmpose3d_inference_api(\n", - " model_type=\"fmpose3d_humans\",\n", - " device=\"cuda:0\", # use \"cpu\" if no GPU is available\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Run end-to-end inference on an image\n", - "image_path = \"path/to/your/image.jpg\" # replace with your image path\n", - "result = human_api.predict(source=image_path)\n", - "\n", - "print(\"3D poses (root-relative):\", result.poses_3d.shape) # (num_frames, 17, 3)\n", - "print(\"3D poses (world coords):\", result.poses_3d_world.shape) # (num_frames, 17, 3)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Accepted input sources\n", - "\n", - "`predict` (and `prepare_2d`) accept a variety of input types:\n", - "\n", - "- A **file path** (`str` or `Path`) to a single image\n", - "- A **directory** of images\n", - "- A **numpy array** β€” either a single frame `(H, W, C)` or a batch `(N, H, W, C)`\n", - "- A **list** of any of the above" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Animal Pose Estimation (end-to-end)\n", - "\n", - "Switching to the **animal** pipeline only requires changing `model_type`.\n", - "This pipeline uses DeepLabCut SuperAnimal for 2D detection and outputs\n", - "26-joint Animal3D skeletons." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Create the animal pose API\n", - "animal_api = get_fmpose3d_inference_api(\n", - " model_type=\"fmpose3d_animals\",\n", - " device=\"cuda:0\",\n", - ")\n", - "\n", - "# Run inference\n", - "animal_image_path = \"path/to/your/animal_image.jpg\"\n", - "animal_result = animal_api.predict(source=animal_image_path)\n", - "\n", - "print(\"3D poses:\", animal_result.poses_3d.shape) # (num_frames, 26, 3)\n", - "print(\"3D poses (regularized):\", animal_result.poses_3d_world.shape)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "```{note}\n", - "For animals, `poses_3d_world` contains **limb-regularized** poses (the\n", - "skeleton is rotated so that the average limb direction is vertical) rather\n", - "than a camera-to-world transform.\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Two-Step Inference (2D then 3D)\n", - "\n", - "For more control, you can run the 2D and 3D stages separately. This is\n", - "useful when you want to inspect or modify 2D keypoints before lifting." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "api = get_fmpose3d_inference_api(model_type=\"fmpose3d_animals\", device=\"cuda:0\")\n", - "\n", - "# Step 1: detect 2D keypoints\n", - "result_2d = api.prepare_2d(source=animal_image_path)\n", - "\n", - "print(\"2D keypoints:\", result_2d.keypoints.shape) # (num_persons, num_frames, J, 2)\n", - "print(\"Confidence scores:\", result_2d.scores.shape) # (num_persons, num_frames, J)\n", - "print(\"Image size (H, W):\", result_2d.image_size)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Step 2: lift 2D keypoints to 3D\n", - "result_3d = api.pose_3d(\n", - " keypoints_2d=result_2d.keypoints,\n", - " image_size=result_2d.image_size,\n", - ")\n", - "\n", - "print(\"Lifted 3D poses:\", result_3d.poses_3d.shape) # (num_frames, J, 3)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Lifting DeepLabCut 2D Predictions to 3D\n", - "\n", - "A common workflow is to use a DeepLabCut model you have already trained for\n", - "2D pose estimation, then lift those predictions to 3D with FMPose3D. The\n", - "example below runs DLC inference with `deeplabcut.analyze_images` and feeds\n", - "the resulting keypoints straight into the 3D lifter.\n", - "\n", - "```{admonition} Keypoint compatibility\n", - ":class: warning\n", - "\n", - "The FMPose3D lifter was trained on specific skeleton layouts (17 H36M joints\n", - "for humans, 26 Animal3D joints for animals). Your DLC model's bodyparts must\n", - "match one of these layouts for the lifted poses to be meaningful. If your\n", - "skeleton differs, you will need to select or re-order the relevant subset of\n", - "keypoints before calling `pose_3d`.\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "import deeplabcut\n", - "\n", - "# ── 1. Run DLC 2D inference ───────────────────────────────────────────────\n", - "# analyze_images returns a dict mapping each image path to its predictions.\n", - "# Each prediction contains a \"bodyparts\" array of shape\n", - "# (num_individuals, num_bodyparts, 3) where 3 = (x, y, likelihood).\n", - "\n", - "config_path = \"path/to/my_dlc_project/config.yaml\"\n", - "image_paths = [\"frame_001.png\", \"frame_002.png\", \"frame_003.png\"]\n", - "\n", - "predictions = deeplabcut.analyze_images(\n", - " config=config_path,\n", - " images=image_paths,\n", - " shuffle=1,\n", - " device=\"cuda:0\",\n", - ")\n", - "\n", - "# ── 2. Extract (x, y) keypoints from each frame ──────────────────────────\n", - "# Stack all frames into a single array and take only the first individual.\n", - "all_bodyparts = np.stack([\n", - " predictions[img][\"bodyparts\"][0] # first individual per frame\n", - " for img in image_paths\n", - "]) # shape: (num_frames, num_bodyparts, 3)\n", - "\n", - "keypoints_2d = all_bodyparts[:, :, :2] # drop likelihood β†’ (num_frames, J, 2)\n", - "print(\"keypoints_2d shape:\", keypoints_2d.shape)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# ── 3. Lift DLC 2D keypoints to 3D ────────────────────────────────────────\n", - "# image_size = (height, width) of the frames the DLC model was run on.\n", - "import cv2\n", - "\n", - "sample_img = cv2.imread(image_paths[0])\n", - "image_size = sample_img.shape[:2] # (height, width)\n", - "\n", - "api = get_fmpose3d_inference_api(model_type=\"fmpose3d_animals\", device=\"cuda:0\")\n", - "result_3d = api.pose_3d(\n", - " keypoints_2d=keypoints_2d,\n", - " image_size=image_size,\n", - " seed=42, # for reproducible sampling\n", - ")\n", - "\n", - "print(\"3D poses (root-relative):\", result_3d.poses_3d.shape) # (num_frames, J, 3)\n", - "print(\"3D poses (post-processed):\", result_3d.poses_3d_world.shape)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "```{tip}\n", - "If you are working with video frames from `deeplabcut.analyze_videos`\n", - "instead of individual images, you can read `image_size` from the video:\n", - "\n", - " import cv2\n", - " cap = cv2.VideoCapture(\"path/to/video.mp4\")\n", - " image_size = (int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)),\n", - " int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)))\n", - " cap.release()\n", - "\n", - "You will also need to load the keypoints from the `.h5` file that\n", - "`analyze_videos` produces:\n", - "\n", - " import pandas as pd\n", - " df = pd.read_hdf(\"path/to/videoDLC_scorer.h5\")\n", - " scorer = df.columns.get_level_values(\"scorer\").unique()[0]\n", - " bodyparts = df[scorer].columns.get_level_values(\"bodyparts\").unique()\n", - " coords = df[scorer].values.reshape(len(df), len(bodyparts), 3)\n", - " keypoints_2d = coords[:, :, :2] # (num_frames, num_bodyparts, 2)\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Further Reading\n", - "\n", - "- [FMPose3D repository](https://github.com/AdaptiveMotorControlLab/FMPose3D)\n", - " β€” full API documentation and model details.\n", - "- [DeepLabCut Model Zoo](https://deeplabcut.github.io/DeepLabCut/docs/ModelZoo.html)\n", - " β€” other pre-trained models available in DeepLabCut." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python", - "version": "3.10.0" - } + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "(fmpose3d-recipe)=\n", + "# 3D Pose Estimation with FMPose3D\n", + "\n", + "## Overview\n", + "**[FMPose3D: monocular 3D pose estimation via flow matching](https://arxiv.org/abs/2602.05755)** by Ti Wang, Xiaohang Yu, and Mackenzie Weygandt Mathis.\n", + "\n", + "| [Paper](https://arxiv.org/abs/2602.05755) | [Project Page](https://xiu-cs.github.io/FMPose3D/) | [GitHub](https://github.com/AdaptiveMotorControlLab/FMPose3D) | [PyPI](https://pypi.org/project/fmpose3d/) |\n", + "\n", + "FMPose3D lifts 2D keypoints from a single image into 3D poses using **flow matching** β€” a generative technique based on ODE sampling. It generates multiple plausible 3D pose hypotheses in just a few steps, then aggregates them using a reprojection-based Bayesian module (RPEA) for accurate predictions, achieving state-of-the-art results on human and animal 3D pose benchmarks.\n", + "\n", + "\n", + "This recipe shows how to use FMPose3D in DeepLabCut for monocular 3D pose\n", + "estimation. Two pipelines are available:\n", + "\n", + "| Pipeline | 2D Estimator | Skeleton | Joints |\n", + "|----------|-------------|----------|--------|\n", + "| **Human** | HRNet + YOLO | H36M | 17 |\n", + "| **Animal** | DeepLabCut SuperAnimal | Animal3D | 26 |\n", + "\n", + "Model weights are hosted on HuggingFace Hub and downloaded automatically on\n", + "first use.\n", + "\n", + "```{admonition} Prerequisites\n", + ":class: note\n", + "\n", + "Install the `fmpose3d` package before running this notebook:\n", + "\n", + " pip install fmpose3d\n", + "\n", + "A GPU is recommended but not required β€” CPU inference works out of the box.\n", + "```" + ] }, - "nbformat": 4, - "nbformat_minor": 4 + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "Import the DeepLabCut convenience wrapper and a few helpers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from mpl_toolkits.mplot3d import Axes3D\n", + "\n", + "from deeplabcut.modelzoo.fmpose_3d.fmpose3d import get_fmpose3d_inference_api" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Human Pose Estimation (end-to-end)\n", + "\n", + "The simplest way to get 3D human poses is the **end-to-end** pipeline.\n", + "`get_fmpose3d_inference_api` creates an inference object that handles\n", + "2D detection and 3D lifting in a single `predict` call. Weights are\n", + "downloaded automatically from HuggingFace on first use." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Create the human pose API (downloads weights on first call)\n", + "human_api = get_fmpose3d_inference_api(\n", + " model_type=\"fmpose3d_humans\",\n", + " device=\"cuda:0\", # use \"cpu\" if no GPU is available\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Run end-to-end inference on an image\n", + "image_path = \"path/to/your/image.jpg\" # replace with your image path\n", + "result = human_api.predict(source=image_path)\n", + "\n", + "print(\"3D poses (root-relative):\", result.poses_3d.shape) # (num_frames, 17, 3)\n", + "print(\"3D poses (world coords):\", result.poses_3d_world.shape) # (num_frames, 17, 3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Accepted input sources\n", + "\n", + "`predict` (and `prepare_2d`) accept a variety of input types:\n", + "\n", + "- A **file path** (`str` or `Path`) to a single image\n", + "- A **directory** of images\n", + "- A **numpy array** β€” either a single frame `(H, W, C)` or a batch `(N, H, W, C)`\n", + "- A **list** of any of the above" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Animal Pose Estimation (end-to-end)\n", + "\n", + "Switching to the **animal** pipeline only requires changing `model_type`.\n", + "This pipeline uses DeepLabCut SuperAnimal for 2D detection and outputs\n", + "26-joint Animal3D skeletons." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Create the animal pose API\n", + "animal_api = get_fmpose3d_inference_api(\n", + " model_type=\"fmpose3d_animals\",\n", + " device=\"cuda:0\",\n", + ")\n", + "\n", + "# Run inference\n", + "animal_image_path = \"path/to/your/animal_image.jpg\"\n", + "animal_result = animal_api.predict(source=animal_image_path)\n", + "\n", + "print(\"3D poses:\", animal_result.poses_3d.shape) # (num_frames, 26, 3)\n", + "print(\"3D poses (regularized):\", animal_result.poses_3d_world.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```{note}\n", + "For animals, `poses_3d_world` contains **limb-regularized** poses (the\n", + "skeleton is rotated so that the average limb direction is vertical) rather\n", + "than a camera-to-world transform.\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Two-Step Inference (2D then 3D)\n", + "\n", + "For more control, you can run the 2D and 3D stages separately. This is\n", + "useful when you want to inspect or modify 2D keypoints before lifting." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "api = get_fmpose3d_inference_api(model_type=\"fmpose3d_animals\", device=\"cuda:0\")\n", + "\n", + "# Step 1: detect 2D keypoints\n", + "result_2d = api.prepare_2d(source=animal_image_path)\n", + "\n", + "print(\"2D keypoints:\", result_2d.keypoints.shape) # (num_persons, num_frames, J, 2)\n", + "print(\"Confidence scores:\", result_2d.scores.shape) # (num_persons, num_frames, J)\n", + "print(\"Image size (H, W):\", result_2d.image_size)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Step 2: lift 2D keypoints to 3D\n", + "result_3d = api.pose_3d(\n", + " keypoints_2d=result_2d.keypoints,\n", + " image_size=result_2d.image_size,\n", + ")\n", + "\n", + "print(\"Lifted 3D poses:\", result_3d.poses_3d.shape) # (num_frames, J, 3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Lifting DeepLabCut 2D Predictions to 3D\n", + "\n", + "A common workflow is to use a DeepLabCut model you have already trained for\n", + "2D pose estimation, then lift those predictions to 3D with FMPose3D. The\n", + "example below runs DLC inference with `deeplabcut.analyze_images` and feeds\n", + "the resulting keypoints straight into the 3D lifter.\n", + "\n", + "```{admonition} Keypoint compatibility\n", + ":class: warning\n", + "\n", + "The FMPose3D lifter was trained on specific skeleton layouts (17 H36M joints\n", + "for humans, 26 Animal3D joints for animals). Your DLC model's bodyparts must\n", + "match one of these layouts for the lifted poses to be meaningful. If your\n", + "skeleton differs, you will need to select or re-order the relevant subset of\n", + "keypoints before calling `pose_3d`.\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import deeplabcut\n", + "\n", + "# ── 1. Run DLC 2D inference ───────────────────────────────────────────────\n", + "# analyze_images returns a dict mapping each image path to its predictions.\n", + "# Each prediction contains a \"bodyparts\" array of shape\n", + "# (num_individuals, num_bodyparts, 3) where 3 = (x, y, likelihood).\n", + "\n", + "config_path = \"path/to/my_dlc_project/config.yaml\"\n", + "image_paths = [\"frame_001.png\", \"frame_002.png\", \"frame_003.png\"]\n", + "\n", + "predictions = deeplabcut.analyze_images(\n", + " config=config_path,\n", + " images=image_paths,\n", + " shuffle=1,\n", + " device=\"cuda:0\",\n", + ")\n", + "\n", + "# ── 2. Extract (x, y) keypoints from each frame ──────────────────────────\n", + "# Stack all frames into a single array and take only the first individual.\n", + "all_bodyparts = np.stack([\n", + " predictions[img][\"bodyparts\"][0] # first individual per frame\n", + " for img in image_paths\n", + "]) # shape: (num_frames, num_bodyparts, 3)\n", + "\n", + "keypoints_2d = all_bodyparts[:, :, :2] # drop likelihood β†’ (num_frames, J, 2)\n", + "print(\"keypoints_2d shape:\", keypoints_2d.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# ── 3. Lift DLC 2D keypoints to 3D ────────────────────────────────────────\n", + "# image_size = (height, width) of the frames the DLC model was run on.\n", + "import cv2\n", + "\n", + "sample_img = cv2.imread(image_paths[0])\n", + "image_size = sample_img.shape[:2] # (height, width)\n", + "\n", + "api = get_fmpose3d_inference_api(model_type=\"fmpose3d_animals\", device=\"cuda:0\")\n", + "result_3d = api.pose_3d(\n", + " keypoints_2d=keypoints_2d,\n", + " image_size=image_size,\n", + " seed=42, # for reproducible sampling\n", + ")\n", + "\n", + "print(\"3D poses (root-relative):\", result_3d.poses_3d.shape) # (num_frames, J, 3)\n", + "print(\"3D poses (post-processed):\", result_3d.poses_3d_world.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```{tip}\n", + "If you are working with video frames from `deeplabcut.analyze_videos`\n", + "instead of individual images, you can read `image_size` from the video:\n", + "\n", + " import cv2\n", + " cap = cv2.VideoCapture(\"path/to/video.mp4\")\n", + " image_size = (int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)),\n", + " int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)))\n", + " cap.release()\n", + "\n", + "You will also need to load the keypoints from the `.h5` file that\n", + "`analyze_videos` produces:\n", + "\n", + " import pandas as pd\n", + " df = pd.read_hdf(\"path/to/videoDLC_scorer.h5\")\n", + " scorer = df.columns.get_level_values(\"scorer\").unique()[0]\n", + " bodyparts = df[scorer].columns.get_level_values(\"bodyparts\").unique()\n", + " coords = df[scorer].values.reshape(len(df), len(bodyparts), 3)\n", + " keypoints_2d = coords[:, :, :2] # (num_frames, num_bodyparts, 2)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Further Reading\n", + "\n", + "- [FMPose3D repository](https://github.com/AdaptiveMotorControlLab/FMPose3D)\n", + " β€” full API documentation and model details.\n", + "- [DeepLabCut Model Zoo](https://deeplabcut.github.io/DeepLabCut/docs/ModelZoo.html)\n", + " β€” other pre-trained models available in DeepLabCut." + ] + } + ], + "metadata": { + "deeplabcut": { + "ignore": false, + "last_content_updated": "2026-02-13", + "last_metadata_updated": "2026-03-06" + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 } diff --git a/docs/recipes/installTips.md b/docs/recipes/installTips.md index ab4565a880..b276a17324 100644 --- a/docs/recipes/installTips.md +++ b/docs/recipes/installTips.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-02-28' + last_metadata_updated: '2026-03-06' + ignore: false +--- (installation-tips)= # Installation Tips diff --git a/docs/recipes/io.md b/docs/recipes/io.md index e97238628b..f18c4d28b6 100644 --- a/docs/recipes/io.md +++ b/docs/recipes/io.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2022-04-11' + last_metadata_updated: '2026-03-06' + ignore: false +--- # Input/output manipulations with DeepLabCut ## Analyzing very large videos in chunks diff --git a/docs/recipes/nn.md b/docs/recipes/nn.md index 9377c446ca..3ca09cf14b 100644 --- a/docs/recipes/nn.md +++ b/docs/recipes/nn.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-06-30' + last_metadata_updated: '2026-03-06' + ignore: false +--- (tf-training-tips-and-tricks)= # Model training tips & tricks diff --git a/docs/recipes/pose_cfg_file_breakdown.md b/docs/recipes/pose_cfg_file_breakdown.md index 2f79ac28d3..dbe97ea273 100644 --- a/docs/recipes/pose_cfg_file_breakdown.md +++ b/docs/recipes/pose_cfg_file_breakdown.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-02-28' + last_metadata_updated: '2026-03-06' + ignore: false +--- # The `pose_cfg.yaml` Guideline Handbook ::::{warning} diff --git a/docs/recipes/post.md b/docs/recipes/post.md index cbcb78c673..1ebdf09ec5 100644 --- a/docs/recipes/post.md +++ b/docs/recipes/post.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2022-06-08' + last_metadata_updated: '2026-03-06' + ignore: false +--- # Some data processing recipes! ## Flagging frames with abnormal bodypart distances diff --git a/docs/recipes/publishing_notebooks_into_the_DLC_main_cookbook.md b/docs/recipes/publishing_notebooks_into_the_DLC_main_cookbook.md index 83dbb8c75e..ca2ff2c727 100644 --- a/docs/recipes/publishing_notebooks_into_the_DLC_main_cookbook.md +++ b/docs/recipes/publishing_notebooks_into_the_DLC_main_cookbook.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-06-30' + last_metadata_updated: '2026-03-06' + ignore: false +--- # Publishing Notebooks into the Main DLC Cookbook ### Your Recipe Guide to Contributing to the DLC Cookbook diff --git a/docs/roadmap.md b/docs/roadmap.md index b303754e37..04228cb56e 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-02-28' + last_metadata_updated: '2026-03-06' + ignore: false +--- (dev-roadmap)= ## A development roadmap for DeepLabCut diff --git a/docs/standardDeepLabCut_UserGuide.md b/docs/standardDeepLabCut_UserGuide.md index f7e653e487..09812b7050 100644 --- a/docs/standardDeepLabCut_UserGuide.md +++ b/docs/standardDeepLabCut_UserGuide.md @@ -1,3 +1,9 @@ +--- +deeplabcut: + last_content_updated: '2025-06-30' + last_metadata_updated: '2026-03-06' + ignore: false +--- (single-animal-userguide)= # DeepLabCut User Guide (for single animal projects) diff --git a/examples/COLAB/COLAB_3miceDemo.ipynb b/examples/COLAB/COLAB_3miceDemo.ipynb index 427602ff4c..43a6260a07 100644 --- a/examples/COLAB/COLAB_3miceDemo.ipynb +++ b/examples/COLAB/COLAB_3miceDemo.ipynb @@ -256,6 +256,11 @@ "name": "Copy of 3micedemo.ipynb", "provenance": [] }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2026-02-10", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { "display_name": "Python 3", "name": "python3" diff --git a/examples/COLAB/COLAB_BUCTD_and_CTD_tracking.ipynb b/examples/COLAB/COLAB_BUCTD_and_CTD_tracking.ipynb index e7e123ca23..43b7def161 100644 --- a/examples/COLAB/COLAB_BUCTD_and_CTD_tracking.ipynb +++ b/examples/COLAB/COLAB_BUCTD_and_CTD_tracking.ipynb @@ -2358,6 +2358,11 @@ "gpuType": "T4", "provenance": [] }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-10-02", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { "display_name": "Python 3", "name": "python3" diff --git a/examples/COLAB/COLAB_DEMO_SuperAnimal.ipynb b/examples/COLAB/COLAB_DEMO_SuperAnimal.ipynb index f6efebe8b0..c0f5c70f46 100644 --- a/examples/COLAB/COLAB_DEMO_SuperAnimal.ipynb +++ b/examples/COLAB/COLAB_DEMO_SuperAnimal.ipynb @@ -213,6 +213,11 @@ "colab": { "provenance": [] }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-06-30", + "last_metadata_updated": "2026-03-06" + }, "gpuClass": "standard", "kernelspec": { "display_name": "dlc", diff --git a/examples/COLAB/COLAB_DEMO_mouse_openfield.ipynb b/examples/COLAB/COLAB_DEMO_mouse_openfield.ipynb index a4d7543147..139e723c0f 100644 --- a/examples/COLAB/COLAB_DEMO_mouse_openfield.ipynb +++ b/examples/COLAB/COLAB_DEMO_mouse_openfield.ipynb @@ -310,6 +310,11 @@ "name": "Colab_DEMO_mouse_openfield.ipynb", "provenance": [] }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-09-16", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { "display_name": "Python [default]", "language": "python", diff --git a/examples/COLAB/COLAB_DLC_ModelZoo.ipynb b/examples/COLAB/COLAB_DLC_ModelZoo.ipynb index 1c312afd22..79b655b915 100644 --- a/examples/COLAB/COLAB_DLC_ModelZoo.ipynb +++ b/examples/COLAB/COLAB_DLC_ModelZoo.ipynb @@ -1,315 +1,320 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "view-in-github" - }, - "source": [ - "\"Open" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "RK255E7YoEIt" - }, - "source": [ - "# DeepLabCut Model Zoo user-contributed models\n", - "\n", - "🚨 **WARNING** -- This is using the old version from 2020-2023 with user-supplied models. Please see the SuperAnimal notebook if you want to use our Foundational Models for Quadrupeds or mice.\n", - "\n", - "![alt text](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1616492373700-PGOAC72IOB6AUE47VTJX/ke17ZwdGBToddI8pDm48kB8JrdUaZR-OSkKLqWQPp_YUqsxRUqqbr1mOJYKfIPR7LoDQ9mXPOjoJoqy81S2I8N_N4V1vUb5AoIIIbLZhVYwL8IeDg6_3B-BRuF4nNrNcQkVuAT7tdErd0wQFEGFSnBqyW03PFN2MN6T6ry5cmXqqA9xITfsbVGDrg_goIDasRCalqV8R3606BuxERAtDaQ/modelzoo.png?format=1000w)\n", - "\n", - "http://modelzoo.deeplabcut.org\n", - "\n", - "You can use this notebook to analyze videos with pretrained networks from our model zoo - NO local installation of DeepLabCut is needed!\n", - "\n", - "- **What you need:** a video of your favorite dog, cat, human, etc: check the list of currently available models here: http://modelzoo.deeplabcut.org\n", - "\n", - "- **What to do:** (1) in the top right corner, click \"CONNECT\". Then, just hit run (play icon) on each cell below and follow the instructions!\n", - "\n", - "## **Please consider giving back and labeling a little data to help make each network even better!**\n", - "\n", - "We have a WebApp, so no need to install anything, just a few clicks! We'd really appreciate your help!\n", - " \n", - "https://contrib.deeplabcut.org/\n", - "\n", - "\n", - "- **Note, if you performance is less that you would like:** firstly check the labeled_video parameters (i.e. \"pcutoff\" in the config.yaml file that will set the video plotting) - see the end of this notebook. You can also use the model in your own projects locally. Please be sure to cite the papers for the model, and http://modelzoo.deeplabcut.org (paper forthcoming!)\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "## **Let's get going: install DeepLabCut into COLAB:**\n", - "\n", - "*Also, be sure you are connected to a GPU: go to menu, click Runtime > Change Runtime Type > select \"GPU\"*\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Install the latest version of DeepLabCut\n", - "!pip install --pre \"deeplabcut[tf,modelzoo]\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Important - Restart the Runtime for the updated packages to be imported!\n", - "\n", - "PLEASE, click \"restart runtime\" from the output above before proceeding!" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "ZT4PwGSbYQEO" - }, - "source": [ - "## Now let's set the backend & import the DeepLabCut package\n", - "### (if colab is buggy/throws an error, just rerun this cell):" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "bvoiWefrYQEP" - }, - "outputs": [], - "source": [ - "import os\n", - "import deeplabcut" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "syweXs88tyuO" - }, - "source": [ - "## Next, run the cell below to upload your video file from your computer:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "7eqEZYs_CaLy" - }, - "outputs": [], - "source": [ - "from google.colab import files\n", - "\n", - "uploaded = files.upload()\n", - "for filepath, content in uploaded.items():\n", - " print(f'User uploaded file \"{filepath}\" with length {len(content)} bytes')\n", - "video_path = os.path.abspath(filepath)\n", - "\n", - "# If this cell fails (e.g., when using Safari in place of Google Chrome),\n", - "# manually upload your video via the Files menu to the left\n", - "# and define `video_path` yourself with right click > copy path on the video." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "YsaqOTkZtf-w" - }, - "source": [ - "## Select your model from the dropdown menu, then below (optionally) input the name you want for the project:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "Ih0t7lUjYQEd" - }, - "outputs": [], - "source": [ - "import ipywidgets as widgets\n", - "from IPython.display import display\n", - "\n", - "model_options = deeplabcut.create_project.modelzoo.Modeloptions\n", - "model_selection = widgets.Dropdown(\n", - " options=model_options,\n", - " value=model_options[0],\n", - " description=\"Choose a DLC ModelZoo model!\",\n", - " disabled=False\n", - ")\n", - "display(model_selection)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "UV0QXswGCFrI" - }, - "outputs": [], - "source": [ - "project_name = 'myDLC_modelZoo'\n", - "your_name = 'teamDLC'\n", - "model2use = model_selection.value\n", - "videotype = os.path.splitext(video_path)[-1].lstrip('.') #or MOV, or avi, whatever you uploaded!" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "JQxko-t3uMVO" - }, - "source": [ - "## Attention on this step !!\n", - "- Please note that for optimal performance your videos should contain frames that are around ~300-600 pixels (on one edge). If you have a larger video (like from an iPhone, first downsize by running this please! :)\n", - "\n", - "- Thus, if you're using an iPhone, or such, you'll need to downsample the video first by running the code below**\n", - "\n", - "(no need to edit it unless you want to change the size)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "WpAX3BKY94e0" - }, - "outputs": [], - "source": [ - "video_path = deeplabcut.DownSampleVideo(video_path, width=300)\n", - "print(video_path)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "KJm_Vbx-s5OY" - }, - "source": [ - "## Lastly, run the cell below to create a pretrained project, analyze your video with your selected pretrained network, plot trajectories, and create a labeled video!:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "T9MGgAdIFKPY" - }, - "outputs": [], - "source": [ - "config_path, train_config_path = deeplabcut.create_pretrained_project(\n", - " project_name,\n", - " your_name,\n", - " [video_path],\n", - " videotype=videotype,\n", - " model=model2use,\n", - " analyzevideo=True,\n", - " createlabeledvideo=True,\n", - " copy_videos=True, #must leave copy_videos=True\n", - " engine=deeplabcut.Engine.TF,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "WS-KxhBMvEBj" - }, - "source": [ - "Now, you can move this project from Colab (i.e. download it to your GoogleDrive), and use it like a normal standard project!\n", - "\n", - "You can analyze more videos, extract outliers, refine then, and/or then add new key points + label new frames, and retrain if desired. We hope this gives you a good launching point for your work!\n", - "\n", - "###Happy DeepLabCutting! Welcome to the Zoo :)\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "KPOqiLmo6d7t" - }, - "source": [ - "## More advanced options:\n", - "\n", - "- If you would now like to customize the video/plots - i.e., color, dot size, threshold for the point to be plotted (pcutoff), please simply edit the \"config.yaml\" file by updating the values below:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "yGLNVK1q6rIp" - }, - "outputs": [], - "source": [ - "# Updating the plotting within the config.yaml file (without opening it ;):\n", - "edits = {\n", - " 'dotsize': 7, # size of the dots!\n", - " 'colormap': 'spring', # any matplotlib colormap!\n", - " 'pcutoff': 0.5, # the higher the more conservative the plotting!\n", - "}\n", - "deeplabcut.auxiliaryfunctions.edit_config(config_path, edits)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "Vlc0wZgB7R5e" - }, - "outputs": [], - "source": [ - "# re-create the labeled video (first you will need to delete in the folder to the LEFT!):\n", - "project_path = os.path.dirname(config_path)\n", - "full_video_path = os.path.join(\n", - " project_path,\n", - " 'videos',\n", - " os.path.basename(video_path),\n", - ")\n", - "\n", - "#filter predictions (should already be done above ;):\n", - "deeplabcut.filterpredictions(config_path, [full_video_path], videotype=videotype)\n", - "\n", - "#re-create the video with your edits!\n", - "deeplabcut.create_labeled_video(config_path, [full_video_path], videotype=videotype, filtered=True)" - ] - } - ], - "metadata": { - "colab": { - "include_colab_link": true, - "name": "Copy of COLAB_DLC_ModelZoo.ipynb", - "provenance": [], - "toc_visible": true - }, - "gpuClass": "standard", - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.7" - } + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "view-in-github" + }, + "source": [ + "\"Open" + ] }, - "nbformat": 4, - "nbformat_minor": 0 + { + "cell_type": "markdown", + "metadata": { + "id": "RK255E7YoEIt" + }, + "source": [ + "# DeepLabCut Model Zoo user-contributed models\n", + "\n", + "🚨 **WARNING** -- This is using the old version from 2020-2023 with user-supplied models. Please see the SuperAnimal notebook if you want to use our Foundational Models for Quadrupeds or mice.\n", + "\n", + "![alt text](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1616492373700-PGOAC72IOB6AUE47VTJX/ke17ZwdGBToddI8pDm48kB8JrdUaZR-OSkKLqWQPp_YUqsxRUqqbr1mOJYKfIPR7LoDQ9mXPOjoJoqy81S2I8N_N4V1vUb5AoIIIbLZhVYwL8IeDg6_3B-BRuF4nNrNcQkVuAT7tdErd0wQFEGFSnBqyW03PFN2MN6T6ry5cmXqqA9xITfsbVGDrg_goIDasRCalqV8R3606BuxERAtDaQ/modelzoo.png?format=1000w)\n", + "\n", + "http://modelzoo.deeplabcut.org\n", + "\n", + "You can use this notebook to analyze videos with pretrained networks from our model zoo - NO local installation of DeepLabCut is needed!\n", + "\n", + "- **What you need:** a video of your favorite dog, cat, human, etc: check the list of currently available models here: http://modelzoo.deeplabcut.org\n", + "\n", + "- **What to do:** (1) in the top right corner, click \"CONNECT\". Then, just hit run (play icon) on each cell below and follow the instructions!\n", + "\n", + "## **Please consider giving back and labeling a little data to help make each network even better!**\n", + "\n", + "We have a WebApp, so no need to install anything, just a few clicks! We'd really appreciate your help!\n", + " \n", + "https://contrib.deeplabcut.org/\n", + "\n", + "\n", + "- **Note, if you performance is less that you would like:** firstly check the labeled_video parameters (i.e. \"pcutoff\" in the config.yaml file that will set the video plotting) - see the end of this notebook. You can also use the model in your own projects locally. Please be sure to cite the papers for the model, and http://modelzoo.deeplabcut.org (paper forthcoming!)\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "## **Let's get going: install DeepLabCut into COLAB:**\n", + "\n", + "*Also, be sure you are connected to a GPU: go to menu, click Runtime > Change Runtime Type > select \"GPU\"*\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Install the latest version of DeepLabCut\n", + "!pip install --pre \"deeplabcut[tf,modelzoo]\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Important - Restart the Runtime for the updated packages to be imported!\n", + "\n", + "PLEASE, click \"restart runtime\" from the output above before proceeding!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZT4PwGSbYQEO" + }, + "source": [ + "## Now let's set the backend & import the DeepLabCut package\n", + "### (if colab is buggy/throws an error, just rerun this cell):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bvoiWefrYQEP" + }, + "outputs": [], + "source": [ + "import os\n", + "import deeplabcut" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "syweXs88tyuO" + }, + "source": [ + "## Next, run the cell below to upload your video file from your computer:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7eqEZYs_CaLy" + }, + "outputs": [], + "source": [ + "from google.colab import files\n", + "\n", + "uploaded = files.upload()\n", + "for filepath, content in uploaded.items():\n", + " print(f'User uploaded file \"{filepath}\" with length {len(content)} bytes')\n", + "video_path = os.path.abspath(filepath)\n", + "\n", + "# If this cell fails (e.g., when using Safari in place of Google Chrome),\n", + "# manually upload your video via the Files menu to the left\n", + "# and define `video_path` yourself with right click > copy path on the video." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YsaqOTkZtf-w" + }, + "source": [ + "## Select your model from the dropdown menu, then below (optionally) input the name you want for the project:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Ih0t7lUjYQEd" + }, + "outputs": [], + "source": [ + "import ipywidgets as widgets\n", + "from IPython.display import display\n", + "\n", + "model_options = deeplabcut.create_project.modelzoo.Modeloptions\n", + "model_selection = widgets.Dropdown(\n", + " options=model_options,\n", + " value=model_options[0],\n", + " description=\"Choose a DLC ModelZoo model!\",\n", + " disabled=False\n", + ")\n", + "display(model_selection)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "UV0QXswGCFrI" + }, + "outputs": [], + "source": [ + "project_name = 'myDLC_modelZoo'\n", + "your_name = 'teamDLC'\n", + "model2use = model_selection.value\n", + "videotype = os.path.splitext(video_path)[-1].lstrip('.') #or MOV, or avi, whatever you uploaded!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JQxko-t3uMVO" + }, + "source": [ + "## Attention on this step !!\n", + "- Please note that for optimal performance your videos should contain frames that are around ~300-600 pixels (on one edge). If you have a larger video (like from an iPhone, first downsize by running this please! :)\n", + "\n", + "- Thus, if you're using an iPhone, or such, you'll need to downsample the video first by running the code below**\n", + "\n", + "(no need to edit it unless you want to change the size)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "WpAX3BKY94e0" + }, + "outputs": [], + "source": [ + "video_path = deeplabcut.DownSampleVideo(video_path, width=300)\n", + "print(video_path)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KJm_Vbx-s5OY" + }, + "source": [ + "## Lastly, run the cell below to create a pretrained project, analyze your video with your selected pretrained network, plot trajectories, and create a labeled video!:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "T9MGgAdIFKPY" + }, + "outputs": [], + "source": [ + "config_path, train_config_path = deeplabcut.create_pretrained_project(\n", + " project_name,\n", + " your_name,\n", + " [video_path],\n", + " videotype=videotype,\n", + " model=model2use,\n", + " analyzevideo=True,\n", + " createlabeledvideo=True,\n", + " copy_videos=True, #must leave copy_videos=True\n", + " engine=deeplabcut.Engine.TF,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WS-KxhBMvEBj" + }, + "source": [ + "Now, you can move this project from Colab (i.e. download it to your GoogleDrive), and use it like a normal standard project!\n", + "\n", + "You can analyze more videos, extract outliers, refine then, and/or then add new key points + label new frames, and retrain if desired. We hope this gives you a good launching point for your work!\n", + "\n", + "###Happy DeepLabCutting! Welcome to the Zoo :)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KPOqiLmo6d7t" + }, + "source": [ + "## More advanced options:\n", + "\n", + "- If you would now like to customize the video/plots - i.e., color, dot size, threshold for the point to be plotted (pcutoff), please simply edit the \"config.yaml\" file by updating the values below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yGLNVK1q6rIp" + }, + "outputs": [], + "source": [ + "# Updating the plotting within the config.yaml file (without opening it ;):\n", + "edits = {\n", + " 'dotsize': 7, # size of the dots!\n", + " 'colormap': 'spring', # any matplotlib colormap!\n", + " 'pcutoff': 0.5, # the higher the more conservative the plotting!\n", + "}\n", + "deeplabcut.auxiliaryfunctions.edit_config(config_path, edits)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Vlc0wZgB7R5e" + }, + "outputs": [], + "source": [ + "# re-create the labeled video (first you will need to delete in the folder to the LEFT!):\n", + "project_path = os.path.dirname(config_path)\n", + "full_video_path = os.path.join(\n", + " project_path,\n", + " 'videos',\n", + " os.path.basename(video_path),\n", + ")\n", + "\n", + "#filter predictions (should already be done above ;):\n", + "deeplabcut.filterpredictions(config_path, [full_video_path], videotype=videotype)\n", + "\n", + "#re-create the video with your edits!\n", + "deeplabcut.create_labeled_video(config_path, [full_video_path], videotype=videotype, filtered=True)" + ] + } + ], + "metadata": { + "colab": { + "include_colab_link": true, + "name": "Copy of COLAB_DLC_ModelZoo.ipynb", + "provenance": [], + "toc_visible": true + }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-10-02", + "last_metadata_updated": "2026-03-06" + }, + "gpuClass": "standard", + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.7" + } + }, + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/examples/COLAB/COLAB_HumanPose_with_RTMPose.ipynb b/examples/COLAB/COLAB_HumanPose_with_RTMPose.ipynb index f3a13ac588..6d3f4be32d 100644 --- a/examples/COLAB/COLAB_HumanPose_with_RTMPose.ipynb +++ b/examples/COLAB/COLAB_HumanPose_with_RTMPose.ipynb @@ -1,1182 +1,1187 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "id": "t3P1R5BTwud1" - }, - "source": [ - "\"Open\n", - "\n", - "# DeepLabCut RTMPose human pose estimation demo" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "tJm8QpTzyAEe" - }, - "source": [ - "Some useful links:\n", - "\n", - "- DeepLabCut's GitHub: [github.com/DeepLabCut/DeepLabCut](https://github.com/DeepLabCut/DeepLabCut/tree/main)\n", - "- DeepLabCut's Documentation: [deeplabcut.github.io/DeepLabCut](https://deeplabcut.github.io/DeepLabCut/README.html)\n", - "\n", - "This notebook illustrates how to use the cloud to run pose estimation on humans using a pre-trained [RTMPose](https://arxiv.org/abs/2303.07399) model. **⚠️Note: It uses DeepLabCut's low-level interface, so may be suited for more experienced users.⚠️**\n", - "\n", - "RTMPose is a top-down pose estimation model, which means that bounding boxes must be obtained for individuals (which is usually done through an [object detection model](https://en.wikipedia.org/wiki/Object_detection)) before running pose estimation. We obtain bounding boxes using a pre-trained object detector provided by [`torchvision`](https://pytorch.org/vision/main/models.html#object-detection-instance-segmentation-and-person-keypoint-detection).\n", - "\n", - "## Selecting the Runtime and Installing DeepLabCut\n", - "\n", - "**First, go to \"Runtime\" ->\"change runtime type\"->select \"Python3\", and then select \"GPU\".**\n", - "\n", - "Next, we need to install DeepLabCut and its dependencies." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "Aj7Fgm0Xx_fS" - }, - "outputs": [], - "source": [ - "# this will take a couple of minutes to install all the dependencies!\n", - "!pip install --pre deeplabcut" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "twiCWHbgzbwH" - }, - "source": [ - "**(Be sure to click \"RESTART RUNTIME\" if it is displayed above before moving on !) You will see this button at the output of the cells above ^.**" - ] + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "t3P1R5BTwud1" + }, + "source": [ + "\"Open\n", + "\n", + "# DeepLabCut RTMPose human pose estimation demo" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tJm8QpTzyAEe" + }, + "source": [ + "Some useful links:\n", + "\n", + "- DeepLabCut's GitHub: [github.com/DeepLabCut/DeepLabCut](https://github.com/DeepLabCut/DeepLabCut/tree/main)\n", + "- DeepLabCut's Documentation: [deeplabcut.github.io/DeepLabCut](https://deeplabcut.github.io/DeepLabCut/README.html)\n", + "\n", + "This notebook illustrates how to use the cloud to run pose estimation on humans using a pre-trained [RTMPose](https://arxiv.org/abs/2303.07399) model. **⚠️Note: It uses DeepLabCut's low-level interface, so may be suited for more experienced users.⚠️**\n", + "\n", + "RTMPose is a top-down pose estimation model, which means that bounding boxes must be obtained for individuals (which is usually done through an [object detection model](https://en.wikipedia.org/wiki/Object_detection)) before running pose estimation. We obtain bounding boxes using a pre-trained object detector provided by [`torchvision`](https://pytorch.org/vision/main/models.html#object-detection-instance-segmentation-and-person-keypoint-detection).\n", + "\n", + "## Selecting the Runtime and Installing DeepLabCut\n", + "\n", + "**First, go to \"Runtime\" ->\"change runtime type\"->select \"Python3\", and then select \"GPU\".**\n", + "\n", + "Next, we need to install DeepLabCut and its dependencies." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Aj7Fgm0Xx_fS" + }, + "outputs": [], + "source": [ + "# this will take a couple of minutes to install all the dependencies!\n", + "!pip install --pre deeplabcut" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "twiCWHbgzbwH" + }, + "source": [ + "**(Be sure to click \"RESTART RUNTIME\" if it is displayed above before moving on !) You will see this button at the output of the cells above ^.**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "x6DugzWMzGoj" + }, + "source": [ + "## Importing Packages and Downloading Model Snapshots" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Y7jKbk_mzPJR" + }, + "source": [ + "Next, we'll need to import `deeplabcut`, `huggingface_hub` and other dependencies needed to run the demo." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "gbXwpGKXzF98", + "outputId": "d7cc8390-e76a-4cc6-b945-42f0951c8d01" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "x6DugzWMzGoj" - }, - "source": [ - "## Importing Packages and Downloading Model Snapshots" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading DLC 3.0.0rc10...\n", + "DLC loaded in light mode; you cannot use any GUI (labeling, relabeling and standalone GUI)\n" + ] + } + ], + "source": [ + "from pathlib import Path\n", + "\n", + "import deeplabcut.pose_estimation_pytorch as dlc_torch\n", + "import huggingface_hub\n", + "import matplotlib.collections as collections\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import torch\n", + "import torchvision.models.detection as detection\n", + "from PIL import Image\n", + "from tqdm import tqdm" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6KWKmWRxzX5R" + }, + "source": [ + "We can now download the pre-trained RTMPose model weights with which we'll run pose estimation." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "L_V11iCszw3s", + "outputId": "8b010e6c-27f5-46ad-f713-2fd07effa3b1" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "Y7jKbk_mzPJR" - }, - "source": [ - "Next, we'll need to import `deeplabcut`, `huggingface_hub` and other dependencies needed to run the demo." - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.11/dist-packages/huggingface_hub/utils/_auth.py:94: UserWarning: \n", + "The secret `HF_TOKEN` does not exist in your Colab secrets.\n", + "To authenticate with the Hugging Face Hub, create a token in your settings tab (https://huggingface.co/settings/tokens), set it as secret in your Google Colab and restart your session.\n", + "You will be able to reuse this secret in all of your notebooks.\n", + "Please note that authentication is recommended but still optional to access public models or datasets.\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "# Folder in COLAB where snapshots will be saved\n", + "model_files = Path(\"hf_files\").resolve()\n", + "model_files.mkdir(exist_ok=True)\n", + "\n", + "# Download the snapshot and model configuration file\n", + "# This is generic code to download any snapshot from HuggingFace\n", + "# To download DeepLabCut SuperAnimal or Model Zoo models, check\n", + "# out dlclibrary!\n", + "path_model_config = Path(\n", + " huggingface_hub.hf_hub_download(\n", + " \"DeepLabCut/HumanBody\",\n", + " \"rtmpose-x_simcc-body7_pytorch_config.yaml\",\n", + " local_dir=model_files,\n", + " )\n", + ")\n", + "path_snapshot = Path(\n", + " huggingface_hub.hf_hub_download(\n", + " \"DeepLabCut/HumanBody\",\n", + " \"rtmpose-x_simcc-body7.pt\",\n", + " local_dir=model_files,\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eEqukXXy0coy" + }, + "source": [ + "We'll now also define some parameters that we'll later use to plot predictions:\n", + "\n", + "- a colormap for the keypoints to plot\n", + "- a colormap for the limbs of the skeleton\n", + "- a skeleton for the model\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "Tam4rfJK0c_b" + }, + "outputs": [], + "source": [ + "cmap_keypoints = plt.get_cmap(\"rainbow\")\n", + "cmap_skeleton = plt.get_cmap(\"rainbow_r\")\n", + "\n", + "bodyparts2connect = [\n", + " (\"right_ankle\", \"right_knee\"),\n", + " (\"right_knee\", \"right_hip\"),\n", + " (\"left_ankle\", \"left_knee\"),\n", + " (\"left_hip\", \"left_knee\"),\n", + " (\"left_hip\", \"right_hip\"),\n", + " (\"right_shoulder\", \"right_hip\"),\n", + " (\"left_shoulder\", \"left_hip\"),\n", + " (\"left_shoulder\", \"right_shoulder\"),\n", + " (\"left_shoulder\", \"left_elbow\"),\n", + " (\"right_shoulder\", \"right_elbow\"),\n", + " (\"left_elbow\", \"left_wrist\"),\n", + " (\"right_elbow\", \"right_wrist\"),\n", + " (\"right_eye\", \"left_ear\"),\n", + " (\"left_eye\", \"right_eye\"),\n", + " (\"left_eye\", \"left_ear\"),\n", + " (\"right_eye\", \"right_ear\"),\n", + " (\"left_ear\", \"left_shoulder\"),\n", + " (\"right_ear\", \"right_shoulder\"),\n", + " (\"left_shoulder\", \"left_elbow\"),\n", + " (\"right_shoulder\", \"right_elbow\"),\n", + "]\n", + "skeleton = [\n", + " [16, 14],\n", + " [14, 12],\n", + " [17, 15],\n", + " [15, 13],\n", + " [12, 13],\n", + " [6, 12],\n", + " [7, 13],\n", + " [6, 7],\n", + " [6, 8],\n", + " [7, 9],\n", + " [8, 10],\n", + " [9, 11],\n", + " [2, 3],\n", + " [1, 2],\n", + " [1, 3],\n", + " [2, 4],\n", + " [3, 5],\n", + " [4, 6],\n", + " [5, 7],\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cCxkkd-b0EJq" + }, + "source": [ + "## Running Inference on Images" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dotn_xN-05gh" + }, + "source": [ + "First, let's upload some images to run inference on. To do so, you can just run the cell below." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 92 }, + "id": "mZtikE1H0D34", + "outputId": "3d47314f-3ed0-40b2-e54d-2677feef9943" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "id": "gbXwpGKXzF98", - "outputId": "d7cc8390-e76a-4cc6-b945-42f0951c8d01", - "colab": { - "base_uri": "https://localhost:8080/" - } - }, - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Loading DLC 3.0.0rc10...\n", - "DLC loaded in light mode; you cannot use any GUI (labeling, relabeling and standalone GUI)\n" - ] - } + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " Upload widget is only available when the cell has been executed in the\n", + " current browser session. Please rerun this cell to enable.\n", + " \n", + " " ], - "source": [ - "from pathlib import Path\n", - "\n", - "import deeplabcut.pose_estimation_pytorch as dlc_torch\n", - "import huggingface_hub\n", - "import matplotlib.collections as collections\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "import torch\n", - "import torchvision.models.detection as detection\n", - "from PIL import Image\n", - "from tqdm import tqdm" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "6KWKmWRxzX5R" - }, - "source": [ - "We can now download the pre-trained RTMPose model weights with which we'll run pose estimation." - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving taylor_swift.jpg to taylor_swift.jpg\n", + "User uploaded file 'taylor_swift.jpg' with length 46915 bytes\n" + ] + } + ], + "source": [ + "from google.colab import files\n", + "\n", + "#JPG or PNG is recommended:\n", + "uploaded = files.upload()\n", + "for filepath, content in uploaded.items():\n", + " print(f\"User uploaded file '{filepath}' with length {len(content)} bytes\")\n", + "\n", + "image_paths = [Path(filepath).resolve() for filepath in uploaded.keys()]\n", + "\n", + "# If this cell fails (e.g., when using Safari in place of Google Chrome),\n", + "# manually upload your image via the Files menu to the left and define\n", + "# `image_paths` yourself with right `click` > `copy path` on the image:\n", + "#\n", + "# image_paths = [\n", + "# Path(\"/path/to/my/image_000.png\"),\n", + "# Path(\"/path/to/my/image_001.png\"),\n", + "# ]\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "nj-HtOBSwtdk", + "outputId": "eb5f3b18-cc89-4dd1-a58e-6c39c62582af" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "id": "L_V11iCszw3s", - "outputId": "8b010e6c-27f5-46ad-f713-2fd07effa3b1", - "colab": { - "base_uri": "https://localhost:8080/" - } - }, - "outputs": [ - { - "output_type": "stream", - "name": "stderr", - "text": [ - "/usr/local/lib/python3.11/dist-packages/huggingface_hub/utils/_auth.py:94: UserWarning: \n", - "The secret `HF_TOKEN` does not exist in your Colab secrets.\n", - "To authenticate with the Hugging Face Hub, create a token in your settings tab (https://huggingface.co/settings/tokens), set it as secret in your Google Colab and restart your session.\n", - "You will be able to reuse this secret in all of your notebooks.\n", - "Please note that authentication is recommended but still optional to access public models or datasets.\n", - " warnings.warn(\n" - ] - } - ], - "source": [ - "# Folder in COLAB where snapshots will be saved\n", - "model_files = Path(\"hf_files\").resolve()\n", - "model_files.mkdir(exist_ok=True)\n", - "\n", - "# Download the snapshot and model configuration file\n", - "# This is generic code to download any snapshot from HuggingFace\n", - "# To download DeepLabCut SuperAnimal or Model Zoo models, check\n", - "# out dlclibrary!\n", - "path_model_config = Path(\n", - " huggingface_hub.hf_hub_download(\n", - " \"DeepLabCut/HumanBody\",\n", - " \"rtmpose-x_simcc-body7_pytorch_config.yaml\",\n", - " local_dir=model_files,\n", - " )\n", - ")\n", - "path_snapshot = Path(\n", - " huggingface_hub.hf_hub_download(\n", - " \"DeepLabCut/HumanBody\",\n", - " \"rtmpose-x_simcc-body7.pt\",\n", - " local_dir=model_files,\n", - " )\n", - ")" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Running object detection\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "eEqukXXy0coy" - }, - "source": [ - "We'll now also define some parameters that we'll later use to plot predictions:\n", - "\n", - "- a colormap for the keypoints to plot\n", - "- a colormap for the limbs of the skeleton\n", - "- a skeleton for the model\n" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 1/1 [00:00<00:00, 1.95it/s]\n" + ] }, { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "id": "Tam4rfJK0c_b" - }, - "outputs": [], - "source": [ - "cmap_keypoints = plt.get_cmap(\"rainbow\")\n", - "cmap_skeleton = plt.get_cmap(\"rainbow_r\")\n", - "\n", - "bodyparts2connect = [\n", - " (\"right_ankle\", \"right_knee\"),\n", - " (\"right_knee\", \"right_hip\"),\n", - " (\"left_ankle\", \"left_knee\"),\n", - " (\"left_hip\", \"left_knee\"),\n", - " (\"left_hip\", \"right_hip\"),\n", - " (\"right_shoulder\", \"right_hip\"),\n", - " (\"left_shoulder\", \"left_hip\"),\n", - " (\"left_shoulder\", \"right_shoulder\"),\n", - " (\"left_shoulder\", \"left_elbow\"),\n", - " (\"right_shoulder\", \"right_elbow\"),\n", - " (\"left_elbow\", \"left_wrist\"),\n", - " (\"right_elbow\", \"right_wrist\"),\n", - " (\"right_eye\", \"left_ear\"),\n", - " (\"left_eye\", \"right_eye\"),\n", - " (\"left_eye\", \"left_ear\"),\n", - " (\"right_eye\", \"right_ear\"),\n", - " (\"left_ear\", \"left_shoulder\"),\n", - " (\"right_ear\", \"right_shoulder\"),\n", - " (\"left_shoulder\", \"left_elbow\"),\n", - " (\"right_shoulder\", \"right_elbow\"),\n", - "]\n", - "skeleton = [\n", - " [16, 14],\n", - " [14, 12],\n", - " [17, 15],\n", - " [15, 13],\n", - " [12, 13],\n", - " [6, 12],\n", - " [7, 13],\n", - " [6, 7],\n", - " [6, 8],\n", - " [7, 9],\n", - " [8, 10],\n", - " [9, 11],\n", - " [2, 3],\n", - " [1, 2],\n", - " [1, 3],\n", - " [2, 4],\n", - " [3, 5],\n", - " [4, 6],\n", - " [5, 7],\n", - "]" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Running pose estimation\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "cCxkkd-b0EJq" - }, - "source": [ - "## Running Inference on Images" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "1it [00:00, 78.27it/s]\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "dotn_xN-05gh" - }, - "source": [ - "First, let's upload some images to run inference on. To do so, you can just run the cell below." - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving the predictions to a CSV file\n", + "Done!\n" + ] + } + ], + "source": [ + "# Define the device on which the models will run\n", + "device = \"cuda\" # e.g. cuda, cpu\n", + "\n", + "# The maximum number of detections to keep in an image\n", + "max_detections = 10\n", + "\n", + "#############################################\n", + "# Run a pretrained detector to get bounding boxes\n", + "\n", + "# Load the detector from torchvision\n", + "weights = detection.FasterRCNN_MobileNet_V3_Large_FPN_Weights.DEFAULT\n", + "detector = detection.fasterrcnn_mobilenet_v3_large_fpn(\n", + " weights=weights, box_score_thresh=0.6,\n", + ")\n", + "detector.eval()\n", + "detector.to(device)\n", + "preprocess = weights.transforms()\n", + "\n", + "# The context is a list containing the bounding boxes predicted\n", + "# for each image; it will be given to the RTMPose model alongside\n", + "# the images.\n", + "context = []\n", + "\n", + "print(\"Running object detection\")\n", + "with torch.no_grad():\n", + " for image_path in tqdm(image_paths):\n", + " image = Image.open(image_path).convert(\"RGB\")\n", + " batch = [preprocess(image).to(device)]\n", + " predictions = detector(batch)[0]\n", + " bboxes = predictions[\"boxes\"].cpu().numpy()\n", + " labels = predictions[\"labels\"].cpu().numpy()\n", + "\n", + " # Obtain the bounding boxes predicted for humans\n", + " human_bboxes = [\n", + " bbox for bbox, label in zip(bboxes, labels) if label == 1\n", + " ]\n", + "\n", + " # Convert bounding boxes to xywh format\n", + " bboxes = np.zeros((0, 4))\n", + " if len(human_bboxes) > 0:\n", + " bboxes = np.stack(human_bboxes)\n", + " bboxes[:, 2] -= bboxes[:, 0]\n", + " bboxes[:, 3] -= bboxes[:, 1]\n", + "\n", + " # Only keep the best N detections\n", + " bboxes = bboxes[:max_detections]\n", + "\n", + " context.append({\"bboxes\": bboxes})\n", + "\n", + "\n", + "#############################################\n", + "# Run inference on the images\n", + "pose_cfg = dlc_torch.config.read_config_as_dict(path_model_config)\n", + "runner = dlc_torch.get_pose_inference_runner(\n", + " pose_cfg,\n", + " snapshot_path=path_snapshot,\n", + " batch_size=16,\n", + " max_individuals=max_detections,\n", + ")\n", + "\n", + "print(\"Running pose estimation\")\n", + "predictions = runner.inference(tqdm(zip(image_paths, context)))\n", + "\n", + "\n", + "#############################################\n", + "# Create a DataFrame with the predictions, and save them to a CSV file.\n", + "print(\"Saving the predictions to a CSV file\")\n", + "df = dlc_torch.build_predictions_dataframe(\n", + " scorer=\"rtmpose-body7\",\n", + " predictions={\n", + " img_path: img_predictions\n", + " for img_path, img_predictions in zip(image_paths, predictions)\n", + " },\n", + " parameters=dlc_torch.PoseDatasetParameters(\n", + " bodyparts=pose_cfg[\"metadata\"][\"bodyparts\"],\n", + " unique_bpts=pose_cfg[\"metadata\"][\"unique_bodyparts\"],\n", + " individuals=[f\"idv_{i}\" for i in range(max_detections)]\n", + " )\n", + ")\n", + "\n", + "# Save to CSV\n", + "df.to_csv(\"image_predictions.csv\")\n", + "\n", + "print(\"Done!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pWtdL4U52OBJ" + }, + "source": [ + "Finally, we can plot the predictions!" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 447 }, + "id": "3slKu6Lr2MUh", + "outputId": "ef7d938c-39fc-473a-9b88-6169cbfbc567" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "id": "mZtikE1H0D34", - "outputId": "3d47314f-3ed0-40b2-e54d-2677feef9943", - "colab": { - "base_uri": "https://localhost:8080/", - "height": 92 - } - }, - "outputs": [ - { - "output_type": "display_data", - "data": { - "text/plain": [ - "" - ], - "text/html": [ - "\n", - " \n", - " \n", - " Upload widget is only available when the cell has been executed in the\n", - " current browser session. Please rerun this cell to enable.\n", - " \n", - " " - ] - }, - "metadata": {} - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Saving taylor_swift.jpg to taylor_swift.jpg\n", - "User uploaded file 'taylor_swift.jpg' with length 46915 bytes\n" - ] - } - ], - "source": [ - "from google.colab import files\n", - "\n", - "#JPG or PNG is recommended:\n", - "uploaded = files.upload()\n", - "for filepath, content in uploaded.items():\n", - " print(f\"User uploaded file '{filepath}' with length {len(content)} bytes\")\n", - "\n", - "image_paths = [Path(filepath).resolve() for filepath in uploaded.keys()]\n", - "\n", - "# If this cell fails (e.g., when using Safari in place of Google Chrome),\n", - "# manually upload your image via the Files menu to the left and define\n", - "# `image_paths` yourself with right `click` > `copy path` on the image:\n", - "#\n", - "# image_paths = [\n", - "# Path(\"/path/to/my/image_000.png\"),\n", - "# Path(\"/path/to/my/image_001.png\"),\n", - "# ]\n" + "data": { + "image/png": "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\n", + "text/plain": [ + "

" ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "#############################################\n", + "# Unpack and plot predictions\n", + "plot_skeleton = True\n", + "plot_pose_markers = True\n", + "plot_bounding_boxes = True\n", + "marker_size = 12\n", + "\n", + "for image_path, image_predictions in zip(image_paths, predictions):\n", + " image = Image.open(image_path).convert(\"RGB\")\n", + "\n", + " pose = image_predictions[\"bodyparts\"]\n", + " bboxes = image_predictions[\"bboxes\"]\n", + " num_individuals, num_bodyparts = pose.shape[:2]\n", + "\n", + " fig, ax = plt.subplots(figsize=(8, 8))\n", + " ax.imshow(image)\n", + " ax.set_xlim(0, image.width)\n", + " ax.set_ylim(image.height, 0)\n", + " ax.axis(\"off\")\n", + " for idv_pose in pose:\n", + " if plot_skeleton:\n", + " bones = []\n", + " for bpt_1, bpt_2 in skeleton:\n", + " bones.append([idv_pose[bpt_1 - 1, :2], idv_pose[bpt_2 - 1, :2]])\n", + "\n", + " bone_colors = cmap_skeleton\n", + " if not isinstance(cmap_skeleton, str):\n", + " bone_colors = cmap_skeleton(np.linspace(0, 1, len(skeleton)))\n", + "\n", + " ax.add_collection(\n", + " collections.LineCollection(bones, colors=bone_colors)\n", + " )\n", + "\n", + " if plot_pose_markers:\n", + " ax.scatter(\n", + " idv_pose[:, 0],\n", + " idv_pose[:, 1],\n", + " c=list(range(num_bodyparts)),\n", + " cmap=\"rainbow\",\n", + " s=marker_size,\n", + " )\n", + "\n", + " if plot_bounding_boxes:\n", + " for x, y, w, h in bboxes:\n", + " ax.plot(\n", + " [x, x + w, x + w, x, x],\n", + " [y, y, y + h, y + h, y],\n", + " c=\"r\",\n", + " )\n", + "\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wO18A_3m5Spk" + }, + "source": [ + "## Running Inference on a Video\n", + "\n", + "Running pose inference on a video is very similar! First, upload a video to Google Drive." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 92 }, + "id": "d9a7gSe15bCa", + "outputId": "698b180c-cd8f-4d17-9c71-f8e58f93631b" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "id": "nj-HtOBSwtdk", - "outputId": "eb5f3b18-cc89-4dd1-a58e-6c39c62582af", - "colab": { - "base_uri": "https://localhost:8080/" - } - }, - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Running object detection\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - "100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 1/1 [00:00<00:00, 1.95it/s]\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Running pose estimation\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - "1it [00:00, 78.27it/s]\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Saving the predictions to a CSV file\n", - "Done!\n" - ] - } + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " Upload widget is only available when the cell has been executed in the\n", + " current browser session. Please rerun this cell to enable.\n", + " \n", + " " ], - "source": [ - "# Define the device on which the models will run\n", - "device = \"cuda\" # e.g. cuda, cpu\n", - "\n", - "# The maximum number of detections to keep in an image\n", - "max_detections = 10\n", - "\n", - "#############################################\n", - "# Run a pretrained detector to get bounding boxes\n", - "\n", - "# Load the detector from torchvision\n", - "weights = detection.FasterRCNN_MobileNet_V3_Large_FPN_Weights.DEFAULT\n", - "detector = detection.fasterrcnn_mobilenet_v3_large_fpn(\n", - " weights=weights, box_score_thresh=0.6,\n", - ")\n", - "detector.eval()\n", - "detector.to(device)\n", - "preprocess = weights.transforms()\n", - "\n", - "# The context is a list containing the bounding boxes predicted\n", - "# for each image; it will be given to the RTMPose model alongside\n", - "# the images.\n", - "context = []\n", - "\n", - "print(\"Running object detection\")\n", - "with torch.no_grad():\n", - " for image_path in tqdm(image_paths):\n", - " image = Image.open(image_path).convert(\"RGB\")\n", - " batch = [preprocess(image).to(device)]\n", - " predictions = detector(batch)[0]\n", - " bboxes = predictions[\"boxes\"].cpu().numpy()\n", - " labels = predictions[\"labels\"].cpu().numpy()\n", - "\n", - " # Obtain the bounding boxes predicted for humans\n", - " human_bboxes = [\n", - " bbox for bbox, label in zip(bboxes, labels) if label == 1\n", - " ]\n", - "\n", - " # Convert bounding boxes to xywh format\n", - " bboxes = np.zeros((0, 4))\n", - " if len(human_bboxes) > 0:\n", - " bboxes = np.stack(human_bboxes)\n", - " bboxes[:, 2] -= bboxes[:, 0]\n", - " bboxes[:, 3] -= bboxes[:, 1]\n", - "\n", - " # Only keep the best N detections\n", - " bboxes = bboxes[:max_detections]\n", - "\n", - " context.append({\"bboxes\": bboxes})\n", - "\n", - "\n", - "#############################################\n", - "# Run inference on the images\n", - "pose_cfg = dlc_torch.config.read_config_as_dict(path_model_config)\n", - "runner = dlc_torch.get_pose_inference_runner(\n", - " pose_cfg,\n", - " snapshot_path=path_snapshot,\n", - " batch_size=16,\n", - " max_individuals=max_detections,\n", - ")\n", - "\n", - "print(\"Running pose estimation\")\n", - "predictions = runner.inference(tqdm(zip(image_paths, context)))\n", - "\n", - "\n", - "#############################################\n", - "# Create a DataFrame with the predictions, and save them to a CSV file.\n", - "print(\"Saving the predictions to a CSV file\")\n", - "df = dlc_torch.build_predictions_dataframe(\n", - " scorer=\"rtmpose-body7\",\n", - " predictions={\n", - " img_path: img_predictions\n", - " for img_path, img_predictions in zip(image_paths, predictions)\n", - " },\n", - " parameters=dlc_torch.PoseDatasetParameters(\n", - " bodyparts=pose_cfg[\"metadata\"][\"bodyparts\"],\n", - " unique_bpts=pose_cfg[\"metadata\"][\"unique_bodyparts\"],\n", - " individuals=[f\"idv_{i}\" for i in range(max_detections)]\n", - " )\n", - ")\n", - "\n", - "# Save to CSV\n", - "df.to_csv(\"image_predictions.csv\")\n", - "\n", - "print(\"Done!\")" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "pWtdL4U52OBJ" - }, - "source": [ - "Finally, we can plot the predictions!" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "id": "3slKu6Lr2MUh", - "outputId": "ef7d938c-39fc-473a-9b88-6169cbfbc567", - "colab": { - "base_uri": "https://localhost:8080/", - "height": 447 - } - }, - "outputs": [ - { - "output_type": "display_data", - "data": { - "text/plain": [ - "
" - ], - "image/png": "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\n" - }, - "metadata": {} - } - ], - "source": [ - "#############################################\n", - "# Unpack and plot predictions\n", - "plot_skeleton = True\n", - "plot_pose_markers = True\n", - "plot_bounding_boxes = True\n", - "marker_size = 12\n", - "\n", - "for image_path, image_predictions in zip(image_paths, predictions):\n", - " image = Image.open(image_path).convert(\"RGB\")\n", - "\n", - " pose = image_predictions[\"bodyparts\"]\n", - " bboxes = image_predictions[\"bboxes\"]\n", - " num_individuals, num_bodyparts = pose.shape[:2]\n", - "\n", - " fig, ax = plt.subplots(figsize=(8, 8))\n", - " ax.imshow(image)\n", - " ax.set_xlim(0, image.width)\n", - " ax.set_ylim(image.height, 0)\n", - " ax.axis(\"off\")\n", - " for idv_pose in pose:\n", - " if plot_skeleton:\n", - " bones = []\n", - " for bpt_1, bpt_2 in skeleton:\n", - " bones.append([idv_pose[bpt_1 - 1, :2], idv_pose[bpt_2 - 1, :2]])\n", - "\n", - " bone_colors = cmap_skeleton\n", - " if not isinstance(cmap_skeleton, str):\n", - " bone_colors = cmap_skeleton(np.linspace(0, 1, len(skeleton)))\n", - "\n", - " ax.add_collection(\n", - " collections.LineCollection(bones, colors=bone_colors)\n", - " )\n", - "\n", - " if plot_pose_markers:\n", - " ax.scatter(\n", - " idv_pose[:, 0],\n", - " idv_pose[:, 1],\n", - " c=list(range(num_bodyparts)),\n", - " cmap=\"rainbow\",\n", - " s=marker_size,\n", - " )\n", - "\n", - " if plot_bounding_boxes:\n", - " for x, y, w, h in bboxes:\n", - " ax.plot(\n", - " [x, x + w, x + w, x, x],\n", - " [y, y, y + h, y + h, y],\n", - " c=\"r\",\n", - " )\n", - "\n", - " plt.show()" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving taylor-dancing.mov to taylor-dancing.mov\n", + "User uploaded file 'taylor-dancing.mov' with length 1415324 bytes\n" + ] + } + ], + "source": [ + "from google.colab import files\n", + "\n", + "uploaded = files.upload()\n", + "for filepath, content in uploaded.items():\n", + " print(f\"User uploaded file '{filepath}' with length {len(content)} bytes\")\n", + "\n", + "\n", + "video_path = [Path(filepath).resolve() for filepath in uploaded.keys()][0]\n", + "\n", + "# If this cell fails (e.g., when using Safari in place of Google Chrome),\n", + "# manually upload your video via the Files menu to the left and define\n", + "# `video_path` yourself with right `click` > `copy path` on the video:\n", + "#\n", + "# video_path = Path(\"/path/to/my/video.mp4\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "I885B01359qu", + "outputId": "0affdeda-a10b-4849-b3cd-edf1cb202b52" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "wO18A_3m5Spk" - }, - "source": [ - "## Running Inference on a Video\n", - "\n", - "Running pose inference on a video is very similar! First, upload a video to Google Drive." - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Running object detection\n" + ] }, { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "id": "d9a7gSe15bCa", - "outputId": "698b180c-cd8f-4d17-9c71-f8e58f93631b", - "colab": { - "base_uri": "https://localhost:8080/", - "height": 92 - } - }, - "outputs": [ - { - "output_type": "display_data", - "data": { - "text/plain": [ - "" - ], - "text/html": [ - "\n", - " \n", - " \n", - " Upload widget is only available when the cell has been executed in the\n", - " current browser session. Please rerun this cell to enable.\n", - " \n", - " " - ] - }, - "metadata": {} - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Saving taylor-dancing.mov to taylor-dancing.mov\n", - "User uploaded file 'taylor-dancing.mov' with length 1415324 bytes\n" - ] - } - ], - "source": [ - "from google.colab import files\n", - "\n", - "uploaded = files.upload()\n", - "for filepath, content in uploaded.items():\n", - " print(f\"User uploaded file '{filepath}' with length {len(content)} bytes\")\n", - "\n", - "\n", - "video_path = [Path(filepath).resolve() for filepath in uploaded.keys()][0]\n", - "\n", - "# If this cell fails (e.g., when using Safari in place of Google Chrome),\n", - "# manually upload your video via the Files menu to the left and define\n", - "# `video_path` yourself with right `click` > `copy path` on the video:\n", - "#\n", - "# video_path = Path(\"/path/to/my/video.mp4\")\n" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + " 81%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ– | 66/81 [00:02<00:00, 25.37it/s]\n" + ] }, { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "id": "I885B01359qu", - "outputId": "0affdeda-a10b-4849-b3cd-edf1cb202b52", - "colab": { - "base_uri": "https://localhost:8080/" - } - }, - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Running object detection\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - " 81%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ– | 66/81 [00:02<00:00, 25.37it/s]\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Running pose estimation\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - " 81%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ– | 66/81 [00:01<00:00, 53.25it/s]\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Saving the predictions to a CSV file\n", - "Done!\n" - ] - } - ], - "source": [ - "# Define the device on which the models will run\n", - "device = \"cuda\" # e.g. cuda, cpu\n", - "\n", - "# The maximum number of individuals to detect in an image\n", - "max_detections = 30\n", - "\n", - "\n", - "#############################################\n", - "# Create a video iterator\n", - "video = dlc_torch.VideoIterator(video_path)\n", - "\n", - "\n", - "#############################################\n", - "# Run a pretrained detector to get bounding boxes\n", - "\n", - "# Load the detector from torchvision\n", - "weights = detection.FasterRCNN_MobileNet_V3_Large_FPN_Weights.DEFAULT\n", - "detector = detection.fasterrcnn_mobilenet_v3_large_fpn(\n", - " weights=weights, box_score_thresh=0.6,\n", - ")\n", - "detector.eval()\n", - "detector.to(device)\n", - "preprocess = weights.transforms()\n", - "\n", - "# The context is a list containing the bounding boxes predicted for each frame\n", - "# in the video.\n", - "context = []\n", - "\n", - "print(\"Running object detection\")\n", - "with torch.no_grad():\n", - " for frame in tqdm(video):\n", - " batch = [preprocess(Image.fromarray(frame)).to(device)]\n", - " predictions = detector(batch)[0]\n", - " bboxes = predictions[\"boxes\"].cpu().numpy()\n", - " labels = predictions[\"labels\"].cpu().numpy()\n", - "\n", - " # Obtain the bounding boxes predicted for humans\n", - " human_bboxes = [\n", - " bbox for bbox, label in zip(bboxes, labels) if label == 1\n", - " ]\n", - "\n", - " # Convert bounding boxes to xywh format\n", - " bboxes = np.zeros((0, 4))\n", - " if len(human_bboxes) > 0:\n", - " bboxes = np.stack(human_bboxes)\n", - " bboxes[:, 2] -= bboxes[:, 0]\n", - " bboxes[:, 3] -= bboxes[:, 1]\n", - "\n", - " # Only keep the top N bounding boxes\n", - " bboxes = bboxes[:max_detections]\n", - "\n", - " context.append({\"bboxes\": bboxes})\n", - "\n", - "# Set the context for the video\n", - "video.set_context(context)\n", - "\n", - "\n", - "#############################################\n", - "# Run inference on the images (in this case a single image)\n", - "pose_cfg = dlc_torch.config.read_config_as_dict(path_model_config)\n", - "runner = dlc_torch.get_pose_inference_runner(\n", - " pose_cfg,\n", - " snapshot_path=path_snapshot,\n", - " batch_size=16,\n", - " max_individuals=max_detections,\n", - ")\n", - "\n", - "print(\"Running pose estimation\")\n", - "predictions = runner.inference(tqdm(video))\n", - "\n", - "\n", - "print(\"Saving the predictions to a CSV file\")\n", - "df = dlc_torch.build_predictions_dataframe(\n", - " scorer=\"rtmpose-body7\",\n", - " predictions={\n", - " idx: img_predictions\n", - " for idx, img_predictions in enumerate(predictions)\n", - " },\n", - " parameters=dlc_torch.PoseDatasetParameters(\n", - " bodyparts=pose_cfg[\"metadata\"][\"bodyparts\"],\n", - " unique_bpts=pose_cfg[\"metadata\"][\"unique_bodyparts\"],\n", - " individuals=[f\"idv_{i}\" for i in range(max_detections)]\n", - " )\n", - ")\n", - "df.to_csv(\"video_predictions.csv\")\n", - "\n", - "print(\"Done!\")" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Running pose estimation\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "altka3NGB_su" - }, - "source": [ - "Finally, we can plot the predictions on the video! The labeled video output is saved in the `\"video_predictions.mp4\"` file, and can be downloaded to be viewed." - ] + "name": "stderr", + "output_type": "stream", + "text": [ + " 81%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ– | 66/81 [00:01<00:00, 53.25it/s]\n" + ] }, { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "id": "xRWxH0gO6oPg", - "outputId": "c2cc9025-7741-4403-d5cc-c62470a4ba74", - "colab": { - "base_uri": "https://localhost:8080/" - } - }, - "outputs": [ - { - "output_type": "stream", - "name": "stderr", - "text": [ - "/usr/local/lib/python3.11/dist-packages/deeplabcut/utils/make_labeled_video.py:146: FutureWarning: DataFrame.groupby with axis=1 is deprecated. Do `frame.T.groupby(...)` without axis instead.\n", - " Dataframe.groupby(level=\"individuals\", axis=1).size().values // 3\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Duration of video [s]: 1.57, recorded with 51.7 fps!\n", - "Overall # of frames: 81 with cropped frame dimensions: 828 768\n", - "Generating frames and creating video.\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - "100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 66/66 [00:01<00:00, 35.27it/s]\n" - ] - } - ], - "source": [ - "from deeplabcut.utils.make_labeled_video import CreateVideo\n", - "from deeplabcut.utils.video_processor import VideoProcessorCV\n", - "\n", - "video_output_path = \"video_predictions.mp4\"\n", - "\n", - "clip = VideoProcessorCV(str(video_path), sname=video_output_path, codec=\"mp4v\")\n", - "CreateVideo(\n", - " clip,\n", - " df,\n", - " pcutoff=0.4,\n", - " dotsize=3,\n", - " colormap=\"rainbow\",\n", - " bodyparts2plot=pose_cfg[\"metadata\"][\"bodyparts\"],\n", - " trailpoints=0,\n", - " cropping=False,\n", - " x1=0,\n", - " x2=clip.w,\n", - " y1=0,\n", - " y2=clip.h,\n", - " bodyparts2connect=bodyparts2connect,\n", - " skeleton_color=\"w\",\n", - " draw_skeleton=True,\n", - " displaycropped=True,\n", - " color_by=\"bodypart\",\n", - ")" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving the predictions to a CSV file\n", + "Done!\n" + ] } - ], - "metadata": { - "accelerator": "GPU", + ], + "source": [ + "# Define the device on which the models will run\n", + "device = \"cuda\" # e.g. cuda, cpu\n", + "\n", + "# The maximum number of individuals to detect in an image\n", + "max_detections = 30\n", + "\n", + "\n", + "#############################################\n", + "# Create a video iterator\n", + "video = dlc_torch.VideoIterator(video_path)\n", + "\n", + "\n", + "#############################################\n", + "# Run a pretrained detector to get bounding boxes\n", + "\n", + "# Load the detector from torchvision\n", + "weights = detection.FasterRCNN_MobileNet_V3_Large_FPN_Weights.DEFAULT\n", + "detector = detection.fasterrcnn_mobilenet_v3_large_fpn(\n", + " weights=weights, box_score_thresh=0.6,\n", + ")\n", + "detector.eval()\n", + "detector.to(device)\n", + "preprocess = weights.transforms()\n", + "\n", + "# The context is a list containing the bounding boxes predicted for each frame\n", + "# in the video.\n", + "context = []\n", + "\n", + "print(\"Running object detection\")\n", + "with torch.no_grad():\n", + " for frame in tqdm(video):\n", + " batch = [preprocess(Image.fromarray(frame)).to(device)]\n", + " predictions = detector(batch)[0]\n", + " bboxes = predictions[\"boxes\"].cpu().numpy()\n", + " labels = predictions[\"labels\"].cpu().numpy()\n", + "\n", + " # Obtain the bounding boxes predicted for humans\n", + " human_bboxes = [\n", + " bbox for bbox, label in zip(bboxes, labels) if label == 1\n", + " ]\n", + "\n", + " # Convert bounding boxes to xywh format\n", + " bboxes = np.zeros((0, 4))\n", + " if len(human_bboxes) > 0:\n", + " bboxes = np.stack(human_bboxes)\n", + " bboxes[:, 2] -= bboxes[:, 0]\n", + " bboxes[:, 3] -= bboxes[:, 1]\n", + "\n", + " # Only keep the top N bounding boxes\n", + " bboxes = bboxes[:max_detections]\n", + "\n", + " context.append({\"bboxes\": bboxes})\n", + "\n", + "# Set the context for the video\n", + "video.set_context(context)\n", + "\n", + "\n", + "#############################################\n", + "# Run inference on the images (in this case a single image)\n", + "pose_cfg = dlc_torch.config.read_config_as_dict(path_model_config)\n", + "runner = dlc_torch.get_pose_inference_runner(\n", + " pose_cfg,\n", + " snapshot_path=path_snapshot,\n", + " batch_size=16,\n", + " max_individuals=max_detections,\n", + ")\n", + "\n", + "print(\"Running pose estimation\")\n", + "predictions = runner.inference(tqdm(video))\n", + "\n", + "\n", + "print(\"Saving the predictions to a CSV file\")\n", + "df = dlc_torch.build_predictions_dataframe(\n", + " scorer=\"rtmpose-body7\",\n", + " predictions={\n", + " idx: img_predictions\n", + " for idx, img_predictions in enumerate(predictions)\n", + " },\n", + " parameters=dlc_torch.PoseDatasetParameters(\n", + " bodyparts=pose_cfg[\"metadata\"][\"bodyparts\"],\n", + " unique_bpts=pose_cfg[\"metadata\"][\"unique_bodyparts\"],\n", + " individuals=[f\"idv_{i}\" for i in range(max_detections)]\n", + " )\n", + ")\n", + "df.to_csv(\"video_predictions.csv\")\n", + "\n", + "print(\"Done!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "altka3NGB_su" + }, + "source": [ + "Finally, we can plot the predictions on the video! The labeled video output is saved in the `\"video_predictions.mp4\"` file, and can be downloaded to be viewed." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { "colab": { - "gpuType": "T4", - "provenance": [], - "include_colab_link": true + "base_uri": "https://localhost:8080/" }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" + "id": "xRWxH0gO6oPg", + "outputId": "c2cc9025-7741-4403-d5cc-c62470a4ba74" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.11/dist-packages/deeplabcut/utils/make_labeled_video.py:146: FutureWarning: DataFrame.groupby with axis=1 is deprecated. Do `frame.T.groupby(...)` without axis instead.\n", + " Dataframe.groupby(level=\"individuals\", axis=1).size().values // 3\n" + ] }, - "language_info": { - "name": "python" + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Duration of video [s]: 1.57, recorded with 51.7 fps!\n", + "Overall # of frames: 81 with cropped frame dimensions: 828 768\n", + "Generating frames and creating video.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 66/66 [00:01<00:00, 35.27it/s]\n" + ] } + ], + "source": [ + "from deeplabcut.utils.make_labeled_video import CreateVideo\n", + "from deeplabcut.utils.video_processor import VideoProcessorCV\n", + "\n", + "video_output_path = \"video_predictions.mp4\"\n", + "\n", + "clip = VideoProcessorCV(str(video_path), sname=video_output_path, codec=\"mp4v\")\n", + "CreateVideo(\n", + " clip,\n", + " df,\n", + " pcutoff=0.4,\n", + " dotsize=3,\n", + " colormap=\"rainbow\",\n", + " bodyparts2plot=pose_cfg[\"metadata\"][\"bodyparts\"],\n", + " trailpoints=0,\n", + " cropping=False,\n", + " x1=0,\n", + " x2=clip.w,\n", + " y1=0,\n", + " y2=clip.h,\n", + " bodyparts2connect=bodyparts2connect,\n", + " skeleton_color=\"w\",\n", + " draw_skeleton=True,\n", + " displaycropped=True,\n", + " color_by=\"bodypart\",\n", + ")" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "T4", + "include_colab_link": true, + "provenance": [] + }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-07-07", + "last_metadata_updated": "2026-03-06" + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" }, - "nbformat": 4, - "nbformat_minor": 0 + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/examples/COLAB/COLAB_YOURDATA_SuperAnimal.ipynb b/examples/COLAB/COLAB_YOURDATA_SuperAnimal.ipynb index c4d1fc575e..f1f0150792 100644 --- a/examples/COLAB/COLAB_YOURDATA_SuperAnimal.ipynb +++ b/examples/COLAB/COLAB_YOURDATA_SuperAnimal.ipynb @@ -1165,6 +1165,11 @@ "gpuType": "T4", "provenance": [] }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-09-10", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", diff --git a/examples/COLAB/COLAB_YOURDATA_TrainNetwork_VideoAnalysis.ipynb b/examples/COLAB/COLAB_YOURDATA_TrainNetwork_VideoAnalysis.ipynb index 71e4e88045..ed04c2f560 100644 --- a/examples/COLAB/COLAB_YOURDATA_TrainNetwork_VideoAnalysis.ipynb +++ b/examples/COLAB/COLAB_YOURDATA_TrainNetwork_VideoAnalysis.ipynb @@ -414,6 +414,11 @@ "provenance": [], "toc_visible": true }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-09-16", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { "display_name": "Python 3.8.12 ('dlc')", "language": "python", diff --git a/examples/COLAB/COLAB_YOURDATA_maDLC_TrainNetwork_VideoAnalysis.ipynb b/examples/COLAB/COLAB_YOURDATA_maDLC_TrainNetwork_VideoAnalysis.ipynb index b8c060960d..08633839fb 100644 --- a/examples/COLAB/COLAB_YOURDATA_maDLC_TrainNetwork_VideoAnalysis.ipynb +++ b/examples/COLAB/COLAB_YOURDATA_maDLC_TrainNetwork_VideoAnalysis.ipynb @@ -529,6 +529,11 @@ "name": "COLAB_maDLC_TrainNetwork_VideoAnalysis.ipynb", "provenance": [] }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2026-02-10", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { "display_name": "Python 3", "name": "python3" diff --git a/examples/COLAB/COLAB_transformer_reID.ipynb b/examples/COLAB/COLAB_transformer_reID.ipynb index 008255692f..e0b222627a 100644 --- a/examples/COLAB/COLAB_transformer_reID.ipynb +++ b/examples/COLAB/COLAB_transformer_reID.ipynb @@ -1,634 +1,639 @@ { - "cells": [ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "view-in-github" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TGChzLdc-lUJ" + }, + "source": [ + "# Demo: How to use our Pose Transformer for unsupervised identity tracking of animals\n", + "![alt text](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1628250004229-KVYD7JJVHYEFDJ32L9VJ/DLClogo2021.jpg?format=1000w)\n", + "\n", + "https://github.com/DeepLabCut/DeepLabCut\n", + "\n", + "### This notebook illustrates how to use the transformer for a multi-animal DeepLabCut (maDLC) Demo tri-mouse project:\n", + "- load our mini-demo data that includes a pretrained model and unlabeled video.\n", + "- analyze a novel video.\n", + "- use the transformer to do unsupervised ID tracking.\n", + "- create quality check plots and video.\n", + "\n", + "### To create a full maDLC pipeline please see our full docs: https://deeplabcut.github.io/DeepLabCut/README.html\n", + "- Of interest is a full how-to for maDLC: https://deeplabcut.github.io/DeepLabCut/docs/maDLC_UserGuide.html\n", + "- a quick guide to maDLC: https://deeplabcut.github.io/DeepLabCut/docs/quick-start/tutorial_maDLC.html\n", + "- a demo COLAB for how to use maDLC on your own data: https://github.com/DeepLabCut/DeepLabCut/blob/main/examples/COLAB/COLAB_YOURDATA_maDLC_TrainNetwork_VideoAnalysis.ipynb\n", + "\n", + "### To get started, please go to \"Runtime\" ->\"change runtime type\"->select \"Python3\", and then select \"GPU\"\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xOe2hvy85EVP" + }, + "source": [ + "‼️ **Attention: this demo is for maDLC, which is version 2.2**\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NXmLeZBX45Oe" + }, + "outputs": [], + "source": [ + "# Install DLC version 2.2-2.3 (pre DLC3):\n", + "!pip install \"deeplabcut[tf]\"" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "TlhrVFKN8euh" + }, + "outputs": [], + "source": [ + "import deeplabcut\n", + "import os" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Wid0GTGMAEnZ" + }, + "source": [ + "## Important - Restart the Runtime for the updated packages to be imported!\n", + "\n", + "PLEASE, click \"restart runtime\" from the output above before proceeding!\n", + "\n", + "No information needs edited in the cells below, you can simply click run on each:\n", + "\n", + "### Download our Demo Project from our server:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "PusLdqbqJi60", + "outputId": "dbe30821-d3a7-443f-de74-6cb0bee49aac" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "view-in-github" - }, - "source": [ - "\"Open" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Downloading demo-me-2021-07-14.zip...\n" + ] + } + ], + "source": [ + "# Download our demo project:\n", + "import requests\n", + "from io import BytesIO\n", + "from zipfile import ZipFile\n", + "\n", + "url_record = \"https://zenodo.org/api/records/7883589\"\n", + "response = requests.get(url_record)\n", + "if response.status_code == 200:\n", + " file = response.json()[\"files\"][0]\n", + " title = file[\"key\"]\n", + " print(f\"Downloading {title}...\")\n", + " with requests.get(file[\"links\"][\"self\"], stream=True) as r:\n", + " with ZipFile(BytesIO(r.content)) as zf:\n", + " zf.extractall(path=\"/content\")\n", + "else:\n", + " raise ValueError(f\"The URL {url_record} could not be reached.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8iXtySnQB0BE" + }, + "source": [ + "## Analyze a novel 3 mouse video with our maDLC DLCRNet, pretrained on 3 mice data\n", + "\n", + "In one step, since `auto_track=True` you extract detections and association costs, create tracklets, & stitch them. We can use this to compare to the transformer-guided tracking below.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "id": "odYrU3o8BSAr" + }, + "outputs": [], + "source": [ + "project_path = \"/content/demo-me-2021-07-14\"\n", + "config_path = os.path.join(project_path, \"config.yaml\")\n", + "video = os.path.join(project_path, \"videos\", \"videocompressed1.mp4\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 520 }, + "id": "U_351Hkv81X-", + "outputId": "f7c30461-101f-47b6-c04f-15809aa5a4bb" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "TGChzLdc-lUJ" - }, - "source": [ - "# Demo: How to use our Pose Transformer for unsupervised identity tracking of animals\n", - "![alt text](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1628250004229-KVYD7JJVHYEFDJ32L9VJ/DLClogo2021.jpg?format=1000w)\n", - "\n", - "https://github.com/DeepLabCut/DeepLabCut\n", - "\n", - "### This notebook illustrates how to use the transformer for a multi-animal DeepLabCut (maDLC) Demo tri-mouse project:\n", - "- load our mini-demo data that includes a pretrained model and unlabeled video.\n", - "- analyze a novel video.\n", - "- use the transformer to do unsupervised ID tracking.\n", - "- create quality check plots and video.\n", - "\n", - "### To create a full maDLC pipeline please see our full docs: https://deeplabcut.github.io/DeepLabCut/README.html\n", - "- Of interest is a full how-to for maDLC: https://deeplabcut.github.io/DeepLabCut/docs/maDLC_UserGuide.html\n", - "- a quick guide to maDLC: https://deeplabcut.github.io/DeepLabCut/docs/quick-start/tutorial_maDLC.html\n", - "- a demo COLAB for how to use maDLC on your own data: https://github.com/DeepLabCut/DeepLabCut/blob/main/examples/COLAB/COLAB_YOURDATA_maDLC_TrainNetwork_VideoAnalysis.ipynb\n", - "\n", - "### To get started, please go to \"Runtime\" ->\"change runtime type\"->select \"Python3\", and then select \"GPU\"\n" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Using snapshot-20000 for model /content/demo-me-2021-07-14/dlc-models/iteration-0/demoJul14-trainset95shuffle0\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "xOe2hvy85EVP" - }, - "source": [ - "‼️ **Attention: this demo is for maDLC, which is version 2.2**\n" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", + " warnings.warn('`layer.apply` is deprecated and '\n" + ] }, { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "NXmLeZBX45Oe" - }, - "outputs": [], - "source": [ - "# Install DLC version 2.2-2.3 (pre DLC3):\n", - "!pip install \"deeplabcut[tf]\"" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Activating extracting of PAFs\n", + "Starting to analyze % /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", + "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", + "Duration of video [s]: 77.67 , recorded with 30.0 fps!\n", + "Overall # of frames: 2330 found with (before cropping) frame dimensions: 640 480\n", + "Starting to extract posture from the video(s) with batchsize: 8\n" + ] }, { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "id": "TlhrVFKN8euh" - }, - "outputs": [], - "source": [ - "import deeplabcut\n", - "import os" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 2330/2330 [00:39<00:00, 58.83it/s]\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "Wid0GTGMAEnZ" - }, - "source": [ - "## Important - Restart the Runtime for the updated packages to be imported!\n", - "\n", - "PLEASE, click \"restart runtime\" from the output above before proceeding!\n", - "\n", - "No information needs edited in the cells below, you can simply click run on each:\n", - "\n", - "### Download our Demo Project from our server:" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Video Analyzed. Saving results in /content/demo-me-2021-07-14/videos...\n" + ] }, { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "PusLdqbqJi60", - "outputId": "dbe30821-d3a7-443f-de74-6cb0bee49aac" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Downloading demo-me-2021-07-14.zip...\n" - ] - } - ], - "source": [ - "# Download our demo project:\n", - "import requests\n", - "from io import BytesIO\n", - "from zipfile import ZipFile\n", - "\n", - "url_record = \"https://zenodo.org/api/records/7883589\"\n", - "response = requests.get(url_record)\n", - "if response.status_code == 200:\n", - " file = response.json()[\"files\"][0]\n", - " title = file[\"key\"]\n", - " print(f\"Downloading {title}...\")\n", - " with requests.get(file[\"links\"][\"self\"], stream=True) as r:\n", - " with ZipFile(BytesIO(r.content)) as zf:\n", - " zf.extractall(path=\"/content\")\n", - "else:\n", - " raise ValueError(f\"The URL {url_record} could not be reached.\")" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.11/dist-packages/deeplabcut/utils/auxfun_multianimal.py:83: UserWarning: default_track_method` is undefined in the config.yaml file and will be set to `ellipse`.\n", + " warnings.warn(\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "8iXtySnQB0BE" - }, - "source": [ - "## Analyze a novel 3 mouse video with our maDLC DLCRNet, pretrained on 3 mice data\n", - "\n", - "In one step, since `auto_track=True` you extract detections and association costs, create tracklets, & stitch them. We can use this to compare to the transformer-guided tracking below.\n" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Using snapshot-20000 for model /content/demo-me-2021-07-14/dlc-models/iteration-0/demoJul14-trainset95shuffle0\n", + "Processing... /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", + "Analyzing /content/demo-me-2021-07-14/videos/videocompressed1DLC_dlcrnetms5_demoJul14shuffle0_20000.h5\n" + ] }, { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "id": "odYrU3o8BSAr" - }, - "outputs": [], - "source": [ - "project_path = \"/content/demo-me-2021-07-14\"\n", - "config_path = os.path.join(project_path, \"config.yaml\")\n", - "video = os.path.join(project_path, \"videos\", \"videocompressed1.mp4\")" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 2330/2330 [00:02<00:00, 1088.72it/s]\n", + "2330it [00:06, 342.29it/s] \n" + ] }, { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 520 - }, - "id": "U_351Hkv81X-", - "outputId": "f7c30461-101f-47b6-c04f-15809aa5a4bb" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Using snapshot-20000 for model /content/demo-me-2021-07-14/dlc-models/iteration-0/demoJul14-trainset95shuffle0\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", - " warnings.warn('`layer.apply` is deprecated and '\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Activating extracting of PAFs\n", - "Starting to analyze % /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", - "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", - "Duration of video [s]: 77.67 , recorded with 30.0 fps!\n", - "Overall # of frames: 2330 found with (before cropping) frame dimensions: 640 480\n", - "Starting to extract posture from the video(s) with batchsize: 8\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 2330/2330 [00:39<00:00, 58.83it/s]\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Video Analyzed. Saving results in /content/demo-me-2021-07-14/videos...\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/lib/python3.11/dist-packages/deeplabcut/utils/auxfun_multianimal.py:83: UserWarning: default_track_method` is undefined in the config.yaml file and will be set to `ellipse`.\n", - " warnings.warn(\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Using snapshot-20000 for model /content/demo-me-2021-07-14/dlc-models/iteration-0/demoJul14-trainset95shuffle0\n", - "Processing... /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", - "Analyzing /content/demo-me-2021-07-14/videos/videocompressed1DLC_dlcrnetms5_demoJul14shuffle0_20000.h5\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 2330/2330 [00:02<00:00, 1088.72it/s]\n", - "2330it [00:06, 342.29it/s] \n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The tracklets were created (i.e., under the hood deeplabcut.convert_detections2tracklets was run). Now you can 'refine_tracklets' in the GUI, or run 'deeplabcut.stitch_tracklets'.\n", - "Processing... /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 4/4 [00:00<00:00, 1488.53it/s]\n", - "/usr/local/lib/python3.11/dist-packages/deeplabcut/refine_training_dataset/stitch.py:934: FutureWarning: Starting with pandas version 3.0 all arguments of to_hdf except for the argument 'path_or_buf' will be keyword-only.\n", - " df.to_hdf(output_name, \"tracks\", format=\"table\", mode=\"w\")\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The videos are analyzed. Time to assemble animals and track 'em... \n", - " Call 'create_video_with_all_detections' to check multi-animal detection quality before tracking.\n", - "If the tracking is not satisfactory for some videos, consider expanding the training set. You can use the function 'extract_outlier_frames' to extract a few representative outlier frames.\n" - ] - }, - { - "data": { - "application/vnd.google.colaboratory.intrinsic+json": { - "type": "string" - }, - "text/plain": [ - "'DLC_dlcrnetms5_demoJul14shuffle0_20000'" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "deeplabcut.analyze_videos(config_path,[video],\n", - " shuffle=0, videotype=\"mp4\",\n", - " auto_track=True)" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "The tracklets were created (i.e., under the hood deeplabcut.convert_detections2tracklets was run). Now you can 'refine_tracklets' in the GUI, or run 'deeplabcut.stitch_tracklets'.\n", + "Processing... /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "zmdSLRTOER00" - }, - "source": [ - "### Next, you compute the local, spatio-temporal grouping and track body part assemblies frame-by-frame:" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 4/4 [00:00<00:00, 1488.53it/s]\n", + "/usr/local/lib/python3.11/dist-packages/deeplabcut/refine_training_dataset/stitch.py:934: FutureWarning: Starting with pandas version 3.0 all arguments of to_hdf except for the argument 'path_or_buf' will be keyword-only.\n", + " df.to_hdf(output_name, \"tracks\", format=\"table\", mode=\"w\")\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "F-d6kXqnGeUP" - }, - "source": [ - "## Create a pretty video output:" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "The videos are analyzed. Time to assemble animals and track 'em... \n", + " Call 'create_video_with_all_detections' to check multi-animal detection quality before tracking.\n", + "If the tracking is not satisfactory for some videos, consider expanding the training set. You can use the function 'extract_outlier_frames' to extract a few representative outlier frames.\n" + ] }, { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "aTRbuUQ1FBO0", - "outputId": "0d182f64-512d-463d-a997-226c7199b724" + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "type": "string" }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Filtering with median model /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", - "Saving filtered csv poses!\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/lib/python3.11/dist-packages/deeplabcut/post_processing/filtering.py:298: FutureWarning: Starting with pandas version 3.0 all arguments of to_hdf except for the argument 'path_or_buf' will be keyword-only.\n", - " data.to_hdf(outdataname, \"df_with_missing\", format=\"table\", mode=\"w\")\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Starting to process video: /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", - "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4 and data.\n", - "Duration of video [s]: 77.67, recorded with 30.0 fps!\n", - "Overall # of frames: 2330 with cropped frame dimensions: 640 480\n", - "Generating frames and creating video.\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/lib/python3.11/dist-packages/deeplabcut/utils/make_labeled_video.py:140: FutureWarning: DataFrame.groupby with axis=1 is deprecated. Do `frame.T.groupby(...)` without axis instead.\n", - " Dataframe.groupby(level=\"individuals\", axis=1).size().values // 3\n", - "100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 2330/2330 [00:31<00:00, 73.04it/s]\n" - ] - }, - { - "data": { - "text/plain": [ - "[True]" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "#Filter the predictions to remove small jitter, if desired:\n", - "deeplabcut.filterpredictions(config_path, [video], shuffle=0, videotype=\"mp4\")\n", - "deeplabcut.create_labeled_video(\n", - " config_path,\n", - " [video],\n", - " videotype=\"mp4\",\n", - " shuffle=0,\n", - " color_by=\"individual\",\n", - " keypoints_only=False,\n", - " draw_skeleton=True,\n", - " filtered=True,\n", - ")" + "text/plain": [ + "'DLC_dlcrnetms5_demoJul14shuffle0_20000'" ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "deeplabcut.analyze_videos(config_path,[video],\n", + " shuffle=0, videotype=\"mp4\",\n", + " auto_track=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zmdSLRTOER00" + }, + "source": [ + "### Next, you compute the local, spatio-temporal grouping and track body part assemblies frame-by-frame:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "F-d6kXqnGeUP" + }, + "source": [ + "## Create a pretty video output:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "aTRbuUQ1FBO0", + "outputId": "0d182f64-512d-463d-a997-226c7199b724" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "AYNlrgeNUG4U" - }, - "source": [ - "Now, on the left panel if you click the folder icon, you will see the project folder \"demo-me..\"; click on this and go into \"videos\" and you can find the \"..._id_labeled.mp4\" video, which you can double-click on to download and inspect!" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Filtering with median model /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", + "Saving filtered csv poses!\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "n7GWMBJUA9x5" - }, - "source": [ - "### Create Plots of your data:\n", - "\n", - "> after running, you can look in \"videos\", \"plot-poses\" to check out the trajectories! (sometimes you need to click the folder refresh icon to see it). Within the folder, for example, see plotmus1.png to vide the bodyparts over time vs. pixel position.\n", - "\n" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.11/dist-packages/deeplabcut/post_processing/filtering.py:298: FutureWarning: Starting with pandas version 3.0 all arguments of to_hdf except for the argument 'path_or_buf' will be keyword-only.\n", + " data.to_hdf(outdataname, \"df_with_missing\", format=\"table\", mode=\"w\")\n" + ] }, { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "7w9BDIA7BB_i", - "outputId": "a163087d-cbcb-4e4d-f461-2e24ed19a80b" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4 and data.\n", - "Plots created! Please check the directory \"plot-poses\" within the video directory\n" - ] - } - ], - "source": [ - "deeplabcut.plot_trajectories(config_path, [video], shuffle=0,videotype=\"mp4\")" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting to process video: /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", + "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4 and data.\n", + "Duration of video [s]: 77.67, recorded with 30.0 fps!\n", + "Overall # of frames: 2330 with cropped frame dimensions: 640 480\n", + "Generating frames and creating video.\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "l7BJQq7nxHVz" - }, - "source": [ - "# Transformer for reID\n", - "\n", - "while the tracking here is very good without using the transformer, we want to demo the workflow for you!" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.11/dist-packages/deeplabcut/utils/make_labeled_video.py:140: FutureWarning: DataFrame.groupby with axis=1 is deprecated. Do `frame.T.groupby(...)` without axis instead.\n", + " Dataframe.groupby(level=\"individuals\", axis=1).size().values // 3\n", + "100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 2330/2330 [00:31<00:00, 73.04it/s]\n" + ] }, { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "5xlO6TVYxQWc", - "outputId": "a433221f-0390-4028-fe68-be0b90adad48" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Using snapshot-20000 for model /content/demo-me-2021-07-14/dlc-models/iteration-0/demoJul14-trainset95shuffle0\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", - " warnings.warn('`layer.apply` is deprecated and '\n", - "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", - " warnings.warn('`layer.apply` is deprecated and '\n", - "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", - " warnings.warn('`layer.apply` is deprecated and '\n", - "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", - " warnings.warn('`layer.apply` is deprecated and '\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Activating extracting of PAFs\n", - "Starting to analyze % /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", - "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", - "Duration of video [s]: 77.67 , recorded with 30.0 fps!\n", - "Overall # of frames: 2330 found with (before cropping) frame dimensions: 640 480\n", - "Starting to extract posture\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 2330/2330 [01:18<00:00, 29.78it/s]\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "If the tracking is not satisfactory for some videos, consider expanding the training set. You can use the function 'extract_outlier_frames' to extract a few representative outlier frames.\n", - "Epoch 10, train acc: 0.61\n", - "Epoch 10, test acc 0.45\n", - "Epoch 20, train acc: 0.74\n", - "Epoch 20, test acc 0.65\n", - "Epoch 30, train acc: 0.78\n", - "Epoch 30, test acc 0.55\n", - "Epoch 40, train acc: 0.76\n", - "Epoch 40, test acc 0.50\n", - "Epoch 50, train acc: 0.85\n", - "Epoch 50, test acc 0.55\n", - "Epoch 60, train acc: 0.84\n", - "Epoch 60, test acc 0.60\n", - "Epoch 70, train acc: 0.85\n", - "Epoch 70, test acc 0.55\n", - "Epoch 80, train acc: 0.79\n", - "Epoch 80, test acc 0.55\n", - "Epoch 90, train acc: 0.88\n", - "Epoch 90, test acc 0.55\n", - "Epoch 100, train acc: 0.84\n", - "Epoch 100, test acc 0.55\n", - "loading params\n", - "Processing... /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 4/4 [00:00<00:00, 483.21it/s]\n", - "/usr/local/lib/python3.11/dist-packages/deeplabcut/refine_training_dataset/stitch.py:934: FutureWarning: Starting with pandas version 3.0 all arguments of to_hdf except for the argument 'path_or_buf' will be keyword-only.\n", - " df.to_hdf(output_name, \"tracks\", format=\"table\", mode=\"w\")\n" - ] - } - ], - "source": [ - "deeplabcut.transformer_reID(\n", - " config_path,\n", - " [video],\n", - " shuffle=0,\n", - " videotype=\"mp4\",\n", - " track_method=\"ellipse\",\n", - " n_triplets=100,\n", - ")" + "data": { + "text/plain": [ + "[True]" ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "#Filter the predictions to remove small jitter, if desired:\n", + "deeplabcut.filterpredictions(config_path, [video], shuffle=0, videotype=\"mp4\")\n", + "deeplabcut.create_labeled_video(\n", + " config_path,\n", + " [video],\n", + " videotype=\"mp4\",\n", + " shuffle=0,\n", + " color_by=\"individual\",\n", + " keypoints_only=False,\n", + " draw_skeleton=True,\n", + " filtered=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "AYNlrgeNUG4U" + }, + "source": [ + "Now, on the left panel if you click the folder icon, you will see the project folder \"demo-me..\"; click on this and go into \"videos\" and you can find the \"..._id_labeled.mp4\" video, which you can double-click on to download and inspect!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "n7GWMBJUA9x5" + }, + "source": [ + "### Create Plots of your data:\n", + "\n", + "> after running, you can look in \"videos\", \"plot-poses\" to check out the trajectories! (sometimes you need to click the folder refresh icon to see it). Within the folder, for example, see plotmus1.png to vide the bodyparts over time vs. pixel position.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "7w9BDIA7BB_i", + "outputId": "a163087d-cbcb-4e4d-f461-2e24ed19a80b" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "uO_yoqN7xiBT" - }, - "source": [ - "now we can make another video with the transformer-guided tracking:\n" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4 and data.\n", + "Plots created! Please check the directory \"plot-poses\" within the video directory\n" + ] + } + ], + "source": [ + "deeplabcut.plot_trajectories(config_path, [video], shuffle=0,videotype=\"mp4\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "l7BJQq7nxHVz" + }, + "source": [ + "# Transformer for reID\n", + "\n", + "while the tracking here is very good without using the transformer, we want to demo the workflow for you!" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "5xlO6TVYxQWc", + "outputId": "a433221f-0390-4028-fe68-be0b90adad48" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "MBMbRFEMxmi4", - "outputId": "5ca4357a-c8e1-46c6-ecad-141bfce48cc5" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4 and data.\n", - "Plots created! Please check the directory \"plot-poses\" within the video directory\n" - ] - } - ], - "source": [ - "deeplabcut.plot_trajectories(\n", - " config_path,\n", - " [video],\n", - " shuffle=0,\n", - " videotype=\"mp4\",\n", - " track_method=\"transformer\",\n", - ")" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Using snapshot-20000 for model /content/demo-me-2021-07-14/dlc-models/iteration-0/demoJul14-trainset95shuffle0\n" + ] }, { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "vx3e-r1CoXaX", - "outputId": "46cdbd39-d1f6-4b78-abba-7e979740f2a2" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Starting to process video: /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", - "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4 and data.\n", - "Duration of video [s]: 77.67, recorded with 30.0 fps!\n", - "Overall # of frames: 2330 with cropped frame dimensions: 640 480\n", - "Generating frames and creating video.\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/lib/python3.11/dist-packages/deeplabcut/utils/make_labeled_video.py:140: FutureWarning: DataFrame.groupby with axis=1 is deprecated. Do `frame.T.groupby(...)` without axis instead.\n", - " Dataframe.groupby(level=\"individuals\", axis=1).size().values // 3\n", - "100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 2330/2330 [00:31<00:00, 73.75it/s]\n" - ] - }, - { - "data": { - "text/plain": [ - "[True]" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "deeplabcut.create_labeled_video(\n", - " config_path,\n", - " [video],\n", - " videotype=\"mp4\",\n", - " shuffle=0,\n", - " color_by=\"individual\",\n", - " keypoints_only=False,\n", - " draw_skeleton=True,\n", - " track_method=\"transformer\"\n", - ")" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", + " warnings.warn('`layer.apply` is deprecated and '\n", + "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", + " warnings.warn('`layer.apply` is deprecated and '\n", + "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", + " warnings.warn('`layer.apply` is deprecated and '\n", + "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", + " warnings.warn('`layer.apply` is deprecated and '\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Activating extracting of PAFs\n", + "Starting to analyze % /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", + "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", + "Duration of video [s]: 77.67 , recorded with 30.0 fps!\n", + "Overall # of frames: 2330 found with (before cropping) frame dimensions: 640 480\n", + "Starting to extract posture\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 2330/2330 [01:18<00:00, 29.78it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "If the tracking is not satisfactory for some videos, consider expanding the training set. You can use the function 'extract_outlier_frames' to extract a few representative outlier frames.\n", + "Epoch 10, train acc: 0.61\n", + "Epoch 10, test acc 0.45\n", + "Epoch 20, train acc: 0.74\n", + "Epoch 20, test acc 0.65\n", + "Epoch 30, train acc: 0.78\n", + "Epoch 30, test acc 0.55\n", + "Epoch 40, train acc: 0.76\n", + "Epoch 40, test acc 0.50\n", + "Epoch 50, train acc: 0.85\n", + "Epoch 50, test acc 0.55\n", + "Epoch 60, train acc: 0.84\n", + "Epoch 60, test acc 0.60\n", + "Epoch 70, train acc: 0.85\n", + "Epoch 70, test acc 0.55\n", + "Epoch 80, train acc: 0.79\n", + "Epoch 80, test acc 0.55\n", + "Epoch 90, train acc: 0.88\n", + "Epoch 90, test acc 0.55\n", + "Epoch 100, train acc: 0.84\n", + "Epoch 100, test acc 0.55\n", + "loading params\n", + "Processing... /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 4/4 [00:00<00:00, 483.21it/s]\n", + "/usr/local/lib/python3.11/dist-packages/deeplabcut/refine_training_dataset/stitch.py:934: FutureWarning: Starting with pandas version 3.0 all arguments of to_hdf except for the argument 'path_or_buf' will be keyword-only.\n", + " df.to_hdf(output_name, \"tracks\", format=\"table\", mode=\"w\")\n" + ] + } + ], + "source": [ + "deeplabcut.transformer_reID(\n", + " config_path,\n", + " [video],\n", + " shuffle=0,\n", + " videotype=\"mp4\",\n", + " track_method=\"ellipse\",\n", + " n_triplets=100,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uO_yoqN7xiBT" + }, + "source": [ + "now we can make another video with the transformer-guided tracking:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "MBMbRFEMxmi4", + "outputId": "5ca4357a-c8e1-46c6-ecad-141bfce48cc5" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4 and data.\n", + "Plots created! Please check the directory \"plot-poses\" within the video directory\n" + ] } - ], - "metadata": { - "accelerator": "GPU", + ], + "source": [ + "deeplabcut.plot_trajectories(\n", + " config_path,\n", + " [video],\n", + " shuffle=0,\n", + " videotype=\"mp4\",\n", + " track_method=\"transformer\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { "colab": { - "gpuType": "A100", - "include_colab_link": true, - "machine_shape": "hm", - "name": "COLAB_transformer_reID.ipynb", - "provenance": [] + "base_uri": "https://localhost:8080/" }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" + "id": "vx3e-r1CoXaX", + "outputId": "46cdbd39-d1f6-4b78-abba-7e979740f2a2" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting to process video: /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", + "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4 and data.\n", + "Duration of video [s]: 77.67, recorded with 30.0 fps!\n", + "Overall # of frames: 2330 with cropped frame dimensions: 640 480\n", + "Generating frames and creating video.\n" + ] }, - "language_info": { - "name": "python" + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.11/dist-packages/deeplabcut/utils/make_labeled_video.py:140: FutureWarning: DataFrame.groupby with axis=1 is deprecated. Do `frame.T.groupby(...)` without axis instead.\n", + " Dataframe.groupby(level=\"individuals\", axis=1).size().values // 3\n", + "100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 2330/2330 [00:31<00:00, 73.75it/s]\n" + ] + }, + { + "data": { + "text/plain": [ + "[True]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" } + ], + "source": [ + "deeplabcut.create_labeled_video(\n", + " config_path,\n", + " [video],\n", + " videotype=\"mp4\",\n", + " shuffle=0,\n", + " color_by=\"individual\",\n", + " keypoints_only=False,\n", + " draw_skeleton=True,\n", + " track_method=\"transformer\"\n", + ")" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "A100", + "include_colab_link": true, + "machine_shape": "hm", + "name": "COLAB_transformer_reID.ipynb", + "provenance": [] + }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2026-02-10", + "last_metadata_updated": "2026-03-06" + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" }, - "nbformat": 4, - "nbformat_minor": 0 + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb b/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb index 42242b6664..a92b639131 100644 --- a/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb +++ b/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb @@ -274,6 +274,11 @@ } ], "metadata": { + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-02-28", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", diff --git a/examples/JUPYTER/Demo_labeledexample_MouseReaching.ipynb b/examples/JUPYTER/Demo_labeledexample_MouseReaching.ipynb index 312e0a1505..dfc597b94c 100644 --- a/examples/JUPYTER/Demo_labeledexample_MouseReaching.ipynb +++ b/examples/JUPYTER/Demo_labeledexample_MouseReaching.ipynb @@ -512,6 +512,11 @@ "provenance": [], "version": "0.3.2" }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-02-28", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", diff --git a/examples/JUPYTER/Demo_labeledexample_Openfield.ipynb b/examples/JUPYTER/Demo_labeledexample_Openfield.ipynb index d4458f1111..d3c824b90a 100644 --- a/examples/JUPYTER/Demo_labeledexample_Openfield.ipynb +++ b/examples/JUPYTER/Demo_labeledexample_Openfield.ipynb @@ -445,6 +445,11 @@ "provenance": [], "version": "0.3.2" }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-02-28", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", diff --git a/examples/JUPYTER/Demo_napari.ipynb b/examples/JUPYTER/Demo_napari.ipynb index 6ebbfd7a6c..3a81f75ff1 100644 --- a/examples/JUPYTER/Demo_napari.ipynb +++ b/examples/JUPYTER/Demo_napari.ipynb @@ -529,6 +529,11 @@ "provenance": [], "version": "0.3.2" }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-09-16", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", diff --git a/examples/JUPYTER/Demo_yourowndata.ipynb b/examples/JUPYTER/Demo_yourowndata.ipynb index 0596204849..d7baf82ca2 100644 --- a/examples/JUPYTER/Demo_yourowndata.ipynb +++ b/examples/JUPYTER/Demo_yourowndata.ipynb @@ -548,6 +548,11 @@ "provenance": [], "version": "0.3.2" }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-09-16", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", diff --git a/examples/JUPYTER/Docker_TrainNetwork_VideoAnalysis.ipynb b/examples/JUPYTER/Docker_TrainNetwork_VideoAnalysis.ipynb index d6699d623c..80d41613c8 100644 --- a/examples/JUPYTER/Docker_TrainNetwork_VideoAnalysis.ipynb +++ b/examples/JUPYTER/Docker_TrainNetwork_VideoAnalysis.ipynb @@ -331,6 +331,11 @@ "toc_visible": true, "version": "0.3.2" }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-09-16", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", From c2e519d8019c815c1f03f749592e85841d09449e Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 17 Mar 2026 11:33:38 +0100 Subject: [PATCH 35/37] Populate live-GUI docs --- docs/dlc-live/deeplabcutlive.md | 5 +++++ docs/dlc-live/dlc-live-gui/index.md | 7 ++++++- docs/dlc-live/dlc-live-gui/quickstart/install.md | 5 +++++ .../user_guide/cameras_backends/aravis_backend.md | 5 +++++ .../user_guide/cameras_backends/basler_backend.md | 7 ++++++- .../user_guide/cameras_backends/camera_support.md | 11 +++++++---- .../user_guide/cameras_backends/gentl_backend.md | 5 +++++ .../user_guide/cameras_backends/opencv_backend.md | 5 +++++ .../dlc-live-gui/user_guide/misc/misc_landing.md | 5 +++++ .../user_guide/misc/modelzoo_downloads.md | 5 +++++ .../dlc-live-gui/user_guide/misc/timestamp_format.md | 7 ++++++- docs/dlc-live/dlc-live-gui/user_guide/overview.md | 5 +++++ 12 files changed, 65 insertions(+), 7 deletions(-) diff --git a/docs/dlc-live/deeplabcutlive.md b/docs/dlc-live/deeplabcutlive.md index 9b61f2f857..1d4b38d9ff 100644 --- a/docs/dlc-live/deeplabcutlive.md +++ b/docs/dlc-live/deeplabcutlive.md @@ -1,3 +1,8 @@ +--- +deeplabcut: + last_metadata_updated: '2026-03-17' + ignore: false +--- (deeplabcut-live)= # Running DeepLabCut models in real-time diff --git a/docs/dlc-live/dlc-live-gui/index.md b/docs/dlc-live/dlc-live-gui/index.md index ad5877061b..8f82588b2d 100644 --- a/docs/dlc-live/dlc-live-gui/index.md +++ b/docs/dlc-live/dlc-live-gui/index.md @@ -1,3 +1,8 @@ +--- +deeplabcut: + last_metadata_updated: '2026-03-17' + ignore: false +--- # DeepLabCut-live-GUI A graphical application for **real-time pose estimation with DeepLabCut** using one or more cameras. @@ -71,5 +76,5 @@ Before getting started, be aware of the following constraints: ## Feedback, issues, and contributions > *This project is under active development. Feedback from real experimental use is highly valued.* -> +> > [Please report issues, suggest features, or contribute to the codebase on GitHub !](https://github.com/DeepLabCut/DeepLabCut-live-GUI) diff --git a/docs/dlc-live/dlc-live-gui/quickstart/install.md b/docs/dlc-live/dlc-live-gui/quickstart/install.md index 1fc163e7ca..fbbd385a28 100644 --- a/docs/dlc-live/dlc-live-gui/quickstart/install.md +++ b/docs/dlc-live/dlc-live-gui/quickstart/install.md @@ -1,3 +1,8 @@ +--- +deeplabcut: + last_metadata_updated: '2026-03-17' + ignore: false +--- # Installation This page explains how to install **DeepLabCut-live-GUI** for interactive, real‑time pose estimation. diff --git a/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/aravis_backend.md b/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/aravis_backend.md index 18a061cf88..9cacbe8bb4 100644 --- a/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/aravis_backend.md +++ b/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/aravis_backend.md @@ -1,3 +1,8 @@ +--- +deeplabcut: + last_metadata_updated: '2026-03-17' + ignore: false +--- (file:dlclivegui-camera-aravis-backend)= # Aravis backend diff --git a/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/basler_backend.md b/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/basler_backend.md index 0abc3dfd8e..7bd5b7097b 100644 --- a/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/basler_backend.md +++ b/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/basler_backend.md @@ -1,3 +1,8 @@ +--- +deeplabcut: + last_metadata_updated: '2026-03-17' + ignore: false +--- (file:dlclivegui-basler-backend)= # Basler backend @@ -11,7 +16,7 @@ This backend requires the optional `pypylon` dependency. If `pypylon` is not ins --- -## Features & design +## Features & design - Native Basler camera support via **pypylon** (Pylon SDK bindings). - Best-effort device discovery without opening cameras (enumerates `DeviceInfo` entries). diff --git a/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/camera_support.md b/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/camera_support.md index a1f8b3b9a0..a3b7e1c440 100644 --- a/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/camera_support.md +++ b/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/camera_support.md @@ -1,3 +1,8 @@ +--- +deeplabcut: + last_metadata_updated: '2026-03-17' + ignore: false +--- (file:dlclivegui-camera-support)= # Camera support @@ -32,9 +37,9 @@ You can select the backend in the GUI from the "Backend" dropdown, or in your co Below are some general recommendations for backend selection based on your operating system and camera type. ```{note} -Please understand this may not reflect the exact capabilities for every setup. +Please understand this may not reflect the exact capabilities for every setup. -Let us know about your experience with different cameras and backends on different platforms to help us improve our documentation and support! +Let us know about your experience with different cameras and backends on different platforms to help us improve our documentation and support! ``` ### Windows @@ -89,5 +94,3 @@ Install vendor-provided camera drivers and SDK. CTI files are typically in: | Windows | βœ… | βœ… | ❌ | βœ… | | Linux | βœ… | βœ… | βœ… | βœ… | | macOS | βœ… | ❌ | ⚠️ | βœ… | - - diff --git a/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/gentl_backend.md b/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/gentl_backend.md index cd94974bfd..84a53aed1a 100644 --- a/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/gentl_backend.md +++ b/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/gentl_backend.md @@ -1,3 +1,8 @@ +--- +deeplabcut: + last_metadata_updated: '2026-03-17' + ignore: false +--- # GenTL backend The GenTL backend provides support for **GenICam / GenTL** compatible cameras using the **Harvesters** Python library (a GenTL consumer). diff --git a/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/opencv_backend.md b/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/opencv_backend.md index 490b7a4ce5..df58205f5b 100644 --- a/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/opencv_backend.md +++ b/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/opencv_backend.md @@ -1,3 +1,8 @@ +--- +deeplabcut: + last_metadata_updated: '2026-03-17' + ignore: false +--- (file:dlclivegui-opencv-backend)= # OpenCV backend diff --git a/docs/dlc-live/dlc-live-gui/user_guide/misc/misc_landing.md b/docs/dlc-live/dlc-live-gui/user_guide/misc/misc_landing.md index 899cee8547..4bfab1712e 100644 --- a/docs/dlc-live/dlc-live-gui/user_guide/misc/misc_landing.md +++ b/docs/dlc-live/dlc-live-gui/user_guide/misc/misc_landing.md @@ -1,3 +1,8 @@ +--- +deeplabcut: + last_metadata_updated: '2026-03-17' + ignore: false +--- # Additional resources In this section, you can find additional resources related to the GUI and DLC-live, including: diff --git a/docs/dlc-live/dlc-live-gui/user_guide/misc/modelzoo_downloads.md b/docs/dlc-live/dlc-live-gui/user_guide/misc/modelzoo_downloads.md index 8ecba380dc..1cd04b34d9 100644 --- a/docs/dlc-live/dlc-live-gui/user_guide/misc/modelzoo_downloads.md +++ b/docs/dlc-live/dlc-live-gui/user_guide/misc/modelzoo_downloads.md @@ -1,3 +1,8 @@ +--- +deeplabcut: + last_metadata_updated: '2026-03-17' + ignore: false +--- (file:dlclivegui-pretrained-models)= # Pre-trained models diff --git a/docs/dlc-live/dlc-live-gui/user_guide/misc/timestamp_format.md b/docs/dlc-live/dlc-live-gui/user_guide/misc/timestamp_format.md index 1b715fdeaa..b0b6958e1f 100644 --- a/docs/dlc-live/dlc-live-gui/user_guide/misc/timestamp_format.md +++ b/docs/dlc-live/dlc-live-gui/user_guide/misc/timestamp_format.md @@ -1,3 +1,8 @@ +--- +deeplabcut: + last_metadata_updated: '2026-03-17' + ignore: false +--- (file:dlclivegui-tinmestamp-format)= # Video timestamp format @@ -92,4 +97,4 @@ for frame_idx, timestamp in enumerate(data['timestamps']): The encoded video is written with a fixed input frame rate configured when recording starts. -The timestamps reflect capture/enqueue timing and may not perfectly match the encoded frame pacing, especially if frames are dropped or capture timing varies. \ No newline at end of file +The timestamps reflect capture/enqueue timing and may not perfectly match the encoded frame pacing, especially if frames are dropped or capture timing varies. diff --git a/docs/dlc-live/dlc-live-gui/user_guide/overview.md b/docs/dlc-live/dlc-live-gui/user_guide/overview.md index c9e5cc764d..fb975b5e22 100644 --- a/docs/dlc-live/dlc-live-gui/user_guide/overview.md +++ b/docs/dlc-live/dlc-live-gui/user_guide/overview.md @@ -1,3 +1,8 @@ +--- +deeplabcut: + last_metadata_updated: '2026-03-17' + ignore: false +--- # GUI overview DeepLabCut-live-GUI (`dlclivegui`) is a **PySide6-based desktop application** for running real-time DeepLabCut pose estimation experiments with **one or multiple cameras**, optional **processor plugins**, and **video recording** (with or without overlays). From 7675a2daddd30a8787bb5af87007879da62a64f1 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Thu, 19 Mar 2026 21:54:43 +0100 Subject: [PATCH 36/37] Update pyproject.toml --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f0354a9a4e..a34f0872fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -112,7 +112,6 @@ Documentation = "https://deeplabcut.github.io/DeepLabCut/README.html" dev = [ "coverage", "nbformat>5", - "pydantic>2", "pydantic>=2,<3", "pytest", "pytest-cov", From 9d31fc05c6a2f88dff3d656a1e9ebcfeb6c61c5c Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Fri, 20 Mar 2026 07:04:52 +0100 Subject: [PATCH 37/37] Update uv.lock --- uv.lock | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/uv.lock b/uv.lock index dd08a6906a..29ba96eafc 100644 --- a/uv.lock +++ b/uv.lock @@ -1069,6 +1069,7 @@ dependencies = [ { name = "pandas", extra = ["hdf5", "performance"] }, { name = "pillow" }, { name = "pycocotools" }, + { name = "pydantic" }, { name = "pyyaml" }, { name = "ruamel-yaml" }, { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -1137,6 +1138,7 @@ wandb = [ [package.dev-dependencies] dev = [ { name = "coverage" }, + { name = "nbformat" }, { name = "pydantic" }, { name = "pytest" }, { name = "pytest-cov" }, @@ -1166,6 +1168,7 @@ requires-dist = [ { name = "pandas", extras = ["hdf5", "performance"], specifier = ">=2.2,<3" }, { name = "pillow", specifier = ">=7.1" }, { name = "pycocotools" }, + { name = "pydantic", specifier = ">=2,<3" }, { name = "pyside6", marker = "platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'gui'", specifier = "<6.10" }, { name = "pyside6", marker = "(platform_machine != 'x86_64' and extra == 'gui') or (sys_platform != 'linux' and extra == 'gui')" }, { name = "pyyaml" }, @@ -1203,6 +1206,7 @@ provides-extras = ["gui", "openvino", "docs", "fmpose3d", "tf", "apple-mchips", [package.metadata.requires-dev] dev = [ { name = "coverage" }, + { name = "nbformat", specifier = ">5" }, { name = "pydantic", specifier = ">=2,<3" }, { name = "pytest" }, { name = "pytest-cov" },