Files
Pre_Seg_Server/doc/04-api-contracts.md
admin 29a1a87e52 feat: 完善 SAM2.1 模型选择与标注工作流
- 后端 SAM2 引擎新增 sam2.1_hiera_tiny、sam2.1_hiera_small、sam2.1_hiera_base_plus、sam2.1_hiera_large 四个变体定义,并按变体维护 checkpoint/config、image predictor、video predictor、加载状态、错误信息和真实状态回报。

- 后端 SAM registry 仅暴露当前产品启用的 SAM2.1 变体,保留 sam2 作为 tiny 兼容别名,拒绝 sam3 产品入口,并把 point、box、interactive、auto、propagate 都分发到所选 SAM2.1 变体。

- 后端默认配置和下载脚本切换到 SAM2.1 checkpoint 命名,支持 legacy SAM2 checkpoint fallback,并在状态消息中标出 fallback 使用情况。

- 前端全局 AI 模型状态新增 SAM2.1 tiny/small/base+/large 类型和默认 tiny,API 请求默认携带 sam2.1_hiera_tiny,AI 页面提供模型变体选择和所选模型状态展示。

- AI 智能分割页移除当前产品不使用的 SAM3/文本提示入口,保留正向点、反向点、框选和参数开关;AI 页只展示本页生成的候选 mask,并支持遮罩清晰度调节、候选 mask 上继续加正/反点、清空本页候选、推送到工作区编辑。

- 工作区和 Canvas 补强 SAM2 交互式细化链路:框选后正/反点继续细化同一个候选 mask,反向点请求启用背景过滤,空结果会移除被否定候选;AI 推送到工作区后保留选中态和未保存 draft mask。

- 工作区标注保存闭环补强:未保存 mask 可归档保存,dirty saved mask 可更新,保存后用后端 saved annotation 替换已提交 draft,清空/删除已保存 mask 时同步后端删除。

- Dashboard 任务进度区改为展示 queued、running、success、failed、cancelled 最近任务,处理中统计只计算 queued/running,并保留近期完成记录。

- 时间轴在顶部时间进度条和底部缩略图导航轴之间新增已编辑帧标记带,基于当前项目帧内 masks 标出已有编辑/标注的帧,并支持点击标记跳转。

- 前端测试覆盖 SAM2.1 变体选择、模型状态徽标、AI 页候选隔离、遮罩透明度、候选上追加正/反点、推送工作区保留选择、Canvas 交互式细化、VideoWorkspace 传播/保存、Dashboard 进度和时间轴已编辑帧标记。

- 后端测试覆盖 SAM2.1 变体状态、sam2 alias 兼容、sam3 禁用、semantic 禁用、传播标注保存、Dashboard 最近任务状态和 SAM3 历史测试跳过说明。

- README、AGENTS 和 doc 文档同步当前真实进度,更新 SAM2.1 变体、SAM3 禁用、接口契约、设计冻结、需求冻结、前端元素审计、实施计划、FastAPI docs 说明和测试矩阵。
2026-05-01 23:39:53 +08:00

282 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 接口契约清单
## 前端 API 基础配置
位置:`src/lib/config.ts``src/lib/api.ts`
```ts
API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://<current-browser-host>:8000'
timeout: 30000
```
前端 request interceptor 会从 localStorage 读取 `token`,附加:
```http
Authorization: Bearer <token>
```
当前后端多数接口没有鉴权依赖,所以这个 header 主要是前端侧行为。
## 前端封装的 API
| 函数 | 方法与路径 | 状态 | 说明 |
|------|------------|------|------|
| `login(username, password)` | `POST /api/auth/login` | 对齐 | 后端返回 `{ token, username }`,前端只使用 token |
| `getProjects()` | `GET /api/projects` | 对齐 | 前端映射 `frame_count``thumbnail_url` 等字段 |
| `createProject(payload)` | `POST /api/projects` | 对齐 | 支持 `name``description``parse_fps` |
| `updateProject(id, payload)` | `PATCH /api/projects/{id}` | 对齐 | 后端是 `PATCH /api/projects/{id}` |
| `deleteProject(id)` | `DELETE /api/projects/{id}` | 对齐 | 当前 UI 未明显接入 |
| `getTemplates()` | `GET /api/templates` | 对齐 | 前端从 `mapping_rules` 取 classes/rules |
| `createTemplate(payload)` | `POST /api/templates` | 对齐 | 后端会打包 classes/rules 到 mapping_rules |
| `updateTemplate(id, payload)` | `PATCH /api/templates/{id}` | 对齐 | 模板编辑页使用 |
| `deleteTemplate(id)` | `DELETE /api/templates/{id}` | 对齐 | 模板编辑页使用 |
| `uploadMedia(file, projectId)` | `POST /api/media/upload` | 对齐 | multipart form-data |
| `uploadDicomBatch(files, projectId)` | `POST /api/media/upload/dicom` | 对齐 | multipart form-data |
| `parseMedia(projectId, options?)` | `POST /api/media/parse?project_id=...` | 对齐 | 创建异步拆帧任务并返回 task由项目库“生成帧”显式调用支持 `parse_fps``max_frames``target_width` |
| `getTask(taskId)` | `GET /api/tasks/{task_id}` | 对齐 | 查询异步任务状态 |
| `cancelTask(taskId)` | `POST /api/tasks/{task_id}/cancel` | 对齐 | 取消 queued/running 任务,后端写 cancelled 并尝试 revoke Celery |
| `retryTask(taskId)` | `POST /api/tasks/{task_id}/retry` | 对齐 | 对 failed/cancelled 任务创建新的 queued 重试任务 |
| `getProjectFrames(projectId)` | `GET /api/projects/{id}/frames` | 对齐 | 后端返回预签名 image_url以及 `timestamp_ms``source_frame_number` |
| `predictMask(payload)` | `POST /api/ai/predict` | 对齐 | 前端发送 `image_id/prompt_type/prompt_data/model`,并把后端 `polygons` 转为 `masks[].pathData` |
| `propagateMasks(payload)` | `POST /api/ai/propagate` | 对齐 | 当前帧 seed mask 向视频片段传播,并保存后续帧标注 |
| `getAiModelStatus(selectedModel?)` | `GET /api/ai/models/status` | 对齐 | 返回 GPU 和四个 SAM 2.1 变体状态;`selected_model=sam3` 返回不支持 |
| `getProjectAnnotations(projectId, frameId?)` | `GET /api/ai/annotations` | 对齐 | 前端加载工作区时用于回显已保存标注 |
| `saveAnnotation(payload)` | `POST /api/ai/annotate` | 对齐 | 工作区归档保存当前项目未保存 mask |
| `updateAnnotation(annotationId, payload)` | `PATCH /api/ai/annotations/{annotation_id}` | 对齐 | 工作区归档保存 dirty mask |
| `deleteAnnotation(annotationId)` | `DELETE /api/ai/annotations/{annotation_id}` | 对齐 | 工作区清空当前帧已保存标注 |
| `importGtMask(file, projectId, frameId, templateId?)` | `POST /api/ai/import-gt-mask` | 对齐 | multipart 上传 GT mask后端按非零像素值/连通域生成 polygon 标注和 seed point |
| `getDashboardOverview()` | `GET /api/dashboard/overview` | 对齐 | Dashboard 初始统计、队列和活动日志 |
| `exportCoco(projectId)` | `GET /api/export/{projectId}/coco` | 对齐 | 后端实际是 `GET /api/export/{project_id}/coco` |
| `exportMasks(projectId)` | `GET /api/export/{projectId}/masks` | 对齐 | 下载单标注 mask、语义融合 mask 和类别映射 ZIP |
## 后端 FastAPI 接口
以下列表来自当前运行的 OpenAPI
| 方法 | 路径 | 用途 |
|------|------|------|
| POST | `/api/auth/login` | 登录 |
| POST | `/api/projects` | 创建项目 |
| GET | `/api/projects` | 项目列表 |
| GET | `/api/projects/{project_id}` | 项目详情 |
| PATCH | `/api/projects/{project_id}` | 更新项目 |
| DELETE | `/api/projects/{project_id}` | 删除项目 |
| POST | `/api/projects/{project_id}/frames` | 添加帧记录 |
| GET | `/api/projects/{project_id}/frames` | 项目帧列表 |
| GET | `/api/projects/{project_id}/frames/{frame_id}` | 单帧详情 |
| POST | `/api/templates` | 创建模板 |
| GET | `/api/templates` | 模板列表 |
| GET | `/api/templates/{template_id}` | 模板详情 |
| PATCH | `/api/templates/{template_id}` | 更新模板 |
| DELETE | `/api/templates/{template_id}` | 删除模板 |
| POST | `/api/media/upload` | 上传视频/图片/DICOM 单文件 |
| POST | `/api/media/upload/dicom` | 批量上传 DICOM |
| POST | `/api/media/parse` | 创建 Celery 拆帧任务query 支持 `project_id``source_type``parse_fps``max_frames``target_width` |
| GET | `/api/tasks` | 查询后台任务列表 |
| GET | `/api/tasks/{task_id}` | 查询单个后台任务 |
| POST | `/api/tasks/{task_id}/cancel` | 取消后台任务 |
| POST | `/api/tasks/{task_id}/retry` | 重试失败或取消的后台任务 |
| POST | `/api/ai/predict` | 当前启用 SAM 2 点/框/interactive 推理 |
| POST | `/api/ai/propagate` | 当前启用 SAM 2 视频片段传播并保存标注 |
| GET | `/api/ai/models/status` | GPU 和 SAM 模型状态 |
| POST | `/api/ai/auto` | 自动分割 |
| POST | `/api/ai/annotate` | 保存 AI 标注 |
| POST | `/api/ai/import-gt-mask` | 导入 GT mask 并生成标注/seed point |
| GET | `/api/ai/annotations` | 查询项目标注,可选按帧过滤 |
| PATCH | `/api/ai/annotations/{annotation_id}` | 更新已保存标注 |
| DELETE | `/api/ai/annotations/{annotation_id}` | 删除已保存标注 |
| GET | `/api/dashboard/overview` | Dashboard 聚合快照 |
| GET | `/api/export/{project_id}/coco` | 导出 COCO JSON |
| GET | `/api/export/{project_id}/masks` | 导出 PNG mask ZIP |
| GET | `/health` | 健康检查 |
| WS | `/ws/progress` | WebSocket 进度通道,未出现在 OpenAPI paths 中 |
### WebSocket 进度通道
`/ws/progress` 用于 Dashboard 实时接收后台任务状态。前端连接成功后会定时发送 `ping` 作为心跳;后端收到任意文本心跳后返回:
```json
{
"type": "status",
"status": "connected",
"message": "Progress stream active",
"timestamp": "2026-05-01T00:00:00+00:00"
}
```
后台任务进度由 Celery worker 写入 Redis `seg:progress` 频道,再由 FastAPI 转发到当前活跃 WebSocket 连接。Dashboard 的“WebSocket 已连接/断开”状态来自浏览器 WebSocket 的 `onopen/onclose/onerror`,不再依赖是否刚好收到任务进度事件。
## 关键请求体
### 登录
```json
{
"username": "admin",
"password": "123456"
}
```
### 创建项目
```json
{
"name": "example.mp4",
"description": "导入说明",
"parse_fps": 30
}
```
### 创建标准帧序列拆帧任务
```text
POST /api/media/parse?project_id=1&parse_fps=15&max_frames=120&target_width=960
```
任务 `payload` 会记录本次拆帧参数;完成后的 `result.frame_sequence` 返回 `original_fps``parse_fps``frame_count``duration_ms``target_width`、帧宽高和 MinIO object prefix。每条 `FrameOut` 包含:
```json
{
"frame_index": 0,
"image_url": "http://...",
"width": 960,
"height": 540,
"timestamp_ms": 0,
"source_frame_number": 0
}
```
### 创建/更新模板
```json
{
"name": "腹腔镜胆囊切除术",
"color": "#06b6d4",
"z_index": 0,
"classes": [
{
"id": "cls-1",
"name": "胆囊",
"color": "#ffae00",
"zIndex": 280,
"category": "腹腔镜胆囊切除术"
}
],
"rules": []
}
```
### AI 推理请求体
前端 `predictMask()` 当前已适配后端 `PredictRequest`
```json
{
"image_id": 123,
"model": "sam2.1_hiera_tiny",
"prompt_type": "point",
"prompt_data": {
"points": [[0.5, 0.5]],
"labels": [1]
}
}
```
`prompt_type` 支持:
- `point`
- `box`
- `interactive`,用于 SAM 2 交互式细化,`prompt_data` 同时携带 `box`、累计 `points``labels`
- `semantic` 当前被禁用;由于产品不提供文本提示,前端不会显示语义文本入口,后端收到 semantic 会返回 400。
SAM 2 点提示和 auto fallback 当前只采用最高分候选 mask避免同一提示下多个备选 mask 被前端叠加显示。
工作区 SAM 2 请求包含反向点时,`CanvasArea` 会发送 `options.auto_filter_background=true``options.min_score=0.05`;如果负向点过滤后没有可用 polygon前端会移除当前旧候选 mask 并要求重新框选或添加正向点。
当前 registry 暴露 `sam2.1_hiera_tiny``sam2.1_hiera_small``sam2.1_hiera_base_plus``sam2.1_hiera_large`,并兼容 `sam2` 作为 tiny 别名;发送 `model=sam3` 会返回 400 Unsupported model。SAM 3 源码文件保留在仓库中,但没有接入当前运行时模型列表。
可选 `options` 字段:
- `crop_to_prompt`:对 point/box/interactive prompt 按锚点或框附近区域裁剪后推理,再把 polygon 回映射到原图坐标。
- `auto_filter_background`:过滤低分结果,并移除包含负向点的 polygon。
- `min_score`:配合 `auto_filter_background` 使用的最低置信度阈值。
后端响应:
```json
{
"polygons": [
[[0.25, 0.25], [0.75, 0.25], [0.75, 0.75], [0.25, 0.75]]
],
"scores": [0.5]
}
```
前端会把上面的 `polygons` 转成:
```json
{
"masks": [
{
"pathData": "M 160 90 L 480 90 L 480 270 L 160 270 Z",
"segmentation": [[160, 90, 480, 90, 480, 270, 160, 270]],
"bbox": [160, 90, 320, 180]
}
]
}
```
### 视频片段传播请求体
工作区“传播片段”调用:
```json
{
"project_id": 1,
"frame_id": 123,
"model": "sam2.1_hiera_tiny",
"direction": "forward",
"max_frames": 30,
"include_source": false,
"save_annotations": true,
"seed": {
"polygons": [[[0.1, 0.1], [0.3, 0.1], [0.3, 0.3]]],
"bbox": [0.1, 0.1, 0.2, 0.2],
"label": "胆囊",
"color": "#ff0000",
"class_metadata": {"id": "c1", "name": "胆囊", "color": "#ff0000", "zIndex": 20},
"template_id": 2
}
}
```
SAM 2.1 变体使用对应 video predictor 的 mask seed 传播;`model=sam2` 会兼容归一化为 tiny`model=sam3` 当前不支持。响应会返回已创建的 `annotations`,保存的 `mask_data.source``<model_id>_propagation`
## 已完成的接口对齐
- `updateProject()` 已从 `PUT` 改为 `PATCH`
- `exportCoco()` 已从 `/api/export/coco/{projectId}` 改为 `/api/export/{projectId}/coco`
- Canvas 已使用真实 `frame.id` 作为 `image_id`
- 点和框坐标已转成后端需要的归一化坐标。
- 后端 `polygons` 已在前端转成 Konva 可渲染的 path。
- `saveAnnotation()` 已接入 `POST /api/ai/annotate`
- `getProjectAnnotations()` 已接入 `GET /api/ai/annotations`
- `updateAnnotation()` 已接入 `PATCH /api/ai/annotations/{annotationId}`
- `deleteAnnotation()` 已接入 `DELETE /api/ai/annotations/{annotationId}`
- `importGtMask()` 已接入 `POST /api/ai/import-gt-mask`,导入后端生成的 polygon 标注、原始 `gt_label_value` 和 seed point。
- `exportMasks()` 已接入 `GET /api/export/{projectId}/masks`
- `parseMedia()` 已改为创建 Celery 后台任务,并返回 `ProcessingTask`
- `getTask()` 已接入 `GET /api/tasks/{taskId}`
- `cancelTask()` 已接入 `POST /api/tasks/{taskId}/cancel`
- `retryTask()` 已接入 `POST /api/tasks/{taskId}/retry`
- `getDashboardOverview()` 已从 `processing_tasks` 聚合解析队列。
- Dashboard 任务列表已展示 queued/running/success/failed/cancelled 任务,并可通过 `getTask()` 查看失败详情;`summary.parsing_task_count` 仍只统计 queued/running。
- 工作区导出按钮已调用 `exportCoco()` / `exportMasks()`,并会先保存未归档 mask。
- PNG mask ZIP 已包含每帧 `semantic_frame_*.png``semantic_classes.json`,重叠区域按 zIndex 裁决。
## 仍需处理的接口问题
- WebSocket 地址已从 `VITE_WS_PROGRESS_URL` 读取,未配置时从 `API_BASE_URL` 推导;部署时仍要确认浏览器能访问该地址。
- Celery worker 进度会写 PostgreSQL 任务表,同时发布到 Redis `seg:progress`FastAPI 订阅后广播到 `/ws/progress`
- 已保存标注目前支持分类级更新、polygon 顶点拖动、顶点删除、边中点插入、多 polygon 子区域选择编辑后的 PATCH 更新和整帧清空删除;复杂洞结构的专业编辑仍未实现。