qlogicagent 2.19.8 → 2.19.10

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.
Files changed (70) hide show
  1. package/dist/agent-contract.js +1 -1
  2. package/dist/agent.js +26 -26
  3. package/dist/cli.js +1 -1
  4. package/dist/default-project-knowledge/INSTRUCTIONS.md +7 -7
  5. package/dist/default-project-knowledge/rules/project-workflow.md +6 -6
  6. package/dist/host-contract.js +1 -1
  7. package/dist/index.js +443 -465
  8. package/dist/protocol.js +1 -1
  9. package/dist/skills/builtin/desktop-screenshot/SKILL.md +62 -62
  10. package/dist/skills/builtin/office-doc-reading/SKILL.md +19 -19
  11. package/dist/types/cli/agent-runtime-bootstrap.d.ts +0 -1
  12. package/dist/types/cli/community-resource-installer.d.ts +10 -1
  13. package/dist/types/cli/handlers/community-handler.d.ts +0 -8
  14. package/dist/types/cli/handlers/files-handler.d.ts +2 -30
  15. package/dist/types/cli/industrial-runtime-store.d.ts +1 -14
  16. package/dist/types/cli/permission-approval-bridge.d.ts +0 -1
  17. package/dist/types/cli/permission-bootstrap.d.ts +0 -1
  18. package/dist/types/cli/stdio-acp-request-host.d.ts +1 -2
  19. package/dist/types/cli/stdio-agent-session-bootstrap.d.ts +0 -1
  20. package/dist/types/cli/stdio-runtime-bootstrap.d.ts +0 -2
  21. package/dist/types/cli/stdio-runtime-services.d.ts +1 -13
  22. package/dist/types/cli/stdio-server.d.ts +0 -5
  23. package/dist/types/cli/turn-media-setup.d.ts +15 -0
  24. package/dist/types/host-contract/index.d.ts +2 -18
  25. package/dist/types/protocol/agent-contract.d.ts +7 -7
  26. package/dist/types/protocol/wire/agent-methods.d.ts +0 -115
  27. package/dist/types/protocol/wire/index.d.ts +1 -2
  28. package/dist/types/protocol/wire/notification-payloads.d.ts +0 -87
  29. package/package.json +3 -21
  30. package/dist/pet-contracts.js +0 -1
  31. package/dist/pet-host.js +0 -19
  32. package/dist/types/cli/handlers/pet-confirm-handler.d.ts +0 -7
  33. package/dist/types/cli/industrial-runtime-archive.d.ts +0 -14
  34. package/dist/types/cli/pet-confirm-coordinator.d.ts +0 -20
  35. package/dist/types/cli/pet-runtime.d.ts +0 -61
  36. package/dist/types/cli/runtime-dependency-catalog.d.ts +0 -112
  37. package/dist/types/cli/windows-authenticode.d.ts +0 -18
  38. package/dist/types/pet-contracts.d.ts +0 -1
  39. package/dist/types/pet-host.d.ts +0 -9
  40. package/dist/types/protocol/wire/pet-contracts.d.ts +0 -76
  41. package/dist/types/runtime/pet/daily-journey-summarizer.d.ts +0 -25
  42. package/dist/types/runtime/pet/hatch-pet-runner.d.ts +0 -18
  43. package/dist/types/runtime/pet/index.d.ts +0 -19
  44. package/dist/types/runtime/pet/journey-catchup.d.ts +0 -14
  45. package/dist/types/runtime/pet/journey-day-aggregator.d.ts +0 -27
  46. package/dist/types/runtime/pet/journey-narrative-prompt.d.ts +0 -8
  47. package/dist/types/runtime/pet/pet-community-assets.d.ts +0 -21
  48. package/dist/types/runtime/pet/pet-file-loader.d.ts +0 -5
  49. package/dist/types/runtime/pet/pet-growth-engine.d.ts +0 -2
  50. package/dist/types/runtime/pet/pet-profile-service.d.ts +0 -111
  51. package/dist/types/runtime/pet/pet-reaction-engine.d.ts +0 -11
  52. package/dist/types/runtime/pet/pet-soul-service.d.ts +0 -21
  53. package/dist/types/runtime/pet/pet-types.d.ts +0 -1
  54. package/dist/types/runtime/pet/petdex-asset.d.ts +0 -60
  55. package/dist/types/runtime/pet/petdex-forge-service.d.ts +0 -21
  56. package/dist/types/runtime/pet/petdex-vision-qa.d.ts +0 -12
  57. package/dist/types/transport/host-pet-client.d.ts +0 -20
  58. package/dist/vendor/hatch-pet/LICENSE.txt +0 -201
  59. package/dist/vendor/hatch-pet/NOTICE.md +0 -25
  60. package/dist/vendor/hatch-pet/references/animation-rows.md +0 -29
  61. package/dist/vendor/hatch-pet/references/codex-pet-contract.md +0 -35
  62. package/dist/vendor/hatch-pet/references/qa-rubric.md +0 -66
  63. package/dist/vendor/hatch-pet/scripts/compose_atlas.py +0 -169
  64. package/dist/vendor/hatch-pet/scripts/derive_running_left_from_running_right.py +0 -150
  65. package/dist/vendor/hatch-pet/scripts/extract_strip_frames.py +0 -408
  66. package/dist/vendor/hatch-pet/scripts/inspect_frames.py +0 -256
  67. package/dist/vendor/hatch-pet/scripts/make_contact_sheet.py +0 -96
  68. package/dist/vendor/hatch-pet/scripts/prepare_pet_run.py +0 -834
  69. package/dist/vendor/hatch-pet/scripts/render_animation_previews.py +0 -78
  70. package/dist/vendor/hatch-pet/scripts/validate_atlas.py +0 -157
@@ -1,150 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Conditionally derive running-left by mirroring the approved running-right strip."""
3
-
4
- from __future__ import annotations
5
-
6
- import argparse
7
- import json
8
- from datetime import datetime, timezone
9
- from pathlib import Path
10
-
11
- from PIL import Image, ImageOps
12
-
13
- RUNNING_FRAME_COUNT = 8
14
-
15
-
16
- def load_manifest(run_dir: Path) -> dict[str, object]:
17
- path = run_dir / "imagegen-jobs.json"
18
- if not path.exists():
19
- raise SystemExit(f"job manifest not found: {path}")
20
- return json.loads(path.read_text(encoding="utf-8"))
21
-
22
-
23
- def job_list(manifest: dict[str, object]) -> list[dict[str, object]]:
24
- jobs = manifest.get("jobs")
25
- if not isinstance(jobs, list):
26
- raise SystemExit("invalid imagegen-jobs.json: jobs must be a list")
27
- return [job for job in jobs if isinstance(job, dict)]
28
-
29
-
30
- def find_job(manifest: dict[str, object], job_id: str) -> dict[str, object]:
31
- for job in job_list(manifest):
32
- if job.get("id") == job_id:
33
- return job
34
- raise SystemExit(f"unknown job id: {job_id}")
35
-
36
-
37
- def image_metadata(path: Path) -> dict[str, object]:
38
- with Image.open(path) as image:
39
- image.verify()
40
- with Image.open(path) as image:
41
- return {
42
- "width": image.width,
43
- "height": image.height,
44
- "mode": image.mode,
45
- "format": image.format,
46
- }
47
-
48
-
49
- def manifest_relative(path: Path, run_dir: Path) -> str:
50
- return str(path.resolve().relative_to(run_dir.resolve()))
51
-
52
-
53
- def mirror_strip_preserving_frame_order(
54
- source: Image.Image,
55
- frame_count: int = RUNNING_FRAME_COUNT,
56
- ) -> Image.Image:
57
- rgba = source.convert("RGBA")
58
- mirrored = Image.new("RGBA", rgba.size, (0, 0, 0, 0))
59
- slot_width = rgba.width / frame_count
60
- for index in range(frame_count):
61
- left = round(index * slot_width)
62
- right = round((index + 1) * slot_width)
63
- mirrored.alpha_composite(
64
- ImageOps.mirror(rgba.crop((left, 0, right, rgba.height))),
65
- (left, 0),
66
- )
67
- return mirrored
68
-
69
-
70
- def main() -> None:
71
- parser = argparse.ArgumentParser(description=__doc__)
72
- parser.add_argument("--run-dir", required=True)
73
- parser.add_argument(
74
- "--confirm-appropriate-mirror",
75
- action="store_true",
76
- help="Required after visually confirming the rightward strip can be mirrored without identity/prop issues.",
77
- )
78
- parser.add_argument(
79
- "--decision-note",
80
- required=True,
81
- help="Short note explaining why mirroring is acceptable for this pet.",
82
- )
83
- parser.add_argument("--force", action="store_true")
84
- args = parser.parse_args()
85
-
86
- if not args.confirm_appropriate_mirror:
87
- raise SystemExit("refusing to mirror without --confirm-appropriate-mirror")
88
- if not args.decision_note.strip():
89
- raise SystemExit("--decision-note must explain why mirroring is appropriate")
90
-
91
- run_dir = Path(args.run_dir).expanduser().resolve()
92
- manifest_path = run_dir / "imagegen-jobs.json"
93
- manifest = load_manifest(run_dir)
94
- right_job = find_job(manifest, "running-right")
95
- left_job = find_job(manifest, "running-left")
96
-
97
- if right_job.get("status") != "complete":
98
- raise SystemExit("running-right must be complete before deriving running-left")
99
- mirror_policy = left_job.get("mirror_policy")
100
- if not isinstance(mirror_policy, dict) or mirror_policy.get("may_derive_from") != "running-right":
101
- raise SystemExit("running-left is not configured for conditional mirroring")
102
-
103
- source = run_dir / "decoded" / "running-right.png"
104
- output = run_dir / "decoded" / "running-left.png"
105
- if not source.is_file():
106
- raise SystemExit(f"running-right decoded strip not found: {source}")
107
- if output.exists() and not args.force:
108
- raise SystemExit(f"{output} already exists; pass --force to replace it")
109
-
110
- output.parent.mkdir(parents=True, exist_ok=True)
111
- with Image.open(source) as image:
112
- mirrored = mirror_strip_preserving_frame_order(image)
113
- mirrored.save(output)
114
-
115
- left_job["status"] = "complete"
116
- left_job["source_path"] = manifest_relative(source, run_dir)
117
- left_job["derived_from"] = "running-right"
118
- left_job["completed_at"] = datetime.now(timezone.utc).isoformat()
119
- left_job["metadata"] = image_metadata(output)
120
- left_job["mirror_decision"] = {
121
- "approved": True,
122
- "approved_at": left_job["completed_at"],
123
- "note": args.decision_note.strip(),
124
- "transform": "framewise-horizontal-mirror-preserving-order",
125
- }
126
- for key in [
127
- "last_error",
128
- "repair_reason",
129
- "queued_at",
130
- ]:
131
- left_job.pop(key, None)
132
-
133
- manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
134
- print(
135
- json.dumps(
136
- {
137
- "ok": True,
138
- "job_id": "running-left",
139
- "derived_from": "running-right",
140
- "output": str(output),
141
- "decision_note": args.decision_note.strip(),
142
- "transform": "framewise-horizontal-mirror-preserving-order",
143
- },
144
- indent=2,
145
- )
146
- )
147
-
148
-
149
- if __name__ == "__main__":
150
- main()
@@ -1,408 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Extract generated horizontal row strips into 192x208 sprite frames."""
3
-
4
- from __future__ import annotations
5
-
6
- import argparse
7
- import json
8
- import math
9
- import re
10
- from pathlib import Path
11
-
12
- from PIL import Image
13
-
14
- CELL_WIDTH = 192
15
- CELL_HEIGHT = 208
16
- ROW_FRAME_COUNTS = {
17
- "idle": 6,
18
- "running-right": 8,
19
- "running-left": 8,
20
- "waving": 4,
21
- "jumping": 5,
22
- "failed": 8,
23
- "waiting": 6,
24
- "running": 6,
25
- "review": 6,
26
- }
27
-
28
-
29
- def parse_states(raw: str) -> list[str]:
30
- if raw.strip().lower() == "all":
31
- return list(ROW_FRAME_COUNTS)
32
- states = [item.strip() for item in raw.split(",") if item.strip()]
33
- unknown = sorted(set(states) - set(ROW_FRAME_COUNTS))
34
- if unknown:
35
- raise SystemExit(f"unknown state(s): {', '.join(unknown)}")
36
- return states
37
-
38
-
39
- def parse_hex_color(value: str) -> tuple[int, int, int]:
40
- if not re.fullmatch(r"#[0-9a-fA-F]{6}", value):
41
- raise SystemExit(f"invalid chroma key color: {value}; expected #RRGGBB")
42
- return tuple(int(value[index : index + 2], 16) for index in (1, 3, 5))
43
-
44
-
45
- def load_chroma_key(decoded_dir: Path, override: str | None) -> tuple[int, int, int]:
46
- if override:
47
- return parse_hex_color(override)
48
- request_path = decoded_dir.parent / "pet_request.json"
49
- if request_path.is_file():
50
- request = json.loads(request_path.read_text(encoding="utf-8"))
51
- chroma_key = request.get("chroma_key")
52
- if isinstance(chroma_key, dict) and isinstance(chroma_key.get("hex"), str):
53
- return parse_hex_color(chroma_key["hex"])
54
- return parse_hex_color("#00FF00")
55
-
56
-
57
- def color_distance(
58
- red: int,
59
- green: int,
60
- blue: int,
61
- key: tuple[int, int, int],
62
- ) -> float:
63
- return math.sqrt((red - key[0]) ** 2 + (green - key[1]) ** 2 + (blue - key[2]) ** 2)
64
-
65
-
66
- def remove_chroma_background(
67
- image: Image.Image,
68
- chroma_key: tuple[int, int, int],
69
- threshold: float,
70
- ) -> Image.Image:
71
- rgba = image.convert("RGBA")
72
- pixels = rgba.load()
73
- for y in range(rgba.height):
74
- for x in range(rgba.width):
75
- red, green, blue, alpha = pixels[x, y]
76
- if color_distance(red, green, blue, chroma_key) <= threshold:
77
- pixels[x, y] = (0, 0, 0, 0)
78
- return rgba
79
-
80
-
81
- def fit_to_cell(image: Image.Image) -> Image.Image:
82
- bbox = image.getbbox()
83
- target = Image.new("RGBA", (CELL_WIDTH, CELL_HEIGHT), (0, 0, 0, 0))
84
- if bbox is None:
85
- return target
86
-
87
- sprite = image.crop(bbox)
88
- max_width = CELL_WIDTH - 10
89
- max_height = CELL_HEIGHT - 10
90
- scale = min(max_width / sprite.width, max_height / sprite.height, 1.0)
91
- if scale != 1.0:
92
- sprite = sprite.resize(
93
- (max(1, round(sprite.width * scale)), max(1, round(sprite.height * scale))),
94
- Image.Resampling.LANCZOS,
95
- )
96
- left = (CELL_WIDTH - sprite.width) // 2
97
- top = (CELL_HEIGHT - sprite.height) // 2
98
- target.alpha_composite(sprite, (left, top))
99
- return target
100
-
101
-
102
- def fit_viewport_to_cell(image: Image.Image) -> Image.Image:
103
- target = Image.new("RGBA", (CELL_WIDTH, CELL_HEIGHT), (0, 0, 0, 0))
104
- if image.getbbox() is None:
105
- return target
106
-
107
- viewport = image.copy()
108
- max_width = CELL_WIDTH - 10
109
- max_height = CELL_HEIGHT - 10
110
- scale = min(max_width / viewport.width, max_height / viewport.height, 1.0)
111
- if scale != 1.0:
112
- viewport = viewport.resize(
113
- (max(1, round(viewport.width * scale)), max(1, round(viewport.height * scale))),
114
- Image.Resampling.LANCZOS,
115
- )
116
- left = (CELL_WIDTH - viewport.width) // 2
117
- top = (CELL_HEIGHT - viewport.height) // 2
118
- target.alpha_composite(viewport, (left, top))
119
- return target
120
-
121
-
122
- def connected_components(image: Image.Image) -> list[dict[str, object]]:
123
- alpha = image.getchannel("A")
124
- width, height = image.size
125
- data = alpha.tobytes()
126
- visited = bytearray(width * height)
127
- components: list[dict[str, object]] = []
128
-
129
- for start, alpha_value in enumerate(data):
130
- if alpha_value <= 16 or visited[start]:
131
- continue
132
-
133
- stack = [start]
134
- visited[start] = 1
135
- pixels: list[int] = []
136
- min_x = width
137
- min_y = height
138
- max_x = 0
139
- max_y = 0
140
-
141
- while stack:
142
- current = stack.pop()
143
- pixels.append(current)
144
- x = current % width
145
- y = current // width
146
- min_x = min(min_x, x)
147
- min_y = min(min_y, y)
148
- max_x = max(max_x, x)
149
- max_y = max(max_y, y)
150
-
151
- if x > 0:
152
- neighbor = current - 1
153
- if not visited[neighbor] and data[neighbor] > 16:
154
- visited[neighbor] = 1
155
- stack.append(neighbor)
156
- if x + 1 < width:
157
- neighbor = current + 1
158
- if not visited[neighbor] and data[neighbor] > 16:
159
- visited[neighbor] = 1
160
- stack.append(neighbor)
161
- if y > 0:
162
- neighbor = current - width
163
- if not visited[neighbor] and data[neighbor] > 16:
164
- visited[neighbor] = 1
165
- stack.append(neighbor)
166
- if y + 1 < height:
167
- neighbor = current + width
168
- if not visited[neighbor] and data[neighbor] > 16:
169
- visited[neighbor] = 1
170
- stack.append(neighbor)
171
-
172
- components.append(
173
- {
174
- "pixels": pixels,
175
- "area": len(pixels),
176
- "bbox": (min_x, min_y, max_x + 1, max_y + 1),
177
- "center_x": (min_x + max_x + 1) / 2,
178
- }
179
- )
180
-
181
- return components
182
-
183
-
184
- def component_group_image(
185
- source: Image.Image,
186
- components: list[dict[str, object]],
187
- padding: int = 4,
188
- ) -> Image.Image:
189
- width, height = source.size
190
- min_x = max(0, min(component["bbox"][0] for component in components) - padding)
191
- min_y = max(0, min(component["bbox"][1] for component in components) - padding)
192
- max_x = min(width, max(component["bbox"][2] for component in components) + padding)
193
- max_y = min(height, max(component["bbox"][3] for component in components) + padding)
194
-
195
- output = Image.new("RGBA", (max_x - min_x, max_y - min_y), (0, 0, 0, 0))
196
- source_pixels = source.load()
197
- output_pixels = output.load()
198
- for component in components:
199
- for pixel_index in component["pixels"]:
200
- x = pixel_index % width
201
- y = pixel_index // width
202
- output_pixels[x - min_x, y - min_y] = source_pixels[x, y]
203
- return output
204
-
205
-
206
- def component_frame_groups(
207
- strip: Image.Image,
208
- frame_count: int,
209
- ) -> list[list[dict[str, object]]] | None:
210
- components = connected_components(strip)
211
- if not components:
212
- return None
213
-
214
- largest_area = max(component["area"] for component in components)
215
- seed_threshold = max(120, largest_area * 0.20)
216
- seeds = [component for component in components if component["area"] >= seed_threshold]
217
- if len(seeds) < frame_count:
218
- seeds = sorted(components, key=lambda component: component["area"], reverse=True)[
219
- :frame_count
220
- ]
221
- if len(seeds) < frame_count:
222
- return None
223
-
224
- seeds = sorted(
225
- sorted(seeds, key=lambda component: component["area"], reverse=True)[:frame_count],
226
- key=lambda component: component["center_x"],
227
- )
228
- seed_ids = {id(seed) for seed in seeds}
229
- groups: list[list[dict[str, object]]] = [[seed] for seed in seeds]
230
- noise_threshold = max(12, largest_area * 0.002)
231
-
232
- for component in components:
233
- if id(component) in seed_ids or component["area"] < noise_threshold:
234
- continue
235
- nearest_index = min(
236
- range(len(seeds)),
237
- key=lambda index: abs(seeds[index]["center_x"] - component["center_x"]),
238
- )
239
- groups[nearest_index].append(component)
240
-
241
- return groups
242
-
243
-
244
- def extract_component_frames(strip: Image.Image, frame_count: int) -> list[Image.Image] | None:
245
- groups = component_frame_groups(strip, frame_count)
246
- if groups is None:
247
- return None
248
- return [fit_to_cell(component_group_image(strip, group)) for group in groups]
249
-
250
-
251
- def component_bounds(components: list[dict[str, object]]) -> tuple[int, int, int, int]:
252
- return (
253
- min(component["bbox"][0] for component in components),
254
- min(component["bbox"][1] for component in components),
255
- max(component["bbox"][2] for component in components),
256
- max(component["bbox"][3] for component in components),
257
- )
258
-
259
-
260
- def extract_slot_frames(strip: Image.Image, frame_count: int) -> list[Image.Image]:
261
- slot_width = strip.width / frame_count
262
- frames = []
263
- for index in range(frame_count):
264
- left = round(index * slot_width)
265
- right = round((index + 1) * slot_width)
266
- crop = strip.crop((left, 0, right, strip.height))
267
- frames.append(fit_to_cell(crop))
268
- return frames
269
-
270
-
271
- def extract_stable_slot_frames(strip: Image.Image, frame_count: int) -> list[Image.Image]:
272
- groups = component_frame_groups(strip, frame_count)
273
- padding = 4
274
- if groups is not None:
275
- bboxes = [component_bounds(group) for group in groups]
276
- shared_top = max(0, min(bbox[1] for bbox in bboxes) - padding)
277
- shared_bottom = min(strip.height, max(bbox[3] for bbox in bboxes) + padding)
278
- viewport_width = max(bbox[2] - bbox[0] for bbox in bboxes) + padding * 2
279
- viewport_height = max(1, shared_bottom - shared_top)
280
- frames = []
281
- for group, bbox in zip(groups, bboxes):
282
- grouped = component_group_image(strip, group, padding=padding)
283
- grouped_top = max(0, bbox[1] - padding)
284
- viewport = Image.new(
285
- "RGBA",
286
- (viewport_width, viewport_height),
287
- (0, 0, 0, 0),
288
- )
289
- left = (viewport_width - grouped.width) // 2
290
- viewport.alpha_composite(grouped, (left, grouped_top - shared_top))
291
- frames.append(fit_viewport_to_cell(viewport))
292
- return frames
293
-
294
- bbox = strip.getbbox()
295
- if bbox is None:
296
- return [
297
- Image.new("RGBA", (CELL_WIDTH, CELL_HEIGHT), (0, 0, 0, 0))
298
- for _ in range(frame_count)
299
- ]
300
-
301
- shared_top = max(0, bbox[1] - padding)
302
- shared_bottom = min(strip.height, bbox[3] + padding)
303
- slot_width = strip.width / frame_count
304
- frames = []
305
- for index in range(frame_count):
306
- left = round(index * slot_width)
307
- right = round((index + 1) * slot_width)
308
- crop = strip.crop((left, shared_top, right, shared_bottom))
309
- frames.append(fit_viewport_to_cell(crop))
310
- return frames
311
-
312
-
313
- def extract_state(
314
- strip_path: Path,
315
- state: str,
316
- output_root: Path,
317
- chroma_key: tuple[int, int, int],
318
- threshold: float,
319
- method: str,
320
- ) -> dict[str, object]:
321
- frame_count = ROW_FRAME_COUNTS[state]
322
- with Image.open(strip_path) as opened:
323
- strip = remove_chroma_background(opened, chroma_key, threshold)
324
-
325
- state_dir = output_root / state
326
- state_dir.mkdir(parents=True, exist_ok=True)
327
-
328
- frames = None
329
- used_method = method
330
- if method in {"auto", "components"}:
331
- frames = extract_component_frames(strip, frame_count)
332
- if frames is None and method == "components":
333
- raise SystemExit(f"could not find {frame_count} sprite components in {strip_path}")
334
- if frames is not None:
335
- used_method = "components"
336
-
337
- if frames is None:
338
- if method == "stable-slots":
339
- frames = extract_stable_slot_frames(strip, frame_count)
340
- used_method = "stable-slots"
341
- else:
342
- frames = extract_slot_frames(strip, frame_count)
343
- used_method = "slots"
344
-
345
- outputs = []
346
- for index, frame in enumerate(frames):
347
- output = state_dir / f"{index:02d}.png"
348
- frame.save(output)
349
- outputs.append(str(output))
350
- return {"state": state, "frames": outputs, "method": used_method}
351
-
352
-
353
- def main() -> None:
354
- parser = argparse.ArgumentParser(description=__doc__)
355
- parser.add_argument("--decoded-dir", required=True)
356
- parser.add_argument("--output-dir", required=True)
357
- parser.add_argument("--states", default="all")
358
- parser.add_argument("--chroma-key", help="Override chroma key as #RRGGBB.")
359
- parser.add_argument("--key-threshold", type=float, default=96.0)
360
- parser.add_argument(
361
- "--method",
362
- choices=("auto", "components", "slots", "stable-slots"),
363
- default="auto",
364
- help="Use connected sprite components when possible, raw equal slots, or row-stable slot viewports.",
365
- )
366
- args = parser.parse_args()
367
-
368
- decoded_dir = Path(args.decoded_dir).expanduser().resolve()
369
- output_dir = Path(args.output_dir).expanduser().resolve()
370
- chroma_key = load_chroma_key(decoded_dir, args.chroma_key)
371
- states = parse_states(args.states)
372
- manifest = []
373
- for state in states:
374
- strip_path = decoded_dir / f"{state}.png"
375
- if not strip_path.is_file():
376
- raise SystemExit(f"missing generated strip for {state}: {strip_path}")
377
- manifest.append(
378
- extract_state(
379
- strip_path,
380
- state,
381
- output_dir,
382
- chroma_key,
383
- args.key_threshold,
384
- args.method,
385
- )
386
- )
387
-
388
- (output_dir / "frames-manifest.json").write_text(
389
- json.dumps(
390
- {
391
- "ok": True,
392
- "chroma_key": {
393
- "hex": f"#{chroma_key[0]:02X}{chroma_key[1]:02X}{chroma_key[2]:02X}",
394
- "rgb": list(chroma_key),
395
- "threshold": args.key_threshold,
396
- },
397
- "rows": manifest,
398
- },
399
- indent=2,
400
- )
401
- + "\n",
402
- encoding="utf-8",
403
- )
404
- print(json.dumps({"ok": True, "frames_root": str(output_dir), "states": states}, indent=2))
405
-
406
-
407
- if __name__ == "__main__":
408
- main()