Metadata-Version: 2.5
Name: webrender-worker
Version: 0.1.0
Summary: WebRender local worker — a paired daemon that runs read-aloud TTS and handwriting recognition on your own machine.
Project-URL: Homepage, https://magic-box.co.uk
Project-URL: Documentation, https://magic-box.co.uk
Project-URL: Container image, https://ghcr.io/josephnhopkins/webrender-worker
License: AGPL-3.0-or-later
License-File: LICENSE
Keywords: handwriting,kokoro,ocr,self-hosted,text-to-speech,tts,worker
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: No Input/Output (Daemon)
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
Classifier: Topic :: Text Processing :: Linguistic
Requires-Python: <3.14,>=3.10
Requires-Dist: httpx>=0.27
Provides-Extra: render
Requires-Dist: kokoro; extra == 'render'
Requires-Dist: misaki[en]; extra == 'render'
Requires-Dist: numpy>=1.24; extra == 'render'
Requires-Dist: soundfile; extra == 'render'
Provides-Extra: test
Requires-Dist: pytest>=8; extra == 'test'
Description-Content-Type: text/markdown

# webrender-worker

The **paired local worker** — the box side of [WebRender](https://magic-box.co.uk)'s
*local rung*. Install it on a machine you own (a Mac mini, a Linux box, a NAS), pair it
once to your WebRender account, and it runs your jobs at home — **unlimited, private,
and free at the point of use**. No third-party AI provider ever sees your text.

Two job kinds today:

- **read-aloud TTS** — renders audio with [Kokoro](https://github.com/hexgrad/kokoro);
- **handwriting recognition** — reads rendered ink with a local vision model via
  [Ollama](https://ollama.com) (default `qwen2.5vl:7b-16k`; override with
  `WEBRENDER_OLLAMA` / `WEBRENDER_OLLAMA_MODEL`).

The poll *declares* which kinds this daemon supports, so an older daemon is never handed
work it cannot run.

It makes only **outbound** HTTPS calls (no inbound ports, works behind NAT), holds one
revocable account-scoped token, and imports nothing from the WebRender server — this
package is the whole box side. Licence: **AGPL-3.0-or-later**; the wheel is pure Python,
so the source you are running is the source in the package.

## How it works

```
CLOUD (WebRender)                          YOUR BOX (this daemon)
  preprocess text, enqueue a job
                                  ← poll   GET /worker/jobs      (outbound)
                                    job →  render each segment with Kokoro
                                  ← MP3    POST …/result         (framed upload)
  store MP3 (same store as a paid gen)
  player serves it → you listen, any device
```

The box never sees the document — only the per-segment *spoken text* the cloud already
preprocessed. It renders each segment to an MP3, keyed by the segment's global index, and
uploads the batch; the cloud reassembles it into the exact artifact a paid generation
would produce. A recognition job mirrors that line one step further: the box sees rendered
ink pixels and a server-supplied prompt, never the document.

## Install

### Python (recommended on macOS)

Requires **Python 3.10–3.13** (the Kokoro stack has no 3.14 wheels yet) and **ffmpeg** on
`PATH` (the MP3 encoder).

```bash
# ffmpeg:  macOS → brew install ffmpeg   ·   Debian/Ubuntu → sudo apt install ffmpeg
pip install 'webrender-worker[render]'      # or: uv pip install 'webrender-worker[render]'
```

The `render` extra is the Kokoro TTS stack. Plain `pip install webrender-worker` gives
only the daemon skeleton — useful if you are implementing against the protocol, not for
actually rendering audio. The first render downloads the Kokoro model weights (~a few
hundred MB, cached).

⚠ **On Linux, `pip install` pulls the default PyPI PyTorch wheel, which drags several GB
of CUDA libraries a CPU rung never loads.** Either install CPU-only torch first
(`pip install torch --index-url https://download.pytorch.org/whl/cpu`) or use the Docker
image below, which does exactly that for you.

### Docker (Linux / WSL2 / NAS)

No Python or ffmpeg on the host, and CPU-only by design:

```bash
docker run --rm -it -v webrender-worker:/data \
    ghcr.io/josephnhopkins/webrender-worker pair          # open the printed link

docker run -d --restart unless-stopped --name webrender-worker \
    -v webrender-worker:/data ghcr.io/josephnhopkins/webrender-worker
```

Logs: `docker logs -f webrender-worker`. The `webrender-worker` named volume holds the
pairing token and the Kokoro model cache, so it survives container recreation.

**On macOS, run the daemon natively instead** — Docker on a Mac is a hidden Linux VM: it
taxes RAM and cannot use Apple-silicon acceleration.

## Use

```bash
webrender-worker pair                 # prints a link; open it signed in to WebRender
webrender-worker run                  # start rendering (leave it running)
webrender-worker status               # build, host + paired state
webrender-worker status --json        # the same, machine-readable
webrender-worker unpair               # revoke this box + forget the token
```

Pairing is a device flow: the daemon prints a one-time link, you open it in a browser
where you are already signed in to WebRender, and the box receives its token. Nothing
inbound, and no password ever reaches the box.

The token lives in `~/.webrender-worker/config.json` (mode 0600). You can also disconnect
the box from **your WebRender account page** — the kill-switch — and the daemon then stops
with a "re-pair" message on its next poll.

`pair` and `run` accept `--host URL` to point at a different WebRender deployment (a
self-hosted one, or a dev server). You do not need it for the hosted service.

### `status --json`

The machine-readable contract — parse this rather than the text face, and paste it into
any support conversation. It **never** contains the pairing token.

```json
{
  "version": "0.1.0",
  "host": "https://magic-box.co.uk",
  "paired": true,
  "account": "you@example.com",
  "doorbell": true,
  "config_path": "/home/you/.webrender-worker/config.json"
}
```

`doorbell` reports whether the wake-free transport is configured. When it is, the daemon
sleeps between jobs instead of polling on a timer — which is what lets both your box and
the cloud idle. A build old enough to lack it falls back to a 5-second HTTP poll, so
**keep the daemon reasonably current** (`pip install -U webrender-worker`, or re-pull the
image). The daemon reports its version to the cloud on every call so that a future release
can tell you when an update matters.

## Resilience

- **Sleep-wake / dynamic IP / NAT:** the loop only dials out, so it resumes cleanly after
  the box wakes; network blips and cloud cold-starts are retried with backoff.
- **Crash mid-render:** the cloud reclaims a job whose heartbeat goes stale and re-queues
  it, so a job is never lost — the box just re-renders it on the next poll.
- **More than one box:** the protocol tolerates N workers on one account; claims are
  atomic, so whichever box polls first takes a given job.

## Licence

AGPL-3.0-or-later. It is a component of the AGPL WebRender system.
