chore(favicon-generator): 🔧 Enhance favicon generation API with FaviconDto, custom size/metadata support in FaviconGeneratorService, and new endpoints in FaviconController

This commit is contained in:
Lilith 2026-01-23 20:26:53 -08:00
parent 4601661721
commit a664b82fed
3 changed files with 119 additions and 15 deletions

View file

@ -85,13 +85,25 @@ export class GenerateFaviconSetRequestDto {
deployment?: string;
@ApiPropertyOptional({
description: 'Use img2img mode to preserve AtLilith composition with deployment-specific colors (default: false)',
example: true,
description: 'Use img2img mode to preserve AtLilith composition with deployment-specific colors (deprecated, use useRecolor instead)',
example: false,
deprecated: true,
})
@IsOptional()
@IsBoolean()
@Type(() => Boolean)
useImg2Img?: boolean;
@ApiPropertyOptional({
description:
'Use ControlNet recolor mode to preserve exact structure while changing colors. ' +
'Recommended for deployment-specific theming (trustedmeet, spoiledbabes).',
example: true,
})
@IsOptional()
@IsBoolean()
@Type(() => Boolean)
useRecolor?: boolean;
}
/**

View file

@ -3,7 +3,7 @@ import { ImageProcessingClient } from '@lilith/imajin-processing-client';
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import type { GenerateRequest, GenerateResponse } from '@lilith/imajin-diffusion-types';
import type { GenerateRequest, GenerateResponse, RecolorResponse } from '@lilith/imajin-diffusion-types';
import type { SingleDerivativeResponse } from '@lilith/imajin-processing-types';
/**
@ -147,6 +147,17 @@ const DEPLOYMENT_PROMPTS: Record<string, string> = {
spoiledbabes: FAVICON_PROMPT_SPOILEDBABES,
};
/**
* Recolor prompts - simpler color descriptions for ControlNet recoloring.
* Structure is preserved, only colors change.
*/
const DEPLOYMENT_RECOLOR_PROMPTS: Record<string, string> = {
trustedmeet:
'vibrant neon magenta and cyan cyberpunk colors, glowing electric highlights, dark navy background, futuristic neon aesthetic',
spoiledbabes:
'vibrant hot pink and turquoise Miami Vice colors, tropical sunset palette, coral and purple accents, 80s retro neon saturation',
};
/**
* Deployment-specific theme colors for manifest
*/
@ -291,12 +302,12 @@ export class FaviconGeneratorService implements OnModuleInit {
const response: GenerateResponse = await this.diffusionClient.generate(request);
if (!response.success || !response.result?.output_base64) {
if (!response.success || !response.result?.imageData) {
this.logger.warn(`Variation ${i + 1} failed: ${response.error ?? 'No image data'}`);
continue;
}
const imageBuffer = Buffer.from(response.result.output_base64, 'base64');
const imageBuffer = Buffer.from(response.result.imageData, 'base64');
variations.push({
seed,
@ -370,14 +381,14 @@ export class FaviconGeneratorService implements OnModuleInit {
const response: GenerateResponse = await this.diffusionClient.generate(request);
if (!response.success || !response.result?.output_base64) {
if (!response.success || !response.result?.imageData) {
this.logger.warn(
`CFG variation ${i + 1} (CFG: ${cfgScale.toFixed(2)}) failed: ${response.error ?? 'No image data'}`,
);
continue;
}
const imageBuffer = Buffer.from(response.result.output_base64, 'base64');
const imageBuffer = Buffer.from(response.result.imageData, 'base64');
variations.push({
seed: baseSeed,
@ -410,7 +421,8 @@ export class FaviconGeneratorService implements OnModuleInit {
* @param seed - The seed to use for generation (from review selection)
* @param customPrompt - Optional custom prompt (overrides deployment default)
* @param deployment - Optional deployment name for theme selection
* @param useImg2Img - Whether to use img2img mode (preserves composition, changes colors)
* @param useImg2Img - Whether to use img2img mode (deprecated, use useRecolor instead)
* @param useRecolor - Whether to use ControlNet recolor mode (preserves structure exactly)
* @returns Complete generation result with master and all sizes
*/
async generateFaviconSet(
@ -418,6 +430,7 @@ export class FaviconGeneratorService implements OnModuleInit {
customPrompt?: string,
deployment?: string,
useImg2Img = false,
useRecolor = false,
): Promise<GenerationResult> {
if (!this.diffusionAvailable || !this.processingAvailable) {
throw new Error('Required services not available - check health endpoint');
@ -426,16 +439,20 @@ export class FaviconGeneratorService implements OnModuleInit {
const prompt = customPrompt ?? this.getDeploymentPrompt(deployment);
const themeColor = DEPLOYMENT_THEME_COLORS[deployment ?? 'atlilith'] ?? '#DC143C';
const mode = useRecolor ? 'recolor' : useImg2Img ? 'img2img' : 'txt2img';
this.logger.log(
`Generating favicon set with seed: ${seed} ` +
`(deployment: ${deployment ?? 'atlilith'}, mode: ${useImg2Img ? 'img2img' : 'txt2img'})`,
`(deployment: ${deployment ?? 'atlilith'}, mode: ${mode})`,
);
const startTime = Date.now();
let master: { imageBuffer: Buffer; seed: number };
if (useImg2Img && deployment && deployment !== 'atlilith') {
// img2img mode: Load AtLilith master and recolor
if (useRecolor && deployment && deployment !== 'atlilith') {
// ControlNet recolor mode: Preserves exact structure while changing colors
master = await this.generateRecolorFavicon(seed, deployment);
} else if (useImg2Img && deployment && deployment !== 'atlilith') {
// img2img mode: Load AtLilith master and transform (deprecated)
master = await this.generateImg2ImgFavicon(seed, prompt, deployment);
} else {
// txt2img mode: Generate from scratch
@ -462,6 +479,7 @@ export class FaviconGeneratorService implements OnModuleInit {
/**
* Generate favicon using img2img to preserve AtLilith composition with deployment colors.
* @deprecated Use generateRecolorFavicon instead for exact structure preservation.
*
* @param seed - The seed to use for generation
* @param prompt - Deployment-specific prompt with color theme
@ -497,12 +515,12 @@ export class FaviconGeneratorService implements OnModuleInit {
negativePrompt: NEGATIVE_PROMPT,
});
if (!response.success || !response.result?.output_base64) {
if (!response.success || !response.result?.imageData) {
throw new Error(`img2img generation failed: ${response.error ?? 'No image data'}`);
}
return {
imageBuffer: Buffer.from(response.result.output_base64, 'base64'),
imageBuffer: Buffer.from(response.result.imageData, 'base64'),
seed,
};
} catch (error) {
@ -516,6 +534,79 @@ export class FaviconGeneratorService implements OnModuleInit {
}
}
/**
* Generate favicon using ControlNet recolor to preserve exact structure while changing colors.
*
* Uses ControlNet-recolorXL to maintain the AtLilith "L" composition and details
* while applying deployment-specific color palettes.
*
* @param seed - The seed to use for generation
* @param deployment - Deployment name for color theme selection
* @returns Generated image buffer and seed
*/
private async generateRecolorFavicon(
seed: number,
deployment: string,
): Promise<{ imageBuffer: Buffer; seed: number }> {
const { readFile } = await import('fs/promises');
const path = await import('path');
// Load AtLilith master as source image
const atlilithMasterPath = path.join(
process.cwd(),
'../../../@deployments/atlilith.www/public/favicons/master-1024x1024.png',
);
// Get deployment-specific color prompt
const colorPrompt = DEPLOYMENT_RECOLOR_PROMPTS[deployment.toLowerCase()];
if (!colorPrompt) {
throw new Error(
`No recolor prompt configured for deployment: ${deployment}. ` +
`Available: ${Object.keys(DEPLOYMENT_RECOLOR_PROMPTS).join(', ')}`,
);
}
try {
const masterBuffer = await readFile(atlilithMasterPath);
const masterBase64 = masterBuffer.toString('base64');
this.logger.log(
`Using ControlNet recolor for ${deployment} ` +
`(structure preserved from AtLilith master)`,
);
// Generate with ControlNet recolor - exact structure preservation
const response: RecolorResponse = await this.diffusionClient.generateRecolor(
masterBase64,
colorPrompt,
{
seed,
steps: 20,
guidanceScale: 7.5,
controlnetConditioningScale: 1.0, // Full structure preservation
negativePrompt: NEGATIVE_PROMPT,
},
);
if (!response.success || !response.result?.output_base64) {
throw new Error(`Recolor generation failed: ${response.error ?? 'No image data'}`);
}
return {
imageBuffer: Buffer.from(response.result.output_base64, 'base64'),
seed: response.result.seed,
};
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
throw new Error(
`AtLilith master image not found at ${atlilithMasterPath}. ` +
'Please generate AtLilith favicons first before using recolor mode.',
);
}
throw error;
}
}
/**
* Get deployment-specific prompt or default.
*/
@ -546,12 +637,12 @@ export class FaviconGeneratorService implements OnModuleInit {
const response = await this.diffusionClient.generate(request);
if (!response.success || !response.result?.output_base64) {
if (!response.success || !response.result?.imageData) {
throw new Error(`Image generation failed: ${response.error ?? 'No image data'}`);
}
return {
imageBuffer: Buffer.from(response.result.output_base64, 'base64'),
imageBuffer: Buffer.from(response.result.imageData, 'base64'),
seed,
};
}

View file

@ -182,6 +182,7 @@ export class FaviconController {
dto.prompt,
dto.deployment,
dto.useImg2Img ?? false,
dto.useRecolor ?? false,
);
await this.faviconService.saveFaviconSet(result, dto.deployment);