osr-forge
Convert tabletop adventure module PDFs into playable osrlib Adventure documents: an LLM-assisted extraction pipeline, deterministic map-geometry synthesis, a human correction loop built on an overrides file, and validation against the real osrlib models.
Standalone package + CLI. Consumers need only its artifacts — adventure.json, report.json, overrides.yaml, SVG map previews — or its CLI, regardless of their own tech stack.
Status: released — install from PyPI, docs at https://mmacy.github.io/osr-forge/:
uv pip install osr-forge # or: pip install osr-forge
The package runs the whole pipeline — preprocessing, the survey (chunked past survey_max_pages) and content extraction stages, monster resolution against the osrlib catalog, deterministic geometry synthesis (doors and secret doors on the stating room's wall, stair and trapdoor transitions from keyed and level-shaped links, guessed landings flagged), and assembly — plus the human correction loop (overrides application, rerun/resume, the playability lint with its smoke delve, cost estimation) and the eval harness that keeps extraction quality a measured number (tools/eval/). The roadmap's phase plans live in docs/ beside the specification.
The library entry points are convert(), assemble(), check(), and estimate(); the osrforge console script wraps them:
osrforge estimate my-module.pdf # preprocess only; rough token/cost estimate
osrforge convert my-module.pdf # full pipeline into ./my-module.forge
osrforge assemble # stage caches + overrides → artifacts, pure
osrforge check # validate_adventure + the playability lint
osrforge report # summarize report.json — the review loop's first read
osrforge preview # regenerate previews/ (SVG maps + index.html) only
osrforge rerun assemble # resume any stage through assemble
The workdir-taking commands discover a missing --workdir on their own: the working directory when it is itself a workdir, else the unique *.forge directory within it (here, ./my-module.forge) — several or none is a loud error, and an explicit --workdir always wins.
Recording sessions and live verification runs are driven via tools/extract/run_extraction.py (see tools/extract/README.md); the on-demand eval harness — corpus, scorer, and scoreboard — lives in tools/eval/ (see tools/eval/README.md). The harness also measures your own retail modules: a private corpus directory side-loads with --corpus DIR (integrity via a local hash sidecar, truth authored per tools/eval/AUTHORING.md), and an explicit publish step copies aggregate-only scores — never module text — onto the committed BYOM scoreboard.
The correction loop
Conversion produces a draft — every gap and guess called out in report.json — and corrections live in overrides.yaml, never in hand-edits to generated output (a fresh convert leaves a commented template there). The loop:
osrforge report(flags grouped by kind, findings, the monsters summary), then openpreviews/index.html— each level's synthesized map beside the module's own map-page renders, with a coordinate ruler printing the cell coordinates geometry overrides use.- Edit
overrides.yaml: monster remaps, per-area field replacement, area adds and removes, geometry (cells, edges, entrance, transitions), town/module metadata. Every entry carries areason, and every entry must take effect — a typo'd address fails assembly loudly instead of silently doing nothing. osrforge assemble && osrforge check— re-assembly is pure and instant (no model calls), andcheckexits 0 once validation passes and no error-severity finding remains.- Repeat until publishable.
Settings changes on an existing workdir go through rerun --set — for example, osrforge rerun preprocess --set 'blank_page_renders=[21]' blanks a render Azure's content filter rejects, or osrforge rerun assemble --set unresolved_fallback=omit flips the stand-in policy without re-rolling the model.
Development quickstart
Requires Python ≥ 3.14 and uv.
uv sync
uv run pytest
The full check suite, as CI runs it:
uv run ruff format --check && uv run ruff check && uv run pyright && uv run pytest
Tests use no network — model interactions replay from recorded fixtures. The only live-network activity in the repo is manual: the Foundry capability spike (tools/spike/) and the extraction runner (tools/extract/).
Pipeline settings
ConversionSettings holds the deterministic knobs, echoed into each workdir's run.json:
| Knob | Default | Meaning |
|---|---|---|
render_dpi |
150 | Page-render resolution (a legibility knob, not a cost knob — see docs/foundry-capabilities.md) |
max_pages |
200 | Source page-count guardrail |
max_source_bytes |
100 MiB | Source file-size guardrail |
blank_page_renders |
() |
Page numbers whose renders are emitted as blank white PNGs (text layer still extracted) — the content-safety-filter workaround; each blanked page is flagged page_unreadable in the report |
content_batch_pages |
8 | Content-pass batch size in pages (floor 2) |
survey_max_pages |
50 | The survey chunk size — the service's measured 50-images-per-request cap: a source at or under it surveys in one request; a larger source surveys in page windows of this size, merged before normalization |
monster_fuzzy_threshold |
0.85 | Monster resolution's fuzzy-tier auto-accept floor, pinned against measured catalog pairs |
monster_llm_top_k |
8 | Candidate templates offered per name in the monster-resolution LLM tier |
custom_monsters |
emit |
Whether the monsters stage runs the stat-block pass feeding custom-template emission: emit gives unresolved names the module's own creatures; off skips the per-unresolved-name model spend and keeps the draft SRD-catalog-pure (monsters-stage-owned — toggling it re-runs monsters, including its LLM tier) |
unresolved_fallback |
best-effort |
Where resolution or parsing came up empty and no usable stat block exists: flagged level-band monster stand-ins and unguarded-treasure rolls (best-effort), or leave the gap (omit) |
On an existing workdir, change a knob with rerun --set KEY=VALUE: the update lands in the run.json settings echo before the chain runs, and a knob owned by a stage upstream of the rerun stage is rejected with the stage to rerun instead.
Provider configuration
The Azure AI Foundry adapter reads its connection from OSRFORGE_FOUNDRY_* environment variables (an osr-forge-specific prefix, deliberately not AZURE_OPENAI_*, to avoid colliding with other tools' conventions):
| Variable | Azure meaning | Required |
|---|---|---|
OSRFORGE_FOUNDRY_ENDPOINT |
The Azure OpenAI resource endpoint (https://<resource>.openai.azure.com) |
yes |
OSRFORGE_FOUNDRY_DEPLOYMENT |
The model deployment name | yes |
OSRFORGE_FOUNDRY_API_KEY |
The API key. Omit to use Entra ID via DefaultAzureCredential, which needs the osr-forge[entra] extra |
no |
Licensing
Package code is MIT (see LICENSE). osr-forge ships no game content: osrlib's OGL data stays in osrlib, and test assets live under tests/assets/ outside the built distribution, each with its provenance and license documented in tests/assets/README.md.
These fences govern what this repository redistributes, not what you convert: bringing your own purchased, non-redistributable module is the primary use case. Conversion runs locally and everything derived from your module stays in your own workdir — nothing is shared unless you share it.
Metadata
Release files for osr-forge 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| osr_forge-0.2.0.tar.gz | 159.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| osr_forge-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 339.1 kB
Release files / osr_forge-0.2.0.tar.gz
| Download URL | osr_forge-0.2.0.tar.gz |
|---|---|
| Size | 159.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f4b799ac70aa7a1a00c97dc801e62d8fcea2077d6df639cfcb8143d0ac4a29ec
|
|
BLAKE2b-256 checksum How to use checksums |
b1ed37a5caa491150f7148af9239f31c9f06ae4a18ff743d771ad0d097d1c834
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.
Transparency logRelease files / osr_forge-0.2.0-py3-none-any.whl
| Download URL | osr_forge-0.2.0-py3-none-any.whl |
|---|---|
| Size | 179.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
418576b37d2b07f718762ef79051438d61c6c84f77c6d88761141a732173df35
|
|
BLAKE2b-256 checksum How to use checksums |
eabffa7cadddf59d1bc77610eb41c22496719ce4a3b49a24334659e81979fbbe
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.
Transparency log