Spaces:
Sleeping
Sleeping
ARCHITECTURE β marker
Tech stack
- Language / runtime: Python 3.11
- Framework: FastAPI + Uvicorn (single-process, GPU-bound work doesn't benefit from multiple workers).
- Database: none. Stateless.
- Key libraries:
marker-pdfβ the actual PDFβmarkdown engine (note: package name ismarker-pdf, import ismarker).fastapi,uvicorn,python-multipartβ HTTP layer + multipart upload.torch(CUDA build) β pulled in transitively by marker-pdf, must match the Space's CUDA version.
- Frontend: none. JSON API only.
- Build / package manager:
uvfor dependency resolution;pipinside the Docker image (smaller layer than installinguvinto the runtime). - Hosting: Hugging Face Space (
hirokamitai/trenchlesspm, public), Docker SDK, A10 small GPU, HF Buckethirokamitai/trenchlesspm-cachemounted at/datafor the model cache.
Components
app/main.pyβ FastAPI app. Public:GET /(endpoint blurb),GET /health(liveness + gpu/cpu). Bearer-gated:POST /parseβ NDJSON streaming. Marker runs onasyncio.to_threadso the route can yield NDJSON heartbeats every 5 s while the parse is in flight (HF's edge proxy closes idle connections mid-parse otherwise). Kept for back-compat and short/CLI use.POST /parse/jobsβ submit. Writes the upload + astate.jsonto the job directory (seeapp/jobs.py), enqueues the job_id, and returns 202 with the job_id in <1 s. Recommended for any non-trivial parse.GET /parse/jobs/{id}β read job state from disk. No coupling to the in-memory queue, so polling clients can hit any replica safely.
app/jobs.pyβ persistent job queue. Single-worker (A10 only fits one parse at a time), backed by/data/jobs/<id>/state.jsonon the HF Bucket. Lifespan-managed: on startup, re-enqueues anypendingor stale-parsingjobs left from a prior container; runs a GC loop that prunes job dirs older than 24 h. Also runs a keepalive loop that self-pings/healthevery 4 min while any job is in-flight, so HF's autosleep timer (5 min idle on inbound HTTP) doesn't trip mid-parse if no client is polling.app/parser.pyβ wraps marker-pdf. Loads the marker model dict once at import time (module-level singleton) so subsequent requests reuse weights on GPU. Exposesparse(pdf_path, mode) -> tuple[str, int]returning(markdown, page_count).mode="quality"flips marker's LLM-enhancement flag on and routes throughmarker.services.claude.ClaudeService(defaults toclaude-sonnet-4-6).Dockerfileβ CUDA-enabled base image, installs system deps marker needs,pip installof marker-pdf + FastAPI stack, setsHF_HOME=/data/hfandTORCH_HOME=/data/torchso model weights persist on the bucket. CMD runs Uvicorn with--timeout-keep-alive 1800so long parses don't drop the connection. Exposes port 7860 (HF Space convention).README.md(Space root) β HF Space frontmatter (sdk: docker,app_port: 7860,sleep_time: 300,suggested_hardware: a10g-small,suggested_storage: small), plus a short usage blurb.
Data flow
1. Caller POSTs PDF as multipart/form-data to /parse
with header `Authorization: Bearer <API_TOKEN>` and form fields:
file: the PDF
parse_mode: "fast" (default) | "quality"
2. FastAPI handler validates the bearer token + form fields and pre-checks
that quality mode has ANTHROPIC_API_KEY. Hard 4xx for these β they're
deterministic config issues, not per-request runtime failures.
3. Upload streams in 1 MiB chunks to a NamedTemporaryFile so 100+ MB PDFs
don't OOM the A10's host RAM.
4. Handler returns 200 with Content-Type: application/x-ndjson and starts
yielding events from an async generator:
- parser.parse(tmp_path, mode) is launched on a worker thread via
asyncio.to_thread (marker is blocking, would otherwise stall the loop).
- The generator wakes every 5s; if the thread is still running, it
yields {"type":"progress","stage":"parsing","elapsed_ms":N} so the
connection has bytes flowing and HF's edge proxy doesn't idle-close.
- When the thread completes, one terminal event is yielded:
{"type":"result","markdown":"...","mode":"...","page_count":N,
"duration_ms":N}
or {"type":"error","error":"..."} if marker raised.
5. quality mode routes the LLM-enhancement pass through Claude (Sonnet 4.6
by default) via marker's `ClaudeService`. See _project/ENVIRONMENT.md.
page_count comes from marker's rendered.metadata, falling back to pypdf.
6. Space autosleeps after 5 min of no requests. Next request triggers a cold
start; model weights load from /data (HF Bucket) instead of re-downloading.
The streaming shape is the only /parse response shape β there is no
synchronous JSON form. Clients must consume line-by-line; the last line
is the terminal event, everything before it is a heartbeat.
Key directories
| Path | Purpose |
|---|---|
app/ |
FastAPI app code (main.py, parser.py, jobs.py). |
/data/ (runtime, on the Space) |
Mount point for HF Bucket hirokamitai/trenchlesspm-cache. Holds every cache marker writes: hf/ (HF Hub), torch/ (Torch hub), cache/ (XDG fallback β covers datalab text-detection + OCR-error-detection models), and datalab/ (explicit datalab override). Also /data/jobs/<job_id>/ for the job queue's state + inputs (auto-GC'd at 24 h). Not in this repo. |
External touchpoints
- Hugging Face Hub β marker pulls its layout / OCR models from the Hub on
first cold start; cached to
/data/hf(HF Bucket mount) thereafter. No auth needed for public models. - Anthropic API (Claude) β only called when
parse_mode=quality. Marker uses its built-inClaudeService; we pinclaude-sonnet-4-6. API key stored as a Space secret. See DEPENDENCIES.md.
Known sharp edges
- Cold start is slow. First request after sleep waits on container boot +
model load from
/data(~30β60s). This is by design for cost; don't try to paper over it with a 24/7 keepalive. (The job worker's conditional keepalive β only while a parse is in flight β is fine and necessary.) - Model cache MUST land on
/data. If the bucket isn't mounted, every cold start re-downloads ~5 GB of weights into ephemeral container disk (lost on the next sleep). The Dockerfile setsHF_HOME=/data/hfandTORCH_HOME=/data/torchso marker writes through the bucket. - Public Space, bearer auth. The Space URL is reachable without HF auth.
Real access control is the bearer-token check in
app/main.pyβ/parse401s withoutAuthorization: Bearer <API_TOKEN>./and/healthare intentionally open and leak nothing. - Package name vs import name.
pip install marker-pdf, butimport marker. Easy to get wrong. - Single-process, GPU-bound. Don't run multiple Uvicorn workers β they'd each try to load the marker models onto the same GPU and OOM. Concurrency comes from the Space scaling, not in-process workers.
- Synchronous from the GPU's perspective; streaming over the wire. A
long parse still holds the HTTP connection open for the duration, but
the response body is NDJSON heartbeats every 5 s, so HF's edge proxy
doesn't idle-close. Callers still need a generous client-side timeout
(a few minutes for big contracts in quality mode), and they MUST consume
the body line-by-line β
await response.json()will not work. - A10 small VRAM (24 GB) is enough for marker including LLM-enhancement post-processing, but the LLM call itself is external β don't try to run a local LLM on the same GPU.