Add sprite review GUI (DearPyGui)
- Desktop GUI similar to Soundgen's Training tab - Thumbnail preview, star rating (1-5), text feedback - Filter: all / unrated / top rated - Export dataset as JSONL - Updated README and requirements
This commit is contained in:
@@ -70,6 +70,20 @@ export IMAGEGEN_OUTPUT_DIR=/path/to/output
|
||||
./venv/bin/python server.py
|
||||
```
|
||||
|
||||
### 5. Review & rate sprites (GUI)
|
||||
|
||||
```bash
|
||||
./venv/bin/pip install dearpygui pillow
|
||||
./venv/bin/python review_gui.py
|
||||
```
|
||||
|
||||
Features:
|
||||
- Thumbnail preview of each generated sprite
|
||||
- Star rating (1-5) with one click
|
||||
- Text feedback per sprite
|
||||
- Filter: all / unrated / top rated
|
||||
- Export rated dataset as JSONL for fine-tuning
|
||||
|
||||
Or configure in your MCP client:
|
||||
|
||||
```json
|
||||
|
||||
@@ -3,4 +3,7 @@ diffusers>=0.37.0
|
||||
transformers
|
||||
accelerate
|
||||
torch
|
||||
peft
|
||||
pillow
|
||||
numpy
|
||||
dearpygui
|
||||
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Sprite Review GUI — rate generated sprites, give feedback, export dataset.
|
||||
|
||||
Desktop GUI (DearPyGui) similar to Soundgen's Training tab.
|
||||
Run: ./venv/bin/python review_gui.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import dearpygui.dearpygui as dpg
|
||||
from PIL import Image
|
||||
|
||||
from feedback import FeedbackDB
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
DB_PATH = os.environ.get("IMAGEGEN_DB_PATH", os.path.join(BASE_DIR, "feedback.db"))
|
||||
THUMB_SIZE = 96
|
||||
|
||||
_db = FeedbackDB.open(DB_PATH)
|
||||
_entries = []
|
||||
_thumbnails = {}
|
||||
_filter = "all"
|
||||
|
||||
|
||||
def _load_thumbnail(path, tag):
|
||||
if not path or not os.path.exists(path):
|
||||
return False
|
||||
try:
|
||||
img = Image.open(path).convert("RGBA")
|
||||
img.thumbnail((THUMB_SIZE, THUMB_SIZE), Image.NEAREST)
|
||||
|
||||
bg = Image.new("RGBA", (THUMB_SIZE, THUMB_SIZE), (40, 40, 40, 255))
|
||||
offset = ((THUMB_SIZE - img.width) // 2, (THUMB_SIZE - img.height) // 2)
|
||||
bg.paste(img, offset, img if img.mode == "RGBA" else None)
|
||||
rgb = bg.convert("RGB")
|
||||
|
||||
dpg.add_static_texture(THUMB_SIZE, THUMB_SIZE, list(rgb.getdata()), tag=tag)
|
||||
_thumbnails[tag] = True
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _refresh():
|
||||
global _entries
|
||||
|
||||
if _filter == "unrated":
|
||||
_entries = _db.get_unrated()
|
||||
elif _filter == "top":
|
||||
_entries = _db.top_rated(200, 1)
|
||||
else:
|
||||
_entries = _db.get_all()
|
||||
|
||||
stats = _db.stats()
|
||||
dpg.set_value(
|
||||
"stats_text",
|
||||
f"{stats.total} sprites | {stats.rated} rated | {stats.unrated} unrated | avg {stats.avg_rating:.1f}",
|
||||
)
|
||||
|
||||
dpg.delete_item("sprite_list", children=True)
|
||||
|
||||
for i, entry in enumerate(_entries):
|
||||
with dpg.group(parent="sprite_list", tag=f"row_{entry.id}"):
|
||||
with dpg.group(horizontal=True):
|
||||
thumb_tag = f"thumb_{entry.id}"
|
||||
if _load_thumbnail(entry.image_path, thumb_tag):
|
||||
dpg.add_image(thumb_tag)
|
||||
else:
|
||||
dpg.add_text("[no img]", default_value="[no img]")
|
||||
|
||||
with dpg.group():
|
||||
prompt_display = (
|
||||
entry.prompt[:90] + "..."
|
||||
if len(entry.prompt) > 90
|
||||
else entry.prompt
|
||||
)
|
||||
dpg.add_text(prompt_display, color=(140, 180, 255))
|
||||
|
||||
if entry.image_path:
|
||||
p = entry.image_path
|
||||
if len(p) > 80:
|
||||
p = "..." + p[-77:]
|
||||
dpg.add_text(p, color=(100, 100, 100))
|
||||
|
||||
with dpg.group(horizontal=True):
|
||||
for star in range(1, 6):
|
||||
filled = star <= entry.rating
|
||||
label = "★" if filled else "☆"
|
||||
color = (255, 200, 80) if filled else (80, 80, 90)
|
||||
dpg.add_button(
|
||||
label=label,
|
||||
tag=f"star_{entry.id}_{star}",
|
||||
callback=lambda s, a, e=entry: _rate(e, s),
|
||||
width=24,
|
||||
)
|
||||
dpg.bind_item_theme(
|
||||
f"star_{entry.id}_{star}", _star_theme(color)
|
||||
)
|
||||
|
||||
rating_text = (
|
||||
f"{entry.rating}★" if entry.rating > 0 else "unrated"
|
||||
)
|
||||
dpg.add_text(rating_text, tag=f"rating_label_{entry.id}")
|
||||
|
||||
dpg.add_input_text(
|
||||
hint="feedback...",
|
||||
default_value=entry.feedback or "",
|
||||
tag=f"feedback_{entry.id}",
|
||||
width=180,
|
||||
)
|
||||
|
||||
dpg.add_button(
|
||||
label="Save",
|
||||
callback=lambda s, a, e=entry: _save_feedback(e),
|
||||
width=50,
|
||||
)
|
||||
|
||||
dpg.add_button(
|
||||
label="Del",
|
||||
callback=lambda s, a, e=entry: _delete(e),
|
||||
width=40,
|
||||
)
|
||||
|
||||
if entry.feedback:
|
||||
dpg.add_text(f'saved: "{entry.feedback}"', color=(80, 180, 100))
|
||||
|
||||
dpg.add_separator()
|
||||
|
||||
|
||||
def _rate(entry, star_tag):
|
||||
star_num = int(star_tag.split("_")[-1])
|
||||
current = _get_current_rating(entry.id)
|
||||
new_rating = 0 if current == star_num else star_num
|
||||
|
||||
fb_widget = f"feedback_{entry.id}"
|
||||
feedback = dpg.get_value(fb_widget) if dpg.does_item_exist(fb_widget) else ""
|
||||
feedback = feedback if feedback else None
|
||||
|
||||
_db.update_rating(entry.id, new_rating, feedback)
|
||||
_refresh()
|
||||
|
||||
|
||||
def _get_current_rating(entry_id):
|
||||
for e in _entries:
|
||||
if e.id == entry_id:
|
||||
return e.rating
|
||||
return 0
|
||||
|
||||
|
||||
def _save_feedback(entry):
|
||||
fb_tag = f"feedback_{entry.id}"
|
||||
feedback = dpg.get_value(fb_tag) if dpg.does_item_exist(fb_tag) else ""
|
||||
rating = _get_current_rating(entry.id)
|
||||
_db.update_rating(entry.id, rating, feedback if feedback else None)
|
||||
_refresh()
|
||||
|
||||
|
||||
def _delete(entry):
|
||||
_db.delete(entry.id)
|
||||
_refresh()
|
||||
|
||||
|
||||
def _on_filter(sender, app_data):
|
||||
global _filter
|
||||
_filter = dpg.get_value("filter_radio")
|
||||
_refresh()
|
||||
|
||||
|
||||
def _export_dataset():
|
||||
import dearpygui.dearpygui as dpg_filedialog
|
||||
|
||||
def _do_export(sender, app_data):
|
||||
path = (
|
||||
app_data["file_path_name"]
|
||||
if "file_path_name" in app_data
|
||||
else app_data.get("file_name", "")
|
||||
)
|
||||
if path:
|
||||
count = _db.export_jsonl(path, min_rating=4)
|
||||
dpg.set_value("stats_text", f"Exported {count} examples to {path}")
|
||||
|
||||
with dpg.file_dialog(
|
||||
directory_selector=False,
|
||||
callback=_do_export,
|
||||
width=600,
|
||||
height=400,
|
||||
tag="export_dialog",
|
||||
):
|
||||
dpg.add_file_extension(".jsonl")
|
||||
|
||||
|
||||
def _star_theme(color):
|
||||
with dpg.theme() as theme:
|
||||
with dpg.theme_component(dpg.mvButton):
|
||||
dpg.add_theme_color(dpg.mvThemeCol_Text, color)
|
||||
dpg.add_theme_color(dpg.mvThemeCol_Button, (0, 0, 0, 0))
|
||||
dpg.add_theme_color(dpg.mvThemeCol_ButtonHovered, (60, 60, 60, 50))
|
||||
dpg.add_theme_color(dpg.mvThemeCol_ButtonActive, (40, 40, 40, 50))
|
||||
with dpg.theme_component(dpg.mvButton, enabled_state=False):
|
||||
dpg.add_theme_color(dpg.mvThemeCol_Text, color)
|
||||
return theme
|
||||
|
||||
|
||||
def main():
|
||||
dpg.create_context()
|
||||
|
||||
with dpg.window(tag="main_window"):
|
||||
with dpg.group(horizontal=True):
|
||||
dpg.add_button(label="Refresh", callback=_refresh)
|
||||
dpg.add_button(label="Export Dataset", callback=_export_dataset)
|
||||
|
||||
dpg.add_text("Filter:", indent=20)
|
||||
dpg.add_radio_button(
|
||||
["all", "unrated", "top"],
|
||||
tag="filter_radio",
|
||||
horizontal=True,
|
||||
callback=_on_filter,
|
||||
default_value="all",
|
||||
)
|
||||
|
||||
dpg.add_text("", tag="stats_text", color=(180, 180, 200))
|
||||
dpg.add_separator()
|
||||
|
||||
with dpg.child_window(tag="sprite_list", autosize_x=True, autosize_y=True):
|
||||
pass
|
||||
|
||||
dpg.create_viewport(title="Imagen — Sprite Review", width=1000, height=750)
|
||||
dpg.set_viewport_resizable(True)
|
||||
dpg.configure_app(init_file="")
|
||||
|
||||
texture_registry = dpg.add_texture_registry()
|
||||
dpg.bind_texture_registry(texture_registry)
|
||||
|
||||
dpg.setup_dearpygui()
|
||||
dpg.show_viewport()
|
||||
dpg.set_primary_window("main_window", True)
|
||||
|
||||
_refresh()
|
||||
|
||||
while dpg.is_dearpygui_running():
|
||||
dpg.render_dearpygui_frame()
|
||||
|
||||
dpg.destroy_context()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user