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:
Emil
2026-06-28 16:27:52 +03:00
parent c4350ac8a7
commit c4fe56021f
3 changed files with 266 additions and 0 deletions
+14
View File
@@ -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
View File
@@ -3,4 +3,7 @@ diffusers>=0.37.0
transformers
accelerate
torch
peft
pillow
numpy
dearpygui
+249
View File
@@ -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()