'use strict'; const assert = require('node:assert/strict'); const Quality = require('../js/capture-quality'); const HeadPose = require('../js/head-pose'); const ReportUI = require('../js/report-ui'); const Diagnostics = require('../lib/capture-diagnostics'); function points(yaw = 0, pitch = 0, roll = 0) { const result = Array.from({ length: 68 }, () => ({ x: 200, y: 150 })); result[36] = { x: 150, y: 100 }; result[45] = { x: 250, y: 100 }; result[19] = { x: 170, y: 70 }; result[24] = { x: 230, y: 70 }; result[8] = { x: 200, y: 240 }; // Solve the empirical yaw geometry rather than duplicating a second policy. let low = -80, high = 80; for (let i = 0; i < 40; i++) { const mid = (low + high) / 2; result[30] = { x: 200 + mid, y: 140 }; result[33] = { x: 200, y: 161.5 }; if (HeadPose.estimate(result, false).yaw < yaw) low = mid; else high = mid; } result[33] = { x: 200, y: 161.5 + pitch / 180 * 170 }; if (roll) { const angle = roll * Math.PI / 180; for (const p of result) { const x = p.x - 200, y = p.y - 150; p.x = 200 + x * Math.cos(angle) - y * Math.sin(angle); p.y = 150 + x * Math.sin(angle) + y * Math.cos(angle); } } return result; } const face = (yaw = 0, pitch = 0, roll = 0) => ({ detection: { score: .95, box: { x: 170, y: 160, width: 260, height: 300 } }, landmarks: { positions: points(yaw, pitch, roll) } }); const options = { count: 1, target: 'front', width: 600, height: 600, offsetX: 0, luma: 180, mirrored: false }; const check = (d, changes) => Quality.evaluate(d, { ...options, ...changes }); assert(check(face()).ok); assert(!check(null).ok); assert(!check(face(), { count: 2 }).ok); for (const yaw of [-17.99, 17.99]) assert(check(face(yaw)).ok, '20% relaxed front limit now passes'); for (const yaw of [-18.01, 18.01, -45, 45]) { assert(!check(face(yaw)).ok); assert.equal(check(face(yaw)).reason, 'angle'); assert.equal(check(face(yaw)).hint, '请正对屏幕', 'front correction must not ask for a side view before capture'); assert.equal(check(face(yaw), { mirrored: true }).hint, '请正对屏幕'); } for (const pitch of [-25.99,25.99]) assert(check(face(0, pitch)).ok); for (const pitch of [-26.01,26.01]) assert.equal(check(face(0, pitch)).hint, '请正对屏幕'); assert.equal(check(face(0), { target: 'right' }).hint, '请向左转头', 'side step retains its proper turn instruction'); assert.equal(check(face(0), { target: 'left' }).hint, '请向右转头'); assert(check(face(0, 0, 12)).ok, 'normal tilt now passes'); for(const roll of [-21.99,21.99]) assert(check(face(0,0,roll)).ok); for(const roll of [-22.01,22.01]) assert.equal(check(face(0,0,roll)).hint,'请勿歪头'); for (const target of ['left','right']) { const sign=target==='left'?-1:1; for (const yaw of [23.99,84.01]) assert(!check(face(sign*yaw),{target}).ok); for (const yaw of [24.01,83.99]) assert(check(face(sign*yaw),{target}).ok); for (const pitch of [-28.99,28.99]) assert(check(face(sign*45,pitch),{target}).ok); for (const pitch of [-29.01,29.01]) assert(!check(face(sign*45,pitch),{target}).ok); } assert(check(face(45), { target: 'right' }).ok); assert(!check(face(45), { target: 'left' }).ok); assert(check(face(-45), { target: 'left' }).ok); assert(check(face(-45), { target: 'right', mirrored: true }).ok); assert(check(face(-45), { target: 'right', ignoreDirection: true }).ok); assert(!check(face(23), { target: 'left', ignoreDirection: true }).ok); for (const luma of [null, 0, 50, 240, 255, NaN]) assert(check(face(), { luma }).ok, 'lighting is advisory, not blocking'); assert.equal(check(face(), { luma: 50 }).reason, undefined, 'lighting must not trigger angle vibration'); assert.equal(check(null).reason, 'quality'); assert.equal(check(face(), { count: 2 }).reason, 'quality'); const brokenLandmarks = face(); brokenLandmarks.landmarks.positions = []; assert.equal(check(brokenLandmarks).reason, 'quality', 'unreadable pose is not confirmed wrong angle'); for (const luma of [110,110.01,120,149.99,180,219.99,220]) assert(check(face(), { luma }).ok); for (const target of ['front','left','right']) for (const luma of [0,65,100,109.99,110,220,220.01,235,255,null,NaN]) { const verdict=check(face(target==='front'?0:target==='left'?-45:45),{target,luma}); assert(verdict.ok);assert.equal(verdict.warning,Number.isFinite(luma)&&luma<110?'建议到光线明亮处拍摄':Number.isFinite(luma)&&luma>220?'光线过强,建议避开强光':''); } assert(check(face(), { offsetX: .15 }).ok); assert(!check(face(), { offsetX: .17 }).ok); for (const changes of [{ height: 190 }, { height: 400 }, { y: 8 }]) { const d = face(); Object.assign(d.detection.box, changes); assert(check(d).ok, 'former framing limits now pass'); } for (const changes of [{ height: 179 }, { height: 410 }, { x: -10 }, { y: 5 }]) { const d = face(); Object.assign(d.detection.box, changes); assert(!check(d).ok); } const weak = face(); weak.detection.score = .45; assert(check(weak).ok); for(const score of [.25,.29,.3,.35,.3999]){const d=face();d.detection.score=score;assert(check(d).ok,'relaxed front confidence');} const low=face();low.detection.score=.2499;assert.equal(check(low).hint,'请保持面部清晰'); for(const target of ['left','right']){const d=face(target==='left'?-45:45);d.detection.score=.2999;assert.equal(check(d,{target}).hint,'请保持面部清晰');for(const score of [.3,.35,.4]){d.detection.score=score;assert(check(d,{target}).ok);}} assert.equal(Quality.scoreThreshold('front'),.25);assert.equal(Quality.scoreThreshold('left'),.3);assert.equal(Quality.scoreThreshold('right'),.3); for(const target of ['front','left','right']) for(const score of [0,-1,NaN,Infinity]){const d=face(target==='front'?0:target==='left'?-45:45);d.detection.score=score;assert.equal(check(d,{target}).hint,'请保持面部清晰');} weak.detection.score = .24; assert(!check(weak).ok); const obs = check(face()).observation; let held = Quality.hold(null, obs, 1000); assert(!held.ready); held = Quality.hold(held.state, obs, 1400); assert(!held.ready); assert(!Quality.hold(held.state, obs, 1449).ready); assert(Quality.hold(held.state, obs, 1450).ready); assert(Quality.hold(Quality.hold(null, obs, 1000).state, obs, 1450).ready, 'two stable frames at 450ms now pass'); assert.equal(Quality.hold(held.state, null, 1700).state, null); assert.equal(Quality.hold(held.state, obs, 3500).state.frames, 1, 'gap resets'); const slow = Quality.hold(Quality.hold(null, obs, 1000).state, obs, 2100); assert(slow.ready, '1100ms inference now only needs two checked frames'); for (const change of [{ yaw: 7 }, { pitch: 7 }, { ratio: .56 }, { center_x: .55 }, { center_y: .55 }]) assert(Quality.hold(held.state, { ...obs, ...change }, 1500).ready, 'small movement no longer resets'); for (const change of [{ yaw: 9 }, { pitch: 9 }, { ratio: .6 }, { center_x: .56 }, { center_y: .58 }]) assert.equal(Quality.hold(held.state, { ...obs, ...change }, 1500).state.frames, 1, 'large motion still resets'); for (const [dims, key] of [ [[10, 50, 90], 'firm'], [[90, 10, 50], 'vita'], [[90, 50, 10], 'stab'], [[50, 50, 90], 'firm'], [[null, 50, 50], 'vita'], [[50.1, 50, 90], 'vita'] ]) { const vm = { dims: Object.fromEntries(['firm', 'vita', 'stab'].map((key, i) => [key, { P: dims[i] }])), focus: { name: '旧名字' } }; const original = JSON.stringify(vm), focus = ReportUI.focusOf(vm); assert.equal(focus.key, key); assert.equal(focus.name, ReportUI.dimNames[key]); assert(focus.tip.endsWith('。')); assert.equal(JSON.stringify(vm), original); } assert.equal(ReportUI.focusOf({ dims: {} }), null); const wrinkle = { key: 'wrinkles', text: '原始皱纹结论。\n分区明细(左右以本人为准):\n抬头纹:皱纹分 13。' }; assert.equal(ReportUI.customerText(wrinkle), '原始皱纹结论。'); assert(wrinkle.text.includes('分区明细'), 'historical snapshot not overwritten'); assert.equal(ReportUI.customerText({ key: 'contour', text: '轮廓清晰;侧脸角度数据未完整获取;其他结论。' }), '轮廓清晰;其他结论。'); for (const key of ['contour','wrinkles','color','smooth','water','sens','acne']) { const missing={key,valid:false,text:'本次未检出有效数据,请重新拍摄后查看。'}; const before=JSON.stringify(missing); assert.equal(ReportUI.customerText(missing),''); assert.equal(JSON.stringify(missing),before,'retain backend diagnostic'); assert.equal(ReportUI.customerText({key,text:'本次未检出有效数据'}),'','legacy missing-data text hidden'); assert.equal(ReportUI.customerText({key,text:'正常结论。',valid:true}),'正常结论。'); } assert.equal(ReportUI.customerText({key:'contour',text:'本次未获取轮廓有效数据。'}),''); const raw = { left_side_result: { left_jawline_info: { left_jawline_quality: 3, left_jawline_angle: 110 } }, side_result: { right_side_result: { right_jawline_info: { right_jawline_quality: 1, right_jawline_angle: 115 } } } }; assert.equal(Diagnostics.assess(raw).status, 'invalid'); assert.equal(Diagnostics.assess({}).status, 'unavailable'); assert.equal(Diagnostics.assess(raw).views.right.status, 'valid'); const normalized = Diagnostics.normalize({ version: Quality.config.version, secret: 'discard', views: { front: { mode: 'live', yaw: 1, score: .9, luma: Infinity, secret: 'discard' }, left: { mode: 'unchecked', reason: 'model-unavailable' } } }); assert.equal(normalized.policy.capture.holdMs, 450); assert.equal(normalized.policy.capture.minFrames, 2); assert.equal(normalized.version, '2026-10-16-preview-only-v11'); assert.equal(normalized.views.front.luma, undefined); assert(!JSON.stringify(normalized).includes('discard')); assert.equal(Diagnostics.normalize({ version: '