import gradio as gr import tempfile import time import os from huggingface_hub import InferenceClient # ── Constants ───────────────────────────────────────────────────────────────── FLUX_MODEL = "black-forest-labs/FLUX.1-schnell" # ── Load token from HF Space secret ────────────────────────────────────────── HF_TOKEN = os.environ.get("HF_TOKEN", "") SIZE_OPTIONS = { "1:1 Square (1024×1024)": (1024, 1024), "4:3 Landscape (1152×896)": (1152, 896), "3:4 Portrait (896×1152)": (896, 1152), "16:9 Wide (1344×768)": (1344, 768), "9:16 Tall (768×1344)": (768, 1344), } STYLE_PRESETS = { "👨🏻🍳 Fine Dining": "Michelin-star restaurant editorial plating, pristine white bone-china plate, " "dramatic chiaroscuro side-lighting, 85mm portrait lens shallow depth of field, " "microgreens and edible flowers garnish, polished dark marble surface, " "deep shadow fill, luxury fine-dining atmosphere, hyper-sharp food detail", "🕯️ Moody & Dark": "dark moody atmospheric food photography, aged oak table, single warm candlelight source, " "deep chestnut and obsidian tones, heavy shadow drama, rich jewel-toned background, " "wisps of steam rising, cinematic Rembrandt lighting, editorial gourmet aesthetic, " "50mm f/1.4 lens, vignette edges, brooding and indulgent mood", "☀️ Bright & Airy": "bright airy lifestyle food photography, white Carrara marble countertop, " "soft diffused morning window light from the left, pale pastel linen napkin, " "fresh botanicals and citrus accents, overexposed highlights, " "clean white negative space, 35mm lens, cheerful and fresh Instagram aesthetic, " "light and breezy Scandinavian mood", "🌿 Fresh & Minimal": "minimalist zen food photography, light grey concrete slab surface, " "soft natural north-facing diffused light, generous intentional negative space, " "single hero ingredient as focal point, dewy fresh herbs accent, " "ultra-sharp macro detail on textures, muted earthy neutral palette, " "Japanese wabi-sabi aesthetic, 100mm macro lens, contemplative and clean", "🏡 Rustic Farmhouse": "rustic farmhouse food photography, reclaimed weathered wood table, " "warm golden-hour side light through linen curtains, linen and burlap napkins, " "cast iron skillet, terracotta ceramics, wildflower sprig accent, " "organic imperfect textures, amber and sienna warm tones, " "nostalgic cozy harvest atmosphere, 50mm lens natural light", "📸 Overhead Flat Lay": "overhead 90-degree flat lay food photography, pure white marble surface, " "soft even studio lighting, zero shadows, symmetrical hero composition, " "food styled with complementary props: linen napkin, vintage cutlery, small ceramic bowl, " "negative space for text, sharp corner-to-corner focus, " "clean editorial top-down perspective, shot on Hasselblad, commercial menu photography", "🔥 BBQ & Smoke": "smoky outdoor BBQ grill food photography, charred cast iron grates, " "dramatic ember glow from below, wisps of smoke curling upward, " "charred grill marks hero detail, rustic weathered picnic wood table, " "warm fiery amber and sienna tones, condensation on nearby cold glass, " "summer cookout atmosphere, golden-hour back light, 50mm lens wide aperture", "🍰 Pastel Patisserie": "French patisserie editorial food photography, soft sage or blush pink background, " "matte white ceramic plate, diffused overcast studio lighting, " "powdered sugar dusting detail, glossy mirror glaze reflection, " "delicate edible flowers and gold leaf accents, " "pastel macarons or petit fours as props, 85mm macro lens, " "dreamy soft-focus bokeh, elegant Parisian confectionery mood", } EXAMPLES = [ # ── 1. Fine Dining ──────────────────────────────────────────────────────── [ "Wagyu beef tenderloin, black truffle, parsnip purée, baby spinach, " "red wine jus, microgreens, edible gold leaf, fleur de sel", "Perfectly seared Wagyu tenderloin medallion resting on a silky parsnip purée, " "crowned with paper-thin black truffle slices and edible gold leaf, " "a glossy red wine jus pooled artfully beside it, finished with microgreens " "and a pinch of fleur de sel on a pristine white bone-china plate.", "👨🏻🍳 Fine Dining", "1:1 Square (1024×1024)", ], # ── 2. Moody & Dark ─────────────────────────────────────────────────────── [ "Beef short ribs, red wine, bone broth, garlic, thyme, rosemary, " "creamy mashed potatoes, crispy shallots, fresh horseradish", "Slow-braised beef short rib collapsed off the bone, lacquered in a deep " "garnet red wine reduction, served over ivory mashed potato, " "scattered with crispy golden shallots and a fresh horseradish snow, " "steam rising from the plate into candlelit shadow.", "🕯️ Moody & Dark", "4:3 Landscape (1152×896)", ], # ── 3. Bright & Airy ────────────────────────────────────────────────────── [ "Açaí, frozen banana, almond milk, granola, fresh strawberries, " "blueberries, kiwi, mango, coconut flakes, honey, chia seeds", "Vibrant deep-purple açaí smoothie bowl topped with neat rows of " "sliced golden mango, fresh kiwi, juicy strawberries and blueberries, " "a sprinkle of toasted granola and coconut flakes, finished with a " "honey drizzle — served in a white ceramic bowl on a sunlit marble counter.", "☀️ Bright & Airy", "1:1 Square (1024×1024)", ], # ── 4. Fresh & Minimal ──────────────────────────────────────────────────── [ "Sashimi-grade salmon, Japanese cucumber, avocado, pickled daikon, " "yuzu juice, toasted sesame seeds, shiso leaf, soy sauce, wasabi", "Three pristine salmon sashimi slices fanned over a paper-thin cucumber ribbon, " "a halved avocado rose alongside, single shiso leaf accent, " "yuzu drops scattered across the surface, sesame seeds dusted lightly — " "composed with precise negative space on a cool light-grey concrete slab.", "🌿 Fresh & Minimal", "4:3 Landscape (1152×896)", ], # ── 5. Rustic Farmhouse ─────────────────────────────────────────────────── [ "Whole roasted chicken, root vegetables, carrots, parsnips, " "turnips, fresh thyme, rosemary, garlic bulb, chicken gravy, sea salt", "Golden whole roasted chicken glistening in herb-infused pan drippings, " "surrounded by caramelised carrots, parsnips and turnips in a well-seasoned " "cast-iron roasting dish, a split roasted garlic bulb alongside, " "fresh thyme and rosemary scattered over the top, rich gravy pooling around the base.", "🏡 Rustic Farmhouse", "4:3 Landscape (1152×896)", ], # ── 6. Overhead Flat Lay ────────────────────────────────────────────────── [ "Hummus, warm pita bread, kalamata olives, roasted red peppers, " "crumbled feta, tabbouleh, stuffed grape leaves, za'atar, olive oil, lemon", "Abundant Mediterranean mezze spread photographed directly overhead — " "a large bowl of creamy hummus with olive oil pool at the centre, " "surrounded by golden warm pita wedges, glossy olives, roasted peppers, " "crumbled feta, vibrant tabbouleh and stuffed grape leaves, " "the whole spread filling the frame edge to edge on white marble.", "📸 Overhead Flat Lay", "4:3 Landscape (1152×896)", ], # ── 7. BBQ & Smoke ──────────────────────────────────────────────────────── [ "Beef brisket, dry rub spices, smoked paprika, brown sugar, garlic, " "apple cider vinegar, BBQ sauce, coleslaw, pickles, brioche bun", "Thick-sliced smoked beef brisket with a deep mahogany bark crust, " "bark cracking open to reveal a pink smoke ring and tender pull-apart interior, " "stacked beside creamy coleslaw and tangy pickles on butcher paper, " "a ramekin of glossy BBQ sauce alongside, ember glow lighting the scene from behind.", "🔥 BBQ & Smoke", "4:3 Landscape (1152×896)", ], # ── 8. Pastel Patisserie ────────────────────────────────────────────────── [ "French macarons, almond flour, egg whites, sugar, raspberry jam, " "rose buttercream, lavender, vanilla, edible dried rose petals, gold dust", "A delicate tower of pastel French macarons — blush rose, " "soft lavender and vanilla cream — stacked on a white ceramic cake stand, " "edible dried rose petals scattered at the base, a dusting of gold " "catching the soft diffused studio light, a single macaron cracked open " "revealing the thick rose buttercream centre on a sage-green backdrop.", "🍰 Pastel Patisserie", "1:1 Square (1024×1024)", ], ] # ── Core generation ─────────────────────────────────────────────────────────── def generate_dish_image(ingredients, description, style_label, size_label, steps, guidance): if not HF_TOKEN: return None, "❌ HF_TOKEN secret is not set. Add it in Space Settings → Variables and secrets." if not ingredients.strip(): return None, "❌ Please list at least one ingredient." if not description.strip(): return None, "❌ Please describe the dish." width, height = SIZE_OPTIONS.get(size_label, (1024, 1024)) style_suffix = STYLE_PRESETS.get(style_label, "") prompt = ( f"Professional food photography of: {description.strip()}. " f"Ingredients: {ingredients.strip()}. " f"{style_suffix}. " f"Mouth-watering, ultra-detailed, award-winning culinary photography, 8K resolution." ) try: t0 = time.time() client = InferenceClient( provider="hf-inference", api_key=HF_TOKEN, ) image = client.text_to_image( prompt, model=FLUX_MODEL, width=width, height=height, num_inference_steps=int(steps), guidance_scale=float(guidance), ) elapsed = time.time() - t0 tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png") image.save(tmp.name) tmp.close() return tmp.name, ( f"✅ Generated in {elapsed:.1f}s · FLUX.1-schnell · " f"{width}×{height} · {int(steps)} steps · FREE 🎉" ) except Exception as exc: err = str(exc) if "401" in err or "unauthorized" in err.lower(): return None, "❌ Invalid token. Check your HF_TOKEN secret." if "403" in err or "forbidden" in err.lower(): return None, ( "❌ 403 Forbidden — enable 'Make calls to Inference Providers' " "permission on your token at huggingface.co/settings/tokens" ) if "429" in err: return None, "❌ Rate limit hit. Wait a moment then try again." return None, f"❌ Error: {err[:300]}" # ── CSS ─────────────────────────────────────────────────────────────────────── css = """ @import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;0,700;1,400&family=Outfit:wght@300;400;500;600;700&display=swap'); *, *::before, *::after { box-sizing: border-box; } body, .gradio-container { font-family: 'Outfit', sans-serif !important; background: #080706 !important; color: #e8e0d5 !important; } /* ── Hero ── */ .hero { text-align: center; padding: 52px 32px 40px; margin-bottom: 4px; border-radius: 20px; background: linear-gradient(160deg, #625b28 0%, #1a160e 40%, #423215 100%) !important; border: 1px solid rgba(200, 150, 60, 0.18); box-shadow: 0 24px 64px rgba(0,0,0,0.6); position: relative; overflow: hidden; } .hero::before { content: ''; position: absolute; top: -80px; left: 50%; transform: translateX(-50%); width: 500px; height: 200px; background: radial-gradient(ellipse, rgba(220,160,60,0.14) 0%, transparent 65%); pointer-events: none; } .hero-eyebrow { font-size: 0.72rem; font-weight: 600; letter-spacing: 0.22em; text-transform: uppercase; color: #c8933a !important; margin-bottom: 14px; } .hero h1 { font-family: 'Cormorant Garamond', serif !important; font-size: clamp(2.4rem, 5vw, 3.8rem) !important; font-weight: 600 !important; color: #f2e8d8 !important; line-height: 1.1 !important; margin-bottom: 12px !important; } .hero h1 em { font-style: italic; color: #d4974a !important; } .hero-sub { font-size: 0.95rem; color: #9a8e7e !important; font-weight: 300; } .hero-badges { display: flex; justify-content: center; gap: 10px; flex-wrap: wrap; margin-top: 20px; } .badge { display: inline-flex; align-items: center; gap: 6px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.09); border-radius: 999px; padding: 5px 14px; font-size: 0.74rem; font-weight: 500; color: #9e8e7a !important; } .badge.green { border-color: rgba(95,207,122,0.3); color: #5fcf7a; background: rgba(95,207,122,0.07); } /* ── Cards ── */ .card { background: #0f0d0a; border: 1px solid rgba(200,150,60,0.13); border-radius: 18px; padding: 24px 22px; box-shadow: 0 8px 32px rgba(0,0,0,0.4); } .card-title { margin-top: 10px !important; margin-left: 5px !important; font-family: 'Cormorant Garamond', serif !important; font-size: 1.2rem !important; font-weight: 700 !important; color: rgb(29 168 62) !important; border-bottom: 1px solid rgba(200,150,60,0.18) !important; padding-bottom: 12px !important; display: flex !important; align-items: center !important; gap: 8px !important; } /* ── Field labels ── */ label span, .gr-form label span { font-family: 'Outfit', sans-serif !important; font-size: 0.8rem !important; font-weight: 600 !important; letter-spacing: 0.1em !important; text-transform: uppercase !important; color: #8c6922 !important; } /* ── Inputs ── */ textarea, input[type="text"], input[type="password"], input[type="number"] { background: #D4974A !important; border: 1px solid rgba(200,150,60,0.15) !important; border-radius: 12px !important; color: #ede5d8 !important; font-family: 'Outfit', sans-serif !important; font-size: 0.94rem !important; font-weight: 400 !important; transition: border-color 0.2s, box-shadow 0.2s !important; } textarea:focus, input:focus { border-color: rgba(200,150,60,0.45) !important; box-shadow: 0 0 0 3px rgba(200,150,60,0.08) !important; } textarea::placeholder, input::placeholder { color: #4a4038 !important; font-style: italic !important; } input[type="range"] { accent-color: #c8933a !important; } /* ── Setting explainer boxes ── */ .setting-explainer { background: rgba(200,150,60,0.06); border: 1px solid rgba(200,150,60,0.14); border-radius: 10px; padding: 12px 14px; margin-top: 10px; margin-bottom: 4px; } .setting-explainer .label { font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; color: #996f29; margin-bottom: 4px; } .setting-explainer .text { font-size: 0.84rem; color: #b0a090; line-height: 1.55; font-weight: 300; color: black !important; } .setting-explainer .text strong { color: #8c6922; font-weight: 600; } /* ── Two-slider row inside full-width settings card ── */ .sliders-row { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; align-items: start; } .slider-block { display: flex; flex-direction: column; } /* ── Generate button ── */ .gen-btn { background: linear-gradient(135deg, #b8832a 0%, #e8bb5a 45%, #c8902e 100%) !important; border: none !important; border-radius: 14px !important; color: #0a0805 !important; font-family: 'Outfit', sans-serif !important; font-size: 1rem !important; font-weight: 700 !important; letter-spacing: 0.07em !important; text-transform: uppercase !important; padding: 17px 0 !important; width: 100% !important; cursor: pointer !important; transition: transform 0.15s, box-shadow 0.25s !important; box-shadow: 0 4px 24px rgba(200,150,60,0.3) !important; margin-top: 6px !important; } .gen-btn:hover { transform: translateY(-2px) !important; box-shadow: 0 8px 36px rgba(200,150,60,0.42) !important; } .gen-btn:active { transform: translateY(0) !important; } /* ── Image output ── */ .img-out img { border-radius: 14px !important; border: 1px solid rgba(200,150,60,0.18) !important; width: 100% !important; object-fit: cover !important; box-shadow: 0 16px 48px rgba(0,0,0,0.6) !important; } .status-out textarea { font-size: 0.88rem !important; color: #d4a650 !important; background: #0a0806 !important; border-color: rgba(200,150,60,0.12) !important; font-weight: 500 !important; } /* ── Info bar ── */ .info-bar { background: #0c0a08; border: 1px solid rgba(200,150,60,0.12); border-radius: 14px; padding: 16px 22px; display: flex; gap: 22px; flex-wrap: wrap; align-items: center; } .info-item { font-size: 0.82rem; color: white !important; } .info-item strong { color: #b8a880 !important; font-weight: 600; } .info-item.free strong { color: #5fcf7a !important; } /* ── Examples table ── */ .examples-label { font-size: 0.76rem; font-weight: 700; letter-spacing: 0.15em; text-transform: uppercase; color: #d4a650 !important; margin: 0 0 8px 2px; } .gr-examples, [id*="examples"], .svelte-examples, div[class*="examples"] { background: #131008 !important; border: 1px solid rgba(200,150,60,0.2) !important; border-radius: 14px !important; overflow: hidden !important; padding: 0 !important; } details > summary, .gr-examples summary, div[class*="examples"] summary { color: #c8933a !important; font-family: 'Outfit', sans-serif !important; font-size: 0.82rem !important; font-weight: 600 !important; letter-spacing: 0.06em !important; padding: 10px 16px !important; background: rgba(200,150,60,0.06) !important; border-bottom: 1px solid rgba(200,150,60,0.12) !important; cursor: pointer !important; list-style: none !important; } table, .gr-samples-table, div[class*="examples"] table { background: #131008 !important; border-collapse: collapse !important; width: 100% !important; } thead, thead tr, .gr-samples-table thead, .gr-samples-table thead tr { background: #1e1a10 !important; } thead th, .gr-samples-table thead th, div[class*="examples"] thead th { color: #d4a650 !important; font-family: 'Outfit', sans-serif !important; font-size: 0.78rem !important; font-weight: 700 !important; text-transform: uppercase !important; letter-spacing: 0.1em !important; padding: 12px 16px !important; border-bottom: 1px solid rgba(200,150,60,0.2) !important; background: #1e1a10 !important; } tbody tr, .gr-samples-table tbody tr, div[class*="examples"] tbody tr { background: #131008 !important; border-bottom: 1px solid rgba(200,150,60,0.1) !important; transition: background 0.15s !important; cursor: pointer !important; } tbody tr:nth-child(even), .gr-samples-table tbody tr:nth-child(even) { background: #171209 !important; } tbody tr:hover, .gr-samples-table tbody tr:hover, div[class*="examples"] tbody tr:hover { background: #252010 !important; } tbody td, .gr-samples-table td, div[class*="examples"] tbody td, div[class*="examples"] td { color: #e0d4c0 !important; font-family: 'Outfit', sans-serif !important; font-size: 0.86rem !important; font-weight: 400 !important; padding: 12px 16px !important; line-height: 1.4 !important; vertical-align: middle !important; } tbody td p, tbody td span, tbody td * { color: #e0d4c0 !important; font-size: 0.86rem !important; } tbody td button, div[class*="examples"] td button { color: #e0d4c0 !important; background: transparent !important; border: none !important; font-family: 'Outfit', sans-serif !important; font-size: 0.86rem !important; text-align: left !important; padding: 0 !important; cursor: pointer !important; width: 100% !important; } tbody td button:hover { color: #f0c860 !important; } /* ── Scrollbar ── */ ::-webkit-scrollbar { width: 5px; } ::-webkit-scrollbar-track { background: #0a0806; } ::-webkit-scrollbar-thumb { background: #4a3a28; border-radius: 999px; } ::-webkit-scrollbar-thumb:hover { background: #c8933a; } """ # ── Build UI ────────────────────────────────────────────────────────────────── with gr.Blocks(title="Dish Image Generator · FLUX.1-schnell", css=css) as demo: # ── Hero ───────────────────────────────────────────────────────────────── gr.HTML("""
Powered by FLUX.1-schnell via Hugging Face InferenceClient · Completely FREE · Apache 2.0 licensed