Overview
TalentID Global parses karting championship PDF result books into a versioned JSON format.- Output directory:
data/out-json/v2/(tabula engine) ordata/out-json/v3/(Claude Vision engine) - Entry point:
python src/unified_parser.py [--file PATH | --championship FOLDER_OR_NUM] - Tests:
pytest tests/test_output_json.py
CLI
Already-parsed files are skipped. Delete the output JSON to force a reparse. When using
--engine claude, run only one process at a time — concurrent parses cause rate-limit contention and may corrupt output files.JSON Schemas
v2 — Tabula Engine
Output mirrorsdata/in-pdf/ under data/out-json/v2/, with JSONs sitting flat inside the year folder (no per-event subfolder).
Session fields: rawSessionName, name, sessionNumber, type, laps (session lap count), results
Result fields: pos, name, nationality, laps, bestLapTime, bestLapNumber, delta, idealLapTime, gapToLeader, gapToNext, raceTime, bestS1/S2/S3, lapData
Lap fields: lapNumber, lapTime, s1, s2, s3, timeOfDay (omitted when empty)
v2.1 — Classification Extension
Output written todata/out-json/v2.1/ in parallel with v2/. Adds session-level fields:
ClassificationEntry fields:
pos, kartNo (integer string, e.g. "509"), driver, laps, gap, gapNoPenalties, penalty, posNoPenalties, positionsGained, firstLapOffset
v3 — Claude Vision Engine
Output atdata/out-json/v3/<championship>/<year>/<code>.json. Version field: "version": "3.0".
status values: classified, notClassified, DNF, DSQ, retired
Valid type values: practice, qualifying, qualifyingHeat, preFinal, final, race
pos is omitted entirely for non-classified drivers. Do not use "pos": null — the field must be absent.Architecture
Tabula Pipeline (v2 / v2.1)
The tabula pipeline reads PDF pages usingtabula-py in stream mode. Championship-specific subclasses of the base parser handle layout differences in column detection, driver name extraction, and sector analysis.
Coordinate configuration lives in config/areas/*.yaml — one file per championship. The Championship dataclass loads all 16 championships from YAML at import time via Championship.from_yaml().
Key files:
Claude Vision Pipeline (v3)
API configuration:
Model:
claude-sonnet-4-6. Rate limiting: max_retries=5, time.sleep(1) between pages (~60 req/min). Estimated cost: ~$3.70–4.50 per PDF.
Page cache (resume on interruption):
Each successful API response is cached immediately to data/out-json/v3/.page-cache/<championship>/<year>/<code>/<page_num:03d>.json. On restart, cached pages are loaded from disk — only uncached pages incur cost. The cache is never deleted automatically.
To re-run pipeline logic without hitting the API: delete only the output JSON, then reparse. The parser logs Resuming: N pages already cached at startup.
Key Design Decisions
Championship-Specific Parsing Rules
Column detection always uses header name, not position — column count and positions vary per championship and even per event.
Shared rules (all championships):
- DNF / non-finisher: Gap contains
"Lap"or"Retired"→firstLapOffset = null - Footnote row: first cell matches
^No\.?\d+→ strip row "Not Classified"literal row: stop processing; skip all rows below- Penalty: strip leading
+, parse as float; default0.0if empty; set offset null if unparseable - Leader gap blank → treat as
0.0; all other positions with blank gap →None(parsing failure) kartNonormalisation: pandas reads numeric columns as floats — always normalise withstr(int(float(raw)))→"509"
Test Suites
The sector sum check (
test_sector_sum_equals_lap_time) is skipped when lapTime > sector_sum × 1.5 — neutralised/pit laps where sectors cover only the on-track portion. The _NEUTRALISED_LAP_RATIO = 1.5 constant in the test guards against false failures.
The v3 test suite requires at least one parsed JSON to exist in
data/out-json/v3/. It skips cleanly if the directory is empty.Gotchas
_canonical_session_name must stay in sync: This function exists in both claude_parser.py and merger.py. Both must apply the same normalisation or lap data silently fails to merge. Strips Results, Lap Time Analysis, Lap Analysis, and category prefixes (e.g. Academy , KZ ).
NaN sort behaviour: nan < x always returns False regardless of x. Always use (math.isnan(v), v) as the sort key when NaN is a possible value — plain min() / sort() with NaN produces undefined ordering.
Kart number exactly 6 chars: 'No.211' has 6 characters. The standard x[6:] slice returns ''. The driver name may appear in the adjacent Sector 2 column — the fallback to self.S2KEY handles this.
MM:SS lap times: Lap times over 60 s are displayed as MM:SS.sss (e.g. 1:08.783). System prompt rule 12 instructs Claude to convert to decimal seconds. If a wet-weather session shows systematic sector sum mismatches, check whether lapTime values are near 1.0x instead of 6x.
Multi-column lap truncation: LAP_ANALYSIS pages use a 3-column layout. Drivers whose lap block starts near the bottom of a column can have continuation rows dropped. Affected drivers have short raceTime and suppressed firstLapOffset. A full reparse is required to fix this.
firstLapOffset reliability guard: If the leader’s lap count differs from the modal lap count by even 1, all firstLapOffset values for that session are suppressed. The most common cause is multi-column lap truncation above.
Session order is reverse-chronological in FAC PDFs: claude_parser.py reverses the sessions list before numbering so session numbers ascend in race-day order. Different PDF ordering in other championships will produce inverted numbers.
Combined QP overview page: FAC events include a Qualifying Practice (QP) session aggregating both QPS1 and QPS2. _is_standings_session in claude_parser.py detects '(qp)' in the lowercased name and skips it. Do not remove this check.
Match-rate guard: Before computing firstLapOffset, the parser checks how many classification driver names appear in sector analysis. If fewer than 50% match, firstLapOffset is suppressed for all entries. This catches old FAC multi-heat combined tables (38 drivers vs a 12-driver per-heat sector analysis).
Old per-event subfolder JSONs: These still exist alongside new flat-structure v2 JSONs and have stale data. Tests pick up both. Consider deleting the subfolder files and reparsing cleanly.
Nationality failures (FAC 2015–2021): OCR-based nationality detection fails for these older PDFs. Affects ~20 events. Not yet investigated. In v3, <UNKNOWN> nationality is returned by Claude when it cannot read a nationality flag — the test suite skips any nationality value starting with <.
Next Steps
Claude Vision pipeline:- Parse remaining FAC 2024 PDFs —
240620_R_FAC_SVK_ACA.pdfand240801_R_FAC_KRI_ACA.pdf - Verify multi-column lap truncation fix after next reparse (system prompt rule 11 strengthened)
- Confirm wet-session sector sum mismatches resolved after MM:SS conversion fix (system prompt rule 12)
firstLapOffset outliers in firstLapOffset_outliers.csv — 284 entries outside [-5, 15]; large outliers (101.446, -32.382) pending investigation
5. Investigate 25 failing test_classification_driver_count_matches_sector_analysis cases (driver count off by 1–4)
6. Implement classification extraction for ESK, BKC, FWC — _extract_classification_df already written; calibrate results_area in each YAML first using scripts/probe_results_area.py
7. Add results_area coordinates to ESK, BKC, FWC YAMLs (probe script currently set to ESK)
General:
8. Delete old per-event subfolder JSONs and reparse all championships cleanly
9. Investigate FIA Academy nationality detection failure for 2015–2021 events