- 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
250 lines
7.7 KiB
Python
250 lines
7.7 KiB
Python
#!/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()
|