Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Extract readable text from document files — PDF, XLSX/CSV/TSV, DOCX, PPTX, and plain-text formats — so any task that arrives with an attachment can actually consume it.
.claude/skills/sonichi-doc-ingest/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-05 | ✗→✓ | ▲ Improved | 902% | 0% |
| case-09 | ✗→✓ | ▲ Improved | -5% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 57% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 34% | 0% |
| case-14 | ✗→✓ | ▲ Improved | -22% | 0% |
Extract readable text from document files — PDF, XLSX/CSV/TSV, DOCX, PPTX, and plain-text formats — so any task that arrives with an attachment can actually consume it.
Usage:
bashpython3 skills/doc-ingest/scripts/ingest.py <file> [<file> ...] [--json] [--csv] [--max-chars N]
Prints the extracted text to stdout (default cap 200k chars per file, --max-chars 0 = uncapped). --json wraps each file's result in {"file", "kind", "ok", "text"|"error"} lines (JSONL) for programmatic callers. --csv switches tabular files to the compute-exact view (below).
When the ask is quantitative over a tabular file — "how many …", "total …", "average …", "what percentage …", any filter/sum/count — do NOT answer by reading the extracted markdown. Load the exact table and compute:
bashpython3 skills/doc-ingest/scripts/ingest.py sheet.xlsx --csv # exact, uncapped per-sheet CSV
then aggregate programmatically (pandas or stdlib csv), keeping a per-row breakdown so the result is auditable. Two exactness guarantees distinguish --csv from the reading view: no default row/char caps (a silently truncated table computes a silently wrong aggregate), and xlsx without openpyxl is refused with a clear error rather than served by the approximate zip-XML fallback (approximate cells are fine to read, not to compute with).
No caps does not mean no bounds — attachments are untrusted, so --csv carries a fail-closed compute budget: inputs over 32 MiB, renders over 64 MiB, or tables over the 1M-cell cap are refused with a loud error (never truncated). A caller that genuinely needs a bigger table passes the explicit --csv-no-budget override.
Why this is a rule and not a preference: on the GAIA file-attached benchmark subset (2026-07-30), switching solvers from reading extracted text to computing over the loaded table flipped 3/3 computable misses (multi-row Whyte-notation sums, filtered counts, parity logic) with no other change — 84.2% → 92.1%. The markdown view is for humans and summaries; numbers come from computation.
[File attached: …] with a document the task needs read (report summarization, spreadsheet questions, contract review).Not for:
skills/audio-transcribe (the script points there and exits 3).| Format | Primary | Fallback | |---|---|---| | .pdf | pdftotext -layout (poppler) | pypdf/fitz if importable, else a clear error naming the missing tool | | .xlsx .xlsm | openpyxl (every sheet → markdown table, row-capped) | dependency-free XML extraction from the zip | | .csv .tsv | stdlib csv → markdown table (row-capped) | — | | .docx | python-docx (paragraphs + tables) | textutil -convert txt (macOS), else zip XML extraction | | .pptx | zip XML extraction (per-slide text, dependency-free) | — | | .zip | member manifest + recursive extraction of the first 20 supported members (flattened basenames — zip-slip safe) | — | | .txt .md .json .jsonl .xml .html code files | bounded streaming read (UTF-8, errors replaced) | — | | .rtf .doc | textutil -convert txt (macOS) | error naming the gap |
Exit codes: 0 all files extracted · 1 at least one failed · 2 bad invocation · 3 file type is handled elsewhere (image/audio pointer printed).
Tabular rendering defaults to 500 rows per sheet (--max-rows). CSV and XLSX rows are consumed incrementally: only the rendered prefix is retained while the computed summary is updated over the stream. Hard shared safety budgets cap compressed/uncompressed table bytes, rows, cells, and cell text; exceeding one fails the file explicitly instead of risking unbounded attachment memory use. The dependency-free XLSX reader rejects sparse cell references before they can expand into a dense row beyond the remaining cell budget or Excel's column limit. Plain-text inputs are decoded in fixed-size chunks and retain only the --max-chars prefix (--max-chars 0 is the explicit uncapped mode). A display-truncation notice is appended whenever the rendered row cap fires, so a consumer never mistakes a prefix for the whole document.
Pure stdlib + optional libraries probed at runtime — the skill works (with reduced format coverage) on a host with no extras installed, and never hard-depends on a library CI lacks. No network, no temp files, read-only on inputs.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 4,653 | 34,186 | +635% | 1 | 1 | 0% | 676 | 1,555 | +130% | 0 | 0 | — |
case-02 | fail→fail | 9,466 | 5,768 | -39% | 1 | 1 | 0% | 1,605 | 1,637 | +2% | 0 | 0 | — |
case-03 | fail→fail | 3,036 | 5,169 | +70% | 1 | 1 | 0% | 402 | 1,500 | +273% | 0 | 0 | — |
case-04 | fail→fail | 2,220 | 9,240 | +316% | 1 | 1 | 0% | 230 | 2,007 | +773% | 0 | 0 | — |
case-05 | fail→pass | 2,401 | 9,244 | +285% | 1 | 1 | 0% | 265 | 2,655 | +902% | 0 | 0 | — |
case-06 | fail→fail | 29,615 | 6,460 | -78% | 1 | 1 | 0% | 6,166 | 1,481 | -76% | 0 | 0 | — |
case-07 | pass→pass | 3,992 | 1,981 | -50% | 1 | 1 | 0% | 611 | 1,485 | +143% | 0 | 0 | — |
case-08 | pass→pass | 14,845 | 6,234 | -58% | 1 | 1 | 0% | 2,465 | 2,176 | -12% | 0 | 0 | — |
case-09 | fail→pass | 10,763 | 1,806 | -83% | 1 | 1 | 0% | 1,615 | 1,529 | -5% | 0 | 0 | — |
case-10 | pass→pass | 9,808 | 1,753 | -82% | 1 | 1 | 0% | 1,389 | 1,473 | +6% | 0 | 0 | — |
case-11 | pass→pass | 3,959 | 1,104 | -72% | 1 | 1 | 0% | 577 | 1,383 | +140% | 0 | 0 | — |
case-12 | fail→pass | 7,018 | 2,681 | -62% | 1 | 1 | 0% | 1,075 | 1,688 | +57% | 0 | 0 | — |
case-13 | fail→pass | 23,599 | 2,561 | -89% | 1 | 1 | 0% | 1,222 | 1,642 | +34% | 0 | 0 | — |
case-14 | fail→pass | 14,127 | 2,849 | -80% | 1 | 1 | 0% | 2,197 | 1,707 | -22% | 0 | 0 | — |
case-15 | fail→pass | 47,740 | 1,782 | -96% | 1 | 1 | 0% | 828 | 1,516 | +83% | 0 | 0 | — |
case-16 | fail→pass | 25,349 | 1,536 | -94% | 1 | 1 | 0% | 3,787 | 1,433 | -62% | 0 | 0 | — |
case-17 | fail→pass | 10,462 | 2,246 | -79% | 1 | 1 | 0% | 1,553 | 1,581 | +2% | 0 | 0 | — |
case-18 | fail→pass | 11,335 | 1,319 | -88% | 1 | 1 | 0% | 1,594 | 1,409 | -12% | 0 | 0 | — |
case-19 | pass→pass | 4,590 | 1,942 | -58% | 1 | 1 | 0% | 615 | 1,518 | +147% | 0 | 0 | — |
case-20 | fail→pass | 13,599 | 3,987 | -71% | 1 | 1 | 0% | 1,787 | 1,777 | -1% | 0 | 0 | — |
case-21 | fail→pass | 15,225 | 5,725 | -62% | 1 | 1 | 0% | 2,398 | 2,057 | -14% | 0 | 0 | — |
case-22 | pass→pass | 6,202 | 2,509 | -60% | 1 | 1 | 0% | 1,024 | 1,618 | +58% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 15 counted toward the lift figure. The other 7 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +50 percentage points is the difference between those two pass rates over the 15 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.