修正了进度展示

main
mula.liu 2026-01-09 16:06:24 +08:00
parent 5a34df3944
commit f616cb3fc3
23 changed files with 525 additions and 672 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -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子查询获取

View File

@ -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

View File

@ -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表示未指定
## 总结
知识库提示词模版选择功能已完全实现并通过测试,与会议总结功能保持一致的设计和实现方式。用户现在可以在创建知识库时选择不同的生成模版,以满足不同场景的需求。

BIN
app.zip

Binary file not shown.

View File

@ -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")

View File

@ -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小时后过期

View File

@ -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")

View File

@ -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)}")

View File

@ -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:

View File

@ -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)

View File

@ -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)}")

View File

@ -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
}

View File

@ -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():

View File

@ -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()

View File

@ -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}")

View File

@ -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:
"""

View File

@ -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

View File

@ -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:
"""

View File

@ -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()

View File

@ -1,7 +0,0 @@
{
"model_name": "qwen-plus",
"template_text": "我正在进行声纹采集,这段语音将用于身份识别和验证。\n声纹技术能够识别每个人独特的声音特征用于人声识别应用。",
"DEFAULT_RESET_PASSWORD": "123456",
"MAX_FILE_SIZE": 209715200,
"TIMELINE_PAGESIZE": 10
}

View File

@ -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';

View File

@ -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💡 提示: 请重启后台服务以使更改生效")