The proofread checkpoint gets its place: nav order, badge, and a rename
Eric's observation: cleaning up raw reads happens BEFORE resolve, but the app ordered Review ahead of Catalog and never said proofreading was a step at all. The pipeline has two human checkpoints — proofread the reads (after extract), decide the matches (after resolve) — and now the app says so: the sidebar runs Pipeline, Photos, Titles, Review, Queue, Library, Help in true workflow order; the Titles page (né Catalog — the old name suggested a finished collection, which is the Library's job; /catalog redirects) gets a badge counting unresolved shaky reads; an edit marks its entry human-verified so the badge drains as you proofread; the extract stage card nudges toward the proofread before resolving; and the Help flow is rewritten as six stages + two checkpoints with the loop called out. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
co-authored by
Claude Fable 5
parent
25c1432a03
commit
f19b861251
@@ -243,6 +243,7 @@ def _merge(a: dict, b: dict) -> dict:
|
||||
# the fields a human correction may override on an extracted entry
|
||||
EDIT_FIELDS = (
|
||||
"title_raw",
|
||||
"confidence",
|
||||
"publisher_hint",
|
||||
"edition_hint",
|
||||
"year_hint",
|
||||
|
||||
@@ -74,6 +74,7 @@ async function refreshBadges() {
|
||||
if (el) el.textContent = n > 0 ? String(n) : "";
|
||||
};
|
||||
set("photos", p.reshoot);
|
||||
set("titles", p.shaky_reads);
|
||||
set("review", p.pending_review);
|
||||
set("queue", p.to_add + p.to_update);
|
||||
return p;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="pagebar">
|
||||
<a href="#flow">The flow</a>
|
||||
<a href="#pages">Pages</a>
|
||||
<a href="#curation">Fixing the catalog</a>
|
||||
<a href="#curation">Fixing the titles</a>
|
||||
<a href="#statuses">Statuses</a>
|
||||
<a href="#keys">Keyboard</a>
|
||||
<a href="#files">Your data</a>
|
||||
@@ -10,34 +10,35 @@
|
||||
|
||||
<h2 id="flow">The flow: shelves → collection</h2>
|
||||
<div class="card">
|
||||
<p>Six stages, run from the <a href="/">Pipeline</a> page (or the CLI — both share all state and either can pick up where the other left off):</p>
|
||||
<p>Six pipeline stages and two checkpoints that are yours. Stages run from the <a href="/">Pipeline</a> page (or the CLI — both share all state and either can pick up where the other left off):</p>
|
||||
<ol>
|
||||
<li><b>extract</b> — every photo goes to Claude vision once; titles and edition cues (publisher, edition wording, year, language) land in the catalog. Boxes it can see but can't read become <b>reshoot tickets</b> on the Photos page.</li>
|
||||
<li><b>resolve</b> — titles are matched to BoardGameGeek games and editions. Anything uncertain is flagged for review, never guessed. Without a BGG API token, titles wait as <i>awaiting BGG</i> and are picked up automatically once the token exists.</li>
|
||||
<li><b>review</b> — you decide the flagged ones on the <a href="/review">Review</a> page. Every decision saves immediately.</li>
|
||||
<li><b>extract</b> — every photo goes to Claude vision once; what it reads (titles plus edition cues: publisher, edition wording, year, language) lands on the Titles page. Boxes it can see but can't read become <b>reshoot tickets</b> on the Photos page.</li>
|
||||
<li><b>proofread</b> <i>(you, on <a href="/titles">Titles</a>)</i> — fix misread titles, add cues you know, split multi-box lines, remove non-games, <a href="#curation">details below</a>. Worth doing <i>before</i> resolve: a fix made now is one BGG search done right; a fix made later sends the title back through resolve again.</li>
|
||||
<li><b>resolve</b> — titles are matched to BoardGameGeek games and editions. Anything uncertain is flagged, never guessed. Without a BGG API token, titles wait as <i>awaiting BGG</i> and are picked up automatically once the token exists.</li>
|
||||
<li><b>review</b> <i>(you, on <a href="/review">Review</a>)</i> — decide the flagged ones: which game, which edition, whether two reads are one box. Every decision saves immediately.</li>
|
||||
<li><b>diff</b> — your existing BGG collection is fetched and compared, per copy. What's genuinely new lands in the <a href="/queue">Queue</a>.</li>
|
||||
<li><b>upload</b> — a real browser logs into BGG and adds each queued game, slowly and politely. Always dry-run first; the buttons enforce that order.</li>
|
||||
<li><b>enrich</b> — full metadata (players, weight, rank, artwork) fills the <a href="/library">Library</a>.</li>
|
||||
</ol>
|
||||
<p>Every stage is resumable: stop anything mid-run and nothing is lost. Re-runs skip work already done.</p>
|
||||
<p>It's a loop, not a line: new photos, edits, and splits feed both checkpoints again, and the sidebar badges show when a page has work for you. Every stage is resumable — stop anything mid-run and nothing is lost; re-runs skip work already done.</p>
|
||||
</div>
|
||||
|
||||
<h2 id="pages">What each page is for</h2>
|
||||
<div class="card">
|
||||
<p><b><a href="/">Pipeline</a></b> — run stages one at a time and watch their live output. Shows what's blocking (missing keys, stub data) and the counts at every step.</p>
|
||||
<p><b><a href="/photos">Photos</a></b> — drag photos in (or drop them in the <code>photos/</code> folder). Each photo has its own page listing every title read from it and any reshoot tickets — boxes seen but not identified. Photograph those up close, drop the new shot in, and extract again. Re-uploading a photo with the same name re-extracts it.</p>
|
||||
<p><b><a href="/titles">Titles</a></b> — every read off your shelves, alphabetized, with its status and photos. This is the proofread checkpoint: <a href="#curation">edit, split, remove</a>. Its badge counts shaky reads (the model wasn't sure) that haven't been resolved or human-verified yet.</p>
|
||||
<p><b><a href="/review">Review</a></b> — the decisions only you can make: which game a title is, which edition a copy is, and whether two same-game reads are really one box (merges show a veto). Keyboard-first; see <a href="#keys">shortcuts</a>.</p>
|
||||
<p><b><a href="/catalog">Catalog</a></b> — every title read from your shelves, alphabetized, with its status and photos. This is also where you <a href="#curation">fix the data itself</a>: edit, split, remove.</p>
|
||||
<p><b><a href="/queue">Queue</a></b> — exactly what upload will do (new entries and version upgrades) and the log of everything it has done. Nothing reaches BGG that isn't visible here first.</p>
|
||||
<p><b><a href="/library">Library</a></b> — your enriched collection: filter by board games or RPGs. RPG matches are identified and enriched but never uploaded — BGG collections can't hold them, so they stay local citizens.</p>
|
||||
</div>
|
||||
|
||||
<h2 id="curation">Fixing the catalog: edit, split, remove</h2>
|
||||
<h2 id="curation">Fixing the titles: edit, split, remove</h2>
|
||||
<div class="card">
|
||||
<p>Vision reads aren't perfect, and you know things the photos don't show. Every catalog line has curation actions, and every one of them is <b>durable</b>: the decision is saved in a small committed file and replayed on every rebuild, so re-running extract or resolve can never undo it.</p>
|
||||
<p>Vision reads aren't perfect, and you know things the photos don't show. Every line on the Titles page has curation actions, and every one of them is <b>durable</b>: the decision is saved in a small committed file and replayed on every rebuild, so re-running extract or resolve can never undo it.</p>
|
||||
<p><b>edit</b> — fix a misread title or add cues you already know (publisher, edition, year, language). A corrected misspelling automatically merges with a correctly-read sighting of the same game from another photo. If the line already had a BGG match, saving re-queues it so resolve searches again with the corrected data.</p>
|
||||
<p><b>split into copies</b> — one line, several physical boxes? Splitting makes each photo its own copy, and each copy picks its own edition afterward. Appears on any line whose title was seen in more than one photo. Splitting one game never affects a same-named different edition.</p>
|
||||
<p><b>remove from catalog</b> (inside the edit panel) — for lines that shouldn't exist at all: a book read as a game, box art misread as a title. The line and its matches are discarded and stay gone. This is different from <i>reject</i> on the Review page, which keeps the line visible as "no BGG match" — right for real games BGG doesn't know.</p>
|
||||
<p><b>remove</b> (inside the edit panel) — for lines that shouldn't exist at all: a book read as a game, box art misread as a title. The line and its matches are discarded and stay gone. This is different from <i>reject</i> on the Review page, which keeps the line visible as "no BGG match" — right for real games BGG doesn't know.</p>
|
||||
<p>Undo: each decision is one record in <code>data/title_edits.json</code>, <code>data/title_splits.json</code>, or <code>data/title_removals.json</code> — delete the record and the next rebuild restores the old state.</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -46,7 +46,12 @@ function render() {
|
||||
· <a href="/queue">inspect the queue</a>`;
|
||||
|
||||
document.getElementById("stages").innerHTML = [
|
||||
stageCard(1, "extract", `read titles off <b>${P.photos}</b> <a href="/photos">photo(s)</a> — <b>${P.titles}</b> so far`, runBtn("extract")),
|
||||
stageCard(1, "extract", `read titles off <b>${P.photos}</b> <a href="/photos">photo(s)</a> — <b>${P.titles}</b> so far`
|
||||
+ (P.titles > 0
|
||||
? P.shaky_reads > 0
|
||||
? ` · <b>${P.shaky_reads}</b> shaky read(s) worth a <a href="/titles">proofread</a> before resolving`
|
||||
: ` · <a href="/titles">proofread the titles</a> before resolving`
|
||||
: ""), runBtn("extract")),
|
||||
stageCard(2, "resolve", `match titles to BGG ids — <b>${(m.auto ?? 0)}</b> auto · <b>${m.ambiguous ?? 0}</b> ambiguous · <b>${m.unmatched ?? 0}</b> unmatched`, runBtn("resolve")),
|
||||
stageCard(3, "review", `<b>${P.pending_review}</b> item(s) waiting for your call`, `<a class="linkbtn" href="/review">Open review</a>`),
|
||||
stageCard(4, "diff", `compare against your BGG collection`, runBtn("diff")),
|
||||
|
||||
@@ -142,7 +142,7 @@ function render() {
|
||||
<div class="done">
|
||||
<h2>${waiting
|
||||
? "Resolved set fully reviewed"
|
||||
: "All reviewed — the catalog is diff-ready"}</h2>
|
||||
: "All reviewed — every title is diff-ready"}</h2>
|
||||
<div class="nums">
|
||||
<div>${s.summary.extracted}<span>extracted</span></div>
|
||||
<div>${s.summary.recognized}<span>recognized</span></div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<h1>Catalog</h1>
|
||||
<h1>Titles</h1>
|
||||
<div class="pagebar"><span id="catcount"></span></div>
|
||||
<div class="filterbar">
|
||||
<input type="search" id="catsearch" placeholder="filter titles…" aria-label="filter catalog titles">
|
||||
@@ -1,6 +1,6 @@
|
||||
"""The local web app: six pages in a shared sidebar shell.
|
||||
|
||||
Pipeline (/), Photos, Review, Catalog, Queue, and Library — rendered
|
||||
Pipeline (/), Photos, Titles, Review, Queue, Library, and Help — rendered
|
||||
server-side from templates/shell.html plus one fragment per page, with
|
||||
static/app.css as the design system and static/app.js as shared client
|
||||
plumbing. No template engine and no build step; served on localhost only.
|
||||
@@ -228,11 +228,13 @@ def _default_stages(cfg: Config) -> dict[str, Callable[..., object]]:
|
||||
|
||||
|
||||
# (href, page-name, label, badge-name) — badge names match app.js
|
||||
# ordered as the real workflow runs: photograph, proofread the reads
|
||||
# (catalog), resolve + decide (review), then what ships (queue, library)
|
||||
NAV_PAGES = (
|
||||
("/", "pipeline", "Pipeline", ""),
|
||||
("/photos", "photos", "Photos", "photos"),
|
||||
("/titles", "titles", "Titles", "titles"),
|
||||
("/review", "review", "Review", "review"),
|
||||
("/catalog", "catalog", "Catalog", ""),
|
||||
("/queue", "queue", "Queue", "queue"),
|
||||
("/library", "library", "Library", ""),
|
||||
("/help", "help", "Help", ""),
|
||||
@@ -544,9 +546,14 @@ def create_app(
|
||||
def review_page() -> str:
|
||||
return render_page("review")
|
||||
|
||||
@app.get("/catalog", response_class=HTMLResponse)
|
||||
def catalog_page() -> str:
|
||||
return render_page("catalog")
|
||||
@app.get("/titles", response_class=HTMLResponse)
|
||||
def titles_page() -> str:
|
||||
return render_page("titles")
|
||||
|
||||
@app.get("/catalog")
|
||||
def catalog_redirect() -> Response:
|
||||
# the page's old name; bookmarks keep working
|
||||
return Response(status_code=307, headers={"Location": "/titles"})
|
||||
|
||||
@app.get("/queue", response_class=HTMLResponse)
|
||||
def queue_page() -> str:
|
||||
@@ -644,6 +651,14 @@ def create_app(
|
||||
"photos": len(photo_names()),
|
||||
"reshoot": len(open_sightings()),
|
||||
"titles": len(session.titles),
|
||||
# unresolved shaky reads: the proofread-before-resolve
|
||||
# signal (an edit marks the entry high — human-verified)
|
||||
"shaky_reads": sum(
|
||||
1
|
||||
for e in session.titles
|
||||
if e.confidence != "high"
|
||||
and e.title_raw not in {r["title_raw"] for r in session.rows}
|
||||
),
|
||||
"matches": dict(match_counts),
|
||||
"pending_review": len(session.pending_rows())
|
||||
+ len(session.version_rows()),
|
||||
@@ -860,6 +875,7 @@ def create_app(
|
||||
record["year_hint"] = int(year) if year else None
|
||||
if not (record.keys() - {"match", "photos"}):
|
||||
raise HTTPException(400, "nothing to change")
|
||||
record["confidence"] = "high" # a human verified this line
|
||||
# write order is crash-safety: drop the stale rows first (worst
|
||||
# case on a crash: resolve recreates them from the uncorrected
|
||||
# entry), then the durable record (replayed by every future
|
||||
|
||||
Reference in New Issue
Block a user