tablewise 1.1.1__tar.gz → 1.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {tablewise-1.1.1 → tablewise-1.1.2}/PKG-INFO +87 -19
  2. {tablewise-1.1.1 → tablewise-1.1.2}/README.md +74 -16
  3. tablewise-1.1.2/motor_office.py +172 -0
  4. tablewise-1.1.2/motor_propio.py +476 -0
  5. tablewise-1.1.2/postpro.py +94 -0
  6. {tablewise-1.1.1 → tablewise-1.1.2}/preconversion.py +41 -0
  7. {tablewise-1.1.1 → tablewise-1.1.2}/pyproject.toml +6 -6
  8. {tablewise-1.1.1 → tablewise-1.1.2}/tablewise.egg-info/PKG-INFO +87 -19
  9. {tablewise-1.1.1 → tablewise-1.1.2}/tablewise.egg-info/SOURCES.txt +10 -1
  10. tablewise-1.1.2/tablewise.egg-info/requires.txt +25 -0
  11. {tablewise-1.1.1 → tablewise-1.1.2}/tablewise.egg-info/top_level.txt +3 -0
  12. {tablewise-1.1.1 → tablewise-1.1.2}/tablewise_app.py +65 -22
  13. tablewise-1.1.2/tablewise_cli.py +401 -0
  14. {tablewise-1.1.1 → tablewise-1.1.2}/tablewise_core.py +122 -3
  15. {tablewise-1.1.1 → tablewise-1.1.2}/tests/test_app_convertir.py +42 -0
  16. tablewise-1.1.2/tests/test_cli.py +355 -0
  17. {tablewise-1.1.1 → tablewise-1.1.2}/tests/test_core.py +69 -2
  18. tablewise-1.1.2/tests/test_motor_office.py +238 -0
  19. tablewise-1.1.2/tests/test_motor_propio.py +365 -0
  20. tablewise-1.1.2/tests/test_motor_propio_escaneados.py +174 -0
  21. tablewise-1.1.2/tests/test_quality.py +57 -0
  22. tablewise-1.1.2/tests/test_sprint_deliver.py +234 -0
  23. tablewise-1.1.2/tests/test_weekly_summary.py +46 -0
  24. tablewise-1.1.1/tablewise.egg-info/requires.txt +0 -11
  25. tablewise-1.1.1/tablewise_cli.py +0 -185
  26. tablewise-1.1.1/tests/test_cli.py +0 -149
  27. {tablewise-1.1.1 → tablewise-1.1.2}/LICENSE +0 -0
  28. {tablewise-1.1.1 → tablewise-1.1.2}/motor_docling.py +0 -0
  29. {tablewise-1.1.1 → tablewise-1.1.2}/motor_excel.py +0 -0
  30. {tablewise-1.1.1 → tablewise-1.1.2}/setup.cfg +0 -0
  31. {tablewise-1.1.1 → tablewise-1.1.2}/tablewise.egg-info/dependency_links.txt +0 -0
  32. {tablewise-1.1.1 → tablewise-1.1.2}/tablewise.egg-info/entry_points.txt +0 -0
  33. {tablewise-1.1.1 → tablewise-1.1.2}/tests/test_cli_motores.py +0 -0
  34. {tablewise-1.1.1 → tablewise-1.1.2}/tests/test_docs.py +0 -0
  35. {tablewise-1.1.1 → tablewise-1.1.2}/tests/test_entradas_externas.py +0 -0
  36. {tablewise-1.1.1 → tablewise-1.1.2}/tests/test_motor_docling.py +0 -0
  37. {tablewise-1.1.1 → tablewise-1.1.2}/tests/test_motor_docling_cli.py +0 -0
  38. {tablewise-1.1.1 → tablewise-1.1.2}/tests/test_motor_docling_convertir.py +0 -0
  39. {tablewise-1.1.1 → tablewise-1.1.2}/tests/test_motor_excel.py +0 -0
  40. {tablewise-1.1.1 → tablewise-1.1.2}/tests/test_preconversion.py +0 -0
  41. {tablewise-1.1.1 → tablewise-1.1.2}/tests/test_preconversion_ejecutables_falsos.py +0 -0
  42. {tablewise-1.1.1 → tablewise-1.1.2}/tests/test_preconversion_errores.py +0 -0
  43. {tablewise-1.1.1 → tablewise-1.1.2}/tests/test_red_sintaxis.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tablewise
3
- Version: 1.1.1
3
+ Version: 1.1.2
4
4
  Summary: Spreadsheet-grade document to Markdown with provenance, ready for RAG and LLM ingestion
5
5
  Author: Luis Francisco Palomares Gamarra
6
6
  License: MIT License
@@ -27,8 +27,6 @@ License: MIT License
27
27
 
28
28
  Project-URL: Homepage, https://tablewise.dev
29
29
  Project-URL: Documentation, https://tablewise.dev/docs
30
- Project-URL: Repository, https://github.com/lfpalomaresg/tablewise
31
- Project-URL: Changelog, https://github.com/lfpalomaresg/tablewise/blob/main/CHANGELOG.md
32
30
  Keywords: markdown,rag,pdf,docx,xlsx,markitdown,docling
33
31
  Classifier: License :: OSI Approved :: MIT License
34
32
  Classifier: Programming Language :: Python :: 3
@@ -44,16 +42,31 @@ Description-Content-Type: text/markdown
44
42
  License-File: LICENSE
45
43
  Provides-Extra: excel
46
44
  Requires-Dist: openpyxl>=3.1; extra == "excel"
45
+ Requires-Dist: xlrd>=2.0; extra == "excel"
47
46
  Provides-Extra: docling
48
47
  Requires-Dist: docling; extra == "docling"
48
+ Provides-Extra: office
49
+ Requires-Dist: python-docx>=1.1; extra == "office"
50
+ Requires-Dist: python-pptx>=0.6.23; extra == "office"
51
+ Provides-Extra: propio
52
+ Requires-Dist: onnxruntime>=1.24; extra == "propio"
53
+ Requires-Dist: pymupdf>=1.24; extra == "propio"
54
+ Requires-Dist: numpy>=1.26; extra == "propio"
55
+ Requires-Dist: pillow>=10; extra == "propio"
56
+ Requires-Dist: rapidocr<4,>=3.9; extra == "propio"
49
57
  Provides-Extra: dev
50
58
  Requires-Dist: pytest>=8; extra == "dev"
51
59
  Requires-Dist: openpyxl>=3.1; extra == "dev"
52
60
  Requires-Dist: PyYAML>=6; extra == "dev"
61
+ Requires-Dist: python-docx>=1.1; extra == "dev"
62
+ Requires-Dist: python-pptx>=0.6.23; extra == "dev"
53
63
  Dynamic: license-file
54
64
 
55
65
  # Tablewise
56
66
 
67
+ [![PyPI](https://img.shields.io/pypi/v/tablewise.svg)](https://pypi.org/project/tablewise/)
68
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
69
+
57
70
  **Spreadsheet-grade document conversion to Markdown, with provenance.**
58
71
  PDF, Word, Excel and PowerPoint in; clean, citable Markdown out — built for RAG,
59
72
  LLM ingestion and audit trails.
@@ -72,10 +85,23 @@ converted: 2026-10-22
72
85
  Three front ends, one decision engine:
73
86
 
74
87
  - **`tablewise` CLI** — one document in, one `.md` out. Scripts, agents, batches.
75
- - **Desktop app** (Tkinter) — for people who do not live in a terminal.
88
+ - **Done-for-you ingestion sprint** — we convert your corpus and check it against a
89
+ sample you approve: https://tablewise.dev/#done-for-you (a Tkinter desktop app ships
90
+ in this repository but is not packaged).
76
91
  - **Hosted API** — `POST /v1/convert`, per-month plans, no install. See
77
92
  [PRICING.md](PRICING.md).
78
93
 
94
+ ## Documentation
95
+
96
+ - [User guide](docs/USER_GUIDE.md) — CLI, desktop app and API, step by step.
97
+ - [Architecture](docs/ARCHITECTURE.md) — components, data model, failure modes.
98
+ - [Maintenance](docs/MAINTENANCE.md) — cadence, automation, incidents.
99
+ - [Business model](docs/BUSINESS_MODEL.md) — public repo + paid product (open-core).
100
+ - [Decisions](docs/DECISIONS.md) — short ADR log (why SQLite, monthly only, engine fallback).
101
+ - [Retainer](docs/RETAINER.md) — how the monthly ingestion service works, end to end.
102
+ - [Operations](docs/OPERATIONS.md) · [Pricing](PRICING.md) · [Security](SECURITY.md) ·
103
+ [Contributing](CONTRIBUTING.md) · [Changelog](CHANGELOG.md).
104
+
79
105
  ## The problem it solves
80
106
 
81
107
  Most converters flatten a financial table into something no model can use. A
@@ -96,23 +122,17 @@ Tablewise routes spreadsheets to an engine that reads the sheet as a sheet:
96
122
  - Hidden sheets kept but labelled `(hidden sheet)`.
97
123
  - A formula that was never recalculated says `(formula not calculated)` instead
98
124
  of an empty cell that a model reads as zero.
99
- - Deterministic: no AI, no network, same bytes in → same Markdown out.
125
+ - Deterministic: no AI, no network, same input and same content (the provenance date is the only variable).
100
126
 
101
127
  ## Quickstart
102
128
 
103
129
  ```bash
104
- pip install -e . # installs the `tablewise` command
130
+ pip install "tablewise[excel]" # from PyPI; add [docling] or [propio] for PDF/DOCX tables
105
131
  tablewise report.pdf # -> ~/md-repo/report.md
106
132
  tablewise budget.xlsx -o ~/md-repo -n "2026 budget"
107
133
  tablewise contract.docx --dry-run # print the engine command, do not run it
108
134
  ```
109
135
 
110
- The desktop app:
111
-
112
- ```bash
113
- python tablewise_app.py
114
- ```
115
-
116
136
  The API:
117
137
 
118
138
  ```bash
@@ -121,6 +141,14 @@ curl -X POST https://api.tablewise.dev/v1/convert \
121
141
  -F "file=@budget.xlsx"
122
142
  ```
123
143
 
144
+ ## Benchmark in one line
145
+
146
+ On the synthetic P&L of the public benchmark (three-level header, merged cells,
147
+ formulas, percentages), MarkItDown produces 11 `Unnamed` columns and 41 empty cells;
148
+ Tablewise produces none, keeps the 3 percentages and labels the 3 unevaluated
149
+ formulas. Reproduce it with `python bench/run_benchmark.py` or download the five
150
+ workbooks from https://tablewise.dev/benchmark.
151
+
124
152
  ## Why not just MarkItDown / Docling?
125
153
 
126
154
  They are good engines and Tablewise uses both. The difference is the routing,
@@ -132,7 +160,6 @@ the table fidelity and the provenance — not a new parser.
132
160
  | Formula never recalculated | empty cell | empty cell | labelled |
133
161
  | Provenance header on every file | no | no | yes |
134
162
  | Deterministic, no AI, no network | yes | yes (heavy) | yes |
135
- | Desktop GUI for non-technical users | no | no | yes |
136
163
  | Hosted API with per-month plans and batch | no | no | yes |
137
164
 
138
165
  ## Engine routing
@@ -140,14 +167,44 @@ the table fidelity and the provenance — not a new parser.
140
167
  Each extension has exactly one owner:
141
168
 
142
169
  - `.xlsx` `.xlsm` `.xltx` `.xltm` → `motor_excel.py` (openpyxl, local, no AI)
143
- - `.pdf` `.docx` `.pptx` → `motor_docling.py` (Docling, local, no AI)
170
+ - `.docx` `.pptx` → `motor_office.py` (python-docx/python-pptx, local, no AI) when the `office` extra is installed; otherwise `motor_docling.py`
171
+ - `.pdf` → `motor_propio.py` when its models are installed; otherwise `motor_docling.py` (Docling, local, no AI) — **optional** in the hosted image (`INSTALL_DOCLING=0` by default)
144
172
  - `.doc` `.rtf` `.odt` → `textutil` (macOS) → HTML → `motor_docling.py`
145
- - `.xls` `.ppt` (Office 97-2003) → LibreOffice → `.xlsx`/`.pptx` → `motor_excel.py` / `motor_docling.py`
173
+ - `.xls` (Office 97-2003) → LibreOffice → `.xlsx` → `motor_excel.py`
174
+ - `.ppt` (Office 97-2003) → LibreOffice → `.pptx` inside `motor_docling.py` (legacy path; `motor_office` does not read `.ppt`)
146
175
  - everything else (CSV, HTML, EPUB, …) → MarkItDown
147
176
 
148
- The two own engines are `motor_excel.py` and `motor_docling.py`. A format
149
- without an available engine is refused with an explicit message, never silently
150
- degraded.
177
+ The own engines are `motor_excel.py`, `motor_office.py`, `motor_docling.py` and `motor_propio.py`.
178
+ Known limits of `motor_office`: headers/footers are not extracted, text boxes
179
+ are appended at the end (visual order is not guaranteed), and a nested table is
180
+ emitted after its parent table. Legacy `.doc/.rtf/.odt/.ppt` stay on Docling.
181
+
182
+ ### PDF tables (own engine)
183
+
184
+ PDFs use the own **`motor_propio.py`** engine (Microsoft Table Transformer +
185
+ RapidOCR, ONNX, CPU) instead of Docling **only when its models are installed**:
186
+ it is faster (~0.9 s/doc digital, ~2.6 s scanned) and more accurate on tables
187
+ (0.94 exact rows on the blind control and 0.92 on the real set; v1: 0.92 / 0.90).
188
+ Full per-set table: [tablewise.dev/benchmark#pdf-engine](https://tablewise.dev/benchmark#pdf-engine).
189
+ Since `modelos-v2` (c07) it runs two detectors (`det` v1 + `det2` c06) and picks
190
+ the better box per page; the structure model is unchanged. The models are
191
+ published at
192
+ [`tablewise-modelos`](https://github.com/lfpalomaresg/tablewise-modelos/releases/tag/modelos-v2);
193
+ install them with:
194
+
195
+ ```bash
196
+ curl -fsSL -O https://github.com/lfpalomaresg/tablewise-modelos/releases/download/modelos-v2/SHA256SUMS
197
+ python3 scripts/modelos_propio.py \
198
+ --url https://github.com/lfpalomaresg/tablewise-modelos/releases/download/modelos-v2/ \
199
+ --sha256 SHA256SUMS
200
+ python3 scripts/modelos_propio.py --verificar # det2: sí
201
+ ```
202
+
203
+ They go to `~/.cache/tablewise/modelos` (or `TABLEWISE_PROPIO_MODELS`); ~347 MB,
204
+ never in the repo or the wheel. `PROPIO_DET2=0` disables the second detector.
205
+ Without the models, PDFs stay on Docling — no
206
+ change in behaviour. A format without an available engine is refused with an
207
+ explicit message, never silently degraded.
151
208
 
152
209
  ## CLI reference
153
210
 
@@ -173,9 +230,14 @@ The API is the same engine behind HTTP, for teams that cannot install anything:
173
230
  | Business | 25,000 | $149 |
174
231
 
175
232
  One unit = one PDF page, one spreadsheet sheet, one slide, or one document.
176
- Overage $0.015/unit. Annual −20%. The full contract (quota, DPA, refunds) is in
233
+ No overage billing yet (upgrade the plan). Annual billing is not available yet. The full contract (quota, DPA, refunds) is in
177
234
  [PRICING.md](PRICING.md) and on the website.
178
235
 
236
+ ## Operations
237
+
238
+ Deploying, billing, support, backups and incident playbook:
239
+ [docs/OPERATIONS.md](docs/OPERATIONS.md).
240
+
179
241
  ## Development
180
242
 
181
243
  Runner: `pytest`, using the engines virtualenv (it carries openpyxl and PyYAML):
@@ -194,6 +256,10 @@ Runner: `pytest`, using the engines virtualenv (it carries openpyxl and PyYAML):
194
256
  | `tests/test_preconversion.py` | LibreOffice (`soffice`): lookup outside PATH and `.xls` → `.xlsx` keeping merges (skipped without LibreOffice) |
195
257
  | `tests/test_preconversion_errores.py` | `preconversion.py` with simulated `subprocess.run`: timeout, tool that cannot start, exit 0 without file, error code, own profile |
196
258
  | `tests/test_motor_docling_convertir.py` | `motor_docling.convertir` with an injected fake Docling: empty output, temp cleanup, `.ppt` routing, uppercase extensions |
259
+ | `tests/test_motor_propio.py` | own PDF engine: model resolution, safe PDF→own/Docling routing, table repairs, CLI error contract |
260
+ | `tests/test_motor_propio_escaneados.py` | own PDF engine on scans: OCR without line-orientation classifier, lone dash in empty value cells |
261
+ | `tests/test_quality.py` | synthetic quality KPI: table/row metric and table-text filtering without loading the ONNX models |
262
+ | `tests/test_motor_office.py` | light Office engine: `.docx`/`.pptx` paragraphs and tables without Docling, CLI contract |
197
263
  | `tests/test_entradas_externas.py` | `.xls` path, malformed array formulas, backslashes in the YAML header, non-UTF-8 output, BOM and CRLF |
198
264
  | `tests/test_preconversion_ejecutables_falsos.py` | textutil/LibreOffice boundary with fake executables |
199
265
  | `tests/test_app_convertir.py` | `App.convert()` with fake tkinter and engine: existing `.md` (cancel / numbered copy / overwrite), failing engine, non-UTF-8 output, partial writes |
@@ -201,6 +267,8 @@ Runner: `pytest`, using the engines virtualenv (it carries openpyxl and PyYAML):
201
267
  | `tests/test_red_sintaxis.py` | regression net: every application file is valid Python and the app imports without opening a window |
202
268
  | `tests/test_cli.py` | `tablewise` CLI: same decisions as the app, `--dry-run`, no overwrite without `--force`, exit codes |
203
269
  | `tests/test_docs.py` | drift net: this README lists every test file and every routed extension |
270
+ | `tests/test_sprint_deliver.py` | `scripts/sprint_deliver.py`: delivery package, manifest hashes, failed files listed, determinism check |
271
+ | `tests/test_weekly_summary.py` | `scripts/weekly_summary.py`: the 5-number weekly funnel summary |
204
272
 
205
273
  No test uses real documents.
206
274
 
@@ -1,5 +1,8 @@
1
1
  # Tablewise
2
2
 
3
+ [![PyPI](https://img.shields.io/pypi/v/tablewise.svg)](https://pypi.org/project/tablewise/)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
5
+
3
6
  **Spreadsheet-grade document conversion to Markdown, with provenance.**
4
7
  PDF, Word, Excel and PowerPoint in; clean, citable Markdown out — built for RAG,
5
8
  LLM ingestion and audit trails.
@@ -18,10 +21,23 @@ converted: 2026-10-22
18
21
  Three front ends, one decision engine:
19
22
 
20
23
  - **`tablewise` CLI** — one document in, one `.md` out. Scripts, agents, batches.
21
- - **Desktop app** (Tkinter) — for people who do not live in a terminal.
24
+ - **Done-for-you ingestion sprint** — we convert your corpus and check it against a
25
+ sample you approve: https://tablewise.dev/#done-for-you (a Tkinter desktop app ships
26
+ in this repository but is not packaged).
22
27
  - **Hosted API** — `POST /v1/convert`, per-month plans, no install. See
23
28
  [PRICING.md](PRICING.md).
24
29
 
30
+ ## Documentation
31
+
32
+ - [User guide](docs/USER_GUIDE.md) — CLI, desktop app and API, step by step.
33
+ - [Architecture](docs/ARCHITECTURE.md) — components, data model, failure modes.
34
+ - [Maintenance](docs/MAINTENANCE.md) — cadence, automation, incidents.
35
+ - [Business model](docs/BUSINESS_MODEL.md) — public repo + paid product (open-core).
36
+ - [Decisions](docs/DECISIONS.md) — short ADR log (why SQLite, monthly only, engine fallback).
37
+ - [Retainer](docs/RETAINER.md) — how the monthly ingestion service works, end to end.
38
+ - [Operations](docs/OPERATIONS.md) · [Pricing](PRICING.md) · [Security](SECURITY.md) ·
39
+ [Contributing](CONTRIBUTING.md) · [Changelog](CHANGELOG.md).
40
+
25
41
  ## The problem it solves
26
42
 
27
43
  Most converters flatten a financial table into something no model can use. A
@@ -42,23 +58,17 @@ Tablewise routes spreadsheets to an engine that reads the sheet as a sheet:
42
58
  - Hidden sheets kept but labelled `(hidden sheet)`.
43
59
  - A formula that was never recalculated says `(formula not calculated)` instead
44
60
  of an empty cell that a model reads as zero.
45
- - Deterministic: no AI, no network, same bytes in → same Markdown out.
61
+ - Deterministic: no AI, no network, same input and same content (the provenance date is the only variable).
46
62
 
47
63
  ## Quickstart
48
64
 
49
65
  ```bash
50
- pip install -e . # installs the `tablewise` command
66
+ pip install "tablewise[excel]" # from PyPI; add [docling] or [propio] for PDF/DOCX tables
51
67
  tablewise report.pdf # -> ~/md-repo/report.md
52
68
  tablewise budget.xlsx -o ~/md-repo -n "2026 budget"
53
69
  tablewise contract.docx --dry-run # print the engine command, do not run it
54
70
  ```
55
71
 
56
- The desktop app:
57
-
58
- ```bash
59
- python tablewise_app.py
60
- ```
61
-
62
72
  The API:
63
73
 
64
74
  ```bash
@@ -67,6 +77,14 @@ curl -X POST https://api.tablewise.dev/v1/convert \
67
77
  -F "file=@budget.xlsx"
68
78
  ```
69
79
 
80
+ ## Benchmark in one line
81
+
82
+ On the synthetic P&L of the public benchmark (three-level header, merged cells,
83
+ formulas, percentages), MarkItDown produces 11 `Unnamed` columns and 41 empty cells;
84
+ Tablewise produces none, keeps the 3 percentages and labels the 3 unevaluated
85
+ formulas. Reproduce it with `python bench/run_benchmark.py` or download the five
86
+ workbooks from https://tablewise.dev/benchmark.
87
+
70
88
  ## Why not just MarkItDown / Docling?
71
89
 
72
90
  They are good engines and Tablewise uses both. The difference is the routing,
@@ -78,7 +96,6 @@ the table fidelity and the provenance — not a new parser.
78
96
  | Formula never recalculated | empty cell | empty cell | labelled |
79
97
  | Provenance header on every file | no | no | yes |
80
98
  | Deterministic, no AI, no network | yes | yes (heavy) | yes |
81
- | Desktop GUI for non-technical users | no | no | yes |
82
99
  | Hosted API with per-month plans and batch | no | no | yes |
83
100
 
84
101
  ## Engine routing
@@ -86,14 +103,44 @@ the table fidelity and the provenance — not a new parser.
86
103
  Each extension has exactly one owner:
87
104
 
88
105
  - `.xlsx` `.xlsm` `.xltx` `.xltm` → `motor_excel.py` (openpyxl, local, no AI)
89
- - `.pdf` `.docx` `.pptx` → `motor_docling.py` (Docling, local, no AI)
106
+ - `.docx` `.pptx` → `motor_office.py` (python-docx/python-pptx, local, no AI) when the `office` extra is installed; otherwise `motor_docling.py`
107
+ - `.pdf` → `motor_propio.py` when its models are installed; otherwise `motor_docling.py` (Docling, local, no AI) — **optional** in the hosted image (`INSTALL_DOCLING=0` by default)
90
108
  - `.doc` `.rtf` `.odt` → `textutil` (macOS) → HTML → `motor_docling.py`
91
- - `.xls` `.ppt` (Office 97-2003) → LibreOffice → `.xlsx`/`.pptx` → `motor_excel.py` / `motor_docling.py`
109
+ - `.xls` (Office 97-2003) → LibreOffice → `.xlsx` → `motor_excel.py`
110
+ - `.ppt` (Office 97-2003) → LibreOffice → `.pptx` inside `motor_docling.py` (legacy path; `motor_office` does not read `.ppt`)
92
111
  - everything else (CSV, HTML, EPUB, …) → MarkItDown
93
112
 
94
- The two own engines are `motor_excel.py` and `motor_docling.py`. A format
95
- without an available engine is refused with an explicit message, never silently
96
- degraded.
113
+ The own engines are `motor_excel.py`, `motor_office.py`, `motor_docling.py` and `motor_propio.py`.
114
+ Known limits of `motor_office`: headers/footers are not extracted, text boxes
115
+ are appended at the end (visual order is not guaranteed), and a nested table is
116
+ emitted after its parent table. Legacy `.doc/.rtf/.odt/.ppt` stay on Docling.
117
+
118
+ ### PDF tables (own engine)
119
+
120
+ PDFs use the own **`motor_propio.py`** engine (Microsoft Table Transformer +
121
+ RapidOCR, ONNX, CPU) instead of Docling **only when its models are installed**:
122
+ it is faster (~0.9 s/doc digital, ~2.6 s scanned) and more accurate on tables
123
+ (0.94 exact rows on the blind control and 0.92 on the real set; v1: 0.92 / 0.90).
124
+ Full per-set table: [tablewise.dev/benchmark#pdf-engine](https://tablewise.dev/benchmark#pdf-engine).
125
+ Since `modelos-v2` (c07) it runs two detectors (`det` v1 + `det2` c06) and picks
126
+ the better box per page; the structure model is unchanged. The models are
127
+ published at
128
+ [`tablewise-modelos`](https://github.com/lfpalomaresg/tablewise-modelos/releases/tag/modelos-v2);
129
+ install them with:
130
+
131
+ ```bash
132
+ curl -fsSL -O https://github.com/lfpalomaresg/tablewise-modelos/releases/download/modelos-v2/SHA256SUMS
133
+ python3 scripts/modelos_propio.py \
134
+ --url https://github.com/lfpalomaresg/tablewise-modelos/releases/download/modelos-v2/ \
135
+ --sha256 SHA256SUMS
136
+ python3 scripts/modelos_propio.py --verificar # det2: sí
137
+ ```
138
+
139
+ They go to `~/.cache/tablewise/modelos` (or `TABLEWISE_PROPIO_MODELS`); ~347 MB,
140
+ never in the repo or the wheel. `PROPIO_DET2=0` disables the second detector.
141
+ Without the models, PDFs stay on Docling — no
142
+ change in behaviour. A format without an available engine is refused with an
143
+ explicit message, never silently degraded.
97
144
 
98
145
  ## CLI reference
99
146
 
@@ -119,9 +166,14 @@ The API is the same engine behind HTTP, for teams that cannot install anything:
119
166
  | Business | 25,000 | $149 |
120
167
 
121
168
  One unit = one PDF page, one spreadsheet sheet, one slide, or one document.
122
- Overage $0.015/unit. Annual −20%. The full contract (quota, DPA, refunds) is in
169
+ No overage billing yet (upgrade the plan). Annual billing is not available yet. The full contract (quota, DPA, refunds) is in
123
170
  [PRICING.md](PRICING.md) and on the website.
124
171
 
172
+ ## Operations
173
+
174
+ Deploying, billing, support, backups and incident playbook:
175
+ [docs/OPERATIONS.md](docs/OPERATIONS.md).
176
+
125
177
  ## Development
126
178
 
127
179
  Runner: `pytest`, using the engines virtualenv (it carries openpyxl and PyYAML):
@@ -140,6 +192,10 @@ Runner: `pytest`, using the engines virtualenv (it carries openpyxl and PyYAML):
140
192
  | `tests/test_preconversion.py` | LibreOffice (`soffice`): lookup outside PATH and `.xls` → `.xlsx` keeping merges (skipped without LibreOffice) |
141
193
  | `tests/test_preconversion_errores.py` | `preconversion.py` with simulated `subprocess.run`: timeout, tool that cannot start, exit 0 without file, error code, own profile |
142
194
  | `tests/test_motor_docling_convertir.py` | `motor_docling.convertir` with an injected fake Docling: empty output, temp cleanup, `.ppt` routing, uppercase extensions |
195
+ | `tests/test_motor_propio.py` | own PDF engine: model resolution, safe PDF→own/Docling routing, table repairs, CLI error contract |
196
+ | `tests/test_motor_propio_escaneados.py` | own PDF engine on scans: OCR without line-orientation classifier, lone dash in empty value cells |
197
+ | `tests/test_quality.py` | synthetic quality KPI: table/row metric and table-text filtering without loading the ONNX models |
198
+ | `tests/test_motor_office.py` | light Office engine: `.docx`/`.pptx` paragraphs and tables without Docling, CLI contract |
143
199
  | `tests/test_entradas_externas.py` | `.xls` path, malformed array formulas, backslashes in the YAML header, non-UTF-8 output, BOM and CRLF |
144
200
  | `tests/test_preconversion_ejecutables_falsos.py` | textutil/LibreOffice boundary with fake executables |
145
201
  | `tests/test_app_convertir.py` | `App.convert()` with fake tkinter and engine: existing `.md` (cancel / numbered copy / overwrite), failing engine, non-UTF-8 output, partial writes |
@@ -147,6 +203,8 @@ Runner: `pytest`, using the engines virtualenv (it carries openpyxl and PyYAML):
147
203
  | `tests/test_red_sintaxis.py` | regression net: every application file is valid Python and the app imports without opening a window |
148
204
  | `tests/test_cli.py` | `tablewise` CLI: same decisions as the app, `--dry-run`, no overwrite without `--force`, exit codes |
149
205
  | `tests/test_docs.py` | drift net: this README lists every test file and every routed extension |
206
+ | `tests/test_sprint_deliver.py` | `scripts/sprint_deliver.py`: delivery package, manifest hashes, failed files listed, determinism check |
207
+ | `tests/test_weekly_summary.py` | `scripts/weekly_summary.py`: the 5-number weekly funnel summary |
150
208
 
151
209
  No test uses real documents.
152
210
 
@@ -0,0 +1,172 @@
1
+ """Conversor ligero de Word (.docx) y PowerPoint (.pptx) a Markdown.
2
+
3
+ Lee la estructura nativa del OOXML con `python-docx` / `python-pptx` (MIT), en
4
+ vez de cargar Docling + torch (~3 GB) solo para reconstruir tablas que ya están
5
+ etiquetadas como tablas en el documento. No usa IA ni modelos: es determinista,
6
+ rápido y conserva el orden párrafo/tabla del cuerpo.
7
+
8
+ Los formatos legacy (.doc/.rtf/.odt/.ppt) siguen en Docling (o en LibreOffice
9
+ antes de Docling): aquí no se adivina un binario de 1997.
10
+
11
+ CLI: python motor_office.py <origen.docx|.pptx> <destino.md>
12
+ Librería: from motor_office import convertir
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ from postpro import _render
20
+ from tablewise_core import escribir_atomico, misma_ruta
21
+
22
+
23
+ class ConversionVacia(Exception):
24
+ """El documento no produjo contenido."""
25
+
26
+
27
+ def _heading(paragraph) -> str:
28
+ estilo = (getattr(paragraph.style, "name", "") or "").lower()
29
+ texto = paragraph.text.strip()
30
+ if not texto:
31
+ return ""
32
+ if estilo == "title":
33
+ return f"# {texto}"
34
+ if estilo.startswith("heading"):
35
+ digits = "".join(ch for ch in estilo if ch.isdigit())
36
+ nivel = max(1, min(6, int(digits) if digits else 1))
37
+ return f"{'#' * nivel} {texto}"
38
+ return texto
39
+
40
+
41
+ def _docx(origen: Path) -> str:
42
+ from docx import Document
43
+ from docx.oxml.ns import qn
44
+ from docx.table import Table
45
+ from docx.text.paragraph import Paragraph
46
+
47
+ doc = Document(str(origen))
48
+ partes: list[str] = []
49
+
50
+ vistos_tablas: set = set()
51
+
52
+ def add_tabla(tabla) -> None:
53
+ # `walk` y `celda.tables` pueden llegar a la misma tabla por caminos
54
+ # distintos (sdt, tabla anidada): se emite una sola vez.
55
+ if tabla._tbl in vistos_tablas:
56
+ return
57
+ vistos_tablas.add(tabla._tbl)
58
+ filas = [[celda.text for celda in fila.cells] for fila in tabla.rows]
59
+ if filas and any(any(c.strip() for c in fila) for fila in filas):
60
+ partes.append(_render(filas))
61
+ # Una tabla anidada en una celda no aparece en cell.text: se emite
62
+ # después de la exterior, en el mismo bloque, para no perderla.
63
+ # `row.cells` repite la celda combinada una vez por columna del grid:
64
+ # sin deduplicar por el XML subyacente, su tabla anidada saldría dos veces.
65
+ vistos_celdas: set = set()
66
+ for fila in tabla.rows:
67
+ for celda in fila.cells:
68
+ tc = celda._tc
69
+ if tc in vistos_celdas:
70
+ continue
71
+ vistos_celdas.add(tc)
72
+ for anidada in celda.tables:
73
+ add_tabla(anidada)
74
+ # Un content control dentro de una celda esconde su tabla y su
75
+ # texto de cell.text/tables. Se recorre por hijos (no con
76
+ # tc.iter, que bajaría dos veces por sdts anidados) y solo
77
+ # desde el sdt: el texto directo de la celda ya está en la tabla.
78
+ walk(tc, solo_sdt=True)
79
+
80
+ def add_parrafo(parrafo) -> None:
81
+ texto = _heading(parrafo)
82
+ if texto:
83
+ partes.append(texto)
84
+
85
+ def walk(parent, *, solo_sdt: bool = False) -> None:
86
+ for child in parent.iterchildren():
87
+ if child.tag == qn("w:sdt"):
88
+ contenido = child.find(qn("w:sdtContent"))
89
+ if contenido is not None:
90
+ walk(contenido)
91
+ elif not solo_sdt and child.tag == qn("w:p"):
92
+ add_parrafo(Paragraph(child, doc))
93
+ elif not solo_sdt and child.tag == qn("w:tbl"):
94
+ add_tabla(Table(child, doc))
95
+
96
+ walk(doc.element.body)
97
+ # Contenido de cuadros de texto: cuelga de un run, no del body, así que
98
+ # p.text no lo ve. Se añade al final para no perderlo (orden no garantizado).
99
+ for caja in doc.element.body.iter(qn("w:txbxContent")):
100
+ for parrafo in caja.iter(qn("w:p")):
101
+ add_parrafo(Paragraph(parrafo, doc))
102
+ return "\n\n".join(partes)
103
+
104
+
105
+ def _pptx(origen: Path) -> str:
106
+ from pptx import Presentation
107
+ from pptx.enum.shapes import MSO_SHAPE_TYPE
108
+
109
+ prs = Presentation(str(origen))
110
+ partes: list[str] = []
111
+
112
+ def render_shape(shape):
113
+ if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
114
+ for hijo in shape.shapes:
115
+ render_shape(hijo)
116
+ return
117
+ if getattr(shape, "has_table", False):
118
+ filas = [[celda.text for celda in fila.cells] for fila in shape.table.rows]
119
+ if filas and any(any(c.strip() for c in fila) for fila in filas):
120
+ partes.append(_render(filas))
121
+ elif getattr(shape, "has_text_frame", False):
122
+ texto = shape.text_frame.text.strip()
123
+ if texto:
124
+ partes.append(texto)
125
+
126
+ for slide in prs.slides:
127
+ titulo = slide.shapes.title
128
+ if titulo is not None and getattr(titulo, "has_text_frame", False):
129
+ texto = titulo.text_frame.text.strip()
130
+ if texto:
131
+ partes.append(f"# {texto}")
132
+ for shape in slide.shapes:
133
+ if titulo is not None and shape.shape_id == titulo.shape_id:
134
+ continue
135
+ render_shape(shape)
136
+ return "\n\n".join(partes)
137
+
138
+
139
+ def convertir(origen: Path) -> str:
140
+ origen = Path(origen)
141
+ ext = origen.suffix.lower()
142
+ if ext == ".docx":
143
+ md = _docx(origen)
144
+ elif ext == ".pptx":
145
+ md = _pptx(origen)
146
+ else:
147
+ raise ValueError(f"motor_office solo convierte .docx/.pptx, no {ext or '(sin extensión)'}")
148
+ if not md.strip():
149
+ raise ConversionVacia(f"motor_office no produjo contenido para {origen.name}.")
150
+ return md
151
+
152
+
153
+ if __name__ == "__main__":
154
+ if len(sys.argv) < 3:
155
+ sys.exit("Uso: python motor_office.py <origen.docx|.pptx> <destino.md>")
156
+ origen = Path(sys.argv[1]).expanduser()
157
+ destino = Path(sys.argv[2]).expanduser()
158
+ if not origen.exists():
159
+ sys.exit(f"No existe: {origen}")
160
+ if misma_ruta(origen, destino):
161
+ sys.exit(f"El destino es el propio documento de origen ({origen.name}): no se sobrescribe.")
162
+ try:
163
+ md = convertir(origen)
164
+ except ValueError as e:
165
+ sys.exit(f"motor_office: {e}")
166
+ except Exception as e:
167
+ sys.exit(f"motor_office falló con {origen.name}: {e}")
168
+ try:
169
+ escribir_atomico(destino, md)
170
+ except (OSError, UnicodeError) as e:
171
+ sys.exit(f"No se pudo escribir {destino}: {e}")
172
+ print(f"Guardado en {destino} ({len(md)} caracteres)")