Immich Ring Visualizer is a Flask backend (ring_viz.py) plus a browser
frontend (templates/index.html + static/*.js) for curating LoRA
training datasets from an Immich photo/video library. It uses Immich's
Postgres/pgvector database and InsightFace (buffalo_l) for face
detection/embedding, plus video frame analysis.
ring_viz.py— Flask app: routes, Immich pgvector queries, face/pose detection, video frame extraction (PyAV — reads directly from uploaded bytes, no disk writes), export/crop pipeline.
templates/index.html— page markup, loads the three JS files below viaurl_for('static', ...)in this exact order.static/style.css— styling.static/viz-render.jsstatic/media-ingest.jsstatic/selection-ui.js
The frontend logic originally lived in one large static/app.js. It
was first split into appa.js/appb.js purely by size (roughly even
halves, ~132,000 characters combined) so editing one area didn't
require holding the whole file in context. That split was never
concern-based — it was just "first half / second half".
It was later reorganized into the three concern-based files below, on
branch refactor/js-module-split (commits f461646, c3d624e, off
main at 12d17fa). This reorg was done in two stages:
-
Bootstrap refactor (no file split yet). The original
appa.js/appb.jsrelied on ~27 pieces of top-level "glue" code (event-listener wiring, IIFEs) whose correctness depended on script load order — e.g.init()was called partway throughappa.js, so all ofappb.js's wiring implicitly ran after it. That glue was wrapped into namedwireXxx()functions and called explicitly, in the exact original order, from onebootstrap()function at the end. This made script load order stop mattering for correctness — only "are all functions defined by the timebootstrap()runs" matters, which any file split satisfies. Verified (mechanically, and by hand-testing in browser) to be a pure reorganization: same 166 top-level statements, same execution order, no functional changes. -
Concern-based split. With load-order no longer fragile, the code was split by concern into the three files below. Verified mechanically: every top-level name (function or variable) assigned to exactly one file, no name split across files where doing so would break execution-order assumptions (checked specifically for top-level variable initializers that reference something defined in a different file).
viz-render.js — ring rendering and its supporting UI controls:
- Core ring layout/sizing (
sizeForSim,radiusForSim,render) - Fisheye lens effect (
applyFisheye,attachLensEffect) - Pose/scale picker grids (build/setup/render/sync for both)
- Squeeze & sharpness cutoff controls, ranked-sort wiring
(
applySqueeze,metricValueForRing) - Video ring rendering, pose list, pose-list scrubber
(
renderVideoRing,renderPoseList,setupPoseListScrubber,flashHighlightFrame)
media-ingest.js — getting frames/video/images into the tool:
- File/dropzone handling, frame seeking, shot-scale classification
- Frame playback controls, audio sync
- Video analysis + polling (
startVideoAnalysis,pollAnalysis) - Folder/zip loader (
startFolderAnalysis) - Immich integration: neighbor loading, running/re-running Immich analysis, adding/removing Immich nodes from the ring
selection-ui.js — selection, export, and app shell:
- Selection state (
selectedFrames, save/restore to storage) - Export parameter form (
getExportParams) - Selection modal (open/close/render, pose-scatter view inside it, pose detection for selected items)
- Immich selection bar, search modal
- App init, hover preview, thumbnail/preview URL helpers, sidebar detail panel, result sorting
init()andbootstrap()—bootstrap()is the very last thing that runs; it calls everywireXxx()setup function (across all three files) plusinit(), in the original execution order. Because it lives in the last-loaded file, all three files' functions are guaranteed to be defined by the time it runs.
index.html loads them in this order — this order matters,
since bootstrap() (in selection-ui.js) must load last:
<script src="{{ url_for('static', filename='viz-render.js') }}"></script>
<script src="{{ url_for('static', filename='media-ingest.js') }}"></script>
<script src="{{ url_for('static', filename='selection-ui.js') }}"></script>- These are plain global-scope scripts (not ES modules) — all three
files share one global namespace, same as the original
app.js. A name only needs to be unique once across all three files. - Because
bootstrap()centralizes all setup calls, adding a new top-leveladdEventListener/wiring block should follow the same pattern: wrap it in a namedwireXxx()function in the appropriate file, and add a call to it inbootstrap()(inselection-ui.js) in the position that matches when it should run relative to the existing calls — rather than relying on script load order again. - No file currently holds a dedicated "shared state" module — each
piece of state lives in whichever file's functions use it. If a
future change needs the same state read/written from two of these
three files, that's a signal it may be time to split out a
state.jsloaded first, rather than duplicating or awkwardly cross-referencing state across files.