Skip to content

Content model: where a node came from, and how its words were obtained #1197

Description

@fcbwilliams

What we need

We're adopting documents.js as the parsing layer for a revenue-diagnostic product that classifies consulting documents (docx/pptx/xlsx/pdf) against a benchmark schema and cites them as evidence in client-facing reports. Two things follow from "cites them as evidence" that the content model has no room for yet.

1. Where a node's content came from (origin)

ContentTable is currently the same type whether it was authored as a native table, read out of a chart's c:numCache, or produced by a layout pass over a sheet. A reader inspecting a document cannot tell which, and neither can anything downstream.

That matters most for the case a consultant hits constantly: the same visual object arrives by two completely different paths. A chart on a slide is either a native c:chart — exact numbers, from the cache — or a screenshot someone pasted in from Excel. To the person flipping through the deck they look identical.

Proposal: an optional origin on content nodes naming what the content ischart, diagram, table, image, notes, body — set by whichever reader knows, which is the only layer that can. styleId survives round-trip but is format-specific and says nothing for a chart or a diagram.

2. How a node's words were obtained (fidelity)

origin alone is not enough, because those two charts differ in something origin cannot express: one's numbers are verbatim, the other's would be a model's reading of pixels. A pasted chart is origin: 'chart' and derived. One enum cannot carry both facts, so this is a second, independent axis:

  • authored — from the file's own markup (c:numCache, a:t, wp:docPr/@descr, a caption paragraph). Verbatim, quotable.
  • ocr — character recognition over pixels. Deterministic and reproducible; misreads glyphs but never invents content.
  • vision — a vision model's reading. Handles layout, rotation and handwriting far better, and can produce plausible text that is not there.

The distinction is not pedantry — it decides whether a sentence may be quoted in a client deliverable or only referenced. We enforce that downstream, but only if the model tells us which we have.

The three-way split (rather than authored/derived) matters for two concrete reasons: OCR is deterministic, so re-parsing a document yields the same text and therefore the same content hash, where a vision re-run does not; and for dense scanned text OCR is both cheaper and safer, since inventing a contract clause is far worse than misreading one.

3. The annotation itself, and why it cannot live on ContentImageBlock

Our first design put transcript and description on the image block. That is wrong, and a PDF is what proves it.

A chart in a PDF is frequently not an image at allpdf-codec returns it as ContentVector rects, lines and paths plus positioned text runs. There is no ContentImageBlock to hang anything off, and the thing that needs interpreting is forty sibling nodes, not one. The same is true of a slide where several grouped shapes together form a diagram, and of a SmartArt diagram where the node text is read but the relationships between nodes are not (a five-stage process arrives as five labels with no indication it is a sequence).

So the annotation needs to be:

  • node-agnostic — attachable to any content node, not just an image;
  • attachable to a container, because the subject is often a set of sibling nodes rather than one.

Shape we'd propose, as an optional field group in the same spirit as the existing sourcePath / frames / source residue that every leaf variant already carries:

interpretation?: {
  transcript?: { text: string; confidence: 'high' | 'medium' | 'low' };
  description?: string;
  fidelity: 'ocr' | 'vision';
  by?: { model: string; at: string };
}

Two fields rather than one marked string, because they carry different rights: a transcription is the document's own words that happen to be in pixels — quotable — while a description is the model's words about the document, referenceable and never quotable. Both can be present for one subject (a slide screenshot with a title above a chart). A flag would let a consumer forget the difference; two fields cannot be confused.

confidence is a three-value enum deliberately, not a 0–1 float: a self-reported numeric confidence from a VLM does not calibrate, and a float invites downstream thresholds that look principled and are not.

4. caption

ContentImageBlock already carries altText (thank you — that saved us a request). What it has no room for is the visible caption beside a figure, which is usually far more informative than alt text and, unlike alt text, is usually present.

We've implemented the docx side of this (a Caption-styled paragraph, preferring the one below the figure, claimed by at most one figure) and will offer it as a PR separately. The schema question is where the association lands: our own answer is that a caption is associated with the figure rather than copied into it, because it is already a paragraph in its own right and copying it would duplicate it in every flat-text projection and search index built from the document.

What we are NOT asking for

Keep the codecs model-free and offline. No OCR, no vision calls, no network. That property is worth protecting, and pdf-codec's README is right to boast about it. We'll call the models; we just need somewhere to put what comes back, and — for the vector and scanned-page cases — a way to get pixels out (that is a separate request about page rasterisation).

Why it can't be done outside the schema

We could hold all of this in a side table keyed by sourcePath. We'd rather not, and the reason is in your own repo history: the two-tree design that correlated a separate LayoutDocument back to ContentDocument nodes by matching sourcePath strings was replaced by putting frames on the nodes themselves. This is the same shape of problem and would earn the same fix.

Happy to write this as a PR rather than leave it as a request if the shape above is roughly right — say which parts you'd want differently first, since it touches every content variant.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions