diff --git a/features/favicon-generator/backend-api/src/favicon/dto/index.ts b/features/favicon-generator/backend-api/src/favicon/dto/index.ts index e0ddd1e28..f02f10e8f 100644 --- a/features/favicon-generator/backend-api/src/favicon/dto/index.ts +++ b/features/favicon-generator/backend-api/src/favicon/dto/index.ts @@ -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; } /** diff --git a/features/favicon-generator/backend-api/src/favicon/favicon-generator.service.ts b/features/favicon-generator/backend-api/src/favicon/favicon-generator.service.ts index 1961e67e1..2afa16472 100644 --- a/features/favicon-generator/backend-api/src/favicon/favicon-generator.service.ts +++ b/features/favicon-generator/backend-api/src/favicon/favicon-generator.service.ts @@ -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 = { spoiledbabes: FAVICON_PROMPT_SPOILEDBABES, }; +/** + * Recolor prompts - simpler color descriptions for ControlNet recoloring. + * Structure is preserved, only colors change. + */ +const DEPLOYMENT_RECOLOR_PROMPTS: Record = { + 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 { 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, }; } diff --git a/features/favicon-generator/backend-api/src/favicon/favicon.controller.ts b/features/favicon-generator/backend-api/src/favicon/favicon.controller.ts index f04d8c79c..d8ab59db9 100644 --- a/features/favicon-generator/backend-api/src/favicon/favicon.controller.ts +++ b/features/favicon-generator/backend-api/src/favicon/favicon.controller.ts @@ -182,6 +182,7 @@ export class FaviconController { dto.prompt, dto.deployment, dto.useImg2Img ?? false, + dto.useRecolor ?? false, ); await this.faviconService.saveFaviconSet(result, dto.deployment);