LuisZermeno commited on
Commit
bd0da6c
·
verified ·
1 Parent(s): a83e182

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +209 -0
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)