'use strict'; const { test } = require('node:test'), assert = require('node:assert/strict'); const quality = require('../utils/native-face-quality'); const h5 = require('../../h5/js/capture-quality'); // A complete synthetic dlib68 face: jaw 0..16, brows 17..26, nose 27..35, // eyes 36..47, mouth 48..67. These are original 720x1280-frame coordinates, // not normalized model inputs or AR106 landmarks. No real face data is used. function face(noseShift = 0) { const positions = []; for (let i = 0; i < 17; i++) { const angle = Math.PI - i * Math.PI / 16; positions.push({ x: 360 + Math.cos(angle) * 165, y: 470 + Math.sin(angle) * 360 }); } for (let i = 0; i < 10; i++) positions.push({ x: 245 + (i < 5 ? i : i + 2) * 18, y: 390 - Math.sin((i % 5) * Math.PI / 4) * 12 }); for (let i = 0; i < 9; i++) positions.push({ x: 360 + noseShift + (i < 4 ? 0 : (i - 6) * 12), y: 465 + i * 18 }); for (const center of [290, 430]) for (let i = 0; i < 6; i++) { const angle = Math.PI - i * Math.PI / 3; positions.push({ x: center + Math.cos(angle) * 30, y: 450 - Math.sin(angle) * 13 }); } for (let i = 0; i < 20; i++) { const angle = i * 2 * Math.PI / (i < 12 ? 12 : 8); positions.push({ x: 360 + Math.cos(angle) * (i < 12 ? 62 : 32), y: 705 + Math.sin(angle) * (i < 12 ? 24 : 10) }); } // Landmark semantics used by H5: eye outer corners, brow peaks, nose tip / // base, chin. The neutral pitch ratio is (618-420)/(830-390)=0.45. for (const [index, x, y] of [[36,260,450],[45,460,450],[19,285,390],[24,435,390], [8,360,830],[30,360+noseShift,530],[33,360+noseShift,618]]) positions[index] = { x, y }; return { detection: { box: { x: 180, y: 250, width: 360, height: 650 }, score: .9 }, landmarks: { positions } }; } function frame(noseShift = 0) { return { width: 720, height: 1280, elapsedMs: 71, detections: [face(noseShift)] }; } test('complete dlib68 model output uses the H5 semantic points and never returns image geometry', () => { const input = frame(), original = JSON.stringify(input), verdict = quality.evaluate(input, 'front'); assert.equal(input.detections[0].landmarks.positions.length, 68); assert.equal(verdict.ok, true); assert.equal(verdict.observation.yaw, 0); assert(Math.abs(verdict.observation.pitch) < 1e-10); assert.equal(verdict.observation.roll, 0); assert.equal(verdict.faceCount, 1); assert.equal(verdict.frameReceived, true); assert.equal(verdict.width, 720); assert.equal(verdict.height, 1280); assert.equal(JSON.stringify(input), original); assert(!/detections|landmarks|positions|capturedImage|anchors|"box"/.test(JSON.stringify(verdict))); // A 68-point input must not be accidentally remapped through AR106 indices. const invalidEye = frame(); invalidEye.detections[0].landmarks.positions[36].x = NaN; assert.equal(quality.evaluate(invalidEye, 'front').ok, false); }); test('front, right and left use the unchanged physical head-turn semantics', () => { for (const [shift, target, sign] of [[0, 'front', 0], [35.4, 'right', 1], [-35.4, 'left', -1]]) { const verdict = quality.evaluate(frame(shift), target); assert.equal(verdict.ok, true, target); if (sign) assert(verdict.observation.yaw * sign > 40 && verdict.observation.yaw * sign < 50); } assert.equal(quality.evaluate(frame(35.4), 'left').hint, '请向右转头'); assert.equal(quality.evaluate(frame(-35.4), 'right').hint, '请向左转头'); assert.equal(quality.evaluate(frame(35.4), 'front').hint, '请正对屏幕'); assert.equal(quality.evaluate(frame(), 'right').ok, false, 'frontal face cannot pass a side step'); }); test('mirroring is explicit and defaults to unmirrored raw-frame points', () => { assert.equal(quality.evaluate(frame(35.4), 'right').ok, true); assert.equal(quality.evaluate(frame(35.4), 'right', { mirrored: false }).ok, true); const mirrored = quality.evaluate(frame(35.4), 'left', { mirrored: true }); assert.equal(mirrored.ok, true); assert(mirrored.observation.yaw < -40); assert.equal(quality.evaluate(frame(35.4), 'right', { mirrored: true }).ok, false); assert.equal(quality.evaluate(frame(), 'front', { mirrored: true }).ok, true); }); test('native side steps accept a small correct turn while H5 retains its original angle gate', () => { for (const [shift, target] of [[12, 'right'], [-12, 'left']]) { const input = frame(shift), verdict = quality.evaluate(input, target); assert.equal(verdict.ok, true); assert(Math.abs(verdict.observation.yaw) >= 12 && Math.abs(verdict.observation.yaw) < 24); assert.equal(h5.evaluate(input.detections[0], { count: 1, target, width: 720, height: 1280, offsetX: 0 }).ok, false); assert.equal(quality.evaluate(input, target === 'left' ? 'right' : 'left').ok, false); assert.equal(quality.evaluate(input, target, { mirrored: true }).ok, false); } assert.equal(quality.evaluate(frame(), 'right').ok, false); assert.equal(quality.evaluate(frame(5), 'right').ok, false); assert.equal(quality.evaluate(frame(), 'left').ok, false); }); test('relaxed side angles retain single-face, framing, confidence and excessive-tilt rejection', () => { const pitched = frame(12); pitched.detections[0].landmarks.positions[33].y += 80; assert.equal(quality.evaluate(pitched, 'right').ok, true, 'about 33 degree pitch is tolerated on side steps'); pitched.detections[0].landmarks.positions[33].y += 40; assert.equal(quality.evaluate(pitched, 'right').ok, false); const tilted = frame(12); tilted.detections[0].landmarks.positions[45].y += 95; assert.equal(quality.evaluate(tilted, 'right').ok, true, 'about 25 degree roll is tolerated on side steps'); tilted.detections[0].landmarks.positions[45].y += 45; assert.equal(quality.evaluate(tilted, 'right').ok, false); const multiple = frame(12); multiple.detections.push(face()); assert.equal(quality.evaluate(multiple, 'right').hint, '请保持单人出镜'); const edge = frame(12); edge.detections[0].detection.box.x = 0; assert.equal(quality.evaluate(edge, 'right').hint, '请将脸部完整移入取景框'); const blurry = frame(12); blurry.detections[0].detection.score = .27; assert.equal(quality.evaluate(blurry, 'right').hint, '请保持面部清晰'); }); test('missing frames and completed no-face results remain distinguishable and fail closed', () => { for (const invalid of [null, {}, { ...frame(), width: NaN }, { ...frame(), height: 0 }, { ...frame(), detections: null }]) { const verdict = quality.evaluate(invalid, 'front'); assert.equal(verdict.frameReceived, false); assert.equal(verdict.reason, 'frame'); assert.equal(verdict.ok, false); assert.equal(verdict.faceCount, 0); assert.equal(verdict.width, 0); assert.equal(verdict.height, 0); } const empty = quality.evaluate({ ...frame(), detections: [] }, 'front'); assert.equal(empty.frameReceived, true); assert.equal(empty.faceCount, 0); assert.equal(empty.hint, '未检测到人脸'); assert.equal(quality.evaluate({ ...frame(), detections: [{}] }, 'front').ok, false); }); test('multiple people, distance, confidence and raw-frame edges retain H5 priorities', () => { const multiple = frame(); multiple.detections.push(face()); assert.equal(quality.evaluate(multiple, 'front').hint, '请保持单人出镜'); const distant = frame(); distant.detections[0].detection.box.height = 350; assert.equal(quality.evaluate(distant, 'front').hint, '请靠近一点'); const close = frame(); close.detections[0].detection.box.height = 880; assert.equal(quality.evaluate(close, 'front').hint, '请远一点'); const lowConfidence = frame(); lowConfidence.detections[0].detection.score = .24; assert.equal(quality.evaluate(lowConfidence, 'front').hint, '请保持面部清晰'); const frontOnlyConfidence = frame(); frontOnlyConfidence.detections[0].detection.score = .27; assert.equal(quality.evaluate(frontOnlyConfidence, 'front').ok, true); assert.equal(quality.evaluate(frontOnlyConfidence, 'right').hint, '请保持面部清晰'); for (const box of [{ x: 0 }, { y: 0 }, { x: 365 }, { y: 650 }]) { const edge = frame(); Object.assign(edge.detections[0].detection.box, box); assert.equal(quality.evaluate(edge, 'front').hint, '请将脸部完整移入取景框'); } }); test('centering uses the visible object-fit cover crop rather than raw-frame width alone', () => { const input = frame(); input.detections[0].detection.box.x += 100; assert.equal(quality.evaluate(input, 'front').ok, true, '100/720 is inside the raw-width centering margin'); assert.equal(quality.evaluate(input, 'front', { viewWidth: 200, viewHeight: 1000 }).hint, '请将脸部移入取景框'); assert.equal(quality.evaluate(input, 'front', { viewWidth: 200, viewHeight: 1000, mirrored: true }).ok, false); assert.equal(quality.evaluate(frame(), 'front', { viewWidth: 200, viewHeight: 1000 }).ok, true); assert.equal(quality.evaluate(input, 'front', { viewWidth: 0, viewHeight: NaN }).ok, true, 'missing viewport falls back to the frame'); }); test('pitch and roll failures use actual nose-base and eye-outer points', () => { const down = frame(); down.detections[0].landmarks.positions[33].y = 490; assert.equal(quality.evaluate(down, 'front').hint, '请正对屏幕'); const tilted = frame(); tilted.detections[0].landmarks.positions[45].y += 100; assert.equal(quality.evaluate(tilted, 'front').hint, '请勿歪头'); const malformed = frame(); malformed.detections[0].landmarks.positions = []; assert.equal(quality.evaluate(malformed, 'front').ok, false); }); test('measured face brightness reaches the H5 warning rules on front and relaxed side steps', () => { for (const [value, warning] of [[50, '建议到光线明亮处拍摄'], [240, '光线过强,建议避开强光'], [160, '']]) { for (const [shift, target] of [[0, 'front'], [12, 'right']]) { const verdict = quality.evaluate({ ...frame(shift), luma: value }, target); assert.equal(verdict.ok, true); assert.equal(verdict.warning, warning); assert.equal(verdict.observation.luma, value); } } const unavailable = quality.evaluate({ ...frame(), luma: NaN }, 'front'); assert.equal(unavailable.ok, true); assert.equal(unavailable.warning, ''); assert.equal(unavailable.observation.luma, null); }); test('lighting stays advisory and stable holding directly uses the H5 implementation', () => { const dim = quality.evaluate(frame(), 'front', { luma: 50 }); assert.equal(dim.ok, true); assert.equal(dim.warning, '建议到光线明亮处拍摄'); assert.equal(dim.observation.luma, 50); assert.equal(quality.evaluate(frame(), 'front', { luma: 240 }).warning, '光线过强,建议避开强光'); assert.equal(quality.evaluate(frame(), 'front', { luma: NaN }).observation.luma, null); assert.equal(quality.hold, require('../vendor/capture-quality').hold); assert.deepEqual(quality.config, h5.config); let held = quality.hold(null, dim.observation, 1000); assert.equal(held.ready, false); held = quality.hold(held.state, dim.observation, 1449); assert.equal(held.ready, false); held = quality.hold(held.state, dim.observation, 1450); assert.equal(held.ready, true); assert.equal(quality.hold(held.state, null, 1500).state, null); assert.equal(quality.hold(held.state, dim.observation, 4000).ready, false); });