Onboarding follows the vision provider

The wizard's fresh config.toml now carries both [vision.*] blocks, and
its credential pass reads the ACTIVE provider before prompting: an
anthropic setup asks for ANTHROPIC_API_KEY as before, a keyed
openai-compatible endpoint asks for its configured key_env instead,
and a keyless local runtime says so and asks for nothing. Doc sweep
for the same: README's stage list and knobs line, CLAUDE.md's config
summary (which still claimed username lived there), the Help flow's
"Claude vision" wording, and .env.example's ANTHROPIC_API_KEY note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
Eric Wagoner
2026-08-03 18:48:22 -04:00
co-authored by Claude Fable 5
parent 118503e4e0
commit 15d3120029
6 changed files with 89 additions and 6 deletions
+2
View File
@@ -8,4 +8,6 @@ BGG_USERNAME=
BGG_PASSWORD= BGG_PASSWORD=
# Anthropic API key for the vision extract stage # Anthropic API key for the vision extract stage
# default vision provider; not needed for an openai-compatible/local
# setup — see the [vision.*] blocks in config.toml
ANTHROPIC_API_KEY= ANTHROPIC_API_KEY=
+1 -1
View File
@@ -21,7 +21,7 @@ Full design lives in `bgg-shelf-pipeline-spec.md` (read it before changing pipel
- `uv sync` — install deps (Python 3.12+, managed by **uv**; use `uv add`, never pip). `uv run bggpipe init` handles first-run setup (folders, .env credentials, the one-time `playwright install chromium`). - `uv sync` — install deps (Python 3.12+, managed by **uv**; use `uv add`, never pip). `uv run bggpipe init` handles first-run setup (folders, .env credentials, the one-time `playwright install chromium`).
- `uv run bggpipe web` — the app: seven pages (Pipeline `/`, Photos, Titles, Review, Queue, Library, Help) in a shared sidebar shell (responsive: hamburger nav + stacked tables under 900px); stage runs execute one-at-a-time in a background job. `--lan` binds 0.0.0.0 behind a per-device access key: persisted in `data/.lan_key` (gitignored), printed as a QR at startup, cookie-paired for a year, required on EVERY network request (loopback clients and `/static/*` are exempt; the Host/Origin guard still applies). Phone camera uploads (generic `image.jpg` names) get minted `shelf-<timestamp>` names — only explicitly-named files trigger the replace-to-reshoot flow. - `uv run bggpipe web` — the app: seven pages (Pipeline `/`, Photos, Titles, Review, Queue, Library, Help) in a shared sidebar shell (responsive: hamburger nav + stacked tables under 900px); stage runs execute one-at-a-time in a background job. `--lan` binds 0.0.0.0 behind a per-device access key: persisted in `data/.lan_key` (gitignored), printed as a QR at startup, cookie-paired for a year, required on EVERY network request (loopback clients and `/static/*` are exempt; the Host/Origin guard still applies). Phone camera uploads (generic `image.jpg` names) get minted `shelf-<timestamp>` names — only explicitly-named files trigger the replace-to-reshoot flow.
- `uv run bggpipe <stage>` — run a pipeline stage. Non-secret settings come from `config.toml` (username, dirs, vision model, rate limit); `--config` overrides the path. - `uv run bggpipe <stage>` — run a pipeline stage. Non-secret settings come from `config.toml` (dirs, rate limit, `vision_provider` + per-provider `[vision.*]` blocks — "anthropic" or any OpenAI-compatible endpoint incl. local Ollama); `--config` overrides the path.
- `uv run pytest` — the suite runs fully offline against fixtures. Tests marked `live` hit the real BGG API (read-only) and are skipped unless you pass `--run-live`. - `uv run pytest` — the suite runs fully offline against fixtures. Tests marked `live` hit the real BGG API (read-only) and are skipped unless you pass `--run-live`.
- `uv run ruff check` / `uv run ruff format` — lint (rules E, F, I, UP, B, SIM) and format. - `uv run ruff check` / `uv run ruff format` — lint (rules E, F, I, UP, B, SIM) and format.
+2 -2
View File
@@ -19,7 +19,7 @@ BGG has no bulk import and no write API. Cataloging a few hundred games by hand
## How it works ## How it works
1. **extract** — Shelf photos go to the Anthropic API (Claude vision), which reads game titles off spines and boxes along with edition cues: publisher, edition wording, print year, language. 1. **extract** — Shelf photos go to a vision model (Claude by default; any OpenAI-compatible endpoint or a local Ollama model works), which reads game titles off spines and boxes along with edition cues: publisher, edition wording, print year, language.
2. **resolve** — Titles are matched to BGG game IDs via the [XML API2](https://boardgamegeek.com/wiki/page/BGG_XML_API2) (exact + fuzzy matching, popularity tiebreaks), then edition cues are matched against BGG's version list for each game. Anything uncertain is flagged rather than guessed. 2. **resolve** — Titles are matched to BGG game IDs via the [XML API2](https://boardgamegeek.com/wiki/page/BGG_XML_API2) (exact + fuzzy matching, popularity tiebreaks), then edition cues are matched against BGG's version list for each game. Anything uncertain is flagged rather than guessed.
3. **review** — A local review step for ambiguous matches: pick the right game/version, or leave the version blank. Wrong guesses never reach your collection. 3. **review** — A local review step for ambiguous matches: pick the right game/version, or leave the version blank. Wrong guesses never reach your collection.
4. **diff** — Your existing BGG collection is fetched and compared, per copy (owning one edition of a game doesn't hide a second edition you also own). 4. **diff** — Your existing BGG collection is fetched and compared, per copy (owning one edition of a game doesn't hide a second edition you also own).
@@ -93,7 +93,7 @@ Secrets live in environment variables only, never in config files, code, or logs
| `BGG_USERNAME` | diff, upload, enrich | Your BGG username (public, but kept in `.env` so it lives in one place) | | `BGG_USERNAME` | diff, upload, enrich | Your BGG username (public, but kept in `.env` so it lives in one place) |
| `BGG_PASSWORD` | upload (website login) | Your BGG password | | `BGG_PASSWORD` | upload (website login) | Your BGG password |
Non-secret knobs (`photos_dir`, `data_dir`, the vision model, the rate limit) live in `config.toml`. From here you can drive everything from the browser: Non-secret knobs (`photos_dir`, `data_dir`, the per-provider vision blocks, the rate limit) live in `config.toml`. From here you can drive everything from the browser:
```sh ```sh
uv run bggpipe web # opens http://127.0.0.1:8377/ — the whole app in the browser uv run bggpipe web # opens http://127.0.0.1:8377/ — the whole app in the browser
+40 -2
View File
@@ -28,8 +28,22 @@ CONFIG_TEMPLATE = """\
photos_dir = "photos" photos_dir = "photos"
data_dir = "data" data_dir = "data"
model = "claude-sonnet-5"
rate_limit_seconds = 2.0 rate_limit_seconds = 2.0
# Which vision backend reads your shelf photos. Both recipes below stay
# on file; this line picks one.
vision_provider = "anthropic"
[vision.anthropic]
# reads ANTHROPIC_API_KEY from the environment
model = "claude-sonnet-5"
[vision."openai-compatible"]
# OpenAI, OpenRouter, or a local runtime (Ollama, LM Studio, vLLM).
# key_env names the env var holding the key; "" = endpoint needs none.
base_url = "http://localhost:11434/v1"
model = "qwen2.5vl:7b"
key_env = ""
""" """
ENV_HEADER = """\ ENV_HEADER = """\
@@ -159,9 +173,33 @@ def run_init(
typer.echo(f" found {config_path}") typer.echo(f" found {config_path}")
# -- credentials ---------------------------------------------------- # -- credentials ----------------------------------------------------
# the vision key follows the configured provider: an openai-compatible
# setup prompts for ITS key env (or none, for a local endpoint)
from bggpipe.config import load_config
active = load_config(config_path if config_path.exists() else None)
env_keys = list(ENV_KEYS)
if active.vision_provider != "anthropic":
env_keys = [k for k in env_keys if k[0] != "ANTHROPIC_API_KEY"]
if active.vision_key_env:
env_keys.insert(
0,
(
active.vision_key_env,
True,
"vision extraction via "
+ (active.vision_base_url or "the configured endpoint"),
"your vision provider's console",
),
)
else:
typer.echo(
" vision: openai-compatible endpoint with no key configured "
"(local runtime) — nothing to prompt for"
)
env_path = project_dir / ".env" env_path = project_dir / ".env"
in_file = _env_file_keys(env_path) in_file = _env_file_keys(env_path)
for key, secret, why, where in ENV_KEYS: for key, secret, why, where in env_keys:
if os.environ.get(key) or key in in_file: if os.environ.get(key) or key in in_file:
report.keys_ready.append(key) report.keys_ready.append(key)
typer.echo(f" {key}: set") typer.echo(f" {key}: set")
+1 -1
View File
@@ -12,7 +12,7 @@
<div class="card prose"> <div class="card prose">
<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> <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> <ol>
<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>extract</b> — every photo goes to the vision model once (Claude by default; a local model works too — see the README); 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>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>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>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>
+43
View File
@@ -147,3 +147,46 @@ def test_env_file_is_owner_only_from_creation(tmp_path, monkeypatch):
_clear_env(monkeypatch) _clear_env(monkeypatch)
_run(tmp_path, answers={"BGG_API_TOKEN": "tok-123"}) _run(tmp_path, answers={"BGG_API_TOKEN": "tok-123"})
assert _os.stat(tmp_path / ".env").st_mode & 0o777 == 0o600 assert _os.stat(tmp_path / ".env").st_mode & 0o777 == 0o600
def test_vision_key_prompt_follows_the_configured_provider(tmp_path, monkeypatch):
_clear_env(monkeypatch)
monkeypatch.delenv("MY_VISION_KEY", raising=False)
# a local, keyless openai-compatible setup: no Anthropic prompt at all
(tmp_path / "config.toml").write_text(
"""
vision_provider = "openai-compatible"
[vision."openai-compatible"]
base_url = "http://localhost:11434/v1"
model = "qwen2.5vl:7b"
key_env = ""
"""
)
report = _run(tmp_path, interactive=False)
assert "ANTHROPIC_API_KEY" not in report.keys_missing
# a keyed endpoint prompts for ITS env var instead
(tmp_path / "config.toml").write_text(
"""
vision_provider = "openai-compatible"
[vision."openai-compatible"]
base_url = "https://openrouter.ai/api/v1"
model = "some/vision-model"
key_env = "MY_VISION_KEY"
"""
)
report = _run(tmp_path, interactive=False)
assert "MY_VISION_KEY" in report.keys_missing
assert "ANTHROPIC_API_KEY" not in report.keys_missing
def test_fresh_config_template_carries_both_vision_blocks(tmp_path, monkeypatch):
_clear_env(monkeypatch)
_run(tmp_path, interactive=False)
from bggpipe.config import load_config
cfg = load_config(tmp_path / "config.toml")
assert cfg.vision_provider == "anthropic"
assert cfg.model == "claude-sonnet-5" # active block applied cleanly