fix(camera): make lighting checks advisory only

This commit is contained in:
xsl
2026-10-16 15:51:07 +08:00
parent 9ff7a582af
commit 6a628f837b
5 changed files with 28 additions and 17 deletions
+9
View File
@@ -0,0 +1,9 @@
# 拍摄光线仅提示
亮度低于 110 时显示「建议到光线明亮处拍摄,保持不动即可拍摄」;高于 220 时显示「光线过强,建议避开强光,保持不动即可拍摄」。110–220 不提示光线问题。
亮度不再是实时捕捉或拍后 JPEG 校验的拦截条件,无法读出亮度也不单独拦截。其他人脸置信度、角度、位置、距离和稳定性要求不变;过暗导致无法识别人脸时仍需要调整,不保证检测接口接受所有极暗或过曝照片。
保留亮度观测值,供后台校准使用;不改变报告计分、结论或付费接口调用次数。策略版本 `2026-10-16-light-advisory-v10`。
验证包含三视图暗/亮边界测试、实时暗光提示、保持暗光完成倒计时和拍后校验、实际本地模型自动三连拍;不调用付费检测。
+1 -1
View File
@@ -1036,7 +1036,7 @@
updateAngleHaptic(!!(verdict && verdict.reason === 'angle'), Date.now());
setMask(ok ? 'ok' : (noFace ? 'no-face' : ''));
// 合格时「保持不动」只显示不播报:随即抓拍,「拍摄完成」会截断它
setHint(ok ? '保持不动' : hint, ok, ok);
setHint(ok ? (verdict.warning ? verdict.warning + ',保持不动即可拍摄' : '保持不动') : hint, ok, ok);
if (!ok) { okSince = 0; holdState = null; return; }
var held = CaptureQuality.hold(holdState, verdict.observation, Date.now());
holdState = held.state;
+6 -6
View File
@@ -6,7 +6,7 @@
'use strict';
// Real-phone feedback: ease alignment and normal hand/head movement while
// retaining two checked frames, one face, framing and final JPEG validation.
var config = Object.freeze({ version: '2026-10-16-confidence-v9', score: 0.3, frontScore: 0.25,
var config = Object.freeze({ version: '2026-10-16-light-advisory-v10', score: 0.3, frontScore: 0.25,
near: 0.30, far: 0.68, center: 0.16, edge: 0.01,
dark: 110, bright: 220, holdMs: 450, minFrames: 2, maxGapMs: 2000,
yawDrift: 8, pitchDrift: 8, centerDrift: 0.055, sizeDrift: 0.065 });
@@ -33,11 +33,11 @@
if (options.ignoreDirection && target !== 'front') target = pose.yaw < 0 ? 'left' : 'right';
var check = HeadPose.check(d.landmarks.positions, target, options.mirrored);
if (!check.ok) return failed(check.hint, finite(check.yaw) ? 'angle' : 'quality');
if (!finite(options.luma)) return failed('请保持面部清晰');
if (options.luma < config.dark) return failed('请到光线充足处');
if (options.luma > config.bright) return failed('光线过强,请避开强光');
return { ok: true, hint: '保持不动', observation: { yaw: pose.yaw, pitch: pose.pitch, roll: pose.roll,
score: d.detection.score, luma: options.luma, ratio: ratio,
// Lighting is advisory for both live frames and saved JPEGs, never a gate.
var warning = finite(options.luma) && options.luma < config.dark ? '建议到光线明亮处拍摄' :
finite(options.luma) && options.luma > config.bright ? '光线过强,建议避开强光' : '';
return { ok: true, hint: '保持不动', warning: warning, observation: { yaw: pose.yaw, pitch: pose.pitch, roll: pose.roll,
score: d.detection.score, luma: finite(options.luma) ? options.luma : null, ratio: ratio,
center_x: (box.x + box.width / 2) / w, center_y: (box.y + box.height / 2) / h } };
}
function hold(state, observation, now) {
+5 -5
View File
@@ -117,10 +117,8 @@ try {
assert((await page.locator('#cam-step').textContent()).includes('1/3'), 'invalid frontal angle never advances');
assert.equal(await page.evaluate(() => window.__voice.some(row => /^hint-turn-|^step[23]-|^shot-done$/.test(row.id))), false, 'no turn/next-step/completion audio before a successful front photo');
await page.evaluate(() => { window.__cameraFixture.light(.6); return window.__cameraFixture.set('C3_180.jpg'); });
await page.waitForFunction(()=>window.__evaluations.some(row=>row.target==='front'&&!row.ok&&row.hint==='请到光线充足处'));
assert((await page.locator('#cam-step').textContent()).includes('1/3'),'dim sample cannot advance');
// Simulate improving room lighting, not bypassing the actual quality gate.
await page.evaluate(()=>window.__cameraFixture.light(1.3));
await page.waitForFunction(()=>document.querySelector('#cam-hint').textContent.includes('建议到光线明亮处拍摄'));
// Keep the dim sample through countdown AND final JPEG validation.
try { await page.waitForFunction(() => document.querySelector('#cam-step').textContent.includes('2/3')); }
catch (error) {
console.log(await page.evaluate(async () => {
@@ -132,6 +130,7 @@ try {
}).then(value => JSON.stringify(value, null, 2)), errors); throw error;
}
await page.waitForFunction(() => window.__voice.some(row => row.id === 'step2-rightface'));
await page.evaluate(()=>window.__cameraFixture.light(1.3));
const firstVoice = await page.evaluate(() => window.__voice);
const completion = firstVoice.find(row => row.id === 'shot-done'), turn = firstVoice.find(row => row.id === 'step2-rightface');
assert(completion && turn && completion.step.includes('1/3') && turn.step.includes('2/3'));
@@ -152,7 +151,8 @@ try {
for (const key of ['front', 'left', 'right']) {
const row = body.capture_diagnostics.views[key];
assert.equal(row.mode, 'live'); assert.equal(row.direction_checked, true);
assert(row.score >= (key==='front'?.3:.4) && row.luma >= 110 && row.luma <= 220);
assert(row.score >= (key==='front'?.25:.3));
if(key==='front') assert(row.luma < 110, 'dim JPEG still passes final validation');
assert(Math.abs(row.roll) <= 22);
}
assert(Math.abs(body.capture_diagnostics.views.front.yaw) <= 18);
+7 -5
View File
@@ -55,15 +55,17 @@ 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, 50, 240, NaN]) assert(!check(face(), { luma }).ok);
assert.equal(check(face(), { luma: 50 }).reason, 'quality', 'lighting must not trigger angle vibration');
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 luma of [65,100,109.99]) assert.equal(check(face(),{luma}).hint,'请到光线充足处');
for (const luma of [220.01,235,255]) assert.equal(check(face(),{luma}).hint,'光线过强,请避开强光');
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 }]) {
@@ -121,7 +123,7 @@ 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-confidence-v9');
assert.equal(normalized.version, '2026-10-16-light-advisory-v10');
assert.equal(normalized.views.front.luma, undefined);
assert(!JSON.stringify(normalized).includes('discard'));
assert.equal(Diagnostics.normalize({ version: '<script>' }).version, 'unknown');