CoreReader / README.md
shreyas-joshi's picture
Fix WS recv race + session recycle 20 with async overlap + Modal deploy
91dd53b
---
title: CoreReader
emoji: 📉
colorFrom: yellow
colorTo: yellow
sdk: docker
pinned: false
---
# CoreReader Backend (Modal)
This folder is a backend-only deployment target for CoreReader / LN-TTS on **Modal**.
It runs a FastAPI server that:
- Scrapes NovelCool chapters + chapter index
- Runs Kokoro ONNX TTS (**CPU-only**)
- Streams **sentence-atomic** PCM16 mono audio over WebSocket
- one binary WebSocket message per sentence (with a short trailing pause)
- sentence audio includes a tiny fade-in/out to avoid boundary clicks
## Endpoints
- GET /health
- GET /voices
- GET /novel_index?url=...
- GET /novel_details?url=... (best-effort cover URL)
- GET /novel_meta?url=...
- GET /novel_chapter?url=...&n=...
- WS /ws
## Use from the Flutter app
After deploying, Modal prints a base URL like:
- https://<user>--corereader-backend-fastapi-app.modal.run
In Settings → WebSocket base URL, set:
- wss://<user>--corereader-backend-fastapi-app.modal.run
The app connects to `/ws` automatically.
## Notes
- Models are downloaded on first cold start (if missing) and cached in a **Modal Volume**.
Subsequent cold starts reuse the cached model files.
- Offline downloads in the app use WS `play` with `realtime=false` so synthesis runs as fast as possible.
## Deploy
Deploy the ASGI app:
```bash
modal deploy modal_app.py
```
This deployment is configured for **3 CPU cores** and **4 GiB RAM**.
### One-time warmup (optional)
To avoid paying download time on a user’s first session, run a one-time warmup after deploy:
```bash
curl -sS https://<user>--corereader-backend-fastapi-app.modal.run/health
```
Protocol note:
- WS `chapter_info` includes `sentence_total` (best-effort) so the client can render accurate download progress rings.
- WS `sentence` events include `ms_start` (timeline), `char_start`/`char_end` (character offsets within the paragraph), and `chunk_bytes`/`chunk_samples` (size of the next sentence PCM chunk) so the client can highlight exactly what is being spoken.
- `/novel_index` chapter numbers are parsed from title or URL (more robust for Chapter 1 / Prologue edge-cases).
## Speed architecture
The `speed` field in the WS `play` command is the **TTS render speed** — it controls how fast Kokoro speaks. It is **baked into the synthesised PCM** at synthesis time.
A separate **Playback Speed** (reader fast-forward, like YouTube 1.5×) is applied by the Flutter client via SoLoud's `setRelativePlaySpeed()`. The backend does not know about it. Highlight sync stays accurate at any playback speed because the client schedules sentence highlights at exact PCM sample positions and triggers them when `SoLoud.getStreamTimeConsumed()` reaches that sample — no additional correction needed.
## Deploy to Azure (Container Apps)
This Docker image can be deployed to Azure Container Apps.
1) Create a resource group + registry:
- az group create -n corereader-rg -l westeurope
- az acr create -n <acrName> -g corereader-rg --sku Basic
2) Build + push to ACR:
- az acr build -r <acrName> -t corereader-backend:v1 .
3) Deploy a public Container App (binds to PORT, default 7860):
- az extension add --name containerapp --upgrade
- az containerapp env create -g corereader-rg -n corereader-env -l westeurope
- loginServer=$(az acr show -n <acrName> -g corereader-rg --query loginServer -o tsv)
- az containerapp create -g corereader-rg -n corereader-backend --environment corereader-env \
--image "$loginServer/corereader-backend:v1" \
--ingress external --target-port 7860 --registry-server "$loginServer"
4) Get the URL:
- fqdn=$(az containerapp show -g corereader-rg -n corereader-backend --query properties.configuration.ingress.fqdn -o tsv)
Paste into the Flutter app Settings:
- wss://$fqdn