Writeup: Let the model propose, keep the human holding the pen
Human-in-the-loop text editing with an LLM. You draft with a model, then select a span and give an instruction. The model's rewrite comes back as a before/after diff that you accept or reject — nothing is written to the document until you approve it.
The default backend is a deterministic stub, so the demo runs offline with no GPU, no network, and no API key. Point it at a real model when you want actual rewrites.
Below is a real run against a local model (google/gemma-4-e4b, served by LM
Studio over its OpenAI-compatible API). The input is samples/lesson.txt, an
original short lesson on the water cycle with some deliberately wordy sentences.
The reviewer selected a sentence in the "Evaporation" section and asked the
model to make it more concise. The rendered HTML diff:
Two of the rewrites the model proposed in that run:
Instruction: Make this more concise
before: Due to the fact that the sun heats the surface of oceans, lakes, and
rivers, a lot of the water there slowly turns into an invisible gas
called water vapour and rises up into the atmosphere.
after: Because the sun heats the surfaces of oceans, lakes, and rivers, much
of the water slowly turns into invisible water vapour that rises into
the atmosphere.
Instruction: Rewrite in a more formal tone
before: Once the water reaches the ground it does a bunch of different things.
after: Upon reaching the surface, the water undergoes several distinct
processes.
The HTML artifacts from this run are in samples/diffs-llm/ (open
index.html). Real-model output is non-deterministic, so re-running produces
different — though similar — rewrites; the stub backend is the reproducible
default and its artifacts in samples/diffs/ are byte-stable across runs. To
reproduce this run yourself:
export CODRAFT_BASE_URL=http://localhost:1234/v1
python -m codraft draft samples/lesson.txt --provider openai --model-name google/gemma-4-e4b
python -m codraft refine samples/lesson.txt \
--select "Once the water reaches the ground it does a bunch of different things." \
--instruction "Rewrite in a more formal tone" \
--provider openai --model-name google/gemma-4-e4b \
--out samples/diffs-llm/formal.htmlAn LLM asked to "rewrite this document" hands back a wall of new text and hides what it changed. That is fine until it quietly drops a qualifier or invents a detail. co-draft keeps the edit small and visible: you pick the span, the model proposes a replacement, and the change is a word-level diff you sign off on. Accepted edits go into a per-section history that can be undone.
raw text ──▶ draft_sections() ──▶ [Section, Section, ...]
│
▼
RefinementSession
│
select span + instruction ──▶ propose() ──▶ Proposal(before, after)
│
render_html_diff() ──▶ self-contained HTML diff
│
accept() ──▶ apply + record in history
reject() ──▶ discard, document unchanged
Everything lives in codraft/:
| Module | What it does |
|---|---|
providers.py |
Model backends behind one interface (rewrite, title): StubModel (offline, no deps), OllamaModel, OpenAICompatibleModel. |
drafting.py |
Splits raw text into titled Sections — by Markdown headings if present, otherwise by paragraph. |
refine.py |
The propose/accept/reject loop. A Proposal is not applied until accept(); reject() is a no-op; undo() reverts the last accepted edit. |
diff_engine.py |
Word-level diff (difflib.SequenceMatcher) plus a self-contained HTML renderer and an index page. |
cli.py |
The demo, draft, and refine subcommands. |
The diff engine tokenises both versions, classifies each span as added, removed, modified, or unchanged, and writes one HTML file per change with no external CSS, JS, or fonts. The file opens straight from disk and follows the system light/dark theme.
The stub demo needs only the Python standard library (Python 3.8+). No install step.
git clone https://github.com/batinium/co-draft.git
cd co-draft
# Draft sections, run three scripted refinements, write HTML diffs. A few seconds.
python -m codraft demo samples/input.txt --out samples/diffs
# Open the result (macOS; use xdg-open on Linux)
open samples/diffs/index.htmlOn systems where python is not on PATH, use python3.
# Draft only: raw text -> section outline
python -m codraft draft samples/input.txt
# One targeted refinement -> a single HTML diff
python -m codraft refine samples/input.txt \
--select "very sleepy and stupid" \
--instruction "make this more concise" \
--out samples/diffs/my_edit.htmlCopy .env.example to .env (gitignored) and pick a backend. Real backends
need requests (pip install -r requirements.txt); it is imported lazily, so
the stub path does not. Endpoints and model ids come from environment variables
or flags — nothing is hard-coded.
LM Studio or any OpenAI-compatible server (local, usually no key):
export CODRAFT_BASE_URL=http://localhost:1234/v1
python -m codraft demo samples/input.txt --provider openai --model-name <model-id>Use whatever model id your server reports at /v1/models.
Ollama:
export OLLAMA_HOST=http://localhost:11434
python -m codraft demo samples/input.txt --provider ollama --model-name llama3An optional conda/micromamba environment is in environment.yml:
micromamba env create -f environment.yml
micromamba activate co-draftsamples/input.txt is a short public-domain excerpt from Alice's Adventures in
Wonderland (Lewis Carroll, 1865; Project Gutenberg), lightly adapted. The diff
artifacts committed in samples/diffs/ were produced by the stub model and are
reproduced exactly by re-running the demo.
samples/lesson.txt is an original, openly-shareable lesson written for this
repo (the water cycle), with a few intentionally wordy sentences to give a real
model something to tighten. The artifacts in samples/diffs-llm/ and the
screenshot in the Example section came from the google/gemma-4-e4b run
described above.
- The stub model does not understand language. It maps instruction intents (concise, formal, simplify, expand) to fixed text transforms. It exists to make the pipeline reproducible and runnable offline — a test double, not a writing assistant. Use a real backend for real edits.
- No change is applied without an explicit accept. That is the whole point; if you wire this into a pipeline that auto-accepts, you have removed the one safeguard it provides.
- An LLM can change meaning, invent facts, or drop nuance even when asked only to tighten a sentence. The diff lets a human catch that; it does not prevent it.
- Not production software: no auth, no persistence beyond files, single-user, English-tuned tokenisation. It is a prototype of one idea — reviewable, human-approved edits.
- The diff is word-level, so a large rewrite shows up as one big modified block rather than fine-grained intra-word changes.
Batın Örene — PhD researcher in AI (LLM evaluation, responsible AI, human-AI teaming). GitHub: batinium
