功能增加: - 在工作区增加按范围传播和传播全部可达入口,支持选中区域或当前帧全部 mask 作为 seed,并按前后帧范围调用 SAM2 传播后刷新已保存标注。 - 在 AI 智能分割中接入框选提示,支持 box prompt 以及 box + 正/反向点的 interactive prompt 细化流程。 - 在 AI 智能分割中增加提示点删除、最近锚点删除、清空锚点、选中 AI 候选删除和 Delete/Backspace 快捷删除。 - 在项目库删除项目后同步清理当前项目、帧、mask 与选区状态,避免删除后工作区残留旧数据。 - 将时间进度条上的已编辑帧提示改为覆盖在进度条上的琥珀色竖线,并保留已编辑帧计数。 - 将 AI 参数文案调整为局部专注模式(自动裁剪无锚区域)和严格除杂模式(自动清理干涉点),仅改善可读性,不改变内部字段。 Bugfix: - 修复 AI 框选工具无实际 prompt 输出的问题。 - 修复多次执行 AI 高精度语义分割时旧候选 mask 叠加显示的问题,改为替换本页 AI 候选。 - 修复删除 AI 候选后选区仍引用已删除 mask 的状态残留。 - 修复进度条当前帧提示与已编辑帧提示颜色/语义混淆的问题,当前帧继续由播放进度和缩略图高亮表达。 测试与文档: - 补充 AI 分割框选、候选替换、提示点删除和快捷删除相关测试。 - 补充工作区传播范围、传播全部可达、编辑区域删除和项目删除状态清理测试。 - 更新 README、AGENTS 和 doc 下需求冻结、设计冻结、接口契约、前端审计、实施计划、测试计划,记录当前真实功能和测试覆盖。
96 lines
3.7 KiB
Python
96 lines
3.7 KiB
Python
from models import Annotation, Frame, Mask, ProcessingTask, Project
|
|
|
|
|
|
def test_project_crud_and_frames(client, monkeypatch):
|
|
monkeypatch.setattr("routers.projects.get_presigned_url", lambda key, expires=3600: f"http://storage/{key}")
|
|
|
|
created = client.post("/api/projects", json={
|
|
"name": "Demo",
|
|
"description": "desc",
|
|
"thumbnail_url": "thumb.jpg",
|
|
"parse_fps": 12,
|
|
})
|
|
assert created.status_code == 201
|
|
project_id = created.json()["id"]
|
|
|
|
frame = client.post(f"/api/projects/{project_id}/frames", json={
|
|
"project_id": project_id,
|
|
"frame_index": 0,
|
|
"image_url": "frames/0.jpg",
|
|
"width": 640,
|
|
"height": 360,
|
|
})
|
|
assert frame.status_code == 201
|
|
frame_id = frame.json()["id"]
|
|
|
|
listing = client.get("/api/projects")
|
|
assert listing.status_code == 200
|
|
assert listing.json()[0]["frame_count"] == 1
|
|
assert listing.json()[0]["thumbnail_url"] == "http://storage/thumb.jpg"
|
|
|
|
frames = client.get(f"/api/projects/{project_id}/frames")
|
|
assert frames.status_code == 200
|
|
assert frames.json()[0]["image_url"] == "http://storage/frames/0.jpg"
|
|
|
|
single_frame = client.get(f"/api/projects/{project_id}/frames/{frame_id}")
|
|
assert single_frame.status_code == 200
|
|
assert single_frame.json()["frame_index"] == 0
|
|
|
|
updated = client.patch(f"/api/projects/{project_id}", json={"name": "Renamed", "status": "ready"})
|
|
assert updated.status_code == 200
|
|
assert updated.json()["name"] == "Renamed"
|
|
assert updated.json()["status"] == "ready"
|
|
|
|
deleted = client.delete(f"/api/projects/{project_id}")
|
|
assert deleted.status_code == 204
|
|
assert client.get(f"/api/projects/{project_id}").status_code == 404
|
|
|
|
|
|
def test_delete_project_cascades_related_records(client, db_session):
|
|
created = client.post("/api/projects", json={"name": "Cascade Delete"})
|
|
assert created.status_code == 201
|
|
project_id = created.json()["id"]
|
|
|
|
frame = client.post(f"/api/projects/{project_id}/frames", json={
|
|
"project_id": project_id,
|
|
"frame_index": 0,
|
|
"image_url": "frames/0.jpg",
|
|
"width": 640,
|
|
"height": 360,
|
|
})
|
|
assert frame.status_code == 201
|
|
frame_id = frame.json()["id"]
|
|
|
|
annotation = client.post("/api/ai/annotate", json={
|
|
"project_id": project_id,
|
|
"frame_id": frame_id,
|
|
"mask_data": {"polygons": [[[0.1, 0.1], [0.2, 0.1], [0.2, 0.2]]]},
|
|
})
|
|
assert annotation.status_code == 201
|
|
annotation_id = annotation.json()["id"]
|
|
|
|
db_session.add(Mask(annotation_id=annotation_id, mask_url="masks/1.png"))
|
|
db_session.add(ProcessingTask(task_type="parse_video", project_id=project_id, status="queued"))
|
|
db_session.commit()
|
|
|
|
deleted = client.delete(f"/api/projects/{project_id}")
|
|
assert deleted.status_code == 204
|
|
assert db_session.query(Project).filter(Project.id == project_id).count() == 0
|
|
assert db_session.query(Frame).filter(Frame.project_id == project_id).count() == 0
|
|
assert db_session.query(Annotation).filter(Annotation.project_id == project_id).count() == 0
|
|
assert db_session.query(Mask).count() == 0
|
|
assert db_session.query(ProcessingTask).filter(ProcessingTask.project_id == project_id).count() == 0
|
|
|
|
|
|
def test_project_and_frame_404s(client):
|
|
assert client.get("/api/projects/999").status_code == 404
|
|
assert client.patch("/api/projects/999", json={"name": "x"}).status_code == 404
|
|
assert client.delete("/api/projects/999").status_code == 404
|
|
assert client.post("/api/projects/999/frames", json={
|
|
"project_id": 999,
|
|
"frame_index": 0,
|
|
"image_url": "missing.jpg",
|
|
}).status_code == 404
|
|
assert client.get("/api/projects/999/frames").status_code == 404
|
|
assert client.get("/api/projects/999/frames/1").status_code == 404
|