Status: current. Version 1 is version 0 (model-container-v0.md) with format version 1 and one added manifest member, calibration. Writers produce version 1; readers read both. A version 0 file loads and has no calibration: its calibrated word confidence is its raw confidence, and the reader reports a diagnostic saying so. Everything else in version 0 stands and is repeated here so this document is complete.
The container is a cross-language contract: one weights file serves every implementation. Everything below is little-endian.
File layout
| Offset | Size | Content |
|---|---|---|
| 0 | 8 | Magic: ASCII ONEOCRM followed by one zero byte |
| 8 | 4 | uint32 format version, 1 (a reader also accepts 0; any other value is an error) |
| 12 | 4 | uint32 manifest length M in bytes |
| 16 | M | Manifest: UTF-8 JSON, no BOM |
| 16 + M | pad | Zero bytes up to the next multiple of 64 — the start of the data region D |
| D | … | Tensor data, each tensor starting at a multiple of 64 from D, zero padding between tensors |
A reader rejects the file — before allocating anything sized by it — when the magic or version is wrong, the manifest is not valid UTF-8 JSON, or any length, offset, shape or count exceeds the reader's limits or the file's size. Tensors may not overlap. All padding bytes (after the manifest and between tensors) must be zero. The file ends exactly where the last tensor ends: no trailing padding, and bytes after the last tensor are an error. Tensors need not be listed in offset order.
The manifest is strict JSON: no comments, no trailing commas, no duplicate member names. Members this version does not name are ignored (provenance among them). In a version 0 file calibration is such a member and is ignored, whatever it holds. Sizes, offsets and shapes are JSON integers.
Manifest
{ "architecture": "crnn-v0", "name": "line-recognizer", "version": "0.1.0", "license": "MIT OR Apache-2.0", "input": { "height": 32, "channels": 1, "mean": 0.5, "std": 0.5, "invert": false }, "charset": ["<blank>", " ", "!", "..."], "config": { }, "tensors": [ { "name": "conv.0.weight", "dtype": "f16", "shape": [64, 1, 3, 3], "offset": 0, "length": 1152 } ], "calibration": { "raw": "word-mean-max-run", "scripts": { "Latin": { "x": [0.0, 0.5, 1.0], "y": [0.0, 0.4, 0.95] }, "Greek": { "x": [0.0, 1.0], "y": [0.0, 1.0] } } }, "provenance": { } }
architecturenames the layer graph (below); a reader rejects any it doesn't know.input: line crops are grayscale, scaled toheightkeeping aspect ratio; a pixelpin 0–255 becomesx = p / 255(1 - p / 255wheninvert), then(x - mean) / std, all in float32 with a correctly rounded division.channelsis 1 forcrnn-v0;stdis positive.charset: the output classes in order. Index 0 is the CTC blank; its text is a placeholder (the exporter writes<blank>) and is never output. Each other entry is a non-empty NFC string, usually one code point; entries, the blank placeholder included, are unique. (Readers do not check NFC; they check emptiness, uniqueness and length.)tensors:dtypeisf16(IEEE 754 binary16) in versions 0 and 1; values are widened tofloat32for computation.offsetis relative to D;lengthequals the element count times 2. Tensors are stored row-major in PyTorch's own dimension order.provenance: free-form record of the training run (commit, config, seed, data manifest hashes). Readers ignore it.
Architecture crnn-v0
A convolutional stack, then a bidirectional LSTM, then a per-timestep classifier, decoded by greedy CTC. config:
{ "conv": [ { "out": 64, "kernel": [3, 3], "padding": [1, 1], "pool": [2, 2] }, { "out": 128, "kernel": [3, 3], "padding": [1, 1], "pool": null } ], "lstm": { "hidden": 256, "layers": 2 }, "classes": 700 }
config.conv[i]:outchannels,kernel,paddingandpool(an array of two integers[h, w], ornull, or absent for no pooling). Kernel and pool sizes are at least 1, padding at least 0.config.classesequals the charset length.- Conv block i (
conv.<i>): 2-D convolution, stride 1, the given kernel[h, w]and zero padding[h, w], with bias; batch normalization is folded into the weight and bias at export. Then ReLU. Then, whenpoolis[ph, pw], max pooling with kernel and stride[ph, pw]and no padding (a trailing partial window is dropped; the block's input hasinchannels, the previous block'sout, orinput.channelsfor the first). A line image too narrow to leave at least one column after every block reads as empty text. Tensors:conv.<i>.weight[out, in, kh, kw],conv.<i>.bias[out]. - To sequence: the stack's output height must be exactly 1 for the configured input height (a reader checks this). Shape
[C, 1, W]becomes a sequence ofWvectors of sizeC, left to right. - BiLSTM (
lstm):layersstacked bidirectional layers; each layer's input is the previous layer's forward and reverse outputs concatenated (forward first). Gate order and equations are PyTorch'snn.LSTM(input, forget, cell, output;c' = f*c + i*g,h' = o*tanh(c')), zero initial state. Tensors use PyTorch's names underlstm.:lstm.weight_ih_l<k>,lstm.weight_hh_l<k>,lstm.bias_ih_l<k>,lstm.bias_hh_l<k>, and the same with suffix_reverse. - Classifier (
fc): linear layer2 * hidden → classeswith bias:fc.weight[classes, 2 * hidden],fc.bias[classes].classesequals the charset length. A reader ignores tensors it does not need. - Decoding: greedy CTC: take the arg-max class per timestep (lowest index on ties), merge repeats, drop blanks, concatenate charset entries. Confidences are defined under Confidence below.
Architecture dbnet-lite-v0
The page detector: text lines, layout regions and page orientation from one page image. The layer graph, pre- and post-processing are in detector-v0.md; this section is what the container carries.
architecture:dbnet-lite-v0;name:text-detector-layout.input:{"height": 0, "channels": 1, "mean": 0.5, "std": 0.5, "invert": false}. Height 0 means "any multiple of 32" and is allowed for this architecture only; every other architecture needs a positive height.channelsis 1.charset:["<blank>", ...layout class names], present only to satisfy the envelope's charset rules; it is not an output alphabet and a reader ignores it for this architecture.calibration: the usual object with an emptyscripts(the detector has no word confidences).config:
{ "arch": "dbnet-lite", "stem": 16, "widths": [32, 72, 144, 224], "depths": [2, 3, 4, 3], "fpn": 56, "text": 32, "layout": 112, "layout_blocks": 3, "classes": 8, "layout_classes": ["text", "title", "list", "table", "figure", "header_footer", "form_field", "checkbox"], "layout_trained": ["text", "title", "list", "table", "figure", "header_footer"], "postprocess": {"text_threshold": 0.3, "text_expand": 1.0, "layout_threshold": 0.6, "layout_expand": 0.85} }
arch must be dbnet-lite; widths and depths have four entries each; every width is at least 1 and within the reader's layer-width limit, every depth at least 1; classes equals the number of layout_classes, whose names are non-empty and unique. layout_trained is optional: the names of the layout classes the model was trained on. A reader never reports a class that is not listed. When the member is absent, every class except form_field and checkbox counts as trained (those two had no training labels in the first model). A name that is not in layout_classes is an error. postprocess is optional, and so is each of its members: text_threshold and layout_threshold are probabilities strictly between 0 and 1, compared as float32 logits; text_expand and layout_expand are factors in (0, 4] on the growth of a text line or layout core (detector-v0.md, "Post-processing"). Absent members take 0.3, 1.0, 0.6 and 0.85. A value out of range is an error.
- Tensors (all
f16, PyTorch layout, batch normalization folded in; names in detector-v0.md):stem,stage<i>.<j>.dw/.pw,lat<i>,smooth8.dw/.pw,smooth4.dw/.pw,layout.<j>.dw/.pw,layout_out,text4.dw/.pw,stem_proj,text2.dw/.pw,prob_out(each with.weightand.bias), andorient.weight[4, widths[3]],orient.bias[4]. A depthwise weight is[c, 1, 3, 3], a 1x1 weight[out, in, 1, 1]. Shapes are checked againstconfig.
Architecture dbnet-lite-v1
The page detector with the optional stride-16 layout branch and two-scale orientation head (detector-v1.md). The envelope, input, charset, calibration, layout_trained, postprocess and every tensor of dbnet-lite-v0 are as in the section above, with architecture dbnet-lite-v1. config gains three members, all optional:
layout16_blocks: integer, 0 (the default) or the number of stride-16 blocks;layout16: the width of those blocks, required and positive whenlayout16_blocksis above 0;orient16: boolean, false by default; when trueorient.weightis[4, widths[2] + widths[3]].
With the branch the file also carries layout16.<j>.dw/.pw (j < layout16_blocks) and layout_mix (.weight [layout, layout16, 1, 1], .bias). The three members in a dbnet-lite-v0 file are an error. Height 0 in input is allowed for both detector architectures.
Architecture docextract-v0
The document-extraction model: a small layout-aware transformer over the words of a page. It reads words (text and box), not pixels. The graph, the featurization and the assembly of labels into fields are in docextract-v0.md; this section is what the container carries.
architecture:docextract-v0;name:document-extraction.inputhas no image members. It is{"kind": "words", "max_words": 512, "max_chars": 32, "bins": 128, "features": 8}: the most words in one window (MaxWordsPerWindowbounds it), the characters kept per word (MaxCharsPerWord), the cells per axis of the position grid (MaxPositionBins) and the numeric features per word (always 8). Every other architecture needs the image members (height,channels,mean,std,invert) and nokind.charset: the alphabet of the character embedding, charset-v1 (entry 0<blank>is padding and never a text character). The embedding table has one more row than the charset: the last row is the unknown character.calibration:{"raw": "word-mean-max-run", "scripts": {}}. The model has no calibrated confidences; field confidence is the mean softmax probability of the chosen tags.config, all integers unless stated:d,layers,heads,ffn,char_dim,char_conv,pair_hidden,rel_dim,max_words(equal toinput.max_words),classes,tags(1 + 2 * classes),roles(5),doc_types,unk_char(equal to the charset length); and the string listsclasses_list(classesunique, non-empty names),roles_list(O, B-KEY, I-KEY, B-VAL, I-VAL) anddoc_types_list.dis a multiple ofheads. Widths are bounded byMaxLayerWidth,layersbyMaxTransformerLayers, the three list lengths byMaxExtractionClasses, and(layers * heads + 2) * max_words^2byMaxActivationElements, all before any tensor is read.- Tensors (names and shapes exactly; a reader checks each against the configuration before reading it):
char_emb.weight [charset + 1, char_dim],char_conv.weight [char_conv, char_dim, 3],char_conv.bias,char_proj.{weight [d, char_conv], bias},pos_x0 / pos_y0 / pos_x1 / pos_y1 .weight [bins, d],feat_proj.{weight [d, 8], bias},emb_ln.{weight, bias} [d], per blockblocks.<l>.{ln1, ln2}.{weight, bias} [d],blocks.<l>.qkv.{weight [3d, d], bias},blocks.<l>.proj.{weight [d, d], bias},blocks.<l>.fc1.{weight [ffn, d], bias},blocks.<l>.fc2.{weight [d, ffn], bias},pair1.{weight [pair_hidden, 13], bias},pair2.{weight [layers * heads + 2, pair_hidden], bias},final_ln.{weight, bias},tag_head.{weight [tags, d], bias},role_head.{weight [5, d], bias},doc_head.{weight [doc_types, d], bias},rel_q.{weight [2 * rel_dim, d], bias},rel_k.{weight [2 * rel_dim, d], bias}. - A model trained for another schema (a fine-tuned head) is the same architecture with its own
classes_listandtag_head.
Calibration (version 1)
A recognizer says how sure it is per character; users want the probability that a word is exactly right. The model file carries the map from one to the other, fitted by the trainer on a held-out calibration split.
Character probability. The network's output at a step is softmax over the classes. A character is a run: a maximal sequence of consecutive steps whose arg-max is the same non-blank class (a blank step ends a run, so "ll" is two runs). Its probability is the largest softmax probability of that class over the run's steps.
Raw word confidence ("raw": "word-mean-max-run", the only value defined; any other is an error): the words of a line are the maximal sequences of runs whose characters are not whitespace. A character is whitespace when its charset entry is non-empty and consists only of whitespace code units (char.IsWhiteSpace-style: U+0020, U+00A0, U+2003 and the other Unicode White_Space characters). Spaces are not part of any word; a line with no non-space run has no words. The raw confidence of a word is the mean of its runs' probabilities, summed left to right in float32 and divided by the number of runs. A whole line's raw confidence (RecognizedLine.Confidence) uses the same rule over every run of the line, spaces included; it is 0 for an empty line.
Script of a word. Count the word's letters (Unicode categories L*, by code point) in three groups: Greek (U+0370–03FF, U+1F00–1FFF), Cyrillic (U+0400–052F, U+1C80–1C8F, U+2DE0–2DFF, U+A640–A69F), Latin (every other letter). The script with the most letters wins; ties go to the first of Latin, Greek, Cyrillic. A word with no letters (digits, punctuation, symbols) is Latin.
Calibrated word confidence. The manifest member:
"calibration": { "raw": "word-mean-max-run", "scripts": { "Latin": { "x": [...], "y": [...] }, "Greek": {...}, "Cyrillic": {...} } }scriptsis an object whose names are exactlyLatin,GreekorCyrillic(case-sensitive, no duplicates). Any other name is an error. A script may be missing; its calibrated confidence is then its raw confidence.scriptsmay be empty.Each script has
xandy, JSON arrays of numbers of equal length, between 1 and 64 knots (more is a limit error,MaxCalibrationKnots).xis strictly increasing and every value is in [0, 1];yis non-decreasing and every value is in [0, 1]. Numbers are IEEE doubles.A version 1 file must have the
calibrationobject; the reader rejects a file without it.The map is piecewise linear through the knots
(x[i], y[i])and constant outside them: forr <= x[0]the value isy[0], forr >= x[last]it isy[last]. Forx[i] < r < x[i+1], in IEEE double precision with each operation rounded on its own (no fused multiply-add):t = (r - x[i]) / (x[i+1] - x[i]),p = (y[i+1] - y[i]) * t,value = y[i] + pand the result is converted to float32. A one-knot curve is the constant
y[0].ris the float32 raw confidence converted to double.
Reporting curve (optional). A version 1 recognizer file may also carry report_calibration, an object of exactly the same shape and rules as calibration (same raw definition, scripts, knot limits and evaluation). Where it is present, the confidence a finished page reports for a word is report_calibration applied to the word's raw confidence (script taken from the word's final text); the word's rawConfidence is unchanged. Every decision the reader makes while building the page (noise and speck filtering, word joining and splitting, the page-angle search, table tests, reading order) keeps using calibration, so the text, boxes and structure of the page are the same with or without the member. The map is applied after the last of those decisions and nothing reads its result. Line and block confidences are recomputed from the reported word confidences by the rule below. A file without the member reports calibration as before; a version 0 file ignores the member like any member that version does not name. A malformed member is an error exactly as a malformed calibration is, and JSON null means absent. The reason for a second curve: a curve fitted to make the reported number predict correctness on a kind of page (receipt photos, say) differs from the one the filters were tuned against, and swapping it would change the text.
Line confidence in the result type is the calibrated confidences of the line's words averaged with each word weighted by its number of runs (double precision, converted to float32); 0 for a line with no words. This is a reader convention for reporting, not part of the file.
Numerics
Weights are stored in fp16 and computed in fp32. Implementations agree with the PyTorch reference within a documented per-layer tolerance; bit-exact agreement is only required between the C# library's own code paths. The order of floating-point operations that makes those paths bit-identical is written down in numerics.md.