eberling1 commited on
Commit
c686511
·
1 Parent(s): 055ac65

updated indexer to handle colors

Browse files
Files changed (4) hide show
  1. CLAUDE.md +85 -0
  2. config.py +1 -0
  3. global_index.py +7 -0
  4. holds.py +15 -0
CLAUDE.md ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What This Is
6
+
7
+ A FastAPI application deployed as a Hugging Face Space (Gradio SDK). It acts as an indexer and API server for a climbing holds dataset hosted on Hugging Face Hub. It handles:
8
+ - Webhook-triggered re-indexation when the HF dataset changes
9
+ - Vote processing for community-driven hold identification
10
+ - Anonymous hold contributions
11
+
12
+ ## Running Locally
13
+
14
+ ```bash
15
+ python -m venv .venv && source .venv/bin/activate
16
+ pip install -r requirements.txt
17
+ uvicorn app:app --reload
18
+ ```
19
+
20
+ Required environment variables (see `.gitignore` — use a `.env` file):
21
+ - `HF_TOKEN` — write access to the main dataset
22
+ - `WEBHOOK_SECRET` — validates incoming HF webhooks
23
+ - `HF_REVISION` — branch to read/write (defaults to `staging`)
24
+ - `HF_ANONYMOUS_TOKEN` — write access to the anonymous contributions repo
25
+ - `HF_ANONYMOUS_REPO_ID` — anonymous contributions repo ID
26
+ - `CORS_ALLOWED_ORIGINS` — comma-separated additional CORS origins
27
+
28
+ ## Manual Re-indexation
29
+
30
+ ```bash
31
+ # Dry run (default) — shows what would change without committing
32
+ python reindex.py
33
+
34
+ # Commit the reindex to the dataset
35
+ python reindex.py --commit
36
+
37
+ # Target a specific repo or revision
38
+ python reindex.py --repo-id owner/dataset --revision main --commit
39
+ ```
40
+
41
+ ## Architecture
42
+
43
+ ### Data Flow: Webhook-triggered Re-indexation
44
+ 1. HF dataset webhook fires → `webhooks.py` verifies secret and receives payload
45
+ 2. `global_index.py` loads the current `global_index.json` from the HF dataset
46
+ 3. `hf_repo.py` lists all `metadata.json` files in the dataset
47
+ 4. `holds.py` normalizes/validates each hold's metadata, infers mesh presence, tracks attention items
48
+ 5. A new `train.jsonl` is built from all holds
49
+ 6. `hf_repo.py` commits if content hash changed (idempotent via SHA-256)
50
+
51
+ ### Data Flow: Vote Processing
52
+ 1. POST `/webhooks/vote` → `votes.py` validates payload (rating 1–5, ISO8601 timestamp)
53
+ 2. Voter fingerprint computed from HF username or hashed IP
54
+ 3. Votes stored in per-hold `votes.json`; duplicate votes rejected
55
+ 4. Dominant manufacturer/model inferred from vote aggregate
56
+ 5. Hold `metadata.json` updated if dominant values changed
57
+
58
+ ### Data Flow: Anonymous Contributions
59
+ 1. POST multipart form → `contributions.py` sanitizes filenames and deduplicates
60
+ 2. Files committed to anonymous HF repo under `pending/<uuid>/`
61
+
62
+ ### Key Files
63
+ - `app.py` — FastAPI app, mounts Gradio UI, wires endpoints
64
+ - `config.py` — all constants: repo IDs, file path names, mesh extensions, managed keys
65
+ - `global_index.py` — index bootstrap, reference validation (manufacturers, hold types, status), attention tracking
66
+ - `hf_repo.py` — all Hugging Face Hub I/O (load/save JSON, list files, commit)
67
+ - `holds.py` — metadata normalization, reference validation with fuzzy-match suggestions, `train.jsonl` builder
68
+ - `votes.py` — vote validation, fingerprinting, aggregation
69
+ - `webhooks.py` — webhook route handlers, CORS setup
70
+ - `contributions.py` — anonymous submission handler
71
+ - `reindex.py` — standalone CLI for manual full reindex
72
+
73
+ ### Dataset Structure on HF Hub
74
+ - `global_index.json` — top-level index with allowed references and attention sets
75
+ - `<hold_id>/metadata.json` — per-hold metadata
76
+ - `<hold_id>/votes.json` — per-hold community votes
77
+ - `train.jsonl` — flat JSONL export of all normalized holds
78
+
79
+ ## No Tests
80
+
81
+ There are no automated tests in this project.
82
+
83
+ ## Deployment
84
+
85
+ This runs as a Hugging Face Space (see the YAML frontmatter in `README.md`). The `app_file` is `app.py`. Pushing to the Space repo triggers a redeploy.
config.py CHANGED
@@ -13,6 +13,7 @@ METADATA_FILENAME = "metadata.json"
13
  VOTES_FILENAME = "votes.json"
14
  MESH_EXTENSIONS = {".glb", ".gltf", ".obj", ".stl"}
15
  MANAGED_ATTENTION_KEYS = {
 
16
  "invalid_hold_type_reference",
17
  "invalid_manufacturer_reference",
18
  "invalid_metadata",
 
13
  VOTES_FILENAME = "votes.json"
14
  MESH_EXTENSIONS = {".glb", ".gltf", ".obj", ".stl"}
15
  MANAGED_ATTENTION_KEYS = {
16
+ "invalid_color_reference",
17
  "invalid_hold_type_reference",
18
  "invalid_manufacturer_reference",
19
  "invalid_metadata",
global_index.py CHANGED
@@ -31,6 +31,7 @@ def bootstrap_global_index(repo_id: str) -> dict[str, Any]:
31
  "manufacturers": [],
32
  "hold_types": [],
33
  "status": ["to_render", "to_clean", "to_identify"],
 
34
  },
35
  "stats": {"total_holds": 0, "to_identify": 0},
36
  "needs_attention": {},
@@ -45,6 +46,7 @@ def ensure_allowed_references(global_index: dict[str, Any]) -> dict[str, set[str
45
  manufacturers = allowed_references.setdefault("manufacturers", [])
46
  hold_types = allowed_references.setdefault("hold_types", [])
47
  statuses = allowed_references.setdefault("status", [])
 
48
 
49
  if not isinstance(manufacturers, list) or not isinstance(hold_types, list):
50
  raise RuntimeError(
@@ -55,11 +57,16 @@ def ensure_allowed_references(global_index: dict[str, Any]) -> dict[str, set[str
55
  raise RuntimeError(
56
  "global_index.json must define a list value for 'allowed_references.status'."
57
  )
 
 
 
 
58
 
59
  return {
60
  "manufacturers": {value for value in map(normalize_reference_value, manufacturers) if value},
61
  "hold_types": {value for value in map(normalize_reference_value, hold_types) if value},
62
  "status": {value for value in map(normalize_reference_value, statuses) if value},
 
63
  }
64
 
65
 
 
31
  "manufacturers": [],
32
  "hold_types": [],
33
  "status": ["to_render", "to_clean", "to_identify"],
34
+ "colors": [],
35
  },
36
  "stats": {"total_holds": 0, "to_identify": 0},
37
  "needs_attention": {},
 
46
  manufacturers = allowed_references.setdefault("manufacturers", [])
47
  hold_types = allowed_references.setdefault("hold_types", [])
48
  statuses = allowed_references.setdefault("status", [])
49
+ colors = allowed_references.setdefault("colors", [])
50
 
51
  if not isinstance(manufacturers, list) or not isinstance(hold_types, list):
52
  raise RuntimeError(
 
57
  raise RuntimeError(
58
  "global_index.json must define a list value for 'allowed_references.status'."
59
  )
60
+ if not isinstance(colors, list):
61
+ raise RuntimeError(
62
+ "global_index.json must define a list value for 'allowed_references.colors'."
63
+ )
64
 
65
  return {
66
  "manufacturers": {value for value in map(normalize_reference_value, manufacturers) if value},
67
  "hold_types": {value for value in map(normalize_reference_value, hold_types) if value},
68
  "status": {value for value in map(normalize_reference_value, statuses) if value},
69
+ "colors": {value for value in map(normalize_reference_value, colors) if value},
70
  }
71
 
72
 
holds.py CHANGED
@@ -147,6 +147,21 @@ def validate_metadata(
147
  attention_bucket=needs_attention["invalid_status_reference"],
148
  )
149
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
  def canonical_metadata_defaults() -> list[tuple[str, Any]]:
152
  return [
 
147
  attention_bucket=needs_attention["invalid_status_reference"],
148
  )
149
 
150
+ allowed_colors = allowed_references.get("colors", set())
151
+ if allowed_colors:
152
+ available_colors = metadata.get("available_colors") or []
153
+ if isinstance(available_colors, list):
154
+ for color_value in available_colors:
155
+ color_ref = global_index.normalize_reference_value(color_value)
156
+ if color_ref is not None and color_ref not in allowed_colors:
157
+ warn_about_reference(
158
+ hold_id=hold_id,
159
+ field_name="available_colors",
160
+ value=color_value,
161
+ allowed_values=allowed_colors,
162
+ attention_bucket=needs_attention["invalid_color_reference"],
163
+ )
164
+
165
 
166
  def canonical_metadata_defaults() -> list[tuple[str, Any]]:
167
  return [