# 当前设计冻结文档 冻结日期:2026-05-01 本文档描述当前代码结构、数据流、接口契约和测试边界。后续实现如果改变这些设计,应同步更新本文档和测试。 ## 总体架构 当前系统由三层组成: - React + TypeScript 前端 SPA。 - FastAPI 后端 API。 - PostgreSQL、MinIO、Redis、SAM 2 等外部基础设施。SAM 3 相关源码保留,但当前产品入口禁用。 开发时前端通过 `server.ts` 启动 Express + Vite middleware;后端通过 `backend/main.py` 启动 FastAPI。前端业务接口访问 FastAPI,`server.ts` 不再保留旧版 `/api/*` mock。 ## 前端模块 | 模块 | 文件 | 设计职责 | |------|------|----------| | 应用入口 | `src/App.tsx` | 根据登录状态和 `activeModule` 切换页面 | | 全局状态 | `src/store/useStore.ts` | Zustand store,保存项目、帧、模板、mask、当前选中 mask ids、工具状态和 mask 撤销/重做历史栈 | | API 封装 | `src/lib/api.ts` | Axios 客户端、字段映射、AI 响应转换 | | 配置 | `src/lib/config.ts` | 推导 API 和 WebSocket 地址 | | WebSocket | `src/lib/websocket.ts` | 进度流连接、订阅、连接状态通知、心跳和重连 | | 模型状态 | `src/components/ModelStatusBadge.tsx` | 展示 GPU 与当前 SAM 模型真实可用状态;工作区顶栏使用 compact 形态,只显示 GPU/CPU 状态,具体传播权重由旁边下拉负责 | | 登录页 | `src/components/Login.tsx` | 调用登录 API,写入 store | | Dashboard | `src/components/Dashboard.tsx` | 展示统计、任务控制、失败详情和 WebSocket 进度消息 | | 项目库 | `src/components/ProjectLibrary.tsx` | 项目列表、新建、删除、导入视频/DICOM、显式生成帧 | | 工作区 | `src/components/VideoWorkspace.tsx` | 加载帧和模板,组织工具栏、Canvas、本体面板、时间轴 | | Canvas | `src/components/CanvasArea.tsx` | 显示帧、缩放平移、点/框提示、渲染 mask | | 工具栏 | `src/components/ToolsPalette.tsx` | 切换工具、跳转 AI 页面、触发 mask 撤销/重做;紧凑垂直布局,高度不足时自身滚动;外层宽 56px,按钮列固定 48px,滚动条使用右侧外扩空间和低对比 `seg-scrollbar` | | 工作区顶栏 | `src/components/VideoWorkspace.tsx` | 保存/导出/传播/按起止帧批量清空遮罩/导入 GT、显式撤销/重做按钮和工作区快捷键 | | 时间轴 | `src/components/FrameTimeline.tsx` | 帧导航、播放进度、视频处理进度条、自动传播历史片段、自动传播范围选择、左右方向键切帧、播放和当前/总时长显示 | | 本体面板 | `src/components/OntologyInspector.tsx` | 模板选择、工作区 mask 透明度、分类树、后端自定义分类、mask 后端属性分析;内容过长时自身滚动,滚动条使用低对比 `seg-scrollbar` | | AI 页面 | `src/components/AISegmentation.tsx` | 独立 AI 推理视图,使用当前项目帧 | | 模板库 | `src/components/TemplateRegistry.tsx` | 模板 CRUD、分类编辑、导入、排序 | | 短提示浮层 | `src/components/TransientNotice.tsx` | 项目库和模板库的非阻塞成功/失败提示,自动消失 | ## 后端模块 | 模块 | 文件 | 设计职责 | |------|------|----------| | 应用入口 | `backend/main.py` | FastAPI app、CORS、路由注册、健康检查、WebSocket | | 配置 | `backend/config.py` | Pydantic settings | | 数据库 | `backend/database.py` | SQLAlchemy engine、session、Base | | 模型 | `backend/models.py` | Project、Frame、Template、Annotation、Mask、ProcessingTask | | Schema | `backend/schemas.py` | Pydantic 请求/响应模型 | | Auth | `backend/routers/auth.py` | 开发登录 | | Projects | `backend/routers/projects.py` | 项目与帧 CRUD | | Templates | `backend/routers/templates.py` | 模板 CRUD 和 mapping_rules 打包/解包 | | Media | `backend/routers/media.py` | 上传媒体和拆帧 | | AI | `backend/routers/ai.py` | 当前启用 SAM 2 推理、视频传播、模型状态和标注保存 | | 传播任务 | `backend/services/propagation_task_runner.py` | Celery 中执行自动传播 steps,写任务进度并保存传播标注 | | Export | `backend/routers/export.py` | COCO 和 PNG mask 导出 | | SAM 2 | `backend/services/sam2_engine.py` | SAM 2 懒加载、状态检测、点/框/自动推理和视频 mask 传播 | | SAM 3 | `backend/services/sam3_engine.py`, `backend/services/sam3_external_worker.py`, `backend/setup_sam3_env.sh` | 历史保留的 SAM 3 桥接源码和脚本;当前未接入 registry | | SAM Registry | `backend/services/sam_registry.py` | 当前暴露 SAM 2.1 四个变体、GPU 状态和推理分发 | ## 状态模型 前端 store 的核心对象: - `Project`:项目基本信息、状态、帧数、fps、媒体路径。 - `Frame`:帧 ID、项目 ID、索引、图片 URL、宽高、序列时间戳和原视频源帧号。 - `Template` / `TemplateClass`:模板和分类定义。 - `Mask`:前端渲染用 mask,包含 `pathData`、`segmentation`、`bbox`、`area`。 - `selectedMaskIds`:Canvas 当前选中的 mask id 列表,供右侧本体面板对已选区域直接换标签。 - `maskHistory` / `maskFuture`:mask 编辑历史栈,用于撤销和重做。 - `activeModule`:当前页面。 - `activeTool`:当前工具。 - `aiModel`:当前启用的 AI 模型,取值为 `sam2.1_hiera_tiny`、`sam2.1_hiera_small`、`sam2.1_hiera_base_plus` 或 `sam2.1_hiera_large`,默认 `sam2.1_hiera_tiny`。 ## 关键数据流 ### 登录 1. `Login` 收集用户名和密码。 2. `login()` 调用 `POST /api/auth/login`。 3. 成功后 store 写入 token,App 渲染主界面。 ### 项目导入与生成帧 1. `ProjectLibrary` 创建项目。 2. 导入视频时上传源视频到 `/api/media/upload` 并关联项目;该步骤不调用 `/api/media/parse`。 3. 用户在项目卡片点击“生成帧”,在弹窗中选择目标 FPS。 4. 前端调用 `/api/media/parse` 创建异步拆帧任务;可通过 `parse_fps`、`max_frames` 和 `target_width` 指定标准帧序列参数。 5. Celery worker 执行 FFmpeg/OpenCV/pydicom 拆帧,视频帧按 `frame_%06d.jpg` 从 `frame_000000.jpg` 连续命名,并按目标宽度缩放。 6. worker 写入 `frames.timestamp_ms` 和 `frames.source_frame_number`,并在任务 `result.frame_sequence` 中记录 FPS、帧数、时长、尺寸和对象存储前缀。 7. worker 持续更新 `processing_tasks`,并发布 Redis `seg:progress`。 8. 刷新项目列表;项目卡片右上角 FPS 徽标显示生成关键帧序列时选择的 `parse_fps`,原始视频 FPS 仅作为底部“原 xx fps”辅助信息显示。 9. 导入视频、生成帧、上传 DICOM 和失败反馈使用 `TransientNotice`,不再使用浏览器 `alert()` 阻塞操作;提示默认数秒后自动消失。 ### 任务控制 1. Dashboard 从 `GET /api/dashboard/overview` 读取 queued/running/success/failed/cancelled 任务;queued/running 代表当前进度,success/failed/cancelled 代表最近任务状态。 2. 用户取消任务时,前端调用 `POST /api/tasks/{task_id}/cancel`;后端写入 `cancelled`、设置 `finished_at`,并尝试 `celery_app.control.revoke(..., terminate=True)`。 3. worker 在下载、解析、上传、写帧等关键阶段刷新任务状态;如果发现 `cancelled`,停止后续写入并发布 cancelled 事件。 4. 用户重试任务时,前端调用 `POST /api/tasks/{task_id}/retry`;后端基于原任务 `payload` 创建新任务,记录 `retry_of` 并重新投递 Celery。 5. 用户打开详情时,前端调用 `GET /api/tasks/{task_id}`,弹窗展示 error、payload、result、Celery ID 和时间。 6. Dashboard 通过 `/ws/progress` 接收 Redis `seg:progress` 转发事件;前端 WebSocket 客户端在 `onopen/onclose/onerror` 主动更新连接状态,并定时发送 `ping` 心跳,服务端返回 `status` 确认连接仍活跃。 ### 工作区加载 1. `VideoWorkspace` 根据 `currentProject.id` 调用 `getProjectFrames()`。 2. 若无帧但项目有 `video_path`,显示“尚未生成帧”的状态提示,不自动触发 `parseMedia()`。 3. 帧数据映射为 store `Frame[]`,包含 `timestampMs` 和 `sourceFrameNumber`,供时间轴和后续视频传播使用。 4. 工作区调用 `GET /api/ai/annotations` 回显已保存标注时,会替换当前项目帧中的已保存 mask,但保留没有 `annotationId` 的未保存 draft mask;这保证 AI 页推送到工作区的候选 mask 不会被异步回显覆盖,并会在合并完成后恢复仍然存在的已选 mask id。 5. `VideoWorkspace` 加载项目帧时会优先按当前选中 mask 的 `frameId` 和当前打开帧 id 恢复 `currentFrameIndex`;只有没有可恢复帧时才回到第一帧,避免 AI 页在非第一帧推送回工作区时视角被重置。 6. `CanvasArea` 会把全局 `selectedMaskIds` 中仍存在于当前帧的 id 同步回本地选区,避免帧初始化时的临时清空覆盖 AI 页推送过来的选中态;如果切换到另一帧时原 id 不存在,但目标帧存在同一自动传播链的结果,前端会用 `source_annotation_id`、`source_mask_id` 和 `propagation_seed_key` 匹配对应传播 mask 并自动选中。 7. `CanvasArea` 根据容器和帧尺寸按 86% 适配比例计算初始 scale/position,使底图默认居中且尽量大,但保留画布边距;滚轮缩放和拖拽平移仍由用户后续控制。 8. `FrameTimeline` 顶部播放进度条显示当前播放位置;其下方视频处理进度条根据 `Mask.metadata.source` / `propagated_from_frame_id` 计算自动传播帧并显示蓝色区段,对人工绘制或 AI 智能分割等非传播 mask 帧显示红色竖线。普通状态下,视频处理进度条可点击跳转到对应帧,红色人工/AI 标注帧和蓝色自动传播帧标识本身也可点击跳转。处理条未处理背景使用中性灰,和红色/蓝色标记保持明显区分。`VideoWorkspace` 会记录当前会话最近 8 次成功处理过的自动传播范围,并通过 `propagationHistory` 传给 `FrameTimeline`;时间轴会把这些片段叠加为不同色系的横向渐变条,片段内按视频时间从深到浅,较早片段降低透明度。底部缩略图导航轴对非当前帧使用红色边框标识人工/AI 标注帧,使用蓝色边框标识自动传播/推理帧;如果同一帧同时存在人工/AI 标注和自动传播结果,红色人工/AI 标注边框优先保留,自动传播状态只作为蓝色内描边。当前帧使用青色外框高亮优先,若当前帧同时是人工/AI 标注帧,则以青色外框加红色内描边同时表达两个状态,外层当前帧框和内层人工/AI 框的顺序固定。工作区进入自动传播或清空片段遮罩范围选择模式时,播放进度条和视频处理进度条显示 amber 覆盖层,并可点击/拖拽设置处理起止帧。 9. 当前帧传入 `CanvasArea`。 10. 工作区顶栏短状态文本会在空闲状态下自动消失;保存、导出、导入 GT 和传播任务运行中仍保留进度状态,无帧项目提示也会保留。 11. 左侧工具栏和右侧本体/语义分类面板使用 `seg-scrollbar` 定制纵向滚动条;默认滚动条 thumb 低透明度融入深色背景,hover/focus 时增强为青色提示,避免系统默认滚动条在工具区中过于突兀。左侧工具栏额外保留右侧滚动条槽位,按钮列仍按原 48px 布局,避免滚动条和图标抢空间。 12. 右侧面板不再显示“本体论与属性分类管理树”固定说明栏,直接展示实际可操作内容。 13. 右侧“遮罩透明度”滑杆写入 Zustand `maskPreviewOpacity`,`CanvasArea` 用该值计算 mask group opacity;选中 mask 在基础透明度上加亮,方便保留选中反馈。 14. Canvas 点击 mask 后,全局 `selectedMaskIds` 会同步到 `OntologyInspector`;本体面板按选中 mask 的 `classId`、`className/label` 和颜色匹配模板分类,自动设置 active class,并把分类按钮滚动/聚焦到可见区域。 15. 工作区顶栏“清空片段遮罩”和“自动传播”共用时间轴范围选择交互;第一次点击“清空片段遮罩”会进入范围选择模式,按钮变为“确认清空”,用户可在播放进度条或视频处理进度条上点击/拖拽选择起止帧;确认执行时对范围内已保存 mask 调用 `DELETE /api/ai/annotations/{id}`,同时移除范围内本地 draft mask 和被清空的选区,范围外 mask 保持不变。 ### AI 点/框推理 1. 用户在 Canvas 选择正向点、反向点或框选。 2. `CanvasArea` 读取当前帧 ID 和宽高。 3. SAM 2.1 框选会创建一个候选 mask,并记录原始框;后续正向点/反向点会累计到同一候选上。 4. `predictMask()` 归一化坐标并携带当前 `model` 调用 `/api/ai/predict`;同时有框和点时发送 `interactive` prompt。 5. SAM 2.1 请求中只要存在反向点,`CanvasArea` 会额外发送 `options.auto_filter_background=true` 和 `options.min_score=0.05`,让后端移除低分结果和包含负向点的 polygon。 6. 后端加载帧图片并通过 SAM registry 分发到所选 SAM 2.1 变体;`model=sam2` 会兼容归一化为 tiny,`model=sam3` 会被拒绝。 7. 前端把 `polygons` 转为 mask;交互式细化会替换同一个候选 mask,而不是新增多个 mask。 8. 若带反向点的 SAM 2.1 细化返回空结果,前端会删除当前旧候选 mask 并提示反向点已排除该区域。 9. AI 页面只按本页最新生成的候选 id 渲染 mask,不把工作区已有 mask 带入 AI 画布;每次 `runInference()` 都先过滤掉旧 `aiMaskIds` 对应候选,再写入本次最高分候选。 10. AI 页面候选 mask 的 Path 点击事件会先判断当前工具;正向/反向选点工具下点击 mask 会继续追加提示点,其他工具下才选中 mask。 11. 工作区 SAM 提示点由 `CanvasArea` 本地 `points` 状态维护;点击已渲染提示点会先 `cancelBubble`,再删除对应点并按剩余提示重新调用 `runInference()`,避免同一次点击继续触发 Stage 加点或 Path 选择。 12. AI 页面边界框选由 `promptBox/boxStart/boxCurrent` 维护;拖拽时渲染蓝色虚线框,鼠标释放后固化 `promptBox` 并清空旧提示点,避免旧点误绑定到新框。 13. AI 页面执行分割时,如果只有 `promptBox` 则发送 `box` prompt;如果 `promptBox` 和 `points` 同时存在,`predictMask()` 会发送 interactive prompt。 14. AI 页面提示点由本地 `points` 状态维护;点击已渲染提示点会按 index 删除对应点,“删除最近锚点”会删除数组最后一个点,不改动候选 mask 列表。 15. AI 页面候选 mask 删除只接受当前 `aiMaskIds` 范围内的已选 id;“删除选中候选”和 Delete/Backspace 都复用该范围过滤,避免删除工作区已有 mask。 16. AI 页面参数开关文案只做展示增强:“局部专注模式(自动裁剪无锚区域)”仍控制 `cropMode/crop_to_prompt`,“严格除杂模式(自动清理干涉点)”仍控制 `autoDeleteBg/auto_filter_background/min_score`。 17. AI 页面“遮罩清晰度”滑杆只调节候选 mask 的 Konva preview opacity,不写入 `Mask.segmentation`、分类元数据或后端 payload。 18. AI 画布左上角根据正向点、反向点、边界框选和视口控制显示上下文提示,说明点击/拖拽、删除提示点和执行推理的操作方式。 19. AI 画布根据容器和当前帧尺寸按 86% 适配比例计算初始 scale/position,使底图默认居中且尽量大,但保留画布边距。 20. Canvas 按当前帧过滤并渲染 mask。 21. 新 mask 会带上当前选择的模板分类元数据,包括 `classId`、`className`、`classZIndex`、`metadata.source=ai_segmentation` 和保存状态 `draft`。 20. 用户点击“结构化归档保存”后,前端将像素 `segmentation` 转成 normalized `mask_data.polygons`;未保存 mask 调用 `POST /api/ai/annotate`,dirty mask 调用 `PATCH /api/ai/annotations/{annotation_id}`;保存成功后本次提交的 draft mask id 会从本地保留列表中排除,并由后端 saved annotation 回显替换。 21. 工作区加载项目帧后通过 `GET /api/ai/annotations` 取回已保存标注并转成前端 mask。 22. 工作区“清空遮罩”删除当前帧已保存标注,并清除当前帧本地 mask。 ### 视频片段传播 1. 用户在工作区打开一帧作为参考帧;该帧全部 mask 都会作为传播 seed,不再提供传播对象下拉。 2. 用户可以直接修改传播起始帧/结束帧数字框,并可通过工作区顶栏“传播权重”下拉独立选择本次传播使用的 SAM 2.1 tiny/small/base+/large 权重;该入口不提供 SAM2/SAM3 家族切换,默认跟随全局 AI 权重,用户手动选择后不再被 AI 页权重切换覆盖。 3. `VideoWorkspace` 以当前参考帧为 seed,将起止帧拆成 `backward` 和/或 `forward` 两段;只包含当前帧时不传播。 4. `VideoWorkspace` 在提交传播前会先调用现有归档保存链路保存当前项目中的 draft/dirty mask,并重新读取 store 中的回显结果;参考帧 seed 因此优先携带稳定的后端 `source_annotation_id`,避免用前端临时 mask id 生成传播结果后,二次传播无法找到旧结果。 5. `VideoWorkspace` 用 `buildAnnotationPayload()` 把每个 seed mask 转成 normalized polygon、bbox、label、color、class 元数据、`geometry_smoothing`、`source_mask_id` 和可用时的 `source_annotation_id`;如果 seed mask 是未编辑的自动传播结果,会沿用其原始 `source_annotation_id/source_mask_id/propagation_seed_signature`,让后端把它识别为原传播链的同一个 seed;如果该传播结果被编辑并保存,更新 payload 只保留 lineage,不保留旧签名,使后端按“已修改”路径清理旧结果并重传。 6. 前端把传播权重 id、每个 seed、每个方向组装成 `steps`,一次调用 `POST /api/ai/propagate/task`,`include_source=false`、`save_annotations=true`;接口先规范化/校验 `model` 字段中的权重 id,再创建 `processing_tasks.task_type=propagate_masks` 并投递 Celery,避免长 HTTP 请求阻塞前端等待。 7. `VideoWorkspace` 记录返回的 `task_id`,轮询 `GET /api/tasks/{task_id}` 显示任务 message、步骤进度、已处理帧次和已保存区域数;任务运行期间提供取消传播按钮,调用通用 `POST /api/tasks/{task_id}/cancel`。 8. Celery worker 逐 step 顺序执行传播,避免多个视频 tracker 并发抢占 GPU;每个 step 开始/完成都会写入 `processing_tasks.progress/result/message` 并发布 Redis `seg:progress`,Dashboard 可同步显示。每个 step 开始前,worker 会在本次目标帧段内用 seed 来源 id、传播方向和包含 `geometry_smoothing` 的 seed 签名查找旧传播标注:同权重、签名相同且目标帧都已有结果时跳过该 seed;签名不同、目标帧只部分覆盖、本次使用了其他 SAM 2.1 权重或平滑参数变化则先删除本次目标帧段内对应方向的旧自动传播标注,再执行新的 video predictor 传播。对旧版本只记录前端临时 `source_mask_id` 的传播标注,worker 会按 label/color/class 做兼容匹配,确保可被后续稳定 `source_annotation_id` 的传播替换;对中间帧人工新增的替代 seed,若缺少旧 source id,worker 仍会用语义信息识别候选旧传播结果,并在写入目标帧新 polygon 前用目标帧 bbox 重叠做二次确认和清理。写入前这层清理不限制旧结果方向,确保 backward 传播可覆盖早先 forward 传播留下的同物体旧 mask。 9. 后端按项目帧序列截取片段,下载对应帧到临时目录,并写成 `000000.jpg` 这类纯数字文件名;这是 `SAM2VideoPredictor` 对视频帧排序的要求,和项目库中持久化的 `frame_%06d.jpg` 对象名无关。 10. `model` 为任一 SAM 2.1 权重变体时,`sam2_engine` 使用对应 checkpoint/config 加载 `SAM2VideoPredictor.add_new_mask()` 注入 seed mask,再用 `propagate_in_video()` 传播;`model=sam2` 会在入队时规范化为 tiny,任务 payload/result 会保留规范化后的权重 id;单个 SAM2 video predictor 调用内部暂不提供逐帧流式进度。 11. `model=sam3` 当前不支持;SAM 3 video tracker 代码保留但没有接入产品路径。 12. 后端把传播返回的 normalized polygon 保存为后续帧 `Annotation`,跳过源帧;如果 seed 带 `geometry_smoothing`,保存前会用同一 Chaikin 平滑参数处理 forward/backward 两个方向的结果。`mask_data.source` 记录权重传播来源,同时写入 `propagation_seed_key`、`propagation_seed_signature`、`propagation_direction`、`source_annotation_id`、`source_mask_id` 和 `geometry_smoothing` 供后续幂等传播判断。 13. 前端轮询到已创建区域后刷新 `GET /api/ai/annotations` 并回显新标注;任务结束后如果后端返回 0 个新区域,工作区会明确提示没有生成新的 mask,若是未改变 seed 被跳过则提示未改变 mask 已跳过。处理过帧次大于 0 的成功任务会追加一条本地传播历史片段,用于视频处理进度条显示最近传播范围;`annotationToMask()` 会保留传播来源 metadata,供时间轴视频处理进度条显示蓝色传播区段。 ### 手工绘制与历史栈 1. 用户在 `ToolsPalette` 选择多边形、矩形、圆、点或线工具。 2. `CanvasArea` 将交互坐标转换成像素 polygon。 3. 多边形工具逐次记录节点,三点后点击首节点或按 Enter 时生成闭合 polygon。 4. Canvas 左上角根据当前工具和操作阶段显示上下文短提示;多边形提示会随已放置点数切换,明确 Enter 完成、Esc 取消和点击首节点闭合。提示会在工具或操作状态变化时出现,并在数秒后自动隐藏,避免长期遮挡底图。 5. mask path 只在 `move`、`edit_polygon`、`area_merge` 和 `area_remove` 工具下拦截点击;绘制和 AI prompt 工具点击已有 mask 时继续冒泡给 Stage。 6. 新 mask 写入 `pathData`、像素 `segmentation`、`bbox`、`area` 和当前模板分类元数据。 7. `addMask()`、`setMasks()`、`updateMask()`、`clearMasks()` 会维护 `maskHistory/maskFuture`。 8. 工具栏按钮、工作区顶栏按钮和 AI 页按钮调用 `undoMasks()` / `redoMasks()`;工作区由 `VideoWorkspace` 统一处理 `Ctrl/Cmd+Z`、`Ctrl/Cmd+Shift+Z` 和 `Ctrl/Cmd+Y`,并在输入框、下拉框和可编辑文本聚焦时跳过快捷键,避免影响帧范围输入。 ### Polygon 逐点编辑 1. 用户选择“调整多边形”或“拖拽/选择”后点击 Canvas 上的 mask path,`CanvasArea` 记录 `selectedMaskId` 并显示该 mask 第一条 polygon 的顶点控制点和边中点插入手柄。 2. 顶点 `mousedown/dragstart` 会立即设置当前顶点选择;拖动过程中通过 `dragMove` 实时重算 `pathData`、像素 `segmentation`、`bbox`、`area`,不需要先单击顶点再拖动。 3. Stage 的 `onDragEnd` 只处理 Stage 自身拖拽;polygon 顶点、GT seed point 等子节点拖拽结束事件会被忽略,避免子节点坐标误写入 Canvas `position` 导致视口跳动。 4. 点击边中点手柄会在该边中点插入新顶点;在“调整多边形”工具下双击 polygon path 会在最接近的线段上按双击位置插入新顶点。 5. 如果 mask 已有 `annotationId`,编辑会把 `saveStatus` 标成 `dirty` 且 `saved=false`。 6. 归档保存时复用现有 `PATCH /api/ai/annotations/{annotation_id}` 链路,把更新后的 normalized polygon 写回后端。 7. 选中顶点后 Delete/Backspace 可删除顶点;前端保持 polygon 至少三点。 8. 未选中具体顶点但选中了 mask 时,Delete/Backspace 从前端 store 删除该 mask;如果包含 `annotationId`,通过工作区回调调用后端删除接口。 ### 区域合并与去除 1. 用户选择 `area_merge` 或 `area_remove` 后,点击多个当前帧 mask 组成选择集。 2. 合并/去除模式隐藏 polygon 顶点和边中点编辑手柄,并在右下角显示已选数量;少于两个 mask 时操作按钮禁用。 3. Canvas 左上角提示布尔选择顺序:第一个选中的是主区域,后续区域参与合并或扣除。 4. 布尔选择态按选择顺序区分角色:第一个选中的主区域使用黄色实线轮廓,后续参与合并/扣除的区域使用红色虚线轮廓;所有已选区域填充透明度保持一致,避免被误解为阴影模式异常。 5. `CanvasArea` 把 `Mask.segmentation` 转为 `polygon-clipping` 的 MultiPolygon。 6. `area_merge` 使用 union,更新第一个选中的主 mask,并从前端 store 移除后续被合并 mask;如果被移除 mask 已保存,会调用工作区传入的删除回调删除后端标注。 7. `area_remove` 使用 difference,从第一个选中的主 mask 中扣除后续选中 mask,扣除对象本身保留;如果 difference 产生内洞,`segmentation` 保留外圈和 hole ring,渲染时使用 even-odd fill。 8. 结果会重算 `pathData`、`segmentation`、`bbox`、`area`,已保存主 mask 会进入 dirty 状态并复用归档 PATCH 链路;带洞结果的面积按外圈减内洞计算。 ### GT Mask 导入 1. 工作区“导入 GT Mask”选择图片文件。 2. 前端 `importGtMask()` 以 multipart form-data 调用 `POST /api/ai/import-gt-mask`,携带 `project_id` 和 `frame_id`。 3. 后端验证项目、帧、模板后使用 OpenCV 读取灰度 mask。 4. 后端按非零像素值拆分多类别标签。 5. 后端对每个类别的前景做 contour 提取,每个连通域保存为一个 `Annotation`。 6. `points` 字段保存距离变换中心 seed point,`mask_data.polygons` 保存 normalized polygon,`mask_data.gt_label_value` 保存原始像素类别值。 7. 前端重新读取项目标注并回显。 8. `annotationToMask()` 会把 normalized seed point 转成像素坐标,Canvas 以可拖拽点显示;拖动后 `buildAnnotationPayload()` 会把点再归一化写回后端。 ### 模板管理 1. `TemplateRegistry` 从后端读取模板。 2. 编辑态在组件本地维护分类列表。 3. 保存时调用 `createTemplate()` 或 `updateTemplate()`。 4. 后端把 `classes`、`rules` 打包进 `mapping_rules`。 5. 返回时再解包给前端。 6. `CanvasArea` 把当前选中的 mask id 同步到全局 `selectedMaskIds`;切换工具、切换帧或卸载 Canvas 时会清空选择。 7. `AISegmentation` 生成 mask 后会写入全局 `masks` 并把生成的 mask id 写入 `selectedMaskIds`;点击 AI 页预览 mask 也会更新 `selectedMaskIds`。 8. AI 页“推送至工作区编辑”会切换到工作区并把 `activeTool` 设为 `edit_polygon`;`CanvasArea` 初始读取全局 `selectedMaskIds`,让 AI 页选中的 mask 在工作区继续保持选中。 9. 工作区帧/标注异步加载完成后,`hydrateSavedAnnotations()` 会合并本地未保存 draft mask 和后端已保存 mask,不会用后端回显结果直接覆盖整个 `masks` store。 10. `OntologyInspector` 可以选择具体分类;选择结果进入全局 store,供 `CanvasArea` 和 `AISegmentation` 新建/更新 mask 时使用。 11. 如果 `selectedMaskIds` 中存在当前 store 的 mask,点击分类时会立即更新这些 mask 的 `templateId`、`classId`、`className`、`classZIndex`、`label` 和 `color`。 12. 同一次点击会把这些已选 mask 移动到前端 `masks` 数组末尾;`CanvasArea` 按数组顺序渲染,后渲染的 Path 显示在最上层,方便用户继续编辑刚换标签的区域。该显示置顶不改变模板 `zIndex` 或后端导出语义覆盖规则。 13. 已保存 mask 被重新分类后进入 `dirty` 且 `saved=false`,继续复用工作区归档保存的 PATCH 链路。 14. 模板保存、删除和 JSON 导入失败使用 `TransientNotice` 非阻塞提示,默认数秒后自动消失。 ### 导出 1. 后端根据项目、帧、标注和模板生成 COCO JSON。 2. PNG mask 导出会把 normalized polygon 渲染为单标注二值 mask。 3. PNG mask 导出还会按 `mask_data.class.zIndex` 或模板 `z_index` 从低到高覆盖,生成每帧语义融合 mask。 4. ZIP 内写入 `semantic_classes.json`,记录语义值到类别、颜色和 zIndex 的映射。 5. 前端“导出 JSON 标注集”和“导出 PNG Mask ZIP”按钮都会在导出前保存待归档标注,然后下载对应文件。 ## 接口契约 接口详情见 `doc/04-api-contracts.md`。测试中重点固定以下契约: - `updateProject()` 使用 `PATCH /api/projects/{id}`。 - `exportCoco()` 使用 `GET /api/export/{projectId}/coco`。 - `exportMasks()` 使用 `GET /api/export/{projectId}/masks`。 - `cancelTask()` 使用 `POST /api/tasks/{taskId}/cancel`。 - `retryTask()` 使用 `POST /api/tasks/{taskId}/retry`。 - `predictMask()` 使用 `POST /api/ai/predict`,请求体为 `image_id`、`prompt_type`、`prompt_data`、`model`。 - `propagateMasks()` 使用 `POST /api/ai/propagate`,请求体为 `project_id`、`frame_id`、`model`、`seed`、`direction`、`max_frames`,作为单 seed 同步兼容接口保留。 - `queuePropagationTask()` 使用 `POST /api/ai/propagate/task`,请求体为 `project_id`、`frame_id`、`model`、`steps`、`include_source`、`save_annotations`,返回 `ProcessingTask`。 - `saveAnnotation()` 使用 `POST /api/ai/annotate`。 - `importGtMask()` 使用 `POST /api/ai/import-gt-mask` multipart form-data。 - `getProjectAnnotations()` 使用 `GET /api/ai/annotations`。 - `updateAnnotation()` 使用 `PATCH /api/ai/annotations/{annotationId}`。 - `deleteAnnotation()` 使用 `DELETE /api/ai/annotations/{annotationId}`。 - `parseMedia()` 使用 `POST /api/media/parse?project_id=...`,可选 `parse_fps`、`max_frames`、`target_width`,用于生成标准帧序列。 - `getProjectFrames()` 返回帧图像 URL、宽高、`timestamp_ms` 和 `source_frame_number`。 - 后端 `/api/ai/predict` 当前支持 SAM 2.1 的 point、box、interactive;`semantic` 文本提示禁用并返回 400。 - SAM 2.1 是点/框交互式分割模型,不做文本语义分割;AI 页面已经移除纯文本输入。 - SAM 2.1 点提示和 auto fallback 只返回一个最高分候选,避免同一提示产生多个重叠候选 mask。 - SAM 3 前端入口、后端 registry 入口和状态展示均已禁用;`model=sam3` 会返回不支持。 - 后端 `/api/ai/predict` 支持可选 `options`:`crop_to_prompt` 会对 point/box/interactive prompt 做局部裁剪推理并回映射 polygon,`auto_filter_background` 会按 `min_score` 和负向点过滤结果。 - 后端 `/api/ai/propagate/task` 当前支持所选 SAM 2.1 mask seed 视频传播后台任务;同步 `/api/ai/propagate` 仍保留为单 seed 兼容接口。 - 后端 `/api/ai/models/status` 返回 GPU 和四个 SAM 2.1 变体的真实运行状态。 - point prompt 支持旧数组形式和 `{ points, labels }` 对象形式。 ## 外部依赖边界 测试不直接依赖以下真实服务: - PostgreSQL:后端测试使用内存 SQLite。 - MinIO:上传、下载、预签名 URL 使用 monkeypatch。 - Redis:单测使用 monkeypatch 验证进度事件发布,不依赖真实 Redis 服务。 - SAM:AI 推理测试使用 fake registry。 - 浏览器 Canvas/Konva 图片加载:前端测试 mock `react-konva` 和 `use-image`。 ## 已知占位设计 以下能力属于当前冻结版本的占位或半可用功能: - Dashboard 初始快照来自 `GET /api/dashboard/overview`;任务进度区由 `processing_tasks` queued/running/success/failed/cancelled 任务生成,处理中统计只计算 queued/running。 - 已保存标注支持通过“应用分类”、polygon 顶点拖动/删除、边中点插入、多 polygon 子区域编辑和区域合并/去除进入 dirty 状态并归档更新;选中整块 mask 可用 Delete/Backspace 删除并同步后端;复杂洞结构编辑尚未实现。 - SAM 3 文本语义分割已从当前产品路径中禁用;相关源码保留,恢复时需要重新接入前端入口、registry、状态接口和测试。 - 自定义分类通过 `PATCH /api/templates/{id}` 写入当前激活模板的 `mapping_rules.classes`。 - 选中 mask 后,本体面板的“特定目标实例属性追踪”标题值来自当前 mask 的 `className/label`,不使用全局 active class;面板调用 `POST /api/ai/analyze-mask` 显示拓扑锚点数量等属性,“重新提取拓扑锚点”会带 `extract_skeleton=true` 重新请求后端分析;“边缘平滑强度/应用边缘平滑”调用 `POST /api/ai/smooth-mask`,由后端按 Chaikin smoothing 返回新 polygon 并把 `geometry_smoothing` 写回 mask metadata。前端不再展示“后端模型置信度”。 - GT mask 导入已完成多类别像素值拆分、contour、distance transform seed point 和前端 seed point 拖拽编辑;骨架提取、HDBSCAN 聚类和模板自动映射尚未实现。