Skip to content
Grant recipient

In the Original

Reading foreign-language literature in the original — from day one.

A live Ruby on Rails product that turns public-domain German literature into a reading experience whose support calibrates to you and recedes as you improve. Behind it sits a durable multi-pass language pipeline, a deterministic evaluation harness that decides which models reach production, and an audio system that certifies its own alignment before a reader ever hears it. You can read a real text in about a minute, without an account.

Sixty seconds

Try it before you read another word about it.

Most portfolio projects ask you to trust a description. This one is running, and the fastest way to evaluate it is to use it.

  1. Open the public library and pick any free German text — Grimm, Kafka, Novalis.
  2. Answer a short calibration so the reader knows roughly where your German is.
  3. Start reading. Support appears inline where you need it and thins out where you don’t.
  4. Play the audio. The narration is phrase-aligned to the text you are looking at.

No account, no card, no sales step. If you would rather see the product and business framing first, the membership page shows how access is actually sold.

Architecture

A pipeline that has to stay ahead of the reader.

Preparing a literary text is six dependent language passes deep. Doing that lazily makes the reader wait; doing it eagerly for the whole library wastes most of the spend. The system does it just in time, with a buffer that stays ahead of whoever is furthest along.

  1. Public-domain source A work enters with its rights evidence and canonical source text recorded, then is divided into sections by a deterministic, paragraph-first planner.
  2. Resumable text preparation Six passes turn source text into reader-ready material. Each saves its own checkpoint, so a crash or provider failure resumes instead of restarting.
    • split it into readable phrases
    • identify each word’s dictionary form
    • translate each phrase
    • add contextual word glosses
    • group related forms for vocabulary tracking
    • generate, align, and certify the narration
  3. Collection publication gate A section becomes readable only when its full publication-complete text passes. Partial work is never shown as finished.
  4. On-demand adaptive reader Display HTML is generated per request from durable state and the reader’s current vocabulary knowledge, so the same section renders differently as they improve.
  5. Reader state and feedback Form-family knowledge, reading position, and progress feed back into what support is shown. Readers can report a bad gloss or request a new text, and both route to operator repair tools.
The sequence reads top to bottom. Each numbered stage completes and commits before the next depends on it, which is what makes the pipeline resumable.

Where the Rails work actually is

The interesting Rails problems here were not CRUD. They came out of the pipeline shape above.

  • Durable workflow state. Each pass reserves a batch, advances a frontier, and commits in a transaction, so progress survives restarts and concurrent workers cannot claim the same work.
  • Row locking and idempotency. Payment webhooks lock the event row and check processed state before acting; checkout requests are idempotent; provider cost records are written once and retried on persistence failure.
  • Entitlements that outlive the subscription. Unlocking a section with a credit creates durable ownership, so a reader who cancels keeps what they already opened. That is a domain rule, and it is enforced in the access gate rather than in the view.
  • Operator repair as a first-class path. When a pass produces something wrong, there are admin surfaces to inspect the input and output and re-run a bounded piece of work, rather than a database console and hope.
The hard part

How I know the language output is good enough to ship.

Six language passes run over literary German. Any of them can be confidently wrong. The question that decided most of this system’s architecture was not which model to call — it was how to tell whether a call was correct.

Correctness is asserted, not eyeballed

Each pass has a golden fixture and a validator that checks named linguistic properties rather than a similarity score. Glossing is checked for contextually appropriate meanings, for verb glosses appearing in the infinitive, and for compound forms such as zum (zu + dem). Translation is checked for idiom literal glosses and forced additions. A pass either satisfies those assertions or it fails.

The fixtures are deliberately adversarial rather than numerous. The segmentation case is a single German passage built to break tokenizers: guillemets, an em-dash, an abbreviation (z.B.), a decimal with a hyphenated suffix (3.147-mal), an ellipsis, a parenthetical, and a long relative clause that splits a separable verb across a dozen words.

The harness chose the models

Because the pipeline runs just in time, model selection was constrained on three axes at once, and they pulled against each other.

  • Latency. A model too slow for the prompts cannot support just-in-time processing at all. Those models were ruled out on architecture, before quality was even a question.
  • Quality under sophisticated prompts. Candidates were run against the deterministic tests. Several could not handle the nuance of natural language well enough to pass. This is where the evaluation harness stopped being hygiene and became the thing that actually made the decision.
  • Cost, multiplied by buffer depth. Several models that passed on quality were slower and more expensive. A slower model needs a deeper buffer to stay ahead of the furthest reader, so its real cost is the increased per-call price multiplied by the additional buffer — not a price-per-token comparison.
Pass Model Why
Segmentation, glossing, lemma work gemini-2.5-flash The only model that handled these prompts fast enough for just-in-time processing while passing the deterministic tests.
Translation gemini-3-flash-preview Smoother translations and prose than 2.5-flash, without giving up complexity handling or the speed budget.

The passes are not uniformly assigned, and that is the point: each was evaluated on its own terms, and translation was upgraded specifically for prose quality.

Non-determinism is measured

The same prompt does not always return the same answer, so the harness can re-run every pass N times against real calls and write a variance report. That turns “it seems fine” into a number I can look at before and after a prompt change.

Cost and failure are engineered, not hoped for

  • Spend is attributed per pass, so a prompt change that doubles cost is visible.
  • Retries are bounded and failure is an explicit state a human can act on, not a silent empty result.
  • Paid provider tests are tagged and excluded from ordinary CI, so the suite stays free to run while the real-call tests remain available on demand.
  • The tests exercise the same models production uses, so a passing suite means something about production.

Sometimes the best LLM is no LLM

Section boundary planning looks like an obvious LLM task. It is not: paragraph structure already carries the answer. The shipped planner is deterministic and paragraph-first and makes no model call in the normal path, and every boundary it accepts is validated against structural rules. It is faster, free, and reproducible. Knowing where not to put a model matters as much as choosing the right one when needed.

Candor

What I built, what I bought, and what I’d fix.

Built and bought

The reading domain is mine: the reader and its dual-layer rendering, vocabulary state and progress, the text ingestion pipeline and its orchestration, the AI validators and repair tooling, audio authoring through alignment and certification, and the credits, ownership, and entitlement model.

The commodity layer is not. Accounts, teams, invitations, and role primitives come from Bullet Train, which is a Rails application framework I chose deliberately so I could spend my time on the domain instead of rebuilding authentication. OpenRouter routes model calls, and ElevenLabs is the text-to-speech provider. Payments run through Stripe.

Two things I would change

  • The model transport needs one owner. The OpenRouter client has no explicit HTTP timeout, and the call is duplicated across the four core pass jobs. It has not bitten me in production, but it is the clearest reliability gap in a system otherwise built around bounded failure — and it is the kind of gap that stays invisible until a provider hangs. The fix is one client that owns timeout and retry policy, with all four jobs calling through it. The audio adapter already works this way, which is exactly why audio has the better failure story.
  • The reader controller has absorbed too much. It is about 1,850 lines, its main action is roughly 528 of them, and it participates in eleven execution flows. It is well covered by tests, which is why it kept growing without pain. The fix is extracting query and presenter objects behind the same action so the reader’s behaviour is unchanged and the responsibilities separate.

Scope I deliberately parked

The system also contains work for institutional use, family accounts, and bulk text acquisition. It is implemented and preserved, but it is not the current public focus: the product is aimed at individual readers of German literature, and new texts arrive by reader request rather than bulk ingestion. Narrowing the public surface was a product decision, not an abandonment.

Contact

Happy to walk through any of this.

The repository is private, but I am glad to give a technical walkthrough of the pipeline, the evaluation harness, or the audio alignment work — including reading the source together — to anyone seriously evaluating my work.