<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: DeepLab</title>
    <description>The latest articles on DEV Community by DeepLab (deeplab).</description>
    <link>https://dev.to/deeplab</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F13595%2Fc667ee8d-bcd6-42f8-8753-66255c1290f1.png</url>
      <title>DEV Community: DeepLab</title>
      <link>https://dev.to/deeplab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/deeplab"/>
    <language>en</language>
    <item>
      <title>Make any scan searchable - locally, no Python, no Ghostscript, no upload</title>
      <dc:creator>Alexander Pervushen</dc:creator>
      <pubDate>Mon, 27 Jul 2026 14:24:58 +0000</pubDate>
      <link>https://dev.to/deeplab/make-any-scan-searchable-locally-no-python-no-ghostscript-no-upload-2g87</link>
      <guid>https://dev.to/deeplab/make-any-scan-searchable-locally-no-python-no-ghostscript-no-upload-2g87</guid>
      <description>&lt;p&gt;I had a folder full of scanned PDFs - old contracts, receipts, a couple of books - and none of them were searchable. No Ctrl-F, no copy-paste, just pictures of text. I wanted to fix that. I did not want to upload any of it to a website, and I &lt;em&gt;really&lt;/em&gt; did not want to spend an evening installing a toolchain to do it.&lt;/p&gt;

&lt;p&gt;That turns out to be a surprisingly awkward corner.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two usual answers, and what each one costs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;OCRmyPDF&lt;/strong&gt; is the tool everyone points you to, and deservedly - it's excellent, mature, does PDF/A, handles dozens of languages. But "just install it" means Python &lt;strong&gt;plus&lt;/strong&gt; Tesseract &lt;strong&gt;plus&lt;/strong&gt; Ghostscript &lt;strong&gt;plus&lt;/strong&gt; unpaper &lt;strong&gt;plus&lt;/strong&gt; pikepdf. On Windows especially that's a bad afternoon. And Ghostscript is AGPL - a copyleft dependency I'd rather not build a product on top of. Great tool; the install is the tax.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Online OCR sites&lt;/strong&gt; are the easy path - until you look at what you're uploading. A passport. A signed contract. A medical scan. A tax form. "Drag your sensitive document onto our server" is exactly the thing you shouldn't do with the documents that most need OCR. Ads, limits, watermarks are the least of it.&lt;/p&gt;

&lt;p&gt;The third option most people pick is &lt;em&gt;nothing&lt;/em&gt; - they just live with PDFs they can't search.&lt;/p&gt;

&lt;p&gt;None of these answered the thing I actually wanted: &lt;em&gt;make this scan searchable, on my machine, with one command.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So I built it. It's called &lt;strong&gt;DeepOCR&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;deepocr scan.pdf &lt;span class="nt"&gt;-o&lt;/span&gt; scan.ocr.pdf
scan.pdf   1 of 1 page&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;   9 words   → scan.ocr.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It takes a scanned PDF (or an image), recognizes the text, and writes a &lt;strong&gt;new&lt;/strong&gt; PDF where the original page image is untouched and an &lt;strong&gt;invisible, selectable text layer&lt;/strong&gt; sits on top, positioned word by word. Open it in any viewer and Ctrl-F just works. The original file is never modified - the result is a separate &lt;code&gt;.ocr.pdf&lt;/code&gt;, the same way a good tool should never overwrite your input.&lt;/p&gt;

&lt;p&gt;It's a single static Rust binary. No Python, no Tesseract, &lt;strong&gt;no Ghostscript&lt;/strong&gt;. The OCR engine is &lt;a href="https://github.com/robertknight/ocrs" rel="noopener noreferrer"&gt;&lt;code&gt;ocrs&lt;/code&gt;&lt;/a&gt; - pure Rust - and the recognition model ships &lt;strong&gt;inside&lt;/strong&gt; the release binary, so it runs completely offline. Nothing touches the network, nothing leaves the machine.&lt;/p&gt;

&lt;p&gt;A few flags cover the real cases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deepocr scan.pdf &lt;span class="nt"&gt;--sidecar&lt;/span&gt;           &lt;span class="c"&gt;# also drop the recognized text as a .txt&lt;/span&gt;
deepocr book.pdf &lt;span class="nt"&gt;--deskew&lt;/span&gt; &lt;span class="nt"&gt;--rotate&lt;/span&gt; auto   &lt;span class="c"&gt;# straighten and orient before OCR&lt;/span&gt;
deepocr ./scans &lt;span class="nt"&gt;--recursive&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; out/  &lt;span class="c"&gt;# a whole folder, tree mirrored&lt;/span&gt;
deepocr contract.pdf &lt;span class="nt"&gt;--optimize&lt;/span&gt;      &lt;span class="c"&gt;# recompress the page images in the output&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Batch skips pages that already have a text layer instead of double-OCR'ing them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;scans/a.pdf  → out/a.ocr.pdf   (12 pages)
&lt;/span&gt;&lt;span class="gp"&gt;scans/c.pdf  ↷ already has text (skipped;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;use &lt;span class="nt"&gt;--force&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;✓ 2 processed, 1 skipped
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The privacy part, said plainly
&lt;/h2&gt;

&lt;p&gt;This is the reason I care about it being local. The documents that most need OCR are the ones you least want to upload: IDs, contracts, financial records, anything medical. DeepOCR never sends them anywhere - the model is on your disk, the work happens in-process, and there's no account, no queue, no "we don't store your files, promise." Because the model is embedded, it works air-gapped, on a machine with the network cable pulled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest about the edges
&lt;/h2&gt;

&lt;p&gt;I'd rather undersell than oversell, so: v0.1 is &lt;strong&gt;Latin script only&lt;/strong&gt; (that's the current limit of the &lt;code&gt;ocrs&lt;/code&gt; model), and it produces a normal searchable PDF, &lt;strong&gt;not&lt;/strong&gt; strict archival PDF/A. Other scripts (CJK, more languages via other backends) and PDF/A are on the roadmap behind a backend abstraction that's already in place - but they are not in v0.1, and I'd rather tell you that up front than have you find out on a Cyrillic scan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it fits: the DeepLab loop
&lt;/h2&gt;

&lt;p&gt;DeepOCR is part of a small set of local-first, dependency-light tools I'm building under &lt;strong&gt;DeepLab&lt;/strong&gt;, and it closes a nice loop with its sibling &lt;a href="https://github.com/deeplabua/deepdoc" rel="noopener noreferrer"&gt;DeepDoc&lt;/a&gt; (any document → clean Markdown for RAG). DeepDoc handles &lt;em&gt;born-digital&lt;/em&gt; documents - real embedded text - and honestly refuses a scan instead of inventing content. DeepOCR is what turns that scan into something born-digital:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deepocr scan.pdf &lt;span class="nt"&gt;-o&lt;/span&gt; scan.ocr.pdf   &lt;span class="c"&gt;# add the text layer&lt;/span&gt;
deepdoc scan.ocr.pdf               &lt;span class="c"&gt;# now it extracts like any other document&lt;/span&gt;
&lt;span class="c"&gt;# ### HELLO WORLD&lt;/span&gt;
&lt;span class="c"&gt;# make scans searchable&lt;/span&gt;
&lt;span class="c"&gt;# DeepOCR reads Latin text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same OCR core under both, so an ingestion pipeline can route a text-layerless scan through DeepOCR and back into DeepDoc without a cloud step anywhere in the chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;deeplabua/tap/deepocr
&lt;span class="c"&gt;# or&lt;/span&gt;
cargo &lt;span class="nb"&gt;install &lt;/span&gt;deepocr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's open source (MIT / Apache), pure Rust, and the whole dependency graph is permissive - no AGPL, no C &lt;code&gt;-sys&lt;/code&gt; crates. Repo: &lt;strong&gt;&lt;a href="https://github.com/deeplabua/deepocr" rel="noopener noreferrer"&gt;https://github.com/deeplabua/deepocr&lt;/a&gt;&lt;/strong&gt; · landing page with all the install options: &lt;strong&gt;&lt;a href="https://deeplab.tools/products/deepocr/" rel="noopener noreferrer"&gt;https://deeplab.tools/products/deepocr/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you've been living with unsearchable scans, or fighting the OCRmyPDF install, I'd love for you to try it and tell me where it falls short - especially which language or script you'd want next.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part of &lt;a href="https://github.com/deeplabua" rel="noopener noreferrer"&gt;DeepLab&lt;/a&gt; - small, local-first, dependency-light Rust tools. See also &lt;a href="https://github.com/deeplabua/deepdoc" rel="noopener noreferrer"&gt;DeepDoc&lt;/a&gt; (documents → Markdown) and &lt;a href="https://github.com/deeplabua/deepshrink" rel="noopener noreferrer"&gt;DeepShrink&lt;/a&gt; (fit any video under a size limit).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>opensource</category>
      <category>ocr</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Parsing documents for air-gapped RAG: no cloud, no JVM, no Python</title>
      <dc:creator>Alexander Pervushen</dc:creator>
      <pubDate>Sat, 25 Jul 2026 10:38:45 +0000</pubDate>
      <link>https://dev.to/deeplab/parsing-documents-for-air-gapped-rag-no-cloud-no-jvm-no-python-3a13</link>
      <guid>https://dev.to/deeplab/parsing-documents-for-air-gapped-rag-no-cloud-no-jvm-no-python-3a13</guid>
      <description>&lt;p&gt;I kept running into the same unglamorous wall.&lt;/p&gt;

&lt;p&gt;Every time I built something on top of an LLM — a search-over-docs feature, a RAG pipeline, an internal assistant — the first step was the same boring problem: &lt;em&gt;"here's a folder of docx, pdf and pptx files, give me clean text."&lt;/em&gt; Not a hard problem. Just one that, somehow, every available tool solved by making me pay a price I didn't want to pay.&lt;/p&gt;

&lt;p&gt;And in my case there was an extra constraint that quietly kills half the options: the documents couldn't leave the machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three usual answers, and what each one costs
&lt;/h2&gt;

&lt;p&gt;If you've done document ingestion, you know the shortlist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apache Tika&lt;/strong&gt; is the honest default. It's been the de-facto standard for 15 years, it reads hundreds of formats, and it works. It's also a JVM. That means standing up a JVM, feeding it, watching it, and carrying it around inside every container — which feels deeply out of place in a Rust or Go service. Tika is great. The JVM tax is the part people are trying to get away from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Python stacks&lt;/strong&gt; — &lt;code&gt;unstructured&lt;/code&gt;, Docling, MarkItDown — are powerful and popular in every RAG tutorial. But "just parse a document" turns into pip, native wheels, and downloading ML model weights before you get a single line of text. They're heavier and slower to cold-start than the job needs for clean, born-digital files, and the ML paths aren't deterministic. Overkill when the document already &lt;em&gt;has&lt;/em&gt; real text in it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloud parsers&lt;/strong&gt; like LlamaParse are the easiest to start with — until you read the two lines of fine print. You upload your documents to someone else's server, and you pay per page. For anyone in a bank, a hospital, a law firm, or any air-gapped environment, "upload the private documents" is where the conversation ends.&lt;/p&gt;

&lt;p&gt;None of them answered the thing I actually wanted: &lt;em&gt;any document → clean Markdown, one binary, on my machine, deterministically.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So I wrote it. It's called &lt;strong&gt;DeepDoc&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually wanted
&lt;/h2&gt;

&lt;p&gt;A single static Rust binary. No JVM, no Python, no model downloads, no runtime dependencies at all. Copy it into a &lt;code&gt;scratch&lt;/code&gt; Docker layer and go. Nothing touches the network, nothing shells out. Same input, same output, every time — because a RAG index you can't reproduce is a bug waiting to happen.&lt;/p&gt;

&lt;p&gt;Here's the whole thing on a real file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;deepdoc report.docx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Q3 Engineering Report&lt;/span&gt;

Prepared by the Platform team. This memo covers delivery and headcount for the third quarter.

&lt;span class="gu"&gt;## Delivery&lt;/span&gt;

We shipped the ingestion rewrite and cut p95 latency by 38%.

The nightly batch now runs in under nine minutes.

&lt;span class="gu"&gt;## Headcount&lt;/span&gt;

| Team     | Engineers | Open roles |
| -------- | --------- | ---------- |
| Platform | 9         | 2          |
| Search   | 6         | 1          |
| Data     | 4         | 0          |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a Word document — headings, paragraphs, and a real table — coming out as clean Markdown, table intact, in one command. No server, no upload, no warm-up.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works (it's boring on purpose)
&lt;/h2&gt;

&lt;p&gt;Every format gets parsed into one neutral &lt;code&gt;Document&lt;/code&gt; model — headings, paragraphs, lists, tables, metadata, page markers. Then that model is serialized to Markdown, JSON or plain text by pure functions. That's the whole design, and the boringness is the point: the parser for &lt;code&gt;.docx&lt;/code&gt; and the parser for &lt;code&gt;.pptx&lt;/code&gt; disagree about almost everything, but once they're in the same shape, "turn this into Markdown" is one code path, tested once.&lt;/p&gt;

&lt;p&gt;v0.1 covers the born-digital formats I actually hit in ingestion work: &lt;code&gt;docx&lt;/code&gt;, &lt;code&gt;pptx&lt;/code&gt;, &lt;code&gt;xlsx&lt;/code&gt;, &lt;code&gt;odt&lt;/code&gt;, &lt;code&gt;ods&lt;/code&gt;, &lt;code&gt;odp&lt;/code&gt;, &lt;code&gt;epub&lt;/code&gt;, &lt;code&gt;html&lt;/code&gt;, &lt;code&gt;rtf&lt;/code&gt;, &lt;code&gt;csv&lt;/code&gt;, &lt;code&gt;md&lt;/code&gt;, &lt;code&gt;txt&lt;/code&gt;, and text-based &lt;code&gt;pdf&lt;/code&gt; — thirteen formats in one binary. Point it at a folder and it walks the tree in parallel and mirrors the structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;deepdoc ./docs &lt;span class="nt"&gt;--recursive&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; out/
./docs/changelog.txt    → out/changelog.md
./docs/meeting-notes.md → out/meeting-notes.md
./docs/report.docx      → out/report.md
✓ 3 extracted, 0 skipped
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The part that matters for RAG: chunking that keeps context
&lt;/h2&gt;

&lt;p&gt;Naive chunkers split on character count and throw away structure. So a chunk about "refunds" stops knowing it lived under &lt;strong&gt;Billing → Refunds&lt;/strong&gt;, and you've thrown away the exact breadcrumb your embedding could have used.&lt;/p&gt;

&lt;p&gt;DeepDoc parses structure first, then chunks on block boundaries while carrying the heading path and the byte range for every chunk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;deepdoc handbook.pdf &lt;span class="nt"&gt;--chunk&lt;/span&gt; 800 &lt;span class="nt"&gt;--format&lt;/span&gt; json | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"chunks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"byte_range"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;483&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"heading_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Employee Handbook"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"handbook.pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"# Employee Handbook&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;## Remote Work&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Employees may work remotely up to three days per week..."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"…"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1 more chunk"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"meta"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"source_format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"page_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"…"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every chunk knows which section it came from and where in the source it lives. That &lt;code&gt;heading_path&lt;/code&gt; goes straight onto the vector as metadata, and the &lt;code&gt;byte_range&lt;/code&gt; means you can always trace a retrieved chunk back to the exact offset in the original file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest boundary
&lt;/h2&gt;

&lt;p&gt;I'll be upfront about what DeepDoc is &lt;em&gt;not&lt;/em&gt;, because overselling a dev tool is how you lose the people you're trying to reach.&lt;/p&gt;

&lt;p&gt;v0.1 is &lt;strong&gt;born-digital&lt;/strong&gt; documents — files with real embedded text. It is &lt;strong&gt;not&lt;/strong&gt; an OCR engine and &lt;strong&gt;not&lt;/strong&gt; a complex-table-from-scans reconstructor. That's genuinely hard, it's ML territory, and tools like Docling and Marker do it well. If you hand DeepDoc a scanned PDF that's really just images, it detects that and exits with a specific code (4) telling you so — instead of confidently emitting garbage into your index. OCR is planned as an optional, feature-gated path later; it is deliberately not bundled into the tiny default binary.&lt;/p&gt;

&lt;p&gt;The bet is simple: roughly 80% of the documents in a real ingestion pipeline are born-digital, and for those you don't need a GPU or a model download. You need something fast, deterministic, and local. That's the lane.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's also a library
&lt;/h2&gt;

&lt;p&gt;The CLI is a thin wrapper. The real entry point is the &lt;code&gt;deepdoc-core&lt;/code&gt; crate, so you can embed extraction directly in a Rust service — no subprocess, no FFI, no shelling out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;deepdoc_core&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_markdown&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"handbook.pdf"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;to_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The closest thing in the ecosystem, &lt;code&gt;extractous&lt;/code&gt;, is actually Tika under GraalVM native-image — not pure Rust, and not a small binary. DeepDoc is Rust top to bottom, and every dependency in the graph is permissively licensed on purpose, so nothing stops you from wrapping it in whatever you're building.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;deeplabua/tap/deepdoc
&lt;span class="c"&gt;# or&lt;/span&gt;
cargo &lt;span class="nb"&gt;install &lt;/span&gt;deepdoc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's open source (MIT / Apache), v0.1, and on GitHub: &lt;strong&gt;&lt;a href="https://github.com/deeplabua/deepdoc" rel="noopener noreferrer"&gt;https://github.com/deeplabua/deepdoc&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're building document ingestion for a local or compliance-bound RAG system, I'd genuinely love your feedback — especially on which formats or edge cases to prioritize next. And if you've been fighting the JVM/Python/cloud trade-off yourself, tell me what you ended up doing; I want to know if I missed a better option.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;DeepDoc is part of a small set of local-first, dependency-light tools I'm building under &lt;a href="https://github.com/deeplabua" rel="noopener noreferrer"&gt;DeepLab&lt;/a&gt; — same idea as &lt;a href="https://github.com/deeplabua/deepshrink" rel="noopener noreferrer"&gt;DeepShrink&lt;/a&gt;, which fits any video under a size limit from one command. Small, honest, no cloud.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>rag</category>
      <category>llm</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Fit any video under the limit: meet DeepShrink, a one-command media compressor</title>
      <dc:creator>Alexander Pervushen</dc:creator>
      <pubDate>Tue, 21 Jul 2026 08:40:51 +0000</pubDate>
      <link>https://dev.to/deeplab/fit-any-video-under-the-limit-meet-deepshrink-a-one-command-media-compressor-2j8b</link>
      <guid>https://dev.to/deeplab/fit-any-video-under-the-limit-meet-deepshrink-a-one-command-media-compressor-2j8b</guid>
      <description>&lt;p&gt;You have a 300 MB screen recording. Discord wants it under 8 MB. Your email won't take more than 20. So you either google &lt;em&gt;"compress video to 8mb"&lt;/em&gt; and land on an ad-riddled web converter that uploads your file to someone else's server and stamps a watermark on it — or you open HandBrake and start guessing at CRF values — or you hand-write an &lt;code&gt;ffmpeg&lt;/code&gt; command, recompute the bitrate for the fifth time this month, and forget the audio track again.&lt;/p&gt;

&lt;p&gt;None of those answer the actual question: &lt;strong&gt;"make this ≤ N MB, without me thinking about it."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the whole reason &lt;a href="https://github.com/deeplabua/deepshrink" rel="noopener noreferrer"&gt;DeepShrink&lt;/a&gt; exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deepshrink gameplay.mp4 &lt;span class="nt"&gt;--for&lt;/span&gt; discord
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  gameplay.mp4   1920x1080  2m14s   312.4 MB
  target         Discord (8 MB)
  plan           H.264 · 456 kbps video · 96 kbps audio · two-pass
  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  100%   pass 2/2

  ✓ gameplay.shrink.mp4   7.6 MB   (−97.6%)   VMAF 91.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command. Local. No upload, no watermark, no guessing. And when you say &lt;code&gt;--target 8MB&lt;/code&gt;, the result is &lt;em&gt;guaranteed&lt;/em&gt; ≤ 8 MB — not "roughly."&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually is
&lt;/h2&gt;

&lt;p&gt;DeepShrink is a small, open-source CLI written in Rust. It doesn't reinvent encoding — it drives &lt;strong&gt;ffmpeg&lt;/strong&gt; for you and does the one thing ffmpeg makes annoying: hitting an exact output size.&lt;/p&gt;

&lt;p&gt;Under the hood it probes the file with &lt;code&gt;ffprobe&lt;/code&gt;, works out the bitrate budget for your target size (with a little headroom for container overhead), runs a two-pass H.264/H.265 encode for video or picks the right codec bitrate for audio (AAC / Opus / MP3), and can optionally hold a quality floor with VMAF so it never over-compresses. You don't touch any of that. You say how big, it figures out how.&lt;/p&gt;

&lt;p&gt;The design goals, in order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One obvious command solves 80% of cases.&lt;/strong&gt; &lt;code&gt;deepshrink video.mp4 --target 8MB&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Presets instead of flags.&lt;/strong&gt; You shouldn't need to know what a bitrate is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never wreck the original.&lt;/strong&gt; Output is a new file (&lt;code&gt;video.shrink.mp4&lt;/code&gt;); the source is untouched unless you explicitly pass &lt;code&gt;--overwrite&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local and private.&lt;/strong&gt; Your file never leaves your machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope today is &lt;strong&gt;video and audio&lt;/strong&gt; — one engine, ffmpeg. Images, PDFs, and other formats are a deliberate future step, not a half-baked afterthought.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing with Homebrew (recommended on macOS)
&lt;/h2&gt;

&lt;p&gt;This is the frictionless path, and it's the one I'd reach for first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;deeplabua/tap/deepshrink
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Because the formula declares &lt;code&gt;depends_on "ffmpeg"&lt;/code&gt;, Homebrew pulls &lt;strong&gt;ffmpeg in as a dependency automatically&lt;/strong&gt; — so there are zero manual runtime steps. Install, and it works.&lt;/p&gt;

&lt;p&gt;If you'd rather tap first and install by short name (handy if you'll install other tools from the same tap later):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew tap deeplabua/tap
brew &lt;span class="nb"&gt;install &lt;/span&gt;deepshrink
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify it landed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deepshrink &lt;span class="nt"&gt;--version&lt;/span&gt;
deepshrink &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Updating later is just &lt;code&gt;brew upgrade deepshrink&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing with cargo (from crates.io)
&lt;/h2&gt;

&lt;p&gt;If you already live in the Rust toolchain, DeepShrink is on crates.io:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;install &lt;/span&gt;deepshrink
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This compiles from source, so it's slower than the prebuilt Homebrew bottle and you need a Rust toolchain installed. Worth knowing about &lt;strong&gt;one difference from the brew path&lt;/strong&gt;: &lt;code&gt;cargo install&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; bring ffmpeg along. DeepShrink expects &lt;code&gt;ffmpeg&lt;/code&gt; and &lt;code&gt;ffprobe&lt;/code&gt; to be on your &lt;code&gt;PATH&lt;/code&gt;, and if they aren't, it exits cleanly (code &lt;code&gt;3&lt;/code&gt;) with a hint rather than crashing.&lt;/p&gt;

&lt;p&gt;So on a fresh machine, install ffmpeg alongside it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;ffmpeg

&lt;span class="c"&gt;# Debian / Ubuntu&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;ffmpeg

&lt;span class="c"&gt;# Windows (Scoop)&lt;/span&gt;
scoop &lt;span class="nb"&gt;install &lt;/span&gt;ffmpeg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the same check as before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deepshrink &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  brew vs cargo, quickly
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;brew install deeplabua/tap/deepshrink&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;cargo install deepshrink&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;Prebuilt bottle, instant&lt;/td&gt;
&lt;td&gt;Compiles from source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rust toolchain needed&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ffmpeg&lt;/td&gt;
&lt;td&gt;Pulled in automatically&lt;/td&gt;
&lt;td&gt;You install it yourself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Most people, especially macOS&lt;/td&gt;
&lt;td&gt;Rust devs who already have the toolchain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Prebuilt binaries for macOS, Linux, and Windows are also attached to every &lt;a href="https://github.com/deeplabua/deepshrink/releases" rel="noopener noreferrer"&gt;GitHub release&lt;/a&gt; if you'd rather not use a package manager at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it
&lt;/h2&gt;

&lt;p&gt;The headline command — fit a file under an absolute size:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deepshrink big.mp4 &lt;span class="nt"&gt;--target&lt;/span&gt; 8MB
deepshrink talk.wav &lt;span class="nt"&gt;--target&lt;/span&gt; 500KB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't want to memorize platform limits? Use a &lt;strong&gt;destination preset&lt;/strong&gt; and DeepShrink applies the right cap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deepshrink clip.mp4 &lt;span class="nt"&gt;--for&lt;/span&gt; discord        &lt;span class="c"&gt;# 8 MB&lt;/span&gt;
deepshrink clip.mp4 &lt;span class="nt"&gt;--for&lt;/span&gt; email          &lt;span class="c"&gt;# 20 MB&lt;/span&gt;
deepshrink clip.mp4 &lt;span class="nt"&gt;--for&lt;/span&gt; telegram       &lt;span class="c"&gt;# only recompresses for quality&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Built-in presets today: &lt;code&gt;discord&lt;/code&gt;, &lt;code&gt;discord-nitro&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;telegram&lt;/code&gt;, &lt;code&gt;whatsapp&lt;/code&gt;, and &lt;code&gt;web&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Prefer a relative cut instead of a hard ceiling?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deepshrink video.mp4 &lt;span class="nt"&gt;--reduce&lt;/span&gt; 70%        &lt;span class="c"&gt;# 70% smaller than the source&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Audio is a first-class citizen, not an afterthought — great for podcasts, voice memos, and lectures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deepshrink lecture.wav &lt;span class="nt"&gt;--target&lt;/span&gt; 10MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  lecture.wav     stereo 48kHz   58m02s   638.1 MB (PCM)
  target          10 MB
  plan            Opus · ~22 kbps · mono (speech)
  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  100%

  ✓ lecture.shrink.opus   9.4 MB   (−98.5%)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Got a whole folder? Batch it — DeepShrink walks the directory, skips files that are already small or unsupported, and prints a summary instead of dying on the first hiccup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deepshrink ./recordings &lt;span class="nt"&gt;--recursive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few flags worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--dry-run&lt;/code&gt; — show the plan (chosen bitrate, expected size) without encoding a thing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--vmaf &amp;lt;n&amp;gt;&lt;/code&gt; — hold a quality floor; it won't compress below the VMAF score you ask for.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--codec h265&lt;/code&gt; — smaller files at the same quality, when you don't need maximum compatibility.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--json&lt;/code&gt; — machine-readable output, for scripts and CI.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--overwrite&lt;/code&gt; — replace the original in place (written atomically via a temp file, so an interrupted run won't corrupt your source).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why bother, over a web converter?
&lt;/h2&gt;

&lt;p&gt;Because DeepShrink is the intersection people actually want: the &lt;strong&gt;precision and privacy of native ffmpeg&lt;/strong&gt; with the &lt;strong&gt;simplicity of a web converter&lt;/strong&gt; — and none of the downsides of either. Your file stays on your disk. You get an exact size, not an approximation. There are no watermarks, no queues, and no "upload your file to continue." It's one static binary that works offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Homebrew:&lt;/strong&gt; &lt;code&gt;brew install deeplabua/tap/deepshrink&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cargo:&lt;/strong&gt; &lt;code&gt;cargo install deepshrink&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source &amp;amp; releases:&lt;/strong&gt; &lt;a href="https://github.com/deeplabua/deepshrink" rel="noopener noreferrer"&gt;github.com/deeplabua/deepshrink&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's open source and free. If it saves you a trip to a sketchy converter even once, consider dropping a ⭐ on the repo — that's what keeps small tools like this alive.&lt;/p&gt;

&lt;p&gt;Try it on that 300 MB file you've been avoiding. &lt;code&gt;--target 8MB&lt;/code&gt;. Done.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>cli</category>
      <category>ffmpeg</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
