diff --git a/.DS_Store b/.DS_Store index 350e4a8..7ba8859 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/DEPLOYMENT_AUDIO_FEATURES.md b/DEPLOYMENT_AUDIO_FEATURES.md deleted file mode 100644 index d7c8c9c..0000000 --- a/DEPLOYMENT_AUDIO_FEATURES.md +++ /dev/null @@ -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子查询获取 diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 2ef37c1..0000000 --- a/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -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 -``` - -返回: -```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 -Content-Type: application/json - -{ - "user_prompt": "重点关注技术讨论", - "prompt_id": 5 -} -``` - -### 3. 上传音频时指定模版 -```bash -POST /api/meetings/upload-audio -Authorization: Bearer -Content-Type: multipart/form-data - -- audio_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 diff --git a/KB_PROMPT_ID_IMPLEMENTATION_SUMMARY.md b/KB_PROMPT_ID_IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index cbc9505..0000000 --- a/KB_PROMPT_ID_IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -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 -``` - -返回: -```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 -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(表示未指定) - -## 总结 - -知识库提示词模版选择功能已完全实现并通过测试,与会议总结功能保持一致的设计和实现方式。用户现在可以在创建知识库时选择不同的生成模版,以满足不同场景的需求。 diff --git a/app.zip b/app.zip index a857980..ee3f4b9 100644 Binary files a/app.zip and b/app.zip differ diff --git a/app/api/endpoints/admin.py b/app/api/endpoints/admin.py index e350b71..5fa8960 100644 --- a/app/api/endpoints/admin.py +++ b/app/api/endpoints/admin.py @@ -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") diff --git a/app/api/endpoints/audio.py b/app/api/endpoints/audio.py index 56d589b..aaef6ba 100644 --- a/app/api/endpoints/audio.py +++ b/app/api/endpoints/audio.py @@ -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小时后过期 diff --git a/app/api/endpoints/dict_data.py b/app/api/endpoints/dict_data.py index b5b2114..b5c03c7 100644 --- a/app/api/endpoints/dict_data.py +++ b/app/api/endpoints/dict_data.py @@ -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") diff --git a/app/api/endpoints/hot_words.py b/app/api/endpoints/hot_words.py new file mode 100644 index 0000000..3f83cc7 --- /dev/null +++ b/app/api/endpoints/hot_words.py @@ -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)}") diff --git a/app/api/endpoints/meetings.py b/app/api/endpoints/meetings.py index 9df41bf..670238f 100644 --- a/app/api/endpoints/meetings.py +++ b/app/api/endpoints/meetings.py @@ -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: diff --git a/app/api/endpoints/users.py b/app/api/endpoints/users.py index ff89524..324a173 100644 --- a/app/api/endpoints/users.py +++ b/app/api/endpoints/users.py @@ -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) diff --git a/app/api/endpoints/voiceprint.py b/app/api/endpoints/voiceprint.py index b7e333b..eca2d48 100644 --- a/app/api/endpoints/voiceprint.py +++ b/app/api/endpoints/voiceprint.py @@ -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)}") diff --git a/app/core/config.py b/app/core/config.py index fd594a1..3ae835c 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -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 } diff --git a/app/main.py b/app/main.py index 4e9e3b8..7635c80 100644 --- a/app/main.py +++ b/app/main.py @@ -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(): diff --git a/app/services/async_meeting_service.py b/app/services/async_meeting_service.py index 363bf7c..41dfbb2 100644 --- a/app/services/async_meeting_service.py +++ b/app/services/async_meeting_service.py @@ -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() diff --git a/app/services/async_transcription_service.py b/app/services/async_transcription_service.py index 4d3b85c..4d14e8c 100644 --- a/app/services/async_transcription_service.py +++ b/app/services/async_transcription_service.py @@ -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}") diff --git a/app/services/llm_service.py b/app/services/llm_service.py index a08ee0d..1f8429f 100644 --- a/app/services/llm_service.py +++ b/app/services/llm_service.py @@ -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: """ diff --git a/app/services/system_config_service.py b/app/services/system_config_service.py new file mode 100644 index 0000000..619f18c --- /dev/null +++ b/app/services/system_config_service.py @@ -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 diff --git a/app/services/voiceprint_service.py b/app/services/voiceprint_service.py index 13b04f7..e394244 100644 --- a/app/services/voiceprint_service.py +++ b/app/services/voiceprint_service.py @@ -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: """ diff --git a/check_db_structure.py b/check_db_structure.py deleted file mode 100644 index 745f056..0000000 --- a/check_db_structure.py +++ /dev/null @@ -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() diff --git a/config/system_config.json b/config/system_config.json deleted file mode 100644 index fed50d7..0000000 --- a/config/system_config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "model_name": "qwen-plus", - "template_text": "我正在进行声纹采集,这段语音将用于身份识别和验证。\n声纹技术能够识别每个人独特的声音特征,用于人声识别应用。", - "DEFAULT_RESET_PASSWORD": "123456", - "MAX_FILE_SIZE": 209715200, - "TIMELINE_PAGESIZE": 10 -} \ No newline at end of file diff --git a/sql/add_hot_words_table.sql b/sql/add_hot_words_table.sql new file mode 100644 index 0000000..ee6351e --- /dev/null +++ b/sql/add_hot_words_table.sql @@ -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'; diff --git a/test_apk_parser.py b/test_apk_parser.py deleted file mode 100644 index 10c03f9..0000000 --- a/test_apk_parser.py +++ /dev/null @@ -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💡 提示: 请重启后台服务以使更改生效")