feat: 完善淘宝小程序拍照、报告与登录流程

接入本地实时识别和自动三连拍,调整报告裁切、商品跳转、分享标题及居中页眉;完善游客会话、后台渠道隔离、部署脚本与回归测试。
This commit is contained in:
xsl
2026-11-04 15:07:51 +08:00
parent 381da60ae4
commit 774376fa16
147 changed files with 17420 additions and 597 deletions
+56
View File
@@ -0,0 +1,56 @@
/* Local-only capture gates; no request, scoring, or report recommendation logic. */
(function(root, factory) {
if (typeof module === 'object' && module.exports) module.exports = factory(require('./head-pose.js'));
else root.CaptureQuality = factory(root.HeadPose);
})(typeof window !== 'undefined' ? window : this, function(HeadPose) {
'use strict';
// Real-phone feedback: ease alignment and normal hand/head movement while
// retaining two checked preview frames, one face and framing. No JPEG recheck.
var config = Object.freeze({ version: '2026-10-16-preview-only-v11', 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 });
function finite(v) { return typeof v === 'number' && isFinite(v); }
function scoreThreshold(target) { return target === 'front' ? config.frontScore : config.score; }
function evaluate(d, options) {
var failed = function(hint, reason) { return { ok: false, hint: hint, reason: reason || 'quality' }; };
if (!d) return failed('未检测到人脸');
if (options.count !== 1) return failed('请保持单人出镜');
var box = d.detection.box, w = options.width, h = options.height;
if (!box || ![box.x, box.y, box.width, box.height, w, h].every(finite) || w <= 0 || h <= 0 || box.width <= 0 || box.height <= 0) return failed('请将脸部完整移入取景框');
var ratio = box.height / h;
if (!finite(d.detection.score) || d.detection.score < scoreThreshold(options.target)) return failed('请保持面部清晰');
if (ratio < config.near) return failed('请靠近一点');
if (ratio > config.far) return failed('请远一点');
if (box.x < w * config.edge || box.y < h * config.edge || box.x + box.width > w * (1 - config.edge) || box.y + box.height > h * (1 - config.edge)) return failed('请将脸部完整移入取景框');
if (!finite(options.offsetX) || Math.abs(options.offsetX) > config.center) return failed('请将脸部移入取景框');
var pose;
try { pose = HeadPose.estimate(d.landmarks.positions, options.mirrored); } catch (_) { return failed('请正对屏幕'); }
if (!pose) return failed('请正对屏幕');
// System-camera files may already be mirrored. Retain the established
// direction-agnostic fallback, but apply the same angle/pitch/roll limits.
var target = options.target;
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');
// 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) {
if (!observation) return { ready: false, state: null };
var reset = !state || now - state.lastAt > config.maxGapMs || now < state.lastAt;
if (!reset) {
var a = state.anchor, b = observation;
reset = Math.abs(a.yaw - b.yaw) > config.yawDrift || Math.abs(a.pitch - b.pitch) > config.pitchDrift ||
Math.abs(a.center_x - b.center_x) > config.centerDrift || Math.abs(a.center_y - b.center_y) > config.centerDrift || Math.abs(a.ratio - b.ratio) > config.sizeDrift;
}
var next = reset ? { since: now, lastAt: now, frames: 1, anchor: observation } :
{ since: state.since, lastAt: now, frames: state.frames + 1, anchor: state.anchor };
return { state: next, ready: now - next.since >= config.holdMs && next.frames >= config.minFrames };
}
return { config: config, scoreThreshold: scoreThreshold, evaluate: evaluate, hold: hold };
});
+362
View File
@@ -0,0 +1,362 @@
/**
* head-pose.js —— 纯姿态估算模块(yaw / pitch / roll)
*
* 输入为 face-api.js 的 68 特征点([{x,y}, ...],图片坐标系、未镜像),
* 输出角度制姿态角。纯数据进、纯数据出:不碰 DOM、不 require face-api,
* 浏览器挂 window.HeadPose,Node 下 module.exports,可直接单测。
*
* ------------------------- 坐标与符号约定 -------------------------
* 68 点索引(dlib/face-api 约定,以「看图视角」描述):
* 36 = 画面左侧眼外角(被摄者右眼外角)
* 45 = 画面右侧眼外角(被摄者左眼外角)
* 30 = 鼻尖,33 = 鼻底,19/24 = 左右眉峰,8 = 下巴尖
* 图片坐标系:x 向右,y 向下。
*
* yaw(左右转头):
* yaw > 0 表示鼻尖偏向画面右侧(45 点一侧),即被摄者向【自己的左侧】转头
* (此时画面中看到被摄者的右脸 → check 的 target 'right');
* yaw < 0 表示被摄者向【自己的右侧】转头(画面中看到左脸 → target 'left')。
* mirrored=true(前置摄像头预览做了水平镜像)时内部翻转 yaw 符号,
* 使输出相对用户所看到的画面方向一致;check() 内部会还原语义方向,
* 因此 check 结果与 mirrored 取值无关。
* ⚠ 方向符号真机需校准:不同机型/镜像策略下若左右相反,
* 只需调整调用方传入的 mirrored 标志或 YAW_COEFF 符号。
*
* pitch(俯仰):
* pitch > 0 表示抬头(鼻底相对眉眼连线中点的纵向下移增大),
* pitch < 0 表示低头。⚠ 经验映射,真机需校准 PITCH_BASELINE / PITCH_COEFF。
*
* roll(平面内歪头):
* 两眼外角连线倾角,roll > 0 表示画面右侧(45 点一侧)偏低。
*
* 所有映射均为基于 68 点几何的轻量经验估算(非 solvePnP 精确解),
* 系数仅保证量级与单调性合理,上线前需用真机样本回归校准。
*/
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory();
} else {
root.HeadPose = factory();
}
}(typeof self !== 'undefined' ? self : this, function () {
'use strict';
// ---- 68 点索引 ----
var IDX = {
EYE_LEFT_OUTER: 36, // 画面左侧眼外角
EYE_RIGHT_OUTER: 45, // 画面右侧眼外角
NOSE_TIP: 30,
NOSE_BASE: 33,
BROW_LEFT: 19,
BROW_RIGHT: 24,
CHIN: 8
};
// ---- 经验系数(2026-09-27 用 C3 三视图(+45°侧脸)模拟摄像头校准) ----
// yaw = atan((dLeft-dRight)/(dLeft+dRight)) × YAW_COEFF。
// 校准:±45° 实拍侧脸读数 ±25°(2.2 偏小)→ 3.8 使读数落在 44° 上下;正脸 ~4°,仍 <12° 阈值
var YAW_COEFF = 3.8;
// pitch:鼻底相对「眉眼连线中点」的纵向偏移 / 脸高(眉 19/24 — 下巴 8),
// 减去中性正脸基线后线性映射为角度。
var PITCH_BASELINE = 0.45; // 校准:0.32 时正脸误读 ~29°(被误判低头),实测基线 0.48,留 0.03 余量
var PITCH_COEFF = 180; // 归一化偏移 → 角度 的增益
// ---- check 阈值(角度制) ----
// Real-phone feedback: relaxed alignment, without accepting frontal photos
// as side views. These empirical angles are not upstream quality scores.
// 2026-10-15: expand accepted angles by 20%; lower side minimum by 20%.
var FRONT_YAW_MAX = 18;
var FRONT_PITCH_MAX = 26;
var SIDE_YAW_MIN = 24;
var SIDE_YAW_MAX = 84;
var SIDE_PITCH_MAX = 29;
var ROLL_MAX = 22;
var HINT_KEEP = '保持不动';
var HINT_TURN_LEFT = '请向左转头'; // 用户向自己的左侧转
var HINT_TURN_RIGHT = '请向右转头'; // 用户向自己的右侧转
var HINT_RAISE = '请略微抬头';
var HINT_LOWER = '请略微低头';
var HINT_FRONT = '请正对屏幕';
var RAD2DEG = 180 / Math.PI;
function dist(a, b) {
var dx = a.x - b.x;
var dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
}
function midpoint(a, b) {
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
}
function clamp(v, lo, hi) {
return v < lo ? lo : (v > hi ? hi : v);
}
function assertPoints(points) {
if (!points || typeof points.length !== 'number' || points.length < 68) {
throw new TypeError('HeadPose.estimate: 需要 68 个特征点数组');
}
var names = Object.keys(IDX);
for (var i = 0; i < names.length; i++) {
var p = points[IDX[names[i]]];
if (!p || typeof p.x !== 'number' || typeof p.y !== 'number' ||
!isFinite(p.x) || !isFinite(p.y)) {
throw new TypeError('HeadPose.estimate: 特征点 ' + IDX[names[i]] + ' 非法');
}
}
}
/**
* estimate(points, mirrored) -> { yaw, pitch, roll }(角度制),非法/退化输入返回 null
* points : face-api 68 特征点 [{x,y}, ...](图片坐标系,未镜像)
* mirrored : 预览画面是否水平镜像(true 时翻转 yaw 符号)
* 返回 null 的情形:特征点重合(dLeft+dRight=0,yaw 退化为 NaN)、
* 脸高过小(pitch 不可信,不再静默置 0)、结果出现 NaN/Infinity。
*/
function estimate(points, mirrored) {
assertPoints(points);
var eyeL = points[IDX.EYE_LEFT_OUTER];
var eyeR = points[IDX.EYE_RIGHT_OUTER];
var noseTip = points[IDX.NOSE_TIP];
var noseBase = points[IDX.NOSE_BASE];
var browMid = midpoint(points[IDX.BROW_LEFT], points[IDX.BROW_RIGHT]);
var chin = points[IDX.CHIN];
// roll:两眼外角连线倾角(y 向下,45 点偏低为正)
var roll = Math.atan2(eyeR.y - eyeL.y, eyeR.x - eyeL.x) * RAD2DEG;
// yaw:鼻尖到左右眼外角距离的不对称度;特征点重合(dLeft+dRight=0)时为 NaN,走非法分支
var dLeft = dist(noseTip, eyeL);
var dRight = dist(noseTip, eyeR);
var dSum = dLeft + dRight;
var yaw = NaN;
if (dSum > 0) yaw = Math.atan((dLeft - dRight) / dSum) * RAD2DEG * YAW_COEFF;
// pitch:鼻底相对眉眼连线中点的纵向偏移,以脸高归一后线性映射;
// 脸高过小时 pitch 不可信,不再静默置 0,走非法分支
var eyeMid = midpoint(eyeL, eyeR);
var browEyeMid = midpoint(browMid, eyeMid);
var faceHeight = chin.y - browMid.y;
var pitch = NaN;
if (faceHeight > 1) {
var ratio = (noseBase.y - browEyeMid.y) / faceHeight;
pitch = (ratio - PITCH_BASELINE) * PITCH_COEFF;
}
if (!isFinite(yaw) || !isFinite(pitch) || !isFinite(roll)) return null; // 退化输入 → 非法三角
yaw = clamp(yaw, -90, 90);
pitch = clamp(pitch, -90, 90);
if (mirrored) { yaw = -yaw; }
return { yaw: yaw, pitch: pitch, roll: roll };
}
/**
* check(points, target, mirrored) -> { ok, hint }
* target: 'front' | 'left' | 'right'
* 'front' : |yaw|<=18 且 |pitch|<=26、|roll|<=22;角度不合格只提示回正,
* 不在正脸拍摄完成前播侧脸转头指令。
* 'left' : 画面中看到左脸 = 被摄者头向右转(语义 yaw ∈ [-84°,-24°]),|pitch|<=29
* 'right' : 画面中看到右脸 = 被摄者头向左转(语义 yaw ∈ [24°,84°]),|pitch|<=29
* ok=true 时 hint='保持不动';输入非法(含 estimate 返回 null 的退化特征点)时 ok=false、hint='请正对屏幕'。
*/
function check(points, target, mirrored) {
var e;
try {
e = estimate(points, mirrored);
} catch (err) {
return { ok: false, hint: HINT_FRONT, yaw: null };
}
if (!e) return { ok: false, hint: HINT_FRONT, yaw: null }; // 退化特征点(重合/脸高过小 → NaN/Infinity)
// 语义 yaw:>0 = 被摄者向自己的左侧转头。estimate(mirrored) 内部已按镜像翻转过符号,
// 这里直接用 e.yaw(此前再翻一次是双重镜像 bug:侧脸引导方向完全相反)
var sYaw = e.yaw;
var pitch = e.pitch;
if (Math.abs(e.roll) > ROLL_MAX) return { ok: false, hint: '请勿歪头', yaw: sYaw };
if (target === 'front') {
if (Math.abs(sYaw) <= FRONT_YAW_MAX && Math.abs(pitch) <= FRONT_PITCH_MAX) {
return { ok: true, hint: HINT_KEEP, yaw: sYaw };
}
return { ok: false, hint: HINT_FRONT, yaw: sYaw };
}
if (target === 'left' || target === 'right') {
// 'left' 需要语义 yaw 为负(头向右转),'right' 需要为正
var want = target === 'left' ? -1 : 1;
var sy = sYaw * want; // 统一折算到正区间判断
if (sy >= SIDE_YAW_MIN && sy <= SIDE_YAW_MAX && Math.abs(pitch) <= SIDE_PITCH_MAX) {
return { ok: true, hint: HINT_KEEP, yaw: sYaw };
}
if (sy < SIDE_YAW_MIN) {
// 转得不够(含转错方向):提示朝目标方向转
return { ok: false, hint: target === 'left' ? HINT_TURN_RIGHT : HINT_TURN_LEFT, yaw: sYaw };
}
if (sy > SIDE_YAW_MAX) {
// 转过了:提示往回
return { ok: false, hint: target === 'left' ? HINT_TURN_LEFT : HINT_TURN_RIGHT, yaw: sYaw };
}
return { ok: false, hint: pitch > 0 ? HINT_LOWER : HINT_RAISE, yaw: sYaw };
}
return { ok: false, hint: HINT_FRONT, yaw: null };
}
return {
estimate: estimate,
check: check,
// 导出阈值与系数便于调用方/真机校准阶段调整
config: {
YAW_COEFF: YAW_COEFF,
PITCH_BASELINE: PITCH_BASELINE,
PITCH_COEFF: PITCH_COEFF,
FRONT_YAW_MAX: FRONT_YAW_MAX,
FRONT_PITCH_MAX: FRONT_PITCH_MAX,
SIDE_YAW_MIN: SIDE_YAW_MIN,
SIDE_YAW_MAX: SIDE_YAW_MAX,
SIDE_PITCH_MAX: SIDE_PITCH_MAX,
ROLL_MAX: ROLL_MAX
}
};
}));
// -------------------------- Node 自测 --------------------------
if (typeof module !== 'undefined' && typeof require !== 'undefined' && require.main === module) {
var HeadPose = module.exports;
var failures = 0;
function ok(cond, msg) {
if (cond) {
console.log('PASS ' + msg);
} else {
failures++;
console.error('FAIL ' + msg);
}
}
// 合成对称正脸:眼距 100,脸高 170,pitch 偏移恰好落在基线上
function makeFrontal() {
var pts = [];
for (var i = 0; i < 68; i++) { pts.push({ x: 200, y: 150 }); }
pts[36] = { x: 150, y: 100 }; // 左眼外角(画面左)
pts[45] = { x: 250, y: 100 }; // 右眼外角(画面右)
pts[19] = { x: 170, y: 70 };
pts[24] = { x: 230, y: 70 };
pts[8] = { x: 200, y: 240 }; // 下巴 → 脸高 240-70=170
pts[30] = { x: 200, y: 140 }; // 鼻尖居中
pts[33] = { x: 200, y: 161.5 }; // (161.5-85)/170 = 0.45 = PITCH_BASELINE
return pts;
}
// 把鼻点横向偏移 17.7px,模拟 ~45° 侧脸(向画面右侧偏 = 被摄者向左转;偏移量按 YAW_COEFF=3.8 数值标定)
function makeSide() {
var pts = makeFrontal();
pts[30] = { x: 217.7, y: 140 };
pts[33] = { x: 217.7, y: 161.5 };
return pts;
}
// 1) 对称正脸 → yaw/pitch/roll ≈ 0
var front = HeadPose.estimate(makeFrontal(), false);
ok(Math.abs(front.yaw) < 0.5, '正脸 yaw≈0 (got ' + front.yaw.toFixed(2) + ')');
ok(Math.abs(front.pitch) < 1, '正脸 pitch≈0 (got ' + front.pitch.toFixed(2) + ')');
ok(Math.abs(front.roll) < 0.5, '正脸 roll≈0 (got ' + front.roll.toFixed(2) + ')');
// 2) 正脸 front check 通过
var fc = HeadPose.check(makeFrontal(), 'front', false);
ok(fc.ok === true && fc.hint === '保持不动', '正脸 front ok,hint=保持不动');
// 3) 45° 侧脸:读数落在合理区间
var side = HeadPose.estimate(makeSide(), false);
ok(side.yaw > 40 && side.yaw < 50, '侧脸 yaw≈45° (got ' + side.yaw.toFixed(2) + ')');
// 4) 方向语义:鼻尖偏画面右 = 被摄者向左转 = target 'right' 通过、'left' 拒绝
var rc = HeadPose.check(makeSide(), 'right', false);
var lc = HeadPose.check(makeSide(), 'left', false);
ok(rc.ok === true, '侧脸(被摄者左转)target=right ok');
ok(lc.ok === false && lc.hint === '请向右转头', '同侧脸 target=left 拒绝并引导向右');
// 5) mirrored=true 翻转 yaw 符号,check 语义随之翻转(同一点集在镜像预览下对应相反的物理转头)
var sideM = HeadPose.estimate(makeSide(), true);
ok(Math.abs(sideM.yaw + side.yaw) < 0.001, 'mirrored=true 时 yaw 符号翻转');
ok(HeadPose.check(makeSide(), 'right', true).ok === false, 'mirrored 语义翻转:check(right) 拒绝');
ok(HeadPose.check(makeSide(), 'left', true).ok === true, 'mirrored 语义翻转:check(left) 通过');
// 6) 反方向侧脸:鼻尖偏画面左 → target 'left' 通过
var back = makeSide();
back[30] = { x: 182.3, y: 140 };
back[33] = { x: 182.3, y: 161.5 };
var bc = HeadPose.check(back, 'left', false);
ok(bc.ok === true, '反向侧脸 target=left ok');
ok(HeadPose.check(back, 'right', false).ok === false, '反向侧脸 target=right 拒绝');
// 7) 正面步骤只提示回正,不能提前引导拍下一张侧脸。
var sc = HeadPose.check(makeSide(), 'front', false);
ok(sc.ok === false && sc.hint === '请正对屏幕', '左转侧脸 front 拒绝,hint=请正对屏幕');
var scb = makeSide();
scb[30] = { x: 182.3, y: 140 };
scb[33] = { x: 182.3, y: 161.5 };
ok(HeadPose.check(scb, 'front', false).hint === '请正对屏幕', '右转侧脸 front 拒绝,hint=请正对屏幕');
// 8) roll:右眼(45 点)下移 10px → roll≈5.7°
var tilted = makeFrontal();
tilted[45] = { x: 250, y: 110 };
ok(Math.abs(HeadPose.estimate(tilted, false).roll - 5.71) < 0.2, 'roll≈5.71°');
// 9) 低头:鼻底上移 → pitch<0,front 拒绝并提示略微抬头
var down = makeFrontal();
down[33] = { x: 200, y: 100 };
var de = HeadPose.estimate(down, false);
var dc = HeadPose.check(down, 'front', false);
ok(de.pitch < -30, '低头 pitch<0 (got ' + de.pitch.toFixed(2) + ')');
ok(dc.ok === false && dc.hint === '请正对屏幕', '低头 front 拒绝,hint=请正对屏幕');
var up = makeFrontal();
up[33] = { x: 200, y: 200 };
ok(HeadPose.check(up, 'front', false).hint === '请正对屏幕', '抬头过多 front 拒绝,hint=请正对屏幕');
// 10) 非法输入:check 不抛异常,返回兜底引导
var bad = HeadPose.check(null, 'front', false);
ok(bad.ok === false && bad.hint === '请正对屏幕', '非法输入兜底 hint=请正对屏幕');
// 11) 退化特征点:全部重合 → dLeft+dRight=0 → yaw=NaN → estimate 返回 null,check 兜底「请正对屏幕」(不再误导「请向左转头」)
var flat = [];
for (var fi = 0; fi < 68; fi++) { flat.push({ x: 100, y: 100 }); }
ok(HeadPose.estimate(flat, false) === null, '特征点重合 → estimate 返回 null');
var fc2 = HeadPose.check(flat, 'front', false);
ok(fc2.ok === false && fc2.hint === '请正对屏幕', '退化输入 check 兜底 hint=请正对屏幕');
// 12) 脸高过小 → pitch 不可信,不再静默置 0,走非法分支
var tiny = makeFrontal();
tiny[8] = { x: 200, y: 70.5 }; // faceHeight = 0.5 ≤ 1
ok(HeadPose.estimate(tiny, false) === null, 'faceHeight 过小 → estimate 返回 null');
ok(HeadPose.check(tiny, 'front', false).hint === '请正对屏幕', 'faceHeight 过小 check 兜底');
// 13) Phone feedback: expand the frontal margin to 18 degrees.
ok(HeadPose.config.FRONT_YAW_MAX === 18, 'config.FRONT_YAW_MAX = 18');
var slight = makeFrontal();
slight[30] = { x: 205.6, y: 140 }; // 偏移 5.6 → yaw≈14.9°(YAW_COEFF=3.8 标定)
slight[33] = { x: 205.6, y: 161.5 };
var se = HeadPose.estimate(slight, false);
ok(se.yaw > 13.5 && se.yaw <= 15, '轻微偏头 yaw∈(13.5,15] (got ' + se.yaw.toFixed(2) + ')');
var sc2 = HeadPose.check(slight, 'front', false);
ok(sc2.ok === true, 'yaw≈14.9° front 通过(真机反馈放宽)');
var slight2 = makeFrontal();
slight2[30] = { x: 206.2, y: 140 }; // 偏移 6.2 → yaw≈16.2°
slight2[33] = { x: 206.2, y: 161.5 };
var se2 = HeadPose.estimate(slight2, false);
ok(se2.yaw > 15 && se2.yaw < 17.5, '偏头 yaw∈(15,17.5) (got ' + se2.yaw.toFixed(2) + ')');
var sc3 = HeadPose.check(slight2, 'front', false);
ok(sc3.ok === true, 'yaw≈16.2° front 通过(放宽20%)');
if (failures > 0) {
console.error('\n' + failures + ' 项自测失败');
process.exit(1);
}
console.log('\n全部自测通过');
}
+3 -1
View File
@@ -1,5 +1,7 @@
{
"skin-score.js": "1f3256fd8d5863de3407c2037df30b6a801ac49858a8a0acab7163f83f642abc",
"report-ui.js": "2c6acaad7cf6e9c826f062f9e79d419470b37da71ab86a5399897d62fd695caf",
"report-copy.js": "402aa543c3437f4e8043e2281b09d7e4d734c0dd2d103636c9f37fe8aa28d6c2"
"report-copy.js": "402aa543c3437f4e8043e2281b09d7e4d734c0dd2d103636c9f37fe8aa28d6c2",
"head-pose.js": "2068b76dca267f7de67fde96d6a257792da079b02b5ca189307ca6953edfc8ae",
"capture-quality.js": "895be1a2a28ae8878f8f24b2e3ad20cfa325b4e73c73b0ed72924124a9c70d23"
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Vincent Mühler
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Portions Copyright (c) Microsoft Corporation.
Portions Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+242
View File
@@ -0,0 +1,242 @@
# Local tensor face engine
Runtime adapter: `taobao/utils/native-face.js`.
The vendor bundle, tiny detector and full 68-point model come from the existing,
unchanged `h5/vendor/faceapi/` files. The added tiny 68-point model is downloaded
only from the same official upstream's `weights/` directory; its manifest,
77224-byte shard and source URLs/hashes are in `reference/native-face-models/`.
Upstream is
https://github.com/justadudewhohacks/face-api.js. MIT and TensorFlow Apache 2.0
licenses are included; original inline Google/Microsoft notices are retained.
Build or verify from the repository root:
```
node taobao/scripts/build-native-face.cjs
node taobao/scripts/build-native-face.cjs --check
node --test taobao/test/native-face.test.cjs
node taobao/scripts/benchmark-native-face.cjs --repeats=2
node taobao/scripts/benchmark-native-face.cjs --guided --input-sizes=256,160,128 --repeats=2
```
`provenance.json` records every source file's SHA-256 and byte length. The
quantized model bytes total 627385 bytes including the optional full model;
the default tiny detector plus tiny landmark weights total 270545 bytes.
Base64 and manifests are packaged in
one CJS module. Runtime decoding uses typed arrays, with no filesystem or Buffer.
The generated face-api copy has a private TFJS global registry and shadows Node
`process`/`Buffer`. Optional Node-only imports (`crypto`, `node-fetch`, `util`,
`fs`) are replaced with functions that explicitly throw if reached. The
tfjs-node help-string reference is also replaced. The build fails if an
unreviewed static `require()` remains, because the IDE resolves unreachable
imports too. No Node polyfill or native AI plugin is bundled.
Create an engine with `create({defaultPixelFormat:'RGBA', maxSide:320,
inputSize:160, landmarkModel:'tiny', scoreThreshold:0.35})`; these are the
defaults. `landmarkModel:'full'` remains available for controlled comparisons.
The runtime default backend remains `cpu` for compatibility. Select WebGL
explicitly with `create({backend:'webgl', platform:my, ...})`; no unsupported
WebGL request silently falls back to the CPU profile that was too slow on phone.
`init()` returns a Promise with backend, inputSize, maxSide, landmarkModel and
cpuOptimization. `detect(frame)` returns a Promise with original width,
height, total faceCount and at most two complete detections. Each detection has
`detection.{box:{x,y,width,height},score}` and `landmarks.positions` containing
68 `{x,y}` points. All coordinates refer to original raw frame pixels; no
mirroring, rotation or preview crop is applied. Two complete detections suffice
for the existing multiple-person rejection gate.
Input must have integer width/height in 1..4096 and tightly packed four-channel
data in an ArrayBuffer or typed-array view. RGBA, BGRA, ARGB, ABGR and their
8888 aliases are supported. An explicit format is authoritative: the configured
default only fills missing metadata. Unknown formats (including NV/YUV), a
stride/bytesPerRow other than width*4, and a nonzero rotation marker are rejected.
`detect()` synchronously snapshots only the downsampled RGB before returning,
so native buffer reuse cannot change a queued inference. All model operations
use the CPU backend. The lower-level tensor crop/landmark path avoids the
high-level `withFaceLandmarks()` canvas dependency. Concurrent inference rejects
with `NATIVE_FACE_BUSY`. `dispose()` is idempotent; pending work rejects with
`NATIVE_FACE_DISPOSED`, and tensors are released after that work settles. The
last engine instance releases the shared model tensors. `stats()` returns only
backend, selected configuration, memory counters and lifecycle booleans.
The CPU `pad-copy` specialization copies contiguous source blocks into the
already zero-filled output tensor. It preserves the original pad values exactly;
unhandled scalar/complex inputs fall back to the bundled implementation. Disable
it with `cpuOptimization:false` (`cpuOptimization:'none'` in returned diagnostics).
It does not change model arithmetic or relax quality/freshness gates. The trial
1x1 convolution specialization showed no stable benefit and was removed.
`detect()` additionally returns inputSize, maxSide, landmarkModel,
cpuOptimization and `timings.{snapshotMs,modelMs,tensorMs,detectorMs,landmarkMs,
totalMs}` in milliseconds. `elapsedMs` remains total elapsed wall time; source
frame age must still be calculated from the camera's frame arrival time.
Tests run the real models without document, Image, canvas, fetch, process,
Buffer or host Node imports. The local face fixture derives from the already
present reference SDK example `C3_180.jpg`; it is never transmitted. Tests check
actual positive face detection and 68 points, all supported byte layouts,
original-coordinate scaling, concurrent/disposed calls, native buffer reuse,
and zero tensors after final disposal. The pad kernel is tested bit-for-bit
against original CPU pad across ranks 1–4, dtypes and padding/constants; the
real full-model detection and all 68 points are identical with it enabled or
disabled. An aspect-preserving two-face composition must reach the actual
native-face-quality single-person rejection gate. Tests print timings/counts,
never the image or point coordinates.
Desktop no-DOM VM stage measurements on the same 540×960 portrait reference
(two samples per profile): old full/256/no specialization took about 934 ms
(detector 730, landmarks 189, snapshot 16). Tiny/160/pad-copy took about 302 ms
(detector 188, landmarks 99, snapshot 15), about 3.1× faster. Default model
tensors occupy 1082180 bytes (47 tensors), versus full's 2200644 bytes (68).
The original small face ratio was 0.208, below the H5 near gate of 0.30. With
an aspect-preserving crop targeting a valid larger face, tiny/128 can detect
one face and 68 points in about 254 ms; however, 128 misses one person in the
two-face regression while 160 detects both. Therefore 160 remains the default.
Reducing maxSide from 320 to 192 saved only about 24 ms on the eligible portrait;
320 is retained for landmark detail. These are desktop CPU observations, not
phone performance guarantees. Benchmark flags `--baseline-cpu`, `--max-side=192`
and `--input-sizes=160,128` support reproducing the separate comparisons. A zero
face result is explicitly flagged by `allSamplesDetected:false`, not a speed win.
## Explicit WebGL candidate
`utils/native-face-webgl.js` obtains an owned logical-layer Canvas using the
official synchronous `my.createOffscreenCanvas(32,32)` and
`getContext('webgl',{enable_flip_y_after_read_pixels:false})`.
The host app must enable its documented Skia capability. The adapter injects the
actual GL1 context into the bundled TFJS backend; it never forges HAS_WEBGL or
float-extension ENV flags. Initialization runs a 16×16 matrix product, a
four-channel convolution, numerical readback checks and observed real shader
draw/readPixels counters. A missing or readonly instrumentable native API fails
explicitly; the adapter does not report GPU success from API names alone.
The current device candidate is `LOCAL-FACE-20261102-07`. The preceding `03`
candidate executed four matrix shader draws on the iPhone, then failed the
strict numerical check with `MATMUL_FAILED`; it did not process camera frames.
Candidate `04` tried `default`, `packed_byte` (disables float downloads),
then `unpacked_byte` (also disables packed shaders). Each attempt runs the same
matrix/convolution/readback checks with the original numerical tolerance. A
failed attempt releases its backend and tracked GL objects before the next
attempt. Only numerical/readback failures permit another profile; missing APIs,
context loss and resource errors stop explicitly. There is no CPU fallback.
All three profiles failed on the device in `04`. The final unpacked byte
attempt observed five draws, one byte read, checksum 0 versus expected 2786,
244 mismatches and maximum absolute error 22. These aggregate values have an
all-zero signature; they do not by themselves prove every output value is zero
or identify the cause. Real camera frames were never processed.
Candidate `05` disables the host's extra readPixels flip using the documented
[Taobao context option](https://developer.alibaba.com/docs/doc.htm?articleId=121761&docType=1&treeId=829).
A canvas-height-based read-coordinate conversion with a smaller framebuffer can
produce zero output in an isolated experiment; that is a testable hypothesis,
not a confirmed description of the device implementation. Before TFJS setup,
the adapter checks a 2×2 RGBA8 framebuffer with clear/readback, an asymmetric
shader pattern, byte-texture sampling and float-texture sampling. Fixed nonzero
synthetic inputs and a prefilled destination sentinel distinguish an unchanged
typed array from incorrect GPU output. Any failed preflight stops initialization
and releases its graphics objects. Passing this preflight does not replace the
original matrix/convolution numerical checks, compatibility selection or model
warmup.
On the device, `05` passed clear/readback but failed the asymmetric shader with
three mismatches: expected checksum 2244, actual 2040. The actual checksum equals
the preceding clear color's checksum. This is consistent with no draw coverage,
but does not uniquely distinguish coverage, coordinates, typed-buffer uploads
or GL state. Real camera inference still did not start.
Candidate `06` explicitly initializes drawing state and clears to black before
each draw. It first checks a constant-color `drawArrays` pass and then an indexed
pass, followed by the original asymmetric shader and byte/float texture tests.
It verifies native strategies in order: `standard`, `buffer_copy` (tightly copy
a view's bytes into an ArrayBuffer for standard bufferData upload),
`framebuffer_y` (compensate viewport/scissor Y only for a bound framebuffer),
then `combined`. These are candidates selected by actual numerical checks, not
assumptions based on phone model. Only `VALUE_MISMATCH` permits another native
strategy; all other failures stop. All four native strategies share one context
and release each probe's objects before the next. The TF profiles still create
at most three contexts. A selected native strategy must pass every native test,
then the unchanged strict matrix/convolution tests and real-model warmup.
The source frame freshness limit remains 1500 ms. Device strategy selection,
compatibility, speed and automatic capture still need phone acceptance.
Candidate `06` failed on the device after its standard constant-array draw
passed: `nativeInitialStage=constant_indexed`. The later coordinate candidate's
black output is not the original failure. Candidate `07` first rebinds the
element buffer immediately before indexed draws. If the indexed result still
fails while the array draw passed, it verifies a four-vertex triangle strip as
an alternative. Strip probes report `constantQuad:passed` and leave
`constantIndexed:not_run`; `drawStrategy:quad_strip` identifies the physical
method without claiming the native indexed API passed.
Only the owned quad with uploaded indices `[0,1,2,2,1,3]` and exact
`TRIANGLES/6/UNSIGNED_SHORT/offset0` arguments can be converted. Upload bytes
are checked within typed-view bounds. Unknown buffers, different signatures,
numeric allocations and buffers modified with bufferSubData are refused;
deletion invalidates the record. This matches the pinned upstream
[quad data](https://raw.githubusercontent.com/tensorflow/tfjs/tfjs-core-v1.7.0/tfjs-core/src/backends/webgl/gpgpu_util.ts)
and retains the original tensor checks and model warmup. Native strategies can
each add a verified strip attempt, up to eight probes on the same context.
This is an experimental device compatibility path, not an assertion that the
Taobao indexed API is generally unsupported. Phone success and speed remain
unverified.
After the numerical checks, initialization loads the selected real models and
warms both the detector on synthetic portrait black pixels and the landmark
network on a synthetic tensor crop. Warmup results are discarded and never
count as user face/pose data. Only after the warmup does initialization return
`backend:'webgl'`, `gpu.selftest:'passed'` and `gpu.stage:'ready'`. The source
frame age and 1500 ms capture gate remain the camera's responsibility.
The returned `gpu` record contains fixed stage/code/profile enums, booleans,
counts and metrics computed only from the fixed synthetic selftest arrays:
requested, selftest, stage, code, contextLost, profile, attemptCount,
matmulDraws, convDraws, pixelReads, floatReads, byteReads, selftestMs and
warmupMs. Each synthetic matrix/convolution record reports actual length,
nonfinite/mismatch counts, maximum absolute error, first expected/actual values
and expected/actual checksums. The camera caches those synthetic metrics,
profile, attempts and read counts only during initialization; later detection
results cannot replace them in copied diagnostics. Errors use
`NATIVE_FACE_WEBGL_*` codes and the same safe record. Native error strings,
renderer strings, image pixels and coordinates do not enter that record. Each
detect checks actual context-loss state before and after model work; a lost
backend must be retired and rebuilt.
The startup record also includes the fixed-input `nativeProbe` stage/result,
sentinel-change and mismatch counts, expected/actual checksums, read return-kind
enum, GL error and framebuffer status. The record also preserves
`nativeInitialStage`/`nativeInitialCode`, selected `nativeStrategy`,
`constantArrays`/`constantIndexed`, optional vertex/index buffer byte counts and
`pixel0` through `pixel3` as big-endian RGBA uint32 values from fixed synthetic
input only. TFJS readback state is collected before
throwing for a numerical mismatch, so a failing value no longer hides a native
GL error. These fields are cached only during initialization and cannot be
replaced by real-camera inference results.
A shared live engine group admits one backend kind at a time. Initialization,
model loading, warmup and inference are queued; decoded weights are not reused
across a CPU/WebGL switch. Disposal waits for pending work, then releases model
tensors, backend programs/textures, the externally supplied GPGPUContext and all
remaining tracked GL objects including TFJS 1.7's vertex buffer/shader leftovers.
Native cleanup exceptions are isolated so they cannot preserve a busy lock or
replace the original inference error.
```
node --test taobao/test/native-face.test.cjs taobao/test/native-face-webgl.test.cjs
```
The WebGL tests run packaged modules with DOM/Node environment names hidden,
explicitly supply real headless WebGL1 and abort all network routes. They cover
numerical shader/readback, corrupted FLOAT readback followed by packed byte
readback, two failed contexts followed by unpacked byte readback, and all three
profiles failing closed. Both compatible paths run the real 68-point model and
are compared with CPU face boxes, confidence and landmark geometry. They also
cover context loss, initialization failure, shared engines, deferred disposal
and rebuilding, with zero final
tensors and tracked GL objects. Desktop SwiftShader proves the computation and
lifecycle paths; it does not establish native Taobao iPhone compatibility or
phone inference speed. Those still require the device's actual diagnostics.
File diff suppressed because one or more lines are too long
+70
View File
@@ -0,0 +1,70 @@
{
"source": "Existing h5/vendor/faceapi (unchanged) plus official upstream tiny 68 model in reference/native-face-models",
"upstream": "https://github.com/justadudewhohacks/face-api.js",
"build": "node taobao/scripts/build-native-face.cjs",
"verify": "node taobao/scripts/build-native-face.cjs --check",
"modifications": [
"Module-local TFJS registry",
"Shadow process and Buffer to keep Node branches inactive",
"Node-only require calls replaced with explicit unsupported stubs"
],
"nodeOnlyStubs": {
"crypto": 1,
"@tensorflow/tfjs-node": 1,
"node-fetch": 1,
"util": 1,
"fs": 1
},
"quantizedBytes": 627385,
"sourceFiles": [
{
"path": "h5/vendor/faceapi/models/tiny_face_detector_model-weights_manifest.json",
"bytes": 2953,
"sha256": "14c60659a31b6b7b1320077171b8f8adcb24ef0e62dde62ce603bcb49a1b49b5"
},
{
"path": "h5/vendor/faceapi/models/tiny_face_detector_model-shard1",
"bytes": 193321,
"sha256": "b7503ce7df31039b1c43316a9b865cab6a70dd748cc602d3fa28b551503c3871"
},
{
"path": "h5/vendor/faceapi/models/face_landmark_68_model-weights_manifest.json",
"bytes": 7889,
"sha256": "d30f6cc341009ea4f8223876959289b96576fc54a2615f92da9741ab9c5f0bbc"
},
{
"path": "h5/vendor/faceapi/models/face_landmark_68_model-shard1",
"bytes": 356840,
"sha256": "4611ef65c87d836d03d684b30eec4d195d8b219fa1dd58fc58945831c6b9299b"
},
{
"path": "reference/native-face-models/face_landmark_68_tiny_model-weights_manifest.json",
"bytes": 4441,
"sha256": "3c63b8984302c187b218d9ef5aa149ed8c2c7fa3fe54db078614692bc48d153c"
},
{
"path": "reference/native-face-models/face_landmark_68_tiny_model-shard1",
"bytes": 77224,
"sha256": "b98e9f2f7da76f8a6dda9741a36ed485b224b889d552de2b2c1bb16217f67bfc"
},
{
"path": "h5/vendor/faceapi/face-api.js",
"bytes": 859250,
"sha256": "24e27b366566d9c871619d4a3dc0187766f054aecd6858fd40a329ec30a57b0b"
}
],
"downloadedSources": [
{
"file": "face_landmark_68_tiny_model-weights_manifest.json",
"source": "https://raw.githubusercontent.com/justadudewhohacks/face-api.js/master/weights/face_landmark_68_tiny_model-weights_manifest.json",
"bytes": 4441,
"sha256": "3c63b8984302c187b218d9ef5aa149ed8c2c7fa3fe54db078614692bc48d153c"
},
{
"file": "face_landmark_68_tiny_model-shard1",
"source": "https://raw.githubusercontent.com/justadudewhohacks/face-api.js/master/weights/face_landmark_68_tiny_model-shard1",
"bytes": 77224,
"sha256": "b98e9f2f7da76f8a6dda9741a36ed485b224b889d552de2b2c1bb16217f67bfc"
}
]
}
File diff suppressed because one or more lines are too long