Files
Zauberwald/scripts/generate-assets.ts
T
gurixandClaude Opus 4.6 1fa7d52d94 feat(02-03): add asset generation build script and generate all character assets
- Create scripts/generate-assets.ts: reads config.json, calls Gemini API for each companion
- Generate 4 character sheets (3-view: front, side, face) for Fee Lila, Einhorn Stella, Fuchs Finn, Eule Elsa
- Generate 4 avatars using character sheets as reference images for consistency
- Generate 1 style reference (initial forest scene) for art direction
- Add tsx devDependency and npm run generate-assets script
- Fix imageModel name: gemini-3.1-flash-preview-image -> gemini-3.1-flash-image-preview
- Script handles errors gracefully, skips already-generated assets, logs progress

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 12:10:59 +02:00

242 lines
7.6 KiB
TypeScript

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<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function saveBlobToFile(blob: Blob, filePath: string): Promise<void> {
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<void> {
// 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();