platform-codebase/features/image-generator/backend-api/scripts/sample-generation.ts

227 lines
6.5 KiB
TypeScript
Executable file

/**
* 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 '@/src/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<Buffer> {
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<boolean> {
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);