techfreakworm commited on
Commit
86c22f6
·
unverified ·
1 Parent(s): 613dab3

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 "&lt;x&gt;" in out
2884
+ assert "&quot;quoted&quot;" 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, label="Prompt")` full width.
117
- - **Model selector** — `gr.Radio(["Base", "Turbo"], value="Turbo", label="Model")`T2I tab only.
118
- - **LoRA loader** — `gr.File(label="LoRA", file_types=[".safetensors"])` + `gr.Slider(0.0, 1.5, value=0.8, label="LoRA strength", step=0.05)`. On phone they stack; on tablet+ they share a row.
119
- - **Parameter sliders** — Steps, CFG (T2I-base only), Width, Height, Seed.
120
- - **ControlNet-only** — additional `gr.Image(label="Control image")` + `gr.Dropdown(["Canny","Depth","Pose","Pre-processed"], value="Canny")` + `gr.Slider(0.0, 2.0, value=1.0, label="ControlNet scale")`.
121
- - **Upscale-only** — additional `gr.Image(label="Input image")`; no model selector; 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(label="Output", show_download_button=True)` + `gr.JSON(label="Meta", value={...seed, steps, model, lora})`.
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