Know whether every exposed roll fits your tanks, reels, and declared chemistry before you open the changing bag.
DarkBatch is an offline command-line planner for analog photographers processing several film rolls in one session. Give it your already-verified recipes, configured tanks, chemistry lots, and rolls. It performs an exact bounded search and either writes a complete batch plan or stops with measured shortage evidence and a repair you can act on.
VALID: Saturday five-roll session; rolls=5, tanks=3, chemistry_lots=2
PLANNED: 5 rolls -> 2 batches; reports=darkbatch-demo
The result is not a timer or a recipe database: it is a feasibility check for the whole session. Your manifest and reports stay on your machine, and the installed program has no runtime dependencies or network access.
Python 3.11 or newer is required. Install the published v0.1.0 wheel directly from its GitHub Release:
python -m pip install https://github.com/KanadeK/darkbatch/releases/download/v0.1.0/darkbatch-0.1.0-py3-none-any.whlConfirm the installed entry point:
darkbatch versiondarkbatch 0.1.0
Download the committed five-roll example, validate it without writing anything, then create the reports:
curl -LO https://raw.githubusercontent.com/KanadeK/darkbatch/v0.1.0/examples/saturday-session.json
darkbatch validate saturday-session.json
darkbatch plan saturday-session.json --out darkbatch-demodarkbatch-demo contains exactly three files:
plan.json— stable machine-readable allocations, quantities, timing, and remaining chemistry;batch-cards.md— printable bench cards with roll IDs and mix quantities;timeline.svg— a standalone visual timeline for the sequential session.
The example's plan is real, not a canned result:
| Batch | Rolls | Tank | Declared chemistry | Mix | Nominal window |
|---|---|---|---|---|---|
| 1 | roll-004, roll-005 |
dual-600 |
color-lot-1 |
150 ml stock + 450 ml water | 0–18 min |
| 2 | roll-001, roll-002, roll-003 |
wide-900 |
mono-lot-1 |
450 ml stock + 450 ml water | 18–32 min |
DarkBatch chose two batches—the fewest possible—then minimized total working solution and stock use. Lexical tie-breaking makes the same manifest produce the same plan.
A manifest is UTF-8 JSON with five required top-level fields. Recipe values are deliberately user-owned: copy only values you have checked against the current chemistry and tank manufacturers' instructions and safety data.
{
"project": "One-roll check",
"recipes": [
{
"id": "my-verified-recipe",
"stock_parts": 1,
"water_parts": 1,
"minimum_stock_ml_per_roll": 100,
"process_minutes": 12
}
],
"tanks": [
{
"id": "my-300ml-tank",
"working_volume_ml": 300,
"reel_capacity": 1,
"formats": ["135"]
}
],
"chemistry": [
{
"id": "opened-bottle-a",
"recipe": "my-verified-recipe",
"stock_ml": 200,
"roll_capacity": 1
}
],
"rolls": [
{
"id": "roll-001",
"format": "135",
"recipe": "my-verified-recipe",
"reel_units": 1
}
]
}For each possible tank load, required stock is the greater of:
ceil(working volume × stock parts / total parts)
minimum stock per roll × number of rolls
Both the declared stock_ml and roll_capacity are consumed. Rolls can share a batch only when
their recipe IDs match exactly, every format is supported by the tank profile, and total
reel_units fit its capacity. Tanks and reels are reusable because v0.1 models one sequential
session.
The full field contract and ranking rules are in the v0.1 specification.
The shortage fixture is structurally valid but cannot cover both rolls:
darkbatch plan examples/insufficient-chemistry.json --out should-not-exist[CHEMISTRY_STOCK_SHORTAGE] Recipe 'recipe-a' lacks enough stock concentrate. Evidence: required=300ml, available=250ml, shortfall=50ml Repair: Provide at least the stated additional stock or use valid lower-volume tanks.
The command exits 1 and does not create should-not-exist. Invalid JSON, missing fields,
broken references, unreadable inputs, and an already-existing output path exit 2 with a stable
code and a repair action.
| Exit | Meaning |
|---|---|
0 |
The manifest is valid; plan also found and wrote a complete allocation. |
1 |
The manifest is valid, but the declared physical resources cannot cover every roll. |
2 |
The input, path, or command is invalid and must be repaired before planning. |
- DarkBatch does not verify recipe truth, developer/film compatibility, chemical safety, temperature, agitation, wash/fix stages, or the physical truth of a tank profile.
- It never recommends a film/developer combination and never controls darkroom hardware.
- v0.1 models one sequential session, not simultaneous processors or replenishment.
- Exact search is intentionally limited to 16 rolls, 8 tank profiles, and 16 chemistry lots.
- A feasible mathematical allocation is not a guarantee of chemical safety, image quality, or correct physical loading. Current manufacturer instructions remain authoritative.
| Message or symptom | What to do |
|---|---|
darkbatch is not found |
Run python -m pip show darkbatch, then ensure that Python's scripts directory is on PATH. You can also run python -m darkbatch version. |
[INPUT_IO] |
Check the manifest path and read permissions. Relative paths are resolved from the current directory. |
[INVALID_JSON], [MISSING_FIELD], or [UNKNOWN_FIELD] |
Fix the named JSON location. Start from a file in examples/ if needed. |
[OUTPUT_EXISTS] |
Choose a new --out directory or move the existing owner data yourself. DarkBatch never overwrites it. |
A chemistry or tank diagnostic exits 1 |
Read its Evidence and Repair text; revise only the real resource declaration or session, then rerun. |
The repository uses uv for a locked development environment:
uv sync --locked --dev
uv run --locked python scripts/check.pyThat single gate is also CI's final command. It verifies formatting, lint, strict types, branch
coverage, all committed examples, local Markdown links, wheel and sdist creation, clean wheel
installation, and a real run through the installed darkbatch entry point.
See CONTRIBUTING.md, the changelog, and the project-selection evidence. Security reports are handled as described in SECURITY.md.
DarkBatch is released under the MIT License.