style: reorganize UI into tabs and refine button hierarchy
Browse files- Implemented gr.Tabs for character attributes
- Aligned randomization dice with dropdowns on the same line
- Reordered output column (prompts above image)
- Grouped Save/Load buttons with example characters
- Moved 'Randomize' button below technical prompt
- Updated design docs with process management lessons
- app.py +119 -114
- design.md +14 -1
- features.yaml +1 -0
app.py
CHANGED
|
@@ -426,8 +426,8 @@ def generate_image_master(refined_prompt, technical_prompt, aspect_ratio, backen
|
|
| 426 |
|
| 427 |
def build_ui():
|
| 428 |
with gr.Blocks(title="RPGPortrait Prompt Builder Pro") as demo:
|
| 429 |
-
gr.Markdown("# 🎨 RPGPortrait
|
| 430 |
-
gr.Markdown("Create professional AI prompts and generate portraits
|
| 431 |
|
| 432 |
dropdowns = []
|
| 433 |
checkboxes = []
|
|
@@ -435,127 +435,121 @@ def build_ui():
|
|
| 435 |
|
| 436 |
def create_feature_ui(category, subcategory, label, default_value):
|
| 437 |
choices = list(features_data[category][subcategory].keys())
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
|
|
|
| 441 |
dropdowns.append(dd)
|
| 442 |
checkboxes.append(cb)
|
| 443 |
|
| 444 |
with gr.Row():
|
| 445 |
-
with gr.Column():
|
| 446 |
-
gr.
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 453 |
|
| 454 |
-
gr.
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 487 |
|
| 488 |
-
with gr.Row():
|
| 489 |
-
with gr.Column():
|
| 490 |
-
gr.Markdown("### 🪄 Style & Effects")
|
| 491 |
-
create_feature_ui('vfx_style', 'vfx', "Special Effects", "None")
|
| 492 |
-
create_feature_ui('vfx_style', 'style', "Art Style", "Digital Illustration")
|
| 493 |
-
create_feature_ui('vfx_style', 'mood', "Mood", "Heroic")
|
| 494 |
-
create_feature_ui('vfx_style', 'camera', "Camera Angle", "Bust")
|
| 495 |
-
extra_sty = gr.Textbox(placeholder="Extra Style details (e.g. specific artists, colors)", label="Additional Style Info")
|
| 496 |
-
extra_texts.append(extra_sty)
|
| 497 |
-
|
| 498 |
-
with gr.Column():
|
| 499 |
-
gr.Markdown("### ⚙️ Technical")
|
| 500 |
-
create_feature_ui('technical', 'aspect_ratio', "Aspect Ratio", "1:1")
|
| 501 |
-
|
| 502 |
-
gr.Markdown("---")
|
| 503 |
-
|
| 504 |
-
with gr.Row():
|
| 505 |
-
regenerate_btn = gr.Button("✨ Randomize Features", variant="secondary")
|
| 506 |
-
save_btn = gr.Button("💾 Save Character", variant="secondary")
|
| 507 |
-
load_btn = gr.UploadButton("📂 Load Character", file_types=[".json"], variant="secondary")
|
| 508 |
-
refine_btn = gr.Button("🧠 Refine with Gemini", variant="primary")
|
| 509 |
-
gen_img_btn = gr.Button("🖼️ Generate Image", variant="primary")
|
| 510 |
-
|
| 511 |
-
with gr.Row():
|
| 512 |
-
ollama_models = get_ollama_models()
|
| 513 |
-
ollama_active = len(ollama_models) > 0
|
| 514 |
-
|
| 515 |
-
refinement_backend = gr.Radio(
|
| 516 |
-
choices=["Gemini (Cloud)", "Ollama (Local)"] if ollama_active else ["Gemini (Cloud)"],
|
| 517 |
-
value="Gemini (Cloud)",
|
| 518 |
-
label="Prompt Refinement Backend",
|
| 519 |
-
info="Ollama is " + ("available" if ollama_active else "offline or no models found") + "."
|
| 520 |
-
)
|
| 521 |
-
|
| 522 |
-
ollama_model_dropdown = gr.Dropdown(
|
| 523 |
-
choices=ollama_models,
|
| 524 |
-
value=ollama_models[0] if ollama_active else None,
|
| 525 |
-
label="Ollama Model",
|
| 526 |
-
visible=ollama_active,
|
| 527 |
-
scale=1
|
| 528 |
-
)
|
| 529 |
-
backend_selector = gr.Radio(
|
| 530 |
-
choices=["Gemini (Cloud)", "ComfyUI (Local)"],
|
| 531 |
-
value="Gemini (Cloud)",
|
| 532 |
-
label="Image Generation Backend",
|
| 533 |
-
info="ComfyUI requires the local server to be running on port 8188."
|
| 534 |
-
)
|
| 535 |
-
|
| 536 |
-
# Simple toggle for Ollama model visibility
|
| 537 |
-
def toggle_ollama_visibility(backend):
|
| 538 |
-
return gr.update(visible=(backend == "Ollama (Local)"))
|
| 539 |
-
|
| 540 |
-
refinement_backend.change(
|
| 541 |
-
fn=toggle_ollama_visibility,
|
| 542 |
-
inputs=refinement_backend,
|
| 543 |
-
outputs=ollama_model_dropdown
|
| 544 |
-
)
|
| 545 |
-
|
| 546 |
-
with gr.Row():
|
| 547 |
-
example_dropdown = gr.Dropdown(choices=get_example_list(), label="Example Characters", scale=3)
|
| 548 |
-
load_example_btn = gr.Button("📂 Load Example", variant="secondary", scale=1)
|
| 549 |
-
|
| 550 |
-
with gr.Row():
|
| 551 |
-
with gr.Column(scale=2):
|
| 552 |
-
prompt_output = gr.Textbox(label="Generated Technical Prompt", lines=5, interactive=False)
|
| 553 |
-
refined_output = gr.Textbox(label="Gemini Refined Prompt", lines=5, interactive=True)
|
| 554 |
with gr.Column(scale=1):
|
| 555 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 556 |
download_img_btn = gr.DownloadButton("📥 Download Portrait (PNG)", variant="secondary", visible=False)
|
| 557 |
-
download_file = gr.File(label="Saved Character JSON", visible=False)
|
| 558 |
status_msg = gr.Markdown("")
|
|
|
|
| 559 |
|
| 560 |
all_input_components = dropdowns + extra_texts
|
| 561 |
|
|
@@ -598,6 +592,17 @@ def build_ui():
|
|
| 598 |
)
|
| 599 |
|
| 600 |
# Save/Load Logic
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 601 |
save_btn.click(
|
| 602 |
fn=save_character,
|
| 603 |
inputs=dropdowns + checkboxes + extra_texts,
|
|
|
|
| 426 |
|
| 427 |
def build_ui():
|
| 428 |
with gr.Blocks(title="RPGPortrait Prompt Builder Pro") as demo:
|
| 429 |
+
gr.Markdown("# 🎨 RPGPortrait Pro")
|
| 430 |
+
gr.Markdown("Create professional AI prompts and generate portraits.")
|
| 431 |
|
| 432 |
dropdowns = []
|
| 433 |
checkboxes = []
|
|
|
|
| 435 |
|
| 436 |
def create_feature_ui(category, subcategory, label, default_value):
|
| 437 |
choices = list(features_data[category][subcategory].keys())
|
| 438 |
+
gr.Markdown(f"**{label}**")
|
| 439 |
+
with gr.Row(equal_height=True):
|
| 440 |
+
dd = gr.Dropdown(choices=choices, value=default_value, scale=12, show_label=False)
|
| 441 |
+
cb = gr.Checkbox(label="🎲", value=False, scale=1, min_width=50, container=False)
|
| 442 |
dropdowns.append(dd)
|
| 443 |
checkboxes.append(cb)
|
| 444 |
|
| 445 |
with gr.Row():
|
| 446 |
+
with gr.Column(scale=2):
|
| 447 |
+
with gr.Tabs():
|
| 448 |
+
with gr.TabItem("👤 Identity & Expression"):
|
| 449 |
+
gr.Markdown("### 👤 Identity")
|
| 450 |
+
create_feature_ui('identity', 'race', "Race", "Human")
|
| 451 |
+
create_feature_ui('identity', 'class', "Class", "Fighter")
|
| 452 |
+
create_feature_ui('identity', 'gender', "Gender", "Male")
|
| 453 |
+
create_feature_ui('identity', 'age', "Age", "Young Adult")
|
| 454 |
+
extra_id = gr.Textbox(placeholder="Extra Identity details (e.g. lineage, title)", label="Additional Identity Info")
|
| 455 |
+
extra_texts.append(extra_id)
|
| 456 |
+
|
| 457 |
+
gr.Markdown("### 🎭 Expression & Pose")
|
| 458 |
+
create_feature_ui('expression_pose', 'expression', "Expression", "Determined")
|
| 459 |
+
create_feature_ui('expression_pose', 'pose', "Pose", "Standing")
|
| 460 |
+
|
| 461 |
+
with gr.TabItem("🎨 Appearance"):
|
| 462 |
+
gr.Markdown("### 🎨 Appearance")
|
| 463 |
+
create_feature_ui('appearance', 'hair_color', "Hair Color", "Brown")
|
| 464 |
+
create_feature_ui('appearance', 'hair_style', "Hair Style", "Short")
|
| 465 |
+
create_feature_ui('appearance', 'eye_color', "Eye Color", "Brown")
|
| 466 |
+
create_feature_ui('appearance', 'build', "Build", "Average")
|
| 467 |
+
create_feature_ui('appearance', 'skin_tone', "Skin Tone", "Fair")
|
| 468 |
+
create_feature_ui('appearance', 'distinguishing_feature', "Distinguishing Feature", "None")
|
| 469 |
+
extra_app = gr.Textbox(placeholder="Extra Appearance details (e.g. tattoos, scars)", label="Additional Appearance Info")
|
| 470 |
+
extra_texts.append(extra_app)
|
| 471 |
+
|
| 472 |
+
with gr.TabItem("⚔️ Equipment"):
|
| 473 |
+
gr.Markdown("### ⚔️ Equipment")
|
| 474 |
+
create_feature_ui('equipment', 'armor', "Armor/Clothing", "Travelers Clothes")
|
| 475 |
+
create_feature_ui('equipment', 'weapon', "Primary Weapon", "Longsword")
|
| 476 |
+
create_feature_ui('equipment', 'accessory', "Accessory 1", "None")
|
| 477 |
+
create_feature_ui('equipment', 'accessory', "Accessory 2", "None")
|
| 478 |
+
create_feature_ui('equipment', 'material', "Material Detail", "Weathered")
|
| 479 |
+
extra_eq = gr.Textbox(placeholder="Extra Equipment details (e.g. weapon enchantments)", label="Additional Equipment Info")
|
| 480 |
+
extra_texts.append(extra_eq)
|
| 481 |
+
|
| 482 |
+
with gr.TabItem("🌍 Environment"):
|
| 483 |
+
gr.Markdown("### 🌍 Environment")
|
| 484 |
+
create_feature_ui('environment', 'background', "Background", "Forest")
|
| 485 |
+
create_feature_ui('environment', 'lighting', "Lighting", "Natural Sunlight")
|
| 486 |
+
create_feature_ui('environment', 'atmosphere', "Atmosphere", "Clear")
|
| 487 |
+
extra_env = gr.Textbox(placeholder="Extra Environment details (e.g. time of day, weather)", label="Additional Environment Info")
|
| 488 |
+
extra_texts.append(extra_env)
|
| 489 |
+
|
| 490 |
+
with gr.TabItem("🪄 Style & Technical"):
|
| 491 |
+
gr.Markdown("### 🪄 Style & Effects")
|
| 492 |
+
create_feature_ui('vfx_style', 'vfx', "Special Effects", "None")
|
| 493 |
+
create_feature_ui('vfx_style', 'style', "Art Style", "Digital Illustration")
|
| 494 |
+
create_feature_ui('vfx_style', 'mood', "Mood", "Heroic")
|
| 495 |
+
create_feature_ui('vfx_style', 'camera', "Camera Angle", "Bust")
|
| 496 |
+
extra_sty = gr.Textbox(placeholder="Extra Style details (e.g. specific artists, colors)", label="Additional Style Info")
|
| 497 |
+
extra_texts.append(extra_sty)
|
| 498 |
+
|
| 499 |
+
gr.Markdown("### ⚙️ Technical")
|
| 500 |
+
create_feature_ui('technical', 'aspect_ratio', "Aspect Ratio", "1:1")
|
| 501 |
+
|
| 502 |
+
gr.Markdown("---")
|
| 503 |
|
| 504 |
+
with gr.Row():
|
| 505 |
+
example_dropdown = gr.Dropdown(choices=get_example_list(), label="Example Characters", scale=3)
|
| 506 |
+
load_example_btn = gr.Button("📂 Load Example", variant="secondary", scale=1)
|
| 507 |
+
save_btn = gr.Button("💾 Save Character", variant="secondary", scale=1)
|
| 508 |
+
load_btn = gr.UploadButton("📂 Load Character", file_types=[".json"], variant="secondary", scale=1)
|
| 509 |
+
|
| 510 |
+
with gr.Group():
|
| 511 |
+
gr.Markdown("### ⚙️ Settings & Generation")
|
| 512 |
+
with gr.Row():
|
| 513 |
+
ollama_models = get_ollama_models()
|
| 514 |
+
ollama_active = len(ollama_models) > 0
|
| 515 |
+
|
| 516 |
+
refinement_backend = gr.Radio(
|
| 517 |
+
choices=["Gemini (Cloud)", "Ollama (Local)"] if ollama_active else ["Gemini (Cloud)"],
|
| 518 |
+
value="Gemini (Cloud)",
|
| 519 |
+
label="Prompt Refinement Backend",
|
| 520 |
+
scale=2
|
| 521 |
+
)
|
| 522 |
+
|
| 523 |
+
ollama_model_dropdown = gr.Dropdown(
|
| 524 |
+
choices=ollama_models,
|
| 525 |
+
value=ollama_models[0] if ollama_active else None,
|
| 526 |
+
label="Ollama Model",
|
| 527 |
+
visible=ollama_active and False,
|
| 528 |
+
scale=1
|
| 529 |
+
)
|
| 530 |
+
|
| 531 |
+
with gr.Row():
|
| 532 |
+
backend_selector = gr.Radio(
|
| 533 |
+
choices=["Gemini (Cloud)", "ComfyUI (Local)"],
|
| 534 |
+
value="Gemini (Cloud)",
|
| 535 |
+
label="Image Generation Backend",
|
| 536 |
+
scale=2
|
| 537 |
+
)
|
| 538 |
+
with gr.Column(scale=1):
|
| 539 |
+
refine_btn = gr.Button("🧠 Refine Prompt", variant="primary")
|
| 540 |
+
gen_img_btn = gr.Button("🖼️ Generate Image", variant="primary")
|
| 541 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 542 |
with gr.Column(scale=1):
|
| 543 |
+
gr.Markdown("### 📝 Prompts & Output")
|
| 544 |
+
prompt_output = gr.Textbox(label="Generated Technical Prompt", lines=4, interactive=False)
|
| 545 |
+
regenerate_btn = gr.Button("✨ Randomize Features", variant="secondary")
|
| 546 |
+
refined_output = gr.Textbox(label="Refined Artistic Prompt", lines=6, interactive=True)
|
| 547 |
+
|
| 548 |
+
gr.Markdown("---")
|
| 549 |
+
image_output = gr.Image(label="Portrait", show_label=False)
|
| 550 |
download_img_btn = gr.DownloadButton("📥 Download Portrait (PNG)", variant="secondary", visible=False)
|
|
|
|
| 551 |
status_msg = gr.Markdown("")
|
| 552 |
+
download_file = gr.File(label="Saved Character JSON", visible=False)
|
| 553 |
|
| 554 |
all_input_components = dropdowns + extra_texts
|
| 555 |
|
|
|
|
| 592 |
)
|
| 593 |
|
| 594 |
# Save/Load Logic
|
| 595 |
+
|
| 596 |
+
# Simple toggle for Ollama model visibility
|
| 597 |
+
def toggle_ollama_visibility(backend):
|
| 598 |
+
return gr.update(visible=(backend == "Ollama (Local)"))
|
| 599 |
+
|
| 600 |
+
refinement_backend.change(
|
| 601 |
+
fn=toggle_ollama_visibility,
|
| 602 |
+
inputs=refinement_backend,
|
| 603 |
+
outputs=ollama_model_dropdown
|
| 604 |
+
)
|
| 605 |
+
|
| 606 |
save_btn.click(
|
| 607 |
fn=save_character,
|
| 608 |
inputs=dropdowns + checkboxes + extra_texts,
|
design.md
CHANGED
|
@@ -49,4 +49,17 @@ The app uses a 3-stage prompt pipeline:
|
|
| 49 |
- **Image Local**: ComfyUI (Local Server)
|
| 50 |
- **Configuration**:
|
| 51 |
- `.env` for API keys and connection hosts/ports (ComfyUI, Ollama).
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
- **Image Local**: ComfyUI (Local Server)
|
| 50 |
- **Configuration**:
|
| 51 |
- `.env` for API keys and connection hosts/ports (ComfyUI, Ollama).
|
| 52 |
+
## Maintenance & Development Lessons
|
| 53 |
+
|
| 54 |
+
### 1. Server Lifecycle
|
| 55 |
+
- **Mandatory Restarts**: Any modification to `app.py` or configuration files (`features.yaml`, `prompts.yaml`) requires a manual restart of the Gradio server to take effect. The UI does not hot-reload backend logic or YAML data.
|
| 56 |
+
- **Process Management (CRITICAL)**: Always ensure the previously running process is fully terminated before starting a new one. Gradio will automatically increment the port (7860 -> 7861 -> 7862) if the old process is still bound to the port.
|
| 57 |
+
- **Command**: Use `Get-Process -Name python | Stop-Process -Force` in PowerShell to clear all existing instances before a fresh run.
|
| 58 |
+
|
| 59 |
+
### 2. UI Verification
|
| 60 |
+
- **Data Dependency**: When testing UI components or dropdowns via browser automation, verify that the target values exist in `features.yaml` first. Testing with invalid data (e.g., "Silver" hair color before it was added) leads to verification failures.
|
| 61 |
+
- **UI State Reset**: Always refresh the browser page after a server restart to ensure the frontend state is synchronized with the new backend.
|
| 62 |
+
|
| 63 |
+
### 3. Documentation & Artifacts
|
| 64 |
+
- **Path Formatting**: For markdown artifacts (like `walkthrough.md`), use absolute paths for media embeds. On Windows, prefix with a leading slash (e.g., `/C:/Users/...`) to comply with internal linting and ensuring reliable rendering.
|
| 65 |
+
- **Visual Proof**: Capture final screenshots of UI layouts to provide immediate visual confirmation of successes, especially after complex layout reorganizations.
|
features.yaml
CHANGED
|
@@ -37,6 +37,7 @@ appearance:
|
|
| 37 |
Blonde: "golden blonde"
|
| 38 |
Red: "fiery red"
|
| 39 |
White: "snow white"
|
|
|
|
| 40 |
Grey: "silver grey"
|
| 41 |
Blue: "deep sapphire blue"
|
| 42 |
Green: "forest green"
|
|
|
|
| 37 |
Blonde: "golden blonde"
|
| 38 |
Red: "fiery red"
|
| 39 |
White: "snow white"
|
| 40 |
+
Silver: "shimmering silver"
|
| 41 |
Grey: "silver grey"
|
| 42 |
Blue: "deep sapphire blue"
|
| 43 |
Green: "forest green"
|