<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Fastapi on </title>
    <link>/tags/fastapi/</link>
    <description>Recent content in Fastapi on </description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Sat, 28 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="/tags/fastapi/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Adding a Web UI to My PDF to Markdown Converter</title>
      <link>/posts/augmented-resilience-posts/adding-a-web-ui-to-my-pdf-to-markdown-converter/</link>
      <pubDate>Sat, 28 Mar 2026 00:00:00 +0000</pubDate>
      
      <guid>/posts/augmented-resilience-posts/adding-a-web-ui-to-my-pdf-to-markdown-converter/</guid>
      <description>&lt;h2 id=&#34;the-promise-i-made-to-myself&#34;&gt;The Promise I Made to Myself&lt;/h2&gt;
&lt;p&gt;In my last post about &lt;a href=&#34;https://augmentedresilience.com/posts/when-your-pdf-workflow-breaks-building-a-markdown-converter-with-claude-code/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;building the PDF to Markdown converter&lt;/a&gt;
, I listed some &amp;ldquo;what&amp;rsquo;s next&amp;rdquo; ideas at the end. One of them was:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;FastAPI wrapper: Create an HTTP API for web apps to use&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Well, I did it. And I went a step further — I built a full drag-and-drop web UI on top of it.&lt;/p&gt;
&lt;p&gt;The CLI still works exactly as before. This is an addition, not a replacement. But now when I want to convert a batch of PDFs without thinking about terminal commands, I just open a browser tab.&lt;/p&gt;</description>
      <content>&lt;h2 id=&#34;the-promise-i-made-to-myself&#34;&gt;The Promise I Made to Myself&lt;/h2&gt;
&lt;p&gt;In my last post about &lt;a href=&#34;https://augmentedresilience.com/posts/when-your-pdf-workflow-breaks-building-a-markdown-converter-with-claude-code/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;building the PDF to Markdown converter&lt;/a&gt;
, I listed some &amp;ldquo;what&amp;rsquo;s next&amp;rdquo; ideas at the end. One of them was:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;FastAPI wrapper: Create an HTTP API for web apps to use&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Well, I did it. And I went a step further — I built a full drag-and-drop web UI on top of it.&lt;/p&gt;
&lt;p&gt;The CLI still works exactly as before. This is an addition, not a replacement. But now when I want to convert a batch of PDFs without thinking about terminal commands, I just open a browser tab.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;what-the-ui-does&#34;&gt;What the UI Does&lt;/h2&gt;
&lt;p&gt;The interface is intentionally minimal:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Drag-and-drop zone&lt;/strong&gt; — drop one PDF or fifty onto it&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Browse button&lt;/strong&gt; — if you prefer clicking&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Convert button&lt;/strong&gt; — kicks off the conversion&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Per-file progress bars&lt;/strong&gt; — live updates as each file converts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Individual download&lt;/strong&gt; — each completed file gets its own Download button&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Download all as ZIP&lt;/strong&gt; — one click to grab everything&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clear&lt;/strong&gt; — resets the session and cleans up temp files server-side&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Everything runs locally. Files go to a temp directory on your machine, get converted, and are served back to you. Nothing hits an external API.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-stack&#34;&gt;The Stack&lt;/h2&gt;
&lt;p&gt;I kept it as simple as possible:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Backend:&lt;/strong&gt; FastAPI + uvicorn&lt;/p&gt;
&lt;p&gt;FastAPI was the obvious choice — it handles file uploads cleanly, has first-class async support, and the &lt;code&gt;python-multipart&lt;/code&gt; library makes multi-file form handling trivial. The conversion logic is unchanged from the CLI: &lt;code&gt;pymupdf4llm.to_markdown()&lt;/code&gt; doing the heavy lifting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Progress updates:&lt;/strong&gt; Server-Sent Events (SSE)&lt;/p&gt;
&lt;p&gt;This is the part I found most interesting. When you hit Convert, the browser opens a persistent connection to &lt;code&gt;/progress/{job_id}&lt;/code&gt; and receives a stream of JSON events — one every 400ms — until the job finishes. No polling loop, no WebSocket complexity. SSE is perfect for this: unidirectional, simple, and built into every modern browser.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;event_stream&lt;/span&gt;():
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;while&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        data &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; json&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;dumps({&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;progress&amp;#34;&lt;/span&gt;: job[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;progress&amp;#34;&lt;/span&gt;], &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;done&amp;#34;&lt;/span&gt;: job[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;done&amp;#34;&lt;/span&gt;]})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;yield&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;data: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;data&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; job[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;done&amp;#34;&lt;/span&gt;]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;break&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; asyncio&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;sleep(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.4&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; StreamingResponse(event_stream(), media_type&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;text/event-stream&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On the frontend, consuming it is three lines:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;eventSource&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;EventSource&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;`/progress/&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;jobId&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;`&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;eventSource&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;onmessage&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;e&lt;/span&gt; =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;progress&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;done&lt;/span&gt; } &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;JSON&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;parse&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;e&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// update the UI...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Threading:&lt;/strong&gt; The conversion itself is synchronous (PyMuPDF4LLM blocks while it processes pages). To keep the FastAPI event loop from freezing during conversion, each job runs in a &lt;code&gt;ThreadPoolExecutor&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;asyncio&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_event_loop()&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;run_in_executor(executor, _convert_job, job_id, job_dir)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Four workers by default — enough to handle several simultaneous conversions without overloading the machine.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Frontend:&lt;/strong&gt; Vanilla JS, no build step&lt;/p&gt;
&lt;p&gt;I deliberately avoided React, Vue, or any framework. The whole UI is a single &lt;code&gt;static/index.html&lt;/code&gt; file. It loads instantly, has no dependencies to install, and is easy to read and modify. For a local tool that one person uses, this is the right call.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;project-structure&#34;&gt;Project Structure&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s what changed from the original CLI project:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pdf-to-markdown/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  pdf2md          — original CLI (unchanged)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  app.py          — FastAPI server (new)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  static/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    index.html    — drag-drop UI (new)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  serve           — start script (new)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  requirements.txt — updated with FastAPI deps
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  venv/           — existing venv, three new packages added
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;serve&lt;/code&gt; script is just:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cd &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;$(&lt;/span&gt;dirname &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$0&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;)&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;source venv/bin/activate
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;uvicorn app:app --host 0.0.0.0 --port &lt;span style=&#34;color:#ae81ff&#34;&gt;8765&lt;/span&gt; --reload
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Run it once, open &lt;code&gt;http://localhost:8765&lt;/code&gt;, and you have a working converter in your browser.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;one-gotcha-pymupdf4llm-is-synchronous&#34;&gt;One Gotcha: PyMuPDF4LLM is Synchronous&lt;/h2&gt;
&lt;p&gt;This tripped me up briefly. &lt;code&gt;pymupdf4llm.to_markdown()&lt;/code&gt; does not return a coroutine — it&amp;rsquo;s a blocking call that can take 10–30 seconds on a large document. If you call it directly in an async FastAPI route handler, you freeze the entire event loop while it runs. No other requests get handled. The SSE stream stops updating.&lt;/p&gt;
&lt;p&gt;The fix is the &lt;code&gt;ThreadPoolExecutor&lt;/code&gt; pattern above — push the blocking work off the event loop entirely. The async route returns immediately, the SSE stream keeps ticking, and the conversion runs in a thread pool where it belongs.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-download-endpoints&#34;&gt;The Download Endpoints&lt;/h2&gt;
&lt;p&gt;Three endpoints handle output:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;GET /download/{job_id}/{filename}    — single .md file
GET /download-all/{job_id}           — all .md files as a ZIP
DELETE /job/{job_id}                 — clean up temp files
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The ZIP is built in memory using Python&amp;rsquo;s &lt;code&gt;zipfile&lt;/code&gt; module and streamed directly to the browser — no intermediate file on disk:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;buf &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; io&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;BytesIO()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;with&lt;/span&gt; zipfile&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ZipFile(buf, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;w&amp;#34;&lt;/span&gt;, zipfile&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ZIP_DEFLATED) &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; zf:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; f &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; md_files:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        zf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write(f, f&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;name)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;buf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;seek(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; StreamingResponse(buf, media_type&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;application/zip&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;what-this-unlocks&#34;&gt;What This Unlocks&lt;/h2&gt;
&lt;p&gt;The CLI was already useful. The web UI adds a few things the CLI cannot easily do:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Non-terminal users.&lt;/strong&gt; Anyone on my network can now use this converter by visiting &lt;code&gt;http://my-machine:8765&lt;/code&gt;. No Python knowledge required.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bulk drop workflows.&lt;/strong&gt; Dragging 20 PDFs from Finder into a browser window and clicking Convert is significantly faster than constructing a &lt;code&gt;--batch&lt;/code&gt; command with the right paths.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Visual feedback.&lt;/strong&gt; The progress bars are not just cosmetic. For large PDFs that take 20–30 seconds, knowing the conversion is running (and roughly how far along it is) removes the anxiety of staring at a terminal cursor.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;whats-next&#34;&gt;What&amp;rsquo;s Next&lt;/h2&gt;
&lt;p&gt;The original roadmap item was &amp;ldquo;FastAPI wrapper.&amp;rdquo; That&amp;rsquo;s done. The next one I&amp;rsquo;m eyeing:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Auto-feed to Obsidian inbox.&lt;/strong&gt; Right now the flow is: convert in the web UI, download the ZIP, unzip, move to Obsidian. I&amp;rsquo;d like to add a toggle: &amp;ldquo;Send output directly to &lt;code&gt;~/projects/obsidian-vault/00-inbox/&lt;/code&gt;&amp;rdquo; — one less manual step.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a small addition to the backend. Coming soon.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;running-it&#34;&gt;Running It&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cd ~/projects/pdf-to-markdown
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;./serve
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Open http://localhost:8765&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The first run installs nothing new — the three new packages (fastapi, uvicorn, python-multipart) are already in the venv. It just works.&lt;/p&gt;
</content>
    </item>
    
  </channel>
</rss>
