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:
parent
4601661721
commit
a664b82fed
3 changed files with 119 additions and 15 deletions
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,6 +182,7 @@ export class FaviconController {
|
|||
dto.prompt,
|
||||
dto.deployment,
|
||||
dto.useImg2Img ?? false,
|
||||
dto.useRecolor ?? false,
|
||||
);
|
||||
await this.faviconService.saveFaviconSet(result, dto.deployment);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue