Files
Pre_Seg_Server/backend/schemas.py
admin f88f9bdbb9 支持中空mask编辑和传播保洞
- 前端按 polygonRingCounts 维护外圈/内洞 ring 分组,中空 mask 在调整多边形时显示内洞顶点和插点手柄。

- 保存与回显标注时将中空结构拆分为 mask_data.polygons 和 mask_data.holes,导入/普通 mask 共享同一编辑体验。

- 自动传播 seed 携带 holes,SAM 2 seed 栅格化时扣除内洞,避免中空 mask 以实心形式传播。

- 传播结果轮廓提取改为保留层级内洞,并在同步传播和 Celery 传播落库时写回 holes 与 hasHoles。

- 传播 seed 签名纳入 holes,并加固保存结果时 holes 与原始 polygon 索引对齐。

- 补充前端保存/回显、Canvas 内洞编辑和后端 SAM 2 hole 处理测试。

- 更新 AGENTS、接口契约、需求冻结、设计冻结和测试计划文档,移除中空结构未实现的旧描述。
2026-05-03 18:28:46 +08:00

385 lines
9.9 KiB
Python

"""Pydantic schemas for request/response validation."""
from datetime import datetime
from typing import Literal, Optional, Any
from pydantic import BaseModel, ConfigDict
# ---------------------------------------------------------------------------
# Auth / user schemas
# ---------------------------------------------------------------------------
class UserOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
username: str
role: str
is_active: int
class LoginResponse(BaseModel):
token: str
token_type: str = "bearer"
username: str
user: UserOut
class AdminUserCreate(BaseModel):
username: str
password: str
role: str = "annotator"
is_active: bool = True
class AdminUserUpdate(BaseModel):
username: Optional[str] = None
password: Optional[str] = None
role: Optional[str] = None
is_active: Optional[bool] = None
class AuditLogOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
actor_user_id: Optional[int] = None
action: str
target_type: Optional[str] = None
target_id: Optional[str] = None
detail: Optional[dict[str, Any]] = None
created_at: datetime
class DemoFactoryResetRequest(BaseModel):
confirmation: str
# ---------------------------------------------------------------------------
# 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 ProjectCopyRequest(BaseModel):
mode: Literal["reset", "full"] = "reset"
name: Optional[str] = None
class ProjectOut(ProjectBase):
model_config = ConfigDict(from_attributes=True)
id: int
owner_user_id: Optional[int] = None
created_at: datetime
updated_at: datetime
frame_count: int = 0
class DemoFactoryResetOut(BaseModel):
admin_user: UserOut
project: ProjectOut
projects: list[ProjectOut]
deleted_counts: dict[str, int]
message: str
# ---------------------------------------------------------------------------
# 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
owner_user_id: Optional[int] = None
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 SmoothMaskRequest(BaseModel):
frame_id: Optional[int] = None
mask_data: dict[str, Any]
points: Optional[list[list[float]]] = None
bbox: Optional[list[float]] = None
strength: float = 0.0
method: str = "chaikin"
class SmoothMaskResponse(BaseModel):
polygons: list[list[list[float]]]
topology_anchor_count: int
topology_anchors: list[list[float]]
area: float
bbox: Optional[list[float]] = None
smoothing: dict[str, Any]
message: str
class PropagationSeed(BaseModel):
polygons: Optional[list[list[list[float]]]] = None
holes: Optional[list[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
source_mask_id: Optional[str] = None
source_annotation_id: Optional[int] = None
propagation_seed_signature: Optional[str] = None
smoothing: Optional[dict[str, Any]] = 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 PropagateTaskStep(BaseModel):
seed: PropagationSeed
direction: str = "forward"
max_frames: int = 30
class PropagateTaskRequest(BaseModel):
project_id: int
frame_id: int
model: Optional[str] = "sam2.1_hiera_tiny"
steps: list[PropagateTaskStep]
include_source: bool = False
save_annotations: bool = True
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