File size: 8,208 Bytes
8e4f215
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3961b2f
8e4f215
 
 
 
3961b2f
 
8e4f215
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8d672b3
 
 
 
 
8e4f215
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8d672b3
 
 
 
 
 
 
 
 
 
8e4f215
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
"""Build the capability gallery: one screenshot per thing the harness can do.

    python showcase.py                  # every capability
    python showcase.py --list           # list the ids without rendering
    python showcase.py --only decisions # render a subset by id substring

The shared state - the catalogue, the renderers, the display - lives in
:mod:`showcase_core`, and the capabilities themselves in :mod:`showcase_catalog`,
so importing this file never makes a second copy of either.
"""

from __future__ import annotations

import json
import sys

import showcase_catalog  # noqa: F401  (importing it registers every shot)
from showcase_core import (
    CATALOG, GALLERY, ROOT, ensure_display, render_desktop, render_terminal, save,
)


def main(argv: list[str] | None = None) -> int:
    args = list(argv if argv is not None else sys.argv[1:])
    only = args[args.index("--only") + 1] if "--only" in args else None
    wanted = (args[args.index("--ids") + 1].split(",") if "--ids" in args else None)
    if "--card" in args:
        print("card section written:", write_card(_read_index()).name)
        return 0
    display = ensure_display()
    picked = [s for s in CATALOG
              if (s.id in wanted if wanted else (not only or only.lower() in s.id.lower()))]
    print(f"display: {display} | {len(picked)}/{len(CATALOG)} capabilities")
    if "--list" in args:
        for s in picked:
            print(f"  {s.id:36} {s.kind:8} {s.category}: {s.title}")
        return 0

    GALLERY.mkdir(exist_ok=True)
    if "--reindex" in args or only:
        # an index built from the catalogue, not from what this run happened to draw
        _write_index()
        if "--reindex" in args:
            print(f"reindexed {len(_read_index())} pictures in {GALLERY}")
            return 0
    tmp = ROOT / ".showcase-tmp"
    tmp.mkdir(exist_ok=True)
    made = []
    for s in picked:
        try:
            image = render_desktop(s, tmp) if s.kind == "desktop" else render_terminal(s, tmp)
            path = save(image, s.id)
            made.append(s)
            print(f"  ok   {s.id:34} {path.name:34} {image.size[0]}x{image.size[1]}")
        except Exception as exc:
            print(f"  FAIL {s.id:34} {type(exc).__name__}: {exc}")

    _write_index()
    print(f"wrote {len(made)} images to {GALLERY}")
    return 0


def _entry(s):
    return {"id": s.id, "category": s.category, "title": s.title, "shows": s.shows,
            "note": s.note, "kind": s.kind}


def _write_index():
    """Index every capability whose picture exists, in catalogue order."""
    entries = [_entry(s) for s in CATALOG if (GALLERY / f"{s.id}.png").exists()]
    (GALLERY / "index.json").write_text(json.dumps(entries, indent=2))
    return entries


def _read_index():
    return json.loads((GALLERY / "index.json").read_text())


def api_surface() -> tuple[list[str], list[str]]:
    """(callables, constants) - the public surface a picture has to cover.

    Introspected rather than hard-coded, so the gallery's promise is checked
    against the package instead of against a list someone forgot to update.
    """
    import importlib
    import inspect

    names, constants = [], []
    modules = ["actions", "act", "observe", "perception", "brain", "models", "loop",
               "safety", "desktop", "demo", "trace", "accessibility", "__main__",
               # 2.0: the operator layer. A picture that ignores it would let the gallery's
               # promise quietly mean "the pre-2.0 API", which is exactly the kind of claim
               # this repository is supposed to be too honest to make.
               "diff", "gates", "runner", "skills", "watchdog", "audit"]
    for module_name in modules:
        module = importlib.import_module(f"tiny_computer_agent.{module_name}")
        for name, obj in vars(module).items():
            if name.startswith("_"):
                continue
            if inspect.isfunction(obj) or inspect.isclass(obj):
                if getattr(obj, "__module__", "") != module.__name__:
                    continue        # imported, not defined here
                names.append(name)
                if inspect.isclass(obj):
                    for member, value in vars(obj).items():
                        if member.startswith("_"):
                            continue
                        if inspect.isfunction(value) or isinstance(value, property):
                            names.append(f"{name}.{member}")
            elif name.isupper() and isinstance(obj, (str, int, float, tuple, frozenset)):
                constants.append(name)
    return names, constants


def write_card(entries: list[dict]) -> Path:
    """Write the gallery page, and the section the model card embeds.

    Every entry carries the callables it demonstrates, so the card can promise -
    and a reader can check - that no part of the public API is left without a
    picture.
    """
    areas: dict[str, list[dict]] = {}
    for entry in entries:
        areas.setdefault(entry["category"], []).append(entry)
    pictured = sorted({name for entry in entries for name in entry["shows"]})
    callables, constants = api_surface()
    missing = sorted(set(callables) - set(pictured))
    if missing:
        print(f"  WARNING: {len(missing)} public callables are not pictured: {missing}")

    lines = [
        "## Capability gallery",
        "",
        f"**{len(entries)} capabilities, one screenshot each - and "
        f"{len(set(callables) & set(pictured))} of the package's {len(callables)} public "
        f"callables (functions, methods, classes and protocols) appear in at least one of "
        f"them**, together with {len(set(constants) & set(pictured))} of its public "
        f"constants.",
        "",
        (f"_Not yet pictured: {len(missing)} callables added in 2.0 - the gate, skill, "
         f"runner, diff, watchdog and audit layers. They are covered by tests; the "
         f"roadmap tracks the pictures._" if missing else
         "_Every public callable in the package is pictured._"),
        "",
        "Nothing here is a mock-up. Each picture is produced by "
        "[`showcase.py`](showcase.py): a command or a call is run for real, and its actual "
        "output - or the real desktop it acted on - is what you see. The desktop shots are "
        "real screen captures on a real X server (Xvfb, 1280x800), including a real mouse "
        "click verified by the target application's own reaction.",
        "",
        "> **An honest note on the platform.** These were captured on Linux, because the "
        "sandbox that builds this card has no Windows: no KVM, no QEMU and no Windows image. "
        "The harness is OS-agnostic (pyautogui uses `SendInput` on Windows, and `GrabScreen` "
        "falls back to Pillow's `ImageGrab`), so these are the same code paths - but they are "
        "not Windows pixels. The Windows twins are generated by the same script in "
        "[`.github/workflows/live-desktop.yml`](.github/workflows/live-desktop.yml).",
        "",
        "Regenerate it with `python showcase.py` (`--list` to see the ids, `--only <id>` for a "
        "subset, `--card` to rewrite this section). It runs the calls for real, so a broken "
        "capability fails loudly instead of shipping a stale picture.",
        "",
        "| area | capabilities |",
        "|---|---:|",
    ]
    lines += [f"| {area} | {len(items)} |" for area, items in areas.items()]
    lines += ["", "---", ""]

    for area, items in areas.items():
        lines += [f"### {area}", ""]
        for entry in items:
            lines += [f"**{entry['title']}**", ""]
            if entry["note"]:
                lines += [f"*{entry['note']}*", ""]
            lines += [f'<img src="gallery/{entry["id"]}.png" width="860">', ""]
            lines += ["`shows:` " + ", ".join(f"`{name}`" for name in entry["shows"]), ""]

    text = "\n".join(lines)
    (GALLERY / "_card_section.md").write_text(text)
    (GALLERY / "README.md").write_text(
        "---\ntitle: Capability gallery\n---\n\n" + text + "\n")
    return GALLERY / "_card_section.md"


if __name__ == "__main__":
    raise SystemExit(main())