import os import io import base64 import numpy as np import gradio as gr import tempfile from dotenv import load_dotenv from huggingface_hub import InferenceClient from PIL import Image, ImageDraw, ImageFilter, ImageFont load_dotenv() api_key = os.getenv("HF_API_TOKEN") client = InferenceClient(api_key=api_key) # Model # IMAGE_MODEL = "black-forest-labs/FLUX.1-dev" IMAGE_MODEL = "black-forest-labs/FLUX.1-schnell" # faster, less quality ODOO_COLORS = { "Odoo Purple #714B67": {"hex": "#714B67", "name": "muted mauve purple"}, "Odoo Teal #1AD3BB": {"hex": "#1AD3BB", "name": "bright teal cyan"}, "Sales Red #E84646": {"hex": "#E84646", "name": "vivid coral red"}, "Purchase Blue #3A97D4": {"hex": "#3A97D4", "name": "medium sky blue"}, "HR Green #1BBC9B": {"hex": "#1BBC9B", "name": "fresh emerald green"}, "Finance Gold #F0A500": {"hex": "#F0A500", "name": "warm amber gold"}, "Inventory #00A09D": {"hex": "#00A09D", "name": "dark teal green"}, "Project Blue #4C6EF5": {"hex": "#4C6EF5", "name": "bright indigo blue"}, "Customβ¦": {"hex": "custom", "name": "custom"}, } ODOO_MODULE_EXAMPLES = { "Sales": "shopping cart, white glyph, bold minimal lines", "Purchase": "shopping bag with arrow, clean outline", "Inventory": "warehouse shelves, flat geometric", "Accounting": "coins stack or ledger book", "HR": "person silhouette with plus sign", "Project": "kanban columns or gantt bars", "CRM": "speech bubble with handshake", "Manufacturing": "gear cog with wrench", "Website": "globe with cursor", "Email Marketing": "envelope with lightning bolt", "Helpdesk": "headset or life-ring", "Fleet": "car front view, minimal", "Timesheets": "clock with checkmark", "Expenses": "receipt paper with dollar sign", "Sign": "pen on document", } def make_transparent(img: Image.Image) -> Image.Image: img = img.convert("RGBA") data = np.array(img) white_mask = ( (data[:, :, 0] > 245) & (data[:, :, 1] > 245) & (data[:, :, 2] > 245) ) data[white_mask, 3] = 0 return Image.fromarray(data) def add_rounded_corners(img: Image.Image, radius: int = 60) -> Image.Image: img = img.convert("RGBA") mask = Image.new("L", img.size, 0) draw = ImageDraw.Draw(mask) draw.rounded_rectangle([0, 0, img.size[0], img.size[1]], radius=radius, fill=255) img.putalpha(mask) return img def flatten_on_white(img: Image.Image) -> Image.Image: img = img.convert("RGBA") white_bg = Image.new("RGBA", img.size, (255, 255, 255, 255)) return Image.alpha_composite(white_bg, img).convert("RGB") def hex_to_rgb(hex_color: str) -> tuple: h = hex_color.lstrip("#") return tuple(int(h[i:i+2], 16) for i in (0, 2, 4)) def add_rounded_corners(img: Image.Image, radius: int = 60) -> Image.Image: """Apply rounded corners with transparency (Odoo uses ~14px radius on 140px icons).""" img = img.convert("RGBA") mask = Image.new("L", img.size, 0) draw = ImageDraw.Draw(mask) draw.rounded_rectangle([0, 0, img.size[0], img.size[1]], radius=radius, fill=255) img.putalpha(mask) return img def description_to_visual_elements(description: str) -> str: """ Manually map module concepts to concrete visual primitives. This is the key step your current prompt skips entirely. """ concept_map = { "invoice": "document sheet with folded corner", "purchase": "shopping cart or package box", "received": "downward arrow entering a box", "quantity": "stacked layers or count marks", "validation": "checkmark shield or lock", "3-way matching": "three overlapping circles or triangles meeting at center", "vendor bill": "stamped receipt", "stock": "warehouse shelf or stacked boxes", "billing": "paper with currency symbol", "prevent": "crossed-out shape or barrier", } found = [] desc_lower = description.lower() for keyword, visual in concept_map.items(): if keyword in desc_lower: found.append(visual) return "; ".join(found[:3]) def build_prompt( description: str, primary_color_name: str, style_hint: str, reference_module: str, ): visual_elements = description_to_visual_elements(description) if not visual_elements: visual_elements = description module_anchor = "" if reference_module and reference_module not in ("", "None"): module_anchor = f"Inspired by the visual language of the Odoo {reference_module} module icon." prompt = f""" Odoo 18 enterprise application icon on pure white background. Flat vector icon depicting: {visual_elements}. Icon composition: Two or three geometric shapes arranged to suggest the concept above. Shapes overlap or nest to create visual hierarchy. Negative space used deliberately as part of the design. Rounded corners on all shapes (8β12px radius equivalent). Color: Primary fill: {primary_color_name}. One or two complementary accent colors from the same tonal family. No gradients. Flat fills only. White used as internal cutout or separator. Style: Clean enterprise SaaS icon. Same visual weight and abstraction level as Google Workspace or Notion icons. Pixel-perfect at 128Γ128. {module_anchor} {style_hint} Output format: SVG-style vector graphic. Transparent background (no square frame, no rounded app tile, no drop shadow). No text, letters, numbers, or labels anywhere in the image. No photorealism. No illustrations. No decorative borders. """.strip() return " ".join(prompt.split()) def generate_icon( description: str, color_choice: str, custom_hex: str, style_hint: str, reference_module: str, reference_image, num_variants: int, apply_rounded: bool, ): if not description.strip(): raise gr.Error("Please describe what the icon should represent.") color_data = ODOO_COLORS.get(color_choice, {"hex": "#714B67", "name": "muted mauve purple"}) bg_hex = color_data["hex"] bg_name = color_data["name"] if bg_hex == "custom": raw = custom_hex.strip() bg_hex = raw if raw.startswith("#") else f"#{raw}" bg_name = "custom color" prompt = build_prompt( description, bg_name, style_hint, reference_module, ) if reference_image is not None: prompt += " Closely match the composition style of the uploaded reference icon." images = [] png_files = [] status_messages = [f"π Prompt: {prompt[:120]}β¦\n"] for i in range(int(1)): try: result = client.text_to_image( prompt=prompt, model=IMAGE_MODEL, width=512, height=512, ) result = make_transparent(result) result = result.resize((140, 140), Image.LANCZOS) if apply_rounded: result = add_rounded_corners(result, radius=40) images.append(result) tmp = tempfile.NamedTemporaryFile( suffix=".png", delete=False ) result.save(tmp.name, format="PNG") png_files.append(tmp.name) status_messages.append( f"β Variant {i+1} generated successfully" ) except Exception as e: status_messages.append(f"β Variant {i+1} failed: {str(e)}") images.append(None) while len(images) < 3: images.append(None) while len(png_files) < 3: png_files.append(None) status = "\n".join(status_messages) return ( images[0], png_files[0], prompt, status, ) CSS = """ .header-box { background: linear-gradient(135deg, #714B67 0%, #9B6B8F 100%); border-radius: 14px; padding: 22px 24px; margin-bottom: 18px; } .header-box h1 { color: white; margin: 0 0 4px; font-size: 1.65em; } .header-box p { color: #e8d5e3; margin: 0; font-size: 0.88em; } .section-label { font-weight: 600; font-size: 0.85em; text-transform: uppercase; letter-spacing: 0.06em; color: #714B67; margin: 14px 0 4px; } .prompt-box textarea { font-family: monospace; font-size: 0.8em; background: #f8f5f7 !important; } .fix-badge { background: #f0fdf4; border: 1px solid #86efac; border-radius: 8px; padding: 8px 12px; font-size: 0.82em; color: #166534; } """ theme = gr.themes.Soft(primary_hue="purple") with gr.Blocks( title="Odoo Icon Generator", ) as demo: gr.HTML("""
Generate flat material-design app icons using FLUX.1-dev Β· White glyph Β· Solid brand color Β· Odoo style