feat(h5): calculate integer total from weighted dimensions across reports and admin

This commit is contained in:
xsl
2026-10-14 14:25:17 +08:00
parent 61a7f7c726
commit d8c67762da
16 changed files with 1891 additions and 1435 deletions
@@ -0,0 +1,38 @@
# 综合得分:三维度按 4:3:3 加权
## 已确认公式
综合得分 = Math.round(紧塑力S × 4 + 透亮感S × 3 + 稳韧力S × 3)
- 三个维度为10分制,综合得分为100分制整数。
- 使用现有维度S分数(保留两位小数)计算,不先取页面显示的一位小数;最终四舍五入一次。
- 三维度任何一个缺失、非数值或越界,返回 null,页面显示 `--`,CSV为空;不按0分计、不重新分配权重。
- 不再读取旷视 `score_info.total_score` 或顶层 `total_score` 作为综合得分。原始接口数据仍原样保留供复核。
例如:紧塑力8、透亮感9、稳韧力8.5 → 84.5 → 85分。
## 生效位置
统一函数 `SkinScore.weightedTotal(dims)`,供评分引擎、H5报告页和后台复用。
- 新检测的 `vm.total` 与后台记录 `total_score` 保存加权结果,规则标记为 `dimensions-433-v1`。
- 后台列表列名“综合得分”,详情明确显示公式与精度规则,CSV列名“综合得分(4:3:3)”。
- 历史报告H5展示、后台列表/详情/CSV按已保存维度计算,不使用旧接口总分;不批量改写历史记录、报告快照或原始返回文件。
- 不修改维度评分、原始肌龄、文案或推荐产品规则;历史推荐保持不变。
- 历史快照和历史列表接口里既有的摘要总分不做迁移;用户页面不会使用该旧摘要分数,报告详情由统一函数计算显示。
## 本地验证
```sh
node h5/test/weighted-total.js
node h5/js/skin-score.js
node h5/test/regression.js
node h5/test/recommendation.js
node h5/test/raw-skin-age.js
node h5/test/accounts.js
H5_ARTIFACT_DIR=/tmp/juzi-weighted-total-20261014 node h5/test/report-persistence.mjs
```
使用临时数据目录、固定微信身份和接口样例进行测试,不调用付费检测,不读写真实线上报告。验证新记录保存、前后端展示一致、CSV、历史记录文件不变、缺失维度、安全隔离与既有产品/文案回归。
已获用户确认,纳入 2026-10-14 发版;部署后只读核对线上静态资源、报告页与后台接口,不调用真实付费检测或新增测试记录。
+8 -26
View File
@@ -25,7 +25,7 @@
"23": "facepp-skinanalyze(旷视皮肤分析专业版调用技能)", "23": "facepp-skinanalyze(旷视皮肤分析专业版调用技能)",
"24": "巨子测肤 · 1'me 语音播报内容设计文档 V1.0", "24": "巨子测肤 · 1'me 语音播报内容设计文档 V1.0",
"25": "mobile-select.iife.min.js", "25": "mobile-select.iife.min.js",
"26": ".extractWeights", "26": "NeuralNetwork",
"27": "gen_voice.py", "27": "gen_voice.py",
"28": "C", "28": "C",
"29": "face-api.js 本地化依赖(h5/vendor/faceapi/)", "29": "face-api.js 本地化依赖(h5/vendor/faceapi/)",
@@ -40,12 +40,12 @@
"38": "FaceLandmark68Net", "38": "FaceLandmark68Net",
"39": "UI交互界面调整", "39": "UI交互界面调整",
"40": "第四轮反馈修改与验收", "40": "第四轮反馈修改与验收",
"41": ".loadFromDisk", "41": "ao",
"42": "layer_at", "42": "layer_at",
"43": "ComputeAllFaceDescriptorsTask", "43": ".run",
"44": "start-local.sh", "44": "start-local.sh",
"45": "report-persistence.mjs", "45": "report-persistence.mjs",
"46": "PredictAllFaceExpressionsWithFaceAlignmentTask", "46": ".forwardInput",
"47": "extractFaceTensors", "47": "extractFaceTensors",
"48": "已修改", "48": "已修改",
"49": "mobile-select/README.md", "49": "mobile-select/README.md",
@@ -63,7 +63,7 @@
"61": "报告布局与跟手交互调整", "61": "报告布局与跟手交互调整",
"62": "提交前 Review(2026-10-11)", "62": "提交前 Review(2026-10-11)",
"63": "e2e-browser.mjs", "63": "e2e-browser.mjs",
"64": "isValidNumber", "64": "综合得分:三维度按 4:3:3 加权",
"65": "微信登录接入与公网部署说明", "65": "微信登录接入与公网部署说明",
"66": "h5/README.md", "66": "h5/README.md",
"67": "ui-20261009.mjs", "67": "ui-20261009.mjs",
@@ -92,13 +92,12 @@
"90": "customer-feedback.mjs", "90": "customer-feedback.mjs",
"91": "wi", "91": "wi",
"92": "客户反馈修改(2026-10-12)", "92": "客户反馈修改(2026-10-12)",
"93": ".runAndExtendWithFaceDetection", "93": ".runAndExtendWithFaceDetections",
"94": "第三轮反馈:PSD 对齐、验收及上线记录", "94": "第三轮反馈:PSD 对齐、验收及上线记录",
"95": "login-performance.mjs", "95": "login-performance.mjs",
"96": "production-loop.mjs", "96": "production-loop.mjs",
"97": "account-client.js", "97": "account-client.js",
"98": ".mul", "98": "TinyYolov2",
"99": "ComputeSingleFaceDescriptorTask",
"100": ".draw", "100": ".draw",
"101": "wechat-fixture.cjs", "101": "wechat-fixture.cjs",
"102": "DetectSingleFaceLandmarksTask", "102": "DetectSingleFaceLandmarksTask",
@@ -110,40 +109,23 @@
"108": "profile-interaction.mjs", "108": "profile-interaction.mjs",
"109": "submitPhotos", "109": "submitPhotos",
"110": "startCamera", "110": "startCamera",
"111": "extractWeightsFactory",
"112": "微信退出登录-2026-10-13.md", "112": "微信退出登录-2026-10-13.md",
"113": "swipe-track.js", "113": "swipe-track.js",
"114": "optimize_delivery_images.py", "114": "optimize_delivery_images.py",
"115": "normalize", "115": "normalize",
"116": ".run", "116": "extractAllFacesAndComputeResults",
"117": "play", "117": "play",
"118": "f", "118": "f",
"119": "PredictSingleFaceExpressionsWithFaceAlignmentTask",
"120": "PredictAllFaceExpressionsTask",
"121": "wechat-browser.mjs", "121": "wechat-browser.mjs",
"122": "doCapture", "122": "doCapture",
"123": "评分圈内居中、基础信息页固定与正脸语音修复", "123": "评分圈内居中、基础信息页固定与正脸语音修复",
"124": "FaceProcessor",
"125": "报告等待页实拍背景-2026-10-12.md", "125": "报告等待页实拍背景-2026-10-12.md",
"126": "PredictSingleFaceExpressionsTask",
"127": "splash-brand-browser.mjs", "127": "splash-brand-browser.mjs",
"128": "NeuralNetwork",
"129": "TinyYolov2Base",
"130": "FaceFeatureExtractor",
"131": "原始肌龄显示与鸿蒙滚轮兼容", "131": "原始肌龄显示与鸿蒙滚轮兼容",
"132": "harmony-picker.mjs", "132": "harmony-picker.mjs",
"133": ".toString",
"134": "camera-flip-browser.mjs", "134": "camera-flip-browser.mjs",
"135": "AgeGenderNet",
"136": "convLayer",
"137": "height", "137": "height",
"138": "age",
"139": "recommendation.js", "139": "recommendation.js",
"140": ".forwardInput",
"141": "FaceRecognitionNet",
"142": ".runAndExtendWithFaceDetections",
"143": "TinyXception",
"144": "FaceExpressionNet",
"145": "PredictAllAgeAndGenderWithFaceAlignmentTask", "145": "PredictAllAgeAndGenderWithFaceAlignmentTask",
"146": "PredictSingleAgeAndGenderWithFaceAlignmentTask", "146": "PredictSingleAgeAndGenderWithFaceAlignmentTask",
"147": "拍摄角度不合格震动提醒-2026-10-13.md" "147": "拍摄角度不合格震动提醒-2026-10-13.md"
+1 -1
View File
@@ -1 +1 @@
{"0": "017a6130270ab9f2", "1": "cb6ea50bd4e4a5ca", "2": "4d1bb0fdf94e716d", "3": "125a85b2d8e78e52", "4": "4b1835ae3a82cf12", "5": "b5b4a03db0f5697e", "6": "16df50aadff513fb", "7": "03b56b13a08b5f83", "8": "a689fcb62e7754af", "9": "a579e3315ea1fb63", "10": "188dd5e33354e35d", "11": "4a490abd5e503318", "12": "aeca274694e158d2", "13": "7e3b558a587a4ebe", "14": "58417b287cc79e90", "15": "4bc370be8afd7db1", "16": "95add6a975c0cb72", "17": "288c2a98831b0021", "18": "f93d5d85e671fcee", "19": "5272556cf61d08b3", "20": "aecf2fce44ba49c5", "21": "2405e1edb003a83e", "22": "54d7329fed675057", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "16bdd538850fbb24", "27": "eab427ce9a5219e9", "28": "f2d553fdb7f3d983", "29": "c608f1c6492e1cb1", "30": "73c13793fca09f56", "31": "fbbcfc059751cc6a", "32": "f6bcf56c4589316b", "33": "75f750c5b7d9f63d", "34": "f024ff7942935690", "35": "241be5abeab1da6f", "36": "a7d375041779bad4", "37": "cb8f26249485324a", "38": "9a25b1230b5fe649", "39": "fdd95375b4a5ddb3", "40": "5e546676ab96de03", "41": "00dab8704da757dc", "42": "916170b9f5d7346b", "43": "3dd68cce72af5314", "44": "852c1bb2446edd9c", "45": "e042d7fe7351bcb9", "46": "65718282b3c32109", "47": "e1aa21690d4f0dae", "48": "cf2f2424bc00436f", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "812950f51be20e40", "56": "9ee2ad45dc27851d", "57": "f2a40b5f1b29f669", "58": "80b3d789d41454ea", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "5209ce2a267bf33b", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "1c27e5f448dc4f9c", "65": "bdee3c4a37df52f8", "66": "9727b30d720298e3", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "be0c8310abda2467", "71": "589a2d5a76f3880a", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "3c4c278d6026bc3a", "77": "979dcc63399620ad", "78": "ac00532d5d77886f", "79": "c6cfdfe181232ecc", "80": "315325cde1352e98", "81": "37c559741825be74", "82": "200f25fbad7af8fb", "83": "1284897282eb47a3", "84": "d8bb296741d3d05c", "85": "7067e88534a91ab4", "86": "386c6fcc7be918e0", "87": "e5fa7717989eda80", "88": "8b5ab5232cef3457", "89": "9e3363b80eeceeea", "90": "dd1c246e19a2d032", "91": "83b0fd3b14363e4d", "92": "fced244d93677d90", "93": "5a9014c4a7f87699", "94": "713d04996c4627df", "95": "59c31f858c7b6970", "96": "4195478d5c528371", "97": "505ea719b994ac5e", "98": "e6ef8c2f1b997e99", "99": "7ac475ea9bc280f8", "100": "bf75bfc99d4f9e34", "101": "beb276de6301634c", "102": "23f1d406f6c779ae", "103": "6ffbe07b521a061b", "104": "e114a7aebaaca2f1", "105": "af50c66d16c81bba", "106": "8bc72c179793e1c2", "107": "3877490226a78145", "108": "e0fc0a42e33bda91", "109": "4afb1715f62865b1", "110": "f1660fde8e75d368", "111": "72618e564850b9ce", "112": "af995dd35403b4dd", "113": "0d99f86b9e41c4bb", "114": "b9d7c266afa84437", "115": "7a0da13554c97235", "116": "8d6f4b5e4f67bcb7", "117": "679b8985ab775959", "118": "f5fa3e3d7d829c42", "119": "8c471c7baeaaca54", "120": "e800d832c255899a", "121": "2b037735e1f5b0f6", "122": "61b6fafa306cb441", "123": "b091e3eb82b456eb", "124": "94ead9feda213ac6", "125": "e4f29216745ec758", "126": "cb5ac3401283c378", "127": "006218e34885dbe0", "128": "d96493a5f110bd4c", "129": "327fcc91276306b0", "130": "9209646b8de02314", "131": "482dbb7ec33d8c23", "132": "3daacabc5062b554", "133": "89e609b2678905d3", "134": "e7c90d6ef8276a1e", "135": "14fdf4e4c496519f", "136": "8616cd6d33fcb6cb", "137": "746b316833e9c203", "138": "b234c5e4ac7f632d", "139": "e594ecaa8e7d1538", "140": "5fec2bbca3634aec", "141": "b186b90877868930", "142": "46204fb2e4d50c53", "143": "9beb9113cf6685a2", "144": "e858d75e995c92e5", "145": "92ab3e5134ed8685", "146": "01f68dfc34a29d32", "147": "0e6cad06ebfcfb29"} {"0": "b513c3d9e2ce0ee9", "1": "cb6ea50bd4e4a5ca", "2": "4d1bb0fdf94e716d", "3": "4ee58d162f4e59ea", "4": "4b1835ae3a82cf12", "5": "5d94cf57d5d1a6fb", "6": "ae50c9da649ec57b", "7": "a906f950ce2f31ab", "8": "94615f6f98a8e1f2", "9": "4a86450ff5444c44", "10": "188dd5e33354e35d", "11": "4a490abd5e503318", "12": "f7306620afc0de70", "13": "7e3b558a587a4ebe", "14": "58417b287cc79e90", "15": "4bc370be8afd7db1", "16": "95add6a975c0cb72", "17": "288c2a98831b0021", "18": "f93d5d85e671fcee", "19": "5272556cf61d08b3", "20": "aecf2fce44ba49c5", "21": "2405e1edb003a83e", "22": "0e697f6aae67b293", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "8e18b3e21166963c", "27": "eab427ce9a5219e9", "28": "8609687e7e66e290", "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": "8e5e97153c881130", "42": "916170b9f5d7346b", "43": "6cda159cdef28f96", "44": "852c1bb2446edd9c", "45": "e042d7fe7351bcb9", "46": "880a74c6d255e5cc", "47": "0303828c18285570", "48": "cf2f2424bc00436f", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "812950f51be20e40", "56": "9ee2ad45dc27851d", "57": "f2a40b5f1b29f669", "58": "5dc697ba1920d5a0", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "5209ce2a267bf33b", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "ad47fc38e493a4e2", "65": "bdee3c4a37df52f8", "66": "9727b30d720298e3", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "be0c8310abda2467", "71": "589a2d5a76f3880a", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "3c4c278d6026bc3a", "77": "979dcc63399620ad", "78": "68c52c80f38f8b2c", "79": "c6cfdfe181232ecc", "80": "315325cde1352e98", "81": "37c559741825be74", "82": "200f25fbad7af8fb", "83": "1284897282eb47a3", "84": "d8bb296741d3d05c", "85": "7067e88534a91ab4", "86": "386c6fcc7be918e0", "87": "e5fa7717989eda80", "88": "8b5ab5232cef3457", "89": "9e3363b80eeceeea", "90": "dd1c246e19a2d032", "91": "83b0fd3b14363e4d", "92": "fced244d93677d90", "93": "f0b2b88fc2d3c999", "94": "713d04996c4627df", "95": "59c31f858c7b6970", "96": "4195478d5c528371", "97": "505ea719b994ac5e", "98": "c96276581a8c98d4", "100": "bf75bfc99d4f9e34", "101": "beb276de6301634c", "102": "9d0d3a0dba1e85c8", "103": "6ffbe07b521a061b", "104": "e114a7aebaaca2f1", "105": "af50c66d16c81bba", "106": "8bc72c179793e1c2", "107": "3877490226a78145", "108": "e0fc0a42e33bda91", "109": "4afb1715f62865b1", "110": "f1660fde8e75d368", "112": "af995dd35403b4dd", "113": "0d99f86b9e41c4bb", "114": "b9d7c266afa84437", "115": "a8dd95af4259d555", "116": "ec2ef7fbebe8cd34", "117": "679b8985ab775959", "118": "50139bb98492555c", "121": "2b037735e1f5b0f6", "122": "61b6fafa306cb441", "123": "b091e3eb82b456eb", "125": "e4f29216745ec758", "127": "006218e34885dbe0", "131": "482dbb7ec33d8c23", "132": "3daacabc5062b554", "134": "e7c90d6ef8276a1e", "137": "afdd5c2f54c40808", "139": "e594ecaa8e7d1538", "145": "92ab3e5134ed8685", "146": "01f68dfc34a29d32", "147": "0e6cad06ebfcfb29"}
+73 -119
View File
@@ -1,16 +1,16 @@
# Graph Report - juzi (2026-10-13) # Graph Report - juzi (2026-10-14)
## Corpus Check ## Corpus Check
- 118 files · ~4,910,480 words - 120 files · ~4,911,203 words
- Verdict: corpus is large enough that graph structure adds value. - Verdict: corpus is large enough that graph structure adds value.
## Summary ## Summary
- 1507 nodes · 2958 edges · 148 communities (112 shown, 36 thin omitted) - 1522 nodes · 2977 edges · 130 communities (105 shown, 25 thin omitted)
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 210 edges (avg confidence: 0.57) - Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 210 edges (avg confidence: 0.57)
- Token cost: 0 input · 0 output - Token cost: 0 input · 0 output
## Graph Freshness ## Graph Freshness
- Built from commit: `73813061` - Built from commit: `61a7f7c7`
- Run `git rev-parse HEAD` and compare to check if the graph is stale. - Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost). - Run `graphify update .` after code changes (no API cost).
@@ -41,7 +41,7 @@
- facepp-skinanalyze(旷视皮肤分析专业版调用技能) - facepp-skinanalyze(旷视皮肤分析专业版调用技能)
- 巨子测肤 · 1'me 语音播报内容设计文档 V1.0 - 巨子测肤 · 1'me 语音播报内容设计文档 V1.0
- mobile-select.iife.min.js - mobile-select.iife.min.js
- .extractWeights - NeuralNetwork
- gen_voice.py - gen_voice.py
- C - C
- face-api.js 本地化依赖(h5/vendor/faceapi/) - face-api.js 本地化依赖(h5/vendor/faceapi/)
@@ -54,12 +54,12 @@
- FaceLandmark68Net - FaceLandmark68Net
- UI交互界面调整 - UI交互界面调整
- 第四轮反馈修改与验收 - 第四轮反馈修改与验收
- .loadFromDisk - ao
- layer_at - layer_at
- ComputeAllFaceDescriptorsTask - .run
- start-local.sh - start-local.sh
- report-persistence.mjs - report-persistence.mjs
- PredictAllFaceExpressionsWithFaceAlignmentTask - .forwardInput
- extractFaceTensors - extractFaceTensors
- 已修改 - 已修改
- mobile-select/README.md - mobile-select/README.md
@@ -74,7 +74,7 @@
- 报告布局与跟手交互调整 - 报告布局与跟手交互调整
- 提交前 Review(2026-10-11) - 提交前 Review(2026-10-11)
- e2e-browser.mjs - e2e-browser.mjs
- isValidNumber - 综合得分:三维度按 4:3:3 加权
- 微信登录接入与公网部署说明 - 微信登录接入与公网部署说明
- h5/README.md - h5/README.md
- ui-20261009.mjs - ui-20261009.mjs
@@ -99,13 +99,12 @@
- 傅艳NEW与项思炼的会话 2026年10月12日.md - 傅艳NEW与项思炼的会话 2026年10月12日.md
- wi - wi
- 客户反馈修改(2026-10-12) - 客户反馈修改(2026-10-12)
- .runAndExtendWithFaceDetection - .runAndExtendWithFaceDetections
- 第三轮反馈:PSD 对齐、验收及上线记录 - 第三轮反馈:PSD 对齐、验收及上线记录
- login-performance.mjs - login-performance.mjs
- production-loop.mjs - production-loop.mjs
- account-client.js - account-client.js
- .mul - TinyYolov2
- ComputeSingleFaceDescriptorTask
- .draw - .draw
- wechat-fixture.cjs - wechat-fixture.cjs
- DetectSingleFaceLandmarksTask - DetectSingleFaceLandmarksTask
@@ -115,36 +114,19 @@
- profile-interaction.mjs - profile-interaction.mjs
- submitPhotos - submitPhotos
- startCamera - startCamera
- extractWeightsFactory
- 微信退出登录-2026-10-13.md - 微信退出登录-2026-10-13.md
- normalize - normalize
- .run - extractAllFacesAndComputeResults
- play - play
- f - f
- PredictSingleFaceExpressionsWithFaceAlignmentTask
- PredictAllFaceExpressionsTask
- wechat-browser.mjs - wechat-browser.mjs
- doCapture - doCapture
- 评分圈内居中、基础信息页固定与正脸语音修复 - 评分圈内居中、基础信息页固定与正脸语音修复
- FaceProcessor
- 报告等待页实拍背景-2026-10-12.md - 报告等待页实拍背景-2026-10-12.md
- PredictSingleFaceExpressionsTask
- NeuralNetwork
- TinyYolov2Base
- FaceFeatureExtractor
- 原始肌龄显示与鸿蒙滚轮兼容 - 原始肌龄显示与鸿蒙滚轮兼容
- harmony-picker.mjs - harmony-picker.mjs
- .toString
- AgeGenderNet
- convLayer
- height - height
- age
- recommendation.js - recommendation.js
- .forwardInput
- FaceRecognitionNet
- .runAndExtendWithFaceDetections
- TinyXception
- FaceExpressionNet
- PredictAllAgeAndGenderWithFaceAlignmentTask - PredictAllAgeAndGenderWithFaceAlignmentTask
- PredictSingleAgeAndGenderWithFaceAlignmentTask - PredictSingleAgeAndGenderWithFaceAlignmentTask
- 拍摄角度不合格震动提醒-2026-10-13.md - 拍摄角度不合格震动提醒-2026-10-13.md
@@ -176,11 +158,11 @@
## Import Cycles ## Import Cycles
- None detected. - None detected.
## Communities (148 total, 36 thin omitted) ## Communities (130 total, 25 thin omitted)
### Community 0 - "face-api.js" ### Community 0 - "face-api.js"
Cohesion: 0.02 Cohesion: 0.02
Nodes (42): boxPredictionLayer(), cn(), computeReshapedDimensions(), createBrowserEnv(), createFileSystem(), createNodejsEnv(), decodeBoxesLayer(), depthwiseConvLayer() (+34 more) Nodes (41): bgrToRgbTensor(), cn(), computeReshapedDimensions(), createBrowserEnv(), createFileSystem(), createNodejsEnv(), Dimensions(), dr() (+33 more)
### Community 1 - "线上闭环测试(不含真实微信授权)" ### Community 1 - "线上闭环测试(不含真实微信授权)"
Cohesion: 0.25 Cohesion: 0.25
@@ -191,32 +173,32 @@ Cohesion: 0.12
Nodes (17): cancelSkinDrag(), decode(), drain(), goSkin(), load(), loginAllowed(), loginWechat(), preload() (+9 more) Nodes (17): cancelSkinDrag(), decode(), drain(), goSkin(), load(), loginAllowed(), loginWechat(), preload() (+9 more)
### Community 3 - "skin-score.js" ### Community 3 - "skin-score.js"
Cohesion: 0.10 Cohesion: 0.08
Nodes (49): acneGrade(), buildViewModel(), calcAcne(), calcContour(), calcSens(), calcSmooth(), calcTone(), calcWrinkles() (+41 more) Nodes (55): acneGrade(), buildViewModel(), calcAcne(), calcContour(), calcSens(), calcSmooth(), calcTone(), calcWrinkles() (+47 more)
### Community 4 - "customer-feedback-second.js" ### Community 4 - "customer-feedback-second.js"
Cohesion: 0.14 Cohesion: 0.14
Nodes (11): assert, context, events, face, fs, geometry, original, path (+3 more) Nodes (11): assert, context, events, face, fs, geometry, original, path (+3 more)
### Community 5 - "disposeUnusedWeightTensors" ### Community 5 - "disposeUnusedWeightTensors"
Cohesion: 0.12 Cohesion: 0.09
Nodes (23): disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$1(), extractParamsFromWeigthMap$2() (+15 more) Nodes (23): disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$1(), extractParamsFromWeigthMap$2() (+15 more)
### Community 6 - "n" ### Community 6 - "n"
Cohesion: 0.12 Cohesion: 0.11
Nodes (36): ae(), be(), bf(), bi(), ce(), de(), e(), Ef() (+28 more) Nodes (43): a(), ae(), be(), bf(), bi(), ce(), de(), e() (+35 more)
### Community 7 - "test/profile-store.js" ### Community 7 - "test/profile-store.js"
Cohesion: 0.14 Cohesion: 0.06
Nodes (13): assert, data, empty, now, other, Profile, quota, regions (+5 more) Nodes (21): age(), clean(), create(), key(), assert, data, empty, now (+13 more)
### Community 8 - "admin.js" ### Community 8 - "admin.js"
Cohesion: 0.20 Cohesion: 0.19
Nodes (29): ageText(), api(), bind(), captureQualityHtml(), closeDetail(), collectFilters(), dimsHtml(), doRetry() (+21 more) Nodes (30): ageText(), api(), bind(), captureQualityHtml(), closeDetail(), collectFilters(), dimsHtml(), doRetry() (+22 more)
### Community 9 - "__awaiter" ### Community 9 - "__awaiter"
Cohesion: 0.14 Cohesion: 0.14
Nodes (13): __awaiter(), extendWithFaceDescriptor(), extendWithFaceExpressions(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult(), fetchImage(), fetchJson(), fetchNetWeights() (+5 more) Nodes (12): __awaiter(), ComposableTask(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow(), __generator(), getModelUris() (+4 more)
### Community 10 - "k" ### Community 10 - "k"
Cohesion: 0.18 Cohesion: 0.18
@@ -228,7 +210,7 @@ Nodes (20): atomic(), body(), cookie(), cookies(), crypto, fs, json(), login() (
### Community 12 - "server.js" ### Community 12 - "server.js"
Cohesion: 0.08 Cohesion: 0.08
Nodes (52): accounts, appendRecord(), callFacepp(), CaptureDiagnostics, CRED, crypto, csvCell(), DATA_DIR (+44 more) Nodes (53): accounts, appendRecord(), callFacepp(), CaptureDiagnostics, CRED, crypto, csvCell(), DATA_DIR (+45 more)
### Community 13 - "files" ### Community 13 - "files"
Cohesion: 0.12 Cohesion: 0.12
@@ -267,8 +249,8 @@ Cohesion: 0.19
Nodes (19): awaitMediaLoaded(), bufferToImage(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractImagePatches(), getContext2dOrThrow(), getEnv() (+11 more) Nodes (19): awaitMediaLoaded(), bufferToImage(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractImagePatches(), getContext2dOrThrow(), getEnv() (+11 more)
### Community 22 - "i" ### Community 22 - "i"
Cohesion: 0.08 Cohesion: 0.11
Nodes (31): ba(), bn(), ca(), cl(), da(), dn(), ee(), et() (+23 more) Nodes (21): ba(), ca(), da(), dn(), ee(), et(), fa(), i() (+13 more)
### Community 23 - "facepp-skinanalyze(旷视皮肤分析专业版调用技能)" ### Community 23 - "facepp-skinanalyze(旷视皮肤分析专业版调用技能)"
Cohesion: 0.20 Cohesion: 0.20
@@ -282,17 +264,17 @@ Nodes (8): 1. 音频格式约定(已定), 2. 播报清单(14 条), 3. 播放策
Cohesion: 0.20 Cohesion: 0.20
Nodes (16): _arrayLikeToArray(), _arrayWithHoles(), _arrayWithoutHoles(), _classCallCheck(), _createClass(), _defineProperties(), h(), _iterableToArray() (+8 more) Nodes (16): _arrayLikeToArray(), _arrayWithHoles(), _arrayWithoutHoles(), _classCallCheck(), _createClass(), _defineProperties(), h(), _iterableToArray() (+8 more)
### Community 26 - ".extractWeights" ### Community 26 - "NeuralNetwork"
Cohesion: 0.22 Cohesion: 0.07
Nodes (10): createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory(), extractorsFactory$1() (+2 more) Nodes (27): createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory(), extractFCParamsFactory() (+19 more)
### Community 27 - "gen_voice.py" ### Community 27 - "gen_voice.py"
Cohesion: 0.30 Cohesion: 0.30
Nodes (11): ndarray, Path, gen_countdown(), main(), normalize_clip(), 倒计时单独按字生成再拼接,避免「三、二、一」整句被读成播报腔、与提示音气质漂移。 与提示音共用同一 speaker + instruct;句间留约…, 裁掉过长首尾静音,保留 ≤150ms 边沿,避免提示显迟钝。, 响度归一 −16 LUFS + 转 MP3 48k CBR mono 22050Hz。 (+3 more) Nodes (11): ndarray, Path, gen_countdown(), main(), normalize_clip(), 倒计时单独按字生成再拼接,避免「三、二、一」整句被读成播报腔、与提示音气质漂移。 与提示音共用同一 speaker + instruct;句间留约…, 裁掉过长首尾静音,保留 ≤150ms 边沿,避免提示显迟钝。, 响度归一 −16 LUFS + 转 MP3 48k CBR mono 22050Hz。 (+3 more)
### Community 28 - "C" ### Community 28 - "C"
Cohesion: 0.10 Cohesion: 0.09
Nodes (28): ao(), Bo(), C(), cd(), co(), Do(), eo(), fo() (+20 more) Nodes (32): bn(), Bo(), C(), cd(), cl(), co(), eo(), fn() (+24 more)
### Community 29 - "face-api.js 本地化依赖(h5/vendor/faceapi/)" ### Community 29 - "face-api.js 本地化依赖(h5/vendor/faceapi/)"
Cohesion: 0.29 Cohesion: 0.29
@@ -322,14 +304,18 @@ Nodes (4): 1meAI测肤旷视UI问题反馈, UI交互界面调整, 交互, **设
Cohesion: 0.22 Cohesion: 0.22
Nodes (8): PSD 来源, 前端参数及限制, 后台校准留痕, 已确认并实现, 生产部署及只读验收, 皱纹(紧塑力)原文, 第四轮反馈修改与验收, 验证 Nodes (8): PSD 来源, 前端参数及限制, 后台校准留痕, 已确认并实现, 生产部署及只读验收, 皱纹(紧塑力)原文, 第四轮反馈修改与验收, 验证
### Community 41 - ".loadFromDisk" ### Community 41 - "ao"
Cohesion: 0.29 Cohesion: 0.33
Nodes (3): ComposableTask(), getModelUris(), TinyFaceDetector() Nodes (6): ao(), Do(), io(), oo(), so(), To()
### Community 42 - "layer_at" ### Community 42 - "layer_at"
Cohesion: 0.18 Cohesion: 0.18
Nodes (11): list, bounds(), layer_at(), main(), save_image(), composite(), main(), sha256() (+3 more) Nodes (11): list, bounds(), layer_at(), main(), save_image(), composite(), main(), sha256() (+3 more)
### Community 43 - ".run"
Cohesion: 0.22
Nodes (3): ComputeAllFaceDescriptorsTask(), ComputeSingleFaceDescriptorTask(), extendWithFaceDescriptor()
### Community 44 - "start-local.sh" ### Community 44 - "start-local.sh"
Cohesion: 0.50 Cohesion: 0.50
Nodes (3): HOST, PORT, start-local.sh script Nodes (3): HOST, PORT, start-local.sh script
@@ -338,9 +324,13 @@ Nodes (3): HOST, PORT, start-local.sh script
Cohesion: 0.29 Cohesion: 0.29
Nodes (6): child, dataRoot, fx, h5, ReportUI, require Nodes (6): child, dataRoot, fx, h5, ReportUI, require
### Community 46 - ".forwardInput"
Cohesion: 0.22
Nodes (9): boxPredictionLayer(), decodeBoxesLayer(), depthwiseConvLayer(), getCenterCoordinatesAndSizesLayer(), getStridesForLayerIdx(), mobileNetV1(), outputLayer(), pointwiseConvLayer() (+1 more)
### Community 47 - "extractFaceTensors" ### Community 47 - "extractFaceTensors"
Cohesion: 0.10 Cohesion: 0.11
Nodes (11): Box(), DetectAllFaceLandmarksTask(), extendWithFaceDetection(), extendWithFaceLandmarks(), extractFaceTensors(), FaceLandmarks(), isWithFaceDetection(), isWithFaceLandmarks() (+3 more) Nodes (8): Box(), DetectAllFaceLandmarksTask(), extendWithFaceLandmarks(), extractFaceTensors(), FaceLandmarks(), isTensor3D(), minBbox(), ObjectDetection()
### Community 48 - "已修改" ### Community 48 - "已修改"
Cohesion: 0.22 Cohesion: 0.22
@@ -367,8 +357,8 @@ Cohesion: 0.60
Nodes (5): diffCtx(), loadJSON(), main(), okNum(), okStr() Nodes (5): diffCtx(), loadJSON(), main(), okNum(), okStr()
### Community 58 - ".forwardInput" ### Community 58 - ".forwardInput"
Cohesion: 0.16 Cohesion: 0.17
Nodes (9): iou(), nonMaxSuppression(), nonMaxSuppression$1(), scale(), SsdMobilenetv1(), stage1(), stage2(), stage3() (+1 more) Nodes (9): FaceRecognitionNet(), iou(), nonMaxSuppression(), nonMaxSuppression$1(), scale(), SsdMobilenetv1(), stage1(), stage2() (+1 more)
### Community 61 - "报告布局与跟手交互调整" ### Community 61 - "报告布局与跟手交互调整"
Cohesion: 0.33 Cohesion: 0.33
@@ -382,9 +372,9 @@ Nodes (4): 仍需公网/业务确认, 已发现并修复, 提交前 Review(202
Cohesion: 0.29 Cohesion: 0.29
Nodes (4): EXPECTED, fails, pageErrors, pwRoot Nodes (4): EXPECTED, fails, pageErrors, pwRoot
### Community 64 - "isValidNumber" ### Community 64 - "综合得分:三维度按 4:3:3 加权"
Cohesion: 0.33 Cohesion: 0.40
Nodes (5): bgrToRgbTensor(), Dimensions(), isValidNumber(), isValidProbablitiy(), isWithGender() Nodes (4): 已确认公式, 本地验证, 生效位置, 综合得分:三维度按 4:3:3 加权
### Community 65 - "微信登录接入与公网部署说明" ### Community 65 - "微信登录接入与公网部署说明"
Cohesion: 0.29 Cohesion: 0.29
@@ -450,9 +440,9 @@ Nodes (9): Ci(), Ei(), Fi(), Ii(), ki(), pe(), ri(), wi() (+1 more)
Cohesion: 0.33 Cohesion: 0.33
Nodes (5): 公网部署与复测, 客户反馈修改(2026-10-12), 皱纹数据说明, 逐项处理, 验证 Nodes (5): 公网部署与复测, 客户反馈修改(2026-10-12), 皱纹数据说明, 逐项处理, 验证
### Community 93 - ".runAndExtendWithFaceDetection" ### Community 93 - ".runAndExtendWithFaceDetections"
Cohesion: 0.33 Cohesion: 0.17
Nodes (5): allFacesMtcnn(), allFacesSsdMobilenetv1(), allFacesTinyYolov2(), detectAllFaces(), DetectSingleFaceTask() Nodes (10): allFacesMtcnn(), allFacesSsdMobilenetv1(), allFacesTinyYolov2(), detectAllFaces(), DetectAllFacesTask(), DetectSingleFaceTask(), extendWithFaceDetection(), isWithFaceDetection() (+2 more)
### Community 94 - "第三轮反馈:PSD 对齐、验收及上线记录" ### Community 94 - "第三轮反馈:PSD 对齐、验收及上线记录"
Cohesion: 0.33 Cohesion: 0.33
@@ -466,14 +456,14 @@ Nodes (3): artifacts, errors, samples
Cohesion: 0.53 Cohesion: 0.53
Nodes (8): browser(), clearGuestCache(), ensureGuest(), guest(), init(), logout(), rememberKind(), request() Nodes (8): browser(), clearGuestCache(), ensureGuest(), guest(), init(), logout(), rememberKind(), request()
### Community 98 - ".mul"
Cohesion: 0.50
Nodes (3): wl(), wn(), xn()
### Community 101 - "wechat-fixture.cjs" ### Community 101 - "wechat-fixture.cjs"
Cohesion: 0.50 Cohesion: 0.50
Nodes (3): crypto, fs, path Nodes (3): crypto, fs, path
### Community 102 - "DetectSingleFaceLandmarksTask"
Cohesion: 0.16
Nodes (3): DetectSingleFaceLandmarksTask(), PredictAllFaceExpressionsWithFaceAlignmentTask(), PredictSingleFaceExpressionsWithFaceAlignmentTask()
### Community 107 - "基础信息记忆与肤质拖动" ### Community 107 - "基础信息记忆与肤质拖动"
Cohesion: 0.40 Cohesion: 0.40
Nodes (4): 回归命令, 基础信息记忆与肤质拖动, 肤质交互, 范围 Nodes (4): 回归命令, 基础信息记忆与肤质拖动, 肤质交互, 范围
@@ -490,25 +480,21 @@ Nodes (12): bindBackdropClose(), closeLoginSuccess(), closeOverlay(), handleCamH
Cohesion: 0.21 Cohesion: 0.21
Nodes (12): beginDetect(), detTick(), enableFallback(), getCamStream(), loadFaceApi(), loadModels(), maybeStartDetect(), onCamTrackEnded() (+4 more) Nodes (12): beginDetect(), detTick(), enableFallback(), getCamStream(), loadFaceApi(), loadModels(), maybeStartDetect(), onCamTrackEnded() (+4 more)
### Community 111 - "extractWeightsFactory"
Cohesion: 0.17
Nodes (8): extractorsFactory(), extractorsFactory$4(), extractParams(), extractParams$5(), extractParams$6(), extractParamsTiny(), extractWeightsFactory(), TinyFaceFeatureExtractor()
### Community 115 - "normalize" ### Community 115 - "normalize"
Cohesion: 0.27 Cohesion: 0.07
Nodes (8): conv(), denseBlock3(), denseBlock4(), depthwiseSeparableConv(), mainBlock(), normalize(), reductionBlock(), __spreadArrays() Nodes (28): conv(), conv$1(), convDown(), convLayer(), convLayer$1(), convNoRelu(), convWithBatchNorm(), denseBlock3() (+20 more)
### Community 116 - ".run" ### Community 116 - "extractAllFacesAndComputeResults"
Cohesion: 0.29 Cohesion: 0.14
Nodes (4): extendWithAge(), extendWithGender(), PredictAllAgeAndGenderTask(), PredictSingleAgeAndGenderTask() Nodes (9): extendWithAge(), extendWithFaceExpressions(), extendWithGender(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult(), PredictAllAgeAndGenderTask(), PredictAllFaceExpressionsTask(), PredictSingleAgeAndGenderTask() (+1 more)
### Community 117 - "play" ### Community 117 - "play"
Cohesion: 0.33 Cohesion: 0.33
Nodes (6): applyMirror(), busyWithFlow(), effMirror(), handleCamVisible(), play(), renderCamStep() Nodes (6): applyMirror(), busyWithFlow(), effMirror(), handleCamVisible(), play(), renderCamStep()
### Community 118 - "f" ### Community 118 - "f"
Cohesion: 0.20 Cohesion: 0.11
Nodes (24): a(), cf(), d(), drawContour(), dt(), ea(), f(), g() (+16 more) Nodes (25): cf(), d(), drawContour(), dt(), ea(), f(), FaceLandmarks5(), FaceMatch() (+17 more)
### Community 121 - "wechat-browser.mjs" ### Community 121 - "wechat-browser.mjs"
Cohesion: 0.22 Cohesion: 0.22
@@ -522,64 +508,32 @@ Nodes (24): armNudge(), captureFromVideo(), captureHaptic(), captureStageHint(),
Cohesion: 0.40 Cohesion: 0.40
Nodes (4): 发布, 范围, 评分圈内居中、基础信息页固定与正脸语音修复, 验证 Nodes (4): 发布, 范围, 评分圈内居中、基础信息页固定与正脸语音修复, 验证
### Community 124 - "FaceProcessor"
Cohesion: 0.22
Nodes (3): extractParams$1(), FaceLandmark68TinyNet(), FaceProcessor()
### Community 129 - "TinyYolov2Base"
Cohesion: 0.29
Nodes (5): convWithBatchNorm(), depthwiseSeparableConv$1(), leaky(), TinyYolov2Base(), validateConfig()
### Community 130 - "FaceFeatureExtractor"
Cohesion: 0.22
Nodes (5): extractFCParamsFactory(), extractorsFactory$8(), extractParams$3(), extractParams$7(), FaceFeatureExtractor()
### Community 131 - "原始肌龄显示与鸿蒙滚轮兼容" ### Community 131 - "原始肌龄显示与鸿蒙滚轮兼容"
Cohesion: 0.50 Cohesion: 0.50
Nodes (3): 原始肌龄显示与鸿蒙滚轮兼容, 回归, 范围 Nodes (3): 原始肌龄显示与鸿蒙滚轮兼容, 回归, 范围
### Community 133 - ".toString"
Cohesion: 0.20
Nodes (7): FaceLandmarks5(), FaceMatch(), getCenterPoint(), la(), pr(), pt(), vt()
### Community 136 - "convLayer"
Cohesion: 0.43
Nodes (8): convLayer(), fullyConnectedLayer(), ONet(), PNet(), prelu(), qh(), RNet(), sharedLayer()
### Community 137 - "height" ### Community 137 - "height"
Cohesion: 0.38 Cohesion: 0.24
Nodes (5): height(), FaceLandmark68NetBase(), getSizesForScale(), normalize$1(), rescaleAndNormalize() Nodes (6): height(), FaceLandmark68NetBase(), getSizesForScale(), normalize$1(), rescaleAndNormalize(), TinyYolov2Base()
### Community 138 - "age"
Cohesion: 0.43
Nodes (4): age(), clean(), create(), key()
### Community 139 - "recommendation.js" ### Community 139 - "recommendation.js"
Cohesion: 0.33 Cohesion: 0.33
Nodes (6): assert, check(), missing, raw, SS, UI Nodes (6): assert, check(), missing, raw, SS, UI
### Community 140 - ".forwardInput"
Cohesion: 0.48
Nodes (6): conv$1(), convDown(), convLayer$1(), convNoRelu(), residual(), residualDown()
### Community 141 - "FaceRecognitionNet"
Cohesion: 0.33
Nodes (3): extractorsFactory$2(), extractParams$4(), FaceRecognitionNet()
## Knowledge Gaps ## Knowledge Gaps
- **345 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+340 more) - **353 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+348 more)
These have ≤1 connection - possible missing edges or undocumented components. These have ≤1 connection - possible missing edges or undocumented components.
- **36 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes. - **25 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions ## Suggested Questions
_Questions this graph is uniquely positioned to answer:_ _Questions this graph is uniquely positioned to answer:_
- **Why does `height()` connect `height` to `app.js`, `customer-feedback.js`, `scan-placeholder.mjs`, `__awaiter`, `profile-interaction.mjs`, `showScreen`, `extractFaceTensors`, `extractFaces`, `.forwardInput`?** - **Why does `height()` connect `height` to `app.js`, `customer-feedback.js`, `scan-placeholder.mjs`, `profile-interaction.mjs`, `showScreen`, `extractFaceTensors`, `extractFaces`, `.forwardInput`?**
_High betweenness centrality (0.068) - this node is a cross-community bridge._ _High betweenness centrality (0.061) - this node is a cross-community bridge._
- **Why does `v()` connect `f` to `report-ui.js`, `vizCompose`, `C`, `face-api.js`?** - **Why does `v()` connect `f` to `report-ui.js`, `vizCompose`, `C`, `face-api.js`?**
_High betweenness centrality (0.046) - this node is a cross-community bridge._ _High betweenness centrality (0.044) - this node is a cross-community bridge._
- **Why does `fill()` connect `report-ui.js` to `f`?** - **Why does `fill()` connect `report-ui.js` to `f`?**
_High betweenness centrality (0.037) - this node is a cross-community bridge._ _High betweenness centrality (0.038) - this node is a cross-community bridge._
- **Are the 24 inferred relationships involving `n()` (e.g. with `newRecordId()` and `ae()`) actually correct?** - **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._ _`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?** - **Are the 27 inferred relationships involving `r()` (e.g. with `handleExport()` and `handleRecordsQuery()`) actually correct?**
@@ -587,4 +541,4 @@ _Questions this graph is uniquely positioned to answer:_
- **Are the 9 inferred relationships involving `i()` (e.g. with `da()` and `f()`) actually correct?** - **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._ _`i()` has 9 INFERRED edges - model-reasoned connections that need verification._
- **What connects `source`, `document_size`, `pages` to the rest of the system?** - **What connects `source`, `document_size`, `pages` to the rest of the system?**
_345 weakly-connected nodes found - possible documentation gaps or missing edges._ _353 weakly-connected nodes found - possible documentation gaps or missing edges._
File diff suppressed because one or more lines are too long
+1635 -1251
View File
File diff suppressed because it is too large Load Diff
+26 -16
View File
@@ -40,8 +40,8 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/js/app.js": { "h5/js/app.js": {
"mtime": 1791884874.9650493, "mtime": 1791957908.9912808,
"ast_hash": "1e78662941dfedadd108f24c820676cc", "ast_hash": "f61716926b4be670fc6910e1b72fdb17",
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/js/head-pose.js": { "h5/js/head-pose.js": {
@@ -50,13 +50,13 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/js/skin-score.js": { "h5/js/skin-score.js": {
"mtime": 1791878308.0809805, "mtime": 1791957908.9912808,
"ast_hash": "59d3331a75a32837e04a21afa24fb34a", "ast_hash": "80c2520d2e3099b7aca3ffb80561b4ef",
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/server.js": { "h5/server.js": {
"mtime": 1791878239.2009728, "mtime": 1791957908.9912808,
"ast_hash": "fd843f8a1ed62e33c8294acbe78b56d0", "ast_hash": "08d8569d26fa2d5baa8654a951c1d200",
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/test/build_fixtures.py": { "h5/test/build_fixtures.py": {
@@ -80,8 +80,8 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/test/regression.js": { "h5/test/regression.js": {
"mtime": 1791735501.3654072, "mtime": 1791957908.995252,
"ast_hash": "4a712e295c115b6051726cd028e67be2", "ast_hash": "cc246a0c8e98c605d10d75c3b8120f2d",
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/vendor/faceapi/face-api.js": { "h5/vendor/faceapi/face-api.js": {
@@ -135,8 +135,8 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/README.md": { "h5/README.md": {
"mtime": 1791878573.716988, "mtime": 1791958083.102039,
"ast_hash": "74712e76c626625674a70aa0dcad5a63", "ast_hash": "9cc46d638c5ce35d9545c8ff8bcb2a15",
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/index.html": { "h5/index.html": {
@@ -815,8 +815,8 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/js/admin.js": { "h5/js/admin.js": {
"mtime": 1791806985.2188702, "mtime": 1791958083.102039,
"ast_hash": "d9d5781dd607497f2803404be4e23e7a", "ast_hash": "896e771978fa1b4fc07128dcef3123c1",
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/js/region-data.js": { "h5/js/region-data.js": {
@@ -860,8 +860,8 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/admin.html": { "h5/admin.html": {
"mtime": 1791735369.9312572, "mtime": 1791957908.9912808,
"ast_hash": "ddfe588d49452e3973e21b080e61dbd7", "ast_hash": "fe728437f4205fcca110afa0aca1d99a",
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/vendor/mobile-select/README.md": { "h5/vendor/mobile-select/README.md": {
@@ -1260,8 +1260,8 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/test/report-persistence.mjs": { "h5/test/report-persistence.mjs": {
"mtime": 1791878308.0849807, "mtime": 1791958553.4716814,
"ast_hash": "00d39c0fc96afe463fb1fbfe08b70656", "ast_hash": "c26d054e330aca635b6bfc08a03e4ea0",
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/test/ui-20261009.mjs": { "h5/test/ui-20261009.mjs": {
@@ -2028,5 +2028,15 @@
"mtime": 1791885450.2930646, "mtime": 1791885450.2930646,
"ast_hash": "fcbbc5db7241f97579efaf14eaa5135e", "ast_hash": "fcbbc5db7241f97579efaf14eaa5135e",
"semantic_hash": "" "semantic_hash": ""
},
"h5/test/weighted-total.js": {
"mtime": 1791957989.9934402,
"ast_hash": "60d8d205e8ddc5a68121507541918503",
"semantic_hash": ""
},
"docs/\u7efc\u5408\u5f97\u5206\u4e09\u7ef4\u5ea6\u52a0\u6743-2026-10-14.md": {
"mtime": 1791959087.1679206,
"ast_hash": "f1d733b0970ac8756c01b11cf0e3fb1f",
"semantic_hash": ""
} }
} }
+1 -1
View File
@@ -94,7 +94,7 @@ node server.js # 默认 8080,可用 PORT=xxx 修改
- S = 7 + 3×P/100;档位:良好(P≥90)/改善(80–90)/预警(<80);生物可视年龄直接展示算法原始肌龄,不再按实际年龄 ±3 修正;三视图版 18 条话术; - S = 7 + 3×P/100;档位:良好(P≥90)/改善(80–90)/预警(<80);生物可视年龄直接展示算法原始肌龄,不再按实际年龄 ±3 修正;三视图版 18 条话术;
- 精华推荐三重判据(均分档/最差项约束/年龄组档)取最强档,triggers 记录命中判据。 - 精华推荐三重判据(均分档/最差项约束/年龄组档)取最强档,triggers 记录命中判据。
4. 前端 `SkinScore.buildViewModel(data.result, data.side, { realAge, skinType })` → viewModel 直渲报告页: 4. 前端 `SkinScore.buildViewModel(data.result, data.side, { realAge, skinType })` → viewModel 直渲报告页:
- 头部:综合评分(`vm.total`,缺失回退三维度 S 均分×10)、肤龄读取 `SkinScore.reportSkinAge(vm, result)`;历史报告同样读取原始肌龄而非旧修正展示值,原值缺失显示 `--`;三维度 S(1 位小数)+ 档位配色(良好绿/改善琥珀/预警红); - 头部:综合得分通过 `SkinScore.weightedTotal(vm.dims)` 计算,紧塑力×4+透亮感×3+稳韧力×3,使用维度S分数(两位小数)加权、四舍五入取整数,不使用接口 `total_score`;任一维度缺失显示 `--`。后台列表、详情、CSV采用相同公式,历史报告按已保存维度计算展示,不改写原文件;肤龄读取 `SkinScore.reportSkinAge(vm, result)`,原值缺失显示 `--`;三维度 S(1 位小数)+ 档位配色(良好绿/改善琥珀/预警红);
- 三个 Tab 面板各展示所辖两分类:分类名、S + 档位徽章、话术正文(`vm.cats.*.text`);P 值不直接展示,`P=null` 的分类显示"本次未检出有效数据"; - 三个 Tab 面板各展示所辖两分类:分类名、S + 档位徽章、话术正文(`vm.cats.*.text`);P 值不直接展示,`P=null` 的分类显示"本次未检出有效数据";
- 报告底部:降级标注(`vm.notes` 中与侧脸/下颌线/补拍相关的条目,去掉接口质量码后展示)、护理建议五段(`vm.careAdvice`)、精华推荐卡(`vm.essence.name` + 命中判据;推荐正文已在护理建议第 4 段,卡内不重复); - 报告底部:降级标注(`vm.notes` 中与侧脸/下颌线/补拍相关的条目,去掉接口质量码后展示)、护理建议五段(`vm.careAdvice`)、精华推荐卡(`vm.essence.name` + 命中判据;推荐正文已在护理建议第 4 段,卡内不重复);
- 近期优先关注 = 最差维度(`vm.focus`);6 分类各配检测可视化图:轮廓=正脸实拍、皱纹=正脸压暗+`texture_enhanced_lines` 皱纹增强线(线图层几乎全透明,直挂白底不可读,故与压暗沉脸合成)、光滑=正脸+色斑/痣/痤疮标记叠加、肤色=`brown_area` 色沉图、敏感=`red_area` 红区图、痘痘=正脸+闭口粉刺/痤疮标记叠加(marks 按原像素坐标叠加到正脸实拍)。 - 近期优先关注 = 最差维度(`vm.focus`);6 分类各配检测可视化图:轮廓=正脸实拍、皱纹=正脸压暗+`texture_enhanced_lines` 皱纹增强线(线图层几乎全透明,直挂白底不可读,故与压暗沉脸合成)、光滑=正脸+色斑/痣/痤疮标记叠加、肤色=`brown_area` 色沉图、敏感=`red_area` 红区图、痘痘=正脸+闭口粉刺/痤疮标记叠加(marks 按原像素坐标叠加到正脸实拍)。
+1 -1
View File
@@ -72,7 +72,7 @@
<thead> <thead>
<tr> <tr>
<th>照片</th><th>记录ID</th><th>年龄/性别/地区/肤质</th><th>拍摄状态</th><th>上传状态</th> <th>照片</th><th>记录ID</th><th>年龄/性别/地区/肤质</th><th>拍摄状态</th><th>上传状态</th>
<th>检测状态</th><th>总分</th><th>肌龄(原始 / 展示)</th><th>三维度评分 / 档位</th><th>推荐精华</th><th>耗时</th><th>创建时间</th> <th>检测状态</th><th title="紧塑力×4+透亮感×3+稳韧力×3,四舍五入取整数">综合得分</th><th>肌龄(原始 / 展示)</th><th>三维度评分 / 档位</th><th>推荐精华</th><th>耗时</th><th>创建时间</th>
</tr> </tr>
</thead> </thead>
<tbody></tbody> <tbody></tbody>
+4 -2
View File
@@ -22,6 +22,7 @@ function fmtTime(iso) {
} }
function fmtMs(ms) { return ms == null ? '-' : ms + 'ms'; } function fmtMs(ms) { return ms == null ? '-' : ms + 'ms'; }
function fmtScore(v) { return v == null ? '-' : v; } function fmtScore(v) { return v == null ? '-' : v; }
function fmtTotal(v) { return v == null ? '--' : v; }
var LV_CLASS = { '预警': 'lv-bad', '改善': 'lv-warn', '良好': 'lv-good' }; var LV_CLASS = { '预警': 'lv-bad', '改善': 'lv-warn', '良好': 'lv-good' };
var DIM_NAMES = ReportUI.dimNames; var DIM_NAMES = ReportUI.dimNames;
@@ -145,7 +146,7 @@ function loadRecords() {
'<td>' + esc(it.upload_status) + '</td>' + '<td>' + esc(it.upload_status) + '</td>' +
'<td><span class="st ' + (ST_CLASS[it.detect_status] || '') + '">' + esc(it.detect_status) + '</span>' + '<td><span class="st ' + (ST_CLASS[it.detect_status] || '') + '">' + esc(it.detect_status) + '</span>' +
(it.fail_code ? '<div class="sub">' + esc(it.fail_code) + '</div>' : '') + '</td>' + (it.fail_code ? '<div class="sub">' + esc(it.fail_code) + '</div>' : '') + '</td>' +
'<td><b>' + fmtScore(it.total_score) + '</b></td>' + '<td><b>' + fmtTotal(it.total_score) + '</b></td>' +
'<td>' + skinAgeHtml(it) + '</td>' + '<td>' + skinAgeHtml(it) + '</td>' +
'<td>' + dimsHtml(it.dims) + '</td>' + '<td>' + dimsHtml(it.dims) + '</td>' +
'<td>' + esc(productName(it.essence)) + '<div class="sub">' + esc(ruleLabel(it)) + '</div></td>' + '<td>' + esc(productName(it.essence)) + '<div class="sub">' + esc(ruleLabel(it)) + '</div></td>' +
@@ -405,13 +406,14 @@ function renderDetail(data) {
/* 成功:评分 + 文案 */ /* 成功:评分 + 文案 */
if (r.detect_status === '成功') { if (r.detect_status === '成功') {
h += dSec('综合', '<div class="d-kv">' + h += dSec('综合', '<div class="d-kv">' +
'<span>总分 <b>' + fmtScore(r.total_score) + '</b></span>' + '<span>综合得分 <b>' + fmtTotal(r.total_score) + '</b></span>' +
'<span>原始肌龄(新推荐依据) <b>' + ageText(r.skin_age) + '</b></span>' + '<span>原始肌龄(新推荐依据) <b>' + ageText(r.skin_age) + '</b></span>' +
'<span>前端展示肌龄 <b>' + ageText(r.skin_age_display) + '</b></span>' + '<span>前端展示肌龄 <b>' + ageText(r.skin_age_display) + '</b></span>' +
'<span>六分类均分S <b>' + fmtScore(r.avg_s) + '</b></span>' + '<span>六分类均分S <b>' + fmtScore(r.avg_s) + '</b></span>' +
'<span>左下颌线角 <b>' + (r.jawline_angle_l != null ? r.jawline_angle_l + '°' : '-') + '</b></span>' + '<span>左下颌线角 <b>' + (r.jawline_angle_l != null ? r.jawline_angle_l + '°' : '-') + '</b></span>' +
'<span>右下颌线角 <b>' + (r.jawline_angle_r != null ? r.jawline_angle_r + '°' : '-') + '</b></span>' + '<span>右下颌线角 <b>' + (r.jawline_angle_r != null ? r.jawline_angle_r + '°' : '-') + '</b></span>' +
'</div>'); '</div>');
h += '<p class="sub">综合得分=紧塑力×4+透亮感×3+稳韧力×3,按已计算的维度S分数(两位小数)加权,四舍五入取整数;任一维度缺失不生成综合得分,不使用接口总分。</p>';
h += '<p class="sub">前端展示肌龄沿用实际年龄±3岁的修正;新推荐只使用原始肌龄。历史报告保留当时的推荐。</p>'; h += '<p class="sub">前端展示肌龄沿用实际年龄±3岁的修正;新推荐只使用原始肌龄。历史报告保留当时的推荐。</p>';
/* 6 分类表格 */ /* 6 分类表格 */
+2 -7
View File
@@ -1487,13 +1487,8 @@
function renderReport(vm, frontPhoto, sidePhoto, payload) { function renderReport(vm, frontPhoto, sidePhoto, payload) {
reportOwner = AccountClient.user() && AccountClient.user().id; reportOwner = AccountClient.user() && AccountClient.user().id;
// 头部:综合评分(vm.total 缺失时回退三维度 S 均分×10) // Historical snapshots also use their saved dimensions, never the old API total.
var total = vm.total; var total = SkinScore.weightedTotal(vm.dims);
if (total == null) {
var ds = [vm.dims.firm.S, vm.dims.vita.S, vm.dims.stab.S], sum = 0, n = 0, i;
for (i = 0; i < 3; i++) if (typeof ds[i] === 'number') { sum += ds[i]; n++; }
if (n) total = Math.round(sum / n * 10);
}
// 环形综合评分:满分 100,环长 2πr(r=52)≈326.73 // 环形综合评分:满分 100,环长 2πr(r=52)≈326.73
$('#stat-score').innerHTML = total != null ? '<b>' + total + '</b><i>分</i>' : '<b>--</b><i>分</i>'; $('#stat-score').innerHTML = total != null ? '<b>' + total + '</b><i>分</i>' : '<b>--</b><i>分</i>';
var ring = $('#ring-fg'); var ring = $('#ring-fg');
+16 -2
View File
@@ -63,6 +63,18 @@
} }
function round2(x) { return Math.round((x + 1e-9) * 100) / 100; } // 1e-9 修正 9.445 类浮点 function round2(x) { return Math.round((x + 1e-9) * 100) / 100; } // 1e-9 修正 9.445 类浮点
function sRaw(P) { return 7 + 3 * P / 100; } function sRaw(P) { return 7 + 3 * P / 100; }
// One shared 100-point total for H5, saved reports and admin. Never use the
// upstream total or reweight an incomplete set of dimensions.
function weightedTotal(dims) {
if (!dims) return null;
var keys = ['firm', 'vita', 'stab'], weights = [4, 3, 3], total = 0;
for (var i = 0; i < keys.length; i++) {
var score = dims[keys[i]] && dims[keys[i]].S;
if (!isNum(score) || score < 0 || score > 10) return null;
total += score * weights[i];
}
return Math.round(total + 1e-9);
}
function sShow(P) { // 展示层钳制:P 可为负(肤色扣分/大面积敏感),S 会 <7,与文档「S 7.0–10.0」口径矛盾; function sShow(P) { // 展示层钳制:P 可为负(肤色扣分/大面积敏感),S 会 <7,与文档「S 7.0–10.0」口径矛盾;
if (P == null) return null; // 仅钳展示值,推荐边界比较仍用未舍入维度 S 原值 if (P == null) return null; // 仅钳展示值,推荐边界比较仍用未舍入维度 S 原值
var s = sRaw(P); var s = sRaw(P);
@@ -814,7 +826,8 @@
careAdvice.push({ tag: '生活方式', text: worst ? LIFESTYLE[worst.catKey] : '' }); careAdvice.push({ tag: '生活方式', text: worst ? LIFESTYLE[worst.catKey] : '' });
return { return {
total: pick(r, 'total_score'), total: weightedTotal(dims),
totalRule: 'dimensions-433-v1',
skinAgeRaw: skinAge, skinAgeRaw: skinAge,
skinAgeDisplay: skinAgeDisplay, skinAgeDisplay: skinAgeDisplay,
ageClampNote: ageClampNote, ageClampNote: ageClampNote,
@@ -902,6 +915,7 @@
return val(result && result.skin_age); return val(result && result.skin_age);
} }
return { return {
weightedTotal: weightedTotal,
reportSkinAge: reportSkinAge, reportSkinAge: reportSkinAge,
buildViewModel: buildViewModel, buildViewModel: buildViewModel,
recommendProduct: recommendProduct, recommendProduct: recommendProduct,
@@ -952,7 +966,7 @@ if (typeof require === 'function' && typeof module !== 'undefined' && require.ma
ok(vm.avgS === 9.32, '六分类S均分 9.32'); ok(vm.avgS === 9.32, '六分类S均分 9.32');
ok(vm.worst.catKey === 'wrinkles' && vm.worst.P === 23, '最差分类 = 皱纹'); ok(vm.worst.catKey === 'wrinkles' && vm.worst.P === 23, '最差分类 = 皱纹');
ok(vm.focus.name === '紧塑力', '近期优先关注 = 最差维度紧塑力'); ok(vm.focus.name === '紧塑力', '近期优先关注 = 最差维度紧塑力');
ok(vm.total === 79, '综合评分 = score_info.total_score'); ok(vm.total === 88, '综合得分 = round(7.69×4+9.45×3+9.55×3) = 88,不使用接口总分79');
ok(vm.summary.indexOf('【综合】') === 0 && vm.summary.indexOf('25+束紧精华') > 0, '综合文案生成'); ok(vm.summary.indexOf('【综合】') === 0 && vm.summary.indexOf('25+束紧精华') > 0, '综合文案生成');
ok(vm.cats.smooth.text.indexOf('粗大毛孔296颗、黑头39颗') >= 0 && vm.cats.smooth.text.indexOf('纹理分87') >= 0, ok(vm.cats.smooth.text.indexOf('粗大毛孔296颗、黑头39颗') >= 0 && vm.cats.smooth.text.indexOf('纹理分87') >= 0,
'光滑话术含粗大毛孔/黑头/纹理分变量(分区计数 4 区求和 296)'); '光滑话术含粗大毛孔/黑头/纹理分变量(分区计数 4 区求和 296)');
+8 -4
View File
@@ -281,6 +281,7 @@ function fillSuccess(rec, fpJson, persist = true) {
rec.recommendation = vm.recommendation; rec.recommendation = vm.recommendation;
rec.presentation = ReportUI.build(vm, fpJson.result); rec.presentation = ReportUI.build(vm, fpJson.result);
rec.total_score = vm.total; rec.total_score = vm.total;
rec.total_score_rule = vm.totalRule;
rec.jawline_angle_l = jawlineAngle(fpJson.left_side_result, 'left'); rec.jawline_angle_l = jawlineAngle(fpJson.left_side_result, 'left');
rec.jawline_angle_r = jawlineAngle(fpJson.right_side_result, 'right'); rec.jawline_angle_r = jawlineAngle(fpJson.right_side_result, 'right');
rec.cats = {}; rec.copy_6 = {}; rec.cats = {}; rec.copy_6 = {};
@@ -517,6 +518,9 @@ function displaySkinAge(rec) {
// Match H5 even for old records; do not rewrite historical files. // Match H5 even for old records; do not rewrite historical files.
return SkinScore.reportSkinAge({ skinAgeRaw: rec.skin_age }); return SkinScore.reportSkinAge({ skinAgeRaw: rec.skin_age });
} }
function displayTotalScore(rec) {
return SkinScore.weightedTotal(rec.dims);
}
function listItem(rec) { function listItem(rec) {
return { return {
record_id: rec.record_id, record_id: rec.record_id,
@@ -526,7 +530,7 @@ function listItem(rec) {
capture_quality: rec.capture_quality || null, capture_quality: rec.capture_quality || null,
capture_policy_version: rec.capture_diagnostics ? rec.capture_diagnostics.version : null, capture_policy_version: rec.capture_diagnostics ? rec.capture_diagnostics.version : null,
fail_code: rec.fail_code, fail_msg: rec.fail_msg, fail_code: rec.fail_code, fail_msg: rec.fail_msg,
total_score: rec.total_score, skin_age: rec.skin_age, total_score: displayTotalScore(rec), total_score_rule: 'dimensions-433-v1', skin_age: rec.skin_age,
skin_age_display: displaySkinAge(rec), recommendation: rec.recommendation || null, skin_age_display: displaySkinAge(rec), recommendation: rec.recommendation || null,
dims: rec.dims, group: rec.group, dims: rec.dims, group: rec.group,
essence: rec.essence ? rec.essence.name : null, essence: rec.essence ? rec.essence.name : null,
@@ -614,7 +618,7 @@ function handleRecordDetail(req, res, id) {
.map(r => ({ record_id: r.record_id, detect_status: r.detect_status, created_at: r.created_at })); .map(r => ({ record_id: r.record_id, detect_status: r.detect_status, created_at: r.created_at }));
sendJson(res, 200, { sendJson(res, 200, {
ok: true, ok: true,
record: Object.assign({}, rec, { skin_age_display: displaySkinAge(rec), record: Object.assign({}, rec, { total_score: displayTotalScore(rec), total_score_rule: 'dimensions-433-v1', skin_age_display: displaySkinAge(rec),
presentation: rec.presentation || accounts.reportPresentation(rec.owner_id, rec.record_id) }), presentation: rec.presentation || accounts.reportPresentation(rec.owner_id, rec.record_id) }),
raw_json: rawJson, raw_json: rawJson,
photos: photoUrls(rec), photos: photoUrls(rec),
@@ -774,7 +778,7 @@ function handleExport(req, res, url) {
const dimS = (r, d) => (r.dims && r.dims[d]) ? r.dims[d].S : null; const dimS = (r, d) => (r.dims && r.dims[d]) ? r.dims[d].S : null;
const dimLv = (r, d) => (r.dims && r.dims[d]) ? ReportUI.levelMap[r.dims[d].level] : null; const dimLv = (r, d) => (r.dims && r.dims[d]) ? ReportUI.levelMap[r.dims[d].level] : null;
const header = ['记录ID', '创建时间', '年龄', '性别', '地区', '日常肤质', '拍摄状态', '上传状态', '检测状态', const header = ['记录ID', '创建时间', '年龄', '性别', '地区', '日常肤质', '拍摄状态', '上传状态', '检测状态',
'失败环节', '失败码', '失败信息', '总分', '原始肌龄(推荐依据)', '前端展示肌龄', '失败环节', '失败码', '失败信息', '综合得分(4:3:3)', '原始肌龄(推荐依据)', '前端展示肌龄',
'紧塑力P', '紧塑力S', '紧塑力档', '透亮感P', '透亮感S', '透亮感档', '稳韧力P', '稳韧力S', '稳韧力档', '紧塑力P', '紧塑力S', '紧塑力档', '透亮感P', '透亮感S', '透亮感档', '稳韧力P', '稳韧力S', '稳韧力档',
'均分S', '产品组别', '推荐精华', '触发判据', '耗时ms', '推荐规则版本', '推荐原因', '侧脸质量状态', '前端拍摄参数版本']; '均分S', '产品组别', '推荐精华', '触发判据', '耗时ms', '推荐规则版本', '推荐原因', '侧脸质量状态', '前端拍摄参数版本'];
const lines = [header.join(',')]; const lines = [header.join(',')];
@@ -782,7 +786,7 @@ function handleExport(req, res, url) {
lines.push([ lines.push([
r.record_id, r.created_at, r.real_age, r.gender, r.region, r.daily_skin_type, r.record_id, r.created_at, r.real_age, r.gender, r.region, r.daily_skin_type,
r.shoot_status, r.upload_status, r.detect_status, r.shoot_status, r.upload_status, r.detect_status,
r.fail_stage, r.fail_code, r.fail_msg, r.total_score, r.skin_age, displaySkinAge(r), r.fail_stage, r.fail_code, r.fail_msg, displayTotalScore(r), r.skin_age, displaySkinAge(r),
dimP(r, 'firm'), dimS(r, 'firm'), dimLv(r, 'firm'), dimP(r, 'firm'), dimS(r, 'firm'), dimLv(r, 'firm'),
dimP(r, 'vita'), dimS(r, 'vita'), dimLv(r, 'vita'), dimP(r, 'vita'), dimS(r, 'vita'), dimLv(r, 'vita'),
dimP(r, 'stab'), dimS(r, 'stab'), dimLv(r, 'stab'), dimP(r, 'stab'), dimS(r, 'stab'), dimLv(r, 'stab'),
+1 -1
View File
@@ -108,7 +108,7 @@ function main() {
okNum(tag + '.AP 左下颌线角度', fixture.side.left.left_jawline_info.left_jawline_angle, tr.jawline.left); okNum(tag + '.AP 左下颌线角度', fixture.side.left.left_jawline_info.left_jawline_angle, tr.jawline.left);
okNum(tag + '.AQ 右下颌线角度', fixture.side.right.right_jawline_info.right_jawline_angle, tr.jawline.right); okNum(tag + '.AQ 右下颌线角度', fixture.side.right.right_jawline_info.right_jawline_angle, tr.jawline.right);
okNum(tag + '.E 肌肤年龄', vm.skinAgeRaw, tr.skinAge); okNum(tag + '.E 肌肤年龄', vm.skinAgeRaw, tr.skinAge);
okNum(tag + '.F 总分', vm.total, tr.totalScore); okNum(tag + '.F 综合得分(4:3:3)', vm.total, Math.round(tr.dims.firm.S * 4 + tr.dims.vita.S * 3 + tr.dims.stab.S * 3));
// 六分类 P/S/档位 + 文案逐字 // 六分类 P/S/档位 + 文案逐字
for (var k in CAT_COLS) { for (var k in CAT_COLS) {
+42
View File
@@ -46,6 +46,9 @@ try {
assert.equal(result.ok, true); assert.equal(result.ok, true);
assert.equal(result.report_saved, true); assert.equal(result.report_saved, true);
assert(result.vm && result.presentation); assert(result.vm && result.presentation);
const expectedTotal = Math.round(result.vm.dims.firm.S * 4 + result.vm.dims.vita.S * 3 + result.vm.dims.stab.S * 3);
assert.equal(result.vm.total, expectedTotal);
assert.notEqual(expectedTotal, result.result.score_info.total_score, 'fixture distinguishes weighted score from API total');
assert(result.vm.skinAgeRaw < 32); assert(result.vm.skinAgeRaw < 32);
assert.equal(result.vm.skinAgeDisplay, result.vm.skinAgeRaw); assert.equal(result.vm.skinAgeDisplay, result.vm.skinAgeRaw);
assert.equal(result.vm.essence.tier, 1); // Raw age, never actual60. assert.equal(result.vm.essence.tier, 1); // Raw age, never actual60.
@@ -55,24 +58,32 @@ try {
assert.deepEqual(adminRecord.recommendation, result.vm.recommendation); assert.deepEqual(adminRecord.recommendation, result.vm.recommendation);
assert.equal(adminRecord.skin_age, result.vm.skinAgeRaw); assert.equal(adminRecord.skin_age, result.vm.skinAgeRaw);
assert.equal(adminRecord.skin_age_display, result.vm.skinAgeDisplay); assert.equal(adminRecord.skin_age_display, result.vm.skinAgeDisplay);
assert.equal(adminRecord.total_score, expectedTotal);
assert.equal(adminRecord.total_score_rule, 'dimensions-433-v1');
for (const key of ['firm', 'vita', 'stab']) assert.equal(adminRecord.dims[key].S, result.vm.dims[key].S); for (const key of ['firm', 'vita', 'stab']) assert.equal(adminRecord.dims[key].S, result.vm.dims[key].S);
const list = await (await api.get(base + 'api/admin/records')).json(); const list = await (await api.get(base + 'api/admin/records')).json();
assert.equal(list.items[0].skin_age, result.vm.skinAgeRaw); assert.equal(list.items[0].skin_age, result.vm.skinAgeRaw);
assert.equal(list.items[0].skin_age_display, result.vm.skinAgeRaw); assert.equal(list.items[0].skin_age_display, result.vm.skinAgeRaw);
assert.deepEqual(list.items[0].recommendation, result.vm.recommendation); assert.deepEqual(list.items[0].recommendation, result.vm.recommendation);
assert.equal(list.items[0].total_score, expectedTotal);
const csv = await (await api.get(base + 'api/admin/export')).text(); const csv = await (await api.get(base + 'api/admin/export')).text();
assert(csv.includes('原始肌龄(推荐依据),前端展示肌龄')); assert(csv.includes('原始肌龄(推荐依据),前端展示肌龄'));
assert(csv.includes(result.vm.recommendation.version)); assert(csv.includes(result.vm.recommendation.version));
assert(csv.includes('综合得分(4:3:3)'));
assert.equal(csv.trim().split('\n')[1].split(',')[12], String(expectedTotal));
const snapshot = await (await api.get(base + 'api/reports/' + result.record_id)).json(); const snapshot = await (await api.get(base + 'api/reports/' + result.record_id)).json();
assert.equal(snapshot.report.photos.length, 3); assert.equal(snapshot.report.photos.length, 3);
assert.deepEqual(snapshot.report.presentation, result.presentation); assert.deepEqual(snapshot.report.presentation, result.presentation);
assert.deepEqual(snapshot.report.payload.result.face_maps, result.result.face_maps); assert.deepEqual(snapshot.report.payload.result.face_maps, result.result.face_maps);
assert.equal(snapshot.report.photos[0], fixture.photos[0]); assert.equal(snapshot.report.photos[0], fixture.photos[0]);
assert.equal(snapshot.report.vm.total, expectedTotal);
assert.equal(JSON.parse(fs.readFileSync(path.join(dataRoot, 'records.jsonl'), 'utf8').trim()).total_score, expectedTotal, 'new records persist weighted score');
const desktop = await browser.newPage({ viewport: { width: 1600, height: 1000 } }); const desktop = await browser.newPage({ viewport: { width: 1600, height: 1000 } });
desktop.on('pageerror', e => errors.push(e.message)); desktop.on('pageerror', e => errors.push(e.message));
await desktop.goto(base + 'admin.html'); await desktop.goto(base + 'admin.html');
const row = desktop.locator('#tbl-records tbody tr').first(); const row = desktop.locator('#tbl-records tbody tr').first();
await row.waitFor(); await row.waitFor();
assert.equal(await row.locator('td').nth(6).textContent(), String(expectedTotal));
assert((await row.textContent()).includes('原始 ' + result.vm.skinAgeRaw + '岁')); assert((await row.textContent()).includes('原始 ' + result.vm.skinAgeRaw + '岁'));
assert((await row.textContent()).includes('展示 ' + result.vm.skinAgeRaw + '岁')); assert((await row.textContent()).includes('展示 ' + result.vm.skinAgeRaw + '岁'));
assert((await row.textContent()).includes("1'me 主场精华 I(25+)")); assert((await row.textContent()).includes("1'me 主场精华 I(25+)"));
@@ -92,6 +103,9 @@ try {
await row.click(); await row.click();
await desktop.waitForFunction(() => document.querySelector('#d-body').textContent.includes('推荐依据')); await desktop.waitForFunction(() => document.querySelector('#d-body').textContent.includes('推荐依据'));
const detailText = await desktop.locator('#d-body').textContent(); const detailText = await desktop.locator('#d-body').textContent();
assert(detailText.includes('综合得分 ' + expectedTotal));
assert(detailText.includes('紧塑力×4+透亮感×3+稳韧力×3'));
if (process.env.H5_ARTIFACT_DIR) await desktop.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, 'admin-weighted-detail.png') });
assert(detailText.includes(result.vm.recommendation.reason)); assert(detailText.includes(result.vm.recommendation.reason));
assert(detailText.includes('前端展示肌龄 ' + result.vm.skinAgeRaw + '岁')); assert(detailText.includes('前端展示肌龄 ' + result.vm.skinAgeRaw + '岁'));
for (const view of result.presentation.views) assert(detailText.includes(view.text)); for (const view of result.presentation.views) assert(detailText.includes(view.text));
@@ -123,6 +137,11 @@ try {
await page.click('[data-action="history"]'); await page.click('[data-action="history"]');
await page.click('[data-report-id]'); await page.click('[data-report-id]');
await page.waitForSelector('#s-report.active'); await page.waitForSelector('#s-report.active');
assert.equal(await page.locator('#stat-score b').textContent(), String(expectedTotal));
if (process.env.H5_ARTIFACT_DIR) {
await page.waitForTimeout(400); // Capture after the report's entrance fade.
await page.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, 'report-weighted-total.png') });
}
await page.waitForFunction(() => [...document.querySelectorAll('.ditem-img')].every(i => !i.hidden && i.naturalWidth)); await page.waitForFunction(() => [...document.querySelectorAll('.ditem-img')].every(i => !i.hidden && i.naturalWidth));
const imagesBefore = await page.locator('.ditem-img').evaluateAll(images => images.map(i => i.src)); const imagesBefore = await page.locator('.ditem-img').evaluateAll(images => images.map(i => i.src));
const copyBefore = await page.locator('.ditem-para').allTextContents(); const copyBefore = await page.locator('.ditem-para').allTextContents();
@@ -147,6 +166,7 @@ try {
legacy.presentation.product = { ...ReportUI.products[1], image: 'assets/img/ui-20261009/product-35.png' }; legacy.presentation.product = { ...ReportUI.products[1], image: 'assets/img/ui-20261009/product-35.png' };
legacy.presentation.views[0].text = '历史报告原文,不应按新文案重新计算。'; legacy.presentation.views[0].text = '历史报告原文,不应按新文案重新计算。';
legacy.vm.skinAgeDisplay = 57; // Old snapshot retains the retired display clamp. legacy.vm.skinAgeDisplay = 57; // Old snapshot retains the retired display clamp.
legacy.vm.total = 12; // Old stored upstream total must not control the current display.
legacy.vm.essence = { name: '35+支撑精华', text: '旧版推荐', triggers: ['最差项约束'] }; legacy.vm.essence = { name: '35+支撑精华', text: '旧版推荐', triggers: ['最差项约束'] };
delete legacy.vm.recommendation; delete legacy.vm.recommendation;
fs.writeFileSync(snapshotFile, JSON.stringify(legacy)); fs.writeFileSync(snapshotFile, JSON.stringify(legacy));
@@ -154,6 +174,7 @@ try {
delete record.presentation; delete record.presentation;
delete record.recommendation; delete record.recommendation;
record.skin_age_display = 57; record.skin_age_display = 57;
record.total_score = 12;
fs.writeFileSync(recordFile, JSON.stringify(record) + '\n'); fs.writeFileSync(recordFile, JSON.stringify(record) + '\n');
const storedBytes = fs.readFileSync(snapshotFile, 'utf8'); const storedBytes = fs.readFileSync(snapshotFile, 'utf8');
const recordBytes = fs.readFileSync(recordFile, 'utf8'); const recordBytes = fs.readFileSync(recordFile, 'utf8');
@@ -161,6 +182,12 @@ try {
await page.click('[data-report-id]'); await page.click('[data-report-id]');
await page.waitForSelector('#s-report.active'); await page.waitForSelector('#s-report.active');
assert.equal(await page.locator('#stat-age b').textContent(), String(result.vm.skinAgeRaw), 'old saved display clamp must not override original skin age'); assert.equal(await page.locator('#stat-age b').textContent(), String(result.vm.skinAgeRaw), 'old saved display clamp must not override original skin age');
assert.equal(await page.locator('#stat-score b').textContent(), String(expectedTotal), 'old H5 report uses saved dimensions, not saved upstream total');
const legacyList = await (await api.get(base + 'api/admin/records')).json();
assert.equal(legacyList.items[0].total_score, expectedTotal);
const legacyDetail = await (await api.get(base + 'api/admin/records/' + result.record_id)).json();
assert.equal(legacyDetail.record.total_score, expectedTotal);
assert.equal((await (await api.get(base + 'api/admin/export')).text()).trim().split('\n')[1].split(',')[12], String(expectedTotal));
assert.equal(await page.locator('#product-title').textContent(), ReportUI.products[1].title); assert.equal(await page.locator('#product-title').textContent(), ReportUI.products[1].title);
assert.equal(await page.locator('#product-image').getAttribute('src'), ReportUI.products[1].image, 'refresh artwork crop, never the historical recommendation/copy'); assert.equal(await page.locator('#product-image').getAttribute('src'), ReportUI.products[1].image, 'refresh artwork crop, never the historical recommendation/copy');
assert((await page.locator('.ditem-para').allTextContents()).includes(legacy.presentation.views[0].text)); assert((await page.locator('.ditem-para').allTextContents()).includes(legacy.presentation.views[0].text));
@@ -171,6 +198,21 @@ try {
assert((await desktop.locator('#d-body').textContent()).includes('前端展示肌龄 ' + result.vm.skinAgeRaw + '岁')); assert((await desktop.locator('#d-body').textContent()).includes('前端展示肌龄 ' + result.vm.skinAgeRaw + '岁'));
assert.equal(fs.readFileSync(snapshotFile, 'utf8'), storedBytes); assert.equal(fs.readFileSync(snapshotFile, 'utf8'), storedBytes);
assert.equal(fs.readFileSync(recordFile, 'utf8'), recordBytes); assert.equal(fs.readFileSync(recordFile, 'utf8'), recordBytes);
// An incomplete set must show no total everywhere, not fall back to the API score.
const missingVm = JSON.parse(JSON.stringify(legacy.vm)); missingVm.dims.stab.S = null;
const missingRec = JSON.parse(JSON.stringify(record)); missingRec.dims.stab.S = null;
fs.writeFileSync(snapshotFile, JSON.stringify({ ...legacy, vm: missingVm }));
fs.writeFileSync(recordFile, JSON.stringify(missingRec) + '\n');
await page.reload(); await page.waitForSelector('#s-splash.active');
await page.click('[data-action="history"]'); await page.click('[data-report-id]');
await page.waitForSelector('#s-report.active');
assert.equal(await page.locator('#stat-score b').textContent(), '--');
assert.equal((await (await api.get(base + 'api/admin/records')).json()).items[0].total_score, null);
assert.equal((await (await api.get(base + 'api/admin/records/' + result.record_id)).json()).record.total_score, null);
await desktop.goto(base + 'admin.html');
assert.equal(await desktop.locator('#tbl-records tbody tr').first().locator('td').nth(6).textContent(), '--');
assert.equal((await (await api.get(base + 'api/admin/export')).text()).trim().split('\n')[1].split(',')[12], '');
fs.writeFileSync(recordFile, recordBytes);
// Missing skin age must not leave the previous product visible in the H5. // Missing skin age must not leave the previous product visible in the H5.
delete legacy.payload.result.skin_age; delete legacy.payload.result.skin_age;
const SkinScore = require('../js/skin-score.js'); const SkinScore = require('../js/skin-score.js');
+31
View File
@@ -0,0 +1,31 @@
'use strict';
const assert = require('node:assert/strict');
const Score = require('../js/skin-score');
const dims = (firm, vita, stab) => ({ firm: { S: firm }, vita: { S: vita }, stab: { S: stab } });
for (const [scores, total] of [
[[8, 9, 8.5], 85], [[10, 10, 10], 100], [[7, 7, 7], 70],
[[10, 7, 7], 82], [[7, 10, 7], 79], [[7, 7, 10], 79],
[[8.01, 8.82, 8], 83], [[7.69, 8.72, 9.1], 84]
]) assert.equal(Score.weightedTotal(dims(...scores)), total, scores.join('/'));
for (const invalid of [null, undefined, NaN, Infinity, -Infinity, '9', -1, 11]) {
for (const key of ['firm', 'vita', 'stab']) {
const d = dims(8, 9, 8.5); d[key].S = invalid;
assert.equal(Score.weightedTotal(d), null, 'no reweighting invalid/missing ' + key);
}
}
for (const d of [null, {}, { firm: { S: 9 }, vita: { S: 8 } }]) assert.equal(Score.weightedTotal(d), null);
const input = JSON.parse(JSON.stringify(Score.MOCK_RESULT));
const before = JSON.stringify(input);
const expected = Score.buildViewModel(input, Score.MOCK_SIDE, {});
assert.equal(expected.totalRule, 'dimensions-433-v1');
assert.equal(JSON.stringify(input), before, 'preserve raw upstream data');
for (const value of [0, 100, null, undefined]) {
const r = JSON.parse(JSON.stringify(input));
if (value === undefined) delete r.score_info.total_score; else r.score_info.total_score = value;
const changed = Score.buildViewModel(r, Score.MOCK_SIDE, {});
assert.equal(changed.total, expected.total, 'API total has no influence');
assert.deepEqual(changed.dims, expected.dims);
assert.deepEqual(changed.recommendation, expected.recommendation);
}
assert.equal(Score.buildViewModel({}, null, {}).total, null);
console.log('PASS weighted total: 4:3:3, integer/half rounding, missing/invalid dimensions, API total ignored, source/scores/recommendation unchanged');