--- 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://--corereader-backend-fastapi-app.modal.run In Settings β†’ WebSocket base URL, set: - wss://--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://--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 -g corereader-rg --sku Basic 2) Build + push to ACR: - az acr build -r -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 -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