Files
reader/docker-compose.yml
T
cnb.dQC5J6ohAGA 0370cd98e8 feat: HTTP TTS 与双端兼容朗读,书架体验与自定义设置
朗读(Closes #13 的朗读部分)

新增服务端 HTTP TTS 代理与客户端第二、第三个朗读引擎,三者在同一个
SpeechEngine 接口下可切换:

- 服务端 GET /api/v1/tts 代理 TTS_URL 指向的合成服务(edge-tts / Piper
  等兼容形态),只代理不内置合成模型;GET /api/v1/tts/voices 回答
  「这个实例能用什么」,未配置时返回 http:false 而不是 404。
  按句合成、磁盘缓存、上游返回非 audio/* 时明确报错(一个指到网页的
  TTS_URL 会以 200 返回 HTML,塞进 audio 里既没声音也没事件)。
  /tts 是精确的查询串令牌白名单项之一:<audio src> 无法带 Authorization。
- 客户端 HttpTtsEngine 用 <audio> 而不是 Web Audio,事件驱动推进队列。
- 客户端 NativeTtsEngine 走 Android 外壳新增的 speech 接口(shell v3):
  WebView 自带的合成器与系统的是两个引擎,手机系统装好的中文语音经常
  不在 getVoices() 里,这是「朗读没有中文声音」最常见的原因。
- speechAvailability 决定 auto 用哪个引擎:native → system → http。
- 原 TtsEngine 抽出 loadQueue/onChunk/onState 为公开字段以结构化满足接口。

书架

- 排序:最近更新 / 最近入库 / 书名 / 作者,中文按数字大小排。
  排序规则抽到 shelf-order.ts,离线路径与服务端用同一条规则。
- 密度、作者显隐、封面进度条;搜索一键清空 + 回车即搜;下拉刷新。
- 服务端 BookDto 增加 addedAt,否则客户端离线排序无法复现服务端结果。

自定义设置

- 朗读引擎可选(引擎多于一个时才显示该行);音调行只对有声调的引擎出现。
- 书架设置独立成 sheet,与阅读面板分开。

测试与文档

- server: tts.test.ts(10 例)。web: tts-http / speech / shelf-order
  新增 39 例,合计 245 passing。
- android/scripts/check-shell.sh 修正为同时校验嵌套的 speech 接口,
  之前它只能看到一个顶层接口。
- README / docs/api.md / docs/architecture.md 补齐三个引擎的取舍、
  环境变量与错误码。

Ref: #13
2026-09-16 23:29:41 +08:00

64 lines
2.8 KiB
YAML

# One command to get running:
#
# docker compose up -d
#
# Then open http://<your-host>:8080 and register the first account, which
# automatically becomes the instance admin.
services:
reader:
image: cnb.cool/hectorqin/reader:latest
# To build from source instead of pulling the published image. The context is
# the repository root because the image bakes in the H5 client built from
# ./web as well as the server.
# build:
# context: .
# dockerfile: server/Dockerfile
container_name: reader
restart: unless-stopped
ports:
- "8080:8080"
environment:
# Polling intervals in seconds. Set to 0 to disable (a manual scan is
# still available from the admin API).
SCAN_INTERVAL: "1800"
WATCH_INTERVAL: "60"
LOG_LEVEL: "info"
# Set this to the address you actually reach the server on, so download
# links work behind a reverse proxy.
# PUBLIC_URL: "https://reader.example.com"
# Uncomment to allow family members to self-register instead of the admin
# creating their accounts:
# ALLOW_REGISTRATION: "true"
# Origins allowed to call the API from a browser, comma separated.
# Empty (the default) reflects whatever origin asks. Fine for a LAN: the
# client does not know the address it will be reached on, and there are no
# cookies in the design. Set it if the instance is on the public internet:
# CORS_ORIGINS: "https://reader.example.com"
# Directory holding the built H5 client, served at `/`. Already set in the
# image; only override if you build the client separately.
# WEB_DIR: "/app/web"
# --- HTTP speech, for clients that have no engine of their own ---
# The server only *proxies*; it does not bundle a synthesizer, because
# that would multiply the image size and a NAS's CPU budget. Point this at
# any service that accepts the classic `?text=...&speed=...` shape
# (edge-tts, a local Piper wrapper, a commercial API with a shim).
#
# TTS_URL: "http://host.docker.internal:5002/tts"
# TTS_TOKEN: "" # bearer token, if the upstream wants one
# TTS_VOICES_URL: "" # defaults to <TTS_URL>/voices
# TTS_CACHE_BYTES: "268435456" # audio cache cap in /data; 0 disables
volumes:
# --- your books, mounted READ-ONLY ---
# The server NEVER writes to this directory. Replace the left-hand side
# with the path to your own library.
- /path/to/your/books:/books:ro
# --- everything the server writes lives here ---
# SQLite database, cover cache, scan journal.
- ./data:/data
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8080/api/v1/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
interval: 30s
timeout: 5s
retries: 3