@inneranimalmedia/agentsam-sdk 2.6.5 → 2.6.7
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/AGENTSAM.md +29 -0
- package/apps/README.md +5 -0
- package/apps/cad-creator/README.md +4 -4
- package/apps/cad-creator/shared/cad/package.json +1 -1
- package/apps/client-cms-editor/AGENTS.md +12 -7
- package/apps/client-cms-editor/README.md +76 -48
- package/apps/client-cms-editor/agentsam.app.json +26 -9
- package/apps/client-cms-editor/backend/package.json +20 -3
- package/apps/client-cms-editor/backend/src/api/client.ts +134 -101
- package/apps/client-cms-editor/backend/src/index.ts +1 -1
- package/apps/client-cms-editor/backend/src/model.ts +11 -115
- package/apps/client-cms-editor/backend/src/preview/bridge.ts +1 -1
- package/apps/client-cms-editor/backend/tsconfig.json +6 -1
- package/apps/client-cms-editor/bin/agentsam-cms.js +262 -0
- package/apps/client-cms-editor/bin/agentsam-cms.mjs +2 -228
- package/apps/client-cms-editor/frontend/package.json +16 -7
- package/apps/client-cms-editor/frontend/src/AgentSamDrawer.tsx +9 -41
- package/apps/client-cms-editor/frontend/src/CmsAgentSurface.tsx +19 -174
- package/apps/client-cms-editor/frontend/src/adapters/heuristic.ts +29 -0
- package/apps/client-cms-editor/frontend/src/adapters/http.ts +628 -0
- package/apps/client-cms-editor/frontend/src/adapters/memory.ts +510 -0
- package/apps/client-cms-editor/frontend/src/editor/CmsEditor.tsx +213 -0
- package/apps/client-cms-editor/frontend/src/editor/CmsEditorProvider.tsx +20 -0
- package/apps/client-cms-editor/frontend/src/editor/canvas/PreviewCanvas.tsx +53 -0
- package/apps/client-cms-editor/frontend/src/{CmsEditor.tsx → editor/legacy/CmsEditorPrototype.tsx} +52 -23
- package/apps/client-cms-editor/frontend/src/editor/useCmsEditorController.ts +218 -0
- package/apps/client-cms-editor/frontend/src/index.ts +19 -5
- package/apps/client-cms-editor/frontend/src/lib/agent-host.ts +24 -0
- package/apps/client-cms-editor/frontend/src/main.tsx +82 -8
- package/apps/client-cms-editor/frontend/src/mount.tsx +92 -30
- package/apps/client-cms-editor/frontend/src/styles/agentsam-drawer.css +1 -1
- package/apps/client-cms-editor/frontend/src/styles/cmsShell.css +8 -8
- package/apps/client-cms-editor/frontend/src/styles/studio.css +55 -3
- package/apps/client-cms-editor/frontend/tsconfig.json +12 -1
- package/apps/client-cms-editor/frontend/vite.config.ts +12 -0
- package/apps/client-cms-editor/package-lock.json +973 -134
- package/apps/client-cms-editor/package.json +118 -6
- package/apps/client-cms-editor/reference/harvest/HARVEST_RECEIPT.json +201 -0
- package/apps/client-cms-editor/reference/harvest/PROVENANCE_HISTORY.txt +10 -0
- package/apps/client-cms-editor/reference/harvest/README.md +8 -0
- package/apps/client-cms-editor/reference/harvest/plans/ARCHIVE-INTAKE.md +135 -0
- package/apps/client-cms-editor/reference/harvest/plans/CHATGPT-IDENTITY-HARVEST.md +83 -0
- package/apps/client-cms-editor/reference/harvest/plans/CMS-EDITOR-HARVEST.md +166 -0
- package/apps/client-cms-editor/reference/harvest/plans/HARVEST-INDEX.md +196 -0
- package/apps/client-cms-editor/reference/harvest/plans/INNERANIMALS-CONTENT-001.md +137 -0
- package/apps/client-cms-editor/reference/harvest/plans/NO-IMPLICIT-SIDE-EFFECTS.md +87 -0
- package/apps/client-cms-editor/reference/harvest/plans/STOREFRONT-SURFACE-MAP.md +140 -0
- package/apps/client-cms-editor/reference/harvest/plans/THEME-INNERANIMALS-HARVEST.md +137 -0
- package/apps/client-cms-editor/reference/harvest/plans/TOOLING-LAB.md +63 -0
- package/apps/client-cms-editor/reference/harvest/plans/harvest-manifest.json +97 -0
- package/apps/client-cms-editor/reference/harvest/plans/local-cms-harvest-schema.sql +328 -0
- package/apps/client-cms-editor/shared/cms/package.json +17 -7
- package/apps/client-cms-editor/shared/cms/src/adapter.ts +204 -0
- package/apps/client-cms-editor/shared/cms/src/agent-context.ts +7 -5
- package/apps/client-cms-editor/shared/cms/src/editor-types.ts +32 -0
- package/apps/client-cms-editor/shared/cms/src/host.ts +82 -0
- package/apps/client-cms-editor/shared/cms/src/index.ts +4 -0
- package/apps/client-cms-editor/shared/cms/src/map.ts +115 -0
- package/apps/client-cms-editor/shared/cms/src/starter-pack.ts +158 -0
- package/contracts/source-types.v1.json +353 -0
- package/docs/CMS_CONTEXTUAL_COMPOSER.md +1 -1
- package/docs/PRODUCT_LIFECYCLE.md +566 -0
- package/docs/THEME_REFINERY.md +2 -2
- package/docs/architecture/AGENTSAM_RUNTIME_PROTOCOL.md +40 -20
- package/docs/content-studio/REVISION_GATE_2026-09-27.md +1 -1
- package/docs/plans/LOCAL_STUDIO_DESKTOP_RELEASE_2026-09-29.md +150 -0
- package/migrations/d1/0017_identity_native_oauth.sql +21 -0
- package/native/agentsam-machine/Cargo.lock +426 -0
- package/native/agentsam-machine/Cargo.toml +9 -0
- package/native/agentsam-machine/cli/Cargo.toml +14 -0
- package/native/agentsam-machine/cli/src/main.rs +77 -0
- package/native/agentsam-machine/core/Cargo.toml +15 -0
- package/native/agentsam-machine/core/src/assets.rs +790 -0
- package/native/agentsam-machine/core/src/bound.rs +298 -0
- package/native/agentsam-machine/core/src/enrich.rs +706 -0
- package/native/agentsam-machine/core/src/frontend.rs +442 -0
- package/native/agentsam-machine/core/src/lib.rs +1086 -0
- package/native/agentsam-machine/core/src/liquid.rs +528 -0
- package/native/agentsam-machine/core/src/source_types.rs +78 -0
- package/package.json +51 -21
- package/packages/agentsam-brand/README.md +1 -1
- package/packages/agentsam-brand/package.json +4 -4
- package/packages/agentsam-brand/src/index.js +1 -1
- package/packages/agentsam-contracts/package.json +13 -3
- package/packages/agentsam-contracts/src/tools.ts +22 -0
- package/packages/agentsam-errors/package.json +13 -3
- package/packages/agentsam-knowledge/package.json +18 -4
- package/packages/agentsam-knowledge/src/autorag/index.js +10 -3
- package/packages/agentsam-knowledge/src/backends/index.js +33 -1
- package/packages/agentsam-knowledge/test/autorag.test.mjs +60 -1
- package/packages/agentsam-nav/package.json +24 -6
- package/packages/agentsam-queue-control/README.md +1 -1
- package/packages/agentsam-queue-control/package.json +7 -2
- package/packages/agentsam-rapid-rust/src/main.rs +1 -1
- package/packages/agentsam-repository/package.json +13 -3
- package/packages/agentsam-repository/src/source-types.js +8 -0
- package/packages/agentsam-vault/package.json +14 -4
- package/packages/agentsam-workbench/package.json +15 -4
- package/packages/agentsam-workbench/src/agent/ToolPermissionRequest.tsx +94 -0
- package/packages/agentsam-workbench/src/agent/index.ts +2 -0
- package/packages/agentsam-workbench/src/agent/tool-permission.css +134 -0
- package/packages/connectors/cloudflare/package.json +13 -3
- package/packages/connectors/cloudflare/src/api-client.js +3 -2
- package/packages/connectors/cloudflare/src/families/vectorize.js +127 -0
- package/packages/connectors/cloudflare/src/index.js +1 -0
- package/packages/heuristic-theme/README.md +2 -2
- package/packages/heuristic-theme/package.json +8 -3
- package/packages/heuristic-theme/src/registry.js +2 -2
- package/packages/heuristic-theme/src/theme.js +1 -1
- package/packages/heuristic-theme/test/theme.test.mjs +1 -1
- package/packages/identity/.agentsam/features/oauth-login-portal/agentsam.feature.json +1 -1
- package/packages/identity/.agentsam/features/oauth-login-portal/providers.json +27 -7
- package/packages/identity/README.md +2 -2
- package/packages/identity/migrations/D1_SCHEMA_MAPPING.md +18 -15
- package/packages/identity/migrations/apply-portable.js +109 -0
- package/packages/identity/migrations/sqlite/004_identity_oauth_consent_catalog.sql +1 -1
- package/packages/identity/migrations/sqlite/005_identity_company_native.sql +52 -0
- package/packages/identity/package.json +27 -6
- package/packages/identity/schema/agentsam.identity/manifest.json +101 -0
- package/packages/identity/scripts/local-identity-bridge.mjs +13 -7
- package/packages/identity/scripts/preview-auth-portal.mjs +6 -6
- package/packages/identity/src/adapters/cloudflare-d1/index.js +8 -401
- package/packages/identity/src/adapters/iam-compat/index.js +540 -0
- package/packages/identity/src/adapters/portable-d1/index.js +18 -0
- package/packages/identity/src/adapters/portable-store.js +479 -0
- package/packages/identity/src/adapters/sqlite/index.js +13 -312
- package/packages/identity/src/contracts/auth-config.js +0 -1
- package/packages/identity/src/contracts/identity-store.js +56 -5
- package/packages/identity/src/contracts/provider.js +1 -0
- package/packages/identity/src/contracts/sign-in-options.js +104 -0
- package/packages/identity/src/core/constants.js +1 -1
- package/packages/identity/src/core/session-policy.js +31 -0
- package/packages/identity/src/frontend/auth-portal/pages/login.html +6 -6
- package/packages/identity/src/frontend/auth-portal/pages/reset.html +5 -5
- package/packages/identity/src/frontend/auth-portal/pages/signup.html +5 -5
- package/packages/identity/src/frontend/auth-portal/shared/company-branding.js +1 -1
- package/packages/identity/src/frontend/auth-portal/shared/desktop-identity-bridge.js +6 -37
- package/packages/identity/src/index.js +28 -1
- package/packages/identity/src/migrations/README.md +23 -23
- package/packages/identity/src/oauth/README.md +1 -1
- package/packages/identity/src/oauth/credentials.js +10 -11
- package/packages/identity/src/oauth/google-desktop-exchange.js +195 -27
- package/packages/identity/src/oauth/iam-platform.js +21 -9
- package/packages/identity/src/oauth/native-handoff.js +136 -0
- package/packages/identity/src/providers/chatgpt/headers.js +65 -0
- package/packages/identity/src/providers/chatgpt/index.js +86 -0
- package/packages/identity/src/providers/chatgpt/mapper.js +43 -0
- package/packages/identity/src/providers/chatgpt/paths.js +45 -0
- package/packages/identity/src/providers/github/oauth.js +1 -2
- package/packages/identity/src/providers/github/profile.js +16 -2
- package/packages/identity/src/providers/index.js +11 -3
- package/packages/identity/src/server/identity-service.js +2 -1
- package/packages/identity/src/server/worker-router.js +71 -18
- package/packages/identity/tests/chatgpt-provider.test.mjs +75 -0
- package/packages/identity/tests/google-desktop-identity-login.test.mjs +210 -0
- package/packages/identity/tests/identity-init-scaffold.test.mjs +15 -4
- package/packages/identity/tests/lane1-identity-store-parity.test.mjs +527 -0
- package/packages/identity/tests/lane1-packed-consumer.test.mjs +175 -0
- package/packages/identity/tests/native-oauth-handoff.test.mjs +292 -0
- package/packages/identity/tests/oauth-credentials.test.mjs +4 -4
- package/packages/identity/tests/preview-auth-portal.test.mjs +4 -2
- package/packages/identity/tests/session-routes-policy.test.mjs +13 -1
- package/packages/identity/tests/worker-runtime-boundary.test.mjs +11 -0
- package/packages/providers/completeful/package.json +7 -2
- package/packages/theme-church-site/package.json +17 -6
- package/packages/theme-church-site/src/harvest.json +4 -2
- package/packages/theme-church-site/src/index.js +7 -3
- package/packages/theme-church-site/test/theme.test.mjs +4 -2
- package/packages/theme-companions-site/package.json +17 -6
- package/packages/theme-companions-site/src/harvest.json +4 -2
- package/packages/theme-companions-site/src/index.js +7 -3
- package/packages/theme-companions-site/test/theme.test.mjs +4 -2
- package/packages/theme-floors-site/package.json +17 -6
- package/packages/theme-floors-site/src/harvest.json +4 -2
- package/packages/theme-floors-site/src/index.js +7 -3
- package/packages/theme-floors-site/test/theme.test.mjs +4 -2
- package/packages/theme-fuelnfree-site/package.json +17 -6
- package/packages/theme-fuelnfree-site/src/harvest.json +4 -2
- package/packages/theme-fuelnfree-site/src/index.js +7 -3
- package/packages/theme-fuelnfree-site/test/theme.test.mjs +4 -2
- package/packages/theme-handyman-site/package.json +17 -6
- package/packages/theme-handyman-site/src/harvest.json +4 -2
- package/packages/theme-handyman-site/src/index.js +7 -3
- package/packages/theme-handyman-site/test/theme.test.mjs +4 -2
- package/packages/theme-insurance-site/package.json +17 -6
- package/packages/theme-insurance-site/src/harvest.json +4 -2
- package/packages/theme-insurance-site/src/index.js +7 -3
- package/packages/theme-insurance-site/test/theme.test.mjs +4 -2
- package/packages/theme-scenes/README.md +1 -1
- package/packages/theme-scenes/package.json +4 -3
- package/packages/theme-scenes/src/registry.js +187 -0
- package/packages/theme-shinshu-site/package.json +17 -6
- package/packages/theme-shinshu-site/src/harvest.json +4 -2
- package/packages/theme-shinshu-site/src/index.js +7 -3
- package/packages/theme-shinshu-site/test/theme.test.mjs +4 -2
- package/packages/work-graph/package.json +14 -4
- package/protocol/FILEMETA_V1.md +2 -2
- package/protocol/capabilities/manifest.json +1 -1
- package/protocol/database/agentsam.schema-pack.v1.schema.json +72 -0
- package/protocol/presets/catalog.json +2 -2
- package/protocol/theme-refinery/lineage-registry.json +105 -0
- package/protocol/theme-refinery/product-metadata.schema.json +21 -1
- package/skills/grok-auth/SKILL.md +110 -0
- package/skills/grok-auth/agentsam.skill.json +25 -0
- package/skills/grok-auth/references/grok-identity.md +64 -0
- package/skills/grok-auth/references/per-user-data.md +49 -0
- package/skills/grok-auth/references/prewired-and-env.md +54 -0
- package/skills/grok-auth/references/session-ui.md +133 -0
- package/skills/grok-auth/references/sign-in-methods.md +55 -0
- package/skills/grok-auth/references/wiring.md +71 -0
- package/skills/grok-building-games/SKILL.md +213 -0
- package/skills/grok-building-games/agentsam.skill.json +25 -0
- package/skills/grok-building-games/references/3d-libs.md +64 -0
- package/skills/grok-building-games/references/ai-pathfinding.md +122 -0
- package/skills/grok-building-games/references/audio.md +129 -0
- package/skills/grok-building-games/references/babylon.md +202 -0
- package/skills/grok-building-games/references/collision-physics.md +122 -0
- package/skills/grok-building-games/references/ecs-architecture.md +111 -0
- package/skills/grok-building-games/references/game-feel-juice.md +110 -0
- package/skills/grok-building-games/references/genres/board-card-chess.md +89 -0
- package/skills/grok-building-games/references/genres/endless-runner.md +83 -0
- package/skills/grok-building-games/references/genres/fps.md +105 -0
- package/skills/grok-building-games/references/genres/platformer-2d.md +97 -0
- package/skills/grok-building-games/references/genres/puzzle-match3-tetris.md +131 -0
- package/skills/grok-building-games/references/genres/racing-kart.md +106 -0
- package/skills/grok-building-games/references/genres/topdown-twin-stick.md +97 -0
- package/skills/grok-building-games/references/genres/tower-defense.md +84 -0
- package/skills/grok-building-games/references/genres/voxel-minecraft.md +89 -0
- package/skills/grok-building-games/references/input.md +139 -0
- package/skills/grok-building-games/references/phaser.md +277 -0
- package/skills/grok-building-games/references/procedural-generation.md +141 -0
- package/skills/grok-building-games/references/save-persistence.md +108 -0
- package/skills/grok-building-games/references/threejs-foundational.md +148 -0
- package/skills/grok-controls/SKILL.md +274 -0
- package/skills/grok-controls/agentsam.skill.json +25 -0
- package/skills/grok-design-ui/SKILL.md +122 -0
- package/skills/grok-design-ui/agentsam.skill.json +25 -0
- package/skills/grok-design-ui/references/animations.md +379 -0
- package/skills/grok-design-ui/references/performance.md +88 -0
- package/skills/grok-design-ui/references/refined-ui.md +384 -0
- package/skills/grok-design-ui/references/surfaces.md +247 -0
- package/skills/grok-design-ui/references/typography.md +123 -0
- package/skills/grok-game-animation-frames/SKILL.md +84 -0
- package/skills/grok-game-animation-frames/agentsam.skill.json +25 -0
- package/skills/grok-game-asset-core/SKILL.md +69 -0
- package/skills/grok-game-asset-core/agentsam.skill.json +25 -0
- package/skills/grok-game-character-consistency/SKILL.md +69 -0
- package/skills/grok-game-character-consistency/agentsam.skill.json +25 -0
- package/skills/grok-game-tilesets/SKILL.md +65 -0
- package/skills/grok-game-tilesets/agentsam.skill.json +25 -0
- package/skills/grok-game-ui-icons/SKILL.md +56 -0
- package/skills/grok-game-ui-icons/agentsam.skill.json +25 -0
- package/skills/grok-generate2dmap/LICENSE +21 -0
- package/skills/grok-generate2dmap/SKILL.md +120 -0
- package/skills/grok-generate2dmap/SOURCE.md +6 -0
- package/skills/grok-generate2dmap/agentsam.skill.json +25 -0
- package/skills/grok-generate2dmap/references/deliverables.md +101 -0
- package/skills/grok-generate2dmap/references/layered-map-contract.md +209 -0
- package/skills/grok-generate2dmap/references/map-strategies.md +341 -0
- package/skills/grok-generate2dmap/references/object-production-gate.md +24 -0
- package/skills/grok-generate2dmap/references/pipeline.md +149 -0
- package/skills/grok-generate2dmap/references/prop-pack-contract.md +174 -0
- package/skills/grok-generate2dmap/references/side-scroll-stages.md +29 -0
- package/skills/grok-generate2dmap/scripts/compose_layered_preview.py +157 -0
- package/skills/grok-generate2dmap/scripts/extract_prop_pack.py +369 -0
- package/skills/grok-generate2dsprite/LICENSE +21 -0
- package/skills/grok-generate2dsprite/SKILL.md +313 -0
- package/skills/grok-generate2dsprite/SOURCE.md +6 -0
- package/skills/grok-generate2dsprite/agentsam.skill.json +25 -0
- package/skills/grok-generate2dsprite/references/modes.md +124 -0
- package/skills/grok-generate2dsprite/references/prompt-rules.md +315 -0
- package/skills/grok-generate2dsprite/scripts/generate2dsprite.py +1031 -0
- package/skills/grok-generate2dsprite/scripts/make_layout_guide.py +109 -0
- package/skills/grok-imagine/SKILL.md +201 -0
- package/skills/grok-imagine/agentsam.skill.json +25 -0
- package/skills/grok-multiplayer-p2p/SKILL.md +111 -0
- package/skills/grok-multiplayer-p2p/agentsam.skill.json +25 -0
- package/skills/grok-multiplayer-p2p/references/react-binding.md +144 -0
- package/skills/grok-multiplayer-p2p/references/signaling-relay.md +269 -0
- package/skills/grok-neon/SKILL.md +157 -0
- package/skills/grok-neon/agentsam.skill.json +25 -0
- package/skills/grok-og/SKILL.md +102 -0
- package/skills/grok-og/agentsam.skill.json +25 -0
- package/skills/grok-og/references/brand-pass.md +53 -0
- package/skills/grok-og/references/custom-card.md +107 -0
- package/skills/grok-og/references/favicon-and-icons.md +48 -0
- package/skills/grok-og/references/og-type-contract.md +18 -0
- package/skills/grok-og/references/placeholder-card.md +21 -0
- package/skills/grok-og/references/x-banner.md +41 -0
- package/skills/grok-threejs/SKILL.md +67 -0
- package/skills/grok-threejs/agentsam.skill.json +25 -0
- package/skills/grok-threejs/references/llms-full.txt +2780 -0
- package/skills/grok-video2dsprite/LICENSE +21 -0
- package/skills/grok-video2dsprite/SKILL.md +236 -0
- package/skills/grok-video2dsprite/SOURCE.md +6 -0
- package/skills/grok-video2dsprite/agentsam.skill.json +25 -0
- package/skills/grok-video2dsprite/references/pipeline.md +90 -0
- package/skills/grok-video2dsprite/references/prompt-rules.md +63 -0
- package/skills/grok-video2dsprite/scripts/video2dsprite.py +490 -0
- package/skills/grok-xai-api/SKILL.md +152 -0
- package/skills/grok-xai-api/agentsam.skill.json +25 -0
- package/src/cli/command-catalog.js +46 -4
- package/src/cli/dispatch.js +1 -0
- package/src/cli.js +10 -0
- package/src/cloudflare/runtime-status.js +2 -1
- package/src/commands/autorag.js +67 -13
- package/src/commands/cheat-sheet.js +29 -4
- package/src/commands/codebaseindex.js +45 -2
- package/src/commands/knowledge.js +59 -2
- package/src/commands/machine-binary.js +103 -0
- package/src/commands/machine.js +137 -0
- package/src/commands/security.js +67 -4
- package/src/commands/setup.js +33 -16
- package/src/commands/status.js +8 -1
- package/src/features/resolve.js +1 -0
- package/src/knowledge/engine.js +21 -2
- package/src/knowledge/runtime-discovery.js +127 -0
- package/src/lib/identity-scaffold.js +53 -13
- package/src/lib/setup/recipes.js +1 -1
- package/src/models/discovery.js +40 -22
- package/src/models/inventory-core.d.ts +105 -0
- package/src/models/inventory-core.js +54 -6
- package/src/security/logs.js +3 -0
- package/src/ui/cli/help.js +41 -0
- package/test/apps-scaffold-contract.test.mjs +3 -2
- package/test/cli/api-key-env-whoami.test.mjs +2 -2
- package/test/cli/catalog-authority.test.mjs +64 -0
- package/test/cli/machine-command.test.mjs +34 -0
- package/test/fixtures/project-authority/customer-app/.agentsam/app.json +5 -0
- package/test/fixtures/project-authority/customer-app/apps/my-product/index.ts +1 -0
- package/test/fixtures/project-authority/customer-app/wrangler.toml +17 -0
- package/test/integration/autorag-project-authority.integration.test.mjs +51 -0
- package/test/integration/autorag-runtime-discovery.test.mjs +107 -0
- package/test/integration/cms-app-scaffold.test.mjs +37 -31
- package/test/integration/cms-site-tenancy-contract.test.mjs +28 -17
- package/test/integration/codebaseindex-ingest.test.mjs +41 -0
- package/test/integration/database-hosted-honesty.test.mjs +49 -0
- package/test/integration/desktop-keychain-boundary.test.mjs +45 -0
- package/test/integration/hosted-work-host-authority.test.mjs +47 -0
- package/test/integration/inventory-eligibility.test.mjs +59 -0
- package/test/integration/local-provider-bridge.test.mjs +65 -0
- package/test/integration/local-studio-settings-production.test.mjs +40 -0
- package/test/integration/lockfile-sync.test.mjs +26 -0
- package/test/integration/provider-env-cli.test.mjs +13 -3
- package/test/integration/runtime-status.integration.test.mjs +16 -1
- package/test/integration/theme-package-registry.test.mjs +31 -0
- package/test/repository-snapshot-view.test.mjs +2 -2
- package/test/security.test.mjs +14 -0
- package/test/terminal-machine-identity.test.mjs +2 -2
- package/apps/client-cms-editor/backend/src/demo-bootstrap.ts +0 -145
- package/packages/agentsam-nav/dist/Nav.d.ts +0 -43
- package/packages/agentsam-nav/dist/NavCollapsible.d.ts +0 -10
- package/packages/agentsam-nav/dist/NavContent.d.ts +0 -2
- package/packages/agentsam-nav/dist/NavFooter.d.ts +0 -2
- package/packages/agentsam-nav/dist/NavGroup.d.ts +0 -3
- package/packages/agentsam-nav/dist/NavHeader.d.ts +0 -2
- package/packages/agentsam-nav/dist/NavMenu.d.ts +0 -22
- package/packages/agentsam-nav/dist/NavProvider.d.ts +0 -35
- package/packages/agentsam-nav/dist/NavTrigger.d.ts +0 -2
- package/packages/agentsam-nav/dist/Surfaces.d.ts +0 -41
- package/packages/agentsam-nav/dist/browser.d.ts +0 -11
- package/packages/agentsam-nav/dist/browser.global.js +0 -132
- package/packages/agentsam-nav/dist/browser.js +0 -613
- package/packages/agentsam-nav/dist/contracts.d.ts +0 -76
- package/packages/agentsam-nav/dist/index.d.ts +0 -11
- package/packages/agentsam-nav/dist/index.js +0 -614
- package/packages/agentsam-nav/dist/legacy.css +0 -83
- package/packages/agentsam-nav/dist/shell.css +0 -113
- package/packages/agentsam-nav/dist/theme.css +0 -113
- package/packages/agentsam-nav/dist/utils.d.ts +0 -1
- package/packages/agentsam-repository/src/goap.js.bak-20260922011251 +0 -670
|
@@ -0,0 +1,1031 @@
|
|
|
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()
|