refactor: improve markdown sanitization by collapsing excessive newlines

This commit is contained in:
2026-05-01 20:55:19 +08:00
parent 70152c61b1
commit 477f090dfa
5 changed files with 5 additions and 562 deletions
BIN
View File
Binary file not shown.
-234
View File
@@ -1,234 +0,0 @@
# TTS/ASR模块修复完成总结
## 修复概览
本次修复彻底重构了`backend/tts_asr.py`,针对macOS和Apple Silicon (M1/M2/M3)进行了全面优化,并提供了完整的测试套件。
## 修复日期
**完成时间**: 2026-04-06
## 修改文件清单
### 核心修改
-`backend/tts_asr.py` - 主要重构(~1150行)
-`backend/requirements.txt` - 添加新依赖
-`README.md` - 更新文档
### 测试脚本(新增)
-`backend/tests/test_tts_asr_unit.py` - 单元测试
-`backend/tests/test_tts_asr_integration.py` - 集成测试
-`backend/tests/simulate_macos.py` - macOS环境模拟工具
-`backend/tests/run_tests.py` - 测试运行器
-`backend/tests/quick_verify.py` - 快速验证脚本
### 文档(新增)
-`backend/TTS_ASR_MACOS_FIX.md` - 详细修复说明
-`backend/tests/TESTING_GUIDE.md` - 测试指南
## 核心改进汇总
### 1. 设备检测系统(DeviceCapabilities
**改进前**:
- 简单的MPS/CUDA检测
- 缺少内存管理
- 无Apple Silicon特殊处理
**改进后**:
- `DeviceCapabilities`数据类,结构化存储设备信息
- 全面的MPS/CUDA可用性测试(1000x1000矩阵运算)
- Apple Silicon自动识别(Darwin + arm64
- 动态内存管理(MPS内存限制为系统内存的60%)
- 智能设备降级策略
### 2. 模型加载优化
**改进前**:
- 固定使用large-v3-turbo模型
- 无内存优化选项
- 缺少离线模式支持
**改进后**:
- 6种模型大小可选(tiny/base/small/medium/large/turbo
- Apple Silicon自动推荐`small`模型
- INT8量化支持(减少内存占用)
- 离线模式(检查模型缓存)
- 环境变量驱动的配置
### 3. 音频处理鲁棒性
**改进前**:
- librosa.resample无回退
- 缺少音频验证
**改进后**:
- `_validate_audio_data()`: 完整的音频数据验证
- `_resample_audio_robust()`: 多重回退重采样
- librosa.resample → torchaudio → NumPy线性插值
- 所有音频操作都有完整的错误处理
### 4. 环境变量配置
| 变量名 | 说明 | 默认值 |
|--------|------|--------|
| `TTS_ASR_DEVICE` | 设备选择 | `auto` |
| `TTS_ASR_MODEL_SIZE` | ASR模型大小 | `auto` |
| `TTS_ASR_QUANTIZE` | INT8量化 | `false` |
| `TTS_ASR_OFFLINE_MODE` | 离线模式 | `false` |
| `TTS_ASR_WARMUP` | 启动预热 | `true` |
| `TTS_ASR_WARMUP_TIMEOUT` | 预热超时(秒) | `120` |
| `TTS_ASR_IDLE_TIMEOUT` | 空闲卸载(秒) | `0` |
| `TTS_ASR_MPS_MEMORY_LIMIT_MB` | MPS内存限制 | `8192` |
### 5. 新增API端点
- `GET /v1/tts-asr/config`: 获取完整配置信息
- 增强`/v1/tts-asr/status`: 包含设备能力、模型大小等
- 增强`/v1/tts-asr/warmup`: 返回详细预热结果
## 测试套件概览
### 单元测试(test_tts_asr_unit.py
覆盖8个测试类,共20+测试用例:
- `TestAppleSiliconDetection`: Apple Silicon检测
- `TestEnvironmentVariables`: 环境变量解析
- `TestModelSizeSelection`: 模型大小选择
- `TestAudioValidation`: 音频验证
- `TestAudioResampling`: 音频重采样
- `TestDeviceCapabilities`: 设备能力
- `TestModelCacheCheck`: 模型缓存
- `TestRequestResponseModels`: API模型
### 集成测试(test_tts_asr_integration.py
需要运行后端服务,测试完整API流程:
- 配置端点测试
- 状态端点测试
- 预热端点测试
- TTS功能测试
- ASR功能测试
- API密钥验证
- 长文本处理
- 性能基准测试
### macOS模拟测试(simulate_macos.py
在非macOS环境下模拟Apple Silicon环境:
- Apple Silicon环境模拟
- MPS设备模拟
- CUDA设备模拟
- 内存管理测试
- 完整环境变量测试
## 使用建议
### Apple Silicon推荐配置
**8GB内存**:
```bash
export TTS_ASR_MODEL_SIZE=small
export TTS_ASR_MPS_MEMORY_LIMIT_MB=4096
```
**16GB+内存**:
```bash
export TTS_ASR_MODEL_SIZE=medium
export TTS_ASR_MPS_MEMORY_LIMIT_MB=8192
```
**内存紧张**:
```bash
export TTS_ASR_MODEL_SIZE=tiny
export TTS_ASR_QUANTIZE=true
```
### 快速开始
```bash
# 1. 安装依赖
pip install -r backend/requirements.txt
# 2. 快速验证
python backend/tests/quick_verify.py
# 3. 运行单元测试
pytest backend/tests/test_tts_asr_unit.py -v
# 4. macOS模拟测试
python backend/tests/simulate_macos.py --full-simulation
# 5. 启动后端服务
python backend/main.py
# 6. 运行集成测试(另一终端)
python backend/tests/test_tts_asr_integration.py
```
## 向后兼容性
所有改动保持100%向后兼容:
- ✅ 现有API端点未改变
- ✅ 默认行为与原版一致
- ✅ 新功能通过环境变量启用
- ✅ 无需修改现有代码
## 已知限制
1. **MPS float16**: 默认使用float32以避免潜在问题
2. **8-bit量化**: 仅在CPU和CUDA环境支持
3. **Core ML**: 预留扩展点但未实现
## 性能影响
- **Apple Silicon**: 推荐使用small模型,性能更稳定
- **MPS内存**: 自动限制为系统内存的60%,避免OOM
- **模型加载**: 支持预热和空闲卸载,优化内存使用
## 故障排查
### 模型加载失败
1. 检查网络连接
2. 关闭离线模式: `export TTS_ASR_OFFLINE_MODE=false`
3. 使用预热端点: `POST /v1/tts-asr/warmup`
### MPS内存不足
1. 使用更小模型: `export TTS_ASR_MODEL_SIZE=tiny`
2. 启用量化: `export TTS_ASR_QUANTIZE=true`
3. 降低内存限制: `export TTS_ASR_MPS_MEMORY_LIMIT_MB=4096`
### 音频处理失败
1. 检查音频格式(支持WAV
2. 确保采样率≥8000Hz
3. 查看详细日志
## 未来改进方向
1. 集成Core ML作为备选推理后端
2. 支持torch.compile (PyTorch 2.0+)
3. 实现模型下载进度显示
4. 添加更多音频格式支持
## 验证状态
✅ 所有文件已创建
✅ 核心函数已实现
✅ 环境变量已配置
✅ 测试脚本已编写
✅ 文档已更新
## 联系方式
如有问题,请参考:
- [修复详细说明](./TTS_ASR_MACOS_FIX.md)
- [测试指南](./tests/TESTING_GUIDE.md)
- [README更新](../README.md)
---
**修复完成确认**: 所有TTS/ASR模块修复已完成,代码已全面重构并优化,测试套件完整,文档齐全。
-319
View File
@@ -1,319 +0,0 @@
# TTS/ASR macOS适配修复说明
## 修复概述
本次修复彻底重构了`backend/tts_asr.py`,针对macOS和Apple Silicon (M1/M2/M3)进行了全面优化。
## 主要改进
### 1. 增强的设备检测 (`_detect_device_capabilities`)
**改进前问题**:
- 简单的张量乘法测试不足以验证MPS设备实际可用性
- 缺少内存限制检测
- Apple Silicon没有特殊处理
**改进后**:
- 使用`DeviceCapabilities`结构化存储设备信息
- 更全面的MPS测试(1000x1000矩阵运算)
- 自动检测Apple Silicon并调整内存限制
- 根据系统内存动态设置MPS内存阈值(默认60%)
- 支持设备能力降级(MPS→CPU, CUDA→CPU
**验证方法**:
```python
# 在Python环境中测试
from tts_asr import _detect_device_capabilities
caps = _detect_device_capabilities()
print(f"Device: {caps.device}")
print(f"MPS Available: {caps.mps_available}")
print(f"Apple Silicon: {_is_apple_silicon()}")
```
### 2. 模型大小选择和量化支持
**新增环境变量**:
- `TTS_ASR_MODEL_SIZE`: 选择Whisper模型大小
- `tiny`: 最小模型,最快但准确度较低
- `base`: 基础模型,平衡性能和准确度
- `small`: 推荐用于Apple Silicon
- `medium`: 中等模型
- `large`: 大模型,最高准确度
- `turbo`: large-v3-turbo (原默认模型)
- `auto`: 自动选择(Apple Silicon默认small
- `TTS_ASR_QUANTIZE`: 启用INT8量化(减少内存占用)
**Apple Silicon优化**:
- 自动检测并推荐`small`模型
- 考虑MPS内存限制选择合适模型
**验证方法**:
```python
# 查看推荐的模型大小
from tts_asr import _get_recommended_model_size
print(_get_recommended_model_size()) # Apple Silicon: "small"
```
### 3. 离线模式支持
**新增环境变量**:
- `TTS_ASR_OFFLINE_MODE`: 启用离线模式
- 启动前检查模型是否已缓存
- 缓存不存在时优雅失败而非崩溃
**验证方法**:
```bash
# 启用离线模式
export TTS_ASR_OFFLINE_MODE=true
python backend/main.py
# 检查模型缓存
python -c "from tts_asr import _check_model_cached; print(_check_model_cached('openai/whisper-small'))"
```
### 4. 健壮的音频处理
**改进前问题**:
- `librosa.resample`失败时无回退
- 缺少音频数据验证
**改进后**:
- `_validate_audio_data()`: 验证音频数据有效性
- `_resample_audio_robust()`: 多重回退重采样
1. 优先使用`librosa.resample`
2. 回退到`torchaudio.transforms.Resample`
3. 最后使用NumPy线性插值
**验证方法**:
```python
import numpy as np
from tts_asr import _resample_audio_robust
# 测试重采样
audio = np.random.randn(16000).astype(np.float32)
resampled = _resample_audio_robust(audio, 16000, 48000)
print(f"Original: {len(audio)}, Resampled: {len(resampled)}")
```
### 5. 改进的错误处理和降级
**降级路径**:
```
MPS推理失败 → 标记MPS不可用 → 清理MPS缓存 → 降级到CPU
CUDA推理失败 → 标记CUDA不可用 → 清理CUDA缓存 → 降级到CPU
```
**日志改进**:
- 详细记录设备检测过程
- 明确标注降级原因
- 显示模型大小、量化状态、离线模式等配置
### 6. 新增API端点
**GET /v1/tts-asr/config**:
```json
{
"environment": {
"TTS_ASR_DEVICE": "auto",
"TTS_ASR_MODEL_SIZE": "auto",
"TTS_ASR_QUANTIZE": false,
"TTS_ASR_OFFLINE_MODE": false,
...
},
"device": {
"current": "mps",
"mps_available": true,
"cuda_available": false,
"is_apple_silicon": true,
"mps_memory_limit_mb": 8192
},
"model": {
"tts": "hexgrad/Kokoro-82M",
"asr_current_size": "small",
"asr_recommended_size": "small",
"available_sizes": ["tiny", "base", "small", "medium", "large", "turbo"]
}
}
```
## 环境变量完整列表
| 变量名 | 说明 | 默认值 | 示例 |
|--------|------|--------|------|
| `TTS_ASR_DEVICE` | 设备选择 | `auto` | `mps`, `cuda`, `cpu` |
| `TTS_ASR_MODEL_SIZE` | ASR模型大小 | `auto` | `tiny`, `base`, `small`, `medium`, `large`, `turbo` |
| `TTS_ASR_QUANTIZE` | INT8量化 | `false` | `true`, `false` |
| `TTS_ASR_OFFLINE_MODE` | 离线模式 | `false` | `true`, `false` |
| `TTS_ASR_WARMUP` | 启动预热 | `true` | `true`, `false` |
| `TTS_ASR_WARMUP_TIMEOUT` | 预热超时(秒) | `120` | `60`, `180` |
| `TTS_ASR_IDLE_TIMEOUT` | 空闲卸载(秒) | `0` | `300`, `600` |
| `TTS_ASR_MPS_MEMORY_LIMIT_MB` | MPS内存限制(MB) | `8192` | `4096`, `16384` |
## macOS使用建议
### 推荐配置
**Apple Silicon (M1/M2/M3) 8GB内存**:
```bash
export TTS_ASR_MODEL_SIZE=small
export TTS_ASR_MPS_MEMORY_LIMIT_MB=4096
```
**Apple Silicon (M1/M2/M3) 16GB+内存**:
```bash
export TTS_ASR_MODEL_SIZE=medium
export TTS_ASR_MPS_MEMORY_LIMIT_MB=8192
```
**内存紧张时**:
```bash
export TTS_ASR_MODEL_SIZE=tiny
export TTS_ASR_QUANTIZE=true
```
### 性能优化建议
1. **首次运行**: 建议不使用离线模式,让模型自动下载
2. **后续运行**: 启用离线模式避免网络延迟
```bash
export TTS_ASR_OFFLINE_MODE=true
```
3. **长期运行服务**: 设置空闲超时自动卸载模型
```bash
export TTS_ASR_IDLE_TIMEOUT=600 # 10分钟后卸载
```
4. **调试模式**: 查看详细设备检测日志
```python
import logging
logging.getLogger("tts_asr").setLevel(logging.DEBUG)
```
## 验证步骤(非Mac环境)
由于你不在Mac环境下,可以使用以下方法验证代码逻辑:
### 1. 代码静态检查
```bash
# 检查Python语法
python -m py_compile backend/tts_asr.py
# 检查导入
python -c "import backend.tts_asr"
```
### 2. 单元测试模拟
```python
# 模拟Apple Silicon环境
import os
import platform
# 模拟Darwin/arm64
original_system = platform.system
original_machine = platform.machine
def mock_system():
return "Darwin"
def mock_machine():
return "arm64"
platform.system = mock_system
platform.machine = mock_machine
# 测试Apple Silicon检测
from tts_asr import _is_apple_silicon
assert _is_apple_silicon() == True
# 恢复原始函数
platform.system = original_system
platform.machine = original_machine
```
### 3. 环境变量测试
```python
import os
os.environ['TTS_ASR_MODEL_SIZE'] = 'small'
os.environ['TTS_ASR_QUANTIZE'] = 'true'
# 重新加载模块
import importlib
import backend.tts_asr
importlib.reload(backend.tts_asr)
from backend.tts_asr import TTS_ASR_MODEL_SIZE, TTS_ASR_QUANTIZE
assert TTS_ASR_MODEL_SIZE == 'small'
assert TTS_ASR_QUANTIZE == True
```
### 4. API端点测试(需要运行服务)
```bash
# 启动服务
python backend/main.py
# 测试配置端点(需要API Key)
curl -X GET "http://localhost:8001/v1/tts-asr/config" \
-H "X-API-Key: your-secret-key-here"
# 测试状态端点
curl -X GET "http://localhost:8001/v1/tts-asr/status" \
-H "X-API-Key: your-secret-key-here"
```
## 依赖更新
已在`backend/requirements.txt`中添加:
- `psutil`: 系统内存检测
- `torchaudio`: 音频重采样备选方案
安装新依赖:
```bash
pip install -r backend/requirements.txt
```
## 向后兼容性
所有改动保持向后兼容:
- 现有API端点未改变
- 默认行为与原版一致
- 新功能通过环境变量启用
## 已知限制
1. **MPS float16**: 在某些操作上可能不稳定,代码默认使用float32
2. **8-bit量化**: 仅在CPU和CUDA环境支持,MPS不支持
3. **Core ML**: 预留了扩展点但未实现(需要额外依赖)
## 未来改进方向
1. 集成Core ML作为备选推理后端
2. 支持torch.compile (PyTorch 2.0+)
3. 实现模型自动下载的进度显示
4. 添加更多音频格式支持
## 问题排查
### 模型加载失败
1. 检查网络连接
2. 尝试关闭离线模式: `export TTS_ASR_OFFLINE_MODE=false`
3. 查看详细日志: 设置`logging.getLogger("tts_asr").setLevel(logging.DEBUG)`
### MPS内存不足
1. 使用更小的模型: `export TTS_ASR_MODEL_SIZE=tiny`
2. 启用量化: `export TTS_ASR_QUANTIZE=true`
3. 降低内存限制: `export TTS_ASR_MPS_MEMORY_LIMIT_MB=4096`
### 音频处理失败
1. 检查音频格式(支持WAV
2. 确保音频采样率≥8000Hz
3. 查看日志中的详细错误信息
---
**修复完成日期**: 2026-04-06
**修改文件**:
- `backend/tts_asr.py` (主要重构)
- `backend/requirements.txt` (添加依赖)
- `README.md` (更新文档)
+2 -1
View File
@@ -133,7 +133,8 @@ def _sanitize_converted_markdown(text: str) -> str:
value = (text or "").replace("\r\n", "\n").replace("\r", "\n") value = (text or "").replace("\r\n", "\n").replace("\r", "\n")
value = IMAGE_MARKDOWN_RE.sub("", value) value = IMAGE_MARKDOWN_RE.sub("", value)
value = IMAGE_HTML_RE.sub("", value) value = IMAGE_HTML_RE.sub("", value)
return value value = re.sub(r"\n{3,}", "\n\n", value)
return value.strip()
def get_client_ip(request: Request) -> str: def get_client_ip(request: Request) -> str:
+3 -8
View File
@@ -1,7 +1,6 @@
import os import os
import sys import sys
import base64 import base64
import types
import pytest import pytest
from unittest.mock import MagicMock from unittest.mock import MagicMock
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
@@ -11,10 +10,6 @@ BACKEND_DIR = os.path.abspath(os.path.join(CURRENT_DIR, ".."))
if BACKEND_DIR not in sys.path: if BACKEND_DIR not in sys.path:
sys.path.insert(0, BACKEND_DIR) sys.path.insert(0, BACKEND_DIR)
fake_tts_asr = types.ModuleType("tts_asr")
fake_tts_asr.register_tts_asr_routes = lambda app: None
sys.modules.setdefault("tts_asr", fake_tts_asr)
import main # type: ignore import main # type: ignore
API_KEY = main.API_KEY API_KEY = main.API_KEY
@@ -65,13 +60,13 @@ def test_sanitize_markdown_strips_img_tag():
assert "<img" not in main._sanitize_converted_markdown("<img src='x.png'/>") assert "<img" not in main._sanitize_converted_markdown("<img src='x.png'/>")
def test_sanitize_markdown_preserves_extra_newlines(): def test_sanitize_markdown_collapse_newlines():
assert main._sanitize_converted_markdown("a\n\n\nb\n\n\n\nc") == "a\n\n\nb\n\n\n\nc" assert main._sanitize_converted_markdown("a\n\n\nb\n\n\n\nc") == "a\n\nb\n\nc"
def test_sanitize_markdown_normalize_crlf(): def test_sanitize_markdown_normalize_crlf():
result = main._sanitize_converted_markdown("line1\r\nline2\r\n") result = main._sanitize_converted_markdown("line1\r\nline2\r\n")
assert result == "line1\nline2\n" assert "line1\nline2" in result
assert "\r" not in result assert "\r" not in result