Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Any
|
2 |
+
import os
|
3 |
+
from PIL import Image
|
4 |
+
import io
|
5 |
+
import base64
|
6 |
+
import requests
|
7 |
+
import time
|
8 |
+
|
9 |
+
#Gradio for the hackaton:
|
10 |
+
import gradio as gr
|
11 |
+
|
12 |
+
# we used uv add mcp[cli] httpx to get these:
|
13 |
+
import httpx
|
14 |
+
from mcp.server.fastmcp import FastMCP
|
15 |
+
|
16 |
+
# Initialize FastMCP server
|
17 |
+
mcp = FastMCP("linkedin-image-processor")
|
18 |
+
|
19 |
+
# Constants
|
20 |
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
21 |
+
|
22 |
+
#let's add our helper functions:
|
23 |
+
|
24 |
+
async def flux_kontext_edit_image(image: Image.Image, prompt: str) -> Image.Image:
|
25 |
+
"""Use Flux Kontext API to edit an image based on a prompt
|
26 |
+
|
27 |
+
Args:
|
28 |
+
image: PIL Image to edit
|
29 |
+
prompt: Text description of what to edit
|
30 |
+
|
31 |
+
Returns:
|
32 |
+
Image.Image: Edited image from Flux Kontext
|
33 |
+
"""
|
34 |
+
try:
|
35 |
+
# Encode image to base64
|
36 |
+
buffered = io.BytesIO()
|
37 |
+
image.save(buffered, format="JPEG")
|
38 |
+
img_str = base64.b64encode(buffered.getvalue()).decode()
|
39 |
+
|
40 |
+
# Make request to Flux Kontext API
|
41 |
+
response = requests.post(
|
42 |
+
'https://api.bfl.ai/v1/flux-kontext-pro',
|
43 |
+
headers={
|
44 |
+
'accept': 'application/json',
|
45 |
+
'x-key': os.environ.get("BFL_API_KEY"),
|
46 |
+
'Content-Type': 'application/json',
|
47 |
+
},
|
48 |
+
json={
|
49 |
+
'prompt': prompt,
|
50 |
+
'input_image': img_str,
|
51 |
+
},
|
52 |
+
)
|
53 |
+
|
54 |
+
if response.status_code != 200:
|
55 |
+
print(f"API request failed: {response.status_code}")
|
56 |
+
return image
|
57 |
+
|
58 |
+
request_data = response.json()
|
59 |
+
request_id = request_data.get("id")
|
60 |
+
|
61 |
+
if not request_id:
|
62 |
+
print("No request ID received")
|
63 |
+
return image
|
64 |
+
|
65 |
+
# Poll for result (simplified polling)
|
66 |
+
max_attempts = 30
|
67 |
+
for attempt in range(max_attempts):
|
68 |
+
time.sleep(2)
|
69 |
+
|
70 |
+
result_response = requests.get(
|
71 |
+
f'https://api.bfl.ai/v1/get_result?id={request_id}',
|
72 |
+
headers={
|
73 |
+
'accept': 'application/json',
|
74 |
+
'x-key': os.environ.get("BFL_API_KEY"),
|
75 |
+
}
|
76 |
+
)
|
77 |
+
|
78 |
+
if result_response.status_code == 200:
|
79 |
+
result_data = result_response.json()
|
80 |
+
|
81 |
+
if result_data.get("status") == "Ready":
|
82 |
+
image_url = result_data.get("result", {}).get("sample")
|
83 |
+
if image_url:
|
84 |
+
# Download and return the edited image
|
85 |
+
img_response = requests.get(image_url)
|
86 |
+
edited_image = Image.open(io.BytesIO(img_response.content))
|
87 |
+
return edited_image
|
88 |
+
|
89 |
+
elif result_data.get("status") == "Error":
|
90 |
+
print(f"Flux Kontext error: {result_data.get('result')}")
|
91 |
+
break
|
92 |
+
|
93 |
+
print("Flux Kontext processing timeout or failed")
|
94 |
+
return image
|
95 |
+
|
96 |
+
except Exception as e:
|
97 |
+
print(f"Error with Flux Kontext API: {e}")
|
98 |
+
return image
|
99 |
+
|
100 |
+
def process_linkedin_image(image) -> Image.Image:
|
101 |
+
"""Process an image for LinkedIn optimization using Flux Kontext
|
102 |
+
|
103 |
+
Args:
|
104 |
+
image: Input image file
|
105 |
+
|
106 |
+
Returns:
|
107 |
+
Image.Image: Processed image optimized for LinkedIn
|
108 |
+
"""
|
109 |
+
if image is None:
|
110 |
+
return None
|
111 |
+
|
112 |
+
try:
|
113 |
+
# Handle different input types
|
114 |
+
if isinstance(image, str):
|
115 |
+
img = Image.open(image)
|
116 |
+
else:
|
117 |
+
img = image
|
118 |
+
|
119 |
+
# Define the fixed professional prompt
|
120 |
+
professional_prompt = "Make the person wear a light blue blazer, make the background white and clean any noise in the foreground. make the hair more orderly. Keep the face of the person intact. keep the gender of the person intact. the image should always be a bust"
|
121 |
+
|
122 |
+
# First, use Flux Kontext to enhance/edit the image
|
123 |
+
import asyncio
|
124 |
+
edited_img = asyncio.run(flux_kontext_edit_image(img, professional_prompt))
|
125 |
+
|
126 |
+
# Then apply LinkedIn optimization
|
127 |
+
target_width = 800
|
128 |
+
target_height = 800
|
129 |
+
|
130 |
+
# Calculate aspect ratio
|
131 |
+
original_width, original_height = edited_img.size
|
132 |
+
original_ratio = original_width / original_height
|
133 |
+
target_ratio = target_width / target_height
|
134 |
+
|
135 |
+
# Resize while maintaining aspect ratio
|
136 |
+
if original_ratio > target_ratio:
|
137 |
+
new_width = target_width
|
138 |
+
new_height = int(target_width / original_ratio)
|
139 |
+
else:
|
140 |
+
new_height = target_height
|
141 |
+
new_width = int(target_height * original_ratio)
|
142 |
+
|
143 |
+
# Resize the image
|
144 |
+
img_resized = edited_img.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
145 |
+
|
146 |
+
# Create a new image with LinkedIn dimensions and white background
|
147 |
+
linkedin_img = Image.new('RGB', (target_width, target_height), 'white')
|
148 |
+
|
149 |
+
# Calculate position to center the resized image
|
150 |
+
x = (target_width - new_width) // 2
|
151 |
+
y = (target_height - new_height) // 2
|
152 |
+
|
153 |
+
# Paste the resized image onto the LinkedIn-sized canvas
|
154 |
+
linkedin_img.paste(img_resized, (x, y))
|
155 |
+
|
156 |
+
return linkedin_img
|
157 |
+
|
158 |
+
except Exception as e:
|
159 |
+
print(f"Error processing image: {e}")
|
160 |
+
return image if image else None
|
161 |
+
|
162 |
+
@mcp.tool()
|
163 |
+
async def create_professional_linkedin_headshot(image_url: str) -> str:
|
164 |
+
"""Transform any photo into a professional LinkedIn headshot using AI.
|
165 |
+
|
166 |
+
Automatically adds professional business attire (light blue blazer), creates a clean white
|
167 |
+
background, tidies hair, removes noise, and formats as an 800x800 centered bust shot while
|
168 |
+
preserving facial features and gender. Perfect for professional headshots, profile pictures,
|
169 |
+
business photos, and LinkedIn optimization.
|
170 |
+
|
171 |
+
Args:
|
172 |
+
image_url: HTTP/HTTPS URL to the input image file (JPEG, PNG supported)
|
173 |
+
|
174 |
+
Returns:
|
175 |
+
str: Success message or error description
|
176 |
+
"""
|
177 |
+
try:
|
178 |
+
processed_img = process_linkedin_image(image_url)
|
179 |
+
if processed_img:
|
180 |
+
return "Professional LinkedIn headshot created successfully - added business attire, clean background, and professional formatting"
|
181 |
+
else:
|
182 |
+
return "Failed to process image for LinkedIn optimization"
|
183 |
+
except Exception as e:
|
184 |
+
return f"Error creating professional headshot: {str(e)}"
|
185 |
+
|
186 |
+
@mcp.resource("config://linkedin-optimizer")
|
187 |
+
async def linkedin_optimizer_resource():
|
188 |
+
"""LinkedIn image optimization resource
|
189 |
+
Provides optimal dimensions and processing for LinkedIn posts
|
190 |
+
"""
|
191 |
+
return {
|
192 |
+
"name": "LinkedIn Image Optimizer",
|
193 |
+
"description": "Optimizes images for LinkedIn posts",
|
194 |
+
"recommended_dimensions": "800x800 pixels",
|
195 |
+
"supported_formats": ["JPEG", "PNG", "GIF"],
|
196 |
+
"max_file_size": "5MB"
|
197 |
+
}
|
198 |
+
|
199 |
+
demo = gr.Interface(
|
200 |
+
fn=process_linkedin_image,
|
201 |
+
inputs=gr.Image(type="pil", label="Upload Your Photo"),
|
202 |
+
outputs=gr.Image(type="pil", label="Professional LinkedIn Photo"),
|
203 |
+
title="Professional LinkedIn Photo Generator",
|
204 |
+
description="Upload a photo and automatically transform it into a professional LinkedIn profile picture."
|
205 |
+
)
|
206 |
+
|
207 |
+
if __name__ == "__main__":
|
208 |
+
# Initialize and run the server
|
209 |
+
demo.launch(mcp_server=True)
|