@leejungkiin/awkit 1.7.1 → 1.7.4

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 (245) hide show
  1. package/bin/awk.js +576 -84
  2. package/core/CLAUDE.md +1 -1
  3. package/core/GEMINI.md +148 -167
  4. package/core/GEMINI.md.bak +149 -116
  5. package/core/skill-runtime-manifest.json +3 -0
  6. package/docs/Claude Fable 5.md +3826 -0
  7. package/docs/android_kotlin_system_instruction.md +210 -0
  8. package/docs/brainstorm_ponytail_integration.md +146 -0
  9. package/docs/brainstorm_smart_setup.md +113 -0
  10. package/docs/deep-research-report (1).md +293 -0
  11. package/docs/history/GEMINI.v1.md +135 -0
  12. package/docs/history/brainstorm_antigravity_unified_architecture.v1.md +105 -0
  13. package/docs/history/implementation_plan.v1.md +58 -0
  14. package/package.json +4 -1
  15. package/scripts/artifact-storage.js +130 -0
  16. package/scripts/automation-gate.js +35 -2
  17. package/scripts/claude-plan.js +76 -0
  18. package/scripts/dependency-manager.js +210 -0
  19. package/scripts/exec-rtk.js +11 -5
  20. package/scripts/i18n-helper.js +381 -0
  21. package/scripts/multi-model-pipeline.js +144 -0
  22. package/skill-packs/mobile-ios/pack.json +4 -2
  23. package/skill-packs/reverse-engineering/pack.json +1 -0
  24. package/skills/CATALOG.md +20 -0
  25. package/skills/GEMINI.md +9 -1
  26. package/skills/TRIGGER_INDEX.md +10 -0
  27. package/skills/ai-music/SKILL.md +275 -0
  28. package/skills/android-re-analyzer/SKILL.md +238 -0
  29. package/skills/android-re-analyzer/references/api-extraction-patterns.md +119 -0
  30. package/skills/android-re-analyzer/references/call-flow-analysis.md +176 -0
  31. package/skills/android-re-analyzer/references/fernflower-usage.md +115 -0
  32. package/skills/android-re-analyzer/references/jadx-usage.md +116 -0
  33. package/skills/android-re-analyzer/references/setup-guide.md +221 -0
  34. package/skills/android-re-analyzer/scripts/check-deps.sh +129 -0
  35. package/skills/android-re-analyzer/scripts/decompile.sh +375 -0
  36. package/skills/android-re-analyzer/scripts/find-api-calls.sh +118 -0
  37. package/skills/android-re-analyzer/scripts/install-dep.sh +448 -0
  38. package/skills/animal-island-ui-style/SKILL.md +1450 -0
  39. package/skills/app-store-review-agent/SKILL.md +164 -0
  40. package/skills/app-store-review-agent/references/guidelines/README.md +154 -0
  41. package/skills/app-store-review-agent/references/guidelines/by-app-type/ai_apps.md +37 -0
  42. package/skills/app-store-review-agent/references/guidelines/by-app-type/all_apps.md +50 -0
  43. package/skills/app-store-review-agent/references/guidelines/by-app-type/crypto_finance.md +31 -0
  44. package/skills/app-store-review-agent/references/guidelines/by-app-type/games.md +31 -0
  45. package/skills/app-store-review-agent/references/guidelines/by-app-type/health_fitness.md +31 -0
  46. package/skills/app-store-review-agent/references/guidelines/by-app-type/kids.md +27 -0
  47. package/skills/app-store-review-agent/references/guidelines/by-app-type/macos.md +38 -0
  48. package/skills/app-store-review-agent/references/guidelines/by-app-type/social_ugc.md +32 -0
  49. package/skills/app-store-review-agent/references/guidelines/by-app-type/subscription_iap.md +34 -0
  50. package/skills/app-store-review-agent/references/guidelines/by-app-type/vpn.md +18 -0
  51. package/skills/app-store-review-agent/references/rules/design/minimum_functionality.md +96 -0
  52. package/skills/app-store-review-agent/references/rules/design/sign_in_with_apple.md +54 -0
  53. package/skills/app-store-review-agent/references/rules/entitlements/unused_entitlements.md +83 -0
  54. package/skills/app-store-review-agent/references/rules/metadata/accurate_metadata.md +54 -0
  55. package/skills/app-store-review-agent/references/rules/metadata/apple_trademark.md +99 -0
  56. package/skills/app-store-review-agent/references/rules/metadata/china_storefront.md +72 -0
  57. package/skills/app-store-review-agent/references/rules/metadata/competitor_terms.md +56 -0
  58. package/skills/app-store-review-agent/references/rules/metadata/subscription_metadata.md +81 -0
  59. package/skills/app-store-review-agent/references/rules/privacy/privacy_manifest.md +84 -0
  60. package/skills/app-store-review-agent/references/rules/privacy/unnecessary_data.md +60 -0
  61. package/skills/app-store-review-agent/references/rules/subscription/misleading_pricing.md +63 -0
  62. package/skills/app-store-review-agent/references/rules/subscription/missing_tos_pp.md +54 -0
  63. package/skills/awf-ponytail/SKILL.md +91 -0
  64. package/skills/awf-ponytail-review/SKILL.md +67 -0
  65. package/skills/awf-session-restore/SKILL.md +3 -3
  66. package/skills/brainstorm-agent/SKILL.md +11 -2
  67. package/skills/brainstorm-agent/templates/brief-template.md +8 -0
  68. package/skills/claude-planner/SKILL.md +47 -0
  69. package/skills/code-review/SKILL.md +87 -0
  70. package/skills/expo-game-development/SKILL.md +163 -0
  71. package/skills/flutter/LICENSE.txt +202 -0
  72. package/skills/flutter/SKILL.md +127 -0
  73. package/skills/flutter-project-creater/LICENSE.txt +202 -0
  74. package/skills/flutter-project-creater/SKILL.md +106 -0
  75. package/skills/game-developer/SKILL.md +163 -0
  76. package/skills/game-developer/references/ecs-patterns.md +501 -0
  77. package/skills/game-developer/references/multiplayer-networking.md +475 -0
  78. package/skills/game-developer/references/performance-optimization.md +422 -0
  79. package/skills/game-developer/references/unity-patterns.md +271 -0
  80. package/skills/game-developer/references/unreal-cpp.md +352 -0
  81. package/skills/generate-gui-assets/SKILL.md +305 -0
  82. package/skills/generate-gui-assets/agents/openai.yaml +4 -0
  83. package/skills/generate-gui-assets/references/catalog-schema.md +58 -0
  84. package/skills/generate-gui-assets/references/extraction-techniques.md +21 -0
  85. package/skills/generate-gui-assets/references/prompt-patterns.md +58 -0
  86. package/skills/generate-gui-assets/scripts/__pycache__/clean_chroma_edges.cpython-311.pyc +0 -0
  87. package/skills/generate-gui-assets/scripts/build_gui_contact_sheet.py +51 -0
  88. package/skills/generate-gui-assets/scripts/clean_chroma_edges.py +262 -0
  89. package/skills/generate-gui-assets/scripts/copy_approved_icons.py +64 -0
  90. package/skills/generate-gui-assets/scripts/prepare_gui_asset_run.py +91 -0
  91. package/skills/generate-gui-assets/scripts/suggest_grid_options.py +63 -0
  92. package/skills/generate-gui-assets/scripts/validate_gui_catalog.py +50 -0
  93. package/skills/godot-game-development/SKILL.md +142 -0
  94. package/skills/hatch-pet/LICENSE.txt +201 -0
  95. package/skills/hatch-pet/SKILL.md +420 -0
  96. package/skills/hatch-pet/agents/openai.yaml +4 -0
  97. package/skills/hatch-pet/references/animation-rows.md +29 -0
  98. package/skills/hatch-pet/references/codex-pet-contract.md +35 -0
  99. package/skills/hatch-pet/references/qa-rubric.md +60 -0
  100. package/skills/hatch-pet/scripts/__pycache__/clean_chroma_edges.cpython-311.pyc +0 -0
  101. package/skills/hatch-pet/scripts/clean_chroma_edges.py +262 -0
  102. package/skills/hatch-pet/scripts/compose_atlas.py +150 -0
  103. package/skills/hatch-pet/scripts/derive_running_left_from_running_right.py +143 -0
  104. package/skills/hatch-pet/scripts/extract_strip_frames.py +323 -0
  105. package/skills/hatch-pet/scripts/finalize_pet_run.py +382 -0
  106. package/skills/hatch-pet/scripts/generate_pet_images.py +287 -0
  107. package/skills/hatch-pet/scripts/inspect_frames.py +246 -0
  108. package/skills/hatch-pet/scripts/make_contact_sheet.py +96 -0
  109. package/skills/hatch-pet/scripts/package_custom_pet.py +108 -0
  110. package/skills/hatch-pet/scripts/pet_job_status.py +117 -0
  111. package/skills/hatch-pet/scripts/prepare_pet_run.py +673 -0
  112. package/skills/hatch-pet/scripts/queue_pet_repairs.py +172 -0
  113. package/skills/hatch-pet/scripts/record_imagegen_result.py +250 -0
  114. package/skills/hatch-pet/scripts/render_animation_videos.py +134 -0
  115. package/skills/hatch-pet/scripts/render_animation_videos.sh +5 -0
  116. package/skills/hatch-pet/scripts/validate_atlas.py +139 -0
  117. package/skills/i18n-orchestrator/SKILL.md +37 -0
  118. package/skills/ios-simulator-skill/SKILL.md +390 -0
  119. package/skills/ios-simulator-skill/scripts/accessibility_audit.py +300 -0
  120. package/skills/ios-simulator-skill/scripts/app_launcher.py +326 -0
  121. package/skills/ios-simulator-skill/scripts/app_state_capture.py +400 -0
  122. package/skills/ios-simulator-skill/scripts/appearance.py +385 -0
  123. package/skills/ios-simulator-skill/scripts/build_and_test.py +348 -0
  124. package/skills/ios-simulator-skill/scripts/clipboard.py +103 -0
  125. package/skills/ios-simulator-skill/scripts/common/__init__.py +61 -0
  126. package/skills/ios-simulator-skill/scripts/common/cache_utils.py +289 -0
  127. package/skills/ios-simulator-skill/scripts/common/device_utils.py +462 -0
  128. package/skills/ios-simulator-skill/scripts/common/env_config.py +35 -0
  129. package/skills/ios-simulator-skill/scripts/common/hang_pipeline.py +862 -0
  130. package/skills/ios-simulator-skill/scripts/common/hang_sessions.py +490 -0
  131. package/skills/ios-simulator-skill/scripts/common/idb_utils.py +180 -0
  132. package/skills/ios-simulator-skill/scripts/common/screenshot_utils.py +338 -0
  133. package/skills/ios-simulator-skill/scripts/container.py +668 -0
  134. package/skills/ios-simulator-skill/scripts/gesture.py +394 -0
  135. package/skills/ios-simulator-skill/scripts/hang_watcher.py +1533 -0
  136. package/skills/ios-simulator-skill/scripts/keyboard.py +391 -0
  137. package/skills/ios-simulator-skill/scripts/localization_audit.py +483 -0
  138. package/skills/ios-simulator-skill/scripts/location.py +467 -0
  139. package/skills/ios-simulator-skill/scripts/log_monitor.py +493 -0
  140. package/skills/ios-simulator-skill/scripts/model_inspector.py +645 -0
  141. package/skills/ios-simulator-skill/scripts/navigator.py +461 -0
  142. package/skills/ios-simulator-skill/scripts/privacy_manager.py +310 -0
  143. package/skills/ios-simulator-skill/scripts/push_notification.py +240 -0
  144. package/skills/ios-simulator-skill/scripts/screen_mapper.py +296 -0
  145. package/skills/ios-simulator-skill/scripts/sim_health_check.sh +245 -0
  146. package/skills/ios-simulator-skill/scripts/sim_list.py +299 -0
  147. package/skills/ios-simulator-skill/scripts/simctl_boot.py +312 -0
  148. package/skills/ios-simulator-skill/scripts/simctl_create.py +316 -0
  149. package/skills/ios-simulator-skill/scripts/simctl_delete.py +357 -0
  150. package/skills/ios-simulator-skill/scripts/simctl_erase.py +351 -0
  151. package/skills/ios-simulator-skill/scripts/simctl_shutdown.py +290 -0
  152. package/skills/ios-simulator-skill/scripts/simulator_selector.py +375 -0
  153. package/skills/ios-simulator-skill/scripts/status_bar.py +250 -0
  154. package/skills/ios-simulator-skill/scripts/test_recorder.py +323 -0
  155. package/skills/ios-simulator-skill/scripts/visual_diff.py +235 -0
  156. package/skills/ios-simulator-skill/scripts/xcode/__init__.py +13 -0
  157. package/skills/ios-simulator-skill/scripts/xcode/builder.py +397 -0
  158. package/skills/ios-simulator-skill/scripts/xcode/cache.py +204 -0
  159. package/skills/ios-simulator-skill/scripts/xcode/config.py +178 -0
  160. package/skills/ios-simulator-skill/scripts/xcode/reporter.py +343 -0
  161. package/skills/ios-simulator-skill/scripts/xcode/xcresult.py +451 -0
  162. package/skills/ios-visual-qa-strategist/SKILL.md +111 -0
  163. package/skills/ios-visual-qa-strategist/agents/openai.yaml +4 -0
  164. package/skills/ios-visual-qa-strategist/references/ios-tool-selection.md +61 -0
  165. package/skills/ios-visual-qa-strategist/references/minimal-capture-policy.md +56 -0
  166. package/skills/ios-visual-qa-strategist/references/visual-reasoning-heuristics.md +53 -0
  167. package/skills/orchestrator/SKILL.md +0 -20
  168. package/skills/persistent-storage/SKILL.md +55 -0
  169. package/skills/short-maker/SKILL.md +23 -0
  170. package/skills/short-maker/scripts/effects.js +56 -0
  171. package/skills/short-maker/scripts/shortmaker-bridge.js +332 -0
  172. package/skills/short-maker/scripts/videomix.js +601 -0
  173. package/skills/short-maker/templates/hyperframes/cinematic-character.template.html +172 -0
  174. package/skills/short-maker/templates/hyperframes/index.template.html +194 -0
  175. package/skills/smali-to-kotlin/SKILL.md +128 -0
  176. package/skills/smali-to-kotlin/examples/getting-started/tech-stack.md +58 -0
  177. package/skills/smali-to-kotlin/examples/pipeline/data-ui-parity.md +118 -0
  178. package/skills/smali-to-kotlin/examples/pipeline/scanner-and-bootstrap.md +106 -0
  179. package/skills/smali-to-kotlin/library-patterns.md +189 -0
  180. package/skills/smali-to-kotlin/phase-0-discovery.md +128 -0
  181. package/skills/smali-to-kotlin/phase-1-architecture.md +166 -0
  182. package/skills/smali-to-kotlin/phase-2-blueprint-ui.md +347 -0
  183. package/skills/smali-to-kotlin/phase-2-blueprint.md +228 -0
  184. package/skills/smali-to-kotlin/phase-3-build.md +248 -0
  185. package/skills/smali-to-kotlin/phase-3-logic-build.md +268 -0
  186. package/skills/smali-to-kotlin/smali-reading-guide.md +310 -0
  187. package/skills/smali-to-kotlin/templates/app-map.md +101 -0
  188. package/skills/smali-to-kotlin/templates/architecture.md +142 -0
  189. package/skills/smali-to-kotlin/templates/blueprint.md +145 -0
  190. package/skills/spec-gate/SKILL.md +6 -2
  191. package/skills/symphony-enforcer/SKILL.md +8 -0
  192. package/skills/symphony-enforcer/examples/mindful-stop.md +2 -0
  193. package/skills/symphony-enforcer/examples/three-phase.md +16 -0
  194. package/skills/symphony-enforcer/examples/trigger-points.md +7 -1
  195. package/skills/unity-game-development/SKILL.md +231 -0
  196. package/skills/video-edit/SKILL.md +36 -0
  197. package/skills/video-edit/scripts/video_edit.py +324 -0
  198. package/templates/project-identity/android.json +2 -2
  199. package/templates/project-identity/backend-nestjs.json +2 -2
  200. package/templates/project-identity/expo.json +2 -2
  201. package/templates/project-identity/ios.json +2 -2
  202. package/templates/project-identity/web-nextjs.json +2 -2
  203. package/templates/setup-mapping.json +48 -0
  204. package/templates/specs/design-template.md +161 -71
  205. package/templates/specs/requirements-template.md +65 -133
  206. package/templates/specs/task-spec-template.xml +3 -0
  207. package/workflows/_uncategorized/critic.md +40 -0
  208. package/workflows/_uncategorized/git-rebase-flow.md +81 -0
  209. package/workflows/_uncategorized/image-gen.md +118 -0
  210. package/workflows/_uncategorized/multi-model-pipeline.md +60 -0
  211. package/workflows/_uncategorized/pixel-gen.md +86 -0
  212. package/workflows/_uncategorized/pixel-setup.md +90 -0
  213. package/workflows/_uncategorized/ponytail-review.md +59 -0
  214. package/workflows/_uncategorized/reverse-android-build.md +222 -0
  215. package/workflows/_uncategorized/reverse-android-design.md +139 -0
  216. package/workflows/_uncategorized/reverse-android-discover.md +150 -0
  217. package/workflows/_uncategorized/reverse-android-scan.md +158 -0
  218. package/workflows/_uncategorized/reverse-android.md +143 -0
  219. package/workflows/_uncategorized/reverse-ios-build.md +240 -0
  220. package/workflows/_uncategorized/reverse-ios-design.md +112 -0
  221. package/workflows/_uncategorized/reverse-ios-discover.md +120 -0
  222. package/workflows/_uncategorized/reverse-ios-scan.md +155 -0
  223. package/workflows/_uncategorized/reverse-ios.md +152 -0
  224. package/workflows/_uncategorized/safety-router.md +34 -0
  225. package/workflows/_uncategorized/teach.md +89 -0
  226. package/workflows/_uncategorized/verify-ui.md +53 -0
  227. package/workflows/_uncategorized/visualize-screenshots.md +34 -0
  228. package/workflows/ads/ads-analyst.md +201 -0
  229. package/workflows/ads/ads-audit.md +106 -0
  230. package/workflows/ads/ads-optimize.md +97 -0
  231. package/workflows/ads/ads-targeting.md +241 -0
  232. package/workflows/ads/adsExpert.md +160 -0
  233. package/workflows/ads/smali-ads-config.md +400 -0
  234. package/workflows/ads/smali-ads-flow.md +331 -0
  235. package/workflows/ads/smali-ads-interstitial.md +377 -0
  236. package/workflows/ads/smali-ads-native.md +382 -0
  237. package/workflows/context/teach.md +89 -0
  238. package/workflows/gitnexus.md +8 -8
  239. package/workflows/lifecycle/brainstorm.md +43 -0
  240. package/workflows/lifecycle/code.md +5 -0
  241. package/workflows/lifecycle/init.md +23 -5
  242. package/workflows/lifecycle/multi-model-pipeline.md +60 -0
  243. package/workflows/quality/ponytail-review.md +59 -0
  244. package/workflows/roles/critic.md +40 -0
  245. package/workflows/roles/safety-router.md +34 -0
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env python3
2
+ """Secondary image generation fallback for Codex pet base art and row strips."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import base64
8
+ import hashlib
9
+ import json
10
+ import os
11
+ import shutil
12
+ import subprocess
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+
16
+ ALL_STATES = [
17
+ "idle",
18
+ "running-right",
19
+ "running-left",
20
+ "waving",
21
+ "jumping",
22
+ "failed",
23
+ "waiting",
24
+ "running",
25
+ "review",
26
+ ]
27
+ CANONICAL_BASE_PATH = "references/canonical-base.png"
28
+
29
+
30
+ def parse_states(raw: str) -> list[str]:
31
+ if raw.strip().lower() == "all":
32
+ return ALL_STATES
33
+ states = [item.strip() for item in raw.split(",") if item.strip()]
34
+ unknown = sorted(set(states) - set(ALL_STATES))
35
+ if unknown:
36
+ raise SystemExit(f"unknown state(s): {', '.join(unknown)}")
37
+ return states
38
+
39
+
40
+ def load_manifest(run_dir: Path) -> dict[str, object]:
41
+ path = run_dir / "imagegen-jobs.json"
42
+ if not path.exists():
43
+ raise SystemExit(f"job manifest not found: {path}")
44
+ return json.loads(path.read_text(encoding="utf-8"))
45
+
46
+
47
+ def manifest_jobs(manifest: dict[str, object]) -> list[dict[str, object]]:
48
+ jobs = manifest.get("jobs")
49
+ if not isinstance(jobs, list):
50
+ raise SystemExit("invalid imagegen-jobs.json: jobs must be a list")
51
+ return [job for job in jobs if isinstance(job, dict)]
52
+
53
+
54
+ def select_jobs(
55
+ manifest: dict[str, object],
56
+ *,
57
+ states: list[str],
58
+ skip_base: bool,
59
+ job_ids: list[str],
60
+ ) -> list[dict[str, object]]:
61
+ selected_ids = set(job_ids)
62
+ if not selected_ids:
63
+ if not skip_base:
64
+ selected_ids.add("base")
65
+ selected_ids.update(states)
66
+ selected = [job for job in manifest_jobs(manifest) if job.get("id") in selected_ids]
67
+ missing = selected_ids - {str(job.get("id")) for job in selected}
68
+ if missing:
69
+ raise SystemExit(f"unknown job id(s): {', '.join(sorted(missing))}")
70
+ return selected
71
+
72
+
73
+ def run_image_edit(
74
+ *,
75
+ model: str,
76
+ prompt_file: Path,
77
+ image_paths: list[Path],
78
+ output_json: Path,
79
+ size: str,
80
+ api_key: str,
81
+ ) -> dict[str, object]:
82
+ output_json.parent.mkdir(parents=True, exist_ok=True)
83
+ command = [
84
+ "curl",
85
+ "-sS",
86
+ "-X",
87
+ "POST",
88
+ "https://api.openai.com/v1/images/edits",
89
+ "-H",
90
+ f"Authorization: Bearer {api_key}",
91
+ "-F",
92
+ f"model={model}",
93
+ ]
94
+ for image_path in image_paths:
95
+ command.extend(["-F", f"image[]=@{image_path}"])
96
+ command.extend(
97
+ [
98
+ "-F",
99
+ f"prompt=<{prompt_file}",
100
+ "-F",
101
+ f"size={size}",
102
+ "-F",
103
+ "output_format=png",
104
+ "-o",
105
+ str(output_json),
106
+ ]
107
+ )
108
+ subprocess.run(command, check=True)
109
+ response = json.loads(output_json.read_text(encoding="utf-8"))
110
+ if response.get("error"):
111
+ raise SystemExit(json.dumps(response["error"], indent=2))
112
+ return response
113
+
114
+
115
+ def run_image_generation(
116
+ *,
117
+ model: str,
118
+ prompt_file: Path,
119
+ output_json: Path,
120
+ size: str,
121
+ api_key: str,
122
+ ) -> dict[str, object]:
123
+ output_json.parent.mkdir(parents=True, exist_ok=True)
124
+ command = [
125
+ "curl",
126
+ "-sS",
127
+ "-X",
128
+ "POST",
129
+ "https://api.openai.com/v1/images/generations",
130
+ "-H",
131
+ f"Authorization: Bearer {api_key}",
132
+ "-F",
133
+ f"model={model}",
134
+ "-F",
135
+ f"prompt=<{prompt_file}",
136
+ "-F",
137
+ f"size={size}",
138
+ "-F",
139
+ "output_format=png",
140
+ "-o",
141
+ str(output_json),
142
+ ]
143
+ subprocess.run(command, check=True)
144
+ response = json.loads(output_json.read_text(encoding="utf-8"))
145
+ if response.get("error"):
146
+ raise SystemExit(json.dumps(response["error"], indent=2))
147
+ return response
148
+
149
+
150
+ def decode_response(response: dict[str, object], output_image: Path) -> None:
151
+ data = response.get("data")
152
+ if not isinstance(data, list) or not data:
153
+ raise SystemExit("image API response did not contain data[0]")
154
+ first = data[0]
155
+ if not isinstance(first, dict) or not isinstance(first.get("b64_json"), str):
156
+ raise SystemExit("image API response did not contain data[0].b64_json")
157
+ output_image.parent.mkdir(parents=True, exist_ok=True)
158
+ output_image.write_bytes(base64.b64decode(first["b64_json"]))
159
+
160
+
161
+ def file_sha256(path: Path) -> str:
162
+ digest = hashlib.sha256()
163
+ with path.open("rb") as file:
164
+ for chunk in iter(lambda: file.read(1024 * 1024), b""):
165
+ digest.update(chunk)
166
+ return digest.hexdigest()
167
+
168
+
169
+ def complete_job(job: dict[str, object], output_path: Path) -> None:
170
+ job["status"] = "complete"
171
+ job["source_path"] = str(output_path)
172
+ job["source_provenance"] = "secondary-fallback-image-api"
173
+ job["source_sha256"] = file_sha256(output_path)
174
+ job["output_sha256"] = file_sha256(output_path)
175
+ job["completed_at"] = datetime.now(timezone.utc).isoformat()
176
+ job["secondary_fallback"] = True
177
+ for key in [
178
+ "last_error",
179
+ "synthetic_test_source",
180
+ "derived_from",
181
+ "mirror_decision",
182
+ "repair_reason",
183
+ "queued_at",
184
+ ]:
185
+ job.pop(key, None)
186
+
187
+
188
+ def write_canonical_base(
189
+ run_dir: Path, manifest: dict[str, object], output_image: Path
190
+ ) -> None:
191
+ canonical = run_dir / CANONICAL_BASE_PATH
192
+ canonical.parent.mkdir(parents=True, exist_ok=True)
193
+ shutil.copy2(output_image, canonical)
194
+ reference = {
195
+ "path": CANONICAL_BASE_PATH,
196
+ "source_job": "base",
197
+ "sha256": file_sha256(canonical),
198
+ }
199
+ manifest["canonical_identity_reference"] = reference
200
+ request_path = run_dir / "pet_request.json"
201
+ if request_path.exists():
202
+ request = json.loads(request_path.read_text(encoding="utf-8"))
203
+ request["canonical_identity_reference"] = reference
204
+ request_path.write_text(json.dumps(request, indent=2) + "\n", encoding="utf-8")
205
+
206
+
207
+ def path_list(run_dir: Path, job: dict[str, object]) -> list[Path]:
208
+ inputs = job.get("input_images")
209
+ if not isinstance(inputs, list):
210
+ raise SystemExit(f"job {job.get('id')} has invalid input_images")
211
+ paths = []
212
+ for item in inputs:
213
+ if not isinstance(item, dict) or not isinstance(item.get("path"), str):
214
+ raise SystemExit(f"job {job.get('id')} has invalid input image entry")
215
+ path = run_dir / item["path"]
216
+ if not path.is_file():
217
+ raise SystemExit(f"input image for job {job.get('id')} not found: {path}")
218
+ paths.append(path)
219
+ return paths
220
+
221
+
222
+ def main() -> None:
223
+ parser = argparse.ArgumentParser(description=__doc__)
224
+ parser.add_argument("--run-dir", required=True)
225
+ parser.add_argument("--model", default="gpt-image-2")
226
+ parser.add_argument("--size", default="1024x1024")
227
+ parser.add_argument("--states", default="all")
228
+ parser.add_argument("--job-id", action="append", default=[])
229
+ parser.add_argument("--skip-base", action="store_true")
230
+ args = parser.parse_args()
231
+
232
+ api_key = os.environ.get("OPENAI_API_KEY")
233
+ if not api_key:
234
+ raise SystemExit("OPENAI_API_KEY is not set")
235
+
236
+ run_dir = Path(args.run_dir).expanduser().resolve()
237
+ manifest_path = run_dir / "imagegen-jobs.json"
238
+ manifest = load_manifest(run_dir)
239
+ jobs = select_jobs(
240
+ manifest,
241
+ states=parse_states(args.states),
242
+ skip_base=args.skip_base,
243
+ job_ids=args.job_id,
244
+ )
245
+ raw_dir = run_dir / "raw"
246
+
247
+ completed = []
248
+ for job in jobs:
249
+ job_id = str(job.get("id"))
250
+ prompt_raw = job.get("prompt_file")
251
+ output_raw = job.get("output_path")
252
+ if not isinstance(prompt_raw, str) or not isinstance(output_raw, str):
253
+ raise SystemExit(f"job {job_id} is missing prompt_file or output_path")
254
+ prompt_file = run_dir / prompt_raw
255
+ output_image = run_dir / output_raw
256
+ print(f"Generating {job_id} with secondary fallback")
257
+ image_paths = path_list(run_dir, job)
258
+ if image_paths:
259
+ response = run_image_edit(
260
+ model=args.model,
261
+ prompt_file=prompt_file,
262
+ image_paths=image_paths,
263
+ output_json=raw_dir / f"{job_id}.response.json",
264
+ size=args.size,
265
+ api_key=api_key,
266
+ )
267
+ else:
268
+ response = run_image_generation(
269
+ model=args.model,
270
+ prompt_file=prompt_file,
271
+ output_json=raw_dir / f"{job_id}.response.json",
272
+ size=args.size,
273
+ api_key=api_key,
274
+ )
275
+ decode_response(response, output_image)
276
+ complete_job(job, output_image)
277
+ if job_id == "base":
278
+ job["canonical_reference_path"] = CANONICAL_BASE_PATH
279
+ write_canonical_base(run_dir, manifest, output_image)
280
+ completed.append({"job_id": job_id, "output": str(output_image)})
281
+
282
+ manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
283
+ print(json.dumps({"ok": True, "completed": completed}, indent=2))
284
+
285
+
286
+ if __name__ == "__main__":
287
+ main()
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env python3
2
+ """Inspect extracted Codex pet frames before atlas composition."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import math
9
+ from pathlib import Path
10
+ from statistics import median
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
+ IMAGE_SUFFIXES = {".png", ".webp", ".jpg", ".jpeg"}
28
+
29
+
30
+ def alpha_nonzero_count(image: Image.Image) -> int:
31
+ alpha = image if image.mode == "L" else image.getchannel("A")
32
+ return sum(alpha.histogram()[1:])
33
+
34
+
35
+ def edge_alpha_count(image: Image.Image, margin: int) -> int:
36
+ alpha = image.getchannel("A")
37
+ width, height = alpha.size
38
+ total = 0
39
+ for box in (
40
+ (0, 0, width, margin),
41
+ (0, height - margin, width, height),
42
+ (0, 0, margin, height),
43
+ (width - margin, 0, width, height),
44
+ ):
45
+ total += alpha_nonzero_count(alpha.crop(box))
46
+ return total
47
+
48
+
49
+ def color_distance(left: tuple[int, int, int], right: tuple[int, int, int]) -> float:
50
+ return math.sqrt(sum((left[index] - right[index]) ** 2 for index in range(3)))
51
+
52
+
53
+ def chroma_adjacent_count(
54
+ image: Image.Image,
55
+ chroma_key: tuple[int, int, int] | None,
56
+ threshold: float,
57
+ ) -> int:
58
+ if chroma_key is None:
59
+ return 0
60
+ rgba = image.convert("RGBA")
61
+ data = rgba.tobytes()
62
+ count = 0
63
+ for index in range(0, len(data), 4):
64
+ red, green, blue, alpha = data[index : index + 4]
65
+ if alpha > 16 and color_distance((red, green, blue), chroma_key) <= threshold:
66
+ count += 1
67
+ return count
68
+
69
+
70
+ def frame_files(state_dir: Path) -> list[Path]:
71
+ if not state_dir.is_dir():
72
+ return []
73
+ return sorted(path for path in state_dir.iterdir() if path.suffix.lower() in IMAGE_SUFFIXES)
74
+
75
+
76
+ def load_manifest(frames_root: Path) -> dict[str, dict[str, object]]:
77
+ manifest_path = frames_root / "frames-manifest.json"
78
+ if not manifest_path.is_file():
79
+ return {}
80
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
81
+ rows = manifest.get("rows", [])
82
+ if not isinstance(rows, list):
83
+ return {}
84
+ return {
85
+ row["state"]: row
86
+ for row in rows
87
+ if isinstance(row, dict) and isinstance(row.get("state"), str)
88
+ }
89
+
90
+
91
+ def load_chroma_key(frames_root: Path) -> tuple[int, int, int] | None:
92
+ manifest_path = frames_root / "frames-manifest.json"
93
+ if not manifest_path.is_file():
94
+ return None
95
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
96
+ chroma_key = manifest.get("chroma_key")
97
+ if not isinstance(chroma_key, dict):
98
+ return None
99
+ rgb = chroma_key.get("rgb")
100
+ if (
101
+ not isinstance(rgb, list)
102
+ or len(rgb) != 3
103
+ or not all(isinstance(value, int) for value in rgb)
104
+ ):
105
+ return None
106
+ return (rgb[0], rgb[1], rgb[2])
107
+
108
+
109
+ def inspect_state(
110
+ frames_root: Path,
111
+ state: str,
112
+ expected_count: int,
113
+ manifest_rows: dict[str, dict[str, object]],
114
+ chroma_key: tuple[int, int, int] | None,
115
+ args: argparse.Namespace,
116
+ ) -> dict[str, object]:
117
+ state_dir = frames_root / state
118
+ files = frame_files(state_dir)
119
+ row_errors: list[str] = []
120
+ row_warnings: list[str] = []
121
+ frames: list[dict[str, object]] = []
122
+ areas: list[int] = []
123
+ manifest_row = manifest_rows.get(state, {})
124
+ method = manifest_row.get("method")
125
+
126
+ if len(files) != expected_count:
127
+ row_errors.append(f"expected {expected_count} frame files for {state}, found {len(files)}")
128
+
129
+ if args.require_components and method and method != "components":
130
+ row_errors.append(
131
+ f"{state} used extraction method {method}; regenerate the row or inspect slot slicing"
132
+ )
133
+ elif method and method != "components":
134
+ row_warnings.append(
135
+ f"{state} used extraction method {method}; component extraction is preferred"
136
+ )
137
+
138
+ for index, frame_path in enumerate(files[:expected_count]):
139
+ with Image.open(frame_path) as opened:
140
+ frame = opened.convert("RGBA")
141
+ nontransparent = alpha_nonzero_count(frame)
142
+ bbox = frame.getbbox()
143
+ edge_pixels = edge_alpha_count(frame, args.edge_margin)
144
+ chroma_adjacent_pixels = chroma_adjacent_count(
145
+ frame,
146
+ chroma_key,
147
+ args.chroma_adjacent_threshold,
148
+ )
149
+ info = {
150
+ "index": index,
151
+ "file": str(frame_path),
152
+ "width": frame.width,
153
+ "height": frame.height,
154
+ "nontransparent_pixels": nontransparent,
155
+ "bbox": list(bbox) if bbox else None,
156
+ "edge_pixels": edge_pixels,
157
+ "chroma_adjacent_pixels": chroma_adjacent_pixels,
158
+ }
159
+ frames.append(info)
160
+ areas.append(nontransparent)
161
+
162
+ if frame.size != (CELL_WIDTH, CELL_HEIGHT):
163
+ row_errors.append(
164
+ f"{state} frame {index:02d} is {frame.width}x{frame.height}; expected {CELL_WIDTH}x{CELL_HEIGHT}"
165
+ )
166
+ if nontransparent < args.min_used_pixels:
167
+ row_errors.append(
168
+ f"{state} frame {index:02d} is empty or too sparse ({nontransparent} pixels)"
169
+ )
170
+ if edge_pixels > args.edge_pixel_threshold:
171
+ row_warnings.append(
172
+ f"{state} frame {index:02d} has {edge_pixels} non-transparent pixels near the cell edge"
173
+ )
174
+ if chroma_adjacent_pixels > args.chroma_adjacent_pixel_threshold:
175
+ row_errors.append(
176
+ f"{state} frame {index:02d} has {chroma_adjacent_pixels} non-transparent pixels close to the chroma key"
177
+ )
178
+
179
+ if areas:
180
+ row_median = median(areas)
181
+ for index, area in enumerate(areas[:expected_count]):
182
+ if row_median > 0 and area < row_median * args.small_outlier_ratio:
183
+ row_warnings.append(
184
+ f"{state} frame {index:02d} is much smaller than the row median ({area} vs {row_median:.0f})"
185
+ )
186
+ if row_median > 0 and area > row_median * args.large_outlier_ratio:
187
+ row_warnings.append(
188
+ f"{state} frame {index:02d} is much larger than the row median ({area} vs {row_median:.0f})"
189
+ )
190
+
191
+ return {
192
+ "state": state,
193
+ "expected_frames": expected_count,
194
+ "actual_frames": len(files),
195
+ "extraction_method": method,
196
+ "ok": not row_errors,
197
+ "errors": row_errors,
198
+ "warnings": row_warnings,
199
+ "frames": frames,
200
+ }
201
+
202
+
203
+ def main() -> None:
204
+ parser = argparse.ArgumentParser(description=__doc__)
205
+ parser.add_argument("--frames-root", required=True)
206
+ parser.add_argument("--json-out", required=True)
207
+ parser.add_argument("--min-used-pixels", type=int, default=400)
208
+ parser.add_argument("--edge-margin", type=int, default=2)
209
+ parser.add_argument("--edge-pixel-threshold", type=int, default=24)
210
+ parser.add_argument("--chroma-adjacent-threshold", type=float, default=150.0)
211
+ parser.add_argument("--chroma-adjacent-pixel-threshold", type=int, default=800)
212
+ parser.add_argument("--small-outlier-ratio", type=float, default=0.35)
213
+ parser.add_argument("--large-outlier-ratio", type=float, default=2.75)
214
+ parser.add_argument(
215
+ "--require-components",
216
+ action="store_true",
217
+ help="Fail rows that fell back to equal-slot extraction.",
218
+ )
219
+ args = parser.parse_args()
220
+
221
+ frames_root = Path(args.frames_root).expanduser().resolve()
222
+ manifest_rows = load_manifest(frames_root)
223
+ chroma_key = load_chroma_key(frames_root)
224
+ rows = [
225
+ inspect_state(frames_root, state, count, manifest_rows, chroma_key, args)
226
+ for state, count in ROW_FRAME_COUNTS.items()
227
+ ]
228
+ errors = [error for row in rows for error in row["errors"]]
229
+ warnings = [warning for row in rows for warning in row["warnings"]]
230
+ result = {
231
+ "ok": not errors,
232
+ "frames_root": str(frames_root),
233
+ "errors": errors,
234
+ "warnings": warnings,
235
+ "rows": rows,
236
+ }
237
+
238
+ json_out = Path(args.json_out).expanduser().resolve()
239
+ json_out.parent.mkdir(parents=True, exist_ok=True)
240
+ json_out.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
241
+ print(json.dumps({k: v for k, v in result.items() if k != "rows"}, indent=2))
242
+ raise SystemExit(0 if result["ok"] else 1)
243
+
244
+
245
+ if __name__ == "__main__":
246
+ main()
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env python3
2
+ """Create a labeled contact sheet from a Codex pet atlas."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ from pathlib import Path
8
+
9
+ from PIL import Image, ImageDraw, ImageFont
10
+
11
+ COLUMNS = 8
12
+ ROWS = 9
13
+ CELL_WIDTH = 192
14
+ CELL_HEIGHT = 208
15
+ LABEL_HEIGHT = 22
16
+ ROW_NAMES = [
17
+ "idle",
18
+ "running-right",
19
+ "running-left",
20
+ "waving",
21
+ "jumping",
22
+ "failed",
23
+ "waiting",
24
+ "running",
25
+ "review",
26
+ ]
27
+ USED_COUNTS = [6, 8, 8, 4, 5, 8, 6, 6, 6]
28
+
29
+
30
+ def checker(size: tuple[int, int], square: int = 16) -> Image.Image:
31
+ image = Image.new("RGB", size, "#ffffff")
32
+ draw = ImageDraw.Draw(image)
33
+ for y in range(0, size[1], square):
34
+ for x in range(0, size[0], square):
35
+ if (x // square + y // square) % 2:
36
+ draw.rectangle((x, y, x + square - 1, y + square - 1), fill="#e8e8e8")
37
+ return image
38
+
39
+
40
+ def main() -> None:
41
+ parser = argparse.ArgumentParser(description=__doc__)
42
+ parser.add_argument("atlas")
43
+ parser.add_argument("--output", required=True)
44
+ parser.add_argument("--scale", type=float, default=0.5)
45
+ args = parser.parse_args()
46
+
47
+ with Image.open(Path(args.atlas).expanduser().resolve()) as opened:
48
+ atlas = opened.convert("RGBA")
49
+
50
+ cell_w = max(1, round(CELL_WIDTH * args.scale))
51
+ cell_h = max(1, round(CELL_HEIGHT * args.scale))
52
+ width = COLUMNS * cell_w
53
+ height = ROWS * (cell_h + LABEL_HEIGHT)
54
+ sheet = Image.new("RGB", (width, height), "#f7f7f7")
55
+ draw = ImageDraw.Draw(sheet)
56
+ font = ImageFont.load_default()
57
+
58
+ for row in range(ROWS):
59
+ y = row * (cell_h + LABEL_HEIGHT)
60
+ draw.rectangle((0, y, width, y + LABEL_HEIGHT - 1), fill="#111111")
61
+ draw.text((6, y + 5), f"row {row}: {ROW_NAMES[row]}", fill="#ffffff", font=font)
62
+ draw.text(
63
+ (width - 92, y + 5),
64
+ f"{USED_COUNTS[row]} frames",
65
+ fill="#ffffff",
66
+ font=font,
67
+ )
68
+ for column in range(COLUMNS):
69
+ crop = atlas.crop(
70
+ (
71
+ column * CELL_WIDTH,
72
+ row * CELL_HEIGHT,
73
+ (column + 1) * CELL_WIDTH,
74
+ (row + 1) * CELL_HEIGHT,
75
+ )
76
+ )
77
+ crop = crop.resize((cell_w, cell_h), Image.Resampling.LANCZOS)
78
+ bg = checker((cell_w, cell_h))
79
+ bg.paste(crop, (0, 0), crop)
80
+ x = column * cell_w
81
+ sheet.paste(bg, (x, y + LABEL_HEIGHT))
82
+ outline = "#18a058" if column < USED_COUNTS[row] else "#cc3344"
83
+ draw.rectangle(
84
+ (x, y + LABEL_HEIGHT, x + cell_w - 1, y + LABEL_HEIGHT + cell_h - 1),
85
+ outline=outline,
86
+ )
87
+ draw.text((x + 4, y + LABEL_HEIGHT + 4), str(column), fill="#111111", font=font)
88
+
89
+ output = Path(args.output).expanduser().resolve()
90
+ output.parent.mkdir(parents=True, exist_ok=True)
91
+ sheet.save(output)
92
+ print(f"wrote {output}")
93
+
94
+
95
+ if __name__ == "__main__":
96
+ main()