fix(h5): show captured face behind report scan animation

This commit is contained in:
xsl
2026-10-12 21:21:49 +08:00
parent ce6dbc20d4
commit 69dc9962c0
14 changed files with 1443 additions and 878 deletions
@@ -0,0 +1,21 @@
# 报告等待页实拍背景
原扫描页直接展示 PSD 的白色人脸占位。改为在原 PSD 人脸蒙版内叠加本次正脸照片,扫描线置于照片上方;原素材不重绘,框、检测中文字、装饰图标与取消入口不变。
- 首张合格正脸就预解码,三张拍完使用同一张正脸,不使用最后一张侧脸或其他报告照片。
- 与拍摄预览一样居中、等比 `cover` 铺满,不拉伸照片。前置实时拍摄保留当时的预览镜像;后置/相册/系统相机文件不额外镜像。镜像只作用于 CSS,不改变送检 JPEG。
- 已有“一次完整扫描才进成功报告”保持不变;照片延迟解码时重启扫描,错误/取消不等待动画。
- 成功、失败、取消、页面隐藏或重新拍摄时清理等待页图片引用,不残留上一轮人脸。照片不新增缓存/上传/持久化。
- 素材或蒙版加载失败时降级为实拍背景、检测中文字和取消入口;照片解码失败使用深色背景,不露出白色占位。
- 不修改拍摄约束、评分、肌龄、推荐规则或报告保存逻辑。
验证命令(本地服务需运行于 8080):
```bash
node h5/test/scan-photo.mjs
node h5/test/capture-quality-browser.mjs
node h5/test/customer-feedback-third.mjs
node h5/test/customer-feedback-fourth.mjs
```
测试只使用模拟账号、拦截分析响应,不创建线上记录、不调用付费检测。覆盖本次正脸照片、前置镜像与文件不镜像、六种视口、扫描完整循环、成功/失败/取消清理、迟到响应及素材失败降级。
+27 -6
View File
@@ -5,7 +5,7 @@
"3": "skin-score.js",
"4": "customer-feedback-second.js",
"5": "f",
"6": "Box",
"6": "ComputeAllFaceDescriptorsTask",
"7": "test/profile-store.js",
"8": "admin.js",
"9": "__awaiter",
@@ -69,14 +69,14 @@
"67": "ui-20261009.mjs",
"68": "analyze-timeout.js",
"69": "misans/README.md",
"70": ".forwardInput",
"71": ".predictAgeAndGender",
"70": "normalize",
"71": "extractAllFacesAndComputeResults",
"72": "build_report_copy.py",
"73": "camera-geometry.js",
"74": "report-copy.js",
"75": "prepare_h5_assets.py",
"76": ".forwardInput",
"77": "extractFaceTensors",
"76": "doCapture",
"77": ".runAndExtendWithFaceDetections",
"78": "n",
"79": "登录反馈与加载优化(2026-10-12)",
"80": "线上微信登录部署记录",
@@ -107,5 +107,26 @@
"105": "capture-quality-browser.mjs",
"106": "第三轮反馈20261012/傅艳NEW与项思炼的会话 2026年10月12日.md",
"107": "基础信息记忆与肤质拖动",
"108": "profile-interaction.mjs"
"108": "profile-interaction.mjs",
"109": "submitPhotos",
"110": "startCamera",
"111": "e",
"112": "showScreen",
"113": "Point",
"114": "initialize",
"115": "FaceLandmarks68",
"116": ".forwardInput",
"117": "ao",
"118": "k",
"119": ".div",
"120": "restoreProfile",
"121": "TinyFaceFeatureExtractor",
"122": "isValidNumber",
"123": "tt",
"124": "TinyYolov2",
"125": "报告等待页实拍背景-2026-10-12.md",
"126": "FaceDetection",
"127": "ObjectDetection",
"128": "PredictAllFaceExpressionsTask",
"129": "PredictSingleAgeAndGenderTask"
}
+1 -1
View File
@@ -1 +1 @@
{"0": "200bd332d330e79e", "1": "cb6ea50bd4e4a5ca", "2": "86eae3fd1b848c2e", "3": "1f737ca8edb978f2", "4": "56bf12952b0de42b", "5": "06906d9c95022851", "6": "7cd2b329434ded47", "7": "03b56b13a08b5f83", "8": "a689fcb62e7754af", "9": "fce137e5717d2018", "10": "2b037735e1f5b0f6", "11": "4a490abd5e503318", "12": "aeca274694e158d2", "13": "7e3b558a587a4ebe", "14": "bf75bfc99d4f9e34", "15": "2c0eaf406546159b", "16": "95add6a975c0cb72", "17": "e042d7fe7351bcb9", "18": "f93d5d85e671fcee", "19": "39abb1eef19df707", "20": "aecf2fce44ba49c5", "21": "02ac512b5e01b983", "22": "f2045a9940596eb3", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "8367cbe6ea2844bc", "27": "eab427ce9a5219e9", "28": "4f01dfb5cd033c39", "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": "d96493a5f110bd4c", "42": "916170b9f5d7346b", "43": "4eaa3e8b9eed3bf1", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "e594ecaa8e7d1538", "48": "cf2f2424bc00436f", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "af2dbb7480405e14", "56": "9ee2ad45dc27851d", "57": "f2a40b5f1b29f669", "58": "5a45b41665ef1112", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "94ead9feda213ac6", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "ce0e68dab611f022", "65": "bdee3c4a37df52f8", "66": "7798792de7e51a25", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "e39b60658e2c785e", "71": "a2b13160223b5a98", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "880a74c6d255e5cc", "77": "2050a9da67ccfa24", "78": "9daf5044cb78b2d2", "79": "86f177bf0a7dd07c", "80": "315325cde1352e98", "81": "37c559741825be74", "82": "200f25fbad7af8fb", "83": "1284897282eb47a3", "84": "2fb17901aa87e0e7", "85": "4bc370be8afd7db1", "86": "e858d75e995c92e5", "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": "cb5ac3401283c378", "100": "b234c5e4ac7f632d", "101": "a0bc88eb14b70170", "102": "8dc2ed447b742130", "103": "6ffbe07b521a061b", "104": "e114a7aebaaca2f1", "105": "0567be4dc8048b9c", "106": "8bc72c179793e1c2", "107": "d9c195d9ccdb137e", "108": "e0fc0a42e33bda91"}
{"0": "f8dacdda1d052754", "1": "cb6ea50bd4e4a5ca", "2": "8fcc50e5e52e3ab3", "3": "1f737ca8edb978f2", "4": "56bf12952b0de42b", "5": "0a7b6d40b794bd19", "6": "3dd68cce72af5314", "7": "03b56b13a08b5f83", "8": "5838caf1c500a4b4", "9": "4a86450ff5444c44", "10": "2b037735e1f5b0f6", "11": "4a490abd5e503318", "12": "aeca274694e158d2", "13": "7e3b558a587a4ebe", "14": "bf75bfc99d4f9e34", "15": "8a30574014478f1f", "16": "95add6a975c0cb72", "17": "e042d7fe7351bcb9", "18": "f93d5d85e671fcee", "19": "39abb1eef19df707", "20": "aecf2fce44ba49c5", "21": "59485c317afc6c2b", "22": "f2045a9940596eb3", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "599f23689a2b8aca", "27": "eab427ce9a5219e9", "28": "5f977d8429c39f6a", "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": "d96493a5f110bd4c", "42": "916170b9f5d7346b", "43": "e649ddeadda89cbb", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "e594ecaa8e7d1538", "48": "cf2f2424bc00436f", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "b2c8c091044064e6", "56": "9ee2ad45dc27851d", "57": "f2a40b5f1b29f669", "58": "0c5797ecc09be26d", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "94ead9feda213ac6", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "ce0e68dab611f022", "65": "bdee3c4a37df52f8", "66": "b0afc4d04f42150f", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "cc7621a6b501b8a2", "71": "5757de52275f3382", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "cdeb7b925ce5043d", "77": "2ee521497bb8506a", "78": "7e842e2bd8e95fd4", "79": "86f177bf0a7dd07c", "80": "315325cde1352e98", "81": "37c559741825be74", "82": "200f25fbad7af8fb", "83": "1284897282eb47a3", "84": "2fb17901aa87e0e7", "85": "4bc370be8afd7db1", "86": "e858d75e995c92e5", "87": "e5fa7717989eda80", "88": "8b5ab5232cef3457", "89": "9e3363b80eeceeea", "90": "dd1c246e19a2d032", "91": "664f8b17c5d7a9aa", "92": "fced244d93677d90", "93": "8bd61518dd267641", "94": "713d04996c4627df", "95": "59c31f858c7b6970", "96": "4195478d5c528371", "97": "01f68dfc34a29d32", "98": "8c471c7baeaaca54", "99": "cb5ac3401283c378", "100": "b234c5e4ac7f632d", "101": "a0bc88eb14b70170", "102": "96b578f45b8bad5e", "103": "6ffbe07b521a061b", "104": "e114a7aebaaca2f1", "105": "0567be4dc8048b9c", "106": "8bc72c179793e1c2", "107": "d9c195d9ccdb137e", "108": "e0fc0a42e33bda91", "109": "930d097b73cefa7c", "110": "86ccefb4c68b732b", "111": "ede7733e4a295879", "112": "71cbe5098f4c1b6f", "113": "c65d8fa08757ee48", "114": "1f03fe34df693fcc", "115": "68c52c80f38f8b2c", "116": "4b58c0e8a44f5aec", "117": "8e5e97153c881130", "118": "d61ef5e4abc4bb10", "119": "39b5f1f56c2c2553", "120": "2a08cdb1d7133600", "121": "e7265770f07fa066", "122": "8f647d3ce08ecd2b", "123": "3088ae48043ee39d", "124": "29790cf710874dd0", "125": "e4f29216745ec758", "126": "287d60115d9acda8", "127": "ee7017d224ec9ad6", "128": "e800d832c255899a", "129": "47128fbd98c3ce4d"}
+128 -63
View File
@@ -1,16 +1,16 @@
# Graph Report - juzi (2026-10-12)
## Corpus Check
- 97 files · ~4,891,452 words
- 99 files · ~4,892,361 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 1420 nodes · 2864 edges · 109 communities (87 shown, 22 thin omitted)
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 199 edges (avg confidence: 0.55)
- 1431 nodes · 2879 edges · 130 communities (98 shown, 32 thin omitted)
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 200 edges (avg confidence: 0.56)
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `c9352fe4`
- Built from commit: `ce6dbc20`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
@@ -21,7 +21,7 @@
- skin-score.js
- customer-feedback-second.js
- f
- Box
- ComputeAllFaceDescriptorsTask
- test/profile-store.js
- admin.js
- __awaiter
@@ -80,11 +80,11 @@
- ui-20261009.mjs
- analyze-timeout.js
- misans/README.md
- .forwardInput
- .predictAgeAndGender
- normalize
- extractAllFacesAndComputeResults
- build_report_copy.py
- .forwardInput
- extractFaceTensors
- doCapture
- .runAndExtendWithFaceDetections
- n
- 登录反馈与加载优化(2026-10-12)
- 线上微信登录部署记录
@@ -113,6 +113,27 @@
- 第三轮反馈20261012/傅艳NEW与项思炼的会话 2026年10月12日.md
- 基础信息记忆与肤质拖动
- profile-interaction.mjs
- submitPhotos
- startCamera
- e
- showScreen
- Point
- initialize
- FaceLandmarks68
- .forwardInput
- ao
- k
- .div
- restoreProfile
- TinyFaceFeatureExtractor
- isValidNumber
- tt
- TinyYolov2
- 报告等待页实拍背景-2026-10-12.md
- FaceDetection
- ObjectDetection
- PredictAllFaceExpressionsTask
- PredictSingleAgeAndGenderTask
## God Nodes (most connected - your core abstractions)
1. `__awaiter()` - 59 edges
@@ -133,27 +154,27 @@
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
- `capture()` --indirect_call--> `el()` [INFERRED]
h5/test/scan-photo.mjs → h5/js/admin.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
## Import Cycles
- None detected.
## Communities (109 total, 22 thin omitted)
## Communities (130 total, 32 thin omitted)
### Community 0 - "face-api.js"
Cohesion: 0.02
Nodes (39): cn(), computeReshapedDimensions(), createBrowserEnv(), createFileSystem(), createNodejsEnv(), Dimensions(), dr(), en() (+31 more)
Nodes (36): cn(), computeReshapedDimensions(), decodeBoxesLayer(), dr(), en(), extractBoundingBoxes(), ff(), ge() (+28 more)
### Community 1 - "线上闭环测试(不含真实微信授权)"
Cohesion: 0.25
Nodes (7): 发现并修复, 可复现命令, 已通过的检查, 线上闭环测试(不含真实微信授权), 结论, 话术及仓库同步, 验证边界
### Community 2 - "app.js"
Cohesion: 0.05
Nodes (90): applyMirror(), armNudge(), beginDetect(), bindBackdropClose(), busyWithFlow(), cancelSkinDrag(), captureFromVideo(), captureHaptic() (+82 more)
Cohesion: 0.12
Nodes (21): decode(), drain(), esc(), load(), openHistory(), preload(), renderHistoryList(), renderReport() (+13 more)
### Community 3 - "skin-score.js"
Cohesion: 0.12
@@ -164,24 +185,20 @@ Cohesion: 0.17
Nodes (10): assert, context, face, fs, geometry, original, path, source (+2 more)
### Community 5 - "f"
Cohesion: 0.12
Nodes (36): a(), b(), ba(), cf(), d(), da(), drawContour(), dt() (+28 more)
### Community 6 - "Box"
Cohesion: 0.11
Nodes (5): Box(), ComputeAllFaceDescriptorsTask(), ComputeSingleFaceDescriptorTask(), extendWithFaceDescriptor(), ObjectDetection()
Cohesion: 0.23
Nodes (21): a(), bf(), cf(), d(), dt(), ea(), f(), g() (+13 more)
### Community 7 - "test/profile-store.js"
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)
Cohesion: 0.14
Nodes (33): ageText(), api(), bind(), captureQualityHtml(), closeDetail(), collectFilters(), dimsHtml(), doRetry() (+25 more)
### Community 9 - "__awaiter"
Cohesion: 0.13
Nodes (13): __awaiter(), ComposableTask(), extractSingleFaceAndComputeResult(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow(), __generator() (+5 more)
Cohesion: 0.14
Nodes (12): __awaiter(), ComposableTask(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow(), __generator(), getModelUris() (+4 more)
### Community 10 - "wechat-browser.mjs"
Cohesion: 0.22
@@ -200,8 +217,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.19
Nodes (18): awaitMediaLoaded(), bufferToImage(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractImagePatches(), getContext2dOrThrow(), getEnv() (+10 more)
Cohesion: 0.14
Nodes (17): awaitMediaLoaded(), Box(), bufferToImage(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractImagePatches(), getContext2dOrThrow() (+9 more)
### Community 16 - "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范"
Cohesion: 0.15
@@ -224,8 +241,8 @@ Cohesion: 0.40
Nodes (9): build_case1(), build_case2(), build_truth(), col(), dump(), main(), parse_value(), set_path() (+1 more)
### Community 21 - "disposeUnusedWeightTensors"
Cohesion: 0.08
Nodes (25): disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$1(), extractParamsFromWeigthMap$2() (+17 more)
Cohesion: 0.10
Nodes (24): disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$1(), extractParamsFromWeigthMap$2() (+16 more)
### Community 23 - "facepp-skinanalyze(旷视皮肤分析专业版调用技能)"
Cohesion: 0.20
@@ -248,8 +265,8 @@ 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.08
Nodes (35): ao(), Bo(), C(), cd(), cl(), co(), Do(), eo() (+27 more)
Cohesion: 0.15
Nodes (22): C(), cd(), co(), eo(), fo(), Ft(), Gc(), gn() (+14 more)
### Community 29 - "face-api.js 本地化依赖(h5/vendor/faceapi/)"
Cohesion: 0.29
@@ -284,8 +301,8 @@ Cohesion: 0.18
Nodes (11): list, bounds(), layer_at(), main(), save_image(), composite(), main(), sha256() (+3 more)
### Community 43 - "convLayer"
Cohesion: 0.23
Nodes (11): convLayer(), convWithBatchNorm(), depthwiseSeparableConv$1(), fullyConnectedLayer(), leaky(), ONet(), PNet(), prelu() (+3 more)
Cohesion: 0.15
Nodes (17): boxPredictionLayer(), convLayer(), convWithBatchNorm(), depthwiseConvLayer(), depthwiseSeparableConv$1(), fullyConnectedLayer(), getStridesForLayerIdx(), leaky() (+9 more)
### Community 44 - "start-local.sh"
Cohesion: 0.50
@@ -308,8 +325,8 @@ Cohesion: 0.11
Nodes (17): 1. 结论和依据, 2. 统一比较口径, 3. 逐页差异和调整内容, 4. 报告数据如何接到新 UI, 5. 登录与“我的报告”的真实能力缺口, 6. 原稿中需要整理的内容, 7. 实施顺序与交付物, 8. 验收定义 (+9 more)
### Community 55 - "i"
Cohesion: 0.09
Nodes (26): bn(), ca(), dn(), ee(), et(), fa(), fn(), i() (+18 more)
Cohesion: 0.14
Nodes (15): bn(), dn(), ee(), et(), fn(), i(), je(), ln() (+7 more)
### Community 56 - "report-ui.js"
Cohesion: 0.50
@@ -320,8 +337,8 @@ Cohesion: 0.60
Nodes (5): diffCtx(), loadJSON(), main(), okNum(), okStr()
### Community 58 - ".dispose"
Cohesion: 0.12
Nodes (13): bgrToRgbTensor(), DetectSingleFaceLandmarksTask(), extendWithFaceLandmarks(), extractBoundingBoxes(), FaceRecognitionNet(), getSizesForScale(), nonMaxSuppression(), normalize$1() (+5 more)
Cohesion: 0.13
Nodes (12): bgrToRgbTensor(), DetectSingleFaceLandmarksTask(), extendWithFaceLandmarks(), FaceRecognitionNet(), getSizesForScale(), nonMaxSuppression(), normalize$1(), rescaleAndNormalize() (+4 more)
### Community 61 - "FaceProcessor"
Cohesion: 0.22
@@ -344,32 +361,32 @@ Cohesion: 0.29
Nodes (7): 公网服务器配置, 公网真机验收清单, 反向代理与后台保护, 已完成的链路, 微信登录接入与公网部署说明, 数据、迁移与备份, 本机开发环境限制(不代表线上状态)
### Community 66 - "FaceLandmarks"
Cohesion: 0.06
Nodes (16): conv(), denseBlock3(), denseBlock4(), depthwiseSeparableConv(), FaceFeatureExtractor(), FaceLandmark68NetBase(), FaceLandmarks(), FaceLandmarks5() (+8 more)
Cohesion: 0.21
Nodes (6): FaceLandmarks(), FaceLandmarks5(), getCenterPoint(), minBbox(), pt(), vt()
### Community 68 - "analyze-timeout.js"
Cohesion: 0.20
Nodes (7): assert, end, fs, path, source, start, vm
### Community 70 - ".forwardInput"
Cohesion: 0.39
Nodes (7): conv$1(), convDown(), convLayer$1(), convNoRelu(), residual(), residualDown(), __spreadArrays()
### Community 70 - "normalize"
Cohesion: 0.36
Nodes (8): conv$1(), convDown(), convLayer$1(), convNoRelu(), normalize(), residual(), residualDown(), __spreadArrays()
### Community 71 - ".predictAgeAndGender"
Cohesion: 0.28
Nodes (4): extendWithAge(), extendWithGender(), PredictAllAgeAndGenderTask(), PredictSingleAgeAndGenderTask()
### Community 71 - "extractAllFacesAndComputeResults"
Cohesion: 0.14
Nodes (8): ComputeSingleFaceDescriptorTask(), extendWithAge(), extendWithFaceDescriptor(), extendWithFaceExpressions(), extendWithGender(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult(), PredictAllAgeAndGenderTask()
### Community 76 - ".forwardInput"
Cohesion: 0.22
Nodes (9): boxPredictionLayer(), decodeBoxesLayer(), depthwiseConvLayer(), getCenterCoordinatesAndSizesLayer(), getStridesForLayerIdx(), mobileNetV1(), outputLayer(), pointwiseConvLayer() (+1 more)
### Community 76 - "doCapture"
Cohesion: 0.16
Nodes (23): applyMirror(), armNudge(), busyWithFlow(), captureFromVideo(), captureHaptic(), checkStill(), doCapture(), dropHint() (+15 more)
### Community 77 - "extractFaceTensors"
Cohesion: 0.12
Nodes (13): DetectAllFacesTask(), DrawBox(), drawDetections(), DrawFaceLandmarks(), extendWithFaceDetection(), extendWithFaceExpressions(), extractAllFacesAndComputeResults(), extractFaceTensors() (+5 more)
### Community 77 - ".runAndExtendWithFaceDetections"
Cohesion: 0.18
Nodes (8): DetectAllFacesTask(), DrawBox(), drawDetections(), DrawFaceLandmarks(), extendWithFaceDetection(), isWithFaceDetection(), isWithFaceLandmarks(), resizeResults()
### Community 78 - "n"
Cohesion: 0.10
Nodes (39): ae(), be(), bf(), bi(), ce(), de(), e(), Ef() (+31 more)
Nodes (38): ae(), be(), bi(), ce(), de(), Ef(), er(), FaceMatch() (+30 more)
### Community 79 - "登录反馈与加载优化(2026-10-12)"
Cohesion: 0.33
@@ -408,8 +425,8 @@ Cohesion: 0.22
Nodes (8): assets, design_scale, document_size, native_header_px, shape_metrics, source, source_sha256, text_styles
### Community 91 - "wi"
Cohesion: 0.28
Nodes (9): Ci(), Ei(), Fi(), Ii(), ki(), pe(), ri(), wi() (+1 more)
Cohesion: 0.24
Nodes (10): Ci(), Ei(), Fi(), Ii(), ki(), pe(), ri(), ve() (+2 more)
### Community 92 - "客户反馈修改(2026-10-12)"
Cohesion: 0.33
@@ -436,8 +453,8 @@ Cohesion: 0.50
Nodes (3): drawFaceExpressions(), FaceExpressions(), isWithFaceExpressions()
### Community 102 - ".detect"
Cohesion: 0.18
Nodes (5): iou(), NetInput(), nonMaxSuppression$1(), TinyYolov2(), TinyYolov2Base()
Cohesion: 0.16
Nodes (6): drawContour(), extractFaceTensors(), SsdMobilenetv1(), TinyYolov2Base(), x(), y()
### Community 107 - "基础信息记忆与肤质拖动"
Cohesion: 0.33
@@ -447,20 +464,68 @@ Nodes (4): 回归命令, 基础信息记忆与肤质拖动, 肤质交互, 范围
Cohesion: 0.40
Nodes (3): dragBy(), h5, touch()
### Community 109 - "submitPhotos"
Cohesion: 0.16
Nodes (17): bindBackdropClose(), clearScanPhoto(), closeLoginSuccess(), closeOverlay(), handleCamHidden(), leaveCamera(), photosByView(), popHistorySilently() (+9 more)
### Community 110 - "startCamera"
Cohesion: 0.17
Nodes (15): beginDetect(), detectFrame(), detTick(), enableFallback(), getCamStream(), handleCamVisible(), loadFaceApi(), loadModels() (+7 more)
### Community 111 - "e"
Cohesion: 0.17
Nodes (15): ba(), Bo(), ca(), cl(), da(), e(), fa(), pa() (+7 more)
### Community 112 - "showScreen"
Cohesion: 0.22
Nodes (11): loadAppFont(), loginAllowed(), loginBrowser(), loginSucceeded(), loginWechat(), openOverlay(), prepareScanArtwork(), renderAccountStatus() (+3 more)
### Community 113 - "Point"
Cohesion: 0.24
Nodes (3): FaceLandmark68NetBase(), Point(), wl()
### Community 114 - "initialize"
Cohesion: 0.22
Nodes (9): createBrowserEnv(), createFileSystem(), createNodejsEnv(), initialize(), isBrowser(), isNodejs(), monkeyPatch(), resolveInput() (+1 more)
### Community 116 - ".forwardInput"
Cohesion: 0.36
Nodes (5): conv(), depthwiseSeparableConv(), mainBlock(), reductionBlock(), TinyXception()
### Community 117 - "ao"
Cohesion: 0.33
Nodes (6): ao(), Do(), io(), oo(), so(), To()
### Community 118 - "k"
Cohesion: 0.33
Nodes (6): b(), k(), ne(), Yt(), zn(), zr()
### Community 120 - "restoreProfile"
Cohesion: 0.50
Nodes (5): cancelSkinDrag(), goSkin(), restoreProfile(), saveProfile(), setPick()
### Community 122 - "isValidNumber"
Cohesion: 0.50
Nodes (4): Dimensions(), isValidNumber(), isValidProbablitiy(), isWithGender()
### Community 123 - "tt"
Cohesion: 0.50
Nodes (4): kn(), tt(), wo(), z()
## Knowledge Gaps
- **305 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+300 more)
- **309 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+304 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **22 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.059) - this node is a cross-community bridge._
_High betweenness centrality (0.060) - this node is a cross-community bridge._
- **Why does `fill()` connect `report-ui.js` to `f`?**
_High betweenness centrality (0.047) - this node is a cross-community bridge._
- **Why does `k()` connect `f` to `face-api.js`, `app.js`, `lib/accounts.js`, `n`, `i`, `C`?**
_High betweenness centrality (0.042) - this node is a cross-community bridge._
_High betweenness centrality (0.049) - this node is a cross-community bridge._
- **Why does `k()` connect `k` to `face-api.js`, `app.js`, `f`, `lib/accounts.js`, `n`, `e`, `C`?**
_High betweenness centrality (0.043) - 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?**
@@ -468,4 +533,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?**
_305 weakly-connected nodes found - possible documentation gaps or missing edges._
_309 weakly-connected nodes found - possible documentation gaps or missing edges._
File diff suppressed because one or more lines are too long
+1076 -779
View File
File diff suppressed because it is too large Load Diff
+22 -12
View File
@@ -40,8 +40,8 @@
"semantic_hash": ""
},
"h5/js/app.js": {
"mtime": 1791810527.8429196,
"ast_hash": "d2cbf922e0f250d270cb3f6880fa5d73",
"mtime": 1791810934.438929,
"ast_hash": "b156d7bffd1daa1e346d127052aff709",
"semantic_hash": ""
},
"h5/js/head-pose.js": {
@@ -135,13 +135,13 @@
"semantic_hash": ""
},
"h5/README.md": {
"mtime": 1791810413.4149451,
"ast_hash": "596d308e73a48f388c67d820e8e073d8",
"mtime": 1791811100.15898,
"ast_hash": "4b8b63aa8184ead90d868aff34e9a6e3",
"semantic_hash": ""
},
"h5/index.html": {
"mtime": 1791810016.13091,
"ast_hash": "454d692d26dc6ed624d5641784c8e61d",
"mtime": 1791810934.438929,
"ast_hash": "c4d2e3bfa60d91dea4c1fdf54780046a",
"semantic_hash": ""
},
"h5/vendor/faceapi/README.md": {
@@ -1695,8 +1695,8 @@
"semantic_hash": ""
},
"h5/test/customer-feedback-third.mjs": {
"mtime": 1791801066.5032508,
"ast_hash": "5fd25617bf8e4eb73ff168f2525e9811",
"mtime": 1791811030.7069488,
"ast_hash": "0a74e23b1968ec821bcb01c3eb7f9a8a",
"semantic_hash": ""
},
"tools/export_third_feedback_assets.py": {
@@ -1815,8 +1815,8 @@
"semantic_hash": ""
},
"h5/test/capture-quality-browser.mjs": {
"mtime": 1791809137.130926,
"ast_hash": "6daff225b0e02cec327882171408e4c5",
"mtime": 1791811030.7069488,
"ast_hash": "311ed23a81012e7a35daef3a0ec40013",
"semantic_hash": ""
},
"h5/test/customer-feedback-fourth.js": {
@@ -1905,8 +1905,18 @@
"semantic_hash": ""
},
"docs/\u57fa\u7840\u4fe1\u606f\u8bb0\u5fc6\u4e0e\u80a4\u8d28\u62d6\u52a8-2026-10-12.md": {
"mtime": 1791810413.4149451,
"ast_hash": "6159880c092b531b465257eec590092a",
"mtime": 1791810600.0989332,
"ast_hash": "cec2f2e829751e8b5d2e5e40c348f5e4",
"semantic_hash": ""
},
"h5/test/scan-photo.mjs": {
"mtime": 1791811100.15898,
"ast_hash": "04e29e75a6707367ec0171b73ce5479c",
"semantic_hash": ""
},
"docs/\u62a5\u544a\u7b49\u5f85\u9875\u5b9e\u62cd\u80cc\u666f-2026-10-12.md": {
"mtime": 1791811100.15898,
"ast_hash": "0362a8e36c7e8d6b16ccb96a666b715a",
"semantic_hash": ""
}
}
+1 -1
View File
@@ -71,7 +71,7 @@ node server.js # 默认 8080,可用 PORT=xxx 修改
- **白屏补光**:全屏纯白覆盖层瞬间点亮,~300ms 后抓全分辨率帧(原始视角 + 最长边 1280 JPEG),保持 ~300ms 再淡出,同步 `navigator.vibrate(30)`。
- **快门只在无法实时识别时出现**:系统相机降级、face-api/模型加载失败、`?mock=1` 调试。文件照片共用同一套本地单人/距离/完整入镜/角度/光线校验,不合格停在当前步;系统照片可能已镜像,侧脸不判断左右,并标记 `direction_checked:false`。本地模型加载中最多等 8 秒;确实不可用时保留原有降级通路,但后台明确记录 `unchecked/model-unavailable`,不冒充检查通过。服务端质量结果仅记录,不新增重拍阻断。
- **报告等待与质量留痕**:原 PSD 画面增加约 1 秒上下往返扫描;成功跳转等待至少一次实际动画循环,错误/取消立即恢复。拍摄质量备注不在用户报告展示,后台列表、详情和 CSV 保留侧脸质量状态与前端参数版本。前端诊断为客户端自报,仅供校准,不作为服务端质量真值。
- **报告等待与质量留痕**:本次正脸实拍覆盖 PSD 白色人脸占位,保持与拍摄页相同的等比铺满和前置预览镜像(仅显示,不改送检 JPEG);保留 PSD 框、文案、装饰图标与约 1 秒上下往返扫描。成功跳转等待至少一次实际动画循环,错误/取消立即恢复并清理等待页照片引用。PSD 素材加载失败仍显示照片、文字和取消入口。拍摄质量备注不在用户报告展示,后台列表、详情和 CSV 保留侧脸质量状态与前端参数版本。前端诊断为客户端自报,仅供校准,不作为服务端质量真值。
- **相册选图**:相机页「相册」按钮可选当前步的图片,与降级拍照同管线(压 1280 → 校验 → 推进)。
- **语音播报**:步骤引导/姿态提示/拍摄完成/分析中/失败,文案与文件名契约见 `../docs/语音播报内容设计文档_V1.0.md`。新增的提示(居中、抬头/低头、光线)暂无音频,清单缺失的条目静默跳过,只显示文字。
- **无进展提示**:单步 12s 仍未拍到,底部浮出「识别不到?请正对镜头,并到光线充足处」(不阻塞流程)。
+11 -3
View File
@@ -75,12 +75,20 @@
/* Fourth feedback adds a fast one-second round trip to original PSD pixels.
The stage/mask share one contain transform, including short/landscape screens. */
#p-analyzing { background: #000; overflow: hidden; }
.scan-stage { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: min(100vw, 480px, calc(100dvh * 1080 / 2091)); height: min(100dvh, calc(min(100vw, 480px) * 2091 / 1080)); }
.scan-stage { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: min(100vw, 480px, calc(100dvh * 1080 / 2091)); height: min(100dvh, calc(min(100vw, 480px) * 2091 / 1080)); overflow: hidden; }
@supports not (height: 100dvh) { .scan-stage { width: min(100vw, 480px, calc(100vh * 1080 / 2091)); height: min(100vh, calc(min(100vw, 480px) * 2091 / 1080)); } }
.scan-artwork { width: 100%; height: 100%; object-fit: contain; }
.scan-face { position: absolute; inset: 0; -webkit-mask-size: 100% 100%; mask-size: 100% 100%; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; overflow: hidden; }
.scan-face { position: absolute; inset: 0; background: #111; -webkit-mask-size: 100% 100%; mask-size: 100% 100%; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; overflow: hidden; }
/* Same centered cover crop as the camera; never stretch the captured JPEG.
The PSD mask clips the real face, covering the original white placeholder. */
.scan-photo { position: absolute; left: 50%; top: 50%; width: min(100vw, 480px); height: 100vh; height: 100dvh; max-width: none; object-fit: cover; transform: translate(-50%, -50%); }
.scan-photo.mirror { transform: translate(-50%, -50%) scaleX(-1); }
.scan-photo:not([src]) { visibility: hidden; }
.scan-beam { position: absolute; left: 0; right: 0; top: 20%; height: 18%; background: linear-gradient(to bottom, #F5F078 0%, #F5F078 35%, rgba(245,240,120,0) 100%); }
#p-analyzing:not(.scan-artwork-ready) .scan-stage { visibility: hidden; }
#p-analyzing:not(.scan-artwork-ready) .scan-artwork { visibility: hidden; }
#p-analyzing:not(.scan-artwork-ready) .scan-face { -webkit-mask-image: none !important; mask-image: none !important; }
#p-analyzing:not(.scan-artwork-ready) .scan-photo { opacity: .6; }
#p-analyzing:not(.scan-artwork-ready) .scan-beam { visibility: hidden; }
.scan-running .scan-beam { animation: report-scan var(--scan-cycle, 1000ms) ease-in-out infinite; }
@keyframes report-scan { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(166%); } }
.scan-running:not(.scan-artwork-ready) .scan-status { animation: report-scan-fallback var(--scan-cycle, 1000ms) ease-in-out infinite; }
+4 -1
View File
@@ -457,7 +457,10 @@
Not camera controls: no action attributes, focus targets or file inputs. -->
<div class="scan-stage" aria-hidden="true">
<img id="scan-artwork" class="scan-artwork" data-src="assets/img/ui-20261012/scan-base.webp" width="1080" height="2091" alt="" draggable="false">
<div id="scan-face" class="scan-face" data-mask="assets/img/ui-20261012/scan-face-mask.png"><i class="scan-beam"></i></div>
<div id="scan-face" class="scan-face" data-mask="assets/img/ui-20261012/scan-face-mask.png">
<img id="scan-photo" class="scan-photo" alt="" draggable="false">
<i class="scan-beam"></i>
</div>
</div>
<p class="scan-status" role="status">检测中…</p>
<button class="scan-cancel" type="button" data-action="cancel-analysis">取消</button>
+31 -6
View File
@@ -143,6 +143,22 @@
/* ─── 弹窗管理 ─── */
var SCAN_CYCLE_MS = 1000, scanCycle = null, scanMask = null;
function setScanPhoto(source, mirrored) {
var image = $('#scan-photo');
image.classList.toggle('mirror', !!mirrored);
if (image.getAttribute('src') === source) return;
image.onload = function () {
// A delayed JPEG decode must still get one complete visible scan cycle.
if ($('#p-analyzing').classList.contains('show')) restartScanCycle();
};
image.onerror = function () { image.removeAttribute('src'); };
image.src = source;
}
function clearScanPhoto() {
var image = $('#scan-photo');
image.onload = image.onerror = null;
image.removeAttribute('src'); image.classList.remove('mirror');
}
function restartScanCycle() {
if (!scanCycle || scanCycle.done) return;
var overlay = $('#p-analyzing'), cycle = scanCycle;
@@ -204,7 +220,10 @@
$('#' + id).classList.add('show');
if (id === 'p-analyzing') { startScanCycle(); $('.scan-cancel').focus({ preventScroll: true }); }
}
function closeOverlay(id) { $('#' + id).classList.remove('show'); if (id === 'p-analyzing') stopScanCycle(); }
function closeOverlay(id) {
$('#' + id).classList.remove('show');
if (id === 'p-analyzing') { stopScanCycle(); clearScanPhoto(); }
}
// 点遮罩空白处关闭(与各自关闭按钮同行为:隐私→回启动页、拍照引导→留在档案页)
// 只响应遮罩本身(拍照引导另含底图层),不误伤弹窗卡片内交互
@@ -442,7 +461,7 @@
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 photos = []; // 三连拍(dataURL),拍摄中与 CAM_STEPS 同序;送检前经 photosByView 重排为 [正脸, 左脸, 右脸]
var photos = [], scanPhotoMirrored = false; // 三连拍(dataURL),拍摄中与 CAM_STEPS 同序;送检前经 photosByView 重排为 [正脸, 左脸, 右脸]
var captureDiagnostics = [], holdState = null;
// 约束逐级放宽:facingMode 裸值在部分 Android 抛 OverconstrainedError,先 ideal 再退 video:true,最后才降级
@@ -568,7 +587,7 @@
hideCamStatus();
shutterLock = false;
if (err) { toast(err); voice.play(VOICE.fail, { cut: true }); return; } // 不合格:停在本步重拍
onPhotoCaptured(url);
onPhotoCaptured(url, false); // Files already have their own camera orientation.
});
};
img.onerror = function () { toast('照片读取失败,请重新选择'); };
@@ -1009,9 +1028,13 @@
var shutterLock = false; // 快门防连点锁:流程进行中直接忽略
// 照片就绪(已通过校验)后推进流程:存图→「拍摄完成」→300ms 过渡→下一步;三步严格按顺序,本步合格才进下一步
function onPhotoCaptured(dataUrl) {
function onPhotoCaptured(dataUrl, mirrored) {
shutterLock = true; // 推进窗口上锁(停顿期间不检测、不接受快门),直到下一步/送检
photos[camStep] = dataUrl;
if (camStep === 0) {
scanPhotoMirrored = !!mirrored;
setScanPhoto(dataUrl, scanPhotoMirrored); // Decode the front photo before the waiting screen opens.
}
voice.play(VOICE.shot, { cut: true }); // 「拍摄完成」:已拍下,本步未播完的引导作废
var seq = camSeq, hasNext = camStep < CAM_STEPS.length - 1;
if (hasNext) {
@@ -1049,7 +1072,7 @@
var fill = $('#cam-fill');
fill.classList.add('on'); // .on 无过渡,瞬间点亮
setTimeout(function () {
var shot = captureFromVideo();
var shot = captureFromVideo(), mirrored = effMirror();
setTimeout(function () {
fill.classList.remove('on'); // 移除 .on 恢复过渡,淡出
capturing = false;
@@ -1062,7 +1085,7 @@
shutterLock = false; holdState = null; okSince = 0;
setHint(error, false); armNudge(); return;
}
onPhotoCaptured(shot);
onPhotoCaptured(shot, mirrored);
});
}, 300);
}, 300);
@@ -1072,6 +1095,7 @@
function submitPhotos(seq) {
camStep = 0;
photos = photosByView(photos);
setScanPhoto(photos[0], scanPhotoMirrored);
openOverlay('p-analyzing');
voice.play(VOICE.analyzing); // 「正在生成您的肌肤报告」
// 档案与检测一起上报(后台记录用):年龄非法值服务端按 null 处理
@@ -1629,6 +1653,7 @@
camSeq++; // 使旧的快门/分析回调失效
camStep = 0;
photos = []; // 新一轮三连拍(从 startCamera 挪出:后台恢复续拍时保留已拍)
scanPhotoMirrored = false; clearScanPhoto();
captureDiagnostics = [];
shutterLock = false; // 重新进入流程,保险解锁
showScreen('s-cam', 'push');
+3
View File
@@ -97,7 +97,10 @@ try {
const sent = page.waitForRequest(r => r.url().endsWith('/api/skin-analyze'));
await page.evaluate(() => window.__cameraFixture.set('C3_-135.jpg'));
const body = (await sent).postDataJSON();
assert.equal(await page.locator('#scan-photo').getAttribute('src'), body.front_image, 'real live waiting uses the captured front photo');
assert.equal(await page.locator('#scan-photo.mirror').count(), 1, 'front camera waiting keeps the preview mirror without changing the sent JPEG');
await page.waitForSelector('#s-report.active');
assert.equal(await page.locator('#scan-photo').getAttribute('src'), null, 'waiting photo reference is cleared after success');
for (const key of ['front', 'left', 'right']) {
const row = body.capture_diagnostics.views[key];
assert.equal(row.mode, 'live'); assert.equal(row.direction_checked, true);
+4 -2
View File
@@ -120,8 +120,10 @@ try {
const context = canvas.getContext('2d'); context.drawImage(image, 0, 0);
return [[540, 720], [540, 1200]].map(([x, y]) => Array.from(context.getImageData(x, y, 1, 1).data));
});
assert(scanPixels[0].slice(0, 3).every(value => value > 245), 'visible PSD face must not be lost to clipping');
assert(scanPixels[1].slice(0, 3).every(value => value > 245), 'base face stays white; yellow scan is now a separate animated layer');
assert(scanPixels[0].slice(0, 3).every(value => value > 245), 'original PSD bitmap is unchanged');
assert(scanPixels[1].slice(0, 3).every(value => value > 245), 'original PSD bitmap is unchanged');
assert.equal(await page.locator('#scan-photo').getAttribute('src'), pending.request().postDataJSON().front_image, 'real front photograph covers the white PSD placeholder');
assert(await page.locator('#scan-photo').evaluate(image => image.complete && image.naturalWidth > 0));
assert.equal(await page.locator('.scan-beam').evaluate(e => getComputedStyle(e).animationName), 'report-scan');
assert.equal(await page.locator('#p-analyzing [data-action]').count(), 1, 'only cancel is actionable; PSD icons decorative');
for (const [width, height] of [[320, 726], [375, 726], [390, 844], [414, 896], [480, 726], [375, 440]]) {
+110
View File
@@ -0,0 +1,110 @@
// Actual three-photo UI with in-memory auth/analysis. No backend writes/paid API.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
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 base = process.env.H5_BASE_URL || 'http://127.0.0.1:8080/';
const samples = fileURLToPath(new URL('../../reference/facepp-skinanalyze/examples/images/', import.meta.url));
const browser = await chromium.launch({ headless: true });
const errors = [], unexpectedWrites = [];
try {
const page = await browser.newPage({ viewport: { width: 375, height: 726 }, isMobile: true, hasTouch: true });
page.setDefaultTimeout(20000);
page.on('pageerror', e => errors.push(e.message));
let pending, artworkFailure = false;
await page.route('**/*', route => {
const req = route.request();
if (req.url().endsWith('/api/auth/session')) return route.fulfill({ json: { ok: true, user: { id: 'scan-photo-fixture', kind: 'browser' } } });
if (req.url().endsWith('/api/skin-analyze')) { pending = route; return; }
if (req.method() !== 'GET') { unexpectedWrites.push(req.url()); return route.abort(); }
if (/\.woff2$|\/vendor\/faceapi\//.test(req.url())) return route.abort();
if (artworkFailure && /scan-base\.webp|scan-face-mask\.png/.test(req.url())) return route.abort();
return route.continue();
});
await page.addInitScript(() => {
navigator.mediaDevices.getUserMedia = () => Promise.reject(new DOMException('fixture denial', 'NotAllowedError'));
window.__scanIterations = [];
document.addEventListener('animationiteration', e => {
if (e.target.matches('.scan-beam,.scan-status')) window.__scanIterations.push({ target: e.target.className, elapsed: e.elapsedTime });
});
});
async function profile(reload = true) {
if (reload) await page.goto(base, { waitUntil: 'domcontentloaded' });
await page.click('[data-action="open-privacy"]');
if (!(await page.locator('#privacy-check').evaluate(el => el.classList.contains('checked')))) await page.click('#privacy-check');
await page.click('[data-action="agree-continue"]');
for (const selector of ['#pick-birth', '#pick-gender', '#pick-region']) {
await page.click(selector); await page.click('.ms-mobile-select.ms-show .ms-ensure'); await page.waitForTimeout(250);
}
}
async function capture() {
pending = null;
await page.click('[data-action="submit-profile"]'); await page.click('[data-action="start-camera"]');
for (const [i, name] of ['C3_180.jpg', 'C3_135.jpg', 'C3_-135.jpg'].entries()) {
await page.waitForFunction(n => document.querySelector('#cam-step').textContent.includes(n + '/3'), i + 1);
await page.waitForTimeout(1600);
await page.setInputFiles('#cam-file', path.join(samples, name));
}
await page.waitForSelector('#p-analyzing.show');
await page.waitForFunction(() => { const img = document.querySelector('#scan-photo'); return img.complete && img.naturalWidth > 0; });
assert(pending);
const sent = pending.request().postDataJSON();
assert.equal(await page.locator('#scan-photo').getAttribute('src'), sent.front_image, 'waiting face is the current FRONT photo, not the last side or a placeholder');
assert.notEqual(sent.front_image, sent.left_image); assert.notEqual(sent.front_image, sent.right_image);
assert.equal(await page.locator('#scan-photo.mirror').count(), 0, 'uploaded/system camera photos must not be mirrored again');
assert.equal(await page.locator('#scan-photo').evaluate(img => getComputedStyle(img).objectFit), 'cover');
assert.equal(await page.locator('#scan-face').evaluate(el => getComputedStyle(el).backgroundColor), 'rgb(17, 17, 17)', 'loading/error does not expose white placeholder');
return sent;
}
function rejectPending(message) { const route = pending; pending = null; return route.fulfill({ json: { ok: false, message } }); }
const cleared = async () => {
assert.equal(await page.locator('#scan-photo').getAttribute('src'), null, 'face reference cleared on exit');
assert.equal(await page.locator('#p-analyzing.scan-running').count(), 0);
};
await profile();
assert.equal(await page.locator('#scan-photo').getAttribute('src'), null, 'no face before capture');
await capture();
await page.waitForSelector('#p-analyzing.scan-artwork-ready');
assert((await page.locator('#scan-face').evaluate(el => getComputedStyle(el).maskImage)).includes('scan-face-mask.png'));
assert.equal(await page.locator('.scan-beam').evaluate(el => getComputedStyle(el).animationName), 'report-scan');
for (const [width, height] of [[320, 726], [375, 726], [390, 844], [414, 896], [480, 726], [375, 440]]) {
await page.setViewportSize({ width, height });
const photo = await page.locator('#scan-photo').boundingBox();
assert(Math.abs(photo.width - Math.min(width, 480)) < 1 && Math.abs(photo.height - height) < 1, 'same cover viewport as camera');
assert(Math.abs(photo.x - (width - photo.width) / 2) < 1 && Math.abs(photo.y) < 1, 'photo crop stays centered independently of PSD contain mask');
assert(await page.locator('.scan-cancel').isVisible());
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
}
await page.setViewportSize({ width: 375, height: 726 });
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, 'scan-photo.png') });
}
await page.mouse.click(187, 350);
assert(await page.locator('#p-analyzing').isVisible(), 'clicking the face does not cancel');
await page.click('[data-action="cancel-analysis"]'); await page.waitForSelector('#s-profile.active'); await cleared();
await rejectPending('迟到的分析不应影响页面'); await page.waitForTimeout(200);
assert(!(await page.locator('#toast').textContent()).includes('迟到'));
await capture(); await rejectPending('fixture analysis failure');
await page.waitForSelector('#s-profile.active'); await cleared();
// Fast success + reduced motion still completes one actual visible cycle.
await page.emulateMedia({ reducedMotion: 'reduce' });
await capture();
await page.evaluate(() => { window.__scanIterations = []; });
const success = await page.evaluate(() => ({ ok: true, result: SkinScore.MOCK_RESULT, side: SkinScore.MOCK_SIDE, report_saved: true }));
await pending.fulfill({ json: success }); pending = null;
await page.waitForSelector('#s-report.active'); await cleared();
assert(await page.evaluate(() => window.__scanIterations.some(row => row.target === 'scan-beam' && row.elapsed >= 1)), 'fast success completes an actual cycle');
// Failed artwork/mask preserves the real photograph + status + cancel.
artworkFailure = true;
await profile(); await capture();
assert.equal(await page.locator('#p-analyzing.scan-artwork-ready').count(), 0);
assert.equal(await page.locator('#scan-face').evaluate(el => getComputedStyle(el).maskImage), 'none');
assert.equal(await page.locator('.scan-status').evaluate(el => getComputedStyle(el).clipPath), 'none');
assert.equal(await page.locator('#scan-photo').evaluate(img => getComputedStyle(img).visibility), 'visible');
await page.click('[data-action="cancel-analysis"]'); await cleared(); await rejectPending('canceled');
assert.deepEqual(errors, []); assert.deepEqual(unexpectedWrites, []);
console.log('PASS: actual three-photo waiting shows current front JPEG; original orientation/cover/PSD mask/scan; six viewport sizes; photo cleanup after cancel/error/success; stale response; one real reduced-motion cycle; image/mask failure fallback; no JS errors/backend writes');
} finally { await browser.close(); }