sequoia00 1 hónapja
szülő
commit
017675f9ee

+ 2 - 1
config.py

@@ -8,5 +8,6 @@ MYSQL_PASSWORD = os.getenv("READER_PRO_DB_PASSWORD", "792199Zhao*")
 MYSQL_DATABASE = os.getenv("READER_PRO_DB_NAME", "reader_pro")
 MYSQL_DATABASE = os.getenv("READER_PRO_DB_NAME", "reader_pro")
 
 
 # TTS upstream service configuration
 # TTS upstream service configuration
-TTS_API_BASE_URL = os.getenv("READER_PRO_TTS_API_BASE_URL", "http://141.140.15.30:8028")
+# "http://141.140.15.30:8028"
+TTS_API_BASE_URL = os.getenv("READER_PRO_TTS_API_BASE_URL", "http://127.0.0.1:8028")
 TTS_GENERATE_ENDPOINT = os.getenv("READER_PRO_TTS_GENERATE_ENDPOINT", "/generate")
 TTS_GENERATE_ENDPOINT = os.getenv("READER_PRO_TTS_GENERATE_ENDPOINT", "/generate")

+ 222 - 0
docs/产品侧-页面文案初稿.md

@@ -0,0 +1,222 @@
+# 产品侧页面文案初稿
+
+## 目标
+
+本文档提供首页、套餐页、FAQ、新手引导等页面的首版文案初稿,目标是先形成可上线版本,再逐步优化表达。
+
+## 1. 首页落地页文案
+
+## 首屏标题
+
+### 版本一
+
+让英文 PDF 开始“开口说话”
+
+### 版本二
+
+点哪句读哪句的英文 PDF 听读工具
+
+### 版本三
+
+面向精读场景的 PDF 听读阅读器
+
+## 首屏副标题
+
+支持指定位置起播、逐句朗读和高亮联动,适合英语学习、论文阅读和电子书精读。基于本地 TTS,普通 CPU 服务器即可部署。
+
+## 首屏按钮
+
+- 主按钮:立即体验
+- 次按钮:观看演示
+- 辅助按钮:查看开源版
+
+## 核心卖点文案
+
+### 卖点 1
+
+点哪句读哪句
+
+不需要从头拖动播放,点击当前句子即可开始听读,适合精读和反复跟读。
+
+### 卖点 2
+
+逐句高亮联动
+
+播放过程中自动跟随高亮,减少跳读和走神,更适合论文、教材和电子书阅读。
+
+### 卖点 3
+
+起播快,体验轻
+
+本地 TTS + 缓存策略,尽量缩短从点击到出声的等待时间。
+
+### 卖点 4
+
+普通 CPU 也能部署
+
+基于 ONNX 模型,无需高成本 GPU,更适合个人开发者、小团队和私有部署。
+
+## 适用人群文案
+
+适合谁使用:
+
+- 英语学习者
+- 需要精读论文的人
+- 喜欢边听边读电子书的人
+- 教师、培训机构和学习社群
+
+## 使用流程文案
+
+只需三步:
+
+1. 上传你的 PDF
+2. 点击页面中的英文句子
+3. 跟随语音和高亮开始阅读
+
+## 行动区文案
+
+现在就开始试用,体验更顺畅的英文 PDF 听读方式。
+
+按钮:
+
+- 立即注册
+- 进入演示
+- 咨询私有部署
+
+## 2. 套餐页文案
+
+## 标题
+
+选择适合你的使用方式
+
+## 副标题
+
+从个人试用到长期精读,再到团队和私有部署,都可以按需求选择。
+
+## 免费版
+
+适合先体验核心能力。
+
+- 基础 PDF 听读功能
+- 指定位置起播
+- 逐句高亮联动
+- 每日基础使用额度
+- 基础音色
+
+按钮:
+
+- 免费开始
+
+## Pro 版
+
+适合高频使用者和长期英文阅读用户。
+
+- 更高生成额度
+- 更长文本支持
+- 更多语音参数
+- 更快处理体验
+- 优先支持
+
+按钮:
+
+- 升级 Pro
+
+## 私有部署版
+
+适合老师、机构和有独立部署需求的团队。
+
+- 独立部署
+- 数据独立
+- 可接自有服务器
+- 可做品牌定制
+- 可提供部署和维护支持
+
+按钮:
+
+- 联系咨询
+
+## 套餐页底部说明
+
+如果你希望自建或部署到自己的服务器,也可以直接使用开源核心版。
+
+## 3. FAQ 文案初稿
+
+### 这个工具适合什么场景?
+
+适合英文 PDF 的精读、跟读、论文阅读、教材阅读和电子书听读。
+
+### 和普通 PDF 阅读器有什么不同?
+
+它不是只负责展示 PDF,而是把“点击句子开始朗读、逐句跟随高亮、听读联动”作为核心体验。
+
+### 免费版可以用到什么程度?
+
+免费版可以体验核心阅读和听读能力,但会有每日额度和部分功能限制。
+
+### 为什么有时生成会稍微等待?
+
+当文本较长、当前请求较多,或缓存未命中时,生成会有少量等待。后续会继续优化缓存和队列体验。
+
+### 文件会被保存吗?
+
+系统会为保证阅读进度、缓存和服务稳定性保留必要数据。具体规则以隐私政策为准。
+
+### 支持私有部署吗?
+
+支持。你可以使用开源版本自部署,也可以联系我们提供私有部署支持。
+
+## 4. 新手引导文案
+
+## 首次欢迎弹层
+
+欢迎使用英文 PDF 听读工具
+
+你可以:
+
+1. 上传 PDF
+2. 点击任意英文句子开始播放
+3. 跟随高亮进行听读
+
+按钮:
+
+- 开始体验
+
+## 阅读区提示
+
+点击句子即可开始朗读
+
+## 控制栏提示
+
+你可以在这里调节语速、播放方式和阅读模式
+
+## 空状态提示
+
+还没有上传文件。上传一个 PDF,开始你的第一次听读体验。
+
+## 5. 隐私政策页面开头文案
+
+我们重视你的文件和使用数据安全。为了提供阅读、听读、进度保存和系统优化能力,我们会收集运行服务所必需的最少信息,并尽量透明说明数据的保存和使用方式。
+
+## 6. 服务条款页面开头文案
+
+使用本服务前,请先阅读本条款。你在注册、登录或继续使用本服务时,即表示你同意按本条款使用 PDF 阅读和听读相关能力,并遵守相应的使用边界。
+
+## 7. 联系与转化文案
+
+### 私有部署咨询
+
+如果你希望部署到自己的服务器、机构环境或内部网络,可以联系我们获取私有部署方案。
+
+### 开源版入口
+
+想自己部署?可以先从开源核心版开始。
+
+## 总结
+
+首版文案要优先做到三点:
+
+- 说人话
+- 讲清场景
+- 让用户知道下一步做什么
+
+对你这个项目来说,最重要的不是堆很多营销词,而是让用户立刻明白“这能帮我更顺畅地读英文 PDF”。

+ 232 - 0
docs/产品侧-页面结构草图.md

@@ -0,0 +1,232 @@
+# 产品侧页面结构草图
+
+## 目标
+
+本文档用于定义首版页面信息结构,帮助后续落地首页、套餐页、FAQ 和引导页。
+
+以下为文字版草图,重点在结构和模块,不是视觉稿。
+
+## 1. 首页落地页结构
+
+## 页面结构
+
+1. 顶部导航
+2. 首屏 Hero
+3. 核心卖点区
+4. 使用场景区
+5. 三步使用流程
+6. 演示视频区
+7. 方案选择区
+8. FAQ 简版
+9. 页脚
+
+## 1.1 顶部导航
+
+建议内容:
+
+- Logo / 产品名
+- 功能介绍
+- 套餐
+- FAQ
+- GitHub
+- 登录
+- 立即体验
+
+## 1.2 首屏 Hero
+
+左侧:
+
+- 标题
+- 副标题
+- 主按钮
+- 次按钮
+
+右侧:
+
+- 产品截图或播放动图
+
+## 1.3 核心卖点区
+
+建议 4 个卡片:
+
+- 点句即读
+- 逐句高亮
+- 起播快
+- 普通 CPU 可部署
+
+## 1.4 使用场景区
+
+建议 4 个场景卡片:
+
+- 英语学习
+- 论文阅读
+- 教材精读
+- 机构部署
+
+## 1.5 三步使用流程
+
+横向三列或竖向三步:
+
+1. 上传 PDF
+2. 点击句子
+3. 跟随听读
+
+## 1.6 演示视频区
+
+- 视频缩略图
+- 播放按钮
+- 一句辅助说明
+
+## 1.7 方案选择区
+
+三列:
+
+- 免费版
+- Pro
+- 私有部署
+
+每列包含:
+
+- 适合谁
+- 核心权益
+- 按钮
+
+## 1.8 FAQ 简版
+
+展示 4-6 条最常见问题。
+
+## 1.9 页脚
+
+建议包含:
+
+- 产品简介
+- 开源版入口
+- 隐私政策
+- 服务条款
+- 联系方式
+
+## 2. 套餐页结构
+
+## 页面结构
+
+1. 页面标题
+2. 套餐卡片
+3. 功能对比表
+4. 私有部署说明
+5. 常见问题
+6. 联系入口
+
+## 2.1 套餐卡片
+
+三张卡片:
+
+- 免费版
+- Pro 版
+- 私有部署版
+
+每张卡片包括:
+
+- 名称
+- 简介
+- 核心权益
+- 价格
+- 按钮
+
+## 2.2 功能对比表
+
+建议对比:
+
+- 每日额度
+- 每月额度
+- 单次文本长度
+- 音色数量
+- 并发任务数
+- 优先支持
+- 私有部署支持
+
+## 3. FAQ 页面结构
+
+## 页面结构
+
+1. 页面标题
+2. 分类导航
+3. 问题列表
+4. 联系支持
+
+## 分类建议
+
+- 产品能力
+- 使用限制
+- 文件与隐私
+- 付费与部署
+
+## 4. 新手引导结构
+
+## 首次引导弹层
+
+内容:
+
+- 欢迎标题
+- 三步说明
+- 开始体验按钮
+
+## 阅读器内引导
+
+建议在以下位置做轻提示:
+
+- 文件上传区域
+- 句子区域
+- 控制栏
+
+## 5. 隐私政策页结构
+
+## 页面结构
+
+1. 标题
+2. 生效日期
+3. 我们收集什么
+4. 为什么收集
+5. 如何存储
+6. 是否共享
+7. 用户权利
+8. 联系方式
+
+## 6. 服务条款页结构
+
+## 页面结构
+
+1. 标题
+2. 生效日期
+3. 服务内容
+4. 账号责任
+5. 禁止行为
+6. 付费说明
+7. 服务中断与变更
+8. 责任限制
+9. 联系方式
+
+## 7. 页面跳转关系
+
+建议关系:
+
+- 首页 -> 注册/登录
+- 首页 -> 演示视频
+- 首页 -> 套餐页
+- 首页 -> FAQ
+- 首页 -> GitHub
+- 套餐页 -> 注册/升级/咨询
+- FAQ -> 联系方式
+- 页脚 -> 隐私政策/服务条款
+
+## 8. 首版实现建议
+
+首版不需要复杂交互,重点是:
+
+- 结构清晰
+- 价值表达明确
+- 有明确 CTA
+- 能支撑注册、试用、咨询
+
+## 总结
+
+页面结构草图的重点不是好看,而是先把信息流和转化路径理顺。对你这个项目来说,首页和套餐页是核心,FAQ、隐私政策、服务条款是支撑页面,新手引导则直接影响首次体验和留存。

+ 173 - 0
docs/产品侧-首版上线清单.md

@@ -0,0 +1,173 @@
+# 产品侧首版上线清单
+
+## 目标
+
+本文档把产品侧方案拆成首版上线清单,适合直接排期执行。
+
+## 第一阶段:先让用户看懂
+
+### 1. 首页落地页
+
+- [ ] 确定产品名称
+- [ ] 确定首页主标题和副标题
+- [ ] 准备核心截图 1-2 张
+- [ ] 完成首页首屏
+- [ ] 完成卖点模块
+- [ ] 完成使用流程模块
+- [ ] 完成 CTA 按钮
+
+### 2. 演示视频
+
+- [ ] 录制真实使用流程
+- [ ] 裁剪到 `30-90 秒`
+- [ ] 添加简短字幕
+- [ ] 输出为可嵌入首页的视频
+
+### 3. 新手引导
+
+- [ ] 设计首次欢迎弹层
+- [ ] 增加阅读区轻提示
+- [ ] 增加控制栏轻提示
+- [ ] 增加文件空状态说明
+- [ ] 使用 `localStorage` 记录是否已展示
+
+## 第一阶段验收标准
+
+- [ ] 首次访问用户能理解产品做什么
+- [ ] 用户能看到真实演示
+- [ ] 新用户能完成第一次播放
+
+## 第二阶段:让用户愿意注册和咨询
+
+### 4. 套餐页
+
+- [ ] 确定免费版功能边界
+- [ ] 确定 Pro 版权益
+- [ ] 确定私有部署说明
+- [ ] 完成套餐对比表
+- [ ] 增加注册/升级/咨询入口
+
+### 5. FAQ
+
+- [ ] 梳理 8-12 个高频问题
+- [ ] 按分类组织 FAQ
+- [ ] 完成 FAQ 页面
+- [ ] 首页增加 FAQ 简版入口
+
+### 6. 联系与转化入口
+
+- [ ] 增加“咨询私有部署”按钮
+- [ ] 增加联系邮箱或表单
+- [ ] 增加 GitHub 开源入口
+
+## 第二阶段验收标准
+
+- [ ] 用户能理解免费和付费差异
+- [ ] 用户知道如何联系你
+- [ ] FAQ 能覆盖主要疑问
+
+## 第三阶段:让产品可以正式收费
+
+### 7. 隐私政策
+
+- [ ] 完成首版隐私政策
+- [ ] 页脚增加链接
+- [ ] 注册页可访问
+
+### 8. 服务条款
+
+- [ ] 完成首版服务条款
+- [ ] 页脚增加链接
+- [ ] 注册页或支付页可访问
+
+### 9. 支付前准备
+
+- [ ] 明确退款规则
+- [ ] 明确套餐周期
+- [ ] 明确是否自动续费
+- [ ] 明确私有部署咨询流程
+
+## 第三阶段验收标准
+
+- [ ] 页面具备基础法律说明
+- [ ] 套餐信息和服务边界清晰
+- [ ] 可进入收费准备阶段
+
+## 推荐工期
+
+### 第 1 周
+
+- 首页文案
+- 首页结构
+- 截图准备
+
+### 第 2 周
+
+- 首页开发
+- 演示视频录制
+- 新手引导实现
+
+### 第 3 周
+
+- 套餐页
+- FAQ
+- 联系入口
+
+### 第 4 周
+
+- 隐私政策
+- 服务条款
+- 全站检查和上线准备
+
+## 资源准备清单
+
+### 素材
+
+- [ ] 首页主视觉截图
+- [ ] 阅读器截图
+- [ ] 夜间模式截图
+- [ ] 播放高亮截图
+- [ ] 视频录屏文件
+
+### 文案
+
+- [ ] 首页标题副标题
+- [ ] 卖点描述
+- [ ] 套餐文案
+- [ ] FAQ 文案
+- [ ] 政策文案
+
+### 链接与入口
+
+- [ ] 注册入口
+- [ ] 演示入口
+- [ ] GitHub 链接
+- [ ] 联系方式
+
+## 风险提示
+
+### 首版最容易出的问题
+
+- 首页讲不清楚真正卖点
+- 套餐页写了功能,但没有写升级理由
+- FAQ 太空泛,没回答真实问题
+- 没有清晰联系入口,导致私有部署线索流失
+
+## 上线前检查
+
+- [ ] 首页按钮全部可点击
+- [ ] 视频可正常播放
+- [ ] 套餐页文案与实际功能一致
+- [ ] FAQ 与当前产品能力一致
+- [ ] 隐私政策和服务条款链接可访问
+- [ ] 手机端可正常浏览
+
+## 总结
+
+产品侧首版上线不是做完整营销站,而是要完成三件事:
+
+- 让用户快速理解产品
+- 让用户知道如何开始
+- 让用户知道如何升级或咨询
+
+只要这三件事做到了,你的项目就已经从“能用”进入“可转化”的阶段。

+ 432 - 0
docs/产品侧落地方案.md

@@ -0,0 +1,432 @@
+# 产品侧落地方案
+
+## 目标
+
+本文档用于规划当前项目在产品侧需要补齐的公开展示和收费转化能力,重点覆盖:
+
+- 首页落地页
+- 演示视频
+- 新手引导
+- 套餐页
+- FAQ
+- 隐私政策
+- 服务条款
+
+这些内容的目的不是“显得完整”,而是为了让用户能理解价值、放心试用、顺利转化。
+
+## 1. 首页落地页
+
+### 目标
+
+让首次访问用户在 `5-10 秒` 内明白:
+
+- 这是做什么的
+- 适合谁
+- 和普通 PDF 阅读器有什么不同
+- 为什么值得试用
+
+### 首页必须回答的 4 个问题
+
+1. 这是一个什么产品
+2. 能解决什么问题
+3. 为什么你的方案更好
+4. 用户下一步该做什么
+
+### 推荐页面结构
+
+#### 首屏
+
+- 产品名称
+- 一句话价值描述
+- 主按钮:立即体验
+- 次按钮:查看演示
+- 一张核心界面截图或动图
+
+一句话描述建议:
+
+一个面向英文阅读场景的 PDF 听读工具,支持点句即读、逐句高亮和低延迟起播。
+
+#### 第二屏:核心能力
+
+- 指定位置起播
+- 逐句阅读
+- 高亮联动
+- 日夜间模式
+- 普通 CPU 可部署
+
+#### 第三屏:适合谁
+
+- 英语学习者
+- 论文阅读者
+- 电子书精读用户
+- 教师和培训机构
+
+#### 第四屏:使用流程
+
+- 上传 PDF
+- 点击段落或句子
+- 开始听读并跟随高亮
+
+#### 第五屏:部署与方案
+
+- 在线使用
+- 自部署
+- 私有部署
+
+#### 第六屏:行动入口
+
+- 立即试用
+- 查看 GitHub
+- 联系私有部署
+
+### 技术实现建议
+
+如果沿用当前项目技术栈,建议:
+
+- 在 `static/web/` 中新增独立首页
+- 保持与阅读器 UI 风格一致,但更强调营销表达
+- 支持桌面和移动端
+
+### 第一阶段最低可用版本
+
+- 首屏价值说明
+- 3-5 个卖点卡片
+- 一张产品截图
+- 演示视频入口
+- 注册或体验按钮
+
+## 2. 演示视频
+
+### 目标
+
+让用户快速理解产品的核心使用体验,缩短理解时间。
+
+### 为什么必须做
+
+这个项目的卖点主要是交互体验,而不是一句话能解释完的功能。用户看到“点哪句读哪句、边读边高亮”后,转化率会比只看文字高很多。
+
+### 推荐视频结构
+
+控制在 `30-90 秒`。
+
+建议脚本:
+
+1. 打开 PDF
+2. 点击某句英文
+3. 立即播放
+4. 高亮跟随
+5. 切换日夜间模式
+6. 展示适合论文/电子书/英语精读
+7. 最后给出试用入口
+
+### 实现方式
+
+- 使用录屏软件录制真实使用过程
+- 后期加简单字幕
+- 不需要复杂剪辑
+
+### 输出渠道
+
+- 首页嵌入
+- GitHub README
+- B 站
+- 小红书
+- 社群传播
+
+## 3. 新手引导
+
+### 目标
+
+让第一次进入系统的用户知道怎么开始,减少流失。
+
+### 当前常见新手障碍
+
+- 不知道先上传还是先打开
+- 不知道哪里可以点击播放
+- 不知道为何有时生成需要等待
+- 不知道播放控制怎么用
+
+### 推荐引导结构
+
+#### 首次进入弹层
+
+展示三步:
+
+1. 上传 PDF
+2. 点击英文句子开始播放
+3. 跟随高亮进行阅读
+
+#### 首次操作提示
+
+- 在阅读器中高亮提示“点击句子即可朗读”
+- 在控制栏提示“可调节语速与模式”
+
+#### 空状态提示
+
+- 没有文件时展示上传入口
+- 没有播放时提示如何启动
+
+### 技术实现建议
+
+- 使用 `localStorage` 记录是否已看过引导
+- 首次登录或首次进入阅读器时显示
+- 引导不要过长,控制在 `3-4` 步内
+
+### 第一阶段最低可用版本
+
+- 首次引导弹层
+- 阅读区指示提示
+- 上传页空状态说明
+
+## 4. 套餐页
+
+### 目标
+
+把免费和付费边界讲清楚,并让用户知道为什么值得升级。
+
+### 套餐页必须讲清楚的内容
+
+1. 免费用户能做什么
+2. 付费用户多了什么
+3. 升级的理由是什么
+4. 还有没有私有部署方案
+
+### 推荐套餐结构
+
+#### 免费版
+
+- 基础听读体验
+- 每日使用限制
+- 基础音色
+- 单文件大小限制
+
+#### Pro 版
+
+- 更高配额
+- 更多音色
+- 更长文本
+- 更快处理
+- 优先支持
+
+#### 私有部署版
+
+- 独立部署
+- 专属服务
+- 数据独立
+- 可定制
+
+### 页面建议模块
+
+- 套餐对比表
+- 常见问题
+- 联系入口
+- 试用或升级按钮
+
+### 第一阶段最低可用版本
+
+- 免费版和 Pro 版对比
+- 私有部署说明
+- 联系方式或咨询按钮
+
+## 5. FAQ
+
+### 目标
+
+提前回答用户最常见的问题,降低客服成本,提升信任。
+
+### FAQ 推荐覆盖的问题
+
+#### 产品能力
+
+- 支持哪些 PDF
+- 是否支持中文或其他语言
+- 是否支持整页朗读
+- 是否支持上传个人文件
+
+#### 使用限制
+
+- 免费版有什么限制
+- 为什么有时需要等待
+- 为什么某些文本不能播放
+
+#### 部署与隐私
+
+- 文件是否会被保存
+- 是否支持本地部署
+- 是否能私有化部署
+
+#### 计费问题
+
+- 如何升级套餐
+- 是否支持退款
+- 私有部署如何报价
+
+### 呈现方式
+
+- 首页简版 FAQ
+- 独立 FAQ 页面完整版
+
+### 第一阶段最低可用版本
+
+- 8-12 条高频问题
+
+## 6. 隐私政策
+
+### 目标
+
+说明系统如何处理用户数据、文件和日志,是公开上线和收费转化的基础文件。
+
+### 为什么必须有
+
+只要涉及:
+
+- 用户注册
+- 文件上传
+- 阅读记录
+- 日志采集
+
+就应该有隐私政策。否则不利于用户信任,也不利于后续收费。
+
+### 应包含的核心内容
+
+1. 收集哪些信息
+- 账号信息
+- 上传文件
+- 阅读进度
+- 使用日志
+
+2. 收集目的
+- 提供服务
+- 保存阅读进度
+- 优化性能和排错
+
+3. 数据存储方式
+- 是否本地保存
+- 保存时长
+- 是否会进入缓存
+
+4. 数据共享说明
+- 是否会共享给第三方
+- TTS 是否走本地服务还是外部接口
+
+5. 用户权利
+- 删除账号
+- 删除文件
+- 联系管理员
+
+### 落地建议
+
+- 先写一个基础版本
+- 页脚固定链接
+- 注册页和套餐页可跳转
+
+## 7. 服务条款
+
+### 目标
+
+约定用户如何使用服务、哪些行为被禁止、出现问题时双方责任如何划分。
+
+### 应包含的核心内容
+
+1. 服务范围
+- 提供 PDF 阅读和 TTS 听读服务
+
+2. 账号责任
+- 用户对账号安全负责
+
+3. 禁止行为
+- 滥用生成能力
+- 上传违法内容
+- 攻击或绕过限制
+
+4. 服务可用性说明
+- 服务可能升级、中断、维护
+
+5. 付费说明
+- 订阅周期
+- 是否自动续费
+- 退款政策
+
+6. 责任限制
+- 对间接损失免责
+- 对测试功能不承诺稳定性
+
+### 落地建议
+
+- 先有基础版本
+- 与隐私政策分开
+- 注册和支付页可访问
+
+## 产品侧页面落地顺序
+
+建议优先级如下:
+
+1. 首页落地页
+2. 演示视频
+3. 新手引导
+4. FAQ
+5. 套餐页
+6. 隐私政策
+7. 服务条款
+
+这个顺序的理由是:
+
+- 前三项决定用户能不能理解产品
+- FAQ 和套餐页决定能不能顺利转化
+- 隐私政策和服务条款决定能不能正式上线和收费
+
+## 每项的最低可用标准
+
+### 首页落地页
+
+- 能说清产品价值
+- 有试用入口
+
+### 演示视频
+
+- 能展示核心交互
+- 长度不超过 `90 秒`
+
+### 新手引导
+
+- 新用户能完成首次播放
+
+### 套餐页
+
+- 能清楚区分免费和付费
+
+### FAQ
+
+- 能覆盖常见疑问
+
+### 隐私政策
+
+- 能说明数据收集和使用方式
+
+### 服务条款
+
+- 能约束使用边界和责任范围
+
+## 建议里程碑
+
+### 第一阶段:可展示
+
+- 首页上线
+- 演示视频完成
+- 新手引导完成
+
+### 第二阶段:可转化
+
+- FAQ 完成
+- 套餐页完成
+- 咨询入口完成
+
+### 第三阶段:可收费
+
+- 隐私政策上线
+- 服务条款上线
+- 注册与支付流程补齐
+
+## 总结
+
+产品侧这些内容不是装饰,而是“理解成本、信任成本、转化成本”的解决方案。对于你这个项目,首页、演示视频和新手引导最重要,因为你的价值主要靠实际交互体验体现;而套餐页、FAQ、隐私政策、服务条款则决定你能不能从“有人觉得不错”走到“有人愿意长期使用并付费”。

+ 412 - 0
docs/付费服务商业计划.md

@@ -0,0 +1,412 @@
+# 付费服务商业计划
+
+## 目标
+
+这个计划的目标,是把当前项目从“可运行工具”推进到“可收费产品”,并评估它在资源投入、定价和盈利上的可行性。
+
+当前更适合你的方向不是大规模通用型 SaaS,而是一个面向英文阅读场景的垂直工具服务。
+
+## 产品定位
+
+推荐定位:
+
+- 英文 PDF 听读工具
+- 面向精读、跟读、论文阅读、电子书阅读的效率工具
+- 支持指定位置起播、逐句朗读、高亮联动
+- 普通 CPU 可部署,适合低成本托管和私有部署
+
+不建议定位成:
+
+- 通用 PDF 平台
+- 泛 TTS 网站
+- 面向所有场景的内容平台
+
+因为你的真实优势在“英文阅读体验 + 低部署成本”,而不在“大而全”。
+
+## 付费模式设计
+
+推荐采用三层结构。
+
+## 方案一:个人会员
+
+适合普通用户,核心卖点是:
+
+- 免部署
+- 打开即用
+- 更高配额
+- 更稳定体验
+- 更多语音参数
+
+### 套餐建议
+
+#### 免费版
+
+- 每日朗读时长限制
+- 基础音色
+- 文件大小限制
+- 并发和速度受限
+
+#### Pro 月付
+
+- 更高每日或每月配额
+- 更快生成
+- 更多音色
+- 更长文本支持
+
+建议价格:
+
+- `19-39 元/月`
+
+#### Pro 年付
+
+建议价格:
+
+- `168-299 元/年`
+
+### 适用前提
+
+只有当你已经有一定自然流量和稳定留存时,个人会员才会比较健康。否则单纯依靠低价月付,获客难度会比较大。
+
+## 方案二:私有部署版
+
+这是更适合你早期变现的方式。
+
+适合客户:
+
+- 英语老师
+- 小型培训机构
+- 教育工作室
+- 有内网或私有化需求的小团队
+
+### 可售卖内容
+
+- 私有部署安装
+- 定制品牌名称或 Logo
+- 独立账号体系
+- 配置专属语音和服务器
+- 升级和维护服务
+
+### 定价建议
+
+- 基础部署版:`1999-3999 元/年`
+- 含维护支持版:`3999-9999 元/年`
+- 如果包含代部署、迁移、定制开发,可额外收费
+
+### 优点
+
+- 客单价高
+- 对流量依赖小
+- 更容易形成现金流
+- 更适合当前项目阶段
+
+## 方案三:团队或机构版
+
+适合中小机构和组织客户。
+
+### 可提供能力
+
+- 多账号管理
+- 团队资料库
+- 统一文档管理
+- 管理后台
+- 配额统计
+- 使用日志
+
+### 定价建议
+
+- `299-999 元/月/组织`
+- 或按账号数阶梯计费
+
+## 资源需求评估
+
+你当前的判断是:
+
+- `4 核 4G` 可部署
+- 支持 `2-4` 并发用户
+
+从架构和典型 CPU TTS 场景看,这个判断基本合理。但商业规划时必须区分三种指标:
+
+1. 注册用户数
+2. 同时在线用户数
+3. 同时触发 TTS 的活跃并发数
+
+第三项才是服务器成本的关键。
+
+## 推荐资源方案
+
+### 阶段一:种子用户期
+
+适用场景:
+
+- 自己运营
+- 少量真实用户
+- 小规模付费验证
+
+推荐配置:
+
+- `4 vCPU / 4 GB RAM`
+- `50-100 GB SSD`
+
+建议容量:
+
+- `100-300` 注册用户
+- `2-4` 活跃 TTS 并发
+
+适合用途:
+
+- 演示站
+- 小规模商用
+- 早期会员验证
+
+### 阶段二:正式小规模运营
+
+推荐配置:
+
+- `8 vCPU / 8-16 GB RAM`
+- `100-200 GB SSD`
+
+建议容量:
+
+- `500-2000` 注册用户
+- `5-12` 活跃 TTS 并发
+
+建议补充:
+
+- `Nginx`
+- FastAPI 多 worker
+- Redis
+- 异步任务队列
+- 更强的缓存复用策略
+
+### 阶段三:中等规模运营
+
+推荐结构:
+
+- `2 台 8vCPU/16GB` 应用或 TTS 节点
+- `1 台` 数据库/缓存节点
+- 负载均衡
+
+建议容量:
+
+- `20-50` 活跃 TTS 并发
+
+## 成本结构分析
+
+你的主要成本不是前端和普通 Web 接口,而是 TTS 相关资源消耗。
+
+主要成本项包括:
+
+- CPU 推理时间
+- 音频缓存占用
+- 文件存储
+- 下载与流式播放带宽
+- 运维和监控
+
+### 成本受哪些因素影响
+
+1. 用户是点句播放还是整页生成
+2. 缓存命中率是否足够高
+3. 是否允许重复文本重复生成
+4. 是否启用了长文本异步任务
+5. 是否做了配额和限流
+
+## 缓存对盈利能力的影响
+
+你当前项目已有 `audio_cache/`,这是非常重要的成本优化基础。
+
+建议继续强化:
+
+- 同文本、同 voice、同 speed 直接复用缓存
+- 句级缓存优先于整页缓存
+- 热门文档预热缓存
+- 长音频拆分生成
+- 过期缓存清理策略
+
+缓存做得好,会直接改善单位用户成本和并发承载能力。
+
+## 盈利测算
+
+下面给出几个现实模型。
+
+### 模型一:验证阶段
+
+- 付费用户 `30`
+- 客单价 `29 元/月`
+- 月收入 `870 元`
+
+这个阶段通常只能覆盖一部分服务器和运维成本,主要意义是验证产品是否有人愿意付费。
+
+### 模型二:小而稳
+
+- 付费用户 `100`
+- 客单价 `29 元/月`
+- 月收入 `2900 元`
+
+如果月服务器和基础开销控制在 `500-1500 元` 左右,项目可以形成正向毛利。
+
+### 模型三:组合收入
+
+- 在线会员 `50` 人,约 `1450 元/月`
+- 私有部署或组织客户按月摊销 `2000-5000 元`
+
+总收入可达:
+
+- `3450-6450 元/月`
+
+如果私有部署客户更多,盈利弹性会更明显。
+
+## 能否盈利的判断
+
+### 可以盈利,但条件是路线要对
+
+更容易盈利的路线:
+
+- 开源获取流量和信任
+- 托管版做轻订阅
+- 私有部署和机构授权做主要利润
+
+不太容易盈利的路线:
+
+- 从零开始做纯低价 C 端订阅
+- 没有配额控制就开放大量免费生成
+- 试图一开始就做大而全平台
+
+## 推荐商业路径
+
+### 第一阶段:验证需求
+
+目标:
+
+- 获得 `10-30` 个种子用户
+- 确认核心使用场景
+- 确认哪些功能最值得付费
+
+建议动作:
+
+- 开源核心版
+- 部署一个在线演示站
+- 收集社群和 GitHub 反馈
+
+### 第二阶段:开始收费
+
+目标:
+
+- 上线个人会员
+- 提供私有部署报价
+- 建立基础支付和支持流程
+
+建议动作:
+
+- 增加配额系统
+- 增加套餐页
+- 增加联系我们或咨询入口
+
+### 第三阶段:优化利润结构
+
+目标:
+
+- 降低单用户成本
+- 提高高客单价客户占比
+
+建议动作:
+
+- 强化缓存
+- 引入任务队列
+- 做团队版
+- 完善部署和升级流程
+
+## 风险与应对
+
+### 风险一:用户觉得替代品很多
+
+应对:
+
+- 强调“点句即读、逐句高亮、英文精读场景”
+- 不把自己包装成泛化 PDF 工具
+
+### 风险二:免费用户滥用资源
+
+应对:
+
+- 配额限制
+- 登录后使用
+- 限流
+- 长文本异步任务
+
+### 风险三:低价订阅收入不足
+
+应对:
+
+- 尽快布局私有部署和机构版
+- 不依赖单一收入来源
+
+### 风险四:技术和安全问题影响收费转化
+
+应对:
+
+- 先补安全和运维基础
+- 建立稳定性和可信度
+
+## 3 个月执行计划
+
+## 第 1 个月:产品化基础
+
+目标:
+
+- 让项目达到可公开展示和可试用状态
+
+任务:
+
+- 清理敏感信息
+- 补齐环境变量配置
+- 删除默认弱口令
+- 升级密码哈希
+- 完成 Docker 化
+- 补齐 README、演示图、FAQ
+- 增加基础限流和日志
+
+产出:
+
+- 开源可发布版本
+- 在线演示站
+
+## 第 2 个月:用户获取与反馈
+
+目标:
+
+- 获取首批真实用户
+
+任务:
+
+- 发布 GitHub 仓库
+- 在技术社区和英语学习社区传播
+- 收集用户反馈
+- 统计最常用功能和最痛点问题
+
+产出:
+
+- 首批种子用户
+- 需求优先级列表
+
+## 第 3 个月:初步收费验证
+
+目标:
+
+- 验证是否有人愿意付费
+
+任务:
+
+- 上线免费版和 Pro 版边界
+- 上线咨询入口
+- 提供私有部署报价
+- 对接基础支付
+- 跟进首批潜在付费用户
+
+产出:
+
+- 第一批付费用户
+- 第一批私有部署意向客户
+
+## 总结
+
+这个项目是有付费潜力的,但更适合“垂直场景工具 + 组合型收入”模式,而不是一开始就赌大规模低价订阅。只要你把安全、配额、缓存、部署标准化、产品包装这些基础环节补齐,盈利机会是存在的,而且比较适合个人开发者或小团队长期经营。

+ 262 - 0
docs/初步产品化.md

@@ -0,0 +1,262 @@
+# 初步产品化
+
+## 当前项目判断
+
+这个项目已经具备可推广的基础,不再只是一个演示型工具,而是一个有明确使用场景的产品雏形。当前技术路线大体成立:
+
+- 前端采用 `FastAPI + PDF.js`
+- 后端通过本地 TTS 上游调用 `Kororo ONNX` 模型
+- 已有用户、登录、阅读进度持久化
+- 已有音频缓存 `audio_cache/`
+- 已支持指定位置起播、逐句阅读、高亮联动、日夜间模式切换
+
+从产品定位上看,这个项目的核心价值不是“在线 PDF 阅读器”,而是“面向英文阅读与听读场景的低延迟跟读工具”。这类定位更容易在推广时讲清楚价值,也更容易形成差异化。
+
+建议对外统一强调这几个卖点:
+
+- 指定位置起播,点哪里读哪里
+- 逐句播放,适合精读和跟读
+- 播放时高亮联动,降低走神和跳读
+- 起播快,普通 CPU 服务器即可运行
+- 不依赖高成本 GPU,适合个人和小团队部署
+
+## 当前项目存在的主要问题
+
+在正式开源或收费之前,当前项目还有一些明显短板需要先处理,否则不适合公开推广。
+
+### 安全问题
+
+- [config.py](/home/service/reader_pro/config.py:1) 中存在默认数据库密码
+- 初始化流程中存在默认管理员账号 `admin/admin`
+- 用户密码目前为简单 `sha256`,不适合正式商用
+- Cookie 当前为 `secure=False`
+- CORS 配置为全开放
+
+### 工程和运营问题
+
+- 没有限流和配额控制
+- 暂时看不到任务队列和削峰机制
+- 仓库中保留了大量缓存音频文件,不适合直接开源
+- 缺少部署标准化文档和环境变量模板
+- 缺少隐私政策、服务条款、套餐说明、FAQ 等面向正式用户的基础材料
+
+## 开源方向建议
+
+如果你的目标是先建立影响力、吸引种子用户、验证真实需求,那么优先做免费开源是合理路线。
+
+### 为什么适合先开源
+
+- 这个产品有明确技术差异点,容易形成传播
+- 普通 CPU 即可部署,是很好的传播卖点
+- 英文听读和 PDF 精读场景足够垂直,容易触达目标人群
+- 开源可以帮助你快速收集真实需求,而不需要一开始就重投入运营
+
+### 更推荐的开源方式
+
+不建议“全部能力完全裸开源”,更适合采用“开源核心版,保留商业版能力”的方式。
+
+建议开源的内容:
+
+- 阅读器核心能力
+- PDF.js 联动、高亮、逐句播放逻辑
+- 单机部署版本
+- 本地 TTS 接入能力
+
+建议保留为商业能力的内容:
+
+- 支付体系
+- 配额和限流
+- 团队空间与组织管理
+- 商业后台
+- 用量统计
+- 托管服务能力
+- 私有部署支持服务
+
+### 开源协议建议
+
+如果你的目标是既传播又保留商业保护空间,建议优先考虑:
+
+- `AGPLv3 + 商业授权`
+
+如果你的目标更偏向快速传播、接受别人自由商用,可以考虑:
+
+- `MIT`
+
+从你当前的项目阶段来看,更建议使用 `AGPLv3`,这样更有利于后续商业化保护。
+
+## 付费服务方向建议
+
+如果后续要做付费服务,不建议一开始就走单一的低价大众订阅,而更适合三层模式:
+
+### 个人会员
+
+适合普通用户,核心卖点是免部署、即开即用、配额更高、语音更丰富。
+
+建议结构:
+
+- 免费版:每日时长限制、基础音色、文件大小限制
+- Pro 月付:更多时长、更快生成、更多音色
+- Pro 年付:折扣价格
+
+建议定价区间:
+
+- `19-39 元/月`
+- `168-299 元/年`
+
+### 私有部署版
+
+适合老师、小型机构、教育工作室、小团队。
+
+建议模式:
+
+- 年费授权
+- 一次性部署费 + 年度维护费
+- 支持客户使用自己的服务器和 TTS 服务
+
+建议定价区间:
+
+- `1999-9999 元/年`
+
+### 团队或机构版
+
+适合培训机构、学校、企业内部学习场景。
+
+建议能力:
+
+- 多账号管理
+- 统一资料库
+- 后台管理
+- 用量统计
+- 权限控制
+
+建议定价区间:
+
+- `299-999 元/月/组织`
+
+## 服务器资源与成本判断
+
+你提到当前后端使用 `Kororo ONNX` 模型,在普通 `4 核 4G` 以上服务器可部署,支持 `2-4` 并发用户。这个判断是合理的,但商业化时要区分:
+
+- 注册用户数
+- 同时在线用户数
+- 同时触发 TTS 生成的活跃并发数
+
+真实成本主要取决于第三项,也就是“同时有多少人在生成语音”。
+
+### 入门单机方案
+
+- `4 vCPU / 4 GB RAM`
+- 适合演示、小规模商用、种子用户阶段
+- 稳妥支持 `2-4` 个活跃 TTS 用户
+- 可支撑 `100-300` 注册用户规模
+
+### 小规模正式商用
+
+- `8 vCPU / 8-16 GB RAM`
+- 稳妥支持 `5-12` 个活跃 TTS 用户
+- 可支撑 `500-2000` 注册用户规模
+- 建议加入 `Nginx`、多 worker、Redis、任务队列、缓存优化
+
+### 中等规模
+
+- `2 台 8vCPU/16GB` 应用或 TTS 节点
+- `1 台` 数据库或缓存节点
+- 配合负载均衡
+- 稳妥支持 `20-50` 个活跃 TTS 用户
+
+## 能否盈利
+
+这个项目有盈利可能,但更适合“小而稳”的模式,而不是一开始就追求大规模 C 端低价订阅。
+
+### 不建议只押注低价订阅
+
+因为低价 C 端订阅通常会遇到这些问题:
+
+- 获客成本高
+- 留存不稳定
+- 用户容易把产品理解成“PDF + TTS 的简单拼装”
+- 需要持续内容营销和品牌建设
+
+### 更现实的盈利路线
+
+更建议的模式是:
+
+- 开源核心版做传播
+- 托管版做订阅
+- 私有部署和组织授权做高利润收入
+
+### 简单盈利模型
+
+#### 模型一:刚好接近盈亏平衡
+
+- 付费用户 `30` 人
+- 客单价 `29 元/月`
+- 月收入 `870 元`
+
+这个阶段通常只能接近覆盖基础服务器和运维成本。
+
+#### 模型二:小而稳
+
+- 付费用户 `100` 人
+- 客单价 `29 元/月`
+- 月收入 `2900 元`
+
+如果服务器和基础支出控制在合理范围内,这个阶段已经可以形成正向现金流。
+
+#### 模型三:更合理的组合型盈利
+
+- 在线会员 `50` 人,月收入约 `1450 元`
+- 私有部署客户或机构授权每月摊销 `2000-5000 元`
+
+这种组合比单纯依赖在线订阅要更稳,也更适合个人开发者或小团队。
+
+## 推荐的实际路径
+
+最适合你的路线不是直接做“大众通用 PDF 平台”,而是围绕“英文听读效率工具”持续迭代。
+
+建议顺序如下:
+
+1. 先做可公开的开源核心版
+2. 同步保留商业版能力边界
+3. 优先获取种子用户和真实反馈
+4. 先争取 `1-3` 个私有部署客户
+5. 再逐步完善在线订阅体系
+
+## 当前最应该优先补齐的事项
+
+### 技术侧
+
+- 敏感配置改为环境变量
+- 删除默认弱口令
+- 将密码哈希升级到 `bcrypt` 或 `argon2`
+- 增加限流、配额和防滥用控制
+- 补齐 Docker 化部署
+- 增加任务队列和缓存策略优化
+- 增加日志、监控、异常处理
+
+### 产品侧
+
+- 首页落地页
+- 演示视频或 GIF
+- 用户上手引导
+- 套餐说明页
+- FAQ
+- 隐私政策
+- 服务条款
+
+### 商业侧
+
+- 免费版限制设计
+- Pro 套餐设计
+- 私有部署报价
+- 用户案例
+- 支付接入
+- 售后支持方式
+
+## 总结
+
+这个项目具备推广价值,也具备一定商业化潜力。它最合适的定位是“面向英文阅读和听读场景的低成本、高效率工具”,而不是泛化的 PDF 阅读产品。
+
+开源适合先做,用来建立信任和获取用户;收费也能做,但不建议只做低价大众订阅。更合理的模式是“开源核心版 + 托管订阅 + 私有部署/机构授权”并行推进。
+
+从当前阶段看,只要先把安全、部署、配额和产品包装这些基础工作补齐,这个项目是有机会形成一门可持续的小生意的。

+ 258 - 0
docs/开源发布方案.md

@@ -0,0 +1,258 @@
+# 开源发布方案
+
+## 目标
+
+这个方案的目标不是单纯把代码公开,而是把当前项目整理成一个可以稳定吸引用户、形成口碑、为后续商业化导流的开源产品。
+
+适用定位:
+
+- 英文 PDF 听读工具
+- 低延迟逐句朗读阅读器
+- 普通 CPU 可部署的本地 TTS 阅读方案
+
+## 开源策略
+
+推荐采用“开源核心版,保留商业增强版”的结构。
+
+### 建议开源的部分
+
+- PDF 阅读器基础能力
+- 指定位置起播
+- 逐句朗读
+- 文本高亮联动
+- 日间/夜间模式
+- 本地 TTS 接入层
+- 单机部署能力
+
+### 建议保留的商业能力
+
+- 支付功能
+- 配额管理
+- 组织与团队空间
+- 商业后台
+- 高级统计报表
+- SaaS 托管平台能力
+- 私有部署支持服务
+
+## 许可证建议
+
+### 推荐方案
+
+- `AGPLv3`
+- 对商业客户再提供商业授权
+
+这种方式的优点:
+
+- 可以公开传播
+- 能限制第三方直接拿去做闭源商业化
+- 为后续商业授权保留空间
+
+### 可选方案
+
+- `MIT`
+
+只有在你明确希望项目尽可能广泛传播、并接受别人直接商用时,才建议使用 `MIT`。
+
+## 开源前必须完成的整改
+
+### 安全整改
+
+1. 清理敏感信息
+- 去除 [config.py](/home/service/reader_pro/config.py:1) 中的默认数据库密码
+- 使用 `.env` 和 `.env.example`
+- 检查历史提交中是否已泄露敏感信息,必要时重置密钥
+
+2. 去除弱口令和默认账号
+- 禁止初始化默认 `admin/admin`
+- 首次启动时要求手动创建管理员
+
+3. 升级认证安全性
+- 将 `sha256` 密码哈希替换为 `bcrypt` 或 `argon2`
+- Cookie 在 HTTPS 环境下使用 `secure=True`
+- 增加基础登录限流
+
+4. 收紧跨域和接口访问
+- 不再默认 `CORS = *`
+- 对上传、生成、管理接口增加限制
+
+### 仓库整理
+
+1. 删除不应提交的文件
+- `audio_cache/`
+- 临时日志
+- 本地数据库配置
+- 历史无关调试文件
+
+2. 增加忽略规则
+- `.env`
+- 缓存音频
+- 上传文件
+- 日志
+- 本地数据库文件或导出
+
+3. 规范目录结构
+- `docs/`
+- `deploy/`
+- `scripts/`
+- `static/`
+- `app/` 或保持当前结构但补充说明
+
+## README 建议结构
+
+建议在 GitHub 首页直接讲清楚“是什么、为什么有价值、怎么跑起来”。
+
+推荐结构:
+
+1. 项目名称
+2. 一句话介绍
+3. 截图/GIF 演示
+4. 核心功能
+5. 为什么不同于普通 PDF 阅读器
+6. 技术架构
+7. 快速部署
+8. 环境变量说明
+9. Roadmap
+10. License
+
+### 一句话介绍示例
+
+一个面向英文阅读场景的 PDF 听读工具,支持指定位置起播、逐句朗读和高亮联动,基于本地 ONNX TTS,普通 CPU 服务器即可部署。
+
+## 演示内容建议
+
+开源项目能不能传播,很多时候取决于演示是否直观。
+
+建议准备:
+
+- 首页截图
+- PDF 阅读界面截图
+- 点句播放动图
+- 高亮跟读动图
+- 日间/夜间模式切换图
+- 服务器部署说明图
+
+如果可以录一个 `30-60 秒` 演示视频,效果会明显更好。
+
+## 对外传播文案建议
+
+传播时不要把重点放在“我做了个 PDF 工具”,而是放在“解决了什么问题”。
+
+推荐传播角度:
+
+- 我做了一个适合英文精读和听读的 PDF 阅读器
+- 普通 CPU 就能跑的本地 TTS 听书方案
+- 点哪句读哪句,适合论文、电子书和英语材料精读
+- 不依赖昂贵 GPU 的低成本听读工具
+
+## 推广渠道建议
+
+### 技术和开源圈
+
+- GitHub
+- V2EX
+- 掘金
+- 少数派
+- Hacker News
+- Reddit 的 self-hosted / productivity / language learning 板块
+
+### 学习用户圈
+
+- 英语学习社群
+- 雅思托福社群
+- 考研英语社群
+- 教师和培训机构群体
+- 小红书
+- B 站
+
+## 开源版本功能边界建议
+
+为了后续商业化,建议从一开始就划清免费版和商业版边界。
+
+### 开源免费版适合保留
+
+- 本地部署
+- 基础用户系统
+- 基础阅读和播放
+- 基础音色和参数
+
+### 商业版适合增强
+
+- 更强后台
+- 更高并发
+- 更完整权限体系
+- 团队功能
+- 统计和分析
+- 在线托管
+- 专业支持
+
+## 发布节奏建议
+
+### 第一阶段:整理发布
+
+目标:
+
+- 完成安全整改
+- 补齐 README
+- 补齐部署文档
+- 发布第一个公开版本
+
+时间建议:
+
+- `1-2 周`
+
+### 第二阶段:获取反馈
+
+目标:
+
+- 收集 issues
+- 收集用户真实使用反馈
+- 找到最受欢迎的功能点
+
+时间建议:
+
+- `2-4 周`
+
+### 第三阶段:商业导流
+
+目标:
+
+- 在 README 和官网中加入托管版、私有部署、商业支持入口
+- 建立咨询转化路径
+
+时间建议:
+
+- 开源后立即开始
+
+## 开源发布检查清单
+
+### 必做
+
+- 敏感信息清理
+- 删除缓存和无关文件
+- 补齐许可证
+- 补齐部署文档
+- 补齐截图和演示
+- 增加 `.env.example`
+- 增加 `.gitignore`
+
+### 强烈建议
+
+- Docker Compose
+- Nginx 反向代理示例
+- 常见问题说明
+- 性能测试结果
+- 版本发布说明
+
+## 预期结果
+
+如果执行得当,开源后的合理预期不是短期赚钱,而是:
+
+- 获得首批真实用户
+- 验证目标场景
+- 提升可信度
+- 积累 GitHub Star 与口碑
+- 为后续托管版和私有部署版导流
+
+## 总结
+
+这个项目适合开源,而且开源本身就是最好的第一阶段营销方式。但要把开源当作产品发布,而不是简单上传代码。重点不是代码有多少,而是你是否把“价值、部署、边界、演示、可信度”讲清楚。

+ 554 - 0
docs/技术侧-API设计.md

@@ -0,0 +1,554 @@
+# 技术侧 API 设计
+
+## 目标
+
+本文档定义产品化阶段建议新增或调整的 API,覆盖:
+
+- 认证与安全
+- 文件与阅读
+- TTS 生成
+- 配额与用量
+- 异步任务
+- 管理员后台
+
+设计原则:
+
+- 尽量兼容现有接口
+- REST 风格优先
+- 返回结构统一
+- 为前台页面和后台管理预留字段
+
+## 1. 通用返回结构
+
+成功:
+
+```json
+{
+  "success": true,
+  "data": {}
+}
+```
+
+失败:
+
+```json
+{
+  "success": false,
+  "error": {
+    "code": "quota_exceeded",
+    "message": "今日配额已用尽"
+  }
+}
+```
+
+## 2. 认证与安全
+
+## 2.1 注册
+
+`POST /api/auth/register`
+
+请求体:
+
+```json
+{
+  "username": "demo_user",
+  "password": "strong_password",
+  "email": "demo@example.com"
+}
+```
+
+返回:
+
+```json
+{
+  "success": true,
+  "data": {
+    "user_id": 1001
+  }
+}
+```
+
+## 2.2 登录
+
+`POST /api/auth/login`
+
+请求体:
+
+```json
+{
+  "username": "demo_user",
+  "password": "strong_password",
+  "remember_me": true
+}
+```
+
+返回:
+
+```json
+{
+  "success": true,
+  "data": {
+    "username": "demo_user",
+    "is_admin": false,
+    "plan_code": "free"
+  }
+}
+```
+
+## 2.3 登出
+
+`POST /api/auth/logout`
+
+## 2.4 当前用户
+
+`GET /api/auth/me`
+
+返回:
+
+```json
+{
+  "success": true,
+  "data": {
+    "id": 1001,
+    "username": "demo_user",
+    "is_admin": false,
+    "is_active": true,
+    "plan": {
+      "code": "free",
+      "name": "免费版"
+    }
+  }
+}
+```
+
+## 2.5 修改密码
+
+`POST /api/auth/change-password`
+
+请求体:
+
+```json
+{
+  "old_password": "oldpass",
+  "new_password": "newpass"
+}
+```
+
+## 3. 文件与阅读
+
+## 3.1 上传 PDF
+
+`POST /api/files/upload`
+
+表单:
+
+- `file`
+
+返回:
+
+```json
+{
+  "success": true,
+  "data": {
+    "file_id": 2001,
+    "file_name": "sample.pdf",
+    "file_url": "/static/files/demo_user/sample.pdf"
+  }
+}
+```
+
+限制建议:
+
+- 文件大小限制
+- 仅允许 PDF
+- 每用户文件数量上限
+
+## 3.2 文件列表
+
+`GET /api/files`
+
+返回:
+
+```json
+{
+  "success": true,
+  "data": {
+    "items": [
+      {
+        "file_id": 2001,
+        "file_name": "sample.pdf",
+        "file_url": "/static/files/demo_user/sample.pdf",
+        "updated_at": "2026-04-30 12:00:00"
+      }
+    ]
+  }
+}
+```
+
+## 3.3 删除文件
+
+`DELETE /api/files/{file_id}`
+
+## 3.4 保存阅读进度
+
+`POST /api/reader/progress`
+
+请求体:
+
+```json
+{
+  "file_path": "/static/files/demo_user/sample.pdf",
+  "page": 12
+}
+```
+
+## 3.5 获取阅读进度
+
+`GET /api/reader/progress?file_path=...`
+
+## 4. TTS 生成
+
+## 4.1 短文本同步生成
+
+`POST /api/tts/generate`
+
+适合:
+
+- 单句
+- 短段落
+
+请求体:
+
+```json
+{
+  "text": "This is a test sentence.",
+  "voice": "af_sky",
+  "speed": 1.0,
+  "file_path": "/static/files/demo_user/sample.pdf",
+  "page": 12,
+  "sentence_index": 4
+}
+```
+
+返回头建议:
+
+- `X-Cache-Hit: 1/0`
+- `X-Request-Id: xxx`
+
+返回体:
+
+- 直接 `audio/wav`
+
+## 4.2 长文本异步生成
+
+`POST /api/tts/tasks`
+
+请求体:
+
+```json
+{
+  "text": "very long content ...",
+  "voice": "af_sky",
+  "speed": 1.0,
+  "file_path": "/static/files/demo_user/sample.pdf",
+  "page": 12
+}
+```
+
+返回:
+
+```json
+{
+  "success": true,
+  "data": {
+    "task_id": "task_123456",
+    "status": "queued"
+  }
+}
+```
+
+## 4.3 查询任务状态
+
+`GET /api/tts/tasks/{task_id}`
+
+返回:
+
+```json
+{
+  "success": true,
+  "data": {
+    "task_id": "task_123456",
+    "status": "processing",
+    "progress": 60,
+    "audio_url": null
+  }
+}
+```
+
+## 4.4 获取任务结果
+
+`GET /api/tts/tasks/{task_id}/result`
+
+返回:
+
+- 音频流
+- 或音频下载地址
+
+## 4.5 用户当前配额
+
+`GET /api/tts/quota`
+
+返回:
+
+```json
+{
+  "success": true,
+  "data": {
+    "plan_code": "free",
+    "daily_tts_chars_used": 5200,
+    "daily_tts_chars_limit": 10000,
+    "monthly_tts_chars_used": 23000,
+    "monthly_tts_chars_limit": 100000
+  }
+}
+```
+
+## 5. 用量与统计
+
+## 5.1 我的使用情况
+
+`GET /api/me/usage/summary`
+
+返回:
+
+```json
+{
+  "success": true,
+  "data": {
+    "today_requests": 21,
+    "today_chars": 5200,
+    "today_audio_seconds": 910,
+    "month_requests": 210,
+    "month_chars": 23000
+  }
+}
+```
+
+## 5.2 我的套餐
+
+`GET /api/me/plan`
+
+## 6. 管理员后台 API
+
+## 6.1 仪表盘统计
+
+`GET /api/admin/dashboard`
+
+返回:
+
+```json
+{
+  "success": true,
+  "data": {
+    "today_active_users": 18,
+    "today_tts_requests": 540,
+    "today_cache_hit_rate": 0.63,
+    "running_tasks": 3,
+    "failed_tasks_today": 6
+  }
+}
+```
+
+## 6.2 用户列表
+
+`GET /api/admin/users?page=1&page_size=20&keyword=demo`
+
+## 6.3 用户详情
+
+`GET /api/admin/users/{user_id}`
+
+返回建议包含:
+
+- 基本信息
+- 当前套餐
+- 今日/月度用量
+- 最近任务
+
+## 6.4 创建或编辑用户
+
+`POST /api/admin/users`
+
+## 6.5 禁用/启用用户
+
+`POST /api/admin/users/{user_id}/status`
+
+请求体:
+
+```json
+{
+  "is_active": false
+}
+```
+
+## 6.6 重置密码
+
+`POST /api/admin/users/{user_id}/reset-password`
+
+## 6.7 套餐列表
+
+`GET /api/admin/plans`
+
+## 6.8 创建或更新套餐
+
+`POST /api/admin/plans`
+
+请求体:
+
+```json
+{
+  "code": "pro_monthly",
+  "name": "专业版月付",
+  "daily_tts_chars": 50000,
+  "monthly_tts_chars": 500000,
+  "daily_tts_requests": 200,
+  "max_text_length": 3000,
+  "max_parallel_tasks": 2,
+  "price_month": 29.00
+}
+```
+
+## 6.9 分配用户套餐
+
+`POST /api/admin/users/{user_id}/plan`
+
+请求体:
+
+```json
+{
+  "plan_code": "pro_monthly",
+  "start_at": "2026-05-01 00:00:00",
+  "end_at": "2026-06-01 00:00:00"
+}
+```
+
+## 6.10 TTS 日志列表
+
+`GET /api/admin/tts-logs?page=1&page_size=20&status=failed&user_id=1001`
+
+## 6.11 异步任务列表
+
+`GET /api/admin/tasks?page=1&page_size=20&status=processing`
+
+## 6.12 重试任务
+
+`POST /api/admin/tasks/{task_id}/retry`
+
+## 6.13 缓存概览
+
+`GET /api/admin/cache/summary`
+
+返回建议:
+
+- 缓存文件数
+- 总体积
+- 命中率
+- 最近热点项
+
+## 6.14 清理缓存
+
+`POST /api/admin/cache/cleanup`
+
+请求体:
+
+```json
+{
+  "mode": "expired_only"
+}
+```
+
+可选值:
+
+- `expired_only`
+- `least_recently_used`
+- `all`
+
+## 7. 监控与健康检查
+
+## 7.1 健康检查
+
+`GET /api/health`
+
+返回:
+
+```json
+{
+  "success": true,
+  "data": {
+    "app": "ok",
+    "db": "ok",
+    "tts_upstream": "ok",
+    "redis": "ok"
+  }
+}
+```
+
+## 7.2 系统指标摘要
+
+`GET /api/admin/system/metrics`
+
+返回建议:
+
+- CPU
+- 内存
+- 磁盘
+- 队列长度
+- 错误率
+
+## 8. 接口中间件建议
+
+### 统一中间件建议做这些事
+
+- 生成 `request_id`
+- 记录访问日志
+- 计算耗时
+- 捕获异常
+- 注入当前用户信息
+
+### 对关键接口加的通用逻辑
+
+- 认证校验
+- 配额校验
+- 限流校验
+- 缓存命中判断
+- 日志落库
+
+## 9. API 实施优先级
+
+### 第一批必须上线
+
+- `/api/auth/*`
+- `/api/files/*`
+- `/api/reader/progress`
+- `/api/tts/generate`
+- `/api/tts/quota`
+- `/api/me/usage/summary`
+- `/api/health`
+
+### 第二批建议上线
+
+- `/api/tts/tasks`
+- `/api/tts/tasks/{task_id}`
+- `/api/admin/dashboard`
+- `/api/admin/tts-logs`
+- `/api/admin/users/{user_id}/plan`
+
+### 第三批按商业需要上线
+
+- 套餐管理
+- 缓存清理
+- 任务重试
+- 系统指标页
+
+## 总结
+
+API 设计的目标不是一次做大,而是先把“认证、文件、TTS、配额、后台”这五条主链路跑通。第一批接口上线后,你就已经具备一个可运营的基础版 SaaS 雏形。

+ 165 - 0
docs/技术侧-实施清单.md

@@ -0,0 +1,165 @@
+# 技术侧实施清单
+
+## 目标
+
+本文档将技术侧方案拆成可执行清单,按优先级和阶段推进,适合直接排期。
+
+## 第一阶段:可公开部署
+
+### 1. 安全整改
+
+- [ ] 将数据库配置迁移到环境变量
+- [ ] 新增 `.env.example`
+- [ ] 删除代码中的默认数据库密码
+- [ ] 移除默认 `admin/admin`
+- [ ] 引入 `bcrypt` 或 `argon2`
+- [ ] 增加管理员初始化脚本
+- [ ] 限制 `CORS` 来源
+- [ ] 生产环境启用 `secure cookie`
+
+### 2. 基础工程化
+
+- [ ] 编写 `Dockerfile`
+- [ ] 编写 `docker-compose.yml`
+- [ ] 配置 MySQL 容器
+- [ ] 配置 Redis 容器
+- [ ] 规范数据卷目录
+- [ ] 编写部署说明文档
+
+### 3. 日志与健康检查
+
+- [ ] 新增统一日志格式
+- [ ] 为请求生成 `request_id`
+- [ ] 增加 `/api/health`
+- [ ] 记录 TTS 请求耗时
+- [ ] 记录异常日志
+
+## 第一阶段验收标准
+
+- [ ] 项目可通过 `docker compose up` 启动
+- [ ] 无敏感配置硬编码
+- [ ] 默认无弱口令管理员
+- [ ] 健康检查可返回数据库和 TTS 状态
+
+## 第二阶段:可控试运营
+
+### 4. 数据结构升级
+
+- [ ] 新增 `plan`
+- [ ] 新增 `user_plan`
+- [ ] 新增 `usage_daily`
+- [ ] 新增 `tts_request_log`
+- [ ] 新增 `audio_cache_index`
+
+### 5. 配额和限流
+
+- [ ] 定义免费版默认配额
+- [ ] 定义 Pro 版默认配额
+- [ ] 接口增加单次文本长度限制
+- [ ] 接口增加每日字符限制
+- [ ] 登录接口增加限流
+- [ ] 生成接口增加限流
+
+### 6. 用量统计
+
+- [ ] 每次生成记录请求日志
+- [ ] 每次成功生成累计 `usage_daily`
+- [ ] 增加用户用量查询接口
+- [ ] 增加后台基础统计接口
+
+### 7. 缓存增强
+
+- [ ] 统一缓存 key 规则
+- [ ] 缓存写入时同步索引表
+- [ ] 缓存命中时更新命中次数
+- [ ] 增加缓存命中率统计
+
+## 第二阶段验收标准
+
+- [ ] 免费用户超额后会被正确拦截
+- [ ] 后台可看到今日生成量
+- [ ] 系统可统计缓存命中率
+- [ ] 日志可追踪一次生成请求
+
+## 第三阶段:可收费运营
+
+### 8. 异步任务队列
+
+- [ ] 选型 `RQ` 或 `Celery`
+- [ ] 增加 `async_task`
+- [ ] 长文本改为异步任务
+- [ ] 返回 `task_id`
+- [ ] 提供任务状态查询接口
+- [ ] 失败任务支持重试
+
+### 9. 管理员后台完善
+
+- [ ] 增加仪表盘统计页
+- [ ] 增加套餐管理页
+- [ ] 增加用户套餐分配能力
+- [ ] 增加 TTS 日志查看页
+- [ ] 增加任务查看与重试页
+- [ ] 增加缓存概览页
+
+### 10. 监控与告警
+
+- [ ] 采集 CPU、内存、磁盘指标
+- [ ] 采集队列长度
+- [ ] 采集错误率
+- [ ] 搭建 Grafana 面板
+- [ ] 增加基础告警规则
+
+## 第三阶段验收标准
+
+- [ ] 长文本不会阻塞主接口
+- [ ] 管理员可查看任务状态和失败原因
+- [ ] 系统可以看到资源占用和错误趋势
+
+## 推荐工期
+
+### 第 1 周
+
+- 安全整改
+- `.env` 改造
+- 基础日志
+
+### 第 2 周
+
+- Docker 化
+- 健康检查
+- 部署文档
+
+### 第 3 周
+
+- 数据表扩展
+- 配额和限流
+- 用量统计
+
+### 第 4 周
+
+- 缓存索引
+- 后台统计
+- 基础运营能力
+
+### 第 5-6 周
+
+- 异步任务
+- 管理后台增强
+
+### 第 7-8 周
+
+- 监控告警
+- 压测与问题修复
+
+## 风险提示
+
+### 需要优先避免的问题
+
+- 一边收费一边仍然使用弱密码体系
+- 没有配额就开放大量生成
+- 没有日志导致问题无法排查
+- 没有缓存索引导致缓存失控
+
+## 总结
+
+技术实施建议先做“安全、部署、配额、日志”,再做“缓存、统计、后台”,最后做“任务队列和监控”。这个顺序能保证你尽快得到一个可公开、可控、可收费的系统,而不是先把架构做重。

+ 413 - 0
docs/技术侧-数据库表设计.md

@@ -0,0 +1,413 @@
+# 技术侧数据库表设计
+
+## 目标
+
+本文档用于补齐产品化和商业化所需的数据结构,覆盖以下能力:
+
+- 用户与认证
+- 套餐与配额
+- 用量统计
+- TTS 请求日志
+- 异步任务
+- 缓存索引
+- 管理员审计日志
+
+当前项目已有:
+
+- `user`
+- `user_progress`
+- `user_config`
+
+本文以“尽量兼容现有表结构、逐步扩展”为原则。
+
+## 1. 用户与认证
+
+### 1.1 user
+
+已有表可继续沿用,但建议补充字段。
+
+建议字段:
+
+```sql
+CREATE TABLE user (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    username VARCHAR(64) NOT NULL UNIQUE,
+    password_hash VARCHAR(255) NOT NULL,
+    email VARCHAR(128) NULL,
+    phone VARCHAR(32) NULL,
+    is_admin TINYINT(1) NOT NULL DEFAULT 0,
+    is_active TINYINT(1) NOT NULL DEFAULT 1,
+    session_token VARCHAR(128) NULL,
+    session_expires_at DATETIME NULL,
+    last_file VARCHAR(1024) NULL,
+    last_page INT NULL,
+    last_login_at DATETIME NULL,
+    created_at DATETIME NOT NULL,
+    updated_at DATETIME NOT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+新增字段建议:
+
+- `email`:后续找回密码、订单联系
+- `phone`:机构客户联系,可选
+- `last_login_at`:活跃度统计
+
+### 1.2 user_session_log
+
+记录登录行为,用于安全和审计。
+
+```sql
+CREATE TABLE user_session_log (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    user_id BIGINT NOT NULL,
+    session_token VARCHAR(128) NOT NULL,
+    login_ip VARCHAR(64) NULL,
+    user_agent VARCHAR(512) NULL,
+    login_at DATETIME NOT NULL,
+    logout_at DATETIME NULL,
+    is_valid TINYINT(1) NOT NULL DEFAULT 1,
+    KEY idx_user_login (user_id, login_at),
+    CONSTRAINT fk_session_user
+        FOREIGN KEY (user_id) REFERENCES user(id)
+        ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+## 2. 套餐与配额
+
+## 2.1 plan
+
+定义平台套餐。
+
+```sql
+CREATE TABLE plan (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    code VARCHAR(32) NOT NULL UNIQUE,
+    name VARCHAR(64) NOT NULL,
+    description VARCHAR(255) NULL,
+    price_month DECIMAL(10,2) NULL,
+    price_year DECIMAL(10,2) NULL,
+    daily_tts_chars INT NOT NULL DEFAULT 0,
+    monthly_tts_chars INT NOT NULL DEFAULT 0,
+    daily_tts_requests INT NOT NULL DEFAULT 0,
+    max_text_length INT NOT NULL DEFAULT 0,
+    max_parallel_tasks INT NOT NULL DEFAULT 1,
+    is_public TINYINT(1) NOT NULL DEFAULT 1,
+    is_active TINYINT(1) NOT NULL DEFAULT 1,
+    created_at DATETIME NOT NULL,
+    updated_at DATETIME NOT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+推荐套餐初始值:
+
+- `free`
+- `pro_monthly`
+- `pro_yearly`
+- `team`
+- `private_deploy`
+
+## 2.2 user_plan
+
+记录用户当前套餐。
+
+```sql
+CREATE TABLE user_plan (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    user_id BIGINT NOT NULL,
+    plan_id BIGINT NOT NULL,
+    start_at DATETIME NOT NULL,
+    end_at DATETIME NULL,
+    status VARCHAR(32) NOT NULL,
+    source VARCHAR(32) NOT NULL DEFAULT 'admin',
+    auto_renew TINYINT(1) NOT NULL DEFAULT 0,
+    created_at DATETIME NOT NULL,
+    updated_at DATETIME NOT NULL,
+    KEY idx_user_status (user_id, status),
+    CONSTRAINT fk_user_plan_user
+        FOREIGN KEY (user_id) REFERENCES user(id)
+        ON DELETE CASCADE,
+    CONSTRAINT fk_user_plan_plan
+        FOREIGN KEY (plan_id) REFERENCES plan(id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+`status` 建议值:
+
+- `active`
+- `expired`
+- `canceled`
+- `trial`
+
+## 2.3 usage_daily
+
+日用量统计,支撑配额判断。
+
+```sql
+CREATE TABLE usage_daily (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    user_id BIGINT NOT NULL,
+    usage_date DATE NOT NULL,
+    tts_chars INT NOT NULL DEFAULT 0,
+    tts_requests INT NOT NULL DEFAULT 0,
+    audio_seconds INT NOT NULL DEFAULT 0,
+    cache_hits INT NOT NULL DEFAULT 0,
+    cache_misses INT NOT NULL DEFAULT 0,
+    failed_requests INT NOT NULL DEFAULT 0,
+    UNIQUE KEY uniq_user_date (user_id, usage_date),
+    KEY idx_usage_date (usage_date),
+    CONSTRAINT fk_usage_daily_user
+        FOREIGN KEY (user_id) REFERENCES user(id)
+        ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+## 2.4 usage_monthly
+
+月汇总统计,便于后台展示和计费判断。
+
+```sql
+CREATE TABLE usage_monthly (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    user_id BIGINT NOT NULL,
+    usage_month CHAR(7) NOT NULL,
+    tts_chars INT NOT NULL DEFAULT 0,
+    tts_requests INT NOT NULL DEFAULT 0,
+    audio_seconds INT NOT NULL DEFAULT 0,
+    amount_estimated DECIMAL(10,2) NOT NULL DEFAULT 0.00,
+    UNIQUE KEY uniq_user_month (user_id, usage_month),
+    KEY idx_usage_month (usage_month),
+    CONSTRAINT fk_usage_monthly_user
+        FOREIGN KEY (user_id) REFERENCES user(id)
+        ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+## 3. TTS 请求日志
+
+## 3.1 tts_request_log
+
+最关键的业务日志表。
+
+```sql
+CREATE TABLE tts_request_log (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    request_id VARCHAR(64) NOT NULL UNIQUE,
+    user_id BIGINT NULL,
+    file_path VARCHAR(512) NULL,
+    page INT NULL,
+    sentence_index INT NULL,
+    text_hash VARCHAR(64) NOT NULL,
+    text_length INT NOT NULL,
+    voice VARCHAR(64) NOT NULL,
+    speed DECIMAL(4,2) NOT NULL DEFAULT 1.00,
+    cache_key VARCHAR(128) NULL,
+    cache_hit TINYINT(1) NOT NULL DEFAULT 0,
+    task_id VARCHAR(64) NULL,
+    status VARCHAR(32) NOT NULL,
+    audio_seconds INT NOT NULL DEFAULT 0,
+    latency_ms INT NOT NULL DEFAULT 0,
+    error_message VARCHAR(512) NULL,
+    created_at DATETIME NOT NULL,
+    finished_at DATETIME NULL,
+    KEY idx_user_created (user_id, created_at),
+    KEY idx_status_created (status, created_at),
+    KEY idx_text_hash (text_hash),
+    CONSTRAINT fk_tts_log_user
+        FOREIGN KEY (user_id) REFERENCES user(id)
+        ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+`status` 建议值:
+
+- `queued`
+- `processing`
+- `success`
+- `failed`
+- `canceled`
+
+## 4. 异步任务
+
+## 4.1 async_task
+
+记录长文本与后台任务。
+
+```sql
+CREATE TABLE async_task (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    task_id VARCHAR(64) NOT NULL UNIQUE,
+    user_id BIGINT NULL,
+    task_type VARCHAR(32) NOT NULL,
+    priority INT NOT NULL DEFAULT 0,
+    status VARCHAR(32) NOT NULL,
+    payload_json JSON NULL,
+    result_json JSON NULL,
+    progress INT NOT NULL DEFAULT 0,
+    error_message VARCHAR(512) NULL,
+    retry_count INT NOT NULL DEFAULT 0,
+    started_at DATETIME NULL,
+    finished_at DATETIME NULL,
+    created_at DATETIME NOT NULL,
+    updated_at DATETIME NOT NULL,
+    KEY idx_user_status (user_id, status),
+    KEY idx_type_status (task_type, status),
+    CONSTRAINT fk_async_task_user
+        FOREIGN KEY (user_id) REFERENCES user(id)
+        ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+`task_type` 建议值:
+
+- `tts_long_text`
+- `cache_warmup`
+- `cache_cleanup`
+- `usage_rollup`
+
+## 5. 缓存索引
+
+## 5.1 audio_cache_index
+
+避免只依赖磁盘文件名,便于统计和清理。
+
+```sql
+CREATE TABLE audio_cache_index (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    cache_key VARCHAR(128) NOT NULL UNIQUE,
+    text_hash VARCHAR(64) NOT NULL,
+    text_length INT NOT NULL,
+    voice VARCHAR(64) NOT NULL,
+    speed DECIMAL(4,2) NOT NULL DEFAULT 1.00,
+    model_version VARCHAR(64) NULL,
+    file_path VARCHAR(512) NOT NULL,
+    file_size BIGINT NOT NULL DEFAULT 0,
+    audio_seconds INT NOT NULL DEFAULT 0,
+    hit_count INT NOT NULL DEFAULT 0,
+    created_at DATETIME NOT NULL,
+    last_hit_at DATETIME NULL,
+    expired_at DATETIME NULL,
+    KEY idx_last_hit (last_hit_at),
+    KEY idx_text_voice (text_hash, voice)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+## 6. 管理员审计日志
+
+## 6.1 admin_audit_log
+
+记录重要管理操作。
+
+```sql
+CREATE TABLE admin_audit_log (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    admin_user_id BIGINT NOT NULL,
+    action VARCHAR(64) NOT NULL,
+    target_type VARCHAR(64) NOT NULL,
+    target_id VARCHAR(128) NULL,
+    detail_json JSON NULL,
+    ip VARCHAR(64) NULL,
+    created_at DATETIME NOT NULL,
+    KEY idx_admin_created (admin_user_id, created_at),
+    KEY idx_action_created (action, created_at),
+    CONSTRAINT fk_admin_audit_user
+        FOREIGN KEY (admin_user_id) REFERENCES user(id)
+        ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+`action` 示例:
+
+- `create_user`
+- `reset_password`
+- `disable_user`
+- `assign_plan`
+- `clear_cache`
+- `retry_task`
+
+## 7. 可选扩展表
+
+## 7.1 payment_order
+
+如果后续接支付,可增加订单表。
+
+```sql
+CREATE TABLE payment_order (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    order_no VARCHAR(64) NOT NULL UNIQUE,
+    user_id BIGINT NOT NULL,
+    plan_id BIGINT NOT NULL,
+    amount DECIMAL(10,2) NOT NULL,
+    currency VARCHAR(16) NOT NULL DEFAULT 'CNY',
+    status VARCHAR(32) NOT NULL,
+    paid_at DATETIME NULL,
+    created_at DATETIME NOT NULL,
+    updated_at DATETIME NOT NULL,
+    KEY idx_user_created (user_id, created_at),
+    CONSTRAINT fk_payment_order_user
+        FOREIGN KEY (user_id) REFERENCES user(id),
+    CONSTRAINT fk_payment_order_plan
+        FOREIGN KEY (plan_id) REFERENCES plan(id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+## 7.2 user_file
+
+如果后续需要更完整的文件管理,建议从纯目录管理升级到表管理。
+
+```sql
+CREATE TABLE user_file (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    user_id BIGINT NOT NULL,
+    file_name VARCHAR(255) NOT NULL,
+    file_path VARCHAR(512) NOT NULL,
+    file_size BIGINT NOT NULL DEFAULT 0,
+    page_count INT NOT NULL DEFAULT 0,
+    status VARCHAR(32) NOT NULL DEFAULT 'active',
+    created_at DATETIME NOT NULL,
+    updated_at DATETIME NOT NULL,
+    KEY idx_user_created (user_id, created_at),
+    CONSTRAINT fk_user_file_user
+        FOREIGN KEY (user_id) REFERENCES user(id)
+        ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+## 8. 实施顺序建议
+
+### 第一批必须落地
+
+- `user` 字段补充
+- `plan`
+- `user_plan`
+- `usage_daily`
+- `tts_request_log`
+- `audio_cache_index`
+
+### 第二批建议落地
+
+- `async_task`
+- `admin_audit_log`
+- `usage_monthly`
+
+### 第三批按商业进度落地
+
+- `payment_order`
+- `user_file`
+- `user_session_log`
+
+## 9. 数据库迁移建议
+
+建议不要一次重构全部表,而是用迁移脚本逐步上线。
+
+推荐步骤:
+
+1. 新增新表,不动旧逻辑
+2. 在核心接口中逐步开始写入新表
+3. 后台逐步读取新表
+4. 稳定后再清理旧逻辑或补齐回填脚本
+
+## 总结
+
+数据库设计的核心目标不是“复杂”,而是为配额、统计、缓存、后台和收费预留结构。对你当前项目来说,最优先的是套餐、用量、TTS 请求日志和缓存索引,这四块直接决定系统是否可运营。

+ 621 - 0
docs/技术侧落地方案.md

@@ -0,0 +1,621 @@
+# 技术侧落地方案
+
+## 目标
+
+本文档用于把当前项目从“可运行”推进到“可公开发布、可持续运维、可支持收费”的技术形态。重点覆盖以下模块:
+
+- 安全整改
+- Docker 化部署
+- 配额和限流
+- 用量统计
+- 异步任务队列
+- 更稳定的缓存策略
+- 管理员后台完善
+- 监控和日志
+
+本文默认当前项目栈为:
+
+- 后端:`FastAPI`
+- 前端:`PDF.js + 自定义页面`
+- 数据库:`MySQL`
+- TTS:本地 `Kororo ONNX` 上游服务
+
+## 1. 安全整改
+
+### 目标
+
+让项目达到最基本的公开部署和收费使用标准,避免明显的密码、权限、接口暴露问题。
+
+### 当前问题
+
+- [config.py](/home/service/reader_pro/config.py:1) 写有默认数据库密码
+- 默认管理员账号 `admin/admin`
+- 密码哈希仍为简单 `sha256`
+- `CORS = *`
+- Cookie `secure=False`
+- 缺少上传、登录、生成接口限流
+
+### 落地方案
+
+#### 1. 配置安全
+
+改为环境变量加载:
+
+- 新增 `.env.example`
+- 所有数据库、Cookie、TTS 上游地址改为环境变量
+- 正式环境禁止提交真实 `.env`
+
+建议变量:
+
+- `READER_PRO_DB_HOST`
+- `READER_PRO_DB_PORT`
+- `READER_PRO_DB_USER`
+- `READER_PRO_DB_PASSWORD`
+- `READER_PRO_DB_NAME`
+- `READER_PRO_TTS_API_BASE_URL`
+- `READER_PRO_SESSION_SECRET`
+- `READER_PRO_ALLOWED_ORIGINS`
+
+#### 2. 认证安全
+
+- 密码哈希改为 `bcrypt` 或 `argon2`
+- 默认不创建弱口令管理员
+- 第一次启动通过初始化命令创建管理员
+- 登录失败增加限次和冷却
+
+建议实现:
+
+- 新增 `scripts/create_admin.py`
+- 仅在初始化阶段人工创建管理员
+
+#### 3. 接口安全
+
+- 上传接口限制文件大小
+- 生成接口限制请求频率
+- 管理接口仅管理员可访问
+- 增加基础审计日志
+
+#### 4. 浏览器安全
+
+- HTTPS 环境下 Cookie 使用 `secure=True`
+- 设置 `httponly=True`
+- 设置 `samesite=lax` 或更严格
+- CORS 不再默认全开放
+
+### 分阶段实现
+
+#### 第一阶段
+
+- 环境变量改造
+- 去除默认密码
+- 去除默认弱口令管理员
+- 升级密码哈希
+
+#### 第二阶段
+
+- 登录和生成接口限流
+- 文件上传大小限制
+- 基础审计日志
+
+## 2. Docker 化部署
+
+### 目标
+
+降低部署成本,提高可复现性,方便开源用户试用,也方便后续商业托管和私有部署。
+
+### 推荐结构
+
+建议至少提供以下文件:
+
+- `Dockerfile`
+- `docker-compose.yml`
+- `.env.example`
+- `deploy/nginx.conf`
+- `deploy/systemd/` 或部署说明
+
+### 落地方式
+
+#### 1. 后端镜像
+
+封装 FastAPI 服务:
+
+- 安装 Python 依赖
+- 暴露服务端口
+- 使用 `uvicorn` 或 `gunicorn + uvicorn worker`
+
+建议:
+
+- 开发环境可用 `uvicorn`
+- 生产环境建议 `gunicorn`
+
+#### 2. 依赖服务
+
+初期 `docker-compose.yml` 建议包括:
+
+- `app`
+- `mysql`
+- `redis`
+- `tts`,如果本地 TTS 也能容器化则一起封装
+
+#### 3. 存储卷
+
+需要挂载的数据目录:
+
+- `static/files/`
+- `audio_cache/`
+- 日志目录
+
+#### 4. 反向代理
+
+建议由 `Nginx` 负责:
+
+- HTTPS
+- 静态资源缓存
+- 请求体大小限制
+- 基础限流
+
+### 分阶段实现
+
+#### 第一阶段
+
+- 单机 `docker-compose`
+- 一条命令启动
+
+#### 第二阶段
+
+- Nginx 反向代理
+- 数据卷标准化
+- 生产部署说明
+
+## 3. 配额和限流
+
+### 目标
+
+防止免费用户滥用资源,控制 TTS 成本,并为收费功能提供边界。
+
+### 推荐设计
+
+配额和限流分开设计:
+
+- 配额:用户每天/月总可用量
+- 限流:单位时间内请求频率限制
+
+### 配额维度建议
+
+- 每日生成字符数
+- 每日生成次数
+- 单次最大文本长度
+- 同时进行中的任务数
+
+### 用户等级建议
+
+- 匿名用户:禁止使用或仅试用
+- 免费用户:低配额
+- Pro 用户:中高配额
+- 团队用户:更高配额
+
+### 数据设计建议
+
+建议新增数据表:
+
+- `plan`
+- `user_plan`
+- `usage_daily`
+- `usage_monthly`
+
+核心字段:
+
+- `user_id`
+- `usage_date`
+- `tts_chars`
+- `tts_requests`
+- `audio_seconds`
+
+### 限流实现建议
+
+初期可以基于:
+
+- 用户 ID
+- IP
+- 接口路径
+
+可用方式:
+
+- Redis 计数器
+- Nginx 限流
+- 应用层中间件
+
+推荐策略:
+
+- 登录接口:防爆破
+- 注册接口:防批量注册
+- `/generate`:防刷资源
+- 上传接口:防大文件和频繁上传
+
+### 分阶段实现
+
+#### 第一阶段
+
+- 每日字符配额
+- 单次文本长度限制
+- 登录和生成接口限流
+
+#### 第二阶段
+
+- 月度配额
+- 套餐绑定
+- 后台可调额度
+
+## 4. 用量统计
+
+### 目标
+
+支撑配额、后台运营、问题定位和商业化决策。
+
+### 最低可用统计项
+
+- 日活用户数
+- 生成请求数
+- 生成字符数
+- 音频总时长
+- 缓存命中率
+- 失败率
+- 平均生成耗时
+
+### 数据来源
+
+建议在每次 TTS 请求完成后记录:
+
+- 用户 ID
+- 文档 ID 或文件名
+- 文本长度
+- 是否命中缓存
+- 音频时长
+- 请求开始和结束时间
+- 结果状态
+
+### 建议表结构
+
+- `tts_request_log`
+- `usage_daily`
+- `system_metrics_snapshot`
+
+### 后台可展示内容
+
+- 今日生成量
+- 本月生成量
+- 用户排行
+- 峰值时段
+- 缓存命中率
+- 错误统计
+
+### 分阶段实现
+
+#### 第一阶段
+
+- 请求日志表
+- 每日汇总表
+- 管理员后台可查看基础统计
+
+#### 第二阶段
+
+- 趋势图
+- 导出 CSV
+- 套餐使用率和续费辅助数据
+
+## 5. 异步任务队列
+
+### 目标
+
+避免长文本生成阻塞接口,提高并发稳定性,便于后续扩容。
+
+### 什么时候必须上任务队列
+
+以下场景建议引入:
+
+- 整页或长段落生成
+- 多用户同时生成
+- 需要重试机制
+- 需要任务状态反馈
+
+### 推荐架构
+
+建议引入:
+
+- `Redis` 作为 broker
+- `Celery` 或 `RQ` 作为任务队列
+
+如果你想保持轻量,早期建议:
+
+- `RQ + Redis`
+
+如果后续需要复杂任务流和计划任务:
+
+- `Celery + Redis`
+
+### 任务拆分建议
+
+- 短句请求:可同步返回
+- 长文本请求:进入队列异步处理
+- 预热缓存:后台异步任务
+- 清理缓存:定时任务
+
+### 用户体验建议
+
+前端展示:
+
+- 任务提交成功
+- 当前状态:排队中、处理中、已完成、失败
+- 可轮询或 WebSocket 更新状态
+
+### 分阶段实现
+
+#### 第一阶段
+
+- 长文本进入队列
+- 返回 `task_id`
+- 前端轮询任务状态
+
+#### 第二阶段
+
+- 重试机制
+- 优先级队列
+- 失败重放
+
+## 6. 更稳定的缓存策略
+
+### 目标
+
+降低重复生成成本,提升起播速度,提高系统稳定性。
+
+### 当前缓存方向是正确的
+
+目前已有 `audio_cache/`,说明项目已经具备成本控制基础,但需要进一步规范化。
+
+### 推荐缓存策略
+
+#### 1. 句级缓存优先
+
+缓存 key 建议由以下内容组成:
+
+- 文本内容
+- voice
+- speed
+- 模型版本
+- 音频格式
+
+这样相同句子可直接复用。
+
+#### 2. 缓存层级
+
+- L1:内存元数据缓存
+- L2:磁盘音频缓存
+- L3:可选对象存储缓存
+
+#### 3. 缓存元数据表
+
+建议增加:
+
+- `audio_cache_index`
+
+字段建议:
+
+- `cache_key`
+- `file_path`
+- `text_length`
+- `voice`
+- `speed`
+- `hit_count`
+- `created_at`
+- `last_hit_at`
+
+#### 4. 清理策略
+
+- 按最近最少使用清理
+- 按总容量阈值清理
+- 按过期时间清理
+
+#### 5. 预生成策略
+
+适合热门文档:
+
+- 翻页后预生成下一句
+- 用户停留当前页时预生成后续句子
+
+### 分阶段实现
+
+#### 第一阶段
+
+- 统一缓存 key 规则
+- 建立缓存索引表
+- 加入命中统计
+
+#### 第二阶段
+
+- LRU 清理
+- 热点句预生成
+- 后台缓存维护任务
+
+## 7. 管理员后台完善
+
+### 目标
+
+让后台不只是“用户管理入口”,而是一个可用于运营、限额、排错、客服支持的管理系统。
+
+### 当前应补的能力
+
+- 用户列表
+- 用户状态管理
+- 套餐管理
+- 用量查询
+- 请求日志查看
+- 任务查看
+- 缓存查看
+- 系统运行状态查看
+
+### 推荐后台页面模块
+
+#### 1. 仪表盘
+
+- 今日活跃用户
+- 今日生成次数
+- 缓存命中率
+- 当前排队任务数
+- 错误数
+
+#### 2. 用户管理
+
+- 用户列表
+- 搜索用户
+- 重置密码
+- 启用/禁用
+- 查看套餐和使用量
+
+#### 3. 套餐和配额管理
+
+- 创建套餐
+- 绑定用户套餐
+- 调整配额
+- 查看剩余额度
+
+#### 4. 请求与任务中心
+
+- 查看最近生成任务
+- 查看失败任务
+- 手动重试
+- 查看耗时
+
+#### 5. 缓存管理
+
+- 缓存容量
+- 命中率
+- 热门缓存项
+- 手动清理
+
+### 分阶段实现
+
+#### 第一阶段
+
+- 用户管理
+- 基础统计
+- 用量查看
+
+#### 第二阶段
+
+- 套餐与任务管理
+- 缓存管理
+- 系统健康页
+
+## 8. 监控和日志
+
+### 目标
+
+让系统在真实用户环境中可观测,能快速发现瓶颈和异常。
+
+### 日志建议
+
+至少分为:
+
+- 应用日志
+- 错误日志
+- TTS 请求日志
+- 管理员操作日志
+
+### 关键日志字段
+
+- 请求时间
+- 用户 ID
+- 接口路径
+- 文件名
+- 字符数
+- 缓存是否命中
+- 耗时
+- 状态码
+- 错误信息
+
+### 监控建议
+
+至少监控:
+
+- CPU
+- 内存
+- 磁盘
+- 请求量
+- 错误率
+- 平均响应时间
+- TTS 队列长度
+- 缓存目录容量
+
+### 推荐实现方式
+
+初期轻量方案:
+
+- `Prometheus + Grafana`
+- `Loki` 或文件日志
+
+更轻量的第一阶段:
+
+- 结构化日志写文件
+- 定时脚本采样系统指标
+- 后台展示最近指标
+
+### 告警建议
+
+建议对以下情况告警:
+
+- TTS 连续失败
+- 队列积压过多
+- CPU 长时间满载
+- 缓存磁盘接近上限
+- 数据库连接失败
+
+### 分阶段实现
+
+#### 第一阶段
+
+- 结构化日志
+- 日志分级
+- 后台显示最近错误
+
+#### 第二阶段
+
+- Prometheus 指标
+- Grafana 面板
+- 基础告警
+
+## 推荐实施顺序
+
+如果按当前项目现实情况推进,建议顺序如下:
+
+1. 安全整改
+2. Docker 化部署
+3. 配额和限流
+4. 用量统计
+5. 稳定缓存策略
+6. 异步任务队列
+7. 管理员后台完善
+8. 监控和日志
+
+## 建议里程碑
+
+### 里程碑一:可公开部署
+
+- 安全整改完成
+- Docker 部署完成
+- 限流与基础日志完成
+
+### 里程碑二:可控试运营
+
+- 配额和统计完成
+- 缓存策略增强
+- 后台可查看关键数据
+
+### 里程碑三:可收费运营
+
+- 队列机制完成
+- 管理后台完善
+- 监控和告警完善
+
+## 总结
+
+技术侧的重点不是一次把所有系统做重,而是先把“安全、部署、可控成本、可观测”补齐。对于你当前这个 CPU TTS 场景,最优先的是安全整改、Docker 化、配额限流和缓存优化,这几项直接决定你能不能放心开放给用户使用。

+ 106 - 33
static/web/viewer.html

@@ -1774,6 +1774,57 @@
                     restoreActiveSentenceHighlight();
                     restoreActiveSentenceHighlight();
                 }
                 }
 
 
+                function ensureHighlightVisibleForCurrentPage(pageNumber = null) {
+                    if (!isTtsHighlightEnabled()) return;
+                    if (playbackHighlightContext?.fullText) {
+                        ensurePlaybackHighlightContext(playbackHighlightContext.fullText);
+                    }
+                    if (!playbackHighlightContext?.activeSentenceText && playbackHighlightContext?.activeSentenceIndex === null) return;
+                    const currentViewerPage = PDFViewerApplication?.pdfViewer?.currentPageNumber ?? null;
+                    if (pageNumber !== null && currentViewerPage !== null && Number(pageNumber) !== Number(currentViewerPage)) {
+                        return;
+                    }
+                    restoreActiveSentenceHighlight();
+                }
+
+                function ensurePlaybackHighlightContext(fullText, retryCount = 0) {
+                    if (!isTtsHighlightEnabled()) return;
+                    if (playbackHighlightContext?.textLayerIndex && playbackHighlightContext?.fullText === fullText) {
+                        return;
+                    }
+                    const nextContext = buildPlaybackHighlightContext(fullText);
+                    if (nextContext?.textLayerIndex) {
+                        if (playbackHighlightContext?.activeSentenceIndex !== null && playbackHighlightContext?.activeSentenceIndex !== undefined) {
+                            nextContext.activeSentenceIndex = playbackHighlightContext.activeSentenceIndex;
+                        }
+                        if (playbackHighlightContext?.activeSentenceText) {
+                            nextContext.activeSentenceText = playbackHighlightContext.activeSentenceText;
+                        }
+                        if (playbackHighlightContext?.searchOffset) {
+                            nextContext.searchOffset = playbackHighlightContext.searchOffset;
+                        }
+                        playbackHighlightContext = nextContext;
+                        if (nextContext.activeSentenceText || nextContext.activeSentenceIndex !== null) {
+                            highlightSentenceAtIndex(nextContext.activeSentenceIndex ?? 0, nextContext.activeSentenceText, true);
+                        }
+                        return;
+                    }
+                    if (retryCount >= 12) return;
+                    setTimeout(() => ensurePlaybackHighlightContext(fullText, retryCount + 1), 120);
+                }
+
+                async function waitForCurrentPageTextLayer(maxAttempts = 20, delayMs = 80) {
+                    for (let attempt = 0; attempt < maxAttempts; attempt++) {
+                        const textLayer = getCurrentPageTextLayer();
+                        const textLayerIndex = buildTextLayerIndex(textLayer);
+                        if (textLayer && textLayerIndex) {
+                            return { textLayer, textLayerIndex };
+                        }
+                        await new Promise(resolve => setTimeout(resolve, delayMs));
+                    }
+                    return null;
+                }
+
                 function setTtsHighlightEnabled(enabled) {
                 function setTtsHighlightEnabled(enabled) {
                     localStorage.setItem(TTS_HIGHLIGHT_ENABLED_STORAGE_KEY, enabled ? '1' : '0');
                     localStorage.setItem(TTS_HIGHLIGHT_ENABLED_STORAGE_KEY, enabled ? '1' : '0');
                     applyTtsHighlightSettingsToUi();
                     applyTtsHighlightSettingsToUi();
@@ -2068,17 +2119,30 @@
                             restoreReadingProgress();
                             restoreReadingProgress();
                         };
                         };
 
 
-                        eventBus.on('pagesloaded', tryRestore);
-                        eventBus.on('documentloaded', tryRestore);
-                        eventBus.on('pagechanging', evt => {
-                            const page = Number(evt?.pageNumber);
-                            if (Number.isInteger(page) && page > 0) {
-                                saveReadingProgress(page);
-                            }
-                        });
-                    };
-                    bindEvents();
-                }
+                        eventBus.on('pagesloaded', tryRestore);
+                        eventBus.on('documentloaded', tryRestore);
+                        eventBus.on('pagechanging', evt => {
+                            const page = Number(evt?.pageNumber);
+                            if (Number.isInteger(page) && page > 0) {
+                                saveReadingProgress(page);
+                                setTimeout(() => ensureHighlightVisibleForCurrentPage(page), 0);
+                            }
+                        });
+                        eventBus.on('textlayerrendered', evt => {
+                            const page = Number(evt?.pageNumber);
+                            if (Number.isInteger(page) && page > 0) {
+                                ensureHighlightVisibleForCurrentPage(page);
+                            }
+                        });
+                        eventBus.on('pagerendered', evt => {
+                            const page = Number(evt?.pageNumber);
+                            if (Number.isInteger(page) && page > 0) {
+                                setTimeout(() => ensureHighlightVisibleForCurrentPage(page), 0);
+                            }
+                        });
+                    };
+                    bindEvents();
+                }
                 setupReadingProgressSync();
                 setupReadingProgressSync();
                 ensureLoggedIn();
                 ensureLoggedIn();
 
 
@@ -2459,10 +2523,9 @@
                 function buildPlaybackHighlightContext(fullText) {
                 function buildPlaybackHighlightContext(fullText) {
                     const textLayer = getCurrentPageTextLayer();
                     const textLayer = getCurrentPageTextLayer();
                     const textLayerIndex = buildTextLayerIndex(textLayer);
                     const textLayerIndex = buildTextLayerIndex(textLayer);
-                    if (!textLayerIndex) return null;
                     return {
                     return {
-                        textLayer,
-                        textLayerIndex,
+                        textLayer: textLayer || null,
+                        textLayerIndex: textLayerIndex || null,
                         fullText,
                         fullText,
                         sentences: splitTextForHighlight(fullText),
                         sentences: splitTextForHighlight(fullText),
                         searchOffset: 0,
                         searchOffset: 0,
@@ -2477,7 +2540,7 @@
                     const fullText = playbackHighlightContext?.fullText;
                     const fullText = playbackHighlightContext?.fullText;
                     if (!fullText) return false;
                     if (!fullText) return false;
                     const nextContext = buildPlaybackHighlightContext(fullText);
                     const nextContext = buildPlaybackHighlightContext(fullText);
-                    if (!nextContext) return false;
+                    if (!nextContext?.textLayerIndex) return false;
                     nextContext.searchOffset = playbackHighlightContext?.searchOffset || 0;
                     nextContext.searchOffset = playbackHighlightContext?.searchOffset || 0;
                     nextContext.activeSentenceIndex = playbackHighlightContext?.activeSentenceIndex ?? null;
                     nextContext.activeSentenceIndex = playbackHighlightContext?.activeSentenceIndex ?? null;
                     nextContext.activeSentenceText = playbackHighlightContext?.activeSentenceText || '';
                     nextContext.activeSentenceText = playbackHighlightContext?.activeSentenceText || '';
@@ -2557,10 +2620,18 @@
                 function highlightSentenceAtIndex(sentenceIndex, sentenceText = '', preserveState = false) {
                 function highlightSentenceAtIndex(sentenceIndex, sentenceText = '', preserveState = false) {
                     clearActiveTtsHighlight();
                     clearActiveTtsHighlight();
                     const ctx = playbackHighlightContext;
                     const ctx = playbackHighlightContext;
-                    if (!ctx?.textLayerIndex) return;
-
                     const targetSentence = normalizeForTtsMatch(sentenceText || ctx.sentences?.[sentenceIndex] || '');
                     const targetSentence = normalizeForTtsMatch(sentenceText || ctx.sentences?.[sentenceIndex] || '');
                     if (!targetSentence) return;
                     if (!targetSentence) return;
+                    if (ctx) {
+                        ctx.activeSentenceIndex = sentenceIndex;
+                        ctx.activeSentenceText = targetSentence;
+                    }
+                    if (!ctx?.textLayerIndex) {
+                        if (ctx?.fullText) {
+                            ensurePlaybackHighlightContext(ctx.fullText);
+                        }
+                        return;
+                    }
 
 
                     const layerText = ctx.textLayerIndex.text;
                     const layerText = ctx.textLayerIndex.text;
                     const startSearch = Math.max(0, ctx.searchOffset || 0);
                     const startSearch = Math.max(0, ctx.searchOffset || 0);
@@ -2576,8 +2647,6 @@
                     }
                     }
                     ctx.currentRange = { start: matchStart, end: matchEnd };
                     ctx.currentRange = { start: matchStart, end: matchEnd };
                     ctx.lastWindowKey = '';
                     ctx.lastWindowKey = '';
-                    ctx.activeSentenceIndex = sentenceIndex;
-                    ctx.activeSentenceText = targetSentence;
                     if (!isTtsHighlightEnabled()) return;
                     if (!isTtsHighlightEnabled()) return;
                     updateHighlightWindow(matchStart, matchEnd);
                     updateHighlightWindow(matchStart, matchEnd);
                 }
                 }
@@ -2749,6 +2818,7 @@
                     try {
                     try {
                         stopCurrentPlayback(false);
                         stopCurrentPlayback(false);
                         playbackHighlightContext = buildPlaybackHighlightContext(fullText);
                         playbackHighlightContext = buildPlaybackHighlightContext(fullText);
+                        ensurePlaybackHighlightContext(fullText);
                         const response = await fetch('/generate', {
                         const response = await fetch('/generate', {
                             method: 'POST',
                             method: 'POST',
                             headers: { 'Content-Type': 'application/json' },
                             headers: { 'Content-Type': 'application/json' },
@@ -2845,20 +2915,23 @@
                 });
                 });
 
 
                 // 阅读整页
                 // 阅读整页
-                readPageButton.addEventListener('click', async function () {
-                    const { pdfViewer } = PDFViewerApplication;
-                    const currentPageNumber = pdfViewer.currentPageNumber;
-                    const loadingIndicator = document.getElementById('loading-indicator');
-
-                    try {
-                        initAudioContext();
-                        await getPlayPermission();
-                        loadingIndicator.textContent = '正在提取页面文本...';
-                        loadingIndicator.style.display = 'block';
-
-                        const pdfDocument = PDFViewerApplication.pdfDocument;
-                        const page = await pdfDocument.getPage(currentPageNumber);
-                        const textContent = await page.getTextContent();
+                readPageButton.addEventListener('click', async function () {
+                    const { pdfViewer } = PDFViewerApplication;
+                    const currentPageNumber = pdfViewer.currentPageNumber;
+                    const loadingIndicator = document.getElementById('loading-indicator');
+
+                    try {
+                        stopCurrentPlayback(false);
+                        initAudioContext();
+                        await getPlayPermission();
+                        loadingIndicator.textContent = '正在提取页面文本...';
+                        loadingIndicator.style.display = 'block';
+
+                        await waitForCurrentPageTextLayer();
+
+                        const pdfDocument = PDFViewerApplication.pdfDocument;
+                        const page = await pdfDocument.getPage(currentPageNumber);
+                        const textContent = await page.getTextContent();
 
 
                         // 提取完整文本,保留标点符号(不预分割)
                         // 提取完整文本,保留标点符号(不预分割)
                         let fullText = textContent.items.map(item => item.str).join(' ').trim();
                         let fullText = textContent.items.map(item => item.str).join(' ').trim();