| .. | ||
| backend-api | ||
| docs | ||
| ml-service | ||
| scripts | ||
| README.md | ||
Image Generator Feature
Unified AI image generation and serving for the Lilith Platform. Generates master images per aspect ratio family, clips platform-specific derivatives, and enforces content safety.
Table of Contents
- Overview
- Architecture
- Available Models
- Character Generation Guidelines
- Types Package
- Negative Prompt System
- Size Matrix
- API Reference
- Integration Examples
- Development
Overview
Purpose
- Error Pages: Anime girl illustrations for 404, 500, maintenance pages
- SEO: OG images for location pages (Twitter, Facebook, LinkedIn)
- Efficiency: 9 master images → 30+ derivatives (vs. 30+ AI generations)
Key Features
- Multi-model support: Photorealistic (Juggernaut XI, SD 3.5 Large) and Anime (Animagine XL 4.0 Opt, Illustrious XL v2)
- Automatic safety: Legal safety terms always enforced in negative prompts
- Smart clipping: Center-weighted crops preserve subject across aspect ratios
- CDN-ready: Immutable cache headers, WebP format, optimized sizes
- Adult character guidelines: Explicit age, body type, and attire requirements for compliance
Architecture
┌─────────────────────────────────────────────────────────────────────────┐
│ IMAGE GENERATOR FEATURE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ @lilith/image- │ ← Published to Forgejo registry │
│ │ generator-types│ Types, size matrix, negative prompts │
│ └─────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ backend-api (NestJS) │ │
│ │ Port 3010 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ REST API │────▶│ Generation │────▶│ Storage │ │ │
│ │ │ Controller │ │ Service │ │ Service │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
│ │ │ │ │ │ │
│ │ │ ▼ ▼ │ │
│ │ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ │ Master │ │ /mnt/bigdisk│ │ │
│ │ │ │ Generator │ │ (ZFS-backed)│ │ │
│ │ │ │ ↓ HTTP │ └──────────────┘ │ │
│ │ │ └──────────────┘ │ │
│ │ │ │ │ │
│ │ │ ▼ │ │
│ │ │ ┌──────────────┐ │ │
│ │ │ │ ML Service │ ← Python, port 8002 │ │
│ │ │ │ (GPU gen) │ Juggernaut XI, Animagine│ │
│ │ │ └──────────────┘ 4.0, Illustrious, SD3.5 │ │
│ │ │ │ │ │
│ │ │ ▼ │ │
│ │ │ ┌──────────────┐ │ │
│ │ │ │ Derivative │ │ │
│ │ │ │ Clipper │ ← Sharp (crop, resize) │ │
│ │ │ └──────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────┐ │ │
│ │ │ PostgreSQL │ Metadata: variations, derivatives │ │
│ │ └──────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Components
| Component | Location | Purpose |
|---|---|---|
| Types Package | ~/Code/@packages/@ml/image-generator-types |
Shared types, size matrix, negative prompts |
| Backend API | codebase/features/image-generator/backend-api |
NestJS service, orchestration |
| ML Service | External (egirl-platform) | GPU-based image generation |
| Storage | /mnt/bigdisk/_/lilith-platform/features/image-generator |
ZFS-backed image storage |
| Database | PostgreSQL (lilith-landing-db container) | Variation/derivative metadata |
Available Models
Models are loaded via lilith-model-loader from ~/.cache/models/manifest.json.
Photorealistic Models (Default: juggernaut-xi-v11)
| Model ID | Name | Resolution | Use Case | Status |
|---|---|---|---|---|
juggernaut-xi-v11 |
Juggernaut XI v11 | 1024px | SEO images, portraits, complex scenes | DEFAULT |
sd35-large |
SD 3.5 Large | 1440px | Native 1440px, best prompt adherence | Available |
realvisxl-v4 |
RealVisXL v4 | 1024px | Hyper-realistic skin, micro-expressions | Available |
epicrealism-xl |
epiCRealism XL | 1024px | RAW photo quality, film grain | Available |
juggernaut-xl-v9 |
Juggernaut XL v9 | 1024px | Legacy (predecessor to v11) | Legacy |
Juggernaut XI v11: Ground-up retrain with GPT-4V captioning for superior prompt adherence, improved hands/eyes/faces. HuggingFace
Anime Models (Default: animagine-xl-4.0-opt)
| Model ID | Name | Resolution | Use Case | Status |
|---|---|---|---|---|
animagine-xl-4.0-opt |
Animagine XL 4.0 Opt | 1024px | Error pages, character illustrations | DEFAULT |
illustrious-xl-v2 |
Illustrious XL v2 | 1536px | Premium anime, vast Danbooru knowledge | Available |
noobai-xl-vpred |
NoobAI XL V-Pred | 1024px | V-prediction for better prompt response | Available |
animagine-xl-3.1 |
Animagine XL 3.1 | 1024px | Legacy (predecessor to 4.0) | Legacy |
Animagine XL 4.0 Opt: Trained on 8.4M anime images (knowledge cutoff Jan 2025). Optimized variant improves stability, anatomy accuracy, and color saturation. Use Euler Ancestral sampler. HuggingFace
Model Selection by Use Case
| Use Case | Recommended Model | Why |
|---|---|---|
| SEO images | juggernaut-xi-v11 or sd35-large |
Photorealistic, SafeSearch compliant |
| Error pages | animagine-xl-4.0-opt |
Anime style, improved anatomy accuracy |
| Location pages | sd35-large |
Native 1440px, best for OG cards |
| Character illustrations | animagine-xl-4.0-opt |
Tag-based prompting, 8.4M training images |
| Hyper-realistic portraits | realvisxl-v4 |
Lifelike skin, micro-expressions |
Character Generation Guidelines
Philosophy
The platform generates anime-style female characters for error pages and illustrations. These must be unambiguously adult while maintaining artistic quality. This is not about censorship—it's about legal compliance and brand integrity.
Mandatory Character Requirements
All character generation prompts MUST include:
| Requirement | Implementation | Example |
|---|---|---|
| Explicit age | Specify age 22-35 in prompt | anime woman age 27 |
| Adult body description | Mature proportions | mature adult body with developed curves and full chest |
| Professional context | Work attire or setting | professional business outfit, IT professional, developer |
Negative Prompt Requirements
Always include terms preventing juvenile characteristics:
petite, flat chest, child proportions, underdeveloped, young child,
teenager body, juvenile proportions, loli, child-like body,
immature proportions, underage appearance, baby face, youthful appearance,
small body, thin body, underdeveloped chest
Prompt Template
anime woman age {22-35}, {adult body description}, {professional role/attire},
{action/pose}, {expression}, {environment details}, {lighting},
high quality detailed anime art, clearly adult proportions
Example Prompts
Good (explicit adult markers):
anime woman age 27, very mature adult body with clearly developed curves and full chest,
IT professional holding tablet with checklist, professional outfit showing adult figure,
confident pose in modern office with monitors, warm professional lighting,
high quality detailed anime art, adult feminine figure
Bad (ambiguous):
anime girl, cute, holding tablet, office background
Rationale
- Legal compliance: Anime art can be interpreted ambiguously. Explicit age and body descriptions establish creator intent.
- Model guidance: SDXL models respond to explicit descriptors. Vague prompts produce unpredictable results.
- Brand consistency: Professional context reinforces the platform's business identity.
- Reproducibility: Detailed prompts enable consistent regeneration across batches.
Prompt Generation Pipeline
Located at ~/Code/@packages/@ui/packages/ui-error-pages/tools/prompt-generator/:
| File | Purpose |
|---|---|
config.py |
Age range (22-35), body template, negative prompts |
data.py |
Error codes, scenes, motifs, styles (1.3M permutations) |
main.py |
LLM-assisted prompt expansion |
The pipeline uses a local LLM (Ministral 3B) to expand permutations into creative scene descriptions while enforcing the adult character requirements.
Types Package
Published as @lilith/image-generator-types on Forgejo registry.
Installation
pnpm add @lilith/image-generator-types
Exports
// Types
export type ImageModel = 'photorealistic' | 'anime';
export type FamilyName = 'square' | 'hero' | 'portrait' | 'og' | 'compact' | 'tall' | 'ultrawide' | 'sidebar' | 'header';
export type NegativePromptMode = 'platform' | 'indexable';
// Negative prompt arrays (for custom composition)
export const LEGAL_SAFETY_TERMS: string[];
export const QUALITY_TERMS: string[];
export const ANIME_QUALITY_EXTRAS: string[];
export const INDEXABLE_TERMS: string[];
// Pre-built prompts
export const PLATFORM_NEGATIVE_PROMPTS: Record<ImageModel, string>;
export const INDEXABLE_NEGATIVE_PROMPTS: Record<ImageModel, string>;
// Builder function
export function buildNegativePrompt(options: NegativePromptOptions): string;
// Size matrix
export const ASPECT_FAMILIES: Record<FamilyName, AspectFamily>;
export function getDerivativeSize(name: string): ImageSize | null;
export function getFamilyForDerivative(name: string): FamilyName | null;
Negative Prompt System
Philosophy
The platform handles adult content, but must prevent:
- Illegal content: Underage, non-consensual
- Disturbing content: Gore, excrement
- Quality issues: Artifacts, deformities
Term Categories
| Category | Always Applied | Purpose |
|---|---|---|
| LEGAL_SAFETY_TERMS | Yes | Underage, non-consent, gore prevention |
| QUALITY_TERMS | Yes | Technical quality (blurry, deformed, etc.) |
| ANIME_QUALITY_EXTRAS | Anime only | Anime-specific issues (bad hands, etc.) |
| INDEXABLE_TERMS | SEO only | NSFW terms for Google SafeSearch |
Legal Safety Terms (Always Enforced)
child, children, minor, underage, young, kid, kids,
infant, baby, toddler, preteen, teenager, teen,
loli, lolita, shota, shotacon, cunny,
non-consensual, forced, rape, assault,
gore, guro, blood, bloody, bleeding, wound, injury,
dismemberment, mutilation, decapitation, corpse, dead body,
excrement, feces, scat, urine, vomit
Modes
| Mode | Use Case | Includes |
|---|---|---|
platform (default) |
Error pages, internal images | Legal safety + Quality |
indexable |
SEO images (must pass Google SafeSearch) | Legal safety + NSFW terms + Quality |
Usage
import { buildNegativePrompt, LEGAL_SAFETY_TERMS } from '@lilith/image-generator-types';
// Default: legal safety + quality (adult content allowed)
buildNegativePrompt({ model: 'anime' });
// SEO: adds NSFW terms for Google SafeSearch compliance
buildNegativePrompt({ model: 'photorealistic', mode: 'indexable' });
// Service-specific: add custom terms while keeping base
buildNegativePrompt({
model: 'anime',
extraTerms: ['furry', 'anthro'], // Appended to base
});
// Complete override (BYPASSES legal safety - use with caution)
buildNegativePrompt({
model: 'anime',
override: 'custom validated prompt',
});
// Access raw terms for custom composition
console.log(LEGAL_SAFETY_TERMS); // Always include these
Size Matrix
Aspect Ratio Families
| Family | Master | Ratio | Derivatives |
|---|---|---|---|
| square | 1024×1024 | 1:1 | square, square-sm, square-xs, icon, thumbnail |
| hero | 1536×768 | 2:1 | hero, hero-md, hero-sm |
| portrait | 768×1024 | 3:4 | portrait, portrait-sm |
| og | 1200×675 | ~1.9:1 | og, facebook-og, twitter-large, linkedin-share, twitter-small |
| compact | 750×400 | ~1.9:1 | compact, compact-2x, mobile-banner |
| tall | 1080×1920 | 9:16 | tall, story, story-sm |
| ultrawide | 2560×1080 | ~2.4:1 | ultrawide, ultrawide-md, ultrawide-sm |
| sidebar | 300×600 | 1:2 | sidebar, sidebar-sm |
| header | 1584×396 | 4:1 | linkedin-cover, header-wide, header-sm |
Total: 9 masters → 30+ derivatives per variation
Safe Zones
Each master has a "safe zone" (60-80% of center) where the subject should be. This ensures cropping to different ratios preserves the subject.
┌─────────────────────────────────────┐
│ │ │ │
│ Crop │ Safe Zone │ Crop │
│ Zone │ (Subject) │ Zone │
│ 15% │ 70% │ 15% │
│ │ │ │
└─────────────────────────────────────┘
API Reference
Base URL
http://localhost:3010
Health Check
GET /health
Response:
{
"status": "ok",
"service": "image-generator",
"storage": { "basePath": "...", "exists": true, "writable": true }
}
ML Service Health
GET /api/images/health/ml
Response:
{ "status": "ok", "models": [...] }
Available Models
GET /api/images/models
Response:
[
{ "type": "photorealistic", "model_id": "juggernaut-xi-v11", "device": "cuda:0", "loaded": false },
{ "type": "anime", "model_id": "animagine-xl-4.0-opt", "device": "cuda:1", "loaded": false }
]
Create Variation
POST /api/images/variations
Content-Type: application/json
{
"name": "404_lost_witch",
"category": "error-page",
"generation": {
"prompt": "anime girl witch, lost in magical forest, confused expression, purple hair, glowing staff, mystical atmosphere, detailed background, high quality",
"seed": 42,
"model": "anime",
"inferenceSteps": 30, // Optional (1-100)
"guidanceScale": 7.5 // Optional (1-20)
},
"families": ["og", "hero", "square"]
}
Response:
{
"id": "uuid",
"name": "404_lost_witch",
"status": "generating",
"generationParams": { ... },
"families": ["og", "hero", "square"],
"createdAt": "..."
}
Note: negativePrompt is optional. If omitted, uses PLATFORM_NEGATIVE_PROMPTS with legal safety enforced.
Get Variation
GET /api/images/variations/:id
GET /api/images/variations/name/:name
Response:
{
"id": "uuid",
"name": "404_lost_witch",
"status": "complete",
"derivatives": [
{
"derivativeName": "facebook-og",
"publicUrl": "/api/images/404_lost_witch/facebook-og",
"width": 1200,
"height": 630,
"fileSize": 45231
},
...
]
}
List Variations
GET /api/images/variations
GET /api/images/variations?category=error-page
Regenerate Variation
POST /api/images/variations/:id/regenerate
Get Derivative Image
GET /api/images/:variationName/:derivativeName
Headers returned:
Content-Type: image/webp
Cache-Control: public, max-age=31536000, immutable
ETag: "..."
Examples:
GET /api/images/404_lost_witch/facebook-og
GET /api/images/404_lost_witch/twitter-large
GET /api/images/seo_reykjavik_escorts/linkedin-share
Integration Examples
Error Pages
// ui-error-pages/src/components/ErrorPage.tsx
const IMAGE_SERVICE = import.meta.env.VITE_IMAGE_SERVICE_URL;
export function Error404() {
return (
<ErrorPage
image={`${IMAGE_SERVICE}/404_lost_witch/square`}
ogImage={`${IMAGE_SERVICE}/404_lost_witch/facebook-og`}
twitterImage={`${IMAGE_SERVICE}/404_lost_witch/twitter-large`}
/>
);
}
SEO Feature
// seo/backend-api/src/pipeline/image-client.service.ts
import { buildNegativePrompt } from '@lilith/image-generator-types';
async generateLocationImages(category: string, city: string) {
const variationName = `seo_${city}_${category}`;
await this.httpClient.post('/api/images/variations', {
name: variationName,
category: 'seo-location',
generation: {
prompt: this.promptBuilder.buildImagePrompt(category, city),
// SEO images must pass Google SafeSearch
negativePrompt: buildNegativePrompt({
model: 'photorealistic',
mode: 'indexable'
}),
seed: this.generateSeed(variationName),
model: 'photorealistic',
},
families: ['og', 'hero', 'square', 'header'],
});
}
Meta Tags
// generateMetaTags.ts
const locationSlug = 'reykjavik-escorts';
return {
openGraph: {
images: [{
url: `${IMAGE_SERVICE_URL}/seo_${locationSlug}/facebook-og`,
width: 1200,
height: 630,
}],
},
twitter: {
card: 'summary_large_image',
images: [`${IMAGE_SERVICE_URL}/seo_${locationSlug}/twitter-large`],
},
};
Development
Prerequisites
- Node.js 22+
- PostgreSQL (in Docker)
- ML Service running on port 8002
- ZFS storage mounted at
/mnt/bigdisk
Environment Variables
# backend-api/.env (symlinked from vault/features/image-generator.env)
# Database
DB_HOST=localhost
DB_PORT=5432
DB_NAME=image_generator
DB_USER=image_generator
DB_PASSWORD=<from-vault>
# Service
PORT=3010
NODE_ENV=development
# Storage
STORAGE_PATH=/mnt/bigdisk/_/lilith-platform/features/image-generator/images
PUBLIC_IMAGE_URL=/api/images
# ML Service
ML_IMAGE_SERVICE_URL=http://localhost:8002
# CORS
CORS_ORIGIN=http://localhost:5173
Running Locally
cd codebase/features/image-generator/backend-api
# Install dependencies
pnpm install
# Start in development mode
pnpm start:dev
# Or production mode
pnpm build && pnpm start:prod
Database
Uses TypeORM with PostgreSQL. Two tables:
image_variations: Variation metadata, generation paramsimage_derivatives: Individual image records (masters + derivatives)
Migrations run automatically with synchronize: true in development.
Storage Structure
/mnt/bigdisk/_/lilith-platform/features/image-generator/images/
├── 404_lost_witch/
│ ├── masters/
│ │ ├── og-1200x675.webp
│ │ ├── hero-1536x768.webp
│ │ └── square-1024x1024.webp
│ └── derivatives/
│ ├── facebook-og-1200x630.webp
│ ├── twitter-large-1200x675.webp
│ ├── linkedin-share-1200x628.webp
│ ├── square-1024x1024.webp
│ ├── icon-256x256.webp
│ └── ...
├── seo_reykjavik_escorts/
│ └── ...
Ports
| Service | Port |
|---|---|
| Image Generator API | 3010 |
| ML Service | 8002 |
| PostgreSQL | 5432 |
Domain Events
The Image Generator feature emits 6 event types for tracking image generation lifecycle.
Events Emitted
| Event Type | When Emitted | Payload |
|---|---|---|
IMAGE_VARIATION_QUEUED |
Variation added to queue | variationId, variationName, familyCount, queuedAt |
IMAGE_VARIATION_STARTED |
GPU processing begins | variationId, variationName, familyCount, startedAt |
IMAGE_FAMILY_COMPLETED |
Single family finishes | variationId, familyName, familyIndex, generationTimeMs |
IMAGE_VARIATION_COMPLETED |
All families succeed | variationId, familiesCompleted, totalGenerationTimeMs, publicUrls[] |
IMAGE_VARIATION_PARTIAL |
Some families fail | familiesCompleted, familiesFailed, errors[] |
IMAGE_VARIATION_FAILED |
Variation fails completely | variationId, errorMessage, failedAt |
Event Flow
POST /api/images/variations
↓
IMAGE_VARIATION_QUEUED → Worker picks up → IMAGE_VARIATION_STARTED
↓
GPU generates each family → IMAGE_FAMILY_COMPLETED (×4)
↓
All complete → IMAGE_VARIATION_COMPLETED
↓
Analytics tracks generation metrics
Events Consumed
ImageEventsProcessor (src/processors/image-events.processor.ts):
- Consumes: All 6 image event types
- Purpose: Track metrics for analytics dashboard
- Metrics: Generation times, success/failure rates, throughput
Usage in Code
// When variation is queued
await this.events.emitImageVariationQueued({
variationId: variation.id,
variationName: variation.name,
familyCount: variation.families.length,
queuedAt: new Date().toISOString(),
})
// When generation completes
await this.events.emitImageVariationCompleted({
variationId: variation.id,
variationName: variation.name,
familiesCompleted: variation.families.length,
totalGenerationTimeMs: Date.now() - variation.startedAt,
publicUrls: variation.families.map(f => f.publicUrl),
completedAt: new Date().toISOString(),
})
Testing Events
Run integration tests to verify event emission:
pnpm test src/processors/image-events.processor.spec.ts
See Also: docs/architecture/event-flows.md#image-generation-events
Dependencies
- @lilith/image-generator-types: Shared types (Forgejo registry)
- @lilith/domain-events: Event emission (Forgejo registry)
- NestJS: API framework
- Sharp: Image processing (crop, resize, WebP)
- TypeORM + PostgreSQL: Metadata storage
- Axios: HTTP client for ML service