File size: 3,762 Bytes
bbe8cec
 
91dd53b
 
 
bbe8cec
 
 
 
91dd53b
c37e1f4
91dd53b
c37e1f4
91dd53b
 
 
 
 
 
c37e1f4
91dd53b
c37e1f4
91dd53b
 
 
 
 
 
 
c37e1f4
91dd53b
c37e1f4
91dd53b
 
 
c37e1f4
91dd53b
c37e1f4
91dd53b
 
 
c37e1f4
 
 
91dd53b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
---
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