227 lines
6.5 KiB
TypeScript
Executable file
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);
|