@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.
- package/LICENSE +21 -0
- package/README.md +293 -2
- package/bin/maafw-live.mjs +25 -0
- package/docs/daemon-protocol.md +121 -0
- package/docs/keyframe-retention-contract-v0.md +225 -0
- package/docs/session-unify-v0.md +115 -0
- package/lib/client/daemon.js +276 -0
- package/lib/commands/annotate.js +179 -0
- package/lib/commands/calibrate.js +61 -0
- package/lib/commands/color.js +73 -0
- package/lib/commands/crop.js +228 -0
- package/lib/commands/dispatch.js +121 -0
- package/lib/commands/env.js +409 -0
- package/lib/commands/index.js +70 -0
- package/lib/commands/input.js +257 -0
- package/lib/commands/kf.js +167 -0
- package/lib/commands/probe.js +70 -0
- package/lib/commands/reco.js +440 -0
- package/lib/commands/repl.js +1501 -0
- package/lib/commands/roi-check.js +273 -0
- package/lib/commands/roi-compare.js +388 -0
- package/lib/commands/runtime.js +718 -0
- package/lib/commands/skill.js +169 -0
- package/lib/commands/timing.js +247 -0
- package/lib/commands/version.js +66 -0
- package/lib/daemon/framed.mjs +3680 -0
- package/lib/daemon/reco_child.mjs +287 -0
- package/lib/exec.js +96 -0
- package/lib/flags.js +44 -0
- package/lib/image.js +90 -0
- package/lib/index.js +143 -0
- package/lib/interface/hotkey.js +142 -0
- package/lib/interface/index.js +12 -0
- package/lib/interface/jsonc.js +95 -0
- package/lib/interface/load.js +397 -0
- package/lib/interface/override.js +287 -0
- package/lib/interface/plan.js +279 -0
- package/lib/option-args.js +29 -0
- package/lib/pkg.js +54 -0
- package/lib/protocol.js +94 -0
- package/lib/runtime/actions.js +225 -0
- package/lib/runtime/keyframes.js +73 -0
- package/lib/runtime/log-watch.js +160 -0
- package/lib/runtime/node-source.js +100 -0
- package/lib/runtime/ocr-model.js +45 -0
- package/lib/runtime/pi-env.js +34 -0
- package/lib/runtime/pretask.js +217 -0
- package/lib/runtime/prune.js +28 -0
- package/lib/runtime/receipts.js +57 -0
- package/lib/runtime/reco-args.js +38 -0
- package/lib/runtime/reco-verdict.js +82 -0
- package/lib/runtime/roi-compare.js +198 -0
- package/lib/runtime/session-forward.js +79 -0
- package/lib/runtime/session-ipc.js +471 -0
- package/lib/runtime/session.js +135 -0
- package/lib/runtime/signal-guard.js +35 -0
- package/lib/types/client/daemon.d.ts +86 -0
- package/lib/types/commands/annotate.d.ts +3 -0
- package/lib/types/commands/calibrate.d.ts +7 -0
- package/lib/types/commands/color.d.ts +9 -0
- package/lib/types/commands/crop.d.ts +3 -0
- package/lib/types/commands/dispatch.d.ts +22 -0
- package/lib/types/commands/env.d.ts +21 -0
- package/lib/types/commands/index.d.ts +13 -0
- package/lib/types/commands/input.d.ts +17 -0
- package/lib/types/commands/kf.d.ts +10 -0
- package/lib/types/commands/probe.d.ts +15 -0
- package/lib/types/commands/reco.d.ts +27 -0
- package/lib/types/commands/repl.d.ts +10 -0
- package/lib/types/commands/roi-check.d.ts +3 -0
- package/lib/types/commands/roi-compare.d.ts +3 -0
- package/lib/types/commands/runtime.d.ts +66 -0
- package/lib/types/commands/skill.d.ts +17 -0
- package/lib/types/commands/timing.d.ts +64 -0
- package/lib/types/commands/version.d.ts +3 -0
- package/lib/types/exec.d.ts +31 -0
- package/lib/types/flags.d.ts +62 -0
- package/lib/types/image.d.ts +27 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/interface/hotkey.d.ts +36 -0
- package/lib/types/interface/index.d.ts +11 -0
- package/lib/types/interface/jsonc.d.ts +9 -0
- package/lib/types/interface/load.d.ts +190 -0
- package/lib/types/interface/override.d.ts +78 -0
- package/lib/types/interface/plan.d.ts +68 -0
- package/lib/types/option-args.d.ts +19 -0
- package/lib/types/pkg.d.ts +14 -0
- package/lib/types/protocol.d.ts +124 -0
- package/lib/types/runtime/actions.d.ts +248 -0
- package/lib/types/runtime/keyframes.d.ts +43 -0
- package/lib/types/runtime/log-watch.d.ts +28 -0
- package/lib/types/runtime/node-source.d.ts +25 -0
- package/lib/types/runtime/ocr-model.d.ts +8 -0
- package/lib/types/runtime/pi-env.d.ts +24 -0
- package/lib/types/runtime/pretask.d.ts +53 -0
- package/lib/types/runtime/prune.d.ts +4 -0
- package/lib/types/runtime/receipts.d.ts +7 -0
- package/lib/types/runtime/reco-args.d.ts +18 -0
- package/lib/types/runtime/reco-verdict.d.ts +50 -0
- package/lib/types/runtime/roi-compare.d.ts +117 -0
- package/lib/types/runtime/session-forward.d.ts +10 -0
- package/lib/types/runtime/session-ipc.d.ts +117 -0
- package/lib/types/runtime/session.d.ts +77 -0
- package/lib/types/runtime/signal-guard.d.ts +19 -0
- package/lib/types/update-check.d.ts +20 -0
- package/lib/types/usage.d.ts +3 -0
- package/lib/update-check.js +97 -0
- package/lib/usage.js +50 -0
- package/package.json +94 -4
- package/skills/README.md +51 -0
- package/skills/maafw-live/SKILL.md +293 -0
- package/skills/maafw-live/references/examples.md +238 -0
- package/skills/maafw-live/references/pitfalls.md +211 -0
- package/skills/maafw-live/references/workflows.md +164 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* annotate(轻量 SoM):候选区域 + 编号回画。
|
|
3
|
+
*
|
|
4
|
+
* 模型消费方式:看回画图选编号 → 查候选表 ctrl 坐标 → click / reco / crop。
|
|
5
|
+
* 候选源:OCR 框(语义最强)、变化 diff 区域、连通域、边缘密度;半透明/粒子/渐变场景
|
|
6
|
+
* 候选质量降级——那时走点选路径(crop --point),两条路径互为降级。
|
|
7
|
+
*
|
|
8
|
+
* 源帧两条路:会话内 L1 小图(缺省,一次性命令自动截一帧)或**关键帧库留存帧**(`--from-kf`,
|
|
9
|
+
* 控制器分辨率、离线、不需要设备)。库帧路径下 diff 源不可用(没有会话事件),工具如实报出,
|
|
10
|
+
* 不拿别的区域顶替——所以它适合"事后在旧帧上重跑候选",不适合替代实时变化检测。
|
|
11
|
+
*/
|
|
12
|
+
import { defaultFramesDir, resolveFrame } from '../runtime/keyframes.js';
|
|
13
|
+
import { EXIT, fail } from '../protocol.js';
|
|
14
|
+
import { annotate, seedObservation, withDaemon } from '../runtime/actions.js';
|
|
15
|
+
import { describeSession, ensureSession, offlineResource } from '../runtime/session.js';
|
|
16
|
+
import { CONNECT_OPTIONS, daemonFail, argPath, sessionOptions } from './runtime.js';
|
|
17
|
+
export const annotateCommand = {
|
|
18
|
+
name: 'annotate',
|
|
19
|
+
summary: '轻量 SoM:OCR/diff/连通域/边缘密度候选区域 + 编号回画(源可为会话内帧或关键帧库留存帧)',
|
|
20
|
+
usage: 'maafw-live annotate [--seq n] [--out <png>] [--resource-dir <dir>] [--project <dir>|--kind ...]\n' +
|
|
21
|
+
' maafw-live annotate --from-kf <kf:库UUID:序号> [--frames-dir <dir>] [--out <png>] [--resource-dir <dir>|--project <dir>]\n' +
|
|
22
|
+
' OCR 候选需要资源目录(OCR 模型);一次性命令自动截一帧;其余候选源无依赖\n' +
|
|
23
|
+
' --from-kf 在库里留存帧上离线跑(不需要设备;该路径没有会话事件,diff 源不可用)\n' +
|
|
24
|
+
' --som-limit n 放开候选上限(缺省 30;网格/表格类界面必给)\n' +
|
|
25
|
+
' --som-max-area-ratio r 巨框剔除阈值(缺省 0.12,按画面面积比;OCR 框不参与)\n' +
|
|
26
|
+
' --emit-crops <dir> 每个进表候选批量出 PNG + .prov.json(出处完整;不跑自匹配,选号验证走正式 crop)\\n' +
|
|
27
|
+
' 回画优先落在 L0 全分辨率原图上;回执带 availability(截断/剔除/分带),别把"30 条"读成"画面就这些"',
|
|
28
|
+
options: {
|
|
29
|
+
...CONNECT_OPTIONS,
|
|
30
|
+
seq: { type: 'string' },
|
|
31
|
+
out: { type: 'string' },
|
|
32
|
+
'resource-dir': { type: 'string' },
|
|
33
|
+
'from-kf': { type: 'string' },
|
|
34
|
+
'frames-dir': { type: 'string' },
|
|
35
|
+
'som-limit': { type: 'string' },
|
|
36
|
+
'som-max-area-ratio': { type: 'string' },
|
|
37
|
+
'emit-crops': { type: 'string' },
|
|
38
|
+
roi: { type: 'string' },
|
|
39
|
+
},
|
|
40
|
+
async run(ctx) {
|
|
41
|
+
const o = sessionOptions(ctx);
|
|
42
|
+
const seq = typeof ctx.values.seq === 'string' ? Number(ctx.values.seq) : undefined;
|
|
43
|
+
if (ctx.values.seq !== undefined && !Number.isFinite(seq)) {
|
|
44
|
+
return fail('BAD_ARGUMENTS', '--seq 必须是数字', undefined, EXIT.USAGE);
|
|
45
|
+
}
|
|
46
|
+
const out = argPath(ctx, typeof ctx.values.out === 'string' ? ctx.values.out : undefined);
|
|
47
|
+
const roi = typeof ctx.values.roi === 'string'
|
|
48
|
+
? ctx.values.roi.split(',').map((x) => Number(String(x).trim()))
|
|
49
|
+
: undefined;
|
|
50
|
+
if (roi && (roi.length !== 4 || roi.some((n) => !Number.isFinite(n)))) {
|
|
51
|
+
return fail('BAD_ARGUMENTS', '--roi 需要 4 个数字:x,y,w,h(控制器分辨率坐标)', undefined, EXIT.USAGE);
|
|
52
|
+
}
|
|
53
|
+
/* 上限/阈值必须**显式拒绝**非法值,不能靠 daemon 的 `> 0` 守卫悄悄换回默认——
|
|
54
|
+
* `--som-limit 0` 被静默当成 30 时,调用方以为"不限量",拿到的是截断表。 */
|
|
55
|
+
const somLimitRaw = ctx.values['som-limit'];
|
|
56
|
+
const somLimit = typeof somLimitRaw === 'string' ? Number(somLimitRaw) : undefined;
|
|
57
|
+
if (somLimitRaw !== undefined && (!Number.isFinite(somLimit) || somLimit <= 0)) {
|
|
58
|
+
return fail('BAD_ARGUMENTS', '--som-limit 必须是正整数(收到 ' + JSON.stringify(somLimitRaw) + ')'
|
|
59
|
+
+ ';0 不等于"不限量"', undefined, EXIT.USAGE);
|
|
60
|
+
}
|
|
61
|
+
const ratioRaw = ctx.values['som-max-area-ratio'];
|
|
62
|
+
const ratio = typeof ratioRaw === 'string' ? Number(ratioRaw) : undefined;
|
|
63
|
+
if (ratioRaw !== undefined && (!Number.isFinite(ratio) || ratio <= 0 || ratio >= 1)) {
|
|
64
|
+
return fail('BAD_ARGUMENTS', '--som-max-area-ratio 要在 (0,1) 之间(画面面积比,收到 ' + JSON.stringify(ratioRaw) + ')'
|
|
65
|
+
+ ';0 不等于"关闭剔除"', undefined, EXIT.USAGE);
|
|
66
|
+
}
|
|
67
|
+
const fromKf = typeof ctx.values['from-kf'] === 'string' ? ctx.values['from-kf'] : undefined;
|
|
68
|
+
if (fromKf !== undefined && seq !== undefined) {
|
|
69
|
+
return fail('BAD_ARGUMENTS', '--from-kf 与 --seq 互斥:前者在库内留存帧上跑,后者在本会话缓冲帧上跑', '取库里有哪些帧:maafw-live kf list', EXIT.USAGE);
|
|
70
|
+
}
|
|
71
|
+
/* 库帧来源:先走离线读侧解析(契约 §4.1),daemon 只接身份与路径 */
|
|
72
|
+
let kfSource;
|
|
73
|
+
if (fromKf !== undefined) {
|
|
74
|
+
const dir = argPath(ctx, typeof ctx.values['frames-dir'] === 'string' ? ctx.values['frames-dir'] : undefined) ?? defaultFramesDir();
|
|
75
|
+
const r = resolveFrame(dir, fromKf);
|
|
76
|
+
if (r.status !== 'available' || !r.path || !r.record) {
|
|
77
|
+
const code = r.status === 'corrupt' ? 'KF_CORRUPT' : (r.status === 'unsupported' ? 'KF_UNSUPPORTED' : 'KF_MISSING');
|
|
78
|
+
return fail(code, '库帧不可用(' + r.status + '):' + String(r.reason ?? '') + '(库 ' + dir + ')', 'kf list 看库内有哪些帧;missing 表示本机不可复核,不代表历史从未观测', EXIT.FINDINGS);
|
|
79
|
+
}
|
|
80
|
+
const rec = r.record;
|
|
81
|
+
kfSource = {
|
|
82
|
+
id: rec.id, path: r.path, sha256: rec.sha256, ctrlW: rec.ctrlW, ctrlH: rec.ctrlH,
|
|
83
|
+
captureSeq: rec.captureSeq, capturedAt: rec.capturedAt,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const offline = kfSource !== undefined;
|
|
87
|
+
try {
|
|
88
|
+
return await withDaemon(ctx, async (client) => {
|
|
89
|
+
const s = offline ? { connected: false } : await ensureSession(client, o);
|
|
90
|
+
let resourceDir = argPath(ctx, typeof ctx.values['resource-dir'] === 'string' ? ctx.values['resource-dir'] : undefined)
|
|
91
|
+
?? s.plan?.resource?.paths[0];
|
|
92
|
+
if (!resourceDir && offline && o.project) {
|
|
93
|
+
/* 离线:项目只作资源的来源(OCR 模型),不扫设备不连接 */
|
|
94
|
+
const off = offlineResource(o.project, o.controller, o.resource, ctx.cwd);
|
|
95
|
+
if (off.error) {
|
|
96
|
+
return fail('PLAN_FAILED', '离线解析项目资源失败:' + off.error, '也可以直接给 --resource-dir <dir>', EXIT.FINDINGS);
|
|
97
|
+
}
|
|
98
|
+
resourceDir = off.paths[0];
|
|
99
|
+
}
|
|
100
|
+
/* 一次性命令缓冲为空:先截一帧(用完即删,不留临时文件)。库帧路径不截帧——源已固定。 */
|
|
101
|
+
if (!offline && seq === undefined) {
|
|
102
|
+
await seedObservation(client, 'annotate');
|
|
103
|
+
}
|
|
104
|
+
const emitCrops = argPath(ctx, typeof ctx.values['emit-crops'] === 'string' ? ctx.values['emit-crops'] : undefined);
|
|
105
|
+
const r = await annotate(client, {
|
|
106
|
+
...(offline ? { kfSource } : {}),
|
|
107
|
+
...(!offline && seq !== undefined ? { seq } : {}),
|
|
108
|
+
...(out ? { out } : {}),
|
|
109
|
+
...(resourceDir ? { resourceDir } : {}),
|
|
110
|
+
...(roi ? { roi } : {}),
|
|
111
|
+
...(somLimit !== undefined ? { somLimit } : {}),
|
|
112
|
+
...(ratio !== undefined ? { somMaxAreaRatio: ratio } : {}),
|
|
113
|
+
...(emitCrops ? { emitCrops } : {}),
|
|
114
|
+
});
|
|
115
|
+
if (r.ok === false) {
|
|
116
|
+
return fail('ANNOTATE', String(r.error ?? '候选生成失败'), offline
|
|
117
|
+
? '库帧候选失败通常是文件被改动或库记录不一致;kf resolve 可复核'
|
|
118
|
+
: '需要观测:REPL 里先 screencap / stream start;一次性命令会自动截一帧', EXIT.FINDINGS);
|
|
119
|
+
}
|
|
120
|
+
const cands = r.candidates ?? [];
|
|
121
|
+
const sources = r.sources;
|
|
122
|
+
const src = r.source;
|
|
123
|
+
const unavailable = r.sourcesUnavailable;
|
|
124
|
+
const ctrl = Array.isArray(r.ctrl) ? r.ctrl : null;
|
|
125
|
+
const avail = r.availability;
|
|
126
|
+
const srcLine = src?.kind === 'kf'
|
|
127
|
+
? '源帧 ' + String(src.id) + '(留存帧,捕获 seq=' + String(src.captureSeq) + ',' +
|
|
128
|
+
String(ctrl?.[0] ?? '?') + 'x' + String(ctrl?.[1] ?? '?') + ',离线)'
|
|
129
|
+
: '源帧 seq=' + String(r.seq) + '(小图 ' + JSON.stringify(r.small) + ' × ctrl ' + JSON.stringify(r.ctrl) + ')';
|
|
130
|
+
/* 可用性三列:**"看起来有结果"不等于"有结果"**。截断/剔除/分带一起读,
|
|
131
|
+
* 才分得清"这一带没东西"与"这一带的东西被上限或门槛拿掉了"。 */
|
|
132
|
+
const availLines = avail
|
|
133
|
+
? [
|
|
134
|
+
' 可用性:合并池 ' + String(avail.mergedTotal ?? '?') + ' → 进表 ' + String(r.count)
|
|
135
|
+
+ '(上限 ' + String(avail.limit ?? '?') + ',截断 ' + String(avail.truncated ?? 0) + ')'
|
|
136
|
+
+ ';剔除 太小 ' + String(avail.dropped?.tooSmall ?? 0) + ' / 巨框 ' + String(avail.dropped?.tooBig ?? 0)
|
|
137
|
+
+ '(阈值 ' + String(avail.maxAreaRatio ?? '?') + ')/ 重复 ' + String(avail.dropped?.duplicated ?? 0),
|
|
138
|
+
...(avail.byBand ? [' 分带(上/中/下):' + avail.byBand.map((b) => String(b.inTable) + '/' + String(b.inPool)).join(' ')] : []),
|
|
139
|
+
...(avail.outResolution ? [' 回画分辨率:' + String(avail.outResolution.w) + 'x' + String(avail.outResolution.h)
|
|
140
|
+
+ '(' + String(avail.outResolution.source) + ')'] : []),
|
|
141
|
+
...(avail.detectionResolution ? [' 检测面分辨率:' + String(avail.detectionResolution.w) + 'x'
|
|
142
|
+
+ String(avail.detectionResolution.h) + '(' + String(avail.detectionResolution.source)
|
|
143
|
+
+ ')——检测面越小,小目标越可能探不到'] : []),
|
|
144
|
+
]
|
|
145
|
+
: [];
|
|
146
|
+
return {
|
|
147
|
+
exitCode: EXIT.OK,
|
|
148
|
+
human: [
|
|
149
|
+
...describeSession(s, ctx.flags.verbose),
|
|
150
|
+
...(offline ? ['离线候选:未连接设备(库帧是本机持久文件)'] : []),
|
|
151
|
+
'SoM 候选 ' + String(r.count) + ' 个(ocr ' + String(sources?.ocr ?? 0) + ' / diff ' + String(sources?.diff ?? 0) +
|
|
152
|
+
' / conn ' + String(sources?.conn ?? 0) + ' / edge ' + String(sources?.edge ?? 0) + ')→ ' + String(r.out),
|
|
153
|
+
' ' + srcLine,
|
|
154
|
+
...availLines,
|
|
155
|
+
...(unavailable ? Object.entries(unavailable).map(([k, v]) => ' ' + k + ' 源不可用:' + v) : []),
|
|
156
|
+
' 坐标为控制器分辨率(面积占比供排序:越小越像小按钮,越大越像面板/背景):',
|
|
157
|
+
...cands.map((c) => ' #' + c.id + ' [' + c.source + '] ctrl=' + c.ctrl.join(',')
|
|
158
|
+
+ (typeof c.areaRatio === 'number' ? ' 面积比=' + c.areaRatio : '')
|
|
159
|
+
+ (typeof c.score === 'number' ? ' score=' + c.score : '')
|
|
160
|
+
+ (c.text ? ' "' + c.text + '"' : '')),
|
|
161
|
+
...(r.warn ? [' 警告:' + String(r.warn)] : []),
|
|
162
|
+
...(r.emitted?.count
|
|
163
|
+
? [' emit-crops:' + String(r.emitted.count) + ' 条已落到 ' + String(r.emitted.dir) + '(含 .prov.json;选号验证仍走正式 crop --roi/--from-kf)']
|
|
164
|
+
: []),
|
|
165
|
+
'选号后:click / reco --node(roi 用 ctrl 坐标)/ crop --roi(可再 crop --from-kf 于同一帧)',
|
|
166
|
+
],
|
|
167
|
+
data: r,
|
|
168
|
+
warnings: r.warn ? [String(r.warn)] : [],
|
|
169
|
+
written: [String(r.out)],
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
return daemonFail(e);
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
export const ANNOTATE_COMMANDS = [annotateCommand];
|
|
179
|
+
//# sourceMappingURL=annotate.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 变化检测阈值校准:静止画面采帧,统计分块亮度差 / 全局位差率的噪声分布,
|
|
3
|
+
* 推荐 blockThresh / changeGlobal。校准期间画面必须静止(max≫p99 会警告重跑)。
|
|
4
|
+
*/
|
|
5
|
+
import { EXIT, fail } from '../protocol.js';
|
|
6
|
+
import { calibrate, withDaemon } from '../runtime/actions.js';
|
|
7
|
+
import { describeSession, ensureSession } from '../runtime/session.js';
|
|
8
|
+
import { CONNECT_OPTIONS, daemonFail, sessionOptions } from './runtime.js';
|
|
9
|
+
export const calibrateCommand = {
|
|
10
|
+
name: 'calibrate',
|
|
11
|
+
summary: '变化检测阈值校准:静止画面采帧定噪声地板,推荐 blockThresh / changeGlobal',
|
|
12
|
+
usage: 'maafw-live calibrate [--frames n] [--interval ms] --project <dir>|--kind ...\n' +
|
|
13
|
+
' 采集期间保持画面静止;结果给出可直接粘贴的 stream start 参数',
|
|
14
|
+
options: { ...CONNECT_OPTIONS, frames: { type: 'string' }, interval: { type: 'string' } },
|
|
15
|
+
async run(ctx) {
|
|
16
|
+
const o = sessionOptions(ctx);
|
|
17
|
+
if (!o.project && !o.kind) {
|
|
18
|
+
return fail('BAD_ARGUMENTS', '校准要设备:给 --project <dir>(推荐)或 --kind win32|adb|gamepad', undefined, EXIT.USAGE);
|
|
19
|
+
}
|
|
20
|
+
const frames = typeof ctx.values.frames === 'string' ? Number(ctx.values.frames) : undefined;
|
|
21
|
+
if (frames !== undefined && !Number.isFinite(frames)) {
|
|
22
|
+
return fail('BAD_ARGUMENTS', '--frames 必须是数字(6..60,默认 24)', undefined, EXIT.USAGE);
|
|
23
|
+
}
|
|
24
|
+
const interval = typeof ctx.values.interval === 'string' ? Number(ctx.values.interval) : undefined;
|
|
25
|
+
if (interval !== undefined && !Number.isFinite(interval)) {
|
|
26
|
+
return fail('BAD_ARGUMENTS', '--interval 必须是毫秒数(默认 300)', undefined, EXIT.USAGE);
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
return await withDaemon(ctx, async (client) => {
|
|
30
|
+
const s = await ensureSession(client, o);
|
|
31
|
+
const r = await calibrate(client, {
|
|
32
|
+
...(frames !== undefined ? { frames } : {}),
|
|
33
|
+
...(interval !== undefined ? { interval } : {}),
|
|
34
|
+
});
|
|
35
|
+
if (r.ok === false) {
|
|
36
|
+
return fail('CALIBRATE', String(r.error ?? '校准失败'), undefined, EXIT.FINDINGS);
|
|
37
|
+
}
|
|
38
|
+
const block = r.block;
|
|
39
|
+
const global = r.global;
|
|
40
|
+
return {
|
|
41
|
+
exitCode: r.warn ? EXIT.FINDINGS : EXIT.OK,
|
|
42
|
+
human: [
|
|
43
|
+
...describeSession(s, ctx.flags.verbose),
|
|
44
|
+
'校准 ' + String(r.frames) + ' 帧(分块亮度差 / 全局位差率的噪声分布):',
|
|
45
|
+
' 分块差 p50=' + block.p50 + ' p99=' + block.p99 + ' max=' + block.max,
|
|
46
|
+
' 位差率 p50=' + global.p50 + ' p99=' + global.p99 + ' max=' + global.max,
|
|
47
|
+
' 推荐:' + String(r.apply),
|
|
48
|
+
...(r.warn ? [' 警告:' + String(r.warn)] : []),
|
|
49
|
+
],
|
|
50
|
+
data: r,
|
|
51
|
+
...(r.warn ? { warnings: [String(r.warn)] } : {}),
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
return daemonFail(e);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
export const CALIBRATE_COMMANDS = [calibrateCommand];
|
|
61
|
+
//# sourceMappingURL=calibrate.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 探色命令:ROI 实测主色/均值(RGB/HSV/灰度)——模型只选色不报色值。
|
|
3
|
+
*
|
|
4
|
+
* 为什么不靠模型读像素:色值必须来自实测(降采样小图按该帧捕获尺寸换算后的 ROI),
|
|
5
|
+
* 模型选完色后用 reco ColorMatch 出框(走已修好的参数透传链),出框与否是事实不是判断。
|
|
6
|
+
*/
|
|
7
|
+
import { EXIT, fail } from '../protocol.js';
|
|
8
|
+
import { colorProbe, seedObservation, withDaemon } from '../runtime/actions.js';
|
|
9
|
+
import { describeSession, ensureSession } from '../runtime/session.js';
|
|
10
|
+
import { CONNECT_OPTIONS, daemonFail, sessionOptions } from './runtime.js';
|
|
11
|
+
export const colorCommand = {
|
|
12
|
+
name: 'color',
|
|
13
|
+
summary: '探色:ROI 实测均值/HSV/主色(控制器分辨率坐标;选中后用 reco ColorMatch 出框)',
|
|
14
|
+
usage: 'maafw-live color [--roi x,y,w,h] [--seq n] --project <dir>|--kind ...\n' +
|
|
15
|
+
' 一次性命令先自动截一帧再测;REPL 里测缓冲最新帧(先 screencap / stream)',
|
|
16
|
+
options: { ...CONNECT_OPTIONS, roi: { type: 'string' }, seq: { type: 'string' } },
|
|
17
|
+
async run(ctx) {
|
|
18
|
+
const o = sessionOptions(ctx);
|
|
19
|
+
if (!o.project && !o.kind) {
|
|
20
|
+
return fail('BAD_ARGUMENTS', '要设备才能探色:给 --project <dir>(推荐)或 --kind win32|adb|gamepad', '多步操作用 maafw-live repl(连接一次后连续观测)', EXIT.USAGE);
|
|
21
|
+
}
|
|
22
|
+
const roi = typeof ctx.values.roi === 'string'
|
|
23
|
+
? ctx.values.roi.split(',').map((x) => Number(String(x).trim()))
|
|
24
|
+
: undefined;
|
|
25
|
+
if (roi && (roi.length !== 4 || roi.some((n) => !Number.isFinite(n)))) {
|
|
26
|
+
return fail('BAD_ARGUMENTS', '--roi 需要 4 个数字:x,y,w,h(控制器分辨率坐标,与 pipeline 的 roi 同空间)', undefined, EXIT.USAGE);
|
|
27
|
+
}
|
|
28
|
+
const seq = typeof ctx.values.seq === 'string' ? Number(ctx.values.seq) : undefined;
|
|
29
|
+
if (ctx.values.seq !== undefined && !Number.isFinite(seq)) {
|
|
30
|
+
return fail('BAD_ARGUMENTS', '--seq 必须是数字', undefined, EXIT.USAGE);
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
return await withDaemon(ctx, async (client) => {
|
|
34
|
+
const s = await ensureSession(client, o);
|
|
35
|
+
/* 一次性命令缓冲为空:先截一帧产生观测(screencap 现在进环,有捕获身份)。
|
|
36
|
+
* 截帧文件用完即删——它只是观测载体,既不落用户 cwd,也不留在临时目录。 */
|
|
37
|
+
if (seq === undefined) {
|
|
38
|
+
await seedObservation(client, 'color');
|
|
39
|
+
}
|
|
40
|
+
const r = await colorProbe(client, {
|
|
41
|
+
...(seq !== undefined ? { seq } : {}),
|
|
42
|
+
...(roi ? { roi } : {}),
|
|
43
|
+
});
|
|
44
|
+
if (r.ok === false) {
|
|
45
|
+
return fail('COLOR_PROBE', String(r.error ?? '探色失败'), 'REPL 里先 screencap 或 stream start;一次性命令会自动截一帧', EXIT.FINDINGS);
|
|
46
|
+
}
|
|
47
|
+
const mean = r.mean;
|
|
48
|
+
const hsv = r.hsv;
|
|
49
|
+
const dom = r.dominant;
|
|
50
|
+
return {
|
|
51
|
+
exitCode: EXIT.OK,
|
|
52
|
+
human: [
|
|
53
|
+
...describeSession(s, ctx.flags.verbose),
|
|
54
|
+
'探色 seq=' + String(r.seq) + (roi ? ' roi=' + roi.join(',') : ' (整帧)'),
|
|
55
|
+
' 均值 RGB ' + mean.r + ',' + mean.g + ',' + mean.b + ' 灰度 ' + mean.gray +
|
|
56
|
+
' HSV ' + hsv.h + '°,' + hsv.s + '%,' + hsv.v + '%',
|
|
57
|
+
' 主色 RGB ' + dom.rgb.join(',') + '(覆盖 ' + Math.round(dom.ratio * 100) + '%)',
|
|
58
|
+
'出框验证:maafw-live reco --type ColorMatch --param \'{"color":[' + dom.rgb.join(',') + '],"threshold":0.05}\' …',
|
|
59
|
+
],
|
|
60
|
+
data: r,
|
|
61
|
+
suggestedCommands: [
|
|
62
|
+
'maafw-live reco --type ColorMatch --param \'{"color":[' + dom.rgb.join(',') + '],"threshold":0.05}\' --image <png> --resource-dir <dir>',
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
return daemonFail(e);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
export const COLOR_COMMANDS = [colorCommand];
|
|
73
|
+
//# sourceMappingURL=color.js.map
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模板裁剪:低精度输入(宽松框或点+外扩)→ snap 收紧 → 从源帧原图裁剪 →
|
|
3
|
+
* 同帧自匹配逐边重试取得分最高边界。
|
|
4
|
+
*
|
|
5
|
+
* 关键约束:模板尺寸必须与识别空间一致——只从控制器分辨率的完整原图裁,
|
|
6
|
+
* 不从降采样小图裁。点→ROI 派生与裁剪共享同一条 snap 链(roadmap 条目4/5)。
|
|
7
|
+
*
|
|
8
|
+
* 源帧两条路:
|
|
9
|
+
* - L0 热缓存(缺省):进程态,seq 或最新;跨帧验证缺省开(新帧再匹配一次)。
|
|
10
|
+
* - 关键帧库留存帧(--from-kf):库内 L0 文件,跨会话可用、**不需要设备**——
|
|
11
|
+
* 不可复现状态的动线正是这条(捕获它的那次会话早已结束)。跨帧验证缺省关:
|
|
12
|
+
* 留存帧对应的状态通常已不在画面上,拿当前帧比只会得到假警告。
|
|
13
|
+
*
|
|
14
|
+
* 裁出的 PNG 是 L2 派生图(契约 §2),旁边写 `<out>.prov.json` 出处记录
|
|
15
|
+
* (契约 §5:L2 记录来源及裁剪 / 缩放变换)——出处与模板同生共死,可一起提交进仓库。
|
|
16
|
+
*/
|
|
17
|
+
import { defaultFramesDir, resolveFrame } from '../runtime/keyframes.js';
|
|
18
|
+
import { EXIT, fail } from '../protocol.js';
|
|
19
|
+
import { seedObservation, tplCrop, withDaemon } from '../runtime/actions.js';
|
|
20
|
+
import { describeSession, ensureSession, offlineResource } from '../runtime/session.js';
|
|
21
|
+
import { CONNECT_OPTIONS, daemonFail, argPath, sessionOptions } from './runtime.js';
|
|
22
|
+
function nums(raw, count, what) {
|
|
23
|
+
if (raw === undefined)
|
|
24
|
+
return undefined;
|
|
25
|
+
const v = raw.split(',').map((x) => Number(String(x).trim()));
|
|
26
|
+
if (v.length !== count || v.some((n) => !Number.isFinite(n))) {
|
|
27
|
+
throw new Error(what + ' 需要 ' + count + ' 个数字');
|
|
28
|
+
}
|
|
29
|
+
return v;
|
|
30
|
+
}
|
|
31
|
+
/** 库里解析出来的帧 → daemon 的裁剪源(身份与路径;daemon 会复核 sha256 与像素尺寸)。 */
|
|
32
|
+
function kfSourceOf(r) {
|
|
33
|
+
const rec = r.record;
|
|
34
|
+
return {
|
|
35
|
+
id: rec.id,
|
|
36
|
+
path: String(r.path),
|
|
37
|
+
sha256: rec.sha256,
|
|
38
|
+
ctrlW: rec.ctrlW,
|
|
39
|
+
ctrlH: rec.ctrlH,
|
|
40
|
+
captureSeq: rec.captureSeq,
|
|
41
|
+
capturedAt: rec.capturedAt,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export const cropCommand = {
|
|
45
|
+
name: 'crop',
|
|
46
|
+
summary: '模板裁剪:宽松框/点 → snap 收紧 → 原图裁剪 → 自匹配验证(源可为 L0 热缓存或关键帧库留存帧)',
|
|
47
|
+
usage: 'maafw-live crop --roi x,y,w,h | --point x,y [--pad n] [--seq n] [--out <png>] [--no-cross] --project <dir>|--kind ...\n' +
|
|
48
|
+
' maafw-live crop --from-kf <kf:库UUID:序号> --roi x,y,w,h [--frames-dir <dir>] [--resource-dir <dir> | --project <dir>] [--cross]\n' +
|
|
49
|
+
' 源帧缺省是 L0 最新原图(screencap/stream/输入都会产生);连接真机时自动做跨帧验证(新帧再匹配一次)\n' +
|
|
50
|
+
' --from-kf 改从关键帧库留存帧裁:离线可用(不需要设备),跨帧验证缺省关(留存帧对应的状态通常已不在画面上)\n' +
|
|
51
|
+
' 裁出的 PNG 是 L2 派生图,旁边写 <out>.prov.json 出处记录(来源 + 裁剪变换;--no-prov 可关)\n' +
|
|
52
|
+
' --keep-source 顺手把本次裁剪用的那一帧升格进关键帧库,出处随即带上可复核的 kf: 身份\n' +
|
|
53
|
+
' (契约的模型是 L0 缓存 → 选择性升格 → 从库帧裁;少了这步,出处只剩一个随进程消失的 seq)\n' +
|
|
54
|
+
' --prov-out <dir> 把出处写到该目录下(同名),用于模板要进资源包、而资源包只装框架要加载的东西:\n' +
|
|
55
|
+
' 约定不变量是"同一相对路径"(如 state-plan/provenance/ 与 image/ 同构)',
|
|
56
|
+
options: {
|
|
57
|
+
...CONNECT_OPTIONS,
|
|
58
|
+
roi: { type: 'string' },
|
|
59
|
+
point: { type: 'string' },
|
|
60
|
+
pad: { type: 'string' },
|
|
61
|
+
seq: { type: 'string' },
|
|
62
|
+
out: { type: 'string' },
|
|
63
|
+
'resource-dir': { type: 'string' },
|
|
64
|
+
'no-cross': { type: 'boolean' },
|
|
65
|
+
'from-kf': { type: 'string' },
|
|
66
|
+
'frames-dir': { type: 'string' },
|
|
67
|
+
cross: { type: 'boolean' },
|
|
68
|
+
'no-prov': { type: 'boolean' },
|
|
69
|
+
'prov-out': { type: 'string' },
|
|
70
|
+
'keep-source': { type: 'boolean' },
|
|
71
|
+
},
|
|
72
|
+
async run(ctx) {
|
|
73
|
+
const o = sessionOptions(ctx);
|
|
74
|
+
let roi;
|
|
75
|
+
let point;
|
|
76
|
+
try {
|
|
77
|
+
roi = nums(typeof ctx.values.roi === 'string' ? ctx.values.roi : undefined, 4, '--roi');
|
|
78
|
+
point = nums(typeof ctx.values.point === 'string' ? ctx.values.point : undefined, 2, '--point');
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
return fail('BAD_ARGUMENTS', e.message, undefined, EXIT.USAGE);
|
|
82
|
+
}
|
|
83
|
+
if (!roi && !point) {
|
|
84
|
+
return fail('BAD_ARGUMENTS', '给 --roi x,y,w,h(宽松框)或 --point x,y(自动外扩成宽松框)', undefined, EXIT.USAGE);
|
|
85
|
+
}
|
|
86
|
+
const seq = typeof ctx.values.seq === 'string' ? Number(ctx.values.seq) : undefined;
|
|
87
|
+
if (ctx.values.seq !== undefined && !Number.isFinite(seq)) {
|
|
88
|
+
return fail('BAD_ARGUMENTS', '--seq 必须是数字', undefined, EXIT.USAGE);
|
|
89
|
+
}
|
|
90
|
+
const pad = typeof ctx.values.pad === 'string' ? Number(ctx.values.pad) : undefined;
|
|
91
|
+
if (pad !== undefined && !Number.isFinite(pad)) {
|
|
92
|
+
return fail('BAD_ARGUMENTS', '--pad 必须是数字(点→ROI 外扩半径,默认 24)', undefined, EXIT.USAGE);
|
|
93
|
+
}
|
|
94
|
+
const out = argPath(ctx, typeof ctx.values.out === 'string' ? ctx.values.out : undefined);
|
|
95
|
+
const fromKf = typeof ctx.values['from-kf'] === 'string' ? ctx.values['from-kf'] : undefined;
|
|
96
|
+
if (fromKf !== undefined && seq !== undefined) {
|
|
97
|
+
return fail('BAD_ARGUMENTS', '--from-kf 与 --seq 互斥:前者从库内留存帧裁,后者从本会话 L0 热缓存裁', '取库里有哪些帧:maafw-live kf list', EXIT.USAGE);
|
|
98
|
+
}
|
|
99
|
+
if (fromKf !== undefined && ctx.values.cross === true && ctx.values['no-cross'] === true) {
|
|
100
|
+
return fail('BAD_ARGUMENTS', '--cross 与 --no-cross 互斥', undefined, EXIT.USAGE);
|
|
101
|
+
}
|
|
102
|
+
/* 库帧来源:先走离线读侧解析(契约 §4.1),daemon 只接身份与路径——
|
|
103
|
+
* 解析失败就在这一层如实报,不把不可复核的帧送进裁剪。 */
|
|
104
|
+
let kfSource;
|
|
105
|
+
if (fromKf !== undefined) {
|
|
106
|
+
const dir = argPath(ctx, typeof ctx.values['frames-dir'] === 'string' ? ctx.values['frames-dir'] : undefined) ?? defaultFramesDir();
|
|
107
|
+
const r = resolveFrame(dir, fromKf);
|
|
108
|
+
if (r.status !== 'available' || !r.path || !r.record) {
|
|
109
|
+
const code = r.status === 'corrupt' ? 'KF_CORRUPT' : (r.status === 'unsupported' ? 'KF_UNSUPPORTED' : 'KF_MISSING');
|
|
110
|
+
return fail(code, '库帧不可用(' + r.status + '):' + String(r.reason ?? '') + '(库 ' + dir + ')', 'kf list 看库内有哪些帧;missing 表示本机不可复核,不代表历史从未观测', EXIT.FINDINGS);
|
|
111
|
+
}
|
|
112
|
+
kfSource = kfSourceOf(r);
|
|
113
|
+
}
|
|
114
|
+
const wantCross = ctx.values.cross === true;
|
|
115
|
+
/* 库帧路径默认离线:留存帧是本机持久文件,裁剪不需要设备。要跨帧验证才连。 */
|
|
116
|
+
const offline = kfSource !== undefined && !wantCross;
|
|
117
|
+
if (kfSource !== undefined && wantCross && !o.project && !o.kind) {
|
|
118
|
+
return fail('BAD_ARGUMENTS', '--cross 需要连接设备:给 --project <dir> 或 --kind', '不留 --cross 就是纯离线裁剪(留存帧的价值所在)', EXIT.USAGE);
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
return await withDaemon(ctx, async (client) => {
|
|
122
|
+
const s = offline ? { connected: false } : await ensureSession(client, o);
|
|
123
|
+
/* 自匹配要加载资源(模板走 override_image,不写项目目录);项目模式用规划出的资源 */
|
|
124
|
+
let resourceDir = argPath(ctx, typeof ctx.values['resource-dir'] === 'string' ? ctx.values['resource-dir'] : undefined)
|
|
125
|
+
?? s.plan?.resource?.paths[0];
|
|
126
|
+
if (!resourceDir && offline && o.project) {
|
|
127
|
+
/* 离线:项目只是资源的来源,不是连接的对象——只读 interface.json,不扫设备 */
|
|
128
|
+
const off = offlineResource(o.project, o.controller, o.resource, ctx.cwd);
|
|
129
|
+
if (off.error) {
|
|
130
|
+
return fail('PLAN_FAILED', '离线解析项目资源失败:' + off.error, '也可以直接给 --resource-dir <dir>', EXIT.FINDINGS);
|
|
131
|
+
}
|
|
132
|
+
resourceDir = off.paths[0];
|
|
133
|
+
}
|
|
134
|
+
if (!resourceDir) {
|
|
135
|
+
return fail('BAD_ARGUMENTS', '自匹配需要资源目录:给 --resource-dir <dir> 或 --project <dir>', undefined, EXIT.USAGE);
|
|
136
|
+
}
|
|
137
|
+
/* L0 是进程态:一次性命令先截一帧产生观测(截帧文件用完即删,不落用户 cwd、也不留在临时目录)。
|
|
138
|
+
* 库帧路径不截帧——源已经固定,重截只会拿到另一个时刻。 */
|
|
139
|
+
if (kfSource === undefined && seq === undefined) {
|
|
140
|
+
await seedObservation(client, 'crop');
|
|
141
|
+
}
|
|
142
|
+
const r = await tplCrop(client, {
|
|
143
|
+
...(roi ? { roi } : {}),
|
|
144
|
+
...(point ? { point } : {}),
|
|
145
|
+
...(pad !== undefined ? { pad } : {}),
|
|
146
|
+
...(kfSource === undefined && seq !== undefined ? { seq } : {}),
|
|
147
|
+
...(kfSource ? { kfSource } : {}),
|
|
148
|
+
...(out ? { out } : {}),
|
|
149
|
+
resourceDir,
|
|
150
|
+
/* 跨帧验证缺省:热缓存路径交给 daemon 判(连接着就做);库帧路径显式关,--cross 才做 */
|
|
151
|
+
...(kfSource !== undefined && !wantCross ? { cross: false } : (wantCross ? { cross: true } : {})),
|
|
152
|
+
...(ctx.values['no-cross'] === true ? { cross: false } : {}),
|
|
153
|
+
...(ctx.values['no-prov'] === true ? { prov: false } : {}),
|
|
154
|
+
...(typeof ctx.values['prov-out'] === 'string' && ctx.values['prov-out']
|
|
155
|
+
? { provOut: ctx.values['prov-out'] }
|
|
156
|
+
: {}),
|
|
157
|
+
...(ctx.values['keep-source'] === true ? { keepSource: true } : {}),
|
|
158
|
+
});
|
|
159
|
+
if (r.ok === false) {
|
|
160
|
+
return fail('TPL_CROP', String(r.error ?? '裁剪失败'), kfSource ? '库帧裁剪失败通常是文件被改动或库记录不一致;kf resolve 可复核' : '源帧取 L0 最新原图:先 screencap / stream / 输入产生观测', EXIT.FINDINGS);
|
|
161
|
+
}
|
|
162
|
+
const box = r.box;
|
|
163
|
+
const src = r.source;
|
|
164
|
+
const cross = r.cross;
|
|
165
|
+
/* 贴合度信号:宽松框 → 最终框的几何对比(得分那一路分辨不出贴合度:三种窗口自匹配都是 1.0) */
|
|
166
|
+
const tg = r.tighten;
|
|
167
|
+
const space = r.spaceCheck;
|
|
168
|
+
const srcLine = src && src.kind === 'kf'
|
|
169
|
+
? '源帧 ' + String(src.id) + '(留存帧,捕获 seq=' + String(src.captureSeq) + ',' +
|
|
170
|
+
String(src.ctrlW) + 'x' + String(src.ctrlH) + ',sha ' + String(src.sha256).slice(0, 12) + ')'
|
|
171
|
+
: '源帧 seq=' + String(r.seq) + '(' + String(r.ctrlW) + 'x' + String(r.ctrlH) + ',L0 热缓存)';
|
|
172
|
+
const spaceLine = space && space.current
|
|
173
|
+
? ' 空间核对:源 ' + String(r.ctrlW) + 'x' + String(r.ctrlH) + (space.match ? ' = ' : ' ≠ ') +
|
|
174
|
+
'当前控制器 ' + space.current.w + 'x' + space.current.h
|
|
175
|
+
: ' 空间核对:未连接设备,未与当前控制器尺寸比对(模板空间 = 源帧捕获时尺寸 ' +
|
|
176
|
+
String(r.ctrlW) + 'x' + String(r.ctrlH) + ')';
|
|
177
|
+
const human = [
|
|
178
|
+
...describeSession(s, ctx.flags.verbose),
|
|
179
|
+
...(offline ? ['离线裁剪:未连接设备(留存帧是本机持久文件,裁剪不需要设备)'] : []),
|
|
180
|
+
'模板 ' + String(r.w) + 'x' + String(r.h) + ' → ' + String(r.path),
|
|
181
|
+
' ' + srcLine,
|
|
182
|
+
' 宽松框 ' + JSON.stringify(r.loose) + ' → 收紧 ' + JSON.stringify(box) +
|
|
183
|
+
(tg?.from && tg?.to
|
|
184
|
+
? '(' + tg.from.join('×') + ' → ' + tg.to.join('×') +
|
|
185
|
+
(tg.areaRatio !== null && tg.areaRatio !== undefined ? ',面积 ' + tg.areaRatio + '×' : '') +
|
|
186
|
+
(tg.grew ? ',反而放大' : '') + ')'
|
|
187
|
+
: '') +
|
|
188
|
+
(tg?.looseMatch
|
|
189
|
+
? ';宽松框自匹配 ' + tg.looseMatch.score + (tg.looseMatch.positionOk ? '(位置正确,说明得分分辨不出贴合度)' : '(位置漂移)')
|
|
190
|
+
: '') +
|
|
191
|
+
(r.snapped ? '' : '(snap 无内容收紧,用原框)'),
|
|
192
|
+
' 自匹配 ' + (r.positionOk === false ? '位置错误(best 落在 ' + JSON.stringify(r.selfMatchBox ?? null) + ')' : '位置正确') +
|
|
193
|
+
',得分 ' + String(r.score) + '(' + String(r.tries) + ' 次评估)',
|
|
194
|
+
...(cross
|
|
195
|
+
? [' 跨帧验证 seq=' + String(cross.seq) + ':得分 ' + String(cross.score) +
|
|
196
|
+
(cross.posOk ? ',位置正确' : ',位置漂移 ' + JSON.stringify(cross.box ?? null))]
|
|
197
|
+
: [kfSource
|
|
198
|
+
? ' 跨帧验证:跳过(源为留存帧,它对应的状态通常已不在当前画面上;状态可复现时给 --cross)'
|
|
199
|
+
: (ctx.values['no-cross'] === true
|
|
200
|
+
? ' 跨帧验证:已按 --no-cross 关闭'
|
|
201
|
+
: ' 跨帧验证:未做(未连接设备)')]),
|
|
202
|
+
spaceLine,
|
|
203
|
+
...(r.provPath
|
|
204
|
+
? ['L2 出处:' + String(r.provPath) + '(来源 + 裁剪变换;随模板一起提交进仓库)']
|
|
205
|
+
: ['L2 出处:未写(--no-prov)']),
|
|
206
|
+
...(r.keptSource && r.keptSource.id
|
|
207
|
+
? ['源帧已留档:' + String(r.keptSource.id) + '(出处随之升级为可复核的 kf: 身份)']
|
|
208
|
+
: []),
|
|
209
|
+
...(r.warns ?? []).map((w) => ' 警告:' + w),
|
|
210
|
+
'pipeline 用法:roi ' + box.join(',') + ' + template 该文件(同帧验证:maafw-live reco --node …)',
|
|
211
|
+
];
|
|
212
|
+
const warns = r.warns ?? [];
|
|
213
|
+
return {
|
|
214
|
+
exitCode: warns.length ? EXIT.FINDINGS : EXIT.OK,
|
|
215
|
+
human,
|
|
216
|
+
data: r,
|
|
217
|
+
written: [String(r.path), ...(r.provPath ? [String(r.provPath)] : [])],
|
|
218
|
+
...(warns.length ? { warnings: warns } : {}),
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
catch (e) {
|
|
223
|
+
return daemonFail(e);
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
export const CROP_COMMANDS = [cropCommand];
|
|
228
|
+
//# sourceMappingURL=crop.js.map
|