@inneranimalmedia/agentsam-sdk 2.6.7 → 2.6.8

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 (318) hide show
  1. package/README.md +37 -0
  2. package/docs/REPO_FILE_MAP.txt +132 -0
  3. package/migrations/d1/0018_goap_control_plane_remaster.sql +40 -0
  4. package/migrations/d1/0019_agentsam_agent_run_plan_todo.sql +14 -0
  5. package/migrations/d1/README_PORTABLE_CONTROL_PLANE.md +20 -9
  6. package/migrations/runtime/0002_goap_blackboard_v1.sql +15 -0
  7. package/migrations/runtime/0004_agent_run_plan_linkage.sql +8 -0
  8. package/native/agentsam-machine/cli/Cargo.toml +8 -1
  9. package/native/agentsam-machine/cli/README.md +21 -0
  10. package/native/agentsam-machine/core/Cargo.toml +7 -0
  11. package/native/agentsam-machine/core/README.md +14 -0
  12. package/native/agentsam-machine/core/build.rs +33 -0
  13. package/native/agentsam-machine/core/contracts/source-types.v1.json +353 -0
  14. package/native/agentsam-machine/core/src/lib.rs +6 -0
  15. package/native/agentsam-machine/core/src/source_types.rs +2 -2
  16. package/package.json +16 -6
  17. package/packages/agentsam-errors/package.json +1 -1
  18. package/packages/agentsam-errors/src/index.js +1 -0
  19. package/packages/agentsam-errors/src/tool.js +56 -0
  20. package/packages/agentsam-errors/test/tool.test.mjs +58 -0
  21. package/packages/agentsam-goap/README.md +52 -0
  22. package/packages/agentsam-goap/package.json +37 -0
  23. package/packages/agentsam-goap/src/adapters/d1-sqlite.js +570 -0
  24. package/packages/agentsam-goap/src/adapters/memory.js +199 -0
  25. package/packages/agentsam-goap/src/contracts.js +226 -0
  26. package/packages/agentsam-goap/src/control-plane.js +112 -0
  27. package/packages/agentsam-goap/src/errors.js +157 -0
  28. package/packages/agentsam-goap/src/index.js +5 -0
  29. package/packages/agentsam-goap/src/world.js +202 -0
  30. package/packages/agentsam-goap/test/goap-control-plane.test.mjs +245 -0
  31. package/packages/agentsam-goap/test/world.test.mjs +79 -0
  32. package/packages/agentsam-nav/dist/Nav.d.ts +43 -0
  33. package/packages/agentsam-nav/dist/NavCollapsible.d.ts +10 -0
  34. package/packages/agentsam-nav/dist/NavContent.d.ts +2 -0
  35. package/packages/agentsam-nav/dist/NavFooter.d.ts +2 -0
  36. package/packages/agentsam-nav/dist/NavGroup.d.ts +3 -0
  37. package/packages/agentsam-nav/dist/NavHeader.d.ts +2 -0
  38. package/packages/agentsam-nav/dist/NavMenu.d.ts +22 -0
  39. package/packages/agentsam-nav/dist/NavProvider.d.ts +35 -0
  40. package/packages/agentsam-nav/dist/NavTrigger.d.ts +2 -0
  41. package/packages/agentsam-nav/dist/Surfaces.d.ts +41 -0
  42. package/packages/agentsam-nav/dist/browser.d.ts +11 -0
  43. package/packages/agentsam-nav/dist/browser.global.js +135 -0
  44. package/packages/agentsam-nav/dist/browser.js +7290 -0
  45. package/packages/agentsam-nav/dist/contracts.d.ts +76 -0
  46. package/packages/agentsam-nav/dist/index.d.ts +11 -0
  47. package/packages/agentsam-nav/dist/index.js +7291 -0
  48. package/packages/agentsam-nav/dist/legacy.css +83 -0
  49. package/packages/agentsam-nav/dist/shell.css +113 -0
  50. package/packages/agentsam-nav/dist/theme.css +113 -0
  51. package/packages/agentsam-nav/dist/utils.d.ts +1 -0
  52. package/packages/agentsam-repository/src/goap.js +13 -14
  53. package/packages/agentsam-repository/src/goap.js.bak-20260922011251 +670 -0
  54. package/packages/agentsam-repository/src/tickets.js +15 -22
  55. package/packages/agentsam-repository/test/goap-tickets-workspace-state.test.mjs +19 -8
  56. package/packages/connectors/cloudflare/package.json +3 -1
  57. package/packages/identity/tests/lane1-packed-consumer.test.mjs +1 -1
  58. package/python/agentsam_sdk/repository/intelligence/discovery.py +27 -2
  59. package/python/agentsam_sdk/repository/intelligence/git.py +4 -0
  60. package/python/tests/test_repository_intelligence.py +30 -0
  61. package/src/cli/command-catalog.js +2 -1
  62. package/src/cli.js +10 -1
  63. package/src/commands/goap-world.js +134 -0
  64. package/src/commands/goap.js +11 -4
  65. package/src/commands/machine-binary.js +33 -27
  66. package/src/commands/machine-runtime.js +266 -0
  67. package/src/commands/machine.js +122 -51
  68. package/src/commands/package.js +530 -0
  69. package/src/commands/plan-ledger.js +370 -0
  70. package/src/commands/plan.js +183 -0
  71. package/src/commands/skill.js +78 -17
  72. package/src/knowledge/source.js +1 -1
  73. package/src/lib/slash-commands.js +1 -1
  74. package/src/local/runtime-store.js +24 -2
  75. package/src/local/sqlite.js +17 -0
  76. package/src/sam/operations/package-audit.js +41 -0
  77. package/src/sam/seed.js +2 -0
  78. package/skills/grok-auth/SKILL.md +0 -110
  79. package/skills/grok-auth/agentsam.skill.json +0 -25
  80. package/skills/grok-auth/references/grok-identity.md +0 -64
  81. package/skills/grok-auth/references/per-user-data.md +0 -49
  82. package/skills/grok-auth/references/prewired-and-env.md +0 -54
  83. package/skills/grok-auth/references/session-ui.md +0 -133
  84. package/skills/grok-auth/references/sign-in-methods.md +0 -55
  85. package/skills/grok-auth/references/wiring.md +0 -71
  86. package/skills/grok-building-games/SKILL.md +0 -213
  87. package/skills/grok-building-games/agentsam.skill.json +0 -25
  88. package/skills/grok-building-games/references/3d-libs.md +0 -64
  89. package/skills/grok-building-games/references/ai-pathfinding.md +0 -122
  90. package/skills/grok-building-games/references/audio.md +0 -129
  91. package/skills/grok-building-games/references/babylon.md +0 -202
  92. package/skills/grok-building-games/references/collision-physics.md +0 -122
  93. package/skills/grok-building-games/references/ecs-architecture.md +0 -111
  94. package/skills/grok-building-games/references/game-feel-juice.md +0 -110
  95. package/skills/grok-building-games/references/genres/board-card-chess.md +0 -89
  96. package/skills/grok-building-games/references/genres/endless-runner.md +0 -83
  97. package/skills/grok-building-games/references/genres/fps.md +0 -105
  98. package/skills/grok-building-games/references/genres/platformer-2d.md +0 -97
  99. package/skills/grok-building-games/references/genres/puzzle-match3-tetris.md +0 -131
  100. package/skills/grok-building-games/references/genres/racing-kart.md +0 -106
  101. package/skills/grok-building-games/references/genres/topdown-twin-stick.md +0 -97
  102. package/skills/grok-building-games/references/genres/tower-defense.md +0 -84
  103. package/skills/grok-building-games/references/genres/voxel-minecraft.md +0 -89
  104. package/skills/grok-building-games/references/input.md +0 -139
  105. package/skills/grok-building-games/references/phaser.md +0 -277
  106. package/skills/grok-building-games/references/procedural-generation.md +0 -141
  107. package/skills/grok-building-games/references/save-persistence.md +0 -108
  108. package/skills/grok-building-games/references/threejs-foundational.md +0 -148
  109. package/skills/grok-controls/SKILL.md +0 -274
  110. package/skills/grok-controls/agentsam.skill.json +0 -25
  111. package/skills/grok-design-ui/SKILL.md +0 -122
  112. package/skills/grok-design-ui/agentsam.skill.json +0 -25
  113. package/skills/grok-design-ui/references/animations.md +0 -379
  114. package/skills/grok-design-ui/references/performance.md +0 -88
  115. package/skills/grok-design-ui/references/refined-ui.md +0 -384
  116. package/skills/grok-design-ui/references/surfaces.md +0 -247
  117. package/skills/grok-design-ui/references/typography.md +0 -123
  118. package/skills/grok-game-animation-frames/SKILL.md +0 -84
  119. package/skills/grok-game-animation-frames/agentsam.skill.json +0 -25
  120. package/skills/grok-game-asset-core/SKILL.md +0 -69
  121. package/skills/grok-game-asset-core/agentsam.skill.json +0 -25
  122. package/skills/grok-game-character-consistency/SKILL.md +0 -69
  123. package/skills/grok-game-character-consistency/agentsam.skill.json +0 -25
  124. package/skills/grok-game-tilesets/SKILL.md +0 -65
  125. package/skills/grok-game-tilesets/agentsam.skill.json +0 -25
  126. package/skills/grok-game-ui-icons/SKILL.md +0 -56
  127. package/skills/grok-game-ui-icons/agentsam.skill.json +0 -25
  128. package/skills/grok-generate2dmap/LICENSE +0 -21
  129. package/skills/grok-generate2dmap/SKILL.md +0 -120
  130. package/skills/grok-generate2dmap/SOURCE.md +0 -6
  131. package/skills/grok-generate2dmap/agentsam.skill.json +0 -25
  132. package/skills/grok-generate2dmap/references/deliverables.md +0 -101
  133. package/skills/grok-generate2dmap/references/layered-map-contract.md +0 -209
  134. package/skills/grok-generate2dmap/references/map-strategies.md +0 -341
  135. package/skills/grok-generate2dmap/references/object-production-gate.md +0 -24
  136. package/skills/grok-generate2dmap/references/pipeline.md +0 -149
  137. package/skills/grok-generate2dmap/references/prop-pack-contract.md +0 -174
  138. package/skills/grok-generate2dmap/references/side-scroll-stages.md +0 -29
  139. package/skills/grok-generate2dmap/scripts/compose_layered_preview.py +0 -157
  140. package/skills/grok-generate2dmap/scripts/extract_prop_pack.py +0 -369
  141. package/skills/grok-generate2dsprite/LICENSE +0 -21
  142. package/skills/grok-generate2dsprite/SKILL.md +0 -313
  143. package/skills/grok-generate2dsprite/SOURCE.md +0 -6
  144. package/skills/grok-generate2dsprite/agentsam.skill.json +0 -25
  145. package/skills/grok-generate2dsprite/references/modes.md +0 -124
  146. package/skills/grok-generate2dsprite/references/prompt-rules.md +0 -315
  147. package/skills/grok-generate2dsprite/scripts/generate2dsprite.py +0 -1031
  148. package/skills/grok-generate2dsprite/scripts/make_layout_guide.py +0 -109
  149. package/skills/grok-imagine/SKILL.md +0 -201
  150. package/skills/grok-imagine/agentsam.skill.json +0 -25
  151. package/skills/grok-multiplayer-p2p/SKILL.md +0 -111
  152. package/skills/grok-multiplayer-p2p/agentsam.skill.json +0 -25
  153. package/skills/grok-multiplayer-p2p/references/react-binding.md +0 -144
  154. package/skills/grok-multiplayer-p2p/references/signaling-relay.md +0 -269
  155. package/skills/grok-neon/SKILL.md +0 -157
  156. package/skills/grok-neon/agentsam.skill.json +0 -25
  157. package/skills/grok-og/SKILL.md +0 -102
  158. package/skills/grok-og/agentsam.skill.json +0 -25
  159. package/skills/grok-og/references/brand-pass.md +0 -53
  160. package/skills/grok-og/references/custom-card.md +0 -107
  161. package/skills/grok-og/references/favicon-and-icons.md +0 -48
  162. package/skills/grok-og/references/og-type-contract.md +0 -18
  163. package/skills/grok-og/references/placeholder-card.md +0 -21
  164. package/skills/grok-og/references/x-banner.md +0 -41
  165. package/skills/grok-threejs/SKILL.md +0 -67
  166. package/skills/grok-threejs/agentsam.skill.json +0 -25
  167. package/skills/grok-threejs/references/llms-full.txt +0 -2780
  168. package/skills/grok-video2dsprite/LICENSE +0 -21
  169. package/skills/grok-video2dsprite/SKILL.md +0 -236
  170. package/skills/grok-video2dsprite/SOURCE.md +0 -6
  171. package/skills/grok-video2dsprite/agentsam.skill.json +0 -25
  172. package/skills/grok-video2dsprite/references/pipeline.md +0 -90
  173. package/skills/grok-video2dsprite/references/prompt-rules.md +0 -63
  174. package/skills/grok-video2dsprite/scripts/video2dsprite.py +0 -490
  175. package/skills/grok-xai-api/SKILL.md +0 -152
  176. package/skills/grok-xai-api/agentsam.skill.json +0 -25
  177. package/test/account-session.test.mjs +0 -101
  178. package/test/agent-capabilities.test.mjs +0 -67
  179. package/test/app-authority-context.test.mjs +0 -32
  180. package/test/app-building-skills.test.mjs +0 -61
  181. package/test/apps-scaffold-contract.test.mjs +0 -115
  182. package/test/blender-cad.test.mjs +0 -146
  183. package/test/cad-docker.test.mjs +0 -96
  184. package/test/capabilities.test.mjs +0 -133
  185. package/test/cli/api-key-env-whoami.test.mjs +0 -129
  186. package/test/cli/catalog-authority.test.mjs +0 -64
  187. package/test/cli/codebaseindex-plan-ux.test.mjs +0 -37
  188. package/test/cli/eval-live.test.mjs +0 -219
  189. package/test/cli/go.test.mjs +0 -128
  190. package/test/cli/local-vault-and-provider-setup.test.mjs +0 -268
  191. package/test/cli/machine-command.test.mjs +0 -34
  192. package/test/cli/mcp-client.test.mjs +0 -256
  193. package/test/cli/preferences-runtime.test.mjs +0 -11
  194. package/test/cli/runtime-persist.test.mjs +0 -56
  195. package/test/cli/runtime-ui.test.mjs +0 -141
  196. package/test/cli/rust-command.test.mjs +0 -21
  197. package/test/cli/skill-npm-and-env.test.mjs +0 -52
  198. package/test/cli/status-actions.test.mjs +0 -88
  199. package/test/cli/wireframes-go-registry.test.mjs +0 -139
  200. package/test/cli-preferences.test.mjs +0 -46
  201. package/test/cloudflare-connector.test.mjs +0 -93
  202. package/test/cloudflare-runtime.test.mjs +0 -82
  203. package/test/context.test.mjs +0 -159
  204. package/test/credentials-audit.test.mjs +0 -113
  205. package/test/deploy-health-scan.test.mjs +0 -95
  206. package/test/deploy-receipt.test.mjs +0 -91
  207. package/test/dockerize.test.mjs +0 -93
  208. package/test/error-diagnostics.test.mjs +0 -124
  209. package/test/eval-context.test.mjs +0 -37
  210. package/test/execution-approvals.test.mjs +0 -27
  211. package/test/fixtures/cad/courtyard-house.json +0 -208
  212. package/test/fixtures/knowledge-rpc-worker.mjs +0 -16
  213. package/test/fixtures/project-authority/customer-app/.agentsam/app.json +0 -5
  214. package/test/fixtures/project-authority/customer-app/apps/my-product/index.ts +0 -1
  215. package/test/fixtures/project-authority/customer-app/wrangler.toml +0 -17
  216. package/test/gcloud-auth.test.mjs +0 -111
  217. package/test/go/build-source-identity.test.mjs +0 -31
  218. package/test/go/cloudflare-probe.test.mjs +0 -317
  219. package/test/go/distribution.test.mjs +0 -28
  220. package/test/google-cloud-connection.test.mjs +0 -83
  221. package/test/google-cloud-inventory.test.mjs +0 -64
  222. package/test/indexing-provider.test.mjs +0 -29
  223. package/test/init-options.test.mjs +0 -35
  224. package/test/integration/ai-access-onboarding.test.mjs +0 -47
  225. package/test/integration/app-command.test.mjs +0 -85
  226. package/test/integration/autorag-project-authority.integration.test.mjs +0 -51
  227. package/test/integration/autorag-runtime-discovery.test.mjs +0 -107
  228. package/test/integration/cad-discovery.test.mjs +0 -158
  229. package/test/integration/cad-docker-executor.test.mjs +0 -249
  230. package/test/integration/cad-execution.test.mjs +0 -184
  231. package/test/integration/cad-project-app.test.mjs +0 -31
  232. package/test/integration/cad-project-d1.test.mjs +0 -25
  233. package/test/integration/cad-project-native.test.mjs +0 -24
  234. package/test/integration/cad-project-workflow.test.mjs +0 -9
  235. package/test/integration/cad-project.test.mjs +0 -85
  236. package/test/integration/cli-help.test.mjs +0 -41
  237. package/test/integration/cms-app-scaffold.test.mjs +0 -77
  238. package/test/integration/cms-site-tenancy-contract.test.mjs +0 -268
  239. package/test/integration/codebaseindex-ingest.test.mjs +0 -255
  240. package/test/integration/core-client.integration.test.mjs +0 -55
  241. package/test/integration/database-hosted-honesty.test.mjs +0 -49
  242. package/test/integration/desktop-keychain-boundary.test.mjs +0 -45
  243. package/test/integration/error-runtime-rpc.test.mjs +0 -71
  244. package/test/integration/features-resolve.test.mjs +0 -147
  245. package/test/integration/hosted-work-host-authority.test.mjs +0 -47
  246. package/test/integration/icon-registry.test.mjs +0 -67
  247. package/test/integration/ingest-discover-models.test.mjs +0 -30
  248. package/test/integration/inspect-path-cli.test.mjs +0 -46
  249. package/test/integration/install-script.test.mjs +0 -110
  250. package/test/integration/inventory-core.test.mjs +0 -53
  251. package/test/integration/inventory-eligibility.test.mjs +0 -59
  252. package/test/integration/inventory-evidence.test.mjs +0 -125
  253. package/test/integration/knowledge-rpc.test.mjs +0 -112
  254. package/test/integration/local-fs.test.mjs +0 -113
  255. package/test/integration/local-provider-bridge.test.mjs +0 -65
  256. package/test/integration/local-studio-settings-production.test.mjs +0 -40
  257. package/test/integration/lockfile-sync.test.mjs +0 -26
  258. package/test/integration/merkle-cli.test.mjs +0 -61
  259. package/test/integration/merkle-persistence-identity.test.mjs +0 -48
  260. package/test/integration/plugins-cloudflare.integration.test.mjs +0 -28
  261. package/test/integration/provider-env-cli.test.mjs +0 -60
  262. package/test/integration/provider-factory.test.mjs +0 -197
  263. package/test/integration/repository-company-graph.test.mjs +0 -90
  264. package/test/integration/runtime-migrations.test.mjs +0 -87
  265. package/test/integration/runtime-status.integration.test.mjs +0 -227
  266. package/test/integration/sam-activity-recovery.test.mjs +0 -147
  267. package/test/integration/sam-decision.test.mjs +0 -584
  268. package/test/integration/sam-kernel.test.mjs +0 -99
  269. package/test/integration/sam-planning-astar.test.mjs +0 -279
  270. package/test/integration/scaffold-cloudflare.test.mjs +0 -46
  271. package/test/integration/skill-runtime.test.mjs +0 -240
  272. package/test/integration/storage-architecture.test.mjs +0 -154
  273. package/test/integration/store-discovery.test.mjs +0 -44
  274. package/test/integration/studio-fs-pty-e2e.test.mjs +0 -294
  275. package/test/integration/terminal-exec.integration.test.mjs +0 -86
  276. package/test/integration/theme-package-registry.test.mjs +0 -31
  277. package/test/integration/tool-runtime.test.mjs +0 -124
  278. package/test/integration/tool-schema.test.mjs +0 -59
  279. package/test/jr-dev-skill.test.mjs +0 -26
  280. package/test/knowledge-context-pack.test.mjs +0 -19
  281. package/test/knowledge-postgres.test.mjs +0 -40
  282. package/test/knowledge-service.test.mjs +0 -110
  283. package/test/knowledge.test.mjs +0 -206
  284. package/test/live/terminal-transport.live.test.mjs +0 -24
  285. package/test/local-sessions.test.mjs +0 -57
  286. package/test/local-studio-deploy.test.mjs +0 -83
  287. package/test/loopback-oauth.test.mjs +0 -53
  288. package/test/merkle-render.test.mjs +0 -31
  289. package/test/mini.test.mjs +0 -140
  290. package/test/model-catalog.test.mjs +0 -70
  291. package/test/models.test.mjs +0 -309
  292. package/test/npm10-lock.test.mjs +0 -29
  293. package/test/ollama.test.mjs +0 -137
  294. package/test/open-url.test.mjs +0 -64
  295. package/test/openai-responses.test.mjs +0 -155
  296. package/test/portable-context.test.mjs +0 -94
  297. package/test/project-config.test.mjs +0 -81
  298. package/test/project-rules.test.mjs +0 -44
  299. package/test/provider-credentials.test.mjs +0 -106
  300. package/test/rehydrate.test.mjs +0 -25
  301. package/test/release-hygiene.test.mjs +0 -42
  302. package/test/repository-snapshot-view.test.mjs +0 -112
  303. package/test/responses-runner.test.mjs +0 -465
  304. package/test/runtime-activity.test.mjs +0 -98
  305. package/test/sdk-worker-contract.test.mjs +0 -72
  306. package/test/security.test.mjs +0 -266
  307. package/test/setup-planner.test.mjs +0 -74
  308. package/test/setup-runtime.test.mjs +0 -51
  309. package/test/shell.test.mjs +0 -281
  310. package/test/skills.test.mjs +0 -22
  311. package/test/smoke.mjs +0 -122
  312. package/test/telemetry.test.mjs +0 -79
  313. package/test/terminal/local-pty.mock.test.mjs +0 -153
  314. package/test/terminal-machine-identity.test.mjs +0 -88
  315. package/test/theme-portability.test.mjs +0 -14
  316. package/test/tools-search.test.mjs +0 -73
  317. package/test/unit/website-assets-bindings.test.mjs +0 -60
  318. package/test/whoami-resume.test.mjs +0 -157
@@ -1,1031 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Build sprite prompts and postprocess generated sprite sheets locally."""
3
-
4
- from __future__ import annotations
5
-
6
- import argparse
7
- import hashlib
8
- import json
9
- import math
10
- import random
11
- import re
12
- from collections import deque
13
- from pathlib import Path
14
-
15
- import numpy as np
16
- from PIL import Image
17
-
18
- ART_STYLE = (
19
- "Original digital monster creature. Digimon/Pokemon inspired pixel art, "
20
- "strong outlines, dynamic, battle-ready. NOT cute, NOT round. "
21
- "SOLID COLORED BODY. Background is 100% solid flat magenta (#FF00FF), no gradients. "
22
- "NO text, NO labels, NO words, NO letters anywhere."
23
- )
24
-
25
- CHAR_STYLE = (
26
- "Top-down 2D pixel art for a 16-bit RPG overworld. 3/4 view from slightly "
27
- "above, you can see the top of the head, shoulders and full body. Chunky "
28
- "pixel-art with crisp dark outlines and saturated colors. Character fills "
29
- "~60% of its cell with margin for the engine to render cleanly. "
30
- "Background is 100% solid flat magenta (#FF00FF), no gradients, no shadow "
31
- "under character. NO text, NO labels, NO UI, NO speech bubbles."
32
- )
33
-
34
- GRID_RULES = (
35
- "ABSOLUTE RULES: "
36
- "1. EXACTLY 4 equal quadrants (2x2). "
37
- "2. NO borders, NO lines, NO frames between quadrants. "
38
- "3. NO text, NO labels. "
39
- "4. Each character fills 80%+ of its quadrant, SAME SIZE in every quadrant. "
40
- "5. Quadrants connected by magenta background only."
41
- )
42
-
43
- GRID_RULES_4X4 = (
44
- "ABSOLUTE RULES: "
45
- "1. EXACTLY 16 equal-size cells arranged in a 4x4 grid (4 rows of 4 columns, every cell the same width and height). "
46
- "2. NO borders, NO lines, NO frames between cells. "
47
- "3. NO text, NO labels, NO numbers, NO arrows. "
48
- "4. CRITICAL CONSISTENCY: the character in every single cell has the IDENTICAL height and IDENTICAL width "
49
- "(same bounding box, same pixel scale). Do NOT zoom in or out between cells. "
50
- "Do NOT crop tighter in some cells. The character's head-to-foot height must be visibly identical in all 16 cells. "
51
- "5. Character is CENTERED horizontally and vertically within its cell. Fills ~60% of the cell, leaving equal magenta margin on all four sides. "
52
- "6. Cells connected ONLY by solid magenta (#FF00FF) background."
53
- )
54
-
55
- NPC_ROLES = {
56
- "starter": "an experienced mentor who hands out starter monsters, wise and welcoming",
57
- "shop": "a merchant or vendor, apron or utility belt, counter accessories",
58
- "healer": "a healer or medic, soft uniform, healing tools, calm posture",
59
- "summoner": "a mystical summoner who calls forth monsters, arcane or gambling vibe",
60
- "sage": "an old wise sage, robes or long coat, staff or crystal",
61
- "trainer": "a rival trainer, confident athletic pose, slight smirk",
62
- "gym_leader": "a gym leader or boss, distinct outfit, most powerful regional trainer",
63
- "villager": "an ordinary townsperson, plain outfit, friendly body language",
64
- "guard": "a city guard, uniform or armor, alert posture",
65
- }
66
-
67
- GENERIC_ASSET_MODES = [
68
- "single",
69
- "idle",
70
- "cast",
71
- "attack",
72
- "shoot",
73
- "jump",
74
- "hurt",
75
- "combat",
76
- "walk",
77
- "run",
78
- "hover",
79
- "charge",
80
- "projectile",
81
- "impact",
82
- "explode",
83
- "death",
84
- "fx",
85
- "sheet",
86
- ]
87
-
88
- # Modes that are single-frame (not a multi-cell grid) when --rows/--cols omitted.
89
- SINGLE_MODES = frozenset({"single", "player", "npc"})
90
-
91
- TARGET_MODES = {
92
- "creature": ["single", "evolution", "idle", "combat", "walk", "actions"],
93
- "player": [
94
- "player",
95
- "player_walk",
96
- "player_sheet",
97
- "player_actions",
98
- "idle",
99
- "walk",
100
- "run",
101
- "attack",
102
- "shoot",
103
- "jump",
104
- "hurt",
105
- "cast",
106
- ],
107
- "npc": ["npc", "npc_walk", "idle", "walk"],
108
- "asset": GENERIC_ASSET_MODES,
109
- }
110
-
111
- GRID_SHAPES = {
112
- "evolution": (2, 2),
113
- "idle": (2, 2),
114
- "cast": (2, 3),
115
- "attack": (2, 2),
116
- "shoot": (2, 2),
117
- "jump": (2, 2),
118
- "hurt": (2, 2),
119
- "combat": (2, 2),
120
- "actions": (2, 2),
121
- "walk": (2, 2),
122
- "run": (2, 2),
123
- "hover": (2, 2),
124
- "charge": (2, 2),
125
- "projectile": (2, 2), # multi-row default for body-attached loops; override with --rows/--cols
126
- "impact": (2, 2),
127
- "explode": (2, 2),
128
- "death": (2, 3),
129
- "fx": (2, 2),
130
- "sheet": (2, 2),
131
- "player_walk": (2, 2),
132
- "player_actions": (2, 2),
133
- "npc_walk": (2, 2),
134
- "player_sheet": (4, 4),
135
- }
136
-
137
- FRAME_LABELS = {
138
- "evolution": ["stage-1", "stage-2", "stage-3", "stage-4"],
139
- "idle": ["idle-1", "idle-2", "idle-3", "idle-4"],
140
- "cast": ["cast-1", "cast-2", "cast-3", "cast-4", "cast-5", "cast-6"],
141
- "attack": ["attack-1", "attack-2", "attack-3", "attack-4"],
142
- "shoot": ["shoot-1", "shoot-2", "shoot-3", "shoot-4"],
143
- "jump": ["jump-1", "jump-2", "jump-3", "jump-4"],
144
- "hurt": ["hurt-1", "hurt-2", "hurt-3", "hurt-4"],
145
- "combat": ["attack-1", "attack-2", "hurt-1", "hurt-2"],
146
- "actions": ["idle-1", "idle-2", "attack", "hurt"],
147
- "walk": ["walk-1", "walk-2", "walk-3", "walk-4"],
148
- "run": ["run-1", "run-2", "run-3", "run-4"],
149
- "hover": ["hover-1", "hover-2", "hover-3", "hover-4"],
150
- "charge": ["charge-1", "charge-2", "charge-3", "charge-4"],
151
- "projectile": ["projectile-1", "projectile-2", "projectile-3", "projectile-4"],
152
- "impact": ["impact-1", "impact-2", "impact-3", "impact-4"],
153
- "explode": ["explode-1", "explode-2", "explode-3", "explode-4"],
154
- "death": ["death-1", "death-2", "death-3", "death-4", "death-5", "death-6"],
155
- "fx": ["fx-1", "fx-2", "fx-3", "fx-4"],
156
- "sheet": ["frame-1", "frame-2", "frame-3", "frame-4"],
157
- "player_walk": ["walk-down-1", "walk-down-2", "walk-down-3", "walk-down-4"],
158
- "player_actions": ["idle", "walk", "attack", "hurt"],
159
- "npc_walk": ["walk-down-1", "walk-down-2", "walk-down-3", "walk-down-4"],
160
- "player_sheet": [
161
- "down-1",
162
- "down-2",
163
- "down-3",
164
- "down-4",
165
- "left-1",
166
- "left-2",
167
- "left-3",
168
- "left-4",
169
- "right-1",
170
- "right-2",
171
- "right-3",
172
- "right-4",
173
- "up-1",
174
- "up-2",
175
- "up-3",
176
- "up-4",
177
- ],
178
- }
179
-
180
- PROCESS_TARGETS = sorted(TARGET_MODES)
181
-
182
- ARCHETYPES = {
183
- "beast": {
184
- "name": "Beast Evolution",
185
- "path": "primal beast to apex predator to mythic god-beast",
186
- },
187
- "mecha": {"name": "Mecha Evolution", "path": "organic to cybernetic to full mecha to mech-god"},
188
- "elemental": {
189
- "name": "Elemental Evolution",
190
- "path": "solid creature to elemental infused to pure energy being",
191
- },
192
- "void": {
193
- "name": "Void Evolution",
194
- "path": "shadow creature to twisted horror to abstract cosmic entity",
195
- },
196
- "crystal": {
197
- "name": "Crystal Evolution",
198
- "path": "rocky creature to crystalline to geometric god",
199
- },
200
- "angelic": {"name": "Angelic Evolution", "path": "creature to holy warrior to divine seraph"},
201
- "parasite": {
202
- "name": "Parasite Evolution",
203
- "path": "small symbiote to merged chimera to eldritch abomination",
204
- },
205
- "myth": {"name": "Myth Evolution", "path": "animal to mythical beast to ancient deity"},
206
- }
207
-
208
- MORPH_AXES = {
209
- "posture": ["quadrupedal", "bipedal", "floating", "abstract or formless"],
210
- "material": ["flesh or organic", "armored or plated", "energy-infused", "pure light or energy"],
211
- "anatomy": [
212
- "compact limbs",
213
- "extended limbs plus tail",
214
- "extra limbs or wings",
215
- "aura replaces body parts",
216
- ],
217
- }
218
-
219
- SILHOUETTES = ["sharp angular", "bulky imposing", "elongated serpentine", "alien geometric"]
220
- SURFACES = ["smooth organic", "armored plates", "crystalline facets", "energy veins"]
221
- VIBES = ["elegant and swift", "brutal and heavy", "mysterious and dark", "sacred and divine"]
222
-
223
-
224
- def stable_seed(target: str, mode: str, prompt: str, role: str) -> int:
225
- raw = f"{target}|{mode}|{prompt}|{role}".encode("utf-8")
226
- return int(hashlib.sha256(raw).hexdigest()[:8], 16)
227
-
228
-
229
- def is_known_target_mode(target: str, mode: str) -> bool:
230
- return target in TARGET_MODES and mode in TARGET_MODES[target]
231
-
232
-
233
- def ensure_valid_target_mode(target: str, mode: str) -> None:
234
- if target not in TARGET_MODES:
235
- raise ValueError(
236
- f"Unknown target '{target}'. Valid targets: {', '.join(sorted(TARGET_MODES))}"
237
- )
238
- if mode not in TARGET_MODES[target]:
239
- allowed = ", ".join(TARGET_MODES[target])
240
- raise ValueError(f"Mode '{mode}' is invalid for target '{target}'. Valid modes: {allowed}")
241
-
242
-
243
- def build_evolution_descs(subject: str, rng: random.Random) -> dict[str, str]:
244
- arch_key = rng.choice(list(ARCHETYPES.keys()))
245
- arch = ARCHETYPES[arch_key]
246
- silhouette = rng.choice(SILHOUETTES)
247
- surface = rng.choice(SURFACES)
248
- vibe = rng.choice(VIBES)
249
- postures = MORPH_AXES["posture"]
250
- materials = MORPH_AXES["material"]
251
- anatomies = MORPH_AXES["anatomy"]
252
-
253
- design_rules = (
254
- f"Evolution archetype: {arch['name']} ({arch['path']}). "
255
- f"Design: {silhouette} silhouette, {surface} surface, {vibe} feel. "
256
- "Ensure DIFFERENT silhouette, texture, posture per stage. "
257
- "Avoid repeating limb structure or proportions."
258
- )
259
-
260
- return {
261
- "1-base": (
262
- f"Stage 1: Base form. The clearest first complete form of {subject}. "
263
- f"Posture: {postures[0]}. Material: {materials[0]}. Anatomy: {anatomies[0]}. "
264
- f"Strong identity and readable silhouette. {design_rules}"
265
- ),
266
- "2-risen": (
267
- f"Stage 2: Developed form. A more dangerous promoted version of {subject}. "
268
- f"Posture: {postures[1]} and clearly different from stage 1. "
269
- f"Material: {materials[1]}. Anatomy: {anatomies[1]}. "
270
- f"REDESIGNED, not just bigger. Combat specialist. {design_rules}"
271
- ),
272
- "3-elite": (
273
- f"Stage 3: Elite war form of {subject}. "
274
- f"Posture: {postures[2]}. Material: {materials[2]}. Anatomy: {anatomies[2]}. "
275
- f"Heavy battlefield presence and advanced redesign. {design_rules}"
276
- ),
277
- "4-mythic": (
278
- f"Stage 4: Mythic ascendant form of {subject}. "
279
- f"Posture: {postures[3]}. Material: {materials[3]}. Anatomy: {anatomies[3]}. "
280
- f"Abstract, cosmic, godlike final evolution. {design_rules}"
281
- ),
282
- }
283
-
284
-
285
- def build_prompt(
286
- target: str, mode: str, prompt: str, role: str | None = None, seed: int | None = None
287
- ) -> tuple[str, int]:
288
- ensure_valid_target_mode(target, mode)
289
- role = role or ""
290
- if seed is None:
291
- seed = stable_seed(target, mode, prompt, role)
292
- rng = random.Random(seed)
293
-
294
- if target == "creature":
295
- if mode == "single":
296
- result = (
297
- f"Single pixel art creature sprite, centered, facing right. {prompt}. {ART_STYLE}"
298
- )
299
- elif mode == "evolution":
300
- descs = build_evolution_descs(prompt, rng)
301
- result = (
302
- f"A 2x2 pixel art image showing 4 evolution stages of {prompt}. "
303
- f"Top-left quadrant: {descs['1-base']} "
304
- f"Top-right quadrant: {descs['2-risen']} "
305
- f"Bottom-left quadrant: {descs['3-elite']} "
306
- f"Bottom-right quadrant: {descs['4-mythic']} "
307
- f"Same color palette in all 4. {ART_STYLE} {GRID_RULES}"
308
- )
309
- elif mode == "idle":
310
- result = (
311
- f"A 2x2 pixel art idle animation sheet of the same {prompt}. "
312
- "Top-left quadrant: neutral idle pose, calm but alert. "
313
- "Top-right quadrant: subtle breath or flame pulse, same facing direction. "
314
- "Bottom-left quadrant: idle shift in weight or aura, still clearly looping. "
315
- "Bottom-right quadrant: strongest idle accent before returning to frame 1. "
316
- f"SAME creature, SAME size, SAME facing direction, SAME palette in all 4 cells. {ART_STYLE} {GRID_RULES}"
317
- )
318
- elif mode == "combat":
319
- result = (
320
- f"A 2x2 pixel art combat sheet of the same {prompt}. "
321
- "Top-left quadrant: attack wind-up, gathering force. "
322
- "Top-right quadrant: attack strike or release, aggressive impact. "
323
- "Bottom-left quadrant: hurt reaction at the moment of impact. "
324
- "Bottom-right quadrant: hurt recovery, regaining stance. "
325
- f"SAME creature, SAME size, SAME facing direction, SAME palette in all 4 cells. {ART_STYLE} {GRID_RULES}"
326
- )
327
- elif mode == "actions":
328
- result = (
329
- f"A 2x2 pixel art sprite sheet of the same {prompt} in 4 poses. "
330
- "Top-left quadrant: standing still, relaxed. "
331
- "Top-right quadrant: same pose, mouth open, one limb lifted. "
332
- "Bottom-left quadrant: lunging right, attacking fiercely. "
333
- "Bottom-right quadrant: leaning back, eyes closed, taking damage. "
334
- f"SAME character, SAME size, facing RIGHT. {ART_STYLE} {GRID_RULES}"
335
- )
336
- else:
337
- result = (
338
- f"A 2x2 pixel art sprite sheet of a walk cycle of the same {prompt}. "
339
- "Top-left quadrant: walking right, right front leg forward. "
340
- "Top-right quadrant: walking right, legs under body, mid-stride. "
341
- "Bottom-left quadrant: walking right, left front leg forward. "
342
- "Bottom-right quadrant: walking right, legs extended, passing pose. "
343
- f"SAME character, SAME size, facing RIGHT. Only leg positions change. {ART_STYLE} {GRID_RULES}"
344
- )
345
- elif target == "player":
346
- if mode == "player":
347
- result = (
348
- "Single hero sprite for a top-down RPG. "
349
- f"CHARACTER: {prompt}. Young adventurer protagonist, distinct heroic silhouette, strongly themed costume. "
350
- "Front-facing (toward camera), idle standing pose, centered in canvas with lots of magenta margin around. "
351
- f"{CHAR_STYLE}"
352
- )
353
- elif mode in {"player_walk", "walk", "run"}:
354
- action = "run" if mode == "run" else "walk"
355
- result = (
356
- f"A 2x2 pixel art sprite sheet of a top-down RPG hero {action} cycle, ALL FRAMES FACING DOWN (toward camera). "
357
- f"CHARACTER: {prompt}. "
358
- "Top-left: neutral standing, both feet together. "
359
- "Top-right: LEFT foot stepping forward, right foot planted. "
360
- "Bottom-left: neutral standing again, both feet together. "
361
- "Bottom-right: RIGHT foot stepping forward, left foot planted. "
362
- "SAME character, SAME costume, SAME palette in every cell. "
363
- f"ONLY the legs and arms swing, head, torso, gear stay identical. {CHAR_STYLE} {GRID_RULES}"
364
- )
365
- elif mode == "player_sheet":
366
- result = (
367
- "A 4x4 pixel art sprite sheet, full 4-direction walk cycle for a top-down RPG hero. "
368
- f"CHARACTER: {prompt}. Young adventurer protagonist. "
369
- "SHEET LAYOUT (rows = facing direction, columns = walk frames): "
370
- "Row 1 (top): facing DOWN (toward camera, face fully visible). "
371
- "Row 2: facing LEFT (left profile or side view). "
372
- "Row 3: facing RIGHT (right profile or side view, mirror of row 2). "
373
- "Row 4 (bottom): facing UP (away from camera, back of head visible). "
374
- "COLUMN 1: neutral pose, both feet together. "
375
- "COLUMN 2: LEFT foot stepping forward. "
376
- "COLUMN 3: neutral pose again, both feet together. "
377
- "COLUMN 4: RIGHT foot stepping forward. "
378
- "IDENTICAL SIZE in every cell: same character height head-to-foot, same width shoulder-to-shoulder, "
379
- "same on-screen pixel scale. No zooming, no cropping differently, only pose and direction change. "
380
- "SAME character identity, SAME costume, SAME palette in all 16 cells. "
381
- "The head and torso orientation must clearly communicate which direction the character is facing in each row. "
382
- f"{CHAR_STYLE} {GRID_RULES_4X4}"
383
- )
384
- elif mode in {"player_actions", "actions"}:
385
- result = (
386
- "A 2x2 pixel art sprite sheet of a top-down RPG hero in 4 action states, all facing DOWN (toward camera). "
387
- f"CHARACTER: {prompt}. "
388
- "Top-left: IDLE, neutral standing, relaxed. "
389
- "Top-right: WALK, mid-step, one leg forward. "
390
- "Bottom-left: ATTACK, arm raised or weapon or fist swung forward aggressively. "
391
- "Bottom-right: HURT, knocked back slightly, expression of pain. "
392
- f"SAME character identity, SAME costume, SAME size in every cell. {CHAR_STYLE} {GRID_RULES}"
393
- )
394
- else:
395
- # Per-action hero sheets (idle/attack/shoot/jump/cast/hurt/…) — one
396
- # coherent action grid, matching GRID_SHAPES / FRAME_LABELS used by process.
397
- rows, cols = GRID_SHAPES.get(mode, (2, 2))
398
- result = (
399
- f"A {rows}x{cols} pixel art animation sheet of a top-down RPG hero performing '{mode}'. "
400
- f"CHARACTER: {prompt}. "
401
- "Same character identity, costume, palette, and body scale in every cell. "
402
- "Body-only unless the mode is intentionally FX/projectile/impact. "
403
- "Centered in each cell with solid magenta (#FF00FF) margins; nothing crosses cell edges. "
404
- f"{CHAR_STYLE}"
405
- )
406
- elif target == "npc":
407
- if role not in NPC_ROLES:
408
- allowed = ", ".join(sorted(NPC_ROLES))
409
- raise ValueError(f"NPC role is required for target=npc. Valid roles: {allowed}")
410
- role_desc = NPC_ROLES[role]
411
- if mode == "npc":
412
- result = (
413
- "Single NPC sprite for a top-down RPG. "
414
- f"ROLE: {role_desc}. "
415
- f"VISUAL DETAILS: {prompt}. "
416
- "Front-facing (toward camera), idle standing pose. "
417
- "Appearance should INSTANTLY communicate the role. "
418
- f"Distinct silhouette and palette so this NPC won't be confused with others. {CHAR_STYLE}"
419
- )
420
- elif mode in {"npc_walk", "walk"}:
421
- result = (
422
- "A 2x2 pixel art sprite sheet, top-down RPG NPC walk cycle, ALL FRAMES FACING DOWN (toward camera). "
423
- f"ROLE: {role_desc}. "
424
- f"VISUAL DETAILS: {prompt}. "
425
- "Top-left: neutral standing, both feet together. "
426
- "Top-right: LEFT foot stepping forward. "
427
- "Bottom-left: neutral standing again. "
428
- "Bottom-right: RIGHT foot stepping forward. "
429
- f"SAME NPC, SAME costume, SAME palette in every cell. {CHAR_STYLE} {GRID_RULES}"
430
- )
431
- else:
432
- rows, cols = GRID_SHAPES.get(mode, (2, 2))
433
- result = (
434
- f"A {rows}x{cols} pixel art animation sheet of a top-down RPG NPC performing '{mode}'. "
435
- f"ROLE: {role_desc}. "
436
- f"VISUAL DETAILS: {prompt}. "
437
- "Same NPC identity, costume, palette, and body scale in every cell. "
438
- "Centered in each cell with solid magenta (#FF00FF) margins; nothing crosses cell edges. "
439
- f"{CHAR_STYLE}"
440
- )
441
- else:
442
- if mode == "single":
443
- result = (
444
- f"Single pixel art asset sprite. SUBJECT: {prompt}. "
445
- "Centered in the canvas with clear magenta margin around it. "
446
- "Readable silhouette, game-ready shape consistency, transparent-background-ready via magenta chroma key. "
447
- f"{ART_STYLE}"
448
- )
449
- else:
450
- rows, cols = GRID_SHAPES.get(mode, (2, 2))
451
- result = (
452
- f"A {rows}x{cols} pixel art animation sheet of the same {prompt}. "
453
- "The same asset identity appears in every cell, with the same bounding box, the same pixel scale, "
454
- "and no part crossing a cell edge. "
455
- "Keep the animation readable for a 2D game sprite, not a splash illustration. "
456
- f"{ART_STYLE}"
457
- )
458
- return result, seed
459
-
460
-
461
- def remove_bg_magenta(
462
- img: Image.Image, threshold: int = 100, edge_threshold: int = 150
463
- ) -> Image.Image:
464
- pixels = img.load()
465
- width, height = img.size
466
-
467
- def dist(r: int, g: int, b: int) -> float:
468
- return math.sqrt((r - 255) ** 2 + g**2 + (b - 255) ** 2)
469
-
470
- for x in range(width):
471
- for y in range(height):
472
- r, g, b, a = pixels[x, y]
473
- if a == 0:
474
- continue
475
- if dist(r, g, b) < threshold:
476
- pixels[x, y] = (0, 0, 0, 0)
477
-
478
- visited: set[tuple[int, int]] = set()
479
- queue: deque[tuple[int, int]] = deque()
480
- for x in range(width):
481
- queue.append((x, 0))
482
- queue.append((x, height - 1))
483
- for y in range(height):
484
- queue.append((0, y))
485
- queue.append((width - 1, y))
486
-
487
- while queue:
488
- x, y = queue.popleft()
489
- if (x, y) in visited or x < 0 or x >= width or y < 0 or y >= height:
490
- continue
491
- visited.add((x, y))
492
- r, g, b, a = pixels[x, y]
493
- if a == 0:
494
- for dx in (-1, 0, 1):
495
- for dy in (-1, 0, 1):
496
- if dx == 0 and dy == 0:
497
- continue
498
- if (x + dx, y + dy) not in visited:
499
- queue.append((x + dx, y + dy))
500
- elif dist(r, g, b) < edge_threshold:
501
- pixels[x, y] = (0, 0, 0, 0)
502
- for dx in (-1, 0, 1):
503
- for dy in (-1, 0, 1):
504
- if dx == 0 and dy == 0:
505
- continue
506
- if (x + dx, y + dy) not in visited:
507
- queue.append((x + dx, y + dy))
508
- return img
509
-
510
-
511
- def trim_border(img: Image.Image, px: int = 4) -> Image.Image:
512
- width, height = img.size
513
- if width > px * 2 and height > px * 2:
514
- return img.crop((px, px, width - px, height - px))
515
- return img
516
-
517
-
518
- def clean_edges(img: Image.Image, depth: int = 3) -> Image.Image:
519
- pixels = img.load()
520
- width, height = img.size
521
- for d in range(depth):
522
- for x in range(width):
523
- for y in (d, height - 1 - d):
524
- if y < 0 or y >= height:
525
- continue
526
- r, g, b, a = pixels[x, y]
527
- if a == 0:
528
- continue
529
- if (r < 40 and g < 40 and b < 40) or math.sqrt(
530
- (r - 255) ** 2 + g**2 + (b - 255) ** 2
531
- ) < 150:
532
- pixels[x, y] = (0, 0, 0, 0)
533
- for y in range(height):
534
- for x in (d, width - 1 - d):
535
- if x < 0 or x >= width:
536
- continue
537
- r, g, b, a = pixels[x, y]
538
- if a == 0:
539
- continue
540
- if (r < 40 and g < 40 and b < 40) or math.sqrt(
541
- (r - 255) ** 2 + g**2 + (b - 255) ** 2
542
- ) < 150:
543
- pixels[x, y] = (0, 0, 0, 0)
544
- return img
545
-
546
-
547
- def connected_components(img: Image.Image, min_area: int = 1) -> list[dict[str, object]]:
548
- alpha = img.getchannel("A")
549
- pixels = alpha.load()
550
- width, height = img.size
551
- visited = [[False] * width for _ in range(height)]
552
- components: list[dict[str, object]] = []
553
-
554
- for y in range(height):
555
- for x in range(width):
556
- if pixels[x, y] == 0 or visited[y][x]:
557
- continue
558
- queue: deque[tuple[int, int]] = deque([(x, y)])
559
- visited[y][x] = True
560
- area = 0
561
- min_x = max_x = x
562
- min_y = max_y = y
563
- touches_edge = x == 0 or y == 0 or x == width - 1 or y == height - 1
564
-
565
- while queue:
566
- cx, cy = queue.popleft()
567
- area += 1
568
- min_x = min(min_x, cx)
569
- min_y = min(min_y, cy)
570
- max_x = max(max_x, cx)
571
- max_y = max(max_y, cy)
572
- if cx == 0 or cy == 0 or cx == width - 1 or cy == height - 1:
573
- touches_edge = True
574
- for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)):
575
- nx, ny = cx + dx, cy + dy
576
- if (
577
- 0 <= nx < width
578
- and 0 <= ny < height
579
- and pixels[nx, ny] > 0
580
- and not visited[ny][nx]
581
- ):
582
- visited[ny][nx] = True
583
- queue.append((nx, ny))
584
-
585
- if area >= min_area:
586
- components.append(
587
- {
588
- "area": area,
589
- "bbox": (min_x, min_y, max_x + 1, max_y + 1),
590
- "touches_edge": touches_edge,
591
- }
592
- )
593
-
594
- components.sort(key=lambda item: int(item["area"]), reverse=True)
595
- return components
596
-
597
-
598
- def pad_bbox(
599
- bbox: tuple[int, int, int, int], padding: int, width: int, height: int
600
- ) -> tuple[int, int, int, int]:
601
- x0, y0, x1, y1 = bbox
602
- return (
603
- max(0, x0 - padding),
604
- max(0, y0 - padding),
605
- min(width, x1 + padding),
606
- min(height, y1 + padding),
607
- )
608
-
609
-
610
- def bbox_touches_edge(
611
- bbox: tuple[int, int, int, int] | None, width: int, height: int, margin: int = 0
612
- ) -> bool:
613
- if not bbox:
614
- return False
615
- x0, y0, x1, y1 = bbox
616
- return x0 <= margin or y0 <= margin or x1 >= width - margin or y1 >= height - margin
617
-
618
-
619
- def center_single_sprite(
620
- img: Image.Image, size: int, threshold: int, edge_threshold: int
621
- ) -> Image.Image:
622
- cleaned = remove_bg_magenta(img.convert("RGBA"), threshold, edge_threshold)
623
- bbox = cleaned.getbbox()
624
- if bbox:
625
- cleaned = cleaned.crop(bbox)
626
- width, height = cleaned.size
627
- canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0))
628
- if width > 0 and height > 0:
629
- scale = min(size / width, size / height) * 0.9
630
- new_width = max(1, int(width * scale))
631
- new_height = max(1, int(height * scale))
632
- cleaned = cleaned.resize((new_width, new_height), Image.Resampling.LANCZOS)
633
- canvas.paste(cleaned, ((size - new_width) // 2, (size - new_height) // 2))
634
- return canvas
635
-
636
-
637
- def split_grid(
638
- img: Image.Image,
639
- rows: int,
640
- cols: int,
641
- cell_size: int,
642
- threshold: int,
643
- edge_threshold: int,
644
- fit_scale: float = 0.85,
645
- trim_border_px: int = 4,
646
- edge_clean_depth: int = 3,
647
- align: str = "center",
648
- shared_scale: bool = False,
649
- component_mode: str = "all",
650
- component_padding: int = 0,
651
- min_component_area: int = 1,
652
- edge_touch_margin: int = 0,
653
- ) -> tuple[list[Image.Image], list[dict[str, object]]]:
654
- cleaned = remove_bg_magenta(img.convert("RGBA"), threshold, edge_threshold)
655
- width, height = cleaned.size
656
- cell_width, cell_height = width // cols, height // rows
657
- cropped_frames: list[Image.Image] = []
658
- frame_info: list[dict[str, object]] = []
659
- for row in range(rows):
660
- for col in range(cols):
661
- box = (
662
- col * cell_width,
663
- row * cell_height,
664
- (col + 1) * cell_width,
665
- (row + 1) * cell_height,
666
- )
667
- frame = cleaned.crop(box)
668
- # Edge-touch QC uses original grid-cell coords (pre-trim/pre-crop).
669
- edge_touch = bbox_touches_edge(
670
- frame.getbbox(), cell_width, cell_height, edge_touch_margin
671
- )
672
- if trim_border_px > 0:
673
- frame = trim_border(frame, px=trim_border_px)
674
- if edge_clean_depth > 0:
675
- frame = clean_edges(frame, depth=edge_clean_depth)
676
- components = connected_components(frame, min_area=min_component_area)
677
- bbox = None
678
- selected_component = None
679
- if component_mode == "largest" and components:
680
- selected_component = components[0]
681
- bbox = pad_bbox(
682
- tuple(selected_component["bbox"]), component_padding, frame.width, frame.height
683
- )
684
- else:
685
- bbox = frame.getbbox()
686
- if bbox:
687
- frame = frame.crop(bbox)
688
- cropped_frames.append(frame)
689
- frame_info.append(
690
- {
691
- "grid": [row, col],
692
- "source_box": list(box),
693
- "component_mode": component_mode,
694
- "component_count": len(components),
695
- "selected_component_area": int(selected_component["area"])
696
- if selected_component
697
- else None,
698
- "selected_component_bbox": list(selected_component["bbox"])
699
- if selected_component
700
- else None,
701
- "crop_bbox": list(bbox) if bbox else None,
702
- "edge_touch": edge_touch,
703
- }
704
- )
705
-
706
- common_scale = None
707
- if shared_scale:
708
- max_width = max((frame.size[0] for frame in cropped_frames), default=0)
709
- max_height = max((frame.size[1] for frame in cropped_frames), default=0)
710
- if max_width > 0 and max_height > 0:
711
- common_scale = min(cell_size / max_width, cell_size / max_height) * fit_scale
712
-
713
- frames: list[Image.Image] = []
714
- for index, frame in enumerate(cropped_frames):
715
- frame_width, frame_height = frame.size
716
- canvas = Image.new("RGBA", (cell_size, cell_size), (0, 0, 0, 0))
717
- if frame_width > 0 and frame_height > 0:
718
- scale = common_scale or (
719
- min(cell_size / frame_width, cell_size / frame_height) * fit_scale
720
- )
721
- new_width = max(1, int(frame_width * scale))
722
- new_height = max(1, int(frame_height * scale))
723
- frame = frame.resize((new_width, new_height), Image.Resampling.LANCZOS)
724
- paste_x = (cell_size - new_width) // 2
725
- if align in {"bottom", "feet"}:
726
- pad = max(0, int(cell_size * (1 - fit_scale) * 0.5))
727
- paste_y = cell_size - new_height - pad
728
- else:
729
- paste_y = (cell_size - new_height) // 2
730
- canvas.paste(frame, (paste_x, paste_y))
731
- frame_info[index]["output_size"] = [new_width, new_height]
732
- frame_info[index]["paste_position"] = [paste_x, paste_y]
733
- else:
734
- frame_info[index]["output_size"] = [0, 0]
735
- frame_info[index]["paste_position"] = [0, 0]
736
- frames.append(canvas)
737
- return frames, frame_info
738
-
739
-
740
- def compose_sheet(frames: list[Image.Image], rows: int, cols: int, cell_size: int) -> Image.Image:
741
- canvas = Image.new("RGBA", (cols * cell_size, rows * cell_size), (0, 0, 0, 0))
742
- for index, frame in enumerate(frames):
743
- row, col = divmod(index, cols)
744
- canvas.paste(frame, (col * cell_size, row * cell_size), frame)
745
- return canvas
746
-
747
-
748
- def save_transparent_gif(frames: list[Image.Image], out_path: Path, duration: int) -> None:
749
- if not frames:
750
- raise ValueError("No frames to encode.")
751
-
752
- key = (255, 0, 254)
753
- width, height = frames[0].size
754
- stacked = Image.new("RGB", (width, height * len(frames)), key)
755
-
756
- for index, frame in enumerate(frames):
757
- r, g, b, a = frame.split()
758
- hard_mask = a.point(lambda value: 255 if value >= 128 else 0)
759
- rgb = Image.merge("RGB", (r, g, b))
760
- stacked.paste(rgb, (0, index * height), hard_mask)
761
-
762
- paletted = stacked.convert(
763
- "P", palette=Image.Palette.ADAPTIVE, colors=256, dither=Image.Dither.NONE
764
- )
765
- palette = list(paletted.getpalette() or [])
766
- while len(palette) < 256 * 3:
767
- palette.append(0)
768
-
769
- key_index = None
770
- for index in range(256):
771
- if palette[index * 3 : index * 3 + 3] == list(key):
772
- key_index = index
773
- break
774
- if key_index is None:
775
- best_distance = None
776
- best_index = 0
777
- for index in range(256):
778
- r, g, b = palette[index * 3], palette[index * 3 + 1], palette[index * 3 + 2]
779
- distance = (r - key[0]) ** 2 + (g - key[1]) ** 2 + (b - key[2]) ** 2
780
- if best_distance is None or distance < best_distance:
781
- best_distance = distance
782
- best_index = index
783
- key_index = best_index
784
-
785
- if key_index != 0:
786
- lut = np.arange(256, dtype=np.uint8)
787
- lut[0], lut[key_index] = key_index, 0
788
- arr = np.array(paletted)
789
- arr = lut[arr]
790
- paletted = Image.fromarray(arr, mode="P")
791
- for channel in range(3):
792
- zero_idx = channel
793
- key_idx = key_index * 3 + channel
794
- palette[zero_idx], palette[key_idx] = palette[key_idx], palette[zero_idx]
795
- paletted.putpalette(palette)
796
-
797
- out_frames = [
798
- paletted.crop((0, index * height, width, (index + 1) * height))
799
- for index in range(len(frames))
800
- ]
801
- out_frames[0].save(
802
- out_path,
803
- format="GIF",
804
- save_all=True,
805
- append_images=out_frames[1:],
806
- duration=duration,
807
- loop=0,
808
- disposal=2,
809
- transparency=0,
810
- background=0,
811
- )
812
-
813
-
814
- def sanitize_slug(text: str) -> str:
815
- slug = re.sub(r"[^a-zA-Z0-9]+", "-", text.strip().lower()).strip("-")
816
- return slug or "sprite"
817
-
818
-
819
- def cmd_list_options() -> None:
820
- print(
821
- json.dumps(
822
- {
823
- "targets": TARGET_MODES,
824
- "npc_roles": NPC_ROLES,
825
- "grid_shapes": GRID_SHAPES,
826
- "frame_labels": FRAME_LABELS,
827
- "processor": {
828
- "component_mode": ["all", "largest"],
829
- "align": ["center", "bottom", "feet"],
830
- },
831
- },
832
- indent=2,
833
- )
834
- )
835
-
836
-
837
- def cmd_build_prompt(args: argparse.Namespace) -> None:
838
- prompt_text, seed = build_prompt(args.target, args.mode, args.prompt, args.role, args.seed)
839
- payload = {
840
- "target": args.target,
841
- "mode": args.mode,
842
- "prompt": args.prompt,
843
- "role": args.role or "",
844
- "seed": seed,
845
- "generated_prompt": prompt_text,
846
- }
847
- if args.write:
848
- args.write.parent.mkdir(parents=True, exist_ok=True)
849
- args.write.write_text(prompt_text, encoding="utf-8")
850
- if args.write_json:
851
- args.write_json.parent.mkdir(parents=True, exist_ok=True)
852
- args.write_json.write_text(json.dumps(payload, indent=2), encoding="utf-8")
853
- print(prompt_text)
854
-
855
-
856
- def cmd_process(args: argparse.Namespace) -> None:
857
- if args.target not in PROCESS_TARGETS:
858
- raise ValueError(
859
- f"Unknown process target '{args.target}'. Valid targets: {', '.join(PROCESS_TARGETS)}"
860
- )
861
- out_dir = args.output_dir
862
- out_dir.mkdir(parents=True, exist_ok=True)
863
-
864
- raw = Image.open(args.input).convert("RGBA")
865
- metadata = {
866
- "target": args.target,
867
- "mode": args.mode,
868
- "prompt": args.prompt or "",
869
- "role": args.role or "",
870
- "input": str(args.input),
871
- "threshold": args.threshold,
872
- "edge_threshold": args.edge_threshold,
873
- "duration": args.duration,
874
- }
875
-
876
- has_custom_grid = args.rows is not None or args.cols is not None
877
- if has_custom_grid and (args.rows is None or args.cols is None):
878
- raise ValueError("Custom grids require both --rows and --cols.")
879
-
880
- use_grid = has_custom_grid or args.mode in GRID_SHAPES
881
- if not use_grid and args.mode not in SINGLE_MODES:
882
- known = ", ".join(sorted(GRID_SHAPES))
883
- singles = ", ".join(sorted(SINGLE_MODES))
884
- raise ValueError(
885
- f"Mode '{args.mode}' is not a known grid shape and --rows/--cols "
886
- f"were not provided. Pass both --rows and --cols for a custom grid, "
887
- f"use a known grid mode ({known}), or a single-frame mode ({singles})."
888
- )
889
-
890
- if use_grid:
891
- if has_custom_grid:
892
- rows, cols = args.rows, args.cols
893
- else:
894
- rows, cols = GRID_SHAPES[args.mode]
895
- cell_size = args.cell_size or (96 if (rows, cols) == (4, 4) else 128)
896
- raw.save(out_dir / "raw-sheet.png")
897
- cleaned = remove_bg_magenta(raw.copy(), args.threshold, args.edge_threshold)
898
- cleaned.save(out_dir / "raw-sheet-clean.png")
899
-
900
- frames, frame_qc = split_grid(
901
- raw,
902
- rows,
903
- cols,
904
- cell_size,
905
- args.threshold,
906
- args.edge_threshold,
907
- fit_scale=args.fit_scale,
908
- trim_border_px=args.trim_border,
909
- edge_clean_depth=args.edge_clean_depth,
910
- align=args.align,
911
- shared_scale=args.shared_scale,
912
- component_mode=args.component_mode,
913
- component_padding=args.component_padding,
914
- min_component_area=args.min_component_area,
915
- edge_touch_margin=args.edge_touch_margin,
916
- )
917
- if has_custom_grid:
918
- prefix = args.label_prefix or args.mode
919
- labels = [f"{prefix}-{index + 1}" for index in range(rows * cols)]
920
- else:
921
- labels = FRAME_LABELS[args.mode]
922
- for label, frame in zip(labels, frames):
923
- frame.save(out_dir / f"{label}.png")
924
-
925
- compose_sheet(frames, rows, cols, cell_size).save(out_dir / "sheet-transparent.png")
926
-
927
- if args.mode == "player_sheet" and not has_custom_grid and (rows, cols) == (4, 4):
928
- directions = ["down", "left", "right", "up"]
929
- for row_index, direction in enumerate(directions):
930
- row_frames = frames[row_index * cols : (row_index + 1) * cols]
931
- compose_sheet(row_frames, 1, cols, cell_size).save(
932
- out_dir / f"{direction}-strip.png"
933
- )
934
- save_transparent_gif(row_frames, out_dir / f"{direction}.gif", args.duration)
935
- metadata["directions"] = directions
936
- else:
937
- save_transparent_gif(frames, out_dir / "animation.gif", args.duration)
938
-
939
- metadata["rows"] = rows
940
- metadata["cols"] = cols
941
- metadata["cell_size"] = cell_size
942
- metadata["fit_scale"] = args.fit_scale
943
- metadata["trim_border"] = args.trim_border
944
- metadata["edge_clean_depth"] = args.edge_clean_depth
945
- metadata["align"] = args.align
946
- metadata["shared_scale"] = args.shared_scale
947
- metadata["component_mode"] = args.component_mode
948
- metadata["component_padding"] = args.component_padding
949
- metadata["min_component_area"] = args.min_component_area
950
- metadata["edge_touch_margin"] = args.edge_touch_margin
951
- metadata["frame_labels"] = labels
952
- metadata["frames"] = frame_qc
953
- metadata["edge_touch_frames"] = [
954
- info["grid"] for info in frame_qc if bool(info.get("edge_touch"))
955
- ]
956
- if args.reject_edge_touch and metadata["edge_touch_frames"]:
957
- raise ValueError(f"Frames touch a cell edge: {metadata['edge_touch_frames']}")
958
- else:
959
- raw.save(out_dir / "raw.png")
960
- centered = center_single_sprite(raw, args.single_size, args.threshold, args.edge_threshold)
961
- centered.save(out_dir / "clean.png")
962
- metadata["single_size"] = args.single_size
963
-
964
- if args.prompt_file and args.prompt_file.exists():
965
- prompt_text = args.prompt_file.read_text(encoding="utf-8")
966
- (out_dir / "prompt-used.txt").write_text(prompt_text, encoding="utf-8")
967
- elif args.prompt:
968
- (out_dir / "prompt-used.txt").write_text(args.prompt, encoding="utf-8")
969
-
970
- (out_dir / "pipeline-meta.json").write_text(json.dumps(metadata, indent=2), encoding="utf-8")
971
- print(str(out_dir.resolve()))
972
-
973
-
974
- def build_parser() -> argparse.ArgumentParser:
975
- parser = argparse.ArgumentParser(description=__doc__)
976
- subparsers = parser.add_subparsers(dest="command", required=True)
977
-
978
- subparsers.add_parser("list-options", help="Print supported targets, modes, and NPC roles.")
979
-
980
- build_prompt_parser = subparsers.add_parser("build-prompt", help="Build a generation prompt.")
981
- build_prompt_parser.add_argument("--target", required=True, choices=sorted(TARGET_MODES))
982
- build_prompt_parser.add_argument("--mode", required=True)
983
- build_prompt_parser.add_argument("--prompt", required=True)
984
- build_prompt_parser.add_argument("--role")
985
- build_prompt_parser.add_argument("--seed", type=int)
986
- build_prompt_parser.add_argument("--write", type=Path)
987
- build_prompt_parser.add_argument("--write-json", type=Path)
988
-
989
- process_parser = subparsers.add_parser("process", help="Postprocess a generated sprite image.")
990
- process_parser.add_argument("--input", required=True, type=Path)
991
- process_parser.add_argument("--target", required=True, choices=PROCESS_TARGETS)
992
- process_parser.add_argument("--mode", required=True)
993
- process_parser.add_argument("--output-dir", required=True, type=Path)
994
- process_parser.add_argument("--role")
995
- process_parser.add_argument("--prompt")
996
- process_parser.add_argument("--prompt-file", type=Path)
997
- process_parser.add_argument("--threshold", type=int, default=100)
998
- process_parser.add_argument("--edge-threshold", type=int, default=150)
999
- process_parser.add_argument("--cell-size", type=int)
1000
- process_parser.add_argument("--rows", type=int)
1001
- process_parser.add_argument("--cols", type=int)
1002
- process_parser.add_argument("--label-prefix")
1003
- process_parser.add_argument("--fit-scale", type=float, default=0.85)
1004
- process_parser.add_argument("--trim-border", type=int, default=4)
1005
- process_parser.add_argument("--edge-clean-depth", type=int, default=3)
1006
- process_parser.add_argument("--align", choices=["center", "bottom", "feet"], default="center")
1007
- process_parser.add_argument("--shared-scale", action="store_true")
1008
- process_parser.add_argument("--component-mode", choices=["all", "largest"], default="all")
1009
- process_parser.add_argument("--component-padding", type=int, default=0)
1010
- process_parser.add_argument("--min-component-area", type=int, default=1)
1011
- process_parser.add_argument("--edge-touch-margin", type=int, default=0)
1012
- process_parser.add_argument("--reject-edge-touch", action="store_true")
1013
- process_parser.add_argument("--single-size", type=int, default=256)
1014
- process_parser.add_argument("--duration", type=int, default=200)
1015
-
1016
- return parser
1017
-
1018
-
1019
- def main() -> None:
1020
- parser = build_parser()
1021
- args = parser.parse_args()
1022
- if args.command == "list-options":
1023
- cmd_list_options()
1024
- elif args.command == "build-prompt":
1025
- cmd_build_prompt(args)
1026
- else:
1027
- cmd_process(args)
1028
-
1029
-
1030
- if __name__ == "__main__":
1031
- main()