perf(report): load side wrinkle layers after report entry

This commit is contained in:
xsl
2026-10-16 16:34:11 +08:00
parent b19811f974
commit 9181069643
8 changed files with 256 additions and 8 deletions
+51 -5
View File
@@ -31,6 +31,32 @@ const CaptureDiagnostics = require('./lib/capture-diagnostics.js');
const WrinkleViews = require('./lib/wrinkle-views.js');
const captureFunnel = require('./lib/capture-funnel.js')(path.join(DATA_ROOT, 'capture-funnel.jsonl'));
const accounts = require('./lib/accounts.js')(path.join(DATA_ROOT, 'accounts'));
const wrinkleJobs = require('./lib/wrinkle-jobs.js')({
dir:path.join(DATA_ROOT,'wrinkle-jobs'),
collect:async context=>WrinkleViews.forReport(await WrinkleViews.collect(context.primary,context.photos,{
key:CRED.key,secret:CRED.secret,url:FACEPP_URL,gapMs:process.env.NODE_ENV==='test'?0:4000,deadline:Date.now()+60000
})),
load(owner,id) {
const snapshot=accounts.reportSnapshot(owner,id);
if (!snapshot) return null;
return {primary:{left_side_result:snapshot.payload.side?.left,right_side_result:snapshot.payload.side?.right},
photos:{left:stripDataPrefix(snapshot.photos[1]||''),right:stripDataPrefix(snapshot.photos[2]||'')}};
},
complete(owner,id,views,cost) {
accounts.updateWrinkleViews(owner,id,views);
const rec=findRecord(id);
if (!rec || rec.owner_id!==owner) return;
rec.wrinkle_view_status=Object.fromEntries(['left','right'].map(v=>[v,{status:views[v]?.status||'unavailable',error_code:views[v]?.error_code||null}]));
rec.wrinkle_cost_ms=cost;rec.updated_at=nowIso();updateRecord(rec);
if (rec.raw_file) {
const file=path.join(DATA_DIR,path.basename(rec.raw_file));
try {
const raw=JSON.parse(fs.readFileSync(file,'utf8'));raw.wrinkle_views=views;
fs.writeFileSync(file+'.wrinkle.tmp',JSON.stringify(raw),{mode:0o600});fs.renameSync(file+'.wrinkle.tmp',file);
} catch (_) {}
}
}
});
/* 服务端评分引擎:与 C 端同一份公式(禁止复制公式,直接复用) */
const SkinScore = require('./js/skin-score.js');
@@ -326,17 +352,17 @@ function fillFailure(rec, code) {
}
/* 执行一次检测:计时调用 Face++,按结果补全记录(成功/失败都落库)。返回 Face++ 原始返回 */
async function runDetect(rec, front, left, right, persist = true) {
async function runDetect(rec, front, left, right, persist = true, deferWrinkles = false) {
rec.detect_status = '检测中';
rec.updated_at = nowIso();
const t0 = Date.now();
const fpJson = await callFacepp(front, left, right);
if (fpJson.result && !fpJson.error_message) {
if (fpJson.result && !fpJson.error_message && !deferWrinkles) {
fpJson.wrinkle_views = await WrinkleViews.collect(fpJson,{left,right},{
key:CRED.key,secret:CRED.secret,url:FACEPP_URL,
gapMs:process.env.NODE_ENV==='test'?0:4000,
// Keep within the existing 150-second browser timeout, including
// upload/serialization headroom. Missing side maps never fail a report.
// Legacy admin retries may still collect inline; customer reports use
// deferred tasks instead. Missing side maps never fail a report.
deadline:t0+120000
});
}
@@ -452,7 +478,7 @@ async function handleAnalyze(req, res) {
console.log(`[record] 新检测记录 ${recordId}(${rec.shoot_status},照片${Object.keys(photos).length}/3)`);
}
const fpJson = await runDetect(rec, front, left, right, persist);
const fpJson = await runDetect(rec, front, left, right, persist, true);
if (persist) updateRecord(rec);
if (fpJson.error_message) {
@@ -484,6 +510,15 @@ async function handleAnalyze(req, res) {
side: { left: fpJson.left_side_result || nested.left_side_result || null,
right: fpJson.right_side_result || nested.right_side_result || null }
};
// The main report never waits for display-only side wrinkle requests.
if (left || right) {
try {
payload.wrinkle_job=wrinkleJobs.register(owner,recordId,{primary:{left_side_result:payload.side.left,right_side_result:payload.side.right},photos:{left,right}});
payload.wrinkle_views={version:WrinkleViews.VERSION,left:{status:left?'pending':'unavailable'},right:{status:right?'pending':'unavailable'}};
rec.wrinkle_view_status={left:{status:left?'pending':'unavailable',error_code:null},right:{status:right?'pending':'unavailable',error_code:null}};
if(persist)updateRecord(rec);
} catch (_) { /* Optional artwork must never block the main report. */ }
}
let reportSaved = false, presentation = null, snapshotVm = null;
try {
snapshotVm = SkinScore.buildViewModel(result, payload.side, scoreProfile(rec));
@@ -501,6 +536,7 @@ async function handleAnalyze(req, res) {
record_id: recordId, report_saved: reportSaved, presentation, vm: snapshotVm,
result,
wrinkle_views: payload.wrinkle_views,
wrinkle_job: payload.wrinkle_job || null,
face_rectangle: fpJson.face_rectangle || null,
side: {
left: fpJson.left_side_result || nested.left_side_result || null,
@@ -876,6 +912,16 @@ async function handleAdmin(req, res, pathname, url) {
http.createServer(async (req, res) => {
const pathname = req.url.split('?')[0];
if (await accounts.route(req, res, pathname)) return;
if (req.method==='POST' && pathname==='/api/wrinkle-views') {
const owner=accounts.session(req);
if(!owner)return sendJson(res,401,{ok:false,message:'请先登录'});
if(!accounts.sameOrigin(req))return sendJson(res,403,{ok:false});
try {
const body=JSON.parse((await readBody(req,4096)).toString('utf8'));
const views=await wrinkleJobs.run(owner,body.token);
return sendJson(res,views?200:404,views?{ok:true,wrinkle_views:views}:{ok:false,message:'标注任务已失效'});
} catch (_) {return sendJson(res,500,{ok:false,message:'侧脸标注暂不可用'});}
}
if (req.method === 'POST' && pathname === '/api/capture-funnel') {
if (!accounts.sameOrigin(req) || !accounts.session(req)) return sendJson(res, 403, { ok: false });
try {