Skip to content

Commit 6cbf057

Browse files
committed
refactor: restructure source tree by concern, declarative request validation
CPU recognizer reaches GPU accuracy parity (FUNSD-50 F1 82.28% -> 85.52%, GPU 85.33%): single-warp crop straight to recognizer input with the shared 32px width floor (was a 320px stretch + double resample), det preprocess in BGR per model spec. Correctness fixes: logger OOB write, batch-det aspect distortion, GPU vertical-text rec bucketing, gRPC lifecycle/shutdown, PPM mid-flush delivery, table/markdown HTML escaping. Split oversized modules into single-concern TUs and regroup directories (table/{slanext,vlm}, formula/{ppformulanet,vlm,routing}, pdf/{render,text}, pipeline/{ocr,pdf}, engine/trt, backend_routing, markdown). Join duplicated logic into shared helpers (pipeline result moves, reading-order assignment, image-size guards, curl/base64/trim utilities, single slanext host decoder). Request validation runs on a declarative per-endpoint spec: routing overrides, text=0 and embed=0 that cannot be honored fail loud; other unsupported params are tolerated with X-Ignored-Params/X-Deprecation response headers (removal planned for v4) or rejected under TURBO_OCR_STRICT_QUERY_PARAMS=1. Parameter classification reads the URL query string only (POST bodies are no longer parsed as parameters). Tests grow to 294 cases; docs updated.
1 parent b7e761b commit 6cbf057

334 files changed

Lines changed: 22471 additions & 18522 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ jobs:
5050
cmake --build build-cpu --target turboocr-cpu-server -j"$(nproc)"
5151
- name: Endpoint matrix smoke test
5252
run: |
53-
HTTP_PORT=18080 GRPC_PORT=18051 HOST=127.0.0.1 OCR_MODEL=tiny \
53+
PORT=18080 GRPC_PORT=18051 BIND_HOST=127.0.0.1 OCR_MODEL=tiny \
5454
./build-cpu/turboocr-cpu-server &
5555
SERVER_PID=$!
5656
for i in $(seq 1 60); do

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
/build_cpu/
33
/build-cpu/
44
/build-coverage/
5+
/build-*/
56
/predeploy/
67
__pycache__/
78
.pytest_cache/

CMakeLists.txt

Lines changed: 208 additions & 164 deletions
Large diffs are not rendered by default.

docs/api/http.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Parsed by `server::parse_query_options()` in
3838
emitted only when non-empty — text-only pages produce a response
3939
indistinguishable from the pre-feature shape. See
4040
`emit_pipeline_result_json` in
41-
`include/turbo_ocr/common/serialization.h:567`.
41+
`include/turbo_ocr/common/serialization/serialization_emit.h`.
4242

4343
!!! warning "LAYOUT_DISABLED"
4444
Requesting `layout=1`, `reading_order=1`, `as_blocks=1`, `tables=1`,
@@ -515,19 +515,19 @@ counterpart of PP-StructureV3 `save_to_markdown`. See
515515
[Faithful Markdown export](../output_markdown.md) for the serialization rules.
516516

517517
- **Body**: raw image bytes (same decoders as `/ocr/raw`).
518-
- **`embed` query** (default `true`): `embed=1` inlines figure/chart crops as
519-
base64 `data:` URIs (self-contained `.md`); `embed=0` emits
520-
`![](assets/blockN.png)` file-reference links (write the crops yourself).
518+
- **`embed` query** (default `true`): figure/chart crops are always inlined as
519+
base64 `data:` URIs (self-contained `.md`). `embed=1` is accepted explicitly;
520+
`embed=0` (file-reference links) is **rejected with
521+
`400 INVALID_PARAMETER`** — the asset PNGs would be written to the server's
522+
filesystem where an HTTP client cannot retrieve them. File-reference export
523+
is available to library consumers via `render_markdown_with_assets`.
521524
- **Requires layout**: against a server started with `DISABLE_LAYOUT=1` the
522525
request returns `400 LAYOUT_DISABLED`.
523526
- **Response**: `text/markdown; charset=utf-8`.
524527

525528
```bash
526529
# self-contained markdown (images inline as data URIs)
527530
curl --data-binary @page.png http://localhost:8000/ocr/markdown > page.md
528-
529-
# file-reference image links
530-
curl --data-binary @page.png 'http://localhost:8000/ocr/markdown?embed=0'
531531
```
532532

533533
Error codes: `EMPTY_BODY`, `LAYOUT_DISABLED`, `IMAGE_DECODE_FAILED`,

docs/build/config.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,81 @@ At startup the server emits one structured INFO line (`Effective server
184184
config`) containing every resolved value — a single grep target for
185185
post-mortems. Recorded warnings are logged immediately after.
186186

187+
188+
## Expert / subsystem knobs
189+
190+
These are read directly by their subsystem (not via `ServerConfig`, so they
191+
do not appear in `--print-config`). Defaults are tuned; override only with a
192+
measured reason.
193+
194+
### Recognition / detection tuning
195+
196+
| Variable | Default | Description |
197+
|---|---|---|
198+
| `REC_BATCH_N` | `32` | Recognition batch size per inference call. |
199+
| `REC_BUCKET_STEP` | `16` | CPU recognizer: snap crop widths UP to this step so batches pad each crop by at most step-1 columns. |
200+
| `REC_ZEROCOPY` | `1` | CPU recognizer: zero-copy batch view into ORT (`0` = copy path). |
201+
| `REC_SELFTEST` | `0` | CPU recognizer: one-shot batch-consistency self-test on first batch. |
202+
| `SIMD_CTC` | `1` | SIMD CTC argmax decode (`0` = scalar fallback). |
203+
| `DET_OPT_BATCH` | `8` | Batch dimension the det TRT profile is optimized for. |
204+
| `TURBO_DET_FUSED_PRE` | `1` | Fused GPU resize+normalize det preprocess (`0` = OpenCV path). |
205+
| `GPU_CCL` | `1` | Det post-process: `2` all-GPU JFA, `1` GPU CCL + CPU contours, `0` CPU contours. |
206+
| `GPU_BOX_THRESH` | model default | Override DB box threshold on the GPU path. |
207+
| `GPU_UNCLIP_SCALE` | `1.0` | Multiplier on the DB unclip ratio (GPU path). |
208+
| `CLS_BATCH` | `32` | Angle-classifier batch size. |
209+
| `MAX_IMAGE_PIXELS_MP` | `128` | Decompression-bomb cap: max decoded image area in megapixels. |
210+
| `MAX_BATCH_PIXELS_MP` | `2048` | Aggregate pixel cap across one /ocr/batch request. |
211+
212+
### ONNX Runtime (CPU / formula backends)
213+
214+
| Variable | Default | Description |
215+
|---|---|---|
216+
| `ORT_EP` | `cpu` | Execution provider for the CPU engine (`cpu` / `coreml`). |
217+
| `ORT_NUM_THREADS` | auto | Intra-op thread count per ORT session. |
218+
| `ORT_GLOBAL_THREADS` | auto | Shared global thread-pool size (with `ORT_SHARED_POOL=1`). |
219+
| `ORT_SHARED_POOL` | `1` | One shared ORT thread pool across sessions instead of per-session pools. |
220+
| `ORT_REC_OPT_CAP` | unset | Cap ORT graph-optimization level for the recognizer. |
221+
| `DISABLE_COREML` / `COREML_FLAGS` | unset | macOS CoreML EP opt-out / flags. |
222+
223+
### Structure stages (tables / formulas / VLM sidecar)
224+
225+
| Variable | Default | Description |
226+
|---|---|---|
227+
| `TABLE_CROP_MODE` | `layout` | `detunion` snaps each table region to the tight AABB of its det boxes. |
228+
| `TABLE_CROP_MARGIN` | `0.03` | Fractional expansion per table-region side before structure decode. |
229+
| `TABLE_MATCH_INTER` | `1` | Cell matcher: intersection-based OCR-fragment assignment. |
230+
| `TABLE_MATCH_FALLBACK` | `1` | Cell matcher: nearest-cell fallback for unmatched fragments. |
231+
| `TABLE_CLS_TRT`, `TABLE_SLANEXT_DICT`, `TABLE_SLANEXT_DECODER_BIN`, `TABLE_SLANEXT_WIRELESS_ENCODER_ONNX`, `TURBO_OCR_TABLE_DICT_PATH` | bundled paths | Override individual SLANeXt model/dict file locations. |
232+
| `PPFNS_CHUNK` | `0` | PP-FormulaNet-S decode chunk size (0 = single pass). |
233+
| `PPFNS_DROP_COLLAPSE` | `1` | Guard that drops collapsed (repeating) formula decodes. |
234+
| `VLM_BACKEND` | `pool` | `legacy` selects the per-request curl path instead of the shared async pool. |
235+
| `VLM_GLOBAL_CONCURRENCY` | `50` | Max in-flight VLM crop requests across the whole process. |
236+
| `VLM_MAX_RETRIES` | `2` | Retries per VLM crop on transient transport failures. |
237+
| `VLM_PNG_THREADS` | `4` | Threads PNG-encoding crops before VLM submit. |
238+
| `VLLM_BASE_URL` / `VLLM_MODEL` | `http://localhost:8000` / `PaddleOCR-VL-1.6-0.9B` | VLM sidecar endpoint and model id. |
239+
| `VLLM_FORMULA_PROMPT` / `VLLM_FORMULA_BATCH` / `VLLM_FORMULA_TIMEOUT_S` / `VLLM_FORMULA_MAX_TOKENS` | `Formula Recognition:` / `8` / `30` / `512` | Formula sidecar request shape. |
240+
| `VLLM_TABLE_BASE_URL` / `VLLM_TABLE_MODEL` / `VLLM_TABLE_PROMPT` / `VLLM_TABLE_BATCH` / `VLLM_TABLE_TIMEOUT_S` / `VLLM_TABLE_MAX_TOKENS` | formula equivalents / `Table Recognition:` / `8` / `60` / `4096` | Table sidecar request shape (falls back to the `VLLM_*` values). |
241+
| `TURBO_ROUTING_CONFIG` | env-synthesized | Path to a routing table JSON replacing the env-derived backend routing. |
242+
| `TURBO_ALLOW_ADHOC_BACKENDS` | `0` | Allow per-request backends outside the routing table. |
243+
244+
### Server / PDF / misc
245+
246+
| Variable | Default | Description |
247+
|---|---|---|
248+
| `BIND_HOST` | `0.0.0.0` | Bind address override. |
249+
| `GRPC_BATCH_GLOBAL_WORKERS` | `16` | Process-wide ceiling on extra gRPC batch fanout threads (each RPC keeps one guaranteed worker). |
250+
| `BATCH_FANOUT_GLOBAL_WORKERS` | `64` | Process-wide ceiling on extra CPU `/ocr/batch` fanout threads (each request keeps one guaranteed worker). |
251+
| `FINALIZE_DEFERRED_TIMEOUT_MS` | request timeout | Await budget for deferred (async VLM) structure results. |
252+
| `PDF_RENDER_REPLY_TIMEOUT_MS` | `120000` | Cap on waiting for a PDF daemon reply. |
253+
| `FASTPDF2PNG_PATH` | bundled | Path to the fastpdf2png daemon binary. |
254+
| `NVJPEG_DEVICE_COPY` | `1` | nvJPEG page-image encode keeps data device-side. |
255+
| `LAYOUT_KEEP_NESTED_CHILDREN` | `0` | Keep child layout blocks nested inside their parents. |
256+
| `TURBO_LAYOUT_DEBUG` | `0` | Verbose layout-stage debug output. |
257+
| `TURBO_OCR_STRICT_QUERY_PARAMS` | `0` | Opt-in: set `1` to reject with 400 any unknown parameter AND any known parameter the endpoint does not support. Default tolerates both (v3.4-compatible) — DEPRECATED: tolerated requests get an `X-Ignored-Params` + `X-Deprecation` response header and v4 rejects them with 400. Routing overrides, `text=0`, and `embed=0` on endpoints that cannot honor them are ALWAYS a 400 — ignoring those would falsify the response. |
258+
| `TURBO_OCR_DISABLE_MALLOC_REAPER` | `0` | Disable the periodic malloc_trim reaper thread. |
259+
| `ENABLE_TIMING` / `PROFILE_STAGES` | `0` | Per-stage timing output / CPU-path stage profiler. |
260+
| `TOCR_LOG_RATELIMIT` | `10:1000` | Per-call-site log rate limit `N[:WINDOW_MS]`; `0` disables. |
261+
187262
!!! info "See also"
188263
- [Build → Docker](docker.md) — image env vars and the nginx front
189264
(`TURBO_OCR_PORT`, `MAX_BODY_MB`).

docs/dev/pre_commit.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Pre-commit checks
2+
3+
Install once per clone:
4+
5+
```bash
6+
bash scripts/install_hooks.sh
7+
```
8+
9+
The hook (`scripts/git-hooks/pre-commit`) runs static checks on **staged
10+
files only** and is hard-capped at **20 seconds** — cheap greps first, then a
11+
time-boxed cppcheck with whatever budget remains.
12+
13+
Each check maps to a defect class that has actually shipped in this repo:
14+
15+
| Check | Why it exists |
16+
|---|---|
17+
| No `std::cout` / `std::cerr` in `src/` | Diagnostics bypassed the structured logger's level control and per-site rate limiting — request-path prints were a log-flood surface. |
18+
| No raw `getenv` in `src/`, `include/` | Config knobs were parsed by hand-pasted `to_int` copies with drifting semantics; `turbo_ocr::env` (common/env_utils.h) is the one parser. |
19+
| No `std::stoi` family | An unguarded `std::stoi` on a daemon reply threw `std::invalid_argument` past the error taxonomy. Use `std::from_chars` with explicit handling. |
20+
| No `file(GLOB)` in CMake | Explicit source lists only. |
21+
| No orphaned `CMakeLists.txt` | Four dead ones misled contributors about the build layout. |
22+
| cppcheck (warning/performance/portability) | General static net, staged `.cpp` only, time-boxed. |
23+
24+
Deliberate exceptions are annotated in-line
25+
(`// pre-commit-allow-stream`, `// pre-commit-allow-getenv`,
26+
`// pre-commit-allow-stoi`) or, for a one-off commit, `git commit --no-verify`
27+
with the reason in the commit message.
28+
29+
## File-length ratchet (max 500 lines)
30+
31+
Any staged `.cpp/.h/.cu/.cuh` over 500 lines fails the commit. The limit is a
32+
ratchet: pre-existing oversized files trip it the first time they are touched,
33+
which is when they get split. Escape hatch for the rare genuinely-unsplittable
34+
file: `// pre-commit-allow-length` within the first 5 lines, next to a short
35+
justification.

docs/models/classification.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ classDiagram
8484
PaddleCls --> TrtEngine
8585
```
8686

87-
`compute_crop_transform` (in `turbo_ocr/common/perspective.h`) produces the
87+
`compute_crop_transform` (in `turbo_ocr/common/geometry/perspective.h`) produces the
8888
`M_inv` 3×3 matrix that lets a single CUDA kernel (`cuda_batch_roi_warp`) warp
8989
the quad directly into the classifier's fixed `80×160` input slot without an
9090
intermediate crop allocation.

docs/output_markdown.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,20 @@ void register_ocr_markdown_route_gpu(server::WorkPool &pool,
137137
"DISABLE_LAYOUT=1)"));
138138
return;
139139
}
140-
// ?embed=0 -> file-ref image links (assets written by an out-of-band
141-
// exporter); default true -> self-contained base64 data: URIs.
142-
bool embed = true;
143-
if (auto p = req->getParameter("embed"); p == "0" || p == "false")
144-
embed = false;
140+
// HTTP is always self-contained base64 data: URIs. The file-ref mode
141+
// (embed_images=false) writes asset PNGs to the SERVER's filesystem —
142+
// unreachable for an HTTP client — so ?embed=0 is rejected with a loud
143+
// 400 INVALID_PARAMETER instead of being silently overridden. File-ref
144+
// markdown remains available to library/CLI consumers of
145+
// render_markdown_with_assets.
146+
if (auto p = req->getParameter("embed"); p == "0" || p == "false") {
147+
callback(server::error_response(
148+
drogon::k400BadRequest, "INVALID_PARAMETER",
149+
"embed=0 (file-ref markdown) is not supported over HTTP; "
150+
"assets are always embedded as data: URIs"));
151+
return;
152+
}
153+
const bool embed = true;
145154

146155
server::submit_work(pool, std::move(callback),
147156
[req, &dispatcher, &decode, embed](server::DrogonCallback &cb) {

include/turbo_ocr/routing/routing_config.h renamed to include/turbo_ocr/backend_routing/routing_config.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
// openai backend at a proprietary/account-gated endpoint is the operator's
2828
// explicit choice.
2929

30-
namespace turbo_ocr::routing {
30+
namespace turbo_ocr::backend_routing {
3131

3232
enum class Kind { Local, Openai };
3333

@@ -110,4 +110,4 @@ const char *kind_name(Kind k) noexcept;
110110
const char *parser_name(Parser p) noexcept;
111111
bool parse_parser(const std::string &s, Parser &out) noexcept;
112112

113-
} // namespace turbo_ocr::routing
113+
} // namespace turbo_ocr::backend_routing

include/turbo_ocr/classification/cpu_paddle_cls.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
#include <opencv2/core.hpp>
88

99
#include "turbo_ocr/engine/cpu_engine.h"
10-
#include "turbo_ocr/common/box.h"
10+
#include "turbo_ocr/common/geometry/box.h"
1111

1212
namespace turbo_ocr::classification {
1313

0 commit comments

Comments
 (0)