ComfyUI AI gen lesson changes

This commit is contained in:
iamdoubz
2026-06-29 09:40:43 -05:00
parent 90c744bf13
commit 89b175faf0
5 changed files with 43 additions and 18 deletions
+4 -2
View File
@@ -21,8 +21,10 @@ SMTP_FROM="DrawIt <no-reply@example.com>"
COMFYUI_URL=http://127.0.0.1:8188 COMFYUI_URL=http://127.0.0.1:8188
COMFYUI_API_KEY= COMFYUI_API_KEY=
# Path to a ComfyUI workflow exported in API format ("Save (API format)"). # Path to a ComfyUI workflow exported in API format ("Save (API format)").
COMFYUI_WORKFLOW=./comfyui/coloring-book.workflow_api.json COMFYUI_WORKFLOW=./comfyui/ColorBook01.json
# Node ids in that workflow to inject into: the positive prompt text, and the sampler seed. # Node ids in that workflow: the positive-prompt node and the sampler (seed) node.
COMFYUI_PROMPT_NODE=6 COMFYUI_PROMPT_NODE=6
COMFYUI_SEED_NODE=3 COMFYUI_SEED_NODE=3
# Placeholder inside the positive prompt that gets replaced with the creator's subject.
COMFYUI_SUBJECT_TOKEN=[DrawItSubject]
COMFYUI_TIMEOUT_MS=120000 COMFYUI_TIMEOUT_MS=120000
+9 -3
View File
@@ -10,11 +10,17 @@ server and turns the result into a lesson. Nothing leaves your machine.
3. Provide a workflow exported in **API format** (ComfyUI → Settings → enable dev mode → 3. Provide a workflow exported in **API format** (ComfyUI → Settings → enable dev mode →
"Save (API format)") and point `COMFYUI_WORKFLOW` at it. "Save (API format)") and point `COMFYUI_WORKFLOW` at it.
`coloring-book.workflow_api.json` here is the **stock SD1.5 text-to-image workflow** as a starting `ColorBook01.json` here is the active workflow (`COMFYUI_WORKFLOW`). Its positive-prompt node contains a
point. DrawIt injects its prompt into the positive-prompt node and a random seed into the sampler node: **`[DrawItSubject]` placeholder** — DrawIt replaces that token with the creator's one-word subject and
sets a fresh random seed; the rest of your prompt is kept exactly as written. (`coloring-book.workflow_api.json`
is the older stock SD1.5 template, kept for reference.)
- `COMFYUI_PROMPT_NODE` (default `6`) — the positive `CLIPTextEncode` node. - `COMFYUI_PROMPT_NODE` (default `6`) — the positive `CLIPTextEncode` node containing `[DrawItSubject]`.
- `COMFYUI_SEED_NODE` (default `3`) — the `KSampler` node. - `COMFYUI_SEED_NODE` (default `3`) — the `KSampler` node.
- `COMFYUI_SUBJECT_TOKEN` (default `[DrawItSubject]`) — the placeholder to substitute.
Example: node 6 text `"coloring book page, single cute [DrawItSubject], black and white line art, …"`
with subject `Zebra` becomes `"coloring book page, single cute Zebra, black and white line art, …"`.
## Tips for clean line art (matters — we vectorize it with potrace) ## Tips for clean line art (matters — we vectorize it with potrace)
+2 -2
View File
@@ -1,6 +1,6 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { requireCreator } from "@/lib/session"; import { requireCreator } from "@/lib/session";
import { isCreateEnabled, buildPrompt } from "@/lib/comfyui"; import { isCreateEnabled, resolvePrompt } from "@/lib/comfyui";
import { moderateSubject } from "@/lib/moderation"; import { moderateSubject } from "@/lib/moderation";
import { createCreatedLesson, processCreation, listCreatedForUser } from "@/lib/createdLessons"; import { createCreatedLesson, processCreation, listCreatedForUser } from "@/lib/createdLessons";
@@ -31,7 +31,7 @@ export async function POST(req: Request) {
return NextResponse.json({ error: "Let's pick a different, friendly thing to draw!" }, { status: 422 }); return NextResponse.json({ error: "Let's pick a different, friendly thing to draw!" }, { status: 422 });
} }
const prompt = buildPrompt(mod.subject); const prompt = resolvePrompt(mod.subject);
const status = mod.decision === "allowlist" ? "pending" : "needs_review"; const status = mod.decision === "allowlist" ? "pending" : "needs_review";
const row = createCreatedLesson({ userId: user.id, level, subject: mod.subject, prompt, moderation: mod.decision, status }); const row = createCreatedLesson({ userId: user.id, level, subject: mod.subject, prompt, moderation: mod.decision, status });
+27 -10
View File
@@ -11,18 +11,30 @@ import path from "node:path";
const URL_BASE = (process.env.COMFYUI_URL || "").replace(/\/$/, ""); const URL_BASE = (process.env.COMFYUI_URL || "").replace(/\/$/, "");
const API_KEY = process.env.COMFYUI_API_KEY || ""; const API_KEY = process.env.COMFYUI_API_KEY || "";
const WORKFLOW_PATH = process.env.COMFYUI_WORKFLOW || "./comfyui/coloring-book.workflow_api.json"; const WORKFLOW_PATH = process.env.COMFYUI_WORKFLOW || "./comfyui/ColorBook01.json";
const TIMEOUT_MS = Number(process.env.COMFYUI_TIMEOUT_MS || 120000); const TIMEOUT_MS = Number(process.env.COMFYUI_TIMEOUT_MS || 120000);
const PROMPT_NODE = process.env.COMFYUI_PROMPT_NODE || "6"; // positive CLIPTextEncode in the default workflow const PROMPT_NODE = process.env.COMFYUI_PROMPT_NODE || "6"; // positive CLIPTextEncode in the workflow
const SEED_NODE = process.env.COMFYUI_SEED_NODE || "3"; // KSampler in the default workflow const SEED_NODE = process.env.COMFYUI_SEED_NODE || "3"; // KSampler in the workflow
// Placeholder inside the workflow's positive prompt that we replace with the creator's subject.
const SUBJECT_TOKEN = process.env.COMFYUI_SUBJECT_TOKEN || "[DrawItSubject]";
export function isCreateEnabled(): boolean { export function isCreateEnabled(): boolean {
return URL_BASE.length > 0; return URL_BASE.length > 0;
} }
/** The fixed coloring-book prompt with the subject interpolated. */ /**
export function buildPrompt(subject: string): string { * The resolved positive prompt that will be sent: the workflow's prompt-node text with the
return `A coloring book page of a ${subject}, clean black-and-white line art, bold crisp outlines, simple composition, large open areas to color, white background, no shading, no grayscale, no color, no gradients, no heavy background detail, printable page.`; * `[DrawItSubject]` placeholder replaced by the subject. Stored for audit. Falls back to the bare
* subject if the workflow can't be read.
*/
export function resolvePrompt(subject: string): string {
try {
const text = loadWorkflow()[PROMPT_NODE]?.inputs?.text;
if (typeof text === "string") return text.split(SUBJECT_TOKEN).join(subject);
} catch {
/* fall through */
}
return subject;
} }
function authHeaders(): Record<string, string> { function authHeaders(): Record<string, string> {
@@ -40,15 +52,20 @@ async function sleep(ms: number) {
} }
/** /**
* Generate coloring-book line art for the given prompt. Returns the PNG bytes. * Generate coloring-book line art for the given subject. Returns the PNG bytes.
* Injects the subject into the workflow's prompt placeholder + a fresh seed.
* Throws on timeout / ComfyUI errors (caller marks the lesson 'failed'). * Throws on timeout / ComfyUI errors (caller marks the lesson 'failed').
*/ */
export async function generateLineArt(prompt: string): Promise<Buffer> { export async function generateLineArt(subject: string): Promise<Buffer> {
if (!isCreateEnabled()) throw new Error("Create is disabled (COMFYUI_URL not set)."); if (!isCreateEnabled()) throw new Error("Create is disabled (COMFYUI_URL not set).");
const workflow = loadWorkflow(); const workflow = loadWorkflow();
// Inject our prompt + a fresh seed into the configured nodes. // Replace the [DrawItSubject] placeholder in the positive prompt with the creator's subject.
if (workflow[PROMPT_NODE]?.inputs) workflow[PROMPT_NODE].inputs.text = prompt; const promptNode = workflow[PROMPT_NODE];
if (promptNode?.inputs && typeof promptNode.inputs.text === "string") {
promptNode.inputs.text = promptNode.inputs.text.split(SUBJECT_TOKEN).join(subject);
}
// Fresh seed each run so repeats vary.
if (workflow[SEED_NODE]?.inputs) workflow[SEED_NODE].inputs.seed = Math.floor(Math.random() * 1e15); if (workflow[SEED_NODE]?.inputs) workflow[SEED_NODE].inputs.seed = Math.floor(Math.random() * 1e15);
const clientId = `drawit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const clientId = `drawit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
+1 -1
View File
@@ -119,7 +119,7 @@ export async function processCreation(id: number): Promise<void> {
updateCreatedStatus(id, "generating"); updateCreatedStatus(id, "generating");
const { generateLineArt } = await import("./comfyui"); const { generateLineArt } = await import("./comfyui");
const { pngToTemplateSvg } = await import("./vectorize"); const { pngToTemplateSvg } = await import("./vectorize");
const png = await generateLineArt(row.prompt); const png = await generateLineArt(row.subject);
const templateSvg = await pngToTemplateSvg(png); const templateSvg = await pngToTemplateSvg(png);
setReady(id, templateSvg, `data:image/png;base64,${png.toString("base64")}`); setReady(id, templateSvg, `data:image/png;base64,${png.toString("base64")}`);
} catch (e) { } catch (e) {