fix(h5): optimize camera flipping and angle haptics, refresh splash branding
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# 拍摄角度不合格震动提醒
|
||||
|
||||
按用户确认的规则实现,纳入 2026-10-13 相机与首页更新发版。
|
||||
|
||||
- 实时检测确认单人、清晰度、距离与居中等前置条件通过,但角度不合格连续约 1 秒后,调用一次 30ms 轻震。
|
||||
- 持续角度不合格时,相邻提醒至少间隔 4 秒;不使用独立定时器,只有收到新检测结果才可能提醒。
|
||||
- 合格、无人脸、其他质量问题或检测失败立即取消震动并重置连续计时;漏帧也中断计时。检测结果间隔超过 2 秒会重新累计,避免推理卡顿被误认为持续不合格。
|
||||
- 自动拍摄、手动快门和拍摄完成均不再震动。
|
||||
- 拍摄锁、摄像头切换/未就绪、离开拍照页及切后台禁止提醒。停止检测、步骤重置或开始拍摄时取消尚未结束的震动。
|
||||
- 前后提示保持 4 秒限频,不因角度短暂合格、镜头切换或步骤重置绕过冷却。
|
||||
- 不支持接口、接口拒绝或抛出错误时静默忽略,不影响原有文字、语音和自动拍摄。
|
||||
|
||||
没有改变拍摄角度、距离、亮度、稳定时间或评分推荐算法。不是所有手机都能产生物理震动,微信/鸿蒙需真机复验,iOS 当前按无震动降级。
|
||||
|
||||
验证命令:
|
||||
|
||||
```sh
|
||||
node h5/test/customer-feedback-second.js
|
||||
node h5/test/customer-feedback-fourth.js
|
||||
node h5/test/camera-flip-browser.mjs
|
||||
node h5/test/capture-quality-browser.mjs
|
||||
```
|
||||
|
||||
测试涵盖定时边界、持续限频、合格取消、无人脸、非角度问题、拍摄/切换/后台隔离、不支持/拒绝安全降级,以及相机翻转和真实本地模型三连拍回归。浏览器测试使用模拟相机和拦截的身份/报告接口,不调用付费检测、不写真实账号或报告。
|
||||
@@ -0,0 +1,29 @@
|
||||
# 相机翻转性能优化
|
||||
|
||||
本次优化纳入 2026-10-13 相机与首页更新发版。
|
||||
|
||||
## 修改范围
|
||||
|
||||
- 相机首帧 readyState ≥ 2 且宽高有效后即可显示,检查周期 50ms;去掉原有三轮 300ms 分辨率预热等待。
|
||||
- 翻转过程中保留上一帧及原来的镜像方向,显示“正在切换摄像头…”。静帧仅保存在内存 Canvas,恢复预览或离开页面后清空,不上传、不持久化。
|
||||
- 启动/翻转时禁用翻转按钮,拍摄和步骤推进期间拒绝翻转。
|
||||
- 新镜头失败、首帧等待超时或浏览器明确返回错误的 facingMode 时,尝试恢复原镜头一次;恢复也失败才降级到系统相机。
|
||||
- 翻转及恢复请求设置 12 秒超时,初次权限询问不设置该超时。首帧单独设置 12 秒超时,不再超时强行认为成功。迟到的流及时停止。
|
||||
- 检测循环增加代次令牌,旧镜头推理结果不可更新新镜头状态或启动第二条循环;旧推理未结束时不叠加新推理。
|
||||
- 模型加载回调也校验镜头令牌,避免旧回调覆盖切换状态。
|
||||
|
||||
拍摄角度、亮度、尺寸、清晰度、稳住时间、照片编码、评分和推荐规则均未修改。
|
||||
|
||||
## 验证
|
||||
|
||||
- `node h5/test/camera-flip-browser.mjs`:模拟相机验证首帧显示时延、静帧、重复点击、拍摄锁、失败/忽略 facingMode/超时恢复、旧推理隔离、离开后迟到流释放。
|
||||
- `node h5/test/capture-quality-browser.mjs`:真实本地模型、三角度自动拍摄、最终 JPEG 复验、语音顺序、摄像头释放通过。
|
||||
- `node h5/test/login-performance.mjs`:登录轻量加载、相机模型按需加载/失败重试等通过。
|
||||
- 无付费检测调用、真实账号和报告写入。
|
||||
|
||||
模拟相机对比:原实现首帧后额外等待约 900ms,两轮优化后约 57~91ms(受测试机器调度影响)。不是手机真机性能结论。
|
||||
|
||||
## 仍需真机验证
|
||||
|
||||
微信 Android、iOS、鸿蒙:连续翻转、后置转前置、翻转时返回/切后台、拒绝相机权限、拍摄中点击翻转。
|
||||
摄像头硬件/微信浏览器重新启动本身的耗时无法消除;不提供 getSettings().facingMode 的浏览器只能依赖 facingMode ideal 选择,无法保证识别其是否忽略请求。
|
||||
@@ -5,18 +5,18 @@
|
||||
"3": "skin-score.js",
|
||||
"4": "customer-feedback-second.js",
|
||||
"5": "disposeUnusedWeightTensors",
|
||||
"6": "f",
|
||||
"6": "n",
|
||||
"7": "test/profile-store.js",
|
||||
"8": "admin.js",
|
||||
"9": "__awaiter",
|
||||
"10": "customer-feedback-third.mjs",
|
||||
"10": "k",
|
||||
"11": "lib/accounts.js",
|
||||
"12": "server.js",
|
||||
"13": "files",
|
||||
"14": "showScreen",
|
||||
"15": "customer-feedback-fourth.mjs",
|
||||
"15": "test/accounts.js",
|
||||
"16": "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范",
|
||||
"17": "report-persistence.mjs",
|
||||
"17": "vizCompose",
|
||||
"18": "1'me肌肤状态检测 H5",
|
||||
"19": "customer-feedback-fourth.js",
|
||||
"20": "build_fixtures.py",
|
||||
@@ -25,7 +25,7 @@
|
||||
"23": "facepp-skinanalyze(旷视皮肤分析专业版调用技能)",
|
||||
"24": "巨子测肤 · 1'me 语音播报内容设计文档 V1.0",
|
||||
"25": "mobile-select.iife.min.js",
|
||||
"26": "NeuralNetwork",
|
||||
"26": ".extractWeights",
|
||||
"27": "gen_voice.py",
|
||||
"28": "C",
|
||||
"29": "face-api.js 本地化依赖(h5/vendor/faceapi/)",
|
||||
@@ -40,11 +40,11 @@
|
||||
"38": "FaceLandmark68Net",
|
||||
"39": "UI交互界面调整",
|
||||
"40": "第四轮反馈修改与验收",
|
||||
"41": "FaceLandmarks68",
|
||||
"41": ".loadFromDisk",
|
||||
"42": "layer_at",
|
||||
"43": "extractAllFacesAndComputeResults",
|
||||
"43": "ComputeAllFaceDescriptorsTask",
|
||||
"44": "start-local.sh",
|
||||
"45": "PredictAllAgeAndGenderWithFaceAlignmentTask",
|
||||
"45": "report-persistence.mjs",
|
||||
"46": "PredictAllFaceExpressionsWithFaceAlignmentTask",
|
||||
"47": "extractFaceTensors",
|
||||
"48": "已修改",
|
||||
@@ -63,7 +63,7 @@
|
||||
"61": "报告布局与跟手交互调整",
|
||||
"62": "提交前 Review(2026-10-11)",
|
||||
"63": "e2e-browser.mjs",
|
||||
"64": "recommendation.js",
|
||||
"64": "isValidNumber",
|
||||
"65": "微信登录接入与公网部署说明",
|
||||
"66": "h5/README.md",
|
||||
"67": "ui-20261009.mjs",
|
||||
@@ -76,15 +76,15 @@
|
||||
"74": "report-copy.js",
|
||||
"75": "prepare_h5_assets.py",
|
||||
"76": "报告页滑动防冲突-2026-10-13.md",
|
||||
"77": "FaceLandmarks",
|
||||
"78": ".toString",
|
||||
"77": "drawFaceExpressions",
|
||||
"78": "FaceLandmarks68",
|
||||
"79": "登录反馈与加载优化(2026-10-12)",
|
||||
"80": "线上微信登录部署记录",
|
||||
"81": "wechat-production.mjs",
|
||||
"82": "拍摄约束放宽与标题更新",
|
||||
"83": "10.09 新版 H5 实施与验收",
|
||||
"84": "微信登录联调:2026-10-12",
|
||||
"85": "head-pose.js",
|
||||
"85": "相机翻转性能优化",
|
||||
"86": "report-touch-layout.mjs",
|
||||
"87": "产品推荐规则:肤龄基准与边界调整",
|
||||
"88": "ui-20261012/manifest.json",
|
||||
@@ -92,13 +92,13 @@
|
||||
"90": "customer-feedback.mjs",
|
||||
"91": "wi",
|
||||
"92": "客户反馈修改(2026-10-12)",
|
||||
"93": ".runAndExtendWithFaceDetections",
|
||||
"93": ".runAndExtendWithFaceDetection",
|
||||
"94": "第三轮反馈:PSD 对齐、验收及上线记录",
|
||||
"95": "pn",
|
||||
"96": "onDetect",
|
||||
"95": "login-performance.mjs",
|
||||
"96": "production-loop.mjs",
|
||||
"97": "account-client.js",
|
||||
"98": "js/profile-store.js",
|
||||
"99": "capture-diagnostics.js",
|
||||
"98": ".mul",
|
||||
"99": "ComputeSingleFaceDescriptorTask",
|
||||
"100": ".draw",
|
||||
"101": "wechat-fixture.cjs",
|
||||
"102": "DetectSingleFaceLandmarksTask",
|
||||
@@ -109,23 +109,42 @@
|
||||
"107": "基础信息记忆与肤质拖动",
|
||||
"108": "profile-interaction.mjs",
|
||||
"109": "submitPhotos",
|
||||
"110": "doCapture",
|
||||
"111": "n",
|
||||
"110": "startCamera",
|
||||
"111": "extractWeightsFactory",
|
||||
"112": "微信退出登录-2026-10-13.md",
|
||||
"113": "swipe-track.js",
|
||||
"114": "optimize_delivery_images.py",
|
||||
"115": "normalize",
|
||||
"117": "startCamera",
|
||||
"118": "PredictSingleAgeAndGenderWithFaceAlignmentTask",
|
||||
"116": ".run",
|
||||
"117": "play",
|
||||
"118": "f",
|
||||
"119": "PredictSingleFaceExpressionsWithFaceAlignmentTask",
|
||||
"122": "onPhotoCaptured",
|
||||
"120": "PredictAllFaceExpressionsTask",
|
||||
"121": "wechat-browser.mjs",
|
||||
"122": "doCapture",
|
||||
"123": "评分圈内居中、基础信息页固定与正脸语音修复",
|
||||
"124": "TinyYolov2",
|
||||
"124": "FaceProcessor",
|
||||
"125": "报告等待页实拍背景-2026-10-12.md",
|
||||
"126": ".forwardInput",
|
||||
"128": "AgeGenderNet",
|
||||
"130": ".predictExpressions",
|
||||
"126": "PredictSingleFaceExpressionsTask",
|
||||
"127": "splash-brand-browser.mjs",
|
||||
"128": "NeuralNetwork",
|
||||
"129": "TinyYolov2Base",
|
||||
"130": "FaceFeatureExtractor",
|
||||
"131": "原始肌龄显示与鸿蒙滚轮兼容",
|
||||
"132": "harmony-picker.mjs",
|
||||
"134": "height"
|
||||
"133": ".toString",
|
||||
"134": "camera-flip-browser.mjs",
|
||||
"135": "AgeGenderNet",
|
||||
"136": "convLayer",
|
||||
"137": "height",
|
||||
"138": "age",
|
||||
"139": "recommendation.js",
|
||||
"140": ".forwardInput",
|
||||
"141": "FaceRecognitionNet",
|
||||
"142": ".runAndExtendWithFaceDetections",
|
||||
"143": "TinyXception",
|
||||
"144": "FaceExpressionNet",
|
||||
"145": "PredictAllAgeAndGenderWithFaceAlignmentTask",
|
||||
"146": "PredictSingleAgeAndGenderWithFaceAlignmentTask",
|
||||
"147": "拍摄角度不合格震动提醒-2026-10-13.md"
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"0": "b513c3d9e2ce0ee9", "1": "cb6ea50bd4e4a5ca", "2": "8bae62aa27da5427", "3": "125a85b2d8e78e52", "4": "56bf12952b0de42b", "5": "3fef2b47fc7c0327", "6": "f5fa3e3d7d829c42", "7": "03b56b13a08b5f83", "8": "a689fcb62e7754af", "9": "58f4243880289b8c", "10": "325690c89d34f602", "11": "8fe2f9d63516be5c", "12": "aeca274694e158d2", "13": "7e3b558a587a4ebe", "14": "767b88826fe02948", "15": "fbc8a72e6fda6359", "16": "95add6a975c0cb72", "17": "e042d7fe7351bcb9", "18": "f93d5d85e671fcee", "19": "2becf835f050dc29", "20": "aecf2fce44ba49c5", "21": "11ae63a0b8df3c18", "22": "57d1520d2b3138d5", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "d2f8327bf38ee13c", "27": "eab427ce9a5219e9", "28": "d5bc4d13da321cc8", "29": "c608f1c6492e1cb1", "30": "73c13793fca09f56", "31": "fbbcfc059751cc6a", "32": "f6bcf56c4589316b", "33": "75f750c5b7d9f63d", "34": "f024ff7942935690", "35": "0a737a97b7cae9fc", "36": "a7d375041779bad4", "37": "cb8f26249485324a", "38": "9a25b1230b5fe649", "39": "fdd95375b4a5ddb3", "40": "5e546676ab96de03", "41": "68c52c80f38f8b2c", "42": "916170b9f5d7346b", "43": "0f4ade1799be2fc4", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "24c736a2281aad6e", "48": "cf2f2424bc00436f", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "812950f51be20e40", "56": "9ee2ad45dc27851d", "57": "f2a40b5f1b29f669", "58": "1b6525c6404105b9", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "5209ce2a267bf33b", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "e594ecaa8e7d1538", "65": "bdee3c4a37df52f8", "66": "9727b30d720298e3", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "be0c8310abda2467", "71": "589a2d5a76f3880a", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "3c4c278d6026bc3a", "77": "f5b5a4d7c34325bf", "78": "c3f9847676ae2caf", "79": "c6cfdfe181232ecc", "80": "315325cde1352e98", "81": "37c559741825be74", "82": "200f25fbad7af8fb", "83": "1284897282eb47a3", "84": "d8bb296741d3d05c", "85": "0a373ad2197fd1a6", "86": "386c6fcc7be918e0", "87": "e5fa7717989eda80", "88": "8b5ab5232cef3457", "89": "9e3363b80eeceeea", "90": "dd1c246e19a2d032", "91": "83b0fd3b14363e4d", "92": "fced244d93677d90", "93": "a38797466ce43978", "94": "713d04996c4627df", "95": "96eb6de87b194490", "96": "69eb267b9ec22897", "97": "505ea719b994ac5e", "98": "9b1cf1985223f90a", "99": "9380a89a868c76d2", "100": "bf75bfc99d4f9e34", "101": "beb276de6301634c", "102": "23f1d406f6c779ae", "103": "6ffbe07b521a061b", "104": "e114a7aebaaca2f1", "105": "af50c66d16c81bba", "106": "8bc72c179793e1c2", "107": "3877490226a78145", "108": "e0fc0a42e33bda91", "109": "4afb1715f62865b1", "110": "47af540ec3714a5e", "111": "16df50aadff513fb", "112": "af995dd35403b4dd", "113": "0d99f86b9e41c4bb", "114": "b9d7c266afa84437", "115": "5a279d83dc286aa4", "117": "4cd7191f102eea22", "118": "01f68dfc34a29d32", "119": "8c471c7baeaaca54", "122": "f4bc824c2d55ef51", "123": "b091e3eb82b456eb", "124": "c96276581a8c98d4", "125": "e4f29216745ec758", "126": "880a74c6d255e5cc", "128": "4b44a21bafc4714c", "130": "3a86f59f8a877384", "131": "482dbb7ec33d8c23", "132": "3daacabc5062b554", "134": "aef6e0c179bd709d"}
|
||||
{"0": "017a6130270ab9f2", "1": "cb6ea50bd4e4a5ca", "2": "4d1bb0fdf94e716d", "3": "125a85b2d8e78e52", "4": "4b1835ae3a82cf12", "5": "b5b4a03db0f5697e", "6": "16df50aadff513fb", "7": "03b56b13a08b5f83", "8": "a689fcb62e7754af", "9": "a579e3315ea1fb63", "10": "188dd5e33354e35d", "11": "4a490abd5e503318", "12": "aeca274694e158d2", "13": "7e3b558a587a4ebe", "14": "58417b287cc79e90", "15": "4bc370be8afd7db1", "16": "95add6a975c0cb72", "17": "288c2a98831b0021", "18": "f93d5d85e671fcee", "19": "5272556cf61d08b3", "20": "aecf2fce44ba49c5", "21": "2405e1edb003a83e", "22": "54d7329fed675057", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "16bdd538850fbb24", "27": "eab427ce9a5219e9", "28": "f2d553fdb7f3d983", "29": "c608f1c6492e1cb1", "30": "73c13793fca09f56", "31": "fbbcfc059751cc6a", "32": "f6bcf56c4589316b", "33": "75f750c5b7d9f63d", "34": "f024ff7942935690", "35": "241be5abeab1da6f", "36": "a7d375041779bad4", "37": "cb8f26249485324a", "38": "9a25b1230b5fe649", "39": "fdd95375b4a5ddb3", "40": "5e546676ab96de03", "41": "00dab8704da757dc", "42": "916170b9f5d7346b", "43": "3dd68cce72af5314", "44": "852c1bb2446edd9c", "45": "e042d7fe7351bcb9", "46": "65718282b3c32109", "47": "e1aa21690d4f0dae", "48": "cf2f2424bc00436f", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "812950f51be20e40", "56": "9ee2ad45dc27851d", "57": "f2a40b5f1b29f669", "58": "80b3d789d41454ea", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "5209ce2a267bf33b", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "1c27e5f448dc4f9c", "65": "bdee3c4a37df52f8", "66": "9727b30d720298e3", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "be0c8310abda2467", "71": "589a2d5a76f3880a", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "3c4c278d6026bc3a", "77": "979dcc63399620ad", "78": "ac00532d5d77886f", "79": "c6cfdfe181232ecc", "80": "315325cde1352e98", "81": "37c559741825be74", "82": "200f25fbad7af8fb", "83": "1284897282eb47a3", "84": "d8bb296741d3d05c", "85": "7067e88534a91ab4", "86": "386c6fcc7be918e0", "87": "e5fa7717989eda80", "88": "8b5ab5232cef3457", "89": "9e3363b80eeceeea", "90": "dd1c246e19a2d032", "91": "83b0fd3b14363e4d", "92": "fced244d93677d90", "93": "5a9014c4a7f87699", "94": "713d04996c4627df", "95": "59c31f858c7b6970", "96": "4195478d5c528371", "97": "505ea719b994ac5e", "98": "e6ef8c2f1b997e99", "99": "7ac475ea9bc280f8", "100": "bf75bfc99d4f9e34", "101": "beb276de6301634c", "102": "23f1d406f6c779ae", "103": "6ffbe07b521a061b", "104": "e114a7aebaaca2f1", "105": "af50c66d16c81bba", "106": "8bc72c179793e1c2", "107": "3877490226a78145", "108": "e0fc0a42e33bda91", "109": "4afb1715f62865b1", "110": "f1660fde8e75d368", "111": "72618e564850b9ce", "112": "af995dd35403b4dd", "113": "0d99f86b9e41c4bb", "114": "b9d7c266afa84437", "115": "7a0da13554c97235", "116": "8d6f4b5e4f67bcb7", "117": "679b8985ab775959", "118": "f5fa3e3d7d829c42", "119": "8c471c7baeaaca54", "120": "e800d832c255899a", "121": "2b037735e1f5b0f6", "122": "61b6fafa306cb441", "123": "b091e3eb82b456eb", "124": "94ead9feda213ac6", "125": "e4f29216745ec758", "126": "cb5ac3401283c378", "127": "006218e34885dbe0", "128": "d96493a5f110bd4c", "129": "327fcc91276306b0", "130": "9209646b8de02314", "131": "482dbb7ec33d8c23", "132": "3daacabc5062b554", "133": "89e609b2678905d3", "134": "e7c90d6ef8276a1e", "135": "14fdf4e4c496519f", "136": "8616cd6d33fcb6cb", "137": "746b316833e9c203", "138": "b234c5e4ac7f632d", "139": "e594ecaa8e7d1538", "140": "5fec2bbca3634aec", "141": "b186b90877868930", "142": "46204fb2e4d50c53", "143": "9beb9113cf6685a2", "144": "e858d75e995c92e5", "145": "92ab3e5134ed8685", "146": "01f68dfc34a29d32", "147": "0e6cad06ebfcfb29"}
|
||||
+183
-134
@@ -1,16 +1,16 @@
|
||||
# Graph Report - juzi (2026-10-13)
|
||||
|
||||
## Corpus Check
|
||||
- 114 files · ~4,908,611 words
|
||||
- 118 files · ~4,910,388 words
|
||||
- Verdict: corpus is large enough that graph structure adds value.
|
||||
|
||||
## Summary
|
||||
- 1491 nodes · 2938 edges · 129 communities (102 shown, 27 thin omitted)
|
||||
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 209 edges (avg confidence: 0.57)
|
||||
- 1507 nodes · 2958 edges · 148 communities (112 shown, 36 thin omitted)
|
||||
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 210 edges (avg confidence: 0.57)
|
||||
- Token cost: 0 input · 0 output
|
||||
|
||||
## Graph Freshness
|
||||
- Built from commit: `4e42e339`
|
||||
- Built from commit: `e71368db`
|
||||
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
||||
- Run `graphify update .` after code changes (no API cost).
|
||||
|
||||
@@ -21,18 +21,18 @@
|
||||
- skin-score.js
|
||||
- customer-feedback-second.js
|
||||
- disposeUnusedWeightTensors
|
||||
- f
|
||||
- n
|
||||
- test/profile-store.js
|
||||
- admin.js
|
||||
- __awaiter
|
||||
- customer-feedback-third.mjs
|
||||
- k
|
||||
- lib/accounts.js
|
||||
- server.js
|
||||
- files
|
||||
- showScreen
|
||||
- customer-feedback-fourth.mjs
|
||||
- test/accounts.js
|
||||
- 巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范
|
||||
- report-persistence.mjs
|
||||
- vizCompose
|
||||
- 1'me肌肤状态检测 H5
|
||||
- customer-feedback-fourth.js
|
||||
- build_fixtures.py
|
||||
@@ -41,7 +41,7 @@
|
||||
- facepp-skinanalyze(旷视皮肤分析专业版调用技能)
|
||||
- 巨子测肤 · 1'me 语音播报内容设计文档 V1.0
|
||||
- mobile-select.iife.min.js
|
||||
- NeuralNetwork
|
||||
- .extractWeights
|
||||
- gen_voice.py
|
||||
- C
|
||||
- face-api.js 本地化依赖(h5/vendor/faceapi/)
|
||||
@@ -54,11 +54,11 @@
|
||||
- FaceLandmark68Net
|
||||
- UI交互界面调整
|
||||
- 第四轮反馈修改与验收
|
||||
- FaceLandmarks68
|
||||
- .loadFromDisk
|
||||
- layer_at
|
||||
- extractAllFacesAndComputeResults
|
||||
- ComputeAllFaceDescriptorsTask
|
||||
- start-local.sh
|
||||
- PredictAllAgeAndGenderWithFaceAlignmentTask
|
||||
- report-persistence.mjs
|
||||
- PredictAllFaceExpressionsWithFaceAlignmentTask
|
||||
- extractFaceTensors
|
||||
- 已修改
|
||||
@@ -74,7 +74,7 @@
|
||||
- 报告布局与跟手交互调整
|
||||
- 提交前 Review(2026-10-11)
|
||||
- e2e-browser.mjs
|
||||
- recommendation.js
|
||||
- isValidNumber
|
||||
- 微信登录接入与公网部署说明
|
||||
- h5/README.md
|
||||
- ui-20261009.mjs
|
||||
@@ -84,28 +84,28 @@
|
||||
- 报告顶部评分与年龄排版
|
||||
- build_report_copy.py
|
||||
- 报告页滑动防冲突-2026-10-13.md
|
||||
- FaceLandmarks
|
||||
- .toString
|
||||
- drawFaceExpressions
|
||||
- FaceLandmarks68
|
||||
- 登录反馈与加载优化(2026-10-12)
|
||||
- 线上微信登录部署记录
|
||||
- wechat-production.mjs
|
||||
- 拍摄约束放宽与标题更新
|
||||
- 10.09 新版 H5 实施与验收
|
||||
- 微信登录联调:2026-10-12
|
||||
- head-pose.js
|
||||
- 相机翻转性能优化
|
||||
- report-touch-layout.mjs
|
||||
- 产品推荐规则:肤龄基准与边界调整
|
||||
- ui-20261012/manifest.json
|
||||
- 傅艳NEW与项思炼的会话 2026年10月12日.md
|
||||
- wi
|
||||
- 客户反馈修改(2026-10-12)
|
||||
- .runAndExtendWithFaceDetections
|
||||
- .runAndExtendWithFaceDetection
|
||||
- 第三轮反馈:PSD 对齐、验收及上线记录
|
||||
- pn
|
||||
- onDetect
|
||||
- login-performance.mjs
|
||||
- production-loop.mjs
|
||||
- account-client.js
|
||||
- js/profile-store.js
|
||||
- capture-diagnostics.js
|
||||
- .mul
|
||||
- ComputeSingleFaceDescriptorTask
|
||||
- .draw
|
||||
- wechat-fixture.cjs
|
||||
- DetectSingleFaceLandmarksTask
|
||||
@@ -114,23 +114,40 @@
|
||||
- 基础信息记忆与肤质拖动
|
||||
- profile-interaction.mjs
|
||||
- submitPhotos
|
||||
- doCapture
|
||||
- n
|
||||
- startCamera
|
||||
- extractWeightsFactory
|
||||
- 微信退出登录-2026-10-13.md
|
||||
- normalize
|
||||
- startCamera
|
||||
- PredictSingleAgeAndGenderWithFaceAlignmentTask
|
||||
- .run
|
||||
- play
|
||||
- f
|
||||
- PredictSingleFaceExpressionsWithFaceAlignmentTask
|
||||
- onPhotoCaptured
|
||||
- PredictAllFaceExpressionsTask
|
||||
- wechat-browser.mjs
|
||||
- doCapture
|
||||
- 评分圈内居中、基础信息页固定与正脸语音修复
|
||||
- TinyYolov2
|
||||
- FaceProcessor
|
||||
- 报告等待页实拍背景-2026-10-12.md
|
||||
- .forwardInput
|
||||
- AgeGenderNet
|
||||
- .predictExpressions
|
||||
- PredictSingleFaceExpressionsTask
|
||||
- NeuralNetwork
|
||||
- TinyYolov2Base
|
||||
- FaceFeatureExtractor
|
||||
- 原始肌龄显示与鸿蒙滚轮兼容
|
||||
- harmony-picker.mjs
|
||||
- .toString
|
||||
- AgeGenderNet
|
||||
- convLayer
|
||||
- height
|
||||
- age
|
||||
- recommendation.js
|
||||
- .forwardInput
|
||||
- FaceRecognitionNet
|
||||
- .runAndExtendWithFaceDetections
|
||||
- TinyXception
|
||||
- FaceExpressionNet
|
||||
- PredictAllAgeAndGenderWithFaceAlignmentTask
|
||||
- PredictSingleAgeAndGenderWithFaceAlignmentTask
|
||||
- 拍摄角度不合格震动提醒-2026-10-13.md
|
||||
|
||||
## God Nodes (most connected - your core abstractions)
|
||||
1. `__awaiter()` - 59 edges
|
||||
@@ -159,35 +176,35 @@
|
||||
## Import Cycles
|
||||
- None detected.
|
||||
|
||||
## Communities (129 total, 27 thin omitted)
|
||||
## Communities (148 total, 36 thin omitted)
|
||||
|
||||
### Community 0 - "face-api.js"
|
||||
Cohesion: 0.02
|
||||
Nodes (41): bgrToRgbTensor(), cn(), computeReshapedDimensions(), createBrowserEnv(), createFileSystem(), createNodejsEnv(), Dimensions(), dr() (+33 more)
|
||||
Nodes (42): boxPredictionLayer(), cn(), computeReshapedDimensions(), createBrowserEnv(), createFileSystem(), createNodejsEnv(), decodeBoxesLayer(), depthwiseConvLayer() (+34 more)
|
||||
|
||||
### Community 1 - "线上闭环测试(不含真实微信授权)"
|
||||
Cohesion: 0.25
|
||||
Nodes (7): 发现并修复, 可复现命令, 已通过的检查, 线上闭环测试(不含真实微信授权), 结论, 话术及仓库同步, 验证边界
|
||||
|
||||
### Community 2 - "app.js"
|
||||
Cohesion: 0.09
|
||||
Nodes (28): cancelSkinDrag(), decode(), drain(), esc(), goSkin(), load(), loginAllowed(), loginWechat() (+20 more)
|
||||
Cohesion: 0.12
|
||||
Nodes (17): cancelSkinDrag(), decode(), drain(), goSkin(), load(), loginAllowed(), loginWechat(), preload() (+9 more)
|
||||
|
||||
### Community 3 - "skin-score.js"
|
||||
Cohesion: 0.10
|
||||
Nodes (49): acneGrade(), buildViewModel(), calcAcne(), calcContour(), calcSens(), calcSmooth(), calcTone(), calcWrinkles() (+41 more)
|
||||
|
||||
### Community 4 - "customer-feedback-second.js"
|
||||
Cohesion: 0.17
|
||||
Nodes (10): assert, context, face, fs, geometry, original, path, source (+2 more)
|
||||
Cohesion: 0.14
|
||||
Nodes (11): assert, context, events, face, fs, geometry, original, path (+3 more)
|
||||
|
||||
### Community 5 - "disposeUnusedWeightTensors"
|
||||
Cohesion: 0.10
|
||||
Cohesion: 0.12
|
||||
Nodes (23): disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$1(), extractParamsFromWeigthMap$2() (+15 more)
|
||||
|
||||
### Community 6 - "f"
|
||||
Cohesion: 0.20
|
||||
Nodes (24): a(), cf(), d(), drawContour(), dt(), ea(), f(), g() (+16 more)
|
||||
### Community 6 - "n"
|
||||
Cohesion: 0.12
|
||||
Nodes (36): ae(), be(), bf(), bi(), ce(), de(), e(), Ef() (+28 more)
|
||||
|
||||
### Community 7 - "test/profile-store.js"
|
||||
Cohesion: 0.14
|
||||
@@ -198,16 +215,16 @@ Cohesion: 0.20
|
||||
Nodes (29): ageText(), api(), bind(), captureQualityHtml(), closeDetail(), collectFilters(), dimsHtml(), doRetry() (+21 more)
|
||||
|
||||
### Community 9 - "__awaiter"
|
||||
Cohesion: 0.11
|
||||
Nodes (17): __awaiter(), bufferToImage(), ComposableTask(), FaceRecognitionNet(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow() (+9 more)
|
||||
|
||||
### Community 10 - "customer-feedback-third.mjs"
|
||||
Cohesion: 0.14
|
||||
Nodes (9): actualLines(), errors, guideLines, manifest, rect(), requests, root, widths (+1 more)
|
||||
Nodes (13): __awaiter(), extendWithFaceDescriptor(), extendWithFaceExpressions(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult(), fetchImage(), fetchJson(), fetchNetWeights() (+5 more)
|
||||
|
||||
### Community 10 - "k"
|
||||
Cohesion: 0.18
|
||||
Nodes (11): b(), gn(), k(), kn(), ne(), tt(), wo(), Yt() (+3 more)
|
||||
|
||||
### Community 11 - "lib/accounts.js"
|
||||
Cohesion: 0.06
|
||||
Nodes (40): atomic(), body(), cookie(), cookies(), crypto, fs, json(), login() (+32 more)
|
||||
Cohesion: 0.17
|
||||
Nodes (20): atomic(), body(), cookie(), cookies(), crypto, fs, json(), login() (+12 more)
|
||||
|
||||
### Community 12 - "server.js"
|
||||
Cohesion: 0.08
|
||||
@@ -218,40 +235,40 @@ Cohesion: 0.12
|
||||
Nodes (15): files, analyze-fail, analyzing, countdown-321, hint-far, hint-front, hint-hold, hint-near (+7 more)
|
||||
|
||||
### Community 14 - "showScreen"
|
||||
Cohesion: 0.19
|
||||
Nodes (13): enterGuest(), loadAppFont(), loginSucceeded(), logoutWechat(), openOverlay(), prepareScanArtwork(), renderAccountStatus(), resetReport() (+5 more)
|
||||
Cohesion: 0.27
|
||||
Nodes (10): enterGuest(), loadAppFont(), loginSucceeded(), logoutWechat(), openOverlay(), prepareScanArtwork(), renderAccountStatus(), resetReport() (+2 more)
|
||||
|
||||
### Community 15 - "customer-feedback-fourth.mjs"
|
||||
Cohesion: 0.20
|
||||
Nodes (8): evaluate(), finite(), CaptureQuality, child, dataRoot, h5, ReportUI, require
|
||||
### Community 15 - "test/accounts.js"
|
||||
Cohesion: 0.22
|
||||
Nodes (9): assert, create, fs, http, os, path, ReportUI, SkinScore (+1 more)
|
||||
|
||||
### Community 16 - "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范"
|
||||
Cohesion: 0.15
|
||||
Nodes (12): 1. 基准与尺寸换算, 2. 色彩规范, 3. 字体规范, 4. 组件切图(assets/svg/), 5. 图片素材(assets/images/,共 15 张), 6. 页面清单(design/ 下有整屏 PNG 预览), 7. 目录结构, 8. 还原建议 (+4 more)
|
||||
|
||||
### Community 17 - "report-persistence.mjs"
|
||||
Cohesion: 0.29
|
||||
Nodes (6): child, dataRoot, fx, h5, ReportUI, require
|
||||
### Community 17 - "vizCompose"
|
||||
Cohesion: 0.14
|
||||
Nodes (14): esc(), openHistory(), renderHistoryList(), renderReport(), renderViz(), setDim(), vizCollectMarks(), vizCompose() (+6 more)
|
||||
|
||||
### Community 18 - "1'me肌肤状态检测 H5"
|
||||
Cohesion: 0.13
|
||||
Nodes (15): 10.09 更新, 1'me肌肤状态检测 H5, Admin API(统一返回 `{ok:false,message}` 错误), Windows 本机部署(提测用), 后台查看(admin.html), 密钥配置, 已知占位清单, 技术说明 (+7 more)
|
||||
|
||||
### Community 19 - "customer-feedback-fourth.js"
|
||||
Cohesion: 0.14
|
||||
Nodes (15): assert, check(), Diagnostics, face(), HeadPose, held, ReportUI, normalized (+7 more)
|
||||
Cohesion: 0.06
|
||||
Nodes (38): evaluate(), finite(), assertPoints(), check(), clamp(), dist(), estimate(), makeFrontal() (+30 more)
|
||||
|
||||
### Community 20 - "build_fixtures.py"
|
||||
Cohesion: 0.40
|
||||
Nodes (9): build_case1(), build_case2(), build_truth(), col(), dump(), main(), parse_value(), set_path() (+1 more)
|
||||
|
||||
### Community 21 - "extractFaces"
|
||||
Cohesion: 0.22
|
||||
Nodes (17): awaitMediaLoaded(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractImagePatches(), getContext2dOrThrow(), getEnv(), getMediaDimensions() (+9 more)
|
||||
Cohesion: 0.19
|
||||
Nodes (19): awaitMediaLoaded(), bufferToImage(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractImagePatches(), getContext2dOrThrow(), getEnv() (+11 more)
|
||||
|
||||
### Community 22 - "i"
|
||||
Cohesion: 0.08
|
||||
Nodes (31): b(), ba(), ca(), cl(), da(), dn(), ee(), et() (+23 more)
|
||||
Nodes (31): ba(), bn(), ca(), cl(), da(), dn(), ee(), et() (+23 more)
|
||||
|
||||
### Community 23 - "facepp-skinanalyze(旷视皮肤分析专业版调用技能)"
|
||||
Cohesion: 0.20
|
||||
@@ -265,17 +282,17 @@ Nodes (8): 1. 音频格式约定(已定), 2. 播报清单(14 条), 3. 播放策
|
||||
Cohesion: 0.20
|
||||
Nodes (16): _arrayLikeToArray(), _arrayWithHoles(), _arrayWithoutHoles(), _classCallCheck(), _createClass(), _defineProperties(), h(), _iterableToArray() (+8 more)
|
||||
|
||||
### Community 26 - "NeuralNetwork"
|
||||
Cohesion: 0.06
|
||||
Nodes (28): createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory(), extractFCParamsFactory() (+20 more)
|
||||
### Community 26 - ".extractWeights"
|
||||
Cohesion: 0.22
|
||||
Nodes (10): createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory(), extractorsFactory$1() (+2 more)
|
||||
|
||||
### Community 27 - "gen_voice.py"
|
||||
Cohesion: 0.30
|
||||
Nodes (11): ndarray, Path, gen_countdown(), main(), normalize_clip(), 倒计时单独按字生成再拼接,避免「三、二、一」整句被读成播报腔、与提示音气质漂移。 与提示音共用同一 speaker + instruct;句间留约…, 裁掉过长首尾静音,保留 ≤150ms 边沿,避免提示显迟钝。, 响度归一 −16 LUFS + 转 MP3 48k CBR mono 22050Hz。 (+3 more)
|
||||
|
||||
### Community 28 - "C"
|
||||
Cohesion: 0.09
|
||||
Nodes (31): ao(), Bo(), C(), cd(), co(), Do(), eo(), fo() (+23 more)
|
||||
Cohesion: 0.10
|
||||
Nodes (28): ao(), Bo(), C(), cd(), co(), Do(), eo(), fo() (+20 more)
|
||||
|
||||
### Community 29 - "face-api.js 本地化依赖(h5/vendor/faceapi/)"
|
||||
Cohesion: 0.29
|
||||
@@ -290,8 +307,8 @@ Cohesion: 0.60
|
||||
Nodes (4): main(), 递归缩放 result 内全部坐标:{left,top,width,height} 矩形与 {x,y} 点,与原数据同为整数。, scale_coords(), to_data_url()
|
||||
|
||||
### Community 35 - "customer-feedback.js"
|
||||
Cohesion: 0.11
|
||||
Nodes (18): alternate, approved, assert, crop, details, excellent, fs, model (+10 more)
|
||||
Cohesion: 0.06
|
||||
Nodes (27): alternate, approved, assert, crop, details, excellent, fs, model (+19 more)
|
||||
|
||||
### Community 37 - "UI 外发版 10.09 图片资源"
|
||||
Cohesion: 0.40
|
||||
@@ -305,21 +322,25 @@ Nodes (4): 1meAI测肤旷视UI问题反馈, UI交互界面调整, 交互, **设
|
||||
Cohesion: 0.22
|
||||
Nodes (8): PSD 来源, 前端参数及限制, 后台校准留痕, 已确认并实现, 生产部署及只读验收, 皱纹(紧塑力)原文, 第四轮反馈修改与验收, 验证
|
||||
|
||||
### Community 41 - ".loadFromDisk"
|
||||
Cohesion: 0.29
|
||||
Nodes (3): ComposableTask(), getModelUris(), TinyFaceDetector()
|
||||
|
||||
### Community 42 - "layer_at"
|
||||
Cohesion: 0.18
|
||||
Nodes (11): list, bounds(), layer_at(), main(), save_image(), composite(), main(), sha256() (+3 more)
|
||||
|
||||
### Community 43 - "extractAllFacesAndComputeResults"
|
||||
Cohesion: 0.12
|
||||
Nodes (9): ComputeAllFaceDescriptorsTask(), ComputeSingleFaceDescriptorTask(), extendWithAge(), extendWithFaceDescriptor(), extendWithGender(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult(), PredictAllAgeAndGenderTask() (+1 more)
|
||||
|
||||
### Community 44 - "start-local.sh"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): HOST, PORT, start-local.sh script
|
||||
|
||||
### Community 45 - "report-persistence.mjs"
|
||||
Cohesion: 0.29
|
||||
Nodes (6): child, dataRoot, fx, h5, ReportUI, require
|
||||
|
||||
### Community 47 - "extractFaceTensors"
|
||||
Cohesion: 0.14
|
||||
Nodes (6): Box(), DetectAllFaceLandmarksTask(), extendWithFaceLandmarks(), extractFaceTensors(), isTensor3D(), ObjectDetection()
|
||||
Cohesion: 0.10
|
||||
Nodes (11): Box(), DetectAllFaceLandmarksTask(), extendWithFaceDetection(), extendWithFaceLandmarks(), extractFaceTensors(), FaceLandmarks(), isWithFaceDetection(), isWithFaceLandmarks() (+3 more)
|
||||
|
||||
### Community 48 - "已修改"
|
||||
Cohesion: 0.22
|
||||
@@ -346,8 +367,8 @@ Cohesion: 0.60
|
||||
Nodes (5): diffCtx(), loadJSON(), main(), okNum(), okStr()
|
||||
|
||||
### Community 58 - ".forwardInput"
|
||||
Cohesion: 0.23
|
||||
Nodes (7): iou(), nonMaxSuppression(), nonMaxSuppression$1(), scale(), stage1(), stage2(), stage3()
|
||||
Cohesion: 0.16
|
||||
Nodes (9): iou(), nonMaxSuppression(), nonMaxSuppression$1(), scale(), SsdMobilenetv1(), stage1(), stage2(), stage3() (+1 more)
|
||||
|
||||
### Community 61 - "报告布局与跟手交互调整"
|
||||
Cohesion: 0.33
|
||||
@@ -361,9 +382,9 @@ Nodes (4): 仍需公网/业务确认, 已发现并修复, 提交前 Review(202
|
||||
Cohesion: 0.29
|
||||
Nodes (4): EXPECTED, fails, pageErrors, pwRoot
|
||||
|
||||
### Community 64 - "recommendation.js"
|
||||
### Community 64 - "isValidNumber"
|
||||
Cohesion: 0.33
|
||||
Nodes (6): assert, check(), missing, raw, SS, UI
|
||||
Nodes (5): bgrToRgbTensor(), Dimensions(), isValidNumber(), isValidProbablitiy(), isWithGender()
|
||||
|
||||
### Community 65 - "微信登录接入与公网部署说明"
|
||||
Cohesion: 0.29
|
||||
@@ -381,13 +402,9 @@ Nodes (7): assert, end, fs, path, source, start, vm
|
||||
Cohesion: 0.29
|
||||
Nodes (3): errors, samples, unexpectedWrites
|
||||
|
||||
### Community 77 - "FaceLandmarks"
|
||||
Cohesion: 0.12
|
||||
Nodes (11): DrawBox(), drawDetections(), drawFaceExpressions(), DrawFaceLandmarks(), FaceExpressions(), FaceLandmarks(), isWithFaceDetection(), isWithFaceExpressions() (+3 more)
|
||||
|
||||
### Community 78 - ".toString"
|
||||
Cohesion: 0.15
|
||||
Nodes (8): FaceLandmarks5(), FaceMatch(), getCenterPoint(), la(), Point(), pr(), pt(), vt()
|
||||
### Community 77 - "drawFaceExpressions"
|
||||
Cohesion: 0.22
|
||||
Nodes (6): DrawBox(), drawDetections(), drawFaceExpressions(), DrawFaceLandmarks(), FaceExpressions(), isWithFaceExpressions()
|
||||
|
||||
### Community 79 - "登录反馈与加载优化(2026-10-12)"
|
||||
Cohesion: 0.25
|
||||
@@ -413,9 +430,9 @@ Nodes (5): 10.09 新版 H5 实施与验收, 按确认项实现, 文档未定义
|
||||
Cohesion: 0.33
|
||||
Nodes (6): 后续真机确认与体验优化, 微信登录联调:2026-10-12, 本轮修正, 白名单与凭据, 真实微信验收与排查, 自动验证
|
||||
|
||||
### Community 85 - "head-pose.js"
|
||||
Cohesion: 0.33
|
||||
Nodes (8): assertPoints(), check(), clamp(), dist(), estimate(), makeFrontal(), makeSide(), midpoint()
|
||||
### Community 85 - "相机翻转性能优化"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 仍需真机验证, 修改范围, 相机翻转性能优化, 验证
|
||||
|
||||
### Community 87 - "产品推荐规则:肤龄基准与边界调整"
|
||||
Cohesion: 0.33
|
||||
@@ -433,33 +450,25 @@ Nodes (9): Ci(), Ei(), Fi(), Ii(), ki(), pe(), ri(), wi() (+1 more)
|
||||
Cohesion: 0.33
|
||||
Nodes (5): 公网部署与复测, 客户反馈修改(2026-10-12), 皱纹数据说明, 逐项处理, 验证
|
||||
|
||||
### Community 93 - ".runAndExtendWithFaceDetections"
|
||||
Cohesion: 0.21
|
||||
Nodes (7): allFacesMtcnn(), allFacesSsdMobilenetv1(), allFacesTinyYolov2(), detectAllFaces(), DetectAllFacesTask(), DetectSingleFaceTask(), extendWithFaceDetection()
|
||||
### Community 93 - ".runAndExtendWithFaceDetection"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): allFacesMtcnn(), allFacesSsdMobilenetv1(), allFacesTinyYolov2(), detectAllFaces(), DetectSingleFaceTask()
|
||||
|
||||
### Community 94 - "第三轮反馈:PSD 对齐、验收及上线记录"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): 原稿与导出可追溯性, 已完成, 本地验证, 生产部署及只读验收, 第三轮反馈:PSD 对齐、验收及上线记录
|
||||
|
||||
### Community 95 - "pn"
|
||||
Cohesion: 0.20
|
||||
Nodes (9): bn(), fn(), ln(), pd(), pn(), un(), wl(), wn() (+1 more)
|
||||
|
||||
### Community 96 - "onDetect"
|
||||
Cohesion: 0.25
|
||||
Nodes (8): captureHaptic(), checkStill(), detectFrame(), detTick(), faceLuma(), faceOffsetX(), onDetect(), setMask()
|
||||
### Community 96 - "production-loop.mjs"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): artifacts, errors, samples
|
||||
|
||||
### Community 97 - "account-client.js"
|
||||
Cohesion: 0.53
|
||||
Nodes (8): browser(), clearGuestCache(), ensureGuest(), guest(), init(), logout(), rememberKind(), request()
|
||||
|
||||
### Community 98 - "js/profile-store.js"
|
||||
Cohesion: 0.67
|
||||
Nodes (3): clean(), create(), key()
|
||||
|
||||
### Community 99 - "capture-diagnostics.js"
|
||||
Cohesion: 0.38
|
||||
Nodes (6): assess(), CaptureQuality, HeadPose, normalize(), scalar(), views
|
||||
### Community 98 - ".mul"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): wl(), wn(), xn()
|
||||
|
||||
### Community 101 - "wechat-fixture.cjs"
|
||||
Cohesion: 0.50
|
||||
@@ -477,60 +486,100 @@ Nodes (3): dragBy(), h5, touch()
|
||||
Cohesion: 0.24
|
||||
Nodes (12): bindBackdropClose(), closeLoginSuccess(), closeOverlay(), handleCamHidden(), leaveCamera(), photosByView(), popHistorySilently(), requestAnalyze() (+4 more)
|
||||
|
||||
### Community 110 - "doCapture"
|
||||
Cohesion: 0.26
|
||||
Nodes (14): armNudge(), beginDetect(), captureFromVideo(), doCapture(), enableFallback(), hideNudge(), isManualMode(), loadFaceApi() (+6 more)
|
||||
### Community 110 - "startCamera"
|
||||
Cohesion: 0.21
|
||||
Nodes (12): beginDetect(), detTick(), enableFallback(), getCamStream(), loadFaceApi(), loadModels(), maybeStartDetect(), onCamTrackEnded() (+4 more)
|
||||
|
||||
### Community 111 - "n"
|
||||
Cohesion: 0.12
|
||||
Nodes (36): ae(), be(), bf(), bi(), ce(), de(), e(), Ef() (+28 more)
|
||||
### Community 111 - "extractWeightsFactory"
|
||||
Cohesion: 0.17
|
||||
Nodes (8): extractorsFactory(), extractorsFactory$4(), extractParams(), extractParams$5(), extractParams$6(), extractParamsTiny(), extractWeightsFactory(), TinyFaceFeatureExtractor()
|
||||
|
||||
### Community 115 - "normalize"
|
||||
Cohesion: 0.07
|
||||
Nodes (28): conv(), conv$1(), convDown(), convLayer(), convLayer$1(), convNoRelu(), convWithBatchNorm(), denseBlock3() (+20 more)
|
||||
|
||||
### Community 117 - "startCamera"
|
||||
Cohesion: 0.29
|
||||
Nodes (7): applyMirror(), effMirror(), getCamStream(), handleCamVisible(), onCamTrackEnded(), startCamera(), waitVideoStable()
|
||||
|
||||
### Community 122 - "onPhotoCaptured"
|
||||
Cohesion: 0.27
|
||||
Nodes (10): busyWithFlow(), captureStageHint(), dropHint(), hideCamStatus(), onPhotoCaptured(), play(), readPhotoFile(), renderCamStep() (+2 more)
|
||||
Nodes (8): conv(), denseBlock3(), denseBlock4(), depthwiseSeparableConv(), mainBlock(), normalize(), reductionBlock(), __spreadArrays()
|
||||
|
||||
### Community 116 - ".run"
|
||||
Cohesion: 0.29
|
||||
Nodes (4): extendWithAge(), extendWithGender(), PredictAllAgeAndGenderTask(), PredictSingleAgeAndGenderTask()
|
||||
|
||||
### Community 117 - "play"
|
||||
Cohesion: 0.33
|
||||
Nodes (6): applyMirror(), busyWithFlow(), effMirror(), handleCamVisible(), play(), renderCamStep()
|
||||
|
||||
### Community 118 - "f"
|
||||
Cohesion: 0.20
|
||||
Nodes (24): a(), cf(), d(), drawContour(), dt(), ea(), f(), g() (+16 more)
|
||||
|
||||
### Community 121 - "wechat-browser.mjs"
|
||||
Cohesion: 0.22
|
||||
Nodes (6): createAccounts, h5, ReportUI, require, root, SkinScore
|
||||
|
||||
### Community 122 - "doCapture"
|
||||
Cohesion: 0.15
|
||||
Nodes (24): armNudge(), captureFromVideo(), captureHaptic(), captureStageHint(), checkStill(), detectFrame(), doCapture(), dropHint() (+16 more)
|
||||
|
||||
### Community 123 - "评分圈内居中、基础信息页固定与正脸语音修复"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 发布, 范围, 评分圈内居中、基础信息页固定与正脸语音修复, 验证
|
||||
|
||||
### Community 126 - ".forwardInput"
|
||||
### Community 124 - "FaceProcessor"
|
||||
Cohesion: 0.22
|
||||
Nodes (9): boxPredictionLayer(), decodeBoxesLayer(), depthwiseConvLayer(), getCenterCoordinatesAndSizesLayer(), getStridesForLayerIdx(), mobileNetV1(), outputLayer(), pointwiseConvLayer() (+1 more)
|
||||
Nodes (3): extractParams$1(), FaceLandmark68TinyNet(), FaceProcessor()
|
||||
|
||||
### Community 130 - ".predictExpressions"
|
||||
Cohesion: 0.15
|
||||
Nodes (4): extendWithFaceExpressions(), FaceExpressionNet(), PredictAllFaceExpressionsTask(), PredictSingleFaceExpressionsTask()
|
||||
### Community 129 - "TinyYolov2Base"
|
||||
Cohesion: 0.29
|
||||
Nodes (5): convWithBatchNorm(), depthwiseSeparableConv$1(), leaky(), TinyYolov2Base(), validateConfig()
|
||||
|
||||
### Community 130 - "FaceFeatureExtractor"
|
||||
Cohesion: 0.22
|
||||
Nodes (5): extractFCParamsFactory(), extractorsFactory$8(), extractParams$3(), extractParams$7(), FaceFeatureExtractor()
|
||||
|
||||
### Community 131 - "原始肌龄显示与鸿蒙滚轮兼容"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): 原始肌龄显示与鸿蒙滚轮兼容, 回归, 范围
|
||||
|
||||
### Community 134 - "height"
|
||||
Cohesion: 0.32
|
||||
### Community 133 - ".toString"
|
||||
Cohesion: 0.20
|
||||
Nodes (7): FaceLandmarks5(), FaceMatch(), getCenterPoint(), la(), pr(), pt(), vt()
|
||||
|
||||
### Community 136 - "convLayer"
|
||||
Cohesion: 0.43
|
||||
Nodes (8): convLayer(), fullyConnectedLayer(), ONet(), PNet(), prelu(), qh(), RNet(), sharedLayer()
|
||||
|
||||
### Community 137 - "height"
|
||||
Cohesion: 0.38
|
||||
Nodes (5): height(), FaceLandmark68NetBase(), getSizesForScale(), normalize$1(), rescaleAndNormalize()
|
||||
|
||||
### Community 138 - "age"
|
||||
Cohesion: 0.43
|
||||
Nodes (4): age(), clean(), create(), key()
|
||||
|
||||
### Community 139 - "recommendation.js"
|
||||
Cohesion: 0.33
|
||||
Nodes (6): assert, check(), missing, raw, SS, UI
|
||||
|
||||
### Community 140 - ".forwardInput"
|
||||
Cohesion: 0.48
|
||||
Nodes (6): conv$1(), convDown(), convLayer$1(), convNoRelu(), residual(), residualDown()
|
||||
|
||||
### Community 141 - "FaceRecognitionNet"
|
||||
Cohesion: 0.33
|
||||
Nodes (3): extractorsFactory$2(), extractParams$4(), FaceRecognitionNet()
|
||||
|
||||
## Knowledge Gaps
|
||||
- **339 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+334 more)
|
||||
- **345 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+340 more)
|
||||
These have ≤1 connection - possible missing edges or undocumented components.
|
||||
- **27 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
- **36 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
|
||||
## Suggested Questions
|
||||
_Questions this graph is uniquely positioned to answer:_
|
||||
|
||||
- **Why does `height()` connect `height` to `app.js`, `scan-placeholder.mjs`, `__awaiter`, `customer-feedback-third.mjs`, `profile-interaction.mjs`, `showScreen`, `extractFaceTensors`, `extractFaces`, `.forwardInput`?**
|
||||
_High betweenness centrality (0.051) - this node is a cross-community bridge._
|
||||
- **Why does `v()` connect `f` to `report-ui.js`, `face-api.js`, `app.js`, `C`?**
|
||||
_High betweenness centrality (0.043) - this node is a cross-community bridge._
|
||||
- **Why does `height()` connect `height` to `app.js`, `customer-feedback.js`, `scan-placeholder.mjs`, `__awaiter`, `profile-interaction.mjs`, `showScreen`, `extractFaceTensors`, `extractFaces`, `.forwardInput`?**
|
||||
_High betweenness centrality (0.068) - this node is a cross-community bridge._
|
||||
- **Why does `v()` connect `f` to `report-ui.js`, `vizCompose`, `C`, `face-api.js`?**
|
||||
_High betweenness centrality (0.046) - this node is a cross-community bridge._
|
||||
- **Why does `fill()` connect `report-ui.js` to `f`?**
|
||||
_High betweenness centrality (0.040) - this node is a cross-community bridge._
|
||||
_High betweenness centrality (0.037) - this node is a cross-community bridge._
|
||||
- **Are the 24 inferred relationships involving `n()` (e.g. with `newRecordId()` and `ae()`) actually correct?**
|
||||
_`n()` has 24 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 27 inferred relationships involving `r()` (e.g. with `handleExport()` and `handleRecordsQuery()`) actually correct?**
|
||||
@@ -538,4 +587,4 @@ _Questions this graph is uniquely positioned to answer:_
|
||||
- **Are the 9 inferred relationships involving `i()` (e.g. with `da()` and `f()`) actually correct?**
|
||||
_`i()` has 9 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **What connects `source`, `document_size`, `pages` to the rest of the system?**
|
||||
_339 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
_345 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
File diff suppressed because one or more lines are too long
+1459
-1050
File diff suppressed because it is too large
Load Diff
+36
-16
@@ -40,8 +40,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/app.js": {
|
||||
"mtime": 1791878787.1929805,
|
||||
"ast_hash": "63cd1c83937dee7144a854b559f9cfd5",
|
||||
"mtime": 1791884874.9650493,
|
||||
"ast_hash": "1e78662941dfedadd108f24c820676cc",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/head-pose.js": {
|
||||
@@ -140,8 +140,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/index.html": {
|
||||
"mtime": 1791878732.5729809,
|
||||
"ast_hash": "ac31b30a7a6a79a97355db95323784f9",
|
||||
"mtime": 1791885347.4210553,
|
||||
"ast_hash": "663942bdfbce6304130bb23712875653",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/vendor/faceapi/README.md": {
|
||||
@@ -1140,8 +1140,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/img/ui-20261009/icon-switch-camera.png": {
|
||||
"mtime": 1791642110.915571,
|
||||
"ast_hash": "6197c8000c21156ffecdf77f9c5ed5ba",
|
||||
"mtime": 1791884685.3970492,
|
||||
"ast_hash": "a2805c01722e185dfca450a1eed8c70d",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/img/ui-20261009/icon-wechat.png": {
|
||||
@@ -1675,8 +1675,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/customer-feedback-second.js": {
|
||||
"mtime": 1791819755.8747528,
|
||||
"ast_hash": "03ea3689f3c86e78a1f5bd46966f4145",
|
||||
"mtime": 1791884874.9650493,
|
||||
"ast_hash": "a16f86030060a31b8e44cd2f673c1559",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/customer-feedback-second.mjs": {
|
||||
@@ -1805,8 +1805,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/capture-quality.js": {
|
||||
"mtime": 1791809137.1269257,
|
||||
"ast_hash": "ec66579ee0cde06a3fa045d612da69c1",
|
||||
"mtime": 1791884831.1770513,
|
||||
"ast_hash": "fa113a7f933091cdec309d5988cfea8a",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/lib/capture-diagnostics.js": {
|
||||
@@ -1820,8 +1820,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/customer-feedback-fourth.js": {
|
||||
"mtime": 1791819755.8706622,
|
||||
"ast_hash": "92a53685400808b24315f85ed851566d",
|
||||
"mtime": 1791884945.1610558,
|
||||
"ast_hash": "716783c536dd5f2b9e6725f2ac759793",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/customer-feedback-fourth.mjs": {
|
||||
@@ -2000,13 +2000,33 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u539f\u59cb\u808c\u9f84\u663e\u793a\u4e0e\u9e3f\u8499\u6eda\u8f6e\u517c\u5bb9-2026-10-13.md": {
|
||||
"mtime": 1791878573.720988,
|
||||
"ast_hash": "7003663bd6ea0a0cf586cb2e3155d925",
|
||||
"mtime": 1791879146.5129797,
|
||||
"ast_hash": "99e4254133761034e9057465d4edeb6f",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u5fae\u4fe1\u9000\u51fa\u767b\u5f55-2026-10-13.md": {
|
||||
"mtime": 1791878884.432984,
|
||||
"ast_hash": "ea59ab0974069d858e08da9789b88bf2",
|
||||
"mtime": 1791879146.5129797,
|
||||
"ast_hash": "e0fe3daeaa1eeeacf18ff18d8eee69e4",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/camera-flip-browser.mjs": {
|
||||
"mtime": 1791881142.0210207,
|
||||
"ast_hash": "b64cb9f61c398ce1aa12f59bc27cb481",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u76f8\u673a\u7ffb\u8f6c\u6027\u80fd\u4f18\u5316-2026-10-13.md": {
|
||||
"mtime": 1791881165.8330255,
|
||||
"ast_hash": "bb925d4f64c3d8275262b8241adc4c1b",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/splash-brand-browser.mjs": {
|
||||
"mtime": 1791885347.4210553,
|
||||
"ast_hash": "257d77c6cfb00b56b0a0dff9fc8955b1",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u62cd\u6444\u89d2\u5ea6\u4e0d\u5408\u683c\u9707\u52a8\u63d0\u9192-2026-10-13.md": {
|
||||
"mtime": 1791884945.1610558,
|
||||
"ast_hash": "a02d1db13bb8aa8e76e58e78fb6b82b7",
|
||||
"semantic_hash": ""
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.5 KiB |
@@ -220,6 +220,11 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
|
||||
#cam-video.mirror { transform: scaleX(-1); } /* 前置镜头镜像预览 */
|
||||
#s-cam.cam-live #cam-video { display: block; }
|
||||
#s-cam.cam-live .cam-bg { display: none; }
|
||||
#cam-frozen { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; display:none; z-index:0; pointer-events:none; }
|
||||
#cam-frozen.mirror { transform:scaleX(-1); }
|
||||
#s-cam.cam-switching #cam-frozen { display:block; }
|
||||
#s-cam.cam-switching .cam-bg { display:none; }
|
||||
.cam-flip:disabled { opacity:.5; }
|
||||
/* 纵向定位按 WebView 口径:Figma 帧 812 高扣掉顶部 86px 微信导航栏 = 726 高视口 */
|
||||
.cam-step {
|
||||
position: absolute; left: 0; right: 0; top: 7.1%;
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
}
|
||||
.guide-description { font-size: 12px; line-height: 1.6; margin-bottom: calc(35 * var(--u)); }
|
||||
.cam-feature-lines { fill: none; stroke: #fff; stroke-width: 2.5; stroke-dasharray: 5 5; filter: drop-shadow(0 1px 1px #000); }
|
||||
#s-cam:not(.cam-live) .cam-feature-lines { stroke: #111; filter: none; }
|
||||
#s-cam:not(.cam-live):not(.cam-switching) .cam-feature-lines { stroke: #111; filter: none; }
|
||||
.font-credit { display: block; text-align: center; color: #888; font-size: 10px; margin: 24px auto 0; }
|
||||
.account-status { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 14px; border-radius: 8px; font-size: 12px; }
|
||||
.account-status span::before { content: ''; display: inline-block; width: 7px; height: 7px; margin-right: 7px; border-radius: 50%; background: #b7bac0; }
|
||||
|
||||
@@ -9,6 +9,13 @@
|
||||
top: max(6.838833%, calc(env(safe-area-inset-top) + 48px));
|
||||
}
|
||||
#s-splash .splash-slogan { top: max(6.838833%, calc(env(safe-area-inset-top) + 48px)); }
|
||||
/* Retain the PSD's two lower slogan lines; the updated brand uses regular type. */
|
||||
.splash-slogan-updated .splash-slogan-copy { width:100%; height:100%; clip-path:inset(32% 0 0); }
|
||||
.splash-slogan-brand { position:absolute; top:0; left:50%; transform:translateX(-50%); font-family:Arial,sans-serif; font-size:23px; font-weight:400; line-height:1; white-space:nowrap; }
|
||||
#s-splash [data-action="open-privacy"],
|
||||
#s-splash [data-action="open-privacy"]:focus,
|
||||
#s-splash [data-action="open-privacy"]:active { border:0; outline:none; box-shadow:none; -webkit-appearance:none; appearance:none; }
|
||||
#s-splash [data-action="open-privacy"]:focus-visible { filter:brightness(.94); }
|
||||
.splash-bottom { bottom: max(5.882353%, env(safe-area-inset-bottom)); gap: calc(7.291667 * var(--u)); padding-bottom: 0; }
|
||||
.brand-row { margin-bottom: calc(10.416667 * var(--u)); gap: 0; }
|
||||
.brand-lockup { width: 179.513889px; height: 20.486111px; }
|
||||
|
||||
+5
-2
@@ -43,7 +43,10 @@
|
||||
<section class="screen" id="s-splash">
|
||||
<div class="splash-bg"></div>
|
||||
<div class="account-status account-status-home"><span data-auth-status></span><button type="button" class="account-logout" data-action="logout-wechat" hidden>退出登录</button></div>
|
||||
<img class="splash-slogan" src="assets/img/ui-20261012/splash-slogan.png" width="718" height="217" loading="lazy" alt="ONE ME,FROM THE INSIDE OUT;以我为始 自有表现">
|
||||
<div class="splash-slogan splash-slogan-updated" role="img" aria-label="1'me,FROM THE INSIDE OUT;以我为始 自有表现">
|
||||
<img class="splash-slogan-copy" src="assets/img/ui-20261012/splash-slogan.png" width="718" height="217" loading="lazy" alt="">
|
||||
<span class="splash-slogan-brand" aria-hidden="true">1'me</span>
|
||||
</div>
|
||||
<div class="splash-bottom">
|
||||
<div class="brand-row">
|
||||
<img src="assets/img/ui-20261012/brand-lockup.png" width="517" height="59" loading="lazy" alt="1'me 肌肤状态检测" class="brand-lockup">
|
||||
@@ -159,7 +162,7 @@
|
||||
<img src="assets/img/ui-20261009/icon-shutter.png" alt="快门">
|
||||
</button>
|
||||
<button class="cam-side cam-flip" data-action="flip" aria-label="切换">
|
||||
<img src="assets/img/ui-20261009/icon-switch-camera.png" alt="切换">
|
||||
<img src="assets/img/ui-20261009/icon-switch-camera.png?v=20261013" alt="切换">
|
||||
</button>
|
||||
</div>
|
||||
<div class="cam-fill" id="cam-fill"></div>
|
||||
|
||||
+119
-35
@@ -402,6 +402,23 @@
|
||||
var IS_MOCK = /[?&]mock=1/.test(location.search); // mock:不加载 face-api、不跑检测,analyze 直返内置样本
|
||||
var camVideo = $('#cam-video'), camFile = $('#cam-file'), albumFile = $('#album-file');
|
||||
var camStream = null, camLive = false, camFallback = false, camSeqCam = 0; // camSeqCam:相机启动令牌,防止迟到的授权回调泄漏摄像头
|
||||
var camBusy = false, camReadyTimer = null, camOpenTimer = null;
|
||||
var camFrozen = document.createElement('canvas');
|
||||
camFrozen.id = 'cam-frozen';
|
||||
camVideo.parentNode.insertBefore(camFrozen, camVideo);
|
||||
function freezeCamera() {
|
||||
if (!camLive || !camVideo.videoWidth) return;
|
||||
camFrozen.width = camVideo.videoWidth;
|
||||
camFrozen.height = camVideo.videoHeight;
|
||||
camFrozen.getContext('2d').drawImage(camVideo, 0, 0);
|
||||
camFrozen.classList.toggle('mirror', camMirrored);
|
||||
$('#s-cam').classList.add('cam-switching');
|
||||
}
|
||||
function setCameraBusy(busy) {
|
||||
camBusy = busy;
|
||||
$('.cam-flip').disabled = busy;
|
||||
$('.cam-flip').setAttribute('aria-busy', String(busy));
|
||||
}
|
||||
var photos = []; // 三连拍(dataURL),拍摄中与 CAM_STEPS 同序;送检前经 photosByView 重排为 [正脸, 左脸, 右脸]
|
||||
var captureDiagnostics = [], holdState = null;
|
||||
|
||||
@@ -428,44 +445,67 @@
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
// 相机预热:部分设备开头 1~2s 以低分辨率出画(画面显脸巨大),等分辨率稳定两拍再放开取景与检测
|
||||
// 首帧可用即展示,不再额外等待三轮 300ms。定时器由 stopCamera 统一取消。
|
||||
function waitVideoStable(done) {
|
||||
var lastW = 0, stableN = 0, tries = 0;
|
||||
var t = setInterval(function () {
|
||||
var w = camVideo.videoWidth;
|
||||
tries++;
|
||||
if (w && w === lastW) {
|
||||
if (++stableN >= 2) { clearInterval(t); done(); }
|
||||
} else { stableN = 0; lastW = w; }
|
||||
if (tries > 40) { clearInterval(t); done(); } // 12s 兜底,异常也不卡死
|
||||
}, 300);
|
||||
var started = Date.now();
|
||||
camReadyTimer = setInterval(function () {
|
||||
var ready = camVideo.readyState >= 2 && camVideo.videoWidth > 0 && camVideo.videoHeight > 0;
|
||||
if (ready || Date.now() - started >= 12000) {
|
||||
clearInterval(camReadyTimer); camReadyTimer = null;
|
||||
done(ready);
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
function startCamera() {
|
||||
if (camStream) { stopCamera(); }
|
||||
function startCamera(restoreFacing, keepFrozen) {
|
||||
stopCamera(keepFrozen);
|
||||
camFallback = false;
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { enableFallback(); return; }
|
||||
setCameraBusy(true);
|
||||
if (keepFrozen) showCamStatus('正在切换摄像头…');
|
||||
camSeqCam++;
|
||||
var token = camSeqCam;
|
||||
function failed() {
|
||||
if (token !== camSeqCam) return;
|
||||
stopCamera(keepFrozen);
|
||||
if (restoreFacing) {
|
||||
camFacing = restoreFacing;
|
||||
camMirrored = camFacing === 'user';
|
||||
toast('切换失败,正在恢复原摄像头');
|
||||
startCamera(null, keepFrozen);
|
||||
} else { stopCamera(); enableFallback(); }
|
||||
}
|
||||
// 翻转/恢复不涉及首次权限询问,超时恢复;迟到的流仍会被令牌检查释放。
|
||||
if (keepFrozen) camOpenTimer = setTimeout(failed, 12000);
|
||||
getCamStream().then(function (stream) {
|
||||
if (token !== camSeqCam) { stream.getTracks().forEach(function (t) { t.stop(); }); return; } // 期间已退出拍照页:立即释放
|
||||
clearTimeout(camOpenTimer); camOpenTimer = null;
|
||||
camStream = stream;
|
||||
camVideo.srcObject = stream;
|
||||
var track = stream.getVideoTracks()[0];
|
||||
var actualFacing = track && track.getSettings && track.getSettings().facingMode;
|
||||
if (actualFacing === 'user' || actualFacing === 'environment') {
|
||||
if (restoreFacing && actualFacing !== camFacing) { failed(); return; }
|
||||
camFacing = actualFacing;
|
||||
}
|
||||
camMirrored = camFacing === 'user';
|
||||
applyMirror();
|
||||
// iOS 来电/切后台等系统打断会结束 track:挂 ended 监听走自动重连
|
||||
stream.getTracks().forEach(function (t) {
|
||||
t.onended = function () { onCamTrackEnded(token); };
|
||||
});
|
||||
waitVideoStable(function () { // 预热完成前不出实时画面(静态示意底图还在),避免开头"大脸"闪变
|
||||
waitVideoStable(function (ready) {
|
||||
if (token !== camSeqCam) return;
|
||||
if (!ready) { failed(); return; }
|
||||
camLive = true;
|
||||
$('#s-cam').classList.add('cam-live');
|
||||
$('#s-cam').classList.remove('cam-switching');
|
||||
camFrozen.width = camFrozen.height = 0;
|
||||
setCameraBusy(false);
|
||||
$('#cam-status').classList.remove('show');
|
||||
beginDetect(); // 确认相机可用后才加载检测模型(无相机权限设备不白加载)
|
||||
maybeStartDetect(); // 模型已就绪时即刻开跑检测循环
|
||||
});
|
||||
}).catch(function () {
|
||||
if (token !== camSeqCam) return;
|
||||
enableFallback();
|
||||
});
|
||||
}).catch(failed);
|
||||
}
|
||||
// track 被系统结束(iOS 打断)且页面仍在拍照前台:toast 后自动重连,重连失败走 startCamera 内的 enableFallback
|
||||
function onCamTrackEnded(token) {
|
||||
@@ -475,8 +515,15 @@
|
||||
toast('摄像头连接中断,正在重连');
|
||||
startCamera();
|
||||
}
|
||||
function stopCamera() {
|
||||
function stopCamera(keepFrozen) {
|
||||
camSeqCam++; // 使进行中的 getUserMedia 回调失效
|
||||
clearInterval(camReadyTimer); clearTimeout(camOpenTimer);
|
||||
camReadyTimer = camOpenTimer = null;
|
||||
setCameraBusy(false);
|
||||
if (!keepFrozen) {
|
||||
$('#s-cam').classList.remove('cam-switching');
|
||||
camFrozen.width = camFrozen.height = 0;
|
||||
}
|
||||
stopDetect(); // 检测循环/无进展提示一并收尾
|
||||
$('#cam-status').classList.remove('show');
|
||||
if (camStream) { camStream.getTracks().forEach(function (t) { t.stop(); }); camStream = null; }
|
||||
@@ -746,9 +793,8 @@
|
||||
}
|
||||
return faceApiPromise;
|
||||
}
|
||||
var detRunning = false, detTimer = null; // 检测循环
|
||||
var detRunning = false, detTimer = null, detGeneration = 0, detInFlight = false; // 检测循环
|
||||
var okSince = 0; // 当前连续 ok 起点(0=未在累积)
|
||||
var readyHapticAt = 0; // 抖动重试时最多每两秒提醒一次
|
||||
var nudgeTimer = null; // 12s 无进展提示
|
||||
var capturing = false; // 白屏补光抓帧中(暂停姿态判定)
|
||||
var lastHint = ''; // 提示文案去重
|
||||
@@ -810,7 +856,7 @@
|
||||
function resetDetectState() {
|
||||
okSince = 0;
|
||||
holdState = null;
|
||||
readyHapticAt = 0;
|
||||
resetAngleHaptic();
|
||||
missFrames = 0;
|
||||
oppFrames = 0;
|
||||
lastHint = '';
|
||||
@@ -835,34 +881,43 @@
|
||||
function maybeStartDetect() {
|
||||
if (detRunning || IS_MOCK || detectDisabled || !modelsOk || !camLive || currentScreen !== 's-cam') return;
|
||||
detRunning = true;
|
||||
detGeneration++;
|
||||
syncCamMode();
|
||||
detTick();
|
||||
detTick(detGeneration);
|
||||
}
|
||||
function stopDetect() {
|
||||
resetAngleHaptic();
|
||||
detRunning = false;
|
||||
detGeneration++;
|
||||
clearTimeout(detTimer);
|
||||
clearTimeout(nudgeTimer);
|
||||
hideNudge();
|
||||
}
|
||||
// 递归 setTimeout 而非 setInterval:检测是异步的,避免帧与帧堆叠
|
||||
function detTick() {
|
||||
if (!detRunning) return;
|
||||
function detTick(generation) {
|
||||
if (!detRunning || generation !== detGeneration) return;
|
||||
detTimer = setTimeout(function () {
|
||||
if (!detRunning) return;
|
||||
detectFrame().then(detTick, detTick);
|
||||
if (!detRunning || generation !== detGeneration) return;
|
||||
if (detInFlight) { detTick(generation); return; }
|
||||
detInFlight = true;
|
||||
function finished() {
|
||||
detInFlight = false;
|
||||
detTick(generation);
|
||||
}
|
||||
Promise.resolve().then(detectFrame).then(finished, finished);
|
||||
}, DETECT_INTERVAL);
|
||||
}
|
||||
function detectFrame() {
|
||||
if (shutterLock || capturing || !camLive) return Promise.resolve(); // 抓帧/流程推进中不判姿态
|
||||
var step = camStep, seq = camSeq; // 快照:检测是异步的,帧落地时步骤/流程可能已变
|
||||
var step = camStep, seq = camSeq, generation = detGeneration; // 旧镜头推理结果不能污染新镜头
|
||||
return faceapi
|
||||
.detectAllFaces(camVideo, new faceapi.TinyFaceDetectorOptions({ inputSize: DETECT_INPUT_SIZE, scoreThreshold: DETECT_SCORE }))
|
||||
.withFaceLandmarks()
|
||||
.then(function (d) {
|
||||
if (step !== camStep || seq !== camSeq) return; // 跨步骤陈帧/流程已重置:丢弃
|
||||
if (step !== camStep || seq !== camSeq || generation !== detGeneration) return;
|
||||
if (detRunning && !shutterLock && !capturing) onDetect(d);
|
||||
})
|
||||
.catch(function () { if (step === camStep && seq === camSeq) { holdState = null; okSince = 0; } }); // Unchecked frames break continuous readiness.
|
||||
.catch(function () { if (step === camStep && seq === camSeq && generation === detGeneration) { holdState = null; okSince = 0; resetAngleHaptic(); } });
|
||||
}
|
||||
// 人脸框中心相对取景中线的水平偏移(占屏宽比例);按 object-fit:cover + 预览缩放换算到屏幕,镜像只影响符号
|
||||
function faceOffsetX(box) {
|
||||
@@ -887,11 +942,34 @@
|
||||
/* camera-haptic-pure-begin */
|
||||
function captureHaptic(nav) {
|
||||
// Unsupported/blocked vibration must never interrupt automatic capture.
|
||||
try { return typeof nav.vibrate === 'function' && nav.vibrate([70, 40, 70]) !== false; }
|
||||
try { return !!nav && typeof nav.vibrate === 'function' && nav.vibrate(30) !== false; }
|
||||
catch (e) { return false; }
|
||||
}
|
||||
var angleHapticState = { since: null, lastFrame: null, lastBuzz: null, vibrating: false };
|
||||
function resetAngleHaptic() {
|
||||
if (angleHapticState.vibrating) {
|
||||
try { if (typeof navigator.vibrate === 'function') navigator.vibrate(0); } catch (_) {}
|
||||
}
|
||||
angleHapticState.since = angleHapticState.lastFrame = null;
|
||||
angleHapticState.vibrating = false;
|
||||
}
|
||||
function updateAngleHaptic(invalid, now) {
|
||||
if (!invalid) { resetAngleHaptic(); return; }
|
||||
var s = angleHapticState;
|
||||
// A missing/failed frame breaks continuity; a stalled detector must not
|
||||
// produce a reminder as though it observed the user throughout the gap.
|
||||
if (s.since === null || now < s.lastFrame || now - s.lastFrame > 2000) s.since = now;
|
||||
s.lastFrame = now;
|
||||
if (now - s.since >= 1000 && (s.lastBuzz === null || now - s.lastBuzz >= 4000)) {
|
||||
s.lastBuzz = now;
|
||||
s.vibrating = captureHaptic(navigator);
|
||||
}
|
||||
}
|
||||
/* camera-haptic-pure-end */
|
||||
function onDetect(d) {
|
||||
if (shutterLock || capturing || camBusy || !camLive || currentScreen !== 's-cam' || document.hidden) {
|
||||
resetAngleHaptic(); return;
|
||||
}
|
||||
var count = Array.isArray(d) ? d.length : d ? 1 : 0;
|
||||
if (Array.isArray(d)) d = d[0];
|
||||
var hint = '', ok = false;
|
||||
@@ -900,6 +978,7 @@
|
||||
if (noFace) {
|
||||
hint = '未检测到人脸';
|
||||
} else if (!hasFace) {
|
||||
resetAngleHaptic();
|
||||
holdState = null; okSince = 0;
|
||||
return; // Keep the hint quiet, but never count a missing frame as stable.
|
||||
} else {
|
||||
@@ -926,6 +1005,7 @@
|
||||
hint = verdict.hint; ok = verdict.ok;
|
||||
window.__dbgRatio = ratio; // 自动化校准/测试用(检测框高/视频高)
|
||||
}
|
||||
updateAngleHaptic(!!(verdict && verdict.reason === 'angle'), Date.now());
|
||||
setMask(ok ? 'ok' : (noFace ? 'no-face' : ''));
|
||||
// 合格时「保持不动」只显示不播报:随即抓拍,「拍摄完成」会截断它
|
||||
setHint(ok ? '保持不动' : hint, ok, ok);
|
||||
@@ -934,7 +1014,6 @@
|
||||
holdState = held.state;
|
||||
if (holdState.frames === 1) {
|
||||
okSince = holdState.since;
|
||||
if (!readyHapticAt || okSince - readyHapticAt >= 2000) { readyHapticAt = okSince; captureHaptic(navigator); }
|
||||
}
|
||||
if (held.ready) { okSince = 0; holdState = null; doCapture(); }
|
||||
}
|
||||
@@ -1009,7 +1088,7 @@
|
||||
if (!camLive) return;
|
||||
shutterLock = true;
|
||||
capturing = true;
|
||||
if (isManualMode()) captureHaptic(navigator); // Manual shutter has no readiness callback.
|
||||
resetAngleHaptic(); // Only invalid angles vibrate, never the shutter itself.
|
||||
clearTimeout(nudgeTimer);
|
||||
hideNudge();
|
||||
var seq = camSeq;
|
||||
@@ -1481,14 +1560,17 @@
|
||||
|
||||
/* ─── 相机可用后加载检测模型(加载完成前快门即手动模式) ─── */
|
||||
function beginDetect() {
|
||||
var token = camSeqCam;
|
||||
if (IS_MOCK || camFallback) { resetDetectState(); return; }
|
||||
detectDisabled = false; // 上次模型加载失败允许重试(modelsPromise 失败时已清空,loadModels 会重新拉取)
|
||||
resetDetectState();
|
||||
if (!modelsOk) showCamStatus('检测引擎加载中…');
|
||||
loadModels().then(function () {
|
||||
if (token !== camSeqCam || !camLive) return;
|
||||
$('#cam-status').classList.remove('show');
|
||||
maybeStartDetect();
|
||||
}).catch(function () {
|
||||
if (token !== camSeqCam || !camLive) return;
|
||||
$('#cam-status').classList.remove('show');
|
||||
detectDisabled = true; // 仅禁用当次;下次进拍照页 beginDetect 复位后重试
|
||||
resetDetectState();
|
||||
@@ -1619,10 +1701,12 @@
|
||||
},
|
||||
'flip': function () { // 切换前后镜头
|
||||
if (camFallback) { toast('系统相机模式下请在系统相机内切换'); return; }
|
||||
if (!camStream) { toast('相机启动中,请稍候'); return; }
|
||||
if (camBusy || !camLive) { toast('相机启动中,请稍候'); return; }
|
||||
if (shutterLock || capturing) { toast('拍摄中,请稍候'); return; }
|
||||
var previousFacing = camFacing;
|
||||
freezeCamera();
|
||||
camFacing = camFacing === 'user' ? 'environment' : 'user';
|
||||
camMirrored = camFacing === 'user';
|
||||
startCamera(); // 复用启停与预热(camSeqCam 防竞态)
|
||||
startCamera(previousFacing, true);
|
||||
},
|
||||
'history': function () { openHistory(); },
|
||||
'history-retry': function () { renderHistoryList(); },
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
yawDrift: 8, pitchDrift: 8, centerDrift: 0.055, sizeDrift: 0.065 });
|
||||
function finite(v) { return typeof v === 'number' && isFinite(v); }
|
||||
function evaluate(d, options) {
|
||||
var failed = function(hint) { return { ok: false, hint: hint }; };
|
||||
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;
|
||||
@@ -31,7 +31,7 @@
|
||||
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);
|
||||
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('光线过强,请避开强光');
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
// Canvas cameras and intercepted auth; no personal images, writes or paid API.
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href);
|
||||
const browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required'] });
|
||||
try {
|
||||
const page = await browser.newPage({ viewport: { width: 375, height: 726 } });
|
||||
const errors = [];
|
||||
page.on('pageerror', e => errors.push(e.message));
|
||||
await page.route('**/api/**', route => {
|
||||
assert.equal(route.request().method(), 'GET', 'no account/report writes');
|
||||
return route.fulfill({ json: { ok: true, user: { id: 'camera-fixture', kind: 'browser' } } });
|
||||
});
|
||||
const source = fs.readFileSync(fileURLToPath(new URL('../js/app.js', import.meta.url)), 'utf8');
|
||||
await page.route('**/js/app.js*', route => route.fulfill({ contentType: 'text/javascript', body: source.replace(/\}\)\(\);\s*$/, `
|
||||
window.__cameraTest = {
|
||||
enter: function() { showScreen('s-cam'); startCamera(); renderCamStep(); },
|
||||
stop: stopCamera,
|
||||
capture: function(value) { shutterLock = value; },
|
||||
startDetection: function() { modelsOk = true; detectDisabled = false; IS_MOCK = false; maybeStartDetect(); },
|
||||
state: function() { return { live: camLive, busy: camBusy, facing: camFacing, fallback: camFallback }; }
|
||||
};
|
||||
})();`) }));
|
||||
await page.addInitScript(() => {
|
||||
const nativeTimeout = window.setTimeout.bind(window);
|
||||
window.setTimeout = (fn, ms, ...args) => nativeTimeout(fn, window.__shortCameraTimeout && ms === 12000 ? 200 : ms, ...args);
|
||||
window.__calls = []; window.__delay = 0; window.__fail = false; window.__ignoreFacing = false;
|
||||
window.__streams = []; window.__frameAt = 0;
|
||||
navigator.mediaDevices.getUserMedia = async constraints => {
|
||||
const facing = constraints.video.facingMode.ideal;
|
||||
window.__calls.push(facing);
|
||||
const fail = window.__fail, delay = window.__delay;
|
||||
window.__fail = false;
|
||||
await new Promise(r => setTimeout(r, delay));
|
||||
if (fail) throw new Error('fixture camera unavailable');
|
||||
const canvas = document.createElement('canvas'); canvas.width = 640; canvas.height = 480;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = facing === 'user' ? '#316a94' : '#d18e48'; ctx.fillRect(0, 0, 640, 480);
|
||||
const stream = canvas.captureStream(20), track = stream.getVideoTracks()[0];
|
||||
const settings = track.getSettings.bind(track);
|
||||
track.getSettings = () => ({ ...settings(), facingMode: window.__ignoreFacing ? 'user' : facing });
|
||||
const timer = setInterval(() => ctx.fillRect(0, 0, 640, 480), 40);
|
||||
const stop = track.stop.bind(track); track.stop = () => { clearInterval(timer); stop(); };
|
||||
window.__streams.push(stream);
|
||||
return stream;
|
||||
};
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelector('#cam-video').addEventListener('loadeddata', () => { window.__frameAt = performance.now(); });
|
||||
});
|
||||
});
|
||||
await page.goto((process.env.H5_BASE_URL || 'http://127.0.0.1:8080/') + '?mock=1');
|
||||
await page.evaluate(() => window.__cameraTest.enter());
|
||||
const live = () => page.waitForFunction(() => window.__cameraTest.state().live);
|
||||
await live();
|
||||
for (const delay of [0, 1000]) {
|
||||
await page.evaluate(delay => { window.__delay = delay; document.querySelector('.cam-flip').click(); }, delay);
|
||||
assert(await page.locator('#cam-frozen').isVisible(), 'frozen preview during switching');
|
||||
const count = await page.evaluate(() => window.__calls.length);
|
||||
await page.evaluate(() => document.querySelector('.cam-flip').click());
|
||||
assert.equal(await page.evaluate(() => window.__calls.length), count, 'rapid click ignored');
|
||||
await live();
|
||||
const extra = await page.evaluate(() => performance.now() - window.__frameAt);
|
||||
assert(extra < 500, `first-frame to visible latency ${extra}ms, no fixed 900ms wait`);
|
||||
assert(!(await page.locator('#cam-frozen').isVisible()));
|
||||
console.log('flip delay', delay, 'first-frame to visible', Math.round(extra), 'ms');
|
||||
}
|
||||
const before = await page.evaluate(() => window.__cameraTest.state().facing);
|
||||
await page.evaluate(() => { window.__delay = 0; window.__fail = true; document.querySelector('.cam-flip').click(); });
|
||||
await live();
|
||||
assert.equal(await page.evaluate(() => window.__cameraTest.state().facing), before, 'failed flip restores camera');
|
||||
assert.equal(await page.evaluate(() => window.__cameraTest.state().fallback), false);
|
||||
await page.evaluate(() => { window.__ignoreFacing = true; document.querySelector('.cam-flip').click(); });
|
||||
await live();
|
||||
assert.equal(await page.evaluate(() => window.__cameraTest.state().facing), before, 'ignored facing is detected/restored');
|
||||
await page.evaluate(() => {
|
||||
window.__ignoreFacing = false; window.__shortCameraTimeout = true; window.__delay = 600;
|
||||
document.querySelector('.cam-flip').click();
|
||||
window.__delay = 0; // restoration succeeds while the original request is still pending
|
||||
});
|
||||
await live();
|
||||
assert.equal(await page.evaluate(() => window.__cameraTest.state().facing), before, 'timeout restores original camera');
|
||||
await page.waitForTimeout(700);
|
||||
assert(await page.evaluate(() => window.__streams.slice(0, -2).every(s => s.getTracks().every(t => t.readyState === 'ended'))));
|
||||
await page.evaluate(() => { window.__shortCameraTimeout = false; });
|
||||
const count = await page.evaluate(() => window.__calls.length);
|
||||
await page.evaluate(() => { window.__cameraTest.capture(true); document.querySelector('.cam-flip').click(); window.__cameraTest.capture(false); });
|
||||
assert.equal(await page.evaluate(() => window.__calls.length), count, 'capture blocks flip');
|
||||
// Pending inference from the old camera must neither update UI nor fork loops.
|
||||
await page.evaluate(() => {
|
||||
window.__detectCalls = 0; window.__concurrent = 0; window.__maxConcurrent = 0;
|
||||
window.faceapi = {
|
||||
nets: { tinyFaceDetector: { loadFromUri: () => Promise.resolve() }, faceLandmark68Net: { loadFromUri: () => Promise.resolve() } },
|
||||
TinyFaceDetectorOptions: function() {},
|
||||
detectAllFaces: function() { return { withFaceLandmarks: function() {
|
||||
window.__detectCalls++; window.__concurrent++;
|
||||
window.__maxConcurrent = Math.max(window.__maxConcurrent, window.__concurrent);
|
||||
return new Promise(resolve => { window.__resolveDetect = () => { window.__concurrent--; resolve([]); }; });
|
||||
} }; }
|
||||
};
|
||||
window.__cameraTest.startDetection();
|
||||
});
|
||||
await page.waitForFunction(() => window.__detectCalls === 1);
|
||||
await page.evaluate(() => { window.__ignoreFacing = false; document.querySelector('.cam-flip').click(); });
|
||||
await live();
|
||||
await page.waitForTimeout(600);
|
||||
assert.equal(await page.evaluate(() => window.__detectCalls), 1, 'do not overlap old inference');
|
||||
await page.evaluate(() => window.__resolveDetect());
|
||||
await page.waitForFunction(() => window.__detectCalls === 2);
|
||||
await page.evaluate(() => window.__resolveDetect());
|
||||
await page.waitForFunction(() => window.__detectCalls === 3);
|
||||
assert.equal(await page.evaluate(() => window.__maxConcurrent), 1, 'single detection loop');
|
||||
await page.evaluate(() => { window.__delay = 600; document.querySelector('.cam-flip').click(); window.__cameraTest.stop(); });
|
||||
await page.waitForTimeout(900);
|
||||
assert.equal(await page.evaluate(() => window.__cameraTest.state().live), false, 'late stream cannot revive stopped camera');
|
||||
assert(await page.evaluate(() => window.__streams.every(s => s.getTracks().every(t => t.readyState === 'ended'))), 'all streams released');
|
||||
assert.equal(await page.evaluate(() => document.querySelector('#cam-frozen').width), 0, 'frozen face released');
|
||||
assert.deepEqual(errors, []);
|
||||
console.log('PASS camera flip: first frame, frozen preview, rapid clicks, recovery/timeout, capture lock, stale inference, cleanup');
|
||||
} finally { await browser.close(); }
|
||||
@@ -31,6 +31,7 @@ assert(!check(null).ok); assert(!check(face(), { count: 2 }).ok);
|
||||
for (const yaw of [-14, 14]) assert(check(face(yaw)).ok, 'former strict front limit now passes');
|
||||
for (const yaw of [-16, 16, -45, 45]) {
|
||||
assert(!check(face(yaw)).ok);
|
||||
assert.equal(check(face(yaw)).reason, 'angle');
|
||||
assert.equal(check(face(yaw)).hint, '请正对屏幕', 'front correction must not ask for a side view before capture');
|
||||
assert.equal(check(face(yaw), { mirrored: true }).hint, '请正对屏幕');
|
||||
}
|
||||
@@ -49,6 +50,11 @@ assert(check(face(-45), { target: 'right', mirrored: true }).ok);
|
||||
assert(check(face(-45), { target: 'right', ignoreDirection: true }).ok);
|
||||
assert(!check(face(25), { 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');
|
||||
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 [70, 230]) assert(check(face(), { luma }).ok);
|
||||
assert(check(face(), { offsetX: .15 }).ok);
|
||||
assert(!check(face(), { offsetX: .17 }).ok);
|
||||
|
||||
@@ -36,8 +36,8 @@ assert.equal(JSON.stringify(geometry.window.CameraGeometry), original);
|
||||
for (const value of [null, '', 'M--Z', 'M1,2L1,2Z', 'M0,0L0,0L0,0Z', 'M0,0Q1,2 3,4Z']) assert.equal(context.smoothCameraOutline(value), value);
|
||||
for (const nav of [null, {}, { vibrate: false }, { vibrate: () => false }, { vibrate: () => { throw Error('denied'); } }]) assert.equal(context.captureHaptic(nav), false);
|
||||
let pattern;
|
||||
assert(context.captureHaptic({ vibrate: p => { pattern = Array.from(p); return true; } }));
|
||||
assert.deepEqual(pattern, [70, 40, 70]);
|
||||
assert(context.captureHaptic({ vibrate: p => { pattern = p; return true; } }));
|
||||
assert.equal(pattern, 30);
|
||||
|
||||
// Exercise the actual readiness state machine; only camera/pose inputs and clock
|
||||
// are stubs; the relaxed shared hold policy now requires 450ms/2 frames.
|
||||
@@ -47,7 +47,8 @@ const state = {
|
||||
faceOffsetX: () => 0, CENTER_TOL: .18, CAM_STEPS: [{ target: 'front' }], camStep: 0,
|
||||
HeadPose: { check: () => ({ ok: true }) }, sideYawFlip: false, faceLuma: () => 100,
|
||||
LUMA_DARK: 65, LUMA_BRIGHT: 235, window: {}, setMask: () => {}, setHint: () => {},
|
||||
okSince: 0, readyHapticAt: 0, OK_HOLD_MS: 450, Date: { now: () => now },
|
||||
okSince: 0, OK_HOLD_MS: 450, Date: { now: () => now },
|
||||
shutterLock: false, capturing: false, camBusy: false, camLive: true, currentScreen: 's-cam', document: { hidden: false },
|
||||
holdState: null, CaptureQuality: {
|
||||
evaluate: d => ({ ok: d.detection.box.height >= 100, hint: '保持不动', observation: { yaw: 0, pitch: 0, center_x: .5, center_y: .5, ratio: .5 } }),
|
||||
hold: require('../js/capture-quality').hold
|
||||
@@ -56,13 +57,13 @@ const state = {
|
||||
};
|
||||
vm.runInNewContext(extract('camera-haptic-pure') + source.slice(source.indexOf(' function onDetect(d)'), source.indexOf(' /* ─── 拍照流程')), state);
|
||||
const face = { detection: { box: { height: 250 } }, landmarks: { positions: [] } };
|
||||
state.onDetect(face); assert.equal(buzzes, 1); assert.equal(captures, 0);
|
||||
now += 200; state.onDetect(face); assert.equal(buzzes, 1); assert.equal(captures, 0);
|
||||
now += 250; state.onDetect(face); assert.equal(buzzes, 1); assert.equal(captures, 1);
|
||||
state.onDetect(face); assert.equal(buzzes, 0); assert.equal(captures, 0);
|
||||
now += 200; state.onDetect(face); assert.equal(buzzes, 0); assert.equal(captures, 0);
|
||||
now += 250; state.onDetect(face); assert.equal(buzzes, 0); assert.equal(captures, 1);
|
||||
now += 100; state.onDetect({ ...face, detection: { box: { height: 20 } } });
|
||||
now += 100; state.onDetect(face); assert.equal(buzzes, 1, 'jitter must not repeatedly buzz');
|
||||
now += 100; state.onDetect(face); assert.equal(buzzes, 0, 'good angles must not buzz');
|
||||
now += 2100; state.onDetect({ ...face, detection: { box: { height: 20 } } });
|
||||
state.onDetect(face); assert.equal(buzzes, 2);
|
||||
state.onDetect(face); assert.equal(buzzes, 0);
|
||||
state.navigator.vibrate = () => { throw Error('blocked'); };
|
||||
now += 300; state.onDetect(face);
|
||||
now += 150; state.onDetect(face); assert.equal(captures, 2, 'vibration failure must not prevent capture');
|
||||
@@ -74,8 +75,39 @@ for (const manual of [true, false]) {
|
||||
$: () => ({ classList: { add: () => {} } }), setTimeout: (_, ms) => scheduled.push(ms) };
|
||||
vm.runInNewContext(extract('camera-haptic-pure') + source.slice(source.indexOf(' function doCapture()'), source.indexOf(' // 三张拍完:按视角重排')), shutter);
|
||||
shutter.doCapture();
|
||||
assert.equal(manualBuzzes, manual ? 1 : 0, 'auto must not double buzz; manual must retain its cue');
|
||||
assert.equal(manualBuzzes, 0, 'neither auto nor manual shutter should buzz');
|
||||
assert(shutter.shutterLock && shutter.capturing);
|
||||
assert.deepEqual(scheduled, [300], 'blocked haptic must still schedule camera capture');
|
||||
}
|
||||
console.log('PASS: three bounded rounded contours; haptic safety, pre-capture cue, jitter throttling; relaxed 450ms/2-frame hold');
|
||||
// Drive actual onDetect with angle-only failures, not UI hint string matching.
|
||||
const events = [];
|
||||
state.navigator.vibrate = p => { events.push({ p, at: now }); return true; };
|
||||
state.CaptureQuality.evaluate = () => ({ ok: false, reason: 'angle', hint: '请正对屏幕' });
|
||||
state.onDetect(face);
|
||||
now += 999; state.onDetect(face); assert.equal(events.length, 0);
|
||||
now += 1; state.onDetect(face); assert.equal(events.length, 1); assert.equal(events[0].p, 30);
|
||||
for (let i = 0; i < 3; i++) { now += 1000; state.onDetect(face); }
|
||||
assert.equal(events.length, 1, 'no repeat before 4 seconds');
|
||||
now += 1000; state.onDetect(face); assert.equal(events.length, 2);
|
||||
state.onDetect(null); assert.equal(events.at(-1).p, 0, 'missing face cancels vibration immediately');
|
||||
const positives = () => events.filter(e => e.p > 0).length;
|
||||
state.onDetect(face); now += 999; state.onDetect(face); assert.equal(positives(), 2, 'new continuous interval required');
|
||||
state.CaptureQuality.evaluate = () => ({ ok: false, reason: 'quality', hint: '请靠近一点' });
|
||||
now += 10000; state.onDetect(face); assert.equal(positives(), 2, 'distance/lighting failure does not buzz');
|
||||
state.CaptureQuality.evaluate = () => ({ ok: false, reason: 'angle', hint: '请正对屏幕' });
|
||||
state.onDetect(face); now += 1000; state.onDetect(face); assert.equal(positives(), 3);
|
||||
state.CaptureQuality.evaluate = () => ({ ok: false, reason: 'quality', hint: '请到光线充足处' });
|
||||
state.onDetect(face); assert.equal(events.at(-1).p, 0, 'valid angle cancels even when lighting fails');
|
||||
for (const [key, value] of [['camBusy', true], ['shutterLock', true], ['capturing', true], ['camLive', false], ['currentScreen', 's-report']]) {
|
||||
const previous = state[key]; state[key] = value;
|
||||
state.CaptureQuality.evaluate = () => ({ ok: false, reason: 'angle' });
|
||||
state.onDetect(face); now += 5000; state.onDetect(face);
|
||||
assert.equal(positives(), 3, key + ' blocks reminders'); state[key] = previous;
|
||||
}
|
||||
state.document.hidden = true; state.onDetect(face); now += 5000; state.onDetect(face); assert.equal(positives(), 3);
|
||||
state.document.hidden = false;
|
||||
for (const nav of [{}, { vibrate: () => false }, { vibrate: () => { throw Error('blocked'); } }]) {
|
||||
state.navigator = nav; state.resetAngleHaptic();
|
||||
state.onDetect(face); now += 1000; assert.doesNotThrow(() => state.onDetect(face));
|
||||
}
|
||||
console.log('PASS: rounded contours; 1s invalid-angle cue, 4s throttle, cancellation/lifecycle, unsupported safety; unchanged 450ms/2-frame capture hold');
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// Read-only fixture: no accounts/reports created.
|
||||
import assert from 'node:assert/strict';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding:'utf8' }).trim() + '/playwright/index.mjs').href);
|
||||
const browser = await chromium.launch();
|
||||
try {
|
||||
const page = await browser.newPage({ viewport:{ width:375, height:726 } });
|
||||
await page.route('**/api/**', route => {
|
||||
assert.equal(route.request().method(), 'GET');
|
||||
return route.fulfill({ json:{ ok:true, user:null } });
|
||||
});
|
||||
await page.goto(process.env.H5_BASE_URL || 'http://127.0.0.1:8080/');
|
||||
await page.click('[data-action="guest-experience"]');
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
const brand = page.locator('#s-splash .splash-slogan');
|
||||
assert.match(await brand.getAttribute('aria-label'), /^1'me/);
|
||||
await page.waitForFunction(() => [...document.querySelectorAll('#s-splash .splash-slogan img')].every(i => i.complete && i.naturalWidth));
|
||||
assert.equal(await page.locator('#s-splash .splash-slogan-brand').isVisible(), true);
|
||||
assert.equal(await page.locator('#s-splash .splash-slogan-brand').textContent(), "1'me");
|
||||
assert.equal(await page.locator('#s-splash .splash-slogan-brand').evaluate(el => getComputedStyle(el).fontWeight), '400');
|
||||
const button = page.locator('#s-splash [data-action="open-privacy"]');
|
||||
await button.focus();
|
||||
const style = await button.evaluate(el => {
|
||||
const s = getComputedStyle(el);
|
||||
return { border:s.borderTopWidth, outline:s.outlineStyle, shadow:s.boxShadow, background:s.backgroundColor };
|
||||
});
|
||||
assert.deepEqual(style, { border:'0px', outline:'none', shadow:'none', background:'rgb(245, 240, 120)' });
|
||||
await button.evaluate(el => el.blur());
|
||||
await page.waitForTimeout(400); // finish the entry screen fade before the screenshot
|
||||
await page.screenshot({ path:'/tmp/juzi-splash-brand-20261013.png' });
|
||||
await button.click();
|
||||
await page.waitForSelector('#p-privacy.show');
|
||||
console.log('PASS splash brand, borderless focused start button, yellow fill, privacy entry');
|
||||
} finally { await browser.close(); }
|
||||
Reference in New Issue
Block a user