From 98ac4baf9e435946c6a25af55fdfda1620ff8fa6 Mon Sep 17 00:00:00 2001 From: faisal burhanudin Date: Fri, 23 Jan 2026 11:09:25 +0700 Subject: [PATCH 1/5] feat: blur background --- src/client/modules/Traits.ts | 7 ++++ src/server/config.ts | 1 + src/server/handlers/portrait-handler.ts | 24 +++++++++++++ src/server/services/image-service.ts | 48 +++++++++++++++++++++++++ src/server/services/prompt-service.ts | 6 ++-- 5 files changed, 82 insertions(+), 4 deletions(-) diff --git a/src/client/modules/Traits.ts b/src/client/modules/Traits.ts index a4533cb..5528049 100644 --- a/src/client/modules/Traits.ts +++ b/src/client/modules/Traits.ts @@ -98,4 +98,11 @@ export const TRAITS_OPTIONS: Trait[] = [ icon: '👨', category: 'age', }, + { + id: 'background-blur', + name: 'Background Blur', + description: 'Blur the background for professional focus', + icon: '🔘', + category: 'effects', + }, ]; diff --git a/src/server/config.ts b/src/server/config.ts index b491731..9c143dd 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -15,4 +15,5 @@ export const settings = { SEGMENT_WRITE_KEY: process.env.SEGMENT_WRITE_KEY || '', GCS_BUCKET_NAME: process.env.GCS_BUCKET_NAME || '', GCS_PROJECT_ID: process.env.GCS_PROJECT_ID || '', + DEEPINFRA_API_KEY: process.env.DEEPINFRA_API_KEY || '', } as const; diff --git a/src/server/handlers/portrait-handler.ts b/src/server/handlers/portrait-handler.ts index c3e4666..5fe1edf 100644 --- a/src/server/handlers/portrait-handler.ts +++ b/src/server/handlers/portrait-handler.ts @@ -52,6 +52,30 @@ export const handlePortraitGeneration = async ( const imageData = await imageService.generate(prompt, uploadedFilePath); + const hasBackgroundBlur = parsedTraits.includes('background-blur'); + + if (hasBackgroundBlur && imageData.b64_json) { + console.log('🔘 Background blur trait detected - applying blur...'); + try { + const blurredBase64 = await imageService.blurBackground( + imageData.b64_json + ); + const fileData = await imageService.saveImageFile( + blurredBase64, + 'bria-blur' + ); + imageData.url = fileData.url; + imageData.filename = fileData.filename; + imageData.fileSize = fileData.fileSize; + imageData.b64_json = blurredBase64; + } catch (blurError) { + console.warn( + '⚠️ Background blur failed, returning original image:', + blurError + ); + } + } + res.json({ success: true, image: { diff --git a/src/server/services/image-service.ts b/src/server/services/image-service.ts index 316e735..3cd1b24 100644 --- a/src/server/services/image-service.ts +++ b/src/server/services/image-service.ts @@ -15,6 +15,7 @@ const portkey = new Portkey({ const genAI = new GoogleGenAI({ apiKey: settings.GEMINI_API_KEY }); const IMAGE_GENERATION_TIMEOUT = 120000; +const BLUR_BACKGROUND_TIMEOUT = 30000; type ImageProvider = 'portkey' | 'google-genai'; @@ -29,6 +30,13 @@ interface ImageData { provider?: string; } +interface DeepInfraResponse { + created: number; + data: Array<{ + b64_json: string; + }>; +} + function getImageProvider(): ImageProvider { if (settings.PORTKEY_API_KEY) { return 'portkey'; @@ -203,6 +211,46 @@ class ImageService { throw new Error('No image data found in Gemini response'); } + async blurBackground(imageBase64: string): Promise { + if (!settings.DEEPINFRA_API_KEY) { + throw new Error('DEEPINFRA_API_KEY not configured'); + } + + console.log('🔘 Applying background blur via DeepInfra Bria API...'); + + const response = await Promise.race([ + fetch('https://api.deepinfra.com/v1/openai/images/edits', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${settings.DEEPINFRA_API_KEY}`, + }, + body: JSON.stringify({ + model: 'Bria/blur_background', + image: imageBase64, + n: 1, + }), + }), + this.createTimeoutPromise(BLUR_BACKGROUND_TIMEOUT), + ]); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `DeepInfra API error: ${response.status} ${response.statusText} - ${errorText}` + ); + } + + const data = (await response.json()) as DeepInfraResponse; + + if (!data.data || !data.data[0]?.b64_json) { + throw new Error('No blurred image data returned from DeepInfra API'); + } + + console.log('✅ Background blur applied successfully'); + return data.data[0].b64_json; + } + private createTimeoutPromise(timeoutMs: number): Promise { return new Promise((_, reject) => { setTimeout(() => { diff --git a/src/server/services/prompt-service.ts b/src/server/services/prompt-service.ts index 75a8dd8..5a31e6a 100644 --- a/src/server/services/prompt-service.ts +++ b/src/server/services/prompt-service.ts @@ -43,6 +43,7 @@ const TRAIT_MAPPINGS = { beard: 'with beard', 'young-adult': 'young adult appearance', 'middle-aged': 'middle-aged appearance', + 'background-blur': '', }; const PROMPT_TEMPLATE = @@ -169,10 +170,7 @@ etc.`; }): string { const styleIds = Array.isArray(imageStyle) ? imageStyle : [imageStyle]; const stylePromptParts = styleIds - .map( - (id) => - STYLE_PROMPTS[id as keyof typeof STYLE_PROMPTS] - ) + .map((id) => STYLE_PROMPTS[id as keyof typeof STYLE_PROMPTS]) .filter(Boolean); const stylePrompt = From 2a3507656e5fa8be567eab74d35595def2f7dc9a Mon Sep 17 00:00:00 2001 From: faisal burhanudin Date: Fri, 23 Jan 2026 11:29:36 +0700 Subject: [PATCH 2/5] feat: implement background blur feature with DeepInfra Bria API - Add DEEPINFRA_API_KEY configuration support - Implement blurBackground() service method using multipart/form-data - Add background blur trait to user options - Integrate blur processing in portrait generation workflow - Return encapsulated ImageData from blur service - Add graceful fallback if blur processing fails --- .env.template | 4 ++++ src/server/handlers/portrait-handler.ts | 11 ++-------- src/server/services/image-service.ts | 29 ++++++++++++++++++------- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/.env.template b/.env.template index 369ccb0..46c5859 100644 --- a/.env.template +++ b/.env.template @@ -12,6 +12,10 @@ MAXMIND_LICENSE_KEY= PORTKEY_API_KEY= GEMINI_API_KEY= +# DeepInfra API (optional) +# Used for background blur when "Background Blur" trait is selected +DEEPINFRA_API_KEY= + SENTRY_DSN= # Google Cloud Storage (optional) diff --git a/src/server/handlers/portrait-handler.ts b/src/server/handlers/portrait-handler.ts index 5fe1edf..37894df 100644 --- a/src/server/handlers/portrait-handler.ts +++ b/src/server/handlers/portrait-handler.ts @@ -57,17 +57,10 @@ export const handlePortraitGeneration = async ( if (hasBackgroundBlur && imageData.b64_json) { console.log('🔘 Background blur trait detected - applying blur...'); try { - const blurredBase64 = await imageService.blurBackground( + const blurredImageData = await imageService.blurBackground( imageData.b64_json ); - const fileData = await imageService.saveImageFile( - blurredBase64, - 'bria-blur' - ); - imageData.url = fileData.url; - imageData.filename = fileData.filename; - imageData.fileSize = fileData.fileSize; - imageData.b64_json = blurredBase64; + Object.assign(imageData, blurredImageData); } catch (blurError) { console.warn( '⚠️ Background blur failed, returning original image:', diff --git a/src/server/services/image-service.ts b/src/server/services/image-service.ts index 3cd1b24..d584e81 100644 --- a/src/server/services/image-service.ts +++ b/src/server/services/image-service.ts @@ -211,25 +211,31 @@ class ImageService { throw new Error('No image data found in Gemini response'); } - async blurBackground(imageBase64: string): Promise { + async blurBackground(imageBase64: string): Promise { if (!settings.DEEPINFRA_API_KEY) { throw new Error('DEEPINFRA_API_KEY not configured'); } console.log('🔘 Applying background blur via DeepInfra Bria API...'); + const imageBuffer = Buffer.from(imageBase64, 'base64'); + const formData = new FormData(); + formData.append( + 'image', + new Blob([imageBuffer], { type: 'image/png' }), + 'image.png' + ); + formData.append('prompt', 'blur background'); + formData.append('model', 'Bria/blur_background'); + formData.append('n', '1'); + const response = await Promise.race([ fetch('https://api.deepinfra.com/v1/openai/images/edits', { method: 'POST', headers: { - 'Content-Type': 'application/json', Authorization: `Bearer ${settings.DEEPINFRA_API_KEY}`, }, - body: JSON.stringify({ - model: 'Bria/blur_background', - image: imageBase64, - n: 1, - }), + body: formData, }), this.createTimeoutPromise(BLUR_BACKGROUND_TIMEOUT), ]); @@ -247,8 +253,15 @@ class ImageService { throw new Error('No blurred image data returned from DeepInfra API'); } + const blurredBase64 = data.data[0].b64_json; + const fileData = await this.saveImageFile(blurredBase64, 'bria-blur'); console.log('✅ Background blur applied successfully'); - return data.data[0].b64_json; + return { + ...fileData, + b64_json: blurredBase64, + width: 1024, + height: 1024, + }; } private createTimeoutPromise(timeoutMs: number): Promise { From 5aa4b596f90028b67d852777ad3e6b00c5a41994 Mon Sep 17 00:00:00 2001 From: faisal burhanudin Date: Fri, 23 Jan 2026 13:12:38 +0700 Subject: [PATCH 3/5] fix: request --- src/server/services/image-service.ts | 43 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/server/services/image-service.ts b/src/server/services/image-service.ts index d584e81..6222543 100644 --- a/src/server/services/image-service.ts +++ b/src/server/services/image-service.ts @@ -218,25 +218,21 @@ class ImageService { console.log('🔘 Applying background blur via DeepInfra Bria API...'); - const imageBuffer = Buffer.from(imageBase64, 'base64'); - const formData = new FormData(); - formData.append( - 'image', - new Blob([imageBuffer], { type: 'image/png' }), - 'image.png' - ); - formData.append('prompt', 'blur background'); - formData.append('model', 'Bria/blur_background'); - formData.append('n', '1'); - const response = await Promise.race([ - fetch('https://api.deepinfra.com/v1/openai/images/edits', { - method: 'POST', - headers: { - Authorization: `Bearer ${settings.DEEPINFRA_API_KEY}`, - }, - body: formData, - }), + fetch( + 'https://api.deepinfra.com/v1/inference/Bria/blur_background?version=0ObxGWB8', + { + method: 'POST', + headers: { + Authorization: `Bearer ${settings.DEEPINFRA_API_KEY}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + image: `data:image/png;base64,${imageBase64}`, + scale: 5, + }), + } + ), this.createTimeoutPromise(BLUR_BACKGROUND_TIMEOUT), ]); @@ -247,13 +243,16 @@ class ImageService { ); } - const data = (await response.json()) as DeepInfraResponse; + const data = (await response.json()) as { images: string[] }; - if (!data.data || !data.data[0]?.b64_json) { - throw new Error('No blurred image data returned from DeepInfra API'); + if (!data.images || !data.images[0]) { + throw new Error('No blurred image URL returned from DeepInfra API'); } - const blurredBase64 = data.data[0].b64_json; + const imageUrl = data.images[0]; + const imageResponse = await fetch(imageUrl); + const imageBuffer = await imageResponse.arrayBuffer(); + const blurredBase64 = Buffer.from(imageBuffer).toString('base64'); const fileData = await this.saveImageFile(blurredBase64, 'bria-blur'); console.log('✅ Background blur applied successfully'); return { From c3b2d985d940bfb2df26122cb5ccaf84eafd0a23 Mon Sep 17 00:00:00 2001 From: faisal burhanudin Date: Fri, 6 Feb 2026 13:29:33 +0700 Subject: [PATCH 4/5] review --- src/server/services/image-service.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/server/services/image-service.ts b/src/server/services/image-service.ts index 6222543..4ed76bd 100644 --- a/src/server/services/image-service.ts +++ b/src/server/services/image-service.ts @@ -19,7 +19,7 @@ const BLUR_BACKGROUND_TIMEOUT = 30000; type ImageProvider = 'portkey' | 'google-genai'; -interface ImageData { +type ImageData = { url?: string; filename?: string; fileSize?: number; @@ -28,14 +28,7 @@ interface ImageData { height?: number; model?: string; provider?: string; -} - -interface DeepInfraResponse { - created: number; - data: Array<{ - b64_json: string; - }>; -} +}; function getImageProvider(): ImageProvider { if (settings.PORTKEY_API_KEY) { @@ -245,7 +238,7 @@ class ImageService { const data = (await response.json()) as { images: string[] }; - if (!data.images || !data.images[0]) { + if (!Array.isArray(data.images) || data.images.length < 1) { throw new Error('No blurred image URL returned from DeepInfra API'); } From b74a7adc6c45f9e2f862a0f7eb4f0cc53fd353c2 Mon Sep 17 00:00:00 2001 From: faisal burhanudin Date: Fri, 6 Feb 2026 13:34:59 +0700 Subject: [PATCH 5/5] fix conflict --- src/server/config.ts | 1 + src/server/services/image-service.ts | 111 ++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 4 deletions(-) diff --git a/src/server/config.ts b/src/server/config.ts index 18627d5..8a338c6 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -16,5 +16,6 @@ export const settings = { GCS_BUCKET_NAME: process.env.GCS_BUCKET_NAME || '', GCS_PROJECT_ID: process.env.GCS_PROJECT_ID || '', DEEPINFRA_API_KEY: process.env.DEEPINFRA_API_KEY || '', + FLUX_API_KEY: process.env.FLUX_API_KEY || '', ALLOW_FACE_UPLOAD: process.env.ALLOW_FACE_UPLOAD === 'true', } as const; diff --git a/src/server/services/image-service.ts b/src/server/services/image-service.ts index 4ed76bd..e35e8ce 100644 --- a/src/server/services/image-service.ts +++ b/src/server/services/image-service.ts @@ -17,7 +17,7 @@ const genAI = new GoogleGenAI({ apiKey: settings.GEMINI_API_KEY }); const IMAGE_GENERATION_TIMEOUT = 120000; const BLUR_BACKGROUND_TIMEOUT = 30000; -type ImageProvider = 'portkey' | 'google-genai'; +type ImageProvider = 'portkey' | 'google-genai' | 'flux'; type ImageData = { url?: string; @@ -37,8 +37,11 @@ function getImageProvider(): ImageProvider { if (settings.GEMINI_API_KEY) { return 'google-genai'; } + if (settings.FLUX_API_KEY) { + return 'flux'; + } throw new Error( - 'No image generation provider configured. Please set either PORTKEY_API_KEY or GEMINI_API_KEY in environment variables.' + 'No image generation provider configured. Please set PORTKEY_API_KEY, GEMINI_API_KEY, or FLUX_API_KEY in environment variables.' ); } @@ -59,11 +62,15 @@ class ImageService { const imageData = provider === 'portkey' ? await this.generateWithPortkey(prompt, imagePath) - : await this.generateWithGoogleGenAI(prompt, imagePath); + : provider === 'flux' + ? await this.generateWithFlux(prompt, imagePath) + : await this.generateWithGoogleGenAI(prompt, imagePath); + const modelName = + provider === 'flux' ? 'flux-pro-1.1' : 'gemini-3-pro-image-preview'; return { ...imageData, - model: 'gemini-3-pro-image-preview', + model: modelName, provider, }; } @@ -204,6 +211,102 @@ class ImageService { throw new Error('No image data found in Gemini response'); } + private async generateWithFlux( + prompt: string, + imagePath?: string + ): Promise { + if (!settings.FLUX_API_KEY) { + throw new Error('FLUX_API_KEY not configured'); + } + + const payload: Record = { + prompt, + width: 1024, + height: 1024, + prompt_upsampling: false, + seed: 42, + safety_tolerance: 2, + output_format: 'jpeg', + }; + + if (imagePath) { + const imageBase64 = await this.readImageAsBase64(imagePath); + payload.image_prompt = imageBase64; + } + + const response = await fetch('https://api.bfl.ai/v1/flux-pro-1.1', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-key': settings.FLUX_API_KEY, + }, + body: JSON.stringify(payload), + }); + + const data = (await response.json()) as { + polling_url?: string; + error?: string; + }; + + if (!data.polling_url) { + throw new Error( + `Flux API error: ${response.statusText} ${data.error || ''}` + ); + } + + const imageUrl = await this.pollFluxResult(data.polling_url); + + const imageResponse = await fetch(imageUrl); + const buffer = Buffer.from(await imageResponse.arrayBuffer()); + const base64Data = buffer.toString('base64'); + + const fileData = await this.saveImageFile(base64Data, 'flux-pro-1.1'); + + return { + ...fileData, + b64_json: base64Data, + width: 1024, + height: 1024, + }; + } + + private async pollFluxResult(pollingUrl: string): Promise { + const startTime = Date.now(); + const timeout = IMAGE_GENERATION_TIMEOUT; + const interval = 500; + + while (Date.now() - startTime < timeout) { + const response = await fetch(pollingUrl, { + headers: { 'x-key': settings.FLUX_API_KEY }, + }); + const data = (await response.json()) as { + status: string; + result?: { sample: string }; + }; + + if (data.status === 'Ready') { + if (!data.result?.sample) { + throw new Error('Flux API returned Ready status but no sample image'); + } + return data.result.sample; + } + + const errorStatuses = [ + 'Task not found', + 'Request Moderated', + 'Content Moderated', + 'Error', + ]; + if (errorStatuses.includes(data.status)) { + throw new Error(`Flux task failed: ${data.status}`); + } + + await new Promise((resolve) => setTimeout(resolve, interval)); + } + + throw new Error('Flux generation timeout'); + } + async blurBackground(imageBase64: string): Promise { if (!settings.DEEPINFRA_API_KEY) { throw new Error('DEEPINFRA_API_KEY not configured');