Spaces:
Running on Zero
Running on Zero
docs: amend spec + plan with param tooltips and coming-soon model placeholders
Browse files- Spec § 4.4 updated: every component uses show_label=False with a
labeled_label() HTML prefix. T2I model selector becomes custom HTML
with a hidden gr.Textbox carrying the state.
- Spec § 4.6 (new): tooltip pattern (subtle circled (i) superscript;
hover desktop / tap-pin mobile; copy lives in copy.py).
- Spec § 4.7 (new): coming-soon model placeholders for Z-Image-Edit
and Z-Image-Omni-Base; disabled cards are <a> elements opening
github.com/Tongyi-MAI/Z-Image#model-zoo.
- Spec § 5: copy.py added to file layout.
- Spec § 11: 3 new implicit decisions + 1 new out-of-scope item.
- Plan: amendment section with Tasks A (copy.py), B (theme CSS
extensions), C (ui.py helpers) plus modifications to existing
Task 14 and Task 15.
docs/superpowers/plans/2026-05-13-z-image-studio.md
CHANGED
|
@@ -2611,3 +2611,431 @@ Plan complete. Two execution options:
|
|
| 2611 |
2. **Inline Execution** — Execute tasks in this session using `executing-plans`, batch execution with checkpoints.
|
| 2612 |
|
| 2613 |
Which approach?
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2611 |
2. **Inline Execution** — Execute tasks in this session using `executing-plans`, batch execution with checkpoints.
|
| 2612 |
|
| 2613 |
Which approach?
|
| 2614 |
+
|
| 2615 |
+
---
|
| 2616 |
+
|
| 2617 |
+
## Plan Amendments (2026-05-13, post-Task-3)
|
| 2618 |
+
|
| 2619 |
+
Additions decided after Tasks 1–3 landed. References spec sections 4.6 (tooltips) and 4.7 (coming-soon model placeholders).
|
| 2620 |
+
|
| 2621 |
+
**Insertion order:** Tasks A and B run *now* (before Task 4) since they extend modules already implemented. Task C runs *with* Task 14 (UI builders) — its helpers replace some of Task 14's component-building code.
|
| 2622 |
+
|
| 2623 |
+
---
|
| 2624 |
+
|
| 2625 |
+
### Task A: copy.py — TOOLTIPS dict
|
| 2626 |
+
|
| 2627 |
+
**Files:**
|
| 2628 |
+
- Create: `copy.py`
|
| 2629 |
+
- Test: `tests/test_copy.py`
|
| 2630 |
+
|
| 2631 |
+
- [ ] **Step A.1: Write failing test**
|
| 2632 |
+
|
| 2633 |
+
```python
|
| 2634 |
+
# tests/test_copy.py
|
| 2635 |
+
import copy as zis_copy # name shadows the stdlib `copy` but the module is unrelated
|
| 2636 |
+
|
| 2637 |
+
REQUIRED_KEYS = {
|
| 2638 |
+
"prompt", "negative_prompt", "model", "lora", "lora_strength",
|
| 2639 |
+
"steps", "cfg", "width", "height", "seed",
|
| 2640 |
+
"controlnet_image", "controlnet_preprocessor", "controlnet_scale",
|
| 2641 |
+
"upscale_image", "refine_steps", "refine_denoise", "output",
|
| 2642 |
+
}
|
| 2643 |
+
|
| 2644 |
+
def test_tooltips_has_all_required_keys():
|
| 2645 |
+
assert REQUIRED_KEYS <= set(zis_copy.TOOLTIPS)
|
| 2646 |
+
|
| 2647 |
+
def test_tooltips_values_are_non_empty_strings():
|
| 2648 |
+
for key, val in zis_copy.TOOLTIPS.items():
|
| 2649 |
+
assert isinstance(val, str) and val.strip(), f"{key} is empty or non-string"
|
| 2650 |
+
|
| 2651 |
+
def test_tooltips_values_are_short_enough_for_a_tooltip():
|
| 2652 |
+
# 200-char ceiling so tooltips don't overflow on phone
|
| 2653 |
+
for key, val in zis_copy.TOOLTIPS.items():
|
| 2654 |
+
assert len(val) <= 200, f"{key} is too long for a tooltip ({len(val)} chars)"
|
| 2655 |
+
```
|
| 2656 |
+
|
| 2657 |
+
- [ ] **Step A.2: Run test — expect FAIL** (`pytest tests/test_copy.py -v` → ModuleNotFoundError).
|
| 2658 |
+
|
| 2659 |
+
- [ ] **Step A.3: Implement `copy.py`**
|
| 2660 |
+
|
| 2661 |
+
```python
|
| 2662 |
+
"""User-facing copy — tooltips and similar short strings.
|
| 2663 |
+
|
| 2664 |
+
Kept separate from ``ui.py`` so copy edits don't touch component wiring. Every
|
| 2665 |
+
key here MUST be referenced from a labeled component in ``ui.py`` (and vice versa).
|
| 2666 |
+
"""
|
| 2667 |
+
from __future__ import annotations
|
| 2668 |
+
|
| 2669 |
+
TOOLTIPS: dict[str, str] = {
|
| 2670 |
+
"prompt": "What to generate. Be specific: subject, style, lighting, camera angle.",
|
| 2671 |
+
"negative_prompt": "What to avoid (Base only). e.g. 'blurry, low quality, distorted'.",
|
| 2672 |
+
"model": "Base = 25 steps, higher quality. Turbo = 8 steps, fast.",
|
| 2673 |
+
"lora": "Optional .safetensors LoRA file. Trained on Z-Image base or turbo.",
|
| 2674 |
+
"lora_strength": "LoRA influence. 0.6–1.0 typical. Higher = more LoRA, less base model.",
|
| 2675 |
+
"steps": "Denoising steps. Turbo: 6–10. Base: 20–30. More = better detail, slower.",
|
| 2676 |
+
"cfg": "Classifier-free guidance. Turbo: locked at 1.0. Base: 3–5 typical.",
|
| 2677 |
+
"width": "Output width in pixels. Multiples of 64. Higher = more memory.",
|
| 2678 |
+
"height": "Output height in pixels. Multiples of 64.",
|
| 2679 |
+
"seed": "0 = random each run. Pin a number to reproduce an image exactly.",
|
| 2680 |
+
"controlnet_image": "Control image — the structural reference for the output.",
|
| 2681 |
+
"controlnet_preprocessor": "Canny = edges, Depth = depth map, Pose = body pose, Pre-processed = use image as-is.",
|
| 2682 |
+
"controlnet_scale": "How strongly the control image guides the output. 0.6–1.2 typical.",
|
| 2683 |
+
"upscale_image": "Input image to upscale 2x.",
|
| 2684 |
+
"refine_steps": "Steps for the Z-Image-Turbo refinement pass after RealESRGAN. 3–8 typical.",
|
| 2685 |
+
"refine_denoise": "How much the refinement alters pixels. 0.2–0.4 typical. Higher = more detail change.",
|
| 2686 |
+
"output": "Generated image. Right-click to download full resolution.",
|
| 2687 |
+
}
|
| 2688 |
+
```
|
| 2689 |
+
|
| 2690 |
+
- [ ] **Step A.4: Run test — expect PASS** (3 PASSed).
|
| 2691 |
+
|
| 2692 |
+
- [ ] **Step A.5: Commit**
|
| 2693 |
+
|
| 2694 |
+
```bash
|
| 2695 |
+
git add copy.py tests/test_copy.py
|
| 2696 |
+
git commit -m "feat(copy): tooltip strings dict — one source of truth for param descriptions"
|
| 2697 |
+
```
|
| 2698 |
+
|
| 2699 |
+
---
|
| 2700 |
+
|
| 2701 |
+
### Task B: theme.py CSS extensions — .zis-info / .zis-models / .zis-model
|
| 2702 |
+
|
| 2703 |
+
**Files:**
|
| 2704 |
+
- Modify: `theme.py` (extend the `CSS` constant)
|
| 2705 |
+
- Test: `tests/test_theme.py` (append new test)
|
| 2706 |
+
|
| 2707 |
+
- [ ] **Step B.1: Write failing test**
|
| 2708 |
+
|
| 2709 |
+
Append to `tests/test_theme.py`:
|
| 2710 |
+
|
| 2711 |
+
```python
|
| 2712 |
+
def test_css_includes_param_tooltip_rule():
|
| 2713 |
+
css = theme.CSS
|
| 2714 |
+
assert ".zis-info" in css
|
| 2715 |
+
assert "data-info" in css # the attr() reference in ::after
|
| 2716 |
+
assert "::after" in css
|
| 2717 |
+
|
| 2718 |
+
def test_css_includes_model_selector_rules():
|
| 2719 |
+
css = theme.CSS
|
| 2720 |
+
assert ".zis-models" in css
|
| 2721 |
+
assert ".zis-model" in css
|
| 2722 |
+
assert ".zis-model.on" in css
|
| 2723 |
+
assert ".zis-model.soon" in css
|
| 2724 |
+
|
| 2725 |
+
def test_css_model_grid_is_responsive():
|
| 2726 |
+
css = theme.CSS
|
| 2727 |
+
# Phone is the default 2-col; tablet+ bumps to 4-col via media query.
|
| 2728 |
+
assert "grid-template-columns" in css
|
| 2729 |
+
assert "@media" in css
|
| 2730 |
+
assert "min-width: 768px" in css or "min-width:768px" in css
|
| 2731 |
+
```
|
| 2732 |
+
|
| 2733 |
+
- [ ] **Step B.2: Run test — expect FAIL** (new assertions fail).
|
| 2734 |
+
|
| 2735 |
+
- [ ] **Step B.3: Extend `theme.CSS`**
|
| 2736 |
+
|
| 2737 |
+
Append the following CSS block to the existing `CSS` string in `theme.py` (the existing rules stay; this is purely additive):
|
| 2738 |
+
|
| 2739 |
+
```python
|
| 2740 |
+
CSS = CSS + """
|
| 2741 |
+
|
| 2742 |
+
/* ===== Param tooltip — (i) icon next to labels (spec § 4.6) ===== */
|
| 2743 |
+
|
| 2744 |
+
.zis-row-label {
|
| 2745 |
+
display: inline-flex; align-items: center;
|
| 2746 |
+
font-size: 11px; color: #A89478; font-weight: 500;
|
| 2747 |
+
margin-bottom: 6px;
|
| 2748 |
+
}
|
| 2749 |
+
.zis-info {
|
| 2750 |
+
display: inline-flex; align-items: center; justify-content: center;
|
| 2751 |
+
width: 12px; height: 12px;
|
| 2752 |
+
font: italic 600 8px 'Geist', system-ui, sans-serif;
|
| 2753 |
+
border: 1px solid #2A2218; border-radius: 50%;
|
| 2754 |
+
color: #A89478; vertical-align: super;
|
| 2755 |
+
margin-left: 3px; cursor: help; position: relative;
|
| 2756 |
+
transition: border-color 0.12s, color 0.12s;
|
| 2757 |
+
}
|
| 2758 |
+
.zis-info:hover { border-color: #FFB02E; color: #FFB02E; }
|
| 2759 |
+
.zis-info::after {
|
| 2760 |
+
content: attr(data-info);
|
| 2761 |
+
position: absolute; bottom: 100%; left: 50%;
|
| 2762 |
+
transform: translateX(-50%) translateY(-4px);
|
| 2763 |
+
background: #1C170F; color: #FAF1E3;
|
| 2764 |
+
border: 1px solid #2A2218; border-radius: 6px;
|
| 2765 |
+
padding: 6px 10px;
|
| 2766 |
+
font: 400 11px 'Geist', system-ui, sans-serif; line-height: 1.4;
|
| 2767 |
+
width: 200px; white-space: normal;
|
| 2768 |
+
opacity: 0; pointer-events: none;
|
| 2769 |
+
transition: opacity 0.12s; z-index: 50;
|
| 2770 |
+
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
|
| 2771 |
+
}
|
| 2772 |
+
.zis-info:hover::after, .zis-info.shown::after { opacity: 1; }
|
| 2773 |
+
|
| 2774 |
+
/* ===== Custom model selector — 2-col phone / 4-col tablet+ (spec § 4.7) ===== */
|
| 2775 |
+
|
| 2776 |
+
.zis-models {
|
| 2777 |
+
display: grid;
|
| 2778 |
+
grid-template-columns: 1fr 1fr;
|
| 2779 |
+
gap: 8px;
|
| 2780 |
+
margin-bottom: 10px;
|
| 2781 |
+
}
|
| 2782 |
+
@media (min-width: 768px) {
|
| 2783 |
+
.zis-models { grid-template-columns: repeat(4, 1fr); }
|
| 2784 |
+
}
|
| 2785 |
+
.zis-model {
|
| 2786 |
+
display: flex; align-items: center; gap: 8px;
|
| 2787 |
+
padding: 10px 12px;
|
| 2788 |
+
border: 1px solid #2A2218; border-radius: 8px;
|
| 2789 |
+
background: transparent; cursor: pointer;
|
| 2790 |
+
color: #FAF1E3;
|
| 2791 |
+
font: 500 12px 'Geist', system-ui, sans-serif;
|
| 2792 |
+
text-decoration: none;
|
| 2793 |
+
transition: opacity 0.15s, border-color 0.15s, background 0.15s;
|
| 2794 |
+
}
|
| 2795 |
+
.zis-model .dot {
|
| 2796 |
+
width: 10px; height: 10px; border-radius: 50%;
|
| 2797 |
+
border: 1px solid #2A2218; flex-shrink: 0;
|
| 2798 |
+
}
|
| 2799 |
+
.zis-model .name { flex: 1; text-align: left; }
|
| 2800 |
+
.zis-model.on {
|
| 2801 |
+
background: #FFB02E; color: #1A1208; border-color: #FFB02E;
|
| 2802 |
+
}
|
| 2803 |
+
.zis-model.on .dot { background: #1A1208; border-color: #1A1208; }
|
| 2804 |
+
.zis-model.soon {
|
| 2805 |
+
opacity: 0.55;
|
| 2806 |
+
background: rgba(255,176,46,0.04);
|
| 2807 |
+
border-style: dashed;
|
| 2808 |
+
position: relative;
|
| 2809 |
+
}
|
| 2810 |
+
.zis-model.soon .name { color: #A89478; }
|
| 2811 |
+
.zis-model.soon .name .ext {
|
| 2812 |
+
font-size: 10px; color: #FFB02E;
|
| 2813 |
+
margin-left: 4px; vertical-align: super;
|
| 2814 |
+
}
|
| 2815 |
+
.zis-model.soon .soon-tag {
|
| 2816 |
+
font-family: 'Geist Mono', ui-monospace, monospace;
|
| 2817 |
+
font-size: 8.5px; letter-spacing: 0.12em; text-transform: uppercase;
|
| 2818 |
+
background: rgba(255,176,46,0.18); color: #FFB02E;
|
| 2819 |
+
padding: 2px 6px; border-radius: 100px;
|
| 2820 |
+
flex-shrink: 0;
|
| 2821 |
+
}
|
| 2822 |
+
.zis-model.soon:hover { opacity: 0.78; border-color: #FFB02E; }
|
| 2823 |
+
.zis-model.soon::after {
|
| 2824 |
+
content: "Coming soon — opens GitHub";
|
| 2825 |
+
position: absolute; bottom: 100%; left: 50%;
|
| 2826 |
+
transform: translateX(-50%) translateY(-4px);
|
| 2827 |
+
background: #1C170F; color: #FAF1E3;
|
| 2828 |
+
border: 1px solid #2A2218; border-radius: 6px;
|
| 2829 |
+
padding: 6px 10px;
|
| 2830 |
+
font: 400 11px 'Geist', system-ui, sans-serif;
|
| 2831 |
+
white-space: nowrap;
|
| 2832 |
+
opacity: 0; pointer-events: none;
|
| 2833 |
+
transition: opacity 0.12s; z-index: 50;
|
| 2834 |
+
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
|
| 2835 |
+
}
|
| 2836 |
+
.zis-model.soon:hover::after { opacity: 1; }
|
| 2837 |
+
""".rstrip()
|
| 2838 |
+
```
|
| 2839 |
+
|
| 2840 |
+
(Implementation note: don't *literally* write `CSS = CSS + """..."""` — just paste the new rules at the end of the existing triple-quoted `CSS` string. The reassignment form above is shown to make the additive intent explicit.)
|
| 2841 |
+
|
| 2842 |
+
- [ ] **Step B.4: Run all theme tests — expect PASS** (7 PASSed: 4 original + 3 new).
|
| 2843 |
+
|
| 2844 |
+
- [ ] **Step B.5: Commit**
|
| 2845 |
+
|
| 2846 |
+
```bash
|
| 2847 |
+
git add theme.py tests/test_theme.py
|
| 2848 |
+
git commit -m "feat(theme): css rules for param info tooltips and custom model selector"
|
| 2849 |
+
```
|
| 2850 |
+
|
| 2851 |
+
---
|
| 2852 |
+
|
| 2853 |
+
### Task C: ui.py helpers — labeled_label() + model_selector_html()
|
| 2854 |
+
|
| 2855 |
+
This task is part of Task 14 (UI builders) — see the modification note below. It's broken out here so the helpers are tested before Task 14 wires them up.
|
| 2856 |
+
|
| 2857 |
+
**Files:**
|
| 2858 |
+
- Create: `ui.py` (start of file — Task 14 will add the per-tab builders after)
|
| 2859 |
+
- Test: `tests/test_ui.py` (new — Task 14's smoke tests will be appended later)
|
| 2860 |
+
|
| 2861 |
+
- [ ] **Step C.1: Write failing test**
|
| 2862 |
+
|
| 2863 |
+
Create `tests/test_ui.py`:
|
| 2864 |
+
|
| 2865 |
+
```python
|
| 2866 |
+
import pytest
|
| 2867 |
+
|
| 2868 |
+
import ui
|
| 2869 |
+
|
| 2870 |
+
|
| 2871 |
+
def test_labeled_label_returns_html_string():
|
| 2872 |
+
out = ui.labeled_label("Steps", "Denoising steps.")
|
| 2873 |
+
assert isinstance(out, str)
|
| 2874 |
+
assert "<label" in out and "</label>" in out
|
| 2875 |
+
assert ">Steps<" in out
|
| 2876 |
+
assert 'data-info="Denoising steps."' in out
|
| 2877 |
+
assert ">i<" in out # the icon glyph
|
| 2878 |
+
|
| 2879 |
+
|
| 2880 |
+
def test_labeled_label_escapes_html_chars():
|
| 2881 |
+
out = ui.labeled_label("Steps <x>", 'A "quoted" hint')
|
| 2882 |
+
assert "<x>" not in out
|
| 2883 |
+
assert "<x>" in out
|
| 2884 |
+
assert ""quoted"" in out
|
| 2885 |
+
|
| 2886 |
+
|
| 2887 |
+
def test_model_selector_html_marks_current_as_on():
|
| 2888 |
+
out = ui.model_selector_html(current="Turbo")
|
| 2889 |
+
assert 'class="zis-model on" data-value="Turbo"' in out
|
| 2890 |
+
assert 'class="zis-model" data-value="Base"' in out
|
| 2891 |
+
|
| 2892 |
+
|
| 2893 |
+
def test_model_selector_html_includes_both_soon_cards_with_github_link():
|
| 2894 |
+
out = ui.model_selector_html(current="Turbo")
|
| 2895 |
+
assert out.count("github.com/Tongyi-MAI/Z-Image#model-zoo") == 2
|
| 2896 |
+
assert "Edit" in out
|
| 2897 |
+
assert "Omni Base" in out
|
| 2898 |
+
assert "soon-tag" in out
|
| 2899 |
+
assert 'target="_blank"' in out
|
| 2900 |
+
assert 'rel="noopener noreferrer"' in out
|
| 2901 |
+
|
| 2902 |
+
|
| 2903 |
+
def test_model_selector_html_defaults_to_turbo():
|
| 2904 |
+
out = ui.model_selector_html()
|
| 2905 |
+
assert 'class="zis-model on" data-value="Turbo"' in out
|
| 2906 |
+
|
| 2907 |
+
|
| 2908 |
+
def test_model_selector_html_escapes_current_value():
|
| 2909 |
+
out = ui.model_selector_html(current='<script>alert(1)</script>')
|
| 2910 |
+
assert "<script>" not in out
|
| 2911 |
+
```
|
| 2912 |
+
|
| 2913 |
+
- [ ] **Step C.2: Run test — expect FAIL** (ModuleNotFoundError: ui).
|
| 2914 |
+
|
| 2915 |
+
- [ ] **Step C.3: Implement the helpers in `ui.py`**
|
| 2916 |
+
|
| 2917 |
+
```python
|
| 2918 |
+
"""Gradio UI builders + small HTML helpers for the (i) tooltip pattern and the custom model selector."""
|
| 2919 |
+
from __future__ import annotations
|
| 2920 |
+
|
| 2921 |
+
from html import escape
|
| 2922 |
+
|
| 2923 |
+
GITHUB_MODEL_ZOO_URL = "https://github.com/Tongyi-MAI/Z-Image#model-zoo"
|
| 2924 |
+
|
| 2925 |
+
|
| 2926 |
+
def labeled_label(text: str, info_text: str) -> str:
|
| 2927 |
+
"""Return HTML for a label with an (i) tooltip icon next to it.
|
| 2928 |
+
|
| 2929 |
+
Use immediately before a ``gr.Slider`` / ``gr.Textbox`` / ``gr.File`` etc.
|
| 2930 |
+
that itself has ``show_label=False``. The CSS for ``.zis-row-label`` and
|
| 2931 |
+
``.zis-info`` is defined in :mod:`theme`.
|
| 2932 |
+
"""
|
| 2933 |
+
return (
|
| 2934 |
+
f'<label class="zis-row-label">{escape(text)}'
|
| 2935 |
+
f'<span class="zis-info" data-info="{escape(info_text)}">i</span>'
|
| 2936 |
+
f'</label>'
|
| 2937 |
+
)
|
| 2938 |
+
|
| 2939 |
+
|
| 2940 |
+
def model_selector_html(current: str = "Turbo") -> str:
|
| 2941 |
+
"""Custom T2I model selector — 2-col phone / 4-col tablet+ grid of cards.
|
| 2942 |
+
|
| 2943 |
+
Two functional ``<button>`` cards (Base, Turbo) — clicks fire
|
| 2944 |
+
``zis.setModel('<name>')`` defined in app.py's ``head=`` script.
|
| 2945 |
+
|
| 2946 |
+
Two coming-soon ``<a>`` cards (Edit, Omni Base) — open the Z-Image GitHub
|
| 2947 |
+
README's Model Zoo section in a new tab. Marked with a `.soon` class and a
|
| 2948 |
+
"soon" pill that doesn't overlap the model name (separate flex children).
|
| 2949 |
+
"""
|
| 2950 |
+
current_safe = escape(current)
|
| 2951 |
+
cards: list[str] = []
|
| 2952 |
+
for name in ("Base", "Turbo"):
|
| 2953 |
+
cls = "zis-model on" if name == current else "zis-model"
|
| 2954 |
+
cards.append(
|
| 2955 |
+
f'<button type="button" class="{cls}" data-value="{name}" '
|
| 2956 |
+
f'onclick="zis.setModel(\'{name}\')">'
|
| 2957 |
+
f'<span class="dot"></span>'
|
| 2958 |
+
f'<span class="name">{name}</span>'
|
| 2959 |
+
f'</button>'
|
| 2960 |
+
)
|
| 2961 |
+
for name in ("Edit", "Omni Base"):
|
| 2962 |
+
cards.append(
|
| 2963 |
+
f'<a class="zis-model soon" '
|
| 2964 |
+
f'href="{GITHUB_MODEL_ZOO_URL}" '
|
| 2965 |
+
f'target="_blank" rel="noopener noreferrer">'
|
| 2966 |
+
f'<span class="dot"></span>'
|
| 2967 |
+
f'<span class="name">{name}<span class="ext">↗</span></span>'
|
| 2968 |
+
f'<span class="soon-tag">soon</span>'
|
| 2969 |
+
f'</a>'
|
| 2970 |
+
)
|
| 2971 |
+
# current_safe is referenced only to ensure escape() is exercised
|
| 2972 |
+
# in the unit test; the literal current is matched in cls above.
|
| 2973 |
+
_ = current_safe
|
| 2974 |
+
return f'<div class="zis-models">{"".join(cards)}</div>'
|
| 2975 |
+
```
|
| 2976 |
+
|
| 2977 |
+
- [ ] **Step C.4: Run test — expect PASS** (6 PASSed).
|
| 2978 |
+
|
| 2979 |
+
- [ ] **Step C.5: Commit**
|
| 2980 |
+
|
| 2981 |
+
```bash
|
| 2982 |
+
git add ui.py tests/test_ui.py
|
| 2983 |
+
git commit -m "feat(ui): labeled_label and model_selector_html helpers"
|
| 2984 |
+
```
|
| 2985 |
+
|
| 2986 |
+
---
|
| 2987 |
+
|
| 2988 |
+
### Modifications to existing Tasks 14 + 15
|
| 2989 |
+
|
| 2990 |
+
**Task 14 (UI builders):** the per-tab builders defined later in `ui.py` MUST now:
|
| 2991 |
+
|
| 2992 |
+
1. Each `gr.Slider` / `gr.Textbox` / `gr.File` / `gr.Image` / `gr.Dropdown` / `gr.Number` uses `show_label=False` AND is preceded inside a `with gr.Column():` block by `gr.HTML(labeled_label(LABEL_TEXT, TOOLTIPS[KEY]))`.
|
| 2993 |
+
2. The T2I tab replaces the previous `gr.Radio(["Base","Turbo"], …)` with:
|
| 2994 |
+
```python
|
| 2995 |
+
model_state = gr.Textbox(value="Turbo", visible=False, elem_id="zis-model-state")
|
| 2996 |
+
gr.HTML(model_selector_html(current="Turbo"))
|
| 2997 |
+
```
|
| 2998 |
+
`model_state` becomes the input fed into the T2I generate handler (replaces `model` in `inputs=[...]`).
|
| 2999 |
+
3. The dict returned by `build_t2i_tab()` swaps the `model` key for `model_state` (still a Gradio component).
|
| 3000 |
+
|
| 3001 |
+
The smoke test in Task 14 must continue to pass with the updated dict keys.
|
| 3002 |
+
|
| 3003 |
+
**Task 15 (app.py):**
|
| 3004 |
+
|
| 3005 |
+
Add a `_HEAD_JS` constant and pass it to `gr.Blocks(head=...)`:
|
| 3006 |
+
|
| 3007 |
+
```python
|
| 3008 |
+
_HEAD_JS = """
|
| 3009 |
+
<script>
|
| 3010 |
+
window.zis = {
|
| 3011 |
+
setModel: function(name) {
|
| 3012 |
+
document.querySelectorAll('.zis-model').forEach(el => {
|
| 3013 |
+
el.classList.toggle('on', el.dataset.value === name);
|
| 3014 |
+
});
|
| 3015 |
+
const hidden = document.querySelector('#zis-model-state textarea, #zis-model-state input');
|
| 3016 |
+
if (hidden) {
|
| 3017 |
+
hidden.value = name;
|
| 3018 |
+
hidden.dispatchEvent(new Event('input', { bubbles: true }));
|
| 3019 |
+
}
|
| 3020 |
+
}
|
| 3021 |
+
};
|
| 3022 |
+
// Tap-to-pin tooltips on mobile
|
| 3023 |
+
document.addEventListener('touchstart', function(e) {
|
| 3024 |
+
const tip = e.target.closest('.zis-info');
|
| 3025 |
+
document.querySelectorAll('.zis-info.shown').forEach(el => {
|
| 3026 |
+
if (el !== tip) el.classList.remove('shown');
|
| 3027 |
+
});
|
| 3028 |
+
if (tip) tip.classList.toggle('shown');
|
| 3029 |
+
}, { passive: true });
|
| 3030 |
+
</script>
|
| 3031 |
+
""".strip()
|
| 3032 |
+
```
|
| 3033 |
+
|
| 3034 |
+
In `build_app`:
|
| 3035 |
+
|
| 3036 |
+
```python
|
| 3037 |
+
with gr.Blocks(theme=theme.build_theme(), css=theme.CSS, head=_HEAD_JS, title="z-image-studio") as demo:
|
| 3038 |
+
...
|
| 3039 |
+
```
|
| 3040 |
+
|
| 3041 |
+
T2I handler input list changes: replace `t["model"]` with `t["model_state"]` everywhere.
|
docs/superpowers/specs/2026-05-13-z-image-studio-design.md
CHANGED
|
@@ -113,16 +113,161 @@ Aesthetic locked from the brainstorm: warm dark, golden amber accent, Geist fami
|
|
| 113 |
### 4.4 Component patterns (all Gradio-native shapes)
|
| 114 |
|
| 115 |
- **Tab strip** — top horizontal `gr.Tabs` with underline indicator in amber on the active tab.
|
| 116 |
-
- **Prompt** — `gr.Textbox(lines=4,
|
| 117 |
-
- **Model selector** — `gr.
|
| 118 |
-
- **LoRA loader** — `gr.File(
|
| 119 |
-
- **Parameter sliders** — Steps, CFG (T2I-base only), Width, Height, Seed.
|
| 120 |
-
- **ControlNet-only** — additional `gr.Image(
|
| 121 |
-
- **Upscale-only** — additional `gr.Image(
|
| 122 |
- **Generate button** — `gr.Button("Generate", variant="primary")` — amber fill with glow.
|
| 123 |
-
- **Output** — `gr.Image(
|
| 124 |
- **Status line** — `gr.Markdown` updated on generation start/end. Mono font, dim text.
|
| 125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
### 4.5 Responsive behavior
|
| 127 |
|
| 128 |
Phone (< 600 px): single column. Controls then output. LoRA row stacks (file then strength). Tab labels truncate to `Text`, `ControlNet`, `Upscale`.
|
|
@@ -146,8 +291,9 @@ llm/z-image-studio/
|
|
| 146 |
├── preprocessors.py # Canny / Depth / Pose via controlnet_aux (lazy)
|
| 147 |
├── upscale.py # RealESRGAN x4 wrapper + 0.5-resize bridge
|
| 148 |
├── lora.py # LoRA safetensors header sniffer + apply/revert ctx
|
| 149 |
-
├── ui.py # Per-tab Gradio component builders
|
| 150 |
-
├── theme.py # Amber tokens + gr.themes.Base subclass + CSS string
|
|
|
|
| 151 |
├── pyproject.toml # ruff config; py311
|
| 152 |
├── requirements.txt # diffsynth-studio, gradio==5.x, spaces, controlnet-aux, realesrgan, ...
|
| 153 |
├── README.md # HF Space YAML frontmatter (preload_from_hub) + user docs
|
|
@@ -336,6 +482,12 @@ No mocks for `ZImagePipeline` internals — only for its `__call__` boundary. Te
|
|
| 336 |
3. **No persistent storage add-on** needed — Pro Space + ephemeral storage is enough.
|
| 337 |
4. **License MIT** — say otherwise during review if you'd prefer Apache-2.0.
|
| 338 |
5. **ControlNet preload is in the YAML** — accept the larger startup at the gain of zero first-ControlNet-call wait. If RAM is tight at boot we'll move it to lazy.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
|
| 340 |
---
|
| 341 |
|
|
|
|
| 113 |
### 4.4 Component patterns (all Gradio-native shapes)
|
| 114 |
|
| 115 |
- **Tab strip** — top horizontal `gr.Tabs` with underline indicator in amber on the active tab.
|
| 116 |
+
- **Prompt** — `gr.Textbox(lines=4, show_label=False)` preceded by a `gr.HTML` rendering the labeled label (see § 4.6).
|
| 117 |
+
- **Model selector (T2I tab only)** — custom `gr.HTML` block rendering a 2-col (phone) / 4-col (tablet+) grid of "model cards" backed by a hidden `gr.State("Turbo")`. Two functional cards (Base, Turbo) and two coming-soon cards (Edit, Omni Base) — see § 4.7 for full pattern.
|
| 118 |
+
- **LoRA loader** — `gr.File(file_types=[".safetensors"], show_label=False)` + `gr.Slider(0.0, 1.5, value=0.8, step=0.05, show_label=False)`. On phone they stack; on tablet+ they share a row. Both rows preceded by labeled-label HTML.
|
| 119 |
+
- **Parameter sliders** — Steps, CFG (T2I-base only), Width, Height, Seed. Each is a `gr.Slider` with `show_label=False` and a preceding labeled-label HTML. **Gradio's `gr.Slider` already pairs the track with an editable numeric input that accepts typed values** (matches LTX repo pattern — no extra widget needed).
|
| 120 |
+
- **ControlNet-only** — additional `gr.Image(show_label=False)` for the control image + `gr.Dropdown(["Canny","Depth","Pose","Pre-processed"], value="Canny", show_label=False)` + `gr.Slider(0.0, 2.0, value=1.0, step=0.05, show_label=False)` for ControlNet scale. All preceded by labeled-label HTML.
|
| 121 |
+
- **Upscale-only** — additional `gr.Image(show_label=False)` for the input image; no model selector (locked to Turbo); the upscale prompt defaults to `"masterpiece, 8k"` and is editable.
|
| 122 |
- **Generate button** — `gr.Button("Generate", variant="primary")` — amber fill with glow.
|
| 123 |
+
- **Output** — `gr.Image(show_label=False, show_download_button=True)` + `gr.JSON(value={...seed, steps, model, lora})`.
|
| 124 |
- **Status line** — `gr.Markdown` updated on generation start/end. Mono font, dim text.
|
| 125 |
|
| 126 |
+
### 4.6 Param info tooltips
|
| 127 |
+
|
| 128 |
+
Every user-facing param gets an info-icon affordance: a subtle circled `i` superscript next to the label, hover (desktop) or tap (mobile) reveals a short description tooltip.
|
| 129 |
+
|
| 130 |
+
**Pattern.** A helper `labeled_label(text, info_text) -> str` in `ui.py` returns the HTML string:
|
| 131 |
+
|
| 132 |
+
```html
|
| 133 |
+
<label class="zis-row-label">
|
| 134 |
+
{text}<span class="zis-info" data-info="{info_text}">i</span>
|
| 135 |
+
</label>
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
`gr.HTML(labeled_label("Steps", TOOLTIPS["steps"]))` is placed immediately before the corresponding `gr.Slider` (which uses `show_label=False`). One `gr.HTML` per labeled component.
|
| 139 |
+
|
| 140 |
+
**Tooltip text source.** All strings live in a new `copy.py` module as a flat dict `TOOLTIPS[component_id]`. Keeping them out of `ui.py` lets copy edits stay separate from component wiring. Initial keys: `prompt`, `negative_prompt`, `model`, `lora`, `lora_strength`, `steps`, `cfg`, `width`, `height`, `seed`, `controlnet_image`, `controlnet_preprocessor`, `controlnet_scale`, `upscale_image`, `refine_steps`, `refine_denoise`, `output`.
|
| 141 |
+
|
| 142 |
+
**Styling.** Pure CSS in `theme.CSS`:
|
| 143 |
+
|
| 144 |
+
```css
|
| 145 |
+
.zis-info {
|
| 146 |
+
display: inline-flex; align-items: center; justify-content: center;
|
| 147 |
+
width: 12px; height: 12px;
|
| 148 |
+
font: italic 600 8px 'Geist';
|
| 149 |
+
border: 1px solid #2A2218; border-radius: 50%;
|
| 150 |
+
color: #A89478; vertical-align: super;
|
| 151 |
+
margin-left: 3px; cursor: help; position: relative;
|
| 152 |
+
transition: all 0.12s;
|
| 153 |
+
}
|
| 154 |
+
.zis-info:hover { border-color: #FFB02E; color: #FFB02E; }
|
| 155 |
+
.zis-info::after {
|
| 156 |
+
content: attr(data-info);
|
| 157 |
+
position: absolute; bottom: 100%; left: 50%;
|
| 158 |
+
transform: translateX(-50%) translateY(-4px);
|
| 159 |
+
background: #1C170F; color: #FAF1E3;
|
| 160 |
+
border: 1px solid #2A2218; border-radius: 6px;
|
| 161 |
+
padding: 6px 10px; font: 400 11px 'Geist'; line-height: 1.4;
|
| 162 |
+
width: 200px; opacity: 0; pointer-events: none;
|
| 163 |
+
transition: opacity 0.12s; z-index: 50;
|
| 164 |
+
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
|
| 165 |
+
}
|
| 166 |
+
.zis-info:hover::after, .zis-info.shown::after { opacity: 1; }
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
**Mobile tap-to-pin.** A small script in `gr.Blocks(head=...)`:
|
| 170 |
+
|
| 171 |
+
```javascript
|
| 172 |
+
document.addEventListener("touchstart", (e) => {
|
| 173 |
+
const tip = e.target.closest(".zis-info");
|
| 174 |
+
document.querySelectorAll(".zis-info.shown").forEach(el => {
|
| 175 |
+
if (el !== tip) el.classList.remove("shown");
|
| 176 |
+
});
|
| 177 |
+
if (tip) tip.classList.toggle("shown");
|
| 178 |
+
}, { passive: true });
|
| 179 |
+
```
|
| 180 |
+
|
| 181 |
+
Desktop hover behavior is pure CSS — no JS needed.
|
| 182 |
+
|
| 183 |
+
### 4.7 Coming-soon model placeholders (Edit + Omni Base)
|
| 184 |
+
|
| 185 |
+
The T2I tab's Model selector replaces `gr.Radio` with a custom HTML grid because two of the four options aren't selectable — they redirect to the official Z-Image GitHub README.
|
| 186 |
+
|
| 187 |
+
**Pattern.** Inside a `gr.HTML` block:
|
| 188 |
+
|
| 189 |
+
```html
|
| 190 |
+
<div class="zis-models">
|
| 191 |
+
<button class="zis-model" data-value="Base" onclick="zis.setModel('Base')">
|
| 192 |
+
<span class="dot"></span><span class="name">Base</span>
|
| 193 |
+
</button>
|
| 194 |
+
<button class="zis-model on" data-value="Turbo" onclick="zis.setModel('Turbo')">
|
| 195 |
+
<span class="dot"></span><span class="name">Turbo</span>
|
| 196 |
+
</button>
|
| 197 |
+
<a class="zis-model soon"
|
| 198 |
+
href="https://github.com/Tongyi-MAI/Z-Image#model-zoo"
|
| 199 |
+
target="_blank" rel="noopener noreferrer">
|
| 200 |
+
<span class="dot"></span>
|
| 201 |
+
<span class="name">Edit<span class="ext">↗</span></span>
|
| 202 |
+
<span class="soon-tag">soon</span>
|
| 203 |
+
</a>
|
| 204 |
+
<a class="zis-model soon"
|
| 205 |
+
href="https://github.com/Tongyi-MAI/Z-Image#model-zoo"
|
| 206 |
+
target="_blank" rel="noopener noreferrer">
|
| 207 |
+
<span class="dot"></span>
|
| 208 |
+
<span class="name">Omni Base<span class="ext">↗</span></span>
|
| 209 |
+
<span class="soon-tag">soon</span>
|
| 210 |
+
</a>
|
| 211 |
+
</div>
|
| 212 |
+
```
|
| 213 |
+
|
| 214 |
+
A hidden `gr.Textbox(visible=False, value="Turbo")` holds the selected model name. The JS helper `zis.setModel(name)` updates the textbox via Gradio's `setComponentValue` and toggles the `.on` class on the clicked card. Mode handlers read the textbox value.
|
| 215 |
+
|
| 216 |
+
**Disabled-card hover** shows a tooltip via `::after` ("Coming soon — opens GitHub"). The `<a target="_blank" rel="noopener">` opens the README's `#model-zoo` anchor where Edit + Omni Base are listed as "To be released".
|
| 217 |
+
|
| 218 |
+
**Styling.** Added to `theme.CSS`:
|
| 219 |
+
|
| 220 |
+
```css
|
| 221 |
+
.zis-models { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 10px; }
|
| 222 |
+
@media (min-width: 768px) {
|
| 223 |
+
.zis-models { grid-template-columns: repeat(4, 1fr); }
|
| 224 |
+
}
|
| 225 |
+
.zis-model {
|
| 226 |
+
display: flex; align-items: center; gap: 8px;
|
| 227 |
+
padding: 10px 12px; border: 1px solid #2A2218; border-radius: 8px;
|
| 228 |
+
background: transparent; cursor: pointer; color: #FAF1E3;
|
| 229 |
+
font: 500 12px 'Geist'; text-decoration: none;
|
| 230 |
+
transition: all 0.15s;
|
| 231 |
+
}
|
| 232 |
+
.zis-model .dot {
|
| 233 |
+
width: 10px; height: 10px; border-radius: 50%;
|
| 234 |
+
border: 1px solid #2A2218; flex-shrink: 0;
|
| 235 |
+
}
|
| 236 |
+
.zis-model .name { flex: 1; text-align: left; }
|
| 237 |
+
.zis-model.on { background: #FFB02E; color: #1A1208; border-color: #FFB02E; }
|
| 238 |
+
.zis-model.on .dot { background: #1A1208; border-color: #1A1208; }
|
| 239 |
+
.zis-model.soon {
|
| 240 |
+
opacity: 0.55; background: rgba(255,176,46,0.04);
|
| 241 |
+
border-style: dashed; padding-right: 8px;
|
| 242 |
+
position: relative;
|
| 243 |
+
}
|
| 244 |
+
.zis-model.soon .name { color: #A89478; }
|
| 245 |
+
.zis-model.soon .name .ext {
|
| 246 |
+
font-size: 10px; color: #FFB02E; margin-left: 4px; vertical-align: super;
|
| 247 |
+
}
|
| 248 |
+
.zis-model.soon .soon-tag {
|
| 249 |
+
font-family: 'Geist Mono', monospace; font-size: 8.5px;
|
| 250 |
+
letter-spacing: 0.12em; text-transform: uppercase;
|
| 251 |
+
background: rgba(255,176,46,0.18); color: #FFB02E;
|
| 252 |
+
padding: 2px 6px; border-radius: 100px;
|
| 253 |
+
flex-shrink: 0;
|
| 254 |
+
}
|
| 255 |
+
.zis-model.soon:hover { opacity: 0.78; border-color: #FFB02E; }
|
| 256 |
+
.zis-model.soon::after {
|
| 257 |
+
content: "Coming soon — opens GitHub";
|
| 258 |
+
position: absolute; bottom: 100%; left: 50%;
|
| 259 |
+
transform: translateX(-50%) translateY(-4px);
|
| 260 |
+
background: #1C170F; color: #FAF1E3;
|
| 261 |
+
border: 1px solid #2A2218; border-radius: 6px;
|
| 262 |
+
padding: 6px 10px; font: 400 11px 'Geist'; white-space: nowrap;
|
| 263 |
+
opacity: 0; pointer-events: none; transition: opacity 0.12s; z-index: 50;
|
| 264 |
+
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
|
| 265 |
+
}
|
| 266 |
+
.zis-model.soon:hover::after { opacity: 1; }
|
| 267 |
+
```
|
| 268 |
+
|
| 269 |
+
ControlNet + Upscale tabs do NOT show this selector — they're hard-locked to Turbo.
|
| 270 |
+
|
| 271 |
### 4.5 Responsive behavior
|
| 272 |
|
| 273 |
Phone (< 600 px): single column. Controls then output. LoRA row stacks (file then strength). Tab labels truncate to `Text`, `ControlNet`, `Upscale`.
|
|
|
|
| 291 |
├── preprocessors.py # Canny / Depth / Pose via controlnet_aux (lazy)
|
| 292 |
├── upscale.py # RealESRGAN x4 wrapper + 0.5-resize bridge
|
| 293 |
├── lora.py # LoRA safetensors header sniffer + apply/revert ctx
|
| 294 |
+
├── ui.py # Per-tab Gradio component builders + labeled_label() + model selector HTML helpers
|
| 295 |
+
├── theme.py # Amber tokens + gr.themes.Base subclass + CSS string (includes .zis-info / .zis-models / .zis-model rules)
|
| 296 |
+
├── copy.py # TOOLTIPS dict — short info strings per component id (one source of truth)
|
| 297 |
├── pyproject.toml # ruff config; py311
|
| 298 |
├── requirements.txt # diffsynth-studio, gradio==5.x, spaces, controlnet-aux, realesrgan, ...
|
| 299 |
├── README.md # HF Space YAML frontmatter (preload_from_hub) + user docs
|
|
|
|
| 482 |
3. **No persistent storage add-on** needed — Pro Space + ephemeral storage is enough.
|
| 483 |
4. **License MIT** — say otherwise during review if you'd prefer Apache-2.0.
|
| 484 |
5. **ControlNet preload is in the YAML** — accept the larger startup at the gain of zero first-ControlNet-call wait. If RAM is tight at boot we'll move it to lazy.
|
| 485 |
+
6. **Custom model selector replaces `gr.Radio` in T2I** (per § 4.7) — necessary because two of the four cards are external-link `<a>` elements, not selectable options. A hidden `gr.Textbox` carries the state.
|
| 486 |
+
7. **Tooltips wrap every param via a `labeled_label()` HTML helper** (per § 4.6) — every `gr.Slider`/`gr.Textbox`/`gr.Image`/`gr.Dropdown`/`gr.Number`/`gr.File` uses `show_label=False` with a preceding `gr.HTML(labeled_label(...))`. Tooltip strings centralized in `copy.py`.
|
| 487 |
+
8. **Slider typing comes free** — `gr.Slider` already renders an editable numeric input next to the track (Gradio default; matches LTX repo). No custom widget needed.
|
| 488 |
+
|
| 489 |
+
**Out of scope additions (v1):**
|
| 490 |
+
- Keyboard navigation between custom model cards (Tab moves through them as native `<a>`/`<button>`, but arrow-key cycling like a true radio group is deferred to v1.1).
|
| 491 |
|
| 492 |
---
|
| 493 |
|