@howaboua/pi-pet 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/SECURITY.md +24 -0
- package/authoring/PET-GUIDE.md +65 -0
- package/authoring/hatch/HATCH-GUIDE.md +136 -0
- package/authoring/hatch/LICENSE.txt +201 -0
- package/authoring/hatch/NOTICE.md +5 -0
- package/authoring/hatch/pyproject.toml +8 -0
- package/authoring/hatch/references/animation-rows.md +29 -0
- package/authoring/hatch/references/final-assembly.md +67 -0
- package/authoring/hatch/references/look-directions.md +35 -0
- package/authoring/hatch/references/pet-contract.md +40 -0
- package/authoring/hatch/references/qa-rubric.md +34 -0
- package/authoring/hatch/references/reviewer-protocol.md +72 -0
- package/authoring/hatch/references/standard-assembly.md +21 -0
- package/authoring/hatch/references/visual-generation.md +59 -0
- package/authoring/hatch/scripts/approve_cardinals.py +110 -0
- package/authoring/hatch/scripts/assemble_extended_atlas.py +684 -0
- package/authoring/hatch/scripts/combine_direction_blind_verdicts.py +59 -0
- package/authoring/hatch/scripts/compose_atlas.py +171 -0
- package/authoring/hatch/scripts/compose_cardinal_anchor_strip.py +45 -0
- package/authoring/hatch/scripts/derive_running_left_from_running_right.py +164 -0
- package/authoring/hatch/scripts/despill_chroma_edges.py +312 -0
- package/authoring/hatch/scripts/extract_cardinal_anchors.py +153 -0
- package/authoring/hatch/scripts/extract_strip_frames.py +409 -0
- package/authoring/hatch/scripts/inspect_frames.py +278 -0
- package/authoring/hatch/scripts/make_contact_sheet.py +116 -0
- package/authoring/hatch/scripts/make_direction_blind_qa_sheet.py +179 -0
- package/authoring/hatch/scripts/make_direction_qa_sheet.py +172 -0
- package/authoring/hatch/scripts/measure_direction_continuity.py +201 -0
- package/authoring/hatch/scripts/prepare_pet_run.py +1207 -0
- package/authoring/hatch/scripts/render_animation_previews.py +80 -0
- package/authoring/hatch/scripts/validate_atlas.py +299 -0
- package/authoring/hatch/scripts/validate_direction_blind_verdicts.py +100 -0
- package/authoring/hatch/uv.lock +99 -0
- package/authoring/references/import-existing-pet.md +47 -0
- package/authoring/references/pet-format.md +88 -0
- package/changelog.ts +327 -0
- package/deploy/pi-pet-hyprland.conf +10 -0
- package/desktop/build.mjs +194 -0
- package/desktop/package-lock.json +887 -0
- package/desktop/package.json +16 -0
- package/dist/authoring/prepare.mjs +3 -0
- package/dist/authoring/rebuild.mjs +4 -0
- package/dist/authoring/validate.mjs +2 -0
- package/dist/desktop/main.mjs +5 -0
- package/dist/desktop/preload.cjs +1 -0
- package/dist/web/app.js +1 -0
- package/dist/web/catalog.json +1 -0
- package/dist/web/index.html +100 -0
- package/dist/web/manifest.webmanifest +16 -0
- package/dist/web/pet-icon.svg +6 -0
- package/dist/web/spritesheet.webp +0 -0
- package/dist/web/styles.css +592 -0
- package/docs/architecture.md +41 -0
- package/docs/pi-pet.png +0 -0
- package/docs/research.md +17 -0
- package/extensions/desktop-command.ts +249 -0
- package/extensions/desktop-config.ts +152 -0
- package/extensions/desktop-launcher.ts +380 -0
- package/extensions/gippity.ts +24 -0
- package/extensions/index.ts +110 -0
- package/extensions/remote-app.ts +61 -0
- package/package.json +101 -0
- package/pets/clawa/PET.md +27 -0
- package/pets/clawa/pet.json +7 -0
- package/pets/clawa/spritesheet.webp +0 -0
- package/pets/clawa/validation.json +808 -0
- package/src/desktop/attention.ts +87 -0
- package/src/desktop/bridge.ts +31 -0
- package/src/desktop/config.ts +145 -0
- package/src/desktop/cursor-provider.ts +150 -0
- package/src/desktop/main.ts +459 -0
- package/src/desktop/preload.ts +19 -0
- package/src/pet-image-size.ts +72 -0
- package/src/pet-loader.ts +278 -0
- package/src/pet-storage.ts +240 -0
- package/src/protocol/index.ts +81 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Render lightweight animated QA previews from extracted Codex pet frames."""
|
|
3
|
+
|
|
4
|
+
# Modified from the Apache-2.0 Hatch Pet workflow for the Pi Pet authoring package.
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from PIL import Image
|
|
13
|
+
|
|
14
|
+
ROW_DURATIONS = {
|
|
15
|
+
"idle": [280, 110, 110, 140, 140, 320],
|
|
16
|
+
"running-right": [120, 120, 120, 120, 120, 120, 120, 220],
|
|
17
|
+
"running-left": [120, 120, 120, 120, 120, 120, 120, 220],
|
|
18
|
+
"waving": [140, 140, 140, 280],
|
|
19
|
+
"jumping": [140, 140, 140, 140, 280],
|
|
20
|
+
"failed": [140, 140, 140, 140, 140, 140, 140, 240],
|
|
21
|
+
"waiting": [150, 150, 150, 150, 150, 260],
|
|
22
|
+
"running": [120, 120, 120, 120, 120, 220],
|
|
23
|
+
"review": [150, 150, 150, 150, 150, 280],
|
|
24
|
+
}
|
|
25
|
+
IMAGE_SUFFIXES = {".png", ".webp", ".jpg", ".jpeg"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def frame_files(state_dir: Path) -> list[Path]:
|
|
29
|
+
if not state_dir.is_dir():
|
|
30
|
+
return []
|
|
31
|
+
return sorted(path for path in state_dir.iterdir() if path.suffix.lower() in IMAGE_SUFFIXES)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_frames(frames_root: Path, state: str, expected_count: int) -> list[Image.Image]:
|
|
35
|
+
files = frame_files(frames_root / state)
|
|
36
|
+
if len(files) != expected_count:
|
|
37
|
+
raise SystemExit(
|
|
38
|
+
f"{state} preview needs {expected_count} frames, found {len(files)} under {frames_root / state}"
|
|
39
|
+
)
|
|
40
|
+
frames = []
|
|
41
|
+
for path in files:
|
|
42
|
+
with Image.open(path) as opened:
|
|
43
|
+
frames.append(opened.convert("RGBA"))
|
|
44
|
+
return frames
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def save_preview(frames: list[Image.Image], durations: list[int], output: Path) -> None:
|
|
48
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
frames[0].save(
|
|
50
|
+
output,
|
|
51
|
+
save_all=True,
|
|
52
|
+
append_images=frames[1:],
|
|
53
|
+
duration=durations,
|
|
54
|
+
loop=0,
|
|
55
|
+
disposal=2,
|
|
56
|
+
optimize=False,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main() -> None:
|
|
61
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
62
|
+
parser.add_argument("--frames-root", required=True)
|
|
63
|
+
parser.add_argument("--output-dir", required=True)
|
|
64
|
+
args = parser.parse_args()
|
|
65
|
+
|
|
66
|
+
frames_root = Path(args.frames_root).expanduser().resolve()
|
|
67
|
+
output_dir = Path(args.output_dir).expanduser().resolve()
|
|
68
|
+
previews = []
|
|
69
|
+
for state, durations in ROW_DURATIONS.items():
|
|
70
|
+
frames = load_frames(frames_root, state, len(durations))
|
|
71
|
+
output = output_dir / f"{state}.gif"
|
|
72
|
+
save_preview(frames, durations, output)
|
|
73
|
+
previews.append({"state": state, "path": str(output), "frames": len(frames)})
|
|
74
|
+
|
|
75
|
+
result = {"ok": True, "output_dir": str(output_dir), "previews": previews}
|
|
76
|
+
print(json.dumps(result, indent=2))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
main()
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validate a Codex pet spritesheet atlas."""
|
|
3
|
+
|
|
4
|
+
# Modified from the Apache-2.0 Hatch Pet workflow for current Pillow pixel access.
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
import math
|
|
11
|
+
import re
|
|
12
|
+
from collections import defaultdict
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from PIL import Image, ImageFilter
|
|
16
|
+
|
|
17
|
+
COLUMNS = 8
|
|
18
|
+
ROWS = 9
|
|
19
|
+
EXTENDED_ROWS = 11
|
|
20
|
+
CELL_WIDTH = 192
|
|
21
|
+
CELL_HEIGHT = 208
|
|
22
|
+
ATLAS_WIDTH = COLUMNS * CELL_WIDTH
|
|
23
|
+
ATLAS_HEIGHT = ROWS * CELL_HEIGHT
|
|
24
|
+
EXTENDED_ATLAS_HEIGHT = EXTENDED_ROWS * CELL_HEIGHT
|
|
25
|
+
ROW_BY_INDEX = {
|
|
26
|
+
0: ("idle", 6),
|
|
27
|
+
1: ("running-right", 8),
|
|
28
|
+
2: ("running-left", 8),
|
|
29
|
+
3: ("waving", 4),
|
|
30
|
+
4: ("jumping", 5),
|
|
31
|
+
5: ("failed", 8),
|
|
32
|
+
6: ("waiting", 6),
|
|
33
|
+
7: ("running", 6),
|
|
34
|
+
8: ("review", 6),
|
|
35
|
+
9: ("look-000-to-157.5", 8),
|
|
36
|
+
10: ("look-180-to-337.5", 8),
|
|
37
|
+
}
|
|
38
|
+
EXTENDED_NEUTRAL_LOOK_FRAME = (0, 6)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def parse_hex_color(value: str) -> tuple[int, int, int]:
|
|
42
|
+
if not re.fullmatch(r"#[0-9a-fA-F]{6}", value):
|
|
43
|
+
raise SystemExit(f"invalid chroma key color: {value}; expected #RRGGBB")
|
|
44
|
+
return tuple(int(value[index : index + 2], 16) for index in (1, 3, 5))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def alpha_nonzero_count(image: Image.Image) -> int:
|
|
48
|
+
alpha = image.getchannel("A")
|
|
49
|
+
return sum(alpha.histogram()[1:])
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def transparent_rgb_residue_count(image: Image.Image) -> int:
|
|
53
|
+
rgba = image.convert("RGBA")
|
|
54
|
+
data = rgba.tobytes()
|
|
55
|
+
count = 0
|
|
56
|
+
for index in range(0, len(data), 4):
|
|
57
|
+
red, green, blue, alpha = data[index : index + 4]
|
|
58
|
+
if alpha == 0 and (red or green or blue):
|
|
59
|
+
count += 1
|
|
60
|
+
return count
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def color_distance(
|
|
64
|
+
red: int,
|
|
65
|
+
green: int,
|
|
66
|
+
blue: int,
|
|
67
|
+
key: tuple[int, int, int],
|
|
68
|
+
) -> float:
|
|
69
|
+
return math.sqrt((red - key[0]) ** 2 + (green - key[1]) ** 2 + (blue - key[2]) ** 2)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def opaque_chroma_key_count(
|
|
73
|
+
image: Image.Image,
|
|
74
|
+
chroma_key: tuple[int, int, int],
|
|
75
|
+
threshold: float,
|
|
76
|
+
) -> int:
|
|
77
|
+
rgba = image.convert("RGBA")
|
|
78
|
+
data = rgba.tobytes()
|
|
79
|
+
count = 0
|
|
80
|
+
for index in range(0, len(data), 4):
|
|
81
|
+
red, green, blue, alpha = data[index : index + 4]
|
|
82
|
+
if alpha > 16 and color_distance(red, green, blue, chroma_key) <= threshold:
|
|
83
|
+
count += 1
|
|
84
|
+
return count
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def is_chroma_contaminated(
|
|
88
|
+
color: tuple[int, int, int],
|
|
89
|
+
chroma_key: tuple[int, int, int],
|
|
90
|
+
distance_threshold: float,
|
|
91
|
+
) -> bool:
|
|
92
|
+
return color_distance(*color, chroma_key) <= distance_threshold
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def chroma_fringe_count(
|
|
96
|
+
image: Image.Image,
|
|
97
|
+
*,
|
|
98
|
+
chroma_key: tuple[int, int, int],
|
|
99
|
+
distance_threshold: float,
|
|
100
|
+
edge_radius: int,
|
|
101
|
+
alpha_minimum: int,
|
|
102
|
+
) -> int:
|
|
103
|
+
rgba = image.convert("RGBA")
|
|
104
|
+
alpha = rgba.getchannel("A")
|
|
105
|
+
visible = [value > 0 for value in alpha.get_flattened_data()]
|
|
106
|
+
transparent = Image.new("L", alpha.size)
|
|
107
|
+
transparent.putdata([255 if not value else 0 for value in visible])
|
|
108
|
+
expanded = transparent.filter(ImageFilter.MaxFilter(edge_radius * 2 + 1))
|
|
109
|
+
return sum(
|
|
110
|
+
alpha_value >= alpha_minimum
|
|
111
|
+
and nearby_transparency > 0
|
|
112
|
+
and is_chroma_contaminated(
|
|
113
|
+
color[:3],
|
|
114
|
+
chroma_key,
|
|
115
|
+
distance_threshold,
|
|
116
|
+
)
|
|
117
|
+
for color, alpha_value, nearby_transparency in zip(
|
|
118
|
+
rgba.get_flattened_data(),
|
|
119
|
+
alpha.get_flattened_data(),
|
|
120
|
+
expanded.get_flattened_data(),
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def main() -> None:
|
|
126
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
127
|
+
parser.add_argument("atlas")
|
|
128
|
+
parser.add_argument("--json-out")
|
|
129
|
+
parser.add_argument("--min-used-pixels", type=int, default=50)
|
|
130
|
+
parser.add_argument("--near-opaque-threshold", type=float, default=0.95)
|
|
131
|
+
parser.add_argument("--chroma-key", default="#00FF00")
|
|
132
|
+
parser.add_argument("--chroma-leak-threshold", type=float, default=36.0)
|
|
133
|
+
parser.add_argument("--max-chroma-leak-pixels", type=int, default=400)
|
|
134
|
+
parser.add_argument("--chroma-fringe-threshold", type=float, default=96.0)
|
|
135
|
+
parser.add_argument("--chroma-fringe-edge-radius", type=int, default=2)
|
|
136
|
+
parser.add_argument("--chroma-fringe-alpha-minimum", type=int, default=16)
|
|
137
|
+
parser.add_argument("--max-chroma-fringe-pixels", type=int, default=0)
|
|
138
|
+
parser.add_argument("--allow-opaque", action="store_true")
|
|
139
|
+
parser.add_argument("--allow-near-opaque-used-cells", action="store_true")
|
|
140
|
+
parser.add_argument("--allow-chroma-leak", action="store_true")
|
|
141
|
+
parser.add_argument("--allow-chroma-fringe", action="store_true")
|
|
142
|
+
parser.add_argument("--require-v2", action="store_true")
|
|
143
|
+
args = parser.parse_args()
|
|
144
|
+
|
|
145
|
+
atlas_path = Path(args.atlas).expanduser().resolve()
|
|
146
|
+
chroma_key = parse_hex_color(args.chroma_key)
|
|
147
|
+
errors: list[str] = []
|
|
148
|
+
warnings: list[str] = []
|
|
149
|
+
near_opaque_used_cells: dict[str, list[int]] = defaultdict(list)
|
|
150
|
+
cells: list[dict[str, object]] = []
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
with Image.open(atlas_path) as opened:
|
|
154
|
+
source_mode = opened.mode
|
|
155
|
+
source_format = opened.format
|
|
156
|
+
image = opened.convert("RGBA")
|
|
157
|
+
except Exception as exc: # noqa: BLE001
|
|
158
|
+
result = {"ok": False, "errors": [f"could not open atlas: {exc}"], "warnings": []}
|
|
159
|
+
print(json.dumps(result, indent=2))
|
|
160
|
+
raise SystemExit(1) from exc
|
|
161
|
+
|
|
162
|
+
expected_heights = (
|
|
163
|
+
{EXTENDED_ATLAS_HEIGHT}
|
|
164
|
+
if args.require_v2
|
|
165
|
+
else {
|
|
166
|
+
ATLAS_HEIGHT,
|
|
167
|
+
EXTENDED_ATLAS_HEIGHT,
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
if image.width != ATLAS_WIDTH or image.height not in expected_heights:
|
|
171
|
+
expected = (
|
|
172
|
+
f"{ATLAS_WIDTH}x{EXTENDED_ATLAS_HEIGHT} for a v2 pet"
|
|
173
|
+
if args.require_v2
|
|
174
|
+
else f"{ATLAS_WIDTH}x{ATLAS_HEIGHT} or {ATLAS_WIDTH}x{EXTENDED_ATLAS_HEIGHT}"
|
|
175
|
+
)
|
|
176
|
+
errors.append(f"expected {expected}, got {image.width}x{image.height}")
|
|
177
|
+
|
|
178
|
+
if source_format not in {"PNG", "WEBP"}:
|
|
179
|
+
errors.append(f"expected PNG or WebP, got {source_format}")
|
|
180
|
+
|
|
181
|
+
if "A" not in source_mode and not args.allow_opaque:
|
|
182
|
+
errors.append("atlas does not have an alpha channel")
|
|
183
|
+
|
|
184
|
+
row_count = image.height // CELL_HEIGHT
|
|
185
|
+
is_extended_atlas = image.height == EXTENDED_ATLAS_HEIGHT
|
|
186
|
+
supported_row_count = row_count in {ROWS, EXTENDED_ROWS}
|
|
187
|
+
for row_index in range(row_count if supported_row_count else 0):
|
|
188
|
+
state, frame_count = ROW_BY_INDEX[row_index]
|
|
189
|
+
for column_index in range(COLUMNS):
|
|
190
|
+
left = column_index * CELL_WIDTH
|
|
191
|
+
top = row_index * CELL_HEIGHT
|
|
192
|
+
cell = image.crop((left, top, left + CELL_WIDTH, top + CELL_HEIGHT))
|
|
193
|
+
nontransparent = alpha_nonzero_count(cell)
|
|
194
|
+
used = column_index < frame_count or (
|
|
195
|
+
is_extended_atlas and (row_index, column_index) == EXTENDED_NEUTRAL_LOOK_FRAME
|
|
196
|
+
)
|
|
197
|
+
cell_info = {
|
|
198
|
+
"state": state,
|
|
199
|
+
"row": row_index,
|
|
200
|
+
"column": column_index,
|
|
201
|
+
"used": used,
|
|
202
|
+
"nontransparent_pixels": nontransparent,
|
|
203
|
+
}
|
|
204
|
+
chroma_leak_pixels = opaque_chroma_key_count(
|
|
205
|
+
cell,
|
|
206
|
+
chroma_key,
|
|
207
|
+
args.chroma_leak_threshold,
|
|
208
|
+
)
|
|
209
|
+
cell_info["opaque_chroma_key_pixels"] = chroma_leak_pixels
|
|
210
|
+
chroma_fringe_pixels = chroma_fringe_count(
|
|
211
|
+
cell,
|
|
212
|
+
chroma_key=chroma_key,
|
|
213
|
+
distance_threshold=args.chroma_fringe_threshold,
|
|
214
|
+
edge_radius=args.chroma_fringe_edge_radius,
|
|
215
|
+
alpha_minimum=args.chroma_fringe_alpha_minimum,
|
|
216
|
+
)
|
|
217
|
+
cell_info["chroma_fringe_pixels"] = chroma_fringe_pixels
|
|
218
|
+
cells.append(cell_info)
|
|
219
|
+
if used and nontransparent < args.min_used_pixels:
|
|
220
|
+
errors.append(
|
|
221
|
+
f"{state} row {row_index} column {column_index} is empty or too sparse ({nontransparent} pixels)"
|
|
222
|
+
)
|
|
223
|
+
if used and chroma_leak_pixels > args.max_chroma_leak_pixels:
|
|
224
|
+
message = (
|
|
225
|
+
f"{state} row {row_index} column {column_index} has {chroma_leak_pixels} "
|
|
226
|
+
f"opaque pixels near chroma key {args.chroma_key}; this usually means "
|
|
227
|
+
"the sprite background was not removed"
|
|
228
|
+
)
|
|
229
|
+
if args.allow_chroma_leak:
|
|
230
|
+
warnings.append(message)
|
|
231
|
+
else:
|
|
232
|
+
errors.append(message)
|
|
233
|
+
if used and chroma_fringe_pixels > args.max_chroma_fringe_pixels:
|
|
234
|
+
message = (
|
|
235
|
+
f"{state} row {row_index} column {column_index} has {chroma_fringe_pixels} "
|
|
236
|
+
f"visible edge pixels contaminated by chroma key {args.chroma_key}"
|
|
237
|
+
)
|
|
238
|
+
if args.allow_chroma_fringe:
|
|
239
|
+
warnings.append(message)
|
|
240
|
+
else:
|
|
241
|
+
errors.append(message)
|
|
242
|
+
if used and nontransparent > CELL_WIDTH * CELL_HEIGHT * args.near_opaque_threshold:
|
|
243
|
+
near_opaque_used_cells[f"{state} row {row_index}"].append(column_index)
|
|
244
|
+
if not used and nontransparent != 0:
|
|
245
|
+
errors.append(
|
|
246
|
+
f"{state} row {row_index} unused column {column_index} is not transparent ({nontransparent} pixels)"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
for row_label, columns in near_opaque_used_cells.items():
|
|
250
|
+
message = (
|
|
251
|
+
f"{row_label} has {len(columns)} nearly opaque used cells; "
|
|
252
|
+
"this usually means the sprite has a non-transparent background"
|
|
253
|
+
)
|
|
254
|
+
if args.allow_near_opaque_used_cells:
|
|
255
|
+
warnings.append(message)
|
|
256
|
+
else:
|
|
257
|
+
errors.append(message)
|
|
258
|
+
|
|
259
|
+
alpha_count = alpha_nonzero_count(image)
|
|
260
|
+
if alpha_count == ATLAS_WIDTH * ATLAS_HEIGHT:
|
|
261
|
+
message = "atlas is fully opaque; custom pets require a transparent sprite background"
|
|
262
|
+
if args.allow_opaque:
|
|
263
|
+
warnings.append(message)
|
|
264
|
+
else:
|
|
265
|
+
errors.append(message)
|
|
266
|
+
|
|
267
|
+
transparent_rgb_residue = transparent_rgb_residue_count(image)
|
|
268
|
+
if transparent_rgb_residue:
|
|
269
|
+
errors.append(
|
|
270
|
+
f"atlas has {transparent_rgb_residue} fully transparent pixels with non-zero RGB residue"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
result = {
|
|
274
|
+
"ok": not errors,
|
|
275
|
+
"file": str(atlas_path),
|
|
276
|
+
"format": source_format,
|
|
277
|
+
"mode": source_mode,
|
|
278
|
+
"columns": COLUMNS,
|
|
279
|
+
"rows": row_count,
|
|
280
|
+
"sprite_version_number": 2 if is_extended_atlas else 1,
|
|
281
|
+
"width": image.width,
|
|
282
|
+
"height": image.height,
|
|
283
|
+
"transparent_rgb_residue_pixels": transparent_rgb_residue,
|
|
284
|
+
"errors": errors,
|
|
285
|
+
"warnings": warnings,
|
|
286
|
+
"cells": cells,
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if args.json_out:
|
|
290
|
+
Path(args.json_out).expanduser().resolve().write_text(
|
|
291
|
+
json.dumps(result, indent=2) + "\n", encoding="utf-8"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
print(json.dumps({k: v for k, v in result.items() if k != "cells"}, indent=2))
|
|
295
|
+
raise SystemExit(0 if result["ok"] else 1)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
if __name__ == "__main__":
|
|
299
|
+
main()
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Compare blind A/B direction classifications with the hidden answer key."""
|
|
3
|
+
|
|
4
|
+
# Modified from the Apache-2.0 Hatch Pet workflow for the Pi Pet authoring package.
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
ALLOWED_DIRECTIONS = {"screen-left", "screen-right", "up", "down", "ambiguous"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_json(path: str) -> dict[str, object]:
|
|
16
|
+
return json.loads(Path(path).expanduser().resolve().read_text(encoding="utf-8"))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main() -> None:
|
|
20
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
21
|
+
parser.add_argument("--answer-key", required=True)
|
|
22
|
+
parser.add_argument("--verdicts", required=True)
|
|
23
|
+
parser.add_argument("--json-out", required=True)
|
|
24
|
+
args = parser.parse_args()
|
|
25
|
+
|
|
26
|
+
answer_key = load_json(args.answer_key)
|
|
27
|
+
verdicts = load_json(args.verdicts)
|
|
28
|
+
expected_by_pair = {entry["pair"]: entry for entry in answer_key.get("pairs", [])}
|
|
29
|
+
verdict_by_pair = {entry["pair"]: entry for entry in verdicts.get("pairs", [])}
|
|
30
|
+
errors: list[str] = []
|
|
31
|
+
warnings: list[str] = []
|
|
32
|
+
unconfirmed: list[str] = []
|
|
33
|
+
results: list[dict[str, object]] = []
|
|
34
|
+
|
|
35
|
+
for pair_id, expected in expected_by_pair.items():
|
|
36
|
+
verdict = verdict_by_pair.get(pair_id)
|
|
37
|
+
if verdict is None:
|
|
38
|
+
errors.append(f"missing blind verdict for {pair_id}")
|
|
39
|
+
continue
|
|
40
|
+
axis = expected.get("axis", "horizontal")
|
|
41
|
+
gate = expected.get("gate", "hard")
|
|
42
|
+
if gate not in {"hard", "review"}:
|
|
43
|
+
errors.append(f"{pair_id} has invalid gate: {gate!r}")
|
|
44
|
+
gate = "hard"
|
|
45
|
+
result: dict[str, object] = {"pair": pair_id, "axis": axis, "gate": gate}
|
|
46
|
+
for slot in ("A", "B"):
|
|
47
|
+
observed = verdict.get(slot)
|
|
48
|
+
expected_direction = expected[slot].get(
|
|
49
|
+
"expected_direction",
|
|
50
|
+
expected[slot].get("expected_horizontal"),
|
|
51
|
+
)
|
|
52
|
+
if observed not in ALLOWED_DIRECTIONS:
|
|
53
|
+
errors.append(f"{pair_id} {slot} has invalid classification: {observed!r}")
|
|
54
|
+
elif observed == "ambiguous":
|
|
55
|
+
message = f"{pair_id} {slot} {axis} axis is ambiguous"
|
|
56
|
+
warnings.append(message)
|
|
57
|
+
if gate == "hard":
|
|
58
|
+
unconfirmed.append(message)
|
|
59
|
+
elif observed != expected_direction:
|
|
60
|
+
message = f"{pair_id} {slot} classified {observed}; expected {expected_direction}"
|
|
61
|
+
if gate == "hard":
|
|
62
|
+
errors.append(message)
|
|
63
|
+
else:
|
|
64
|
+
warnings.append(message)
|
|
65
|
+
result[slot] = {
|
|
66
|
+
"observed": observed,
|
|
67
|
+
"expected": expected_direction,
|
|
68
|
+
"source_direction": expected[slot]["source_direction"],
|
|
69
|
+
"pass": observed == expected_direction,
|
|
70
|
+
}
|
|
71
|
+
if verdict.get("A") == verdict.get("B") and verdict.get("A") != "ambiguous":
|
|
72
|
+
message = f"{pair_id} A and B were classified as the same {axis} direction"
|
|
73
|
+
if gate == "hard":
|
|
74
|
+
errors.append(message)
|
|
75
|
+
else:
|
|
76
|
+
warnings.append(message)
|
|
77
|
+
results.append(result)
|
|
78
|
+
|
|
79
|
+
extra_pairs = sorted(set(verdict_by_pair) - set(expected_by_pair))
|
|
80
|
+
if extra_pairs:
|
|
81
|
+
errors.append(f"unexpected blind verdict pairs: {', '.join(extra_pairs)}")
|
|
82
|
+
|
|
83
|
+
output = {
|
|
84
|
+
"ok": not errors and not unconfirmed,
|
|
85
|
+
"errors": errors,
|
|
86
|
+
"warnings": warnings,
|
|
87
|
+
"unconfirmed": unconfirmed,
|
|
88
|
+
"reviewRequired": bool(warnings),
|
|
89
|
+
"pairs": results,
|
|
90
|
+
}
|
|
91
|
+
output_path = Path(args.json_out).expanduser().resolve()
|
|
92
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
output_path.write_text(json.dumps(output, indent=2) + "\n", encoding="utf-8")
|
|
94
|
+
print(json.dumps(output, indent=2))
|
|
95
|
+
if errors or unconfirmed:
|
|
96
|
+
raise SystemExit(1)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
if __name__ == "__main__":
|
|
100
|
+
main()
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = ">=3.11"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "hatch-pi-pet-tools"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
source = { virtual = "." }
|
|
9
|
+
dependencies = [
|
|
10
|
+
{ name = "pillow" },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[package.metadata]
|
|
14
|
+
requires-dist = [{ name = "pillow", specifier = ">=12,<14" }]
|
|
15
|
+
|
|
16
|
+
[[package]]
|
|
17
|
+
name = "pillow"
|
|
18
|
+
version = "12.3.0"
|
|
19
|
+
source = { registry = "https://pypi.org/simple" }
|
|
20
|
+
sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
|
|
21
|
+
wheels = [
|
|
22
|
+
{ url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" },
|
|
23
|
+
{ url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" },
|
|
24
|
+
{ url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" },
|
|
25
|
+
{ url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" },
|
|
26
|
+
{ url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" },
|
|
27
|
+
{ url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" },
|
|
28
|
+
{ url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" },
|
|
29
|
+
{ url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" },
|
|
30
|
+
{ url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" },
|
|
31
|
+
{ url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" },
|
|
32
|
+
{ url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" },
|
|
33
|
+
{ url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" },
|
|
34
|
+
{ url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" },
|
|
35
|
+
{ url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" },
|
|
36
|
+
{ url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" },
|
|
37
|
+
{ url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" },
|
|
38
|
+
{ url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" },
|
|
39
|
+
{ url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" },
|
|
40
|
+
{ url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" },
|
|
41
|
+
{ url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" },
|
|
42
|
+
{ url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" },
|
|
43
|
+
{ url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" },
|
|
44
|
+
{ url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" },
|
|
45
|
+
{ url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" },
|
|
46
|
+
{ url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" },
|
|
47
|
+
{ url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" },
|
|
48
|
+
{ url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" },
|
|
49
|
+
{ url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" },
|
|
50
|
+
{ url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" },
|
|
51
|
+
{ url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" },
|
|
52
|
+
{ url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" },
|
|
53
|
+
{ url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" },
|
|
54
|
+
{ url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" },
|
|
55
|
+
{ url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" },
|
|
56
|
+
{ url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" },
|
|
57
|
+
{ url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" },
|
|
58
|
+
{ url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" },
|
|
59
|
+
{ url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" },
|
|
60
|
+
{ url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" },
|
|
61
|
+
{ url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" },
|
|
62
|
+
{ url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" },
|
|
63
|
+
{ url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" },
|
|
64
|
+
{ url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" },
|
|
65
|
+
{ url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" },
|
|
66
|
+
{ url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" },
|
|
67
|
+
{ url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" },
|
|
68
|
+
{ url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" },
|
|
69
|
+
{ url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" },
|
|
70
|
+
{ url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" },
|
|
71
|
+
{ url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" },
|
|
72
|
+
{ url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" },
|
|
73
|
+
{ url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" },
|
|
74
|
+
{ url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" },
|
|
75
|
+
{ url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" },
|
|
76
|
+
{ url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" },
|
|
77
|
+
{ url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" },
|
|
78
|
+
{ url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" },
|
|
79
|
+
{ url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" },
|
|
80
|
+
{ url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" },
|
|
81
|
+
{ url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" },
|
|
82
|
+
{ url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" },
|
|
83
|
+
{ url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" },
|
|
84
|
+
{ url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" },
|
|
85
|
+
{ url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" },
|
|
86
|
+
{ url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" },
|
|
87
|
+
{ url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" },
|
|
88
|
+
{ url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" },
|
|
89
|
+
{ url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" },
|
|
90
|
+
{ url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" },
|
|
91
|
+
{ url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" },
|
|
92
|
+
{ url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" },
|
|
93
|
+
{ url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" },
|
|
94
|
+
{ url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" },
|
|
95
|
+
{ url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" },
|
|
96
|
+
{ url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" },
|
|
97
|
+
{ url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" },
|
|
98
|
+
{ url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" },
|
|
99
|
+
]
|