功能新增: - 新增 POST /api/ai/analyze-mask 后端接口,基于 mask polygon、bbox、points 和 score 返回置信度来源、面积、拓扑锚点和后端分析提示。 - 前端新增 analyzeMask API 封装,并在本体检查面板读取选中 mask 的后端几何属性和重新提取拓扑锚点结果。 - 右侧语义分类树点击分类时,会给当前选中 mask 换标签、更新 class 元数据,并将选中 mask 移到前端渲染最上层,方便继续编辑。 - 分割工作区画布新增上下文操作提示,覆盖多边形 Enter 完成、Esc 取消、首节点闭合、拖拽图形、点区域、SAM 点/框提示、区域合并/去除选择顺序和多边形编辑。 - AI 智能分割画布新增正向点、反向点、边界框选和视口控制的上下文提示。 - 自动传播交互收敛为参考帧加起止帧范围加单个“自动传播”按钮,默认使用当前参考帧全部 mask 作为 seed。 - 时间轴改为用浅蓝色进度条区段标记自动传播生成的帧,而不是已编辑帧竖线提示。 Bugfix: - AI 分割页无当前帧时移除外部演示背景图,改为明确空状态提示,避免误以为外部图片可参与真实推理。 - 工具栏魔法棒文案改为“打开 AI 智能分割”,避免误导为直接触发 SAM 推理。 - Canvas 底部当前图层信息改为显示真实选中 mask 标签和 annotation id,不再使用固定占位文本。 - 已保存标注回显时保留 mask metadata 中的传播来源、score 等字段,供时间轴和属性面板识别。 - 清理 server.ts 中遗留的 /api/login、/api/projects、/api/templates 内存 mock API,避免和 FastAPI 真实后端混淆。 测试: - 补充 analyze-mask 后端测试,覆盖后端几何属性和锚点返回。 - 补充 api.analyzeMask 前端契约测试,覆盖 normalized polygon、bbox、points 和 extract_skeleton payload。 - 补充本体面板测试,覆盖后端属性读取、自定义分类写回后端模板、选中 mask 换标签和置顶显示。 - 补充 Canvas 测试,覆盖上下文提示、多边形完成提示、布尔选择顺序提示、当前图层真实显示和编辑优先级。 - 补充 AI 分割测试,覆盖无帧空状态和提示工具上下文提示。 - 更新 Konva 测试 mock,支持拖动过程、stroke/dash/fillRule 等渲染断言。 文档: - 更新 README 和 AGENTS,说明 server.ts 不再保留业务 mock API。 - 更新 doc/02、doc/03、doc/04、doc/05、doc/07、doc/08、doc/09,记录后端属性分析、分类置顶显示、上下文提示、自动传播按钮、传播帧标记、测试覆盖和当前剩余限制。
282 lines
7.4 KiB
Python
282 lines
7.4 KiB
Python
"""Pydantic schemas for request/response validation."""
|
|
|
|
from datetime import datetime
|
|
from typing import Optional, Any
|
|
from pydantic import BaseModel, ConfigDict
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Project schemas
|
|
# ---------------------------------------------------------------------------
|
|
class ProjectBase(BaseModel):
|
|
name: str
|
|
description: Optional[str] = None
|
|
video_path: Optional[str] = None
|
|
thumbnail_url: Optional[str] = None
|
|
status: Optional[str] = "pending"
|
|
source_type: Optional[str] = "video"
|
|
original_fps: Optional[float] = None
|
|
parse_fps: Optional[float] = 30.0
|
|
|
|
|
|
class ProjectCreate(ProjectBase):
|
|
pass
|
|
|
|
|
|
class ProjectUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
description: Optional[str] = None
|
|
video_path: Optional[str] = None
|
|
thumbnail_url: Optional[str] = None
|
|
status: Optional[str] = None
|
|
source_type: Optional[str] = None
|
|
original_fps: Optional[float] = None
|
|
parse_fps: Optional[float] = None
|
|
|
|
|
|
class ProjectOut(ProjectBase):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
frame_count: int = 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Frame schemas
|
|
# ---------------------------------------------------------------------------
|
|
class FrameBase(BaseModel):
|
|
frame_index: int
|
|
image_url: str
|
|
width: Optional[int] = None
|
|
height: Optional[int] = None
|
|
timestamp_ms: Optional[float] = None
|
|
source_frame_number: Optional[int] = None
|
|
|
|
|
|
class FrameCreate(FrameBase):
|
|
project_id: int
|
|
|
|
|
|
class FrameOut(FrameBase):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
project_id: int
|
|
created_at: datetime
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Template schemas
|
|
# ---------------------------------------------------------------------------
|
|
class TemplateBase(BaseModel):
|
|
name: str
|
|
description: Optional[str] = None
|
|
color: str
|
|
z_index: int = 0
|
|
mapping_rules: Optional[dict[str, Any]] = None
|
|
classes: Optional[list[dict[str, Any]]] = None
|
|
rules: Optional[list[dict[str, Any]]] = None
|
|
|
|
|
|
class TemplateCreate(TemplateBase):
|
|
pass
|
|
|
|
|
|
class TemplateUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
description: Optional[str] = None
|
|
color: Optional[str] = None
|
|
z_index: Optional[int] = None
|
|
mapping_rules: Optional[dict[str, Any]] = None
|
|
classes: Optional[list[dict[str, Any]]] = None
|
|
rules: Optional[list[dict[str, Any]]] = None
|
|
|
|
|
|
class TemplateOut(TemplateBase):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
created_at: datetime
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Annotation schemas
|
|
# ---------------------------------------------------------------------------
|
|
class AnnotationBase(BaseModel):
|
|
project_id: int
|
|
frame_id: Optional[int] = None
|
|
template_id: Optional[int] = None
|
|
mask_data: Optional[dict[str, Any]] = None
|
|
points: Optional[list[list[float]]] = None
|
|
bbox: Optional[list[float]] = None
|
|
|
|
|
|
class AnnotationCreate(AnnotationBase):
|
|
pass
|
|
|
|
|
|
class AnnotationUpdate(BaseModel):
|
|
mask_data: Optional[dict[str, Any]] = None
|
|
points: Optional[list[list[float]]] = None
|
|
bbox: Optional[list[float]] = None
|
|
template_id: Optional[int] = None
|
|
|
|
|
|
class AnnotationOut(AnnotationBase):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Mask schemas
|
|
# ---------------------------------------------------------------------------
|
|
class MaskBase(BaseModel):
|
|
annotation_id: int
|
|
mask_url: str
|
|
format: str = "png"
|
|
|
|
|
|
class MaskCreate(MaskBase):
|
|
pass
|
|
|
|
|
|
class MaskOut(MaskBase):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
created_at: datetime
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Processing task schemas
|
|
# ---------------------------------------------------------------------------
|
|
class ProcessingTaskOut(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
task_type: str
|
|
status: str
|
|
progress: int
|
|
message: Optional[str] = None
|
|
project_id: Optional[int] = None
|
|
celery_task_id: Optional[str] = None
|
|
payload: Optional[dict[str, Any]] = None
|
|
result: Optional[dict[str, Any]] = None
|
|
error: Optional[str] = None
|
|
created_at: datetime
|
|
started_at: Optional[datetime] = None
|
|
finished_at: Optional[datetime] = None
|
|
updated_at: datetime
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# AI schemas
|
|
# ---------------------------------------------------------------------------
|
|
class PredictRequest(BaseModel):
|
|
image_id: int
|
|
prompt_type: str # point / box / semantic
|
|
prompt_data: Any
|
|
model: Optional[str] = None
|
|
options: Optional[dict[str, Any]] = None
|
|
|
|
|
|
class PredictResponse(BaseModel):
|
|
polygons: list[list[list[float]]]
|
|
scores: Optional[list[float]] = None
|
|
|
|
|
|
class MaskAnalysisRequest(BaseModel):
|
|
frame_id: Optional[int] = None
|
|
mask_data: dict[str, Any]
|
|
points: Optional[list[list[float]]] = None
|
|
bbox: Optional[list[float]] = None
|
|
extract_skeleton: bool = False
|
|
|
|
|
|
class MaskAnalysisResponse(BaseModel):
|
|
confidence: Optional[float] = None
|
|
confidence_source: str
|
|
topology_anchor_count: int
|
|
topology_anchors: list[list[float]]
|
|
area: float
|
|
bbox: Optional[list[float]] = None
|
|
source: Optional[str] = None
|
|
message: str
|
|
|
|
|
|
class PropagationSeed(BaseModel):
|
|
polygons: Optional[list[list[list[float]]]] = None
|
|
bbox: Optional[list[float]] = None
|
|
points: Optional[list[list[float]]] = None
|
|
labels: Optional[list[int]] = None
|
|
label: Optional[str] = None
|
|
color: Optional[str] = None
|
|
class_metadata: Optional[dict[str, Any]] = None
|
|
template_id: Optional[int] = None
|
|
|
|
|
|
class PropagateRequest(BaseModel):
|
|
project_id: int
|
|
frame_id: int
|
|
model: Optional[str] = "sam2.1_hiera_tiny"
|
|
seed: PropagationSeed
|
|
direction: str = "forward"
|
|
max_frames: int = 30
|
|
include_source: bool = False
|
|
save_annotations: bool = True
|
|
|
|
|
|
class PropagateResponse(BaseModel):
|
|
model: str
|
|
direction: str
|
|
source_frame_id: int
|
|
processed_frame_count: int
|
|
created_annotation_count: int
|
|
annotations: list[AnnotationOut]
|
|
|
|
|
|
class AiModelStatus(BaseModel):
|
|
id: str
|
|
label: str
|
|
available: bool
|
|
loaded: bool = False
|
|
device: str
|
|
supports: list[str]
|
|
message: str
|
|
package_available: bool = False
|
|
checkpoint_exists: bool = False
|
|
checkpoint_path: Optional[str] = None
|
|
python_ok: bool = True
|
|
torch_ok: bool = True
|
|
cuda_required: bool = False
|
|
external_available: bool = False
|
|
external_python: Optional[str] = None
|
|
|
|
|
|
class GpuStatus(BaseModel):
|
|
available: bool
|
|
device: str
|
|
name: Optional[str] = None
|
|
torch_available: bool
|
|
torch_version: Optional[str] = None
|
|
cuda_version: Optional[str] = None
|
|
|
|
|
|
class AiRuntimeStatus(BaseModel):
|
|
selected_model: str
|
|
gpu: GpuStatus
|
|
models: list[AiModelStatus]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Export schemas
|
|
# ---------------------------------------------------------------------------
|
|
class ExportStatus(BaseModel):
|
|
url: str
|
|
format: str
|