限定DEL人工帧删除范围
- 为清空/删除传播链确认区分 clear 与 delete 操作来源 - DEL/Delete 确认删除人工或 AI 帧时只删除选中或同传播链对应 mask - 保持清空遮罩操作确认后可按原逻辑清空人工/AI 帧范围 - 调整人工/AI 帧确认弹窗文案,避免误导为 DEL 会整帧清空 - 补充 VideoWorkspace 回归测试,覆盖同一人工帧其它 mask 不被 DEL 删除 - 更新项目指南和设计冻结文档
This commit is contained in:
@@ -267,7 +267,7 @@ uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
|||||||
- 右侧实例属性面板“边缘平滑强度/应用边缘平滑”已接入 `POST /api/ai/smooth-mask`;滑杆会即时更新数值,但后端预览请求有短防抖,避免拖动时连续请求卡顿;预览不写入撤销历史也不标 dirty;点击应用后会把返回 polygon 作为新的实际 mask 几何写入当前 mask 和同传播链前后对应 mask,整次应用作为一个撤销/重做历史步骤,相关 mask 标记为 dirty/draft,平滑强度重置为 0,用户可继续用 polygon 编辑工具调整新多边形。
|
- 右侧实例属性面板“边缘平滑强度/应用边缘平滑”已接入 `POST /api/ai/smooth-mask`;滑杆会即时更新数值,但后端预览请求有短防抖,避免拖动时连续请求卡顿;预览不写入撤销历史也不标 dirty;点击应用后会把返回 polygon 作为新的实际 mask 几何写入当前 mask 和同传播链前后对应 mask,整次应用作为一个撤销/重做历史步骤,相关 mask 标记为 dirty/draft,平滑强度重置为 0,用户可继续用 polygon 编辑工具调整新多边形。
|
||||||
- 工作区“自动传播”按钮位于左侧工具栏橡皮擦下方,并已接入 `POST /api/ai/propagate/task`;若用户尚未显式设置范围,第一次点击会进入时间轴范围选择模式,顶栏才显示传播权重和向前/向后帧数,第二次点击“开始传播”才提交后台任务;当前启用所选 SAM 2.1 变体的视频 predictor 后台任务,运行中轮询任务进度,完成后刷新后端已保存标注;同一参考帧多个同类别 seed 会按来源 id 分开传播,不会因 label/color 相同互相覆盖;中空 seed 会把内洞传给后端,SAM 2 seed mask 栅格化时扣除内洞,传播结果保存时也会保留 `holes`;GPU/CPU 模型状态只在左侧 Sidebar 底部用紧凑徽标展示,工作区顶栏不再重复显示,具体 SAM 2.1 传播权重由顶栏下拉选择;同步 `POST /api/ai/propagate` 仍作为单 seed 兼容接口保留。
|
- 工作区“自动传播”按钮位于左侧工具栏橡皮擦下方,并已接入 `POST /api/ai/propagate/task`;若用户尚未显式设置范围,第一次点击会进入时间轴范围选择模式,顶栏才显示传播权重和向前/向后帧数,第二次点击“开始传播”才提交后台任务;当前启用所选 SAM 2.1 变体的视频 predictor 后台任务,运行中轮询任务进度,完成后刷新后端已保存标注;同一参考帧多个同类别 seed 会按来源 id 分开传播,不会因 label/color 相同互相覆盖;中空 seed 会把内洞传给后端,SAM 2 seed mask 栅格化时扣除内洞,传播结果保存时也会保留 `holes`;GPU/CPU 模型状态只在左侧 Sidebar 底部用紧凑徽标展示,工作区顶栏不再重复显示,具体 SAM 2.1 传播权重由顶栏下拉选择;同步 `POST /api/ai/propagate` 仍作为单 seed 兼容接口保留。
|
||||||
- 工作区顶栏短状态会自动消失;保存、导出、导入 GT、传播进行中和无帧项目提示会保留到状态变化。
|
- 工作区顶栏短状态会自动消失;保存、导出、导入 GT、传播进行中和无帧项目提示会保留到状态变化。
|
||||||
- 工作区“清空遮罩”和左侧 `DEL` 删除只从左侧工具栏或键盘触发,会在删除已保存标注前预检当前项目仍存在的 annotation id,只对存在的 id 调用 `DELETE /api/ai/annotations/{id}`;如果当前帧有选中 mask 则优先清/删选中 mask,没有选中时清当前帧全部 mask;如果对象没关联其它传播帧则直接处理当前帧,如果存在传播链结果则弹窗在同一行选择取消、只处理当前帧、按帧范围选择或清空所有传播帧;按帧范围选择复用工作区时间轴范围选择和最终确认弹窗;按范围清空或清空所有传播帧时,如果目标帧范围内包含人工绘制或独立 AI 标注帧,会再提示是否删除人工/AI 标注帧;清空所有传播帧会按传播链 seed 与传播结果跨越的完整帧段检查人工/AI 帧,不只检查实际要删除的自动传播 mask 所在帧;选择否时整帧保留,只清其它自动传播帧;工作区顶栏不再提供重复的“清空片段遮罩”。
|
- 工作区“清空遮罩”和左侧 `DEL` 删除只从左侧工具栏或键盘触发,会在删除已保存标注前预检当前项目仍存在的 annotation id,只对存在的 id 调用 `DELETE /api/ai/annotations/{id}`;如果当前帧有选中 mask 则优先清/删选中 mask,没有选中时清当前帧全部 mask;如果对象没关联其它传播帧则直接处理当前帧,如果存在传播链结果则弹窗在同一行选择取消、只处理当前帧、按帧范围选择或清空所有传播帧;按帧范围选择复用工作区时间轴范围选择和最终确认弹窗;按范围清空或清空所有传播帧时,如果目标帧范围内包含人工绘制或独立 AI 标注帧,会再提示是否删除人工/AI 标注帧;清空所有传播帧会按传播链 seed 与传播结果跨越的完整帧段检查人工/AI 帧,不只检查实际要删除的自动传播 mask 所在帧;选择否时整帧保留,只清其它自动传播帧;左侧 `DEL` 或键盘 Delete/Backspace 触发同一确认时,选择删除人工/AI 帧只会删除这些帧中本次选中或同传播链对应的 mask,不会清掉同帧其它 mask;工作区顶栏不再提供重复的“清空片段遮罩”。
|
||||||
- 项目状态已统一为 `pending`、`parsing`、`ready`、`error`;前端 `src/lib/api.ts` 会兼容归一化旧库中可能存在的 `Ready`、`Parsing`、`Error`。
|
- 项目状态已统一为 `pending`、`parsing`、`ready`、`error`;前端 `src/lib/api.ts` 会兼容归一化旧库中可能存在的 `Ready`、`Parsing`、`Error`。
|
||||||
- 项目库的视频导入与生成帧是两个独立动作:导入视频只上传源文件,并通过 Axios `onUploadProgress` 在项目库显示导入进度;生成帧按钮才会带 `parse_fps` 调用 `/api/media/parse`;DICOM 批量导入也会显示上传进度和文件数量,上传完成后创建解析任务并轮询显示解析进度。工作区不会再因“有视频但无帧”自动创建拆帧任务。
|
- 项目库的视频导入与生成帧是两个独立动作:导入视频只上传源文件,并通过 Axios `onUploadProgress` 在项目库显示导入进度;生成帧按钮才会带 `parse_fps` 调用 `/api/media/parse`;DICOM 批量导入也会显示上传进度和文件数量,上传完成后创建解析任务并轮询显示解析进度。工作区不会再因“有视频但无帧”自动创建拆帧任务。
|
||||||
- `server.ts` 不再提供旧版 `/api/login`、`/api/projects`、`/api/templates` mock;当前前端真实 API 调用走 FastAPI 的 `/api/auth/*`、`/api/projects`、`/api/templates` 等接口。
|
- `server.ts` 不再提供旧版 `/api/login`、`/api/projects`、`/api/templates` mock;当前前端真实 API 调用走 FastAPI 的 `/api/auth/*`、`/api/projects`、`/api/templates` 等接口。
|
||||||
|
|||||||
@@ -158,7 +158,7 @@
|
|||||||
21. 新 mask 会带上当前选择的模板分类元数据,包括 `classId`、`className`、`classZIndex`、`metadata.source=ai_segmentation` 和保存状态 `draft`。
|
21. 新 mask 会带上当前选择的模板分类元数据,包括 `classId`、`className`、`classZIndex`、`metadata.source=ai_segmentation` 和保存状态 `draft`。
|
||||||
20. 顶栏保存状态按钮按当前项目待保存数量显示为“保存 X 个改动”或“已全部保存”;用户点击保存后,前端将像素 `segmentation` 转成 normalized `mask_data.polygons`;未保存 mask 调用 `POST /api/ai/annotate`,dirty mask 会先读取当前后端标注 id 列表,已知存在的 id 调用 `PATCH /api/ai/annotations/{annotation_id}`,已知缺失的本地旧 id 直接保留同一 `mask_data`、几何、分类和传播 lineage metadata 改用 `POST /api/ai/annotate` 重新创建;如果预检后发生并发删除导致 `PATCH` 返回 404,也会降级为重新创建,并在随后回显时排除本地旧 mask id;保存成功后本次提交的 draft mask id 会从本地保留列表中排除,并由后端 saved annotation 回显替换。
|
20. 顶栏保存状态按钮按当前项目待保存数量显示为“保存 X 个改动”或“已全部保存”;用户点击保存后,前端将像素 `segmentation` 转成 normalized `mask_data.polygons`;未保存 mask 调用 `POST /api/ai/annotate`,dirty mask 会先读取当前后端标注 id 列表,已知存在的 id 调用 `PATCH /api/ai/annotations/{annotation_id}`,已知缺失的本地旧 id 直接保留同一 `mask_data`、几何、分类和传播 lineage metadata 改用 `POST /api/ai/annotate` 重新创建;如果预检后发生并发删除导致 `PATCH` 返回 404,也会降级为重新创建,并在随后回显时排除本地旧 mask id;保存成功后本次提交的 draft mask id 会从本地保留列表中排除,并由后端 saved annotation 回显替换。
|
||||||
21. 工作区加载项目帧后通过 `GET /api/ai/annotations` 取回已保存标注并转成前端 mask。
|
21. 工作区加载项目帧后通过 `GET /api/ai/annotations` 取回已保存标注并转成前端 mask。
|
||||||
22. 工作区“清空遮罩”只从左侧工具栏触发;如果当前帧存在选中 mask,则以当前帧选中 mask 为清空对象,否则以当前帧全部 mask 为清空对象。如果清空对象没有关联其它传播帧,直接删除当前帧已保存标注并清除当前帧本地 mask,不弹确认;如果存在传播链结果,`VideoWorkspace` 弹出范围选择,用户可在同一行选择取消、只清当前帧、按帧范围选择或清空当前帧及同传播链所有自动传播帧;按帧范围选择复用时间轴范围选择并在顶栏“确认清空”后最终确认。按范围清空或清空所有传播帧时,如果目标帧范围包含人工/AI 标注帧,会二次询问是否删除;其中清空所有传播帧会用传播链 seed 与传播结果跨越的完整帧段检查人工/AI 帧,避免从传播结果帧触发时漏掉中间独立 AI/人工帧;选择是会删除这些人工/AI 标注帧中的全部 mask,选择否时这些帧整帧保留,只清其它自动传播帧。本操作不删除其它帧独立 AI 推理或人工 mask。左侧工具栏的 `DEL` 按钮和键盘 Delete/Backspace 删除整块 mask 时复用同一传播链范围确认;删除已保存标注前会通过 `GET /api/ai/annotations` 预检当前项目仍存在的 annotation id,只对存在的 id 发送 `DELETE`。
|
22. 工作区“清空遮罩”只从左侧工具栏触发;如果当前帧存在选中 mask,则以当前帧选中 mask 为清空对象,否则以当前帧全部 mask 为清空对象。如果清空对象没有关联其它传播帧,直接删除当前帧已保存标注并清除当前帧本地 mask,不弹确认;如果存在传播链结果,`VideoWorkspace` 弹出范围选择,用户可在同一行选择取消、只清当前帧、按帧范围选择或清空当前帧及同传播链所有自动传播帧;按帧范围选择复用时间轴范围选择并在顶栏“确认清空”后最终确认。按范围清空或清空所有传播帧时,如果目标帧范围包含人工/AI 标注帧,会二次询问是否删除;其中清空所有传播帧会用传播链 seed 与传播结果跨越的完整帧段检查人工/AI 帧,避免从传播结果帧触发时漏掉中间独立 AI/人工帧;选择是会删除这些人工/AI 标注帧中的全部 mask,选择否时这些帧整帧保留,只清其它自动传播帧。左侧工具栏的 `DEL` 按钮和键盘 Delete/Backspace 删除整块 mask 时复用同一传播链范围确认,但 DEL/键盘删除在人工/AI 帧确认选择“是”时只删除本次选中或同传播链对应 mask,不会清掉同帧其它 mask;删除已保存标注前会通过 `GET /api/ai/annotations` 预检当前项目仍存在的 annotation id,只对存在的 id 发送 `DELETE`。
|
||||||
|
|
||||||
### 视频片段传播
|
### 视频片段传播
|
||||||
|
|
||||||
|
|||||||
@@ -735,6 +735,65 @@ describe('VideoWorkspace', () => {
|
|||||||
expect(useStore.getState().masks.map((mask) => mask.id).sort()).toEqual(['annotation-1']);
|
expect(useStore.getState().masks.map((mask) => mask.id).sort()).toEqual(['annotation-1']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('deletes only the selected manual mask when DEL includes manual frames', async () => {
|
||||||
|
apiMock.getProjectFrames.mockResolvedValueOnce([
|
||||||
|
{ id: 10, project_id: 1, frame_index: 0, image_url: '/frame.jpg', width: 640, height: 360 },
|
||||||
|
{ id: 11, project_id: 1, frame_index: 1, image_url: '/frame-1.jpg', width: 640, height: 360 },
|
||||||
|
]);
|
||||||
|
apiMock.deleteAnnotation.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
render(<VideoWorkspace />);
|
||||||
|
await waitFor(() => expect(useStore.getState().frames).toHaveLength(2));
|
||||||
|
act(() => {
|
||||||
|
useStore.setState({
|
||||||
|
masks: [
|
||||||
|
{
|
||||||
|
id: 'annotation-1',
|
||||||
|
annotationId: '1',
|
||||||
|
frameId: '10',
|
||||||
|
pathData: 'M 0 0 Z',
|
||||||
|
label: 'Selected Seed',
|
||||||
|
color: '#06b6d4',
|
||||||
|
saved: true,
|
||||||
|
saveStatus: 'saved',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'annotation-2',
|
||||||
|
annotationId: '2',
|
||||||
|
frameId: '10',
|
||||||
|
pathData: 'M 5 5 Z',
|
||||||
|
label: 'Other manual mask',
|
||||||
|
color: '#f97316',
|
||||||
|
saved: true,
|
||||||
|
saveStatus: 'saved',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'annotation-10',
|
||||||
|
annotationId: '10',
|
||||||
|
frameId: '11',
|
||||||
|
pathData: 'M 1 1 Z',
|
||||||
|
label: 'Propagated',
|
||||||
|
color: '#06b6d4',
|
||||||
|
saved: true,
|
||||||
|
saveStatus: 'saved',
|
||||||
|
metadata: { source: 'sam2_propagation', source_annotation_id: 1, source_mask_id: 'annotation-1', propagation_seed_key: 'annotation:1' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selectedMaskIds: ['annotation-1'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'DEL' }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '清空所有传播帧' }));
|
||||||
|
expect(screen.getByText('是否删除人工/AI 标注帧')).toBeInTheDocument();
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '是,删除人工帧' }));
|
||||||
|
|
||||||
|
await waitFor(() => expect(apiMock.deleteAnnotation).toHaveBeenCalledWith('1'));
|
||||||
|
expect(apiMock.deleteAnnotation).toHaveBeenCalledWith('10');
|
||||||
|
expect(apiMock.deleteAnnotation).not.toHaveBeenCalledWith('2');
|
||||||
|
expect(useStore.getState().masks.map((mask) => mask.id)).toEqual(['annotation-2']);
|
||||||
|
});
|
||||||
|
|
||||||
it('asks about AI-only frames across the propagation span when clearing all propagated masks', async () => {
|
it('asks about AI-only frames across the propagation span when clearing all propagated masks', async () => {
|
||||||
apiMock.getProjectFrames.mockResolvedValueOnce([
|
apiMock.getProjectFrames.mockResolvedValueOnce([
|
||||||
{ id: 10, project_id: 1, frame_index: 0, image_url: '/frame-0.jpg', width: 640, height: 360 },
|
{ id: 10, project_id: 1, frame_index: 0, image_url: '/frame-0.jpg', width: 640, height: 360 },
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ type PropagationHistorySegment = {
|
|||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
type RangeSelectionMode = 'propagation' | 'export' | 'boolean' | 'clear' | null;
|
type RangeSelectionMode = 'propagation' | 'export' | 'boolean' | 'clear' | null;
|
||||||
|
type ClearOperationKind = 'clear' | 'delete';
|
||||||
type CurrentClearConfirmState = {
|
type CurrentClearConfirmState = {
|
||||||
|
operation: ClearOperationKind;
|
||||||
currentFrameNumber: number;
|
currentFrameNumber: number;
|
||||||
scopeLabel: string;
|
scopeLabel: string;
|
||||||
currentMaskIds: string[];
|
currentMaskIds: string[];
|
||||||
@@ -64,6 +66,7 @@ type ClearManualFrameConfirmState = {
|
|||||||
allMaskIds: string[];
|
allMaskIds: string[];
|
||||||
autoOnlyMaskIds: string[];
|
autoOnlyMaskIds: string[];
|
||||||
manualFrameNumbers: number[];
|
manualFrameNumbers: number[];
|
||||||
|
manualFrameIncludeMode: 'all-frame-masks' | 'target-masks';
|
||||||
messageScope: string;
|
messageScope: string;
|
||||||
resetRangeAfterClear: boolean;
|
resetRangeAfterClear: boolean;
|
||||||
};
|
};
|
||||||
@@ -911,6 +914,7 @@ export function VideoWorkspace({ onNavigateToAI }: { onNavigateToAI?: () => void
|
|||||||
}
|
}
|
||||||
|
|
||||||
setPendingCurrentClearConfirm({
|
setPendingCurrentClearConfirm({
|
||||||
|
operation: 'clear',
|
||||||
currentFrameNumber,
|
currentFrameNumber,
|
||||||
scopeLabel,
|
scopeLabel,
|
||||||
currentMaskIds,
|
currentMaskIds,
|
||||||
@@ -948,6 +952,7 @@ export function VideoWorkspace({ onNavigateToAI }: { onNavigateToAI?: () => void
|
|||||||
}
|
}
|
||||||
|
|
||||||
setPendingCurrentClearConfirm({
|
setPendingCurrentClearConfirm({
|
||||||
|
operation: 'delete',
|
||||||
currentFrameNumber,
|
currentFrameNumber,
|
||||||
scopeLabel,
|
scopeLabel,
|
||||||
currentMaskIds,
|
currentMaskIds,
|
||||||
@@ -968,13 +973,20 @@ export function VideoWorkspace({ onNavigateToAI }: { onNavigateToAI?: () => void
|
|||||||
const requestClearMasksWithManualFrameConfirm = useCallback((
|
const requestClearMasksWithManualFrameConfirm = useCallback((
|
||||||
maskIdsToClear: string[],
|
maskIdsToClear: string[],
|
||||||
messageScope: string,
|
messageScope: string,
|
||||||
options?: { manualFrameScopeIds?: string[]; resetRangeAfterClear?: boolean },
|
options?: {
|
||||||
|
manualFrameScopeIds?: string[];
|
||||||
|
manualFrameTargetMaskIds?: string[];
|
||||||
|
manualFrameIncludeMode?: 'all-frame-masks' | 'target-masks';
|
||||||
|
resetRangeAfterClear?: boolean;
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
const latestMasks = useStore.getState().masks;
|
const latestMasks = useStore.getState().masks;
|
||||||
const targetMaskIdSet = new Set(maskIdsToClear);
|
const targetMaskIdSet = new Set(maskIdsToClear);
|
||||||
|
const manualTargetMaskIdSet = new Set([...(options?.manualFrameTargetMaskIds || []), ...maskIdsToClear]);
|
||||||
const targetMasks = latestMasks.filter((mask) => targetMaskIdSet.has(mask.id));
|
const targetMasks = latestMasks.filter((mask) => targetMaskIdSet.has(mask.id));
|
||||||
const targetFrameIds = new Set(targetMasks.map((mask) => String(mask.frameId)));
|
const targetFrameIds = new Set(targetMasks.map((mask) => String(mask.frameId)));
|
||||||
const manualScopeFrameIds = new Set((options?.manualFrameScopeIds || Array.from(targetFrameIds)).map(String));
|
const manualScopeFrameIds = new Set((options?.manualFrameScopeIds || Array.from(targetFrameIds)).map(String));
|
||||||
|
const includeAllManualFrameMasks = options?.manualFrameIncludeMode !== 'target-masks';
|
||||||
const manualFrameIds = new Set(
|
const manualFrameIds = new Set(
|
||||||
latestMasks
|
latestMasks
|
||||||
.filter((mask) => manualScopeFrameIds.has(String(mask.frameId)) && !isPropagatedMask(mask))
|
.filter((mask) => manualScopeFrameIds.has(String(mask.frameId)) && !isPropagatedMask(mask))
|
||||||
@@ -993,7 +1005,10 @@ export function VideoWorkspace({ onNavigateToAI }: { onNavigateToAI?: () => void
|
|||||||
.filter((frameNumber): frameNumber is number => Boolean(frameNumber))
|
.filter((frameNumber): frameNumber is number => Boolean(frameNumber))
|
||||||
.sort((a, b) => a - b);
|
.sort((a, b) => a - b);
|
||||||
const manualFrameMaskIds = latestMasks
|
const manualFrameMaskIds = latestMasks
|
||||||
.filter((mask) => manualFrameIds.has(String(mask.frameId)))
|
.filter((mask) => (
|
||||||
|
manualFrameIds.has(String(mask.frameId))
|
||||||
|
&& (includeAllManualFrameMasks || manualTargetMaskIdSet.has(mask.id))
|
||||||
|
))
|
||||||
.map((mask) => mask.id);
|
.map((mask) => mask.id);
|
||||||
const autoOnlyMaskIds = targetMasks
|
const autoOnlyMaskIds = targetMasks
|
||||||
.filter((mask) => !manualFrameIds.has(String(mask.frameId)))
|
.filter((mask) => !manualFrameIds.has(String(mask.frameId)))
|
||||||
@@ -1005,6 +1020,7 @@ export function VideoWorkspace({ onNavigateToAI }: { onNavigateToAI?: () => void
|
|||||||
allMaskIds,
|
allMaskIds,
|
||||||
autoOnlyMaskIds,
|
autoOnlyMaskIds,
|
||||||
manualFrameNumbers,
|
manualFrameNumbers,
|
||||||
|
manualFrameIncludeMode: includeAllManualFrameMasks ? 'all-frame-masks' : 'target-masks',
|
||||||
messageScope,
|
messageScope,
|
||||||
resetRangeAfterClear: Boolean(options?.resetRangeAfterClear),
|
resetRangeAfterClear: Boolean(options?.resetRangeAfterClear),
|
||||||
});
|
});
|
||||||
@@ -1029,6 +1045,11 @@ export function VideoWorkspace({ onNavigateToAI }: { onNavigateToAI?: () => void
|
|||||||
return frames.slice(startIndex, endIndex + 1).map((frame) => String(frame.id));
|
return frames.slice(startIndex, endIndex + 1).map((frame) => String(frame.id));
|
||||||
}, [frameNumberById, frames]);
|
}, [frameNumberById, frames]);
|
||||||
|
|
||||||
|
const chainMaskIdsForClearRequest = useCallback((request: CurrentClearConfirmState): string[] => {
|
||||||
|
const latestMasks = useStore.getState().masks;
|
||||||
|
return Array.from(findPropagationChainMaskIds(request.currentMaskIds, latestMasks));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleResolveClearManualFrameConfirm = useCallback(async (includeManualFrames: boolean) => {
|
const handleResolveClearManualFrameConfirm = useCallback(async (includeManualFrames: boolean) => {
|
||||||
if (!pendingClearManualFrameConfirm) return;
|
if (!pendingClearManualFrameConfirm) return;
|
||||||
const targetMaskIds = includeManualFrames
|
const targetMaskIds = includeManualFrames
|
||||||
@@ -1122,10 +1143,17 @@ export function VideoWorkspace({ onNavigateToAI }: { onNavigateToAI?: () => void
|
|||||||
requestClearMasksWithManualFrameConfirm(
|
requestClearMasksWithManualFrameConfirm(
|
||||||
confirmState.targetMaskIds,
|
confirmState.targetMaskIds,
|
||||||
`第 ${confirmState.rangeStartIndex + 1}-${confirmState.rangeEndIndex + 1} 帧传播链`,
|
`第 ${confirmState.rangeStartIndex + 1}-${confirmState.rangeEndIndex + 1} 帧传播链`,
|
||||||
{ manualFrameScopeIds, resetRangeAfterClear: true },
|
{
|
||||||
|
manualFrameScopeIds,
|
||||||
|
manualFrameTargetMaskIds: confirmState.request.operation === 'delete'
|
||||||
|
? chainMaskIdsForClearRequest(confirmState.request)
|
||||||
|
: undefined,
|
||||||
|
manualFrameIncludeMode: confirmState.request.operation === 'delete' ? 'target-masks' : 'all-frame-masks',
|
||||||
|
resetRangeAfterClear: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
setPendingClearPropagationRangeConfirm(null);
|
setPendingClearPropagationRangeConfirm(null);
|
||||||
}, [frames, requestClearMasksWithManualFrameConfirm]);
|
}, [chainMaskIdsForClearRequest, frames, requestClearMasksWithManualFrameConfirm]);
|
||||||
|
|
||||||
const handleBooleanFrameRangeRequest = useCallback((request: BooleanFrameRangeRequest) => {
|
const handleBooleanFrameRangeRequest = useCallback((request: BooleanFrameRangeRequest) => {
|
||||||
const candidateFrameNumbers = request.candidateFrameIds
|
const candidateFrameNumbers = request.candidateFrameIds
|
||||||
@@ -2307,7 +2335,15 @@ export function VideoWorkspace({ onNavigateToAI }: { onNavigateToAI?: () => void
|
|||||||
onClick={() => requestClearMasksWithManualFrameConfirm(
|
onClick={() => requestClearMasksWithManualFrameConfirm(
|
||||||
pendingCurrentClearConfirm.propagatedMaskIds,
|
pendingCurrentClearConfirm.propagatedMaskIds,
|
||||||
'当前帧及传播链',
|
'当前帧及传播链',
|
||||||
{ manualFrameScopeIds: propagationSpanFrameIdsForClearRequest(pendingCurrentClearConfirm) },
|
{
|
||||||
|
manualFrameScopeIds: propagationSpanFrameIdsForClearRequest(pendingCurrentClearConfirm),
|
||||||
|
manualFrameTargetMaskIds: pendingCurrentClearConfirm.operation === 'delete'
|
||||||
|
? chainMaskIdsForClearRequest(pendingCurrentClearConfirm)
|
||||||
|
: undefined,
|
||||||
|
manualFrameIncludeMode: pendingCurrentClearConfirm.operation === 'delete'
|
||||||
|
? 'target-masks'
|
||||||
|
: 'all-frame-masks',
|
||||||
|
},
|
||||||
)}
|
)}
|
||||||
className="rounded bg-red-500 px-2 py-2 text-xs font-semibold text-white hover:bg-red-400 disabled:opacity-60"
|
className="rounded bg-red-500 px-2 py-2 text-xs font-semibold text-white hover:bg-red-400 disabled:opacity-60"
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
@@ -2328,7 +2364,9 @@ export function VideoWorkspace({ onNavigateToAI }: { onNavigateToAI?: () => void
|
|||||||
是否同时删除这些帧中的遮罩?
|
是否同时删除这些帧中的遮罩?
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-2 text-xs leading-relaxed text-amber-100/70">
|
<p className="mt-2 text-xs leading-relaxed text-amber-100/70">
|
||||||
选择删除时会删除这些帧中的全部遮罩;选择保留时,这些人工/AI 标注帧会整帧保留,只清空其它自动传播帧。
|
{pendingClearManualFrameConfirm.manualFrameIncludeMode === 'target-masks'
|
||||||
|
? '选择删除时只会删除这些帧中本次选中或同传播链对应的遮罩;选择保留时,这些人工/AI 标注帧会整帧保留,只清空其它自动传播帧。'
|
||||||
|
: '选择删除时会删除这些帧中的全部遮罩;选择保留时,这些人工/AI 标注帧会整帧保留,只清空其它自动传播帧。'}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-5 grid grid-cols-3 gap-2">
|
<div className="mt-5 grid grid-cols-3 gap-2">
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user