An AI-powered pipeline that generates realistic synthetic PDF documents from text prompts and/or reference images. Uses LLMs (Gemini / local) to design layouts, ReportLab to build PDFs, Faker to populate data, and Augraphy for visual post-processing.
- API: FastAPI + Uvicorn
- LLM: Gemini (online) / local model via LangChain
- PDF: ReportLab
- Data: Faker
- Augmentation: Augraphy
- Validation: Pydantic
graph TD
A["User Input (Text/Image)"] --> B{"Manifest Provided?"}
B -- No --> C["Step 1: Architect (LLM)"]
C --> D["Step 2: Pydantic Validation"]
D -- "Fail" --> E["Step 3: Auditor (LLM Correction)"]
E --> D
D -- "Pass" --> F["Step 4: Template Building (ReportLab)"]
B -- "Yes" --> F
F --> G["Step 5: Data Filling (Faker/LLM)"]
G --> H["Step 6: Document Rendering (PDF)"]
H --> I{"Apply Filters?"}
I -- "Yes" --> J["Step 7: Visual Rendering (Augraphy)"]
J --> K["Final Output (PDF/Image)"]
I -- "No" --> K
K --> L["Step 8: Manifest Export (JSON)"]
- Schema Generation: The Architect LLM takes the user prompt or reference image and generates a structured
DocumentManifest(JSON). - Validation & Correction: Pydantic validates the JSON. If it fails, the Auditor LLM is called with the error log to fix the manifest.
- Template Engineering: The manifest is translated into a ReportLab layout object with defined frames, tables, and styles.
- Synthetic Data Injection: Placeholders in the layout are filled using Faker (or the Data Synth LLM) based on field types defined in the manifest.
- PDF Generation: The filled layout is rendered into a standard PDF byte stream.
- Visual Augmentation (Optional): The PDF is rasterized, processed through Augraphy (scanned effect, stains, folds), and converted back to PDF.
- Export: The final document is saved, and the manifest is tagged with a UUID and exported for reproducibility.
- Architect — generates a JSON manifest (
DocumentManifest) based on user input and/or reference image - Auditor — reviews a manifest + validation errors and returns a corrected version (retry loop)
- Data Synth (optional) — generates domain-aware synthetic data via LLM instead of Faker
Decisions needed before implementation:
- Should Data Synth persona use the LLM for domain-aware data, or is Faker sufficient?
- How many Auditor retry loops before failing? (suggested: 3)
- What DPI for the render step's PDF → image → PDF roundtrip? (suggested: 300)
- Should
render()also support returning images directly (useful for ML training), or always PDF?
pAIge/
├── app/
│ ├── main.py # FastAPI app + route definitions
│ ├── config.py # Settings (API keys, defaults, DPI, retry limits)
│ ├── models/
│ │ ├── schemas.py # Pydantic: manifest, API request/response
│ │ └── filters.py # Pydantic: filter config
│ ├── services/
│ │ ├── pipeline.py # generate() orchestrator
│ │ ├── schema_gen.py # generate_schema()
│ │ ├── template.py # build_template()
│ │ ├── data_fill.py # layout_data()
│ │ ├── builder.py # build_document()
│ │ ├── renderer.py # render()
│ │ └── manifest.py # export_manifest()
│ ├── llm/
│ │ ├── client.py # query_model()
│ │ └── prompts.py # System prompts
│ └── utils/
│ └── errors.py # Custom exception types
├── tests/
│ ├── test_pipeline.py
│ ├── test_schema_gen.py
│ ├── test_template.py
│ ├── test_data_fill.py
│ ├── test_builder.py
│ ├── test_renderer.py
│ └── test_manifest.py
├── requirements.txt
└── README.md
Main endpoint that builds documents.
Expected input (GenerateRequest):
| Field | Type | Required | Default |
|---|---|---|---|
prompt |
str |
optional | — |
reference_image |
UploadFile |
optional | — |
mode |
str |
no | "online" |
file_path |
str |
yes | — |
apply_filter |
bool |
no | false |
filter_config |
dict[str, float] |
no | — |
count |
int |
no | 1 |
manifest |
DocumentManifest |
optional | — |
Edge case: error if both prompt and reference_image are empty (and no manifest provided).
Flow:
for i in range(count):
if manifest is empty:
manifest = generate_schema(prompt, reference_image)
layout = build_template(manifest)
filled = layout_data(layout, manifest)
pdf = build_document(filled)
if apply_filter:
pdf = render(pdf, filter_config)
save pdf to file_path
manifest_id = export_manifest(manifest)
return GenerateResponse(file_paths, manifest_id)
Health check endpoint.
Generates a validated JSON manifest via LLM.
call query_model(ARCHITECT, prompt, image)
validate with Pydantic (DocumentManifest)
if valid:
return manifest
else:
for attempt in range(MAX_AUDITOR_RETRIES):
append validation errors to context
call query_model(AUDITOR, manifest + errors)
validate again
if valid: return manifest
raise SchemaGenerationError
Dispatches to online (Gemini) or local model via LangChain.
Input:
persona— system prompt (ARCHITECT, AUDITOR, or DATA_SYNTH)mode— "online" or "offline"text_input— user text prompt (optional)image_input— reference image (optional)
Edge case: raise ValueError if both text and image are empty.
Translates manifest elements into ReportLab Flowable / Frame / PageTemplate objects. Returns a structured layout (not yet a PDF).
Input: JSON manifest Output: ReportLab layout object
Populates placeholder slots in the layout with synthetic data.
Input: ReportLab layout + manifest (for field type hints) Output: Filled ReportLab layout
Uses Faker, seeded by manifest field types (text, numbers, dates, addresses, etc.).
Renders the filled ReportLab layout to PDF bytes.
Input: Filled ReportLab layout Output: PDF byte stream
Note: only needs the filled layout, not the manifest.
Applies Augraphy visual augmentations.
Input: PDF bytes + filter config {filter_name: intensity} + DPI
Output: Modified PDF bytes
convert PDF to image (via pdf2image at configured DPI)
map filter_config keys to Augraphy augmentation classes
apply augmentations in order
convert image back to PDF
⚠️ This is a lossy operation — text selectability and vector graphics are lost (intentional for training-data use cases).
Renders filled ReportLab layout to PDF.
Assigns a UUID, writes manifest to disk as JSON, returns the UUID for reproducibility.
PaigeError (base)
├── SchemaGenerationError — LLM/validation failures after retries
├── TemplateError — ReportLab layout construction failures
├── BuildError — PDF rendering failures
└── RenderError — Augraphy/image conversion failures
Each error carries context (manifest, step, attempt number). FastAPI exception handlers map these to HTTP status codes.
The core schema the LLM must produce. Strict Pydantic validation.
- Page dimensions (width, height, units)
- Elements list: text blocks, tables, images, lines, shapes
- Each element: type, position (x, y, w, h), font, size, style, content placeholder
- Ordered dict:
{augraphy_filter_name: intensity} - Validator ensures names match valid Augraphy augmentation classes
- Intensity range: 0.0–1.0
pytest tests/ -v| Test File | Covers |
|---|---|
test_schema_gen.py |
Pydantic validation pass/fail, Auditor retry loop, exhaustion |
test_template.py |
Manifest → ReportLab layout for various element types |
test_data_fill.py |
Faker populates correct field types; empty manifest edge case |
test_builder.py |
Produces valid PDF bytes (check %PDF magic bytes) |
test_renderer.py |
Augraphy filter application; invalid filter name rejection |
test_manifest.py |
UUID assignment; JSON file written to disk |
test_pipeline.py |
Full end-to-end (mocked LLM); count > 1; filter on/off |
LLM calls are mocked in tests to avoid API costs and ensure determinism.
- Start:
uvicorn app.main:app --reload - Open:
http://localhost:8000/docs - Send
POST /generatewith prompt: "Generate a single-page invoice with header, company logo, line items table, and total" - Verify: output PDF opens correctly, layout matches prompt
- Test with
apply_filter=true, confirm output looks visually augmented