fix(camera): constrain brightness and adjust side photo previews

This commit is contained in:
xsl
2026-10-15 20:41:28 +08:00
parent 480fed40cc
commit 8115db4ec2
6 changed files with 56 additions and 15 deletions
+9 -3
View File
@@ -51,14 +51,16 @@ try {
AudioRecorder.prototype = OriginalAudioContext.prototype;
window.AudioContext = window.webkitAudioContext = AudioRecorder;
const canvas = document.createElement('canvas'); canvas.width = 800; canvas.height = 600;
let currentImage, offsetX = 0;
let currentImage, offsetX = 0, lightGain = 1;
function draw() {
if (!currentImage) return;
const c = canvas.getContext('2d'); c.fillStyle = '#111'; c.fillRect(0, 0, canvas.width, canvas.height);
c.filter='brightness('+lightGain+')';
c.drawImage(currentImage, offsetX, 0, canvas.width, canvas.height);
c.filter='none';
}
setInterval(draw, 100);
window.__cameraFixture = { canvas, set: async name => {
window.__cameraFixture = { canvas, light:gain=>{lightGain=gain;draw();}, set: async name => {
const image = new Image(); image.src = 'sample-' + name; await image.decode();
// Simulate a person following the centering prompt, not an off-center
// landscape source stretched into a portrait camera frame.
@@ -115,6 +117,10 @@ 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.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));
try { await page.waitForFunction(() => document.querySelector('#cam-step').textContent.includes('2/3')); }
catch (error) {
console.log(await page.evaluate(async () => {
@@ -146,7 +152,7 @@ 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 >= .4 && row.luma >= 100 && row.luma <= 235);
assert(row.score >= .4 && row.luma >= 150 && row.luma <= 220);
assert(Math.abs(row.roll) <= 18);
}
assert(Math.abs(body.capture_diagnostics.views.front.yaw) <= 18);
+5 -4
View File
@@ -24,7 +24,7 @@ function points(yaw = 0, pitch = 0, roll = 0) {
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: 120, mirrored: false };
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);
@@ -61,8 +61,9 @@ 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 [100, 100.01, 230, 235]) assert(check(face(), { luma }).ok);
for (const luma of [65,70,99.99]) assert.equal(check(face(),{luma}).hint,'请到光线充足处');
for (const luma of [150, 150.01, 180, 219.99, 220]) assert(check(face(), { luma }).ok);
for (const luma of [65,100,120,149.99]) assert.equal(check(face(),{luma}).hint,'请到光线充足处');
for (const luma of [220.01,235,255]) assert.equal(check(face(),{luma}).hint,'光线过强,请避开强光');
assert(check(face(), { offsetX: .15 }).ok);
assert(!check(face(), { offsetX: .17 }).ok);
for (const changes of [{ height: 190 }, { height: 400 }, { y: 8 }]) {
@@ -115,7 +116,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-15-dark-100-v4');
assert.equal(normalized.version, '2026-10-15-light-150-220-v5');
assert.equal(normalized.views.front.luma, undefined);
assert(!JSON.stringify(normalized).includes('discard'));
assert.equal(Diagnostics.normalize({ version: '<script>' }).version, 'unknown');
+23
View File
@@ -0,0 +1,23 @@
'use strict';
const assert=require('node:assert/strict'),fs=require('node:fs'),vm=require('node:vm');
const source=fs.readFileSync(require('node:path').join(__dirname,'../js/app.js'),'utf8');
const helper=source.slice(source.indexOf(' function reportZoomSource('),source.indexOf(" document.querySelectorAll('.card-detail').forEach(function (card, group)"));
const drawn=[],canvases=[];
const frame={getBoundingClientRect:()=>({left:0,top:0,width:100,height:90})};
const img={naturalWidth:1000,naturalHeight:1000,src:'original',getBoundingClientRect:()=>({left:-90,top:-104,width:280,height:252})};
const context={getComputedStyle:e=>e===frame?{backgroundColor:'#f6f6f6'}:{objectFit:'cover',objectPosition:'50% 50%'},document:{createElement:()=>{
const canvas={getContext:()=>({fillRect(){},drawImage(...args){drawn.push(args);}}),toDataURL:()=> 'cropped'};canvases.push(canvas);return canvas;
}}};
vm.runInNewContext(helper,context);
assert.equal(context.reportZoomSource(img,frame,1),'cropped');
assert.equal(context.reportZoomSource(img,frame,.5),'cropped');
assert.equal(drawn[1][3],drawn[0][3]*.5);assert.equal(drawn[1][4],drawn[0][4]*.5);
assert.equal(canvases[0].width,canvases[1].width);assert.equal(canvases[0].height,canvases[1].height);
assert(drawn[1][1]<=0&&drawn[1][1]+drawn[1][3]>=canvases[1].width);
assert(drawn[1][2]<=0&&drawn[1][2]+drawn[1][4]>=canvases[1].height);
assert.equal(context.reportZoomSource(img,frame,.5,.5),'cropped');
assert.equal(drawn[2][2]-drawn[1][2],drawn[1][4]*.5,'move down by exactly half the scaled photo height');
assert.equal(drawn[2][1],drawn[1][1]);assert.equal(drawn[2][3],drawn[1][3]);assert.equal(drawn[2][4],drawn[1][4]);
assert.equal(canvases[2].height,canvases[1].height);
assert(source.includes('sideButton ? .5 : 1'),'half-sized fill applies only to side-photo large previews');
console.log('PASS side zoom fill: size halved, then moved down exactly 50% photo height; preview frame/front/small images unchanged');