A LAN web UI and job runner for okforge knowledge bases: drop a scanned book in an inbox and drive it through VLM OCR, optional translation, and ingestion into an LLM-synthesized wiki — then browse, query, and publish the result. Built for local-first setups where the LLM is your own llama.cpp/vLLM box, not a cloud API.
The pipeline is five screens: probe (inspect the PDF: text layer?
language? page count) → pilot (OCR a page or three, check the
transcription and image crops before committing) → project (pick or
name the project that collects the output) → run (chunked OCR →
translate → markdown with live progress) → verify & use (review the
markdown, ingest it into the project's knowledge base, then query and
publish). OCR and ingestion are always separate steps, so the same tool
doubles as a pure PDF→markdown converter — skip the ingest and take the
files from md-out/.
Inputs: PDFs, page-scan images (jpg/png/tif/bmp — wrapped into PDF on upload so the OCR pipeline handles them), and your own markdown/text documents (added straight to a project, no OCR). Selecting several PDFs/images at once combines them into one PDF in natural file-name order — the upload shows the exact order and combined name before anything is sent. Other formats (docx, pptx, html …) should be pre-converted to markdown first.
- Serial job queue (sqlite + one worker, on purpose): one
addor OCR run at a time protects single-slot LLM hosts and the engine's per-KB ingest lock. Jobs survive backend restarts; every finished job keeps its log. One-click resume/retry, a stall watchdog (flags, never kills), per-chunk ETA from real history, and a git pre-ingest snapshot of the KB before every add. - Markdown first, ingest second: every run OCRs into the project's
md-out/<name>/folder (chunked.md+ page maps + image crops); hand-made markdown/text files can be added to the same folder from the UI ("Add markdown…" — no OCR involved); ingesting that markdown into the knowledge base is a separate step — a one-click button in the verify stage (KB stats update chunk by chunk) or an auto-ingest toggle on the run. The KB is created on first ingest — or never, if all you wanted was the markdown. - Archive-first deletes: removing an uploaded PDF, a project's
markdown, a published site, or a whole project moves it to
trash/(KBs retire tokbs-retired/) — nothing in the UI is destructive, restore is amvback. - OCR + image extraction via okforge-vision-ocr (one VLM call per page: markdown transcription + photo bounding boxes together), with a table mode for pages the fast path mangles and a per-page re-OCR + re-ingest repair loop.
- Translation workflow for non-English scans: faithful transcription first, page-by-page translation second — both language versions share one image directory and page citations survive.
- Wiki browser with lexical search (source hits carry real page numbers), image lightbox, and markdown rendering.
- MCP server at
/mcp(streamable HTTP):list_projects,project_status,ask,search,read_wiki_page. Connect any MCP client, e.g.claude mcp add --transport http okforge http://<host>/mcp. Clients that don't surface MCP server instructions (Open-WebUI and other OpenAPI-bridged clients) should get the recommended system prompt fromdocs/MCP_CLIENT_PROMPT.md. - Static-site publishing per KB via Quartz — full-text search, graph view, backlinks — one button, then a printed rsync command to go public.
- An OpenAI-compatible LLM endpoint — the whole pipeline's brain (llama.cpp or vLLM on your own hardware, or a hosted service; see Configuration). For the OCR path the endpoint's model must be vision-capable (Qwen-VL family or similar) — it reads page images. Text-layer extraction, ingestion, and querying work with any capable chat model.
- Python 3.10+
- Git — for the clone; on Windows it also provides the
grepbinary the engine's query agent uses. - Node.js 18+ — only for the optional static-site publishing
(Quartz); everything else runs without it. Quartz is a one-time,
once-per-machine install into the shared quartz dir (
<base>/quartz, orOKFORGE_WEBUI_QUARTZ_DIR) — clone it, check out v5,npm ci, thennpx quartz plugin install; the exact steps are in OPERATIONS. Skipping theplugin installstep is the usual first-publish failure: the build aborts withCould not resolve "../../.quartz/plugins", which is exactly the dir that step generates. On a LAN-only / offline box, also disable the default og-image emitter inquartz.config.ts(it fetches a font over the network at build time and otherwise fails); nothing else in Quartz needs outbound access.
<base>/ e.g. /opt/okforge/ or C:\okforge\
okforge-webui/ ← this repo; .venv/ inside it
kbs/<Subject>/ ← one self-contained knowledge base per subject
inbox/ ← PDF drop point for the web UI
md-out/<Project>/ ← OCR'd markdown per project (created on demand)
kbs-retired/ ← retired KBs (archive-first "delete")
trash/ ← web-UI deletes move things here, never erase
quartz/ ← shared Quartz install (site publishing, optional)
sites/<Subject>/ ← published static sites (optional)
The base directory can be anywhere — all defaults are relative to where
this repo sits (each is individually overridable by env var, see
Configuration). Every KB is self-contained (sources, wiki, engine state,
.env, its own git history) — copy the directory and you've copied the
KB. The UI discovers KBs by scanning the KB root; nothing is registered
anywhere else.
Linux/macOS:
cd /opt/okforge # or any base dir
git clone https://github.com/okforge/okforge-webui
cd okforge-webui
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
mkdir -p ../kbs ../inboxWindows (PowerShell):
cd C:\okforge # or any base dir
git clone https://github.com/okforge/okforge-webui
cd okforge-webui
python -m venv .venv
.venv\Scripts\pip install -r requirements.txt
md ..\kbs, ..\inbox(PowerShell gotcha: anything in the current directory needs a .\
prefix to run — .\script.ps1, not script.ps1. The .venv\Scripts\…
forms above already qualify.)
requirements.txt pins the two okforge packages from PyPI — the
okforge engine (ingestion, wiki
compilation, query; see its
GETTING_STARTED)
and okforge-vision-ocr
(pre-conversion console scripts) — plus the FastAPI backend's own
dependencies.
One process serves the frontend, the API, and the MCP server. Run it
from this repo's directory — python -m webui resolves the webui
package relative to the current dir, so from anywhere else Python exits
with No module named webui:
cd <base>/okforge-webui
.venv/bin/python -m webui # Linux/macOS
.venv\Scripts\python -m webui # Windows
# browse http://<host>:8500/OKFORGE_WEBUI_HOST / OKFORGE_WEBUI_PORT change the bind (default
0.0.0.0:8500 — LAN-visible; use 127.0.0.1 to keep it local). Same
trust model in every mode: LAN-only, no auth — don't expose it beyond a
network you trust.
First time? Follow the small-test walkthrough below before pointing it at a whole book.
To run it as a service: on Linux,
webui/deploy/okforge-webui-standalone.service
is a ready-to-edit systemd unit; on Windows, use Task Scheduler
("At startup", run <repo>\.venv\Scripts\python.exe -m webui) or
NSSM.
For port 80, a LAN vhost name, and an easy basic-auth option, deploy.sh
installs Apache (static docroot + /api/ reverse proxy) in front of the
same backend under systemd:
SERVER_NAME=okforge.local OKFORGE_WEBUI_ENDPOINTS="gpu1=http://gpu1:8080/v1" \
webui/deploy.shdeploy.sh is idempotent — rerun it after changes (it restarts the
backend, so never while a job is running). In this mode frontend
files are served by Apache, so frontend-only changes are
sudo rsync -a --delete webui/static/ /var/www/okforge-webui/
(standalone mode serves them straight from the repo — nothing to copy).
Prove the whole loop — endpoint, OCR quality, ingest, query — on a
handful of pages before committing to a book. Five pages take about ten
minutes on a local GPU; a 300-page book is an overnight-plus run (see
ingest cost).
Everything below happens in the browser at http://<host>:8500/.
- Check the header. Pick your LLM server in the dropdown; the status light beside it polls the server, so a steady light means you're actually talking to it. This choice gets baked into the knowledge base at first ingest (queries and MCP clients then use it too — changeable later).
- Stage 1 — get a document in. Upload a short PDF — or a few phone photos of pages, which combine into one PDF (the panel shows the page order before anything uploads; it comes from the file names). The probe runs automatically: scan means the OCR pipeline (the normal path), text means an embedded text layer you can optionally trust in stage 4.
- Stage 2 — pilot one page. Enter one page number with real content on it (not the cover) and Run pilot. Read the transcription beside the rendered page; check the image crops. Bad OCR here means bad OCR everywhere, so fix it now — table mode for complex tables, --figures if line drawings were missed, or an OCR hint ("ignore marginalia"). Re-run until the page reads right.
- Stage 3 — create a project. Use a throwaway name like
MyBook-test— you'll delete it after the test (one click, and nothing is ever erased — it all moves totrash/). - Stage 4 — run a small range. Set From page / to to a few content pages, tick ingest into KB when OCR finishes, and Start run. The queue shows one plain-language row ("working — n/m chunks OCR'd"; ▸ expands the technical steps) and markdown appears in stage 5 as chunks finish.
- Stage 5 — verify and ask. Read the markdown. Watch the knowledge-base stats tick up as chunks ingest; a one-line project description is written automatically at the end, and Publish unlocks when the last chunk is in. Then ask the knowledge base a question — answers cite source pages as (p. N).
- Happy? Delete the test and run for real. Delete project… in stage 3, then repeat with the real project name and the full page range. (If you'd rather keep the test: make its range exactly the first chunk — e.g. pages 1–20 at the default 20 pages per chunk — and the full run will skip it instead of re-OCRing it.)
What a small test catches early: a wrong endpoint or non-vision model
(pilot fails or returns junk), OCR quirks your document needs hints
for, and a misconfigured model paying a hidden reasoning block on every
call — a 20-page chunk should ingest in a couple of minutes on a local
27B model, not 27 (see the llm_extra_body note below).
Everything is an environment variable (for the systemd deployments, set
them in the unit — deploy.sh passes any that are exported when it
runs). <base> below means the directory this repo sits in.
Note: these use the
OKFORGE_WEBUI_*prefix. The pre-rebrandOPENKB_WEBUI_*names (andOPENKB_DIRfor the engine dir) still work — the backend reads the new name first and falls back to the old one, printing a one-time deprecation line on stderr naming what to rename.
| variable | default | meaning |
|---|---|---|
OKFORGE_WEBUI_HOST |
0.0.0.0 |
bind address (python -m webui) |
OKFORGE_WEBUI_PORT |
8500 |
bind port (python -m webui) |
OKFORGE_WEBUI_ENDPOINTS |
local=http://localhost:8080/v1 |
LLM endpoints for the UI dropdown, comma-separated label=url[|key[|model]] — key and model only for hosted services (see below) |
OKFORGE_WEBUI_DEFAULT_ENDPOINT |
first label | pre-selected endpoint |
OKFORGE_WEBUI_MODEL |
openai/Qwen3.6-27B-MTP |
model string new KBs are initialized with (per-endpoint model overrides it) |
OKFORGE_WEBUI_KB_ROOT |
<base>/kbs |
where KBs live |
OKFORGE_WEBUI_INBOX |
<base>/inbox |
PDF drop dir |
OKFORGE_WEBUI_MD_OUT |
<base>/md-out |
per-project OCR'd markdown |
OKFORGE_WEBUI_RETIRED_DIR |
<base>/kbs-retired |
where retired KBs move |
OKFORGE_WEBUI_TRASH |
<base>/trash |
where web-UI deletes move things |
OKFORGE_WEBUI_QUARTZ_DIR |
<base>/quartz |
shared Quartz install |
OKFORGE_WEBUI_SITES_DIR |
<base>/sites |
published-site output |
OKFORGE_WEBUI_PUBLIC_SITE_HOST |
localhost |
public host for published sites' baseUrl |
OKFORGE_WEBUI_PUBLIC_SITE_DEST |
user@host:/var/www/sites |
rsync target shown by the go-public helper |
OKFORGE_WEBUI_NODE |
node on PATH, else /usr/bin/node |
node binary for Quartz builds |
OKFORGE_WEBUI_ENGINE_DIR |
this repo | dir whose .venv holds the engine + okforge-vision-ocr console scripts — set this only if that .venv lives somewhere other than this repo (shared or parent-dir venv) |
The documented install puts .venv inside the repo, which is why OKFORGE_WEBUI_ENGINE_DIR needs no setting by default. If you instead share one .venv across checkouts or keep it in the base dir, point this var at the directory that contains it — otherwise the UI looks for openkb/okforge-vision-ocr under <repo>/.venv/bin and reports them missing.
Local llama.cpp/vLLM endpoints need only label=url. A hosted
OpenAI-compatible service takes two more |-separated fields — its API
key and the model in LiteLLM provider/model format:
OKFORGE_WEBUI_ENDPOINTS="gpu1=http://gpu1:8080/v1,openrouter=https://openrouter.ai/api/v1|sk-or-v1-...|openrouter/qwen/qwen3.6-27b"
KBs created on such an endpoint get the key in their .env, the model
in their config.yaml, and a provider-appropriate thinking-off block
(OpenRouter's reasoning.enabled=false instead of llama.cpp's
chat_template_kwargs). OCR/translate jobs strip the LiteLLM provider
prefix and pass the rest as the OpenAI-protocol model name. Note the
key lives in plain text in the unit file/environment — acceptable on a
single-operator LAN box; protect the systemd drop-in accordingly
(chmod 640).
The endpoint picked in the header is baked into each knowledge base
at first ingest: that's when the KB's own .env (OPENAI_API_BASE,
LLM_API_KEY) and config.yaml (model, thinking-off block) are
written. From then on, everything that touches that KB — later
ingests, the stage-5 Ask box, and the MCP server's ask tool — uses
the KB's own endpoint, regardless of what the header currently
shows. (The header selection still drives OCR/translate runs, which
produce markdown before any KB exists.) So two KBs on one machine can
happily run against two different LLM servers, and an MCP client
querying a project lights up whichever server that project pins.
To repoint an existing KB (LLM moved to a new box, or a new
model): edit those two files in the KB directory — .env for the URL
and key, config.yaml for model: — and the next call uses them; no
restart, nothing to re-register. Keep the llm_extra_body block
appropriate for the new host (see below). The stage-3 project info box
shows which endpoint a KB currently points at.
One setting matters more than all the others on llama.cpp hosts serving Qwen-family models with thinking enabled by default — without it every ingest silently pays a hidden reasoning block (measured: a 2-minute add becomes 27):
llm_extra_body:
chat_template_kwargs:
enable_thinking: falseKBs created through the web UI get this automatically. Pilot and OCR
runs are the exception — they happen before a KB exists, calling the
vision model directly. That path already sends enable_thinking: false
(and reasoning.enabled: false) and strips any stray <think> block
from the transcript, so nothing is normally needed. But if your model's
own chat template overrides that request-level flag — the
Qwen3.6-27B-MTP template is one that keeps thinking on regardless — the
model still generates (and you still pay for) the reasoning block even
though the text is discarded. That can only be fixed at the server: use
a llama.cpp/vLLM preset (or a --jinja chat template) that honors
enable_thinking: false. Symptom: pilot/OCR pages take far longer than
their token count suggests.
One more expectation worth setting for large collections: ingest cost
scales with the size of the wiki, not the document being added —
recurring concept and entity pages are rewritten on every mention — so
a document that takes 3 minutes into a fresh KB takes ~9 into the same
KB 300 documents later. llama.cpp's prefix cache absorbs most of the
growth (84% of input tokens were cache hits over a measured
364-document run); see
docs/OPERATIONS.md
for the measured figures before planning a multi-day run.
- First run: the small-test walkthrough above is the intended on-ramp.
docs/INSTALL_PROMPT.md— a prompt to hand a coding agent so it walks you through install and configuration interactively, Quartz included.docs/OPERATIONS.md— operating KBs day to day: anatomy, copying between machines, re-ingest semantics (what happens when you add the same document twice), Obsidian editing safety, Quartz publishing, troubleshooting.docs/MCP_CLIENT_PROMPT.md— the system prompt to paste into MCP clients that don't surface the server's owninstructions(Open WebUI and most OpenAPI-bridged clients); also served from the server as the MCP promptkb-search-guide.webui/PLAN.md— architecture and API design notes.
MIT — see LICENSE.
