修复 Autocal 录像故障处理、复核回放与汇总统计
This commit is contained in:
@@ -104,6 +104,8 @@ finger_F001_<时间>_<标识>/
|
||||
`summary_export` 写 `derived/summary_*/` 与 `summary-latest.json`,按 `hold_id`(`reference.csv`
|
||||
数据行序号)区分重测,不自动标记稳定,不把 raw 与 N 混用。
|
||||
|
||||
见[录像工作流](recording-workflow.zh.md)。
|
||||
|
||||
### 多点会话格式
|
||||
|
||||
多点标定按测点组织(`sites/<site_id>/repeat-00N/`),复核输入见[多点标定会话格式](multipoint-data-format.zh.md)。
|
||||
|
||||
@@ -0,0 +1,611 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"diagram_type": "workflow",
|
||||
"meta": {
|
||||
"title": "Autocal 当前录像与复核流程",
|
||||
"locale": "zh-CN",
|
||||
"quality_profile": "showcase",
|
||||
"output": "apps/autocal/docs/diagrams/recording-20261017-110144/recording-workflow.html",
|
||||
"repository": {
|
||||
"url": "ssh://git@892887cvmk08.aicp.io:24928/orisys-tech/orisys-app.git",
|
||||
"revision": "19426c3c4a648ca7f4d3785e2e11b1bf124a8e31",
|
||||
"link_mode": "local-only"
|
||||
},
|
||||
"legend": {
|
||||
"entries": {
|
||||
"frontend": {
|
||||
"label": "操作界面"
|
||||
},
|
||||
"backend": {
|
||||
"label": "软件处理"
|
||||
},
|
||||
"database": {
|
||||
"label": "数据与文件"
|
||||
},
|
||||
"security": {
|
||||
"label": "身份与完整性"
|
||||
},
|
||||
"messagebus": {
|
||||
"label": "帧队列"
|
||||
},
|
||||
"external": {
|
||||
"label": "图像来源"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"lanes": [
|
||||
{
|
||||
"id": "operator",
|
||||
"label": "操作与试验状态"
|
||||
},
|
||||
{
|
||||
"id": "record",
|
||||
"label": "逐帧采集与后台写入"
|
||||
},
|
||||
{
|
||||
"id": "exceptions",
|
||||
"label": "异常证据与完整性",
|
||||
"variant": "exception"
|
||||
},
|
||||
{
|
||||
"id": "failed",
|
||||
"label": "已关闭的失败记录",
|
||||
"variant": "exception"
|
||||
},
|
||||
{
|
||||
"id": "review",
|
||||
"label": "离线复核与操作员汇总"
|
||||
}
|
||||
],
|
||||
"mainPath": [
|
||||
"start",
|
||||
"camera",
|
||||
"guard",
|
||||
"queue",
|
||||
"writer",
|
||||
"closing",
|
||||
"verdict"
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "start",
|
||||
"lane": "operator",
|
||||
"col": 0,
|
||||
"type": "frontend",
|
||||
"label": "开始本次试验",
|
||||
"sublabel": "固定测点与重复身份",
|
||||
"sources": [
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/multipoint.py",
|
||||
"line": 1025,
|
||||
"end_line": 1106,
|
||||
"label": "HEAD:登记 repeat"
|
||||
}
|
||||
],
|
||||
"width": 156
|
||||
},
|
||||
{
|
||||
"id": "holds",
|
||||
"lane": "operator",
|
||||
"col": 4,
|
||||
"type": "frontend",
|
||||
"label": "人工确认载荷",
|
||||
"sublabel": "保持窗口与事件时间",
|
||||
"sources": [
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/multipoint.py",
|
||||
"line": 707,
|
||||
"end_line": 734,
|
||||
"label": "HEAD:保持事件"
|
||||
}
|
||||
],
|
||||
"width": 156
|
||||
},
|
||||
{
|
||||
"id": "closing",
|
||||
"lane": "operator",
|
||||
"col": 5,
|
||||
"type": "backend",
|
||||
"label": "异步停止与排空",
|
||||
"sublabel": "移除 sink 后轮询收尾",
|
||||
"sources": [
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/multipoint.py",
|
||||
"line": 1146,
|
||||
"end_line": 1187,
|
||||
"label": "HEAD:异步收尾"
|
||||
}
|
||||
],
|
||||
"width": 156
|
||||
},
|
||||
{
|
||||
"id": "verdict",
|
||||
"lane": "exceptions",
|
||||
"col": 5,
|
||||
"type": "security",
|
||||
"label": "判定证据完整性",
|
||||
"sublabel": "report 与视频探测",
|
||||
"sources": [
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/capture_guard.py",
|
||||
"line": 149,
|
||||
"end_line": 225
|
||||
}
|
||||
],
|
||||
"width": 156
|
||||
},
|
||||
{
|
||||
"id": "camera",
|
||||
"lane": "record",
|
||||
"col": 0,
|
||||
"type": "external",
|
||||
"label": "相机输入画面",
|
||||
"sublabel": "帧号与主机采集时间",
|
||||
"sources": [
|
||||
{
|
||||
"path": "shared/src/orisys_app_shared/acquisition/finger_worker.py",
|
||||
"line": 945,
|
||||
"end_line": 1010
|
||||
}
|
||||
],
|
||||
"width": 156
|
||||
},
|
||||
{
|
||||
"id": "guard",
|
||||
"lane": "record",
|
||||
"col": 1,
|
||||
"type": "security",
|
||||
"label": "检查来源状态",
|
||||
"sublabel": "检测断流与来源重启",
|
||||
"sources": [
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/capture_guard.py",
|
||||
"line": 49,
|
||||
"end_line": 78
|
||||
},
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/multipoint.py",
|
||||
"line": 1288,
|
||||
"end_line": 1300,
|
||||
"label": "HEAD:来源健康检查"
|
||||
}
|
||||
],
|
||||
"width": 156
|
||||
},
|
||||
{
|
||||
"id": "queue",
|
||||
"lane": "record",
|
||||
"col": 2,
|
||||
"type": "messagebus",
|
||||
"label": "有界录像队列",
|
||||
"sublabel": "采集线程不等待编码",
|
||||
"sources": [
|
||||
{
|
||||
"path": "shared/src/orisys_app_shared/record/experiment.py",
|
||||
"line": 549,
|
||||
"end_line": 641
|
||||
}
|
||||
],
|
||||
"width": 156
|
||||
},
|
||||
{
|
||||
"id": "writer",
|
||||
"lane": "record",
|
||||
"col": 3,
|
||||
"type": "backend",
|
||||
"label": "后台写入视频",
|
||||
"sublabel": "MP4 与逐帧 CSV",
|
||||
"sources": [
|
||||
{
|
||||
"path": "shared/src/orisys_app_shared/record/experiment.py",
|
||||
"line": 737,
|
||||
"end_line": 828
|
||||
}
|
||||
],
|
||||
"width": 156
|
||||
},
|
||||
{
|
||||
"id": "raw",
|
||||
"lane": "record",
|
||||
"col": 4,
|
||||
"type": "database",
|
||||
"label": "原始试验目录",
|
||||
"sublabel": "视频、力、参考与事件",
|
||||
"sources": [
|
||||
{
|
||||
"path": "shared/src/orisys_app_shared/record/experiment.py",
|
||||
"line": 1,
|
||||
"end_line": 24
|
||||
},
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/multipoint.py",
|
||||
"line": 1089,
|
||||
"end_line": 1106,
|
||||
"label": "HEAD:reference/events"
|
||||
}
|
||||
],
|
||||
"width": 156
|
||||
},
|
||||
{
|
||||
"id": "fault",
|
||||
"lane": "exceptions",
|
||||
"col": 3,
|
||||
"type": "security",
|
||||
"label": "中止当前试验",
|
||||
"sublabel": "断流、重启或写入失败",
|
||||
"sources": [
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/multipoint.py",
|
||||
"line": 806,
|
||||
"end_line": 830,
|
||||
"label": "HEAD:中止路径"
|
||||
},
|
||||
{
|
||||
"path": "shared/src/orisys_app_shared/record/experiment.py",
|
||||
"line": 897,
|
||||
"end_line": 921,
|
||||
"label": "写线程失败状态"
|
||||
}
|
||||
],
|
||||
"width": 156
|
||||
},
|
||||
{
|
||||
"id": "incomplete",
|
||||
"lane": "failed",
|
||||
"col": 5,
|
||||
"type": "database",
|
||||
"label": "保留不完整证据",
|
||||
"sublabel": "记录原因,收尾后可诊断",
|
||||
"sources": [
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/capture_guard.py",
|
||||
"line": 194,
|
||||
"end_line": 225
|
||||
},
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/multipoint.py",
|
||||
"line": 1231,
|
||||
"end_line": 1262,
|
||||
"label": "HEAD:保留失败记录"
|
||||
}
|
||||
],
|
||||
"width": 156
|
||||
},
|
||||
{
|
||||
"id": "reopen",
|
||||
"lane": "review",
|
||||
"col": 0,
|
||||
"type": "frontend",
|
||||
"label": "打开标定记录",
|
||||
"sublabel": "成功与已关闭失败记录",
|
||||
"sources": [
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/session_review.py",
|
||||
"line": 408,
|
||||
"end_line": 435
|
||||
}
|
||||
],
|
||||
"width": 156
|
||||
},
|
||||
{
|
||||
"id": "frames",
|
||||
"lane": "review",
|
||||
"col": 1,
|
||||
"type": "backend",
|
||||
"label": "按采集时间定位",
|
||||
"sublabel": "帧身份与分段位置关联",
|
||||
"sources": [
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/video_review.py",
|
||||
"line": 126,
|
||||
"end_line": 187,
|
||||
"label": "HEAD:索引定位"
|
||||
}
|
||||
],
|
||||
"width": 156
|
||||
},
|
||||
{
|
||||
"id": "selection",
|
||||
"lane": "review",
|
||||
"col": 2,
|
||||
"type": "frontend",
|
||||
"label": "检查与选择区间",
|
||||
"sublabel": "对应画面与原始力曲线",
|
||||
"sources": [
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/session_review.py",
|
||||
"line": 310,
|
||||
"end_line": 366
|
||||
}
|
||||
],
|
||||
"width": 156
|
||||
},
|
||||
{
|
||||
"id": "save",
|
||||
"lane": "review",
|
||||
"col": 3,
|
||||
"type": "database",
|
||||
"label": "保存复核快照",
|
||||
"sublabel": "新版本,不覆盖采集文件",
|
||||
"sources": [
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/session_review.py",
|
||||
"line": 598,
|
||||
"end_line": 622
|
||||
}
|
||||
],
|
||||
"width": 156
|
||||
},
|
||||
{
|
||||
"id": "summary",
|
||||
"lane": "review",
|
||||
"col": 4,
|
||||
"type": "backend",
|
||||
"label": "计算窗口与灵敏度",
|
||||
"sublabel": "候选与确认分别统计",
|
||||
"sources": [
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/summary.py",
|
||||
"line": 447,
|
||||
"end_line": 500,
|
||||
"label": "HEAD:分集合拟合"
|
||||
}
|
||||
],
|
||||
"width": 156
|
||||
},
|
||||
{
|
||||
"id": "publish",
|
||||
"lane": "review",
|
||||
"col": 5,
|
||||
"type": "database",
|
||||
"label": "发布操作员汇总",
|
||||
"sublabel": "报告、两表与最新指针",
|
||||
"sources": [
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/summary_export.py",
|
||||
"line": 308,
|
||||
"end_line": 366
|
||||
}
|
||||
],
|
||||
"width": 156
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "begin-capture",
|
||||
"from": "start",
|
||||
"to": "camera",
|
||||
"variant": "emphasis"
|
||||
},
|
||||
{
|
||||
"id": "begin-holds",
|
||||
"from": "start",
|
||||
"to": "holds",
|
||||
"label": "逐档确认",
|
||||
"role": "branch"
|
||||
},
|
||||
{
|
||||
"id": "capture-guard",
|
||||
"from": "camera",
|
||||
"to": "guard",
|
||||
"variant": "emphasis"
|
||||
},
|
||||
{
|
||||
"id": "guard-queue",
|
||||
"from": "guard",
|
||||
"to": "queue",
|
||||
"variant": "emphasis"
|
||||
},
|
||||
{
|
||||
"id": "queue-write",
|
||||
"from": "queue",
|
||||
"to": "writer",
|
||||
"variant": "emphasis",
|
||||
"role": "async"
|
||||
},
|
||||
{
|
||||
"id": "write-files",
|
||||
"from": "writer",
|
||||
"to": "raw",
|
||||
"label": "写盘"
|
||||
},
|
||||
{
|
||||
"id": "holds-files",
|
||||
"from": "holds",
|
||||
"to": "raw",
|
||||
"label": "参考与事件",
|
||||
"role": "branch",
|
||||
"variant": "dashed"
|
||||
},
|
||||
{
|
||||
"id": "holds-close",
|
||||
"from": "holds",
|
||||
"to": "closing",
|
||||
"label": "完成或中止",
|
||||
"fromSide": "top",
|
||||
"toSide": "top",
|
||||
"via": [
|
||||
[
|
||||
872,
|
||||
65
|
||||
],
|
||||
[
|
||||
1056,
|
||||
65
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "write-close",
|
||||
"from": "writer",
|
||||
"to": "closing",
|
||||
"label": "结束后收尾",
|
||||
"role": "main"
|
||||
},
|
||||
{
|
||||
"id": "close-verdict",
|
||||
"from": "closing",
|
||||
"to": "verdict",
|
||||
"label": "writer 终态",
|
||||
"variant": "emphasis",
|
||||
"fromSide": "right",
|
||||
"toSide": "top",
|
||||
"via": [
|
||||
[
|
||||
1204,
|
||||
119
|
||||
],
|
||||
[
|
||||
1204,
|
||||
315
|
||||
],
|
||||
[
|
||||
1056,
|
||||
315
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "write-fault",
|
||||
"from": "writer",
|
||||
"to": "fault",
|
||||
"label": "异常",
|
||||
"role": "error",
|
||||
"variant": "security"
|
||||
},
|
||||
{
|
||||
"id": "fault-close",
|
||||
"from": "fault",
|
||||
"to": "closing",
|
||||
"label": "停止后排空",
|
||||
"role": "error",
|
||||
"variant": "security",
|
||||
"fromSide": "right",
|
||||
"toSide": "bottom",
|
||||
"via": [
|
||||
[
|
||||
968,
|
||||
367
|
||||
],
|
||||
[
|
||||
968,
|
||||
175
|
||||
],
|
||||
[
|
||||
1056,
|
||||
175
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "verdict-failed",
|
||||
"from": "verdict",
|
||||
"to": "incomplete",
|
||||
"label": "未通过",
|
||||
"role": "error",
|
||||
"variant": "security"
|
||||
},
|
||||
{
|
||||
"id": "verdict-review",
|
||||
"from": "verdict",
|
||||
"to": "reopen",
|
||||
"label": "收尾后复核",
|
||||
"role": "return"
|
||||
},
|
||||
{
|
||||
"id": "reopen-frames",
|
||||
"from": "reopen",
|
||||
"to": "frames",
|
||||
"variant": "emphasis"
|
||||
},
|
||||
{
|
||||
"id": "frames-select",
|
||||
"from": "frames",
|
||||
"to": "selection",
|
||||
"variant": "emphasis"
|
||||
},
|
||||
{
|
||||
"id": "select-save",
|
||||
"from": "selection",
|
||||
"to": "save",
|
||||
"variant": "emphasis"
|
||||
},
|
||||
{
|
||||
"id": "save-summary",
|
||||
"from": "save",
|
||||
"to": "summary",
|
||||
"variant": "emphasis"
|
||||
},
|
||||
{
|
||||
"id": "summary-publish",
|
||||
"from": "summary",
|
||||
"to": "publish",
|
||||
"variant": "emphasis"
|
||||
},
|
||||
{
|
||||
"id": "source-fault",
|
||||
"from": "guard",
|
||||
"to": "fault",
|
||||
"label": "来源异常",
|
||||
"role": "error",
|
||||
"variant": "security",
|
||||
"fromSide": "bottom",
|
||||
"toSide": "left",
|
||||
"via": [
|
||||
[
|
||||
310,
|
||||
367
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "failed-review",
|
||||
"from": "incomplete",
|
||||
"to": "reopen",
|
||||
"label": "诊断回放",
|
||||
"role": "branch",
|
||||
"variant": "dashed",
|
||||
"fromSide": "left",
|
||||
"toSide": "left",
|
||||
"via": [
|
||||
[
|
||||
26,
|
||||
491
|
||||
],
|
||||
[
|
||||
26,
|
||||
615
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"cards": [
|
||||
{
|
||||
"dot": "cyan",
|
||||
"title": "每次试验的一条证据链",
|
||||
"items": [
|
||||
"连续保留零载、换档、加载与卸载;保持段不拆视频。",
|
||||
"单次试验写 trials;多点试验写 sites/repeat/capture。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dot": "emerald",
|
||||
"title": "复核与汇总",
|
||||
"items": [
|
||||
"用主机采集时间关联窗口,用帧索引定位视频。",
|
||||
"回放仅解码所选区间;不累积保存所有图像。",
|
||||
"结束整场可先发布候选汇总,保存复核后发布新版本。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dot": "amber",
|
||||
"title": "版本与验证边界",
|
||||
"items": [
|
||||
"当前图包含五项未提交修复;源码链接对应 HEAD 基线的模块归属。",
|
||||
"工作树补丁与文件哈希见同目录 snapshot.json。",
|
||||
"录像支持视觉复核;不代表独立精度验证或 FT 物理同步。"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"receiptId": "5aac4630-c1c0-45fa-9b13-87c930df7706",
|
||||
"status": "current",
|
||||
"command": "deliver",
|
||||
"type": "workflow",
|
||||
"input": "D:\\orisys_ws\\orisys-app\\apps\\autocal\\docs\\diagrams\\recording-20261017-110144\\candidate.json",
|
||||
"output": "D:\\orisys_ws\\orisys-app\\apps\\autocal\\docs\\diagrams\\recording-20261017-110144\\recording-workflow.html",
|
||||
"specification": {
|
||||
"sha256": "6778c4a565b36a780a741a791aa59580682eb6c3d61092b1da110319faff96c1",
|
||||
"bytes": 14323
|
||||
},
|
||||
"artifact": {
|
||||
"sha256": "33f8f732ff5f7540d55690a3a8f7a2115b247ade43f3918bdd0654bc87f90e91",
|
||||
"bytes": 779934
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"format_version": "autocal-workflow-snapshot-1.0",
|
||||
"baseline_revision": "19426c3c4a648ca7f4d3785e2e11b1bf124a8e31",
|
||||
"source_links": "HEAD module evidence; current fixes are described by working-tree.patch and file hashes",
|
||||
"patch": "working-tree.patch",
|
||||
"patch_sha256": "dce358a6882f44e02069ebcae65707503018958d9884aab534c8702402bc36db",
|
||||
"candidate_sha256": "6778c4a565b36a780a741a791aa59580682eb6c3d61092b1da110319faff96c1",
|
||||
"html_sha256": "33f8f732ff5f7540d55690a3a8f7a2115b247ade43f3918bdd0654bc87f90e91",
|
||||
"files": [
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/capture_guard.py",
|
||||
"head_sha256": "3db256e207cff524074b80b1725fa9f3551b63ce56de4601d7a0a6a56b0668f1",
|
||||
"worktree_sha256": "3db256e207cff524074b80b1725fa9f3551b63ce56de4601d7a0a6a56b0668f1"
|
||||
},
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/multipoint.py",
|
||||
"head_sha256": "345eeaa41d1e12b3fa237f8eea239d9955716b20b23f1ba2d5bbb4e49d39968b",
|
||||
"worktree_sha256": "fad7e9bbdc41472806972be45eea346f7d14eb72fb2f2f7c74d7e07108586ebb"
|
||||
},
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/session_review.py",
|
||||
"head_sha256": "61d01756593629c7b6282dbaf3adbd043a94b4b7a30468c52e61e55cb778fc3e",
|
||||
"worktree_sha256": "21619b4185abcb15cbb6e508acfcbcc63b618f590d4107cbbc687fc344096318"
|
||||
},
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/summary.py",
|
||||
"head_sha256": "84fd0ff5f6c3465f0d4a3526178a37c32b64304e62cdd10cbc88f0c52e5202f2",
|
||||
"worktree_sha256": "d15e7f00e944305f730075fd2e302cda4e44b0aed14bebc6b0d473c4c5ec0f2f"
|
||||
},
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/summary_export.py",
|
||||
"head_sha256": "a49ac5d0c55eed301d413af7b0b35d46fc4ba9bec9bf0a05fb421831b5102e9f",
|
||||
"worktree_sha256": "a49ac5d0c55eed301d413af7b0b35d46fc4ba9bec9bf0a05fb421831b5102e9f"
|
||||
},
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/ui/record_overview_dialog.py",
|
||||
"head_sha256": "5e4b204cc1304ac3d9235d2c4d0f287a45b86c3fc3c24dc6ad141a958a4c366a",
|
||||
"worktree_sha256": "472dc129a68fad5ec40673185a9ce7549a7310b817b20a5636379ded4e8d57e6"
|
||||
},
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/ui/session_review_dialog.py",
|
||||
"head_sha256": "b9fe09f64abc5f508a2f47c877faee3e2ec7827dd1719dac77aeba685a12d408",
|
||||
"worktree_sha256": "fe50e1b3b9fc1edb1b64c36fc44997416db818c10363341021598d46864f8640"
|
||||
},
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/ui/video_review_dialog.py",
|
||||
"head_sha256": "96d363c5fcfee9fc11367273ddcf36f2730595d3c957e62d91fe30946ce94ce9",
|
||||
"worktree_sha256": "f03648ac69197e9bb5f91a425e8d8b376f568396f3ce81f6d90007a6c092ace9"
|
||||
},
|
||||
{
|
||||
"path": "apps/autocal/src/orisys_autocal/video_review.py",
|
||||
"head_sha256": "44233205b41a075f8abd89f4a1f01c6079dd092d739d1842bc66cde19e2b65e1",
|
||||
"worktree_sha256": "18992c25777afd8a241545693c63d91b321de7c3cb08690b606fcc54fb6d192a"
|
||||
},
|
||||
{
|
||||
"path": "shared/src/orisys_app_shared/acquisition/finger_worker.py",
|
||||
"head_sha256": "a9bff46aef6191a99efdc019aff4d4a4908c2a39e9bfcb0c4dc29ec6a6909a33",
|
||||
"worktree_sha256": "db9e59c728fef241e5fdfde6cdf683c70c9785efea1dc2139c0080695ecd110b"
|
||||
},
|
||||
{
|
||||
"path": "shared/src/orisys_app_shared/record/experiment.py",
|
||||
"head_sha256": "3f0c6fba0cbfd711d4b9db8a62aa325887073ef08b29f7561d24de27e9a666a2",
|
||||
"worktree_sha256": "405abe333319a8f0b9aeb6b60a15e2d7cedbfdcec9a84d82a3685c4ec23008ed"
|
||||
}
|
||||
],
|
||||
"verification": {
|
||||
"regression_before": "5 failed",
|
||||
"focused_after": "45 passed",
|
||||
"autocal_after": "572 passed; 0 skipped",
|
||||
"lint": "pass",
|
||||
"archify_gates": {
|
||||
"validate": "pass",
|
||||
"deliver": "pass",
|
||||
"check": "pass",
|
||||
"browser-check": "pass"
|
||||
},
|
||||
"visual_review": "light and dark screenshots inspected"
|
||||
},
|
||||
"scope": "software only; no physical camera, controller or load operation"
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
diff --git a/apps/autocal/src/orisys_autocal/multipoint.py b/apps/autocal/src/orisys_autocal/multipoint.py
|
||||
index c0bfc92..e7461f4 100644
|
||||
--- a/apps/autocal/src/orisys_autocal/multipoint.py
|
||||
+++ b/apps/autocal/src/orisys_autocal/multipoint.py
|
||||
@@ -1298,6 +1298,10 @@ class Campaign:
|
||||
# called complete evidence.
|
||||
self._source_stopped = True
|
||||
self.stop("the image source stopped")
|
||||
+ elif self._recorder is not None and (
|
||||
+ not self._recorder.is_recording or self._recorder.stats().write_errors
|
||||
+ ):
|
||||
+ self.stop("the recording writer stopped or failed")
|
||||
if self.capture_state == "closing":
|
||||
self._finalise_close()
|
||||
return {
|
||||
@@ -1329,6 +1333,9 @@ class Campaign:
|
||||
self.capture_state == "recording"
|
||||
and self._guard is not None
|
||||
and not self._guard.failed
|
||||
+ and self._recorder is not None
|
||||
+ and self._recorder.is_recording
|
||||
+ and not self._recorder.stats().write_errors
|
||||
)
|
||||
|
||||
def capture_summary(self) -> dict:
|
||||
diff --git a/apps/autocal/src/orisys_autocal/summary.py b/apps/autocal/src/orisys_autocal/summary.py
|
||||
index e282a78..d8c5105 100644
|
||||
--- a/apps/autocal/src/orisys_autocal/summary.py
|
||||
+++ b/apps/autocal/src/orisys_autocal/summary.py
|
||||
@@ -502,18 +502,19 @@ def build_sensitivity(windows: list[dict]) -> list[dict]:
|
||||
|
||||
def aggregate_sensitivity(rows: list[dict]) -> list[dict]:
|
||||
"""Mean and sample deviation of the per-trial slopes. Not a joint fit."""
|
||||
- groups: dict[tuple[str, str], list[dict]] = {}
|
||||
+ groups: dict[tuple[str, str, str], list[dict]] = {}
|
||||
for row in rows:
|
||||
if row["status"] != "available" or row["slope_raw_per_N"] is None:
|
||||
continue
|
||||
- groups.setdefault((row["site_id"], row["component"]), []).append(row)
|
||||
+ groups.setdefault((row["site_id"], row["component"], row["review_status"]), []).append(row)
|
||||
result = []
|
||||
- for (site_id, component), group in sorted(groups.items()):
|
||||
+ for (site_id, component, review_status), group in sorted(groups.items()):
|
||||
slopes = [float(row["slope_raw_per_N"]) for row in group]
|
||||
result.append({
|
||||
"site_id": site_id,
|
||||
"component": component,
|
||||
- "trials": len(slopes),
|
||||
+ "review_status": review_status,
|
||||
+ "trials": len({row["trial_id"] for row in group}),
|
||||
"slope_mean_raw_per_N": float(np.mean(slopes)),
|
||||
"slope_std_raw_per_N": float(np.std(slopes, ddof=1)) if len(slopes) >= 2 else None,
|
||||
"window_count": sum(int(row["window_count"] or 0) for row in group),
|
||||
diff --git a/apps/autocal/src/orisys_autocal/ui/record_overview_dialog.py b/apps/autocal/src/orisys_autocal/ui/record_overview_dialog.py
|
||||
index 23c9b74..e6f7c12 100644
|
||||
--- a/apps/autocal/src/orisys_autocal/ui/record_overview_dialog.py
|
||||
+++ b/apps/autocal/src/orisys_autocal/ui/record_overview_dialog.py
|
||||
@@ -551,6 +551,7 @@ class RecordOverviewDialog(QDialog):
|
||||
|
||||
def _video(self):
|
||||
"""Open the pictures of the selected point's current window."""
|
||||
+ from ..video_review import trial_is_closed
|
||||
from .video_review_dialog import VideoReviewDialog
|
||||
|
||||
index = self.table.currentRow()
|
||||
@@ -571,8 +572,7 @@ class RecordOverviewDialog(QDialog):
|
||||
(item for item in self.review.get("trials") or []
|
||||
if item["trial_id"] == window["trial_id"]), None,
|
||||
)
|
||||
- if trial is None or trial.get("status") != "complete":
|
||||
- # An unclosed trial still has the writer's file open.
|
||||
+ if not trial_is_closed(self.directory, trial):
|
||||
self.notice.setText(t("overview.guide_incomplete"))
|
||||
return None
|
||||
dialog = VideoReviewDialog(
|
||||
diff --git a/apps/autocal/src/orisys_autocal/ui/session_review_dialog.py b/apps/autocal/src/orisys_autocal/ui/session_review_dialog.py
|
||||
index 6992bac..5a25696 100644
|
||||
--- a/apps/autocal/src/orisys_autocal/ui/session_review_dialog.py
|
||||
+++ b/apps/autocal/src/orisys_autocal/ui/session_review_dialog.py
|
||||
@@ -14,6 +14,7 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from ..session_review import review_session, save_review, load_review, reselect_review
|
||||
+from ..video_review import trial_is_closed
|
||||
from .review_job import ReviewJob
|
||||
from .chart_helpers import add_value_axes, clear_chart
|
||||
|
||||
@@ -231,8 +232,7 @@ class SessionReviewDialog(QDialog):
|
||||
(item for item in self.review.get("trials") or []
|
||||
if item["trial_id"] == window["trial_id"]), None,
|
||||
)
|
||||
- if trial is None or trial.get("status") != "complete":
|
||||
- # Only a closed trial has a finished container to decode.
|
||||
+ if not trial_is_closed(self.directory, trial):
|
||||
self.message.setText(t("overview.guide_incomplete"))
|
||||
return None
|
||||
from .video_review_dialog import VideoReviewDialog
|
||||
diff --git a/apps/autocal/src/orisys_autocal/ui/video_review_dialog.py b/apps/autocal/src/orisys_autocal/ui/video_review_dialog.py
|
||||
index fbf2547..cfe2242 100644
|
||||
--- a/apps/autocal/src/orisys_autocal/ui/video_review_dialog.py
|
||||
+++ b/apps/autocal/src/orisys_autocal/ui/video_review_dialog.py
|
||||
@@ -293,13 +293,18 @@ class VideoReviewDialog(QDialog):
|
||||
window = self.window
|
||||
if not window:
|
||||
return
|
||||
- origin = int(window["hold_start_host_mono_ns"])
|
||||
+ origin = int(window["t_start_host_mono_ns"])
|
||||
+ duration = max((int(window["t_end_host_mono_ns"]) - origin) / 1e9, 0.001)
|
||||
+ selected_trace = [
|
||||
+ (stamp, value) for stamp, _frame, value in window.get("trace") or []
|
||||
+ if origin <= stamp < int(window["t_end_host_mono_ns"])
|
||||
+ ]
|
||||
trace = QScatterSeries()
|
||||
trace.setMarkerSize(3)
|
||||
trace.setName(t("review.valid_samples"))
|
||||
trace.replace([
|
||||
QPointF((stamp - origin) / 1e9, value)
|
||||
- for stamp, _frame, value in window.get("trace") or []
|
||||
+ for stamp, value in selected_trace
|
||||
])
|
||||
self.curve.addSeries(trace)
|
||||
self.curve_line = QLineSeries()
|
||||
@@ -308,15 +313,15 @@ class VideoReviewDialog(QDialog):
|
||||
reference = QLineSeries()
|
||||
reference.setName(t("video.reference"))
|
||||
reference.append(0.0, float(window.get("reference_n") or 0.0))
|
||||
- reference.append(max((window["hold_end_host_mono_ns"] - origin) / 1e9, 0.001),
|
||||
+ reference.append(duration,
|
||||
float(window.get("reference_n") or 0.0))
|
||||
self.curve.addSeries(reference)
|
||||
axis_x = QValueAxis()
|
||||
axis_x.setTitleText(t("review.hold_time_axis"))
|
||||
- axis_x.setRange(0, max((window["hold_end_host_mono_ns"] - origin) / 1e9, 0.001))
|
||||
+ axis_x.setRange(0, duration)
|
||||
axis_y = QValueAxis()
|
||||
axis_y.setTitleText(t("review.raw_axis"))
|
||||
- values = [value for _stamp, _frame, value in window.get("trace") or []]
|
||||
+ values = [value for _stamp, value in selected_trace]
|
||||
values.append(float(window.get("reference_n") or 0.0))
|
||||
low, high = (min(values), max(values)) if values else (0.0, 1.0)
|
||||
margin = max((high - low) * 0.1, 0.1)
|
||||
@@ -335,7 +340,7 @@ class VideoReviewDialog(QDialog):
|
||||
return
|
||||
window = self.window
|
||||
frame = self.video.frames[index]
|
||||
- origin = int(window["hold_start_host_mono_ns"])
|
||||
+ origin = int(window["t_start_host_mono_ns"])
|
||||
x = (frame.t_host_mono_ns - origin) / 1e9
|
||||
axis = self.curve.axes(Qt.Orientation.Vertical)
|
||||
low, high = (axis[0].min(), axis[0].max()) if axis else (0.0, 1.0)
|
||||
diff --git a/apps/autocal/src/orisys_autocal/video_review.py b/apps/autocal/src/orisys_autocal/video_review.py
|
||||
index 4391a74..ad15f97 100644
|
||||
--- a/apps/autocal/src/orisys_autocal/video_review.py
|
||||
+++ b/apps/autocal/src/orisys_autocal/video_review.py
|
||||
@@ -11,7 +11,8 @@ segment stays a reason. The decoder is released by :meth:`WindowVideo.close`.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
-from dataclasses import dataclass, field
|
||||
+import json
|
||||
+from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
@@ -79,6 +80,24 @@ def _trial(review: dict, trial_id: str) -> dict | None:
|
||||
return None
|
||||
|
||||
|
||||
+def trial_is_closed(root: Path, trial: dict | None) -> bool:
|
||||
+ """Closed failed evidence is replayable; a live writer's container is not."""
|
||||
+ if trial is None or trial.get("capture_status") in {"idle", "starting", "recording", "closing"}:
|
||||
+ return False
|
||||
+ if (trial.get("capture_report") or {}).get("completed") is True:
|
||||
+ return True
|
||||
+ if trial.get("status") == "complete":
|
||||
+ return True
|
||||
+ directory = resolve_capture_directory(root, trial_directory=str(trial.get("directory") or ""))
|
||||
+ if directory is None:
|
||||
+ return False
|
||||
+ try:
|
||||
+ metadata = json.loads((directory / "session.json").read_text(encoding="utf-8"))
|
||||
+ except (OSError, ValueError):
|
||||
+ return False
|
||||
+ return bool(metadata.get("ended_at"))
|
||||
+
|
||||
+
|
||||
def in_window_frames(
|
||||
review: dict, *, trial_id: str, step_index: int
|
||||
) -> list[FrameRow] | str:
|
||||
@@ -97,8 +116,8 @@ def in_window_frames(
|
||||
)
|
||||
if directory is None:
|
||||
return NO_FRAME_SOURCE
|
||||
- start = int(window["hold_start_host_mono_ns"])
|
||||
- end = int(window["hold_end_host_mono_ns"])
|
||||
+ start = int(window.get("t_start_host_mono_ns", window["hold_start_host_mono_ns"]))
|
||||
+ end = int(window.get("t_end_host_mono_ns", window["hold_end_host_mono_ns"]))
|
||||
unique, _duplicated = unique_frames(read_frame_index(directory))
|
||||
frames = [
|
||||
frame for frame in unique.values()
|
||||
@@ -205,7 +224,6 @@ class WindowVideo:
|
||||
frames: list[FrameRow]
|
||||
#: Index of the frame currently decoded, or -1 when the decoder was reset.
|
||||
position: int = -1
|
||||
- _images: dict[int, np.ndarray] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def frame_count(self) -> int:
|
||||
@@ -214,8 +232,6 @@ class WindowVideo:
|
||||
def frame(self, index: int) -> np.ndarray | None:
|
||||
"""Image for ``frames[index]``, or ``None`` when that frame has no picture."""
|
||||
index = max(0, min(int(index), self.frame_count - 1))
|
||||
- if index in self._images:
|
||||
- return self._images[index]
|
||||
row = self.frames[index]
|
||||
try:
|
||||
image, served = self.reader.video_frame_at(row.t_rel_s)
|
||||
@@ -231,12 +247,10 @@ class WindowVideo:
|
||||
self.position = -1
|
||||
return None
|
||||
self.position = index
|
||||
- self._images[index] = image
|
||||
return image
|
||||
|
||||
def release(self) -> None:
|
||||
self.reader.close()
|
||||
- self._images.clear()
|
||||
self.position = -1
|
||||
|
||||
def close(self) -> None:
|
||||
@@ -84,7 +84,9 @@ idle → starting → recording → closing → complete
|
||||
|
||||
`sensitivity_values.csv` 每点每 trial 每轴每种审核集合一行;纵向结构允许缺轴。含身份、`component,review_status,method,slope_raw_per_N,intercept_raw,R2,rmse_raw,endpoint_raw_per_N,initial_step_raw,reference_min_N,reference_max_N,window_count,status,reason`。导出保持双精度,不沿用旧表六位有效数字作为计算输入。
|
||||
|
||||
方法固定为 `raw_on_reference_ols_v1`:以实际参考 N 为 x,原始窗口均值为 y,在同一 trial 内用初始零载和加载窗口等权拟合 `y=a*x+b`。卸载单独展示,不进入该拟合。至少两个不同参考力,否则 unavailable;不跨 trial 混合不同零点。报告按点汇总合格 trial 斜率的均值、样本标准差和数量,不把它冒充新的联合拟合,不平均 R²。
|
||||
方法固定为 `raw_on_reference_ols_v1`:以实际参考 N 为 x,原始窗口均值为 y,在同一 trial 内用初始零载和加载窗口等权拟合 `y=a*x+b`。卸载单独展示,不进入该拟合。至少两个不同参考力,否则 unavailable;不跨 trial 混合不同零点。
|
||||
|
||||
`sensitivity_summary` 按测点、分量和 `review_status` 分组,分别汇总候选/已确认 trial 斜率的均值与样本标准差,`trials` 统计唯一 trial_id;不混合两种审核集合、不计算联合拟合或平均 R²。
|
||||
|
||||
R² 为 `1-SSE/SST`;常量信号 SST=0 时为空并标记 flat_signal,零斜率不反推增益。endpoint 每 trial 用最高加载与初始零载的差除以参考力差;initial_step 为最低正加载均值减初始零载。跨 trial 只汇总这些量的均值及数量,缺配对或分母为零为空;不跨 trial 拼端点。
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Autocal 当前录像工作流
|
||||
|
||||
状态:2026-10-17,软件实现说明,待实机验收。打开[交互工作流图](diagrams/recording-20261017-110144/recording-workflow.html)查看采集、异常收尾、视频复核和汇总发布;支持浅色/深色、放大及图片导出。
|
||||
|
||||
图中原有模块归属的源码引用固定在 `19426c3c4a648ca7f4d3785e2e11b1bf124a8e31`。本次五项修复尚未提交:写线程失败时中止、候选与确认结果分别汇总、按所选区间回放、允许已关闭失败记录诊断,以及取消全部解码图像的累积缓存。当前实现以[工作树快照清单](diagrams/recording-20261017-110144/snapshot.json)及其同目录补丁为依据,不将工作树字节冒充 HEAD 源码。
|
||||
|
||||
每 trial 连续保存零载、换档、加载和卸载画面,MP4 为有损输入录像。保持窗口依靠主机采集时间与帧身份关联,不能用容器 FPS 推算实验同步。操作员手动确认参考载荷;该流程不自动加载,也不证明 FT 物理同步。
|
||||
|
||||
原始文件继续使用[单次](calibration-data-format.zh.md)与[多点](multipoint-data-format.zh.md)目录。保持段不拆成新视频,复核与汇总另存版本;字段及计算方法见[汇总契约](recording-summary-contract.zh.md)。
|
||||
|
||||
验证:五个回归用例在修复前失败;修复后的相关 45 项测试和 Autocal 全量 572 项测试通过。Archify 的 showcase 校验、产物检查及真实浏览器检查通过,并检查了截图。未操作真实相机、平台或载荷。
|
||||
Reference in New Issue
Block a user