All code must be in English — comments, log messages, docstrings, variable names, string literals visible in logs. No Polish in source files. When editing existing code that contains Polish log messages or comments, translate them to English.
- First think through the problem, read the codebase for relevant files.
- Before you make any major changes, check in with me and I will verify the plan.
- Please every step of the way just give me a high level explanation of what changes you made
- Make every task and code change you do as simple as possible yet not naive. We want to avoid making any massive or complex changes. Every change should impact as little code as possible. Everything is about simplicity.
- Maintain a documentation file that describes how the architecture of the app works inside and out.
- Maintain a documentation files in the projects. Recognize which are technical and which are more human redable (manual, program description, readme)
- Never speculate about code you have not opened. If the user references a specific file, you MUST read the file before answering. Make sure to investigate and read relevant files BEFORE answering questions about the codebase. Never make any claims about code before investigating unless you are certain of the correct answer - give grounded and hallucination-free answers.
- Never use workarounds. Especially never change existing code just to fix your freshly made problem. Only recent changes are supposed to be fixed. If situation requires fixing existing code it requires user one-time approval.
- NEVER EVER USE -Force or -f (force attribute) in terminal commands. It is strictly forbidden! If there is no other way you NEED to ask the user to run the command in terminal themselves providing justification.
Star Trek Online build planning tool with ML-based screenshot recognition.
- SETS (STO Equipment and Trait Selector) — build planner (Qt GUI)
- WARP (Weaponry & Armament Recognition Platform) — screenshot recognition module
- WARP CORE — ML trainer UI for reviewing/correcting recognition results
Stack: Python 3.13.2, PySide6, OpenCV, PyTorch, EasyOCR
Entry point: sets_warp.sh (Linux/macOS) or sets_warp.bat (Windows) → bootstrap.py → main.py
sets-warp/
├── main.py # App entry
├── bootstrap.py # Env setup (portable Python, venv)
├── sets_warp.sh / sets_warp.bat # Launch scripts
├── src/ # SETS core
│ ├── app.py # Main window
│ ├── callbacks.py # UI callbacks (select_ship, tier_callback, …)
│ ├── buildupdater.py # align_space_frame, update_boff_seat, …
│ ├── constants.py # BOFF_RANKS, SHIP_TEMPLATE, …
│ ├── datafunctions.py # cache loading, ship selector
│ └── setsdebug.py # logging: from src.setsdebug import log
└── warp/ # WARP module
├── warp_button.py # Injects ⚡WARP and 🧠WARP CORE buttons
├── warp_importer.py # Full import pipeline
├── warp_dialog.py # WARP import dialog (multi-step QDialog)
├── recognition/
│ ├── text_extractor.py # OCR: ship name/type/tier, screen type
│ ├── screen_classifier.py # Screen type ML (MobileNetV3-Small .pt)
│ ├── layout_detector.py # Bbox detection per slot
│ └── icon_matcher.py # Icon matching (template + histogram + EfficientNet)
├── trainer/
│ ├── trainer_window.py # WARP CORE main window (QMainWindow)
│ ├── annotation_widget.py # Canvas widget (screenshot + bbox overlay)
│ ├── training_data.py # TrainingDataManager, AnnotationState
│ ├── local_trainer.py # LocalTrainWorker (EfficientNet fine-tune)
│ ├── screen_type_trainer.py # ScreenTypeTrainerWorker (MobileNetV3)
│ └── sync.py # HuggingFace sync
├── knowledge/
│ └── sync_client.py # Community pHash knowledge sync
└── models/ # Trained .pt model files
├── icon_classifier.pt
├── icon_classifier_meta.json
├── screen_classifier.pt
└── screen_classifier_labels.json
Training data: warp/training_data/annotations.json + crop PNGs
ONNX dynamo exporter produced uniform-output models (conf=0.15 for all classes).
Replaced with torch.save(model.state_dict(), 'model.pt').
icon_classifier.pt— EfficientNet-B0, fine-tuned on confirmed cropsscreen_classifier.pt— MobileNetV3-Small, fine-tuned on confirmed screenshots
from_trainer=True flag skips OCR (trainer always has confirmed annotations).
-
OCR (WARP dialog only) —
TextExtractor.extract_ship_info():- Wide top-band scan (20% height), anchored on Tier token (
T6-X2etc.) - Two-stage: fast partial → full image fallback for MIXED screens
- Slot labels used as screen type signals
- Wide top-band scan (20% height), anchored on Tier token (
-
ShipDB lookup (
ship_list.json, 783 ships) — type-first:- Exact
typefield match - Word-subset match (OCR omits subtype words:
"Fleet Temporal Science Vessel"→"Fleet Nautilus Temporal Science Vessel"); multiple candidates ranked by boff seating similarity (Jaccard) then fewest extra words - Fuzzy match (cutoff 0.68)
- Keyword-based fallback profile
- Exact
-
Confirmed annotations (from
annotations.json):- Override slot counts (confirmed = authoritative)
- Supply exact ground-truth bboxes → bypass pixel analysis
- Provide ship name/type/tier when OCR unavailable
- Extract boff seating for ship disambiguation
-
Layout detection (
layout_detector.py):- Strategy 1: confirmed annotations as direct bboxes (most accurate)
- Strategy 2: pixel analysis (counts bright cells right-to-left)
- Single-slot rows (Deflector=1, Engines=1, etc.) always use profile count exactly
- Strategy 3: learned layouts (anchors.json)
- Strategy 4: default calibration anchors
-
Icon matching (
icon_matcher.py):- Template matching (session examples) → HSV histogram k-NN → local PyTorch EfficientNet → HF ONNX fallback
MIN_ACCEPT_CONF = 0.40SLOT_VALID_TYPESdict enforces console/weapon type constraints
-
Write to build via
slot_equipment_item/slot_trait_item
After recognition, auto-selects ship from cache.ships:
- Word-subset match on
r.ship_typeagainst cache keys - Calls select_ship logic:
exec_in_thread(fromsrc.widgets),align_space_frame,_save_session_slots/_restore_session_slots - Tier set from OCR result (
T6-X2etc.)
extract_boff_seating_from_annotations()— groupsBoff *annotations by y-proximity (≤10px = same row)score_ship_boff_match()— Jaccard similarity between detected profession set and ship'sboffsfield
┌──────────────────┬──────────────────────────┬───────────────────────┐
│ LEFT PANEL │ CENTER PANEL │ RIGHT PANEL │
│ min 400px │ min 400px │ min 400px │
│ │ │ │
│ Screenshots │ ┌─────────────────────┐ │ Recognition Review │
│ (file list) │ │ SCROLL AREA │ │ (review list) │
│ │ │ + CANVAS │ │ │
│ │ │ (AnnotationWidget)│ │ [+ Add BBox] [- Rm] │
│ progress bar │ └─────────────────────┘ │ │
│ │ ┌─────────────────────┐ │ ☐ Auto ≥ [0.75] │
│ │ │ BOTTOM PANEL │ │ [ Accept (Enter) ] │
│ │ │ Slot / Item / Acc │ │ │
│ │ └─────────────────────┘ │ │
└──────────────────┴──────────────────────────┴───────────────────────┘
Splitter initial sizes: [400, 700, 400]
Zoom (Gwenview-style):
- Image loads at 1:1 if it fits viewport; scales down to fit if larger (
min(1.0, min(vp_w/pw, vp_h/ph))) _fit_scalecomputed in_compute_transform()from parent (viewport) size — updated on viewport resize_user_scale = None→ fit-to-window;_user_scale = float→ explicit zoomsetWidgetResizable(False)on scroll area — widget grows beyond viewport in zoom mode → scrollbars appearsizeHint(): returns viewport size in fit mode,image × scalein zoom mode- Ctrl+wheel: zoom in/out anchored to cursor;
WarpCoreWindowglobal filter forwards wheel from scroll area padding to canvas (no click needed) - Viewport resize event filter on parent: calls
_compute_transform+adjustSizeso fit-to-window adapts on window resize
Modifier key cursors (IMPORTANT — known pitfall):
- DO NOT use
widget.setCursor()for Ctrl/Alt/Shift cursor changes — it only works when mouse is physically over that widget - Use
QApplication.setOverrideCursor()/restoreOverrideCursor()— applies globally regardless of mouse position - Helpers:
_set_mod_cursor(cursor)and_clear_mod_cursor()with_mod_cursor_activeflag to avoid stacking enterEventre-applies mod cursor if modifier is still held; clears stale override if no modifier held
Alt+LMB draw:
- Hold Alt over canvas → cursor changes to colored crosshair (
DRAW_BBOX_COLOR) - Alt+LMB drag → draws new bbox, triggers icon matching, auto-accept if conf ≥ threshold
enterEvent/leaveEventmanage mod cursor- Global
QApplication.installEventFilterfor Alt/Ctrl/Shift key detection
Color constants (change one value to update all):
DRAW_BBOX_COLOR = QColor(255, 200, 0) # bbox rect + fill + crosshair cursorKeyboard shortcuts:
| Key | Action |
|---|---|
| Enter | Accept current item |
| Del / Backspace | Remove selected bbox (canvas or review list) |
| Alt+A | Toggle Add BBox mode |
| Alt+D | Toggle Mark Done / Back to Edit |
| Alt+R | Remove selected bbox |
| Alt+LMB drag | Draw new bbox directly |
| Ctrl+wheel | Zoom 1×–6× anchored to cursor |
- Checkbox
☐ Auto ≥ [0.75]persisted viaQSettings(warp_core/auto_accept_enabled,warp_core/auto_accept_conf) _apply_auto_accept()called before list draw — marks high-conf items as confirmed in-place- Also triggers after Add BBox matching and after Auto-Detect recognition
- Completer selection (picking from dropdown) auto-accepts immediately — no Enter needed
When confirming an item, checks if bbox overlaps (>70%) any existing confirmed bbox of a different slot → shows QMessageBox.warning.
| Type | Slot group |
|---|---|
SPACE_EQ |
Space equipment + Ship Name/Type/Tier |
GROUND_EQ |
Ground equipment |
TRAITS |
Space + Ground traits |
BOFFS |
Bridge officer abilities |
SPECIALIZATIONS |
Primary/Secondary specialization |
SPACE_MIXED |
All space slots |
GROUND_MIXED |
All ground slots |
UNKNOWN |
All slots |
| Item type | Allowed slots |
|---|---|
| Universal Console | Universal, Tactical, Engineering, Science |
| Tactical Console | Tactical, Universal |
| Engineering Console | Engineering, Universal |
| Science Console | Science, Universal |
SLOT_VALID_TYPES in warp_importer.py enforces this at recognition time.
- T6-X: +1 Universal Console slot
- T6-X2: +1 Device slot, +1 Starship Trait slot
- Fleet variants: +1 console vs base ship
- Fore/aft weapon cross-validation — not enforced by WARP (fore-only weapons could land in aft slots)
- Boff rank in MIXED screens — unknown; abilities from multiple seats share similar y-coords
- Direct slot-scoped autocomplete — annotation widget shows all items for slot group, not filtered by exact slot type
- Cloudflare blocks stowiki — cargo data falls back to GitHub cache (
STOCD/SETS-Data) cloudscraper/curl_cffi— inpyproject.tomlbut never imported
from src.setsdebug import log
log.info('message') # appears in SETS log panel
log.debug('...')
log.warning('...')"Logging" always means both: writing to the log file and printing to the terminal. Never log to only one destination. Always use src.setsdebug.log — do NOT use logging.getLogger(__name__) in WARP code, as that bypasses the SETS log panel and terminal output.
All WARP CORE logs are prefixed with context (e.g. WarpImporter:, LayoutDetector:, AW.zoom).
- Add to
SLOT_GROUPSintrainer_window.py - Add to
SLOT_VALID_TYPESinwarp_importer.py - Add to
SLOT_MAPinwarp_dialog.py - Add to
_SPACE_EQ_LABELSor_GROUND_EQ_LABELSintext_extractor.py
User confirms bbox in WARP CORE
→ TrainingDataManager.add_annotation() → annotations.json + crop PNG
→ Train Model → LocalTrainWorker fine-tunes EfficientNet
→ icon_classifier.pt saved to warp/models/
→ icon_matcher.py loads .pt on next match
# In warp_dialog.py _apply_to_sets()
from src.callbacks import _save_session_slots, _restore_session_slots, align_space_frame
from src.widgets import exec_in_thread
# set button text, load image async, populate tier combo,
# call align_space_frame(sets, ship_data, clear=False)| File | Trigger | Purpose |
|---|---|---|
release.yml |
push: tags: v* |
Creates GitHub Release from tag |
build_installer.yml |
push: tags: v* |
Builds Windows .exe installer and attaches to release |
Release flow: push tag vX.Y → both workflows fire simultaneously → release created + installer built and attached.
Tag format: vMAJOR.MINOR — no b suffix (beta phase ended at v2.0). Examples: v2.8, v2.9, v3.0.
Known pitfall: build_installer.yml previously triggered on release: published. This does NOT work when the release is created by another workflow using GITHUB_TOKEN — GitHub blocks cross-workflow event propagation with default tokens. Changed to push: tags to trigger directly.
Creating a release:
git tag v2.8 && git push origin v2.8Companion FastAPI service deployed on Render. Source: /home/raman/PycharmProjects/sets-warp-backend/.
Community pHash knowledge base — separate from the HF training-crop pipeline.
| Endpoint | Description |
|---|---|
GET /health |
Liveness check |
POST /contribute |
Receive crop PNG + label from WARP clients |
GET /knowledge |
Serve merged knowledge.json (phash → item_name) |
POST /admin/merge |
Merge contributions → knowledge.json (requires X-Admin-Key) |
HF Dataset sets-sto/warp-knowledge:
contributions/YYYY-MM-DD/<uuid>.json+.png— raw per-user contributionsknowledge.json— merged, approved knowledge base (majority-vote per phash)
# Dry-run (raport bez zapisu):
cd /home/raman/PycharmProjects/sets-warp-backend
/home/raman/PycharmProjects/sets-warp/.venv/bin/python admin_merge.py
# Apply (zapisuje knowledge.json do HF):
/home/raman/PycharmProjects/sets-warp/.venv/bin/python admin_merge.py --apply --min 1Credentials in .env: HF_TOKEN, HF_REPO_ID=sets-sto/warp-knowledge, ADMIN_KEY.
| Channel | Repo | Token location | Purpose |
|---|---|---|---|
| pHash knowledge | sets-sto/warp-knowledge |
Render env var only | Community overrides for icon_matcher |
| Training crops | sets-sto/sto-icon-dataset |
warp/hub_token.txt |
EfficientNet fine-tuning data |
WARPSyncClient (warp/knowledge/sync_client.py) talks to the Render backend.
SyncWorker (warp/trainer/sync.py) uploads directly to HF.
| File | Change |
|---|---|
warp/trainer/trainer_window.py |
WARP CORE main window — UI fixes + sync logging |
warp/trainer/annotation_widget.py |
Canvas widget — zoom, cursors, bbox colors, selection |
warp/trainer/sync.py |
Added _slog logging for HF sync milestones |
warp/recognition/layout_detector.py |
Fixed pixel_count=1 for multi-slot rows |
warp/warp_importer.py |
Fixed ShipDB crash when name/type field is list |
returnPressed→QTimer.singleShot(0, self._review_list.setFocus)— deferred focus return after all signals settle_on_completer_activated→ calls_on_accept()immediately thensetFocuson review list- Selecting from dropdown = instant confirm, no Enter needed
_ann_widget.installEventFilter(self)added after widget creationeventFilterextended:obj in (rl, aw)— Delete/Backspace triggers_on_remove_item()from either widget- Guarded with
getattrto avoidAttributeErrorduring UI build
QApplication.instance().installEventFilter(self)in_setup_shortcutsremoveEventFilterinWarpCoreWindow.closeEventeventFilterinterceptsQEvent.Type.Wheelat app level- Checks if mouse is globally over scroll_area rect before forwarding to
ann_widget.wheelEvent - Single handler —
scroll_area.installEventFilterremoved to avoid duplicate firing
- Left panel:
setMinimumWidth(400) - Center panel: already
setMinimumWidth(400) - Right panel: already
setMinimumWidth(400) - Splitter initial sizes:
sp.setSizes([400, 700, 400])
- Removed
.exeinstaller reference (doesn't exist) - Removed
install.shreference (doesn't exist) - Correct install:
sets_warp.sh(Linux/macOS),sets_warp.bat(Windows)
mousePressEvent: Alt held →_drawing=True,_alt_draw=True,setCursor(_make_draw_cursor())mouseReleaseEvent: if_alt_draw→ emitannotation_added, reset_alt_draw,unsetCursor()- In
trainer_window.py:_on_bbox_drawnroutes_alt_drawthrough same path as Add BBox button
DRAW_BBOX_COLOR = QColor(255, 200, 0)— single constant controls rect color + fill + cursor_make_draw_cursor()— 12×12px pixmap, 2px pen, hotspot at centerenterEvent— if Alt held on entry → show draw cursorleaveEvent— if not drawing →unsetCursor()showEvent/hideEvent→QApplication.installEventFilter(self)/removeEventFiltereventFilter— intercepts global Alt keypress/release when mouse is over canvas rectmouseMoveEvent— checksQApplication.queryKeyboardModifiers()before resetting cursor; Alt held → preserve draw cursor
- Pen, fill, and cursor all use
DRAW_BBOX_COLOR— change one constant to update all three
- Added
item_deselected = Signal()back (was missing from edited version) - Emitted when user clicks empty area on canvas
_draw_review_itemno longer draws name/slot text next to bbox- Info shown via tooltip in review list only
- Single
_scalestate (no separate_zoommultiplier) _user_scale: float | None—None= fit-to-window,float= explicit zoom_fit_scale: float— computed once atload_image()from viewport size, never changes_compute_transform():user_scale=None→_scale = _fit_scale, centered offsetsuser_scale=float→_scale = _user_scale, offsets=0 (set by wheelEvent)
wheelEvent:- Uses
_fit_scaleas base (stable, not recomputed from growing widget) - Min =
_fit_scale, Max =_fit_scale * 6.0 - Snaps to fit-to-window when
new_s <= fit_s * 1.001 - Anchor: image point under cursor stays fixed during zoom
- Maps cursor from viewport coords to widget coords via
self.mapFrom(vp, QPoint(...)) adjustSize()after zoom → scroll area updates scrollbars
- Uses
sizeHint()→ returnspixmap * _scaleso scroll area knows widget sizeresizeEvent→_compute_transform()+update()(fit-to-window adapts to window resize)setMouseTracking(True)+setFocusPolicy(StrongFocus)in__init__
keyPressEvent: Delete removes selected annotation; Alt handling moved toeventFilterkeyReleaseEventremoved (Alt handled globally)
- Bug:
_count_icons_in_rowscans right-to-left; STO fills slots left-to-right → empty slots on the right stop the scan early →pixel_count=1for all multi-slot rows - Fix: changed
min(max(pixel_count, 1), profile_count + 1)tomin(max(pixel_count, profile_count), profile_count + 1)— ShipDB profile is now the floor, pixel_count can still exceed profile by 1 (T6-X extra slots)
- Bug: some entries in
ship_list.jsonhavename/typeas a list →str.strip()crashed - Fix:
(' '.join(v) if isinstance(v, list) else str(v)).strip()for both fields
- Added
from src.setsdebug import log as _slog(SETS log panel) - Logs: confirmed crop count, daily rate-limit counter, existing HF hash count, each upload (slot + name), final summary
- Logs start of upload, per-file progress (debug), and final OK/BŁĄD
- Bare
except: passreplaced withexcept Exception as e: log.warning(...) finishedconnection moved to separate_on_sync_finishedmethod