Files
scrollsmith/imageGenerator.js
Madison Grubb f51a5a6e0c
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
shrink image more. improve prompts
2025-09-04 22:38:03 -04:00

208 lines
6.6 KiB
JavaScript

import path from "path";
import { mkdir, writeFile } from "fs/promises";
import { fileURLToPath } from "url";
import { callOllama } from "./ollamaClient.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const COMFYUI_URL = process.env.COMFYUI_URL || "http://localhost:8188";
// Drawing style prefix
const STYLE_PREFIX = `a high-contrast, black and white pen and ink drawing, hand-drawn sketch aesthetic, very low detail, visible loose linework, expressive simple hatching for shadows, quick conceptual sketch, subtle color accent`;
// 1. Generate engineered visual prompt
async function generateVisualPrompt(flavor) {
const rawPrompt = await callOllama(
`You are a prompt engineer specializing in visual prompts for AI image generation (e.g., Stable Diffusion, ComfyUI). Given a piece of fantasy flavor text, your job is to extract and translate the visual elements into a highly descriptive image prompt.
Your output should be structured like a list of visual tags, not a story or paragraph. Focus on describing the environment, mood, architecture, lighting, materials, and color. Avoid abstract, emotional, and visual language. Be literal, specific, and visual only.
Rules:
- Do NOT repeat phrases or wording from the input.
- Only include things that could be seen in a single, still image.
- Use visual keywords, rich adjectives, and clear scene descriptors.
- Keep the prompt concise, 40-80 words.
- Maintain simplicity in descriptions. The image must be a minimal, hand drawn sketch aesthetic with low detail.
- Avoid characters or creatures unless clearly described.
- Avoid referencing rendering style, color technique, camera effects, or drawing medium — focus only on the visual content of the scene.
- Do NOT include phrases like “an image of” or “a scene showing”.
Input:
${flavor}
Output:`,
"gemma3n:e4b", 3, "Generate Visual Prompt"
);
return `${STYLE_PREFIX}, ${rawPrompt.trim().replace(/\n/g, " ")}`;
}
// 2. Save image buffer
async function saveImage(buffer, filename) {
const filepath = path.join(__dirname, filename);
await mkdir(__dirname, { recursive: true });
await writeFile(filepath, buffer);
console.log(`✅ Saved image: ${filepath}`);
return filepath;
}
// 3. Build workflow payload
function buildComfyWorkflow(promptText, negativeText = "") {
return {
"3": {
"inputs": {
"seed": Math.floor(Math.random() * 100000),
"steps": 4,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "simple",
"denoise": 1,
"model": ["4", 0],
"positive": ["6", 0],
"negative": ["7", 0],
"latent_image": ["5", 0]
},
"class_type": "KSampler"
},
"4": {
"inputs": {
"unet_name": "flux1-schnell-fp8.safetensors",
"weight_dtype": "fp8_e4m3fn"
},
"class_type": "UNETLoader"
},
"5": {
"inputs": {
"width": 640,
"height": 448,
"batch_size": 1
},
"class_type": "EmptyLatentImage"
},
"6": {
"inputs": {
"text": promptText,
"clip": ["10", 0]
},
"class_type": "CLIPTextEncode"
},
"7": {
"inputs": {
"text": negativeText,
"clip": ["10", 0]
},
"class_type": "CLIPTextEncode"
},
"10": {
"inputs": {
"clip_name1": "clip_l.safetensors",
"clip_name2": "t5xxl_fp8_e4m3fn.safetensors",
"type": "flux"
},
"class_type": "DualCLIPLoader"
},
"11": {
"inputs": {
"vae_name": "ae.safetensors"
},
"class_type": "VAELoader"
},
"8": {
"inputs": {
"samples": ["3", 0],
"vae": ["11", 0]
},
"class_type": "VAEDecode"
},
"9": {
"inputs": {
"filename_prefix": "ComfyUI_Flux",
"images": ["8", 0]
},
"class_type": "SaveImage"
}
};
}
// 4a. Wait for ComfyUI to finish image generation
async function waitForImage(promptId, timeout = 900000) {
const start = Date.now();
while (Date.now() - start < timeout) {
const res = await fetch(`${COMFYUI_URL}/history`);
const data = await res.json();
const historyEntry = data[promptId];
if (historyEntry?.outputs) {
const images = Object.values(historyEntry.outputs).flatMap(o => o.images || []);
if (images.length > 0) return images.map(i => i.filename);
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
throw new Error("Timed out waiting for ComfyUI image result.");
}
// 4b. Download image from ComfyUI server
async function downloadImage(filename, localFilename) {
const url = `${COMFYUI_URL}/view?filename=${filename}`;
const res = await fetch(url);
if (!res.ok) throw new Error(`Failed to fetch image: ${res.statusText}`);
const buffer = Buffer.from(await res.arrayBuffer());
return await saveImage(buffer, localFilename);
}
// 4c. Submit prompt and handle full image pipeline
async function generateImageViaComfyUI(prompt, filename) {
const workflow = buildComfyWorkflow(prompt, "photorealism, hyper-realistic, high detail, 3D render, CGI, ray tracing, glossy, polished, smooth shading, realistic lighting, digital painting, anime, cartoon, pixelated, noisy, cluttered, blurry, sharp focus, symmetrical, perfect perspective, detailed textures, high-resolution textures, high contrast lighting, lens flare, bokeh, camera artifacts, text, logo, signature, watermark, overexposed, underexposed, glowing edges");
try {
console.log("Submitting prompt to ComfyUI...");
const res = await fetch(`${COMFYUI_URL}/prompt`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: workflow })
});
if (!res.ok) {
throw new Error(`ComfyUI error: ${res.statusText}`);
}
const { prompt_id } = await res.json();
console.log("Waiting for image result...");
const filenames = await waitForImage(prompt_id);
if (filenames.length === 0) throw new Error("No image generated");
const comfyFilename = filenames[0];
console.log("Downloading image...");
const filepath = await downloadImage(comfyFilename, filename);
return filepath;
} catch (err) {
console.error("Error generating image:", err.message);
return null;
}
}
// 5. Main export
export async function generateDungeonImages({ flavor }) {
console.log("Generating dungeon image...");
const finalPrompt = await generateVisualPrompt(flavor);
console.log("Engineered visual prompt:\n", finalPrompt);
const filename = `dungeon.png`;
const filepath = await generateImageViaComfyUI(finalPrompt, filename);
if (!filepath) {
throw new Error("Failed to generate dungeon image.");
}
return filepath;
}