Skip to content

Commit 3829e5c

Browse files
committed
refined processing speed for rust native , tested against actual files , revised compilation of python engine and support for desktop
1 parent eaeaa8b commit 3829e5c

51 files changed

Lines changed: 3572 additions & 163 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/app-release.yml

Lines changed: 87 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -73,16 +73,19 @@ jobs:
7373
target: x86_64-unknown-linux-gnu
7474
bundles: deb,appimage
7575
compress: true
76+
pdfium_asset: pdfium-linux-x64
7677
- os: ubuntu-24.04-arm
7778
label: linux-aarch64
7879
target: aarch64-unknown-linux-gnu
7980
bundles: deb
8081
compress: true
82+
pdfium_asset: pdfium-linux-arm64
8183
- os: windows-latest
8284
label: windows-x86_64
8385
target: x86_64-pc-windows-msvc
8486
bundles: msi,nsis
8587
compress: true
88+
pdfium_asset: pdfium-win-x64
8689
# macOS x86_64 (Intel artifacts): CROSS-COMPILED on the Apple
8790
# Silicon runner instead of the Intel `macos-13` runner. Intel
8891
# macOS hosted runners are scarce / being phased down and jobs can
@@ -95,14 +98,40 @@ jobs:
9598
bundles: app,dmg
9699
compress: false
97100
rosetta: true
101+
pdfium_asset: pdfium-mac-x64
98102
- os: macos-14
99103
label: macos-aarch64
100104
target: aarch64-apple-darwin
101105
bundles: app,dmg
102106
compress: false
107+
pdfium_asset: pdfium-mac-arm64
103108
steps:
104109
- uses: actions/checkout@v4
105110

111+
# Download the PDFium shared library matching the TARGET arch (not the
112+
# runner — macOS x86_64 is cross-built on the arm64 runner). Staged into
113+
# the desktop's Tauri resources and exported for the CLI/MCP build + smoke.
114+
- name: Download + stage PDFium (fast PDF backend)
115+
shell: bash
116+
run: |
117+
set -euo pipefail
118+
url="https://github.com/bblanchon/pdfium-binaries/releases/latest/download/${{ matrix.pdfium_asset }}.tgz"
119+
curl -fsSL -o pdfium.tgz "$url"
120+
mkdir -p pdfium_extract && tar xzf pdfium.tgz -C pdfium_extract
121+
case "$RUNNER_OS" in
122+
Windows) src="pdfium_extract/bin/pdfium.dll"; libname="pdfium.dll" ;;
123+
macOS) src="pdfium_extract/lib/libpdfium.dylib"; libname="libpdfium.dylib" ;;
124+
*) src="pdfium_extract/lib/libpdfium.so"; libname="libpdfium.so" ;;
125+
esac
126+
test -f "$src" || { echo "::error::PDFium lib not found at $src"; exit 1; }
127+
# Bundle into the desktop app (Tauri resource glob pdfium/*).
128+
mkdir -p app/desktop/src-tauri/pdfium
129+
cp "$src" "app/desktop/src-tauri/pdfium/$libname"
130+
# Export for later steps (CLI/MCP build, smoke, packaging).
131+
echo "PDFIUM_LIB=$PWD/$src" >> "$GITHUB_ENV"
132+
echo "PDFIUM_LIBNAME=$libname" >> "$GITHUB_ENV"
133+
echo "staged PDFium ($libname) for ${{ matrix.label }}"
134+
106135
- name: Install Tauri system dependencies (Linux only)
107136
if: runner.os == 'Linux'
108137
run: |
@@ -140,9 +169,9 @@ jobs:
140169
run: cargo test --locked
141170

142171
# ---- build standalone CLI + MCP (release, native target) ------------
143-
- name: Build CLI + MCP (release)
172+
- name: Build CLI + MCP (release, with PDFium)
144173
working-directory: app
145-
run: cargo build --release --locked --target ${{ matrix.target }} -p markitdown-cli -p markitdown-mcp
174+
run: cargo build --release --locked --features pdfium --target ${{ matrix.target }} -p markitdown-cli -p markitdown-mcp
146175

147176
# ---- compress the standalone binaries (UPX) -------------------------
148177
# The release profile already does LTO + strip + opt-level="s". UPX
@@ -182,7 +211,9 @@ jobs:
182211
# empty or errors, and asserts a non-format binary is still rejected.
183212
- name: Smoke + format-regression test on the released binary
184213
shell: bash
185-
run: BIN="app/target/${{ matrix.target }}/release" bash .github/scripts/smoke.sh "${{ matrix.label }}"
214+
# MARKITDOWN_PDFIUM_LIB makes the smoke exercise the bundled PDFium
215+
# backend (and proves it loads on this OS/arch).
216+
run: MARKITDOWN_PDFIUM_LIB="$PDFIUM_LIB" BIN="app/target/${{ matrix.target }}/release" bash .github/scripts/smoke.sh "${{ matrix.label }}"
186217

187218
# ---- install frontend deps + build the Tauri desktop installers -----
188219
- name: Install frontend dependencies
@@ -205,12 +236,15 @@ jobs:
205236
mkdir -p out staging
206237
cp "app/target/$TRIPLE/release/markitdown$EXE" staging/
207238
cp "app/target/$TRIPLE/release/markitdown-mcp$EXE" staging/
239+
# Bundle the PDFium library next to the binaries so the fast PDF
240+
# backend is auto-discovered (next-to-exe). Keep them together.
241+
cp "$PDFIUM_LIB" "staging/$PDFIUM_LIBNAME"
208242
(
209243
cd staging
210244
if [[ "$RUNNER_OS" == "Windows" ]]; then
211-
7z a "../out/markitdown-cli-mcp-$LABEL.zip" "markitdown$EXE" "markitdown-mcp$EXE"
245+
7z a "../out/markitdown-cli-mcp-$LABEL.zip" "markitdown$EXE" "markitdown-mcp$EXE" "$PDFIUM_LIBNAME"
212246
else
213-
tar czf "../out/markitdown-cli-mcp-$LABEL.tar.gz" markitdown markitdown-mcp
247+
tar czf "../out/markitdown-cli-mcp-$LABEL.tar.gz" markitdown markitdown-mcp "$PDFIUM_LIBNAME"
214248
fi
215249
)
216250
# Desktop installers (only the types that exist for this platform).
@@ -254,14 +288,19 @@ jobs:
254288
if-no-files-found: error
255289

256290
# OPTIONAL Python fallback engine (OCR, transcription, Azure, plugins),
257-
# packaged so users can enable `--engine auto` fallback without building it
258-
# themselves. Best-effort: the whole job is `continue-on-error` so a flaky
259-
# PyInstaller build NEVER blocks the lightweight Rust release. PyInstaller
260-
# cannot cross-compile, so this covers the 4 natively-buildable targets;
261-
# Intel macOS users build locally with python-engine/build_binary.sh (an
262-
# Apple-Silicon Python binary can't run on Intel).
291+
# packaged for EVERY OS/arch so `--engine auto`/`python` works without users
292+
# building it. Covers Linux x86_64/aarch64, Windows x86_64, and macOS Apple
293+
# Silicon + Intel. PyInstaller cannot cross-compile, so each arch builds on
294+
# its own native runner (Intel macOS uses the scarce `macos-13` image).
295+
#
296+
# Robustness: this job `needs: release` and runs AFTER the main release is
297+
# already published, then APPENDS each binary to that release. Combined with
298+
# `continue-on-error`, a flaky or slow runner (e.g. a queued macos-13) can
299+
# NEVER block or delay the Rust/desktop release — the Python asset simply
300+
# shows up late, or not at all for that one arch.
263301
python-engine:
264302
name: python fallback (${{ matrix.label }})
303+
needs: release
265304
continue-on-error: true
266305
strategy:
267306
fail-fast: false
@@ -271,11 +310,16 @@ jobs:
271310
- { os: ubuntu-24.04-arm, label: linux-aarch64 }
272311
- { os: windows-latest, label: windows-x86_64 }
273312
- { os: macos-14, label: macos-aarch64 }
313+
- { os: macos-13, label: macos-x86_64 } # Intel; runner may be scarce
274314
runs-on: ${{ matrix.os }}
275315
steps:
276316
- uses: actions/checkout@v4
277317
- uses: actions/setup-python@v5
278318
with:
319+
# 3.12: a supported version with prebuilt wheels for markitdown[all]'s
320+
# native deps (magika/onnxruntime, lxml, numpy). MUST stay within the
321+
# 3.10–3.13 range enforced by app/python-engine/build_binary.sh, and
322+
# matches that script's preferred default so local and CI builds agree.
279323
python-version: "3.12"
280324

281325
- name: Build the PyInstaller fallback binary
@@ -288,36 +332,44 @@ jobs:
288332
bash app/python-engine/build_binary.sh
289333
fi
290334
291-
- name: Smoke test + stage the fallback binary
335+
- name: Smoke test + archive the fallback engine (onedir)
292336
shell: bash
293337
run: |
294338
set -euo pipefail
295339
EXE=""; [[ "$RUNNER_OS" == "Windows" ]] && EXE=".exe"
296-
PY="app/python-engine/dist/markitdown-py$EXE"
297-
test -f "$PY" || { echo "::error::fallback binary not produced"; exit 1; }
340+
# build_binary defaults to onedir: a folder at dist/markitdown-py/
341+
# whose launcher is dist/markitdown-py/markitdown-py[.exe].
342+
DIR="app/python-engine/dist/markitdown-py"
343+
PY="$DIR/markitdown-py$EXE"
344+
test -x "$PY" || { echo "::error::fallback engine not produced at $PY"; exit 1; }
298345
# It must actually convert a real document.
299346
"$PY" packages/markitdown/tests/test_files/test.docx > py_out.md
300347
grep -q AutoGen py_out.md
301348
mkdir -p out
302-
cp "$PY" "out/markitdown-py-${{ matrix.label }}$EXE"
303-
echo "fallback engine OK on ${{ matrix.label }} ($(du -h "$PY" | cut -f1))"
349+
# Archive the whole folder (onedir must travel together). Users extract
350+
# and point at the inner markitdown-py[.exe].
351+
if [[ "$RUNNER_OS" == "Windows" ]]; then
352+
(cd app/python-engine/dist && 7z a "$GITHUB_WORKSPACE/out/markitdown-py-${{ matrix.label }}.zip" markitdown-py >/dev/null)
353+
else
354+
tar czf "out/markitdown-py-${{ matrix.label }}.tar.gz" -C app/python-engine/dist markitdown-py
355+
fi
356+
echo "fallback engine OK on ${{ matrix.label }} ($(du -sh "$DIR" | cut -f1))"
304357
305-
- name: Upload fallback binary
306-
uses: actions/upload-artifact@v4
358+
- name: Append the fallback engine to the release
359+
uses: softprops/action-gh-release@v2
307360
with:
308-
name: markitdown-py-${{ matrix.label }}
309-
path: out/*
310-
if-no-files-found: error
361+
tag_name: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
362+
files: out/markitdown-py-${{ matrix.label }}.*
363+
fail_on_unmatched_files: true
311364

312-
# Gather every platform's artifacts and publish them as ONE GitHub Release.
313-
# Runs both for `app-v*` tag pushes (normal release) AND for manual
314-
# workflow_dispatch runs (publishes a prerelease named by the `tag` input),
315-
# so the .dmg / installers / binaries are always visible on the Releases
316-
# page — not buried under the Actions run's Artifacts. Includes the Python
317-
# fallback binaries when their (best-effort) job produced any.
365+
# Gather the Rust + desktop artifacts and publish them as ONE GitHub Release.
366+
# Runs for `app-v*` tag pushes AND manual workflow_dispatch (prerelease named
367+
# by the `tag` input). Depends ONLY on `build` so it publishes promptly; the
368+
# Python fallback binaries are appended afterward by the `python-engine` job
369+
# (which `needs: release`), so a slow Python runner never delays the release.
318370
release:
319371
name: publish release
320-
needs: [build, python-engine]
372+
needs: build
321373
if: >-
322374
always() && needs.build.result == 'success' &&
323375
(startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch')
@@ -377,10 +429,13 @@ jobs:
377429
- Windows: `*.msi` + `*-setup.exe`
378430
379431
**Optional Python fallback engine** (enables OCR / transcription /
380-
Azure via `--engine auto` + `MARKITDOWN_PY_BIN`) — large, only needed
381-
for those extras:
382-
- `markitdown-py-{linux-x86_64,linux-aarch64,windows-x86_64,macos-aarch64}`
383-
- Intel macOS: build locally with `app/python-engine/build_binary.sh`.
432+
Azure, and is much faster on very large PDFs) — large, only needed
433+
for those extras; appended shortly after the release is published:
434+
- `markitdown-py-<platform>.{tar.gz,zip}` for linux-x86_64,
435+
linux-aarch64, windows-x86_64, macos-aarch64, macos-x86_64.
436+
- It's a *folder* build (fast startup). Extract it, keep the folder
437+
intact, and point the desktop **Settings → Python engine** field
438+
(or `MARKITDOWN_PY_BIN`) at the inner `markitdown-py` executable.
384439
385440
- `SHA256SUMS.txt` — checksums for every asset.
386441

app/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,8 @@ formats.txt
3939
conv_out.md
4040
mcp_req.txt
4141
mcp_resp.txt
42+
test_files/
43+
44+
# Bundled PDFium libs (downloaded at release time); keep only the README.
45+
desktop/src-tauri/pdfium/*
46+
!desktop/src-tauri/pdfium/README.md

app/CHANGELOG.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,108 @@ app, and the optional Python fallback). Format loosely follows
77
## [Unreleased]
88

99
### Added
10+
- **Native YouTube transcripts (no Python needed).** The YouTube converter now
11+
fetches the video transcript itself, the same way
12+
[`youtube-transcript-api`](https://github.com/jdepoix/youtube-transcript-api)
13+
does: it reads `captionTracks[]` out of the embedded `ytInitialPlayerResponse`,
14+
picks a track (preferring an English, manually-created one over auto-generated),
15+
GETs that track's timedtext endpoint, and parses the `<text>` lines into a
16+
`### Transcript` section (double-encoded entities decoded). Requires the `net`
17+
feature (on by default); when captions are absent or the build is offline the
18+
result is flagged degraded so the Python engine can try. The Python fallback
19+
binary now installs `youtube-transcript-api` explicitly (on top of
20+
`markitdown[all]`) so the transcript path can't silently disappear.
21+
- **MHTML (saved-webpage, `.mhtml`/`.mht`) support.** Instead of dumping the raw
22+
MIME container (quoted-printable/base64 and all) as plain text, the converter
23+
parses the multipart message, decodes the root `text/html` part (QP/base64 +
24+
charset), and runs it through the shared HTML→Markdown pipeline — embedded
25+
image/CSS parts are dropped. Verified on a real saved Wikipedia article.
26+
- **Generic structured-XML (`.xml`) support.** Non-feed XML (RSS/Atom is still
27+
handled first by the feed converter) now renders meaningfully: a parent with
28+
≥2 repeating same-tag records → a GFM table (columns = union of field tags +
29+
`@attributes`); anything else → a readable nested outline (`- **tag**
30+
(attrs): text`). Verified on a record-style catalog (→ table) and a nested
31+
config (→ outline).
32+
- **ODS (OpenDocument spreadsheet) support** via `calamine` — each sheet → GFM
33+
table. Notably the upstream Python markitdown can't read ODS, so the Rust
34+
engine is the only one that handles it.
35+
- **MOBI / Kindle (.mobi/.azw/.azw3) support** via the `mobi` crate → HTML →
36+
Markdown (lossy decode for legacy cp1252 books). Also unsupported upstream.
37+
- **Richer desktop Markdown preview.** The Rendered view's dependency-free
38+
renderer now handles `<!-- Slide N -->` markers (→ dividers), advisory notes
39+
(→ asides), in-cell `<br>`, and `~~strikethrough~~`, on top of headings,
40+
bold/italic, code, links, images, lists, blockquotes and GFM tables — all
41+
HTML-escaped for safety. Covered by `src/markdown.test.ts` (Node's built-in
42+
test runner via type-stripping; `npm test`, no new dependencies). Still no
43+
heavy Markdown library — kept ~5 KB for fast rendering.
44+
- **Progress across all formats** (not just PDF): per-sheet (XLSX), per-slide
45+
(PPTX), per-file (ZIP), per-page (PDF), plus coarse `detect → convert → done`
46+
for everything. Still opt-in (a sink must be attached) so default conversions
47+
have zero overhead.
48+
- **Formatting-fidelity regression tests** (`tests/formatting.rs`): a synthetic
49+
DOCX verifies `Heading→#`, `w:b→**bold**`, `w:i→*italic*`, and `w:tbl→GFM
50+
table`; XLSX/PPTX assert sheet/slide tables + headings on real fixtures.
51+
- **Progress reporting for heavy conversions.** A 350 MB PDF no longer looks
52+
like a hung process:
53+
- Core: optional `ConvertOptions.progress` sink (`ProgressCallback`) emitting
54+
phase + percentage; the PDF converter extracts **page-by-page with real
55+
`page X/N (Y%)`** when a sink is set (and survives a corrupt page). Zero
56+
overhead and unchanged fast path when no sink is installed.
57+
- CLI: `-V` / `--verbose` streams phase + per-page % + timing to **stderr**
58+
(stdout stays clean Markdown); percent lines throttled to whole-percent
59+
changes. Works for the rust, python, and auto engines (shows fallback
60+
delegation).
61+
- MCP: conversion progress logged to the server's stderr via `tracing`.
62+
- Desktop: live `job:progress` events drive a per-job determinate progress
63+
bar + percentage label and Logs-panel lines, so heavy files visibly move.
64+
- **Dev-build speed fix.** `[profile.dev.package."*"] opt-level = 3` (workspace
65+
+ desktop) so heavy deps like `pdf-extract` run optimized under `tauri dev` /
66+
debug `cargo` — a large PDF that took 10+ min unoptimized now runs in ~release
67+
time. (The shipped release app was always optimized.)
68+
- **PDFium PDF backend, bundled in releases.** Uses Google's PDFium for PDF text
69+
extraction — **~1.3s** on a 342 MB PDF vs ~21s default Rust / ~6s Python
70+
(≈16× / ≈5×). The release workflow downloads the matching PDFium per OS/arch
71+
(Linux x64/arm64, Windows x64, macOS Intel/Apple-Silicon) and ships it next to
72+
the CLI/MCP binaries and as a desktop Tauri resource (wired up at startup), so
73+
installed apps are fast with **zero setup**. Building from source is opt-in
74+
(`--features pdfium`); a plain build stays pure-Rust/static. The library is
75+
discovered via `MARKITDOWN_PDFIUM_LIB` → next to the binary → system, and
76+
conversion **falls back to pure-Rust** if it's missing — never a hard failure.
77+
- **~3× faster large-PDF loading.** Re-enabled lopdf's `rayon` feature (which
78+
`pdf-extract` disables via `default-features=false`) through Cargo feature
79+
unification — PDF object parsing now runs across all cores. A 342 MB PDF went
80+
**57s → ~21s** end-to-end with byte-identical output and no code change.
81+
- **Parallel PDF extraction** (rayon) on the progress path: pages render across
82+
all cores and a corrupt page is skipped rather than failing the document.
83+
Note: for image-heavy PDFs the time is dominated by the serial document
84+
*load* in the PDF library, so this mainly helps text-heavy, many-page files.
85+
- **Python engine builds as onedir (fast startup) for all OSes.** A PyInstaller
86+
one-file binary re-extracts ~100 MB per launch (≈45s/run on macOS via
87+
Gatekeeper); `build_binary.{sh,ps1}` now default to **onedir** (~0.5s startup
88+
after first run). The release workflow builds it for Linux x86_64/aarch64,
89+
Windows x86_64, and macOS Apple Silicon **and Intel**, archives each onedir
90+
folder, and appends it to the release without blocking the main release.
91+
- **Python build version guard.** `build_binary.{sh,ps1}` require Python
92+
3.10–3.13 (prefer 3.12, matching CI's `setup-python`) and stop with clear
93+
guidance instead of hanging on a too-new interpreter (e.g. 3.14, whose native
94+
deps lack wheels).
95+
- **Python engine works in installed apps (no env needed).** `resolve_python_bin`
96+
now auto-discovers a `markitdown-py` binary next to the running executable
97+
(e.g. a bundled Tauri sidecar) or on `PATH`, in addition to `--python-bin` /
98+
`MARKITDOWN_PY_BIN`. Only the exact `markitdown-py` name is searched (never the
99+
bare `markitdown` Rust CLI). Fixes GUI "missing dependency" failures, since
100+
Finder/dock-launched apps don't inherit the shell environment.
101+
- **Desktop: Settings → Python engine** path field (+ Browse + capability pill),
102+
passed into conversions, so users can point the GUI at the binary directly.
103+
- **Python-engine heartbeat logging.** The Python subprocess is opaque (no
104+
per-page signal), so while it runs the engine now emits an elapsed-time
105+
heartbeat (`Python engine running… Ns elapsed`) — the path shows liveness in
106+
the CLI (`-V`), desktop logs, and MCP logs, like the Rust path.
107+
- **Profiled the engines on a 342 MB / 69-page PDF** (documented in the README):
108+
Rust pdf-extract ≈ 57s (55.7s of it is the serial `lopdf` load); the **Python
109+
engine (pdfminer) ≈ 6s** and yields richer table output. For very large PDFs,
110+
`--engine python` (or `auto` with `MARKITDOWN_PY_BIN` set, which falls back
111+
for scanned PDFs) is dramatically faster.
10112
- **LLM provider registry** (`crates/markitdown-core/src/llm_providers.rs`) —
11113
one customizable list of OpenAI-compatible **vision** providers: OpenAI,
12114
**Anthropic/Claude** (OpenAI-compatible endpoint), Ollama, LM Studio,

0 commit comments

Comments
 (0)