Skip to main content

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:

  1. osrforge report (flags grouped by kind, findings, the monsters summary), then open previews/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.
  2. Edit overrides.yaml: monster remaps, per-area field replacement, area adds and removes, geometry (cells, edges, entrance, transitions), town/module metadata. Every entry carries a reason, and every entry must take effect — a typo'd address fails assembly loudly instead of silently doing nothing.
  3. osrforge assemble && osrforge check — re-assembly is pure and instant (no model calls), and check exits 0 once validation passes and no error-severity finding remains.
  4. 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)

Source distribution for osr-forge 0.2.0
File Size Uploaded
osr_forge-0.2.0.tar.gz 159.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for osr-forge 0.2.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page