--- title: Structured Text Extraction emoji: 📚 colorFrom: blue colorTo: yellow sdk: docker pinned: false license: mit short_description: A structured text extraction using the ellmer package --- # AI-Powered Text Extraction Tool A Shiny app that turns free text, PDFs, scanned PDFs and images into a table. You define the columns (a label, a description and a type for each field), and a large language model fills in one row per item. The table can be downloaded as a CSV file. This is Application 1 of the paper "What Quality Engineers Need to Know About Generative AI: Structured Data Extraction" by Fadel M. Megahed, Ying-Ju Chen, Yamin Dahwich, Arthur Carvalho, L. Allison Jones-Farmer, Ibrahim Yousif, and Inez M. Zwetsloot. The paper is in preparation for submission to *Quality Engineering*. Hosted app: https://huggingface.co/spaces/fmegahed/structured_text_extraction ## What the app does 1. **Input source.** Choose one of six tabs: demo data (two NHTSA recall summaries), pasted text, a text file (.txt, .csv, .md), a readable PDF, a scanned PDF, or images (PNG, JPEG, WebP, GIF). Only the tab that is open when you click Extract Data is used. 2. **Field definitions.** Each field has a label (the column name), a description (the instruction the model receives for that field) and a type (text, number, yes/no, or list of text). You can start from a template for a common quality document: recall notice, 8D / corrective action report, customer complaint record, or inspection / nonconformance note. Field definitions can be saved to a JSON file and loaded again. 3. **Extract Data.** The app sends one request per item to the model. Each item is sent in a new conversation. If an item fails, the others are still processed. 4. **Results.** One row per item, with a `source_id` column, one column per field, and the columns `status` (`ok` or `failed`) and `status_message`. 5. **Reproduce in code.** The app shows R ([ellmer](https://ellmer.tidyverse.org/)) and Python ([chatlas](https://posit-dev.github.io/chatlas/)) code that runs the same extraction with your fields on your own data. The "?" buttons in the app explain each part, including what is sent to the model. ## What to keep in mind - **The results need checking.** The model can misread a document or return a value that the document does not contain. A status of `ok` means that the request completed. It does not mean that the values are correct. - **Your input leaves your computer.** The hosted app sends the content of the active input source and your field definitions to an OpenAI model. Do not submit confidential or proprietary records to the hosted app. To keep data in house, run the app yourself or adapt the code from "Reproduce in code" to another provider or a local model. - **The descriptions are the prompt.** The app adds no instructions of its own. A good description says what to return, in what format, and what to return when the document does not contain the information. ## Limits The limits are constants in `R/config.R`, and the text in the app is generated from them. | Limit | Constant | | --- | --- | | PDF pages (readable or scanned) or images per run | `MAX_PAGES_OR_IMAGES` | | Size of one uploaded file, in MB | `MAX_UPLOAD_MB` | | Number of fields | `MAX_FIELDS` | A readable PDF is sent as one item (the text of its pages). A scanned PDF gives one item per page, and each image is one item. Text is split into items at blank lines. A .csv file gives one item per row of its first column, and its first row is treated as a header. ## Model The model id is set in one place, `DEFAULT_EXTRACTION_MODEL` in `R/config.R`. To use another OpenAI model without editing code, set the environment variable `EXTRACTION_MODEL`. The call, the help text and the generated code all read this value. A test fails if a model id is written anywhere else in the app code, apart from the record of the model that the paper's version used. ## Versions | Version | Date | Model | Notes | |---|---|---|---| | 3.0.1 | 9 October 2026 | `gpt-6-luna` | Released after the paper was drafted. Field templates, saved field definitions, per-item progress and failure handling, code that reproduces an extraction, and in-app help. | | [2.0.1](https://huggingface.co/spaces/fmegahed/structured_text_extraction/tree/v2.0.1-paper) | 25 June 2026 | `gpt-5-mini` | The version the paper describes (tag `v2.0.1-paper`). | The app has been updated since the paper was drafted, and this is intentional. Language models are replaced often, and users find problems that only appear in use, so an app built on a language model needs regular updating. The header of the app links to a short "What changed" note that is generated from the values in `R/config.R`. ## Run it yourself The app needs an OpenAI API key in the environment variable `OPENAI_API_KEY`. On Hugging Face, set it as a Space secret. Never commit the key (`.env` is listed in `.gitignore` and `.dockerignore`). With R (4.5.2 is the version used in the Docker image) and the packages shiny, ellmer, pdftools, magick, base64enc and jsonlite: ```r Sys.setenv(OPENAI_API_KEY = "your key") shiny::runApp(".", port = 7860) ``` With Docker: ```bash docker build -t structured-text-extraction . docker run --rm -p 7860:7860 -e OPENAI_API_KEY="your key" structured-text-extraction ``` Then open http://localhost:7860. ## Repository layout | Path | Content | | --- | --- | | `app.R` | User interface and server | | `R/config.R` | Model id and limits | | `R/fields.R` | Field definitions: validation, JSON import and export, templates, ellmer types | | `R/sources.R` | Reading the input sources and resolving the active tab into items | | `R/extraction.R` | One model call per item, failure handling, and assembly of the results | | `R/snippets.R` | Generated R and Python code | | `R/help.R` | Text of the "?" help dialogs | | `config/templates/` | Field templates, one JSON file each | | `www/` | Theme, client-side script, logos | | `tests/` | Unit and server tests, and a live smoke test | To add a template, add a JSON file to `config/templates/` with the keys `id`, `name`, `description`, `order` and `fields` (each field has a `label`, a `description` and a `type` that is one of `text`, `number`, `yes_no` or `list`). ## Tests ```bash Rscript tests/testthat.R ``` The tests need the packages testthat and withr. They replace the model with a mock, so they need no API key and make no model calls. `.github/workflows/tests.yml` runs them on every push when the repository is hosted on GitHub. `tests/live_smoke.R` is a separate script that makes three short calls to the configured model (the two demo texts and one generated image). It needs an API key: ```bash Rscript tests/live_smoke.R # key from OPENAI_API_KEY Rscript tests/live_smoke.R path/to/.env # key from a KEY=VALUE file ``` ## License MIT