670 lines
19 KiB
Python
670 lines
19 KiB
Python
"""
|
||
Review System API Routes
|
||
|
||
审核系统 API 路由
|
||
|
||
提供审核配置、执行、维度和自定义规则的管理接口
|
||
"""
|
||
from fastapi import APIRouter, Depends, HTTPException, status
|
||
from typing import List, Optional
|
||
import uuid
|
||
from datetime import datetime
|
||
|
||
from app.models.review import (
|
||
ReviewConfig,
|
||
ReviewConfigUpdate,
|
||
ReviewResult,
|
||
ReviewRequest,
|
||
ReviewResponse,
|
||
DimensionInfo,
|
||
DimensionConfig,
|
||
DimensionType,
|
||
CustomRule,
|
||
CustomRuleCreate,
|
||
CustomRuleUpdate,
|
||
ReviewPreset
|
||
)
|
||
from app.models.project import SeriesProject, Episode
|
||
from app.core.review.review_manager import ReviewManager, get_review_manager
|
||
from app.db.repositories import project_repo, episode_repo
|
||
from app.utils.logger import get_logger
|
||
|
||
|
||
logger = get_logger(__name__)
|
||
|
||
router = APIRouter(prefix="/review", tags=["审核系统"])
|
||
|
||
|
||
# ============================================
|
||
# 项目审核配置管理
|
||
# ============================================
|
||
|
||
@router.get("/projects/{project_id}/review-config", response_model=ReviewConfig)
|
||
async def get_review_config(project_id: str):
|
||
"""
|
||
获取项目的审核配置
|
||
|
||
Args:
|
||
project_id: 项目ID
|
||
|
||
Returns:
|
||
ReviewConfig: 审核配置
|
||
"""
|
||
project = await project_repo.get(project_id)
|
||
if not project:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"项目不存在: {project_id}"
|
||
)
|
||
|
||
# 从项目配置中获取审核配置
|
||
# 如果还没有配置,返回默认配置
|
||
review_config = getattr(project, 'reviewConfig', None)
|
||
if not review_config:
|
||
review_config = ReviewConfig()
|
||
|
||
return review_config
|
||
|
||
|
||
@router.put("/projects/{project_id}/review-config", response_model=ReviewConfig)
|
||
async def update_review_config(
|
||
project_id: str,
|
||
config_update: ReviewConfigUpdate
|
||
):
|
||
"""
|
||
更新项目的审核配置
|
||
|
||
Args:
|
||
project_id: 项目ID
|
||
config_update: 配置更新数据
|
||
|
||
Returns:
|
||
ReviewConfig: 更新后的配置
|
||
"""
|
||
project = await project_repo.get(project_id)
|
||
if not project:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"项目不存在: {project_id}"
|
||
)
|
||
|
||
try:
|
||
# 获取现有配置
|
||
current_config = getattr(project, 'reviewConfig', ReviewConfig())
|
||
|
||
# 更新配置
|
||
update_data = config_update.dict(exclude_unset=True)
|
||
|
||
for field, value in update_data.items():
|
||
setattr(current_config, field, value)
|
||
|
||
# 保存到项目
|
||
await project_repo.update(project_id, {
|
||
"reviewConfig": current_config.dict()
|
||
})
|
||
|
||
logger.info(f"更新项目审核配置: {project_id}")
|
||
|
||
return current_config
|
||
|
||
except Exception as e:
|
||
logger.error(f"更新审核配置失败: {str(e)}")
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"更新配置失败: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.get("/projects/{project_id}/review-presets", response_model=List[ReviewPreset])
|
||
async def get_review_presets(project_id: str = None):
|
||
"""
|
||
获取审核预设列表
|
||
|
||
Args:
|
||
project_id: 可选的项目ID,用于返回项目特定的预设
|
||
|
||
Returns:
|
||
List[ReviewPreset]: 预设列表
|
||
"""
|
||
# 内置预设
|
||
presets = [
|
||
ReviewPreset(
|
||
id="strict",
|
||
name="严格审核",
|
||
description="适用于高质量要求,检查所有维度",
|
||
category="quality",
|
||
config=ReviewConfig(
|
||
enabled_review_skills=["consistency_checker"],
|
||
overall_strictness=0.9,
|
||
dimension_settings={
|
||
DimensionType.consistency: DimensionConfig(enabled=True, strictness=0.9, weight=1.2),
|
||
DimensionType.quality: DimensionConfig(enabled=True, strictness=0.8, weight=1.0),
|
||
DimensionType.pacing: DimensionConfig(enabled=True, strictness=0.7, weight=0.8),
|
||
DimensionType.dialogue: DimensionConfig(enabled=True, strictness=0.8, weight=1.0),
|
||
DimensionType.character: DimensionConfig(enabled=True, strictness=0.9, weight=1.1),
|
||
DimensionType.plot: DimensionConfig(enabled=True, strictness=0.9, weight=1.2)
|
||
},
|
||
pass_threshold=85.0
|
||
)
|
||
),
|
||
ReviewPreset(
|
||
id="balanced",
|
||
name="平衡审核",
|
||
description="适用于一般创作需求,平衡质量和效率",
|
||
category="general",
|
||
config=ReviewConfig(
|
||
enabled_review_skills=["consistency_checker"],
|
||
overall_strictness=0.7,
|
||
dimension_settings={
|
||
DimensionType.consistency: DimensionConfig(enabled=True, strictness=0.7, weight=1.2),
|
||
DimensionType.quality: DimensionConfig(enabled=True, strictness=0.6, weight=1.0),
|
||
DimensionType.pacing: DimensionConfig(enabled=False, strictness=0.5, weight=0.8),
|
||
DimensionType.dialogue: DimensionConfig(enabled=True, strictness=0.6, weight=1.0),
|
||
DimensionType.character: DimensionConfig(enabled=True, strictness=0.7, weight=1.1),
|
||
DimensionType.plot: DimensionConfig(enabled=True, strictness=0.7, weight=1.2)
|
||
},
|
||
pass_threshold=75.0
|
||
)
|
||
),
|
||
ReviewPreset(
|
||
id="quick",
|
||
name="快速审核",
|
||
description="只检查核心维度,适用于快速迭代",
|
||
category="efficiency",
|
||
config=ReviewConfig(
|
||
enabled_review_skills=["consistency_checker"],
|
||
overall_strictness=0.5,
|
||
dimension_settings={
|
||
DimensionType.consistency: DimensionConfig(enabled=True, strictness=0.7, weight=1.5),
|
||
DimensionType.quality: DimensionConfig(enabled=False, strictness=0.5, weight=1.0),
|
||
DimensionType.pacing: DimensionConfig(enabled=False, strictness=0.5, weight=0.8),
|
||
DimensionType.dialogue: DimensionConfig(enabled=False, strictness=0.5, weight=1.0),
|
||
DimensionType.character: DimensionConfig(enabled=False, strictness=0.5, weight=1.1),
|
||
DimensionType.plot: DimensionConfig(enabled=True, strictness=0.6, weight=1.5)
|
||
},
|
||
pass_threshold=70.0
|
||
)
|
||
),
|
||
ReviewPreset(
|
||
id="dialogue_focused",
|
||
name="对话专项",
|
||
description="专注于对话质量和人物口吻",
|
||
category="specialized",
|
||
config=ReviewConfig(
|
||
enabled_review_skills=[],
|
||
overall_strictness=0.8,
|
||
dimension_settings={
|
||
DimensionType.consistency: DimensionConfig(enabled=True, strictness=0.6, weight=0.8),
|
||
DimensionType.dialogue: DimensionConfig(enabled=True, strictness=0.9, weight=1.5),
|
||
DimensionType.character: DimensionConfig(enabled=True, strictness=0.8, weight=1.2)
|
||
},
|
||
pass_threshold=75.0
|
||
)
|
||
)
|
||
]
|
||
|
||
return presets
|
||
|
||
|
||
# ============================================
|
||
# 审核执行
|
||
# ============================================
|
||
|
||
@router.post("/projects/{project_id}/episodes/{episode_number}/review", response_model=ReviewResponse)
|
||
async def review_episode(
|
||
project_id: str,
|
||
episode_number: int,
|
||
request: ReviewRequest,
|
||
review_mgr: ReviewManager = Depends(get_review_manager)
|
||
):
|
||
"""
|
||
执行剧集审核
|
||
|
||
Args:
|
||
project_id: 项目ID
|
||
episode_number: 集数
|
||
request: 审核请求
|
||
review_mgr: 审核管理器(依赖注入)
|
||
|
||
Returns:
|
||
ReviewResponse: 审核结果
|
||
"""
|
||
# 获取项目
|
||
project = await project_repo.get(project_id)
|
||
if not project:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"项目不存在: {project_id}"
|
||
)
|
||
|
||
# 获取剧集
|
||
episodes = await episode_repo.list_by_project(project_id)
|
||
episode = None
|
||
for ep in episodes:
|
||
if ep.number == episode_number:
|
||
episode = ep
|
||
break
|
||
|
||
if not episode:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"剧集不存在: EP{episode_number}"
|
||
)
|
||
|
||
# 检查剧集是否有内容
|
||
if not episode.content:
|
||
return ReviewResponse(
|
||
success=False,
|
||
message=f"剧集 EP{episode_number} 没有内容,无法审核",
|
||
episode_id=episode.id
|
||
)
|
||
|
||
try:
|
||
# 获取审核配置
|
||
config = getattr(project, 'reviewConfig', ReviewConfig())
|
||
|
||
# 执行审核
|
||
logger.info(f"开始审核: 项目 {project_id}, EP{episode_number}")
|
||
|
||
result = await review_mgr.review_episode(
|
||
project=project,
|
||
episode=episode,
|
||
config=config,
|
||
dimensions=request.dimensions
|
||
)
|
||
|
||
# 保存审核结果到剧集
|
||
await episode_repo.update(project_id, {
|
||
"reviewResult": result.dict(),
|
||
"status": "completed" if result.passed else "needs-review"
|
||
})
|
||
|
||
logger.info(f"审核完成: EP{episode_number}, 分数={result.overall_score:.1f}")
|
||
|
||
return ReviewResponse(
|
||
success=True,
|
||
review_result=result,
|
||
message=f"审核完成,总分: {result.overall_score:.1f}",
|
||
episode_id=episode.id
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"审核失败: {str(e)}")
|
||
return ReviewResponse(
|
||
success=False,
|
||
message=f"审核失败: {str(e)}",
|
||
episode_id=episode.id
|
||
)
|
||
|
||
|
||
@router.get("/projects/{project_id}/episodes/{episode_number}/review-result", response_model=Optional[ReviewResult])
|
||
async def get_review_result(project_id: str, episode_number: int):
|
||
"""
|
||
获取剧集的审核结果
|
||
|
||
Args:
|
||
project_id: 项目ID
|
||
episode_number: 集数
|
||
|
||
Returns:
|
||
ReviewResult: 审核结果,如果未审核则返回null
|
||
"""
|
||
episodes = await episode_repo.list_by_project(project_id)
|
||
episode = None
|
||
for ep in episodes:
|
||
if ep.number == episode_number:
|
||
episode = ep
|
||
break
|
||
|
||
if not episode:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"剧集不存在: EP{episode_number}"
|
||
)
|
||
|
||
# 返回审核结果(如果有)
|
||
return getattr(episode, 'reviewResult', None)
|
||
|
||
|
||
# ============================================
|
||
# 维度管理
|
||
# ============================================
|
||
|
||
@router.get("/dimensions", response_model=List[DimensionInfo])
|
||
async def list_dimensions():
|
||
"""
|
||
列出所有可用的审核维度
|
||
|
||
Returns:
|
||
List[DimensionInfo]: 维度信息列表
|
||
"""
|
||
from app.core.review.review_manager import ReviewManager
|
||
|
||
temp_manager = ReviewManager()
|
||
|
||
dimensions = []
|
||
for dim_type, dim_info in temp_manager.DIMENSIONS.items():
|
||
dimension_info = DimensionInfo(
|
||
id=dim_type,
|
||
name=dim_info["name"],
|
||
description=dim_info["description"],
|
||
default_strictness=dim_info["default_strictness"],
|
||
supported_skill_ids=dim_info["skill_ids"]
|
||
)
|
||
dimensions.append(dimension_info)
|
||
|
||
return dimensions
|
||
|
||
|
||
@router.get("/dimensions/{dimension_id}", response_model=DimensionInfo)
|
||
async def get_dimension_info(dimension_id: DimensionType):
|
||
"""
|
||
获取单个维度的详细信息
|
||
|
||
Args:
|
||
dimension_id: 维度ID
|
||
|
||
Returns:
|
||
DimensionInfo: 维度信息
|
||
"""
|
||
from app.core.review.review_manager import ReviewManager
|
||
|
||
temp_manager = ReviewManager()
|
||
dim_info = temp_manager.DIMENSIONS.get(dimension_id)
|
||
|
||
if not dim_info:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"维度不存在: {dimension_id}"
|
||
)
|
||
|
||
return DimensionInfo(
|
||
id=dimension_id,
|
||
name=dim_info["name"],
|
||
description=dim_info["description"],
|
||
default_strictness=dim_info["default_strictness"],
|
||
supported_skill_ids=dim_info["skill_ids"]
|
||
)
|
||
|
||
|
||
# ============================================
|
||
# 自定义规则管理
|
||
# ============================================
|
||
|
||
@router.get("/custom-rules", response_model=List[CustomRule])
|
||
async def list_custom_rules(
|
||
project_id: Optional[str] = None,
|
||
dimension: Optional[DimensionType] = None
|
||
):
|
||
"""
|
||
列出自定义规则
|
||
|
||
Args:
|
||
project_id: 可选的项目ID,只返回该项目的规则
|
||
dimension: 可选的维度筛选
|
||
|
||
Returns:
|
||
List[CustomRule]: 自定义规则列表
|
||
"""
|
||
if project_id:
|
||
# 从项目获取规则
|
||
project = await project_repo.get(project_id)
|
||
if not project:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"项目不存在: {project_id}"
|
||
)
|
||
|
||
config = getattr(project, 'reviewConfig', ReviewConfig())
|
||
rules = config.custom_rules
|
||
else:
|
||
# 返回空列表(暂时不支持全局规则)
|
||
rules = []
|
||
|
||
# 按维度筛选
|
||
if dimension:
|
||
rules = [r for r in rules if r.dimension == dimension]
|
||
|
||
return rules
|
||
|
||
|
||
@router.post("/custom-rules", response_model=CustomRule, status_code=status.HTTP_201_CREATED)
|
||
async def create_custom_rule(
|
||
rule_data: CustomRuleCreate,
|
||
project_id: str
|
||
):
|
||
"""
|
||
创建自定义规则
|
||
|
||
Args:
|
||
rule_data: 规则创建数据
|
||
project_id: 项目ID
|
||
|
||
Returns:
|
||
CustomRule: 创建的规则
|
||
"""
|
||
project = await project_repo.get(project_id)
|
||
if not project:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"项目不存在: {project_id}"
|
||
)
|
||
|
||
try:
|
||
# 创建规则
|
||
new_rule = CustomRule(
|
||
id=str(uuid.uuid4()),
|
||
name=rule_data.name,
|
||
description=rule_data.description,
|
||
trigger_condition=rule_data.trigger_condition,
|
||
dimension=rule_data.dimension,
|
||
severity=rule_data.severity,
|
||
enabled=True
|
||
)
|
||
|
||
# 添加到项目配置
|
||
config = getattr(project, 'reviewConfig', ReviewConfig())
|
||
config.custom_rules.append(new_rule)
|
||
|
||
# 保存
|
||
await project_repo.update(project_id, {
|
||
"reviewConfig": config.dict()
|
||
})
|
||
|
||
logger.info(f"创建自定义规则: {new_rule.id}")
|
||
|
||
return new_rule
|
||
|
||
except Exception as e:
|
||
logger.error(f"创建自定义规则失败: {str(e)}")
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"创建规则失败: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.put("/custom-rules/{rule_id}", response_model=CustomRule)
|
||
async def update_custom_rule(
|
||
rule_id: str,
|
||
rule_update: CustomRuleUpdate,
|
||
project_id: str
|
||
):
|
||
"""
|
||
更新自定义规则
|
||
|
||
Args:
|
||
rule_id: 规则ID
|
||
rule_update: 规则更新数据
|
||
project_id: 项目ID
|
||
|
||
Returns:
|
||
CustomRule: 更新后的规则
|
||
"""
|
||
project = await project_repo.get(project_id)
|
||
if not project:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"项目不存在: {project_id}"
|
||
)
|
||
|
||
config = getattr(project, 'reviewConfig', ReviewConfig())
|
||
|
||
# 查找规则
|
||
rule = None
|
||
for r in config.custom_rules:
|
||
if r.id == rule_id:
|
||
rule = r
|
||
break
|
||
|
||
if not rule:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"规则不存在: {rule_id}"
|
||
)
|
||
|
||
try:
|
||
# 更新规则
|
||
update_data = rule_update.dict(exclude_unset=True)
|
||
|
||
for field, value in update_data.items():
|
||
setattr(rule, field, value)
|
||
|
||
rule.updated_at = datetime.now()
|
||
|
||
# 保存
|
||
await project_repo.update(project_id, {
|
||
"reviewConfig": config.dict()
|
||
})
|
||
|
||
logger.info(f"更新自定义规则: {rule_id}")
|
||
|
||
return rule
|
||
|
||
except Exception as e:
|
||
logger.error(f"更新自定义规则失败: {str(e)}")
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"更新规则失败: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.delete("/custom-rules/{rule_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||
async def delete_custom_rule(rule_id: str, project_id: str):
|
||
"""
|
||
删除自定义规则
|
||
|
||
Args:
|
||
rule_id: 规则ID
|
||
project_id: 项目ID
|
||
|
||
Returns:
|
||
None
|
||
"""
|
||
project = await project_repo.get(project_id)
|
||
if not project:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"项目不存在: {project_id}"
|
||
)
|
||
|
||
config = getattr(project, 'reviewConfig', ReviewConfig())
|
||
|
||
# 查找并删除规则
|
||
original_count = len(config.custom_rules)
|
||
config.custom_rules = [r for r in config.custom_rules if r.id != rule_id]
|
||
|
||
if len(config.custom_rules) == original_count:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"规则不存在: {rule_id}"
|
||
)
|
||
|
||
try:
|
||
# 保存
|
||
await project_repo.update(project_id, {
|
||
"reviewConfig": config.dict()
|
||
})
|
||
|
||
logger.info(f"删除自定义规则: {rule_id}")
|
||
|
||
return None
|
||
|
||
except Exception as e:
|
||
logger.error(f"删除自定义规则失败: {str(e)}")
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"删除规则失败: {str(e)}"
|
||
)
|
||
|
||
|
||
@router.post("/custom-rules/{rule_id}/test")
|
||
async def test_custom_rule(
|
||
rule_id: str,
|
||
episode_id: str,
|
||
project_id: str,
|
||
review_mgr: ReviewManager = Depends(get_review_manager)
|
||
):
|
||
"""
|
||
测试自定义规则
|
||
|
||
Args:
|
||
rule_id: 规则ID
|
||
episode_id: 剧集ID
|
||
project_id: 项目ID
|
||
review_mgr: 审核管理器
|
||
|
||
Returns:
|
||
测试结果
|
||
"""
|
||
project = await project_repo.get(project_id)
|
||
if not project:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"项目不存在: {project_id}"
|
||
)
|
||
|
||
episode = await episode_repo.get(episode_id)
|
||
if not episode:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"剧集不存在: {episode_id}"
|
||
)
|
||
|
||
config = getattr(project, 'reviewConfig', ReviewConfig())
|
||
|
||
# 查找规则
|
||
rule = None
|
||
for r in config.custom_rules:
|
||
if r.id == rule_id:
|
||
rule = r
|
||
break
|
||
|
||
if not rule:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=f"规则不存在: {rule_id}"
|
||
)
|
||
|
||
try:
|
||
# 测试规则
|
||
issues = await review_mgr._check_single_rule(
|
||
episode=episode,
|
||
rule=rule,
|
||
config=config
|
||
)
|
||
|
||
return {
|
||
"rule_id": rule_id,
|
||
"rule_name": rule.name,
|
||
"episode_id": episode_id,
|
||
"violations": len(issues),
|
||
"issues": [issue.dict() for issue in issues]
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"测试规则失败: {str(e)}")
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"测试失败: {str(e)}"
|
||
)
|