@leejungkiin/awkit 1.7.0 → 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 (241) 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 +40 -7
  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/verification-gate/SKILL.md +4 -2
  197. package/skills/video-edit/SKILL.md +36 -0
  198. package/skills/video-edit/scripts/video_edit.py +324 -0
  199. package/templates/setup-mapping.json +48 -0
  200. package/templates/specs/design-template.md +161 -71
  201. package/templates/specs/requirements-template.md +65 -133
  202. package/templates/specs/task-spec-template.xml +3 -0
  203. package/workflows/_uncategorized/critic.md +40 -0
  204. package/workflows/_uncategorized/git-rebase-flow.md +81 -0
  205. package/workflows/_uncategorized/image-gen.md +118 -0
  206. package/workflows/_uncategorized/multi-model-pipeline.md +60 -0
  207. package/workflows/_uncategorized/pixel-gen.md +86 -0
  208. package/workflows/_uncategorized/pixel-setup.md +90 -0
  209. package/workflows/_uncategorized/ponytail-review.md +59 -0
  210. package/workflows/_uncategorized/reverse-android-build.md +222 -0
  211. package/workflows/_uncategorized/reverse-android-design.md +139 -0
  212. package/workflows/_uncategorized/reverse-android-discover.md +150 -0
  213. package/workflows/_uncategorized/reverse-android-scan.md +158 -0
  214. package/workflows/_uncategorized/reverse-android.md +143 -0
  215. package/workflows/_uncategorized/reverse-ios-build.md +240 -0
  216. package/workflows/_uncategorized/reverse-ios-design.md +112 -0
  217. package/workflows/_uncategorized/reverse-ios-discover.md +120 -0
  218. package/workflows/_uncategorized/reverse-ios-scan.md +155 -0
  219. package/workflows/_uncategorized/reverse-ios.md +152 -0
  220. package/workflows/_uncategorized/safety-router.md +34 -0
  221. package/workflows/_uncategorized/teach.md +89 -0
  222. package/workflows/_uncategorized/verify-ui.md +53 -0
  223. package/workflows/_uncategorized/visualize-screenshots.md +34 -0
  224. package/workflows/ads/ads-analyst.md +201 -0
  225. package/workflows/ads/ads-audit.md +106 -0
  226. package/workflows/ads/ads-optimize.md +97 -0
  227. package/workflows/ads/ads-targeting.md +241 -0
  228. package/workflows/ads/adsExpert.md +160 -0
  229. package/workflows/ads/smali-ads-config.md +400 -0
  230. package/workflows/ads/smali-ads-flow.md +331 -0
  231. package/workflows/ads/smali-ads-interstitial.md +377 -0
  232. package/workflows/ads/smali-ads-native.md +382 -0
  233. package/workflows/context/teach.md +89 -0
  234. package/workflows/gitnexus.md +8 -8
  235. package/workflows/lifecycle/brainstorm.md +43 -0
  236. package/workflows/lifecycle/code.md +5 -0
  237. package/workflows/lifecycle/init.md +23 -5
  238. package/workflows/lifecycle/multi-model-pipeline.md +60 -0
  239. package/workflows/quality/ponytail-review.md +59 -0
  240. package/workflows/roles/critic.md +40 -0
  241. package/workflows/roles/safety-router.md +34 -0
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env python3
2
+ """Clean leftover chroma-key fringe from transparent PNG assets.
3
+
4
+ This is intentionally conservative: it only targets low-alpha pixels close to the
5
+ configured key color, then bleeds nearby foreground RGB into fully transparent
6
+ edge pixels so image scaling cannot sample hidden chroma color.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ from pathlib import Path
13
+
14
+ import numpy as np
15
+ from PIL import Image
16
+
17
+
18
+ DEFAULT_PATHS = (
19
+ "assets/ui/icons/v2/final",
20
+ "assets/mascot/v2/final",
21
+ "assets/mascot/v3/final",
22
+ "assets/illustrations/v1/final",
23
+ )
24
+
25
+
26
+ def smoothstep(edge0: float, edge1: float, x: np.ndarray) -> np.ndarray:
27
+ t = np.clip((x - edge0) / (edge1 - edge0), 0.0, 1.0)
28
+ return t * t * (3.0 - 2.0 * t)
29
+
30
+
31
+ def shifted(values: np.ndarray, dy: int, dx: int) -> np.ndarray:
32
+ out = np.zeros_like(values)
33
+ src_y0 = max(0, -dy)
34
+ src_y1 = values.shape[0] - max(0, dy)
35
+ src_x0 = max(0, -dx)
36
+ src_x1 = values.shape[1] - max(0, dx)
37
+ dst_y0 = max(0, dy)
38
+ dst_y1 = values.shape[0] - max(0, -dy)
39
+ dst_x0 = max(0, dx)
40
+ dst_x1 = values.shape[1] - max(0, -dx)
41
+ out[dst_y0:dst_y1, dst_x0:dst_x1] = values[src_y0:src_y1, src_x0:src_x1]
42
+ return out
43
+
44
+
45
+ def bleed_rgb(
46
+ rgb: np.ndarray,
47
+ alpha: np.ndarray,
48
+ chroma_mask: np.ndarray,
49
+ target_mask: np.ndarray,
50
+ iterations: int,
51
+ ) -> np.ndarray:
52
+ result = rgb.copy()
53
+ valid = (alpha > 8) & ~chroma_mask
54
+ seed = valid.copy()
55
+ remaining = target_mask.copy()
56
+
57
+ for _ in range(iterations):
58
+ accum = np.zeros_like(result, dtype=np.float32)
59
+ counts = np.zeros(alpha.shape, dtype=np.float32)
60
+
61
+ for dy in (-1, 0, 1):
62
+ for dx in (-1, 0, 1):
63
+ if dy == 0 and dx == 0:
64
+ continue
65
+ neighbor_valid = shifted(seed, dy, dx)
66
+ fill = remaining & neighbor_valid
67
+ if not np.any(fill):
68
+ continue
69
+ neighbor_rgb = shifted(result, dy, dx)
70
+ accum[fill] += neighbor_rgb[fill]
71
+ counts[fill] += 1.0
72
+
73
+ filled = counts > 0
74
+ if not np.any(filled):
75
+ break
76
+ result[filled] = np.clip(accum[filled] / counts[filled, None], 0, 255).astype(np.uint8)
77
+ seed[filled] = True
78
+ remaining[filled] = False
79
+
80
+ return result
81
+
82
+
83
+ def expand_mask(mask: np.ndarray, iterations: int) -> np.ndarray:
84
+ result = mask.copy()
85
+ for _ in range(iterations):
86
+ expanded = result.copy()
87
+ for dy in (-1, 0, 1):
88
+ for dx in (-1, 0, 1):
89
+ if dy == 0 and dx == 0:
90
+ continue
91
+ expanded |= shifted(result, dy, dx)
92
+ if np.array_equal(expanded, result):
93
+ break
94
+ result = expanded
95
+ return result
96
+
97
+
98
+ def clean_array(
99
+ rgba: np.ndarray,
100
+ key: tuple[int, int, int],
101
+ hard_dist: float,
102
+ soft_dist: float,
103
+ max_alpha: int,
104
+ bleed_iterations: int,
105
+ edge_despill: bool,
106
+ edge_radius: int,
107
+ edge_strength: float,
108
+ ) -> tuple[np.ndarray, dict[str, int]]:
109
+ arr = rgba.copy()
110
+ rgb = arr[..., :3].astype(np.float32)
111
+ alpha = arr[..., 3].astype(np.float32)
112
+ key_rgb = np.array(key, dtype=np.float32)
113
+ dist = np.sqrt(np.sum((rgb - key_rgb) ** 2, axis=2))
114
+
115
+ candidate = (alpha > 0) & (alpha <= max_alpha) & (dist < soft_dist)
116
+ hard = candidate & (dist <= hard_dist)
117
+ feather = candidate & (dist > hard_dist)
118
+
119
+ matte = smoothstep(hard_dist, soft_dist, dist)
120
+ new_alpha = alpha.copy()
121
+ new_alpha[hard] = 0.0
122
+ new_alpha[feather] = np.minimum(new_alpha[feather], alpha[feather] * matte[feather])
123
+
124
+ old_a = np.clip(alpha / 255.0, 1.0 / 255.0, 1.0)
125
+ unmixed = (rgb - key_rgb * (1.0 - old_a[..., None])) / old_a[..., None]
126
+ spill = candidate & ~hard
127
+ rgb[spill] = np.clip(unmixed[spill], 0, 255)
128
+
129
+ chroma_mask = dist < soft_dist
130
+ rgb_uint8 = np.clip(rgb, 0, 255).astype(np.uint8)
131
+ alpha_uint8 = np.clip(np.rint(new_alpha), 0, 255).astype(np.uint8)
132
+ near_foreground = expand_mask(alpha_uint8 > 8, min(3, bleed_iterations))
133
+ hidden_chroma = (alpha <= 8) & chroma_mask & near_foreground
134
+ rgb_uint8 = bleed_rgb(rgb_uint8, alpha_uint8, chroma_mask, hidden_chroma, bleed_iterations)
135
+
136
+ arr[..., :3] = rgb_uint8
137
+ arr[..., 3] = alpha_uint8
138
+
139
+ edge_pixels = 0
140
+ if edge_despill:
141
+ arr, edge_pixels = despill_opaque_edge(arr, edge_radius, edge_strength, bleed_iterations)
142
+
143
+ changed = np.any(arr != rgba, axis=2)
144
+ stats = {
145
+ "changed": int(np.count_nonzero(changed)),
146
+ "keyed": int(np.count_nonzero(hard)),
147
+ "feathered": int(np.count_nonzero(feather)),
148
+ "edge": edge_pixels,
149
+ }
150
+ return arr, stats
151
+
152
+
153
+ def despill_opaque_edge(
154
+ rgba: np.ndarray,
155
+ radius: int,
156
+ strength: float,
157
+ bleed_iterations: int,
158
+ ) -> tuple[np.ndarray, int]:
159
+ arr = rgba.copy()
160
+ rgb = arr[..., :3].astype(np.uint8)
161
+ alpha = arr[..., 3]
162
+ opaque = alpha > 8
163
+ edge = expand_mask(~opaque, radius) & opaque
164
+
165
+ r = rgb[..., 0].astype(np.int16)
166
+ g = rgb[..., 1].astype(np.int16)
167
+ b = rgb[..., 2].astype(np.int16)
168
+ magenta_score = (np.minimum(r, b) - g) + (b - g)
169
+ edge_spill = (
170
+ edge
171
+ & (r > 130)
172
+ & (b > 115)
173
+ & ((np.minimum(r, b) - g) > 22)
174
+ & ((b - g) > 18)
175
+ )
176
+ if not np.any(edge_spill):
177
+ return arr, 0
178
+
179
+ repaired_rgb = bleed_rgb(rgb, alpha, edge_spill, edge_spill, bleed_iterations)
180
+ matte = smoothstep(55.0, 275.0, magenta_score.astype(np.float32))
181
+ new_alpha = alpha.astype(np.float32)
182
+ new_alpha[edge_spill] *= 1.0 - np.clip(strength, 0.0, 1.0) * matte[edge_spill]
183
+
184
+ arr[..., :3] = repaired_rgb
185
+ arr[..., 3] = np.clip(np.rint(new_alpha), 0, 255).astype(np.uint8)
186
+ return arr, int(np.count_nonzero(edge_spill))
187
+
188
+
189
+ def png_paths(paths: list[str]) -> list[Path]:
190
+ files: list[Path] = []
191
+ for item in paths:
192
+ path = Path(item)
193
+ if path.is_file() and path.suffix.lower() == ".png":
194
+ files.append(path)
195
+ elif path.is_dir():
196
+ files.extend(sorted(path.rglob("*.png")))
197
+ return sorted(set(files))
198
+
199
+
200
+ def parse_key(value: str) -> tuple[int, int, int]:
201
+ raw = value.strip().lstrip("#")
202
+ if len(raw) != 6:
203
+ raise argparse.ArgumentTypeError("key must be a 6-digit hex color")
204
+ return tuple(int(raw[i : i + 2], 16) for i in (0, 2, 4))
205
+
206
+
207
+ def main() -> int:
208
+ parser = argparse.ArgumentParser()
209
+ parser.add_argument("paths", nargs="*", default=list(DEFAULT_PATHS))
210
+ parser.add_argument("--key", type=parse_key, default=parse_key("#ff00ff"))
211
+ parser.add_argument("--hard-dist", type=float, default=42.0)
212
+ parser.add_argument("--soft-dist", type=float, default=118.0)
213
+ parser.add_argument("--max-alpha", type=int, default=180)
214
+ parser.add_argument("--bleed-iterations", type=int, default=10)
215
+ parser.add_argument("--edge-despill", action="store_true")
216
+ parser.add_argument("--edge-radius", type=int, default=3)
217
+ parser.add_argument("--edge-strength", type=float, default=0.75)
218
+ parser.add_argument("--apply", action="store_true")
219
+ args = parser.parse_args()
220
+
221
+ files = png_paths(args.paths)
222
+ totals = {"files": 0, "changed": 0, "keyed": 0, "feathered": 0}
223
+
224
+ for path in files:
225
+ original = np.array(Image.open(path).convert("RGBA"))
226
+ cleaned, stats = clean_array(
227
+ original,
228
+ args.key,
229
+ args.hard_dist,
230
+ args.soft_dist,
231
+ args.max_alpha,
232
+ args.bleed_iterations,
233
+ args.edge_despill,
234
+ args.edge_radius,
235
+ args.edge_strength,
236
+ )
237
+ if stats["changed"] == 0:
238
+ continue
239
+
240
+ totals["files"] += 1
241
+ totals["changed"] += stats["changed"]
242
+ totals["keyed"] += stats["keyed"]
243
+ totals["feathered"] += stats["feathered"]
244
+ totals["edge"] = totals.get("edge", 0) + stats["edge"]
245
+ action = "cleaned" if args.apply else "would clean"
246
+ print(
247
+ f"{action} {path}: changed={stats['changed']} "
248
+ f"keyed={stats['keyed']} feathered={stats['feathered']} edge={stats['edge']}"
249
+ )
250
+ if args.apply:
251
+ Image.fromarray(cleaned, "RGBA").save(path)
252
+
253
+ mode = "APPLIED" if args.apply else "DRY_RUN"
254
+ print(
255
+ f"{mode} files={totals['files']} changed={totals['changed']} "
256
+ f"keyed={totals['keyed']} feathered={totals['feathered']} edge={totals.get('edge', 0)}"
257
+ )
258
+ return 0
259
+
260
+
261
+ if __name__ == "__main__":
262
+ raise SystemExit(main())
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env python3
2
+ """Compose or normalize a Codex pet spritesheet atlas."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ from pathlib import Path
8
+
9
+ from PIL import Image
10
+
11
+ COLUMNS = 8
12
+ ROWS = 9
13
+ CELL_WIDTH = 192
14
+ CELL_HEIGHT = 208
15
+ ATLAS_WIDTH = COLUMNS * CELL_WIDTH
16
+ ATLAS_HEIGHT = ROWS * CELL_HEIGHT
17
+ ATLAS_ASPECT_RATIO = ATLAS_WIDTH / ATLAS_HEIGHT
18
+ ROW_SPECS = [
19
+ ("idle", 0, 6),
20
+ ("running-right", 1, 8),
21
+ ("running-left", 2, 8),
22
+ ("waving", 3, 4),
23
+ ("jumping", 4, 5),
24
+ ("failed", 5, 8),
25
+ ("waiting", 6, 6),
26
+ ("running", 7, 6),
27
+ ("review", 8, 6),
28
+ ]
29
+ IMAGE_SUFFIXES = {".png", ".webp", ".jpg", ".jpeg"}
30
+
31
+
32
+ def image_files(path: Path) -> list[Path]:
33
+ return sorted(p for p in path.iterdir() if p.suffix.lower() in IMAGE_SUFFIXES)
34
+
35
+
36
+ def find_row_frames(root: Path, state: str, row_index: int) -> list[Path]:
37
+ candidates = [
38
+ root / state,
39
+ root / f"row-{row_index}",
40
+ root / f"row{row_index}",
41
+ root / f"{row_index}-{state}",
42
+ ]
43
+ for candidate in candidates:
44
+ if candidate.is_dir():
45
+ files = image_files(candidate)
46
+ if files:
47
+ return files
48
+ globs = [
49
+ f"{state}_*",
50
+ f"{state}-*",
51
+ f"row{row_index}_*",
52
+ f"row-{row_index}-*",
53
+ ]
54
+ files: list[Path] = []
55
+ for pattern in globs:
56
+ files.extend(p for p in root.glob(pattern) if p.suffix.lower() in IMAGE_SUFFIXES)
57
+ return sorted(set(files))
58
+
59
+
60
+ def paste_centered(atlas: Image.Image, source: Image.Image, row: int, column: int) -> None:
61
+ frame = source.convert("RGBA")
62
+ if frame.size != (CELL_WIDTH, CELL_HEIGHT):
63
+ frame.thumbnail((CELL_WIDTH, CELL_HEIGHT), Image.Resampling.LANCZOS)
64
+ left = column * CELL_WIDTH + (CELL_WIDTH - frame.width) // 2
65
+ top = row * CELL_HEIGHT + (CELL_HEIGHT - frame.height) // 2
66
+ atlas.alpha_composite(frame, (left, top))
67
+
68
+
69
+ def compose_from_source_atlas(path: Path, resize_source: bool) -> Image.Image:
70
+ with Image.open(path) as opened:
71
+ source = opened.convert("RGBA")
72
+ if source.size != (ATLAS_WIDTH, ATLAS_HEIGHT):
73
+ if not resize_source:
74
+ raise SystemExit(
75
+ f"source atlas must be {ATLAS_WIDTH}x{ATLAS_HEIGHT}; got {source.width}x{source.height}"
76
+ )
77
+ source_ratio = source.width / source.height
78
+ if abs(source_ratio - ATLAS_ASPECT_RATIO) > 0.02:
79
+ raise SystemExit(
80
+ "refusing to resize source atlas because its aspect ratio does not match "
81
+ f"the Codex atlas ratio {ATLAS_ASPECT_RATIO:.3f}; got {source_ratio:.3f}. "
82
+ "Generate exact atlas dimensions or use --frames-root."
83
+ )
84
+ source = source.resize((ATLAS_WIDTH, ATLAS_HEIGHT), Image.Resampling.LANCZOS)
85
+
86
+ atlas = Image.new("RGBA", (ATLAS_WIDTH, ATLAS_HEIGHT), (0, 0, 0, 0))
87
+ for _state, row, frame_count in ROW_SPECS:
88
+ for column in range(frame_count):
89
+ left = column * CELL_WIDTH
90
+ top = row * CELL_HEIGHT
91
+ cell = source.crop((left, top, left + CELL_WIDTH, top + CELL_HEIGHT))
92
+ atlas.alpha_composite(cell, (left, top))
93
+ return atlas
94
+
95
+
96
+ def compose_from_frames(root: Path) -> Image.Image:
97
+ atlas = Image.new("RGBA", (ATLAS_WIDTH, ATLAS_HEIGHT), (0, 0, 0, 0))
98
+ for state, row, frame_count in ROW_SPECS:
99
+ files = find_row_frames(root, state, row)
100
+ if len(files) < frame_count:
101
+ raise SystemExit(
102
+ f"{state} row needs {frame_count} frames, found {len(files)} under {root}"
103
+ )
104
+ for column, frame_path in enumerate(files[:frame_count]):
105
+ with Image.open(frame_path) as frame:
106
+ paste_centered(atlas, frame, row, column)
107
+ return atlas
108
+
109
+
110
+ def save_outputs(atlas: Image.Image, output: Path, webp_output: Path | None) -> None:
111
+ output.parent.mkdir(parents=True, exist_ok=True)
112
+ atlas.save(output)
113
+ if webp_output is not None:
114
+ webp_output.parent.mkdir(parents=True, exist_ok=True)
115
+ atlas.save(webp_output, format="WEBP", lossless=True, quality=100, method=6)
116
+
117
+
118
+ def main() -> None:
119
+ parser = argparse.ArgumentParser(description=__doc__)
120
+ source = parser.add_mutually_exclusive_group(required=True)
121
+ source.add_argument("--source-atlas")
122
+ source.add_argument("--frames-root")
123
+ parser.add_argument("--output", required=True)
124
+ parser.add_argument("--webp-output")
125
+ parser.add_argument(
126
+ "--resize-source",
127
+ action="store_true",
128
+ help="Resize a lower-resolution source atlas only when it already has the Codex atlas aspect ratio.",
129
+ )
130
+ args = parser.parse_args()
131
+
132
+ if args.source_atlas:
133
+ atlas = compose_from_source_atlas(
134
+ Path(args.source_atlas).expanduser().resolve(), args.resize_source
135
+ )
136
+ else:
137
+ atlas = compose_from_frames(Path(args.frames_root).expanduser().resolve())
138
+
139
+ save_outputs(
140
+ atlas,
141
+ Path(args.output).expanduser().resolve(),
142
+ Path(args.webp_output).expanduser().resolve() if args.webp_output else None,
143
+ )
144
+ print(f"wrote {Path(args.output).expanduser().resolve()}")
145
+ if args.webp_output:
146
+ print(f"wrote {Path(args.webp_output).expanduser().resolve()}")
147
+
148
+
149
+ if __name__ == "__main__":
150
+ main()
@@ -0,0 +1,143 @@
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 hashlib
8
+ import json
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+
12
+ from PIL import Image, ImageOps
13
+
14
+
15
+ def load_manifest(run_dir: Path) -> dict[str, object]:
16
+ path = run_dir / "imagegen-jobs.json"
17
+ if not path.exists():
18
+ raise SystemExit(f"job manifest not found: {path}")
19
+ return json.loads(path.read_text(encoding="utf-8"))
20
+
21
+
22
+ def job_list(manifest: dict[str, object]) -> list[dict[str, object]]:
23
+ jobs = manifest.get("jobs")
24
+ if not isinstance(jobs, list):
25
+ raise SystemExit("invalid imagegen-jobs.json: jobs must be a list")
26
+ return [job for job in jobs if isinstance(job, dict)]
27
+
28
+
29
+ def find_job(manifest: dict[str, object], job_id: str) -> dict[str, object]:
30
+ for job in job_list(manifest):
31
+ if job.get("id") == job_id:
32
+ return job
33
+ raise SystemExit(f"unknown job id: {job_id}")
34
+
35
+
36
+ def file_sha256(path: Path) -> str:
37
+ digest = hashlib.sha256()
38
+ with path.open("rb") as file:
39
+ for chunk in iter(lambda: file.read(1024 * 1024), b""):
40
+ digest.update(chunk)
41
+ return digest.hexdigest()
42
+
43
+
44
+ def image_metadata(path: Path) -> dict[str, object]:
45
+ with Image.open(path) as image:
46
+ image.verify()
47
+ with Image.open(path) as image:
48
+ return {
49
+ "width": image.width,
50
+ "height": image.height,
51
+ "mode": image.mode,
52
+ "format": image.format,
53
+ }
54
+
55
+
56
+ def manifest_relative(path: Path, run_dir: Path) -> str:
57
+ return str(path.resolve().relative_to(run_dir.resolve()))
58
+
59
+
60
+ def main() -> None:
61
+ parser = argparse.ArgumentParser(description=__doc__)
62
+ parser.add_argument("--run-dir", required=True)
63
+ parser.add_argument(
64
+ "--confirm-appropriate-mirror",
65
+ action="store_true",
66
+ help="Required after visually confirming the rightward strip can be mirrored without identity/prop issues.",
67
+ )
68
+ parser.add_argument(
69
+ "--decision-note",
70
+ required=True,
71
+ help="Short note explaining why mirroring is acceptable for this pet.",
72
+ )
73
+ parser.add_argument("--force", action="store_true")
74
+ args = parser.parse_args()
75
+
76
+ if not args.confirm_appropriate_mirror:
77
+ raise SystemExit("refusing to mirror without --confirm-appropriate-mirror")
78
+ if not args.decision_note.strip():
79
+ raise SystemExit("--decision-note must explain why mirroring is appropriate")
80
+
81
+ run_dir = Path(args.run_dir).expanduser().resolve()
82
+ manifest_path = run_dir / "imagegen-jobs.json"
83
+ manifest = load_manifest(run_dir)
84
+ right_job = find_job(manifest, "running-right")
85
+ left_job = find_job(manifest, "running-left")
86
+
87
+ if right_job.get("status") != "complete":
88
+ raise SystemExit("running-right must be complete before deriving running-left")
89
+ mirror_policy = left_job.get("mirror_policy")
90
+ if not isinstance(mirror_policy, dict) or mirror_policy.get("may_derive_from") != "running-right":
91
+ raise SystemExit("running-left is not configured for conditional mirroring")
92
+
93
+ source = run_dir / "decoded" / "running-right.png"
94
+ output = run_dir / "decoded" / "running-left.png"
95
+ if not source.is_file():
96
+ raise SystemExit(f"running-right decoded strip not found: {source}")
97
+ if output.exists() and not args.force:
98
+ raise SystemExit(f"{output} already exists; pass --force to replace it")
99
+
100
+ output.parent.mkdir(parents=True, exist_ok=True)
101
+ with Image.open(source) as image:
102
+ mirrored = ImageOps.mirror(image.convert("RGBA"))
103
+ mirrored.save(output)
104
+
105
+ left_job["status"] = "complete"
106
+ left_job["source_path"] = manifest_relative(source, run_dir)
107
+ left_job["source_provenance"] = "deterministic-mirror"
108
+ left_job["derived_from"] = "running-right"
109
+ left_job["source_sha256"] = file_sha256(source)
110
+ left_job["output_sha256"] = file_sha256(output)
111
+ left_job["completed_at"] = datetime.now(timezone.utc).isoformat()
112
+ left_job["metadata"] = image_metadata(output)
113
+ left_job["mirror_decision"] = {
114
+ "approved": True,
115
+ "approved_at": left_job["completed_at"],
116
+ "note": args.decision_note.strip(),
117
+ }
118
+ for key in [
119
+ "last_error",
120
+ "secondary_fallback",
121
+ "synthetic_test_source",
122
+ "repair_reason",
123
+ "queued_at",
124
+ ]:
125
+ left_job.pop(key, None)
126
+
127
+ manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
128
+ print(
129
+ json.dumps(
130
+ {
131
+ "ok": True,
132
+ "job_id": "running-left",
133
+ "derived_from": "running-right",
134
+ "output": str(output),
135
+ "decision_note": args.decision_note.strip(),
136
+ },
137
+ indent=2,
138
+ )
139
+ )
140
+
141
+
142
+ if __name__ == "__main__":
143
+ main()