studio: finish Hanzo rebrand — sidebar mark, black+purple theme, progress favicon
- Sidebar logo: replace the inline ComfyLogo Vue component (top-left mark, compiled into the JS bundle, missed by the file-asset pass) with the Hanzo mark via branding/patch_sidebar_logo.py (keyed off the stable __name:`ComfyLogo`). - Theme: branding/hanzo-theme.css forces a pure-black shell + Hanzo-purple accents (AA contrast), injected as the last stylesheet; patch_theme.py also rewrites the dark palette's canvas clear color to #000000 (CSS can't reach the litegraph <canvas>). - Favicon: make_progress_favicons.py renders the 10 progress frames with the Hanzo mark + purple ring, fixing the tab reverting to the Comfy mark during generations. Committed PNGs like favicon.ico (Docker needs no rasterizer). - Tab title: rebrand the runtime ' - ComfyUI' title suffix the string pass missed. - apply-branding.sh replays all of the above on frontend-package upgrades. - Rename fork-owned update_comfyui{,_stable}.bat -> update{,_stable}.bat.
@@ -11,7 +11,7 @@ echo "=== Hanzo Studio Branding ==="
|
||||
echo "Target: $STATIC_DIR"
|
||||
|
||||
# --- 1. Replace in-app logos ---
|
||||
echo "[1/5] Replacing logos..."
|
||||
echo "[1/7] Replacing logos..."
|
||||
for logo_file in comfy-logo-single.svg comfy-logo-mono.svg comfy-cloud-logo.svg; do
|
||||
target="$STATIC_DIR/assets/images/$logo_file"
|
||||
if [ -f "$target" ]; then
|
||||
@@ -25,16 +25,25 @@ done
|
||||
# --- 2. Install favicon (Hanzo mark) ---
|
||||
# The frontend sets the runtime favicon to /assets/favicon.ico via useFavicon();
|
||||
# the web root copies cover the pre-boot page load and the default /favicon.ico
|
||||
# request. favicon.ico is a committed artifact rendered from favicon.svg by
|
||||
# make_favicon.py (the Docker image has no rasterizer).
|
||||
echo "[2/5] Installing favicon..."
|
||||
# request. During a generation it instead cycles the 10 progress frames under
|
||||
# assets/images/favicon_progress_16x16/, so those must be Hanzo-branded too or the
|
||||
# tab reverts to the Comfy mark while a job runs. All PNG/ICO are committed
|
||||
# artifacts (rendered by make_favicon.py / make_progress_favicons.py) so the Docker
|
||||
# image needs no rasterizer.
|
||||
echo "[2/7] Installing favicon..."
|
||||
cp "$BRANDING_DIR/favicon.ico" "$STATIC_DIR/assets/favicon.ico"
|
||||
cp "$BRANDING_DIR/favicon.ico" "$STATIC_DIR/favicon.ico"
|
||||
cp "$BRANDING_DIR/favicon.svg" "$STATIC_DIR/favicon.svg"
|
||||
echo " Installed favicon.ico + favicon.svg"
|
||||
progress_dst="$STATIC_DIR/assets/images/favicon_progress_16x16"
|
||||
if [ -d "$progress_dst" ]; then
|
||||
cp "$BRANDING_DIR"/favicon_progress_16x16/frame_*.png "$progress_dst/"
|
||||
echo " Installed favicon.ico + favicon.svg + progress frames"
|
||||
else
|
||||
echo " Installed favicon.ico + favicon.svg (progress dir absent, skipped)"
|
||||
fi
|
||||
|
||||
# --- 3. Patch index.html (title + icon link + loading text) ---
|
||||
echo "[3/5] Patching index.html..."
|
||||
echo "[3/7] Patching index.html..."
|
||||
if [ -f "$STATIC_DIR/index.html" ]; then
|
||||
# Rename the title and inject icon links (the upstream head has none).
|
||||
sed -i 's#<title>ComfyUI</title>#<title>Hanzo Studio</title><link rel="icon" type="image/svg+xml" href="favicon.svg"/><link rel="alternate icon" href="favicon.ico"/>#g' "$STATIC_DIR/index.html"
|
||||
@@ -44,13 +53,25 @@ if [ -f "$STATIC_DIR/index.html" ]; then
|
||||
fi
|
||||
|
||||
# --- 4. Patch manifest JSON files ---
|
||||
echo "[4/5] Patching manifests..."
|
||||
echo "[4/7] Patching manifests..."
|
||||
find "$STATIC_DIR" -name "manifest*.json" -exec sed -i 's/"ComfyUI"/"Hanzo Studio"/g' {} \;
|
||||
find "$STATIC_DIR" -name "manifest*.json" -exec sed -i 's/studio\.org/hanzo.ai/g' {} \;
|
||||
echo " Done"
|
||||
|
||||
# --- 5. Smart JS/CSS/JSON display-string patching ---
|
||||
echo "[5/5] Smart JS patching..."
|
||||
echo "[5/7] Smart JS patching..."
|
||||
python3 "$BRANDING_DIR/patch_frontend.py" "$STATIC_DIR"
|
||||
|
||||
# --- 6. Replace the inline sidebar logo (ComfyLogo Vue component) ---
|
||||
# This is the top-left in-app mark, an inline SVG compiled into a JS chunk — a
|
||||
# different asset from the file-based logos handled in step 1.
|
||||
echo "[6/7] Replacing in-app sidebar logo..."
|
||||
python3 "$BRANDING_DIR/patch_sidebar_logo.py" "$STATIC_DIR"
|
||||
|
||||
# --- 7. Apply the black + Hanzo-purple theme ---
|
||||
# Installs hanzo-theme.css, injects it as the last stylesheet in index.html, and
|
||||
# rewrites the "dark" palette's canvas clear color to pure black.
|
||||
echo "[7/7] Applying black + purple theme..."
|
||||
python3 "$BRANDING_DIR/patch_theme.py" "$STATIC_DIR" "$BRANDING_DIR"
|
||||
|
||||
echo "=== Hanzo Studio branding complete ==="
|
||||
|
||||
|
After Width: | Height: | Size: 466 B |
|
After Width: | Height: | Size: 506 B |
|
After Width: | Height: | Size: 537 B |
|
After Width: | Height: | Size: 577 B |
|
After Width: | Height: | Size: 625 B |
|
After Width: | Height: | Size: 654 B |
|
After Width: | Height: | Size: 688 B |
|
After Width: | Height: | Size: 710 B |
|
After Width: | Height: | Size: 750 B |
|
After Width: | Height: | Size: 760 B |
@@ -0,0 +1,64 @@
|
||||
/* Hanzo Studio — black surface + Hanzo-purple accent theme.
|
||||
* Minimal override: only background/surface/accent tokens are touched so the
|
||||
* rest of the frontend's dark palette (text, node colors, borders) is intact.
|
||||
* Loaded LAST in index.html so it beats PrimeVue's runtime-injected tokens;
|
||||
* every declaration is !important because those tokens are injected after this
|
||||
* sheet at boot. Colors chosen for WCAG AA contrast on black:
|
||||
* accent #8B5CF6 on #000 -> 6.2:1 (AA normal text)
|
||||
* button #ffffff on #7C3AED -> 5.25:1 (AA normal text)
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* --- App shell (ComfyUI-native vars from the "dark" palette) --- */
|
||||
--bg-color: #000000 !important;
|
||||
--fg-color: #ffffff !important;
|
||||
--comfy-menu-bg: #0a0a0a !important;
|
||||
--comfy-menu-secondary-bg: #141414 !important;
|
||||
--comfy-input-bg: #0a0a0a !important;
|
||||
--content-bg: #0a0a0a !important;
|
||||
--content-hover-bg: #171717 !important;
|
||||
--border-color: #1f1f1f !important;
|
||||
--tr-even-bg-color: #0a0a0a !important;
|
||||
--tr-odd-bg-color: #141414 !important;
|
||||
|
||||
/* --- PrimeVue surface ramp: darken the panel/sidebar/topbar shades only.
|
||||
* 500..900 are the backgrounds; 100/300 stay light (used for text/borders). */
|
||||
--p-surface-900: #000000 !important;
|
||||
--p-surface-800: #0a0a0a !important;
|
||||
--p-surface-700: #141414 !important;
|
||||
--p-surface-600: #1c1c1c !important;
|
||||
--p-surface-500: #242424 !important;
|
||||
|
||||
/* --- PrimeVue semantic surfaces --- */
|
||||
--p-content-background: #000000 !important;
|
||||
--p-content-hover-background: #171717 !important;
|
||||
--p-overlay-modal-background: #0a0a0a !important;
|
||||
--p-overlay-popover-background: #0a0a0a !important;
|
||||
--p-mask-background: rgba(0, 0, 0, 0.72) !important;
|
||||
|
||||
/* --- Hanzo purple accent --- */
|
||||
--p-primary-color: #8b5cf6 !important; /* icons/links/active — AA on black */
|
||||
--p-primary-contrast-color: #ffffff !important;
|
||||
--p-primary-400: #a78bfa !important;
|
||||
--p-primary-500: #8b5cf6 !important;
|
||||
--p-primary-600: #7c3aed !important;
|
||||
--p-focus-ring-color: #8b5cf6 !important;
|
||||
--p-highlight-background: rgba(124, 58, 237, 0.24) !important;
|
||||
--p-highlight-focus-background: rgba(124, 58, 237, 0.32) !important;
|
||||
--p-highlight-color: #ede9fe !important;
|
||||
|
||||
/* --- Primary buttons: white on deeper violet keeps AA (5.25:1) --- */
|
||||
--p-button-primary-background: #7c3aed !important;
|
||||
--p-button-primary-hover-background: #6d28d9 !important;
|
||||
--p-button-primary-active-background: #5b21b6 !important;
|
||||
--p-button-primary-border-color: #7c3aed !important;
|
||||
--p-button-primary-color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Pure-black canvas gutter behind the litegraph <canvas>. */
|
||||
html,
|
||||
body,
|
||||
body.litegraph,
|
||||
#vue-app {
|
||||
background-color: #000000 !important;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render the 10 animated progress-favicon frames with the Hanzo mark.
|
||||
|
||||
During a generation the frontend cycles
|
||||
`/assets/images/favicon_progress_16x16/frame_{0..9}.png` as the browser-tab
|
||||
favicon. Upstream ships ComfyUI-branded frames, so the tab reverts to the Comfy
|
||||
mark whenever a job runs. These frames are the Hanzo mark (white on a black tile,
|
||||
matching favicon.svg) with a Hanzo-purple progress ring that fills 0 -> 100%.
|
||||
|
||||
Like favicon.ico, the PNGs are committed build artifacts so apply-branding.sh can
|
||||
install them without a rasterizer in the Docker image. Re-run only if the mark or
|
||||
palette changes: python3 branding/make_progress_favicons.py
|
||||
"""
|
||||
import os
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
OUT = os.path.join(HERE, "favicon_progress_16x16")
|
||||
|
||||
SS = 128 # supersampled canvas, downsampled to 16x16
|
||||
FRAMES = 10
|
||||
MARK_SCALE = 0.92
|
||||
RING_BBOX = (12, 12, SS - 12, SS - 12)
|
||||
RING_WIDTH = 10
|
||||
BG = (0, 0, 0, 255) # black tile, like favicon.svg
|
||||
MARK = (255, 255, 255, 255) # white Hanzo mark
|
||||
RING = (139, 92, 246, 255) # Hanzo purple #8B5CF6
|
||||
|
||||
# Hanzo mark: five subpaths from branding/hanzo-logo.svg, as polygons (0..67 space).
|
||||
POLYS = [
|
||||
[(22.21, 67), (22.21, 44.6369), (0, 44.6369), (0, 67)],
|
||||
[(66.7038, 22.3184), (22.2534, 22.3184), (0.0878906, 44.6367), (44.4634, 44.6367)],
|
||||
[(22.21, 0), (0, 0), (0, 22.3184), (22.21, 22.3184)],
|
||||
[(66.7198, 0), (44.5098, 0), (44.5098, 22.3184), (66.7198, 22.3184)],
|
||||
[(66.7198, 67), (66.7198, 44.6369), (44.5098, 44.6369), (44.5098, 67)],
|
||||
]
|
||||
MARK_W = 66.7198
|
||||
MARK_H = 67.0
|
||||
OFF_X = (SS - MARK_W * MARK_SCALE) / 2
|
||||
OFF_Y = (SS - MARK_H * MARK_SCALE) / 2
|
||||
|
||||
|
||||
def draw_frame(frac: float) -> Image.Image:
|
||||
img = Image.new("RGBA", (SS, SS), (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(img)
|
||||
d.rounded_rectangle((0, 0, SS - 1, SS - 1), radius=20, fill=BG)
|
||||
for poly in POLYS:
|
||||
pts = [(x * MARK_SCALE + OFF_X, y * MARK_SCALE + OFF_Y) for x, y in poly]
|
||||
d.polygon(pts, fill=MARK)
|
||||
if frac > 0:
|
||||
d.arc(RING_BBOX, start=-90, end=-90 + frac * 360, fill=RING, width=RING_WIDTH)
|
||||
return img.resize((16, 16), Image.LANCZOS)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
for i in range(FRAMES):
|
||||
frac = (i + 1) / FRAMES
|
||||
draw_frame(frac).save(os.path.join(OUT, f"frame_{i}.png"))
|
||||
print(f"Wrote {FRAMES} progress frames to {OUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -41,6 +41,10 @@ DISPLAY_REPLACEMENTS = [
|
||||
# Exact display strings — multi-word phrases are safe to replace globally
|
||||
# because they only appear as display text, not in code identifiers.
|
||||
# Order matters: longer matches first to avoid partial replacements.
|
||||
# Browser-tab title suffix: the frontend builds document.title as
|
||||
# `${workflowName} - ComfyUI`. The " - ComfyUI" constant has a space (not a
|
||||
# quote) before ComfyUI, so the quoted-string patterns below miss it.
|
||||
(' - ComfyUI', ' - Hanzo Studio'),
|
||||
('ComfyUI Issues', 'Hanzo Studio Issues'),
|
||||
('ComfyUI Docs', 'Hanzo Studio Docs'),
|
||||
('ComfyUI Forum', 'Hanzo Studio Forum'),
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
r"""Replace the in-app top-left sidebar logo (the ComfyUI mark) with the Hanzo mark.
|
||||
|
||||
The sidebar logo is NOT a file asset — it is an inline single-path SVG compiled
|
||||
into the `ComfyLogo` Vue component inside a hashed JS chunk (e.g. GraphView-*.js).
|
||||
`branding/apply-branding.sh` already swaps the file-based logos in assets/images/;
|
||||
this handles the inline one that pass missed.
|
||||
|
||||
We key off the stable Vue component name `__name:\`ComfyLogo\`` so the patch replays
|
||||
across frontend-package upgrades even as chunk hashes and minified identifiers change.
|
||||
Within that component we rewrite the viewBox and the single path `d` to the Hanzo
|
||||
mark (five filled rectangles forming the H glyph, in a 0..67 coordinate space).
|
||||
The component renders with mode="fill" and color=currentColor(white), so the mark
|
||||
fills white exactly like the original.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
STATIC_DIR = sys.argv[1]
|
||||
|
||||
HANZO_VIEWBOX = "0 0 67 67"
|
||||
# Five subpaths in one `d`, matching branding/hanzo-logo.svg (0..67 space).
|
||||
HANZO_PATH = (
|
||||
"M22.21 67V44.6369H0V67H22.21Z"
|
||||
"M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z"
|
||||
"M22.21 0H0V22.3184H22.21V0Z"
|
||||
"M66.7198 0H44.5098V22.3184H66.7198V0Z"
|
||||
"M66.7198 67V44.6369H44.5098V67H66.7198Z"
|
||||
)
|
||||
|
||||
ANCHOR = "__name:`ComfyLogo`"
|
||||
SEGMENT_LEN = 3000
|
||||
|
||||
|
||||
def patch_content(content: str):
|
||||
idx = content.find(ANCHOR)
|
||||
if idx == -1:
|
||||
return content, False
|
||||
head, seg, tail = content[:idx], content[idx:idx + SEGMENT_LEN], content[idx + SEGMENT_LEN:]
|
||||
orig = seg
|
||||
seg = re.sub(r'viewBox:`0 0 [0-9.]+ [0-9.]+`', f'viewBox:`{HANZO_VIEWBOX}`', seg, count=1)
|
||||
seg = re.sub(r'd:`M[0-9][^`]*`', f'd:`{HANZO_PATH}`', seg, count=1)
|
||||
if seg == orig:
|
||||
return content, False
|
||||
return head + seg + tail, True
|
||||
|
||||
|
||||
def main() -> int:
|
||||
found = 0
|
||||
for root, _dirs, files in os.walk(STATIC_DIR):
|
||||
for fname in files:
|
||||
if not fname.endswith(".js"):
|
||||
continue
|
||||
path = os.path.join(root, fname)
|
||||
try:
|
||||
with open(path, encoding="utf-8", errors="ignore") as f:
|
||||
content = f.read()
|
||||
except OSError:
|
||||
continue
|
||||
if ANCHOR not in content:
|
||||
continue
|
||||
found += 1
|
||||
new_content, changed = patch_content(content)
|
||||
if changed:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(new_content)
|
||||
print(f" Replaced ComfyLogo sidebar mark in {fname}")
|
||||
elif HANZO_PATH in content:
|
||||
print(f" ComfyLogo already Hanzo mark in {fname}")
|
||||
else:
|
||||
print(f" WARNING: ComfyLogo in {fname} but path not matched")
|
||||
return 1
|
||||
if found == 0:
|
||||
print(" WARNING: ComfyLogo component not found — sidebar logo unchanged")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Apply the Hanzo Studio black + purple theme to the prebuilt frontend.
|
||||
|
||||
Three orthogonal levers, each in its own lane:
|
||||
1. CSS shell skin -> copy branding/hanzo-theme.css into static/ and inject a
|
||||
<link> as the LAST stylesheet in index.html (idempotent).
|
||||
2. Canvas pixels -> CSS cannot reach the litegraph <canvas>; rewrite the
|
||||
"dark" palette's CLEAR_BACKGROUND_COLOR to pure black in
|
||||
the JS bundle. Keyed off the stable palette id+name so it
|
||||
replays across upgrades even if the value changes.
|
||||
|
||||
Usage: patch_theme.py <static_dir> <branding_dir>
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
STATIC_DIR = sys.argv[1]
|
||||
BRANDING_DIR = sys.argv[2]
|
||||
|
||||
THEME_CSS = "hanzo-theme.css"
|
||||
LINK_TAG = f'<link rel="stylesheet" href="{THEME_CSS}"/>'
|
||||
PALETTE_ANCHOR = "id:`dark`,name:`Dark (Default)`"
|
||||
PALETTE_SEGMENT_LEN = 1600
|
||||
|
||||
|
||||
def install_css() -> None:
|
||||
src = os.path.join(BRANDING_DIR, THEME_CSS)
|
||||
dst = os.path.join(STATIC_DIR, THEME_CSS)
|
||||
shutil.copyfile(src, dst)
|
||||
print(f" Installed {THEME_CSS}")
|
||||
|
||||
|
||||
def inject_link() -> None:
|
||||
index = os.path.join(STATIC_DIR, "index.html")
|
||||
if not os.path.isfile(index):
|
||||
print(" WARNING: index.html not found — theme link not injected")
|
||||
return
|
||||
with open(index, encoding="utf-8") as f:
|
||||
html = f.read()
|
||||
if LINK_TAG in html:
|
||||
print(" Theme link already present in index.html")
|
||||
return
|
||||
if "</head>" not in html:
|
||||
print(" WARNING: no </head> in index.html — theme link not injected")
|
||||
return
|
||||
html = html.replace("</head>", LINK_TAG + "</head>", 1)
|
||||
with open(index, "w", encoding="utf-8") as f:
|
||||
f.write(html)
|
||||
print(" Injected hanzo-theme.css link into index.html")
|
||||
|
||||
|
||||
def blacken_canvas() -> None:
|
||||
patched = 0
|
||||
for root, _dirs, files in os.walk(STATIC_DIR):
|
||||
for fname in files:
|
||||
if not fname.endswith(".js"):
|
||||
continue
|
||||
path = os.path.join(root, fname)
|
||||
try:
|
||||
with open(path, encoding="utf-8", errors="ignore") as f:
|
||||
content = f.read()
|
||||
except OSError:
|
||||
continue
|
||||
if PALETTE_ANCHOR not in content:
|
||||
continue
|
||||
new_content = content
|
||||
for m in re.finditer(re.escape(PALETTE_ANCHOR), content):
|
||||
start = m.start()
|
||||
seg = new_content[start:start + PALETTE_SEGMENT_LEN]
|
||||
seg2 = re.sub(
|
||||
r'CLEAR_BACKGROUND_COLOR:`#[0-9a-fA-F]{3,8}`',
|
||||
"CLEAR_BACKGROUND_COLOR:`#000000`",
|
||||
seg,
|
||||
count=1,
|
||||
)
|
||||
if seg2 != seg:
|
||||
new_content = new_content[:start] + seg2 + new_content[start + PALETTE_SEGMENT_LEN:]
|
||||
if new_content != content:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(new_content)
|
||||
print(f" Blackened canvas clear color in {fname}")
|
||||
patched += 1
|
||||
elif "CLEAR_BACKGROUND_COLOR:`#000000`" in content:
|
||||
print(f" Canvas clear color already black in {fname}")
|
||||
patched += 1
|
||||
if patched == 0:
|
||||
print(" WARNING: 'dark' palette not found — canvas color unchanged")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
install_css()
|
||||
inject_link()
|
||||
blacken_canvas()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||