Zoravur's Bearblog

[AI Article] How I vibecoded an EPUB translator in about 30 minutes

I wanted to read an EPUB in a language I don't speak, and I didn't want to pay a subscription or upload my book to someone's server. So instead of writing code, I described an app out loud and let an agent build it. About half an hour later I had a working, client-side EPUB translator.

This post is reconstructed from the agent's own memory log (memories.db) — 332 recorded steps across five sessions. Nothing here is from memory; it's all timestamps and diffs.

The ask

One paragraph. That's the entire spec I wrote by hand:

Greenfield project! Build a web app that accepts an epub upload and processes it to create a reader in-browser. Create a left panel where I can paste an deepseek api key and send the epub text to deepseek. The left panel should have a from->to language dropdown for converting from the source to any language. Finally, it should give the ability to download the translated epub.

Then I typed please continue a bunch of times and watched.

What it became

A single-page app, no backend, no build step, no npm dependencies (vendor/jszip.min.js is vendored):

The timeline

Time (Sep 2026) What happened
20:14 I sent the prompt above.
20:16–20:26 Agent wrote the whole thing — server, EPUB parser, text scanner, DeepSeek client, reader, UI, tests — and had 19/19 logic checks and 22/22 headless-browser checks passing. First working version.
20:38–20:41 It used a web-search tool to check the current DeepSeek models and rewrote the model list.
20:54–20:59 Added the token/cost meters.
21:13–21:16 git init, added the GitHub remote, added a GitHub Pages workflow.

The first fully working version existed roughly 12 minutes after my first prompt. Count the follow-on features and the two coffee-break gaps, and it's "about 30 minutes" of real hands-on work in a single evening session.

The four moves that made it fast

1. One good prompt, then hand over the wheel. The initial prompt named the inputs, the UI shape, the API, and the outputs — and nothing else. That was enough for the agent to pick sensible defaults and start. Everything after that was please continue.

2. Let it discover the architecture instead of me dictating it. I assumed I'd need a little backend to proxy the LLM API. The agent checked the actual CORS response and found DeepSeek returns permissive CORS headers, so a purely client-side app was possible — no server, and my key never leaves the browser. That single discovery deleted an entire tier of the design.

3. Faithful output over clever output. Rather than re-serializing each chapter through the DOM (which mangles entities, comments, and namespaces), src/epub-text.js scans the raw markup and splices only the translated text runs back in, re-escaping them. Everything untouched — images, fonts, the cover, the OPF — is copied across byte-for-byte. It's the least glamorous part of the design and the reason the exported books actually look right.

4. Tests as guardrails, not afterthoughts. The agent wrote a real headless-Chrome E2E suite that loads a generated fixture EPUB through the actual file input, translates against a stubbed DeepSeek endpoint, and re-opens the exported zip to verify the translations landed. It caught two real bugs I'd never have found by clicking around:

Where vibecoding bites

It wasn't frictionless, and the failure modes are worth naming.

Its training data was stale — and it knew to check. The app shipped defaulting to deepseek-chat. When I asked it to look up the latest models, it searched the live docs and found that deepseek-chat / deepseek-reasoner had been retired, replaced by deepseek-flash. It then rewrote the model list, switched the default, and — nicely — explicitly disabled "thinking" mode for translation (thinking: {type: "disabled"}) because chain-of-thought costs latency and output tokens for no gain on a single-shot transformation. A model's confident memory of an API is not the same as the API. Verify against the live source.

The agent's confidence isn't calibration. The log is full of it getting its own tool syntax wrong — "I keep forgetting the hunk header needs line numbers" appears more than once — and correcting itself. Cheap to make, cheap to fix, but you do have to read the output.

Security is not a vibe. Left alone, the app rendered chapter HTML with event handlers intact. A malicious EPUB could execute JavaScript in the app's origin and read your pasted API key. I hadn't asked for that analysis; the audit found it. The fix: render books in a sandboxed iframe with scripts disabled, sanitize markup against an allowlist, bind the dev server to 127.0.0.1 and serve only an explicit asset allowlist, and bound archive decompression so a crafted zip can't exhaust the tab. Shipping fast and shipping safe are different muscle groups — use both.

Even the memory tool had a blind spot. At one point the agent tried to save reusable lessons and hit Structured memory is unavailable… use search_memories, or migrate. The thing writing this post couldn't write to its own database. Meta, but instructive: read-only memory still reconstructs the story, you just have to query it.

The receipts

The takeaway

The interesting part isn't that an LLM wrote some JavaScript. It's that the whole loop — design, code, test, harden, ship — ran at the speed of conversation, with a real test suite and a real security pass built in along the way. The things I'd do again: write one precise prompt, let the agent pick the architecture, and treat tests and a security review as part of "vibecoding" rather than the thing you skip. The thing to watch: never trust a model's memory of a fast-moving API.

About 30 minutes, one paragraph of prompt, and a book I can finally read.