@@ -22,61 +22,33 @@ class WordmarkDescription(dspy.Signature):
2222 )
2323
2424
25- class IconDescription (dspy .Signature ):
26- """Generate a creative description for a square icon (no text). The icon should be clean, modern, and recognizable."""
27-
28- project_name : str = dspy .InputField ()
29- suggestion : str = dspy .InputField (
30- desc = "Optional suggestion to guide the icon description generation"
31- )
32- is_dark_mode : bool = dspy .InputField (
33- desc = "Whether this is for dark mode (light colors) or light mode (dark colors)"
34- )
35- icon_description : str = dspy .OutputField (
36- desc = "A creative description for a square icon without any text. Focus on simple shapes, clean lines, and a professional look."
37- )
38-
39-
4025client = genai .Client (api_key = global_config .GEMINI_API_KEY )
4126
4227
4328def remove_greenscreen (img : Image .Image , tolerance : int = 60 ) -> Image .Image :
44- """Remove lime green/ greenscreen background using aggressive chroma key removal ."""
29+ """Remove greenscreen with better edge preservation ."""
4530 if img .mode != "RGBA" :
4631 img = img .convert ("RGBA" )
4732
4833 data = np .array (img , dtype = np .float32 )
4934 r , g , b , alpha = data [:, :, 0 ], data [:, :, 1 ], data [:, :, 2 ], data [:, :, 3 ]
5035
51- # Step 1: Identify obvious greenscreen pixels (more aggressive threshold)
52- green_high = g > 160 # Lowered from 180
53- green_dominant = (g > r + tolerance ) & (g > b + tolerance )
54- red_blue_low = (r < 200 ) & (b < 200 ) # Raised from 180
55- greenscreen_mask = green_high & green_dominant & red_blue_low
36+ # More conservative greenscreen detection
37+ green_high = g > 180 # Higher threshold
38+ green_dominant = (g > r + tolerance + 20 ) & (g > b + tolerance + 20 )
39+ greenscreen_mask = green_high & green_dominant
5640
5741 # Set alpha to 0 for greenscreen pixels
5842 alpha [greenscreen_mask ] = 0
5943
60- # Step 2: Aggressively remove green spill from ALL pixels with green tint
61- visible = alpha > 0
62- has_green_tint = g > r + 10 # Very low threshold
63- has_strong_green_tint = g > b + 10
64-
65- green_tinted = visible & has_green_tint & has_strong_green_tint
44+ # Gentler green spill removal - only on visible pixels
45+ visible = alpha > 128 # Only strong pixels
46+ has_green_tint = (g > r + 20 ) & (g > b + 20 )
47+ green_tinted = visible & has_green_tint
6648
6749 if np .any (green_tinted ):
68- # Reduce green channel more aggressively
6950 avg_rb = (r [green_tinted ] + b [green_tinted ]) / 2
70- # Take whichever is lower: 30% of original green, or average of R&B
71- g [green_tinted ] = np .minimum (g [green_tinted ] * 0.3 , avg_rb )
72-
73- # Step 3: Make any remaining greenish pixels more transparent
74- still_greenish = (alpha > 0 ) & (g > r + 5 ) & (g > b + 5 )
75- alpha [still_greenish ] *= 0.5 # Make them much more transparent
76-
77- # Step 4: Completely remove very faint greenish pixels
78- very_faint_green = (alpha > 0 ) & (alpha < 150 ) & (g > np .maximum (r , b ))
79- alpha [very_faint_green ] = 0
51+ g [green_tinted ] = np .minimum (g [green_tinted ] * 0.6 , avg_rb ) # Less aggressive
8052
8153 data [:, :, 0 ] = r
8254 data [:, :, 1 ] = g
@@ -121,19 +93,23 @@ def invert_colors(img: Image.Image) -> Image.Image:
12193async def generate_logo (
12294 project_name : str , suggestion : str | None = None , output_dir : Path | None = None
12395) -> dict [str , Image .Image ]:
124- """Generate logo assets using the new pipeline:
125- 1. Generate light mode wordmark
126- 2. Reduce color variance
127- 3. Invert colors for dark mode
128- 4. Generate square icons for both light and dark modes
96+ """Generate logo assets using AI-powered pipeline with consistent branding:
97+ 1. Generate light mode wordmark with greenscreen
98+ 2. Extract icon from wordmark (removes text, keeps icon)
99+ 3. Remove greenscreen from both wordmark and icon
100+ 4. Invert colors for dark mode wordmark
101+ 5. Invert colors for dark mode icon
102+ 6. Save all assets including favicon
103+
104+ This ensures the icon in the wordmark matches the standalone icon perfectly.
129105
130106 Args:
131107 project_name: Name of the project
132108 suggestion: Optional suggestion to guide the logo generation
133109 output_dir: Output directory for the generated images. Defaults to docs/public/
134110
135111 Returns:
136- Dictionary of generated images
112+ Dictionary of generated images (wordmark_light, wordmark_dark, icon_light, icon_dark, favicon)
137113 """
138114 # Determine output directory
139115 if output_dir is None :
@@ -175,106 +151,70 @@ async def generate_logo(
175151 if light_img is None :
176152 raise ValueError ("No light mode wordmark generated" )
177153
178- print ("Removing greenscreen..." )
179- light_img = remove_greenscreen (light_img )
180-
181- # ============================================================
182- # 2. Reduce color variance
183- # ============================================================
184- print ("\n === Step 2: Reducing Color Variance ===" )
185- print ("Quantizing colors to reduce variance..." )
186- light_img = reduce_color_variance (light_img , colors = 6 )
187-
188- light_path = output_dir / "logo-light.png"
189- light_img .save (light_path )
190- print (f"✓ Light mode wordmark saved to: { light_path } " )
191- results ["wordmark_light" ] = light_img
192-
193154 # ============================================================
194- # 3. Generate dark mode by inverting colors
155+ # 2. Extract icon from wordmark (before greenscreen removal)
195156 # ============================================================
196- print ("\n === Step 3: Generating Dark Mode (Invert Colors) ===" )
197- print ("Inverting colors from light mode..." )
198- dark_img = invert_colors (light_img )
157+ print ("\n === Step 2: Extracting Square Icon from Wordmark ===" )
158+ print ("Asking AI to remove text and preserve only the icon..." )
199159
200- dark_path = output_dir / "logo-dark.png"
201- dark_img .save (dark_path )
202- print (f"✓ Dark mode wordmark saved to: { dark_path } " )
203- results ["wordmark_dark" ] = dark_img
204-
205- # ============================================================
206- # 4. Generate square icons for light mode
207- # ============================================================
208- print ("\n === Step 4: Generating Square Icon (Light Mode) ===" )
209- icon_inf = DSPYInference (pred_signature = IconDescription , observe = False )
210- icon_light_result = await icon_inf .run (
211- project_name = project_name ,
212- suggestion = suggestion or "" ,
213- is_dark_mode = False ,
214- )
160+ icon_extract_prompt = f"Remove ALL TEXT from this image. Keep ONLY the icon/symbol on the left side. Output a SQUARE 1:1 aspect ratio image with the icon centered. Preserve the BRIGHT LIME GREEN (#00FF00) GREENSCREEN background exactly as it is. Do not change any colors of the icon itself - keep them identical to the original. Just remove the text '{ project_name } ' and center the icon in a square format."
215161
216- print (f"Light icon description: { icon_light_result .icon_description } " )
217-
218- icon_light_prompt = f"{ icon_light_result .icon_description } . Create a SQUARE 1:1 aspect ratio icon/symbol. NO TEXT should appear. Use DARK colors (black, dark gray, dark blue, etc.) suitable for light backgrounds. The icon should be bold, simple, and instantly recognizable. Center it with minimal padding. CRITICAL: Use a BRIGHT LIME GREEN (#00FF00) GREENSCREEN background. Do not use lime green in the icon itself."
219-
220- print ("Generating light mode icon with Gemini..." )
221- icon_light_resp = client .models .generate_content (
162+ print ("Generating square icon by extracting from wordmark..." )
163+ icon_extract_resp = client .models .generate_content (
222164 model = "gemini-3-pro-image-preview" ,
223- contents = [icon_light_prompt ],
165+ contents = [icon_extract_prompt , light_img ],
224166 config = types .GenerateContentConfig (response_modalities = ["TEXT" , "IMAGE" ]),
225167 )
226168
227169 icon_light_img = None
228- for part in icon_light_resp .candidates [0 ].content .parts : # type: ignore
170+ for part in icon_extract_resp .candidates [0 ].content .parts : # type: ignore
229171 if part .inline_data and part .inline_data .mime_type .startswith ("image/" ): # type: ignore
230172 icon_light_img = Image .open (BytesIO (part .inline_data .data )) # type: ignore
231173 break
232174
233175 if icon_light_img is None :
234- raise ValueError ("No light mode icon generated " )
176+ raise ValueError ("No light mode icon extracted " )
235177
236- print ("Removing greenscreen from light icon..." )
178+ print ("Removing greenscreen from extracted icon..." )
237179 icon_light_img = remove_greenscreen (icon_light_img )
238180
239181 # ============================================================
240- # 5. Generate square icons for dark mode
182+ # 3. Remove greenscreen from wordmark
241183 # ============================================================
242- print ("\n === Step 5: Generating Square Icon (Dark Mode) ===" )
243- icon_dark_result = await icon_inf .run (
244- project_name = project_name ,
245- suggestion = suggestion or "" ,
246- is_dark_mode = True ,
247- )
248-
249- print (f"Dark icon description: { icon_dark_result .icon_description } " )
250-
251- icon_dark_prompt = f"{ icon_dark_result .icon_description } . Create a SQUARE 1:1 aspect ratio icon/symbol. NO TEXT should appear. Use LIGHT colors (white, light gray, light cyan, etc.) suitable for dark backgrounds. The icon should be bold, simple, and instantly recognizable. Center it with minimal padding. CRITICAL: Use a BRIGHT LIME GREEN (#00FF00) GREENSCREEN background. Do not use lime green in the icon itself."
184+ print ("\n === Step 3: Removing Greenscreen from Wordmark ===" )
185+ print ("Removing greenscreen..." )
186+ light_img = remove_greenscreen (light_img )
252187
253- print ("Generating dark mode icon with Gemini..." )
254- icon_dark_resp = client .models .generate_content (
255- model = "gemini-3-pro-image-preview" ,
256- contents = [icon_dark_prompt ],
257- config = types .GenerateContentConfig (response_modalities = ["TEXT" , "IMAGE" ]),
258- )
188+ light_path = output_dir / "logo-light.png"
189+ light_img .save (light_path )
190+ print (f"✓ Light mode wordmark saved to: { light_path } " )
191+ results ["wordmark_light" ] = light_img
259192
260- icon_dark_img = None
261- for part in icon_dark_resp .candidates [0 ].content .parts : # type: ignore
262- if part .inline_data and part .inline_data .mime_type .startswith ("image/" ): # type: ignore
263- icon_dark_img = Image .open (BytesIO (part .inline_data .data )) # type: ignore
264- break
193+ # ============================================================
194+ # 4. Generate dark mode by inverting colors
195+ # ============================================================
196+ print ("\n === Step 4: Generating Dark Mode (Invert Colors) ===" )
197+ print ("Inverting colors from light mode for wordmark..." )
198+ dark_img = invert_colors (light_img )
265199
266- if icon_dark_img is None :
267- raise ValueError ("No dark mode icon generated" )
200+ dark_path = output_dir / "logo-dark.png"
201+ dark_img .save (dark_path )
202+ print (f"✓ Dark mode wordmark saved to: { dark_path } " )
203+ results ["wordmark_dark" ] = dark_img
268204
269- print ("Removing greenscreen from dark icon..." )
270- icon_dark_img = remove_greenscreen (icon_dark_img )
205+ # ============================================================
206+ # 5. Generate dark mode icon by inverting
207+ # ============================================================
208+ print ("\n === Step 5: Inverting Icon for Dark Mode ===" )
209+ print ("Inverting colors from light mode icon..." )
210+ icon_dark_img = invert_colors (icon_light_img )
271211
272212 # ============================================================
273213 # 6. Save icon versions (use light mode icon for favicon)
274214 # ============================================================
275215 print ("\n === Saving Icon Versions ===" )
276216
277- # Ensure icon is square
217+ # Ensure light icon is square
278218 width , height = icon_light_img .size
279219 if width != height :
280220 size = max (width , height )
@@ -284,22 +224,36 @@ async def generate_logo(
284224 new_icon .paste (icon_light_img , (paste_x , paste_y ))
285225 icon_light_img = new_icon
286226
287- # Generate favicon sizes
227+ # Ensure dark icon is square
228+ width , height = icon_dark_img .size
229+ if width != height :
230+ size = max (width , height )
231+ new_icon = Image .new ("RGBA" , (size , size ), (255 , 255 , 255 , 0 ))
232+ paste_x = (size - width ) // 2
233+ paste_y = (size - height ) // 2
234+ new_icon .paste (icon_dark_img , (paste_x , paste_y ))
235+ icon_dark_img = new_icon
236+
237+ # Generate favicon and icon sizes
288238 favicon_32 = icon_light_img .resize ((32 , 32 ), Image .Resampling .LANCZOS )
289- icon_512 = icon_light_img .resize ((512 , 512 ), Image .Resampling .LANCZOS )
239+ icon_light_512 = icon_light_img .resize ((512 , 512 ), Image .Resampling .LANCZOS )
240+ icon_dark_512 = icon_dark_img .resize ((512 , 512 ), Image .Resampling .LANCZOS )
290241
291242 # Save icon versions
292- icon_path = output_dir / "icon.png"
243+ icon_light_path = output_dir / "icon-light.png"
244+ icon_dark_path = output_dir / "icon-dark.png"
293245 favicon_path = output_dir / "favicon.ico"
294246
295- icon_512 .save (icon_path )
247+ icon_light_512 .save (icon_light_path )
248+ icon_dark_512 .save (icon_dark_path )
296249 favicon_32 .save (favicon_path , format = "ICO" )
297250
298- print (f"✓ Icon saved to: { icon_path } " )
251+ print (f"✓ Light icon saved to: { icon_light_path } " )
252+ print (f"✓ Dark icon saved to: { icon_dark_path } " )
299253 print (f"✓ Favicon saved to: { favicon_path } " )
300254
301- results ["icon_light" ] = icon_light_img
302- results ["icon_dark" ] = icon_dark_img
255+ results ["icon_light" ] = icon_light_512
256+ results ["icon_dark" ] = icon_dark_512
303257 results ["favicon" ] = favicon_32
304258
305259 print ("\n === All assets generated successfully! ===" )
0 commit comments