@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,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()
|