/** * Sample generation script to test the full image generation pipeline. * * Uses the ML image generation service to generate real AI images * and then clips derivatives from them. * * Run with: pnpm run sample * * Options: * --prompt Custom prompt (default: anime witch in enchanted forest) * --seed Seed for reproducible generation (default: random) */ import * as fs from 'fs'; import * as path from 'path'; import { ASPECT_FAMILIES, type FamilyName, } from '@lilith/image-generator-types'; import { DerivativeClipperService } from '@/generation/derivative-clipper.service'; const OUTPUT_DIR = path.join(__dirname, '../test-output'); const ML_SERVICE_URL = process.env.ML_IMAGE_SERVICE_URL || 'http://localhost:8002'; // Parse CLI args const args = process.argv.slice(2); const promptIdx = args.indexOf('--prompt'); const seedIdx = args.indexOf('--seed'); const PROMPT = promptIdx >= 0 ? args[promptIdx + 1] : 'beautiful anime witch girl in enchanted magical forest, glowing mushrooms, fireflies, detailed, masterpiece'; const NEGATIVE_PROMPT = 'low quality, blurry, deformed, ugly, bad anatomy'; const SEED = seedIdx >= 0 ? parseInt(args[seedIdx + 1], 10) : Math.floor(Math.random() * 2147483647); interface MLGenerateResponse { success: boolean; result?: { id: string; model: string; layout: string; width: number; height: number; safe_zone: string; image_data?: string; generation_time_ms: number; }; error?: string; } /** * Generate image using ML service */ async function generateWithML( width: number, height: number, seed: number ): Promise { console.log(` Calling ML service at ${ML_SERVICE_URL}...`); const response = await fetch(`${ML_SERVICE_URL}/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: PROMPT, negative_prompt: NEGATIVE_PROMPT, model: 'anime', width, height, steps: 30, guidance_scale: 7.5, seed, output_format: 'png', }), signal: AbortSignal.timeout(300000), // 5 minutes }); if (!response.ok) { const errorText = await response.text(); throw new Error(`ML service error: ${response.status} - ${errorText}`); } const result = (await response.json()) as MLGenerateResponse; if (!result.success || !result.result?.image_data) { throw new Error(`ML generation failed: ${result.error ?? 'No image data'}`); } console.log(` Generated in ${result.result.generation_time_ms}ms`); return Buffer.from(result.result.image_data, 'base64'); } /** * Check if ML service is available */ async function checkMLService(): Promise { try { const response = await fetch(`${ML_SERVICE_URL}/health`, { signal: AbortSignal.timeout(5000), }); const data = await response.json() as { status: string; gpuAvailable: boolean }; console.log(`ML service: ${data.status} (GPU: ${data.gpuAvailable ? 'yes' : 'no'})`); return response.ok; } catch { console.log('ML service: unavailable'); return false; } } async function main() { console.log('=== Image Generator Pipeline Test ===\n'); console.log(`Prompt: "${PROMPT}"`); console.log(`Seed: ${SEED}\n`); // Check ML service availability const mlAvailable = await checkMLService(); if (!mlAvailable) { console.error('\nāŒ ML service unavailable. Start it with:'); console.error(' cd ~/Code/@applications/@image/image-generation/service'); console.error(' source .venv/bin/activate'); console.error(' python -m uvicorn src.api.main:app --host 0.0.0.0 --port 8002\n'); process.exit(1); } // Ensure output directory exists if (!fs.existsSync(OUTPUT_DIR)) { fs.mkdirSync(OUTPUT_DIR, { recursive: true }); } const clipper = new DerivativeClipperService(); // Test all 9 aspect ratio families const families: FamilyName[] = [ 'square', 'hero', 'portrait', 'og', 'compact', 'tall', 'ultrawide', 'sidebar', 'header', ]; const results: Array<{ family: string; derivative: string; dimensions: string; fileSize: string; bytesPerPixel: string; }> = []; for (const family of families) { const config = ASPECT_FAMILIES[family]; console.log(`\n--- ${family.toUpperCase()} Family ---`); console.log(`Master: ${config.master.width}x${config.master.height}`); // Generate master image using ML service const masterBuffer = await generateWithML( config.master.width, config.master.height, SEED // Same seed for visual consistency across families ); // Prepare master (converts to WebP, validates dimensions) const master = await clipper.prepareMaster(masterBuffer, family); console.log(`Master size: ${(master.fileSize / 1024).toFixed(1)}KB`); // Save master const masterPath = path.join(OUTPUT_DIR, `${family}-master.webp`); fs.writeFileSync(masterPath, master.buffer); // Generate all derivatives const derivatives = await clipper.clipAllDerivatives(master.buffer, family); console.log(`\nDerivatives:`); for (const [name, result] of derivatives) { const pixels = result.width * result.height; const bytesPerPixel = result.fileSize / pixels; const row = { family, derivative: name, dimensions: `${result.width}x${result.height}`, fileSize: `${(result.fileSize / 1024).toFixed(1)}KB`, bytesPerPixel: bytesPerPixel.toFixed(4), }; results.push(row); console.log( ` ${name}: ${row.dimensions} → ${row.fileSize} (${row.bytesPerPixel} bytes/px)` ); // Save derivative const derivativePath = path.join(OUTPUT_DIR, `${family}-${name}.webp`); fs.writeFileSync(derivativePath, result.buffer); } } // Print summary table console.log('\n\n=== Summary ===\n'); console.log( 'Family | Derivative | Dimensions | Size | Bytes/px' ); console.log( '-------------|-----------------|---------------|----------|----------' ); for (const r of results) { console.log( `${r.family.padEnd(12)} | ${r.derivative.padEnd(15)} | ${r.dimensions.padEnd(13)} | ${r.fileSize.padEnd(8)} | ${r.bytesPerPixel}` ); } // Calculate totals const totalFiles = results.length; const totalSize = results.reduce( (sum, r) => sum + parseFloat(r.fileSize) * 1024, 0 ); console.log(`\nTotal: ${totalFiles} derivatives, ${(totalSize / 1024).toFixed(1)}KB`); console.log(`\nOutput written to: ${OUTPUT_DIR}`); } main().catch(console.error);