| import base64 |
| import io |
| import os |
| import gradio as gr |
| from PIL import Image |
|
|
| from app import analyze, FORMATS |
|
|
| |
|
|
| 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 = """ |
| @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 = f""" |
| <link href="https://fonts.googleapis.com/css2?family=Barlow:wght@400;600;700&display=swap" rel="stylesheet"> |
| <div style="text-align:center; padding: 20px 0 8px;"> |
| <img src="data:image/png;base64,{_LOGO_B64}" |
| style="height:130px; width:auto; max-width:85%; display:block; margin:0 auto;" |
| alt="Paint Match"> |
| <div style="font-family:'Barlow',sans-serif; color:#b0a878; font-size:13px; letter-spacing:5px; font-weight:700; |
| margin-top:-42px; position:relative; z-index:1; padding-bottom:20px;"> |
| COLOUR IDENTIFICATION UNIT |
| </div> |
| </div> |
| """ |
|
|
| |
|
|
| 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("<div class='sec-head'>Document classification</div>") |
| 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("<div class='sec-head'>Field report</div>") |
| 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("<div class='sec-head'>Resupply manifest</div>") |
| 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/"]) |
|
|