ming-image-layered / theme.py
6chan's picture
style: align layout, radio pills, and button styling with HF App reference
3075e82 verified
Raw History Blame Contribute Delete
20 kB
# -*- coding: utf-8 -*-
"""Universal Space Design System and Theme Utilities (Catppuccin Minimalist Edition).
Theme: Catppuccin Mocha (#1e1e2e Base / #181825 Mantle / #cba6f7 Mauve)
Attribution: Built by @6chan
"""
from __future__ import annotations
import base64
import io
from typing import List, Sequence
import gradio as gr
from PIL import Image
# Catppuccin Mocha Tokens
CAT_BASE = "#1e1e2e"
CAT_MANTLE = "#181825"
CAT_SURFACE = "#242436"
CAT_ELEVATED = "#2f2f45"
CAT_BORDER = "#313244"
CAT_BORDER_HOVER = "#45475a"
CAT_TEXT = "#cdd6f4"
CAT_SUBTEXT = "#a6adc8"
CAT_MUTED = "#6c7086"
CAT_MAUVE = "#cba6f7"
CAT_LAVENDER = "#b4befe"
CAT_DARK = "#11111b"
def build_catppuccin_theme() -> gr.Theme:
"""Build the clean Catppuccin Mocha Gradio theme aligned with Hugging Face App styling."""
return gr.themes.Default(
primary_hue=gr.themes.colors.indigo,
secondary_hue=gr.themes.colors.slate,
neutral_hue=gr.themes.colors.slate,
radius_size=gr.themes.sizes.radius_md,
font=[
gr.themes.GoogleFont("Inter"),
"system-ui",
"-apple-system",
"sans-serif",
],
font_mono=[
gr.themes.GoogleFont("JetBrains Mono"),
"ui-monospace",
"monospace",
],
).set(
body_background_fill=CAT_MANTLE,
body_text_color=CAT_TEXT,
block_background_fill=CAT_BASE,
block_border_color=CAT_BORDER,
block_border_width="1px",
block_radius="8px",
block_label_background_fill=CAT_SURFACE,
block_label_border_color=CAT_BORDER,
block_label_text_color=CAT_SUBTEXT,
block_title_text_color=CAT_SUBTEXT,
input_background_fill=CAT_MANTLE,
input_border_color=CAT_BORDER,
input_border_color_focus=CAT_MAUVE,
input_radius="6px",
checkbox_background_color=CAT_MANTLE,
checkbox_background_color_selected=CAT_MAUVE,
checkbox_border_color=CAT_BORDER,
checkbox_border_color_selected=CAT_MAUVE,
checkbox_label_background_fill=CAT_MANTLE,
checkbox_label_background_fill_hover=CAT_SURFACE,
checkbox_label_background_fill_selected=CAT_ELEVATED,
checkbox_label_border_color=CAT_BORDER,
checkbox_label_border_color_hover=CAT_BORDER_HOVER,
checkbox_label_border_color_selected=CAT_MAUVE,
checkbox_label_text_color=CAT_SUBTEXT,
checkbox_label_text_color_selected=CAT_TEXT,
button_primary_background_fill=CAT_MAUVE,
button_primary_background_fill_hover=CAT_LAVENDER,
button_primary_text_color=CAT_DARK,
button_primary_border_color=CAT_MAUVE,
button_secondary_background_fill=CAT_SURFACE,
button_secondary_background_fill_hover=CAT_ELEVATED,
button_secondary_text_color=CAT_TEXT,
button_secondary_border_color=CAT_BORDER,
body_background_fill_dark=CAT_MANTLE,
body_text_color_dark=CAT_TEXT,
block_background_fill_dark=CAT_BASE,
block_border_color_dark=CAT_BORDER,
block_border_width_dark="1px",
block_label_background_fill_dark=CAT_SURFACE,
block_label_border_color_dark=CAT_BORDER,
block_label_text_color_dark=CAT_SUBTEXT,
block_title_text_color_dark=CAT_SUBTEXT,
input_background_fill_dark=CAT_MANTLE,
input_border_color_dark=CAT_BORDER,
input_border_color_focus_dark=CAT_MAUVE,
checkbox_background_color_dark=CAT_MANTLE,
checkbox_background_color_selected_dark=CAT_MAUVE,
checkbox_border_color_dark=CAT_BORDER,
checkbox_border_color_selected_dark=CAT_MAUVE,
checkbox_label_background_fill_dark=CAT_MANTLE,
checkbox_label_background_fill_hover_dark=CAT_SURFACE,
checkbox_label_background_fill_selected_dark=CAT_ELEVATED,
checkbox_label_border_color_dark=CAT_BORDER,
checkbox_label_border_color_hover_dark=CAT_BORDER_HOVER,
checkbox_label_border_color_selected_dark=CAT_MAUVE,
checkbox_label_text_color_dark=CAT_SUBTEXT,
checkbox_label_text_color_selected_dark=CAT_TEXT,
button_primary_background_fill_dark=CAT_MAUVE,
button_primary_background_fill_hover_dark=CAT_LAVENDER,
button_primary_text_color_dark=CAT_DARK,
button_primary_border_color_dark=CAT_MAUVE,
button_secondary_background_fill_dark=CAT_SURFACE,
button_secondary_background_fill_hover_dark=CAT_ELEVATED,
button_secondary_text_color_dark=CAT_TEXT,
button_secondary_border_color_dark=CAT_BORDER,
)
build_slate_theme = build_catppuccin_theme
build_charcoal_theme = build_catppuccin_theme
build_pi_theme = build_catppuccin_theme
SPACE_CSS = """
/* Clean Minimal Catppuccin Typography */
body, input, textarea, select, button {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
-webkit-font-smoothing: antialiased;
}
code, pre, .font-mono {
font-family: 'JetBrains Mono', ui-monospace, monospace !important;
}
.gradio-container {
max-width: 1260px !important;
margin: 0 auto !important;
padding: 1.5rem 1.5rem 2rem 1.5rem !important;
}
/* Centered Hugging Face App Header */
.space-header {
text-align: center;
margin-bottom: 1.25rem;
padding-bottom: 0.75rem;
}
.space-title {
margin: 0 0 0.4rem 0;
font-size: 1.55rem;
font-weight: 700;
letter-spacing: -0.02em;
color: #cdd6f4;
}
.space-subtitle {
margin: 0 0 0.65rem 0;
font-size: 0.92rem;
color: #a6adc8;
}
.space-links {
display: inline-flex;
align-items: center;
gap: 0.55rem;
font-size: 0.84rem;
color: #6c7086;
}
.space-links a {
color: #b4befe;
text-decoration: none;
font-weight: 500;
transition: color 0.12s ease;
}
.space-links a:hover {
color: #cba6f7;
text-decoration: underline;
}
.space-signature {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 0.76rem;
font-weight: 500;
padding: 0.12rem 0.48rem;
background: #242436;
color: #cba6f7 !important;
border-radius: 5px;
border: 1px solid #313244;
text-decoration: none !important;
}
.space-signature:hover {
border-color: #cba6f7;
background: #2f2f45;
}
.space-helper-line {
font-size: 0.85rem;
color: #a6adc8;
margin-bottom: 0.65rem;
}
/* Hugging Face App Style Radio Pills & Inputs */
.gradio-container .wrap.svelte-1cl284s,
.gradio-container .form {
border-color: #313244 !important;
}
/* Primary Action Button (Aligned with HF App Button Layout) */
.btn-extract {
background: #cba6f7 !important;
color: #11111b !important;
font-weight: 600 !important;
font-size: 0.92rem !important;
height: 42px !important;
border-radius: 6px !important;
border: 1px solid #cba6f7 !important;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2) !important;
transition: background-color 0.14s ease, border-color 0.14s ease, transform 0.08s ease !important;
margin-top: 0.25rem !important;
}
.btn-extract:hover {
background: #b4befe !important;
border-color: #b4befe !important;
}
.btn-extract:active {
transform: translateY(1px) !important;
}
/* Workstation Tabs */
#workbench-tabs {
border: none !important;
background: transparent !important;
}
#workbench-tabs .tab-nav {
border-bottom: 1px solid #313244 !important;
margin-bottom: 0.5rem !important;
}
#workbench-tabs .tab-nav button {
font-size: 0.82rem !important;
font-weight: 500 !important;
padding: 0.45rem 0.9rem !important;
color: #a6adc8 !important;
border: none !important;
background: transparent !important;
}
#workbench-tabs .tab-nav button.selected {
color: #cba6f7 !important;
border-bottom: 2px solid #cba6f7 !important;
font-weight: 600 !important;
}
/* Checkerboard Background for RGBA Gallery & Preview */
#layers .preview,
#layers .thumbnail-item,
#layers .gallery-item,
#layers img,
#preview-container img {
background-color: #181825 !important;
background-image:
linear-gradient(45deg, #242436 25%, transparent 25%),
linear-gradient(-45deg, #242436 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #242436 75%),
linear-gradient(-45deg, transparent 75%, #242436 75%) !important;
background-size: 16px 16px !important;
background-position: 0 0, 0 8px, 8px -8px, -8px 0 !important;
border-radius: 6px !important;
}
.layer-caption-note {
font-size: 0.8rem;
color: #a6adc8;
margin: 0.35rem 0 0.65rem 0.15rem;
}
/* Result status bar */
.result-status {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 0.76rem;
padding: 0.4rem 0.7rem;
border-radius: 6px;
background: #242436;
border: 1px solid #313244;
color: #cdd6f4;
margin-bottom: 0.5rem;
}
/* Interactive Canvas Stage */
.sl-panel {
display: flex;
gap: 16px;
align-items: flex-start;
flex-wrap: wrap;
width: 100%;
padding: 4px 0;
}
.sl-panel .sl-stagewrap {
flex: 1 1 360px;
min-width: 280px;
}
.sl-panel .sl-stage {
position: relative;
width: 100%;
overflow: hidden;
border-radius: 8px;
border: 1px solid #313244;
touch-action: none;
cursor: grab;
background-color: #181825;
background-image:
linear-gradient(45deg, #242436 25%, transparent 25%, transparent 75%, #242436 75%),
linear-gradient(45deg, #242436 25%, transparent 25%, transparent 75%, #242436 75%);
background-size: 20px 20px;
background-position: 0 0, 10px 10px;
}
.sl-panel .sl-stage:active {
cursor: grabbing;
}
.sl-panel .sl-lyr {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: fill;
user-select: none;
-webkit-user-drag: none;
pointer-events: none;
transition: opacity 0.15s ease;
}
.sl-panel .sl-lyr.sl-off {
opacity: 0 !important;
}
.sl-panel .sl-side {
flex: 0 0 230px;
display: flex;
flex-direction: column;
gap: 6px;
}
.sl-panel .sl-sidehead {
font-size: 11px;
font-weight: 500;
color: #a6adc8;
margin-bottom: 2px;
display: flex;
justify-content: space-between;
}
.sl-panel .sl-row {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 6px;
border: 1px solid transparent;
cursor: pointer;
background: #242436;
transition: all 0.12s ease;
}
.sl-panel .sl-row:hover {
background: #2f2f45;
border-color: #45475a;
}
.sl-panel .sl-row.sl-sel {
border-color: #cba6f7 !important;
background-color: #2f2f45 !important;
}
.sl-panel .sl-row.sl-dim, .sl-panel .sl-row.sl-blank {
opacity: 0.35;
}
.sl-panel .sl-eye {
background: transparent;
border: none;
cursor: pointer;
padding: 3px;
color: #6c7086;
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px;
}
.sl-panel .sl-eye:hover {
color: #cdd6f4;
}
.sl-panel .sl-th {
width: 34px;
height: 34px;
object-fit: contain;
border-radius: 4px;
flex: 0 0 34px;
border: 1px solid #313244;
background-color: #181825;
background-image:
linear-gradient(45deg, #242436 25%, transparent 25%, transparent 75%, #242436 75%),
linear-gradient(45deg, #242436 25%, transparent 25%, transparent 75%, #242436 75%);
background-size: 8px 8px;
background-position: 0 0, 4px 4px;
}
.sl-panel .sl-name {
font-size: 12px;
font-weight: 500;
line-height: 1.25;
color: #cdd6f4;
display: flex;
flex-direction: column;
}
.sl-panel .sl-name em {
font-style: normal;
font-size: 10px;
color: #6c7086;
font-family: 'JetBrains Mono', ui-monospace, monospace;
}
.sl-panel .sl-reset {
margin-top: 6px;
font-size: 11px;
font-weight: 500;
padding: 6px 10px;
border-radius: 5px;
border: 1px solid #313244;
background: transparent;
color: #a6adc8;
cursor: pointer;
transition: all 0.12s ease;
width: 100%;
text-align: center;
}
.sl-panel .sl-reset:hover {
color: #cdd6f4;
border-color: #45475a;
background: #242436;
}
/* Footer with Signature */
.space-footer {
margin-top: 1.75rem;
padding-top: 1rem;
text-align: center;
border-top: 1px solid #313244;
font-size: 0.76rem;
color: #6c7086;
}
.space-footer a {
color: #b4befe;
text-decoration: none;
font-weight: 500;
transition: color 0.12s ease;
}
.space-footer a:hover {
color: #cba6f7;
text-decoration: underline;
}
"""
PANEL_JS = r"""
() => {
if (window.__slPanelWired) return;
window.__slPanelWired = true;
document.addEventListener("click", (e) => {
const eyeBtn = e.target.closest(".sl-eye");
if (eyeBtn) {
e.stopPropagation();
const panel = eyeBtn.closest(".sl-panel");
if (!panel) return;
const l = panel.querySelector('.sl-lyr[data-i="' + eyeBtn.dataset.eye + '"]');
if (!l) return;
const hidden = l.classList.toggle("sl-off");
eyeBtn.classList.toggle("sl-hid", hidden);
const row = eyeBtn.closest(".sl-row");
if (row) row.classList.toggle("sl-dim", hidden);
return;
}
const resetBtn = e.target.closest(".sl-reset");
if (resetBtn) {
e.stopPropagation();
const panel = resetBtn.closest(".sl-panel");
if (!panel) return;
panel.querySelectorAll(".sl-lyr").forEach((l) => {
l.classList.remove("sl-off");
l.style.transform = "";
l.dataset.tx = 0;
l.dataset.ty = 0;
});
panel.querySelectorAll(".sl-eye").forEach((b) => b.classList.remove("sl-hid"));
panel.querySelectorAll(".sl-row").forEach((r) => r.classList.remove("sl-dim"));
return;
}
const row = e.target.closest(".sl-row");
if (row) {
const panel = row.closest(".sl-panel");
if (!panel) return;
panel.querySelectorAll(".sl-row").forEach((r) => r.classList.remove("sl-sel"));
row.classList.add("sl-sel");
panel.dataset.sel = row.dataset.i;
}
});
let drag = null;
const pt = (e) => {
const t = e.touches && e.touches[0];
return t ? { x: t.clientX, y: t.clientY } : { x: e.clientX, y: e.clientY };
};
const move = (e) => {
if (!drag) return;
const s = pt(e);
const tx = drag.tx + (s.x - drag.sx);
const ty = drag.ty + (s.y - drag.sy);
drag.l.dataset.tx = tx;
drag.l.dataset.ty = ty;
drag.l.style.transform = "translate(" + tx + "px," + ty + "px)";
e.preventDefault();
};
const up = () => {
drag = null;
document.removeEventListener("mousemove", move);
document.removeEventListener("mouseup", up);
document.removeEventListener("touchmove", move);
document.removeEventListener("touchend", up);
};
const down = (e) => {
const stage = e.target.closest(".sl-stage");
if (!stage) return;
const panel = stage.closest(".sl-panel");
if (!panel) return;
const l = panel.querySelector('.sl-lyr[data-i="' + panel.dataset.sel + '"]');
if (!l || l.classList.contains("sl-off")) return;
const s = pt(e);
drag = {
l: l,
sx: s.x,
sy: s.y,
tx: parseFloat(l.dataset.tx || 0),
ty: parseFloat(l.dataset.ty || 0),
};
document.addEventListener("mousemove", move);
document.addEventListener("mouseup", up);
document.addEventListener("touchmove", move, { passive: false });
document.addEventListener("touchend", up);
e.preventDefault();
};
document.addEventListener("mousedown", down);
document.addEventListener("touchstart", down, { passive: false });
}
"""
SVG_EYE_ICON = """<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"></path><circle cx="12" cy="12" r="3"></circle></svg>"""
def pil_to_data_uri(im: Image.Image, max_dim: int | None = None) -> str:
"""Encode PIL image to inline base64 PNG data URI."""
if max_dim:
w, h = im.size
scale = max_dim / max(w, h)
if scale < 1.0:
im = im.resize((max(1, int(w * scale)), max(1, int(h * scale))), Image.Resampling.LANCZOS)
buf = io.BytesIO()
im.save(buf, format="PNG")
b64 = base64.b64encode(buf.getvalue()).decode("ascii")
return f"data:image/png;base64,{b64}"
def build_layer_panel(
layer_pils: list[Image.Image],
coverage: list[float],
layer_names: Sequence[str] | None = None,
width: int = 512,
height: int = 552,
) -> str:
"""Render minimalist Catppuccin layer canvas and inspector."""
n = len(layer_pils)
if n == 0:
return '<div class="sl-panel"><div class="text-xs text-[#a6adc8]">No layers extracted yet.</div></div>'
sel_i = next((i for i in range(n - 1, 0, -1) if coverage[i] >= 0.5), 0)
stack = []
rows = []
for i, pil in enumerate(layer_pils):
uri = pil_to_data_uri(pil)
stack.append(
f'<img class="sl-lyr" data-i="{i}" src="{uri}" draggable="false" alt="layer {i}">'
)
for i in range(n - 1, -1, -1):
if layer_names and i < len(layer_names):
name = layer_names[i]
elif i == 0:
name = "Background"
elif i == n - 1:
name = "Foreground"
else:
name = f"Layer {i + 1}"
pct = coverage[i]
empty = pct < 0.5
tag = "Empty" if empty else f"{pct:.1f}% cover"
cls = " sl-sel" if i == sel_i else ""
cls += " sl-blank" if empty else ""
th_uri = pil_to_data_uri(layer_pils[i], max_dim=72)
rows.append(
f'<div class="sl-row{cls}" data-i="{i}">'
f'<button class="sl-eye" data-eye="{i}" title="Toggle visibility">{SVG_EYE_ICON}</button>'
f'<img class="sl-th" src="{th_uri}" draggable="false" alt="">'
f'<span class="sl-name">{name}<em>{tag}</em></span>'
f'</div>'
)
return (
f'<div class="sl-panel" data-sel="{sel_i}">'
f' <div class="sl-stagewrap">'
f' <div class="sl-stage" style="aspect-ratio:{width}/{height}">{"".join(stack)}</div>'
f' </div>'
f' <div class="sl-side">'
f' <div class="sl-sidehead"><span>Layers ({n})</span><span>Drag on canvas 路 Eye to hide</span></div>'
f' {"".join(rows)}'
f' <button class="sl-reset">Reset positions</button>'
f' </div>'
f'</div>'
)
def render_header(
title: str = "Ming-Image-Layers",
handle: str = "6chan",
subtitle: str = "Decompose flattened designs into independently editable transparent layers.",
) -> str:
"""Render a centered Hugging Face App header with author signature."""
return f"""
<div class="space-header">
<h1 class="space-title">{title}</h1>
<p class="space-subtitle">{subtitle}</p>
<div class="space-links">
<a href="https://huggingface.co/6chan/Ming-Image-0.1-Design-Layer-FP8" target="_blank">FP8 Model</a>
<span>路</span>
<a href="https://huggingface.co/inclusionAI/Ming-Image-0.1-Design-Layer" target="_blank">Base Model</a>
<span>路</span>
<a href="https://github.com/inclusionAI/Ming-Image" target="_blank">GitHub</a>
<span>路</span>
<a href="https://huggingface.co/{handle}" target="_blank" class="space-signature">@{handle}</a>
</div>
</div>
<div class="space-helper-line">
Describe the elements you want on each layer, ordered front to back, with the background last.
</div>
""".strip()
def render_footer(
handle: str = "6chan",
model_name: str | None = None,
model_url: str | None = None,
extra_links: list[tuple[str, str]] | None = None,
) -> str:
"""Render a minimalist developer footer with clear attribution."""
links_html = []
if model_name and model_url:
links_html.append(f'<a href="{model_url}" target="_blank">{model_name}</a>')
if extra_links:
for name, url in extra_links:
links_html.append(f'<a href="{url}" target="_blank">{name}</a>')
links_str = " 路 ".join(links_html)
links_block = f'<div style="margin-top: 0.25rem;">{links_str}</div>' if links_str else ""
return f"""
<div class="space-footer">
<div>Built by <a href="https://huggingface.co/{handle}" target="_blank">@{handle}</a> 路 Layer decomposition studio</div>
{links_block}
</div>
""".strip()