- server.js: face_maps 改为白名单 5 图层完整 dataURL 透传(REPORT_MAPS,按魔数判 mime),marks 坐标随 result 原样透传;实测 return_marks 不支持毛孔/黑头等扩展标记 - skin-score.js: 按需求文档 V2.2 逐字审计修正 9 处(黑眼圈 dark_circle_severity、oil_desc/spot_flag §7.2 拼接、skintone 中文映射、轮廓预警档前缀、痘痘 heavy_note、50+精华全文、综合文案数值格式),契约引用 V2.0→V2.2 - app.js/index.html/css: 报告页 6 分类检测可视化(轮廓=正脸实拍、皱纹=正脸压暗+皱纹增强线、光滑=正脸+色斑/痣/痤疮标记、肤色=色沉图、敏感=红区图、痘痘=正脸+闭口/痤疮标记),数据驱动 VIZ_CAT 配置,优雅降级 - h5/test/: Excel 两组三视图真值回归 84/84(含案例2 AY 列扁平化数据重建 fixture) - h5/assets/mock/: 真实案例1 报告页 fixture(坐标与照片同比例缩放),?mock=1&screen=report 直渲 - e2e: Chrome 无头闭环 55/55(mock 全流程/直渲/真实 API 降级链路),截图存档 preview/e2e-20260926 - 自测: skin-score 83/83,fixture 冒烟 35/35
129 lines
6.9 KiB
JavaScript
129 lines
6.9 KiB
JavaScript
/* ═════ 巨子测肤 H5 — 评分引擎真值回归(Excel 三视图表 × SkinScore)═════
|
||
* 用法:node h5/test/regression.js
|
||
* fixture 重建:python3 h5/test/build_fixtures.py(Excel 或参考 JSON 变更后需重建)
|
||
*
|
||
* 断言目标:docs/皮肤检测测试结果_三视图_20260926.xlsx「三视图检测汇总」第 3/4 行
|
||
* - 六分类 P(I/M/Q/U/Y/AC)、S(J/N/R/V/Z/AD)、档位(K/O/S/W/AA/AE)
|
||
* - 三维度 P/S/档位(AG..AO)、下颌线角度(AP/AQ)、产品组别(AT)、精华线(AU)、均分(AV)
|
||
* - 6 分类文案(L/P/T/X/AB/AF)、综合文案(AW)、生活方式(AX) 逐字一致
|
||
* - 浮点容差 0.05;文案逐字无容差
|
||
*
|
||
* 引擎与真值不一致时:不改 h5/js/skin-score.js(审计代理负责),差异登记在 KNOWN_DIFFS
|
||
* 并输出明细;未登记的新差异 → 退出码 1。登记项若转为通过(引擎已修复)会提示清理。
|
||
*/
|
||
'use strict';
|
||
|
||
var fs = require('fs');
|
||
var path = require('path');
|
||
var SkinScore = require(path.join(__dirname, '..', 'js', 'skin-score.js'));
|
||
|
||
/* ─── 已知差异登记:键 = 案例.列,值 = 差异原因(逐条说明)。当前为空:V2.2 审计后
|
||
引擎与两组真值全部一致;今后出现引擎/真值不一致时在此登记,未登记的新差异 → 退出码 1 ─── */
|
||
var KNOWN_DIFFS = {};
|
||
|
||
/* ─── 断言计数与输出 ─── */
|
||
var TOL = 0.05 + 1e-6; // 浮点容差 0.05(1e-6 仅抵二进制表示误差,如 98.65 vs 98.7)
|
||
var pass = 0, known = 0, fail = 0;
|
||
var failLines = [], knownLines = [];
|
||
|
||
function diffCtx(exp, act) { // 首个分歧点 ±30 字上下文
|
||
var i = 0, n = Math.min(exp.length, act.length);
|
||
while (i < n && exp[i] === act[i]) i++;
|
||
var lo = Math.max(0, i - 30);
|
||
return ' 分歧@' + i + ':\n 真值 …' + exp.slice(lo, i + 30) + '…\n 引擎 …' + act.slice(lo, i + 30) + '…';
|
||
}
|
||
function okNum(label, actual, expected) {
|
||
if (typeof actual === 'number' && typeof expected === 'number' && Math.abs(actual - expected) <= TOL) {
|
||
pass++; console.log(' ✓ ' + label + ' = ' + actual + (actual !== expected ? '(真值 ' + expected + ',容差内)' : ''));
|
||
} else {
|
||
fail++; failLines.push(label + ':引擎 ' + actual + ' ≠ 真值 ' + expected);
|
||
console.error(' ✗ ' + label + ':引擎 ' + actual + ' ≠ 真值 ' + expected);
|
||
}
|
||
}
|
||
function okStr(label, actual, expected, knownKey) {
|
||
if (actual === expected) { pass++; console.log(' ✓ ' + label); return; }
|
||
var reasons = knownKey ? KNOWN_DIFFS[knownKey] : null;
|
||
if (reasons) { // 已登记:已知差异,输出明细,不计失败
|
||
known++;
|
||
knownLines.push(knownKey);
|
||
console.log(' Δ ' + label + ' 已知差异(' + reasons.length + ' 条原因):');
|
||
for (var i = 0; i < reasons.length; i++) console.log(' 原因' + (i + 1) + ':' + reasons[i]);
|
||
console.log(diffCtx(String(expected), String(actual)));
|
||
} else {
|
||
fail++;
|
||
failLines.push(label + ':文案不一致\n' + diffCtx(String(expected), String(actual)));
|
||
console.error(' ✗ ' + label + ':文案不一致(未登记差异)\n' + diffCtx(String(expected), String(actual)));
|
||
}
|
||
}
|
||
|
||
/* ─── 列号映射:分类(文案列)与维度 ─── */
|
||
var CAT_COLS = { contour: 'I/J/K/L', wrinkles: 'M/N/O/P', smooth: 'Q/R/S/T', tone: 'U/V/W/X', sens: 'Y/Z/AA/AB', acne: 'AC/AD/AE/AF' };
|
||
var CAT_COPY_COL = { contour: 'L', wrinkles: 'P', smooth: 'T', tone: 'X', sens: 'AB', acne: 'AF' };
|
||
var DIM_COLS = { firm: 'AG/AH/AI', vita: 'AJ/AK/AL', stab: 'AM/AN/AO' };
|
||
|
||
/* ─── 主流程 ─── */
|
||
function loadJSON(name) {
|
||
return JSON.parse(fs.readFileSync(path.join(__dirname, 'fixtures', name), 'utf8'));
|
||
}
|
||
|
||
function main() {
|
||
var truth = loadJSON('truth.json');
|
||
var cases = [
|
||
{ tag: 'case1', fixture: loadJSON('case1.json'), truth: truth.case1 },
|
||
{ tag: 'case2', fixture: loadJSON('case2.json'), truth: truth.case2 }
|
||
];
|
||
|
||
for (var ci = 0; ci < cases.length; ci++) {
|
||
var tag = cases[ci].tag, fixture = cases[ci].fixture, tr = cases[ci].truth;
|
||
console.log('== ' + tag + '(realAge=' + tr.profile.realAge + ',skinType=' + tr.profile.skinType + ') ==');
|
||
var vm = SkinScore.buildViewModel(fixture.result, fixture.side, tr.profile);
|
||
|
||
// 下颌线角度 AP/AQ:fixture 保真校验(引擎消费角度后由 L 列逐字断言兜底)
|
||
okNum(tag + '.AP 左下颌线角度', fixture.side.left.left_jawline_info.left_jawline_angle, tr.jawline.left);
|
||
okNum(tag + '.AQ 右下颌线角度', fixture.side.right.right_jawline_info.right_jawline_angle, tr.jawline.right);
|
||
okNum(tag + '.E 肌肤年龄', vm.skinAgeRaw, tr.skinAge);
|
||
okNum(tag + '.F 总分', vm.total, tr.totalScore);
|
||
|
||
// 六分类 P/S/档位 + 文案逐字
|
||
for (var k in CAT_COLS) {
|
||
var cols = CAT_COLS[k].split('/'), c = vm.cats[k], t = tr.cats[k];
|
||
okNum(tag + '.' + cols[0] + ' ' + c.name + '·P', c.P, t.P);
|
||
okNum(tag + '.' + cols[1] + ' ' + c.name + '·S', c.S, t.S);
|
||
okStr(tag + '.' + cols[2] + ' ' + c.name + '·档位', c.level ? c.level + '档' : String(c.level), t.tier);
|
||
okStr(tag + '.' + CAT_COPY_COL[k] + ' ' + c.name + '·文案', c.text, t.copy, tag + '.' + CAT_COPY_COL[k]);
|
||
}
|
||
// 三维度 P/S/档位
|
||
for (var d in DIM_COLS) {
|
||
var dcols = DIM_COLS[d].split('/'), dd = vm.dims[d], dt = tr.dims[d];
|
||
okNum(tag + '.' + dcols[0] + ' ' + dd.name + '·P', dd.P, dt.P);
|
||
okNum(tag + '.' + dcols[1] + ' ' + dd.name + '·S', dd.S, dt.S);
|
||
okStr(tag + '.' + dcols[2] + ' ' + dd.name + '·档位', dd.level ? dd.level + '档' : String(dd.level), dt.tier);
|
||
}
|
||
// 组别/精华/均分/综合文案/生活方式
|
||
okStr(tag + '.AT 产品组别', String(vm.group), tr.group);
|
||
okStr(tag + '.AU 精华产品线', vm.essence.name, tr.essence);
|
||
okNum(tag + '.AV 六分类S均分', vm.avgS, tr.avgS);
|
||
okStr(tag + '.AW 综合文案', vm.summary, tr.summary, tag + '.AW');
|
||
okStr(tag + '.AX 生活方式', vm.lifestyle, tr.lifestyle);
|
||
}
|
||
|
||
// 登记项清理提示:已知差异键本轮全部命中为差异;若某键实际通过则提示移除登记
|
||
var knownHit = {}, i;
|
||
for (i = 0; i < knownLines.length; i++) knownHit[knownLines[i]] = 1;
|
||
for (var key in KNOWN_DIFFS) {
|
||
if (!knownHit[key]) console.log(' ◆ 提示:登记项 ' + key + ' 本轮通过(引擎或真值已变化),可从 KNOWN_DIFFS 移除');
|
||
}
|
||
|
||
console.log('----------------------------------------');
|
||
console.log('断言 ' + (pass + known + fail) + ' 条:通过 ' + pass + ',已知差异 ' + known + ',失败 ' + fail);
|
||
if (knownLines.length) console.log('已知差异明细:' + knownLines.join('、') + '(原因见上,待 skin-score.js 审计代理处理)');
|
||
if (failLines.length) {
|
||
console.error('失败明细:');
|
||
for (i = 0; i < failLines.length; i++) console.error(' ✗ ' + failLines[i]);
|
||
}
|
||
if (fail > 0) process.exit(1);
|
||
console.log(known > 0 ? '结论:全绿(仅剩已登记、已说明原因的已知差异)' : '结论:全绿');
|
||
}
|
||
|
||
main();
|