diff --git a/package-lock.json b/package-lock.json index ba11f83..8933471 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "devDependencies": { "@biomejs/biome": "^1.9.0", "fake-indexeddb": "^6.2.5", + "tsx": "^4.0.0", "typescript": "~5.8.0", "vite": "^6.0.0", "vitest": "^3.0.0" @@ -1305,6 +1306,19 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/js-tokens": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", @@ -1421,6 +1435,16 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/rollup": { "version": "4.60.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", @@ -1571,6 +1595,510 @@ "node": ">=14.0.0" } }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", diff --git a/package.json b/package.json index 0496578..812e0e1 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,13 @@ "test": "vitest run", "test:watch": "vitest", "lint": "biome check .", - "lint:fix": "biome check --write ." + "lint:fix": "biome check --write .", + "generate-assets": "tsx scripts/generate-assets.ts" }, "devDependencies": { "@biomejs/biome": "^1.9.0", "fake-indexeddb": "^6.2.5", + "tsx": "^4.0.0", "typescript": "~5.8.0", "vite": "^6.0.0", "vitest": "^3.0.0" diff --git a/scripts/generate-assets.ts b/scripts/generate-assets.ts new file mode 100644 index 0000000..1cc9118 --- /dev/null +++ b/scripts/generate-assets.ts @@ -0,0 +1,241 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; +import { generateImage } from "../src/api/gemini"; +import { loadConfigFromObject } from "../src/api/config"; +import type { GeminiConfig } from "../src/api/config"; + +// Visual descriptions per companion (from SPEC.md section 9.2) +const companionDefs = [ + { + type: "fee-lila", + name: "Fee Lila", + visualDescription: + "a small fairy with lavender-purple wings, a flowing lilac dress, big warm brown eyes, a tiny flower crown made of forget-me-nots, light skin, and short wavy auburn hair", + }, + { + type: "einhorn-stella", + name: "Einhorn Stella", + visualDescription: + "a small unicorn with a white coat, a shimmering golden horn, a flowing mane in soft rainbow pastels (pink, lavender, light blue), big dark eyes with long lashes, and small silver hooves", + }, + { + type: "fuchs-finn", + name: "Fuchs Finn", + visualDescription: + "a young red fox with warm orange-brown fur, a white chest and belly, a bushy tail with a white tip, bright curious amber eyes, and slightly oversized pointed ears", + }, + { + type: "eule-elsa", + name: "Eule Elsa", + visualDescription: + "a small round owl with soft brown-and-cream feathers, a heart-shaped face, large round golden eyes, tiny ear tufts, and small talons perched on a mossy branch", + }, +]; + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function saveBlobToFile(blob: Blob, filePath: string): Promise { + const buffer = Buffer.from(await blob.arrayBuffer()); + const dir = path.dirname(filePath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(filePath, buffer); +} + +async function main(): Promise { + // Load config from public/config.json + const configPath = path.resolve("public/config.json"); + if (!fs.existsSync(configPath)) { + console.error("ERROR: public/config.json not found"); + process.exit(1); + } + + const configData = JSON.parse( + fs.readFileSync(configPath, "utf-8"), + ) as GeminiConfig; + + if (!configData.geminiApiKey || !configData.imageModel) { + console.error("ERROR: config.json missing geminiApiKey or imageModel"); + process.exit(1); + } + + loadConfigFromObject(configData); + + let failures = 0; + + // Generate character sheets and avatars for each companion + for (const companion of companionDefs) { + const charSheetPath = path.resolve( + `src/assets/companions/${companion.type}/character-sheet.png`, + ); + const avatarPath = path.resolve( + `src/assets/companions/${companion.type}/avatar.png`, + ); + + // Skip if both files already exist and are non-empty + if ( + fs.existsSync(charSheetPath) && + fs.statSync(charSheetPath).size > 1024 && + fs.existsSync(avatarPath) && + fs.statSync(avatarPath).size > 1024 + ) { + console.log( + `Skipping ${companion.name} — character-sheet.png and avatar.png already exist`, + ); + continue; + } + + // Generate character sheet + console.log(`Generating character sheet for ${companion.name}...`); + const charSheetPrompt = `Character sheet for a children's book. The character is ${companion.name}, ${companion.visualDescription}. +Show the character in 3 views: front view, side view, and a close-up of the face. +All views must show the EXACT SAME character with identical colors, proportions, and features. +Art style: Watercolor children's book illustration, warm pastel colors, soft edges, magical forest theme. White background. +Label each view clearly: "FRONT" "SIDE" "FACE" +The character must be: cute, friendly, approachable, with large expressive eyes. Suitable for a 7-year-old audience.`; + + try { + const charSheetBlob = await generateImage( + charSheetPrompt, + [], + configData, + ); + if (charSheetBlob) { + await saveBlobToFile(charSheetBlob, charSheetPath); + console.log( + ` Saved character sheet: ${charSheetPath} (${fs.statSync(charSheetPath).size} bytes)`, + ); + } else { + console.error( + ` ERROR: Failed to generate character sheet for ${companion.name}`, + ); + failures++; + continue; // Skip avatar if character sheet failed (needed as reference) + } + } catch (err) { + console.error( + ` ERROR generating character sheet for ${companion.name}:`, + err, + ); + failures++; + continue; + } + + await sleep(2000); // Rate limit delay + + // Generate avatar using character sheet as reference + console.log(`Generating avatar for ${companion.name}...`); + const avatarPrompt = `Create a single portrait of this exact character from the reference sheet. +Show only the face and upper body, looking directly at the viewer with a warm, friendly expression. +Same art style as the reference. Square format. No text. No background (or simple soft gradient).`; + + try { + // Read the character sheet back as a reference image + const charSheetBuffer = fs.readFileSync(charSheetPath); + const charSheetRef = new Blob([charSheetBuffer], { + type: "image/png", + }); + + const avatarBlob = await generateImage( + avatarPrompt, + [charSheetRef], + configData, + ); + if (avatarBlob) { + await saveBlobToFile(avatarBlob, avatarPath); + console.log( + ` Saved avatar: ${avatarPath} (${fs.statSync(avatarPath).size} bytes)`, + ); + } else { + console.error( + ` ERROR: Failed to generate avatar for ${companion.name}`, + ); + failures++; + } + } catch (err) { + console.error( + ` ERROR generating avatar for ${companion.name}:`, + err, + ); + failures++; + } + + await sleep(2000); // Rate limit delay + } + + // Generate style reference (initial forest scene) + const styleRefPath = path.resolve( + "src/assets/style-reference/initial-forest-scene.png", + ); + + if ( + fs.existsSync(styleRefPath) && + fs.statSync(styleRefPath).size > 1024 + ) { + console.log( + "Skipping style reference — initial-forest-scene.png already exists", + ); + } else { + console.log("Generating style reference (initial forest scene)..."); + const styleRefPrompt = `Watercolor children's book illustration of a magical forest clearing. +Warm pastel colors, soft edges, dreamy atmosphere. A gentle meadow surrounded +by friendly trees, dappled sunlight, small flowers, and a hint of magic sparkles. +No text, no letters, no characters. This sets the visual style for all future forest images.`; + + try { + const styleRefBlob = await generateImage( + styleRefPrompt, + [], + configData, + ); + if (styleRefBlob) { + await saveBlobToFile(styleRefBlob, styleRefPath); + console.log( + ` Saved style reference: ${styleRefPath} (${fs.statSync(styleRefPath).size} bytes)`, + ); + } else { + console.error(" ERROR: Failed to generate style reference"); + failures++; + } + } catch (err) { + console.error(" ERROR generating style reference:", err); + failures++; + } + } + + // Summary + console.log("\n--- Generation Summary ---"); + const pngFiles = [ + "src/assets/companions/fee-lila/character-sheet.png", + "src/assets/companions/fee-lila/avatar.png", + "src/assets/companions/einhorn-stella/character-sheet.png", + "src/assets/companions/einhorn-stella/avatar.png", + "src/assets/companions/fuchs-finn/character-sheet.png", + "src/assets/companions/fuchs-finn/avatar.png", + "src/assets/companions/eule-elsa/character-sheet.png", + "src/assets/companions/eule-elsa/avatar.png", + "src/assets/style-reference/initial-forest-scene.png", + ]; + + let existing = 0; + for (const f of pngFiles) { + const resolved = path.resolve(f); + if (fs.existsSync(resolved) && fs.statSync(resolved).size > 1024) { + existing++; + console.log(` OK: ${f}`); + } else { + console.log(` MISSING: ${f}`); + } + } + + console.log(`\n${existing}/9 assets generated successfully.`); + if (failures > 0) { + console.log(`${failures} generation(s) failed.`); + } + process.exit(failures > 0 ? 1 : 0); +} + +main(); diff --git a/src/assets/companions/einhorn-stella/avatar.png b/src/assets/companions/einhorn-stella/avatar.png new file mode 100644 index 0000000..79d56de Binary files /dev/null and b/src/assets/companions/einhorn-stella/avatar.png differ diff --git a/src/assets/companions/einhorn-stella/character-sheet.png b/src/assets/companions/einhorn-stella/character-sheet.png new file mode 100644 index 0000000..d6d538a Binary files /dev/null and b/src/assets/companions/einhorn-stella/character-sheet.png differ diff --git a/src/assets/companions/eule-elsa/avatar.png b/src/assets/companions/eule-elsa/avatar.png new file mode 100644 index 0000000..781605b Binary files /dev/null and b/src/assets/companions/eule-elsa/avatar.png differ diff --git a/src/assets/companions/eule-elsa/character-sheet.png b/src/assets/companions/eule-elsa/character-sheet.png new file mode 100644 index 0000000..b83f006 Binary files /dev/null and b/src/assets/companions/eule-elsa/character-sheet.png differ diff --git a/src/assets/companions/fee-lila/avatar.png b/src/assets/companions/fee-lila/avatar.png new file mode 100644 index 0000000..2bd4b55 Binary files /dev/null and b/src/assets/companions/fee-lila/avatar.png differ diff --git a/src/assets/companions/fee-lila/character-sheet.png b/src/assets/companions/fee-lila/character-sheet.png new file mode 100644 index 0000000..6e70708 Binary files /dev/null and b/src/assets/companions/fee-lila/character-sheet.png differ diff --git a/src/assets/companions/fuchs-finn/avatar.png b/src/assets/companions/fuchs-finn/avatar.png new file mode 100644 index 0000000..0327d50 Binary files /dev/null and b/src/assets/companions/fuchs-finn/avatar.png differ diff --git a/src/assets/companions/fuchs-finn/character-sheet.png b/src/assets/companions/fuchs-finn/character-sheet.png new file mode 100644 index 0000000..f7369fc Binary files /dev/null and b/src/assets/companions/fuchs-finn/character-sheet.png differ diff --git a/src/assets/style-reference/initial-forest-scene.png b/src/assets/style-reference/initial-forest-scene.png new file mode 100644 index 0000000..ff6adaa Binary files /dev/null and b/src/assets/style-reference/initial-forest-scene.png differ