版本:2.1 | 更新:2026-08-10(短/长文本分治、单词语音校验闭环、词库缓存 Phase 2) | 语言:Python 3.12 | 框架:FastAPI + Uvicorn | TTS:Qwen3-TTS | ASR:Faster-Whisper
🔥 修改代码前必读:约束与约定文档(架构/模型/接口/缓存/并发边界,约束编号 C/M/G/D/A/P/T)
MemoryServerTTS 是"记忆英语"(Memory English Learning App)生态中的 语音服务中间件,为上层的 Java SpringBoot 后端(MemoryServer)提供三大智能语音能力:
- 语音合成(TTS):基于阿里通义千问第 3 代语音合成模型 Qwen3-TTS,将文本转化为自然流畅的多语言语音
- 语音识别(ASR):基于 CTranslate2 加速的 Faster-Whisper,将用户语音转写为文字
- 发音评价:评估用户发音准确度,提供从整体评分到逐词、逐音素的精细化诊断反馈
graph LR
subgraph Client["📱 Android 客户端"]
APP["MemoryApp"]
end
subgraph JavaServer["🖥️ MemoryServer (Java SpringBoot)"]
TTS_SVC["TTSService"]
PRON_SVC["PronunciationService"]
CONV_SVC["ConversationService"]
end
subgraph PythonServer["🎵 MemoryServerTTS (Python FastAPI)"](
TTS["TTS 合成"]
ASR["语音识别"]
EVAL["发音评价"]
end
Client <--> JavaServer
JavaServer -->|"HTTP REST + WebSocket"| PythonServer
| 能力 | 核心技术 | 输入 | 输出 | 典型调用方 |
|---|---|---|---|---|
| TTS 合成 | Qwen3-TTS 0.6B/1.7B | 文本 + 音色 + 语言 | WAV 音频流 | AI 对话、听写播放、单词发音 |
| TTS 流式 | Qwen3-TTS + WebSocket | 分段文本 | Base64 PCM16 音频块 | 实时对话语音回复 |
| ASR 转录 | Faster-Whisper base | 音频文件 (.wav/.mp3/.flac/.m4a) | 文本 + 时间戳 + 语言检测 | AI 对话语音输入、发音评价前置 |
| 音素评价 | ASR + G2P + 编辑距离 | 学生录音 + 参考文本 | 逐词/逐音素诊断 | 发音纠正模块 |
| 声学评价 | MFCC + DTW | 学生录音 + 标准音频 | 整体相似度评分 | 跟读对比练习 |
| )( |
MemoryServerTTS/
├── main.py # 应用入口(uvicorn 启动,RELOAD 环境变量控制热重载)
├── requirements.txt # Python 依赖清单
├── Dockerfile # Docker 容器化配置
├── README.md # 项目简介
├── "# environment.yml" # Conda 环境定义(memory-tts)
├── config/ # 模块配置(YAML,TTSConfig/OCRConfig 读取)
│ ├── tts.yaml # TTS 配置:模型/解码策略/校验闭环/词库缓存
│ └── ocr.yaml # OCR 配置
├── start_server.bat # Windows 一键启动
├── start_server.sh # Linux/macOS 一键启动
│
├── src/ # 核心源代码(按模块分包)
│ ├── __init__.py
│ ├── server.py # FastAPI 主服务(路由挂载 + 生命周期 + WebSocket)
│ ├── common/ # 公共组件
│ │ ├── base_config.py # 配置基类(YAML + 环境变量覆盖)
│ │ └── logging.py # 统一日志([模块] 前缀 + ANSI 彩色)
│ ├── tts/ # TTS 模块(Qwen3-TTS)
│ │ ├── config.py # TTSConfig(解码参数/校验参数/词库缓存配置)
│ │ ├── model_loader.py # TTSModelManager(单例 + 1.7B/0.6B 降级 + 短长文本分治)
│ │ ├── router.py # /api/v1/tts/* 路由
│ │ └── verifier.py # ASR 回读校验(宽松匹配 + 置信度门槛)
│ ├── asr/ # ASR 模块(Faster-Whisper)
│ │ ├── model_loader.py # ASRModelManager(单例)
│ │ └── router.py # /api/v1/asr/* 路由
│ ├── pronunciation/ # 发音评价模块
│ │ ├── evaluator.py # MFCC+DTW 声学评价器
│ │ ├── phoneme_evaluator.py # G2P+ASR 音素级评价器(核心创新)
│ │ ├── g2p_engine.py # G2P 引擎(英文 g2p-en / 中文 pypinyin)
│ │ └── router.py # /api/v1/pronunciation/* 路由
│ ├── ocr/ # OCR 模块(PaddleOCR)
│ │ ├── config.py / engine.py / router.py
│ ├── dictation/ # 🔥 词库缓存(听写场景,Phase 2)
│ │ ├── cache.py # 缓存存储(key=hash(word|voice|lang|instruct|版本),原子写入)
│ │ ├── generator.py # best-of-N 预生成 + quality_score 评分
│ │ ├── spec.py # 词条规格归一化(语言别名 + 母语音色匹配)
│ │ ├── router.py # /api/v1/dictation/* 路由(含管理端接口)
│ │ └── pregenerate.py # 离线预生成 CLI(强制 1.7B)
│ └── dashboard/ # 管理后台
│ ├── router.py # /admin 页面路由
│ └── templates/index.html # 单页后台(系统概览/TTS/ASR/发音/OCR/词库管理/测速)
│
├── models/ # AI 模型文件
│ ├── qwen-1.7b/ # Qwen3-TTS 1.7B 主模型(推荐,~3.4GB VRAM)
│ │ ├── config.json / generation_config.json / model.safetensors
│ │ ├── tokenizer_config.json / vocab.json / merges.txt
│ │ └── speech_tokenizer/ # 语音分词器子模型
│ └── qwen-0.6b/ # Qwen3-TTS 0.6B 降级模型(~1.2GB VRAM,不支持 instruct)
│ └── ...(结构同上)
│
├── tests/ # 单元测试(unittest,python -m unittest discover -s tests)
│ ├── test_tts_verifier.py # ASR 校验匹配逻辑(19 用例)
│ ├── test_dictation_cache.py # 词库缓存/评分/择优(21 用例)
│ ├── test_tts_model_loader.py # 单词语音判定 is_single_word(10 用例)
│ └── test_official.py # Qwen3-TTS 官方接口测试脚本
│
├── tts-audio/ # 流式/合成输出音频(/tts-audio 静态挂载)
├── word-cache/ # 🔥 词库音频缓存(运行时生成,/api/v1/dictation 使用)
├── voices/ # 音色克隆数据目录(运行时生成)
│
└── docs/ # 文档目录
├── API_DOCUMENTATION.md # API 集成文档(面向调用方/SpringBoot 开发者)
├── PROJECT_DOCUMENTATION.md # 本项目完整技术文档(本文件)
├── CONSTRAINTS.md # 🔥 约束与约定文档(架构/模型/接口/性能边界,必读)
├── OCR_INTEGRATION.md # OCR 模块接入说明
├── phoneme-score-fix.md # 音素评分接口修复记录
└── TROUBLESHOOTING_SPRINGBOOT.md # SpringBoot 接入排错指南
graph TB
subgraph Clients["客户端调用层"]
SB["🖥️ MemoryServer<br/>Java SpringBoot 3.5"]
Curl["🔧 curl / Postman"]
UI["🎛️ Gradio Debug UI<br/>:7860"]
end
subgraph Server["FastAPI 服务层 (port 8000)"]
direction TB
subgraph Routes["路由层"]
TTS_R["/api/v1/tts/*"]
ASR_R["/api/v1/asr/*"]
PRON_R["/api/v1/pronunciation/*"]
SYS_R["/api/v1/health"]
end
subgraph Services["业务逻辑层"]
TTS_SVC_IN["TTS 合成<br/>Lock 保护"]
ASR_SVC_IN["ASR 转录<br/>+ 格式标准化"]
PRON_SVC_IN["发音评价<br/>双模式"]
end
subgraph Lifecycle["生命周期管理"]
START["@app.on_event('startup')<br/>加载所有模型单例"]
end
end
subgraph Models["模型层(单例模式)"]
TTS_M["TTSModelManager<br/>Qwen3-TTS<br/>0.6B / 1.7B 自动降级"]
ASR_M["ASRModelManager<br/>Faster-Whisper<br/>base 模型"]
G2P_M["G2PEngine<br/>EnglishG2P (g2p-en)<br/>ChineseG2P (pypinyin)"]
PRON_M["PronunciationEvaluator<br/>MFCC + DTW"]
PHON_M["PhonemeEvaluator<br/>ASR + G2P + 音素对齐"]
end
subgraph Compute["计算后端"]
GPU["NVIDIA GPU<br/>CUDA + bfloat16/float32"]
CPU["CPU 回退<br/>int8 量化"]
end
Clients --> Routes
Routes --> Services
Services --> Models
TTS_M --> GPU
ASR_M --> GPU
ASR_M --> CPU
START --> Models
| 层级 | 技术 | 版本 | 用途 | 选型理由 |
|---|---|---|---|---|
| Web 框架 | FastAPI | latest | REST API + WebSocket | 高性能异步框架,原生 OpenAPI 文档,类型安全 |
| ASGI 服务器 | Uvicorn | latest | 运行 FastAPI,热重载 | 轻量高速,支持 --reload 开发模式 |
| TTS 引擎 | Qwen3-TTS (qwen_tts) |
latest | 多语言语音合成 | 阿里通义千问 3 代,9 个预设音色,情感指令控制 |
| ASR 引擎 | Faster-Whisper | latest | 语音转录 + 语言检测 | Whisper 的 CTranslate2 加速版,推理速度提升 4 倍 |
| 深度学习框架 | PyTorch | 2.7.0+cu128 | GPU 推理 | CUDA 12.8,bfloat16 加速 |
| Transformers | HuggingFace Transformers | 4.57.3 | 模型加载基座 | Qwen3-TTS 依赖 |
| 英文 G2P | g2p-en |
latest | 英文文字→音素(ARPAbet) | 基于 CMU Pronouncing Dictionary + NLTK POS tagger |
| 中文 G2P | pypinyin |
latest | 中文文字→拼音声韵母 | 支持声调,声母韵母智能拆分 |
| 音频处理 | librosa |
latest | MFCC 特征提取、音频加载 | 经典的音频分析库 |
| 音频 I/O | soundfile |
latest | WAV 读写、格式标准化 | 高效、格式兼容性好 |
| DTW 算法 | fastdtw |
latest | 动态时间规整 | O(N) 近似 DTW,比标准 O(N²) 快 |
| 序列比对 | difflib (标准库) |
— | 词级/音素级编辑距离对齐 | 原生支持 equal/replace/delete/insert 四类操作 |
| 数据模型 | Pydantic | latest | 请求体校验 | FastAPI 原生支持 |
| 调试 UI | Gradio | latest | TTS 可视化测试界面 | 快速搭建 ML 模型演示 |
| 容器化 | Docker | — | 标准化部署 | Python 3.12-slim 基础镜像 |
以下序列图展示了 MemoryServer(Java SpringBoot)与 MemoryServerTTS(Python FastAPI)之间在一次"AI 英语对话"中的完整交互过程:
sequenceDiagram
actor User as 👤 用户
participant App as 📱 Android App
participant MS as 🖥️ MemoryServer<br/>(Java)
participant TTS as 🎵 MemoryServerTTS<br/>(Python)
participant AI as 🤖 DeepSeek API
)(
Note over User,AI: ── AI 英语对话完整流程 ──
)(
User->>App: 语音输入 "How are you?"
App->>MS: POST /conversation/message<br/>(multipart: audio)
MS->>TTS: POST /api/v1/asr/transcribe<br/>(multipart: audio)
Note over TTS: Faster-Whisper 转录
TTS-->>MS: { text: "How are you", language: "en" }
MS->>AI: 对话生成请求
AI-->>MS: { reply: "I'm doing great!" }
MS->>TTS: POST /api/v1/tts/synthesize<br/>(JSON: text, voice, language)
Note over TTS: Qwen3-TTS 合成语音<br/>(asyncio.Lock 保护)
TTS-->>MS: WAV 音频文件
MS->>AI: 对话质量评估
AI-->>MS: { pronunciation: 4, fluency: 4, ... }
MS-->>App: { aiReply, audioUrl, evaluation }
App->>User: 播放 AI 语音回复 + 显示评估
| 组件 | 最低要求 | 推荐配置 | 说明 |
|---|---|---|---|
| Python | 3.10+ | 3.12 | 运行环境 |
| PyTorch | 2.0.1+ (CUDA) | 2.7.0+ (CUDA 12.8) | GPU 推理必需 |
| 显存 (0.6B 模型) | 2 GB | 4 GB | 轻量 TTS 模型 |
| 显存 (1.7B 模型) | 4 GB | 8 GB | 高质量 TTS 模型 |
| 磁盘空间 | 3 GB | 10 GB | 含两套模型文件 |
| 操作系统 | Windows / Linux / macOS | Linux (生产) | — |
| CUDA | 11.8+ | 12.8 | NVIDIA GPU 必需 |
Faster-Whisper 模型:首次运行时会从 HuggingFace Hub 自动下载
base模型(约 140 MB),缓存于~/.cache/huggingface/。
TTS 模型管理器是整个语音合成能力的核心。设计上遵循四个原则:
- 单例模式:Qwen3-TTS 模型加载到 GPU 显存后,每个实例占用 1-4 GB 显存,加载耗时 20-60 秒。单例确保全局只有一个模型实例,避免显存爆炸和重复加载。
- 主备降级(1.7B 优先):默认加载 1.7B 主模型(质量好、支持
instruct),本地缺失或加载失败时降级到 0.6B 备选模型(轻量,但不支持instruct)。⚠️ 注意:早期版本为"0.6B 优先",2026-08 已反转。 - 计算精度自适应:根据 GPU 架构自动选择
bfloat16(Ampere SM 8.0+,如 RTX 30xx/A100/H100)或float32(旧架构/CPU)。 - 短/长文本分治(2026-08 起):单词语音走"确定性解码 + ASR 校验闭环",短句/长文本走"随机采样 + 轻量时长校验",详见 4.1.1。
class TTSModelManager:
_instance = None # 类变量,保存唯一实例
def __new__(cls, config=None):
if cls._instance is None: # 首次调用
cls._instance = super().__new__(cls) # 调用 object.__new__
cls._instance._config = config or TTSConfig() # 注入配置
cls._instance._load_model() # 加载模型
return cls._instance # 后续调用直接返回已有实例关键点:Python 的 __new__ 方法在 __init__ 之前调用,负责创建实例。通过重写 __new__ 实现单例,确保无论如何调用 TTSModelManager(),返回的都是同一个实例。
flowchart TD
START["TTSModelManager() 被首次调用"] --> TRY0{"本地 1.7B 存在?<br/>models/qwen-1.7b/"}
TRY0 -->|是| LOAD0["加载本地 1.7B<br/>约 20-60 秒"]
TRY0 -->|否| TRY1{"HuggingFace 1.7B<br/>可访问?"}
TRY1 -->|是| LOAD1["下载+加载 1.7B<br/>约 2-5 分钟"]
TRY1 -->|否| FAIL0["1.7B 加载失败"]
LOAD0 --> DONE["✅ 模型就绪"]
LOAD1 --> DONE
FAIL0 --> TRY2{"本地 0.6B 存在?<br/>models/qwen-0.6b/"}
TRY2 -->|是| LOAD2["加载本地 0.6B<br/>约 20-40 秒"]
TRY2 -->|否| TRY3{"HuggingFace 0.6B<br/>可访问?"}
TRY3 -->|是| LOAD3["下载+加载 0.6B<br/>约 1-3 分钟"]
TRY3 -->|否| FAIL["❌ 所有模型加载失败<br/>抛出 RuntimeError"]
LOAD2 --> DONE
LOAD3 --> DONE
配置方式:可通过环境变量覆盖默认路径(config/tts.yaml 的 tts.model_path 亦生效)
| 环境变量 | 默认值 | 说明 |
|---|---|---|
QWEN_TTS_MODEL_PATH |
./models/qwen-1.7b(或 yaml tts.model_path) |
主模型本地路径 |
QWEN_TTS_MODEL |
Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice |
主模型 HF ID |
TTSCONF_MODEL_PATH |
同 QWEN_TTS_MODEL_PATH |
TTSConfig 环境变量覆盖(词库 CLI 用它强制 1.7B) |
⚠️ 0.6B 模型不支持instruct:qwen_tts 包会在 0.6B 上静默丢弃指令。听写词库预生成(src/dictation/pregenerate.py)通过TTSCONF_MODEL_PATH强制 1.7B,规避该限制。
caps = torch.cuda.get_device_capability()
supports_bf16 = caps[0] >= 8 # Ampere (SM 8.0+) 支持 bfloat16
compute_dtype = torch.bfloat16 if (use_gpu and supports_bf16) else torch.float32- NVIDIA 计算能力(Compute Capability) 是一个版本号
major.minor,get_device_capability()返回(major, minor) - SM 8.0 = Ampere 架构(RTX 3090, A100, A6000),SM 8.6 = RTX 3060/3070/3080,SM 8.9 = RTX 4060/4070/4080/4090
- SM 9.0 = Hopper 架构(H100)
bfloat16(Brain Floating Point)是一种 16 位浮点格式,与 float32 有相同的指数范围(8 位),但尾数只有 7 位。相比 float16,bfloat16 不容易溢出/下溢,适合深度学习推理- 旧架构(如 GTX 10xx 的 SM 6.1、RTX 20xx 的 SM 7.5)不支持 bfloat16 硬件加速,回退到 float32
torch.set_float32_matmul_precision('high')全局开启 TF32 加速(Ampere+,精度损失 <0.1%,速度提升 ~20%)
"attn_implementation": "sdpa"固定使用 PyTorch 原生的 Scaled Dot-Product Attention (SDPA),避免对 flash-attn 的额外依赖。SDPA 是 PyTorch 2.0+ 内置的融合注意力算子,自动选择最优实现(FlashAttention、Memory-Efficient Attention 或朴素实现)。
def generate(self, text: str, voice: str = "", language: str = "",
instructions: str = "", streaming: bool = False,
verify: bool | None = None, seed: int | None = None
) -> tuple[list[np.ndarray], int, dict]:| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
text |
str | — | 待合成文本 |
voice |
str | "" |
音色 ID;留空按语言自动匹配母语音色(English→aiden) |
language |
str | "English" |
语言("Chinese", "English" 等,兼容 en/zh 简写) |
instructions |
str | "" |
情感/风格指令(业务侧传入,服务端不注入,见 CONSTRAINTS.md C1) |
streaming |
bool | False |
True=流式模式(不做校验) |
verify |
bool|None | None |
None=自动(仅单词语音校验);true=强制(仅单词语音生效);false=跳过 ASR 校验 |
seed |
int|None | None |
基础随机种子(单词语音重试时 seed+i) |
返回值:(wavs, sample_rate, meta) — meta 含 verified / attempts / asr_text / confidence / duration / strategy(short|long) / seed / details。
generate() 依据 is_single_word()(src/tts/model_loader.py)判定输入类型:
| 输入类型 | 判定 | 解码策略 | 校验 |
|---|---|---|---|
单词语音(ahead/well/你好) |
剥尾标点后 1 词、无内部空白、长度 ≤ 40 | 确定性:temp 0.5 / top_k 20 / top_p 0.9 / rep 1.2 / max_new 512 / 固定 seed | ASR 回读校验 + 换 seed 重试(≤3 次) |
短句/多词短语(How are you? 等) |
非单词 | 随机采样(temp 0.9 / max_new 2048) | 轻量时长校验 + 重试 1 次 |
| 长文本(>250 字符) | 非单词 | 分句后逐块随机采样 | 逐块轻量时长校验 |
校验细节(宽松匹配 + 置信度门槛、失败降级语义)见 docs/API_DOCUMENTATION.md 4.4 节与 docs/CONSTRAINTS.md G 组约束。
ASR 模型管理器封装了 Faster-Whisper 的加载和转录调用。与 TTS 管理器一样采用单例模式,同时针对不同硬件环境自动选择计算精度。
Faster-Whisper 是 OpenAI Whisper 模型的 CTranslate2 重实现,核心优化手段:
| 优化技术 | 说明 | 加速效果 |
|---|---|---|
| CTranslate2 推理引擎 | 针对 Transformer 模型的专用推理框架 | 主要加速来源 |
| INT8 量化(CPU) | 将权重和激活值量化为 8 位整数 | CPU 上提速 2-3 倍 |
| FP16 推理(GPU) | 使用半精度浮点计算 | GPU 上提速 1.5-2 倍 |
| 算子融合 | 合并多个连续操作为单个 CUDA kernel | 减少显存带宽压力 |
| KV Cache 优化 | 重用已计算的 Key/Value 矩阵 | 解码阶段加速 |
Whisper 模型架构:基于 Encoder-Decoder Transformer:
音频 → Log-Mel 频谱图 → Encoder (多层 Transformer) → 隐藏表示
↓
文本 ← 自回归解码 ← Decoder (多层 Transformer + Cross-Attention)
| 模型 | 参数量 | 显存需求 | 英文 WER | 多语言 WER | 推理速度 |
|---|---|---|---|---|---|
tiny |
39M | ~1 GB | 7.5% | 17.2% | 最快 |
base |
74M | ~1.5 GB | 5.5% | 13.9% | 快 |
small |
244M | ~2.5 GB | 4.1% | 11.2% | 中等 |
medium |
769M | ~5 GB | 3.4% | 9.0% | 较慢 |
large-v3 |
1.55B | ~10 GB | 2.7% | 7.5% | 最慢 |
device = "cuda" if torch.cuda.is_available() else "cpu"
compute_type = "float16" if device == "cuda" else "int8"- GPU 上使用
float16充分利用 Tensor Core - CPU 上使用
int8量化减少内存和计算量
def transcribe(self, audio_path: str, language: str = "",
task: str = "transcribe", beam_size: int = 5,
word_timestamps: bool = False) -> dict:| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
audio_path |
str | — | 音频文件路径 |
language |
str | ""(自动检测) |
语言代码,如 "en", "zh", "ja" |
task |
str | "transcribe" |
"transcribe" 转录 / "translate" 翻译为英文 |
beam_size |
int | 5 |
束搜索宽度,越大越准但越慢 |
word_timestamps |
bool | False |
是否返回单词级时间戳 |
返回值结构:
{
"text": "Hello world", # 完整转录文本
"language": "en", # 检测到的语言代码
"language_probability": 0.95, # 语言检测置信度
"segments": [ # 分段结果
{
"id": 0,
"start": 0.0, # 该段起始时间(秒)
"end": 2.5, # 该段结束时间(秒)
"text": "Hello world", # 该段文本
"confidence": -0.12, # 平均对数概率
"words": [...] # 单词级时间戳(仅 word_timestamps=True)
}
]
}这是最初的发音评价方案,通过对比学生录音和标准参考录音的声学特征相似度来打分。它不需要理解语言内容,只关心"听起来像不像"。
flowchart LR
subgraph Input["输入"]
SA["🎤 学生音频"]
RA["🔊 参考音频"]
end
subgraph Feature["特征提取"]
MFCC_S["librosa MFCC<br/>13维 × N帧"]
MFCC_R["librosa MFCC<br/>13维 × M帧"]
end
subgraph Align["对齐与评分"]
DTW["fastdtw<br/>动态时间规整"]
SCORE["Score = max(0, 100 - distance/max_dist × 100)"]
end
SA --> MFCC_S
RA --> MFCC_R
MFCC_S --> DTW
MFCC_R --> DTW
DTW --> SCORE
SCORE --> RESULT["{score, level, feedback}"]
MFCC(Mel-Frequency Cepstral Coefficients,梅尔频率倒谱系数) 是语音识别中最经典的声学特征。提取过程:
- 预加重:$y[n] = x[n] - 0.97 \cdot x[n-1]$,增强高频分量
- 分帧:25ms 窗口 + 10ms 步长
- 加窗(汉明窗):减少频谱泄露
- FFT:将时域信号转到频域
-
Mel 滤波器组:将线性频率映射到 Mel 尺度(人耳感知的非线性频率尺度)
$$mel(f) = 2595 \cdot \log_{10}(1 + f/700)$$ - 对数运算:$\log$ 压缩动态范围
- DCT(离散余弦变换):去相关,取前 13 个系数
为什么是 13 维? 13 是语音识别领域经过大量实验验证的最优值——足够捕获声道形状信息,又不至于包含过多无关变化。
问题:两个人读同一个词,时长往往不同(例如 "hello" 可能 0.3 秒 vs 0.5 秒)。直接用欧氏距离比较 MFCC 序列是不公平的。
DTW 解决思路:寻找两条序列之间的最优对齐路径,允许时间轴上的非线性拉伸/压缩。
学生: [a1, a2, a3, a4, a5] (5 帧)
参考: [b1, b2, b3] (3 帧)
DTW 对齐(示例):
a1 → b1
a2 → b1 ← 拉伸
a3 → b2
a4 → b2 ← 拉伸
a5 → b3
累计距离 = d(a1,b1) + d(a2,b1) + d(a3,b2) + d(a4,b2) + d(a5,b3)
本项目使用 fastdtw(快速 DTW),通过 radius 参数限制搜索窗口(默认 radius=1),将复杂度从
其中 max_distance = 1000 是归一化参数。DTW 距离越小,说明两条音频的声学特征越接近,评分越高。
- 需要参考音频,使用场景受限
- 无法定位具体错误,只能给整体分数
- 对背景噪声敏感
这些局限性促使了后续 G2P+ASR 音素评价器(第 4.5 节)的开发。
G2P(Grapheme-to-Phoneme,字形到音素转换)是音素级发音评价的前置基础。它负责将书写的文字(Grapheme)转化为发音的音素序列(Phoneme),是连接"文字"和"声音"的桥梁。
设计采用 策略模式(Strategy Pattern):定义抽象基类 G2PEngine,英文和中文各有一个具体实现,通过工厂函数 get_g2p_engine() 按语言动态选择。
classDiagram
class G2PEngine {
<<abstract>>
+word_to_phonemes(word) list[str]
+word_to_phoneme_string(word) str
+text_to_phonemes(text) list[list[str]]
+text_to_word_phoneme_pairs(text) list[tuple]
#_tokenize(text) list[str]$
}
class EnglishG2P {
-_g2p: G2p
-_strip_stress: Pattern
+word_to_phonemes(word) list[str]
+word_to_phoneme_string(word) str
+word_to_phonemes_with_stress(word) list[str]
}
class ChineseG2P {
-_pinyin: pinyin
-_style: Style
-_with_tone: bool
+word_to_phonemes(word) list[str]
+word_to_phoneme_string(word) str
#_split_initial_final(pinyin) tuple$
#_tokenize(text) list[str]$
}
G2PEngine <|-- EnglishG2P
G2PEngine <|-- ChineseG2P
ARPAbet 是 CMU 发音词典使用的音素集,共 39 个音素:
| 类别 | 音素 | 示例 |
|---|---|---|
| 元音 | AA, AE, AH, AO, AW, AY, EH, ER, EY, IH, IY, OW, OY, UH, UW | AA = father, IY = beat |
| 塞音 | B, D, G, K, P, T | P = pen |
| 擦音 | DH, F, S, SH, TH, V, Z, ZH | DH = this, TH = thin |
| 塞擦音 | CH, JH | CH = chin |
| 鼻音 | M, N, NG | NG = sing |
| 流音/滑音 | HH, L, R, W, Y | L = light |
去重音处理:g2p-en 输出的 ARPAbet 带重音标记(AH0, AH1, AH2),音素比对时通过正则 [0-2]$ 去除:
# "hello" → ['HH', 'AH0', 'L', 'OW1']
# 去重音后 → ['HH', 'AH', 'L', 'OW']
self._strip_stress = re.compile(r'[0-2]$')
return [self._strip_stress.sub('', p) for p in raw]中文没有标准音素集,使用拼音的声母+韵母作为"音素"的近似替代。
声韵母拆分算法:
def _split_initial_final(pinyin: str) -> tuple[str, str]:
initials = ['zh', 'ch', 'sh', # 翘舌音优先(长前缀匹配)
'b', 'p', 'm', 'f', 'd', 't', 'n', 'l',
'g', 'k', 'h', 'j', 'q', 'x',
'z', 'c', 's', 'r', 'y', 'w']
for init in initials:
if pinyin.startswith(init):
return init, pinyin[len(init):]
return '', pinyin示例:
| 汉字 | 拼音 | 声母 | 韵母 |
|---|---|---|---|
| 我 | wo3 | w | o3 |
| 学 | xue2 | x | ue2 |
| 中 | zhong1 | zh | ong1 |
优先级设计:zh/ch/sh 必须在 z/c/s 之前匹配,因为 zhong1 如果先匹配 z 会错误拆成 z + hong1。
def get_g2p_engine(language: str) -> G2PEngine:
lang_lower = language.lower() if language else "en"
if lang_lower in ("en", "english", "eng"):
return EnglishG2P()
elif lang_lower in ("zh", "chinese", "chi", "cn", "mandarin"):
return ChineseG2P()
else:
return EnglishG2P() # 默认回退英文这是本项目的核心创新模块,也是发音评价的推荐方案。
与 MFCC+DTW 评价器的核心区别在于:不再依赖标准参考音频,只需要参考文本。通过 ASR 理解学生说了什么,再用 G2P 将文字转为音素,在"音素语义层"做比对。
这个设计使得发音评价能够:
- 精准定位错误:不仅知道分数低,还知道"哪个词的哪个音素读错了"
- 区分错误类型:音素替换?遗漏?多读了不该读的音?
- 给出教学建议:告诉学生具体该练习什么
| 维度 | MFCC+DTW 评价器 | 音素评价器(推荐) |
|---|---|---|
| 参考输入 | 需要标准参考音频 | 仅需参考文本 ✅ |
| 评价粒度 | 整体评分 | 逐词、逐音素 ✅ |
| 错误类型 | 无 | 替换/遗漏/插入 ✅ |
| 语义理解 | 无(纯声学对比) | 有(结合 ASR 文本)✅ |
| 噪声敏感度 | 高 | 低(Whisper 鲁棒性强)✅ |
| 适用场景 | 跟读对比 | 自由发音、口语考试 |
flowchart TD
Input["📥 输入:学生录音 + 参考文本"] --> A1["Step 1: ASR 转录<br/>Faster-Whisper + word_timestamps"]
Input --> A2["Step 2: G2P 处理参考文本<br/>→ 期望音素序列"]
A1 --> A3["Step 3: G2P 处理 ASR 文本<br/>→ 实际音素序列"]
A1 --> A4["Step 4: 词级对齐<br/>difflib.SequenceMatcher"]
A2 --> A4
A3 --> A4
A4 --> A5["Step 5: 逐词音素比对<br/>音素级 SequenceMatcher"]
A5 --> A6["Step 6: 综合评分<br/>音素准确率 × 100 - 惩罚项"]
A6 --> Output["📤 输出:总分 + 逐词诊断"]
调用 ASRModelManager.transcribe(),必须开启 word_timestamps=True:
asr_result = self.asr_model.transcribe(
audio_path=audio_path,
word_timestamps=True, # 必需:用于后续对齐
language=language, # 透传给 Faster-Whisper
)为什么需要单词时间戳? 一方面用于输出中标注每个词的起止时间;另一方面如果未来扩展到"音节/音素级时间对齐",单词时间戳是基础。
# 参考文本 → [(word, [phonemes]), ...]
ref_word_phonemes = g2p.text_to_word_phoneme_pairs(reference_text)
# 例如 "hello world" → [("hello", ["HH","AH","L","OW"]), ("world", ["W","ER","L","D"])]
# ASR 文本 → [(word, [phonemes]), ...]
asr_word_phonemes = g2p.text_to_word_phoneme_pairs(spoken_text)使用 Python 标准库 difflib.SequenceMatcher,将参考文本的单词序列与 ASR 输出的单词序列对齐:
ref_words = [item[0] for item in ref_word_phonemes] # ["hello", "world"]
asr_words = [item[0] for item in asr_word_phonemes] # ["hello", "word"]
matcher = difflib.SequenceMatcher(None, ref_words, asr_words)
opcodes = matcher.get_opcodes()
# 返回: [("equal", 0,1,0,1), ("replace", 1,2,1,2)]
# → "hello" 匹配 "hello", "world" 替换为 "word"SequenceMatcher 的优势:直接返回 equal/replace/delete/insert 四种操作类型的结构化结果,天然映射为 correct/mispronounced/missing/extra 四种评价状态。
对每个对齐的词对,再次使用 SequenceMatcher 进行音素级比对:
ref_phons = ["L", "ER", "N", "IH", "NG"] # "Learning" 的期望音素
asr_phons = ["L", "AH", "N", "IH", "NG"] # 学生实际发的音
matcher = difflib.SequenceMatcher(None, ref_phons, asr_phons)
# → [("equal", 0,1,0,1), ("replace", 1,2,1,2), ("equal", 2,5,2,5)]
# → 音素"ER"被替换为"AH"(substitution 错误)三类错误:
| 错误类型 | opcode | 含义 | 示例 |
|---|---|---|---|
substitution |
replace | 音素被其他音素替换 | /ER/ → /AH/ |
deletion |
delete | 音素被遗漏 | "and" 读成 "an"(漏了 /D/) |
insertion |
insert | 多读了额外音素 | "cat" 读成 "cater"(多了 /ER/) |
# 基础分 = 音素准确率 × 100
base_score = phoneme_accuracy * 100
# 缺失词惩罚:每个没读的词扣 (20 / 总词数)
missing_penalty = (missing_count / ref_word_count) * 20
# 多余词惩罚:最多扣 10 分
extra_penalty = min(extra_count * 2, 10)
overall_score = max(0, base_score - missing_penalty - extra_penalty)评分哲学:惩罚设计反映了教学优先级——遗漏单词比读错单词更严重(说明学生根本没开口),所以缺失惩罚权重更大。
{
"overall_score": 85.3, # 综合评分 0-100
"phoneme_accuracy": 0.875, # 音素准确率
"word_count_reference": 5, # 参考词数
"word_count_spoken": 5, # 实际词数
"asr_transcript": "hello world", # ASR 转录文本
"level": "good", # 等级
"feedback": "发音良好。需重点练习的词汇:world",
"words": [{
"word": "world",
"spoken_word": "world",
"score": 75.0,
"expected_phonemes": ["W", "ER", "L", "D"],
"actual_phonemes": ["W", "ER", "L"],
"phoneme_accuracy": 0.75,
"errors": [
{"type": "deletion", "expected": "D", "actual": None, "position": 3}
],
"status": "mispronounced"
}]
}在 MemoryServer 中,PronunciationService 调用本接口实现完整的发音纠正流程:
Android 录音 → MemoryServer PronunciationService
→ HTTP multipart POST → MemoryServerTTS phoneme-score
→ 返回逐词/逐音素诊断
→ MemoryServer 解析结果,封装为客户端友好的格式
→ Android 展示:总分、每个词的发音状况、具体音素错误
这使得 Android 客户端可以做到:
- 高亮显示发音有问题的单词(红色=错误,黄色=有瑕疵,绿色=完美)
- 点击单词查看具体的音素错误详情
- 播放标准 TTS 发音进行对比
server.py 是整个服务的编排层,负责:
- 生命周期管理:启动时加载所有模型,确保服务就绪后才接受请求
- 路由分发:将 HTTP 请求路由到对应的处理器
- 并发控制:通过
asyncio.Lock序列化 TTS GPU 推理请求 - 安全处理:音频文件上传的多重校验和格式标准化
@app.on_event("startup")
async def startup_event():
app.state.tts_config = TTSConfig() # TTS 配置(解码/校验/词库缓存参数)
app.state.model = TTSModelManager(config=app.state.tts_config) # 加载 TTS 模型 (~20-60s)
app.state.asr_model = ASRModelManager() # 加载 ASR 模型 (~5-10s)
app.state.pronunciation_evaluator = PronunciationEvaluator() # 初始化 MFCC 评价器
app.state.phoneme_evaluator = PhonemeEvaluator(app.state.asr_model) # 初始化音素评价器(注入 ASR 依赖)
app.state.ocr_engine = OCREngine(...) # 初始化 OCR 引擎启动顺序:TTS(最耗时)→ ASR → 评价器 → OCR。所有模型作为 app.state 属性存储,依赖注入到路由函数中。
挂载的路由:tts / asr / pronunciation / ocr / dictation(词库缓存)/ dashboard(管理后台),另挂载 /tts-audio 静态目录(/stream 与 include_meta 返回的 audioUrl 依赖它)。
app.state.model_lock = asyncio.Lock()
# 所有 TTS 生成操作必须获取锁(含词库生成/校验,见 CONSTRAINTS.md P1)
async with app.state.model_lock:
wavs, sr, meta = model.generate(...)为什么需要锁? Qwen3-TTS 模型的 generate_custom_voice() 方法不是线程安全/协程安全的。并发调用会导致 GPU 显存竞争,产生如下问题:
- 两个请求的文本混在一起输出
- 显存溢出(OOM)
- CUDA 错误
asyncio.Lock 确保同一时刻只有一个 TTS 请求在 GPU 上推理,其他请求排队等待。由于 TTS 推理本身很快(通常 1-5 秒),锁的排队延迟在可接受范围内。注意:单词语音校验闭环(ASR 回读)也在锁内执行,听写请求延迟增加约 0.5-1.5s。
所有接收音频文件的接口都执行以下安全处理:
flowchart LR
A["1. 格式校验<br/>扩展名白名单"] --> B["2. 空文件检查<br/>os.path.getsize()"]
B --> C["3. 时长校验<br/>音素评分 ≥ 0.3s"]
C --> D["4. 格式标准化<br/>soundfile → 16-bit PCM WAV"]
D --> E["5. 临时文件清理<br/>BackgroundTask / finally"]
格式标准化原理:使用 soundfile 将任意支持的格式(WAV/MP3/FLAC/M4A)重编码为 16-bit PCM WAV,原因:
- Faster-Whisper 对某些编码格式兼容性不佳
- 统一格式避免后续处理链的格式问题
- 16-bit PCM 是最通用的无损格式
音素评价接口的输出自动转换为 camelCase,方便 Java/JavaScript 客户端使用:
def _snake_to_camel(data):
if isinstance(data, dict):
return {_to_camel(k): _snake_to_camel(v) for k, v in data.items()}
if isinstance(data, list):
return [_snake_to_camel(item) for item in data]
return data例如:overall_score → overallScore,phoneme_accuracy → phonemeAccuracy
| 方法 | 路径 | 请求格式 | 响应格式 | 锁保护 |
|---|---|---|---|---|
GET |
/api/v1/health |
— | JSON | — |
GET |
/api/v1/tts/voices |
— | JSON | — |
POST |
/api/v1/tts/synthesize |
JSON | audio/wav | ✅ |
WebSocket |
/api/v1/tts/stream |
JSON 消息帧 | JSON 音频帧 | ✅ |
POST |
/api/v1/tts/clone |
multipart | JSON | — |
POST |
/api/v1/asr/transcribe |
multipart | JSON | — |
GET |
/api/v1/asr/models |
— | JSON | — |
POST |
/api/v1/pronunciation/score |
multipart (2 files) | JSON | — |
POST |
/api/v1/pronunciation/batch-score |
JSON | JSON | — |
POST |
/api/v1/pronunciation/phoneme-score |
multipart | JSON (camelCase) | — |
POST |
/api/v1/pronunciation/phoneme-score-with-text |
multipart | JSON (camelCase) | — |
POST |
/api/v1/pronunciation/phoneme-batch-score |
JSON | JSON | — |
浏览器访问 http://localhost:8000/admin,集成各模块调试面板(单页 tab 结构):
- 系统概览:GPU/显存状态、模型加载状态
- TTS 合成 / ASR 识别 / 发音评价 / OCR 扫描:各模块功能调试与测速
- 🔥 词库管理:听写词库缓存统计、批量预生成(任务进度轮询)、词条表格(试听 / 标记 bad / 重新生成)
- 测速工作台:TTS/ASR/OCR 响应延迟基准
# 全量测试(约 6 秒,无需 GPU)
python -m unittest discover -s tests -p "test_*.py"
# 单模块
python -m unittest tests.test_tts_verifier tests.test_dictation_cache tests.test_tts_model_loader -v| 测试文件 | 覆盖内容 |
|---|---|
test_tts_verifier.py |
ASR 校验宽松匹配(分词/前缀/编辑距离)、置信度门槛、语言映射 |
test_dictation_cache.py |
缓存 key 稳定性/区分度/配置变更失效、原子写入、bad 标记、评分与 best-of-N 择优 |
test_tts_model_loader.py |
单词语音判定 is_single_word(含短句排除) |
test_official.py |
Qwen3-TTS 官方接口连通性(需 GPU + 模型) |
用于直接测试 Qwen3-TTS 官方 API,验证模型是否正确安装:
python tests/test_official.py执行流程:
- 检查 CUDA 是否可用
- 加载 1.7B 模型(优先本地,回退 HuggingFace)
- 生成一句英文测试语音
- 保存为
test_emotion_output.wav
python -m src.dictation.pregenerate --words ahead,behind,cat
python -m src.dictation.pregenerate --file words.csv --best-of 5基础 URL:http://<server-ip>:8000
所有接口统一前缀 /api/v1/。
| # | 方法 | 路径 | 说明 | 请求类型 | 响应类型 |
|---|---|---|---|---|---|
| 1 | GET |
/api/v1/health |
健康检查 | — | JSON |
| 2 | GET |
/api/v1/tts/voices |
音色列表 | — | JSON |
| 3 | POST |
/api/v1/tts/synthesize |
文本合成 WAV | JSON | binary/WAV |
| 4 | WS |
/api/v1/tts/stream |
流式合成 | WebSocket JSON | WebSocket JSON |
| 5 | POST |
/api/v1/tts/clone |
音色克隆(模拟) | multipart | JSON |
| 6 | POST |
/api/v1/asr/transcribe |
语音转录 | multipart | JSON |
| 7 | GET |
/api/v1/asr/models |
ASR 模型列表 | — | JSON |
| 8 | POST |
/api/v1/pronunciation/score |
MFCC+DTW 评分 | multipart | JSON |
| 9 | POST |
/api/v1/pronunciation/batch-score |
批量 MFCC 评分 | JSON | JSON |
| 10 | POST |
/api/v1/pronunciation/phoneme-score |
🔥 音素评分 | multipart | JSON (camelCase) |
| 11 | POST |
/api/v1/pronunciation/phoneme-score-with-text |
音素评分(别名) | multipart | JSON (camelCase) |
| 12 | POST |
/api/v1/pronunciation/phoneme-batch-score |
批量音素评分 | JSON | JSON |
POST /api/v1/tts/synthesize
将文本合成为语音,返回 WAV 格式的音频二进制流。
请求体 (JSON):
{
"text": "Hello, welcome to Memory English Learning App!",
"voice": "aiden",
"language": "English",
"instructions": "Speak with a happy and encouraging tone.",
"output_format": "wav"
}| 字段 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
text |
string | ✅ | — | 要合成的文本,支持中英文混合 |
voice |
string | ❌ | ""(自动) |
音色 ID(见下方音色表);留空按语言自动匹配母语音色(English→aiden) |
language |
string | ❌ | "English" |
语言:"Chinese", "English", "Japanese", "Korean" 等,兼容 en/zh 简写 |
instructions |
string | ❌ | null |
情感/风格指令,如 "Speak sadly", "Whisper softly"(业务侧传入,服务端不注入) |
output_format |
string | ❌ | "wav" |
输出格式,当前仅支持 "wav" |
verify |
bool|null | ❌ | null |
null=自动(仅单词语音走 ASR 校验闭环);true=强制(仅对单词语音生效);false=跳过 ASR 校验 |
seed |
int|null | ❌ | null |
基础随机种子(单词语音重试时 seed+i) |
include_meta |
bool | ❌ | false |
true 返回 JSON(含 verified/attempts/asrText),否则返回 WAV 流(校验信息在 X-TTS-* 响应头) |
成功响应:
- 状态码:
200 OK - Content-Type:
audio/wav - 响应体:二进制 WAV 音频数据
- 文件名:
tts_<32位hex>.wav - 响应头:
X-TTS-Verified/X-TTS-Attempts/X-TTS-Strategy/X-TTS-Duration/X-TTS-Asr-Text/X-TTS-Asr-Confidence
错误响应:
{ "detail": "Only wav output is supported currently." }音色速查表(与模型 README 官方清单一致):
| 音色 ID | 母语 | 性别 | 风格描述 |
|---|---|---|---|
ryan |
英文 | 男 | 富有节奏感、动感 |
aiden |
英文 | 男 | 阳光、音色明亮 |
vivian |
中文 | 女 | 明亮、略带锋芒 |
serena |
中文 | 女 | 温暖、温柔 |
uncle_fu |
中文 | 男 | 低沉柔和,经验丰富 |
dylan |
中文(北京话) | 男 | 清晰自然 |
eric |
中文(四川话) | 男 | 活泼、沙哑明亮 |
ono_anna |
日文 | 女 | 轻盈灵巧 |
sohee |
韩文 | 女 | 情感丰富 |
⚠️ 音色 ID 不区分大小写(模型侧自动归一);官方建议使用音色母语生成以获得最佳质量。旧文档中的 "Ono_Anna 英文男声" 为错误标注——Ono_Anna 是日语女声。
SpringBoot 调用示例:
// 构建请求
JSONObject body = new JSONObject();
body.put("text", "Hello, how are you?");
body.put("voice", "aiden");
body.put("language", "English");
body.put("instructions", "Speak cheerfully.");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> request = new HttpEntity<>(body.toString(), headers);
// 发送请求,获取 WAV 字节
ResponseEntity<byte[]> response = restTemplate.postForEntity(
"http://localhost:8000/api/v1/tts/synthesize", request, byte[].class);
byte[] wavBytes = response.getBody();
// 保存为文件
Files.write(Paths.get("output.wav"), wavBytes);curl 示例:
curl -X POST http://localhost:8000/api/v1/tts/synthesize \
-H "Content-Type: application/json" \
-d '"'"'{"text":"Hello world","voice":"aiden","language":"English"}'"'"' \
--output output.wavWebSocket /api/v1/tts/stream
适用于长文本或需要低延迟首字响应的场景。通过 WebSocket 分块发送文本,服务端实时返回 PCM16 音频块。
连接地址:ws://localhost:8000/api/v1/tts/stream
客户端 → 服务端(文本块):
{
"type": "text_chunk",
"data": "Hello, welcome to Memory English Learning App!",
"voice": "aiden",
"language": "English",
"instructions": "Speak with a happy tone."
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
type |
string | ✅ | 固定为 "text_chunk" |
data |
string | ✅ | 待合成文本片段 |
voice |
string | ❌ | 音色 ID,默认 "aiden"(英文) |
language |
string | ❌ | 语言,默认 "English" |
instructions |
string | ❌ | 情感指令 |
客户端 → 服务端(结束信号):
{ "type": "end" }服务端 → 客户端(音频块):
{
"type": "audio_chunk",
"sample_rate": 24000,
"format": "pcm16",
"data": "<base64_encoded_pcm_bytes>"
}| 字段 | 类型 | 说明 |
|---|---|---|
type |
string | "audio_chunk" |
sample_rate |
int | 采样率(Hz) |
format |
string | "pcm16"(16-bit 有符号小端序 PCM) |
data |
string | Base64 编码的 PCM 音频数据 |
PCM 数据解码(Java):
byte[] pcmBytes = Base64.getDecoder().decode(base64Data);
// PCM 16-bit 小端序 → 可保存为 WAV 或直接播放
// WAV 文件 = 44字节头部 + PCM 数据服务端 → 客户端(结束/错误):
{ "type": "end_of_stream" }
{ "type": "error", "message": "错误描述" }POST /api/v1/asr/transcribe
将音频文件转录为文字。
请求:multipart/form-data
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
audio |
File | ✅ | — | 音频文件(.wav/.mp3/.flac/.m4a) |
language |
string | ❌ | null(自动检测) |
语言代码:"en", "zh", "ja" 等 |
task |
string | ❌ | "transcribe" |
"transcribe" 转录 / "translate" 译为英文 |
beam_size |
int | ❌ | 5 |
束搜索宽度 |
word_timestamps |
bool | ❌ | false |
是否返回单词级时间戳 |
成功响应:
{
"text": "Hello, welcome to Memory English Learning App.",
"language": "en",
"language_probability": 0.98,
"segments": [
{
"id": 0,
"start": 0.0,
"end": 3.5,
"text": " Hello, welcome to Memory English Learning App.",
"confidence": -0.08,
"words": [
{ "word": "Hello", "start": 0.0, "end": 0.4, "probability": 0.99 },
{ "word": "welcome", "start": 0.5, "end": 1.1, "probability": 0.97 }
]
}
]
}| 字段 | 类型 | 说明 |
|---|---|---|
text |
string | 完整转录文本 |
language |
string | 检测到的语言代码 |
language_probability |
float | 语言检测置信度 |
segments[].start |
float | 该段起始时间(秒) |
segments[].end |
float | 该段结束时间(秒) |
segments[].confidence |
float | 平均对数概率(负值,越接近0越好) |
segments[].words |
array | 单词时间戳(仅 word_timestamps=true) |
curl 示例:
curl -X POST http://localhost:8000/api/v1/asr/transcribe \
-F "audio=@recording.wav" \
-F "language=en" \
-F "word_timestamps=true"POST /api/v1/pronunciation/score
请求:multipart/form-data
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
student_audio |
File | ✅ | 学生录音(.wav/.mp3/.flac) |
reference_audio |
File | ✅ | 标准参考音频(同格式) |
响应:
{
"score": 85.3,
"distance": 147.0,
"max_distance": 1000,
"level": "good",
"feedback": "发音良好,注意个别音节的准确性"
}| 字段 | 类型 | 说明 |
|---|---|---|
score |
float | 综合评分 0-100 |
distance |
float | DTW 距离值(越小越好) |
level |
string | excellent(≥90) / good(≥75) / fair(≥60) / poor(≥40) / very_poor(<40) |
feedback |
string | 中文反馈建议 |
POST /api/v1/pronunciation/phoneme-score
核心优势:不需要标准参考音频,只需参考文本。能精确定位到"哪个词的哪个音素"发错了。
请求:multipart/form-data
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
student_audio |
File | ✅ | — | 学生录音(.wav/.mp3/.flac/.m4a),自动标准化 |
reference_text |
string | ✅ | — | 期望朗读的参考文本 |
language |
string | ❌ | null(英语) |
"en" 英文 / "zh" 中文 |
⚠️ 音频要求:时长 ≥ 0.3 秒,非空非静音。上传后自动标准化为 16-bit PCM WAV。
成功响应(camelCase 格式):
{
"overallScore": 85.3,
"phonemeAccuracy": 0.92,
"wordCountReference": 5,
"wordCountSpoken": 5,
"asrTranscript": "Hello, welcome to Memory English Learning App.",
"referenceText": "Hello, welcome to Memory English Learning App.",
"level": "good",
"feedback": "发音良好。需重点练习的词汇:Learning",
"words": [
{
"word": "Learning",
"spokenWord": "Learning",
"startTime": 2.0,
"endTime": 2.8,
"score": 75.0,
"expectedPhonemes": ["L", "ER", "N", "IH", "NG"],
"actualPhonemes": ["L", "AH", "N", "IH", "NG"],
"phonemeAccuracy": 0.8,
"errors": [
{
"type": "substitution",
"expected": "ER",
"actual": "AH",
"position": 1
}
],
"status": "mispronounced"
}
]
}| 字段 | 类型 | 说明 |
|---|---|---|
overallScore |
float | 综合评分 0-100 |
phonemeAccuracy |
float | 全局音素准确率 |
words[].status |
string | correct / mispronounced / missing / extra |
words[].errors[].type |
string | substitution / deletion / insertion |
words[].errors[].expected |
string | null |
words[].errors[].actual |
string | null |
⚠️ 重要:该接口响应字段为 camelCase(如overallScore),与其他接口的 snake_case 不同。这是通过_snake_to_camel()自动转换的。
SpringBoot 调用示例:
LinkedMultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("student_audio", new FileSystemResource("recording.wav"));
body.add("reference_text", "Hello, welcome to Memory English Learning App!");
body.add("language", "en");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<LinkedMultiValueMap<String, Object>> request = new HttpEntity<>(body, headers);
JSONObject result = restTemplate.postForObject(
"http://localhost:8000/api/v1/pronunciation/phoneme-score",
request, JSONObject.class);
double score = result.getDouble("overallScore"); // camelCase!
JSONArray words = result.getJSONArray("words");curl 示例:
curl -X POST http://localhost:8000/api/v1/pronunciation/phoneme-score \
-F "student_audio=@recording.wav" \
-F "reference_text=Hello world" \
-F "language=en"GET /api/v1/health
响应:
{
"status": "healthy",
"model_loaded": true,
"gpu_available": true,
"gpu_utilization_percent": 15,
"vram_used_mb": 3584.0,
"vram_total_mb": 8192.0
}MemoryServer 的
TTSService在每次调用前都会先调此接口确认服务可用。若返回unhealthy,MemoryServer 会跳过 TTS 调用并使用备用方案。
GET /api/v1/tts/voices
返回所有可用音色(9 个预设 + voices/ 目录下的克隆音色)。详见 音色速查表。
GET /api/v1/asr/models
{
"models": ["tiny", "base", "small", "medium", "large-v1", "large-v2", "large-v3", "distil-large-v2"]
}graph TB
subgraph "Android 客户端"
APP["MemoryApp"]
end
subgraph "MemoryServer (Java SpringBoot :8080)"
CONV["ConversationService<br/>AI 对话"]
PRON["PronunciationService<br/>发音纠正"]
TTS_S["TTSService<br/>音频管理"]
DICT["DictationService<br/>听写练习"]
end
subgraph "MemoryServerTTS (Python FastAPI :8000)"
TTS_API["/tts/synthesize<br/>/tts/stream"]
ASR_API["/asr/transcribe"]
PHON_API["/pronunciation/phoneme-score"]
end
subgraph "外部 AI"
DS["DeepSeek API"]
end
APP -->|"对话/发音/听写"| CONV
APP -->|"发音纠正"| PRON
CONV -->|"语音合成"| TTS_API
CONV -->|"语音识别"| ASR_API
CONV -->|"对话生成"| DS
PRON -->|"音素评价"| PHON_API
TTS_S -->|"单词/句子合成"| TTS_API
DICT -->|"听写音频合成"| TTS_API
MemoryServer 通过调用 MemoryServerTTS 的 TTS 接口获取音频,然后管理两类音频的生命周期:
| 音频类型 | TTS 端点 | MemoryServer 存储 | 生命周期 |
|---|---|---|---|
| 会话音频(AI 对话 TTS) | /tts/synthesize |
tts-audio/ 根目录 |
定时清理(默认 7 天) |
| 单词音频(学习/听写) | 🔥 /api/v1/dictation/audio |
word-cache/(服务端缓存) |
永久保留,命中零生成;由管理员预生成/标记 bad/重生成 |
| 听写语境音频 | /tts/synthesize |
dictation_audio_cache 表 |
30 天未访问即清理 |
单词音频复用(2026-08 起):听写单词音频改由 MemoryServerTTS 侧词库缓存提供——
GET /api/v1/dictation/audio?word=ahead&voice=...&language=...&instruct=...。 缓存 key = hash(单词|音色|语言|instruct|gen_config_version),命中直接返回(零生成延迟); 未命中自动生成并回填;管理员通过/admin"词库管理"页或 CLI(python -m src.dictation.pregenerate)预生成/试听/标记 bad/重生成。 旧方案(客户端自管tts-audio/words/{word}.wav)仍可用,但不再推荐——服务端缓存自带 ASR 校验闭环与版本换代。instruct由业务侧传入并参与缓存 key:不同情绪指令 = 不同音频条目,互不污染。
sequenceDiagram
participant User
participant App as 📱 Android
participant MS as 🖥️ MemoryServer
participant TTS as 🎵 MemoryServerTTS
participant AI as 🤖 DeepSeek
User->>App: 🎤 录音 "How are you?"
App->>MS: POST /conversation/message (audio)
Note over MS,TTS: Step 1: ASR 语音转文字
MS->>TTS: POST /api/v1/asr/transcribe
TTS-->>MS: { text: "How are you", language: "en" }
Note over MS,AI: Step 2: AI 对话生成
MS->>AI: 对话请求 (含历史上下文)
AI-->>MS: { reply: "I'm doing great, thanks!" }
Note over MS,TTS: Step 3: TTS 语音合成
MS->>TTS: POST /api/v1/tts/synthesize
TTS-->>MS: WAV 音频文件
Note over MS,AI: Step 4: 对话质量评估
MS->>AI: 评估请求
AI-->>MS: { pronunciation:4, fluency:4, grammar:4, vocabulary:4, overall:4 }
MS-->>App: { aiReply, audioUrl, evaluation }
App->>User: 🔊 播放 AI 语音 + 📊 显示五维评分
这个序列图中,MemoryServerTTS 提供了 Step 1(ASR)和 Step 3(TTS)两个关键能力,是整个对话流程不可替代的语音层。
sequenceDiagram
participant User
participant App as 📱 Android
participant MS as 🖥️ MemoryServer
participant TTS as 🎵 MemoryServerTTS
User->>App: 🎤 跟读 "welcome"
App->>MS: POST /pronunciation/correct<br/>(multipart: audio + referenceText)
MS->>TTS: POST /api/v1/pronunciation/phoneme-score<br/>(multipart: audio + reference_text + language)
Note over TTS: ASR → G2P → 音素对齐 → 综合评分
TTS-->>MS: { overallScore, words: [{ word, status, errors }] }
MS->>MS: 解析诊断结果<br/>封装为客户端格式
MS-->>App: { overallScore: 85, words: [{ word:"welcome", score:80, errors:[...] }] }
App->>User: 🟡 "welcome" 发音有瑕疵,点击查看音素详情
| 变量名 | 默认值 | 说明 |
|---|---|---|
QWEN_TTS_MODEL_PATH |
./models/qwen-1.7b |
主 TTS 模型本地路径(1.7B) |
QWEN_TTS_MODEL |
Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice |
主 TTS 模型 HF ID |
TTSCONF_MODEL_PATH |
同 QWEN_TTS_MODEL_PATH |
TTSConfig 环境变量覆盖(词库 CLI 用它强制 1.7B) |
WHISPER_MODEL_SIZE |
base |
Faster-Whisper 模型大小 |
RELOAD |
0 |
是否启用热重载(1/true 启用) |
LOG_LEVEL |
INFO |
日志级别(DEBUG/INFO/WARN/ERROR) |
HF_HUB_DISABLE_SSL_VERIFY |
(未设置) | 禁用 SSL 验证(内网环境用) |
TTSConfig 支持
TTSCONF_<KEY>形式的环境变量覆盖任意 yaml 配置项(env_prefix = "TTSCONF_")。
models/
├── qwen-1.7b/ # 主模型(默认,~3.4 GB 显存,支持 instruct)
│ ├── model.safetensors # 模型权重
│ ├── config.json / generation_config.json # 模型/生成配置
│ ├── tokenizer_config.json # 分词器配置
│ ├── vocab.json + merges.txt # 词表
│ └── speech_tokenizer/ # 语音分词器
│
└── qwen-0.6b/ # 降级备选模型(~1.2 GB 显存,不支持 instruct)
└── ...(结构同上)
flowchart LR
A["启动"] --> B{"本地 1.7B?"}
B -->|是| C["加载 1.7B ✅"]
B -->|否| D{"HF 1.7B?"}
D -->|是| C
D -->|否| E{"本地 0.6B?"}
E -->|是| F["加载 0.6B ✅(丢 instruct)"]
E -->|否| G{"HF 0.6B?"}
G -->|是| F
G -->|否| H["❌ 启动失败"]
| 配置项 | 默认值 | 说明 |
|---|---|---|
tts.model_path / tts.fallback_model_path |
./models/qwen-1.7b / ./models/qwen-0.6b |
主/备模型路径 |
tts.engine |
sdpa |
注意力实现(sdpa | flash_attention_2) |
tts.dtype |
bfloat16 |
计算精度 |
tts.default_voice / tts.default_language |
aiden / English |
兜底音色/语言(voice 留空时按语言自动匹配母语音色) |
tts.performance.compile |
false |
torch.compile 优化(首次编译 2-5 分钟) |
tts.text.verify_text_threshold |
40 |
is_single_word 单词长度兜底上限 |
tts.decoding.short |
temp 0.5 / top_k 20 / rep 1.2 / max_new 512 / seed 42 | 单词语音确定性解码参数 |
tts.decoding.long |
temp 0.9 / max_new 2048 | 短句/长文本随机采样参数 |
tts.verification.max_retries |
3 |
单词语音 ASR 校验最大重试(换 seed) |
tts.verification.asr_confidence_threshold |
-1.0 |
ASR 置信度门槛 |
tts.verification.word_max_duration_s |
5.0 |
单词音频时长上限 |
tts.dictation.cache_dir |
word-cache |
词库缓存目录 |
tts.dictation.best_of / seed_base |
3 / 1000 |
预生成候选数与 seed 起点 |
修改
decoding.*/verification.*/model_path会自动改变gen_config_version,词库缓存条目随之失效换代(见 CONSTRAINTS.md D2)。
# 1. 创建虚拟环境
conda create -n memory-tts python=3.12
conda activate memory-tts
# 2. 安装依赖
pip install -r requirements.txt
# 3. 确保模型文件在 models/ 目录下
# 4. 启动服务
python main.py
# 或启用热重载(开发模式)
set RELOAD=1 && python main.py # Windows
RELOAD=1 python main.py # Linux/macOSFROM python:3.12-slim
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "src/server.py"]# 构建
docker build -t memory-tts .
# 运行(GPU 模式)
docker run -d -p 8000:8000 --gpus all -v ./models:/app/models memory-tts
# 运行(CPU 模式,速度较慢)
docker run -d -p 8000:8000 memory-tts- GPU 选择:推荐 NVIDIA T4 / A10 / A100,至少 8 GB 显存
- 反向代理:Nginx 提供 SSL 终止和请求缓存
- 进程管理:使用 systemd / supervisor 确保崩溃自动重启
- 健康监控:定时调用
/api/v1/health - 模型预热:启动后发送一次预热请求,避免首个请求超时
- 多实例:多 GPU 场景可部署多个实例 + 负载均衡
# 健康检查
curl http://localhost:8000/api/v1/health
# 音色列表
curl http://localhost:8000/api/v1/tts/voices
# TTS 合成测试
curl -X POST http://localhost:8000/api/v1/tts/synthesize \
-H "Content-Type: application/json" \
-d '"'"'{"text":"Hello world","voice":"aiden","language":"English"}'"'"' \
--output test.wav
# ASR 转录测试
curl -X POST http://localhost:8000/api/v1/asr/transcribe \
-F "audio=@test.wav" -F "language=en"
# 音素发音评价测试
curl -X POST http://localhost:8000/api/v1/pronunciation/phoneme-score \
-F "student_audio=@recording.wav" \
-F "reference_text=hello world" \
-F "language=en"浏览器访问 http://localhost:8000/admin 打开调试面板(含词库管理,见 4.7 节)。旧版 Gradio 调试界面(debug_ui.py)已移除。
服务启动后访问:
- Swagger UI:
http://localhost:8000/docs - ReDoc:
http://localhost:8000/redoc
- 命名:Python 使用
snake_case,API 输出支持 camelCase 转换 - 类型注解:所有函数参数和返回值均使用类型注解
- 错误处理:
HTTPException返回统一格式{"detail": "..."} - 🔥 架构约束:TTS 服务端不注入情绪指令、单词语音判定规则、缓存换代机制、模型锁并发边界等必须遵守的约束见
docs/CONSTRAINTS.md(修改相关代码前必读)
UserWarning: flash-attn is not installed, using pytorch sdpa implementation
原因:flash-attn 是可选优化库,非必需。
解决:忽略此警告,项目已使用 PyTorch 原生 SDPA(Scaled Dot-Product Attention)替代。
RuntimeError: CUDA out of memory
解决:
- 确保使用 0.6B 小模型(默认)
- 关闭其他占用显存的程序(如
nvidia-smi查看) - 降级到 CPU 模式(移除 CUDA 版 PyTorch)
requests.exceptions.SSLError: certificate verify failed
解决:
set HF_HUB_DISABLE_SSL_VERIFY=1 # Windows
export HF_HUB_DISABLE_SSL_VERIFY=1 # Linux/macOS或直接使用本地模型文件(放在 models/ 目录下)。
现象:服务启动时模型加载失败。
排查步骤:
- 检查
models/qwen-0.6b/和models/qwen-1.7b/目录是否存在 - 检查 HuggingFace 网络连通性
- 检查 PyTorch 版本与 CUDA 版本匹配
- 如两套模型都失败,服务将抛出
RuntimeError无法启动
现象:POST /api/v1/pronunciation/phoneme-score 返回 400。
常见原因:
- 未传入
reference_text非文件参数(需使用Form(...)声明) - 音频格式不支持(仅
.wav/.mp3/.flac/.m4a) - 音频为空或时长 < 0.3 秒
# Windows
netstat -ano | findstr :8000
# 在任务管理器中结束对应 PID
# Linux
lsof -i :8000
kill -9 <PID>| 音色 ID | 名称 | 母语 | 性别 | 风格描述 | 类型 |
|---|---|---|---|---|---|
ryan |
Ryan | 英文 | 男 | 富有节奏感的动感男声 | preset |
aiden |
Aiden | 英文 | 男 | 阳光美式男声,音色明亮 | preset |
vivian |
Vivian | 中文 | 女 | 明亮、略带锋芒的年轻女性声音 | preset |
serena |
Serena | 中文 | 女 | 温暖、温柔的年轻女性声音 | preset |
uncle_fu |
Uncle_Fu | 中文 | 男 | 经验丰富的男性嗓音,低沉柔和 | preset |
dylan |
Dylan | 中文(北京话) | 男 | 年轻北京男性,清晰自然 | preset |
eric |
Eric | 中文(四川话) | 男 | 活泼成都男声,沙哑明亮 | preset |
ono_anna |
Ono_Anna | 日文 | 女 | 活泼日本女性,轻盈灵巧 | preset |
sohee |
Sohee | 韩文 | 女 | 温暖韩国女性,情感丰富 | preset |
与模型 README 官方清单一致;
voice留空时按language自动匹配母语音色(English→aiden、Chinese→vivian、Japanese→ono_anna、Korean→sohee)。
| 分数范围 | 等级 | 反馈建议 |
|---|---|---|
| 90-100 | excellent | 发音非常标准,继续保持! |
| 75-89 | good | 发音良好,注意个别音节的准确性 |
| 60-74 | fair | 发音基本正确,需要加强练习 |
| 40-59 | poor | 发音有待改进,建议多听标准发音 |
| 0-39 | very_poor | 发音需要大幅改进,建议从基础音标开始练习 |
| 模型 | 参数量 | 相对速度 | 英文 WER | 推荐显存 | 适用场景 |
|---|---|---|---|---|---|
tiny |
39M | 10× | 7.5% | 1 GB | 低资源设备 |
base |
74M | 6× | 5.5% | 1.5 GB | 当前默认 |
small |
244M | 2.5× | 4.1% | 2.5 GB | 精度与速度平衡 |
medium |
769M | 1× | 3.4% | 5 GB | 高精度需求 |
large-v3 |
1.55B | 0.4× | 2.7% | 10 GB | 最高精度 |
当前服务通过
WHISPER_MODEL_SIZE=base环境变量配置,默认使用base模型。
| 类别 | 音素 | 示例词 | 发音提示 |
|---|---|---|---|
| 元音 | AA | father | 大开口低元音 |
| 元音 | AE | cat | 次低前元音 |
| 元音 | AH | hut | 中央元音 |
| 元音 | AO | dog | 半低后圆唇元音 |
| 元音 | AW | how | AA+UH 双元音 |
| 元音 | AY | my | AA+IH 双元音 |
| 元音 | EH | bed | 半低前元音 |
| 元音 | ER | her | 卷舌中央元音 |
| 元音 | EY | say | EH+IH 双元音 |
| 元音 | IH | bit | 次高前元音 |
| 元音 | IY | beat | 高前元音 |
| 元音 | OW | go | AH+UH 双元音 |
| 元音 | OY | boy | AO+IH 双元音 |
| 元音 | UH | put | 次高后圆唇元音 |
| 元音 | UW | too | 高后圆唇元音 |
| 塞音 | P | pen | 双唇清塞音 |
| 塞音 | B | bad | 双唇浊塞音 |
| 塞音 | T | tea | 齿龈清塞音 |
| 塞音 | D | dog | 齿龈浊塞音 |
| 塞音 | K | cat | 软腭清塞音 |
| 塞音 | G | go | 软腭浊塞音 |
| 擦音 | F | fish | 唇齿清擦音 |
| 擦音 | V | van | 唇齿浊擦音 |
| 擦音 | TH | thin | 齿间清擦音 |
| 擦音 | DH | this | 齿间浊擦音 |
| 擦音 | S | sit | 齿龈清擦音 |
| 擦音 | Z | zoo | 齿龈浊擦音 |
| 擦音 | SH | she | 腭龈清擦音 |
| 擦音 | ZH | measure | 腭龈浊擦音 |
| 塞擦音 | CH | chin | T+SH 组合 |
| 塞擦音 | JH | jump | D+ZH 组合 |
| 鼻音 | M | mom | 双唇鼻音 |
| 鼻音 | N | no | 齿龈鼻音 |
| 鼻音 | NG | sing | 软腭鼻音 |
| 流音 | L | light | 齿龈边音 |
| 流音 | R | red | 卷舌近音 |
| 滑音 | W | wet | 双唇软腭近音 |
| 滑音 | Y | yes | 硬腭近音 |
| 清声门擦音 | HH | hat | 声门清擦音 |