feat: 打通全栈标注闭环、异步拆帧与模型状态

后端能力:

- 新增 Celery app、worker task、ProcessingTask 模型、/api/tasks 查询接口和 media_task_runner,将 /api/media/parse 改为创建后台任务并由 worker 执行 FFmpeg/OpenCV/pydicom 拆帧。

- 新增 Redis 进度事件模块和 FastAPI Redis pub/sub 订阅,将 worker 任务进度广播到 /ws/progress;Dashboard 后端概览接口改为聚合 projects/frames/annotations/templates/processing_tasks。

- 统一项目状态为 pending/parsing/ready/error,新增共享 status 常量,并让前端兼容归一化旧状态值。

- 扩展 AI 后端:新增 SAM registry、SAM2 真实运行状态、SAM3 状态检测与文本语义推理适配入口,以及 /api/ai/models/status GPU/模型状态接口。

- 补齐标注保存/更新/删除、COCO/PNG mask 导出相关后端契约和模板 mapping_rules 打包/解包行为。

前端能力:

- 新增运行时 API/WS 地址推导配置,前端 API 封装对齐 FastAPI 路由、字段映射、任务轮询、标注归档、导出下载和 AI 预测响应转换。

- Dashboard 改为读取 /api/dashboard/overview,并订阅 WebSocket progress/complete/error/status 更新解析队列和实时流转记录。

- 项目库导入视频/DICOM 后创建项目、上传媒体、触发异步解析并刷新真实项目列表。

- 工作区加载真实帧、无帧时触发解析任务、回显已保存标注、保存未归档 mask、更新 dirty mask、清空当前帧后端标注、导出 COCO JSON。

- Canvas 支持当前帧点/框提示调用后端 AI、渲染推理/已保存 mask、应用模板分类并维护保存状态计数;时间轴按项目 fps 播放。

- AI 页面新增 SAM2/SAM3 模型选择,预测请求携带 model;侧边栏和工作区新增真实 GPU/SAM 状态徽标。

- 模板库和本体面板接入真实模板 CRUD、分类编辑、拖拽排序、JSON 导入、默认腹腔镜分类和本地自定义分类选择。

测试与文档:

- 新增 Vitest 配置、前端测试 setup、API/config/websocket/store/组件测试,覆盖登录、项目库、Dashboard、Canvas、工作区、模型状态、时间轴、本体和模板库。

- 新增 pytest 后端测试夹具和 auth/projects/templates/media/AI/export/dashboard/tasks/progress 测试,使用 SQLite、fake MinIO、fake SAM registry 和 Redis monkeypatch 隔离外部服务。

- 新增 doc/ 文档结构,冻结当前需求、设计、接口契约、测试计划、前端逐元素审计、实现地图和后续实施计划,并同步更新 README 与 AGENTS。

验证:

- conda run -n seg_server pytest backend/tests:27 passed。

- npm run test:run:54 passed。

- npm run lint、npm run build、compileall、git diff --check 均通过;Vite 仅提示大 chunk 警告。
This commit is contained in:
2026-05-01 13:29:14 +08:00
parent 4d65c37c73
commit f020ff3b4f
78 changed files with 7089 additions and 456 deletions

View File

@@ -4,7 +4,7 @@ export interface Project {
id: string;
name: string;
description?: string;
status: 'Ready' | 'Parsing' | 'Error';
status: 'pending' | 'parsing' | 'ready' | 'error';
fps?: string;
frames?: number;
thumbnail?: string;
@@ -17,6 +17,8 @@ export interface Project {
updatedAt?: string;
}
export type AiModelId = 'sam2' | 'sam3';
export interface Frame {
id: string;
projectId: string;
@@ -42,6 +44,13 @@ export interface Annotation {
export interface Mask {
id: string;
frameId: string;
annotationId?: string;
templateId?: string;
classId?: string;
className?: string;
classZIndex?: number;
saveStatus?: 'draft' | 'saved' | 'dirty' | 'saving' | 'error';
saved?: boolean;
pathData: string;
label: string;
color: string;
@@ -96,24 +105,32 @@ export interface AppState {
// Workspace
activeModule: string;
activeTool: string;
aiModel: AiModelId;
frames: Frame[];
currentFrameIndex: number;
annotations: Annotation[];
masks: Mask[];
setActiveModule: (module: string) => void;
setActiveTool: (tool: string) => void;
setAiModel: (model: AiModelId) => void;
setFrames: (frames: Frame[]) => void;
setCurrentFrame: (index: number) => void;
addAnnotation: (annotation: Annotation) => void;
addMask: (mask: Mask) => void;
updateMask: (id: string, updates: Partial<Mask>) => void;
setMasks: (masks: Mask[]) => void;
clearMasks: () => void;
removeAnnotation: (id: string) => void;
// Templates
templates: Template[];
activeTemplateId: string | null;
activeClassId: string | null;
activeClass: TemplateClass | null;
setTemplates: (templates: Template[]) => void;
setActiveTemplateId: (id: string | null) => void;
setActiveClassId: (id: string | null) => void;
setActiveClass: (templateClass: TemplateClass | null) => void;
addTemplate: (template: Template) => void;
updateTemplate: (template: Template) => void;
removeTemplate: (id: string) => void;
@@ -144,6 +161,9 @@ export const useStore = create<AppState>((set) => ({
frames: [],
annotations: [],
masks: [],
activeTemplateId: null,
activeClassId: null,
activeClass: null,
});
},
@@ -162,18 +182,25 @@ export const useStore = create<AppState>((set) => ({
// Workspace
activeModule: 'workspace',
activeTool: 'move',
aiModel: 'sam2',
frames: [],
currentFrameIndex: 0,
annotations: [],
masks: [],
setActiveModule: (activeModule: string) => set({ activeModule }),
setActiveTool: (activeTool: string) => set({ activeTool }),
setAiModel: (aiModel: AiModelId) => set({ aiModel }),
setFrames: (frames: Frame[]) => set({ frames }),
setCurrentFrame: (currentFrameIndex: number) => set({ currentFrameIndex }),
addAnnotation: (annotation: Annotation) =>
set((state) => ({ annotations: [...state.annotations, annotation] })),
addMask: (mask: Mask) =>
set((state) => ({ masks: [...state.masks, mask] })),
updateMask: (id: string, updates: Partial<Mask>) =>
set((state) => ({
masks: state.masks.map((mask) => (mask.id === id ? { ...mask, ...updates } : mask)),
})),
setMasks: (masks: Mask[]) => set({ masks }),
clearMasks: () => set({ masks: [] }),
removeAnnotation: (id: string) =>
set((state) => ({
@@ -183,8 +210,15 @@ export const useStore = create<AppState>((set) => ({
// Templates
templates: [],
activeTemplateId: null,
activeClassId: null,
activeClass: null,
setTemplates: (templates: Template[]) => set({ templates }),
setActiveTemplateId: (activeTemplateId: string | null) => set({ activeTemplateId }),
setActiveClassId: (activeClassId: string | null) => set({ activeClassId }),
setActiveClass: (activeClass: TemplateClass | null) => set({
activeClass,
activeClassId: activeClass?.id || null,
}),
addTemplate: (template: Template) =>
set((state) => ({ templates: [...state.templates, template] })),
updateTemplate: (template: Template) =>