# DrawIt — Design: "Create" (AI-generated custom lessons) + the Creator role _Designed June 2026. Purpose: let a privileged user type a subject (e.g. "Zebra"), have a self-hosted **ComfyUI** server generate a coloring-book line-art image, and turn that image into a real DrawIt lesson with the usual design elements. This is a design/research document — nothing is built yet. It assumes the current curriculum/runner architecture documented in `RESEARCH-beginner-level.md` and `RESEARCH-early-beginner-expansion.md`._ ## 1. Goal & summary A new **"Create"** option (visible only to a new **Creator** role) appears on the Early Beginner and Beginner levels. The Creator types a subject; the app sends a fixed prompt to a configurable ComfyUI endpoint; ComfyUI returns black-and-white line art; DrawIt processes that image into a playable lesson (trace + color phases, badge, progress, gallery — the same machinery every other lesson uses). The fixed prompt (subject interpolated): > 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. Two hard problems drive the design: **(a)** an AI returns *one raster image*, but our lessons are multi-phase, SVG-step constructions; and **(b)** this is a children's app accepting free-text that feeds an image generator, so **safety/moderation is mandatory, not optional.** ## 2. The Creator role Today `Role = "admin" | "learner"` (`src/lib/types.ts`); the DB `users.role` is already a free-text column defaulting to `learner`, and the first registered account is forced to `admin`. Adding a role is small and backward-compatible: - **Type:** `Role = "admin" | "learner" | "creator"`. A Creator has **all Learner permissions plus the Create ability**. Admins implicitly have it too. - **No schema change** — `role` is already TEXT. Existing rows stay `learner`/`admin`. - **Assignment:** only an admin grants it, via the existing admin users API (`src/app/api/admin/users/[id]/route.ts`) — add `"creator"` to the allowed set (today it coerces to `admin`/`learner`). Signup is unchanged (first = admin, everyone else = learner). - **Server guard:** add `requireCreator()` in `src/lib/session.ts` (mirrors `requireAdmin()`): returns the user iff `role === "creator" || role === "admin"`. A tiny `canCreate(user)` helper feeds the UI. **Every Create endpoint must call this server-side** — never trust the client. ## 3. End-to-end user flow 1. Creator opens Early Beginner or Beginner → taps **✨ Create your own**. 2. Types a subject ("Zebra"). Client does light validation; server does the real moderation (§5). 3. Server creates a `created_lessons` row (`status: 'pending'`), interpolates the subject into the fixed prompt, and queues the ComfyUI workflow (`status: 'generating'`). 4. Client polls status (or subscribes). ComfyUI renders (seconds to ~1 min on a GPU box). 5. Server fetches the image, post-processes it (§6), derives the lesson assets, sets `status: 'ready'` (or `'needs_review'` if moderation requires admin approval). 6. The lesson appears under a **"My Creations"** group on that level and plays through the normal runners (trace → color), earning a badge and saving to the gallery like any lesson. ## 4. ComfyUI integration **Why ComfyUI / self-hosted:** it keeps DrawIt's privacy promise — the subject text and image never leave a server the operator runs. No third-party AI cloud. The endpoint is **configurable** so each self-hoster points at their own GPU box (or none, disabling Create). **Config (env, mirroring the existing SMTP pattern in `.env.example`):** ``` COMFYUI_URL= # e.g. http://127.0.0.1:8188 — blank disables the Create feature COMFYUI_API_KEY= # optional, if the box is behind an auth proxy COMFYUI_WORKFLOW=./comfyui/coloring-book.workflow_api.json # API-format workflow template COMFYUI_TIMEOUT_MS=120000 COMFYUI_PROMPT_NODE=6 # id of the positive CLIP-text node to inject the prompt into ``` **API calls** (ComfyUI's HTTP API): 1. `POST {COMFYUI_URL}/prompt` with `{ prompt: , client_id }`. We load the saved **API-format workflow** (exported from ComfyUI's "Save (API format)"), and set the positive prompt node's `inputs.text` to our interpolated prompt (and randomize the seed node). Returns `prompt_id`. 2. Poll `GET {COMFYUI_URL}/history/{prompt_id}` until the entry exists; read `outputs[node].images[]` → `{ filename, subfolder, type }`. (Optionally subscribe to `GET /ws?clientId=` for live progress to show a friendly "drawing your zebra…" bar.) 3. `GET {COMFYUI_URL}/view?filename=…&subfolder=…&type=output` → the PNG bytes. **Async model:** generation is slow, so treat it as a job. `created_lessons.status` is the state machine: `pending → generating → (needs_review) → ready | failed | blocked`. The POST route kicks off generation server-side and returns immediately; the client polls `GET /api/create/:id`. On a single self-hosted node an in-process async task is fine; document that horizontal scaling would want a real queue. Always set a timeout and a retry-once policy; surface `failed` with a kid-friendly message. ## 5. Safety & moderation (mandatory — this is a kids' app) Free text → image generation for children is the riskiest part. Layered defenses, **input + output**: **Input (the subject string):** - **Sanitize:** trim; cap length (~30 chars); collapse whitespace; allow only letters/spaces/hyphens; reject newlines and any prompt-control tokens. Because we only interpolate `[subject]` into a fixed prompt, prompt-injection surface is small — but still strip anything that could break out of the phrase. - **Allowlist-first (recommended for kids):** maintain a curated allowlist of safe, drawable subjects (animals, objects, food, vehicles, nature, shapes…). If the subject is on the allowlist → proceed automatically. This is the safest posture and the literature's recommendation for child contexts. - **Blocklist backstop:** a denylist of profanity, violence, weapons, drugs, sexual, hateful, and self-harm terms (and obfuscations) rejects anything obviously unsafe that slips past. - **Anything else → admin review queue** (`status: 'needs_review'`): not on the allowlist and not on the blocklist → a human approves before it generates or goes live. Keeps the door open to new subjects without exposing kids to raw generation. - **Rate-limit** per user (e.g. N creations/day) to bound abuse and GPU cost. **Output (the generated image):** even safe prompts can misfire. Before a lesson is playable, require a **preview/approve step** — minimally the creating user sees it and confirms; for stricter operators, an admin approval queue. (Optional later: an automated NSFW image classifier as a pre-filter.) **Logging vs. privacy:** store only what's needed to run/curate the feature (subject, status, moderation decision). Consistent with "no analytics, data stays local." ## 6. From image → level (the core pipeline) **What a lesson needs today:** a `Lesson` with phases. Trace phases (`construct`/`outline`/`detail`) use per-step `lines: string[]` of SVG elements that `LineDraw` reveals one at a time; color/light phases use `baseSvg` which `ColoringRunner` **rasterizes** for the fill boundary and overlay. Badge, `sublevel`, `subjectKey`, and a group complete the level. **The gap:** ComfyUI returns *one raster PNG*. It is not pre-split into outline-vs-detail phases, and it is pixels, not SVG strokes. So a Created lesson can't perfectly mirror a hand-authored 3–4-phase subject. **Chosen approach (per §13): Option C — vectorize the line art with potrace for a stroke-by-stroke Trace phase, and use the raster for the Color phase (2 phases total).** The three options below are kept for context; the build targets C. - **Option A — Raster-native, 2-phase (recommended MVP).** Treat the cleaned PNG as the lesson art. - **Trace** phase: show the line art faintly as a guide to trace over (reuse `TraceRunner`'s base layer, fed a raster instead of SVG). Optional "watch it appear" = a left-to-right wipe reveal of the bitmap rather than true per-stroke animation. - **Color** phase: `ColoringRunner` already rasterizes its template to a boundary + overlay, so it can take the PNG **directly** — flood-fill and overlay work as-is. This phase is essentially free. - For Beginner-created lessons, optionally add the existing **Light** phase (sun overlay) on top. - Smallest change: add a `templateUrl?` (raster) alongside `baseSvg` to `TraceRunner`/`ColoringRunner` and the lesson-page meta; when present, use the bitmap path. - **Option B — Vectorize for true line-by-line reveal.** Run the PNG through **potrace** (pure-JS npm, no native build — important given our better-sqlite3 build friction) to get SVG paths, then split the SVG into multiple `lines` (by `M` subpaths) so `LineDraw` animates it stroke-by-stroke like the hand-made lessons. Quality varies with image cleanliness; treat as an enhancement over A. - **Option C — Hybrid (target end state).** Vectorize for the trace reveal (B) **and** keep the raster for coloring (A). Optionally heuristically split paths into a simpler "outline" (large/long paths) vs "detail" (small/short paths) pass to approximate the two trace phases — best-effort, not perfect. **Image post-processing** (server-side, before storing) with a pure-JS lib like **jimp** (avoids native deps) or **sharp** (faster, native): grayscale → threshold to crisp black/white → auto-trim margins → fit to the lessons' aspect (the runners use a 4:3 / 400×300 viewBox and an 880×660 color canvas) → flatten on white → downscale. Store the cleaned PNG; if vectorizing, store the derived SVG too. **Badge & metadata:** Created lessons get a generic medal — a default emoji (✏️ or 🎨, or a creator-picked one) plus a name like "**Zebra Artist**". Reuse the existing emoji-medal `BadgeArt` and award-PDF machinery untouched. ## 7. Data model No destructive changes; one new table (plus the `Role` union value): ```sql CREATE TABLE created_lessons ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, level TEXT NOT NULL, -- 'early-beginner' | 'beginner' subject TEXT NOT NULL, -- sanitized, e.g. 'Zebra' slug TEXT NOT NULL UNIQUE, -- e.g. 'created-42-zebra' status TEXT NOT NULL DEFAULT 'pending', -- pending|generating|needs_review|ready|failed|blocked image TEXT, -- cleaned line-art (data URL, like drawings/avatars) template_svg TEXT, -- optional vectorized paths (Option B/C) prompt TEXT NOT NULL, -- full prompt sent (audit) moderation TEXT, -- 'allowlist' | 'review' | 'blocked:' emoji TEXT, -- badge emoji created_at TEXT NOT NULL DEFAULT (datetime('now')) ); ``` **Integration with existing systems (reuse, don't fork):** - **Sublevel namespacing:** created lessons keep their `level` (so the unlock gate / grouping still apply) and use a high synthetic `sublevel = 900000 + id`, guaranteeing no collision with static lessons. Progress, completions, drawings, and badges then work through the *existing* tables keyed by `(user_id, level, sublevel)` with zero changes. - **Badge key:** `created-`; `badgeDefs()` gains the user's ready created lessons at request time. - **Not in static `LESSONS`.** Add resolver helpers that the lesson page and learn page call: `getCreatedLesson(slug)` builds a `Lesson`-shaped object on the fly from the row (so the runners need no special-casing), and `listCreatedLessons(userId, level)` feeds the "My Creations" group. - **Images as data URLs in SQLite** matches the existing privacy-consistent storage for drawings and avatars (note: line-art PNGs are small once thresholded; watch row size, consider a files dir if big). - **Backups:** the new table rides along with the existing Litestream/SQLite backup automatically. ## 8. API routes (Next.js App Router) - `POST /api/create` — `requireCreator`; body `{ level, subject }`. Validate + moderate (§5). On pass: insert row, interpolate prompt, kick off ComfyUI job, return `{ id, slug, status }`. On block: 422 with a gentle message. - `GET /api/create/:id` — owner (or admin); returns `{ status, slug, image? }` for polling. - `GET /api/create/:id/image` — serves the stored line art (or inline data URL). - `POST /api/admin/create/:id` — admin approve/block for the review queue. - All Create routes are no-ops/404 when `COMFYUI_URL` is unset (feature disabled cleanly). ## 9. UI - **Entry point:** a "✨ Create your own" card at the top of the Early Beginner and Beginner sections of the Learn page, **rendered only when `canCreate(user)`**. (Also a nav entry, optional.) - **Create view:** subject input + the fixed-prompt explainer ("we'll draw a coloring page of your subject"), a live status/progress state, then a preview with **"Start the lesson"** / **"Try again"**. - **My Creations group:** a `getGroupedSubjects`-style group on the level listing the user's ready creations, each opening into the normal trace/color runner. - **Gating:** non-creators never see Create (UI), and the API rejects them regardless (defense in depth). ## 10. Config & ops - Requires a reachable ComfyUI with an image model + the saved API-format workflow JSON checked in at `comfyui/coloring-book.workflow_api.json` (with documented node ids for prompt + seed). - New deps (all optional/pure-JS preferred): `potrace` (vectorize), `jimp` or `sharp` (image cleanup). - Document GPU expectations, timeout, and that Create is **off by default** (blank `COMFYUI_URL`), so the core app and existing deploys are unaffected. ## 11. Privacy & principles (unchanged) Self-hosted ComfyUI means the child's subject text and the image stay on the operator's own infrastructure — no third-party calls, consistent with "ABSOLUTELY NO DATA COLLECTED." Generated assets live in the operator's SQLite. The feature is fully optional and disabled unless configured. ## 12. Phased rollout - **Phase 1 — Plumbing:** Creator role + `requireCreator` + admin can assign; env config; `created_lessons` table; ComfyUI client (queue → poll → fetch) behind a feature flag. - **Phase 2 — Lesson pipeline (Option C):** image cleanup + **potrace vectorization** → Trace (stroke reveal) + Color; subject **allowlist + admin review queue** + blocklist + sanitize; preview/approve; "My Creations" group (private to creator); badge + gallery via existing tables; rate limiting. - **Phase 3 — Curation:** admin **promote-to-global** (creation enters the shared curriculum with attribution; owner becomes system/global); review-queue UI polish. ## 13. Decisions (locked June 2026) Settled with Dan; these drive the build: 1. **Phases:** a created lesson is **2-phase — Trace + Color** (one AI image isn't cleanly splittable into outline vs detail). Beginner creations may add the Light phase later, but the baseline is two. 2. **Moderation:** **allowlist + admin review queue** (+ blocklist backstop + input sanitize). Curated safe subjects auto-pass; anything new is held for admin approval before it generates/goes live. 3. **Line reveal:** **vectorize up front** — run the cleaned line art through **potrace** so the Trace phase animates **stroke-by-stroke** (via `LineDraw`) from day one, exactly like hand-made lessons. The raster is still used for the Color phase (`ColoringRunner` rasterizes anyway). So the pipeline is Option C (hybrid): **vectorized SVG for Trace + raster for Color**. 4. **Visibility:** creations are **private to the creator by default, but admin-promotable to the global curriculum** (with attribution). So the data model must support a "promoted/global" flag and an owner of `null`/system once promoted. Still open (smaller, decide at build time): **image storage** (data URLs in SQLite vs. a files dir for the larger line-art PNGs) and **which ComfyUI model/workflow** is the reference (affects line-art cleanliness, hence vectorization quality — pick a model that yields bold, closed outlines). ## Sources - [ComfyUI API — Hosting a Workflow via API (9elements)](https://9elements.com/blog/hosting-a-comfyui-workflow-via-api/) - [ComfyUI API: The Complete Developer's Guide (Runflow)](https://www.runflow.io/blog/comfyui-api-developer-guide) - [ComfyUI API Endpoints — Complete Reference (Runflow)](https://www.runflow.io/blog/comfyui-api-endpoints) - [ComfyUI: Using the API, Part 1 (Medium)](https://medium.com/@yushantripleseven/comfyui-using-the-api-261293aa055a) - [potrace — npm (Node bitmap→SVG)](https://www.npmjs.com/package/potrace) - [imagetracerjs — npm (JS raster vectorizer)](https://github.com/jankovicsandras/imagetracerjs) - [Filtering Techniques: Blocklists and Allowlists for Safe AI Prompts (Learn Prompting)](https://learnprompting.org/docs/prompt_hacking/defensive_measures/filtering) - [Azure AI Content Safety — Filters and Best Practices](https://jannikreinhard.com/2026/02/18/what-azure-ai-content-safety-does-and-why-it-matters/) - [Safety Filtering in AI: How to Block Harmful Model Outputs](https://www.practical-devsecops.com/glossary/safety-filtering/)