@windsland52/maafw-live 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -2
  3. package/bin/maafw-live.mjs +25 -0
  4. package/docs/daemon-protocol.md +121 -0
  5. package/docs/keyframe-retention-contract-v0.md +225 -0
  6. package/docs/session-unify-v0.md +115 -0
  7. package/lib/client/daemon.js +276 -0
  8. package/lib/commands/annotate.js +179 -0
  9. package/lib/commands/calibrate.js +61 -0
  10. package/lib/commands/color.js +73 -0
  11. package/lib/commands/crop.js +228 -0
  12. package/lib/commands/dispatch.js +121 -0
  13. package/lib/commands/env.js +409 -0
  14. package/lib/commands/index.js +70 -0
  15. package/lib/commands/input.js +257 -0
  16. package/lib/commands/kf.js +167 -0
  17. package/lib/commands/probe.js +70 -0
  18. package/lib/commands/reco.js +440 -0
  19. package/lib/commands/repl.js +1501 -0
  20. package/lib/commands/roi-check.js +273 -0
  21. package/lib/commands/roi-compare.js +388 -0
  22. package/lib/commands/runtime.js +718 -0
  23. package/lib/commands/skill.js +169 -0
  24. package/lib/commands/timing.js +247 -0
  25. package/lib/commands/version.js +66 -0
  26. package/lib/daemon/framed.mjs +3680 -0
  27. package/lib/daemon/reco_child.mjs +287 -0
  28. package/lib/exec.js +96 -0
  29. package/lib/flags.js +44 -0
  30. package/lib/image.js +90 -0
  31. package/lib/index.js +143 -0
  32. package/lib/interface/hotkey.js +142 -0
  33. package/lib/interface/index.js +12 -0
  34. package/lib/interface/jsonc.js +95 -0
  35. package/lib/interface/load.js +397 -0
  36. package/lib/interface/override.js +287 -0
  37. package/lib/interface/plan.js +279 -0
  38. package/lib/option-args.js +29 -0
  39. package/lib/pkg.js +54 -0
  40. package/lib/protocol.js +94 -0
  41. package/lib/runtime/actions.js +225 -0
  42. package/lib/runtime/keyframes.js +73 -0
  43. package/lib/runtime/log-watch.js +160 -0
  44. package/lib/runtime/node-source.js +100 -0
  45. package/lib/runtime/ocr-model.js +45 -0
  46. package/lib/runtime/pi-env.js +34 -0
  47. package/lib/runtime/pretask.js +217 -0
  48. package/lib/runtime/prune.js +28 -0
  49. package/lib/runtime/receipts.js +57 -0
  50. package/lib/runtime/reco-args.js +38 -0
  51. package/lib/runtime/reco-verdict.js +82 -0
  52. package/lib/runtime/roi-compare.js +198 -0
  53. package/lib/runtime/session-forward.js +79 -0
  54. package/lib/runtime/session-ipc.js +471 -0
  55. package/lib/runtime/session.js +135 -0
  56. package/lib/runtime/signal-guard.js +35 -0
  57. package/lib/types/client/daemon.d.ts +86 -0
  58. package/lib/types/commands/annotate.d.ts +3 -0
  59. package/lib/types/commands/calibrate.d.ts +7 -0
  60. package/lib/types/commands/color.d.ts +9 -0
  61. package/lib/types/commands/crop.d.ts +3 -0
  62. package/lib/types/commands/dispatch.d.ts +22 -0
  63. package/lib/types/commands/env.d.ts +21 -0
  64. package/lib/types/commands/index.d.ts +13 -0
  65. package/lib/types/commands/input.d.ts +17 -0
  66. package/lib/types/commands/kf.d.ts +10 -0
  67. package/lib/types/commands/probe.d.ts +15 -0
  68. package/lib/types/commands/reco.d.ts +27 -0
  69. package/lib/types/commands/repl.d.ts +10 -0
  70. package/lib/types/commands/roi-check.d.ts +3 -0
  71. package/lib/types/commands/roi-compare.d.ts +3 -0
  72. package/lib/types/commands/runtime.d.ts +66 -0
  73. package/lib/types/commands/skill.d.ts +17 -0
  74. package/lib/types/commands/timing.d.ts +64 -0
  75. package/lib/types/commands/version.d.ts +3 -0
  76. package/lib/types/exec.d.ts +31 -0
  77. package/lib/types/flags.d.ts +62 -0
  78. package/lib/types/image.d.ts +27 -0
  79. package/lib/types/index.d.ts +1 -0
  80. package/lib/types/interface/hotkey.d.ts +36 -0
  81. package/lib/types/interface/index.d.ts +11 -0
  82. package/lib/types/interface/jsonc.d.ts +9 -0
  83. package/lib/types/interface/load.d.ts +190 -0
  84. package/lib/types/interface/override.d.ts +78 -0
  85. package/lib/types/interface/plan.d.ts +68 -0
  86. package/lib/types/option-args.d.ts +19 -0
  87. package/lib/types/pkg.d.ts +14 -0
  88. package/lib/types/protocol.d.ts +124 -0
  89. package/lib/types/runtime/actions.d.ts +248 -0
  90. package/lib/types/runtime/keyframes.d.ts +43 -0
  91. package/lib/types/runtime/log-watch.d.ts +28 -0
  92. package/lib/types/runtime/node-source.d.ts +25 -0
  93. package/lib/types/runtime/ocr-model.d.ts +8 -0
  94. package/lib/types/runtime/pi-env.d.ts +24 -0
  95. package/lib/types/runtime/pretask.d.ts +53 -0
  96. package/lib/types/runtime/prune.d.ts +4 -0
  97. package/lib/types/runtime/receipts.d.ts +7 -0
  98. package/lib/types/runtime/reco-args.d.ts +18 -0
  99. package/lib/types/runtime/reco-verdict.d.ts +50 -0
  100. package/lib/types/runtime/roi-compare.d.ts +117 -0
  101. package/lib/types/runtime/session-forward.d.ts +10 -0
  102. package/lib/types/runtime/session-ipc.d.ts +117 -0
  103. package/lib/types/runtime/session.d.ts +77 -0
  104. package/lib/types/runtime/signal-guard.d.ts +19 -0
  105. package/lib/types/update-check.d.ts +20 -0
  106. package/lib/types/usage.d.ts +3 -0
  107. package/lib/update-check.js +97 -0
  108. package/lib/usage.js +50 -0
  109. package/package.json +94 -4
  110. package/skills/README.md +51 -0
  111. package/skills/maafw-live/SKILL.md +293 -0
  112. package/skills/maafw-live/references/examples.md +238 -0
  113. package/skills/maafw-live/references/pitfalls.md +211 -0
  114. package/skills/maafw-live/references/workflows.md +164 -0
@@ -0,0 +1,273 @@
1
+ /**
2
+ * ROI 验真:`maafw-live roi-check` —— 用**外来目标**判断一个 ROI 该落在哪一套候选相位上。
3
+ *
4
+ * 为什么需要它(G17):`crop` 的自匹配是**自洽**检查——模板把背景连元素一起裁进去也照样
5
+ * 「位置正确」;`reco` 只回答你问的问题,没法告诉你问题本身问错了。本案例里 8 个几何脚本
6
+ * 全部收敛到错误的周期相位(棋盘 3×5:格心间距 ≈136px,格与格之间的分隔槽也是 ≈136px),
7
+ * 每套假设各自等距、自洽、看着都对,**没有一个脚本承认过自己不确定**。
8
+ *
9
+ * 判据(三点是灵魂):
10
+ * 1. **必须用外来目标**(`--target`,资源目录里的模板):用"假说自己"当模板,两套假设各自
11
+ * 1.000,什么都没证明;
12
+ * 2. **必须能说「分不出来」**:本位置与对照位置的分差 < 0.15 → `AMBIGUOUS` + 退出 3,
13
+ * 把"该做放大对比"变成工具主动喊出来的事;
14
+ * 3. **两个假设的分数都要给**:单边只能证明"旧的不对",证明不了"新的对"。
15
+ *
16
+ * 本命令现在是**薄包装**:候选集合比较的判据(生成 / 排序 / bestGap / 并列带 / 三态)都在
17
+ * `src/runtime/roi-compare.ts`,这里只负责把它接到本命令既有的回执与文案上。
18
+ * 候选生成器 `--pitch`(本位置 + 沿 x 偏移 ±pitch/2)是**案例专用**的形状:它假定单一周期、
19
+ * 且 y 从不偏移;泛用的形态是 `maafw-live roi-compare`(`--rois` / `--offsets` / `--scan`)。
20
+ * 缺 `--pitch` 时**不做对照**:推测周期正是本条批评的静默错,缺省只报单边分 + 明说"未做对照"。
21
+ */
22
+ import { existsSync } from 'node:fs';
23
+ import { EXIT, fail } from '../protocol.js';
24
+ import { recoTest, withDaemon } from '../runtime/actions.js';
25
+ import { aggregate, DEFAULT_GAP, fromOffsets, sampleOf, } from '../runtime/roi-compare.js';
26
+ import { daemonFail, argPath } from './runtime.js';
27
+ const fmt = (n) => (n === null ? '(无得分)' : String(Math.round(n * 1000) / 1000));
28
+ export const roiCheckCommand = {
29
+ name: 'roi-check',
30
+ summary: 'ROI 验真:用外来模板比对「本位置」与「偏移半周期」两套候选,分不出来时明确报 AMBIGUOUS',
31
+ usage: 'maafw-live roi-check --image <png> --roi x,y,w,h --target <资源目录内的模板> [--pitch <周期>]\n' +
32
+ ' [--resource-dir <dir>] [--threshold <t>]\n' +
33
+ ' 判据是**外来目标**(不是自匹配):本位置分 / 对照位置分 / 差值 / 结论(HIT|MISS|AMBIGUOUS)\n' +
34
+ ' 对照位置 = roi 沿 x 偏移 ±pitch/2,取更相似的那个;缺 --pitch 只报单边分并明说未做对照\n' +
35
+ ' --pitch 是**案例专用的候选生成器**(单一周期、y 不偏移);泛用形态见 `maafw-live roi-compare`\n' +
36
+ ' (--rois 显式全套 / --offsets 相对平移 / --scan 发现模式)\n' +
37
+ ' 退出码:0 命中 / 3 歧义或未命中 / 2 参数错',
38
+ options: {
39
+ image: { type: 'string' },
40
+ roi: { type: 'string' },
41
+ target: { type: 'string' },
42
+ pitch: { type: 'string' },
43
+ 'resource-dir': { type: 'string' },
44
+ threshold: { type: 'string' },
45
+ name: { type: 'string' },
46
+ },
47
+ async run(ctx) {
48
+ /* kf: 前缀必须在 argPath 之前判——先 resolve 会变成 C:\...\kf:x 绕过本校验 */
49
+ const imageRaw = typeof ctx.values.image === 'string' ? ctx.values.image : undefined;
50
+ const target = typeof ctx.values.target === 'string' ? ctx.values.target : undefined;
51
+ const resourceDir = argPath(ctx, typeof ctx.values['resource-dir'] === 'string' ? ctx.values['resource-dir'] : undefined);
52
+ if (!imageRaw || !target) {
53
+ return fail('BAD_ARGUMENTS', '缺 --image <png> 或 --target <模板>', '外来目标是本命令的灵魂:用"假说自己"当模板,两套假设各自 1.000,什么都证明不了', EXIT.USAGE);
54
+ }
55
+ if (imageRaw.startsWith('kf:')) {
56
+ return fail('BAD_ARGUMENTS', '--image 要文件路径;kf: 开头的库帧 ID 请走 reco --from-kf(本命令暂不支持库帧)', undefined, EXIT.USAGE);
57
+ }
58
+ const image = argPath(ctx, imageRaw); /* imageRaw 上面已判非空 */
59
+ if (!existsSync(image)) {
60
+ return fail('BAD_ARGUMENTS', '图像不存在:' + image, undefined, EXIT.USAGE);
61
+ }
62
+ if (!resourceDir) {
63
+ return fail('BAD_ARGUMENTS', '缺 --resource-dir:模板与 OCR 模型都要靠它加载,不给只会得到零例数(看不出病因)', '模板路径按资源目录的相对路径给,如 CritterCrash/P40/Leaders_30027.png', EXIT.USAGE);
64
+ }
65
+ const roiRaw = typeof ctx.values.roi === 'string' ? ctx.values.roi : undefined;
66
+ const roi = roiRaw ? roiRaw.split(',').map((x) => Number(String(x).trim())) : undefined;
67
+ if (!roi || roi.length !== 4 || roi.some((n) => !Number.isFinite(n))) {
68
+ return fail('BAD_ARGUMENTS', '--roi 需要 4 个数字:x,y,w,h(按 --image 的像素坐标给)', undefined, EXIT.USAGE);
69
+ }
70
+ const pitchRaw = typeof ctx.values.pitch === 'string' ? ctx.values.pitch : undefined;
71
+ const pitch = pitchRaw !== undefined ? Number(pitchRaw) : undefined;
72
+ if (pitchRaw !== undefined && (!Number.isFinite(pitch) || pitch <= 0)) {
73
+ return fail('BAD_ARGUMENTS', '--pitch 必须是正数(周期像素;缺省不做对照,不按 roi 推测周期)', undefined, EXIT.USAGE);
74
+ }
75
+ const thrRaw = typeof ctx.values.threshold === 'string' ? ctx.values.threshold : undefined;
76
+ const threshold = thrRaw !== undefined ? Number(thrRaw) : undefined;
77
+ if (thrRaw !== undefined && (!Number.isFinite(threshold) || threshold < 0 || threshold > 1)) {
78
+ return fail('BAD_ARGUMENTS', '--threshold 要在 [0,1](TemplateMatch 的置信度门槛;缺省只报分数不下结论)', undefined, EXIT.USAGE);
79
+ }
80
+ /* 同 roi-compare:只认 TemplateMatch。`--target` 是必给的模板,别的识别类型不读 template,
81
+ * 放行它们等于让 `--target` 被静默忽略、分数换成另一套含义,而回执照旧打「外来目标」。 */
82
+ const nameRaw = typeof ctx.values.name === 'string' ? ctx.values.name.trim() : '';
83
+ if (nameRaw && nameRaw !== 'TemplateMatch') {
84
+ return fail('BAD_ARGUMENTS', '--name 只接受 TemplateMatch,收到:' + nameRaw, '别的识别类型(OCR / ColorMatch …)不读 template:--target 会被静默忽略、分数也换成另一套含义', EXIT.USAGE);
85
+ }
86
+ const type = 'TemplateMatch';
87
+ /* `--pitch` 就是候选生成器:本位置(偏移 0)+ 沿 x 的 ±pitch/2。y 分量恒为 0 正是它的形状限制,
88
+ * 也是泛用形态存在的理由(真实棋盘行距 ≠ 列距)。 */
89
+ const half = pitch !== undefined ? pitch / 2 : 0;
90
+ const offsets = pitch !== undefined
91
+ ? [{ dx: 0, dy: 0 }, { dx: -half, dy: 0 }, { dx: half, dy: 0 }]
92
+ : [{ dx: 0, dy: 0 }];
93
+ const build = (size) => fromOffsets(offsets, roi, size);
94
+ const sent = build().candidates;
95
+ try {
96
+ return await withDaemon(ctx, async (c) => {
97
+ const t0 = Date.now();
98
+ const r = await recoTest(c, {
99
+ resourceDir, type, image,
100
+ param: { template: target },
101
+ candidates: sent.map((x) => x.roi),
102
+ }, Math.max(90000, sent.length * 5000) + 20000);
103
+ const elapsedMs = Date.now() - t0;
104
+ const results = r.results ?? [];
105
+ const meta = r.meta;
106
+ const imgW = Number(meta?.w);
107
+ const imgH = Number(meta?.h);
108
+ const known = Number.isFinite(imgW) && Number.isFinite(imgH) && imgW > 0 && imgH > 0;
109
+ if (r.ok === false && !results.length) {
110
+ return fail('ROI_CHECK', String(r.error ?? '识别失败'), undefined, EXIT.FINDINGS);
111
+ }
112
+ if (results.length !== sent.length) {
113
+ return fail('ROI_CHECK', '识别回执例数 ' + results.length + ' ≠ 发出的候选数 ' + sent.length + ':逐例对应关系断了,不猜', undefined, EXIT.FINDINGS);
114
+ }
115
+ const set = build(known ? { w: imgW, h: imgH } : undefined);
116
+ const byOrigin = new Map();
117
+ sent.forEach((x, i) => byOrigin.set(x.origin, sampleOf(results[i])));
118
+ /* 本位置越界:这是**参数用错空间**,先于一切判据报出来(roiOutOfBounds 连左上负坐标一起判)。 */
119
+ const baseSkip = set.skipped.find((s) => s.origin === 0);
120
+ if (baseSkip) {
121
+ return fail('ROI_OUT_OF_BOUNDS', baseSkip.reason, 'roi 要按 --image 的像素坐标给(控制器的 roi 差一个 scale)', EXIT.FINDINGS);
122
+ }
123
+ const base = byOrigin.get(0);
124
+ const baseRoi = set.candidates.find((x) => x.origin === 0).roi;
125
+ const human = [
126
+ 'ROI 验真(外来目标):' + target + ' on ' + image,
127
+ ' 图像 ' + (known ? imgW + 'x' + imgH : '尺寸未知')
128
+ + ' roi=' + baseRoi.join(','),
129
+ ' 本位置 score=' + fmt(base.score) + (base.ok ? ' [命中]' : ' [未中]')
130
+ + (base.box.length ? ' box=' + JSON.stringify(base.box) : ''),
131
+ ];
132
+ /* 对照候选(origin ≥ 1)里界内的那些;出界的一律进"已跳过"计数并按顺序报出。
133
+ * 候选**不预过滤**:左右都交给 roiOutOfBounds 判——同一条判据不能有两套口径。 */
134
+ const altCand = set.candidates.filter((x) => x.origin > 0);
135
+ const altSkipped = set.skipped.filter((x) => x.origin > 0);
136
+ let alt = null;
137
+ let whyNoCompare = '';
138
+ if (pitch === undefined) {
139
+ whyNoCompare = '未给 --pitch';
140
+ }
141
+ else if (!altCand.length) {
142
+ whyNoCompare = '±pitch/2 的对照位置越出图像(' + offsets.slice(1).map((o) => [baseRoi[0] + o.dx, baseRoi[1] + o.dy, baseRoi[2], baseRoi[3]].join(',')).join(' / ')
143
+ + ')';
144
+ }
145
+ else {
146
+ if (altSkipped.length) {
147
+ human.push(' 对照位置:' + altSkipped.length + ' 个越出图像,已跳过('
148
+ + altSkipped.map((x) => x.roi.join(',')).join(' / ') + ')');
149
+ }
150
+ for (const cand of altCand) {
151
+ const p = byOrigin.get(cand.origin);
152
+ if (!alt || (p.score ?? -1) > (alt.score ?? -1)) {
153
+ alt = { roi: cand.roi, origin: cand.origin, ...p };
154
+ }
155
+ }
156
+ }
157
+ const imageSize = known ? { w: imgW, h: imgH } : null;
158
+ if (!alt) {
159
+ human.push(' 对照位置:未做(' + whyNoCompare + ')——**未做验真**,本回执只证明外来目标在此位置的分数,'
160
+ + '不能证明这个位置比"偏移半周期"更对(推测周期正是本命令拒绝做的事)');
161
+ /* 单边结论与双边共用一条门槛口径:给了 --threshold 就按它断言,没给就按"有无得分"断言
162
+ * (未命中不是调低阈值的理由)。这是单边,不据此声称"偏移半周期"不成立。 */
163
+ const miss = base.score === null || (threshold !== undefined && base.score < threshold);
164
+ const verdict = miss ? 'MISS' : 'HIT';
165
+ human.push(' 结论:' + verdict
166
+ + (threshold === undefined
167
+ ? '(未给 --threshold:只按有无得分判,未做门槛断言)'
168
+ : miss && base.score !== null ? '(得分 ' + fmt(base.score) + ' < 门槛 ' + threshold + ')' : ''));
169
+ return {
170
+ exitCode: verdict === 'HIT' ? EXIT.OK : EXIT.FINDINGS,
171
+ human,
172
+ data: {
173
+ image, roi: baseRoi, target, type, pitch: pitch ?? null, threshold: threshold ?? null,
174
+ imageSize,
175
+ scores: { base: { score: base.score, ok: base.ok, box: base.box }, alt: null },
176
+ delta: null, gap: DEFAULT_GAP, varied: false, verdict,
177
+ compare: 'none',
178
+ compareSkipped: whyNoCompare,
179
+ bestGap: null, withinGap: [], elapsedMs,
180
+ },
181
+ };
182
+ }
183
+ /* 三态判据的事实部分(排序 / bestGap / 并列带)来自核心;本命令保留自己既有的
184
+ * **优先序与方向**(单测钉着):缺证据先于歧义、歧义先于门槛、"本位置 vs 对照"看方向。
185
+ * 泛用的 roi-compare 与之只在"门槛 vs 歧义"的次序上不同(文档定的优先序)。 */
186
+ const items = [
187
+ { roi: baseRoi, origin: 0, score: base.score, ok: base.ok, box: base.box },
188
+ { roi: alt.roi, origin: alt.origin, score: alt.score, ok: alt.ok, box: alt.box },
189
+ ];
190
+ const outcome = aggregate(items, { gap: DEFAULT_GAP });
191
+ const bothScored = base.score !== null && alt.score !== null;
192
+ const failThreshold = threshold !== undefined && base.score !== null && base.score < threshold;
193
+ let verdict;
194
+ if (!bothScored) {
195
+ /* 任一位置无证据 → 谈不上"分不出来":最多只能说某一边有分。 */
196
+ verdict = 'MISS';
197
+ }
198
+ else if (outcome.withinGap.length >= 2) {
199
+ verdict = 'AMBIGUOUS';
200
+ }
201
+ else if (failThreshold) {
202
+ verdict = 'MISS';
203
+ }
204
+ else {
205
+ verdict = base.score > alt.score ? 'HIT' : 'MISS';
206
+ }
207
+ const delta = bothScored ? base.score - alt.score : null;
208
+ human.push(' 对照位置 score=' + fmt(alt.score) + (alt.ok ? ' [命中]' : ' [未中]')
209
+ + ' roi=' + alt.roi.join(',')
210
+ + (alt.box.length ? ' box=' + JSON.stringify(alt.box) : ''));
211
+ human.push(delta === null
212
+ ? ' 差值:不适用(有一侧无得分 —— 无证据不等于 0 分)'
213
+ : ' 差值(本位置 − 对照)= ' + (Math.round(delta * 1000) / 1000) + ',判定门槛 |Δ| ≥ ' + DEFAULT_GAP);
214
+ if (verdict === 'AMBIGUOUS') {
215
+ human.push(' AMBIGUOUS:无法区分,需人工放大对比'
216
+ + '(两侧分差 ' + (Math.round(Math.abs(delta) * 1000) / 1000) + ' < ' + DEFAULT_GAP
217
+ + ';本案例的判据正是 4× 放大 A/B 四联图,不是分数)');
218
+ }
219
+ else if (verdict === 'HIT') {
220
+ human.push(' 结论:HIT —— 外来目标在本位置明显优于偏移半周期('
221
+ + (threshold !== undefined ? '且 ≥ 门槛 ' + threshold : '未做门槛断言(未给 --threshold)') + ')');
222
+ }
223
+ else if (!bothScored) {
224
+ /* 文案必须与上面打印的两侧分数逐字一致:漏分的那一侧是 base 还是 alt,
225
+ * 处置方向完全不同(目标有问题 vs 对照位置有问题)。 */
226
+ if (base.score === null && alt.score === null) {
227
+ human.push(' 结论:MISS —— 本位置与对照位置都没有得分(外来目标在这张图上什么都没匹配上),'
228
+ + '不是"分不出来";先核对目标模板与图像是否同源、roi 是否用错空间');
229
+ }
230
+ else if (base.score === null) {
231
+ human.push(' 结论:MISS —— 本位置没有得分,而对照位置有(' + fmt(alt.score) + ');'
232
+ + '先核对目标模板与图像是否同源、roi 是否用错空间,别当成歧义去人工对比');
233
+ }
234
+ else {
235
+ human.push(' 结论:MISS —— 本位置有分(score=' + fmt(base.score) + (base.ok ? ' 命中' : ' 未中')
236
+ + ')但对照位置没有得分:**缺的是对照证据,不是本位置没分**,所以不据此宣布胜出;'
237
+ + '对照位置无分多为它落在图像边界外或该处无内容——先看上面的对照 roi 是否越界,再看 pitch 是否合理');
238
+ }
239
+ }
240
+ else {
241
+ human.push(' 结论:MISS —— 对照位置更优'
242
+ + (failThreshold ? ',且本位置得分 ' + fmt(base.score) + ' < 门槛 ' + threshold : '')
243
+ + ';这个 roi 要么偏了半周期,要么指错了地方');
244
+ }
245
+ return {
246
+ exitCode: verdict === 'HIT' ? EXIT.OK : EXIT.FINDINGS,
247
+ human,
248
+ data: {
249
+ image, roi: baseRoi, target, type, pitch: pitch ?? null, threshold: threshold ?? null,
250
+ imageSize,
251
+ scores: {
252
+ base: { score: base.score, ok: base.ok, box: base.box },
253
+ alt: { score: alt.score, ok: alt.ok, box: alt.box, roi: alt.roi },
254
+ },
255
+ delta: delta === null ? null : Math.round(delta * 1000) / 1000,
256
+ gap: DEFAULT_GAP,
257
+ bestGap: outcome.bestGap,
258
+ withinGap: outcome.withinGap,
259
+ varied: true,
260
+ verdict,
261
+ compare: bothScored ? 'both-scored' : (base.score === null ? 'base-missing' : 'alt-missing'),
262
+ elapsedMs,
263
+ },
264
+ };
265
+ });
266
+ }
267
+ catch (e) {
268
+ return daemonFail(e);
269
+ }
270
+ },
271
+ };
272
+ export const ROI_CHECK_COMMANDS = [roiCheckCommand];
273
+ //# sourceMappingURL=roi-check.js.map