cardbuddy 0.14.3 → 0.16.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/dist/cli.js +0 -0
- package/dist/preview-editor/assets/index-BkbBxDTT.css +1 -0
- package/dist/preview-editor/assets/index-Bro07DTa.js +313 -0
- package/dist/preview-editor/index.html +2 -2
- package/dist/standalone.js +3 -0
- package/dist/vendored/card-editor/runtime/adapter.js +2 -2
- package/dist/vendored/mcp/client.mjs +9 -2
- package/dist/vendored/server/core/ai-chat.js +1702 -134
- package/dist/vendored/server/core/browser.js +12 -3
- package/dist/vendored/server/core/cardFiles.js +3 -0
- package/dist/vendored/server/core/cardHistory.js +1 -0
- package/dist/vendored/server/core/cardImportMeta.js +7 -6
- package/dist/vendored/server/core/cardOpenFolder.js +1 -0
- package/dist/vendored/server/core/cardSave.js +1 -0
- package/dist/vendored/server/core/cardSync.js +3 -0
- package/dist/vendored/server/core/cardValidate.js +16 -5
- package/dist/vendored/server/core/codeBridge.js +5 -1
- package/dist/vendored/server/core/logStream.js +1 -0
- package/dist/vendored/server/core/sliceStatic.js +24 -4
- package/dist/vendored/server/core/sliceUpload.js +1 -0
- package/dist/vendored/server/core/turnJournal.js +6 -0
- package/dist/vendored/skills/card-skill-trainer/SKILL.md +257 -201
- package/dist/vendored/skills/card-skill-trainer/scripts/diagnose_case.mjs +14 -82
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/diagnosis.mjs +207 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/layout-correction-sim.mjs +62 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/paradigm-router.mjs +76 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/pixel-worker-runner.mjs +31 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/pixel-worker.mjs +138 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/scoring.mjs +24 -20
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/telemetry.mjs +46 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/plan_next_round.mjs +4 -1
- package/dist/vendored/skills/card-skill-trainer/scripts/run_round.mjs +279 -90
- package/dist/vendored/skills/card-skill-trainer/scripts/score_round.mjs +74 -2
- package/dist/vendored/skills/card-skill-trainer/scripts/sink_rules.mjs +127 -0
- package/dist/vendored/skills/dsl-card-ai/SKILL.md +1 -1
- package/dist/vendored/skills/dsl-card-generator/SKILL.md +208 -380
- package/dist/vendored/skills/dsl-card-generator/references/dsl_structure.md +1 -15
- package/dist/vendored/skills/dsl-card-generator/references/expression_engine.md +11 -20
- package/dist/vendored/skills/dsl-card-generator/references/generation_pitfalls.md +233 -61
- package/dist/vendored/skills/dsl-card-generator/references/ir-format.md +202 -4
- package/dist/vendored/skills/dsl-card-generator/references/iteration_protocol.md +87 -0
- package/dist/vendored/skills/dsl-card-generator/references/previewer_contract.md +1 -1
- package/dist/vendored/skills/dsl-card-generator/references/quickref.md +42 -33
- package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/title-underline-overlay.json +59 -11
- package/dist/vendored/skills/dsl-card-generator/references/recipes.md +44 -8
- package/dist/vendored/skills/dsl-card-generator/references/rules.md +26 -6
- package/dist/vendored/skills/dsl-card-generator/references/scripts_reference.md +54 -0
- package/dist/vendored/skills/dsl-card-generator/references/semantic_adjudication.md +23 -0
- package/dist/vendored/skills/dsl-card-generator/references/slices_convention.md +6 -6
- package/dist/vendored/skills/dsl-card-generator/references/spec_to_schema_patterns.md +33 -4
- package/dist/vendored/skills/dsl-card-generator/references/ui_fidelity_rules.md +13 -1
- package/dist/vendored/skills/dsl-card-generator/scripts/add_component.mjs +5 -14
- package/dist/vendored/skills/dsl-card-generator/scripts/autofix_schema.py +133 -2
- package/dist/vendored/skills/dsl-card-generator/scripts/check_fidelity.mjs +226 -19
- package/dist/vendored/skills/dsl-card-generator/scripts/check_magic_values.mjs +6 -1
- package/dist/vendored/skills/dsl-card-generator/scripts/check_topology.py +0 -35
- package/dist/vendored/skills/dsl-card-generator/scripts/crop_slices.mjs +92 -6
- package/dist/vendored/skills/dsl-card-generator/scripts/expand_ir.mjs +277 -27
- package/dist/vendored/skills/dsl-card-generator/scripts/gate.mjs +147 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/generate_cards.mjs +12 -8
- package/dist/vendored/skills/dsl-card-generator/scripts/ir_merge.mjs +288 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/ir_restore.mjs +175 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/iterate.mjs +1015 -54
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/component_contract.mjs +315 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/geometry_audit.mjs +131 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/layout-tokens.mjs +1 -1
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/layout_rules.mjs +160 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.mjs +23 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.py +0 -37
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/python.mjs +99 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/adaptive-width.mjs +105 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/bindings.mjs +260 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/expr.mjs +52 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/image.mjs +96 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/registry.mjs +166 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/structure.mjs +127 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/walk.mjs +61 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/scope_alias.mjs +125 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/slicePath.mjs +17 -5
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/slice_dedupe.mjs +102 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_assemble.mjs +5727 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_extract.mjs +4214 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_shared.mjs +1110 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lint_card.mjs +322 -71
- package/dist/vendored/skills/dsl-card-generator/scripts/preflight.mjs +87 -43
- package/dist/vendored/skills/dsl-card-generator/scripts/readiness.mjs +634 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/register_card_template.mjs +5 -8
- package/dist/vendored/skills/dsl-card-generator/scripts/render_card.mjs +91 -43
- package/dist/vendored/skills/dsl-card-generator/scripts/route_input.mjs +7 -11
- package/dist/vendored/skills/dsl-card-generator/scripts/spec_audit.mjs +272 -46
- package/dist/vendored/skills/dsl-card-generator/scripts/spec_to_schema_v3.mjs +43 -10499
- package/dist/vendored/skills/dsl-card-generator/scripts/v3_regression.mjs +188 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/validate_schema.py +23 -15
- package/dist/vendored/skills/shared/exit-codes.md +79 -0
- package/dist/vendored/skills/shared/spec-contract.md +118 -0
- package/dist/vendored/skills/sketch-spec-to-card/SKILL.md +43 -20
- package/dist/vendored/skills/sketch-spec-to-card/references/spec-schema.md +4 -4
- package/dist/vendored/skills/sketch-spec-to-card/scripts/build_prompt.py +47 -9
- package/dist/vendored/skills/sketch-spec-to-card/scripts/cluster_states.py +14 -25
- package/dist/vendored/skills/sketch-spec-to-card/scripts/parse_meaxure.py +546 -91
- package/dist/vendored/skills/sketch-spec-to-card/scripts/png_utils.py +51 -5
- package/dist/vendored/skills/sketch-spec-to-card/scripts/test_text_only_artboard.py +217 -0
- package/dist/vendored/skills/sketch-spec-to-card/scripts/vision.py +64 -34
- package/package.json +1 -1
- package/dist/preview-editor/assets/index-Cm0USikc.js +0 -307
- package/dist/preview-editor/assets/index-cbTCHp8J.css +0 -1
- package/dist/vendored/skills/card-skill-trainer/scripts/test_missing_slice.py +0 -122
- package/dist/vendored/skills/dsl-card-generator/scripts/analyze_component_usage.mjs +0 -153
|
@@ -24,8 +24,9 @@ import path from 'node:path'
|
|
|
24
24
|
import { pathToFileURL } from 'node:url'
|
|
25
25
|
import { PNG } from 'pngjs'
|
|
26
26
|
import {
|
|
27
|
-
attributeFailure, evaluatePassGate, exclusionCoverage, failureSignature,
|
|
27
|
+
attributeFailure, evaluatePassGate, exclusionCoverage, failureSignature,
|
|
28
28
|
} from './lib/scoring.mjs'
|
|
29
|
+
import { pixelBands, pairDeviations, deriveHints } from './lib/diagnosis.mjs'
|
|
29
30
|
|
|
30
31
|
const repoRoot = path.resolve(import.meta.dirname, '../../..')
|
|
31
32
|
|
|
@@ -118,72 +119,8 @@ function buildSideImage(designPng, renderPng, sidePath) {
|
|
|
118
119
|
return sidePath
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const h = Math.min(designPng.height, renderPng.height)
|
|
124
|
-
const mask = new Uint8Array(w * h)
|
|
125
|
-
for (const rc of exclusions || []) {
|
|
126
|
-
const x0 = Math.floor((rc.x ?? 0) * w), y0 = Math.floor((rc.y ?? 0) * h)
|
|
127
|
-
const x1 = Math.min(w, Math.ceil(((rc.x ?? 0) + (rc.width ?? 0)) * w))
|
|
128
|
-
const y1 = Math.min(h, Math.ceil(((rc.y ?? 0) + (rc.height ?? 0)) * h))
|
|
129
|
-
for (let y = y0; y < y1; y++) for (let x = x0; x < x1; x++) mask[y * w + x] = 1
|
|
130
|
-
}
|
|
131
|
-
const BANDS = 20
|
|
132
|
-
const bandDiff = new Array(BANDS).fill(0)
|
|
133
|
-
const bandTotal = new Array(BANDS).fill(0)
|
|
134
|
-
let totalDiff = 0
|
|
135
|
-
let totalCount = 0
|
|
136
|
-
for (let y = 0; y < h; y++) {
|
|
137
|
-
const band = Math.min(BANDS - 1, Math.floor((y / h) * BANDS))
|
|
138
|
-
for (let x = 0; x < w; x++) {
|
|
139
|
-
if (mask[y * w + x]) continue
|
|
140
|
-
const i = (y * designPng.width + x) * 4
|
|
141
|
-
const j = (y * renderPng.width + x) * 4
|
|
142
|
-
const diff = Math.abs(designPng.data[i] - renderPng.data[j]) > 15
|
|
143
|
-
|| Math.abs(designPng.data[i + 1] - renderPng.data[j + 1]) > 15
|
|
144
|
-
|| Math.abs(designPng.data[i + 2] - renderPng.data[j + 2]) > 15
|
|
145
|
-
if (diff) { bandDiff[band]++; totalDiff++ }
|
|
146
|
-
bandTotal[band]++; totalCount++
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
const bandPct = bandDiff.map((c, i) => (bandTotal[i] ? c / bandTotal[i] : 0))
|
|
150
|
-
const hotBands = bandPct.map((pct, i) => ({ band: i, y: Math.round((i / BANDS) * h), pct }))
|
|
151
|
-
.filter((b) => b.pct >= 0.05).sort((a, b) => b.pct - a.pct)
|
|
152
|
-
return { hotBands, diffRatio: totalCount ? totalDiff / totalCount : null }
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function pairDeviations(specTexts, renderTexts) {
|
|
156
|
-
const usedSpec = new Set()
|
|
157
|
-
const deviations = []
|
|
158
|
-
for (const rt of renderTexts) {
|
|
159
|
-
const key = norm(rt.text)
|
|
160
|
-
if (!key) continue
|
|
161
|
-
let best = null
|
|
162
|
-
let bestScore = 0
|
|
163
|
-
for (let i = 0; i < specTexts.length; i++) {
|
|
164
|
-
if (usedSpec.has(i)) continue
|
|
165
|
-
const st = specTexts[i]
|
|
166
|
-
let score = 0
|
|
167
|
-
if (st.text === key) score = 1
|
|
168
|
-
else if (st.text.includes(key) || key.includes(st.text)) score = 0.9
|
|
169
|
-
if (score > bestScore) { bestScore = score; best = { ...st, idx: i } }
|
|
170
|
-
}
|
|
171
|
-
if (!best || bestScore < 0.9) continue
|
|
172
|
-
usedSpec.add(best.idx)
|
|
173
|
-
deviations.push({
|
|
174
|
-
text: key.slice(0, 18),
|
|
175
|
-
dy: +(rt.y - best.y).toFixed(1),
|
|
176
|
-
dx: +(rt.x - best.x).toFixed(1),
|
|
177
|
-
dw: +((rt.w || 0) - best.w).toFixed(1),
|
|
178
|
-
dh: +((rt.h || 0) - best.h).toFixed(1),
|
|
179
|
-
render: { x: rt.x, y: rt.y, w: rt.w, h: rt.h },
|
|
180
|
-
spec: { x: best.x, y: best.y, w: best.w, h: best.h, fs: best.fs, lineHeight: best.lineHeight },
|
|
181
|
-
cid: rt.cid,
|
|
182
|
-
})
|
|
183
|
-
}
|
|
184
|
-
deviations.sort((a, b) => (Math.abs(b.dy) + Math.abs(b.dx)) - (Math.abs(a.dy) + Math.abs(a.dx)))
|
|
185
|
-
return deviations
|
|
186
|
-
}
|
|
122
|
+
// pixelBands / pairDeviations / deriveHints 已迁至 lib/diagnosis.mjs(run_round 与
|
|
123
|
+
// diagnose_case 共用单一来源,避免两处漂移);本脚本仅按需引入。
|
|
187
124
|
|
|
188
125
|
async function main() {
|
|
189
126
|
const results = []
|
|
@@ -302,23 +239,18 @@ async function main() {
|
|
|
302
239
|
provHits = [{ error: String(e.message || e) }]
|
|
303
240
|
}
|
|
304
241
|
|
|
305
|
-
// —— 5.
|
|
242
|
+
// —— 5. 失败类建议(与 run_round 在环归因共用 deriveHints,单一口径)——
|
|
306
243
|
const px = report.pixel || {}
|
|
307
244
|
const fid = report.fidelity || {}
|
|
308
|
-
const hints =
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
hints.push(`掩膜覆盖 ${(coverage.total * 100).toFixed(1)}% ≥ ${(MASK_REVIEW_THRESHOLD * 100).toFixed(0)}%:得分不代表整卡,必须看图复核`)
|
|
318
|
-
}
|
|
319
|
-
const bigDy = deviations.filter((d) => Math.abs(d.dy) >= 3)
|
|
320
|
-
if (bigDy.length >= 3) hints.push(`${bigDy.length} 个文本 |dy|≥3:纵向节奏问题(行距/padding 体系),非单点`)
|
|
321
|
-
if (!hints.length) hints.push('像素差异弥散且量级低:多为 1-2px 抗锯齿/字体度量差异,接近噪声底')
|
|
245
|
+
const hints = deriveHints({
|
|
246
|
+
attribution: attribution ? { failureClass: attribution.failureClass, reason: attribution.reason } : null,
|
|
247
|
+
pixel: px,
|
|
248
|
+
fidelity: fid,
|
|
249
|
+
duplication: report.duplication || null,
|
|
250
|
+
coverage,
|
|
251
|
+
hotBands: bands.hotBands,
|
|
252
|
+
deviations,
|
|
253
|
+
})
|
|
322
254
|
|
|
323
255
|
// —— 6. 输出 ——
|
|
324
256
|
console.log('═══════════════════════════════════════════════════════════')
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* diagnosis.mjs — 失败案例结构化证据包的纯函数核心(2026-09-25)。
|
|
4
|
+
*
|
|
5
|
+
* 单一来源:run_round.mjs(在环归因,零额外渲染复用已有数据)与 diagnose_case.mjs
|
|
6
|
+
* (按需深度诊断,含 provenance 溯源)共用本模块,避免两处漂移。
|
|
7
|
+
*
|
|
8
|
+
* 导出:
|
|
9
|
+
* - decodeBufferToPng(buf) Buffer → PNG.sync 对象(失败返回 null)
|
|
10
|
+
* - pixelBands(design, render, exclusions) 逐带像素差异率 + 热区带
|
|
11
|
+
* - pairDeviations(specTexts, renderTexts) 标注几何 vs 渲染盒偏差表(dy/dx)
|
|
12
|
+
* - deriveHints(ctx) 失败类建议(与评分口径同源)
|
|
13
|
+
* - buildInlineDiagnosis(report, ctx) run_round 在环归因用的轻量结构化包
|
|
14
|
+
*
|
|
15
|
+
* 设计约束:本模块只做纯计算,不触碰 Chrome / 文件系统 / 任何门禁口径。
|
|
16
|
+
* 图像解码只在 buildInlineDiagnosis 内按需发生,且仅针对「确实失败且像素可比对」的案例。
|
|
17
|
+
*/
|
|
18
|
+
import { PNG } from 'pngjs'
|
|
19
|
+
import { MASK_REVIEW_THRESHOLD, exclusionCoverage } from './scoring.mjs'
|
|
20
|
+
|
|
21
|
+
const norm = (value) => String(value || '').replace(/\s+/g, '')
|
|
22
|
+
|
|
23
|
+
export function decodeBufferToPng(buf) {
|
|
24
|
+
try { return buf ? PNG.sync.read(buf) : null } catch { return null }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function pixelBands(designPng, renderPng, exclusions) {
|
|
28
|
+
const w = Math.min(designPng.width, renderPng.width)
|
|
29
|
+
const h = Math.min(designPng.height, renderPng.height)
|
|
30
|
+
const mask = new Uint8Array(w * h)
|
|
31
|
+
for (const rc of exclusions || []) {
|
|
32
|
+
const x0 = Math.floor((rc.x ?? 0) * w), y0 = Math.floor((rc.y ?? 0) * h)
|
|
33
|
+
const x1 = Math.min(w, Math.ceil(((rc.x ?? 0) + (rc.width ?? 0)) * w))
|
|
34
|
+
const y1 = Math.min(h, Math.ceil(((rc.y ?? 0) + (rc.height ?? 0)) * h))
|
|
35
|
+
for (let y = y0; y < y1; y++) for (let x = x0; x < x1; x++) mask[y * w + x] = 1
|
|
36
|
+
}
|
|
37
|
+
const BANDS = 20
|
|
38
|
+
const bandDiff = new Array(BANDS).fill(0)
|
|
39
|
+
const bandTotal = new Array(BANDS).fill(0)
|
|
40
|
+
let totalDiff = 0
|
|
41
|
+
let totalCount = 0
|
|
42
|
+
for (let y = 0; y < h; y++) {
|
|
43
|
+
const band = Math.min(BANDS - 1, Math.floor((y / h) * BANDS))
|
|
44
|
+
for (let x = 0; x < w; x++) {
|
|
45
|
+
if (mask[y * w + x]) continue
|
|
46
|
+
const i = (y * designPng.width + x) * 4
|
|
47
|
+
const j = (y * renderPng.width + x) * 4
|
|
48
|
+
const diff = Math.abs(designPng.data[i] - renderPng.data[j]) > 15
|
|
49
|
+
|| Math.abs(designPng.data[i + 1] - renderPng.data[j + 1]) > 15
|
|
50
|
+
|| Math.abs(designPng.data[i + 2] - renderPng.data[j + 2]) > 15
|
|
51
|
+
if (diff) { bandDiff[band]++; totalDiff++ }
|
|
52
|
+
bandTotal[band]++; totalCount++
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const bandPct = bandDiff.map((c, i) => (bandTotal[i] ? c / bandTotal[i] : 0))
|
|
56
|
+
const hotBands = bandPct.map((pct, i) => ({ band: i, y: Math.round((i / BANDS) * h), pct }))
|
|
57
|
+
.filter((b) => b.pct >= 0.05).sort((a, b) => b.pct - a.pct)
|
|
58
|
+
return { hotBands, diffRatio: totalCount ? totalDiff / totalCount : null }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function pairDeviations(specTexts, renderTexts) {
|
|
62
|
+
const usedSpec = new Set()
|
|
63
|
+
const deviations = []
|
|
64
|
+
for (const rt of renderTexts) {
|
|
65
|
+
const key = norm(rt.text)
|
|
66
|
+
if (!key) continue
|
|
67
|
+
let best = null
|
|
68
|
+
let bestScore = 0
|
|
69
|
+
let bestDist = Infinity
|
|
70
|
+
for (let i = 0; i < specTexts.length; i++) {
|
|
71
|
+
if (usedSpec.has(i)) continue
|
|
72
|
+
const st = specTexts[i]
|
|
73
|
+
let score = 0
|
|
74
|
+
if (st.text === key) score = 1
|
|
75
|
+
else if (st.text.includes(key) || key.includes(st.text)) score = 0.9
|
|
76
|
+
// Geometric tiebreaker: when multiple specs have same similarity (e.g. identical texts
|
|
77
|
+
// like two "转账计划"), prefer the nearest by center distance — crossed matching produces
|
|
78
|
+
// false huge dy/dx that misleads the diagnosis.
|
|
79
|
+
const dist = Math.hypot(((st.x||0)+(st.w||0)/2)-((rt.x||0)+(rt.w||0)/2), ((st.y||0)+(st.h||0)/2)-((rt.y||0)+(rt.h||0)/2))
|
|
80
|
+
if (score > bestScore + 1e-9 || (score >= bestScore - 1e-9 && dist < bestDist)) {
|
|
81
|
+
bestScore = score; best = { ...st, idx: i }; bestDist = dist
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!best || bestScore < 0.9) continue
|
|
85
|
+
usedSpec.add(best.idx)
|
|
86
|
+
deviations.push({
|
|
87
|
+
text: key.slice(0, 18),
|
|
88
|
+
dy: +(rt.y - best.y).toFixed(1),
|
|
89
|
+
dx: +(rt.x - best.x).toFixed(1),
|
|
90
|
+
dw: +((rt.w || 0) - best.w).toFixed(1),
|
|
91
|
+
dh: +((rt.h || 0) - best.h).toFixed(1),
|
|
92
|
+
render: { x: rt.x, y: rt.y, w: rt.w, h: rt.h },
|
|
93
|
+
spec: { x: best.x, y: best.y, w: best.w, h: best.h, fs: best.fs, lineHeight: best.lineHeight },
|
|
94
|
+
cid: rt.cid,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
deviations.sort((a, b) => (Math.abs(b.dy) + Math.abs(b.dx)) - (Math.abs(a.dy) + Math.abs(a.dx)))
|
|
98
|
+
return deviations
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 失败类建议:与 lib/scoring.mjs 的归因/门禁口径同源,run_round 与 diagnose_case 共用。
|
|
103
|
+
* ctx: { attribution:{failureClass,reason}, pixel, fidelity, duplication, coverage, hotBands, deviations }
|
|
104
|
+
*/
|
|
105
|
+
export function deriveHints(ctx) {
|
|
106
|
+
const {
|
|
107
|
+
attribution = null, pixel = {}, fidelity = {}, duplication = null,
|
|
108
|
+
coverage = {}, hotBands = [], deviations = [],
|
|
109
|
+
} = ctx || {}
|
|
110
|
+
const num = (v, d = 4) => (typeof v === 'number' && Number.isFinite(v)) ? v.toFixed(d) : 'N/A'
|
|
111
|
+
const hints = []
|
|
112
|
+
if (attribution) hints.push(`归因 ${attribution.failureClass}:${attribution.reason}`)
|
|
113
|
+
if (pixel.bestAlignDy && Math.abs(pixel.bestAlignDy) >= 4) hints.push(`全局纵向偏移 dy=${pixel.bestAlignDy}:查行高/行距累积(Input 高度、行距标注优先、分割线渲染盒高)`)
|
|
114
|
+
if (hotBands.length && hotBands[0].pct >= 0.2) hints.push(`单带高热 (${(hotBands[0].pct * 100).toFixed(0)}%@y≈${hotBands[0].y}):查该区域语义块(换行折点/缺组件/结构错配)`)
|
|
115
|
+
if ((fidelity.components ?? 1) < 0.9) hints.push(`组件计数缺失 comp=${num(fidelity.components, 2)}:查语义消费(媒体未渲染/文本被吞/重复消费)`)
|
|
116
|
+
if ((duplication?.overall ?? 1) < 0.95) {
|
|
117
|
+
hints.push(`重影:查同源按钮/文本双路径渲染(${(duplication?.offenders || []).map((o) => `「${o.text}」x${o.rendered}/${o.annotated}`).join(' ')})`)
|
|
118
|
+
}
|
|
119
|
+
const covTotal = coverage?.total ?? 0
|
|
120
|
+
if (covTotal >= MASK_REVIEW_THRESHOLD) {
|
|
121
|
+
hints.push(`掩膜覆盖 ${(covTotal * 100).toFixed(1)}% ≥ ${(MASK_REVIEW_THRESHOLD * 100).toFixed(0)}%:得分不代表整卡,必须看图复核`)
|
|
122
|
+
}
|
|
123
|
+
const bigDy = deviations.filter((d) => Math.abs(d.dy) >= 3)
|
|
124
|
+
if (bigDy.length >= 3) hints.push(`${bigDy.length} 个文本 |dy|≥3:纵向节奏问题(行距/padding 体系),非单点`)
|
|
125
|
+
if (!hints.length) hints.push('像素差异弥散且量级低:多为 1-2px 抗锯齿/字体度量差异,接近噪声底')
|
|
126
|
+
return hints
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function annDefectCount(upstream) {
|
|
130
|
+
if (!upstream) return 0
|
|
131
|
+
const ad = upstream.annotationDefects
|
|
132
|
+
if (Array.isArray(ad)) return ad.length
|
|
133
|
+
return typeof ad === 'number' ? ad : 0
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* run_round 在环归因用的轻量结构化包:复用 processCase 已算出的 report / 设计稿 buffer /
|
|
138
|
+
* 标注文本 / 渲染盒,零额外 Chrome 渲染。provenance(cid→生成器源码行)因需 SCHEMA_PROV
|
|
139
|
+
* 重生成,不在本包内——按需由 diagnose_case.mjs 深度补全。
|
|
140
|
+
*
|
|
141
|
+
* ctx: { designBuf, renderBuf, specTexts, renderedTexts }
|
|
142
|
+
* specTexts: run_round 过滤后的标注文本数组(含 rawText/x/y/w/h/fs)
|
|
143
|
+
* renderedTexts: 渲染盒数组(含 text/x/y/w/h/cid,已过滤 Text/Input/Button)
|
|
144
|
+
*/
|
|
145
|
+
export function buildInlineDiagnosis(report, ctx) {
|
|
146
|
+
const { designBuf = null, renderBuf = null, specTexts = [], renderedTexts = [] } = ctx || {}
|
|
147
|
+
const designPng = designBuf ? decodeBufferToPng(designBuf) : null
|
|
148
|
+
const renderPng = renderBuf ? decodeBufferToPng(renderBuf) : null
|
|
149
|
+
const exclusions = report?.pixelExclusions || []
|
|
150
|
+
const bands = (designPng && renderPng)
|
|
151
|
+
? pixelBands(designPng, renderPng, exclusions)
|
|
152
|
+
: { hotBands: [], diffRatio: null }
|
|
153
|
+
|
|
154
|
+
const specForDev = specTexts.map((el) => ({
|
|
155
|
+
text: norm(el.rawText ?? el.text ?? ''),
|
|
156
|
+
x: el.x ?? 0, y: el.y ?? 0, w: el.w ?? 0, h: el.h ?? 0,
|
|
157
|
+
fs: el.fs ?? el.textStyle?.fontSize ?? 14, lineHeight: el.lineHeight ?? null,
|
|
158
|
+
}))
|
|
159
|
+
const renderForDev = renderedTexts.map((el) => ({
|
|
160
|
+
text: el.text ?? '', x: el.x ?? 0, y: el.y ?? 0, w: el.w ?? 0, h: el.h ?? 0, cid: el.cid,
|
|
161
|
+
}))
|
|
162
|
+
const deviations = pairDeviations(specForDev, renderForDev)
|
|
163
|
+
|
|
164
|
+
const coverage = report?.exclusionCoverage || exclusionCoverage(exclusions)
|
|
165
|
+
const attribution = report?.failureClass
|
|
166
|
+
? { failureClass: report.failureClass, reason: report.failureReason || '' }
|
|
167
|
+
: null
|
|
168
|
+
const hints = deriveHints({
|
|
169
|
+
attribution,
|
|
170
|
+
pixel: report?.pixel || {},
|
|
171
|
+
fidelity: report?.fidelity || {},
|
|
172
|
+
duplication: report?.duplication || null,
|
|
173
|
+
coverage,
|
|
174
|
+
hotBands: bands.hotBands,
|
|
175
|
+
deviations,
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
gateFailures: report?.gateFailures || [],
|
|
180
|
+
failureClass: attribution?.failureClass || null,
|
|
181
|
+
failureReason: attribution?.reason || null,
|
|
182
|
+
metrics: {
|
|
183
|
+
fidelity: report?.fidelity?.overall ?? null,
|
|
184
|
+
pixel: report?.pixel?.ratio ?? null,
|
|
185
|
+
structure: report?.pixel?.structure ?? null,
|
|
186
|
+
dimension: report?.pixel?.dimension ?? null,
|
|
187
|
+
color: report?.pixel?.color ?? null,
|
|
188
|
+
position: report?.position?.overall ?? null,
|
|
189
|
+
components: report?.fidelity?.components ?? null,
|
|
190
|
+
duplication: report?.duplication?.overall ?? null,
|
|
191
|
+
bestAlignDy: report?.pixel?.bestAlignDy ?? null,
|
|
192
|
+
},
|
|
193
|
+
exclusionCoverage: coverage,
|
|
194
|
+
upstream: report?.upstream ? {
|
|
195
|
+
passed: report.upstream.passed,
|
|
196
|
+
annotationCompleteness: report.upstream.annotationCompleteness,
|
|
197
|
+
textRetention: report.upstream.textRetention,
|
|
198
|
+
structuralFocus: report.upstream.structuralFocus,
|
|
199
|
+
annotationDefects: annDefectCount(report.upstream),
|
|
200
|
+
} : null,
|
|
201
|
+
hotBands: bands.hotBands.slice(0, 8),
|
|
202
|
+
deviations: deviations.slice(0, 10),
|
|
203
|
+
layoutCorrections: report?.layoutCorrections || [],
|
|
204
|
+
correctorRejected: report?.correctorRejected || null,
|
|
205
|
+
hints,
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Layout-correction rigid-body simulation for the trainer's reject fast-path.
|
|
2
|
+
//
|
|
3
|
+
// When correctCardLayout only adjusts paddingTop (Y axis) inside a vertical Column,
|
|
4
|
+
// the correction is an exact rigid body shift: shifting child#i by Δ also shifts every
|
|
5
|
+
// later sibling by the cumulative Δ. The same logic extends to paddingLeft (X axis):
|
|
6
|
+
// a child's paddingLeft change shifts that child's whole subtree horizontally, but —
|
|
7
|
+
// unlike Y — does NOT cascade to later siblings in a vertical flow (each child keeps
|
|
8
|
+
// its own independent horizontal padding). So X deltas are per-child and non-cumulative,
|
|
9
|
+
// while Y deltas accumulate across siblings.
|
|
10
|
+
//
|
|
11
|
+
// Keeping this here (instead of inline in run_round.mjs) makes the admit/translate rules
|
|
12
|
+
// unit-testable without spinning up Chrome. Zero gate behavior changes: the simulator
|
|
13
|
+
// only decides whether a real re-render can be skipped; the final keep/revert decision
|
|
14
|
+
// always goes through the real position + pixel guard.
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Whether the rigid-body reject fast-path may replace a real re-render.
|
|
18
|
+
* @param {Array} adjustments entries pushed by correctCardLayout (axis 'y'|'x', rigidSafe)
|
|
19
|
+
* @param {Map} cidToChild cid -> child index (built inside correctCardLayout)
|
|
20
|
+
* @param {Array} [children] card column direct children (to exclude AbsoluteLayout)
|
|
21
|
+
*/
|
|
22
|
+
export function canSimulateReject(adjustments, cidToChild, children) {
|
|
23
|
+
if (!adjustments?.length || !cidToChild) return false
|
|
24
|
+
if (!adjustments.every((a) => a.rigidSafe !== false && (a.axis === 'y' || a.axis === 'x'))) return false
|
|
25
|
+
// AbsoluteLayout children break the simple rigid model (their own left/positioning
|
|
26
|
+
// context means paddingLeft/paddingTop do not translate the subtree predictably).
|
|
27
|
+
if (children && !children.every((c) => c.name !== 'AbsoluteLayout')) return false
|
|
28
|
+
return true
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Apply the rigid-body deltas to rendered boxes, returning shifted copies.
|
|
33
|
+
* Boxes whose cid is not mapped to a child are returned unchanged.
|
|
34
|
+
* @returns {{ shifted: Array, appliedAxes: Set<string> }}
|
|
35
|
+
*/
|
|
36
|
+
export function simulateLayoutCorrection(boxes, adjustments, cidToChild) {
|
|
37
|
+
const deltasY = new Map()
|
|
38
|
+
const deltasX = new Map()
|
|
39
|
+
const appliedAxes = new Set()
|
|
40
|
+
for (const a of adjustments || []) {
|
|
41
|
+
if (a.axis === 'y') {
|
|
42
|
+
deltasY.set(a.childIndex, (deltasY.get(a.childIndex) || 0) + (a.to - a.from))
|
|
43
|
+
appliedAxes.add('y')
|
|
44
|
+
} else if (a.axis === 'x') {
|
|
45
|
+
// X is per-child, no cross-sibling cascade.
|
|
46
|
+
deltasX.set(a.childIndex, (deltasX.get(a.childIndex) || 0) + (a.to - a.from))
|
|
47
|
+
appliedAxes.add('x')
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const shiftForY = (idx) => {
|
|
51
|
+
let acc = 0
|
|
52
|
+
for (const [i, d] of deltasY) if (i <= idx) acc += d
|
|
53
|
+
return acc
|
|
54
|
+
}
|
|
55
|
+
const shiftForX = (idx) => deltasX.get(idx) || 0
|
|
56
|
+
const shifted = (boxes || []).map((b) => {
|
|
57
|
+
const idx = cidToChild.get(b.cid)
|
|
58
|
+
if (idx == null) return b
|
|
59
|
+
return { ...b, y: b.y + shiftForY(idx), x: b.x + shiftForX(idx) }
|
|
60
|
+
})
|
|
61
|
+
return { shifted, appliedAxes }
|
|
62
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// paradigm-router.mjs — 把高频失败签名映射到已沉淀的生成器通用范式引用。
|
|
2
|
+
//
|
|
3
|
+
// 用途:plan_next_round 的 `same-signature-3-rounds` 信号原本只说「脚本补丁无效,
|
|
4
|
+
// 需升级 Skill 级范式」——笼统、Agent 仍需从零救火。本模块把维度级签名(不认业务词)
|
|
5
|
+
// 路由到已落盘的 recipes 章节 / v3 链路铁律 / 生成器识别 intent,让 Agent 直接套用范式,
|
|
6
|
+
// 把迭代预算从「每失败例救火」转向「每签名沉淀一条通用规则」(对应 trainer 效率维度三)。
|
|
7
|
+
//
|
|
8
|
+
// 失败签名 key 形如 `${failureClass}/${signature}`:
|
|
9
|
+
// failureClass ∈ {PARSER, DSL, STYLE, DATA, ASSET, EVALUATOR}
|
|
10
|
+
// signature 是门维度(structure/position/overall/color/pixelDiff/components/...)
|
|
11
|
+
// 纯维度信号粗分模式,契合 skill 纯净铁律(禁硬编码业务实体名称)。
|
|
12
|
+
|
|
13
|
+
export const PARADIGMS = {
|
|
14
|
+
// STYLE + 结构/位置/整体维度低 = 整块漏建(图表/雷达/分区)
|
|
15
|
+
'chart-radar-block': {
|
|
16
|
+
label: '图表/雷达/分区整块漏建',
|
|
17
|
+
recipeSections: [
|
|
18
|
+
'recipes.md §1 Canvas 画布标题头(onMeasured 自绘)',
|
|
19
|
+
'recipes.md §10 Canvas 甜甜圈/环形比例图(donut chart)',
|
|
20
|
+
'v3_assemble.mjs 散点带拓扑(中心媒体 + 环绕胶囊簇)',
|
|
21
|
+
],
|
|
22
|
+
generatorIntents: [
|
|
23
|
+
'extract_handcrafted.py canvas_chart(Canvas+onMeasured 识别)',
|
|
24
|
+
'resolve_card_template.mjs 「雷达图 仪表盘」模板',
|
|
25
|
+
'v3_shared.mjs R62 图表绘制层序铁律(坐标/参考系/数据图元分层)',
|
|
26
|
+
],
|
|
27
|
+
action: '套用图表通用范式(Canvas 自绘 + onMeasured),禁止用 Text/Image 堆砌或整块漏建;治本在 IR 路径强制把图表几何信号路由到 canvas_chart 识别',
|
|
28
|
+
},
|
|
29
|
+
// STYLE + 像素/色维度低 = 底色整层错涂
|
|
30
|
+
'bg-fill': {
|
|
31
|
+
label: '底色/背景整层错涂',
|
|
32
|
+
recipeSections: ['spec_audit.mjs 底色采样(角部 + gutter 众数聚类,禁用 center 采样)'],
|
|
33
|
+
generatorIntents: ['spec_audit fill 角部 + gutter 众数聚类'],
|
|
34
|
+
action: '底色取角部/边缘众数,禁止照抄 center 点(雷达/图表中心色常被误当卡底色);治本在 spec_audit 采样逻辑',
|
|
35
|
+
},
|
|
36
|
+
// DSL + 组件计数缺口 = 媒体/列表漏建
|
|
37
|
+
'list-or-chart-missing': {
|
|
38
|
+
label: '组件/图表计数缺口(媒体或列表漏建)',
|
|
39
|
+
recipeSections: ['ir-format.md §2f 列表数组化', 'recipes.md §10 图表范式'],
|
|
40
|
+
generatorIntents: ['bindings.mjs collectArrayConsumption', 'canvas_chart intent'],
|
|
41
|
+
action: '先查 canvas_chart 识别与雷达/环图数据绑定(radarData/donutData),再查列表数组化;禁止静态展开或整块漏建',
|
|
42
|
+
},
|
|
43
|
+
// 兜底:仍要求沉淀通用规则,但无既有范式可直引
|
|
44
|
+
generic: {
|
|
45
|
+
label: '通用门维度失败',
|
|
46
|
+
recipeSections: [],
|
|
47
|
+
generatorIntents: [],
|
|
48
|
+
action: '升级 Skill 级范式:沉淀一条通用规则而非逐例救火',
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 基于 failureClass + 门维度签名粗分范式模式(不依赖业务词)。 */
|
|
53
|
+
export function classifyParadigm(signatureKey) {
|
|
54
|
+
const [failureClass = '', signature = ''] = String(signatureKey || '').split('/')
|
|
55
|
+
const sig = signature.toLowerCase()
|
|
56
|
+
if (failureClass === 'STYLE' && /^(structure|position|overall)$/.test(sig)) return 'chart-radar-block'
|
|
57
|
+
if (failureClass === 'STYLE' && /^(pixeldiff|color)$/.test(sig)) return 'bg-fill'
|
|
58
|
+
if (failureClass === 'DSL' && sig === 'components') return 'list-or-chart-missing'
|
|
59
|
+
return 'generic'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 取范式引用详情,未知 key 回退 generic。 */
|
|
63
|
+
export function paradigmRefs(key) {
|
|
64
|
+
return PARADIGMS[key] || PARADIGMS.generic
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 给 same-signature-3-rounds 信号生成增强 detail(含范式模式 + 引用 + 行动项)。 */
|
|
68
|
+
export function enrichSameSignatureDetail(item) {
|
|
69
|
+
const patternKey = classifyParadigm(item.signature)
|
|
70
|
+
const refs = paradigmRefs(patternKey)
|
|
71
|
+
const base = `签名 ${item.signature} 在 ${item.occurrences} 轮重复出现(${item.rounds.join(' → ')}):脚本补丁无效,需升级 Skill 级范式。`
|
|
72
|
+
const parts = [base, `模式=${refs.label}。${refs.action}。`]
|
|
73
|
+
if (refs.recipeSections.length) parts.push(`参照:${refs.recipeSections.join(';')}。`)
|
|
74
|
+
if (refs.generatorIntents.length) parts.push(`生成器侧已沉淀:${refs.generatorIntents.join(';')}。`)
|
|
75
|
+
return { pattern: patternKey, detail: parts.join('') }
|
|
76
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// 像素评分 worker 运行器(P0-2):在独立线程内执行 imageFidelity,避免阻塞主事件循环。
|
|
2
|
+
// 纯 JS + pngjs,无 DOM 依赖,可直接在 worker_threads 内 import。
|
|
3
|
+
// 计算异常就地捕获并回传 error,由主侧 scorePixel 触发同步回退,保证训练轮不崩。
|
|
4
|
+
import { parentPort } from 'node:worker_threads'
|
|
5
|
+
import { imageFidelity } from './scoring.mjs'
|
|
6
|
+
|
|
7
|
+
parentPort.on('message', ({ id, payload }) => {
|
|
8
|
+
try {
|
|
9
|
+
// Buffer 经 worker_threads 结构化克隆后可能退化为 Uint8Array,PNG.sync.read 仍需真 Buffer;
|
|
10
|
+
// Buffer.from 对已是 Buffer 的入参仅复制、对 Uint8Array 则还原,两路都安全。
|
|
11
|
+
const designBuffer = Buffer.isBuffer(payload.designBuffer)
|
|
12
|
+
? payload.designBuffer
|
|
13
|
+
: Buffer.from(payload.designBuffer)
|
|
14
|
+
const renderBuffer = Buffer.isBuffer(payload.renderBuffer)
|
|
15
|
+
? payload.renderBuffer
|
|
16
|
+
: Buffer.from(payload.renderBuffer)
|
|
17
|
+
const result = imageFidelity(
|
|
18
|
+
designBuffer,
|
|
19
|
+
renderBuffer,
|
|
20
|
+
payload.exclusionRects,
|
|
21
|
+
payload.pixelOnlyRects,
|
|
22
|
+
payload.opts,
|
|
23
|
+
)
|
|
24
|
+
parentPort.postMessage({ id, result })
|
|
25
|
+
} catch (error) {
|
|
26
|
+
parentPort.postMessage({
|
|
27
|
+
id,
|
|
28
|
+
error: { message: String(error?.message || error), stack: String(error?.stack || '') },
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
})
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// 像素评分 worker 池(P0-2):把 imageFidelity 的同步重计算(含 240×H 全图 diff / 灰度剖面 /
|
|
2
|
+
// 颜色直方图)下沉到 worker_threads,避免并发渲染时评分串行卡住主事件循环——
|
|
3
|
+
// 这正是此前把 --concurrency 锁死在 4 的根因(run_round.mjs 注释)。
|
|
4
|
+
// 纯 JS + pngjs,无 DOM 依赖,可安全在 worker 内 import。
|
|
5
|
+
//
|
|
6
|
+
// 健壮性:worker 创建 / 初始化 / 单任务计算任一环节失败,scorePixel 自动回退到同步
|
|
7
|
+
// imageFidelity,绝不抛出中断训练轮。env CARD_TRAINER_NO_PIXEL_WORKER=1 可强制关闭 worker。
|
|
8
|
+
import { Worker } from 'node:worker_threads'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
import { imageFidelity } from './scoring.mjs'
|
|
11
|
+
|
|
12
|
+
const runnerPath = fileURLToPath(new URL('./pixel-worker-runner.mjs', import.meta.url))
|
|
13
|
+
|
|
14
|
+
class PixelWorkerPool {
|
|
15
|
+
constructor(size) {
|
|
16
|
+
this.size = Math.max(1, size)
|
|
17
|
+
this.workers = []
|
|
18
|
+
this.idle = []
|
|
19
|
+
this.queue = []
|
|
20
|
+
this.pending = new Map()
|
|
21
|
+
this.nextId = 1
|
|
22
|
+
this.available = false
|
|
23
|
+
try {
|
|
24
|
+
for (let i = 0; i < this.size; i += 1) {
|
|
25
|
+
const worker = new Worker(runnerPath)
|
|
26
|
+
worker.__currentId = null
|
|
27
|
+
worker.on('message', (msg) => this._onMessage(worker, msg))
|
|
28
|
+
worker.on('error', () => this._onError(worker))
|
|
29
|
+
this.workers.push(worker)
|
|
30
|
+
this.idle.push(worker)
|
|
31
|
+
}
|
|
32
|
+
this.available = this.workers.length > 0
|
|
33
|
+
} catch {
|
|
34
|
+
this.available = false
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_onMessage(worker, msg) {
|
|
39
|
+
const { id, result, error } = msg
|
|
40
|
+
worker.__currentId = null
|
|
41
|
+
const pending = this.pending.get(id)
|
|
42
|
+
if (!pending) return
|
|
43
|
+
this.pending.delete(id)
|
|
44
|
+
this.idle.push(worker)
|
|
45
|
+
if (error) pending.reject(new Error(error.message))
|
|
46
|
+
else pending.resolve(result)
|
|
47
|
+
this._drain()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_onError(worker) {
|
|
51
|
+
const id = worker.__currentId
|
|
52
|
+
worker.__dead = true
|
|
53
|
+
worker.__currentId = null
|
|
54
|
+
if (id != null) {
|
|
55
|
+
const pending = this.pending.get(id)
|
|
56
|
+
this.pending.delete(id)
|
|
57
|
+
if (pending) pending.reject(new Error('pixel-worker-failed'))
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_drain() {
|
|
62
|
+
while (this.queue.length) {
|
|
63
|
+
// 优先挑一个存活 worker;无可用则把积压任务全部 reject,交由调用方同步回退。
|
|
64
|
+
const liveIdx = this.idle.findIndex((worker) => !worker.__dead)
|
|
65
|
+
if (liveIdx < 0) {
|
|
66
|
+
for (const item of this.queue) {
|
|
67
|
+
const pending = this.pending.get(item.id)
|
|
68
|
+
this.pending.delete(item.id)
|
|
69
|
+
if (pending) pending.reject(new Error('pixel-worker-unavailable'))
|
|
70
|
+
}
|
|
71
|
+
this.queue.length = 0
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
const worker = this.idle.splice(liveIdx, 1)[0]
|
|
75
|
+
const item = this.queue.shift()
|
|
76
|
+
worker.__currentId = item.id
|
|
77
|
+
worker.postMessage({ id: item.id, payload: item.payload })
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
run(payload) {
|
|
82
|
+
if (!this.available) return Promise.reject(new Error('pool-unavailable'))
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const id = this.nextId++
|
|
85
|
+
this.pending.set(id, { resolve, reject })
|
|
86
|
+
this.queue.push({ id, payload })
|
|
87
|
+
this._drain()
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async close() {
|
|
92
|
+
await Promise.all(this.workers.map((worker) => new Promise((resolve) => {
|
|
93
|
+
try { worker.on('exit', () => resolve()); worker.terminate() } catch { resolve() }
|
|
94
|
+
})))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let _pool = null
|
|
99
|
+
let _status = 'uninit' // 'on' | 'off' | 'fallback'
|
|
100
|
+
|
|
101
|
+
export function initPixelWorkers(size) {
|
|
102
|
+
if (process.env.CARD_TRAINER_NO_PIXEL_WORKER) {
|
|
103
|
+
_pool = null
|
|
104
|
+
_status = 'off'
|
|
105
|
+
return _status
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
_pool = new PixelWorkerPool(size)
|
|
109
|
+
_status = _pool.available ? 'on' : 'fallback'
|
|
110
|
+
} catch {
|
|
111
|
+
_pool = null
|
|
112
|
+
_status = 'fallback'
|
|
113
|
+
}
|
|
114
|
+
return _status
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function pixelWorkerStatus() {
|
|
118
|
+
return _status
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function scorePixel(designBuffer, renderBuffer, exclusionRects = [], pixelOnlyRects = [], opts = {}) {
|
|
122
|
+
if (_pool && _status === 'on') {
|
|
123
|
+
try {
|
|
124
|
+
return await _pool.run({ designBuffer, renderBuffer, exclusionRects, pixelOnlyRects, opts })
|
|
125
|
+
} catch {
|
|
126
|
+
// 任一 worker 失败 → 同步回退,绝不让训练轮崩。
|
|
127
|
+
return imageFidelity(designBuffer, renderBuffer, exclusionRects, pixelOnlyRects, opts)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return imageFidelity(designBuffer, renderBuffer, exclusionRects, pixelOnlyRects, opts)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function closePixelWorkers() {
|
|
134
|
+
if (_pool) {
|
|
135
|
+
await _pool.close()
|
|
136
|
+
_pool = null
|
|
137
|
+
}
|
|
138
|
+
}
|