@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.
- package/bin/awk.js +576 -84
- package/core/CLAUDE.md +1 -1
- package/core/GEMINI.md +148 -167
- package/core/GEMINI.md.bak +149 -116
- package/core/skill-runtime-manifest.json +3 -0
- package/docs/Claude Fable 5.md +3826 -0
- package/docs/android_kotlin_system_instruction.md +210 -0
- package/docs/brainstorm_ponytail_integration.md +146 -0
- package/docs/brainstorm_smart_setup.md +113 -0
- package/docs/deep-research-report (1).md +293 -0
- package/docs/history/GEMINI.v1.md +135 -0
- package/docs/history/brainstorm_antigravity_unified_architecture.v1.md +105 -0
- package/docs/history/implementation_plan.v1.md +58 -0
- package/package.json +4 -1
- package/scripts/artifact-storage.js +130 -0
- package/scripts/automation-gate.js +35 -2
- package/scripts/claude-plan.js +76 -0
- package/scripts/dependency-manager.js +210 -0
- package/scripts/exec-rtk.js +11 -5
- package/scripts/i18n-helper.js +381 -0
- package/scripts/multi-model-pipeline.js +144 -0
- package/skill-packs/mobile-ios/pack.json +4 -2
- package/skill-packs/reverse-engineering/pack.json +1 -0
- package/skills/CATALOG.md +20 -0
- package/skills/GEMINI.md +9 -1
- package/skills/TRIGGER_INDEX.md +10 -0
- package/skills/ai-music/SKILL.md +275 -0
- package/skills/android-re-analyzer/SKILL.md +238 -0
- package/skills/android-re-analyzer/references/api-extraction-patterns.md +119 -0
- package/skills/android-re-analyzer/references/call-flow-analysis.md +176 -0
- package/skills/android-re-analyzer/references/fernflower-usage.md +115 -0
- package/skills/android-re-analyzer/references/jadx-usage.md +116 -0
- package/skills/android-re-analyzer/references/setup-guide.md +221 -0
- package/skills/android-re-analyzer/scripts/check-deps.sh +129 -0
- package/skills/android-re-analyzer/scripts/decompile.sh +375 -0
- package/skills/android-re-analyzer/scripts/find-api-calls.sh +118 -0
- package/skills/android-re-analyzer/scripts/install-dep.sh +448 -0
- package/skills/animal-island-ui-style/SKILL.md +1450 -0
- package/skills/app-store-review-agent/SKILL.md +164 -0
- package/skills/app-store-review-agent/references/guidelines/README.md +154 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/ai_apps.md +37 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/all_apps.md +50 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/crypto_finance.md +31 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/games.md +31 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/health_fitness.md +31 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/kids.md +27 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/macos.md +38 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/social_ugc.md +32 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/subscription_iap.md +34 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/vpn.md +18 -0
- package/skills/app-store-review-agent/references/rules/design/minimum_functionality.md +96 -0
- package/skills/app-store-review-agent/references/rules/design/sign_in_with_apple.md +54 -0
- package/skills/app-store-review-agent/references/rules/entitlements/unused_entitlements.md +83 -0
- package/skills/app-store-review-agent/references/rules/metadata/accurate_metadata.md +54 -0
- package/skills/app-store-review-agent/references/rules/metadata/apple_trademark.md +99 -0
- package/skills/app-store-review-agent/references/rules/metadata/china_storefront.md +72 -0
- package/skills/app-store-review-agent/references/rules/metadata/competitor_terms.md +56 -0
- package/skills/app-store-review-agent/references/rules/metadata/subscription_metadata.md +81 -0
- package/skills/app-store-review-agent/references/rules/privacy/privacy_manifest.md +84 -0
- package/skills/app-store-review-agent/references/rules/privacy/unnecessary_data.md +60 -0
- package/skills/app-store-review-agent/references/rules/subscription/misleading_pricing.md +63 -0
- package/skills/app-store-review-agent/references/rules/subscription/missing_tos_pp.md +54 -0
- package/skills/awf-ponytail/SKILL.md +91 -0
- package/skills/awf-ponytail-review/SKILL.md +67 -0
- package/skills/awf-session-restore/SKILL.md +3 -3
- package/skills/brainstorm-agent/SKILL.md +11 -2
- package/skills/brainstorm-agent/templates/brief-template.md +8 -0
- package/skills/claude-planner/SKILL.md +47 -0
- package/skills/code-review/SKILL.md +87 -0
- package/skills/expo-game-development/SKILL.md +163 -0
- package/skills/flutter/LICENSE.txt +202 -0
- package/skills/flutter/SKILL.md +127 -0
- package/skills/flutter-project-creater/LICENSE.txt +202 -0
- package/skills/flutter-project-creater/SKILL.md +106 -0
- package/skills/game-developer/SKILL.md +163 -0
- package/skills/game-developer/references/ecs-patterns.md +501 -0
- package/skills/game-developer/references/multiplayer-networking.md +475 -0
- package/skills/game-developer/references/performance-optimization.md +422 -0
- package/skills/game-developer/references/unity-patterns.md +271 -0
- package/skills/game-developer/references/unreal-cpp.md +352 -0
- package/skills/generate-gui-assets/SKILL.md +305 -0
- package/skills/generate-gui-assets/agents/openai.yaml +4 -0
- package/skills/generate-gui-assets/references/catalog-schema.md +58 -0
- package/skills/generate-gui-assets/references/extraction-techniques.md +21 -0
- package/skills/generate-gui-assets/references/prompt-patterns.md +58 -0
- package/skills/generate-gui-assets/scripts/__pycache__/clean_chroma_edges.cpython-311.pyc +0 -0
- package/skills/generate-gui-assets/scripts/build_gui_contact_sheet.py +51 -0
- package/skills/generate-gui-assets/scripts/clean_chroma_edges.py +262 -0
- package/skills/generate-gui-assets/scripts/copy_approved_icons.py +64 -0
- package/skills/generate-gui-assets/scripts/prepare_gui_asset_run.py +91 -0
- package/skills/generate-gui-assets/scripts/suggest_grid_options.py +63 -0
- package/skills/generate-gui-assets/scripts/validate_gui_catalog.py +50 -0
- package/skills/godot-game-development/SKILL.md +142 -0
- package/skills/hatch-pet/LICENSE.txt +201 -0
- package/skills/hatch-pet/SKILL.md +420 -0
- package/skills/hatch-pet/agents/openai.yaml +4 -0
- package/skills/hatch-pet/references/animation-rows.md +29 -0
- package/skills/hatch-pet/references/codex-pet-contract.md +35 -0
- package/skills/hatch-pet/references/qa-rubric.md +60 -0
- package/skills/hatch-pet/scripts/__pycache__/clean_chroma_edges.cpython-311.pyc +0 -0
- package/skills/hatch-pet/scripts/clean_chroma_edges.py +262 -0
- package/skills/hatch-pet/scripts/compose_atlas.py +150 -0
- package/skills/hatch-pet/scripts/derive_running_left_from_running_right.py +143 -0
- package/skills/hatch-pet/scripts/extract_strip_frames.py +323 -0
- package/skills/hatch-pet/scripts/finalize_pet_run.py +382 -0
- package/skills/hatch-pet/scripts/generate_pet_images.py +287 -0
- package/skills/hatch-pet/scripts/inspect_frames.py +246 -0
- package/skills/hatch-pet/scripts/make_contact_sheet.py +96 -0
- package/skills/hatch-pet/scripts/package_custom_pet.py +108 -0
- package/skills/hatch-pet/scripts/pet_job_status.py +117 -0
- package/skills/hatch-pet/scripts/prepare_pet_run.py +673 -0
- package/skills/hatch-pet/scripts/queue_pet_repairs.py +172 -0
- package/skills/hatch-pet/scripts/record_imagegen_result.py +250 -0
- package/skills/hatch-pet/scripts/render_animation_videos.py +134 -0
- package/skills/hatch-pet/scripts/render_animation_videos.sh +5 -0
- package/skills/hatch-pet/scripts/validate_atlas.py +139 -0
- package/skills/i18n-orchestrator/SKILL.md +37 -0
- package/skills/ios-simulator-skill/SKILL.md +390 -0
- package/skills/ios-simulator-skill/scripts/accessibility_audit.py +300 -0
- package/skills/ios-simulator-skill/scripts/app_launcher.py +326 -0
- package/skills/ios-simulator-skill/scripts/app_state_capture.py +400 -0
- package/skills/ios-simulator-skill/scripts/appearance.py +385 -0
- package/skills/ios-simulator-skill/scripts/build_and_test.py +348 -0
- package/skills/ios-simulator-skill/scripts/clipboard.py +103 -0
- package/skills/ios-simulator-skill/scripts/common/__init__.py +61 -0
- package/skills/ios-simulator-skill/scripts/common/cache_utils.py +289 -0
- package/skills/ios-simulator-skill/scripts/common/device_utils.py +462 -0
- package/skills/ios-simulator-skill/scripts/common/env_config.py +35 -0
- package/skills/ios-simulator-skill/scripts/common/hang_pipeline.py +862 -0
- package/skills/ios-simulator-skill/scripts/common/hang_sessions.py +490 -0
- package/skills/ios-simulator-skill/scripts/common/idb_utils.py +180 -0
- package/skills/ios-simulator-skill/scripts/common/screenshot_utils.py +338 -0
- package/skills/ios-simulator-skill/scripts/container.py +668 -0
- package/skills/ios-simulator-skill/scripts/gesture.py +394 -0
- package/skills/ios-simulator-skill/scripts/hang_watcher.py +1533 -0
- package/skills/ios-simulator-skill/scripts/keyboard.py +391 -0
- package/skills/ios-simulator-skill/scripts/localization_audit.py +483 -0
- package/skills/ios-simulator-skill/scripts/location.py +467 -0
- package/skills/ios-simulator-skill/scripts/log_monitor.py +493 -0
- package/skills/ios-simulator-skill/scripts/model_inspector.py +645 -0
- package/skills/ios-simulator-skill/scripts/navigator.py +461 -0
- package/skills/ios-simulator-skill/scripts/privacy_manager.py +310 -0
- package/skills/ios-simulator-skill/scripts/push_notification.py +240 -0
- package/skills/ios-simulator-skill/scripts/screen_mapper.py +296 -0
- package/skills/ios-simulator-skill/scripts/sim_health_check.sh +245 -0
- package/skills/ios-simulator-skill/scripts/sim_list.py +299 -0
- package/skills/ios-simulator-skill/scripts/simctl_boot.py +312 -0
- package/skills/ios-simulator-skill/scripts/simctl_create.py +316 -0
- package/skills/ios-simulator-skill/scripts/simctl_delete.py +357 -0
- package/skills/ios-simulator-skill/scripts/simctl_erase.py +351 -0
- package/skills/ios-simulator-skill/scripts/simctl_shutdown.py +290 -0
- package/skills/ios-simulator-skill/scripts/simulator_selector.py +375 -0
- package/skills/ios-simulator-skill/scripts/status_bar.py +250 -0
- package/skills/ios-simulator-skill/scripts/test_recorder.py +323 -0
- package/skills/ios-simulator-skill/scripts/visual_diff.py +235 -0
- package/skills/ios-simulator-skill/scripts/xcode/__init__.py +13 -0
- package/skills/ios-simulator-skill/scripts/xcode/builder.py +397 -0
- package/skills/ios-simulator-skill/scripts/xcode/cache.py +204 -0
- package/skills/ios-simulator-skill/scripts/xcode/config.py +178 -0
- package/skills/ios-simulator-skill/scripts/xcode/reporter.py +343 -0
- package/skills/ios-simulator-skill/scripts/xcode/xcresult.py +451 -0
- package/skills/ios-visual-qa-strategist/SKILL.md +111 -0
- package/skills/ios-visual-qa-strategist/agents/openai.yaml +4 -0
- package/skills/ios-visual-qa-strategist/references/ios-tool-selection.md +61 -0
- package/skills/ios-visual-qa-strategist/references/minimal-capture-policy.md +56 -0
- package/skills/ios-visual-qa-strategist/references/visual-reasoning-heuristics.md +53 -0
- package/skills/orchestrator/SKILL.md +0 -20
- package/skills/persistent-storage/SKILL.md +55 -0
- package/skills/short-maker/SKILL.md +23 -0
- package/skills/short-maker/scripts/effects.js +56 -0
- package/skills/short-maker/scripts/shortmaker-bridge.js +332 -0
- package/skills/short-maker/scripts/videomix.js +601 -0
- package/skills/short-maker/templates/hyperframes/cinematic-character.template.html +172 -0
- package/skills/short-maker/templates/hyperframes/index.template.html +194 -0
- package/skills/smali-to-kotlin/SKILL.md +128 -0
- package/skills/smali-to-kotlin/examples/getting-started/tech-stack.md +58 -0
- package/skills/smali-to-kotlin/examples/pipeline/data-ui-parity.md +118 -0
- package/skills/smali-to-kotlin/examples/pipeline/scanner-and-bootstrap.md +106 -0
- package/skills/smali-to-kotlin/library-patterns.md +189 -0
- package/skills/smali-to-kotlin/phase-0-discovery.md +128 -0
- package/skills/smali-to-kotlin/phase-1-architecture.md +166 -0
- package/skills/smali-to-kotlin/phase-2-blueprint-ui.md +347 -0
- package/skills/smali-to-kotlin/phase-2-blueprint.md +228 -0
- package/skills/smali-to-kotlin/phase-3-build.md +248 -0
- package/skills/smali-to-kotlin/phase-3-logic-build.md +268 -0
- package/skills/smali-to-kotlin/smali-reading-guide.md +310 -0
- package/skills/smali-to-kotlin/templates/app-map.md +101 -0
- package/skills/smali-to-kotlin/templates/architecture.md +142 -0
- package/skills/smali-to-kotlin/templates/blueprint.md +145 -0
- package/skills/spec-gate/SKILL.md +6 -2
- package/skills/symphony-enforcer/SKILL.md +8 -0
- package/skills/symphony-enforcer/examples/mindful-stop.md +2 -0
- package/skills/symphony-enforcer/examples/three-phase.md +16 -0
- package/skills/symphony-enforcer/examples/trigger-points.md +7 -1
- package/skills/unity-game-development/SKILL.md +231 -0
- package/skills/video-edit/SKILL.md +36 -0
- package/skills/video-edit/scripts/video_edit.py +324 -0
- package/templates/project-identity/android.json +2 -2
- package/templates/project-identity/backend-nestjs.json +2 -2
- package/templates/project-identity/expo.json +2 -2
- package/templates/project-identity/ios.json +2 -2
- package/templates/project-identity/web-nextjs.json +2 -2
- package/templates/setup-mapping.json +48 -0
- package/templates/specs/design-template.md +161 -71
- package/templates/specs/requirements-template.md +65 -133
- package/templates/specs/task-spec-template.xml +3 -0
- package/workflows/_uncategorized/critic.md +40 -0
- package/workflows/_uncategorized/git-rebase-flow.md +81 -0
- package/workflows/_uncategorized/image-gen.md +118 -0
- package/workflows/_uncategorized/multi-model-pipeline.md +60 -0
- package/workflows/_uncategorized/pixel-gen.md +86 -0
- package/workflows/_uncategorized/pixel-setup.md +90 -0
- package/workflows/_uncategorized/ponytail-review.md +59 -0
- package/workflows/_uncategorized/reverse-android-build.md +222 -0
- package/workflows/_uncategorized/reverse-android-design.md +139 -0
- package/workflows/_uncategorized/reverse-android-discover.md +150 -0
- package/workflows/_uncategorized/reverse-android-scan.md +158 -0
- package/workflows/_uncategorized/reverse-android.md +143 -0
- package/workflows/_uncategorized/reverse-ios-build.md +240 -0
- package/workflows/_uncategorized/reverse-ios-design.md +112 -0
- package/workflows/_uncategorized/reverse-ios-discover.md +120 -0
- package/workflows/_uncategorized/reverse-ios-scan.md +155 -0
- package/workflows/_uncategorized/reverse-ios.md +152 -0
- package/workflows/_uncategorized/safety-router.md +34 -0
- package/workflows/_uncategorized/teach.md +89 -0
- package/workflows/_uncategorized/verify-ui.md +53 -0
- package/workflows/_uncategorized/visualize-screenshots.md +34 -0
- package/workflows/ads/ads-analyst.md +201 -0
- package/workflows/ads/ads-audit.md +106 -0
- package/workflows/ads/ads-optimize.md +97 -0
- package/workflows/ads/ads-targeting.md +241 -0
- package/workflows/ads/adsExpert.md +160 -0
- package/workflows/ads/smali-ads-config.md +400 -0
- package/workflows/ads/smali-ads-flow.md +331 -0
- package/workflows/ads/smali-ads-interstitial.md +377 -0
- package/workflows/ads/smali-ads-native.md +382 -0
- package/workflows/context/teach.md +89 -0
- package/workflows/gitnexus.md +8 -8
- package/workflows/lifecycle/brainstorm.md +43 -0
- package/workflows/lifecycle/code.md +5 -0
- package/workflows/lifecycle/init.md +23 -5
- package/workflows/lifecycle/multi-model-pipeline.md +60 -0
- package/workflows/quality/ponytail-review.md +59 -0
- package/workflows/roles/critic.md +40 -0
- package/workflows/roles/safety-router.md +34 -0
|
@@ -0,0 +1,323 @@
|
|
|
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] = (red, green, blue, 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 connected_components(image: Image.Image) -> list[dict[str, object]]:
|
|
103
|
+
alpha = image.getchannel("A")
|
|
104
|
+
width, height = image.size
|
|
105
|
+
data = alpha.tobytes()
|
|
106
|
+
visited = bytearray(width * height)
|
|
107
|
+
components: list[dict[str, object]] = []
|
|
108
|
+
|
|
109
|
+
for start, alpha_value in enumerate(data):
|
|
110
|
+
if alpha_value <= 16 or visited[start]:
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
stack = [start]
|
|
114
|
+
visited[start] = 1
|
|
115
|
+
pixels: list[int] = []
|
|
116
|
+
min_x = width
|
|
117
|
+
min_y = height
|
|
118
|
+
max_x = 0
|
|
119
|
+
max_y = 0
|
|
120
|
+
|
|
121
|
+
while stack:
|
|
122
|
+
current = stack.pop()
|
|
123
|
+
pixels.append(current)
|
|
124
|
+
x = current % width
|
|
125
|
+
y = current // width
|
|
126
|
+
min_x = min(min_x, x)
|
|
127
|
+
min_y = min(min_y, y)
|
|
128
|
+
max_x = max(max_x, x)
|
|
129
|
+
max_y = max(max_y, y)
|
|
130
|
+
|
|
131
|
+
if x > 0:
|
|
132
|
+
neighbor = current - 1
|
|
133
|
+
if not visited[neighbor] and data[neighbor] > 16:
|
|
134
|
+
visited[neighbor] = 1
|
|
135
|
+
stack.append(neighbor)
|
|
136
|
+
if x + 1 < width:
|
|
137
|
+
neighbor = current + 1
|
|
138
|
+
if not visited[neighbor] and data[neighbor] > 16:
|
|
139
|
+
visited[neighbor] = 1
|
|
140
|
+
stack.append(neighbor)
|
|
141
|
+
if y > 0:
|
|
142
|
+
neighbor = current - width
|
|
143
|
+
if not visited[neighbor] and data[neighbor] > 16:
|
|
144
|
+
visited[neighbor] = 1
|
|
145
|
+
stack.append(neighbor)
|
|
146
|
+
if y + 1 < height:
|
|
147
|
+
neighbor = current + width
|
|
148
|
+
if not visited[neighbor] and data[neighbor] > 16:
|
|
149
|
+
visited[neighbor] = 1
|
|
150
|
+
stack.append(neighbor)
|
|
151
|
+
|
|
152
|
+
components.append(
|
|
153
|
+
{
|
|
154
|
+
"pixels": pixels,
|
|
155
|
+
"area": len(pixels),
|
|
156
|
+
"bbox": (min_x, min_y, max_x + 1, max_y + 1),
|
|
157
|
+
"center_x": (min_x + max_x + 1) / 2,
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return components
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def component_group_image(
|
|
165
|
+
source: Image.Image,
|
|
166
|
+
components: list[dict[str, object]],
|
|
167
|
+
padding: int = 4,
|
|
168
|
+
) -> Image.Image:
|
|
169
|
+
width, height = source.size
|
|
170
|
+
min_x = max(0, min(component["bbox"][0] for component in components) - padding)
|
|
171
|
+
min_y = max(0, min(component["bbox"][1] for component in components) - padding)
|
|
172
|
+
max_x = min(width, max(component["bbox"][2] for component in components) + padding)
|
|
173
|
+
max_y = min(height, max(component["bbox"][3] for component in components) + padding)
|
|
174
|
+
|
|
175
|
+
output = Image.new("RGBA", (max_x - min_x, max_y - min_y), (0, 0, 0, 0))
|
|
176
|
+
source_pixels = source.load()
|
|
177
|
+
output_pixels = output.load()
|
|
178
|
+
for component in components:
|
|
179
|
+
for pixel_index in component["pixels"]:
|
|
180
|
+
x = pixel_index % width
|
|
181
|
+
y = pixel_index // width
|
|
182
|
+
output_pixels[x - min_x, y - min_y] = source_pixels[x, y]
|
|
183
|
+
return output
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def extract_component_frames(strip: Image.Image, frame_count: int) -> list[Image.Image] | None:
|
|
187
|
+
components = connected_components(strip)
|
|
188
|
+
if not components:
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
largest_area = max(component["area"] for component in components)
|
|
192
|
+
seed_threshold = max(120, largest_area * 0.20)
|
|
193
|
+
seeds = [component for component in components if component["area"] >= seed_threshold]
|
|
194
|
+
if len(seeds) < frame_count:
|
|
195
|
+
seeds = sorted(components, key=lambda component: component["area"], reverse=True)[
|
|
196
|
+
:frame_count
|
|
197
|
+
]
|
|
198
|
+
if len(seeds) < frame_count:
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
seeds = sorted(
|
|
202
|
+
sorted(seeds, key=lambda component: component["area"], reverse=True)[:frame_count],
|
|
203
|
+
key=lambda component: component["center_x"],
|
|
204
|
+
)
|
|
205
|
+
seed_ids = {id(seed) for seed in seeds}
|
|
206
|
+
groups: list[list[dict[str, object]]] = [[seed] for seed in seeds]
|
|
207
|
+
noise_threshold = max(12, largest_area * 0.002)
|
|
208
|
+
|
|
209
|
+
for component in components:
|
|
210
|
+
if id(component) in seed_ids or component["area"] < noise_threshold:
|
|
211
|
+
continue
|
|
212
|
+
nearest_index = min(
|
|
213
|
+
range(len(seeds)),
|
|
214
|
+
key=lambda index: abs(seeds[index]["center_x"] - component["center_x"]),
|
|
215
|
+
)
|
|
216
|
+
groups[nearest_index].append(component)
|
|
217
|
+
|
|
218
|
+
return [fit_to_cell(component_group_image(strip, group)) for group in groups]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def extract_slot_frames(strip: Image.Image, frame_count: int) -> list[Image.Image]:
|
|
222
|
+
slot_width = strip.width / frame_count
|
|
223
|
+
frames = []
|
|
224
|
+
for index in range(frame_count):
|
|
225
|
+
left = round(index * slot_width)
|
|
226
|
+
right = round((index + 1) * slot_width)
|
|
227
|
+
crop = strip.crop((left, 0, right, strip.height))
|
|
228
|
+
frames.append(fit_to_cell(crop))
|
|
229
|
+
return frames
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def extract_state(
|
|
233
|
+
strip_path: Path,
|
|
234
|
+
state: str,
|
|
235
|
+
output_root: Path,
|
|
236
|
+
chroma_key: tuple[int, int, int],
|
|
237
|
+
threshold: float,
|
|
238
|
+
method: str,
|
|
239
|
+
) -> dict[str, object]:
|
|
240
|
+
frame_count = ROW_FRAME_COUNTS[state]
|
|
241
|
+
with Image.open(strip_path) as opened:
|
|
242
|
+
strip = remove_chroma_background(opened, chroma_key, threshold)
|
|
243
|
+
|
|
244
|
+
state_dir = output_root / state
|
|
245
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
246
|
+
|
|
247
|
+
frames = None
|
|
248
|
+
used_method = method
|
|
249
|
+
if method in {"auto", "components"}:
|
|
250
|
+
frames = extract_component_frames(strip, frame_count)
|
|
251
|
+
if frames is None and method == "components":
|
|
252
|
+
raise SystemExit(f"could not find {frame_count} sprite components in {strip_path}")
|
|
253
|
+
if frames is not None:
|
|
254
|
+
used_method = "components"
|
|
255
|
+
|
|
256
|
+
if frames is None:
|
|
257
|
+
frames = extract_slot_frames(strip, frame_count)
|
|
258
|
+
used_method = "slots"
|
|
259
|
+
|
|
260
|
+
outputs = []
|
|
261
|
+
for index, frame in enumerate(frames):
|
|
262
|
+
output = state_dir / f"{index:02d}.png"
|
|
263
|
+
frame.save(output)
|
|
264
|
+
outputs.append(str(output))
|
|
265
|
+
return {"state": state, "frames": outputs, "method": used_method}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def main() -> None:
|
|
269
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
270
|
+
parser.add_argument("--decoded-dir", required=True)
|
|
271
|
+
parser.add_argument("--output-dir", required=True)
|
|
272
|
+
parser.add_argument("--states", default="all")
|
|
273
|
+
parser.add_argument("--chroma-key", help="Override chroma key as #RRGGBB.")
|
|
274
|
+
parser.add_argument("--key-threshold", type=float, default=96.0)
|
|
275
|
+
parser.add_argument(
|
|
276
|
+
"--method",
|
|
277
|
+
choices=("auto", "components", "slots"),
|
|
278
|
+
default="auto",
|
|
279
|
+
help="Use connected sprite components when possible, or fixed equal slots.",
|
|
280
|
+
)
|
|
281
|
+
args = parser.parse_args()
|
|
282
|
+
|
|
283
|
+
decoded_dir = Path(args.decoded_dir).expanduser().resolve()
|
|
284
|
+
output_dir = Path(args.output_dir).expanduser().resolve()
|
|
285
|
+
chroma_key = load_chroma_key(decoded_dir, args.chroma_key)
|
|
286
|
+
states = parse_states(args.states)
|
|
287
|
+
manifest = []
|
|
288
|
+
for state in states:
|
|
289
|
+
strip_path = decoded_dir / f"{state}.png"
|
|
290
|
+
if not strip_path.is_file():
|
|
291
|
+
raise SystemExit(f"missing generated strip for {state}: {strip_path}")
|
|
292
|
+
manifest.append(
|
|
293
|
+
extract_state(
|
|
294
|
+
strip_path,
|
|
295
|
+
state,
|
|
296
|
+
output_dir,
|
|
297
|
+
chroma_key,
|
|
298
|
+
args.key_threshold,
|
|
299
|
+
args.method,
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
(output_dir / "frames-manifest.json").write_text(
|
|
304
|
+
json.dumps(
|
|
305
|
+
{
|
|
306
|
+
"ok": True,
|
|
307
|
+
"chroma_key": {
|
|
308
|
+
"hex": f"#{chroma_key[0]:02X}{chroma_key[1]:02X}{chroma_key[2]:02X}",
|
|
309
|
+
"rgb": list(chroma_key),
|
|
310
|
+
"threshold": args.key_threshold,
|
|
311
|
+
},
|
|
312
|
+
"rows": manifest,
|
|
313
|
+
},
|
|
314
|
+
indent=2,
|
|
315
|
+
)
|
|
316
|
+
+ "\n",
|
|
317
|
+
encoding="utf-8",
|
|
318
|
+
)
|
|
319
|
+
print(json.dumps({"ok": True, "frames_root": str(output_dir), "states": states}, indent=2))
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
if __name__ == "__main__":
|
|
323
|
+
main()
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Finalize a Codex pet run after all imagegen jobs are complete."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from PIL import Image, ImageOps
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run(command: list[str], *, check: bool = True) -> subprocess.CompletedProcess[str]:
|
|
18
|
+
print("+ " + " ".join(command))
|
|
19
|
+
return subprocess.run(command, check=check, text=True)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_json(path: Path) -> dict[str, object]:
|
|
23
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def file_sha256(path: Path) -> str:
|
|
27
|
+
digest = hashlib.sha256()
|
|
28
|
+
with path.open("rb") as file:
|
|
29
|
+
for chunk in iter(lambda: file.read(1024 * 1024), b""):
|
|
30
|
+
digest.update(chunk)
|
|
31
|
+
return digest.hexdigest()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def is_relative_to(path: Path, root: Path) -> bool:
|
|
35
|
+
try:
|
|
36
|
+
path.relative_to(root)
|
|
37
|
+
except ValueError:
|
|
38
|
+
return False
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def default_generated_images_root() -> Path:
|
|
43
|
+
return default_codex_home() / "generated_images"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def default_codex_home() -> Path:
|
|
47
|
+
return Path(os.environ.get("CODEX_HOME") or "~/.codex").expanduser().resolve()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def manifest_path(raw: object, *, run_dir: Path, field: str, job_id: str) -> Path:
|
|
51
|
+
if not isinstance(raw, str) or not raw:
|
|
52
|
+
raise SystemExit(f"job {job_id} has no {field}")
|
|
53
|
+
path = Path(raw).expanduser()
|
|
54
|
+
if not path.is_absolute():
|
|
55
|
+
path = run_dir / path
|
|
56
|
+
return path.resolve()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def validate_hash(job: dict[str, object], *, source: Path, output: Path, job_id: str) -> None:
|
|
60
|
+
expected_hash = job.get("source_sha256")
|
|
61
|
+
if not isinstance(expected_hash, str) or not expected_hash:
|
|
62
|
+
raise SystemExit(
|
|
63
|
+
f"job {job_id} is missing source_sha256; ingest visual outputs with "
|
|
64
|
+
"record_imagegen_result.py instead of editing imagegen-jobs.json"
|
|
65
|
+
)
|
|
66
|
+
if not source.is_file():
|
|
67
|
+
raise SystemExit(f"job {job_id} source image no longer exists: {source}")
|
|
68
|
+
if not output.is_file():
|
|
69
|
+
raise SystemExit(f"job {job_id} decoded output is missing: {output}")
|
|
70
|
+
source_hash = file_sha256(source)
|
|
71
|
+
output_hash = file_sha256(output)
|
|
72
|
+
if source_hash != expected_hash:
|
|
73
|
+
raise SystemExit(f"job {job_id} source image hash does not match imagegen-jobs.json")
|
|
74
|
+
if output_hash != expected_hash:
|
|
75
|
+
raise SystemExit(
|
|
76
|
+
f"job {job_id} decoded output does not match its recorded source image; "
|
|
77
|
+
"do not rewrite decoded visual outputs locally"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def validate_mirror_hash(job: dict[str, object], *, source: Path, output: Path, job_id: str) -> None:
|
|
82
|
+
if job_id != "running-left":
|
|
83
|
+
raise SystemExit(f"job {job_id} may not use deterministic mirror provenance")
|
|
84
|
+
if job.get("derived_from") != "running-right":
|
|
85
|
+
raise SystemExit("running-left mirror job must derive from running-right")
|
|
86
|
+
decision = job.get("mirror_decision")
|
|
87
|
+
if not isinstance(decision, dict) or decision.get("approved") is not True:
|
|
88
|
+
raise SystemExit(
|
|
89
|
+
"running-left mirror job is missing an approved mirror_decision; "
|
|
90
|
+
"use derive_running_left_from_running_right.py after visual review"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
expected_source_hash = job.get("source_sha256")
|
|
94
|
+
expected_output_hash = job.get("output_sha256")
|
|
95
|
+
if not isinstance(expected_source_hash, str) or not expected_source_hash:
|
|
96
|
+
raise SystemExit("running-left mirror job is missing source_sha256")
|
|
97
|
+
if not isinstance(expected_output_hash, str) or not expected_output_hash:
|
|
98
|
+
raise SystemExit("running-left mirror job is missing output_sha256")
|
|
99
|
+
if not source.is_file():
|
|
100
|
+
raise SystemExit(f"running-left mirror source image no longer exists: {source}")
|
|
101
|
+
if not output.is_file():
|
|
102
|
+
raise SystemExit(f"running-left mirrored output is missing: {output}")
|
|
103
|
+
if source.name != "running-right.png" or source.parent.name != "decoded":
|
|
104
|
+
raise SystemExit("running-left mirror source must be decoded/running-right.png")
|
|
105
|
+
if output.name != "running-left.png" or output.parent.name != "decoded":
|
|
106
|
+
raise SystemExit("running-left mirror output must be decoded/running-left.png")
|
|
107
|
+
if file_sha256(source) != expected_source_hash:
|
|
108
|
+
raise SystemExit("running-left mirror source hash does not match imagegen-jobs.json")
|
|
109
|
+
if file_sha256(output) != expected_output_hash:
|
|
110
|
+
raise SystemExit(
|
|
111
|
+
"running-left mirrored output hash does not match imagegen-jobs.json; "
|
|
112
|
+
"rerun derive_running_left_from_running_right.py"
|
|
113
|
+
)
|
|
114
|
+
with Image.open(source) as source_image, Image.open(output) as output_image:
|
|
115
|
+
expected = ImageOps.mirror(source_image.convert("RGBA"))
|
|
116
|
+
actual = output_image.convert("RGBA")
|
|
117
|
+
if expected.size != actual.size or expected.tobytes() != actual.tobytes():
|
|
118
|
+
raise SystemExit(
|
|
119
|
+
"running-left mirrored output is not an exact horizontal mirror of running-right"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def validate_completed_job_source(
|
|
124
|
+
job: dict[str, object],
|
|
125
|
+
*,
|
|
126
|
+
run_dir: Path,
|
|
127
|
+
allow_synthetic_test_sources: bool,
|
|
128
|
+
) -> None:
|
|
129
|
+
job_id = str(job.get("id") or "")
|
|
130
|
+
source = manifest_path(job.get("source_path"), run_dir=run_dir, field="source_path", job_id=job_id)
|
|
131
|
+
output = manifest_path(job.get("output_path"), run_dir=run_dir, field="output_path", job_id=job_id)
|
|
132
|
+
|
|
133
|
+
blocked_flags = [
|
|
134
|
+
flag
|
|
135
|
+
for flag in ("deterministic_pet_row", "cute_raster_row", "local_raster_row")
|
|
136
|
+
if job.get(flag)
|
|
137
|
+
]
|
|
138
|
+
if blocked_flags:
|
|
139
|
+
raise SystemExit(
|
|
140
|
+
f"job {job_id} was marked as a local/synthetic row ({', '.join(blocked_flags)}); "
|
|
141
|
+
"regenerate it with $imagegen"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if job.get("synthetic_test_source"):
|
|
145
|
+
if not allow_synthetic_test_sources:
|
|
146
|
+
raise SystemExit(
|
|
147
|
+
f"job {job_id} uses a synthetic test source; rerun with real $imagegen output"
|
|
148
|
+
)
|
|
149
|
+
validate_hash(job, source=source, output=output, job_id=job_id)
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
if job.get("secondary_fallback"):
|
|
153
|
+
if job.get("source_provenance") != "secondary-fallback-image-api":
|
|
154
|
+
raise SystemExit(f"job {job_id} has invalid secondary fallback provenance")
|
|
155
|
+
validate_hash(job, source=source, output=output, job_id=job_id)
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
if job.get("source_provenance") == "deterministic-mirror":
|
|
159
|
+
validate_mirror_hash(job, source=source, output=output, job_id=job_id)
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
if job.get("source_provenance") != "built-in-imagegen":
|
|
163
|
+
raise SystemExit(
|
|
164
|
+
f"job {job_id} was not recorded as a built-in $imagegen output; "
|
|
165
|
+
"use record_imagegen_result.py with the selected $CODEX_HOME/generated_images/.../ig_*.png file"
|
|
166
|
+
)
|
|
167
|
+
if is_relative_to(source, run_dir):
|
|
168
|
+
raise SystemExit(
|
|
169
|
+
f"job {job_id} source image is inside the pet run directory; "
|
|
170
|
+
"do not use locally generated row artifacts as visual sources"
|
|
171
|
+
)
|
|
172
|
+
generated_root = default_generated_images_root()
|
|
173
|
+
if not is_relative_to(source, generated_root) or not source.name.startswith("ig_"):
|
|
174
|
+
raise SystemExit(
|
|
175
|
+
f"job {job_id} source image is not a built-in $imagegen output under "
|
|
176
|
+
f"{generated_root}/.../ig_*.png"
|
|
177
|
+
)
|
|
178
|
+
validate_hash(job, source=source, output=output, job_id=job_id)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def require_complete_jobs(run_dir: Path, *, allow_synthetic_test_sources: bool) -> None:
|
|
182
|
+
manifest_path = run_dir / "imagegen-jobs.json"
|
|
183
|
+
manifest = load_json(manifest_path)
|
|
184
|
+
jobs = manifest.get("jobs")
|
|
185
|
+
if not isinstance(jobs, list):
|
|
186
|
+
raise SystemExit("invalid imagegen-jobs.json: jobs must be a list")
|
|
187
|
+
incomplete = [
|
|
188
|
+
str(job.get("id"))
|
|
189
|
+
for job in jobs
|
|
190
|
+
if isinstance(job, dict) and job.get("status", "pending") != "complete"
|
|
191
|
+
]
|
|
192
|
+
if incomplete:
|
|
193
|
+
raise SystemExit(
|
|
194
|
+
"imagegen jobs are not complete; run pet_job_status.py and finish: "
|
|
195
|
+
+ ", ".join(incomplete)
|
|
196
|
+
)
|
|
197
|
+
for job in jobs:
|
|
198
|
+
if isinstance(job, dict):
|
|
199
|
+
validate_completed_job_source(
|
|
200
|
+
job,
|
|
201
|
+
run_dir=run_dir,
|
|
202
|
+
allow_synthetic_test_sources=allow_synthetic_test_sources,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def review_failures(review: dict[str, object]) -> list[str]:
|
|
207
|
+
rows = review.get("rows")
|
|
208
|
+
if not isinstance(rows, list):
|
|
209
|
+
return ["review did not contain row-level results"]
|
|
210
|
+
failures = []
|
|
211
|
+
for row in rows:
|
|
212
|
+
if not isinstance(row, dict):
|
|
213
|
+
continue
|
|
214
|
+
errors = row.get("errors")
|
|
215
|
+
if isinstance(errors, list) and errors:
|
|
216
|
+
failures.append(f"{row.get('state')}: {'; '.join(str(error) for error in errors)}")
|
|
217
|
+
return failures
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def main() -> None:
|
|
221
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
222
|
+
parser.add_argument("--run-dir", required=True)
|
|
223
|
+
parser.add_argument("--allow-slot-extraction", action="store_true")
|
|
224
|
+
parser.add_argument("--skip-videos", action="store_true")
|
|
225
|
+
parser.add_argument("--skip-package", action="store_true")
|
|
226
|
+
parser.add_argument(
|
|
227
|
+
"--package-dir",
|
|
228
|
+
default="",
|
|
229
|
+
help="Exact pet package directory. Defaults to ${CODEX_HOME:-$HOME/.codex}/pets/<pet-name>.",
|
|
230
|
+
)
|
|
231
|
+
parser.add_argument("--ffmpeg", default="")
|
|
232
|
+
parser.add_argument("--allow-synthetic-test-sources", action="store_true", help=argparse.SUPPRESS)
|
|
233
|
+
args = parser.parse_args()
|
|
234
|
+
|
|
235
|
+
scripts_dir = Path(__file__).resolve().parent
|
|
236
|
+
run_dir = Path(args.run_dir).expanduser().resolve()
|
|
237
|
+
request = load_json(run_dir / "pet_request.json")
|
|
238
|
+
pet_id = str(request.get("pet_id") or "")
|
|
239
|
+
display_name = str(request.get("display_name") or "")
|
|
240
|
+
description = str(request.get("description") or "")
|
|
241
|
+
if not pet_id or not display_name or not description:
|
|
242
|
+
raise SystemExit("pet_request.json is missing pet_id, display_name, or description")
|
|
243
|
+
|
|
244
|
+
require_complete_jobs(
|
|
245
|
+
run_dir,
|
|
246
|
+
allow_synthetic_test_sources=args.allow_synthetic_test_sources,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
final_dir = run_dir / "final"
|
|
250
|
+
qa_dir = run_dir / "qa"
|
|
251
|
+
final_dir.mkdir(parents=True, exist_ok=True)
|
|
252
|
+
qa_dir.mkdir(parents=True, exist_ok=True)
|
|
253
|
+
|
|
254
|
+
run(
|
|
255
|
+
[
|
|
256
|
+
sys.executable,
|
|
257
|
+
str(scripts_dir / "extract_strip_frames.py"),
|
|
258
|
+
"--decoded-dir",
|
|
259
|
+
str(run_dir / "decoded"),
|
|
260
|
+
"--output-dir",
|
|
261
|
+
str(run_dir / "frames"),
|
|
262
|
+
"--states",
|
|
263
|
+
"all",
|
|
264
|
+
"--method",
|
|
265
|
+
"auto",
|
|
266
|
+
]
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
review_path = qa_dir / "review.json"
|
|
270
|
+
inspect_command = [
|
|
271
|
+
sys.executable,
|
|
272
|
+
str(scripts_dir / "inspect_frames.py"),
|
|
273
|
+
"--frames-root",
|
|
274
|
+
str(run_dir / "frames"),
|
|
275
|
+
"--json-out",
|
|
276
|
+
str(review_path),
|
|
277
|
+
]
|
|
278
|
+
if not args.allow_slot_extraction:
|
|
279
|
+
inspect_command.append("--require-components")
|
|
280
|
+
run(inspect_command, check=False)
|
|
281
|
+
review = load_json(review_path)
|
|
282
|
+
if not review.get("ok"):
|
|
283
|
+
failures = review_failures(review)
|
|
284
|
+
print(
|
|
285
|
+
json.dumps(
|
|
286
|
+
{
|
|
287
|
+
"ok": False,
|
|
288
|
+
"review": str(review_path),
|
|
289
|
+
"repair_hint": "Run queue_pet_repairs.py, regenerate the reopened row jobs with $imagegen, then finalize again.",
|
|
290
|
+
"failures": failures,
|
|
291
|
+
},
|
|
292
|
+
indent=2,
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
raise SystemExit(1)
|
|
296
|
+
|
|
297
|
+
run(
|
|
298
|
+
[
|
|
299
|
+
sys.executable,
|
|
300
|
+
str(scripts_dir / "compose_atlas.py"),
|
|
301
|
+
"--frames-root",
|
|
302
|
+
str(run_dir / "frames"),
|
|
303
|
+
"--output",
|
|
304
|
+
str(final_dir / "spritesheet.png"),
|
|
305
|
+
"--webp-output",
|
|
306
|
+
str(final_dir / "spritesheet.webp"),
|
|
307
|
+
]
|
|
308
|
+
)
|
|
309
|
+
run(
|
|
310
|
+
[
|
|
311
|
+
sys.executable,
|
|
312
|
+
str(scripts_dir / "validate_atlas.py"),
|
|
313
|
+
str(final_dir / "spritesheet.webp"),
|
|
314
|
+
"--json-out",
|
|
315
|
+
str(final_dir / "validation.json"),
|
|
316
|
+
]
|
|
317
|
+
)
|
|
318
|
+
run(
|
|
319
|
+
[
|
|
320
|
+
sys.executable,
|
|
321
|
+
str(scripts_dir / "make_contact_sheet.py"),
|
|
322
|
+
str(final_dir / "spritesheet.webp"),
|
|
323
|
+
"--output",
|
|
324
|
+
str(qa_dir / "contact-sheet.png"),
|
|
325
|
+
]
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
if not args.skip_videos:
|
|
329
|
+
video_command = [
|
|
330
|
+
sys.executable,
|
|
331
|
+
str(scripts_dir / "render_animation_videos.py"),
|
|
332
|
+
str(final_dir / "spritesheet.webp"),
|
|
333
|
+
"--output-dir",
|
|
334
|
+
str(qa_dir / "videos"),
|
|
335
|
+
]
|
|
336
|
+
if args.ffmpeg:
|
|
337
|
+
video_command.extend(["--ffmpeg", args.ffmpeg])
|
|
338
|
+
run(video_command)
|
|
339
|
+
|
|
340
|
+
if not args.skip_package:
|
|
341
|
+
package_command = [
|
|
342
|
+
sys.executable,
|
|
343
|
+
str(scripts_dir / "package_custom_pet.py"),
|
|
344
|
+
"--pet-name",
|
|
345
|
+
pet_id,
|
|
346
|
+
"--display-name",
|
|
347
|
+
display_name,
|
|
348
|
+
"--description",
|
|
349
|
+
description,
|
|
350
|
+
"--spritesheet",
|
|
351
|
+
str(final_dir / "spritesheet.webp"),
|
|
352
|
+
"--force",
|
|
353
|
+
]
|
|
354
|
+
if args.package_dir:
|
|
355
|
+
package_command.extend(["--output-dir", str(Path(args.package_dir).expanduser().resolve())])
|
|
356
|
+
run(package_command)
|
|
357
|
+
|
|
358
|
+
package_dir = None
|
|
359
|
+
if not args.skip_package:
|
|
360
|
+
package_dir = (
|
|
361
|
+
Path(args.package_dir).expanduser().resolve()
|
|
362
|
+
if args.package_dir
|
|
363
|
+
else default_codex_home() / "pets" / pet_id
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
summary = {
|
|
367
|
+
"ok": True,
|
|
368
|
+
"run_dir": str(run_dir),
|
|
369
|
+
"spritesheet": str(final_dir / "spritesheet.webp"),
|
|
370
|
+
"validation": str(final_dir / "validation.json"),
|
|
371
|
+
"contact_sheet": str(qa_dir / "contact-sheet.png"),
|
|
372
|
+
"review": str(review_path),
|
|
373
|
+
"videos": None if args.skip_videos else str(qa_dir / "videos"),
|
|
374
|
+
"package": None if package_dir is None else str(package_dir),
|
|
375
|
+
}
|
|
376
|
+
summary_path = qa_dir / "run-summary.json"
|
|
377
|
+
summary_path.write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8")
|
|
378
|
+
print(json.dumps(summary, indent=2))
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
if __name__ == "__main__":
|
|
382
|
+
main()
|