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:
co-authored by
Claude Fable 5
parent
118503e4e0
commit
15d3120029
@@ -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=
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user