import base64 import io import os import gradio as gr from PIL import Image from app import analyze, FORMATS # ---------- Assets ---------- def _load_asset_b64(filename: str, max_px: int | None = None) -> str: path = os.path.join(os.path.dirname(__file__), "assets", filename) if max_px: img = Image.open(path) if max(img.size) > max_px: img.thumbnail((max_px, max_px)) buf = io.BytesIO() fmt = "JPEG" if filename.lower().endswith((".jpg", ".jpeg")) else "PNG" save_kw = {"quality": 85} if fmt == "JPEG" else {} img.save(buf, format=fmt, **save_kw) return base64.b64encode(buf.getvalue()).decode() with open(path, "rb") as f: return base64.b64encode(f.read()).decode() _TEXTURE_B64 = _load_asset_b64("green_metal_rust_diff_4k.jpg", max_px=600) _LOGO_B64 = _load_asset_b64("logo-kopia.png") # ---------- CSS ---------- CSS = """ @import url('https://fonts.googleapis.com/css2?family=Barlow:wght@400;600;700&display=swap'); /* ── Theme overrides ── */ :root { --body-background-fill: #c8b878; --body-text-color: #a8b080; --body-text-weight: 400; --background-fill-primary: rgba(38,44,26,0.92); --background-fill-secondary: rgba(28,34,16,0.96); --block-background-fill: rgba(38,44,26,0.92); --block-border-color: #5a6840; --block-border-width: 1.5px; --block-label-text-color: #b8c090; --block-label-text-size: var(--text-sm); --block-label-text-weight: 600; --block-label-background-fill: linear-gradient(180deg, rgba(48,56,32,0.98) 0%, rgba(28,34,16,0.98) 100%); --block-label-border-color: #485830; --input-background-fill: rgba(18,24,10,0.78); --input-border-color: rgba(70,90,45,0.55); --neutral-950: rgba(18,24,10,0.99); --neutral-900: rgba(22,28,14,0.99); --neutral-800: rgba(28,34,18,0.97); --neutral-700: rgba(35,42,22,0.95); --button-primary-background-fill: linear-gradient(180deg, #e09030 0%, #b06010 55%, #904800 100%); --button-primary-background-fill-hover: linear-gradient(180deg, #f0a040 0%, #c07020 55%, #a05800 100%); --button-primary-border-color: #c07020; --button-primary-border-color-hover: #d08030; --button-primary-text-color: #fff8e0; --button-primary-shadow: 0 4px 10px rgba(0,0,0,0.5); } /* ── Font ── */ *, *::before, *::after { font-family: 'Barlow', sans-serif !important; } /* ── Page background (texture + fallback colour) ── */ body, gradio-app { background-image: url('data:image/jpeg;base64,TEXTURE_DATA_URI') !important; background-size: 400px !important; background-repeat: repeat !important; background-color: #c8b878 !important; } /* ── Max-width and centering ── */ .app.svelte-182fdeq, .app { max-width: 900px !important; margin: 0 auto !important; padding: 16px !important; } /* ── Label uppercase + letter-spacing ── */ label > span, .block label span { text-transform: uppercase !important; letter-spacing: 2px !important; } /* ── Button pill ── */ #analyze-btn button { border-radius: 26px !important; font-size: 14px !important; font-weight: 600 !important; letter-spacing: 3px !important; padding: 14px 0 !important; width: 100% !important; } /* ── Header: transparent — elem_id is the direct element (container=False) ── */ #app-header { background: transparent !important; background-color: transparent !important; border: none !important; box-shadow: none !important; padding: 0 !important; } /* ── Olive block backgrounds — correct container class for Gradio 6.16 ── */ .gradio-container-6-16-0 .block, .gradio-container-6-16-0 .form { background: rgba(38,44,26,0.92) !important; } /* ── Gradient label strips — correct selectors from Gradio 6 source ── */ label[data-testid="block-label"], button.label-wrap { background: linear-gradient(180deg, rgba(48,56,32,0.98) 0%, rgba(28,34,16,0.98) 100%) !important; border-bottom: 1px solid #485830 !important; } /* ── Consistent label font ── */ label[data-testid="block-label"] span, button.label-wrap > span:first-child { font-size: 11px !important; text-transform: uppercase !important; letter-spacing: 2px !important; font-weight: 600 !important; color: #b8c090 !important; } /* ── Unified section headings (gr.HTML, one mechanism for all three) ── */ .sec-head { color: #b8c090 !important; font-size: 13px !important; font-weight: 600 !important; letter-spacing: 2px !important; text-align: left !important; padding: 2px 0 !important; margin: 0 !important; } /* ── Dropdown options popup ── */ .options, ul[role="listbox"] { background: rgba(28,34,16,0.98) !important; border-color: #485830 !important; } /* ── Empty manifest: collapse when no content ── */ #manifest { min-height: 0 !important; padding: 0 !important; } /* ── Manifest text (match Field report olive); links stay orange below ── */ #manifest, #manifest li { color: #a8b080 !important; } /* ── Manifest list: plain lines (no bullets), comfortable rhythm ── */ #manifest ul { list-style: none !important; margin: 0 !important; padding: 2px 0 !important; } #manifest li { display: flex !important; flex-wrap: wrap !important; align-items: baseline !important; gap: 0.3em !important; line-height: 1.7 !important; margin: 0 !important; } /* ── Field report: same looser line spacing ── */ #raw-output textarea { line-height: 1.7 !important; } /* ── Manifest links ── */ #manifest a { color: #e09030 !important; margin: 0 !important; padding: 0 !important; } .in-stock { color: #a0c890 !important; } /* ── Input text colour ── */ input, textarea, select { color: #a8b080 !important; border-radius: 7px !important; } /* ── Mobile: stack columns vertically ── */ @media (max-width: 768px) { .gap-4 { flex-direction: column !important; } .gap-4 > * { width: 100% !important; min-width: 100% !important; } } /* ── Upload zone: custom emoji icon ── */ #img-upload .wrap::before { content: "📂"; font-size: 30px; display: block; text-align: center; padding: 14px 0 2px; line-height: 1; } #img-upload .wrap svg { display: none !important; } @media (max-width: 768px) { #img-upload .wrap::before { content: "📸"; } } """ CSS = CSS.replace("TEXTURE_DATA_URI", _TEXTURE_B64) # ---------- Header HTML ---------- HEADER_HTML = f"""
Paint Match
COLOUR IDENTIFICATION UNIT
""" # ---------- Layout ---------- with gr.Blocks(title="Paint Match") as demo: gr.HTML(HEADER_HTML, elem_id="app-header") with gr.Row(): with gr.Column(): with gr.Group(): gr.HTML("
Document classification
") format_in = gr.Dropdown( choices=[(cfg["label"], fid) for fid, cfg in FORMATS.items()], value="airfix_shop", show_label=False, elem_id="format-dropdown", ) img_in = gr.Image(type="pil", label="Upload Document", elem_id="img-upload") btn = gr.Button("◉ COMMENCE ANALYSIS", variant="primary", elem_id="analyze-btn") with gr.Column(): with gr.Group(): gr.HTML("
Field report
") raw_out = gr.Textbox( show_label=False, lines=8, elem_id="raw-output", placeholder="Intelligence report — commence analysis to populate.", ) with gr.Group(): gr.HTML("
Resupply manifest
") codes_out = gr.HTML(elem_id="manifest") btn.click( analyze, inputs=[img_in, format_in], outputs=[raw_out, codes_out], show_progress="hidden", ) if __name__ == "__main__": os.environ.setdefault("GRADIO_DEFAULT_LOCALE", "en") demo.launch(css=CSS, allowed_paths=["assets/"])