feat: 报告页 V2.2 检测可视化 + 评分引擎审计 + 真值回归/e2e 闭环
- 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
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# ═════ 真值 fixture 构建脚本(零第三方新依赖:openpyxl 环境已装,仅用于建 fixture)═════
|
||||
# 用法:python3 h5/test/build_fixtures.py
|
||||
# 产出:
|
||||
# fixtures/case1.json ← reference/facepp-skinanalyze/examples/ref_3view_raw.json
|
||||
# (案例1 完整原始返回,精简坐标数组与 face_maps base64)
|
||||
# fixtures/case2.json ← docs/皮肤检测测试结果_三视图_20260926.xlsx AY4 扁平化文本解析回嵌套
|
||||
# fixtures/truth.json ← 同表第 3/4 行真值(P/S/档位/文案等断言目标)
|
||||
# 两个 case fixture 均为服务端 /api/skin-analyze 响应形状:
|
||||
# { result, face_rectangle, side: { left: left_side_result, right: right_side_result } }
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
import openpyxl
|
||||
|
||||
ROOT = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..'))
|
||||
REF_JSON = os.path.join(ROOT, 'reference', 'facepp-skinanalyze', 'examples', 'ref_3view_raw.json')
|
||||
XLSX = os.path.join(ROOT, 'docs', '皮肤检测测试结果_三视图_20260926.xlsx')
|
||||
OUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'fixtures')
|
||||
|
||||
|
||||
# ─── 通用:递归精简(坐标数组→[],face_maps→{};评分只用标量与 count)───
|
||||
def slim(node, key=''):
|
||||
if isinstance(node, dict):
|
||||
if key == 'face_maps': # base64 整图,评分不需要,整体置空对象
|
||||
return {}
|
||||
return {k: slim(v, k) for k, v in node.items()}
|
||||
if isinstance(node, list):
|
||||
# 元素为 dict 的坐标/标记数组(rectangle/polygon/*_mark/*_outline)→ 空数组;
|
||||
# 纯数值数组(confidence 列表等)保留
|
||||
if node and all(isinstance(x, dict) for x in node):
|
||||
return []
|
||||
return [slim(x, key) for x in node]
|
||||
return node
|
||||
|
||||
|
||||
# ─── 案例1:原始 pro 返回 → 服务端响应形状 ───
|
||||
def build_case1():
|
||||
with open(REF_JSON, encoding='utf-8') as f:
|
||||
raw = json.load(f)
|
||||
fixture = {
|
||||
'request_id': raw.get('request_id'),
|
||||
'time_used': raw.get('time_used'),
|
||||
'result': slim(raw['result']),
|
||||
'face_rectangle': raw.get('face_rectangle'),
|
||||
'side': {
|
||||
'left': slim(raw.get('left_side_result')),
|
||||
'right': slim(raw.get('right_side_result'))
|
||||
}
|
||||
}
|
||||
return fixture
|
||||
|
||||
|
||||
# ─── 案例2:AY4 扁平化「路径 = 值」文本 → 嵌套 JSON ───
|
||||
SEC_RE = re.compile(r'^——\s*(.+?)\s*——$')
|
||||
KV_RE = re.compile(r'^([^=]+?)\s*=\s*(.*)$')
|
||||
NUMLIST_RE = re.compile(r'^[\d\s,.\-eE+]+$')
|
||||
INT_RE = re.compile(r'^-?\d+$')
|
||||
FLOAT_RE = re.compile(r'^-?(?:\d+\.\d*|\.\d+)(?:[eE][-+]?\d+)?$|^-?\d+[eE][-+]?\d+$')
|
||||
MS_RE = re.compile(r'^(-?\d+(?:\.\d+)?)\s*ms$')
|
||||
|
||||
skipped = [] # [摘要] 跳过的路径(坐标数组/base64/深层结构)
|
||||
|
||||
|
||||
def parse_value(path, s):
|
||||
s = s.strip()
|
||||
if s == '':
|
||||
return None, False
|
||||
if s.startswith('{'): # 行内 JSON(face_rectangle)
|
||||
try:
|
||||
return json.loads(s), True
|
||||
except ValueError:
|
||||
skipped.append(path + ' [行内JSON解析失败]')
|
||||
return None, False
|
||||
if s.startswith('['):
|
||||
if s == '[]':
|
||||
return [], True
|
||||
inner = s[1:-1].strip() if s.endswith(']') else ''
|
||||
if inner and NUMLIST_RE.match(inner): # 纯数值数组(confidence 列表)
|
||||
out = []
|
||||
for tok in inner.split(','):
|
||||
tok = tok.strip()
|
||||
if tok == '':
|
||||
continue
|
||||
out.append(float(tok) if ('.' in tok or 'e' in tok or 'E' in tok) else int(tok))
|
||||
return out, True
|
||||
skipped.append(path + ' ' + s[:40]) # [N组坐标]/[N项]/[base64图,…]/[深层结构省略]
|
||||
return None, False
|
||||
m = MS_RE.match(s) # time_used 的「751 ms」
|
||||
if m:
|
||||
return float(m.group(1)) if '.' in m.group(1) else int(m.group(1)), True
|
||||
if INT_RE.match(s):
|
||||
return int(s), True
|
||||
if FLOAT_RE.match(s):
|
||||
return float(s), True
|
||||
return s, True # 字符串(request_id 等)
|
||||
|
||||
|
||||
def set_path(root, path, value):
|
||||
segs = path.split('.')
|
||||
cur = root
|
||||
for i, seg in enumerate(segs[:-1]):
|
||||
nxt = segs[i + 1]
|
||||
if seg.isdigit(): # 数字段:上级应是 list(本数据仅 skin_type.details.N,均被摘要跳过)
|
||||
idx = int(seg)
|
||||
while len(cur) <= idx:
|
||||
cur.append({} if not nxt.isdigit() else [])
|
||||
if cur[idx] is None:
|
||||
cur[idx] = [] if nxt.isdigit() else {}
|
||||
cur = cur[idx]
|
||||
else:
|
||||
if seg not in cur or not isinstance(cur[seg], (dict, list)):
|
||||
cur[seg] = [] if nxt.isdigit() else {}
|
||||
cur = cur[seg]
|
||||
last = segs[-1]
|
||||
if last.isdigit():
|
||||
idx = int(last)
|
||||
while len(cur) <= idx:
|
||||
cur.append(None)
|
||||
cur[idx] = value
|
||||
else:
|
||||
cur[last] = value
|
||||
|
||||
|
||||
def build_case2(flat_text):
|
||||
doc = {}
|
||||
section = None # 当前 —— section —— 名;None=顶层
|
||||
for line in flat_text.split('\n'):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
m = SEC_RE.match(line)
|
||||
if m:
|
||||
section = m.group(1)
|
||||
continue
|
||||
m = KV_RE.match(line)
|
||||
if not m:
|
||||
continue
|
||||
path, raw_v = m.group(1).strip(), m.group(2)
|
||||
full_path = (section + '.' + path) if section else path
|
||||
value, ok = parse_value(full_path, raw_v)
|
||||
if not ok:
|
||||
continue
|
||||
target = doc
|
||||
if section:
|
||||
if section not in doc or not isinstance(doc[section], dict):
|
||||
doc[section] = {}
|
||||
target = doc[section]
|
||||
set_path(target, path, value)
|
||||
fixture = {
|
||||
'request_id': doc.get('request_id'),
|
||||
'time_used': doc.get('time_used'),
|
||||
'result': slim(doc.get('result', {})),
|
||||
'face_rectangle': doc.get('face_rectangle'),
|
||||
'side': {
|
||||
'left': slim(doc.get('left_side_result')),
|
||||
'right': slim(doc.get('right_side_result'))
|
||||
}
|
||||
}
|
||||
return fixture
|
||||
|
||||
|
||||
# ─── 真值表:Excel 第 3/4 行 → truth.json ───
|
||||
def col(ws, row, letter):
|
||||
return ws['%s%d' % (letter, row)].value
|
||||
|
||||
|
||||
def build_truth(ws, row):
|
||||
def cat(p_col, s_col, tier_col, copy_col):
|
||||
return {
|
||||
'P': col(ws, row, p_col), 'S': col(ws, row, s_col),
|
||||
'tier': col(ws, row, tier_col), 'copy': col(ws, row, copy_col)
|
||||
}
|
||||
def dim(p_col, s_col, tier_col):
|
||||
return {'P': col(ws, row, p_col), 'S': col(ws, row, s_col), 'tier': col(ws, row, tier_col)}
|
||||
return {
|
||||
'profile': {'realAge': col(ws, row, 'AS'), 'skinType': '混合性'}, # 综合文案实测输出「混合性」
|
||||
'skinAge': col(ws, row, 'E'),
|
||||
'totalScore': col(ws, row, 'F'),
|
||||
'cats': {
|
||||
'contour': cat('I', 'J', 'K', 'L'),
|
||||
'wrinkles': cat('M', 'N', 'O', 'P'),
|
||||
'smooth': cat('Q', 'R', 'S', 'T'),
|
||||
'tone': cat('U', 'V', 'W', 'X'),
|
||||
'sens': cat('Y', 'Z', 'AA', 'AB'),
|
||||
'acne': cat('AC', 'AD', 'AE', 'AF')
|
||||
},
|
||||
'dims': {
|
||||
'firm': dim('AG', 'AH', 'AI'),
|
||||
'vita': dim('AJ', 'AK', 'AL'),
|
||||
'stab': dim('AM', 'AN', 'AO')
|
||||
},
|
||||
'jawline': {'left': col(ws, row, 'AP'), 'right': col(ws, row, 'AQ')},
|
||||
'group': col(ws, row, 'AT'),
|
||||
'essence': col(ws, row, 'AU'),
|
||||
'avgS': col(ws, row, 'AV'),
|
||||
'summary': col(ws, row, 'AW'),
|
||||
'lifestyle': col(ws, row, 'AX')
|
||||
}
|
||||
|
||||
|
||||
def dump(name, obj):
|
||||
path = os.path.join(OUT_DIR, name)
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
json.dump(obj, f, ensure_ascii=False, indent=1)
|
||||
print(' 写出 %s(%d 字节)' % (os.path.relpath(path, ROOT), os.path.getsize(path)))
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
print('== 案例1 fixture ← ref_3view_raw.json ==')
|
||||
dump('case1.json', build_case1())
|
||||
|
||||
print('== 案例2 fixture ← Excel AY4 扁平化解析 ==')
|
||||
wb = openpyxl.load_workbook(XLSX, data_only=True)
|
||||
ws = wb['三视图检测汇总']
|
||||
case2 = build_case2(ws['AY4'].value)
|
||||
dump('case2.json', case2)
|
||||
print(' [摘要] 跳过 %d 个路径(坐标数组/base64/深层结构,评分只用标量,不影响):' % len(skipped))
|
||||
for s in skipped:
|
||||
print(' - ' + s)
|
||||
|
||||
print('== 真值表 ← Excel 第 3/4 行 ==')
|
||||
dump('truth.json', {'case1': build_truth(ws, 3), 'case2': build_truth(ws, 4)})
|
||||
|
||||
# 构建期自检:fixture 关键标量 vs 真值
|
||||
c1 = json.load(open(os.path.join(OUT_DIR, 'case1.json'), encoding='utf-8'))
|
||||
c2 = json.load(open(os.path.join(OUT_DIR, 'case2.json'), encoding='utf-8'))
|
||||
t = json.load(open(os.path.join(OUT_DIR, 'truth.json'), encoding='utf-8'))
|
||||
for tag, fx, tr in (('案例1', c1, t['case1']), ('案例2', c2, t['case2'])):
|
||||
la = fx['side']['left']['left_jawline_info']['left_jawline_angle']
|
||||
ra = fx['side']['right']['right_jawline_info']['right_jawline_angle']
|
||||
sa = fx['result']['skin_age']['value']
|
||||
assert abs(la - tr['jawline']['left']) < 1e-9, (tag, '左下颌角', la, tr['jawline']['left'])
|
||||
assert abs(ra - tr['jawline']['right']) < 1e-9, (tag, '右下颌角', ra, tr['jawline']['right'])
|
||||
assert sa == tr['skinAge'], (tag, '肤龄', sa, tr['skinAge'])
|
||||
print(' %s 自检通过:下颌角 %.2f/%.2f、肤龄 %d 与真值一致' % (tag, la, ra, sa))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Vendored
+1064
File diff suppressed because it is too large
Load Diff
Vendored
+589
@@ -0,0 +1,589 @@
|
||||
{
|
||||
"request_id": "1790403132,0f99111b-571a-46f5-8540-8209c21b3755",
|
||||
"time_used": 751,
|
||||
"result": {
|
||||
"skin_age": {
|
||||
"value": 50
|
||||
},
|
||||
"eye_pouch": {
|
||||
"value": 0,
|
||||
"confidence": 0.9219674
|
||||
},
|
||||
"dark_circle": {
|
||||
"value": 3,
|
||||
"confidence": 1
|
||||
},
|
||||
"dark_circle_severity": {
|
||||
"value": 0,
|
||||
"confidence": 1
|
||||
},
|
||||
"forehead_wrinkle": {
|
||||
"value": 1,
|
||||
"confidence": 0.57314694
|
||||
},
|
||||
"crows_feet": {
|
||||
"value": 0,
|
||||
"confidence": 0.6948663
|
||||
},
|
||||
"eye_finelines": {
|
||||
"value": 0,
|
||||
"confidence": 0.5346259
|
||||
},
|
||||
"glabella_wrinkle": {
|
||||
"value": 0,
|
||||
"confidence": 0.65657294
|
||||
},
|
||||
"nasolabial_fold": {
|
||||
"value": 1,
|
||||
"confidence": 0.80668426
|
||||
},
|
||||
"nasolabial_fold_severity": {
|
||||
"value": 0,
|
||||
"confidence": 0.3970088
|
||||
},
|
||||
"skin_type": {
|
||||
"skin_type": 3
|
||||
},
|
||||
"pores_forehead": {
|
||||
"value": 0,
|
||||
"confidence": 1
|
||||
},
|
||||
"pores_left_cheek": {
|
||||
"value": 0,
|
||||
"confidence": 1
|
||||
},
|
||||
"pores_right_cheek": {
|
||||
"value": 0,
|
||||
"confidence": 1
|
||||
},
|
||||
"pores_jaw": {
|
||||
"value": 0,
|
||||
"confidence": 1
|
||||
},
|
||||
"blackhead": {
|
||||
"value": 0,
|
||||
"confidence": 1
|
||||
},
|
||||
"skintone_ita": {
|
||||
"ITA": 29.471874,
|
||||
"skintone": 3
|
||||
},
|
||||
"skin_hue_ha": {
|
||||
"HA": 57.833736,
|
||||
"skin_hue": 0
|
||||
},
|
||||
"sensitivity": {
|
||||
"sensitivity_area": 0,
|
||||
"sensitivity_intensity": 0
|
||||
},
|
||||
"acne": {
|
||||
"rectangle": [],
|
||||
"confidence": [],
|
||||
"polygon": [],
|
||||
"count": 0
|
||||
},
|
||||
"mole": {
|
||||
"confidence": [
|
||||
0.3529357
|
||||
],
|
||||
"count": 1
|
||||
},
|
||||
"brown_spot": {
|
||||
"count": 23
|
||||
},
|
||||
"closed_comedones": {
|
||||
"rectangle": [],
|
||||
"confidence": [],
|
||||
"polygon": [],
|
||||
"count": 0
|
||||
},
|
||||
"acne_mark": {
|
||||
"rectangle": [],
|
||||
"confidence": [],
|
||||
"polygon": [],
|
||||
"count": 0
|
||||
},
|
||||
"acne_nodule": {
|
||||
"rectangle": [],
|
||||
"confidence": [],
|
||||
"polygon": [],
|
||||
"count": 0
|
||||
},
|
||||
"acne_pustule": {
|
||||
"rectangle": [],
|
||||
"confidence": [],
|
||||
"polygon": [],
|
||||
"count": 0
|
||||
},
|
||||
"blackhead_count": 3,
|
||||
"skintone": {
|
||||
"value": 2,
|
||||
"confidence": 0.7146322
|
||||
},
|
||||
"fine_line": {
|
||||
"forehead_count": 1,
|
||||
"left_undereye_count": 3,
|
||||
"right_undereye_count": 3,
|
||||
"left_cheek_count": 0,
|
||||
"right_cheek_count": 0,
|
||||
"left_crowsfeet_count": 1,
|
||||
"right_crowsfeet_count": 0,
|
||||
"glabella_count": 0
|
||||
},
|
||||
"wrinkle_count": {
|
||||
"forehead_count": 1,
|
||||
"left_undereye_count": 2,
|
||||
"right_undereye_count": 3,
|
||||
"left_mouth_count": 0,
|
||||
"right_mouth_count": 0,
|
||||
"left_nasolabial_count": 1,
|
||||
"right_nasolabial_count": 1,
|
||||
"glabella_count": 0,
|
||||
"left_cheek_count": 0,
|
||||
"right_cheek_count": 0,
|
||||
"left_crowsfeet_count": 0,
|
||||
"right_crowsfeet_count": 0
|
||||
},
|
||||
"oily_intensity": {
|
||||
"t_zone": {
|
||||
"area": 0.62,
|
||||
"intensity": 2
|
||||
},
|
||||
"left_cheek": {
|
||||
"area": 0.16,
|
||||
"intensity": 1
|
||||
},
|
||||
"right_cheek": {
|
||||
"area": 0.16,
|
||||
"intensity": 1
|
||||
},
|
||||
"chin_area": {
|
||||
"area": 0,
|
||||
"intensity": 0
|
||||
},
|
||||
"full_face": {
|
||||
"area": 0.49,
|
||||
"intensity": 2
|
||||
}
|
||||
},
|
||||
"enlarged_pore_count": {
|
||||
"forehead_count": 81,
|
||||
"left_cheek_count": 10,
|
||||
"right_cheek_count": 9,
|
||||
"chin_count": 0
|
||||
},
|
||||
"red_spot": {
|
||||
"red_spot_area": 0.009,
|
||||
"red_spot_intensity": 3
|
||||
},
|
||||
"right_dark_circle_rete": {
|
||||
"value": 0
|
||||
},
|
||||
"left_dark_circle_rete": {
|
||||
"value": 0
|
||||
},
|
||||
"right_dark_circle_pigment": {
|
||||
"value": 0
|
||||
},
|
||||
"left_dark_circle_pigment": {
|
||||
"value": 0
|
||||
},
|
||||
"right_dark_circle_structural": {
|
||||
"value": 0
|
||||
},
|
||||
"left_dark_circle_structural": {
|
||||
"value": 1
|
||||
},
|
||||
"dark_circle_mark": {
|
||||
"left_eye_rect": {
|
||||
"left": 102,
|
||||
"top": 137,
|
||||
"width": 68,
|
||||
"height": 59
|
||||
},
|
||||
"right_eye_rect": {
|
||||
"left": 214,
|
||||
"top": 136,
|
||||
"width": 67,
|
||||
"height": 59
|
||||
}
|
||||
},
|
||||
"water": {
|
||||
"water_severity": 7,
|
||||
"water_area": 0.068,
|
||||
"water_forehead": {
|
||||
"area": 0.111
|
||||
},
|
||||
"water_leftcheek": {
|
||||
"area": 0.007
|
||||
},
|
||||
"water_rightcheek": {
|
||||
"area": 0.007
|
||||
}
|
||||
},
|
||||
"rough": {
|
||||
"rough_severity": 16,
|
||||
"rough_area": 0.28,
|
||||
"rough_forehead": {
|
||||
"area": 0.352
|
||||
},
|
||||
"rough_leftcheek": {
|
||||
"area": 0.297
|
||||
},
|
||||
"rough_rightcheek": {
|
||||
"area": 0.194
|
||||
},
|
||||
"rough_jaw": {
|
||||
"area": 0.008
|
||||
}
|
||||
},
|
||||
"left_mouth_wrinkle_severity": {
|
||||
"value": 0
|
||||
},
|
||||
"right_mouth_wrinkle_severity": {
|
||||
"value": 0
|
||||
},
|
||||
"forehead_wrinkle_severity": {
|
||||
"value": 0
|
||||
},
|
||||
"left_crows_feet_severity": {
|
||||
"value": 1
|
||||
},
|
||||
"right_crows_feet_severity": {
|
||||
"value": 0
|
||||
},
|
||||
"left_eye_finelines_severity": {
|
||||
"value": 0
|
||||
},
|
||||
"right_eye_finelines_severity": {
|
||||
"value": 0
|
||||
},
|
||||
"glabella_wrinkle_severity": {
|
||||
"value": 0
|
||||
},
|
||||
"left_nasolabial_fold_severity": {
|
||||
"value": 2
|
||||
},
|
||||
"right_nasolabial_fold_severity": {
|
||||
"value": 2
|
||||
},
|
||||
"left_cheek_wrinkle_severity": {
|
||||
"value": 0
|
||||
},
|
||||
"right_cheek_wrinkle_severity": {
|
||||
"value": 0
|
||||
},
|
||||
"forehead_wrinkle_info": {
|
||||
"wrinkle_score": 5,
|
||||
"wrinkle_severity_level": 0,
|
||||
"wrinkle_norm_length": 0.5042114066911291,
|
||||
"wrinkle_norm_depth": 0.2771696504688832,
|
||||
"wrinkle_pixel_density": 0.026174111741415557,
|
||||
"wrinkle_area_ratio": 0.04610318331503842,
|
||||
"wrinkle_deep_ratio": 0.6260869565217392,
|
||||
"wrinkle_deep_num": 1,
|
||||
"wrinkle_shallow_num": 1
|
||||
},
|
||||
"left_eye_wrinkle_info": {
|
||||
"wrinkle_score": 6,
|
||||
"wrinkle_severity_level": 0,
|
||||
"wrinkle_norm_length": 1.3718454398776636,
|
||||
"wrinkle_norm_depth": 0.13984917043740575,
|
||||
"wrinkle_pixel_density": 0.4036391390409279,
|
||||
"wrinkle_area_ratio": 0.09728506787330317,
|
||||
"wrinkle_deep_ratio": 0.35384615384615387,
|
||||
"wrinkle_deep_num": 2,
|
||||
"wrinkle_shallow_num": 3
|
||||
},
|
||||
"right_eye_wrinkle_info": {
|
||||
"wrinkle_score": 8,
|
||||
"wrinkle_severity_level": 0,
|
||||
"wrinkle_norm_length": 1.7560485062524749,
|
||||
"wrinkle_norm_depth": 0.16168202220647293,
|
||||
"wrinkle_pixel_density": 0.4840998584804119,
|
||||
"wrinkle_area_ratio": 0.08267090620031796,
|
||||
"wrinkle_deep_ratio": 0.5783132530120482,
|
||||
"wrinkle_deep_num": 3,
|
||||
"wrinkle_shallow_num": 3
|
||||
},
|
||||
"left_crowsfeet_wrinkle_info": {
|
||||
"wrinkle_score": 10,
|
||||
"wrinkle_severity_level": 1,
|
||||
"wrinkle_norm_length": 0.46431691811243997,
|
||||
"wrinkle_norm_depth": 0.11122994652406416,
|
||||
"wrinkle_pixel_density": 0.42449501019699687,
|
||||
"wrinkle_area_ratio": 0.050966608084358524,
|
||||
"wrinkle_deep_ratio": 0,
|
||||
"wrinkle_deep_num": 0,
|
||||
"wrinkle_shallow_num": 1
|
||||
},
|
||||
"right_crowsfeet_wrinkle_info": {
|
||||
"wrinkle_score": 0,
|
||||
"wrinkle_severity_level": 0,
|
||||
"wrinkle_norm_length": 0,
|
||||
"wrinkle_norm_depth": 0,
|
||||
"wrinkle_pixel_density": 0,
|
||||
"wrinkle_area_ratio": 0,
|
||||
"wrinkle_deep_ratio": 0,
|
||||
"wrinkle_deep_num": 0,
|
||||
"wrinkle_shallow_num": 0
|
||||
},
|
||||
"glabella_wrinkle_info": {
|
||||
"wrinkle_score": 0,
|
||||
"wrinkle_severity_level": 0,
|
||||
"wrinkle_norm_length": 0,
|
||||
"wrinkle_norm_depth": 0,
|
||||
"wrinkle_pixel_density": 0,
|
||||
"wrinkle_area_ratio": 0,
|
||||
"wrinkle_deep_ratio": 0,
|
||||
"wrinkle_deep_num": 0,
|
||||
"wrinkle_shallow_num": 0
|
||||
},
|
||||
"left_mouth_wrinkle_info": {
|
||||
"wrinkle_score": 0,
|
||||
"wrinkle_severity_level": 0,
|
||||
"wrinkle_norm_length": 0,
|
||||
"wrinkle_norm_depth": 0,
|
||||
"wrinkle_pixel_density": 0,
|
||||
"wrinkle_area_ratio": 0,
|
||||
"wrinkle_deep_ratio": 0,
|
||||
"wrinkle_deep_num": 0,
|
||||
"wrinkle_shallow_num": 0
|
||||
},
|
||||
"right_mouth_wrinkle_info": {
|
||||
"wrinkle_score": 0,
|
||||
"wrinkle_severity_level": 0,
|
||||
"wrinkle_norm_length": 0,
|
||||
"wrinkle_norm_depth": 0,
|
||||
"wrinkle_pixel_density": 0,
|
||||
"wrinkle_area_ratio": 0,
|
||||
"wrinkle_deep_ratio": 0,
|
||||
"wrinkle_deep_num": 0,
|
||||
"wrinkle_shallow_num": 0
|
||||
},
|
||||
"left_nasolabial_wrinkle_info": {
|
||||
"wrinkle_score": 36,
|
||||
"wrinkle_severity_level": 2,
|
||||
"wrinkle_norm_length": 0.23676013879409538,
|
||||
"wrinkle_norm_depth": 0.3566448801742919,
|
||||
"wrinkle_pixel_density": 0.06861427532071777,
|
||||
"wrinkle_area_ratio": 0.09025069637883008,
|
||||
"wrinkle_deep_ratio": 1,
|
||||
"wrinkle_deep_num": 1,
|
||||
"wrinkle_shallow_num": 0
|
||||
},
|
||||
"right_nasolabial_wrinkle_info": {
|
||||
"wrinkle_score": 50,
|
||||
"wrinkle_severity_level": 2,
|
||||
"wrinkle_norm_length": 0.22799124476468444,
|
||||
"wrinkle_norm_depth": 0.49550173010380627,
|
||||
"wrinkle_pixel_density": 0.07321052192999311,
|
||||
"wrinkle_area_ratio": 0.056790123456790124,
|
||||
"wrinkle_deep_ratio": 1,
|
||||
"wrinkle_deep_num": 1,
|
||||
"wrinkle_shallow_num": 0
|
||||
},
|
||||
"left_cheek_wrinkle_info": {
|
||||
"wrinkle_score": 0,
|
||||
"wrinkle_severity_level": 0,
|
||||
"wrinkle_norm_length": 0,
|
||||
"wrinkle_norm_depth": 0,
|
||||
"wrinkle_pixel_density": 0,
|
||||
"wrinkle_area_ratio": 0,
|
||||
"wrinkle_deep_ratio": 0,
|
||||
"wrinkle_deep_num": 0,
|
||||
"wrinkle_shallow_num": 0
|
||||
},
|
||||
"right_cheek_wrinkle_info": {
|
||||
"wrinkle_score": 0,
|
||||
"wrinkle_severity_level": 0,
|
||||
"wrinkle_norm_length": 0,
|
||||
"wrinkle_norm_depth": 0,
|
||||
"wrinkle_pixel_density": 0,
|
||||
"wrinkle_area_ratio": 0,
|
||||
"wrinkle_deep_ratio": 0,
|
||||
"wrinkle_deep_num": 0,
|
||||
"wrinkle_shallow_num": 0
|
||||
},
|
||||
"score_info": {
|
||||
"dark_circle_score": 89,
|
||||
"skin_type_score": 35,
|
||||
"wrinkle_score": 90,
|
||||
"oily_intensity_score": 38,
|
||||
"pores_score": 97,
|
||||
"blackhead_score": 99,
|
||||
"acne_score": 100,
|
||||
"sensitivity_score": 100,
|
||||
"melanin_score": 100,
|
||||
"water_score": 93,
|
||||
"rough_score": 84,
|
||||
"total_score": 88,
|
||||
"pores_type_score": {
|
||||
"pores_forehead_score": 92,
|
||||
"pores_leftcheek_score": 98,
|
||||
"pores_rightcheek_score": 98,
|
||||
"pores_jaw_score": 100
|
||||
},
|
||||
"dark_circle_type_score": {
|
||||
"left_dark_circle_score": 89,
|
||||
"right_dark_circle_score": 100
|
||||
},
|
||||
"red_spot_score": 97
|
||||
},
|
||||
"left_eye_pouch_rect": {
|
||||
"left": 102,
|
||||
"top": 137,
|
||||
"width": 68,
|
||||
"height": 59
|
||||
},
|
||||
"right_eye_pouch_rect": {
|
||||
"left": 214,
|
||||
"top": 136,
|
||||
"width": 67,
|
||||
"height": 59
|
||||
},
|
||||
"melasma": {
|
||||
"value": 1,
|
||||
"confidence": 0.50907385
|
||||
},
|
||||
"freckle": {
|
||||
"value": 0,
|
||||
"confidence": 0.45538568
|
||||
},
|
||||
"image_quality": {
|
||||
"face_rect": {
|
||||
"left": 81,
|
||||
"top": 29,
|
||||
"width": 229,
|
||||
"height": 320
|
||||
},
|
||||
"face_ratio": 0.42792428,
|
||||
"hair_occlusion": 0.11386067,
|
||||
"face_orientation": {
|
||||
"yaw": -5.9413967,
|
||||
"pitch": 6.6220365,
|
||||
"roll": 2.846047
|
||||
},
|
||||
"glasses": 0
|
||||
},
|
||||
"sensitivity_type_v1": 1
|
||||
},
|
||||
"face_rectangle": {
|
||||
"top": 108,
|
||||
"left": 77,
|
||||
"width": 238,
|
||||
"height": 238
|
||||
},
|
||||
"side": {
|
||||
"left": {
|
||||
"left_jawline_info": {
|
||||
"left_jawline_quality": 1,
|
||||
"left_jawline_angle_level": 1,
|
||||
"left_jawline_angle": 114.81
|
||||
},
|
||||
"left_acne_info": {
|
||||
"score": 98,
|
||||
"acne": {
|
||||
"rectangle": [],
|
||||
"confidence": [],
|
||||
"polygon": [],
|
||||
"count": 0
|
||||
},
|
||||
"acne_pustule": {
|
||||
"rectangle": [],
|
||||
"confidence": [],
|
||||
"polygon": [],
|
||||
"count": 0
|
||||
},
|
||||
"acne_nodule": {
|
||||
"rectangle": [],
|
||||
"confidence": [],
|
||||
"polygon": [],
|
||||
"count": 0
|
||||
},
|
||||
"acne_mark": {
|
||||
"rectangle": [],
|
||||
"confidence": [],
|
||||
"polygon": [],
|
||||
"count": 0
|
||||
},
|
||||
"closed_comedones": {
|
||||
"confidence": [
|
||||
0.33919
|
||||
],
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"left_melanin_info": {
|
||||
"mole": {
|
||||
"confidence": [
|
||||
0.35832882
|
||||
],
|
||||
"count": 1
|
||||
},
|
||||
"brown_spot": {
|
||||
"confidence": [
|
||||
0.4598653,
|
||||
0.40733993,
|
||||
0.3679863,
|
||||
0.35319555,
|
||||
0.3008949
|
||||
],
|
||||
"count": 5
|
||||
}
|
||||
}
|
||||
},
|
||||
"right": {
|
||||
"right_jawline_info": {
|
||||
"right_jawline_quality": 1,
|
||||
"right_jawline_angle_level": 1,
|
||||
"right_jawline_angle": 103.95
|
||||
},
|
||||
"right_acne_info": {
|
||||
"score": 100,
|
||||
"acne": {
|
||||
"rectangle": [],
|
||||
"confidence": [],
|
||||
"polygon": [],
|
||||
"count": 0
|
||||
},
|
||||
"acne_pustule": {
|
||||
"rectangle": [],
|
||||
"confidence": [],
|
||||
"polygon": [],
|
||||
"count": 0
|
||||
},
|
||||
"acne_nodule": {
|
||||
"rectangle": [],
|
||||
"confidence": [],
|
||||
"polygon": [],
|
||||
"count": 0
|
||||
},
|
||||
"acne_mark": {
|
||||
"rectangle": [],
|
||||
"confidence": [],
|
||||
"polygon": [],
|
||||
"count": 0
|
||||
},
|
||||
"closed_comedones": {
|
||||
"rectangle": [],
|
||||
"confidence": [],
|
||||
"polygon": [],
|
||||
"count": 0
|
||||
}
|
||||
},
|
||||
"right_melanin_info": {
|
||||
"mole": {
|
||||
"confidence": [
|
||||
0.43544632,
|
||||
0.34137803
|
||||
],
|
||||
"count": 2
|
||||
},
|
||||
"brown_spot": {
|
||||
"count": 8
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+146
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"case1": {
|
||||
"profile": {
|
||||
"realAge": 30,
|
||||
"skinType": "混合性"
|
||||
},
|
||||
"skinAge": 27,
|
||||
"totalScore": 70,
|
||||
"cats": {
|
||||
"contour": {
|
||||
"P": 75.5,
|
||||
"S": 9.27,
|
||||
"tier": "预警档",
|
||||
"copy": "下颌线角度左104.54°/右103.75°(标准区间116–120°,双侧均偏离标准,提示下颌缘支撑力下降);眼袋1级、黑眼圈1级;中面部支撑与轮廓清晰度处于预警水平。建议加强轮廓紧致护理:重点针对下颌线与下颌缘区域使用紧致类精华,配合由下向上的提拉按摩手法,同时加强眼周支撑护理,将紧致防线前置。"
|
||||
},
|
||||
"wrinkles": {
|
||||
"P": 23,
|
||||
"S": 7.69,
|
||||
"tier": "预警档",
|
||||
"copy": "全脸皱纹159条、细纹150条,最差区域为左嘴角纹(分区分77,最高严重度3级),部分区域已现静态纹趋势。建议针对眼周、法令纹等高发区使用视黄醇/胜肽类淡纹精华,遏制静态纹转化。"
|
||||
},
|
||||
"smooth": {
|
||||
"P": 57.2,
|
||||
"S": 8.72,
|
||||
"tier": "预警档",
|
||||
"copy": "皮肤表面平整度较低,最差毛孔区位于额头(分区毛孔分32),粗大毛孔1041颗、黑头113颗,T区出油面积57%、左右脸颊20%/2%,纹理分仅82,整体细腻度不足。建议避免过度清洁,重点关注额头,配合低浓度酸类周期护理与柔润精华。"
|
||||
},
|
||||
"tone": {
|
||||
"P": 85,
|
||||
"S": 9.55,
|
||||
"tier": "改善档",
|
||||
"copy": "肤色ITA 58.6(极白,HA 56.2),色素分92,整体稳定但存在局部暗沉倾向(未见明显色斑标记,检出褐色斑37处);水分分78,缺水面积19.8%、严重度22。建议加强抗氧化与光泽管理,做好基础防晒与补水。"
|
||||
},
|
||||
"sens": {
|
||||
"P": 99,
|
||||
"S": 9.97,
|
||||
"tier": "良好档",
|
||||
"copy": "泛红面积0.6%,红区分99,屏障状态稳定,未见明显敏感信号。建议维持现有温和护理,避免过度去角质。"
|
||||
},
|
||||
"acne": {
|
||||
"P": 70,
|
||||
"S": 9.1,
|
||||
"tier": "预警档",
|
||||
"copy": "检出9处皮损(闭口粉刺6处、丘疹3处)。建议尽快进行针对性抗痘护理,避免挤压与刺激,必要时寻求皮肤科专业处理。"
|
||||
}
|
||||
},
|
||||
"dims": {
|
||||
"firm": {
|
||||
"P": 23,
|
||||
"S": 7.69,
|
||||
"tier": "预警档"
|
||||
},
|
||||
"vita": {
|
||||
"P": 57.2,
|
||||
"S": 8.72,
|
||||
"tier": "预警档"
|
||||
},
|
||||
"stab": {
|
||||
"P": 70,
|
||||
"S": 9.1,
|
||||
"tier": "预警档"
|
||||
}
|
||||
},
|
||||
"jawline": {
|
||||
"left": 104.54,
|
||||
"right": 103.75
|
||||
},
|
||||
"group": "25+组",
|
||||
"essence": "35+支撑精华",
|
||||
"avgS": 9.05,
|
||||
"summary": "【综合】实际年龄30岁(接口测得肌肤年龄27岁,差3岁),产品组别25+组;六分类S均分9.05、最差分类皱纹 S=7.69。按三重判据(触发:最差项约束)推荐35+支撑精华:优先改善皱纹(P=23.0)与光滑(P=57.2),其余维度维持现有护理节奏。整体肤质混合性,T区出油面积57%、左右脸颊20%/2%。",
|
||||
"lifestyle": "保持侧卧改仰卧的睡姿习惯,减少枕巾摩擦;控制屏幕时间,缓解眼周疲劳"
|
||||
},
|
||||
"case2": {
|
||||
"profile": {
|
||||
"realAge": 47,
|
||||
"skinType": "混合性"
|
||||
},
|
||||
"skinAge": 50,
|
||||
"totalScore": 88,
|
||||
"cats": {
|
||||
"contour": {
|
||||
"P": 75.9,
|
||||
"S": 9.28,
|
||||
"tier": "预警档",
|
||||
"copy": "下颌线角度左114.81°/右103.95°(标准区间116–120°,双侧均偏离标准,提示下颌缘支撑力下降);眼袋0级、黑眼圈0级;中面部支撑与轮廓清晰度处于预警水平。建议加强轮廓紧致护理:重点针对下颌线与下颌缘区域使用紧致类精华,配合由下向上的提拉按摩手法,同时加强眼周支撑护理,将紧致防线前置。"
|
||||
},
|
||||
"wrinkles": {
|
||||
"P": 50,
|
||||
"S": 8.5,
|
||||
"tier": "预警档",
|
||||
"copy": "全脸皱纹8条、细纹8条,最差区域为右法令纹(分区分50,最高严重度2级),部分区域已现静态纹趋势。建议针对眼周、法令纹等高发区使用视黄醇/胜肽类淡纹精华,遏制静态纹转化。"
|
||||
},
|
||||
"smooth": {
|
||||
"P": 91.7,
|
||||
"S": 9.75,
|
||||
"tier": "良好档",
|
||||
"copy": "皮肤表面细腻平滑,最差毛孔区额头控制良好(分区毛孔分92,粗大毛孔100颗、黑头3颗),纹理分84,角质层排列整齐。建议维持温和清洁与保湿节奏。"
|
||||
},
|
||||
"tone": {
|
||||
"P": 91.5,
|
||||
"S": 9.75,
|
||||
"tier": "良好档",
|
||||
"copy": "肤色ITA 29.5(小麦色,HA 57.8),色素分100,肤色均匀透亮(黄褐斑,检出褐色斑23处、痣1处);水分分93,缺水面积6.8%、严重度7。色素与水润状态均衡,建议持续做好防晒与基础保湿。"
|
||||
},
|
||||
"sens": {
|
||||
"P": 98.7,
|
||||
"S": 9.96,
|
||||
"tier": "良好档",
|
||||
"copy": "泛红面积0.9%,红区分100,屏障状态稳定,未见明显敏感信号。建议维持现有温和护理,避免过度去角质。"
|
||||
},
|
||||
"acne": {
|
||||
"P": 100,
|
||||
"S": 10,
|
||||
"tier": "良好档",
|
||||
"copy": "未检出痤疮与皮损。建议维持清洁与控油节奏,注意作息与饮食。"
|
||||
}
|
||||
},
|
||||
"dims": {
|
||||
"firm": {
|
||||
"P": 50,
|
||||
"S": 8.5,
|
||||
"tier": "预警档"
|
||||
},
|
||||
"vita": {
|
||||
"P": 91.5,
|
||||
"S": 9.75,
|
||||
"tier": "良好档"
|
||||
},
|
||||
"stab": {
|
||||
"P": 98.7,
|
||||
"S": 9.96,
|
||||
"tier": "良好档"
|
||||
}
|
||||
},
|
||||
"jawline": {
|
||||
"left": 114.81,
|
||||
"right": 103.95
|
||||
},
|
||||
"group": "50+组",
|
||||
"essence": "50+重塑精华",
|
||||
"avgS": 9.54,
|
||||
"summary": "【综合】实际年龄47岁(接口测得肌肤年龄50岁,差3岁),产品组别50+组;六分类S均分9.54、最差分类皱纹 S=8.50。按三重判据(触发:年龄组档)推荐50+重塑精华:优先改善皱纹(P=50.0)与轮廓(P=75.9),其余维度维持现有护理节奏。整体肤质混合性,T区出油面积62%、左右脸颊16%/16%。",
|
||||
"lifestyle": "保持侧卧改仰卧的睡姿习惯,减少枕巾摩擦;控制屏幕时间,缓解眼周疲劳"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/* ═════ 巨子测肤 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();
|
||||
Reference in New Issue
Block a user