24 KiB
Speakr - 定制版
基于 murtaza-nasir/speakr 二次开发的语音转录与智能笔记平台。
项目简介
本项目是一个自托管的 AI 语音转录和智能笔记平台,支持:
- 语音录制与上传 - 浏览器直接录音或上传已有音频文件
- AI 转录 - 高准确率语音转文字,支持说话人识别
- 交互式对话 - 对录音提问,获取 AI 回答
- 智能标签 - 支持带自定义 AI 提示词的标签
- 安全分享 - 生成安全链接分享录音
- 草稿功能 - 本地草稿保存与上传
- 声纹管理 - 自定义说话人声纹识别
- 标准词映射 - 自动纠正专业术语和人名
项目结构
speakr/
├── app.py # 入口文件(启动脚本)
├── src/
│ ├── app.py # Flask 应用主逻辑(create_app 工厂模式)
│ ├── config.py # 统一配置文件(所有环境变量集中管理)
│ ├── logging_config.py # 日志配置模块(格式化、输出目标、日志级别)
│ ├── flask_ext.py # Flask 扩展实例集合(LoginManager、Bcrypt、Limiter、CSRFProtect)
│ ├── clients/ # 外部服务客户端层
│ │ ├── __init__.py # 统一导出所有客户端和配置
│ │ ├── database.py # 数据库客户端:db 实例 + 配置 + 初始化
│ │ ├── llm_client.py # LLM 客户端:OpenAI 客户端初始化
│ │ └── voiceprint_client.py # 声纹客户端:VoiceprintClient
│ ├── api/ # API 路由层(Flask 蓝图)
│ │ ├── __init__.py # 统一蓝图注册函数 register_blueprints()
│ │ ├── template_filters.py # 模板过滤器注册(now、localdatetime)
│ │ ├── auth.py # 认证路由(登录、注册、账户管理)
│ │ ├── recording.py # 录音管理(CRUD、上传、转录、下载)
│ │ ├── share.py # 分享功能
│ │ ├── tag.py # 标签管理
│ │ ├── speaker.py # 说话人管理
│ │ ├── admin.py # 管理后台
│ │ ├── search.py # 搜索和 Inquire
│ │ ├── voiceprint.py # 声纹管理
│ │ ├── config.py # 系统配置管理
│ │ ├── hotword.py # 热词管理
│ │ ├── template.py # 转录模板
│ │ ├── event.py # 事件管理
│ │ └── main.py # 主页和杂项
│ ├── models/ # 数据模型层(每个模型独立文件)
│ │ ├── __init__.py # 统一导出所有模型
│ │ ├── user.py # 用户模型
│ │ ├── speaker.py # 说话人模型
│ │ ├── recording.py # 录音模型
│ │ ├── tag.py # 标签模型
│ │ ├── recording_tag.py # 录音-标签关联模型
│ │ ├── event.py # 事件模型
│ │ ├── share.py # 分享模型
│ │ ├── transcript_chunk.py # 转录块模型
│ │ ├── transcript_template.py # 转录模板模型
│ │ ├── inquire_session.py # 询问会话模型
│ │ ├── voiceprint.py # 声纹模型
│ │ ├── system_setting.py # 系统设置模型
│ │ ├── system_config.py # 系统配置模型
│ │ ├── hotword.py # 热词模型
│ │ ├── draft_recording.py # 草稿录音模型
│ │ ├── draft_segment.py # 草稿片段模型
│ │ └── forms.py # 表单类(注册、登录)
│ ├── services/ # 业务逻辑层
│ │ ├── __init__.py # 统一导出所有服务函数
│ │ ├── llm_service.py # LLM 服务
│ │ ├── transcription_service.py # 转录服务
│ │ ├── summary_service.py # 摘要服务
│ │ ├── embedding_service.py # 嵌入向量服务
│ │ ├── event_service.py # 事件服务
│ │ ├── document_service.py # 文档服务
│ │ ├── speaker_service.py # 说话人服务
│ │ ├── voiceprint_service.py # 声纹服务(业务逻辑)
│ │ ├── auto_process_service.py # 自动处理服务
│ │ ├── auth_service.py # 认证服务
│ │ ├── sync_service.py # 远程数据同步服务
│ │ ├── audio_chunking.py # 音频分块服务
│ │ └── file_monitor.py # 文件监控服务
│ ├── utils/ # 工具函数层
│ │ ├── __init__.py
│ │ ├── markdown.py # Markdown 转 HTML 工具
│ │ ├── datetime.py # 时区转换工具
│ │ ├── text_utils.py # 文本处理工具
│ │ ├── json_utils.py # JSON 处理工具
│ │ └── standard_word_mappings.py # 标准词映射工具
├── templates/ # Jinja2 模板
├── static/ # 静态资源(CSS/JS/图片)
├── instance/ # SQLite 数据库文件
├── uploads/ # 上传的音频文件
└── .env # 环境变量配置
启动方式
前置条件
- Python 环境(已安装 conda 环境
speakr) - Nginx 反向代理(必须启动)
启动步骤
# 1. 进入项目根目录(speakr 2 目录)
cd "c:\Work\zdht\python-projects\speakr 2"
# 2. 启动 Nginx 反向代理(必须先启动)
# Nginx 负责将 https://localhost:8083/tool/speakr/ 请求转发到 Flask 后端
# ⚠️ 注意:不启动 Nginx 将无法访问应用
nginx # 或根据你的安装方式启动 nginx
# 3. 激活 conda 环境并启动应用
conda activate speakr
python .\speakr\app.py
Flask 后端默认监听 5000 端口,Nginx 负责将 https://localhost:8083/tool/speakr/ 请求转发到 http://localhost:5000。
访问地址
- 主页面:
https://localhost:8083/tool/speakr/ - 管理后台:
https://localhost:8083/tool/speakr/admin - 账户设置:
https://localhost:8083/tool/speakr/account
常见错误
| 问题 | 原因 | 解决 |
|---|---|---|
| 页面无法访问 | Nginx 未启动 | 启动 Nginx 反向代理 |
ModuleNotFoundError: No module named 'xxx' |
conda 环境未激活 | 执行 conda activate speakr |
| 数据库文件位置错误 | 从错误目录启动 | 确保从 speakr 2/ 目录执行 python .\speakr\app.py |
环境配置
编辑 .env 文件,主要配置项:
# ASR 服务配置
USE_ASR_ENDPOINT=true
ASR_BASE_URL=http://10.100.3.22:6688
ASR_DIARIZE=true
# 文本生成模型配置
TEXT_MODEL_BASE_URL=http://10.100.3.22:9800/v1
TEXT_MODEL_API_KEY=EMPTY
TEXT_MODEL_NAME=minimax-m2.5
# 数据库配置
SQLALCHEMY_DATABASE_URI=sqlite:///transcriptions.db
# PostgreSQL 配置示例(切换时只需修改 URI 即可)
# SQLALCHEMY_DATABASE_URI=postgresql://user:password@localhost/speakr
# DB_POOL_SIZE=10
# DB_MAX_OVERFLOW=20
# DB_POOL_TIMEOUT=30
# DB_POOL_RECYCLE=3600
# 外部访问端口(Nginx 监听端口)
EXTERNAL_PORT=8083
# 时区配置
TIMEZONE=Asia/Shanghai
重构计划与当前进度
拆分原则
- ⭐ 标记的模块是业务特色,拆分时只改代码组织结构,不改业务逻辑
- 拆分顺序:
- 第一步:抽出 Models(最安全,纯数据定义)
- 第二步:抽出 Services(把 app.py 中的业务逻辑函数提取到独立文件)
- 第三步:抽出 API 蓝图(把路由按功能域分组注册)
- 第四步:抽出 Utils(工具函数独立化)
- 第五步:模板组件化(可选,优先级较低)
app.py最终只保留:Flask 初始化、数据库初始化、蓝图注册、全局错误处理- 保持向后兼容:拆分过程中所有 API 路径不变,前端无需改动
需要特别注意的业务逻辑
| 功能 | 说明 | 拆分注意事项 |
|---|---|---|
| ⭐ 声纹管理 | VoiceprintClient 类、声纹注册/比对/同步 | 保持 VoiceprintClient 与外部平台的交互逻辑不变 |
| ⭐ 系统配置 | SystemConfig 动态配置读写 | 保持配置缓存和热加载机制 |
| ⭐ 草稿 API | 已有独立蓝图 draft_bp | 直接迁移,几乎不需改动 |
| ⭐ 标准词映射 | 转写后的文本纠正 | 保持映射规则和应用逻辑 |
| ⭐ 自动处理 | 定时/触发式自动转写 | 保持调度逻辑和状态管理 |
| ⭐ 收件箱 | 录音的收件箱/高亮状态 | 保持查询和状态切换逻辑 |
| ⭐ 热词 | 热词管理和应用 | 保持与转写服务的集成 |
当前进度
✅ 已完成:Phase 1 - Models 层抽取
已将 src/app.py 中的 16 个数据模型 和 2 个表单类 从主文件中抽取到独立模块:
| 状态 | 任务 | 说明 |
|---|---|---|
| ✅ | src/database.py(已移至 clients) |
创建独立数据库实例文件,集中管理 SQLAlchemy db 对象(Phase 2.5 移至 clients/database.py) |
| ✅ | src/models/ |
每个模型独立文件(user.py, speaker.py, recording.py 等 16 个模型) |
| ✅ | src/models/forms.py |
表单类独立(RegistrationForm, LoginForm, password_check) |
| ✅ | src/utils/markdown.py |
Markdown 转 HTML 工具函数 |
| ✅ | src/utils/datetime.py |
时区转换工具函数 |
| ✅ | src/app.py |
删除模型定义,改为从 src.models 导入 |
验证结果:应用可正常启动,数据库文件路径正确(speakr/instance/transcriptions.db)
✅ 已完成:Phase 2 - Services 层抽取
服务文件已全部创建(src/services/ 目录下 11 个服务文件),app.py 中的旧函数定义已全部删除。
| 状态 | 服务文件 | 包含函数 |
|---|---|---|
| ✅ | llm_service.py |
call_llm_completion, clean_llm_response, format_transcription_for_llm, process_streaming_with_thinking, extract_thinking_content, preprocess_long_transcription, extract_corrected_text, format_api_error_message |
| ✅ | transcription_service.py |
transcribe_audio_asr, transcribe_audio_task, transcribe_single_file, transcribe_with_chunking, extract_audio_from_video, convert_to_wav |
| ✅ | summary_service.py |
generate_title_task, generate_summary_only_task |
| ✅ | embedding_service.py |
get_embedding_model, chunk_transcription, generate_embeddings, serialize_embedding, deserialize_embedding, process_recording_chunks, basic_text_search_chunks, semantic_search_chunks |
| ✅ | event_service.py |
extract_events_from_transcript, generate_ics_content, escape_ical_text |
| ✅ | document_service.py |
process_markdown_to_docx, get_recording_display_title, sanitize_download_filename_component, build_docx_download_filename, set_download_filename_header |
| ✅ | speaker_service.py |
update_speaker_usage, identify_speakers_from_text, identify_unidentified_speakers_from_text |
| ✅ | voiceprint_service.py |
sync_voiceprint_to_platform(VoiceprintClient 已移至 clients 层) |
| ✅ | auto_process_service.py |
initialize_file_monitor, get_file_monitor_functions |
| ✅ | auth_service.py |
is_safe_url, auto_login |
| ✅ | __init__.py |
统一导出所有服务函数 |
已完成的工作:
- ✅ 删除了 app.py 中的 22 个重复函数定义,改为从服务层导入
- ✅ 创建了
src/utils/text_utils.py和src/utils/json_utils.py,将工具函数移出 app.py - ✅ 添加了服务层和工具层的导入
- ✅ 核心功能测试通过,应用运行正常
✅ 已完成:Phase 2.5 - Clients 层创建
按照行业规范创建了 src/clients/ 层,统一管理与外部服务的连接客户端:
| 状态 | 客户端 | 说明 |
|---|---|---|
| ✅ | database.py |
数据库客户端:db 实例 + 配置 + init_database() 初始化逻辑 |
| ✅ | llm_client.py |
LLM 客户端:OpenAI 客户端初始化(client, TEXT_MODEL_NAME, TEXT_MODEL_API_KEY 等) |
| ✅ | voiceprint_client.py |
声纹客户端:VoiceprintClient 类 + voiceprint_client() 延迟初始化 |
| ✅ | __init__.py |
统一导出所有客户端 |
重构成果:
- 删除了旧
src/database.py(空实例文件),合并到clients/database.py - 删除了
services/voiceprint_service.py中的VoiceprintClient类,移到clients/voiceprint_client.py - 17 个模型文件 + 6 个服务文件的
db导入统一改为from src.clients import db - 消除了层层传递
client参数的问题 - 外部服务客户端与业务逻辑分离,架构更清晰
✅ 已完成:Phase 2.6 - 服务层注释中文化
将所有服务层文件中的英文注释和日志消息翻译为中文,提升代码可读性:
| 文件 | 翻译内容 |
|---|---|
clients/llm_client.py |
URL 注释清理逻辑、客户端初始化逻辑 |
clients/voiceprint_client.py |
声纹客户端注释和日志(新建文件,全中文) |
clients/database.py |
数据库配置和初始化注释(新建文件,全中文) |
services/llm_service.py |
思考内容提取、流式处理、API 错误格式化、分段总结等函数的注释和日志 |
services/summary_service.py |
摘要生成全流程的注释和日志(标签提示词、语言要求、系统/用户消息构建等) |
services/transcription_service.py |
音频提取、ASR 转录、分块转录、Whisper 调用等全流程注释和日志 |
services/embedding_service.py |
文本分块、嵌入向量生成、语义搜索等全流程注释和日志 |
services/voiceprint_service.py |
声纹同步业务逻辑注释 |
✅ 已完成:Phase 3 - API 蓝图抽取
将 src/app.py 中的约 100 个路由定义按功能域分组抽取到独立的 Flask 蓝图中,app.py 从约 10000 行精简到约 700 行:
| 状态 | 蓝图文件 | 路由数量 | 说明 |
|---|---|---|---|
| ✅ | api/auth.py |
5 | 注册、登录、登出、账户管理、修改密码 |
| ✅ | api/recording.py |
24 | 录音 CRUD、上传、转录、下载、状态管理、ASR 校正 |
| ✅ | api/share.py |
7 | 分享创建、查看、访问 |
| ✅ | api/tag.py |
6 | 标签 CRUD、应用到录音 |
| ✅ | api/speaker.py |
5 | 说话人管理、搜索 |
| ✅ | api/admin.py |
18 | 管理后台、统计分析、录音管理、配置管理 |
| ✅ | api/search.py |
7 | 搜索、Inquire 对话、收件箱 |
| ✅ | api/voiceprint.py |
6 | 声纹注册、管理、同步 |
| ✅ | api/config.py |
6 | 系统配置读写、分段服务管理 |
| ✅ | api/hotword.py |
2 | 热词管理 |
| ✅ | api/template.py |
5 | 转录模板 CRUD |
| ✅ | api/event.py |
3 | 事件提取、ICS 导出 |
| ✅ | api/main.py |
5 | 主页、摘要生成、对话 |
架构改进:
- 创建
api/__init__.py中的register_blueprints()函数,统一管理所有蓝图注册和初始化 - 使用
init_xxx_bp()延迟注入模式解决循环导入问题(limiter、csrf、bcrypt 等全局对象) - 所有 API 路径保持不变,前端无需任何修改
app.py仅保留:Flask 初始化、配置加载、蓝图注册、全局错误处理、启动逻辑
重构后修复的问题:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 重定向循环 | main.py 中的 index() 路由误加了 @login_required 装饰器,丢失了自动登录逻辑 |
恢复备份中的 token 验证和自动登录逻辑,移除装饰器 |
| WebSocket 连接失败 | getUserConfig 路由缺少 FUNASR_WEBSOCKET_IP 和 SCREEN_PUSH_IP 配置 |
在 main.py 的 get_user_config() 中补充 WebSocket 配置字段 |
| 循环导入 bcrypt | main.py 在模块级别导入 bcrypt 导致循环依赖 |
改为在 init_main_bp() 中延迟注入 bcrypt_instance |
✅ 已完成:统一配置管理
创建 src/config.py 统一配置文件,将所有散落的环境变量读取集中管理:
| 配置域 | 包含变量 |
|---|---|
| 基础配置 | SECRET_KEY, UPLOAD_FOLDER, LOG_LEVEL, MAX_FILE_SIZE_MB |
| ASR 配置 | USE_ASR_ENDPOINT, ASR_BASE_URL, ASR_DIARIZE, ASR_MIN_SPEAKERS, ASR_MAX_SPEAKERS |
| LLM 配置 | TEXT_MODEL_API_KEY, TEXT_MODEL_BASE_URL, TEXT_MODEL_NAME |
| 分段配置 | ENABLE_CHUNKING |
| Inquire 配置 | ENABLE_INQUIRE_MODE |
| 数据库配置 | SQLALCHEMY_DATABASE_URI, DB_POOL_SIZE, DB_MAX_OVERFLOW, DB_POOL_TIMEOUT, DB_POOL_RECYCLE |
| 外部 API | TRANSCRIPTION_BASE_URL, VALID_TOKEN, EMBEDDINGS_AVAILABLE |
| 代理配置 | HTTP_PROXY, HTTPS_PROXY |
重构成果:
clients/llm_client.py、clients/voiceprint_client.py从src.config导入配置clients/__init__.py统一导出所有配置项app.py通过from src.clients import ...统一导入配置,消除散落的环境变量读取- 提供
validate_config()函数在启动时验证关键配置
✅ 已完成:Phase 3.5 - app.py 清理与同步服务抽取
将 src/app.py 中残留的约 400 行业务逻辑彻底清理,app.py 从约 700 行精简到约 234 行:
| 状态 | 任务 | 说明 |
|---|---|---|
| ✅ | 删除重复的 preprocess 函数 | 删除了 preprocess_long_transcription 等 6 个重复定义的函数(已在 llm_service.py 中) |
| ✅ | 抽取远程同步服务 | 创建了 services/sync_service.py,将约 180 行同步逻辑从 app.py 移出 |
| ✅ | 清理无用 import | 删除了 30+ 个未使用的导入(render_template、OpenAI、httpx、subprocess、markdown 等) |
| ✅ | 注释中文化 | 将所有英文注释翻译为中文,删除无意义注释 |
| ✅ | 修复函数签名 | start_sync_thread() 增加 app 参数,调用处同步更新 |
| ✅ | 优化配置读取 | preprocess_long_transcription 等函数直接从 src.config 读取配置,不再通过参数传递 |
✅ 已完成:Phase 4 - app.py 工厂模式重构
采用 Flask create_app() 工厂模式,将 src/app.py 从 234 行进一步精简到 60 行,各层职责清晰分离:
新建模块:
| 文件 | 职责 |
|---|---|
src/logging_config.py |
日志配置模块 |
src/flask_ext.py |
Flask 扩展实例集合(login_manager、bcrypt、limiter、csrf) |
src/services/register.py |
服务层统一初始化(音频分段、同步线程、文件监控、表结构迁移) |
src/api/template_filters.py |
模板过滤器注册 |
修改模块:
| 文件 | 修改内容 |
|---|---|
src/config.py |
新增 get_app_config() 函数,一次性返回所有需写入 app.config 的配置项。新增配置只需改此函数,无需修改 app.py |
src/clients/__init__.py |
集中管理 chunking_service 和 EMBEDDINGS_AVAILABLE 的初始化与导出 |
src/api/__init__.py |
简化 register_blueprints() 函数,直接从 src.flask_ext 导入扩展实例 |
src/app.py |
采用 create_app() 工厂模式,仅负责编排各层初始化顺序 |
app.py 最终结构(60 行):
create_app()
├── 1. configure_logging() ← src/logging_config.py
├── 2. 创建 Flask 应用实例
├── 3. 加载应用配置 ← src/config.py 的 get_app_config()
├── 4. 配置反向代理支持
├── 5. 初始化 Flask 扩展 ← src/flask_ext.py
├── 6. 初始化数据库 ← src/clients/database.py
├── 7. 注册蓝图 ← src/api/__init__.py
├── 8. 注册模板过滤器 ← src/api/template_filters.py
└── 9. 初始化服务层 ← src/services/register.py
关键设计决策:
| 决策 | 理由 |
|---|---|
flask_ext.py 独立 |
Flask 扩展是全局单例,不应放在 clients 或 services 中 |
get_app_config() 集中管理 |
新增配置只需改 config.py,app.py 永远不需要改动 |
register_blueprints 自行读扩展 |
蓝图注册最清楚自己需要什么扩展,无需 app.py 中转 |
init_services 放在最后 |
服务依赖数据库和蓝图,必须在它们之后初始化 |
开发规范
API 接口返回格式规范
重要:所有新建/修改的 API 接口必须使用
ApiResponse工具类统一返回格式。旧接口暂不强制迁移。
工具位置
src/api/api_response.py
使用方式
from src.api.api_response import ApiResponse
# 成功响应(返回数据)
@blueprint.route('/xxx', methods=['POST'])
def create_xxx():
result = do_something()
return ApiResponse.success({'xxx': result.to_dict()})
# 成功响应(带提示消息)
return ApiResponse.success({'xxx': result.to_dict()}, '创建成功')
# 成功响应(自定义状态码,如 201 Created)
return ApiResponse.success({'xxx': result.to_dict()}, '创建成功', status_code=201)
# 错误响应
if not xxx:
return ApiResponse.error('资源未找到', 404)
# 错误响应(带详情)
if not valid:
return ApiResponse.error('参数校验失败', 400, detail='email 格式不正确')
# 分页响应
return ApiResponse.paginated(
items=[item.to_dict() for item in items],
pagination={
'page': page,
'per_page': per_page,
'total': total,
'total_pages': total_pages,
'has_next': has_next,
'has_prev': has_prev
}
)
响应格式约定
成功响应结构:
{
"success": true,
"message": "操作成功(可选)",
"xxx": {...} // 业务数据直接合并到顶层
}
分页响应结构:
{
"success": true,
"items": [...],
"pagination": {
"page": 1,
"total": 100,
...
}
}
错误响应结构:
{
"error": "用户友好的错误提示"
}
注意事项
data参数必须是字典类型,列表数据请使用paginated()或直接合并到字典中- 前端通过
response.ok(HTTP 状态码)判断成功/失败,通过data.error获取错误信息 - 旧接口暂不迁移,新接口和修改的接口必须使用
ApiResponse - 不要在
ApiResponse中直接传递原始异常信息给用户(str(e)),应转为友好提示
技术栈
- 后端: Python/Flask + SQLAlchemy
- 前端: Vue.js 3 + Tailwind CSS
- AI: OpenAI Whisper (ASR) + Ollama/OpenRouter (LLM)
- 数据库: SQLite(默认)/ PostgreSQL(支持环境变量切换)
- 部署: Nginx 反向代理 + Flask 开发服务器
依赖安装
conda activate speakr
pip install -r requirements.txt
主要依赖:
flask==2.3.3flask-sqlalchemy==3.1.1flask-login==0.6.3flask-wtf==1.2.2flask-bcrypt==1.0.1openai==1.3.0markdown==3.5.1python-docx==1.1.0
许可证
本项目基于 AGPL v3.0 开源协议。