Spaces:
Sleeping
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
playwithrealtime=falseso synthesis runs as fast as possible.
Deploy
Deploy the ASGI app:
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:
curl -sS https://<user>--corereader-backend-fastapi-app.modal.run/health
Protocol note:
- WS
chapter_infoincludessentence_total(best-effort) so the client can render accurate download progress rings. - WS
sentenceevents includems_start(timeline),char_start/char_end(character offsets within the paragraph), andchunk_bytes/chunk_samples(size of the next sentence PCM chunk) so the client can highlight exactly what is being spoken. /novel_indexchapter 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.
- Create a resource group + registry:
- az group create -n corereader-rg -l westeurope
- az acr create -n -g corereader-rg --sku Basic
- Build + push to ACR:
- az acr build -r -t corereader-backend:v1 .
- 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"
- 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