Generative AI · Document AI · Synthetic data
Synthetic Document Factory
Generates labelled document-layout datasets with a local LLM: PDFs, page images and exact bounding boxes in COCO and YOLO format, using DocLayNet's label set.
- Python
- Ollama
- ReportLab
- Matplotlib
- pypdfium2
- pytest
Overview
Training a layout detection model such as YOLO, DETR or LayoutLMv3 needs pages where every title, paragraph, list, table and figure has a box and a label, and drawing those boxes by hand is slow. In this tool, a local Ollama model writes the document, the tool lays it out in a random style, and it records where each element lands while the PDF is being drawn. The boxes are exact, not guessed afterwards.

System architecture
Topic + page count
-> Outline the LLM plans the section titles
-> Write two LLM calls per section, so small models still reach the word target
-> Polish weak paragraphs (generic openers, filler endings, repeats) get rewritten
-> Visuals the LLM returns table and chart data as JSON; charts drawn with matplotlib
-> Render ReportLab lays out the PDF while a tracking frame records every box
-> Fit pages add or trim text and re-render until the page count is right
-> Variants re-render the same text in new random layouts
-> Export page images, YOLO labels, COCO annotations and data.yamlTechnical implementation
Box tracking inside the renderer. A ReportLab Frame subclass wraps each labelled element's drawOn call and records its page, coordinates and text at the moment it's drawn. When a paragraph or table splits across pages or columns, each piece gets its own box, which is how DocLayNet labels them.
class _TrackingFrame(Frame):
def add(self, flowable, canv, trySplit=0):
label = getattr(flowable, "_layout_label", None)
if not label:
return Frame._add(self, flowable, canv, trySplit)
boxes = self._boxes
original = flowable.drawOn
def drawOn(c, x, y, _sW=0):
x = flowable._hAlignAdjust(x, _sW)
w, h = _flowable_size(flowable)
x0, x1 = _paragraph_extent(flowable, x, w) if isinstance(flowable, Paragraph) else (x, x + w)
boxes.append({
"page": c.getPageNumber(), "label": label,
"x0": x0, "y0": y, "x1": x1, "y1": y + h,
"text": _flowable_text(flowable),
})
return original(c, x, y)
flowable.drawOn = drawOn
try:
return Frame._add(self, flowable, canv, trySplit)
finally:
del flowable.drawOn- Layout variety: one or two columns, Times or Helvetica, four font sizes, three margin sizes, A4 or Letter, running headers, numbered or plain headings, and justified or left-aligned text.
- Cheap augmentation:
--variants 5renders the same text in five layouts without any extra LLM calls. - Page targeting: after the first render, the tool adds or trims text and re-renders until the document has the requested number of pages, and stops if the count stalls.
- Export: one PNG per page, YOLO label files, COCO annotations in pixels that also store each box's text (useful for text-aware models like LayoutLM), and a
data.yamlready for YOLO training.
Engineering decisions
Record boxes during rendering, not after
Detecting elements on a finished page would add its own errors. Recording them while ReportLab draws makes every box exact by construction.
Match DocLayNet's labels and ids
The output uses the same 11 labels and ids as DocLayNet, so it can be mixed with that dataset or used with models trained on it. Footnote and Formula are kept in the map even though they aren't generated.
Reuse text across layouts
The local LLM is the slow part. Rendering the same text in several layouts multiplies the labelled pages without more generation.
Verification
The test suite runs without Ollama. It checks that every labelled element lies inside its page, reads the text back out of each box to confirm it matches the labelled text, checks that a paragraph split across pages gets a box on each page, checks that export writes COCO, YOLO and images, and checks that random layouts repeat with a seed.
The repository doesn't include results from training a detector on the generated data, so this page doesn't quote any.
Limitations
- Text is English only (Latin alphabet), because the PDFs use the built-in PDF fonts.
- A document takes several minutes on a laptop, since the local LLM writes every section.
data.yamlpoints train and validation at the same images, so they need splitting before real training.