fix(h5): center score stack, lock profile scroll and gate turn guidance
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
# 报告布局与跟手交互调整
|
||||
|
||||
> 综合评分布局已按后续要求调整为标题和分数整体在圈内居中;年龄标签与数字间距为 25px。最新实现与验证见《评分圈内居中与正脸语音修复-2026-10-12.md》。本文件其余跟手交互、JPG 优化与历史发布记录保留。
|
||||
|
||||
## 本次范围
|
||||
|
||||
1. 综合评分:标题移到黄色圆环外正上方,与「生物可视年龄」同高;分数数字本身水平、垂直居中,「分」独立放在数字右侧,不把分数挤偏。
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# 评分圈内居中、基础信息页固定与正脸语音修复
|
||||
|
||||
## 范围
|
||||
|
||||
1. 报告:综合评分标题和分数在圆环内上下排列,作为一个整体垂直居中,标题与数字分别水平居中。数字右侧保留「分」。年龄标题到年龄数字的边界距离固定为 25 CSS px,不随屏宽比例增大;原显示文案不更名。
|
||||
2. 基础信息:页面禁止纵向滚动、滚动链及下拉回弹,肤质轮播也不再把纵向手势交给页面。正常高度保留原间距;短视口只压缩纵向留白及肤质插画的可用高度,控件、说明、提交按钮完整可见,不以截掉下半页实现禁滚。出生年月、性别、省市的独立滚轮弹层仍支持纵向滑动。触摸防回弹监听只挂在基础信息页,不拦截弹层或报告页。
|
||||
3. 语音:根因在 `HeadPose.check` 的正脸分支——正脸角度偏向一侧时,用「请向左/右转头」提示回正,和第二步侧脸指令混淆。现在正脸角度不合格统一提示「请正对屏幕」,不播侧脸转头指令;`setHint` 还按当前步骤和正脸照片是否已保存做防护。下一步语音必须有上一张已校验保存的照片,既有单通道队列保证「拍摄完成」先播完、再播侧脸引导。侧脸阶段的方向提示不变。
|
||||
|
||||
不更改拍照角度/距离/光线/稳定时长阈值,不改检测算法、报告保存、推荐、微信配置或数据。现有白色人头扫描页和三组报告跟手交互不变。未追踪的 `new_feedback/` 不纳入提交。
|
||||
|
||||
## 验证
|
||||
|
||||
本地 8080 服务运行后:
|
||||
|
||||
```bash
|
||||
H5_ARTIFACT_DIR=/tmp/juzi-profile-voice-fix-20261012 node h5/test/report-touch-layout.mjs
|
||||
H5_PROFILE_FONTS=1 H5_ARTIFACT_DIR=/tmp/juzi-profile-voice-fix-20261012 node h5/test/profile-interaction.mjs
|
||||
node h5/test/capture-quality-browser.mjs
|
||||
node h5/test/customer-feedback-second.js
|
||||
node h5/test/customer-feedback-fourth.js
|
||||
node h5/test/customer-feedback-fourth.mjs
|
||||
node h5/test/customer-feedback-third.mjs
|
||||
node h5/test/scan-placeholder.mjs
|
||||
node h5/test/login-performance.mjs
|
||||
node h5/test/report-persistence.mjs
|
||||
node h5/test/recommendation.js
|
||||
node h5/test/regression.js
|
||||
```
|
||||
|
||||
- 五种屏宽下检查 0/75/82/100/缺失分数,标题加分数整体居中、均在圆内,年龄间距精确为 25px,报告的纵向滚动与三组横滑保留。
|
||||
- 基础信息在 320×568、375×667、375×726、390×664、414×896、480×726、375×440 下不滚动且控件完整;真实触摸纵拖及鼠标滚轮不移动页面,之后横滑仍正常。用真实纵向触摸确认出生年月滚轮可改变选择,验证取消不保存以及账号隔离/重载记忆。
|
||||
- 用真实本地人脸模型和已有照片组成合成相机视频:先提供侧脸作为不合格正脸,停留第一步并实际播「请正对屏幕」,无「转头」/下一步/完成音;再提供合格正脸,最终 JPEG 校验通过后自动保存并播「拍摄完成」,实际音频播完再开始第二步引导,随后完成三视图检测流程。
|
||||
- 语音测试记录真实 MP3 解码后的 WebAudio 起播时间及当时屏幕步骤,不把语音文件预加载误认为起播。浏览器、无音频支持、真机系统音量/权限等行为仍以设备实际能力为准。
|
||||
- 浏览器测试拦截模拟账号和分析响应或使用隔离数据服务,不写线上报告、不调用付费接口。
|
||||
|
||||
## 发布
|
||||
|
||||
基线 `2a49ebd`,目标 `ubuntu@1me.xajuzi.com:/home/ubuntu/juzi`、`juzi-h5.service`。沿用 Git bundle 快进同步,不代用户推送 origin,不改服务器配置和历史报告。发布后经公网执行布局、基础信息和真实自动拍照/语音测试,核对资源哈希、服务健康及匿名访问保护。
|
||||
@@ -4,8 +4,8 @@
|
||||
"2": "app.js",
|
||||
"3": "skin-score.js",
|
||||
"4": "customer-feedback-second.js",
|
||||
"5": "n",
|
||||
"6": "f",
|
||||
"5": "t",
|
||||
"6": "n",
|
||||
"7": "test/profile-store.js",
|
||||
"8": "admin.js",
|
||||
"9": "__awaiter",
|
||||
@@ -20,7 +20,7 @@
|
||||
"18": "1'me肌肤状态检测 H5",
|
||||
"19": "customer-feedback-fourth.js",
|
||||
"20": "build_fixtures.py",
|
||||
"21": "AgeGenderNet",
|
||||
"21": "disposeUnusedWeightTensors",
|
||||
"22": ".loadFromDisk",
|
||||
"23": "facepp-skinanalyze(旷视皮肤分析专业版调用技能)",
|
||||
"24": "巨子测肤 · 1'me 语音播报内容设计文档 V1.0",
|
||||
@@ -42,7 +42,7 @@
|
||||
"40": "第四轮反馈修改与验收",
|
||||
"41": "SsdMobilenetv1",
|
||||
"42": "layer_at",
|
||||
"43": "height",
|
||||
"43": "TinyYolov2Base",
|
||||
"44": "start-local.sh",
|
||||
"45": "PredictAllAgeAndGenderWithFaceAlignmentTask",
|
||||
"46": "PredictAllFaceExpressionsWithFaceAlignmentTask",
|
||||
@@ -63,21 +63,21 @@
|
||||
"61": "报告布局与跟手交互调整",
|
||||
"62": "提交前 Review(2026-10-11)",
|
||||
"63": "e2e-browser.mjs",
|
||||
"64": "start",
|
||||
"64": "play",
|
||||
"65": "微信登录接入与公网部署说明",
|
||||
"66": "normalize",
|
||||
"67": "ui-20261009.mjs",
|
||||
"68": "analyze-timeout.js",
|
||||
"69": "misans/README.md",
|
||||
"70": "scan-placeholder.mjs",
|
||||
"71": ".predictAgeAndGender",
|
||||
"71": "age",
|
||||
"72": "build_report_copy.py",
|
||||
"73": "camera-geometry.js",
|
||||
"74": "report-copy.js",
|
||||
"75": "prepare_h5_assets.py",
|
||||
"76": "onPhotoCaptured",
|
||||
"76": "readPhotoFile",
|
||||
"77": ".runAndExtendWithFaceDetections",
|
||||
"78": ".toString",
|
||||
"78": "Point",
|
||||
"79": "登录反馈与加载优化(2026-10-12)",
|
||||
"80": "线上微信登录部署记录",
|
||||
"81": "wechat-production.mjs",
|
||||
@@ -100,7 +100,7 @@
|
||||
"98": "PredictSingleFaceExpressionsWithFaceAlignmentTask",
|
||||
"99": "DetectSingleFaceLandmarksTask",
|
||||
"100": "ComputeSingleFaceDescriptorTask",
|
||||
"101": "PredictSingleFaceExpressionsTask",
|
||||
"101": "k",
|
||||
"102": "Box",
|
||||
"103": "第四轮反馈/傅艳NEW与项思炼的会话 2026年10月12日.md",
|
||||
"104": "export_fourth_feedback_assets.py",
|
||||
@@ -111,9 +111,22 @@
|
||||
"109": "submitPhotos",
|
||||
"110": "doCapture",
|
||||
"111": "i",
|
||||
"112": "showScreen",
|
||||
"112": "vizCompose",
|
||||
"113": "swipe-track.js",
|
||||
"114": "optimize_delivery_images.py",
|
||||
"115": "FaceProcessor",
|
||||
"116": "convLayer",
|
||||
"117": "startCamera",
|
||||
"118": "AgeGenderNet",
|
||||
"119": "FaceLandmarks",
|
||||
"120": ".forwardInput",
|
||||
"121": "browser",
|
||||
"122": ".forwardInput",
|
||||
"123": "评分圈内居中、基础信息页固定与正脸语音修复",
|
||||
"124": "TinyYolov2",
|
||||
"125": "报告等待页实拍背景-2026-10-12.md"
|
||||
"125": "报告等待页实拍背景-2026-10-12.md",
|
||||
"126": "FaceExpressionNet",
|
||||
"127": "FaceLandmark68NetBase",
|
||||
"128": "drawFaceExpressions",
|
||||
"129": "ComputeAllFaceDescriptorsTask"
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"0": "350e88c1423ab124", "1": "cb6ea50bd4e4a5ca", "2": "3427a6e8a9c7cb3e", "3": "1f737ca8edb978f2", "4": "56bf12952b0de42b", "5": "171f3b1f99ef2776", "6": "f074a081a584c171", "7": "417f570ec3320aca", "8": "a689fcb62e7754af", "9": "ff49d722f6484b4e", "10": "2b037735e1f5b0f6", "11": "4a490abd5e503318", "12": "aeca274694e158d2", "13": "7e3b558a587a4ebe", "14": "bf75bfc99d4f9e34", "15": "3866142b66f8bfdd", "16": "95add6a975c0cb72", "17": "e042d7fe7351bcb9", "18": "f93d5d85e671fcee", "19": "39abb1eef19df707", "20": "aecf2fce44ba49c5", "21": "0c9d5cdd4092b114", "22": "f125f2a02294efd7", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "ab23d3f368091829", "27": "eab427ce9a5219e9", "28": "d5bc4d13da321cc8", "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": "2f0c86fdda7fa4a4", "42": "916170b9f5d7346b", "43": "3b9b3f4698579e2e", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "e594ecaa8e7d1538", "48": "cf2f2424bc00436f", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "68c52c80f38f8b2c", "56": "9ee2ad45dc27851d", "57": "f2a40b5f1b29f669", "58": "0c0cbd63a7b39bc6", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "4b39db1973f82bfe", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "a01bccd7f7c24c78", "65": "bdee3c4a37df52f8", "66": "2a59363e5caa0628", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "be0c8310abda2467", "71": "ecb8093dbcce277d", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "50a4503d0a778ed2", "77": "429cd62f9137881c", "78": "71f300fdfae6e5a6", "79": "86f177bf0a7dd07c", "80": "315325cde1352e98", "81": "37c559741825be74", "82": "200f25fbad7af8fb", "83": "1284897282eb47a3", "84": "2fb17901aa87e0e7", "85": "4bc370be8afd7db1", "86": "386c6fcc7be918e0", "87": "e5fa7717989eda80", "88": "8b5ab5232cef3457", "89": "9e3363b80eeceeea", "90": "dd1c246e19a2d032", "91": "83b0fd3b14363e4d", "92": "fced244d93677d90", "93": "8bd61518dd267641", "94": "713d04996c4627df", "95": "59c31f858c7b6970", "96": "4195478d5c528371", "97": "01f68dfc34a29d32", "98": "8c471c7baeaaca54", "99": "23f1d406f6c779ae", "100": "7ac475ea9bc280f8", "101": "cb5ac3401283c378", "102": "04e5ac36860f09d1", "103": "6ffbe07b521a061b", "104": "e114a7aebaaca2f1", "105": "0567be4dc8048b9c", "106": "8bc72c179793e1c2", "107": "d9c195d9ccdb137e", "108": "e0fc0a42e33bda91", "109": "2c27e5c32bc10822", "110": "b7f77cb02f68832b", "111": "f707b3512f2738ed", "112": "71cbe5098f4c1b6f", "113": "0d99f86b9e41c4bb", "114": "b9d7c266afa84437", "124": "c96276581a8c98d4", "125": "e4f29216745ec758"}
|
||||
{"0": "7a1176758119a6e8", "1": "cb6ea50bd4e4a5ca", "2": "fd2eb9185c61f1fe", "3": "1f737ca8edb978f2", "4": "56bf12952b0de42b", "5": "e8ddef85a868abc9", "6": "2c4c92dd6e627244", "7": "03b56b13a08b5f83", "8": "a689fcb62e7754af", "9": "3370a263a4641111", "10": "2b037735e1f5b0f6", "11": "4a490abd5e503318", "12": "aeca274694e158d2", "13": "7e3b558a587a4ebe", "14": "bf75bfc99d4f9e34", "15": "523e783b24871b14", "16": "95add6a975c0cb72", "17": "e042d7fe7351bcb9", "18": "f93d5d85e671fcee", "19": "39abb1eef19df707", "20": "aecf2fce44ba49c5", "21": "a4ff643a50f0c3df", "22": "00dab8704da757dc", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "10c2b0fcd8bec2c1", "27": "eab427ce9a5219e9", "28": "4d97202be5bb16ac", "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": "5d43ff2eaff3e484", "42": "916170b9f5d7346b", "43": "04c475f726d2508f", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "e594ecaa8e7d1538", "48": "cf2f2424bc00436f", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "68c52c80f38f8b2c", "56": "9ee2ad45dc27851d", "57": "f2a40b5f1b29f669", "58": "504ce5bc46e6237e", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "5209ce2a267bf33b", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "0adfe3c7d77dcbe1", "65": "bdee3c4a37df52f8", "66": "f05da4095366f6d4", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "be0c8310abda2467", "71": "b234c5e4ac7f632d", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "4e91cfdf55362604", "77": "2ee521497bb8506a", "78": "aa921a06e1bd1a1d", "79": "86f177bf0a7dd07c", "80": "315325cde1352e98", "81": "37c559741825be74", "82": "200f25fbad7af8fb", "83": "1284897282eb47a3", "84": "2fb17901aa87e0e7", "85": "4bc370be8afd7db1", "86": "386c6fcc7be918e0", "87": "e5fa7717989eda80", "88": "8b5ab5232cef3457", "89": "9e3363b80eeceeea", "90": "dd1c246e19a2d032", "91": "83b0fd3b14363e4d", "92": "fced244d93677d90", "93": "8bd61518dd267641", "94": "713d04996c4627df", "95": "59c31f858c7b6970", "96": "4195478d5c528371", "97": "01f68dfc34a29d32", "98": "8c471c7baeaaca54", "99": "23f1d406f6c779ae", "100": "7ac475ea9bc280f8", "101": "f065b001b69290e9", "102": "074575463a8da45a", "103": "6ffbe07b521a061b", "104": "e114a7aebaaca2f1", "105": "af50c66d16c81bba", "106": "8bc72c179793e1c2", "107": "d9c195d9ccdb137e", "108": "e0fc0a42e33bda91", "109": "4afb1715f62865b1", "110": "45259f492e4f2efb", "111": "b9ba0a5c51b155b1", "112": "a005abe41943c7cd", "113": "0d99f86b9e41c4bb", "114": "b9d7c266afa84437", "115": "57344ed1fd5c260f", "116": "0dda68823cc5a1d7", "117": "02c78dd73c398bfa", "118": "14fdf4e4c496519f", "119": "a64c13723656b663", "120": "5fec2bbca3634aec", "121": "ce0e68dab611f022", "122": "15f58f7494ac2b87", "123": "b091e3eb82b456eb", "124": "29790cf710874dd0", "125": "e4f29216745ec758", "126": "e858d75e995c92e5", "127": "e2c998de34fdd08d", "128": "a0bc88eb14b70170", "129": "3dd68cce72af5314"}
|
||||
+123
-86
@@ -1,16 +1,16 @@
|
||||
# Graph Report - juzi (2026-10-12)
|
||||
|
||||
## Corpus Check
|
||||
- 104 files · ~4,904,196 words
|
||||
- 105 files · ~4,905,053 words
|
||||
- Verdict: corpus is large enough that graph structure adds value.
|
||||
|
||||
## Summary
|
||||
- 1444 nodes · 2892 edges · 117 communities (92 shown, 25 thin omitted)
|
||||
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 209 edges (avg confidence: 0.57)
|
||||
- 1452 nodes · 2901 edges · 130 communities (98 shown, 32 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: `63c9e065`
|
||||
- Built from commit: `2a49ebda`
|
||||
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
||||
- Run `graphify update .` after code changes (no API cost).
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
- app.js
|
||||
- skin-score.js
|
||||
- customer-feedback-second.js
|
||||
- t
|
||||
- n
|
||||
- f
|
||||
- test/profile-store.js
|
||||
- admin.js
|
||||
- __awaiter
|
||||
@@ -36,7 +36,7 @@
|
||||
- 1'me肌肤状态检测 H5
|
||||
- customer-feedback-fourth.js
|
||||
- build_fixtures.py
|
||||
- AgeGenderNet
|
||||
- disposeUnusedWeightTensors
|
||||
- .loadFromDisk
|
||||
- facepp-skinanalyze(旷视皮肤分析专业版调用技能)
|
||||
- 巨子测肤 · 1'me 语音播报内容设计文档 V1.0
|
||||
@@ -56,7 +56,7 @@
|
||||
- 第四轮反馈修改与验收
|
||||
- SsdMobilenetv1
|
||||
- layer_at
|
||||
- height
|
||||
- TinyYolov2Base
|
||||
- start-local.sh
|
||||
- PredictAllAgeAndGenderWithFaceAlignmentTask
|
||||
- PredictAllFaceExpressionsWithFaceAlignmentTask
|
||||
@@ -74,18 +74,18 @@
|
||||
- 报告布局与跟手交互调整
|
||||
- 提交前 Review(2026-10-11)
|
||||
- e2e-browser.mjs
|
||||
- start
|
||||
- play
|
||||
- 微信登录接入与公网部署说明
|
||||
- normalize
|
||||
- ui-20261009.mjs
|
||||
- analyze-timeout.js
|
||||
- misans/README.md
|
||||
- scan-placeholder.mjs
|
||||
- .predictAgeAndGender
|
||||
- age
|
||||
- build_report_copy.py
|
||||
- onPhotoCaptured
|
||||
- readPhotoFile
|
||||
- .runAndExtendWithFaceDetections
|
||||
- .toString
|
||||
- Point
|
||||
- 登录反馈与加载优化(2026-10-12)
|
||||
- 线上微信登录部署记录
|
||||
- wechat-production.mjs
|
||||
@@ -107,7 +107,7 @@
|
||||
- PredictSingleFaceExpressionsWithFaceAlignmentTask
|
||||
- DetectSingleFaceLandmarksTask
|
||||
- ComputeSingleFaceDescriptorTask
|
||||
- PredictSingleFaceExpressionsTask
|
||||
- k
|
||||
- Box
|
||||
- 第四轮反馈/傅艳NEW与项思炼的会话 2026年10月12日.md
|
||||
- 第三轮反馈20261012/傅艳NEW与项思炼的会话 2026年10月12日.md
|
||||
@@ -116,9 +116,22 @@
|
||||
- submitPhotos
|
||||
- doCapture
|
||||
- i
|
||||
- showScreen
|
||||
- vizCompose
|
||||
- FaceProcessor
|
||||
- convLayer
|
||||
- startCamera
|
||||
- AgeGenderNet
|
||||
- FaceLandmarks
|
||||
- .forwardInput
|
||||
- browser
|
||||
- .forwardInput
|
||||
- 评分圈内居中、基础信息页固定与正脸语音修复
|
||||
- TinyYolov2
|
||||
- 报告等待页实拍背景-2026-10-12.md
|
||||
- FaceExpressionNet
|
||||
- FaceLandmark68NetBase
|
||||
- drawFaceExpressions
|
||||
- ComputeAllFaceDescriptorsTask
|
||||
|
||||
## God Nodes (most connected - your core abstractions)
|
||||
1. `__awaiter()` - 59 edges
|
||||
@@ -135,31 +148,31 @@
|
||||
## Surprising Connections (you probably didn't know these)
|
||||
- `actualLines()` --indirect_call--> `el()` [INFERRED]
|
||||
h5/test/customer-feedback-third.mjs → h5/js/admin.js
|
||||
- `browser()` --indirect_call--> `key()` [INFERRED]
|
||||
h5/js/account-client.js → h5/js/profile-store.js
|
||||
- `init()` --indirect_call--> `key()` [INFERRED]
|
||||
h5/js/account-client.js → h5/js/profile-store.js
|
||||
- `loadRecords()` --indirect_call--> `it()` [INFERRED]
|
||||
h5/js/admin.js → h5/vendor/faceapi/face-api.js
|
||||
- `bind()` --indirect_call--> `a()` [INFERRED]
|
||||
h5/js/admin.js → h5/vendor/faceapi/face-api.js
|
||||
- `checkStill()` --indirect_call--> `d()` [INFERRED]
|
||||
h5/js/app.js → h5/vendor/faceapi/face-api.js
|
||||
- `detectFrame()` --indirect_call--> `d()` [INFERRED]
|
||||
h5/js/app.js → h5/vendor/faceapi/face-api.js
|
||||
|
||||
## Import Cycles
|
||||
- None detected.
|
||||
|
||||
## Communities (117 total, 25 thin omitted)
|
||||
## Communities (130 total, 32 thin omitted)
|
||||
|
||||
### Community 0 - "face-api.js"
|
||||
Cohesion: 0.02
|
||||
Nodes (33): bgrToRgbTensor(), cn(), createBrowserEnv(), createFileSystem(), createNodejsEnv(), Dimensions(), dr(), en() (+25 more)
|
||||
Nodes (46): bgrToRgbTensor(), cn(), computeReshapedDimensions(), createBrowserEnv(), createFaceDetectionNet(), createFileSystem(), createNodejsEnv(), createSsdMobilenetv1() (+38 more)
|
||||
|
||||
### Community 1 - "线上闭环测试(不含真实微信授权)"
|
||||
Cohesion: 0.25
|
||||
Nodes (7): 发现并修复, 可复现命令, 已通过的检查, 线上闭环测试(不含真实微信授权), 结论, 话术及仓库同步, 验证边界
|
||||
|
||||
### Community 2 - "app.js"
|
||||
Cohesion: 0.11
|
||||
Nodes (22): cancelSkinDrag(), esc(), goSkin(), openHistory(), renderHistoryList(), renderReport(), renderViz(), restartScanCycle() (+14 more)
|
||||
Cohesion: 0.13
|
||||
Nodes (22): cancelSkinDrag(), esc(), goSkin(), loadAppFont(), loginAllowed(), loginBrowser(), loginSucceeded(), loginWechat() (+14 more)
|
||||
|
||||
### Community 3 - "skin-score.js"
|
||||
Cohesion: 0.12
|
||||
@@ -169,25 +182,25 @@ Nodes (45): acneGrade(), buildViewModel(), calcAcne(), calcContour(), calcSens()
|
||||
Cohesion: 0.17
|
||||
Nodes (10): assert, context, face, fs, geometry, original, path, source (+2 more)
|
||||
|
||||
### Community 5 - "n"
|
||||
Cohesion: 0.12
|
||||
Nodes (37): ae(), be(), bf(), bi(), ce(), de(), e(), Ef() (+29 more)
|
||||
### Community 5 - "t"
|
||||
Cohesion: 0.11
|
||||
Nodes (17): bi(), Ef(), er(), FaceMatch(), ia(), Ie(), j(), Kt() (+9 more)
|
||||
|
||||
### Community 6 - "f"
|
||||
Cohesion: 0.21
|
||||
Nodes (23): a(), cf(), d(), drawContour(), dt(), ea(), f(), g() (+15 more)
|
||||
### Community 6 - "n"
|
||||
Cohesion: 0.12
|
||||
Nodes (48): a(), ae(), be(), bf(), ce(), cf(), d(), da() (+40 more)
|
||||
|
||||
### Community 7 - "test/profile-store.js"
|
||||
Cohesion: 0.12
|
||||
Nodes (21): browser(), init(), read(), rememberKind(), request(), clean(), create(), key() (+13 more)
|
||||
Cohesion: 0.14
|
||||
Nodes (13): assert, data, empty, now, other, Profile, quota, regions (+5 more)
|
||||
|
||||
### Community 8 - "admin.js"
|
||||
Cohesion: 0.20
|
||||
Nodes (29): ageText(), api(), bind(), captureQualityHtml(), closeDetail(), collectFilters(), dimsHtml(), doRetry() (+21 more)
|
||||
|
||||
### Community 9 - "__awaiter"
|
||||
Cohesion: 0.13
|
||||
Nodes (14): __awaiter(), bufferToImage(), extendWithFaceExpressions(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult(), FaceRecognitionNet(), fetchImage(), fetchJson() (+6 more)
|
||||
Cohesion: 0.12
|
||||
Nodes (16): __awaiter(), extendWithAge(), extendWithFaceDescriptor(), extendWithFaceExpressions(), extendWithGender(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult(), FaceRecognitionNet() (+8 more)
|
||||
|
||||
### Community 10 - "wechat-browser.mjs"
|
||||
Cohesion: 0.22
|
||||
@@ -206,8 +219,8 @@ Cohesion: 0.12
|
||||
Nodes (15): files, analyze-fail, analyzing, countdown-321, hint-far, hint-front, hint-hold, hint-near (+7 more)
|
||||
|
||||
### Community 15 - "extractFaces"
|
||||
Cohesion: 0.16
|
||||
Nodes (20): awaitMediaLoaded(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractFaceTensors(), extractImagePatches(), getContext2dOrThrow(), getEnv() (+12 more)
|
||||
Cohesion: 0.15
|
||||
Nodes (23): height(), awaitMediaLoaded(), bufferToImage(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractFaceTensors(), extractImagePatches() (+15 more)
|
||||
|
||||
### Community 16 - "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范"
|
||||
Cohesion: 0.15
|
||||
@@ -229,13 +242,13 @@ Nodes (37): evaluate(), finite(), assertPoints(), check(), clamp(), dist(), esti
|
||||
Cohesion: 0.40
|
||||
Nodes (9): build_case1(), build_case2(), build_truth(), col(), dump(), main(), parse_value(), set_path() (+1 more)
|
||||
|
||||
### Community 21 - "AgeGenderNet"
|
||||
Cohesion: 0.05
|
||||
Nodes (28): AgeGenderNet(), disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParams$1(), extractParamsFromWeigthMap() (+20 more)
|
||||
### Community 21 - "disposeUnusedWeightTensors"
|
||||
Cohesion: 0.09
|
||||
Nodes (25): disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$1(), extractParamsFromWeigthMap$2() (+17 more)
|
||||
|
||||
### Community 22 - ".loadFromDisk"
|
||||
Cohesion: 0.24
|
||||
Nodes (5): ComposableTask(), getModelUris(), Gf(), loadWeightMap(), TinyFaceDetector()
|
||||
Cohesion: 0.29
|
||||
Nodes (3): ComposableTask(), getModelUris(), TinyFaceDetector()
|
||||
|
||||
### Community 23 - "facepp-skinanalyze(旷视皮肤分析专业版调用技能)"
|
||||
Cohesion: 0.20
|
||||
@@ -251,15 +264,15 @@ Nodes (16): _arrayLikeToArray(), _arrayWithHoles(), _arrayWithoutHoles(), _class
|
||||
|
||||
### Community 26 - "NeuralNetwork"
|
||||
Cohesion: 0.07
|
||||
Nodes (25): createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory(), extractFCParamsFactory() (+17 more)
|
||||
Nodes (24): createFaceRecognitionNet(), createMtcnn(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory(), extractFCParamsFactory(), extractorsFactory(), extractorsFactory$1() (+16 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.08
|
||||
Nodes (36): ao(), Bo(), C(), cd(), cl(), co(), Do(), eo() (+28 more)
|
||||
|
||||
### Community 29 - "face-api.js 本地化依赖(h5/vendor/faceapi/)"
|
||||
Cohesion: 0.29
|
||||
@@ -291,16 +304,12 @@ Nodes (8): PSD 来源, 前端参数及限制, 后台校准留痕, 已确认并
|
||||
|
||||
### Community 41 - "SsdMobilenetv1"
|
||||
Cohesion: 0.18
|
||||
Nodes (10): boxPredictionLayer(), decodeBoxesLayer(), depthwiseConvLayer(), getCenterCoordinatesAndSizesLayer(), getStridesForLayerIdx(), mobileNetV1(), outputLayer(), pointwiseConvLayer() (+2 more)
|
||||
Nodes (9): decodeBoxesLayer(), depthwiseConvLayer(), getCenterCoordinatesAndSizesLayer(), getStridesForLayerIdx(), mobileNetV1(), outputLayer(), pointwiseConvLayer(), predictionLayer() (+1 more)
|
||||
|
||||
### Community 42 - "layer_at"
|
||||
Cohesion: 0.18
|
||||
Nodes (11): list, bounds(), layer_at(), main(), save_image(), composite(), main(), sha256() (+3 more)
|
||||
|
||||
### Community 43 - "height"
|
||||
Cohesion: 0.27
|
||||
Nodes (5): height(), FaceLandmark68NetBase(), sigmoid(), TinyYolov2Base(), validateConfig()
|
||||
|
||||
### Community 44 - "start-local.sh"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): HOST, PORT, start-local.sh script
|
||||
@@ -330,12 +339,12 @@ Cohesion: 0.60
|
||||
Nodes (5): diffCtx(), loadJSON(), main(), okNum(), okStr()
|
||||
|
||||
### Community 58 - ".forwardInput"
|
||||
Cohesion: 0.14
|
||||
Nodes (15): computeReshapedDimensions(), extractBoundingBoxes(), getSizesForScale(), iou(), isEven(), nonMaxSuppression(), nonMaxSuppression$1(), normalize$1() (+7 more)
|
||||
Cohesion: 0.19
|
||||
Nodes (10): extendWithFaceLandmarks(), extractBoundingBoxes(), iou(), NetInput(), nonMaxSuppression(), nonMaxSuppression$1(), scale(), stage1() (+2 more)
|
||||
|
||||
### Community 61 - "报告布局与跟手交互调整"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 报告布局与跟手交互调整, 本次范围, 资源体积, 验证
|
||||
Cohesion: 0.33
|
||||
Nodes (5): 报告布局与跟手交互调整, 本次范围, 线上验收(2026-10-12), 资源体积, 验证
|
||||
|
||||
### Community 62 - "提交前 Review(2026-10-11)"
|
||||
Cohesion: 0.40
|
||||
@@ -345,17 +354,17 @@ Nodes (4): 仍需公网/业务确认, 已发现并修复, 提交前 Review(202
|
||||
Cohesion: 0.29
|
||||
Nodes (4): EXPECTED, fails, pageErrors, pwRoot
|
||||
|
||||
### Community 64 - "start"
|
||||
Cohesion: 0.29
|
||||
Nodes (7): decode(), drain(), load(), preload(), start(), stopCur(), unlock()
|
||||
### Community 64 - "play"
|
||||
Cohesion: 0.22
|
||||
Nodes (9): busyWithFlow(), decode(), drain(), load(), play(), preload(), start(), stopCur() (+1 more)
|
||||
|
||||
### Community 65 - "微信登录接入与公网部署说明"
|
||||
Cohesion: 0.29
|
||||
Nodes (7): 公网服务器配置, 公网真机验收清单, 反向代理与后台保护, 已完成的链路, 微信登录接入与公网部署说明, 数据、迁移与备份, 本机开发环境限制(不代表线上状态)
|
||||
|
||||
### Community 66 - "normalize"
|
||||
Cohesion: 0.08
|
||||
Nodes (28): conv(), conv$1(), convDown(), convLayer(), convLayer$1(), convNoRelu(), convWithBatchNorm(), denseBlock3() (+20 more)
|
||||
Cohesion: 0.20
|
||||
Nodes (9): conv(), denseBlock3(), denseBlock4(), depthwiseSeparableConv(), FaceFeatureExtractor(), mainBlock(), normalize(), reductionBlock() (+1 more)
|
||||
|
||||
### Community 68 - "analyze-timeout.js"
|
||||
Cohesion: 0.20
|
||||
@@ -365,21 +374,21 @@ Nodes (7): assert, end, fs, path, source, start, vm
|
||||
Cohesion: 0.29
|
||||
Nodes (3): errors, samples, unexpectedWrites
|
||||
|
||||
### Community 71 - ".predictAgeAndGender"
|
||||
Cohesion: 0.29
|
||||
Nodes (5): age(), extendWithAge(), extendWithGender(), PredictAllAgeAndGenderTask(), PredictSingleAgeAndGenderTask()
|
||||
### Community 71 - "age"
|
||||
Cohesion: 0.43
|
||||
Nodes (4): age(), clean(), create(), key()
|
||||
|
||||
### Community 76 - "onPhotoCaptured"
|
||||
Cohesion: 0.18
|
||||
Nodes (15): busyWithFlow(), captureHaptic(), checkStill(), dropHint(), faceLuma(), faceOffsetX(), handleCamVisible(), hideCamStatus() (+7 more)
|
||||
### Community 76 - "readPhotoFile"
|
||||
Cohesion: 0.40
|
||||
Nodes (5): checkStill(), faceLuma(), hideCamStatus(), readPhotoFile(), showCamStatus()
|
||||
|
||||
### Community 77 - ".runAndExtendWithFaceDetections"
|
||||
Cohesion: 0.15
|
||||
Nodes (11): DetectAllFacesTask(), DrawBox(), drawDetections(), drawFaceExpressions(), DrawFaceLandmarks(), extendWithFaceDetection(), FaceExpressions(), isWithFaceDetection() (+3 more)
|
||||
Cohesion: 0.18
|
||||
Nodes (8): DetectAllFacesTask(), DrawBox(), drawDetections(), DrawFaceLandmarks(), extendWithFaceDetection(), isWithFaceDetection(), isWithFaceLandmarks(), resizeResults()
|
||||
|
||||
### Community 78 - ".toString"
|
||||
Cohesion: 0.12
|
||||
Nodes (11): FaceLandmarks5(), FaceMatch(), getCenterPoint(), la(), Point(), pr(), pt(), vt() (+3 more)
|
||||
### Community 78 - "Point"
|
||||
Cohesion: 0.22
|
||||
Nodes (5): FaceLandmarks5(), getCenterPoint(), Point(), pt(), vt()
|
||||
|
||||
### Community 79 - "登录反馈与加载优化(2026-10-12)"
|
||||
Cohesion: 0.33
|
||||
@@ -437,9 +446,9 @@ Nodes (5): 原稿与导出可追溯性, 已完成, 本地验证, 生产部署及
|
||||
Cohesion: 0.50
|
||||
Nodes (3): artifacts, errors, samples
|
||||
|
||||
### Community 102 - "Box"
|
||||
Cohesion: 0.11
|
||||
Nodes (8): Box(), ComputeAllFaceDescriptorsTask(), extendWithFaceDescriptor(), extendWithFaceLandmarks(), FaceLandmarks(), isDimensions(), minBbox(), ObjectDetection()
|
||||
### Community 101 - "k"
|
||||
Cohesion: 0.17
|
||||
Nodes (13): b(), ba(), ca(), fa(), k(), ne(), pa(), ua() (+5 more)
|
||||
|
||||
### Community 107 - "基础信息记忆与肤质拖动"
|
||||
Cohesion: 0.33
|
||||
@@ -450,35 +459,63 @@ Cohesion: 0.40
|
||||
Nodes (3): dragBy(), h5, touch()
|
||||
|
||||
### Community 109 - "submitPhotos"
|
||||
Cohesion: 0.17
|
||||
Nodes (16): applyMirror(), bindBackdropClose(), closeLoginSuccess(), closeOverlay(), effMirror(), getCamStream(), handleCamHidden(), leaveCamera() (+8 more)
|
||||
Cohesion: 0.24
|
||||
Nodes (12): bindBackdropClose(), closeLoginSuccess(), closeOverlay(), handleCamHidden(), leaveCamera(), photosByView(), popHistorySilently(), requestAnalyze() (+4 more)
|
||||
|
||||
### Community 110 - "doCapture"
|
||||
Cohesion: 0.17
|
||||
Nodes (19): armNudge(), beginDetect(), captureFromVideo(), detectFrame(), detTick(), doCapture(), enableFallback(), hideNudge() (+11 more)
|
||||
Cohesion: 0.15
|
||||
Nodes (24): armNudge(), beginDetect(), captureFromVideo(), captureHaptic(), captureStageHint(), detectFrame(), detTick(), doCapture() (+16 more)
|
||||
|
||||
### Community 111 - "i"
|
||||
Cohesion: 0.07
|
||||
Nodes (36): b(), ba(), bn(), ca(), cl(), da(), dn(), ee() (+28 more)
|
||||
Cohesion: 0.11
|
||||
Nodes (20): bn(), dn(), ee(), et(), fn(), i(), je(), ln() (+12 more)
|
||||
|
||||
### Community 112 - "showScreen"
|
||||
Cohesion: 0.22
|
||||
Nodes (11): loadAppFont(), loginAllowed(), loginBrowser(), loginSucceeded(), loginWechat(), openOverlay(), prepareScanArtwork(), renderAccountStatus() (+3 more)
|
||||
### Community 112 - "vizCompose"
|
||||
Cohesion: 0.18
|
||||
Nodes (11): renderReport(), renderViz(), setDim(), vizCollectMarks(), vizCompose(), vizFaceCrop(), vizLoadImage(), vizMapURL() (+3 more)
|
||||
|
||||
### Community 116 - "convLayer"
|
||||
Cohesion: 0.36
|
||||
Nodes (9): boxPredictionLayer(), convLayer(), fullyConnectedLayer(), ONet(), PNet(), prelu(), qh(), RNet() (+1 more)
|
||||
|
||||
### Community 117 - "startCamera"
|
||||
Cohesion: 0.29
|
||||
Nodes (8): applyMirror(), effMirror(), getCamStream(), handleCamVisible(), onCamTrackEnded(), renderCamStep(), startCamera(), waitVideoStable()
|
||||
|
||||
### Community 120 - ".forwardInput"
|
||||
Cohesion: 0.48
|
||||
Nodes (6): conv$1(), convDown(), convLayer$1(), convNoRelu(), residual(), residualDown()
|
||||
|
||||
### Community 121 - "browser"
|
||||
Cohesion: 0.80
|
||||
Nodes (5): browser(), init(), read(), rememberKind(), request()
|
||||
|
||||
### Community 122 - ".forwardInput"
|
||||
Cohesion: 0.47
|
||||
Nodes (3): convWithBatchNorm(), depthwiseSeparableConv$1(), leaky()
|
||||
|
||||
### Community 123 - "评分圈内居中、基础信息页固定与正脸语音修复"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 发布, 范围, 评分圈内居中、基础信息页固定与正脸语音修复, 验证
|
||||
|
||||
### Community 128 - "drawFaceExpressions"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): drawFaceExpressions(), FaceExpressions(), isWithFaceExpressions()
|
||||
|
||||
## Knowledge Gaps
|
||||
- **314 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+309 more)
|
||||
- **318 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+313 more)
|
||||
These have ≤1 connection - possible missing edges or undocumented components.
|
||||
- **25 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
- **32 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 `v()` connect `f` to `report-ui.js`, `face-api.js`, `app.js`, `C`?**
|
||||
_High betweenness centrality (0.046) - this node is a cross-community bridge._
|
||||
- **Why does `height()` connect `height` to `app.js`, `customer-feedback.js`, `scan-placeholder.mjs`, `extractFaces`, `showScreen`, `.forwardInput`?**
|
||||
- **Why does `height()` connect `extractFaces` to `app.js`, `customer-feedback.js`, `scan-placeholder.mjs`, `TinyYolov2Base`, `profile-interaction.mjs`, `.forwardInput`, `FaceLandmark68NetBase`?**
|
||||
_High betweenness centrality (0.062) - this node is a cross-community bridge._
|
||||
- **Why does `v()` connect `n` to `vizCompose`, `face-api.js`, `report-ui.js`, `C`?**
|
||||
_High betweenness centrality (0.045) - this node is a cross-community bridge._
|
||||
- **Why does `k()` connect `i` to `face-api.js`, `app.js`, `n`, `f`, `lib/accounts.js`, `C`?**
|
||||
_High betweenness centrality (0.040) - this node is a cross-community bridge._
|
||||
- **Why does `k()` connect `k` to `face-api.js`, `t`, `n`, `lib/accounts.js`, `vizCompose`, `C`?**
|
||||
_High betweenness centrality (0.039) - 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?**
|
||||
@@ -486,4 +523,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?**
|
||||
_314 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
_318 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
File diff suppressed because one or more lines are too long
+1007
-816
File diff suppressed because it is too large
Load Diff
+23
-18
@@ -40,13 +40,13 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/app.js": {
|
||||
"mtime": 1791814713.4789917,
|
||||
"ast_hash": "daf6b9c597517206ce9477d07bcdccf7",
|
||||
"mtime": 1791819697.652488,
|
||||
"ast_hash": "e0e2ef463d0c51e4c3b5148fb32d3a11",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/head-pose.js": {
|
||||
"mtime": 1791809137.1269257,
|
||||
"ast_hash": "6c71a4db97fa9b08ec4f6ec5ca54d31b",
|
||||
"mtime": 1791819697.652488,
|
||||
"ast_hash": "303298be5be4af0e25b609a83795a810",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/skin-score.js": {
|
||||
@@ -140,8 +140,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/index.html": {
|
||||
"mtime": 1791814981.3589842,
|
||||
"ast_hash": "eda8371282c16b85f04dc741b4655440",
|
||||
"mtime": 1791819697.652488,
|
||||
"ast_hash": "45a0411d9100e3874ab1fb0460d0c16d",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/vendor/faceapi/README.md": {
|
||||
@@ -1675,8 +1675,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/customer-feedback-second.js": {
|
||||
"mtime": 1791809137.130926,
|
||||
"ast_hash": "d494abe8a486bb753d671f9c4c8b533f",
|
||||
"mtime": 1791819755.8747528,
|
||||
"ast_hash": "03ea3689f3c86e78a1f5bd46966f4145",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/customer-feedback-second.mjs": {
|
||||
@@ -1815,13 +1815,13 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/capture-quality-browser.mjs": {
|
||||
"mtime": 1791815038.0590124,
|
||||
"ast_hash": "7cc5f0db42934862b36ef947a86f55bf",
|
||||
"mtime": 1791819803.979456,
|
||||
"ast_hash": "a4164c6ce0209e18a0b22a93745c90cf",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/customer-feedback-fourth.js": {
|
||||
"mtime": 1791809197.5909035,
|
||||
"ast_hash": "818c6b58ba8b4771275457bb13604f1f",
|
||||
"mtime": 1791819755.8706622,
|
||||
"ast_hash": "92a53685400808b24315f85ed851566d",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/customer-feedback-fourth.mjs": {
|
||||
@@ -1895,8 +1895,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/profile-interaction.mjs": {
|
||||
"mtime": 1791810527.8429196,
|
||||
"ast_hash": "7a370108ec4c922412fa0f278ab59f6a",
|
||||
"mtime": 1791820047.336818,
|
||||
"ast_hash": "2efa96cee5dcf3671644b1b82f890ee3",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/profile-store.js": {
|
||||
@@ -1925,8 +1925,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/report-touch-layout.mjs": {
|
||||
"mtime": 1791816131.9590158,
|
||||
"ast_hash": "8d8d8f76a50da655e7d577660c14579e",
|
||||
"mtime": 1791819897.8782563,
|
||||
"ast_hash": "b7cd460af266717e0b1b4a27d2bdac83",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/scan-placeholder.mjs": {
|
||||
@@ -1955,8 +1955,13 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u62a5\u544a\u5e03\u5c40\u4e0e\u8ddf\u624b\u4ea4\u4e92\u8c03\u6574-2026-10-12.md": {
|
||||
"mtime": 1791815756.2269309,
|
||||
"ast_hash": "97b6337ca9d46e521225c2d312e2efb7",
|
||||
"mtime": 1791819897.8782563,
|
||||
"ast_hash": "a98504fb21d73751f558491aa6f1cfe5",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u8bc4\u5206\u5708\u5185\u5c45\u4e2d\u4e0e\u6b63\u8138\u8bed\u97f3\u4fee\u590d-2026-10-12.md": {
|
||||
"mtime": 1791819897.8782563,
|
||||
"ast_hash": "2879d154d155aad6b87effd5a4c499e1",
|
||||
"semantic_hash": ""
|
||||
}
|
||||
}
|
||||
+40
-7
@@ -107,19 +107,52 @@
|
||||
#dims-note .dims-note-l1 { font-size: 12px; line-height: 1.5; }
|
||||
#dims-note .dims-note-l2 { font-size: 11px; line-height: 1.5; margin-top: 3px; }
|
||||
|
||||
/* Labels share a top baseline outside the dial; center the score digits, not
|
||||
digits plus unit, inside the yellow ring. */
|
||||
.sum-score, .sum-age { flex-direction: column; align-items: center; justify-content: flex-start; gap: calc(6 * var(--u)); }
|
||||
.sum-age { align-self: stretch; }
|
||||
.sum-score > .stat-label, .sum-age > .stat-label { font-size: 12px; line-height: 1.5; }
|
||||
/* Center the complete score stack inside the ring, with the age label exactly
|
||||
25 CSS px above its number. Units do not offset the score digits. */
|
||||
.sum-score, .sum-age { flex-direction: column; align-items: center; justify-content: center; gap: 0; }
|
||||
.sum-age { align-self: stretch; gap: 25px; }
|
||||
.ring-center { gap: 6px; }
|
||||
.ring-center .stat-label, .sum-age > .stat-label { margin: 0; font-size: 12px; line-height: 1.5; color: var(--ink); }
|
||||
.score-dial, .age-dial { position: relative; width: calc(108 * var(--u)); height: calc(108 * var(--u)); }
|
||||
.age-dial { display: flex; align-items: center; justify-content: center; }
|
||||
.age-dial .stat-age { margin-top: 0; }
|
||||
.age-dial { height: auto; display: flex; align-items: center; justify-content: center; }
|
||||
.age-dial .stat-age { margin-top: 0; display: flex; align-items: baseline; }
|
||||
.age-dial .stat-age b { display: block; line-height: 1; }
|
||||
#stat-score { position: relative; }
|
||||
#stat-score b { display: block; letter-spacing: 0; }
|
||||
#stat-score i { position: absolute; left: 100%; bottom: 3px; margin-left: 2px; }
|
||||
.card-care .card-heading { text-align: left; }
|
||||
|
||||
/* The profile is a fixed page, not a scroll container. Keep normal-height PSD
|
||||
spacing; shrink only vertical whitespace/photo height on shorter viewports.
|
||||
Wheel pickers are mounted outside this screen and retain their own gestures. */
|
||||
#s-profile {
|
||||
overflow: hidden; overscroll-behavior: none; touch-action: none;
|
||||
--profile-space: min(var(--u), max(.3px, calc((100vh - 420px) / 306)));
|
||||
}
|
||||
@supports (height: 100dvh) {
|
||||
#s-profile { --profile-space: min(var(--u), max(.3px, calc((100dvh - 420px) / 306))); }
|
||||
}
|
||||
#s-profile .profile-body {
|
||||
height: 100%; min-height: 0; display: flex; flex-direction: column;
|
||||
padding-top: max(env(safe-area-inset-top), calc(34 * var(--profile-space)));
|
||||
padding-bottom: max(env(safe-area-inset-bottom), calc(40 * var(--profile-space)));
|
||||
}
|
||||
#s-profile .profile-body > * { flex-shrink: 0; }
|
||||
#s-profile .page-title { margin-bottom: calc(22 * var(--profile-space)); }
|
||||
#s-profile .field { margin-bottom: calc(19 * var(--profile-space)); }
|
||||
#s-profile .field-label { margin-bottom: calc(9 * var(--profile-space)); }
|
||||
#s-profile .profile-skin-field { flex: 0 1 auto; min-height: 0; display: flex; flex-direction: column; }
|
||||
#s-profile .skin-card { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; padding-top: calc(21 * var(--profile-space)); padding-bottom: calc(24 * var(--profile-space)); }
|
||||
#s-profile .skin-axis { flex-shrink: 0; margin-bottom: calc(18 * var(--profile-space)); }
|
||||
#s-profile .skin-stage { flex: 1 1 auto; min-height: 0; display: flex; }
|
||||
#s-profile .skin-carousel { flex: 1; min-width: 0; min-height: 0; display: flex; touch-action: none; }
|
||||
#s-profile .skin-track { flex: 0 0 100%; min-width: 0; min-height: 0; }
|
||||
#s-profile .skin-item img { flex-shrink: 1; min-height: 0; object-fit: contain; }
|
||||
#s-profile .skin-name { flex-shrink: 0; margin-top: calc(16 * var(--profile-space)); }
|
||||
#s-profile .skin-desc { flex-shrink: 0; width: calc(150 * var(--u)); min-height: calc(2 * 1.55 * 11.5px); }
|
||||
#s-profile .skin-dots { flex-shrink: 0; margin-top: calc(22 * var(--profile-space)); }
|
||||
#s-profile .btn-submit { margin-top: calc(25 * var(--profile-space)); }
|
||||
|
||||
.detail-viewport { overflow: hidden; touch-action: pan-y; transition: height .32s ease; }
|
||||
.detail-viewport.dragging { transition: none; }
|
||||
.detail-panels { display: flex; align-items: flex-start; transition: transform .32s cubic-bezier(.22,.61,.36,1); }
|
||||
|
||||
+2
-2
@@ -80,7 +80,7 @@
|
||||
<input type="hidden" id="f-gender">
|
||||
<input type="hidden" id="f-region">
|
||||
|
||||
<div class="field">
|
||||
<div class="field profile-skin-field">
|
||||
<label class="field-label">您的日常肤质</label>
|
||||
<div class="skin-card">
|
||||
<div class="skin-axis">
|
||||
@@ -173,13 +173,13 @@
|
||||
<!-- 总分卡:左环形综合评分 / 右肌肤年龄 + 与实际年龄对比 -->
|
||||
<div class="card card-summary">
|
||||
<div class="sum-score">
|
||||
<span class="stat-label">综合评分</span>
|
||||
<div class="score-dial">
|
||||
<svg class="ring" viewBox="0 0 120 120" aria-hidden="true">
|
||||
<circle class="ring-bg" cx="60" cy="60" r="52"/>
|
||||
<circle class="ring-fg" id="ring-fg" cx="60" cy="60" r="52"/>
|
||||
</svg>
|
||||
<div class="ring-center">
|
||||
<span class="stat-label">综合评分</span>
|
||||
<span class="stat-value" id="stat-score"><b>--</b><i>分</i></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+13
-1
@@ -304,6 +304,11 @@
|
||||
|
||||
var carousel = $('#skin-carousel');
|
||||
skinSwipe = SwipeTrack.create(carousel, track, { count: SKINS.length, onSelect: goSkin });
|
||||
// Limit the iOS rubber-band fallback to the fixed profile itself. The mobile
|
||||
// wheel pickers live under body, so their vertical scrolling is untouched.
|
||||
$('#s-profile').addEventListener('touchmove', function (e) {
|
||||
if (e.cancelable) e.preventDefault();
|
||||
}, { passive: false });
|
||||
function cancelSkinDrag() { skinSwipe.cancel(); }
|
||||
carousel.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') { e.preventDefault(); goSkin(skinIndex + (e.key === 'ArrowRight' ? 1 : -1)); }
|
||||
@@ -766,7 +771,14 @@
|
||||
m.classList.toggle('no-face', cls === 'no-face');
|
||||
m.classList.toggle('ok', cls === 'ok');
|
||||
}
|
||||
/* camera-hint-stage-pure-begin */
|
||||
function captureStageHint(text, target, hasFrontPhoto) {
|
||||
if ((target === 'front' || !hasFrontPhoto) && (text === '请向左转头' || text === '请向右转头')) return '请正对屏幕';
|
||||
return text;
|
||||
}
|
||||
/* camera-hint-stage-pure-end */
|
||||
function setHint(text, ok, silent) {
|
||||
text = captureStageHint(text, CAM_STEPS[camStep].target, !!photos[0]);
|
||||
var h = $('#cam-hint');
|
||||
if (!text) { h.classList.remove('show', 'ok'); lastHint = ''; voice.dropHint(); return; }
|
||||
if (text !== lastHint) {
|
||||
@@ -949,7 +961,7 @@
|
||||
$('#cam-next').classList.remove('show');
|
||||
applyMirror();
|
||||
resetDetectState();
|
||||
voice.play(VOICE.step[camStep]); // 步骤引导语音
|
||||
if (camStep === 0 || photos[camStep - 1]) voice.play(VOICE.step[camStep]); // 前一步合格照片已保存,才播下一步引导
|
||||
}
|
||||
|
||||
var camSeq = 0; // 流程令牌:退出拍照/开始拍照时递增,使未决的快门/分析回调失效
|
||||
|
||||
+3
-8
@@ -158,8 +158,8 @@
|
||||
/**
|
||||
* check(points, target, mirrored) -> { ok, hint }
|
||||
* target: 'front' | 'left' | 'right'
|
||||
* 'front' : |yaw|<=15 且 |pitch|<=18、|roll|<=15;不 ok 时给方向提示
|
||||
* (UI 反馈 2026-10:动态提示「请向左转头」「请略微抬头」等,取代此前只提示回正)
|
||||
* 'front' : |yaw|<=15 且 |pitch|<=18、|roll|<=15;角度不合格只提示回正,
|
||||
* 不在正脸拍摄完成前播侧脸转头指令。
|
||||
* 'left' : 画面中看到左脸 = 被摄者头向右转(语义 yaw ∈ [-70°,-30°]),|pitch|<=20
|
||||
* 'right' : 画面中看到右脸 = 被摄者头向左转(语义 yaw ∈ [30°,70°]),|pitch|<=20
|
||||
* ok=true 时 hint='保持不动';输入非法(含 estimate 返回 null 的退化特征点)时 ok=false、hint='请正对屏幕'。
|
||||
@@ -183,12 +183,7 @@
|
||||
if (Math.abs(sYaw) <= FRONT_YAW_MAX && Math.abs(pitch) <= FRONT_PITCH_MAX) {
|
||||
return { ok: true, hint: HINT_KEEP, yaw: sYaw };
|
||||
}
|
||||
// 按超出阈值比例更大的一项引导回正:头偏向自己左侧(yaw>0)→请向右转头;抬头过多(pitch>0)→请略微低头
|
||||
var yawOver = Math.abs(sYaw) / FRONT_YAW_MAX, pitchOver = Math.abs(pitch) / FRONT_PITCH_MAX;
|
||||
if (yawOver >= pitchOver) {
|
||||
return { ok: false, hint: sYaw > 0 ? HINT_TURN_RIGHT : HINT_TURN_LEFT, yaw: sYaw };
|
||||
}
|
||||
return { ok: false, hint: pitch > 0 ? HINT_LOWER : HINT_RAISE, yaw: sYaw };
|
||||
return { ok: false, hint: HINT_FRONT, yaw: sYaw };
|
||||
}
|
||||
|
||||
if (target === 'left' || target === 'right') {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { execSync } from 'node:child_process';
|
||||
import { pathToFileURL, fileURLToPath } from 'node:url';
|
||||
const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href);
|
||||
const base = process.env.H5_BASE_URL || 'http://127.0.0.1:8080/';
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const browser = await chromium.launch({ headless: true, args: ['--autoplay-policy=no-user-gesture-required'] });
|
||||
try {
|
||||
const page = await browser.newPage({ viewport: { width: 375, height: 726 } });
|
||||
page.setDefaultTimeout(60000);
|
||||
@@ -19,6 +19,37 @@ try {
|
||||
await page.route('**/sample-' + name, route => route.fulfill({ contentType: 'image/jpeg', body }));
|
||||
}
|
||||
await page.addInitScript(() => {
|
||||
// Record actual decoded audio starts, not preload requests. Include the
|
||||
// visible capture step so an early "turn left" instruction is detectable.
|
||||
const ids = new WeakMap(), buffers = new WeakMap(), originalFetch = window.fetch;
|
||||
window.__voice = [];
|
||||
window.fetch = function(url, options) {
|
||||
const match = String(url).match(/voice\/([a-z0-9-]+)\.mp3/);
|
||||
return originalFetch.call(this, url, options).then(response => {
|
||||
if (match) {
|
||||
const original = response.arrayBuffer.bind(response);
|
||||
response.arrayBuffer = () => original().then(data => { ids.set(data, match[1]); return data; });
|
||||
}
|
||||
return response;
|
||||
});
|
||||
};
|
||||
const OriginalAudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
function AudioRecorder(...args) {
|
||||
const context = new OriginalAudioContext(...args), decode = context.decodeAudioData.bind(context);
|
||||
context.decodeAudioData = (data, ok, error) => decode(data, buffer => { buffers.set(buffer, ids.get(data)); if (ok) ok(buffer); }, error);
|
||||
const create = context.createBufferSource.bind(context);
|
||||
context.createBufferSource = () => {
|
||||
const source = create(), start = source.start.bind(source);
|
||||
source.start = (...args) => {
|
||||
window.__voice.push({ id: buffers.get(source.buffer), at: Date.now(), duration: source.buffer.duration * 1000, step: document.querySelector('#cam-step').textContent });
|
||||
return start(...args);
|
||||
};
|
||||
return source;
|
||||
};
|
||||
return context;
|
||||
}
|
||||
AudioRecorder.prototype = OriginalAudioContext.prototype;
|
||||
window.AudioContext = window.webkitAudioContext = AudioRecorder;
|
||||
const canvas = document.createElement('canvas'); canvas.width = 800; canvas.height = 600;
|
||||
let currentImage, offsetX = 0;
|
||||
function draw() {
|
||||
@@ -39,7 +70,7 @@ try {
|
||||
currentImage = image; offsetX = nextOffset; draw();
|
||||
} };
|
||||
navigator.mediaDevices.getUserMedia = async () => {
|
||||
await window.__cameraFixture.set('C3_180.jpg');
|
||||
await window.__cameraFixture.set('C3_135.jpg'); // invalid front: must not prompt a next-step turn
|
||||
return canvas.captureStream(10);
|
||||
};
|
||||
});
|
||||
@@ -78,6 +109,12 @@ try {
|
||||
};
|
||||
new MutationObserver(() => { window.__hints.push(document.querySelector('#cam-hint').textContent); if (window.__hints.length > 20) window.__hints.shift(); }).observe(document.querySelector('#cam-hint'), { childList: true, subtree: true });
|
||||
});
|
||||
await page.evaluate(() => window.__cameraFixture.set('C3_135.jpg'));
|
||||
await page.waitForFunction(() => window.__evaluations.some(row => row.target === 'front' && !row.ok && row.hint === '请正对屏幕'));
|
||||
await page.waitForFunction(() => window.__voice.some(row => row.id === 'hint-front'));
|
||||
assert((await page.locator('#cam-step').textContent()).includes('1/3'), 'invalid frontal angle never advances');
|
||||
assert.equal(await page.evaluate(() => window.__voice.some(row => /^hint-turn-|^step[23]-|^shot-done$/.test(row.id))), false, 'no turn/next-step/completion audio before a successful front photo');
|
||||
await page.evaluate(() => window.__cameraFixture.set('C3_180.jpg'));
|
||||
try { await page.waitForFunction(() => document.querySelector('#cam-step').textContent.includes('2/3')); }
|
||||
catch (error) {
|
||||
console.log(await page.evaluate(async () => {
|
||||
@@ -88,6 +125,11 @@ try {
|
||||
ratio: window.__dbgRatio, faces: faces.map(d => ({ score: d.detection.score, pose: HeadPose.estimate(d.landmarks.positions, false), box: d.detection.box })), holds: window.__holdSamples, hints: window.__hints };
|
||||
}).then(value => JSON.stringify(value, null, 2)), errors); throw error;
|
||||
}
|
||||
await page.waitForFunction(() => window.__voice.some(row => row.id === 'step2-rightface'));
|
||||
const firstVoice = await page.evaluate(() => window.__voice);
|
||||
const completion = firstVoice.find(row => row.id === 'shot-done'), turn = firstVoice.find(row => row.id === 'step2-rightface');
|
||||
assert(completion && turn && completion.step.includes('1/3') && turn.step.includes('2/3'));
|
||||
assert(turn.at >= completion.at + completion.duration - 100, 'turn instruction waits for captured-front completion speech to finish');
|
||||
await page.waitForFunction(() => window.__evaluations.some(row => row.target === 'right' && !row.ok));
|
||||
assert((await page.locator('#cam-step').textContent()).includes('2/3'), 'front photo cannot pass the side gate');
|
||||
await page.evaluate(() => window.__cameraFixture.set('C3_135.jpg'));
|
||||
@@ -112,5 +154,5 @@ try {
|
||||
assert(body.capture_diagnostics.views.right.yaw >= 30);
|
||||
assert(await page.evaluate(() => document.querySelector('#cam-video').srcObject === null), 'camera released after success');
|
||||
assert.deepEqual(errors, []);
|
||||
console.log('PASS: actual local models + automatic three-view capture + final JPEG revalidation; front/wrong-direction frames blocked locally; all three approved samples recorded; camera released; no paid call or backend writes');
|
||||
console.log('PASS: actual local models + automatic three-view capture + final JPEG revalidation; invalid front stays on step 1 with neutral audio, no early turn; actual decoded shot-done finishes before side guidance; wrong-direction frames blocked; approved samples/camera cleanup; no paid call/backend writes');
|
||||
} finally { await browser.close(); }
|
||||
|
||||
@@ -29,9 +29,15 @@ const check = (d, changes) => Quality.evaluate(d, { ...options, ...changes });
|
||||
assert(check(face()).ok);
|
||||
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]) assert(!check(face(yaw)).ok);
|
||||
for (const yaw of [-16, 16, -45, 45]) {
|
||||
assert(!check(face(yaw)).ok);
|
||||
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, '请正对屏幕');
|
||||
}
|
||||
for (const pitch of [-17, 17]) assert(check(face(0, pitch)).ok);
|
||||
for (const pitch of [-20, 20]) assert(!check(face(0, pitch)).ok);
|
||||
for (const pitch of [-20, 20]) assert.equal(check(face(0, pitch)).hint, '请正对屏幕');
|
||||
assert.equal(check(face(0), { target: 'right' }).hint, '请向左转头', 'side step retains its proper turn instruction');
|
||||
assert.equal(check(face(0), { target: 'left' }).hint, '请向右转头');
|
||||
assert(check(face(0, 0, 12)).ok, 'normal tilt now passes');
|
||||
assert.equal(check(face(0, 0, 16)).hint, '请勿歪头');
|
||||
for (const yaw of [25, 72]) assert(!check(face(yaw), { target: 'right' }).ok);
|
||||
|
||||
@@ -6,7 +6,14 @@ const path = require('node:path');
|
||||
const source = fs.readFileSync(path.join(__dirname, '../js/app.js'), 'utf8');
|
||||
function extract(name) { return source.split('/* ' + name + '-begin */')[1].split('/* ' + name + '-end */')[0]; }
|
||||
const context = {};
|
||||
vm.runInNewContext(extract('camera-outline-pure') + extract('camera-haptic-pure'), context);
|
||||
vm.runInNewContext(extract('camera-outline-pure') + extract('camera-haptic-pure') + extract('camera-hint-stage-pure'), context);
|
||||
for (const hint of ['请向左转头', '请向右转头']) {
|
||||
assert.equal(context.captureStageHint(hint, 'front', false), '请正对屏幕');
|
||||
assert.equal(context.captureStageHint(hint, 'front', true), '请正对屏幕');
|
||||
assert.equal(context.captureStageHint(hint, 'right', false), '请正对屏幕');
|
||||
assert.equal(context.captureStageHint(hint, 'right', true), hint);
|
||||
}
|
||||
assert.equal(context.captureStageHint('请靠近一点', 'front', false), '请靠近一点');
|
||||
const geometry = { window: {} };
|
||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../js/camera-geometry.js'), 'utf8'), geometry);
|
||||
const original = JSON.stringify(geometry.window.CameraGeometry);
|
||||
|
||||
@@ -33,7 +33,7 @@ try {
|
||||
await page.route('**/*', route => {
|
||||
const req = route.request();
|
||||
if (req.method() !== 'GET') { blockedWrites.push(new URL(req.url()).pathname); return route.abort(); }
|
||||
if (/\.woff2?(\?|$)/.test(req.url())) return route.abort();
|
||||
if (!process.env.H5_PROFILE_FONTS && /\.woff2?(\?|$)/.test(req.url())) return route.abort();
|
||||
if (req.url().endsWith('/api/auth/session')) return route.fulfill({ json: { ok: true, user } });
|
||||
return route.continue();
|
||||
});
|
||||
@@ -66,6 +66,7 @@ try {
|
||||
await page.waitForTimeout(250);
|
||||
}
|
||||
await enter();
|
||||
if (process.env.H5_PROFILE_FONTS) await page.evaluate(() => document.fonts.ready);
|
||||
let blank = await values();
|
||||
assert.equal(blank.birth, ''); assert.equal(blank.gender, ''); assert.equal(blank.region, ''); assert.equal(blank.skin, 0);
|
||||
await pick('#pick-birth', [62, 10]); // current year minus 37, November
|
||||
@@ -133,11 +134,56 @@ try {
|
||||
const size = await geometry(); assert(Math.abs(await offset() + 3 * size.width) < 2, 'resize keeps selection aligned at ' + width);
|
||||
assert(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth), 'no horizontal page overflow');
|
||||
}
|
||||
for (const [width, height] of [[320, 568], [375, 667], [375, 726], [390, 664], [414, 896], [480, 726], [375, 440]]) {
|
||||
await page.setViewportSize({ width, height }); await page.waitForTimeout(400);
|
||||
const layout = await page.evaluate(() => {
|
||||
const profile = document.querySelector('#s-profile'), submit = document.querySelector('.btn-submit');
|
||||
const skin = document.querySelector('.skin-card').getBoundingClientRect();
|
||||
const image = document.querySelector('.skin-item img').getBoundingClientRect();
|
||||
const desc = document.querySelector('.skin-desc').getBoundingClientRect();
|
||||
const viewport = document.querySelector('#skin-carousel').getBoundingClientRect();
|
||||
const textBottom = Math.max(...[...document.querySelectorAll('.skin-desc')].map(el => {
|
||||
const range = document.createRange(); range.selectNodeContents(el);
|
||||
return Math.max(range.getBoundingClientRect().bottom, el.getBoundingClientRect().bottom);
|
||||
}));
|
||||
return { overflow: getComputedStyle(profile).overflowY, touch: getComputedStyle(profile).touchAction,
|
||||
client: profile.clientHeight, scroll: profile.scrollHeight, top: profile.scrollTop,
|
||||
submit: submit.getBoundingClientRect().bottom, image: image.height, desc: desc.bottom, skin: skin.bottom, textBottom, viewportBottom: viewport.bottom };
|
||||
});
|
||||
assert.equal(layout.overflow, 'hidden'); assert.equal(layout.touch, 'none');
|
||||
assert(layout.scroll <= layout.client + 1 && layout.submit <= height, 'all profile controls fit without clipping/scroll: ' + JSON.stringify({ width, height, ...layout }));
|
||||
assert(layout.image > 25 && layout.desc <= layout.skin, 'short screens retain skin illustration and description');
|
||||
assert(layout.textBottom <= layout.viewportBottom + 1, 'all five skin descriptions fit inside the clipped carousel: ' + JSON.stringify({ width, height, ...layout }));
|
||||
if (process.env.H5_ARTIFACT_DIR) {
|
||||
fs.mkdirSync(process.env.H5_ARTIFACT_DIR, { recursive: true });
|
||||
await page.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, 'profile-fixed-' + width + '-' + height + '.png') });
|
||||
}
|
||||
const point = await geometry();
|
||||
await touch('touchStart', point.x, point.y);
|
||||
await touch('touchMove', point.x + 2, point.y + 70);
|
||||
await touch('touchEnd', 0, 0);
|
||||
await page.mouse.wheel(0, 300); await page.waitForTimeout(100);
|
||||
assert.equal(await page.locator('#s-profile').evaluate(el => el.scrollTop), 0, 'touch/wheel cannot pull the page down');
|
||||
assert.equal(await page.evaluate(() => document.documentElement.scrollTop), 0);
|
||||
await dragBy(-70); assert.equal((await values()).skin, 4, 'horizontal swipe still works after vertical gesture');
|
||||
await dragBy(70); assert.equal((await values()).skin, 3);
|
||||
}
|
||||
// The screen's scroll lock must not prevent the separate picker from moving.
|
||||
await page.click('#pick-birth'); await page.waitForTimeout(400);
|
||||
assert.equal(await page.locator('.ms-mobile-select.ms-show').evaluate(el => !!el.closest('#s-profile')), false);
|
||||
const wheel = page.locator('.ms-mobile-select.ms-show .ms-wheel').first(), wheelBox = await wheel.boundingBox();
|
||||
const wheelValue = await page.evaluate(() => window.__pickers['#pick-birth'].getCurValue());
|
||||
const wx = wheelBox.x + wheelBox.width / 2, wy = wheelBox.y + wheelBox.height / 2;
|
||||
await touch('touchStart', wx, wy);
|
||||
await touch('touchMove', wx, wy - 40); await touch('touchMove', wx, wy - 80);
|
||||
await touch('touchEnd', 0, 0); await page.waitForTimeout(300);
|
||||
assert.notDeepEqual(await page.evaluate(() => window.__pickers['#pick-birth'].getCurValue()), wheelValue, 'birth wheel still responds to real vertical touch');
|
||||
await page.click('.ms-mobile-select.ms-show .ms-cancel'); await page.waitForTimeout(300);
|
||||
await enter(); assert.equal((await values()).skin, 3, 'skin selection also survives reload');
|
||||
await page.click('[data-action="submit-profile"]');
|
||||
await page.waitForSelector('#p-photoguide.show');
|
||||
assert.deepEqual(errors, []); assert.deepEqual(blockedWrites, [], 'no backend writes or paid requests');
|
||||
console.log('PASS: actual profile defaults/wheels/reload/account isolation; real mobile touch follows finger/snap/flick/edges/cancel/vertical scroll; arrows/dots/keyboard; three responsive widths; submission; no JS errors/backend writes');
|
||||
console.log('PASS: actual profile defaults/wheels/reload/account isolation; real mobile finger-follow/snap/flick/edges/cancel; fixed profile/no vertical movement/controls fit in seven viewport sizes; real birth-wheel vertical touch still works; arrows/dots/keyboard/submission; no JS errors/backend writes');
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
if (child && child.exitCode == null) { const stopped = once(child, 'exit'); child.kill(); await stopped; }
|
||||
|
||||
@@ -33,21 +33,21 @@ try {
|
||||
for (const width of [320, 375, 390, 414, 480]) {
|
||||
await page.setViewportSize({ width, height: 844 });
|
||||
await page.waitForTimeout(400);
|
||||
for (const score of ['0', '75', '100', '--']) {
|
||||
for (const score of ['0', '75', '82', '100', '--']) {
|
||||
await page.locator('#stat-score b').evaluate((el, value) => { el.textContent = value; }, score);
|
||||
const layout = await page.evaluate(() => {
|
||||
const rect = selector => {
|
||||
const r = document.querySelector(selector).getBoundingClientRect();
|
||||
return { x: r.x, y: r.y, width: r.width, height: r.height, bottom: r.bottom };
|
||||
};
|
||||
return { dial: rect('.score-dial'), score: rect('#stat-score b'), title: rect('.sum-score > .stat-label'),
|
||||
ageTitle: rect('.sum-age > .stat-label'), age: rect('.age-dial'),
|
||||
return { dial: rect('.score-dial'), score: rect('#stat-score b'), title: rect('.ring-center .stat-label'),
|
||||
ageTitle: rect('.sum-age > .stat-label'), age: rect('#stat-age b'),
|
||||
careAlign: getComputedStyle(document.querySelector('.card-care .card-heading')).textAlign };
|
||||
});
|
||||
assert(Math.abs(layout.score.x + layout.score.width / 2 - layout.dial.x - layout.dial.width / 2) < 1, 'score horizontally centered: ' + width + '/' + score);
|
||||
assert(Math.abs(layout.score.y + layout.score.height / 2 - layout.dial.y - layout.dial.height / 2) < 1, 'score vertically centered');
|
||||
assert(layout.title.bottom < layout.dial.y && layout.ageTitle.bottom < layout.age.y, 'labels outside above their values');
|
||||
assert(Math.abs(layout.title.y - layout.ageTitle.y) < 1, 'labels share top alignment');
|
||||
assert(Math.abs((layout.title.y + layout.score.bottom) / 2 - layout.dial.y - layout.dial.height / 2) < 1, 'complete label + score stack vertically centered');
|
||||
assert(layout.title.y > layout.dial.y && layout.score.bottom < layout.dial.bottom, 'both label and score stay inside the circle');
|
||||
assert(Math.abs(layout.age.y - layout.ageTitle.bottom - 25) < 1, 'age label-to-number gap is exactly 25 CSS px');
|
||||
assert.equal(layout.careAlign, 'left');
|
||||
}
|
||||
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
||||
@@ -127,5 +127,5 @@ try {
|
||||
await page.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, 'report-summary.png') });
|
||||
}
|
||||
assert.deepEqual(errors, []); assert.deepEqual(writes, []);
|
||||
console.log('PASS: score centered/labels above/care left in five widths; real finger-following three report groups, flick/snap/cancel/vertical/edges/keyboard/zoom/resize/height/ARIA; smaller JPEG hashes/dimensions/MIME; no JS errors/backend writes');
|
||||
console.log('PASS: whole score stack centered inside circle/age gap 25px/care left in five widths; real finger-following three report groups, flick/snap/cancel/vertical/edges/keyboard/zoom/resize/height/ARIA; smaller JPEG hashes/dimensions/MIME; no JS errors/backend writes');
|
||||
} finally { await browser.close(); }
|
||||
|
||||
Reference in New Issue
Block a user