修正了进度展示
parent
5a34df3944
commit
f616cb3fc3
|
|
@ -1,106 +0,0 @@
|
|||
# 音频处理功能部署说明
|
||||
|
||||
## 1. 安装Python依赖
|
||||
|
||||
```bash
|
||||
cd /Users/jiliu/工作/projects/imeeting/backend
|
||||
source venv/bin/activate
|
||||
pip install tinytag
|
||||
```
|
||||
|
||||
## 2. 执行数据库迁移
|
||||
|
||||
### 2.1 创建用户日志表
|
||||
```bash
|
||||
mysql -u root -p imeeting < sql/migrations/create_user_logs_table.sql
|
||||
```
|
||||
|
||||
### 2.2 为audio_files表添加duration字段
|
||||
```bash
|
||||
mysql -u root -p imeeting < sql/migrations/add_duration_to_audio_files.sql
|
||||
```
|
||||
|
||||
## 3. 功能说明
|
||||
|
||||
### 3.1 用户日志表 (user_logs)
|
||||
- 记录用户登录活动
|
||||
- 字段:log_id, user_id, action_type, ip_address, user_agent, metadata, created_at
|
||||
- 管理后台可查询用户最后登录时间
|
||||
|
||||
### 3.2 音频处理优化
|
||||
**音频时长计算:**
|
||||
- 使用TinyTag库读取音频文件时长
|
||||
- 纯Python实现,无需外部依赖
|
||||
- 支持mp3, m4a, mp4, wav, ogg, flac等常见格式
|
||||
- 能处理特殊/损坏元数据的文件
|
||||
- 时长保存到audio_files.duration字段(单位:秒)
|
||||
- 时长读取功能位于`app/utils/audio_parser.py`
|
||||
|
||||
### 3.3 管理后台用户统计
|
||||
- 排除没有会议的用户
|
||||
- 显示字段:
|
||||
- ID
|
||||
- 用户名
|
||||
- 姓名
|
||||
- 注册时间
|
||||
- 最新登录时间(从user_logs表查询)
|
||||
- 会议数量
|
||||
- 会议时长(从audio_files.duration汇总,格式:Xh Ym)
|
||||
|
||||
## 4. 代码变更清单
|
||||
|
||||
### 后端变更:
|
||||
- `/backend/app/utils/audio_parser.py` - 新增音频时长解析工具(使用TinyTag)
|
||||
- `/backend/app/api/endpoints/meetings.py` - 完整文件上传时调用audio_parser获取时长,修复Safari播放问题
|
||||
- `/backend/app/api/endpoints/audio.py` - Stream上传完成时调用audio_parser获取时长
|
||||
- `/backend/app/services/audio_service.py` - 增加duration参数
|
||||
- `/backend/app/api/endpoints/auth.py` - 登录时记录日志
|
||||
- `/backend/app/api/endpoints/admin_dashboard.py` - 更新用户统计查询
|
||||
- `/backend/app/models/models.py` - 新增UserLog模型
|
||||
|
||||
### 前端变更:
|
||||
- `/frontend/src/pages/HomePage.jsx` - 密码显示/隐藏切换
|
||||
- `/frontend/src/pages/HomePage.css` - 密码切换按钮样式
|
||||
- `/frontend/src/pages/AdminDashboard.jsx` - 用户列表增加会议时长列
|
||||
- `/frontend/src/pages/MeetingPreview.jsx` - 新增转录标签页,修复Safari音频播放
|
||||
- `/frontend/src/pages/MeetingDetails.jsx` - 修复Safari音频播放问题
|
||||
|
||||
### 数据库变更:
|
||||
- `sql/migrations/create_user_logs_table.sql` - 创建用户日志表
|
||||
- `sql/migrations/add_duration_to_audio_files.sql` - 添加音频时长字段
|
||||
|
||||
## 5. 测试检查清单
|
||||
|
||||
- [ ] 安装Python依赖成功
|
||||
- [ ] 数据库迁移执行成功
|
||||
- [ ] 用户登录后,user_logs表有记录
|
||||
- [ ] 上传音频文件,Safari可以正常播放
|
||||
- [ ] 上传音频后,audio_files.duration有正确的时长值
|
||||
- [ ] 管理后台用户列表显示正确(排除无会议用户)
|
||||
- [ ] 用户列表中会议时长统计正确
|
||||
- [ ] 登录页面密码显示/隐藏按钮正常工作
|
||||
- [ ] 会议预览页面音频播放正常(Safari/Chrome)
|
||||
- [ ] 会议详情页面音频播放正常(Safari/Chrome)
|
||||
|
||||
## 6. 注意事项
|
||||
|
||||
1. **音频时长计算**:
|
||||
- 使用TinyTag纯Python库
|
||||
- 无需系统依赖,跨平台兼容
|
||||
- 计算失败时duration为0,不影响其他功能
|
||||
- 支持所有TinyTag支持的格式
|
||||
|
||||
2. **Safari音频播放兼容性**:
|
||||
- 使用直接src属性而非source子元素
|
||||
- preload="metadata"模式
|
||||
- 后端支持HTTP Range请求
|
||||
- 正确的MIME类型由后端自动设置
|
||||
|
||||
3. **用户日志**:
|
||||
- 登录日志记录失败不影响登录流程
|
||||
- IP地址优先从X-Forwarded-For获取(考虑代理场景)
|
||||
|
||||
4. **管理后台统计**:
|
||||
- 使用INNER JOIN过滤无会议用户
|
||||
- 时长汇总从audio_files.duration计算
|
||||
- 最后登录时间从user_logs子查询获取
|
||||
|
|
@ -1,167 +0,0 @@
|
|||
# 提示词模版选择功能实现总结
|
||||
|
||||
## 功能概述
|
||||
实现了用户在创建会议时可以选择使用的总结模版功能。支持两种场景:
|
||||
1. 手动生成会议总结时选择模版
|
||||
2. 上传音频文件时选择模版,自动总结时使用该模版
|
||||
|
||||
## 实现的功能点
|
||||
|
||||
### 1. 新增API接口 ✅
|
||||
**文件**: `app/api/endpoints/prompts.py`
|
||||
- 新增 `GET /prompts/active/{task_type}` 接口
|
||||
- 功能:获取指定任务类型的所有启用状态的提示词模版
|
||||
- 返回字段:id, name, is_default
|
||||
- 按默认模版优先、创建时间倒序排列
|
||||
|
||||
### 2. 修改LLM服务 ✅
|
||||
**文件**: `app/services/llm_service.py`
|
||||
- 修改 `get_task_prompt()` 方法,增加 `prompt_id` 可选参数
|
||||
- 逻辑:
|
||||
- 如果指定 prompt_id,查询该ID对应的提示词(需验证task_type和is_active)
|
||||
- 如果不指定,使用默认提示词(is_default=TRUE)
|
||||
- 如果都查不到,返回代码中的默认值
|
||||
|
||||
### 3. 修改会议总结服务 ✅
|
||||
**文件**: `app/services/async_meeting_service.py`
|
||||
|
||||
#### 3.1 修改任务创建
|
||||
- `start_summary_generation()`: 增加 `prompt_id` 参数
|
||||
- 将 prompt_id 存储到 Redis 和数据库
|
||||
|
||||
#### 3.2 修改任务处理
|
||||
- `_process_task()`: 从 Redis 读取 prompt_id,传递给 `_build_prompt()`
|
||||
- `_build_prompt()`: 增加 `prompt_id` 参数,传递给 `llm_service.get_task_prompt()`
|
||||
- `_save_task_to_db()`: 增加 `prompt_id` 参数,存储到数据库
|
||||
|
||||
#### 3.3 修改自动总结监控
|
||||
- `monitor_and_auto_summarize()`: 增加 `prompt_id` 参数
|
||||
- 在转录完成后启动总结任务时,传递 prompt_id
|
||||
|
||||
### 4. 修改音频服务 ✅
|
||||
**文件**: `app/services/audio_service.py`
|
||||
- `handle_audio_upload()`: 增加 `prompt_id` 参数
|
||||
- 将 prompt_id 传递给 `monitor_and_auto_summarize()`
|
||||
|
||||
### 5. 修改会议API接口 ✅
|
||||
**文件**: `app/api/endpoints/meetings.py`
|
||||
|
||||
#### 5.1 手动生成总结
|
||||
- `GenerateSummaryRequest` 模型:增加 `prompt_id` 字段
|
||||
- `POST /meetings/{meeting_id}/generate-summary-async`: 传递 prompt_id 给服务层
|
||||
|
||||
#### 5.2 音频上传
|
||||
- `POST /meetings/upload-audio`: 增加 `prompt_id` 表单参数
|
||||
- 将 prompt_id 传递给 `handle_audio_upload()`
|
||||
|
||||
### 6. 数据库迁移 ✅
|
||||
**文件**: `sql/add_prompt_id_to_llm_tasks.sql`
|
||||
- 为 `llm_tasks` 表添加 `prompt_id` 列
|
||||
- 类型:int(11)
|
||||
- 可空:YES
|
||||
- 默认值:NULL
|
||||
- 索引:idx_prompt_id
|
||||
|
||||
## 数据流向
|
||||
|
||||
### 手动生成总结
|
||||
```
|
||||
前端 → POST /meetings/{id}/generate-summary-async (prompt_id)
|
||||
→ async_meeting_service.start_summary_generation(prompt_id)
|
||||
→ 存储到 Redis 和 DB (llm_tasks.prompt_id)
|
||||
→ _process_task() 读取 prompt_id
|
||||
→ _build_prompt(prompt_id)
|
||||
→ llm_service.get_task_prompt('MEETING_TASK', prompt_id)
|
||||
→ 获取指定模版或默认模版
|
||||
```
|
||||
|
||||
### 音频上传自动总结
|
||||
```
|
||||
前端 → POST /meetings/upload-audio (prompt_id, auto_summarize=true)
|
||||
→ handle_audio_upload(prompt_id)
|
||||
→ transcription_service.start_transcription()
|
||||
→ monitor_and_auto_summarize(prompt_id)
|
||||
→ 等待转录完成
|
||||
→ start_summary_generation(prompt_id)
|
||||
→ (后续流程同手动生成总结)
|
||||
```
|
||||
|
||||
## 向后兼容性
|
||||
|
||||
所有新增的 `prompt_id` 参数都是可选的(Optional[int] = None),确保:
|
||||
1. 不传递 prompt_id 时,自动使用默认模版
|
||||
2. 现有代码无需修改即可正常工作
|
||||
3. 数据库中 prompt_id 允许为 NULL
|
||||
|
||||
## 测试结果
|
||||
|
||||
执行 `test_prompt_id_feature.py` 测试脚本,所有测试通过:
|
||||
- ✅ 获取启用的提示词列表 (6个模版)
|
||||
- ✅ 通过prompt_id获取提示词内容
|
||||
- ✅ 获取默认提示词(不指定prompt_id)
|
||||
- ✅ 验证方法签名支持prompt_id参数
|
||||
- ✅ 验证数据库schema包含prompt_id列
|
||||
- ✅ 验证API端点定义正确
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 1. 获取启用的会议任务模版列表
|
||||
```bash
|
||||
GET /api/prompts/active/MEETING_TASK
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
返回:
|
||||
```json
|
||||
{
|
||||
"code": "200",
|
||||
"message": "获取启用模版列表成功",
|
||||
"data": {
|
||||
"prompts": [
|
||||
{"id": 1, "name": "默认会议总结", "is_default": true},
|
||||
{"id": 5, "name": "产品会议总结", "is_default": false}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 手动生成总结时指定模版
|
||||
```bash
|
||||
POST /api/meetings/123/generate-summary-async
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"user_prompt": "重点关注技术讨论",
|
||||
"prompt_id": 5
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 上传音频时指定模版
|
||||
```bash
|
||||
POST /api/meetings/upload-audio
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
- audio_file: <file>
|
||||
- meeting_id: 123
|
||||
- auto_summarize: true
|
||||
- prompt_id: 5
|
||||
```
|
||||
|
||||
## 文件变更列表
|
||||
|
||||
1. `app/api/endpoints/prompts.py` - 新增API接口
|
||||
2. `app/api/endpoints/meetings.py` - 修改两个端点
|
||||
3. `app/services/llm_service.py` - 修改get_task_prompt方法
|
||||
4. `app/services/async_meeting_service.py` - 修改4个方法
|
||||
5. `app/services/audio_service.py` - 修改handle_audio_upload方法
|
||||
6. `sql/add_prompt_id_to_llm_tasks.sql` - 数据库迁移脚本
|
||||
7. `test_prompt_id_feature.py` - 测试脚本
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. prompt_id 会与 task_type 一起验证,防止使用错误类型的模版
|
||||
2. 如果指定的 prompt_id 不存在或未启用,会自动使用默认模版
|
||||
3. 历史任务记录保留 prompt_id,即使对应的提示词被删除
|
||||
4. Redis 中 prompt_id 存储为字符串,使用时需转换为 int
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
# 知识库提示词模版选择功能实现总结
|
||||
|
||||
## 功能概述
|
||||
为知识库生成功能添加了提示词模版选择支持,用户在创建知识库时可以选择使用的生成模版。
|
||||
|
||||
## 实现的功能点
|
||||
|
||||
### 1. 修改请求模型 ✅
|
||||
**文件**: `app/models/models.py`
|
||||
- `CreateKnowledgeBaseRequest` 模型增加 `prompt_id` 字段
|
||||
- 类型:Optional[int] = None
|
||||
- 不指定时使用默认模版
|
||||
|
||||
### 2. 修改知识库异步服务 ✅
|
||||
**文件**: `app/services/async_knowledge_base_service.py`
|
||||
|
||||
#### 2.1 修改任务创建
|
||||
- `start_generation()`: 增加 `prompt_id` 参数
|
||||
- 将 prompt_id 存储到 Redis 和数据库
|
||||
- 支持通过 cursor 参数直接插入(事务场景)
|
||||
|
||||
#### 2.2 修改任务处理
|
||||
- `_process_task()`: 从 Redis 读取 prompt_id,传递给 `_build_prompt()`
|
||||
- 处理空字符串情况,转换为 None
|
||||
|
||||
#### 2.3 修改提示词构建
|
||||
- `_build_prompt()`: 增加 `prompt_id` 参数
|
||||
- 调用 `llm_service.get_task_prompt('KNOWLEDGE_TASK', prompt_id=prompt_id)`
|
||||
- 支持获取指定模版或默认模版
|
||||
|
||||
#### 2.4 修改数据库保存
|
||||
- `_save_task_to_db()`: 增加 `prompt_id` 参数
|
||||
- 插入时包含 prompt_id 字段
|
||||
|
||||
### 3. 修改API接口 ✅
|
||||
**文件**: `app/api/endpoints/knowledge_base.py`
|
||||
- `create_knowledge_base`: 从请求中获取 `prompt_id`
|
||||
- 调用 `async_kb_service.start_generation()` 时传递 `prompt_id`
|
||||
|
||||
### 4. 数据库字段 ✅
|
||||
- `knowledge_base_tasks` 表已包含 `prompt_id` 列(用户已添加)
|
||||
- 类型:int
|
||||
- 可空:NO(默认值:0)
|
||||
|
||||
## 数据流向
|
||||
|
||||
```
|
||||
前端 → POST /api/knowledge-bases (prompt_id)
|
||||
→ CreateKnowledgeBaseRequest (prompt_id)
|
||||
→ async_kb_service.start_generation(prompt_id)
|
||||
→ 存储到 Redis 和 DB (knowledge_base_tasks.prompt_id)
|
||||
→ _process_task() 读取 prompt_id
|
||||
→ _build_prompt(prompt_id)
|
||||
→ llm_service.get_task_prompt('KNOWLEDGE_TASK', prompt_id)
|
||||
→ 获取指定模版或默认模版
|
||||
```
|
||||
|
||||
## 向后兼容性
|
||||
|
||||
所有新增的 `prompt_id` 参数都是可选的(Optional[int] = None),确保:
|
||||
1. 不传递 prompt_id 时,自动使用默认模版
|
||||
2. 现有代码无需修改即可正常工作
|
||||
3. 数据库中 prompt_id 有默认值 0
|
||||
|
||||
## 测试结果
|
||||
|
||||
执行 `test_kb_prompt_id_feature.py` 测试脚本,所有测试通过:
|
||||
- ✅ 获取启用的知识库提示词列表 (3个模版)
|
||||
- ✅ 通过prompt_id获取提示词内容
|
||||
- ✅ 获取默认提示词(不指定prompt_id)
|
||||
- ✅ 验证方法签名支持prompt_id参数
|
||||
- ✅ 验证数据库schema包含prompt_id列
|
||||
- ✅ 验证API模型定义正确
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 1. 获取启用的知识库任务模版列表
|
||||
```bash
|
||||
GET /api/prompts/active/KNOWLEDGE_TASK
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
返回:
|
||||
```json
|
||||
{
|
||||
"code": "200",
|
||||
"message": "获取启用模版列表成功",
|
||||
"data": {
|
||||
"prompts": [
|
||||
{"id": 2, "name": "默认知识库生成", "is_default": true},
|
||||
{"id": 13, "name": "分析总结模版", "is_default": false}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 创建知识库时指定模版
|
||||
```bash
|
||||
POST /api/knowledge-bases
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"title": "产品会议知识库",
|
||||
"is_shared": false,
|
||||
"user_prompt": "重点提取产品功能相关信息",
|
||||
"source_meeting_ids": "1,2,3",
|
||||
"tags": "产品,功能",
|
||||
"prompt_id": 13
|
||||
}
|
||||
```
|
||||
|
||||
## 文件变更列表
|
||||
|
||||
1. `app/models/models.py` - 修改CreateKnowledgeBaseRequest模型
|
||||
2. `app/services/async_knowledge_base_service.py` - 修改5个方法
|
||||
3. `app/api/endpoints/knowledge_base.py` - 修改create_knowledge_base端点
|
||||
4. `test_kb_prompt_id_feature.py` - 测试脚本
|
||||
|
||||
## 与会议总结功能的一致性
|
||||
|
||||
知识库的实现与会议总结功能保持一致:
|
||||
- 相同的prompt_id传递机制
|
||||
- 相同的Redis存储格式(字符串)
|
||||
- 相同的数据库字段类型
|
||||
- 相同的向后兼容策略
|
||||
- 相同的验证逻辑(task_type + is_active)
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. prompt_id 会与 task_type='KNOWLEDGE_TASK' 一起验证
|
||||
2. 如果指定的 prompt_id 不存在或未启用,会自动使用默认模版
|
||||
3. 历史任务记录保留 prompt_id,即使对应的提示词被删除
|
||||
4. Redis 中 prompt_id 存储为字符串,使用时需转换为 int
|
||||
5. 数据库 prompt_id 默认值为 0(表示未指定)
|
||||
|
||||
## 总结
|
||||
|
||||
知识库提示词模版选择功能已完全实现并通过测试,与会议总结功能保持一致的设计和实现方式。用户现在可以在创建知识库时选择不同的生成模版,以满足不同场景的需求。
|
||||
|
|
@ -1,126 +1,12 @@
|
|||
from fastapi import APIRouter, Depends
|
||||
from app.core.auth import get_current_admin_user, get_current_user
|
||||
from app.core.config import LLM_CONFIG, DEFAULT_RESET_PASSWORD, MAX_FILE_SIZE, VOICEPRINT_CONFIG, TIMELINE_PAGESIZE
|
||||
from app.core.response import create_api_response
|
||||
from app.core.database import get_db_connection
|
||||
from app.models.models import MenuInfo, MenuListResponse, RolePermissionInfo, UpdateRolePermissionsRequest, RoleInfo
|
||||
from pydantic import BaseModel
|
||||
from typing import List
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# 配置文件路径
|
||||
CONFIG_FILE = Path(__file__).parent.parent.parent.parent / "config" / "system_config.json"
|
||||
|
||||
class SystemConfigModel(BaseModel):
|
||||
model_name: str
|
||||
template_text: str
|
||||
DEFAULT_RESET_PASSWORD: str
|
||||
MAX_FILE_SIZE: int # 字节为单位
|
||||
TIMELINE_PAGESIZE: int # 分页数量
|
||||
|
||||
def load_config_from_file():
|
||||
"""从文件加载配置,如果文件不存在则返回默认配置"""
|
||||
try:
|
||||
if CONFIG_FILE.exists():
|
||||
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 返回默认配置
|
||||
return {
|
||||
'model_name': LLM_CONFIG['model_name'],
|
||||
'template_text': VOICEPRINT_CONFIG['template_text'],
|
||||
'DEFAULT_RESET_PASSWORD': DEFAULT_RESET_PASSWORD,
|
||||
'MAX_FILE_SIZE': MAX_FILE_SIZE,
|
||||
'TIMELINE_PAGESIZE': TIMELINE_PAGESIZE
|
||||
}
|
||||
|
||||
def save_config_to_file(config_data):
|
||||
"""将配置保存到文件"""
|
||||
try:
|
||||
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(config_data, f, ensure_ascii=False, indent=2)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"保存配置文件失败: {e}")
|
||||
return False
|
||||
|
||||
@router.get("/admin/system-config")
|
||||
async def get_system_config(current_user=Depends(get_current_user)):
|
||||
"""
|
||||
获取系统配置
|
||||
普通用户也可以获取
|
||||
"""
|
||||
try:
|
||||
config = load_config_from_file()
|
||||
response_data = {
|
||||
'model_name': config.get('model_name', LLM_CONFIG['model_name']),
|
||||
'template_text': config.get('template_text', VOICEPRINT_CONFIG['template_text']),
|
||||
'DEFAULT_RESET_PASSWORD': config.get('DEFAULT_RESET_PASSWORD', DEFAULT_RESET_PASSWORD),
|
||||
'MAX_FILE_SIZE': config.get('MAX_FILE_SIZE', MAX_FILE_SIZE),
|
||||
'TIMELINE_PAGESIZE': config.get('TIMELINE_PAGESIZE', TIMELINE_PAGESIZE),
|
||||
}
|
||||
return create_api_response(code="200", message="配置获取成功", data=response_data)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取配置失败: {str(e)}")
|
||||
|
||||
@router.put("/admin/system-config")
|
||||
async def update_system_config(
|
||||
config: SystemConfigModel,
|
||||
current_user=Depends(get_current_admin_user)
|
||||
):
|
||||
"""
|
||||
更新系统配置
|
||||
只有管理员才能访问
|
||||
"""
|
||||
try:
|
||||
config_data = {
|
||||
'model_name': config.model_name,
|
||||
'template_text': config.template_text,
|
||||
'DEFAULT_RESET_PASSWORD': config.DEFAULT_RESET_PASSWORD,
|
||||
'MAX_FILE_SIZE': config.MAX_FILE_SIZE,
|
||||
'TIMELINE_PAGESIZE': config.TIMELINE_PAGESIZE
|
||||
}
|
||||
|
||||
if not save_config_to_file(config_data):
|
||||
return create_api_response(code="500", message="配置保存到文件失败")
|
||||
|
||||
# 更新运行时配置
|
||||
LLM_CONFIG['model_name'] = config.model_name
|
||||
VOICEPRINT_CONFIG['template_text'] = config.template_text
|
||||
import app.core.config as config_module
|
||||
config_module.DEFAULT_RESET_PASSWORD = config.DEFAULT_RESET_PASSWORD
|
||||
config_module.MAX_FILE_SIZE = config.MAX_FILE_SIZE
|
||||
config_module.TIMELINE_PAGESIZE = config.TIMELINE_PAGESIZE
|
||||
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="配置更新成功,重启服务后完全生效",
|
||||
data=config_data
|
||||
)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"更新配置失败: {str(e)}")
|
||||
|
||||
# 在应用启动时加载配置
|
||||
def load_system_config():
|
||||
"""在应用启动时调用,加载保存的配置"""
|
||||
try:
|
||||
config = load_config_from_file()
|
||||
LLM_CONFIG['model_name'] = config.get('model_name', LLM_CONFIG['model_name'])
|
||||
VOICEPRINT_CONFIG['template_text'] = config.get('template_text', VOICEPRINT_CONFIG['template_text'])
|
||||
import app.core.config as config_module
|
||||
config_module.DEFAULT_RESET_PASSWORD = config.get('DEFAULT_RESET_PASSWORD', DEFAULT_RESET_PASSWORD)
|
||||
config_module.MAX_FILE_SIZE = config.get('MAX_FILE_SIZE', MAX_FILE_SIZE)
|
||||
config_module.TIMELINE_PAGESIZE = config.get('TIMELINE_PAGESIZE', TIMELINE_PAGESIZE)
|
||||
print(f"系统配置加载成功: model={config.get('model_name')}, pagesize={config.get('TIMELINE_PAGESIZE')}")
|
||||
except Exception as e:
|
||||
print(f"加载系统配置失败,使用默认配置: {e}")
|
||||
|
||||
# ========== 菜单权限管理接口 ==========
|
||||
|
||||
@router.get("/admin/menus")
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ TEMP_UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
|||
|
||||
# 配置常量
|
||||
MAX_CHUNK_SIZE = 2 * 1024 * 1024 # 2MB per chunk
|
||||
MAX_TOTAL_SIZE = 500 * 1024 * 1024 # 500MB total
|
||||
MAX_TOTAL_SIZE = 500 * 1024 * 1024 # 500MB total (流式上传不受系统参数控制)
|
||||
MAX_DURATION = 3600 # 1 hour max recording
|
||||
SESSION_EXPIRE_HOURS = 1 # 会话1小时后过期
|
||||
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ async def get_dict_data_by_code(dict_type: str, dict_code: str):
|
|||
query = """
|
||||
SELECT id, dict_type, dict_code, parent_code, tree_path,
|
||||
label_cn, label_en, sort_order, extension_attr,
|
||||
is_default, status, create_time
|
||||
is_default, status, create_time, update_time
|
||||
FROM dict_data
|
||||
WHERE dict_type = %s AND dict_code = %s
|
||||
LIMIT 1
|
||||
|
|
@ -211,6 +211,17 @@ async def create_dict_data(
|
|||
创建码表数据(仅管理员)
|
||||
"""
|
||||
try:
|
||||
# 验证extension_attr的JSON格式
|
||||
if request.extension_attr:
|
||||
try:
|
||||
# 尝试序列化,验证是否为有效的JSON对象
|
||||
json.dumps(request.extension_attr)
|
||||
except (TypeError, ValueError) as e:
|
||||
return create_api_response(
|
||||
code="400",
|
||||
message=f"扩展属性格式错误,必须是有效的JSON对象: {str(e)}"
|
||||
)
|
||||
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
|
|
@ -233,7 +244,7 @@ async def create_dict_data(
|
|||
sort_order, extension_attr, is_default, status
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
"""
|
||||
extension_json = json.dumps(request.extension_attr) if request.extension_attr else None
|
||||
extension_json = json.dumps(request.extension_attr, ensure_ascii=False) if request.extension_attr else None
|
||||
|
||||
cursor.execute(query, (
|
||||
request.dict_type,
|
||||
|
|
@ -274,6 +285,17 @@ async def update_dict_data(
|
|||
更新码表数据(仅管理员)
|
||||
"""
|
||||
try:
|
||||
# 验证extension_attr的JSON格式
|
||||
if request.extension_attr is not None:
|
||||
try:
|
||||
# 尝试序列化,验证是否为有效的JSON对象
|
||||
json.dumps(request.extension_attr)
|
||||
except (TypeError, ValueError) as e:
|
||||
return create_api_response(
|
||||
code="400",
|
||||
message=f"扩展属性格式错误,必须是有效的JSON对象: {str(e)}"
|
||||
)
|
||||
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
|
|
@ -309,7 +331,7 @@ async def update_dict_data(
|
|||
|
||||
if request.extension_attr is not None:
|
||||
update_fields.append("extension_attr = %s")
|
||||
params.append(json.dumps(request.extension_attr))
|
||||
params.append(json.dumps(request.extension_attr, ensure_ascii=False))
|
||||
|
||||
if request.is_default is not None:
|
||||
update_fields.append("is_default = %s")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,173 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from app.core.database import get_db_connection
|
||||
from app.core.auth import get_current_admin_user
|
||||
from app.core.response import create_api_response
|
||||
from app.core.config import QWEN_API_KEY
|
||||
from app.services.system_config_service import SystemConfigService
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
import json
|
||||
import dashscope
|
||||
from dashscope.audio.asr import VocabularyService
|
||||
from datetime import datetime
|
||||
from http import HTTPStatus
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class HotWordItem(BaseModel):
|
||||
id: int
|
||||
text: str
|
||||
weight: int
|
||||
lang: str
|
||||
status: int
|
||||
create_time: datetime
|
||||
update_time: datetime
|
||||
|
||||
class CreateHotWordRequest(BaseModel):
|
||||
text: str
|
||||
weight: int = 4
|
||||
lang: str = "zh"
|
||||
status: int = 1
|
||||
|
||||
class UpdateHotWordRequest(BaseModel):
|
||||
text: Optional[str] = None
|
||||
weight: Optional[int] = None
|
||||
lang: Optional[str] = None
|
||||
status: Optional[int] = None
|
||||
|
||||
@router.get("/admin/hot-words", response_model=dict)
|
||||
async def list_hot_words(current_user: dict = Depends(get_current_admin_user)):
|
||||
"""获取热词列表"""
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT * FROM hot_words ORDER BY update_time DESC")
|
||||
items = cursor.fetchall()
|
||||
cursor.close()
|
||||
return create_api_response(code="200", message="获取成功", data=items)
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取失败: {str(e)}")
|
||||
|
||||
@router.post("/admin/hot-words", response_model=dict)
|
||||
async def create_hot_word(request: CreateHotWordRequest, current_user: dict = Depends(get_current_admin_user)):
|
||||
"""创建热词"""
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
query = "INSERT INTO hot_words (text, weight, lang, status) VALUES (%s, %s, %s, %s)"
|
||||
cursor.execute(query, (request.text, request.weight, request.lang, request.status))
|
||||
new_id = cursor.lastrowid
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
return create_api_response(code="200", message="创建成功", data={"id": new_id})
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"创建失败: {str(e)}")
|
||||
|
||||
@router.put("/admin/hot-words/{id}", response_model=dict)
|
||||
async def update_hot_word(id: int, request: UpdateHotWordRequest, current_user: dict = Depends(get_current_admin_user)):
|
||||
"""更新热词"""
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
update_fields = []
|
||||
params = []
|
||||
if request.text is not None:
|
||||
update_fields.append("text = %s")
|
||||
params.append(request.text)
|
||||
if request.weight is not None:
|
||||
update_fields.append("weight = %s")
|
||||
params.append(request.weight)
|
||||
if request.lang is not None:
|
||||
update_fields.append("lang = %s")
|
||||
params.append(request.lang)
|
||||
if request.status is not None:
|
||||
update_fields.append("status = %s")
|
||||
params.append(request.status)
|
||||
|
||||
if not update_fields:
|
||||
return create_api_response(code="400", message="无更新内容")
|
||||
|
||||
query = f"UPDATE hot_words SET {', '.join(update_fields)} WHERE id = %s"
|
||||
params.append(id)
|
||||
cursor.execute(query, tuple(params))
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
return create_api_response(code="200", message="更新成功")
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"更新失败: {str(e)}")
|
||||
|
||||
@router.delete("/admin/hot-words/{id}", response_model=dict)
|
||||
async def delete_hot_word(id: int, current_user: dict = Depends(get_current_admin_user)):
|
||||
"""删除热词"""
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM hot_words WHERE id = %s", (id,))
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
return create_api_response(code="200", message="删除成功")
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"删除失败: {str(e)}")
|
||||
|
||||
@router.post("/admin/hot-words/sync", response_model=dict)
|
||||
async def sync_hot_words(current_user: dict = Depends(get_current_admin_user)):
|
||||
"""同步热词到阿里云 DashScope"""
|
||||
try:
|
||||
dashscope.api_key = QWEN_API_KEY
|
||||
|
||||
# 1. 获取所有启用的热词
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT text, weight, lang FROM hot_words WHERE status = 1")
|
||||
hot_words = cursor.fetchall()
|
||||
cursor.close()
|
||||
|
||||
# 2. 获取现有的 vocabulary_id
|
||||
existing_vocab_id = SystemConfigService.get_asr_vocabulary_id()
|
||||
|
||||
# 构建热词列表
|
||||
vocabulary_list = [{"text": hw['text'], "weight": hw['weight'], "lang": hw['lang']} for hw in hot_words]
|
||||
|
||||
if not vocabulary_list:
|
||||
return create_api_response(code="400", message="没有启用的热词可同步")
|
||||
|
||||
# 3. 调用阿里云 API
|
||||
service = VocabularyService()
|
||||
vocab_id = existing_vocab_id
|
||||
|
||||
try:
|
||||
if existing_vocab_id:
|
||||
# 尝试更新现有的热词表
|
||||
try:
|
||||
service.update_vocabulary(
|
||||
vocabulary_id=existing_vocab_id,
|
||||
vocabulary=vocabulary_list
|
||||
)
|
||||
# 更新成功,保持原有ID
|
||||
except Exception as update_error:
|
||||
# 如果更新失败(如资源不存在),尝试创建新的
|
||||
print(f"Update vocabulary failed: {update_error}, trying to create new one.")
|
||||
existing_vocab_id = None # 重置,触发创建逻辑
|
||||
|
||||
if not existing_vocab_id:
|
||||
# 创建新的热词表
|
||||
vocab_id = service.create_vocabulary(
|
||||
prefix='imeeting',
|
||||
target_model='paraformer-v2',
|
||||
vocabulary=vocabulary_list
|
||||
)
|
||||
|
||||
except Exception as api_error:
|
||||
return create_api_response(code="500", message=f"同步到阿里云失败: {str(api_error)}")
|
||||
|
||||
# 4. 更新数据库中的 vocabulary_id
|
||||
if vocab_id:
|
||||
SystemConfigService.set_config(
|
||||
SystemConfigService.ASR_VOCABULARY_ID,
|
||||
vocab_id
|
||||
)
|
||||
|
||||
return create_api_response(code="200", message="同步成功", data={"vocabulary_id": vocab_id})
|
||||
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"同步异常: {str(e)}")
|
||||
|
|
@ -8,6 +8,7 @@ from app.services.llm_service import LLMService
|
|||
from app.services.async_transcription_service import AsyncTranscriptionService
|
||||
from app.services.async_meeting_service import async_meeting_service
|
||||
from app.services.audio_service import handle_audio_upload
|
||||
from app.services.system_config_service import SystemConfigService
|
||||
from app.utils.audio_parser import get_audio_duration
|
||||
from app.core.auth import get_current_user, get_optional_current_user
|
||||
from app.core.response import create_api_response
|
||||
|
|
@ -143,11 +144,9 @@ def get_meetings(
|
|||
tags: Optional[str] = None,
|
||||
filter_type: str = "all"
|
||||
):
|
||||
from app.core.config import TIMELINE_PAGESIZE
|
||||
|
||||
# 使用配置的默认页面大小
|
||||
if page_size is None:
|
||||
page_size = TIMELINE_PAGESIZE
|
||||
page_size = SystemConfigService.get_timeline_pagesize(default=10)
|
||||
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
|
@ -486,8 +485,14 @@ async def upload_audio(
|
|||
"""
|
||||
auto_summarize_bool = auto_summarize.lower() in ("true", "1", "yes")
|
||||
|
||||
# 打印接收到的 prompt_id
|
||||
print(f"[Upload Audio] Meeting ID: {meeting_id}, Received prompt_id: {prompt_id}, Type: {type(prompt_id)}, Auto-summarize: {auto_summarize_bool}")
|
||||
# 0. 如果没有传入 prompt_id,尝试获取默认模版ID
|
||||
if prompt_id is None:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor()
|
||||
cursor.execute(
|
||||
"SELECT id FROM prompts WHERE task_type = 'MEETING_TASK' AND is_default = TRUE AND is_active = TRUE LIMIT 1"
|
||||
)
|
||||
prompt_id = cursor.fetchone()[0]
|
||||
|
||||
# 1. 文件类型验证
|
||||
file_extension = os.path.splitext(audio_file.filename)[1].lower()
|
||||
|
|
@ -498,7 +503,7 @@ async def upload_audio(
|
|||
)
|
||||
|
||||
# 2. 文件大小验证
|
||||
max_file_size = getattr(config_module, 'MAX_FILE_SIZE', 100 * 1024 * 1024)
|
||||
max_file_size = SystemConfigService.get_max_audio_size(default=100) * 1024 * 1024 # MB转字节
|
||||
if audio_file.size > max_file_size:
|
||||
return create_api_response(
|
||||
code="400",
|
||||
|
|
@ -987,6 +992,7 @@ def get_meeting_preview_data(meeting_id: int):
|
|||
- 200: 会议已完成(summary已生成)
|
||||
- 400: 会议处理中(转译或总结阶段)
|
||||
- 503: 处理失败(转译或总结失败)
|
||||
- 504: 数据异常(流程完成但summary未生成)
|
||||
- 404: 会议不存在
|
||||
"""
|
||||
try:
|
||||
|
|
@ -1049,7 +1055,19 @@ def get_meeting_preview_data(meeting_id: int):
|
|||
}
|
||||
)
|
||||
|
||||
# 情况3: 全部完成 → 返回200,提供完整预览数据
|
||||
# 情况3: 全部完成但Summary缺失 → 返回504
|
||||
if overall_status == "completed" and not meeting['summary']:
|
||||
return create_api_response(
|
||||
code="504",
|
||||
message="处理已完成,AI总结尚未同步,请稍后重试",
|
||||
data={
|
||||
"meeting_id": meeting_id,
|
||||
"title": meeting['title'],
|
||||
"processing_status": progress_info
|
||||
}
|
||||
)
|
||||
|
||||
# 情况4: 全部完成 → 返回200,提供完整预览数据
|
||||
if overall_status == "completed" and meeting['summary']:
|
||||
# 获取参会人员信息
|
||||
with get_db_connection() as connection:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from app.models.models import UserInfo, PasswordChangeRequest, UserListResponse,
|
|||
from app.core.database import get_db_connection
|
||||
from app.core.auth import get_current_user
|
||||
from app.core.response import create_api_response
|
||||
from app.services.system_config_service import SystemConfigService
|
||||
import app.core.config as config_module
|
||||
import hashlib
|
||||
import datetime
|
||||
|
|
@ -46,7 +47,7 @@ def create_user(request: CreateUserRequest, current_user: dict = Depends(get_cur
|
|||
if cursor.fetchone():
|
||||
return create_api_response(code="400", message="用户名已存在")
|
||||
|
||||
password = request.password if request.password else config_module.DEFAULT_RESET_PASSWORD
|
||||
password = request.password if request.password else SystemConfigService.get_default_reset_password()
|
||||
hashed_password = hash_password(password)
|
||||
|
||||
query = "INSERT INTO users (username, password_hash, caption, email, role_id, created_at) VALUES (%s, %s, %s, %s, %s, %s)"
|
||||
|
|
@ -138,7 +139,7 @@ def reset_password(user_id: int, current_user: dict = Depends(get_current_user))
|
|||
if not cursor.fetchone():
|
||||
return create_api_response(code="404", message="用户不存在")
|
||||
|
||||
hashed_password = hash_password(config_module.DEFAULT_RESET_PASSWORD)
|
||||
hashed_password = hash_password(SystemConfigService.get_default_reset_password())
|
||||
|
||||
query = "UPDATE users SET password_hash = %s WHERE user_id = %s"
|
||||
cursor.execute(query, (hashed_password, user_id))
|
||||
|
|
@ -162,7 +163,7 @@ def get_all_users(
|
|||
count_params = []
|
||||
|
||||
if role_id is not None:
|
||||
where_conditions.append("role_id = %s")
|
||||
where_conditions.append("u.role_id = %s")
|
||||
count_params.append(role_id)
|
||||
|
||||
if search:
|
||||
|
|
@ -171,7 +172,7 @@ def get_all_users(
|
|||
count_params.extend([search_pattern, search_pattern])
|
||||
|
||||
# 统计查询
|
||||
count_query = "SELECT COUNT(*) as total FROM users"
|
||||
count_query = "SELECT COUNT(*) as total FROM users u"
|
||||
if where_conditions:
|
||||
count_query += " WHERE " + " AND ".join(where_conditions)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from app.models.models import VoiceprintStatus, VoiceprintTemplate
|
|||
from app.core.auth import get_current_user
|
||||
from app.core.response import create_api_response
|
||||
from app.services.voiceprint_service import voiceprint_service
|
||||
from app.services.system_config_service import SystemConfigService
|
||||
import app.core.config as config_module
|
||||
|
||||
router = APIRouter()
|
||||
|
|
@ -23,7 +24,16 @@ def get_voiceprint_template(current_user: dict = Depends(get_current_user)):
|
|||
权限:需要登录
|
||||
"""
|
||||
try:
|
||||
template_data = VoiceprintTemplate(**config_module.VOICEPRINT_CONFIG)
|
||||
# 动态从数据库获取声纹模版
|
||||
template_text = SystemConfigService.get_voiceprint_template(
|
||||
default="我正在进行声纹采集,这段语音将用于身份识别和验证。\n\n声纹技术能够准确识别每个人独特的声音特征。"
|
||||
)
|
||||
template_data = VoiceprintTemplate(
|
||||
template_text=template_text,
|
||||
duration_seconds=config_module.VOICEPRINT_CONFIG.get('duration_seconds', 12),
|
||||
sample_rate=config_module.VOICEPRINT_CONFIG.get('sample_rate', 16000),
|
||||
channels=config_module.VOICEPRINT_CONFIG.get('channels', 1)
|
||||
)
|
||||
return create_api_response(code="200", message="获取朗读模板成功", data=template_data.dict())
|
||||
except Exception as e:
|
||||
return create_api_response(code="500", message=f"获取朗读模板失败: {str(e)}")
|
||||
|
|
|
|||
|
|
@ -29,10 +29,10 @@ CLIENT_DIR.mkdir(exist_ok=True)
|
|||
|
||||
# 数据库配置
|
||||
DATABASE_CONFIG = {
|
||||
'host': os.getenv('DB_HOST', '10.100.51.161'),
|
||||
'host': os.getenv('DB_HOST', '10.100.51.51'),
|
||||
'user': os.getenv('DB_USER', 'root'),
|
||||
'password': os.getenv('DB_PASSWORD', 'sagacity'),
|
||||
'database': os.getenv('DB_NAME', 'imeeting'),
|
||||
'password': os.getenv('DB_PASSWORD', 'Unis@123'),
|
||||
'database': os.getenv('DB_NAME', 'imeeting_dev'),
|
||||
'port': int(os.getenv('DB_PORT', '3306')),
|
||||
'charset': 'utf8mb4'
|
||||
}
|
||||
|
|
@ -56,10 +56,10 @@ APP_CONFIG = {
|
|||
|
||||
# Redis配置
|
||||
REDIS_CONFIG = {
|
||||
'host': os.getenv('REDIS_HOST', '10.100.51.161'),
|
||||
'host': os.getenv('REDIS_HOST', '10.100.51.51'),
|
||||
'port': int(os.getenv('REDIS_PORT', '6379')),
|
||||
'db': int(os.getenv('REDIS_DB', '0')),
|
||||
'password': os.getenv('REDIS_PASSWORD', ''),
|
||||
'password': os.getenv('REDIS_PASSWORD', 'Unis@123'),
|
||||
'decode_responses': True
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,9 +13,8 @@ import uvicorn
|
|||
from fastapi import FastAPI, Request, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from app.api.endpoints import auth, users, meetings, tags, admin, admin_dashboard, tasks, prompts, knowledge_base, client_downloads, voiceprint, audio, dict_data
|
||||
from app.api.endpoints import auth, users, meetings, tags, admin, admin_dashboard, tasks, prompts, knowledge_base, client_downloads, voiceprint, audio, dict_data, hot_words
|
||||
from app.core.config import UPLOAD_DIR, API_CONFIG
|
||||
from app.api.endpoints.admin import load_system_config
|
||||
|
||||
app = FastAPI(
|
||||
title="iMeeting API",
|
||||
|
|
@ -23,9 +22,6 @@ app = FastAPI(
|
|||
version="1.0.2"
|
||||
)
|
||||
|
||||
# 加载系统配置
|
||||
load_system_config()
|
||||
|
||||
# 添加CORS中间件
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
|
|
@ -53,6 +49,7 @@ app.include_router(client_downloads.router, prefix="/api", tags=["ClientDownload
|
|||
app.include_router(dict_data.router, prefix="/api", tags=["DictData"])
|
||||
app.include_router(voiceprint.router, prefix="/api", tags=["Voiceprint"])
|
||||
app.include_router(audio.router, prefix="/api", tags=["Audio"])
|
||||
app.include_router(hot_words.router, prefix="/api", tags=["HotWords"])
|
||||
|
||||
@app.get("/")
|
||||
def read_root():
|
||||
|
|
|
|||
|
|
@ -404,23 +404,6 @@ class AsyncMeetingService:
|
|||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor()
|
||||
|
||||
# 如果没有指定 prompt_id,获取默认的会议总结模版ID
|
||||
if prompt_id is None:
|
||||
print(f"[Meeting Service] prompt_id is None, fetching default template for MEETING_TASK")
|
||||
cursor.execute(
|
||||
"SELECT id FROM prompts WHERE task_type = 'MEETING_TASK' AND is_default = TRUE AND is_active = TRUE LIMIT 1"
|
||||
)
|
||||
default_prompt = cursor.fetchone()
|
||||
if default_prompt:
|
||||
prompt_id = default_prompt[0]
|
||||
print(f"[Meeting Service] Found default template ID: {prompt_id}")
|
||||
else:
|
||||
print(f"[Meeting Service] WARNING: No default template found for MEETING_TASK!")
|
||||
else:
|
||||
print(f"[Meeting Service] Using provided prompt_id: {prompt_id}")
|
||||
|
||||
print(f"[Meeting Service] Inserting task into llm_tasks - task_id: {task_id}, meeting_id: {meeting_id}, prompt_id: {prompt_id}")
|
||||
insert_query = "INSERT INTO llm_tasks (task_id, meeting_id, user_prompt, prompt_id, status, progress, created_at) VALUES (%s, %s, %s, %s, 'pending', 0, NOW())"
|
||||
cursor.execute(insert_query, (task_id, meeting_id, user_prompt, prompt_id))
|
||||
connection.commit()
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from dashscope.audio.asr import Transcription
|
|||
|
||||
from app.core.config import QWEN_API_KEY, REDIS_CONFIG, APP_CONFIG
|
||||
from app.core.database import get_db_connection
|
||||
from app.services.system_config_service import SystemConfigService
|
||||
|
||||
|
||||
class AsyncTranscriptionService:
|
||||
|
|
@ -58,17 +59,24 @@ class AsyncTranscriptionService:
|
|||
# 2. 构造完整的文件URL
|
||||
file_url = f"{self.base_url}{audio_file_path}"
|
||||
|
||||
print(f"Starting transcription for meeting_id: {meeting_id}, file_url: {file_url}")
|
||||
# 获取热词表ID (asr_vocabulary_id)
|
||||
vocabulary_id = SystemConfigService.get_asr_vocabulary_id()
|
||||
|
||||
print(f"Starting transcription for meeting_id: {meeting_id}, file_url: {file_url}, vocabulary_id: {vocabulary_id}")
|
||||
|
||||
# 3. 调用Paraformer异步API
|
||||
task_response = Transcription.async_call(
|
||||
model='paraformer-v2',
|
||||
file_urls=[file_url],
|
||||
language_hints=['zh', 'en'],
|
||||
disfluency_removal_enabled=True,
|
||||
diarization_enabled=True,
|
||||
speaker_count=10
|
||||
)
|
||||
call_params = {
|
||||
'model': 'paraformer-v2',
|
||||
'file_urls': [file_url],
|
||||
'language_hints': ['zh', 'en'],
|
||||
'disfluency_removal_enabled': True,
|
||||
'diarization_enabled': True,
|
||||
'speaker_count': 10
|
||||
}
|
||||
if vocabulary_id:
|
||||
call_params['vocabulary_id'] = vocabulary_id
|
||||
|
||||
task_response = Transcription.async_call(**call_params)
|
||||
|
||||
if task_response.status_code != HTTPStatus.OK:
|
||||
print(f"Failed to start transcription: {task_response.status_code}, {task_response.message}")
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from http import HTTPStatus
|
|||
from typing import Optional, Dict, List, Generator
|
||||
import app.core.config as config_module
|
||||
from app.core.database import get_db_connection
|
||||
from app.services.system_config_service import SystemConfigService
|
||||
|
||||
|
||||
class LLMService:
|
||||
|
|
@ -16,27 +17,28 @@ class LLMService:
|
|||
@property
|
||||
def model_name(self):
|
||||
"""动态获取模型名称"""
|
||||
return config_module.LLM_CONFIG["model_name"]
|
||||
return SystemConfigService.get_llm_model_name(default="qwen-plus")
|
||||
|
||||
@property
|
||||
def system_prompt(self):
|
||||
"""动态获取系统提示词"""
|
||||
return config_module.LLM_CONFIG["system_prompt"]
|
||||
"""动态获取系统提示词(fallback,优先使用prompts表)"""
|
||||
# 保留config中的system_prompt作为后备
|
||||
return config_module.LLM_CONFIG.get("system_prompt", "请根据提供的内容进行总结和分析。")
|
||||
|
||||
@property
|
||||
def time_out(self):
|
||||
"""动态获取超时时间"""
|
||||
return config_module.LLM_CONFIG["time_out"]
|
||||
return SystemConfigService.get_llm_timeout(default=120)
|
||||
|
||||
@property
|
||||
def temperature(self):
|
||||
"""动态获取temperature"""
|
||||
return config_module.LLM_CONFIG["temperature"]
|
||||
return SystemConfigService.get_llm_temperature(default=0.7)
|
||||
|
||||
@property
|
||||
def top_p(self):
|
||||
"""动态获取top_p"""
|
||||
return config_module.LLM_CONFIG["top_p"]
|
||||
return SystemConfigService.get_llm_top_p(default=0.9)
|
||||
|
||||
def get_task_prompt(self, task_type: str, cursor=None, prompt_id: Optional[int] = None) -> str:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -0,0 +1,237 @@
|
|||
import json
|
||||
from typing import Optional, Dict, Any
|
||||
from app.core.database import get_db_connection
|
||||
|
||||
|
||||
class SystemConfigService:
|
||||
"""系统配置服务 - 从 dict_data 表中读取和保存 system_config 类型的配置"""
|
||||
|
||||
DICT_TYPE = 'system_config'
|
||||
|
||||
# 配置键常量
|
||||
ASR_VOCABULARY_ID = 'asr_vocabulary_id'
|
||||
VOICEPRINT_TEMPLATE = 'voiceprint_template'
|
||||
TIMELINE_PAGESIZE = 'timeline_pagesize'
|
||||
DEFAULT_RESET_PASSWORD = 'default_reset_password'
|
||||
MAX_AUDIO_SIZE = 'max_audio_size'
|
||||
|
||||
# LLM模型配置
|
||||
LLM_MODEL_NAME = 'llm_model_name'
|
||||
LLM_TIMEOUT = 'llm_timeout'
|
||||
LLM_TEMPERATURE = 'llm_temperature'
|
||||
LLM_TOP_P = 'llm_top_p'
|
||||
|
||||
@classmethod
|
||||
def get_config(cls, dict_code: str, default_value: Any = None) -> Any:
|
||||
"""
|
||||
获取指定配置项的值
|
||||
|
||||
Args:
|
||||
dict_code: 配置项编码
|
||||
default_value: 默认值,如果配置不存在则返回此值
|
||||
|
||||
Returns:
|
||||
配置项的值
|
||||
"""
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
query = """
|
||||
SELECT extension_attr
|
||||
FROM dict_data
|
||||
WHERE dict_type = %s AND dict_code = %s AND status = 1
|
||||
LIMIT 1
|
||||
"""
|
||||
cursor.execute(query, (cls.DICT_TYPE, dict_code))
|
||||
result = cursor.fetchone()
|
||||
cursor.close()
|
||||
|
||||
if result and result['extension_attr']:
|
||||
try:
|
||||
ext_attr = json.loads(result['extension_attr']) if isinstance(result['extension_attr'], str) else result['extension_attr']
|
||||
return ext_attr.get('value', default_value)
|
||||
except (json.JSONDecodeError, AttributeError):
|
||||
pass
|
||||
|
||||
return default_value
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting config {dict_code}: {e}")
|
||||
return default_value
|
||||
|
||||
@classmethod
|
||||
def set_config(cls, dict_code: str, value: Any, label_cn: str = None) -> bool:
|
||||
"""
|
||||
设置指定配置项的值
|
||||
|
||||
Args:
|
||||
dict_code: 配置项编码
|
||||
value: 配置值
|
||||
label_cn: 配置项中文名称(仅在配置不存在时需要)
|
||||
|
||||
Returns:
|
||||
是否设置成功
|
||||
"""
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
# 检查配置是否存在
|
||||
cursor.execute(
|
||||
"SELECT id FROM dict_data WHERE dict_type = %s AND dict_code = %s",
|
||||
(cls.DICT_TYPE, dict_code)
|
||||
)
|
||||
existing = cursor.fetchone()
|
||||
|
||||
extension_attr = json.dumps({"value": value}, ensure_ascii=False)
|
||||
|
||||
if existing:
|
||||
# 更新现有配置
|
||||
update_query = """
|
||||
UPDATE dict_data
|
||||
SET extension_attr = %s, update_time = NOW()
|
||||
WHERE dict_type = %s AND dict_code = %s
|
||||
"""
|
||||
cursor.execute(update_query, (extension_attr, cls.DICT_TYPE, dict_code))
|
||||
else:
|
||||
# 插入新配置
|
||||
if not label_cn:
|
||||
label_cn = dict_code
|
||||
|
||||
insert_query = """
|
||||
INSERT INTO dict_data (
|
||||
dict_type, dict_code, parent_code, label_cn,
|
||||
extension_attr, status, sort_order
|
||||
) VALUES (%s, %s, 'ROOT', %s, %s, 1, 0)
|
||||
"""
|
||||
cursor.execute(insert_query, (cls.DICT_TYPE, dict_code, label_cn, extension_attr))
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error setting config {dict_code}: {e}")
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_all_configs(cls) -> Dict[str, Any]:
|
||||
"""
|
||||
获取所有系统配置
|
||||
|
||||
Returns:
|
||||
配置字典 {dict_code: value}
|
||||
"""
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
query = """
|
||||
SELECT dict_code, label_cn, extension_attr
|
||||
FROM dict_data
|
||||
WHERE dict_type = %s AND status = 1
|
||||
ORDER BY sort_order
|
||||
"""
|
||||
cursor.execute(query, (cls.DICT_TYPE,))
|
||||
results = cursor.fetchall()
|
||||
cursor.close()
|
||||
|
||||
configs = {}
|
||||
for row in results:
|
||||
if row['extension_attr']:
|
||||
try:
|
||||
ext_attr = json.loads(row['extension_attr']) if isinstance(row['extension_attr'], str) else row['extension_attr']
|
||||
configs[row['dict_code']] = ext_attr.get('value')
|
||||
except (json.JSONDecodeError, AttributeError):
|
||||
configs[row['dict_code']] = None
|
||||
else:
|
||||
configs[row['dict_code']] = None
|
||||
|
||||
return configs
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting all configs: {e}")
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def batch_set_configs(cls, configs: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
批量设置配置项
|
||||
|
||||
Args:
|
||||
configs: 配置字典 {dict_code: value}
|
||||
|
||||
Returns:
|
||||
是否全部设置成功
|
||||
"""
|
||||
success = True
|
||||
for dict_code, value in configs.items():
|
||||
if not cls.set_config(dict_code, value):
|
||||
success = False
|
||||
return success
|
||||
|
||||
# 便捷方法:获取特定配置
|
||||
@classmethod
|
||||
def get_asr_vocabulary_id(cls) -> Optional[str]:
|
||||
"""获取ASR热词字典ID"""
|
||||
return cls.get_config(cls.ASR_VOCABULARY_ID)
|
||||
|
||||
@classmethod
|
||||
def get_voiceprint_template(cls, default: str = "") -> str:
|
||||
"""获取声纹采集模版"""
|
||||
return cls.get_config(cls.VOICEPRINT_TEMPLATE, default)
|
||||
|
||||
@classmethod
|
||||
def get_timeline_pagesize(cls, default: int = 10) -> int:
|
||||
"""获取会议时间轴每页数量"""
|
||||
value = cls.get_config(cls.TIMELINE_PAGESIZE, str(default))
|
||||
try:
|
||||
return int(value)
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
@classmethod
|
||||
def get_default_reset_password(cls, default: str = "111111") -> str:
|
||||
"""获取默认重置密码"""
|
||||
return cls.get_config(cls.DEFAULT_RESET_PASSWORD, default)
|
||||
|
||||
@classmethod
|
||||
def get_max_audio_size(cls, default: int = 100) -> int:
|
||||
"""获取上传音频文件大小限制(MB)"""
|
||||
value = cls.get_config(cls.MAX_AUDIO_SIZE, str(default))
|
||||
try:
|
||||
return int(value)
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
# LLM模型配置获取方法
|
||||
@classmethod
|
||||
def get_llm_model_name(cls, default: str = "qwen-plus") -> str:
|
||||
"""获取LLM模型名称"""
|
||||
return cls.get_config(cls.LLM_MODEL_NAME, default)
|
||||
|
||||
@classmethod
|
||||
def get_llm_timeout(cls, default: int = 120) -> int:
|
||||
"""获取LLM超时时间(秒)"""
|
||||
value = cls.get_config(cls.LLM_TIMEOUT, str(default))
|
||||
try:
|
||||
return int(value)
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
@classmethod
|
||||
def get_llm_temperature(cls, default: float = 0.7) -> float:
|
||||
"""获取LLM temperature参数"""
|
||||
value = cls.get_config(cls.LLM_TEMPERATURE, str(default))
|
||||
try:
|
||||
return float(value)
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
@classmethod
|
||||
def get_llm_top_p(cls, default: float = 0.9) -> float:
|
||||
"""获取LLM top_p参数"""
|
||||
value = cls.get_config(cls.LLM_TOP_P, str(default))
|
||||
try:
|
||||
return float(value)
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
|
@ -17,7 +17,6 @@ class VoiceprintService:
|
|||
|
||||
def __init__(self):
|
||||
self.voiceprint_dir = config_module.VOICEPRINT_DIR
|
||||
self.voiceprint_config = config_module.VOICEPRINT_CONFIG
|
||||
|
||||
def get_user_voiceprint_status(self, user_id: int) -> Dict:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
import mysql.connector
|
||||
|
||||
# 连接数据库
|
||||
conn = mysql.connector.connect(
|
||||
host="10.100.51.51",
|
||||
port=3306,
|
||||
user="root",
|
||||
password="Unis@123",
|
||||
database="imeeting_dev"
|
||||
)
|
||||
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("=" * 80)
|
||||
print("dict_data 表结构:")
|
||||
print("=" * 80)
|
||||
cursor.execute("SHOW CREATE TABLE dict_data")
|
||||
result = cursor.fetchone()
|
||||
print(result[1])
|
||||
print("\n")
|
||||
|
||||
print("=" * 80)
|
||||
print("dict_data 表数据:")
|
||||
print("=" * 80)
|
||||
cursor.execute("SELECT * FROM dict_data ORDER BY dict_code, sort_order")
|
||||
rows = cursor.fetchall()
|
||||
cursor.execute("SHOW COLUMNS FROM dict_data")
|
||||
columns = [col[0] for col in cursor.fetchall()]
|
||||
print(" | ".join(columns))
|
||||
print("-" * 80)
|
||||
for row in rows:
|
||||
print(" | ".join(str(val) for val in row))
|
||||
print("\n")
|
||||
|
||||
print("=" * 80)
|
||||
print("client_download 表结构:")
|
||||
print("=" * 80)
|
||||
cursor.execute("SHOW CREATE TABLE client_download")
|
||||
result = cursor.fetchone()
|
||||
print(result[1])
|
||||
print("\n")
|
||||
|
||||
print("=" * 80)
|
||||
print("client_download 表数据:")
|
||||
print("=" * 80)
|
||||
cursor.execute("SELECT * FROM client_download")
|
||||
rows = cursor.fetchall()
|
||||
cursor.execute("SHOW COLUMNS FROM client_download")
|
||||
columns = [col[0] for col in cursor.fetchall()]
|
||||
print(" | ".join(columns))
|
||||
print("-" * 80)
|
||||
for row in rows:
|
||||
print(" | ".join(str(val) for val in row))
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"model_name": "qwen-plus",
|
||||
"template_text": "我正在进行声纹采集,这段语音将用于身份识别和验证。\n声纹技术能够识别每个人独特的声音特征,用于人声识别应用。",
|
||||
"DEFAULT_RESET_PASSWORD": "123456",
|
||||
"MAX_FILE_SIZE": 209715200,
|
||||
"TIMELINE_PAGESIZE": 10
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
CREATE TABLE IF NOT EXISTS `hot_words` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`text` VARCHAR(255) NOT NULL COMMENT '热词内容',
|
||||
`weight` INT NOT NULL DEFAULT 4 COMMENT '词汇权重 (1-10)',
|
||||
`lang` VARCHAR(20) NOT NULL DEFAULT 'zh' COMMENT '语言 (zh/en)',
|
||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态 (1:启用, 0:禁用)',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_text_lang` (`text`, `lang`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统语音识别热词表';
|
||||
|
||||
-- 预留存储 Vocabulary ID 的配置项(如果不想用字典表存储配置,也可以在系统配置表中增加)
|
||||
INSERT INTO `dict_data` (dict_type, dict_code, parent_code, label_cn, status)
|
||||
VALUES ('system_config', 'asr_vocabulary_id', 'ROOT', '阿里云ASR热词表ID', 1)
|
||||
ON DUPLICATE KEY UPDATE label_cn='阿里云ASR热词表ID';
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
"""
|
||||
测试APK解析功能
|
||||
"""
|
||||
from app.utils.apk_parser import parse_apk_with_androguard
|
||||
|
||||
# 测试导入是否正常
|
||||
try:
|
||||
from androguard.core.apk import APK
|
||||
print("✅ androguard 导入成功")
|
||||
print(f" androguard 模块路径: {APK.__module__}")
|
||||
except ImportError as e:
|
||||
print(f"❌ androguard 导入失败: {e}")
|
||||
exit(1)
|
||||
|
||||
# 如果有测试APK文件,可以在这里测试解析
|
||||
# apk_path = "path/to/your/test.apk"
|
||||
# result = parse_apk_with_androguard(apk_path)
|
||||
# print(f"解析结果: {result}")
|
||||
|
||||
print("\n📋 测试结果:")
|
||||
print(" 1. androguard 库已成功安装")
|
||||
print(" 2. 导入路径正确: androguard.core.apk.APK")
|
||||
print(" 3. 可以正常使用 APK 解析功能")
|
||||
print("\n💡 提示: 请重启后台服务以使更改生效")
|
||||
Loading…
Reference in New Issue