'use strict'; // Display-only side analyses. Never merge these scores into the primary // three-view result or re-run detection when reading a historical report. const VERSION = 'independent-side-wrinkles-v1'; function hasAnnotation(result) { if (typeof result?.face_maps?.texture_enhanced_lines === 'string' && result.face_maps.texture_enhanced_lines.trim()) return true; return Object.values(result?.wrinkle_mark || {}).some(points => Array.isArray(points) && points.filter(p => Number.isFinite(p?.x) && Number.isFinite(p?.y)).length >= 3); } async function collect(primary, photos, options) { const {key, secret, url} = options; const fetchImpl = options.fetch || fetch; const sleep = options.sleep || (ms => new Promise(resolve => setTimeout(resolve, ms))); const now = options.now || Date.now; const deadline = options.deadline || now() + 90000; const gap = options.gapMs ?? 4000; const views = {version: VERSION}; for (const view of ['left','right']) { const existing = primary[view + '_side_result'] || primary.side_result?.[view + '_side_result']; const existingResult = existing?.result || existing; if (hasAnnotation(existingResult)) { views[view] = {status:'ready',source:'primary-side-result',result:existingResult}; continue; } const photo = photos[view]; if (!photo) { views[view] = {status:'unavailable',error_code:'MISSING_SIDE_PHOTO'}; continue; } if (deadline - now() <= gap + 1000) { views[view] = {status:'unavailable',error_code:'WRINKLE_TIME_BUDGET'}; continue; } await sleep(gap); const timeout = Math.min(options.timeoutMs || 20000, deadline - now()); if (timeout <= 0) { views[view] = {status:'unavailable',error_code:'WRINKLE_TIME_BUDGET'}; continue; } try { const form = new FormData(); form.append('api_key',key); form.append('api_secret',secret); // A side photo must be the main image of ITS OWN request to obtain // wrinkle maps in the corresponding coordinate space. form.append('image_file',new Blob([Buffer.from(photo,'base64')]),view+'.jpg'); form.append('return_maps','texture_enhanced_lines'); form.append('return_marks','wrinkle_mark'); const response = await fetchImpl(url,{method:'POST',body:form,signal:AbortSignal.timeout(timeout)}); const raw = await response.json(); const code = raw.error_message ? String(raw.error_message).split(':')[0] : !response.ok ? 'WRINKLE_HTTP_'+response.status : !hasAnnotation(raw.result) ? 'WRINKLE_ANNOTATION_NOT_RETURNED' : null; views[view] = Object.assign({},raw,{status:code?'unavailable':'ready',source:'independent-side-request',error_code:code}); } catch (e) { views[view] = {status:'unavailable',source:'independent-side-request',error_code:e.name==='TimeoutError'||e.name==='AbortError'?'WRINKLE_TIMEOUT':'WRINKLE_REQUEST_FAILED'}; } } return views; } function forReport(views) { if (!views) return null; const out = {version:views.version}; for (const view of ['left','right']) { const item=views[view]; if (!item) continue; out[view]={status:item.status,error_code:item.error_code||null,face_rectangle:item.face_rectangle||null}; if (item.status==='ready'&&item.result) out[view].result={ face_maps:{texture_enhanced_lines:item.result.face_maps?.texture_enhanced_lines||null}, wrinkle_mark:item.result.wrinkle_mark||null }; } return out; } module.exports={VERSION,hasAnnotation,collect,forReport};