Developing Binah¶
This is the page for working on Binah's code: how it's laid out, how the pieces fit, how to run a copy you can break, and the tools and checks that come with it. The plan and the reasons behind it are in DESIGN.md, and the passage engine has its own page, ENGINE.md.
How it works¶
- Server: Python (FastAPI) with SQLite. Everything personal lives in a state folder outside the repo (
/srv/docker/binahby default, orBINAH_STATE): the database, the login, the settings, and anything mined from your own books. - Web app: vanilla JavaScript with no build step (
web/), with a service worker so it opens offline. One stylesheet has two looks: the page putsm3on<html>when the window is under 900px wide, and gets Material 3 (a large title, a tonal Continue card, a bottom bar); otherwise it looks like Apple's own apps, with a sidebar (/api/navgives its counts), a 52px toolbar row, and games split into a reading pane and an answer panel at 1100px and up. - Grading: done by the Claude Code CLI (
claude -p) on the owner's own subscription, through one small wrapper (binah/claude_cli.py). Tools, settings and session history are switched off, the output is structured JSON, and a missing result gets a retry and then "grading later", never a made-up score. There's a daily cap on graded calls. - Speech: faster-whisper on the server's CPU, with voice detection for pauses, and filler and repetition counts done by code.
- Two pools of material, kept strictly apart. Items from your own books are the private pool and stay in your own copy. The public pool is the original content written for Binah (
content/), and the benchmark draws on public-domain essays. Every item carries its pool and licence from the moment it's made.
The layout¶
binah/app.py |
the web API and the static app. The order inside every open answer (answer with a self-rating, verdict, one revision, reference) is enforced here, and the hidden key is never sent before the answer is in |
binah/games.py |
the games: what each one shows, how code scores it, the rubrics, and the grading prompts |
binah/grader.py |
the grading queue, a background thread that works jobs through claude_cli.ask, with the daily cap |
binah/edition.py |
the daily edition, the week and the counts |
binah/ratings.py |
Elo for skills and items, and picking the next item |
binah/golden.py |
anchors for grading prompts, and the agreement report |
binah/benchmark.py, binah/benchgen.py |
scoring a sitting, and writing sealed forms |
binah/speech.py |
transcription and the speech numbers |
binah/items.py |
importing items from the engine and content/ (an upsert that keeps ratings) |
binah/auth.py, binah/config.py, binah/db.py, binah/exporter.py, binah/cli.py |
the login and device tokens, paths and settings, the schema, the Markdown export, the command line |
engine/ |
the passage engine: epub.py, mine.py, features.py, rate.py, export.py, purge.py |
web/ |
style.css (both looks), app.js (routing, the sidebar and bottom bar, Today, Practice, Skills, the golden set, Settings), play.js (the games), bench.js (the benchmark) |
content/ |
technique.json, jam_topics.json, and table_topics.json for Table Topics, which isn't built yet |
mac/ |
the native Mac app (SwiftUI over API.md), with its own README |
android/ |
the Android app, native Kotlin and Jetpack Compose over the JSON API, with its own README |
deploy/ |
systemd user units for the server and the nightly run |
demo/ |
the public taste: Technique or Fair Move? played entirely in the browser (index.html, demo.js, demo.css, shots/) |
tools/ |
content and icon tools, the demo builder, a speech smoke test, a race check, and the release script |
Running it from source¶
You'll need Python 3.12 and the Claude Code CLI logged in on the same machine. faster-whisper brings its own audio decoding, so there's no ffmpeg to install. A Calibre library is optional; it's what the passage engine reads.
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python -m binah.cli set-password --user me
.venv/bin/python -m binah.server # http://127.0.0.1:8470
Settings live in STATE/settings.json (see binah/config.py for every option, including where your Calibre library is and a private list of title terms the engine must never mine). To mine your own books and draft items:
.venv/bin/python engine/mine.py
.venv/bin/python engine/rate.py all
.venv/bin/python -m binah.cli import
The command line:
.venv/bin/python -m binah.cli doctor # what's configured, live items, the queue
.venv/bin/python -m binah.cli import # reload items from the engine and content/
.venv/bin/python -m binah.cli golden # the golden-set agreement report
.venv/bin/python -m binah.cli set-password # new password (signs out every browser)
.venv/bin/python -m binah.benchgen --forms 3 # write sealed benchmark forms into STATE/benchmark/forms
A sandbox¶
Work against a copy, never against a state folder you care about. Point BINAH_STATE at a fresh folder (or a copy) and use another port:
BINAH_STATE=/tmp/binah-sandbox BINAH_PORT=8471 BINAH_HOSTS=127.0.0.1 .venv/bin/python -m binah.server
A fresh folder starts empty: set a password in it with BINAH_STATE=... python -m binah.cli set-password, and the server imports content/ when it starts. Grading calls the real claude on your subscription, so a sandbox that you click through costs real calls. The apps' self-tests are written to run against a sandbox like this, never the real copy, because they make device tokens and save passages.
Tools¶
tools/gen_content.pydrafts the practice content incontent/with Claude and checks it blind (--work FOLDER all, or one ofjam,table,technique). Read the output over before committing it.tools/content_stats.pyprints counts and field spreads forcontent/and, for the sealed forms, only counts, types and answer positions, never an item.tools/make_icons.pydraws the icons (the letter ב in Frank Ruhl Libre on a parchment tile). It needs fontTools, Brotli and Pillow, which are commented inrequirements.txt.tools/build_demo.py OUT_DIRputs the public taste together:demo/, the app'sstyle.csswith its font paths made relative, the fonts, the icon andcontent/technique.json. Every file comes from the repo, with no server and nothing from the state folder, so the folder can be served as plain static files. Its screenshots live indemo/shots/.tools/release.sh X.Y.Z "notes"cuts a release: it raises the version (Android'sversionCodeandversionName, which the Mac app follows), tags it, builds the signed APK and the Mac app on a Mac from a fresh clone of the tag, refuses to go on if anything personal is in either, publishes both on GitHub Releases (Obtainium watches it), and updatesCasks/binah.rbin franz-rosenzweig/homebrew-tap. Where it builds and how it signs live intools/release.env, which git leaves out. The APK is always signed with the same key and the Mac app with the same name-free certificate, or updates break.tools/check_races.py BASE STATE_DIR USER PASSWORDhammers a sandbox (it refuses the live app) for two races that once broke the first load of a day and the read right after an answer: it should print zero failures for both.tools/test_speech.pycuts a 60-second clip from any spoken-word file you have, re-encodes it the way browsers record, runstranscribe()on it, and checks that silent, empty and broken files behave.
The apps¶
The page is the whole interface on the web. The Mac and Android apps are native and talk to the JSON API directly (API.md). Building and testing each one is in mac/README.md (mac/build.sh, mac/selftest.sh) and android/README.md (android/release.sh, android/check.sh BASE TOKEN).
Keep signing identities, team IDs, keystores and tokens out of the repo. It's public.