File size: 4,615 Bytes
290ff9e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
"""Capture sample responses from the CV Tool API.



Usage:

    uv run python scripts/capture_cv_samples.py path/to/image.jpg

    uv run python scripts/capture_cv_samples.py https://example.com/image.png

    uv run python scripts/capture_cv_samples.py img.jpg --heavy



Reads CV_API_BASE_URL and CV_API_KEY from the environment. Writes one JSON

per tool to tests/fixtures/cv/<tool>.json — these become the source of truth

for the typed `result` models in cv/signals.py and for snapshot tests.



Re-run when the CV service schema changes. The fixtures pin our expectations;

shape drift surfaces when the typed models fail to validate.



Heavy outputs (segmentation masks, depth maps) are omitted by default to keep

fixtures small. Pass --heavy to include them when needed for annotation work.

"""

from __future__ import annotations

import argparse
import asyncio
import json
import os
import sys
from pathlib import Path

from ergo_agentic.cv import HttpCVClient
from ergo_agentic.cv.client import (
    TOOL_DEPTH,
    TOOL_MEDIAPIPE_HANDS,
    TOOL_MEDIAPIPE_POSE,
    TOOL_RFDETR,
    TOOL_SAM2,
    TOOL_YOLO_DETECT,
    TOOL_YOLO_POSE,
    TOOL_YOLO_SEG,
)

# All CV signal-producing tools. gemma-ergonomics is intentionally excluded —
# it's a VLM-with-structured-output, treated as a peer model in vision_pass,
# not a CV signal source. sam2-segment-boxes is excluded because it requires
# input boxes (not a one-shot capture).
ALL_TOOLS: tuple[str, ...] = (
    TOOL_MEDIAPIPE_POSE,
    TOOL_MEDIAPIPE_HANDS,
    TOOL_YOLO_POSE,
    TOOL_YOLO_DETECT,
    TOOL_YOLO_SEG,
    TOOL_SAM2,
    TOOL_RFDETR,
    TOOL_DEPTH,
)

HEAVY_TOOLS: frozenset[str] = frozenset({TOOL_YOLO_SEG, TOOL_SAM2, TOOL_DEPTH, TOOL_RFDETR})


async def _run() -> int:
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument("image", help="image path or URL to send to each tool")
    parser.add_argument(
        "--base-url",
        default=os.environ.get("CV_API_BASE_URL", "http://localhost:7860"),
        help="CV service base URL (default: env CV_API_BASE_URL or http://localhost:7860)",
    )
    parser.add_argument(
        "--api-key",
        default=os.environ.get("CV_API_KEY"),
        help="API key for X-Tool-API-Key header (default: env CV_API_KEY)",
    )
    parser.add_argument(
        "--out",
        default="tests/fixtures/cv",
        help="output directory (default: tests/fixtures/cv)",
    )
    parser.add_argument(
        "--heavy",
        action="store_true",
        help="request heavy outputs (masks, depth maps). Larger fixtures.",
    )
    parser.add_argument(
        "--tools",
        nargs="+",
        choices=list(ALL_TOOLS),
        help="subset of tools to capture (default: all)",
    )
    args = parser.parse_args()

    if not args.api_key:
        parser.error("CV_API_KEY env var or --api-key required")

    out_dir = Path(args.out)
    out_dir.mkdir(parents=True, exist_ok=True)

    image = {"image_id": "sample", "url": args.image, "label": None}
    tools = tuple(args.tools) if args.tools else ALL_TOOLS

    print(f"CV service: {args.base_url}")
    print(f"Image:      {args.image}")
    print(f"Output:     {out_dir}")
    print(f"Heavy:      {args.heavy}")
    print()

    client = HttpCVClient(base_url=args.base_url, api_key=args.api_key)
    failures = 0
    try:
        for tool in tools:
            include_heavy = args.heavy and tool in HEAVY_TOOLS
            print(f"  → {tool:24s}", end=" ", flush=True)
            try:
                resp = await client.call(tool, image, include_heavy_outputs=include_heavy)
                out_path = out_dir / f"{tool}.json"
                out_path.write_text(json.dumps(resp.model_dump(), indent=2, default=str))
                result_size = len(json.dumps(resp.result, default=str))
                tag = " [heavy omitted]" if resp.heavy_outputs_omitted else ""
                tag += f" [warnings: {len(resp.warnings)}]" if resp.warnings else ""
                print(f"{resp.status:7s}  result={result_size:>7d}B{tag}")
            except Exception as e:  # noqa: BLE001
                print(f"FAILED  {type(e).__name__}: {e}")
                failures += 1
    finally:
        await client.aclose()

    print()
    print(f"Done. Wrote {len(tools) - failures}/{len(tools)} fixtures to {out_dir}")
    return 0 if failures == 0 else 1


if __name__ == "__main__":
    sys.exit(asyncio.run(_run()))