# 当前需求冻结文档

冻结日期：2026-05-01

本文档描述当前仓库已经实现或明确保留为占位的需求。测试用例以本文档为准，不把早期设想或 Word 文档中的远期能力当作当前版本必须实现的功能。

## R1 登录与会话

- 系统提供登录页。
- 默认开发管理员为启动时种子化的 `admin / 123456`，密码以哈希形式存入 `users` 表。
- 登录成功后前端保存签名 JWT，并进入主应用。
- 页面刷新后前端会用已有 token 调用 `/api/auth/me` 恢复当前用户。
- 登录失败时显示错误信息。
- 业务接口必须校验 Bearer token；缺失或无效 token 返回 401。
- 项目、帧、标注、任务、Dashboard 和导出必须按当前用户的项目隔离；用户不能读取、修改或删除其他用户项目资源。
- 角色包括 `admin`、`annotator` 和 `viewer`；`admin/annotator` 可写入业务数据和触发 AI/传播，`viewer` 只能访问读接口，用户管理后台仅 `admin` 可用。
- 管理员侧栏显示“用户管理”入口；管理员可以新增用户、修改角色、停用/启用、修改密码、删除无项目用户。
- 系统记录登录成功/失败和用户管理操作到 `audit_logs`，管理员后台可查看最近审计日志。
- 管理员后台提供“恢复演示出厂设置”危险操作；前端必须二次确认，后端也必须校验 `confirmation=RESET_DEMO_FACTORY`，执行后只保留默认 admin 账号、系统模板、演示视频项目和一个已按文件名自然顺序生成帧的演示 DICOM 项目，清空其它用户、项目、帧、标注、任务、用户模板和旧审计记录，并写入本次重置审计。
- 系统默认模板至少包含“腹腔镜胆囊切除术”和“头颈部CT分割”；头颈部 CT 默认分类名必须使用纯中文，不带括号英文翻译；恢复演示出厂设置不得删除系统默认模板，并必须重建缺失的默认模板、覆盖恢复被修改或删减的默认语义分类树。

## R2 项目管理

- 前端展示项目库，并从 `GET /api/projects` 获取项目列表。
- 用户可以新建项目，前端调用 `POST /api/projects`；后端把项目归属到当前登录用户。
- 用户可以选择项目，进入工作区。
- 用户可以导入视频文件，前端创建项目、上传文件并刷新项目列表；导入视频不自动拆帧。
- 用户可以对已导入且尚未生成帧的视频项目点击“生成帧”，在弹窗中选择目标 FPS 后创建拆帧任务；项目名称编辑状态下不能显示/触发生成帧入口，DICOM 项目不能显示生成帧入口。
- 用户可以导入 DICOM 序列，前端上传 DICOM、触发拆帧、刷新项目列表。
- 用户可以在项目库项目卡片上修改项目名称，名称不能为空。
- 用户可以在项目卡片删除按钮旁复制项目；复制时可选择“新项目重置”或“全内容复制”。新项目重置必须复制项目媒体字段和已生成帧序列，但不复制标注或 mask 元数据；全内容复制必须额外复制标注和关联 mask 元数据，并将复制标注重新指向新项目中的对应帧。任务运行历史不复制。
- 用户可以在项目卡片上删除项目；前端调用 `DELETE /api/projects/{id}`，删除成功后从项目库移除，若删除当前项目则清空工作区当前项目、帧、mask 和选区。
- 后端支持项目创建、列表、详情、局部更新、复制和删除。
- 后端删除项目时通过 ORM 级联删除项目帧、标注、导出 mask 元数据和后台任务记录。
- 后端支持项目帧创建、列表和单帧查询。

## R3 媒体上传与拆帧

- 后端允许上传视频、图片、DICOM 文件，其他扩展名返回 400。
- 未提供项目 ID 上传时，后端自动创建项目。
- 提供项目 ID 上传时，后端把上传对象关联到该项目。
- 项目库导入视频和导入 DICOM 序列时，前端必须显示导入进度条；浏览器提供上传总字节数时显示百分比和已上传/总字节数，未提供总字节数时显示已上传字节的非确定进度。DICOM 导入还必须显示本次有效 `.dcm` 文件数量，并在上传完成后持续显示解析任务进度，直到成功、失败或取消。
- 拆帧接口根据项目 `source_type` 处理视频或 DICOM。
- 拆帧接口支持 `parse_fps`、`max_frames` 和 `target_width` 参数，用于生成可被 SAM 2 视频处理复用的标准帧序列。
- DICOM 批量导入和解析必须按文件名自然顺序处理 `.dcm` 文件，避免数字文件名被字符串排序打乱。
- 视频/DICOM 解析后都使用连续 `frame_%06d.jpg` 命名，默认从 `frame_000000.jpg` 开始；视频帧按 `target_width` 缩放。
- 拆帧完成后写入 `frames` 记录，并把项目状态设为 `ready`。
- 每条帧记录包含 `frame_index`、`image_url`、`width`、`height`、`timestamp_ms` 和 `source_frame_number`。
- 任务完成结果包含 `frame_sequence` 元数据：`original_fps`、`parse_fps`、`frame_count`、`duration_ms`、`target_width`、帧宽高和对象存储前缀。
- 拆帧接口会创建 `processing_tasks` 记录并投递 Celery worker。
- 前端可通过 `GET /api/tasks/{task_id}` 查询任务状态。
- 后端支持 `POST /api/tasks/{task_id}/cancel` 取消 queued/running 任务，写入 `cancelled` 状态并尝试 revoke Celery。
- 后端支持 `POST /api/tasks/{task_id}/retry` 对 failed/cancelled 任务创建新的 queued 任务。
- worker 会在关键阶段检查任务是否已取消，取消后停止继续写帧。

## R4 工作区与帧浏览

- 工作区根据当前项目加载帧列表。
- 若项目有媒体但无帧，工作区只提示需要先在项目库生成帧，不再自动触发拆帧。
- Canvas 显示当前帧图片。
- Canvas 支持滚轮缩放、移动工具拖拽、鼠标坐标显示。
- Canvas 未选中特定 mask 时，mask 显示顺序必须遵循右侧“语义分类树”拖拽得到的内部覆盖优先级：低优先级先渲染，高优先级后渲染并显示在上层；选中 mask 后可以为了编辑交互临时置顶。
- 时间轴支持缩略图点击切帧、range 拖动切帧、视频处理进度条点击切帧、人工/AI 标注帧和自动传播帧标识点击切帧、键盘左右方向键切帧、播放/暂停顺序推进帧。
- 顶栏旧“清空片段遮罩”入口已移除；当前清空/DEL 只在目标 mask 存在传播链结果时进入范围选择。用户选择按帧范围清空后，必须复用时间轴范围选择并最终确认；范围内只清空同一传播链自动传播结果，不能清空无关人工绘制或独立 AI 智能分割 mask。按范围清空或清空所有传播帧时，如果目标帧范围内包含人工绘制或独立 AI 智能分割 mask，必须二次询问是否删除人工/AI 标注帧；用户选择是时删除这些人工/AI 标注帧中的全部 mask，用户选择否时这些帧整帧保留，只清空其它自动传播帧。用户取消确认时不能删除本地 mask、后端标注或传播历史条。
- 用户在某帧选中 mask 后，如果切换到同一自动传播结果覆盖的其他帧，工作区应自动识别并选中目标帧中对应的传播 mask；匹配依据为传播结果回显到 mask metadata 的 seed 来源和传播链字段，而不是仅凭标签或颜色。
- 播放帧率使用项目 `parse_fps` 或 `original_fps`，限制在 1 到 30 FPS。
- 时间轴显示当前帧时间和总时长，时间基准使用项目 `parse_fps` 或 `original_fps`，格式为 `mm:ss.cc`。
- 时间轴顶部播放进度条只表达当前播放位置；其下方的视频处理进度条表达处理状态：当前帧位置用白色竖线贯穿播放进度条和视频处理进度条；人工绘制或 AI 智能分割生成的帧显示红色竖线，自动传播生成的帧显示蓝色区段，最近自动传播处理过的片段叠加同一蓝色系纯色条，按距最新传播的时间顺序逐次变暗，且第 5 次及更早统一为阈值旧记录色，帮助识别第一次、第二次、第 N 次传播；清空片段遮罩后，与清空范围重叠的最近传播历史条必须同步移除或裁剪，不应继续显示已经清空的传播范围；未处理背景使用中性灰以和标记保持明显区分。进入自动传播或清空遮罩范围选择时，起始帧和结束帧必须额外显示两条贯穿两条进度条的高对比边界线，颜色避开青色播放进度、红色标注、蓝色传播、amber 选区和深色背景。底部帧可视化栏中，人工/AI 标注帧缩略图边框为红色，自动传播/推理帧缩略图边框为蓝色，当前帧仍用青色外框高亮优先；如果同一帧既有人工/AI 标注又有自动传播结果，红色人工/AI 标注框优先保留，自动传播状态只作为蓝色内描边或次级提示；如果当前帧同时是人工/AI 标注帧，则显示青色外框加红色内描边，外层选中框和内层标注框顺序不能交换。
- 自动传播提交前支持独立选择传播权重，范围限定为 SAM 2.1 tiny/small/base+/large 四个权重变体；该选择只影响传播任务，不提供 SAM2/SAM3 家族切换，也不改变 AI 智能分割页的单帧推理权重。

## R5 工具栏

- 工具栏可以切换当前 active tool。
- 工作区左侧工具栏不展示正向点、反向点、框选工具；这些入口只属于 AI 智能分割页。
- 侧栏“AI智能分割”和工作区工具栏 AI 跳转入口必须使用带明确 AI 语义的图标，而不是普通魔法棒等泛化工具图标。
- 工作区 AI 智能分割入口切换到 AI 页面。
- 多边形、矩形、圆、画笔、橡皮擦工具会在 Canvas 上生成或编辑可保存的 polygon mask；左侧工具栏不再提供创建点和创建线段入口。
- 多边形通过点击取点并按 Enter 完成，也支持三点后点击首节点闭合；矩形、圆通过拖拽生成；画笔和橡皮擦支持调整大小。
- 画笔工具只在语义分类树有选中类别时可用，按住拖动时以圆形笔触采样，鼠标松开后一次性 union 成连续区域；如果笔触与当前选中 mask 连通，默认合并到该 mask，否则生成新的当前类别 mask。
- 橡皮擦工具只在当前帧已选中 mask 时可用，按住拖动时以圆形笔触采样，鼠标松开后从选中 mask 中 difference 扣除；扣空时删除该 mask，已保存 mask 仍需同步后端删除。
- 创建多边形、创建矩形、区域合并/去除、调整多边形等 Canvas 左上角上下文提示只作为短提示，切换工具或操作状态变化时显示，数秒后自动隐藏，避免长期遮挡待编辑图像；再次切换工具或操作状态变化会重新显示。
- 绘制工具点击已有 mask 时应继续执行当前绘制动作，不应被 mask 选择逻辑吞掉。
- 所有 polygon mask 都不显示黄色 seed point，也不提供 seed point 拖动；普通手工/AI/GT mask 在画布上应保持一致的区域渲染、选择、顶点编辑、拓扑统计、边缘平滑和保存体验。
- 工具栏提供“调整多边形”工具，用户可以点击 mask 进入 polygon 顶点编辑态；按住顶点即可直接拖动并实时更新 mask 几何，不需要先单击选中顶点，已保存 mask 会标记为 dirty；顶点拖拽不能冒泡成画布拖拽，编辑结束后 Canvas 当前缩放和平移视口必须保持不变。
- 工具栏按浅灰分隔线分组：拖拽/选择到创建圆为基础绘制组，画笔/橡皮擦为局部笔触组，区域合并/重叠区域去除/DEL/清空遮罩为布尔与删除组，导入 GT Mask/AI 智能分割为外部动作组；`data-testid="tool-group-separator"` 位于清空遮罩下方并分隔外部动作组。工作区在“清空遮罩”上方提供 `DEL` 按钮，语义等同键盘 Delete/Backspace；“导入 GT Mask”入口使用区别于普通编辑工具的紫色底色，不切换 activeTool。
- 顶点编辑态显示边中点插入手柄；点击边中点会在该边中间新增顶点。
- “调整多边形”工具下双击 polygon 边界时，会在最接近的线段上按双击位置新增顶点。
- 顶点编辑态下选中顶点后可用 Delete/Backspace 删除顶点，但不会让 polygon 少于三点。
- 中空 mask 必须保留外圈与内洞 ring 分组；进入“调整多边形”后，外圈和内洞都应显示可拖动顶点与边中点插入手柄，内洞顶点拖动、插点和保存后的回显都不能把 mask 变成实心。
- 多 polygon 或分离区域组成的同一个 mask 进入“调整多边形”后，所有分离 polygon 都应显示可拖动顶点与边中点插入手柄，不能只显示第一个主区域。
- 选中整个 mask 且未选中具体顶点时，Delete/Backspace 删除该 mask；左侧 `DEL` 按钮复用同一删除链路。删除已保存 mask 前，前端必须用当前后端标注列表预检 `annotationId`，只对仍存在的 id 发送 `DELETE /api/ai/annotations/{id}`，避免本地陈旧 id 导致浏览器控制台出现 404 红字；如果删除对象属于自动传播链或是传播 seed，应同步删除同一传播链上的自动传播 mask，但不能删除其他帧独立 AI 推理或人工标注 mask。
- 撤销、重做绑定全局 `maskHistory/maskFuture`，工作区支持顶栏按钮和全局快捷键 `Ctrl/Cmd+Z`、`Ctrl/Cmd+Shift+Z`、`Ctrl/Cmd+Y`；快捷键监听应在 capture 阶段处理，并在 `event.key` 不可靠时兼容 `event.code=KeyZ/KeyY`，但输入框、文本域、下拉框和可编辑文本聚焦时不能拦截；AI 页支持自己的按钮；左侧工具栏不重复放置撤销/重做入口。
- 区域合并工具支持多选当前帧 mask，并使用 polygon union 生成合并后的主 mask；若主区域和参与区域存在同一传播链上的对应 mask，合并前必须弹出范围选择，让用户选择只处理当前帧、处理所有传播帧或按帧范围选择；按帧范围选择进入和自动传播/清空一致的时间轴范围选择，点击确认后再弹出最终确认。选择所有传播帧或范围帧时，同一次合并必须同步应用到对应传播帧中的主区域和参与区域，只删除每个已同步帧里的参与合并 mask，不能把未参与本次同步或范围外的同链对象整链误删。
- 区域去除工具支持多选当前帧 mask，并从第一个选中的主 mask 中扣除后续选中 mask；若主区域和参与区域存在同一传播链上的对应 mask，去除前必须弹出范围选择，让用户选择只处理当前帧、处理所有传播帧或按帧范围选择；按帧范围选择进入和自动传播/清空一致的时间轴范围选择，点击确认后再弹出最终确认。选择所有传播帧或范围帧时，同一次去除必须同步应用到对应传播帧中的主区域和参与区域，参与扣除的 mask 本身保留。
- 区域合并/去除同步到传播帧时必须保留传播 mask 原有 `source`、`source_annotation_id`、`source_mask_id`、`propagation_seed_key`、`propagation_seed_signature` 等 lineage metadata；这些帧可以进入 dirty 待保存状态，但不能因为几何同步在时间轴上从自动传播帧变成人工/AI 标注帧。
- 区域合并/去除模式显示已选数量，并隐藏 polygon 编辑手柄以避免手柄抢占多选点击；第一个选中的主区域使用黄色实线轮廓，后续参与合并/扣除的区域使用红色虚线轮廓。
- 区域去除结果包含内洞时，前端保留 hole ring 并用 even-odd 规则渲染，保存时把外圈写入 `mask_data.polygons`、把每个外圈对应内洞写入 `mask_data.holes`，并用 `metadata.polygonRingCounts` 支撑前端 ring 回显。

## R6 AI 推理

- 当前 AI 页面支持选择 `sam2.1_hiera_tiny`、`sam2.1_hiera_small`、`sam2.1_hiera_base_plus`、`sam2.1_hiera_large`；SAM 3 选择、文本输入和相关状态展示已隐藏。
- 前端和工作区通过 `GET /api/ai/models/status` 展示 GPU 和四个 SAM 2.1 变体的真实运行状态；`selected_model=sam3` 会被后端拒绝。
- 前端 `predictMask()` 调用 `POST /api/ai/predict`。
- 前端发送后端契约：`image_id`、`prompt_type`、`prompt_data`、`model`。
- 点提示传 `{ points, labels }`，正向点 label 为 1，反向点 label 为 0。
- AI 页面在已有候选 mask 上点击正向/反向选点时，应继续添加提示点，不应被 mask 选择事件拦截。
- AI 页面点击已有提示点应删除对应点；“删除最近锚点”只移除最近放置的提示点，不删除候选 mask 或工作区 mask。
- AI 页面“删除选中候选”和 Delete/Backspace 只删除本页生成且已选中的 AI 候选 mask，不删除工作区已有 mask。
- 工作区点击已有 SAM 提示点应优先删除该点并按剩余提示重新推理；该事件不得冒泡成新增提示点、mask 选择或其它画布点击行为。
- 框选提示传归一化 `[x1, y1, x2, y2]`。
- AI 页面边界框选应支持画布拖拽预览框；执行分割时只框选不加点发送 `box` prompt，框选后继续加点发送 `interactive` prompt。
- 工作区 SAM 2.1 框选会建立一个候选 mask；后续正向点/反向点会携带原始框和累计点，以 `interactive` prompt 细化并替换同一个候选 mask。
- 工作区 SAM 2.1 一旦包含反向点，会随请求启用 `auto_filter_background` 和 `min_score=0.05`；若后端判定反向点排除了当前候选区域并返回空结果，前端会移除旧候选 mask，避免继续显示已被否定的区域。
- SAM 2.1 不支持文本语义提示；当前 AI 页面不提供文本语义输入，必须使用点/框提示。
- SAM 2.1 点提示和 auto fallback 默认只采用一个最高分候选 mask，避免多个候选 mask 作为同一结果重叠显示。
- AI 页面只渲染本页最新生成的候选 mask；重复执行高精度分割会替换上一次 AI 页候选，工作区已有手工、保存、传播或 GT 导入 mask 不会自动进入 AI 画布，也不会被替换。
- AI 页面提供“AI 遮罩透明度”滑杆，并与右侧“遮罩透明度”共享 `maskPreviewOpacity`；调节任一入口都会改变 AI 候选 mask 预览透明度，不改变 mask 几何、分类或保存数据。
- AI 页面参数开关展示文案使用“局部专注模式（自动裁剪无锚区域）”和“严格除杂模式（自动清理干涉点）”；这是 UI 可读性文案，不改变 `cropMode`、`autoDeleteBg` 或后端 `options` 字段。
- AI 页面生成的 SAM 2.1 mask 会写入全局 `masks`，自动同步到当前项目帧，并写入全局 `selectedMaskIds`；右侧语义分类树可以直接给新生成 mask 换标签。
- AI 页“清空全体锚点”只清空本页提示点和本页生成的候选 mask，不删除工作区已有 mask。
- AI 页面“推送至工作区编辑”必须先校验待推送 AI 候选 mask 已有语义分类；没有 `classId` 或 `className` 时用右上角 error toast 明确提示并停留在 AI 页，不允许进入工作区，确保工作区内 mask 都有语义。
- 如果用户不通过推送按钮而是直接离开 AI 页面，仍未选择语义分类的 AI 候选 mask 必须从全局 `masks` 和 `selectedMaskIds` 中清理，避免无语义候选通过侧栏切换进入工作区。
- AI 页面“推送至工作区编辑”在语义校验通过后会切回工作区并把工具切到“调整多边形”，保留当前选中的 AI mask 和当前帧视角，以便继续编辑轮廓和归档保存；如果 AI 操作发生在非第一帧，回到工作区后不得强制跳回第一帧。
- 工作区加载后端已保存标注时，必须保留当前项目帧里尚未保存的 AI/手工 draft mask，避免 AI 页推送到工作区的候选 mask 被异步回显流程覆盖。
- 语义文本提示 `semantic` 当前被后端禁用并返回 400。
- SAM 3 源码和历史测试保留，但不属于当前产品可用功能；前端不再展示 SAM 3 入口，后端 registry 不暴露 `sam3`。
- 工作区传播功能以当前打开帧作为参考帧，并使用该帧全部 mask 作为 seed；用户不再选择“选中区域/当前帧全部”传播对象。
- 工作区传播功能允许设置传播起始帧和传播结束帧；前端以当前参考帧为 seed，只向起止范围内位于参考帧之前和之后的帧传播，源帧不重复保存。
- 工作区只保留一个“自动传播”按钮，点击后在指定范围内按前向/后向自动生成 mask。
- 工作区进入自动传播范围选择时，顶栏必须显示本次传播权重以及按当前参考帧计算的向前/向后传播帧数，避免用户只看到起止帧而不清楚实际传播方向。
- 当前参考帧没有 mask 时，点击“开始传播”必须提示“当前参考帧无遮罩”，且不得提交传播任务或保存其它帧标注。
- 自动传播提交前，前端必须只保存当前参考帧中的 draft/dirty mask；参考帧 seed 优先使用后端 `annotation_id` 作为稳定来源，避免第一次用前端临时 id 传播、后续保存后无法替换旧传播结果；其它帧的脏标注不能在传播准备阶段触发无关后端更新。
- 前端会把多个 seed 或双向范围拆成 `steps`，通过 `POST /api/ai/propagate/task` 创建 `propagate_masks` 后台任务，避免长 HTTP 请求卡在浏览器侧，同时避免并发抢占 GPU。
- `POST /api/ai/propagate` 作为单 seed 同步兼容接口保留；`POST /api/ai/propagate/task` 是工作区自动传播使用的任务接口。两者当前支持四个 SAM 2.1 变体；兼容 `model=sam2` 并归一化为 tiny。SAM 2.1 使用官方 `SAM2VideoPredictor.add_new_mask()` 和 `propagate_in_video()`。
- 自动传播任务写入 `processing_tasks`，前端轮询 `GET /api/tasks/{task_id}` 显示进度并刷新标注；Dashboard 也能看到该任务，任务可取消和重试。
- 传播 seed 若包含中空结构，前端必须把内洞按外圈对齐传入 `holes`；后端栅格化 SAM 2 seed mask 时先填充外圈再扣除内洞，不能以实心 mask 注入 video predictor；seed 签名也必须包含 `holes`，避免中空编辑后被误判为未变化。
- 传播结果会写入后续帧 `annotations`，`mask_data.source` 标记为 `<model_id>_propagation`，并保留 label、color、class 元数据、seed 来源 id、seed 签名和传播方向；后端从传播二值 mask 提取轮廓时必须保留内洞，保存为与 `polygons` 对齐的 `mask_data.holes`，前端回显后仍能编辑内洞；如果历史或外部 seed 带 `geometry_smoothing` 平滑参数，worker 保存前仍必须对传播返回的 polygon 实际应用同一平滑几何，不能只更新拓扑锚点或 metadata。当前工作区平滑按钮应用后会直接改写实际 polygon 并清除平滑参数，后续传播以新几何本身参与签名。
- 自动传播任务必须避免重复叠加：同一目标帧段内，同一参考 seed、同一权重、同一方向且所有目标帧已有未变化结果时，worker 直接跳过；同一参考 seed 已变化、目标帧段只部分覆盖或用户改用其他 SAM 2.1 权重时，worker 先删除本次目标帧段内对应旧自动传播标注，再保存新传播结果；对早期只记录前端临时 `source_mask_id` 的旧传播结果，worker 会按传播方向和语义信息做兼容清理。用户在自动传播链中间帧人工新增或修改同一物体 mask 后重新向前/向后传播时，即使新 seed 缺少旧传播链 source id，也要按语义信息和目标帧空间重叠清理旧传播结果后再写入新结果；写入前清理不受旧结果 `propagation_direction` 限制，因此当前帧向前传播时也会替换原先由更早帧向后传播出来的旧 mask，避免同一物体新旧 mask 堆叠。未编辑的自动传播结果再次作为参考 seed 时，会继承原始 `propagation_seed_signature` 以避免重复传播；被编辑后的传播结果只保留 lineage，不继承旧签名，以便触发删除旧结果并重新传播。历史带 `geometry_smoothing` 的 seed 在 forward/backward 两个方向都会用同一参数平滑保存结果。
- AI 页面会对未放置点提示、后端错误和返回 0 个 mask 的情况显示明确反馈。
- AI 参数支持 `crop_to_prompt`、`auto_filter_background` 和 `min_score`；点/框 prompt 可以裁剪局部区域推理并回映射结果，背景过滤会移除低分结果和包含负向点的 polygon。
- 后端返回 `polygons` 和 `scores`。
- 前端把后端 `polygons` 转成 Konva `pathData`、`segmentation`、`bbox`、`area`。
- AI 推理结果先存放在前端 store 的 `masks` 中，顶栏保存状态按钮会按待保存数量显示“保存 X 个改动”或“已全部保存”；点击保存后持久化到后端标注表。

## R7 标注保存

- 后端提供 `POST /api/ai/annotate` 保存标注。
- 保存时必须存在项目；如果传入 `frame_id`，帧也必须存在。
- 后端提供 `GET /api/ai/annotations` 查询项目标注，可选按 `frame_id` 过滤。
- 后端提供 `PATCH /api/ai/annotations/{annotation_id}` 更新已保存标注的 `mask_data`、`points`、`bbox` 和 `template_id`。
- 后端提供 `DELETE /api/ai/annotations/{annotation_id}` 删除已保存标注。
- 当前前端保存状态按钮会保存当前项目未保存 mask，并会更新已标记为 dirty 的已保存 mask。
- 如果 dirty mask 携带的本地旧 `annotationId` 在后端已经不存在，前端保存链路必须先用当前后端标注列表做存在性预检，已知缺失的 id 直接用同一几何和 metadata 重新 `POST` 创建标注；如果预检后发生并发删除导致 `PATCH` 返回 404，也必须降级为重新创建，并重新拉取后端标注替换本地旧 id；点击“开始传播”前的参考帧保存也必须复用该容错逻辑，不能因陈旧 id 中断传播。
- 保存成功后，前端会重新拉取后端标注，并用后端 saved annotation 替换本次提交的 draft mask；未提交的其他 draft mask 仍保留。
- 工作区“清空遮罩”只从左侧工具栏触发；当前帧有选中 mask 时以选中 mask 为对象，没有选中时以当前帧全部 mask 为对象。若目标 mask 没有关联其它传播帧，则直接删除当前帧已保存标注并清空当前帧未保存 mask，不弹确认；若目标 mask 存在传播链上的其它帧结果，则弹出范围确认，用户可在同一行选择“取消”、“只清当前帧”、“按帧范围选择”或“清空所有传播帧”；按帧范围选择进入和自动传播/布尔操作一致的时间轴范围选择模式，并在顶栏“确认清空”后最终确认。清空所有传播帧或范围帧时若目标帧范围包含人工/AI 标注帧，会二次询问是否删除；选择是会删除这些人工/AI 标注帧中的全部 mask，选择否会保留这些人工/AI 标注帧整帧，只同步清空其它同传播链自动传播结果，不能删除其它帧独立 AI 推理或人工标注 mask。
- 工作区加载项目帧后会查询已保存标注并回显。
- 工作区支持导入 GT mask 图片，前端调用 `POST /api/ai/import-gt-mask`。
- 导入 GT Mask 时，前端必须让用户选择未知 maskid 处理策略：舍弃未知类别，或导入为“未定义类别”等待后续重新命名。
- 后端导入 GT mask 时必须仅支持 8-bit 二值/灰度 `GT_label图`，以及 8-bit RGB 三通道完全相同的 `[X,X,X]` maskid 图；0 是背景，X 是 1-255 的 maskid。灰度/RGB 等通道图按当前模板 `maskId` 匹配类别，超出现有类别时按用户选择的策略处理；16-bit/uint16 GT_label 和普通彩色 RGB 类别图不再视为合法 GT mask，必须返回图片不符合要求的明确错误。
- 后端导入 GT mask 时必须把全背景 0 图视为非法 GT mask，返回“GT Mask 图片中没有非背景 maskid 区域。”，前端导入预览也必须保留同一提示并禁止继续导入。
- 导入 GT mask 前端必须提供导入结果预览，显示检测到的 maskid、未知 maskid 和尺寸适配提示；如果 mask 图片尺寸与当前帧不同，后端导入前必须按当前帧长宽用最近邻插值拉伸，使 mask 可适配当前图片。
- 后端导入 GT mask 时按非背景像素值或颜色拆分多类别区域，再按连通域生成高精度 polygon 标注；轮廓提取应尽量保留边界细节，同时对单轮廓点数设置上限避免严重影响前端渲染和编辑性能；可通过距离变换写入内部 `points` seed 供数据兼容。
- 前端不回显导入标注的 seed point，也不提供 seed point 拖动；导入 mask 必须与普通 mask 共用拓扑锚点统计、边缘平滑、顶点编辑、分类和保存能力。

## R8 模板库

- 前端展示模板列表，调用 `GET /api/templates`。
- 用户可以新建、编辑、删除模板，也可以在“生效中模板架构清单”中用鼠标复制现有模板为当前用户私有副本。
- 模板分类存放在 `mapping_rules.classes`，规则存放在 `mapping_rules.rules`。
- 所有新建、复制、批量导入和后端返回的模板必须包含 `maskid: 0`、颜色 `[0,0,0]`/`#000000`、名称为“待分类”的保留分类；该分类固定显示在语义分类树最后，不能删除，也不能通过拖拽上移。
- 前端支持添加/删除分类、拖拽排序后更新内部覆盖优先级和 JSON 批量导入。JSON 批量导入必须支持 `[[colors], [names]]` 和 `{colors, names}` 两种格式，并兼容常见粘贴内容中的前缀、代码块、未加引号 keys、单引号、中文逗号/冒号和尾随逗号。模板详情页分类区标题必须显示为“语义分类树（拖拽调层级）”，右上角按钮必须显示为带编辑图标的“编辑模板”；分类行右侧不得显示“未分类/批量导入/模板名”等描述标签，必须显示垃圾桶图标并可点击删除该 label。编辑模板弹窗点击分类后只允许编辑分类名称，不得显示或编辑旧 `category` 来源元信息。复制模板必须保留分类名称、颜色、`maskid`、内部层级顺序和规则，但要重建类别内部 id。界面不展示内部优先级数值，只展示每个类别稳定的 `maskid`。
- 后端支持模板创建、列表、详情、局部更新和删除。

## R9 本体检查面板

- 工作区右侧可以选择模板。
- 面板显示模板分类；新增自定义分类会写入当前激活模板的后端 `mapping_rules.classes`。
- 右侧面板修改激活模板时，如果当前项目已有任意 mask，必须弹窗提示用户确认；确认后清空当前项目所有本地 mask 和已保存后端标注，再切换激活模板。当前项目没有任何 mask 时切换激活模板不需要提示。
- 用户可以选择具体分类；新 AI mask 会记录 `classId`、`className`、`classZIndex`，并在保存时写入 `mask_data.class`。
- 如果 Canvas 当前已经选中一个或多个 mask，点击语义分类树会把这些 mask 的 `label`、`color` 和 class 元数据改为该分类；如果这些 mask 属于自动传播链，还必须通过 `source_annotation_id`、`source_mask_id`、`propagation_seed_key` 和 `propagation_seed_signature` 同步更新同一传播链前后帧的对应 mask；已保存 mask 会进入 `dirty` 状态，归档保存时更新后端。
- 打开工作区回显项目标注时，如果已保存 mask 的 class 不再存在于其所属模板中，前端必须把该 mask 转为 `maskid: 0` 的“待分类”mask，保留几何，标记为 dirty，等待用户重新分类并保存。
- 添加自定义分类需要先选择模板，保存时调用 `PATCH /api/templates/{id}` 并同步全局模板 store。
- “特定目标实例属性追踪”下方显示当前选中 mask 的 `className/label`，不显示全局 active class 的旧值。
- 当前实例属性面板不展示“当前选中区域”计数；当前 mask 交互以单选为主，计数长期为 1，不作为有效业务信息展示。
- 选中 mask 后，拓扑锚点调用 `POST /api/ai/analyze-mask` 自动读取，不再显示固定占位值；后端 `topology_anchor_count` 必须表示 polygon 的真实顶点数量，不能用抽样后的展示点数代替；前端必须静默忽略 abort/cancel 或过期的分析请求，避免快速切换 mask、拖动平滑预览或卸载组件时误显示“后端属性读取失败”；前端不再展示“后端模型置信度”条目，也不再提供“重新提取拓扑锚点”调试按钮。
- 选中 mask 后，右侧实例属性面板提供“边缘平滑强度”和“应用边缘平滑”；调整滑杆时必须立即更新数值，但后端预览请求需要做短防抖，用户停止拖动约 220ms 后再调用 `POST /api/ai/smooth-mask` 并用返回 polygon 临时预览当前 mask 边缘，避免连续拖动时请求过密造成卡顿；预览阶段不标 dirty；点击“应用边缘平滑”后确认当前预览结果，前端必须把平滑 polygon 作为新的实际 mask 几何写入当前 mask，并同步写入同一传播链前后对应 mask；整次平滑应用必须作为一个撤销/重做历史步骤，撤销/重做要同时作用于当前 mask 和传播链对应 mask；应用后相关 mask 标记为 dirty/draft，平滑强度重置为 0，用户仍可继续用 polygon 编辑工具编辑平滑后的新多边形，并通过顶栏保存状态按钮落库；保存 dirty 传播链 mask 时必须保留传播来源 metadata，不能让原本自动传播帧变为人工/AI 标注帧。后端平滑必须对 AI/SAM 密集轮廓执行去噪简化、Chaikin 平滑和二次简化，使结果 polygon 的密集边缘点实际减少；强度映射必须低段温和、高段继续递进，避免 20% 左右已经过度平滑且后续档位无明显变化。

## R10 Dashboard 与 WebSocket

- Dashboard 显示基础统计、任务进度和活动日志。
- Dashboard 初始数据来自 `GET /api/dashboard/overview`。
- 后端聚合项目数、处理中任务数、标注数、帧数、模板数和主机 load average。
- 任务进度由 `processing_tasks` 中的 queued/running/success/failed/cancelled 任务生成，避免刚完成任务从进度区立即消失；处理中任务数统计只计算 queued/running；活动日志由最近任务、项目、标注和模板记录生成。
- Dashboard 对 queued/running 任务提供取消按钮，对 failed/cancelled 任务提供重试按钮。
- Dashboard 任务详情会读取 `GET /api/tasks/{task_id}` 并展示失败 error、payload、result、Celery ID 和时间信息。
- Dashboard 会连接 `/ws/progress`。
- 收到 progress、complete、error、status 消息时，前端会更新队列或日志。
- 收到 cancelled 消息时，前端会把对应任务标记为已取消。
- Celery worker 每次更新 `processing_tasks` 后会发布 Redis `seg:progress` 事件，FastAPI 订阅并广播给 `/ws/progress` 客户端。
- 后端 WebSocket 接收到客户端消息后返回 status heartbeat。

## R11 导出

- 后端支持 `GET /api/export/{project_id}/coco` 导出 COCO JSON。
- 后端支持 `GET /api/export/{project_id}/masks` 导出 PNG mask ZIP。
- 后端支持 `GET /api/export/{project_id}/results` 统一导出分割结果 ZIP，参数支持整体视频、特定范围帧和当前图片三种范围，并支持分开二值 mask、GT_label 黑白图、Pro_label 彩色图和 Mix_label 原图叠加图；Mix_label 透明度默认 0.3。
- 统一导出 ZIP 必须固定包含 `maskid_GT像素值_类别映射.json`，记录当前导出中每个类别的 `maskid`、GT_label 像素值、中文名、类别名、RGB 值、颜色和类别 key；GT_label 必须固定输出 8-bit uint8 PNG，背景值固定为 0，语义类别值使用类别真实 maskid，缺失 maskid 的旧标注才补下一个可用正整数，且同一类别跨图片保持一致；`maskid: 0` 的“待分类”必须在映射中保留为 0，GT_label 内与背景同为 0；正整数 maskid 超出 1-255 时必须拒绝导出。
- 统一导出 ZIP 必须固定包含 `原始图片/` 文件夹，导出范围内每帧的原始图片命名为 `视频名称_时间戳_项目帧序号` 加原图片扩展名；视频名称来自项目视频文件名，时间戳来自帧 `timestamp_ms` 并格式化为 `0h00m00s000ms`，帧号使用项目抽帧后的 1-based `frame_index + 1`，不使用原视频帧号。
- 选择“分开 Mask”时，统一导出 ZIP 必须包含 `分开Mask分割结果/`；每帧建立 `{视频名称_时间戳_项目帧序号}_分别导出` 子文件夹，同一帧同一类别的所有 annotation 合并为一张二值 PNG，文件名包含 `视频名称_时间戳_项目帧序号_{类别名称}_maskid{maskid}`。
- 选择“GT_label 黑白图”时，统一导出 ZIP 必须包含 `GT_label图/`；每帧输出一张融合后的 GT_label PNG，文件名为 `视频名称_时间戳_项目帧序号`，重叠区域仍按内部拖拽排序从低到高覆盖；maskid 不构成排序规范。
- 选择“Pro_label 彩色图”时，统一导出 ZIP 必须包含 `Pro_label彩色分割结果/`；每帧输出一张按类别 RGB 上色的 PNG，背景为 `[0,0,0]`，`maskid: 0` 的“待分类”也必须输出为黑色 `[0,0,0]`。
- 选择“Mix_label 叠加图”时，统一导出 ZIP 必须包含 `Mix_label重叠覆盖彩色分割结果/`；每帧输出一张彩色 label 叠加原始图片的 PNG，透明度可选且默认为 0.3。
- GT_label、Pro_label 和 Mix_label 的重叠区域覆盖顺序必须和右侧“语义分类树”的内部覆盖优先级一致，低优先级先写入，高优先级后写入。
- 分割结果导出 ZIP 文件名必须使用 `{项目库项目名}_seg_T_{起始时间戳}-{结束时间戳}_P_{起始项目帧序号}-{结束项目帧序号}.zip`；项目名来自项目库中的 `Project.name`，时间戳来自导出范围首尾帧 `timestamp_ms` 并格式化为 `0h00m00s000ms`，帧号使用项目抽帧后的 1-based `frame_index + 1`。
- 当前前端 `exportCoco()` API 封装已对齐后端路径。
- 当前前端 `exportMasks()` API 封装已对齐后端路径。
- 当前前端 `exportSegmentationResults()` API 封装已对齐统一导出路径。
- 工作区“分割结果导出”按钮已替代原 JSON/PNG 两个按钮；点击后在下拉栏内选择导出范围、勾选导出内容，并在选择 Mix_label 时调节遮罩透明度和查看当前/待导出第一帧预览；导出范围默认选中“当前图片”，导出前会先保存当前未归档 mask。选择“特定范围帧”时，用户既可以直接修改起止帧输入框，也可以像自动传播、清空遮罩一样在播放进度条或视频处理进度条上点击/拖拽选择导出范围。
- PNG mask ZIP 包含单标注二值 mask、按 zIndex 融合后的每帧语义 mask 和 `semantic_classes.json`。
- 统一导出的 GT_label 图必须是 8-bit uint8 PNG，背景值固定为 0，所有语义类别值优先保留模板类别真实 maskid，缺失 maskid 的旧标注才按下一个可用正整数补值；有效类别值范围为 0-255，其中 0 仅用于背景和系统保留的“待分类”。

## R12 配置

- 前端 API 地址由 `src/lib/config.ts` 统一推导。
- `VITE_API_BASE_URL` 优先级高于自动推导。
- `VITE_WS_PROGRESS_URL` 优先级高于从 API 地址推导 WebSocket 地址。
- 未设置环境变量时，前端按当前浏览器 hostname 推导 `http://<host>:8000`。

## R13 文档与测试

- `doc/` 目录保存当前实现审计、接口契约、需求冻结、设计冻结和测试计划。
- 测试应覆盖当前冻结需求中的真实功能、半可用行为和明确占位行为。
- 对外部服务依赖 PostgreSQL、MinIO、Redis、SAM 模型的测试应使用 mock 或测试替身，不依赖真实服务可用性。
