feat(h5): align third feedback with latest PSD and scanning screen
@@ -0,0 +1,501 @@
|
||||
{
|
||||
"source": "UI外发版10.09.psd",
|
||||
"source_sha256": "c7698f5900adc7476286818450d928e8fa2ce2e58cda09375ff2b861629fd58f",
|
||||
"document_size": [
|
||||
7330,
|
||||
16411
|
||||
],
|
||||
"native_header_px": 248,
|
||||
"design_scale": 2.88,
|
||||
"assets": [
|
||||
{
|
||||
"file": "h5/assets/img/ui-20261012/splash-slogan.png",
|
||||
"source_layers": [
|
||||
"0/5",
|
||||
"0/4"
|
||||
],
|
||||
"bbox": [
|
||||
1415,
|
||||
391,
|
||||
2133,
|
||||
608
|
||||
],
|
||||
"size": [
|
||||
718,
|
||||
217
|
||||
],
|
||||
"bytes": 12781,
|
||||
"sha256": "c9dee693eb3c1d566bb08d3770e7dd2f0c22e7d4e0b81bbe5e10ad8219b26044"
|
||||
},
|
||||
{
|
||||
"file": "h5/assets/img/ui-20261012/brand-lockup.png",
|
||||
"source_layers": [
|
||||
"0/1"
|
||||
],
|
||||
"bbox": [
|
||||
1515,
|
||||
1861,
|
||||
2032,
|
||||
1920
|
||||
],
|
||||
"size": [
|
||||
517,
|
||||
59
|
||||
],
|
||||
"bytes": 7013,
|
||||
"sha256": "3cb697d6c5542562a9cea20554b0772f2c0137b9c3977ff258908024d5a8e9ed"
|
||||
},
|
||||
{
|
||||
"file": "h5/assets/img/ui-20261012/button-start-label.png",
|
||||
"source_layers": [
|
||||
"0/3/1"
|
||||
],
|
||||
"bbox": [
|
||||
1696,
|
||||
2005,
|
||||
1857,
|
||||
2045
|
||||
],
|
||||
"size": [
|
||||
161,
|
||||
40
|
||||
],
|
||||
"bytes": 2163,
|
||||
"sha256": "fd88b174f120845d8045a78a36bc7665516d91011f7ea0ebc7fe200271e17738"
|
||||
},
|
||||
{
|
||||
"file": "h5/assets/img/ui-20261012/button-history-label.png",
|
||||
"source_layers": [
|
||||
"0/2/2/2"
|
||||
],
|
||||
"bbox": [
|
||||
1696,
|
||||
2252,
|
||||
1859,
|
||||
2291
|
||||
],
|
||||
"size": [
|
||||
163,
|
||||
39
|
||||
],
|
||||
"bytes": 2024,
|
||||
"sha256": "0253d761f63bda89301c1eec9df84352a2f5dd1aa97884e27ebf91092c0816d8"
|
||||
},
|
||||
{
|
||||
"file": "h5/assets/img/ui-20261012/product-25.webp",
|
||||
"source_layers": [
|
||||
"8/1/0"
|
||||
],
|
||||
"bbox": [
|
||||
1765,
|
||||
14677,
|
||||
2269,
|
||||
15180
|
||||
],
|
||||
"size": [
|
||||
504,
|
||||
503
|
||||
],
|
||||
"bytes": 276778,
|
||||
"sha256": "c18827342d23bbff2de04c3a2ecbeeae9b16eb96f3a9f7e30f2a66cdc21beddc"
|
||||
},
|
||||
{
|
||||
"file": "h5/assets/img/ui-20261012/product-35.webp",
|
||||
"source_layers": [
|
||||
"8/1/1"
|
||||
],
|
||||
"bbox": [
|
||||
1765,
|
||||
15228,
|
||||
2269,
|
||||
15731
|
||||
],
|
||||
"size": [
|
||||
504,
|
||||
503
|
||||
],
|
||||
"bytes": 267094,
|
||||
"sha256": "c1dc455c8072da17ac06cee386248b358947da65bd190b714f2ccb25360afea4"
|
||||
},
|
||||
{
|
||||
"file": "h5/assets/img/ui-20261012/product-50.webp",
|
||||
"source_layers": [
|
||||
"8/1/2"
|
||||
],
|
||||
"bbox": [
|
||||
1765,
|
||||
15778,
|
||||
2269,
|
||||
16281
|
||||
],
|
||||
"size": [
|
||||
504,
|
||||
503
|
||||
],
|
||||
"bytes": 331924,
|
||||
"sha256": "86647c8882caf62c43eafb1efe495bad3289126cab73a88c8963b7ad64ffa084"
|
||||
},
|
||||
{
|
||||
"file": "h5/assets/img/ui-20261012/scan-waiting.webp",
|
||||
"source_layers": [
|
||||
"9"
|
||||
],
|
||||
"bbox": [
|
||||
6250,
|
||||
5500,
|
||||
7330,
|
||||
7591
|
||||
],
|
||||
"size": [
|
||||
1080,
|
||||
2091
|
||||
],
|
||||
"bytes": 11690,
|
||||
"sha256": "6b661f3d454e9b696b70ca8846fed11243d3f53b95506c2d78b11b6b1a6d9f77"
|
||||
}
|
||||
],
|
||||
"text_styles": [
|
||||
{
|
||||
"path": "0/4",
|
||||
"text": "以我为始 自有表现",
|
||||
"bbox": [
|
||||
1502,
|
||||
545,
|
||||
2045,
|
||||
608
|
||||
],
|
||||
"transform": [
|
||||
0.7386429422796229,
|
||||
0.0,
|
||||
-1.0090361144648896e-16,
|
||||
0.7386429422796231,
|
||||
1772.6682649161658,
|
||||
602.0113961671652
|
||||
],
|
||||
"runs": [
|
||||
{
|
||||
"font": "MiSans-Demibold",
|
||||
"font_size": 90.25561,
|
||||
"leading": 104.16666
|
||||
},
|
||||
{
|
||||
"font": "MiSans-Demibold",
|
||||
"font_size": 90.25561,
|
||||
"leading": 104.16666
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "0/5",
|
||||
"text": " ONE ME,\rFROM THE INSIDE OUT",
|
||||
"bbox": [
|
||||
1415,
|
||||
391,
|
||||
2133,
|
||||
520
|
||||
],
|
||||
"transform": [
|
||||
0.7386429422796229,
|
||||
0.0,
|
||||
-1.0090361144648896e-16,
|
||||
0.7386429422796231,
|
||||
1771.9296219738862,
|
||||
442.37366417300353
|
||||
],
|
||||
"runs": [
|
||||
{
|
||||
"font": "MiSans-Regular",
|
||||
"font_size": 90.25561,
|
||||
"leading": 104.16666
|
||||
},
|
||||
{
|
||||
"font": "MiSans-Regular",
|
||||
"font_size": 90.25561,
|
||||
"leading": 104.16666
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "0/1/1",
|
||||
"text": "肌肤状态检测",
|
||||
"bbox": [
|
||||
1664,
|
||||
1862,
|
||||
2032,
|
||||
1920
|
||||
],
|
||||
"transform": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.9999999999999999,
|
||||
1662.281376176642,
|
||||
1868.3475610977566
|
||||
],
|
||||
"runs": [
|
||||
{
|
||||
"font": "MiSans-Demibold",
|
||||
"font_size": 62.5,
|
||||
"leading": null
|
||||
},
|
||||
{
|
||||
"font": "MiSans-Demibold",
|
||||
"font_size": 62.5,
|
||||
"leading": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "1/3/0",
|
||||
"text": "欢迎体验1'me\r肌肤状态评估服务!",
|
||||
"bbox": [
|
||||
2664,
|
||||
832,
|
||||
3218,
|
||||
979
|
||||
],
|
||||
"transform": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.9999999999999999,
|
||||
2662.281376176642,
|
||||
840.3475610977557
|
||||
],
|
||||
"runs": [
|
||||
{
|
||||
"font": "MiSans-Demibold",
|
||||
"font_size": 66.66666,
|
||||
"leading": 83.33334
|
||||
},
|
||||
{
|
||||
"font": "MiSans-Demibold",
|
||||
"font_size": 66.66666,
|
||||
"leading": 83.33334
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "1/3/1",
|
||||
"text": "我们将采集您的面部影像,为您生成专属肌肤分析与状态管理方案。所有数据严格保密,仅用于本次检测分析。",
|
||||
"bbox": [
|
||||
2663,
|
||||
1045,
|
||||
3361,
|
||||
1177
|
||||
],
|
||||
"transform": [
|
||||
0.9999999999999999,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
2662.281376176642,
|
||||
1049.3475610977548
|
||||
],
|
||||
"runs": [
|
||||
{
|
||||
"font": "MiSans-Regular",
|
||||
"font_size": 33.33333,
|
||||
"leading": 50.0
|
||||
},
|
||||
{
|
||||
"font": "MiSans-Regular",
|
||||
"font_size": 33.33333,
|
||||
"leading": 50.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "4/7",
|
||||
"text": "请拍摄或上传\r正面及左右侧面照片",
|
||||
"bbox": [
|
||||
1442,
|
||||
6160,
|
||||
2036,
|
||||
6307
|
||||
],
|
||||
"transform": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.9999999999999999,
|
||||
1439.281376176642,
|
||||
6167.347561097756
|
||||
],
|
||||
"runs": [
|
||||
{
|
||||
"font": "MiSans-Demibold",
|
||||
"font_size": 66.66666,
|
||||
"leading": 83.33334
|
||||
},
|
||||
{
|
||||
"font": "MiSans-Demibold",
|
||||
"font_size": 66.66666,
|
||||
"leading": 83.33334
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "4/8",
|
||||
"text": "为使测试结果更准确,请关闭美颜/滤镜、并确保光线充足均匀。",
|
||||
"bbox": [
|
||||
1443,
|
||||
6372,
|
||||
2119,
|
||||
6454
|
||||
],
|
||||
"transform": [
|
||||
0.9999999999999999,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
1442.281376176642,
|
||||
6376.347561097755
|
||||
],
|
||||
"runs": [
|
||||
{
|
||||
"font": "MiSans-Regular",
|
||||
"font_size": 33.33333,
|
||||
"leading": 50.0
|
||||
},
|
||||
{
|
||||
"font": "MiSans-Regular",
|
||||
"font_size": 33.33333,
|
||||
"leading": 50.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "8/1/0/5",
|
||||
"text": "以我本然有光蓄活\r双仓精华液",
|
||||
"bbox": [
|
||||
1362,
|
||||
14850,
|
||||
1627,
|
||||
14932
|
||||
],
|
||||
"transform": [
|
||||
1.7691030436028,
|
||||
0.0,
|
||||
0.0,
|
||||
1.769103043602818,
|
||||
1361.2813761766417,
|
||||
14854.052217587123
|
||||
],
|
||||
"runs": [
|
||||
{
|
||||
"font": "MiSans-Semibold",
|
||||
"font_size": 18.84194,
|
||||
"leading": 28.26291
|
||||
},
|
||||
{
|
||||
"font": "MiSans-Semibold",
|
||||
"font_size": 18.84194,
|
||||
"leading": 28.26291
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "8/1/0/6",
|
||||
"text": "1'me 主场精华 I",
|
||||
"bbox": [
|
||||
1363,
|
||||
14779,
|
||||
1653,
|
||||
14820
|
||||
],
|
||||
"transform": [
|
||||
1.5358216667040816,
|
||||
0.0,
|
||||
0.0,
|
||||
1.5358216667040878,
|
||||
1361.2813761766417,
|
||||
14784.058678621632
|
||||
],
|
||||
"runs": [
|
||||
{
|
||||
"font": "MiSans-Semibold",
|
||||
"font_size": 27.12989,
|
||||
"leading": 28.26291
|
||||
},
|
||||
{
|
||||
"font": "MiSans-Semibold",
|
||||
"font_size": 27.12989,
|
||||
"leading": 28.26291
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"shape_metrics": [
|
||||
{
|
||||
"path": "0/3/0",
|
||||
"bbox": [
|
||||
1516,
|
||||
1971,
|
||||
2038,
|
||||
2083
|
||||
],
|
||||
"corner_radius_psd_px": 20.000065
|
||||
},
|
||||
{
|
||||
"path": "0/2/0",
|
||||
"bbox": [
|
||||
1516,
|
||||
2104,
|
||||
2038,
|
||||
2216
|
||||
],
|
||||
"corner_radius_psd_px": 20.000065
|
||||
},
|
||||
{
|
||||
"path": "1/2/0",
|
||||
"bbox": [
|
||||
2753,
|
||||
1410,
|
||||
3275,
|
||||
1522
|
||||
],
|
||||
"corner_radius_psd_px": 20.000065
|
||||
},
|
||||
{
|
||||
"path": "4/2/0",
|
||||
"bbox": [
|
||||
1517,
|
||||
6604,
|
||||
2039,
|
||||
6716
|
||||
],
|
||||
"corner_radius_psd_px": 20.000065
|
||||
},
|
||||
{
|
||||
"path": "8/1/0/0",
|
||||
"bbox": [
|
||||
1285,
|
||||
14677,
|
||||
2269,
|
||||
15180
|
||||
],
|
||||
"corner_radius_psd_px": 15.000158
|
||||
},
|
||||
{
|
||||
"path": "1/1",
|
||||
"bbox": [
|
||||
2571,
|
||||
719,
|
||||
3458,
|
||||
1621
|
||||
],
|
||||
"corner_radius_psd_px": 24.999972
|
||||
},
|
||||
{
|
||||
"path": "4/1",
|
||||
"bbox": [
|
||||
1334,
|
||||
5971,
|
||||
2221,
|
||||
6873
|
||||
],
|
||||
"corner_radius_psd_px": 24.999972
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,78 @@
|
||||
# 第三轮反馈:PSD 对齐及本地验收
|
||||
|
||||
日期:2026-10-12。依据 `new_feedback/第三轮反馈20261012` 的客户会话、四张批注及新版 `UI外发版10.09.psd`。本轮完成本地开发,不包含 Git 提交、推送或生产部署。
|
||||
|
||||
## 已完成
|
||||
|
||||
| 范围 | 实现 |
|
||||
| --- | --- |
|
||||
| 首页/引导背景 | 直接导出 PSD 中三行口号、Logo 与中文组合,保留原始字形、字重及可见高度比例;首页两个按钮的静态文字也从 PSD 导出,不再依赖手机系统字体。 |
|
||||
| 介绍弹窗 | 完全采用用户确认的 PSD 原文,按原稿校准弹窗位置、标题、正文、勾选区及按钮;服务端保存和回看保持不变。 |
|
||||
| 拍照引导 | 固定设计稿的两行断句、字号及文本栏;弹窗固定约 308×313 CSS px,320px 窄屏也能完整展示,不机械缩小文字。 |
|
||||
| 产品卡 | 使用 Semibold 原字体,标题约 14.47px、名称约 11.57px;校准左侧约 26.74px 留白、品牌标识及纵向位置。右图按 PSD 白卡的实际可见区域导出,不再二次居中裁切。 |
|
||||
| 生成报告等待 | 三张照片拍完后显示 PSD 原始黑底、白色人脸、黄色扫描区和「检测中…」。相册/翻转图标仅是图片内容,不可点击、不可聚焦;保留明确取消、返回、失败恢复及请求完成后的报告跳转。 |
|
||||
|
||||
产品仍只展示算法推荐的一款;没有修改评分、肤龄、推荐规则、三视角顺序或自动拍摄阈值。历史报告保留原推荐和原文案,仅按已有产品档位更新展示图片的裁切,不回写历史数据。
|
||||
|
||||
用户确认的弹窗原文:
|
||||
|
||||
> 我们将采集您的面部影像,为您生成专属肌肤分析与状态管理方案。所有数据严格保密,仅用于本次检测分析。
|
||||
|
||||
引导正文的 PSD 断点(包括「确/保」之间的换行):
|
||||
|
||||
```text
|
||||
为使测试结果更准确,请关闭美颜/滤镜、并确
|
||||
保光线充足均匀。
|
||||
```
|
||||
|
||||
## 原稿与导出可追溯性
|
||||
|
||||
- 新 PSD 为 7330×16411、11 个顶层分组,新增第 9 号「扫描」,原「登录」变为第 10 号。旧导出器改为按唯一名称定位「登录」,拒绝缺失或重名,避免切图串页。
|
||||
- 本轮选择性导出脚本:`tools/export_third_feedback_assets.py`。运行方式:
|
||||
|
||||
```bash
|
||||
python3 tools/export_third_feedback_assets.py 'new_feedback/第三轮反馈20261012/UI外发版10.09.psd'
|
||||
```
|
||||
|
||||
- 原 PSD SHA-256:`c7698f5900adc7476286818450d928e8fa2ce2e58cda09375ff2b861629fd58f`。
|
||||
- `design/ui-20261012/manifest.json` 记录源文件校验、图层路径、原始坐标、字体及变换矩阵、输出尺寸和逐资源 SHA-256。测量包含文字图层的缩放,不直接把原始 FontSize 当最终字号。
|
||||
- 另读取矢量路径确定原稿圆角:弹窗 25 个设计像素、按钮 20px、产品卡 15px(375px 基准分别约 8.68/6.94/5.21 CSS px),不是继续沿用旧版估计值;产品卡宽高按 984×503 的原稿比例校准。
|
||||
- 八份前端资源位于 `h5/assets/img/ui-20261012/`;PNG/无损 WebP 均来自 PSD,不使用生成式替代图、不改人物或产品包装。原 10.09 资源和字体文件保留不动。
|
||||
- 扫描页须整体合成,保留跨图层剪贴依赖,再裁掉 248px 微信原生顶栏;单独导出图标组会漏掉人脸。导出器及浏览器测试都检查白色人脸、黄色下半脸像素。
|
||||
- 原稿预览保存在 `design/ui-20261012/page-{0,1,4,9}.webp`,仅用于核对,不将整个交互页面替换为背景截图。微信原生导航由宿主提供,不叠加进 H5。
|
||||
- 等待画面保持原始长宽比,异形屏用黑色留白适配;额外的「取消」为保留已有功能的必要入口。不新增虚构百分比、扫描动画或固定长时间等待,沿用原有至少 1.2 秒防闪及 150 秒请求预算。
|
||||
- 扫描图未加载或加载失败时仍显示「检测中…」文字并可取消,不留下无提示黑屏;正常加载后仅展示原稿文字,不重复叠字。
|
||||
- 静态品牌图规避系统字体差异;其余动态文字使用完整原始 MiSans,保留 `font-display: swap`。小字号开启几何精度渲染,减少浏览器将小数像素字宽取整造成的断行偏差;不同系统的文字抗锯齿仍可能有细微区别。
|
||||
|
||||
## 本地验证
|
||||
|
||||
已通过以下检查,未调用收费 Face++ 或改动线上账号、照片、报告:
|
||||
|
||||
- 第三轮专项:源资源校验/PSD 字体与原文、320/375/390/414/480px 弹窗和产品排版,三款推荐产品、固定断行、字体失败降级、短视口及等待页六种尺寸;实际三张上传推进、装饰图标、键盘焦点、取消/迟到响应、失败重试、成功进入报告。
|
||||
- 第二轮专项:全屏相机、三种圆角轮廓、五官辅助线、震动安全降级、450ms 自动抓拍条件、协议及报告实际字体、首屏无大字体/模型请求。
|
||||
- 原反馈回归:报告图放大、三个维度独立滑动切换、上下滚动、七张真实数据可视化及皱纹/毛孔文案。
|
||||
- 推荐规则 126 个用例、原算法 86 条断言,以及 150 秒超时/迟到回调单测。
|
||||
- 隔离服务器的报告持久化:后台/H5/CSV 一致、最新五份报告、用户隔离、历史快照不回写,包含旧图片路径升级展示的验证。
|
||||
- 登录及模拟微信:隔离 HTTPS `/state/` 回调、Secure cookie、账号切换、跨设备同账号和不同账号隔离;不是对真实手机授权的替代验收。
|
||||
- 登录性能回归:150ms 延迟、约 2Mbps 下载及 4 倍 CPU 降速下冷启动约 **1.64 秒**、资源 **264610 字节**;新增扫描图约 11.7KB,在进入档案/相机后才加载,不竞争登录首屏。
|
||||
- 切图映射 3 个单测、JS/Python 语法及 diff 空白检查。按项目约定执行 `graphify update .`(AST 更新,无付费语义提取)。
|
||||
|
||||
主要复验命令:
|
||||
|
||||
```bash
|
||||
python3 tools/test_export_psd_assets.py
|
||||
H5_ARTIFACT_DIR=/tmp/juzi-third-final node h5/test/customer-feedback-third.mjs
|
||||
node h5/test/customer-feedback-second.js
|
||||
node h5/test/customer-feedback-second.mjs
|
||||
node h5/test/customer-feedback.js
|
||||
node h5/test/customer-feedback.mjs
|
||||
node h5/test/recommendation.js
|
||||
node h5/test/regression.js
|
||||
node h5/test/analyze-timeout.js
|
||||
node h5/test/report-persistence.mjs
|
||||
node h5/test/accounts.js
|
||||
node h5/test/login-performance.mjs
|
||||
node h5/test/wechat-browser.mjs
|
||||
```
|
||||
|
||||
仍需部署后在真实手机微信中做最终视觉/相机验收。当前线上版本未改变;客户反馈原件及 PSD 未修改、未自动纳入提交。
|
||||
@@ -6,26 +6,26 @@
|
||||
"4": "customer-feedback-second.js",
|
||||
"5": "n",
|
||||
"6": ".runAndExtendWithFaceDetection",
|
||||
"7": "k",
|
||||
"7": ".toString",
|
||||
"8": "admin.js",
|
||||
"9": "__awaiter",
|
||||
"10": "mn",
|
||||
"10": "FaceLandmarks",
|
||||
"11": "lib/accounts.js",
|
||||
"12": "server.js",
|
||||
"13": "files",
|
||||
"14": ".draw",
|
||||
"15": "extractFaces",
|
||||
"16": "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范",
|
||||
"17": "FaceLandmarks68",
|
||||
"17": "k",
|
||||
"18": "1'me 在线肌肤测试 H5",
|
||||
"19": "head-pose.js",
|
||||
"20": "build_fixtures.py",
|
||||
"21": "AgeGenderNet",
|
||||
"22": "extractAllFacesAndComputeResults",
|
||||
"21": "disposeUnusedWeightTensors",
|
||||
"22": ".predictAgeAndGender",
|
||||
"23": "facepp-skinanalyze(旷视皮肤分析专业版调用技能)",
|
||||
"24": "巨子测肤 · 1'me 语音播报内容设计文档 V1.0",
|
||||
"25": "mobile-select.iife.min.js",
|
||||
"26": "wi",
|
||||
"26": ".extractWeights",
|
||||
"27": "gen_voice.py",
|
||||
"28": "C",
|
||||
"29": "face-api.js 本地化依赖(h5/vendor/faceapi/)",
|
||||
@@ -39,9 +39,8 @@
|
||||
"37": "UI 外发版 10.09 图片资源",
|
||||
"38": "FaceLandmark68Net",
|
||||
"39": "UI交互界面调整",
|
||||
"40": "extractFaceTensors",
|
||||
"41": "NeuralNetwork",
|
||||
"42": "export_psd_assets.py",
|
||||
"42": "layer_at",
|
||||
"43": "normalize",
|
||||
"44": "start-local.sh",
|
||||
"45": "PredictAllAgeAndGenderWithFaceAlignmentTask",
|
||||
@@ -60,7 +59,6 @@
|
||||
"58": ".dispose",
|
||||
"59": "call_skinanalyze.sh",
|
||||
"60": "report-fixture.js",
|
||||
"61": ".extractWeights",
|
||||
"62": "提交前 Review(2026-10-11)",
|
||||
"63": "e2e-browser.mjs",
|
||||
"64": "account-client.js",
|
||||
@@ -70,13 +68,12 @@
|
||||
"68": "analyze-timeout.js",
|
||||
"69": "misans/README.md",
|
||||
"70": ".forwardInput",
|
||||
"71": ".toString",
|
||||
"72": "build_report_copy.py",
|
||||
"73": "camera-geometry.js",
|
||||
"74": "report-copy.js",
|
||||
"75": "prepare_h5_assets.py",
|
||||
"76": ".detect",
|
||||
"77": ".runAndExtendWithFaceDetections",
|
||||
"76": "TinyYolov2Base",
|
||||
"77": "drawFaceExpressions",
|
||||
"78": "t",
|
||||
"79": "登录反馈与加载优化(2026-10-12)",
|
||||
"80": "线上微信登录部署记录",
|
||||
@@ -84,11 +81,20 @@
|
||||
"82": "wechat-browser.mjs",
|
||||
"83": "10.09 新版 H5 实施与验收",
|
||||
"84": "微信登录联调:2026-10-12",
|
||||
"86": "LabeledFaceDescriptors",
|
||||
"85": "test/accounts.js",
|
||||
"87": "产品推荐规则:肤龄基准与边界调整",
|
||||
"88": "ui-20261012/manifest.json",
|
||||
"89": "傅艳NEW与项思炼的会话 2026年10月12日.md",
|
||||
"90": "customer-feedback.mjs",
|
||||
"91": "wi",
|
||||
"92": "客户反馈修改(2026-10-12)",
|
||||
"93": ".runAndExtendWithFaceDetections",
|
||||
"94": "第三轮反馈:PSD 对齐及本地验收",
|
||||
"95": "login-performance.mjs",
|
||||
"96": "production-loop.mjs",
|
||||
"97": "PredictSingleAgeAndGenderWithFaceAlignmentTask",
|
||||
"98": "PredictSingleFaceExpressionsWithFaceAlignmentTask"
|
||||
"98": "PredictSingleFaceExpressionsWithFaceAlignmentTask",
|
||||
"99": "DetectSingleFaceLandmarksTask",
|
||||
"100": "extractAllFacesAndComputeResults",
|
||||
"106": "第三轮反馈20261012/傅艳NEW与项思炼的会话 2026年10月12日.md"
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"0": "00c97d478a8f90eb", "1": "cb6ea50bd4e4a5ca", "2": "00705ebb76871f0f", "3": "1f737ca8edb978f2", "4": "56bf12952b0de42b", "5": "93c69860c6f6563c", "6": "5a9014c4a7f87699", "7": "188dd5e33354e35d", "8": "1ecf47531e1d507b", "9": "64a6935321c48bb6", "10": "9ac0935bc0d2c1fd", "11": "ed0d13cfa8571d24", "12": "53594b653ad48743", "13": "7e3b558a587a4ebe", "14": "bf75bfc99d4f9e34", "15": "72d3264e772df1cb", "16": "95add6a975c0cb72", "17": "15606ce5d8eec23a", "18": "38bb0b185ceed116", "19": "0a373ad2197fd1a6", "20": "aecf2fce44ba49c5", "21": "224bdcd6528fddbf", "22": "e42f384dfedcff89", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "83b0fd3b14363e4d", "27": "eab427ce9a5219e9", "28": "9c6f14efe476acf4", "29": "c608f1c6492e1cb1", "30": "73c13793fca09f56", "31": "fbbcfc059751cc6a", "32": "f6bcf56c4589316b", "33": "75f750c5b7d9f63d", "34": "f024ff7942935690", "35": "c4fa292c3dfb0d0e", "36": "a7d375041779bad4", "37": "cb8f26249485324a", "38": "9a25b1230b5fe649", "39": "fdd95375b4a5ddb3", "40": "c6e2efd58425d13d", "41": "d96493a5f110bd4c", "42": "ffd960136fde00ad", "43": "5b04375ec4594cb1", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "e594ecaa8e7d1538", "48": "1e2c13775a52fdea", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "54d7329fed675057", "56": "8c9a3fc5edff7e44", "57": "f2a40b5f1b29f669", "58": "d0d4f9872c8bcc83", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "2d008ec4a8df7849", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "ce0e68dab611f022", "65": "74ae017de0ffd36b", "66": "e042d7fe7351bcb9", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "880a74c6d255e5cc", "71": "cd6f168abba94fea", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "68250611c66f576f", "77": "429cd62f9137881c", "78": "f4aae1bece83f9e0", "79": "c6cfdfe181232ecc", "80": "315325cde1352e98", "81": "37c559741825be74", "82": "2b037735e1f5b0f6", "83": "1284897282eb47a3", "84": "d8bb296741d3d05c", "86": "f8b38aeaaab11ec4", "87": "e5fa7717989eda80", "89": "9e3363b80eeceeea", "90": "dd1c246e19a2d032", "92": "fced244d93677d90", "97": "01f68dfc34a29d32", "98": "8c471c7baeaaca54"}
|
||||
{"0": "ea3f8d8f67782fdc", "1": "cb6ea50bd4e4a5ca", "2": "485914b81d357062", "3": "1f737ca8edb978f2", "4": "56bf12952b0de42b", "5": "ecc33b1dfcb6c0a4", "6": "8bd61518dd267641", "7": "cd6f168abba94fea", "8": "1ecf47531e1d507b", "9": "8abf7998360e0ccc", "10": "7fb7f7cc828b286c", "11": "4a490abd5e503318", "12": "53594b653ad48743", "13": "7e3b558a587a4ebe", "14": "bf75bfc99d4f9e34", "15": "fb002c2979ef551d", "16": "95add6a975c0cb72", "17": "09246ceabd90e01c", "18": "38bb0b185ceed116", "19": "0a373ad2197fd1a6", "20": "aecf2fce44ba49c5", "21": "254ec4cab6c4ac57", "22": "a2b13160223b5a98", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "487edffbbcedb8cb", "27": "eab427ce9a5219e9", "28": "121c45cc1e571288", "29": "c608f1c6492e1cb1", "30": "73c13793fca09f56", "31": "fbbcfc059751cc6a", "32": "f6bcf56c4589316b", "33": "75f750c5b7d9f63d", "34": "f024ff7942935690", "35": "5a17f8810dae4ff2", "36": "a7d375041779bad4", "37": "cb8f26249485324a", "38": "9a25b1230b5fe649", "39": "fdd95375b4a5ddb3", "41": "d96493a5f110bd4c", "42": "916170b9f5d7346b", "43": "e48804ba2a6c4e86", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "e594ecaa8e7d1538", "48": "cf2f2424bc00436f", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "b9ba0a5c51b155b1", "56": "8c9a3fc5edff7e44", "57": "f2a40b5f1b29f669", "58": "3ba0641876109aff", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "ce0e68dab611f022", "65": "74ae017de0ffd36b", "66": "e042d7fe7351bcb9", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "880a74c6d255e5cc", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "04c475f726d2508f", "77": "979dcc63399620ad", "78": "bf1217db5748fa4e", "79": "c6cfdfe181232ecc", "80": "315325cde1352e98", "81": "37c559741825be74", "82": "2b037735e1f5b0f6", "83": "1284897282eb47a3", "84": "d8bb296741d3d05c", "85": "4bc370be8afd7db1", "87": "e5fa7717989eda80", "88": "8b5ab5232cef3457", "89": "9e3363b80eeceeea", "90": "dd1c246e19a2d032", "91": "83b0fd3b14363e4d", "92": "fced244d93677d90", "93": "435ca504c453b5ca", "94": "70616dc5d4c69039", "95": "59c31f858c7b6970", "96": "4195478d5c528371", "97": "01f68dfc34a29d32", "98": "8c471c7baeaaca54", "99": "da489b69f7f553c4", "100": "adaed54e9cb4adfc", "106": "8bc72c179793e1c2"}
|
||||
@@ -1,16 +1,16 @@
|
||||
# Graph Report - juzi (2026-10-12)
|
||||
|
||||
## Corpus Check
|
||||
- 77 files · ~4,389,750 words
|
||||
- 83 files · ~4,627,050 words
|
||||
- Verdict: corpus is large enough that graph structure adds value.
|
||||
|
||||
## Summary
|
||||
- 1280 nodes · 2677 edges · 92 communities (75 shown, 17 thin omitted)
|
||||
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 181 edges (avg confidence: 0.53)
|
||||
- 1326 nodes · 2732 edges · 98 communities (78 shown, 20 thin omitted)
|
||||
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 183 edges (avg confidence: 0.54)
|
||||
- Token cost: 0 input · 0 output
|
||||
|
||||
## Graph Freshness
|
||||
- Built from commit: `fe9623a1`
|
||||
- Built from commit: `f3e7eac9`
|
||||
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
||||
- Run `graphify update .` after code changes (no API cost).
|
||||
|
||||
@@ -22,26 +22,26 @@
|
||||
- customer-feedback-second.js
|
||||
- n
|
||||
- .runAndExtendWithFaceDetection
|
||||
- k
|
||||
- .toString
|
||||
- admin.js
|
||||
- __awaiter
|
||||
- mn
|
||||
- FaceLandmarks
|
||||
- lib/accounts.js
|
||||
- server.js
|
||||
- files
|
||||
- .draw
|
||||
- extractFaces
|
||||
- 巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范
|
||||
- FaceLandmarks68
|
||||
- k
|
||||
- 1'me 在线肌肤测试 H5
|
||||
- head-pose.js
|
||||
- build_fixtures.py
|
||||
- AgeGenderNet
|
||||
- extractAllFacesAndComputeResults
|
||||
- disposeUnusedWeightTensors
|
||||
- .predictAgeAndGender
|
||||
- facepp-skinanalyze(旷视皮肤分析专业版调用技能)
|
||||
- 巨子测肤 · 1'me 语音播报内容设计文档 V1.0
|
||||
- mobile-select.iife.min.js
|
||||
- wi
|
||||
- .extractWeights
|
||||
- gen_voice.py
|
||||
- C
|
||||
- face-api.js 本地化依赖(h5/vendor/faceapi/)
|
||||
@@ -53,9 +53,8 @@
|
||||
- UI 外发版 10.09 图片资源
|
||||
- FaceLandmark68Net
|
||||
- UI交互界面调整
|
||||
- extractFaceTensors
|
||||
- NeuralNetwork
|
||||
- export_psd_assets.py
|
||||
- layer_at
|
||||
- normalize
|
||||
- start-local.sh
|
||||
- PredictAllAgeAndGenderWithFaceAlignmentTask
|
||||
@@ -71,7 +70,6 @@
|
||||
- regression.js
|
||||
- .dispose
|
||||
- call_skinanalyze.sh
|
||||
- .extractWeights
|
||||
- 提交前 Review(2026-10-11)
|
||||
- e2e-browser.mjs
|
||||
- account-client.js
|
||||
@@ -81,10 +79,9 @@
|
||||
- analyze-timeout.js
|
||||
- misans/README.md
|
||||
- .forwardInput
|
||||
- .toString
|
||||
- build_report_copy.py
|
||||
- .detect
|
||||
- .runAndExtendWithFaceDetections
|
||||
- TinyYolov2Base
|
||||
- drawFaceExpressions
|
||||
- t
|
||||
- 登录反馈与加载优化(2026-10-12)
|
||||
- 线上微信登录部署记录
|
||||
@@ -92,12 +89,21 @@
|
||||
- wechat-browser.mjs
|
||||
- 10.09 新版 H5 实施与验收
|
||||
- 微信登录联调:2026-10-12
|
||||
- LabeledFaceDescriptors
|
||||
- test/accounts.js
|
||||
- 产品推荐规则:肤龄基准与边界调整
|
||||
- ui-20261012/manifest.json
|
||||
- 傅艳NEW与项思炼的会话 2026年10月12日.md
|
||||
- wi
|
||||
- 客户反馈修改(2026-10-12)
|
||||
- .runAndExtendWithFaceDetections
|
||||
- 第三轮反馈:PSD 对齐及本地验收
|
||||
- login-performance.mjs
|
||||
- production-loop.mjs
|
||||
- PredictSingleAgeAndGenderWithFaceAlignmentTask
|
||||
- PredictSingleFaceExpressionsWithFaceAlignmentTask
|
||||
- DetectSingleFaceLandmarksTask
|
||||
- extractAllFacesAndComputeResults
|
||||
- 第三轮反馈20261012/傅艳NEW与项思炼的会话 2026年10月12日.md
|
||||
|
||||
## God Nodes (most connected - your core abstractions)
|
||||
1. `__awaiter()` - 59 edges
|
||||
@@ -112,6 +118,8 @@
|
||||
10. `loadRecords()` - 20 edges
|
||||
|
||||
## Surprising Connections (you probably didn't know these)
|
||||
- `actualLines()` --indirect_call--> `el()` [INFERRED]
|
||||
h5/test/customer-feedback-third.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]
|
||||
@@ -120,17 +128,15 @@
|
||||
h5/js/app.js → h5/vendor/faceapi/face-api.js
|
||||
- `vizCompose()` --indirect_call--> `rh()` [INFERRED]
|
||||
h5/js/app.js → h5/vendor/faceapi/face-api.js
|
||||
- `vizCompose()` --indirect_call--> `scale()` [INFERRED]
|
||||
h5/js/app.js → h5/vendor/faceapi/face-api.js
|
||||
|
||||
## Import Cycles
|
||||
- None detected.
|
||||
|
||||
## Communities (92 total, 17 thin omitted)
|
||||
## Communities (98 total, 20 thin omitted)
|
||||
|
||||
### Community 0 - "face-api.js"
|
||||
Cohesion: 0.02
|
||||
Nodes (43): ce(), cn(), computeReshapedDimensions(), createBrowserEnv(), createFileSystem(), createNodejsEnv(), Dimensions(), dr() (+35 more)
|
||||
Nodes (37): bgrToRgbTensor(), cn(), computeReshapedDimensions(), createBrowserEnv(), createFileSystem(), createNodejsEnv(), Dimensions(), dr() (+29 more)
|
||||
|
||||
### Community 1 - "线上闭环测试(不含真实微信授权)"
|
||||
Cohesion: 0.25
|
||||
@@ -138,7 +144,7 @@ Nodes (7): 发现并修复, 可复现命令, 已通过的检查, 线上闭环测
|
||||
|
||||
### Community 2 - "app.js"
|
||||
Cohesion: 0.05
|
||||
Nodes (81): applyMirror(), armNudge(), beginDetect(), bindBackdropClose(), busyWithFlow(), captureFromVideo(), captureHaptic(), checkStill() (+73 more)
|
||||
Nodes (82): applyMirror(), armNudge(), beginDetect(), bindBackdropClose(), busyWithFlow(), captureFromVideo(), captureHaptic(), checkStill() (+74 more)
|
||||
|
||||
### Community 3 - "skin-score.js"
|
||||
Cohesion: 0.12
|
||||
@@ -149,16 +155,16 @@ Cohesion: 0.17
|
||||
Nodes (10): assert, context, face, fs, geometry, original, path, source (+2 more)
|
||||
|
||||
### Community 5 - "n"
|
||||
Cohesion: 0.17
|
||||
Nodes (39): a(), ae(), be(), bf(), cf(), d(), de(), drawContour() (+31 more)
|
||||
Cohesion: 0.15
|
||||
Nodes (36): a(), ae(), bf(), cf(), d(), drawContour(), dt(), ea() (+28 more)
|
||||
|
||||
### Community 6 - ".runAndExtendWithFaceDetection"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): allFacesMtcnn(), allFacesSsdMobilenetv1(), allFacesTinyYolov2(), detectAllFaces(), DetectSingleFaceTask()
|
||||
Cohesion: 0.23
|
||||
Nodes (6): allFacesMtcnn(), allFacesSsdMobilenetv1(), allFacesTinyYolov2(), DetectAllFaceLandmarksTask(), detectAllFaces(), DetectSingleFaceTask()
|
||||
|
||||
### Community 7 - "k"
|
||||
Cohesion: 0.18
|
||||
Nodes (11): b(), gn(), k(), kn(), ne(), tt(), wo(), Yt() (+3 more)
|
||||
### Community 7 - ".toString"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): FaceMatch(), la(), pr(), pt(), vt()
|
||||
|
||||
### Community 8 - "admin.js"
|
||||
Cohesion: 0.21
|
||||
@@ -166,15 +172,15 @@ Nodes (28): ageText(), api(), bind(), closeDetail(), collectFilters(), dimsHtml(
|
||||
|
||||
### Community 9 - "__awaiter"
|
||||
Cohesion: 0.14
|
||||
Nodes (13): __awaiter(), bufferToImage(), ComposableTask(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow(), __generator() (+5 more)
|
||||
Nodes (12): __awaiter(), ComposableTask(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow(), __generator(), getModelUris() (+4 more)
|
||||
|
||||
### Community 10 - "mn"
|
||||
Cohesion: 0.17
|
||||
Nodes (11): Lu(), mn(), Point(), Pu(), Uu(), vn(), Vu(), wl() (+3 more)
|
||||
### Community 10 - "FaceLandmarks"
|
||||
Cohesion: 0.09
|
||||
Nodes (8): FaceLandmark68NetBase(), FaceLandmarks(), FaceLandmarks5(), FaceLandmarks68(), getCenterPoint(), minBbox(), Point(), wl()
|
||||
|
||||
### Community 11 - "lib/accounts.js"
|
||||
Cohesion: 0.07
|
||||
Nodes (34): atomic(), body(), cookie(), cookies(), crypto, fs, json(), login() (+26 more)
|
||||
Cohesion: 0.17
|
||||
Nodes (20): atomic(), body(), cookie(), cookies(), crypto, fs, json(), login() (+12 more)
|
||||
|
||||
### Community 12 - "server.js"
|
||||
Cohesion: 0.08
|
||||
@@ -185,16 +191,16 @@ 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.21
|
||||
Nodes (17): awaitMediaLoaded(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractImagePatches(), getContext2dOrThrow(), getEnv(), getMediaDimensions() (+9 more)
|
||||
Cohesion: 0.12
|
||||
Nodes (22): awaitMediaLoaded(), Box(), bufferToImage(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractFaceTensors(), extractImagePatches() (+14 more)
|
||||
|
||||
### Community 16 - "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范"
|
||||
Cohesion: 0.15
|
||||
Nodes (12): 1. 基准与尺寸换算, 2. 色彩规范, 3. 字体规范, 4. 组件切图(assets/svg/), 5. 图片素材(assets/images/,共 15 张), 6. 页面清单(design/ 下有整屏 PNG 预览), 7. 目录结构, 8. 还原建议 (+4 more)
|
||||
|
||||
### Community 17 - "FaceLandmarks68"
|
||||
Cohesion: 0.19
|
||||
Nodes (3): FaceLandmarks5(), FaceLandmarks68(), getCenterPoint()
|
||||
### Community 17 - "k"
|
||||
Cohesion: 0.11
|
||||
Nodes (21): b(), ba(), be(), ca(), da(), fa(), jt(), k() (+13 more)
|
||||
|
||||
### Community 18 - "1'me 在线肌肤测试 H5"
|
||||
Cohesion: 0.13
|
||||
@@ -208,13 +214,13 @@ Nodes (8): assertPoints(), check(), clamp(), dist(), estimate(), makeFrontal(),
|
||||
Cohesion: 0.40
|
||||
Nodes (9): build_case1(), build_case2(), build_truth(), col(), dump(), main(), parse_value(), set_path() (+1 more)
|
||||
|
||||
### Community 21 - "AgeGenderNet"
|
||||
Cohesion: 0.05
|
||||
Nodes (28): AgeGenderNet(), disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$1() (+20 more)
|
||||
### Community 21 - "disposeUnusedWeightTensors"
|
||||
Cohesion: 0.08
|
||||
Nodes (24): disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$1(), extractParamsFromWeigthMap$2() (+16 more)
|
||||
|
||||
### Community 22 - "extractAllFacesAndComputeResults"
|
||||
Cohesion: 0.09
|
||||
Nodes (12): ComputeAllFaceDescriptorsTask(), ComputeSingleFaceDescriptorTask(), extendWithAge(), extendWithFaceDescriptor(), extendWithFaceExpressions(), extendWithGender(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult() (+4 more)
|
||||
### Community 22 - ".predictAgeAndGender"
|
||||
Cohesion: 0.28
|
||||
Nodes (4): extendWithAge(), extendWithGender(), PredictAllAgeAndGenderTask(), PredictSingleAgeAndGenderTask()
|
||||
|
||||
### Community 23 - "facepp-skinanalyze(旷视皮肤分析专业版调用技能)"
|
||||
Cohesion: 0.20
|
||||
@@ -228,17 +234,17 @@ Nodes (8): 1. 音频格式约定(已定), 2. 播报清单(14 条), 3. 播放策
|
||||
Cohesion: 0.20
|
||||
Nodes (16): _arrayLikeToArray(), _arrayWithHoles(), _arrayWithoutHoles(), _classCallCheck(), _createClass(), _defineProperties(), h(), _iterableToArray() (+8 more)
|
||||
|
||||
### Community 26 - "wi"
|
||||
Cohesion: 0.28
|
||||
Nodes (9): Ci(), Ei(), Fi(), Ii(), ki(), pe(), ri(), wi() (+1 more)
|
||||
### Community 26 - ".extractWeights"
|
||||
Cohesion: 0.05
|
||||
Nodes (29): AgeGenderNet(), createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory() (+21 more)
|
||||
|
||||
### Community 27 - "gen_voice.py"
|
||||
Cohesion: 0.30
|
||||
Nodes (11): ndarray, Path, gen_countdown(), main(), normalize_clip(), 倒计时单独按字生成再拼接,避免「三、二、一」整句被读成播报腔、与提示音气质漂移。 与提示音共用同一 speaker + instruct;句间留约…, 裁掉过长首尾静音,保留 ≤150ms 边沿,避免提示显迟钝。, 响度归一 −16 LUFS + 转 MP3 48k CBR mono 22050Hz。 (+3 more)
|
||||
|
||||
### Community 28 - "C"
|
||||
Cohesion: 0.10
|
||||
Nodes (22): ao(), Bo(), C(), cd(), co(), Do(), eo(), fo() (+14 more)
|
||||
Cohesion: 0.09
|
||||
Nodes (31): ao(), C(), cd(), co(), Do(), eo(), fo(), Ft() (+23 more)
|
||||
|
||||
### Community 29 - "face-api.js 本地化依赖(h5/vendor/faceapi/)"
|
||||
Cohesion: 0.29
|
||||
@@ -253,8 +259,8 @@ Cohesion: 0.60
|
||||
Nodes (4): main(), 递归缩放 result 内全部坐标:{left,top,width,height} 矩形与 {x,y} 点,与原数据同为整数。, scale_coords(), to_data_url()
|
||||
|
||||
### Community 35 - "customer-feedback.js"
|
||||
Cohesion: 0.11
|
||||
Nodes (17): alternate, assert, crop, details, excellent, fs, model, modelBefore (+9 more)
|
||||
Cohesion: 0.06
|
||||
Nodes (26): alternate, assert, crop, details, excellent, fs, model, modelBefore (+18 more)
|
||||
|
||||
### Community 37 - "UI 外发版 10.09 图片资源"
|
||||
Cohesion: 0.40
|
||||
@@ -264,16 +270,12 @@ Nodes (4): UI 外发版 10.09 图片资源, 前端素材对应, 文件位置,
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 1meAI测肤旷视UI问题反馈, UI交互界面调整, 交互, **设计**
|
||||
|
||||
### Community 40 - "extractFaceTensors"
|
||||
Cohesion: 0.08
|
||||
Nodes (10): bgrToRgbTensor(), Box(), DetectAllFaceLandmarksTask(), DetectSingleFaceLandmarksTask(), extendWithFaceLandmarks(), extractFaceTensors(), FaceLandmarks(), isTensor3D() (+2 more)
|
||||
|
||||
### Community 42 - "export_psd_assets.py"
|
||||
Cohesion: 0.70
|
||||
Nodes (4): bounds(), layer_at(), main(), save_image()
|
||||
### Community 42 - "layer_at"
|
||||
Cohesion: 0.18
|
||||
Nodes (11): list, bounds(), layer_at(), main(), save_image(), composite(), main(), sha256() (+3 more)
|
||||
|
||||
### Community 43 - "normalize"
|
||||
Cohesion: 0.07
|
||||
Cohesion: 0.09
|
||||
Nodes (27): conv(), conv$1(), convDown(), convLayer(), convLayer$1(), convNoRelu(), convWithBatchNorm(), denseBlock3() (+19 more)
|
||||
|
||||
### Community 44 - "start-local.sh"
|
||||
@@ -285,8 +287,8 @@ Cohesion: 0.38
|
||||
Nodes (6): assert, check(), missing, raw, SS, UI
|
||||
|
||||
### Community 48 - "已修改"
|
||||
Cohesion: 0.25
|
||||
Nodes (7): 12 日第二批:明确项修改, 准备捕捉提醒, 字重, 已修改, 拍照展示, 首页高清批注, 验证与状态
|
||||
Cohesion: 0.22
|
||||
Nodes (8): 12 日第二批:明确项修改, 公网部署与复核(2026-10-12,北京时间), 准备捕捉提醒, 字重, 已修改, 拍照展示, 首页高清批注, 验证与状态
|
||||
|
||||
### Community 53 - "护理建议(全量文本,按需求摘抄)"
|
||||
Cohesion: 0.10
|
||||
@@ -297,8 +299,8 @@ Cohesion: 0.11
|
||||
Nodes (17): 1. 结论和依据, 2. 统一比较口径, 3. 逐页差异和调整内容, 4. 报告数据如何接到新 UI, 5. 登录与“我的报告”的真实能力缺口, 6. 原稿中需要整理的内容, 7. 实施顺序与交付物, 8. 验收定义 (+9 more)
|
||||
|
||||
### Community 55 - "i"
|
||||
Cohesion: 0.08
|
||||
Nodes (31): ba(), bn(), ca(), cl(), da(), dn(), ee(), et() (+23 more)
|
||||
Cohesion: 0.11
|
||||
Nodes (20): bn(), dn(), ee(), et(), fn(), i(), je(), ln() (+12 more)
|
||||
|
||||
### Community 56 - "report-ui.js"
|
||||
Cohesion: 0.50
|
||||
@@ -309,12 +311,8 @@ Cohesion: 0.60
|
||||
Nodes (5): diffCtx(), loadJSON(), main(), okNum(), okStr()
|
||||
|
||||
### Community 58 - ".dispose"
|
||||
Cohesion: 0.21
|
||||
Nodes (9): FaceRecognitionNet(), getSizesForScale(), nonMaxSuppression(), normalize$1(), rescaleAndNormalize(), scale(), stage1(), stage2() (+1 more)
|
||||
|
||||
### Community 61 - ".extractWeights"
|
||||
Cohesion: 0.09
|
||||
Nodes (26): createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory(), extractFCParamsFactory() (+18 more)
|
||||
Cohesion: 0.14
|
||||
Nodes (13): extendWithFaceLandmarks(), getSizesForScale(), iou(), nonMaxSuppression(), nonMaxSuppression$1(), normalize$1(), ObjectDetection(), rescaleAndNormalize() (+5 more)
|
||||
|
||||
### Community 62 - "提交前 Review(2026-10-11)"
|
||||
Cohesion: 0.40
|
||||
@@ -344,21 +342,13 @@ Nodes (7): assert, end, fs, path, source, start, vm
|
||||
Cohesion: 0.22
|
||||
Nodes (9): boxPredictionLayer(), decodeBoxesLayer(), depthwiseConvLayer(), getCenterCoordinatesAndSizesLayer(), getStridesForLayerIdx(), mobileNetV1(), outputLayer(), pointwiseConvLayer() (+1 more)
|
||||
|
||||
### Community 71 - ".toString"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): FaceMatch(), la(), pr(), pt(), vt()
|
||||
|
||||
### Community 76 - ".detect"
|
||||
Cohesion: 0.15
|
||||
Nodes (6): iou(), NetInput(), nonMaxSuppression$1(), SsdMobilenetv1(), TinyYolov2(), TinyYolov2Base()
|
||||
|
||||
### Community 77 - ".runAndExtendWithFaceDetections"
|
||||
Cohesion: 0.15
|
||||
Nodes (11): DetectAllFacesTask(), DrawBox(), drawDetections(), drawFaceExpressions(), DrawFaceLandmarks(), extendWithFaceDetection(), FaceExpressions(), isWithFaceDetection() (+3 more)
|
||||
### Community 77 - "drawFaceExpressions"
|
||||
Cohesion: 0.22
|
||||
Nodes (6): DrawBox(), drawDetections(), drawFaceExpressions(), DrawFaceLandmarks(), FaceExpressions(), isWithFaceExpressions()
|
||||
|
||||
### Community 78 - "t"
|
||||
Cohesion: 0.14
|
||||
Nodes (14): bi(), Ef(), er(), ia(), Ie(), j(), Kt(), Mf() (+6 more)
|
||||
Cohesion: 0.10
|
||||
Nodes (26): bi(), Bo(), ce(), cl(), de(), e(), Ef(), er() (+18 more)
|
||||
|
||||
### Community 79 - "登录反馈与加载优化(2026-10-12)"
|
||||
Cohesion: 0.25
|
||||
@@ -384,28 +374,56 @@ Nodes (5): 10.09 新版 H5 实施与验收, 按确认项实现, 文档未定义
|
||||
Cohesion: 0.33
|
||||
Nodes (6): 后续真机确认与体验优化, 微信登录联调:2026-10-12, 本轮修正, 白名单与凭据, 真实微信验收与排查, 自动验证
|
||||
|
||||
### Community 85 - "test/accounts.js"
|
||||
Cohesion: 0.22
|
||||
Nodes (9): assert, create, fs, http, os, path, ReportUI, SkinScore (+1 more)
|
||||
|
||||
### Community 87 - "产品推荐规则:肤龄基准与边界调整"
|
||||
Cohesion: 0.33
|
||||
Nodes (6): 产品推荐规则:肤龄基准与边界调整, 决策顺序, 后台与前端一致性, 实施与验证范围, 已明确的要求, 已确认的三个条件
|
||||
|
||||
### Community 88 - "ui-20261012/manifest.json"
|
||||
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)
|
||||
|
||||
### Community 92 - "客户反馈修改(2026-10-12)"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): 公网部署与复测, 客户反馈修改(2026-10-12), 皱纹数据说明, 逐项处理, 验证
|
||||
|
||||
### Community 93 - ".runAndExtendWithFaceDetections"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): DetectAllFacesTask(), extendWithFaceDetection(), isWithFaceDetection(), isWithFaceLandmarks(), resizeResults()
|
||||
|
||||
### Community 94 - "第三轮反馈:PSD 对齐及本地验收"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 原稿与导出可追溯性, 已完成, 本地验证, 第三轮反馈:PSD 对齐及本地验收
|
||||
|
||||
### Community 96 - "production-loop.mjs"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): artifacts, errors, samples
|
||||
|
||||
### Community 100 - "extractAllFacesAndComputeResults"
|
||||
Cohesion: 0.11
|
||||
Nodes (9): ComputeAllFaceDescriptorsTask(), ComputeSingleFaceDescriptorTask(), extendWithFaceDescriptor(), extendWithFaceExpressions(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult(), FaceRecognitionNet(), PredictAllFaceExpressionsTask() (+1 more)
|
||||
|
||||
## Knowledge Gaps
|
||||
- **236 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+231 more)
|
||||
- **256 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+251 more)
|
||||
These have ≤1 connection - possible missing edges or undocumented components.
|
||||
- **17 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
- **20 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 `r()` connect `n` to `face-api.js`, `.toString`, `mn`, `normalize`, `server.js`, `t`, `i`, `C`?**
|
||||
_High betweenness centrality (0.050) - this node is a cross-community bridge._
|
||||
- **Why does `k()` connect `k` to `face-api.js`, `app.js`, `n`, `lib/accounts.js`, `t`, `i`?**
|
||||
_High betweenness centrality (0.043) - this node is a cross-community bridge._
|
||||
- **Why does `renderReport()` connect `app.js` to `t`, `k`?**
|
||||
_High betweenness centrality (0.041) - this node is a cross-community bridge._
|
||||
- **Why does `k()` connect `k` to `face-api.js`, `app.js`, `n`, `lib/accounts.js`, `t`, `C`?**
|
||||
_High betweenness centrality (0.061) - this node is a cross-community bridge._
|
||||
- **Why does `renderReport()` connect `app.js` to `k`, `t`?**
|
||||
_High betweenness centrality (0.048) - this node is a cross-community bridge._
|
||||
- **Why does `rate()` connect `lib/accounts.js` to `k`?**
|
||||
_High betweenness centrality (0.048) - 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?**
|
||||
@@ -413,4 +431,4 @@ _Questions this graph is uniquely positioned to answer:_
|
||||
- **Are the 7 inferred relationships involving `t()` (e.g. with `renderReport()` and `e()`) actually correct?**
|
||||
_`t()` has 7 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **What connects `source`, `document_size`, `pages` to the rest of the system?**
|
||||
_236 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
_256 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
@@ -40,8 +40,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/app.js": {
|
||||
"mtime": 1791786351.8390813,
|
||||
"ast_hash": "7bfa025f6ac5b97e62075cd762a1eee0",
|
||||
"mtime": 1791793504.3911867,
|
||||
"ast_hash": "bedf52ab54b171d46f36b14e40c942a9",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/head-pose.js": {
|
||||
@@ -140,8 +140,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/index.html": {
|
||||
"mtime": 1791786653.9070883,
|
||||
"ast_hash": "39e41fec33bce3bf22520b9a3128b2ec",
|
||||
"mtime": 1791792928.367181,
|
||||
"ast_hash": "f55cc02cf612e5d2526b3ef61ebb4d54",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/vendor/faceapi/README.md": {
|
||||
@@ -835,8 +835,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"tools/export_psd_assets.py": {
|
||||
"mtime": 1791642102.9273047,
|
||||
"ast_hash": "7cee68c3db2cd4a52b58b8e380e2fe8d",
|
||||
"mtime": 1791792360.291166,
|
||||
"ast_hash": "9a30784b1e310d7079127af2d5090e85",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"MP_verify_kpx234XimQGtFPo6.txt": {
|
||||
@@ -1245,8 +1245,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/report-ui.js": {
|
||||
"mtime": 1791779513.7549675,
|
||||
"ast_hash": "100c3438318d0f7a8048c74ef3455252",
|
||||
"mtime": 1791792515.5711725,
|
||||
"ast_hash": "ba1192ff1f00a5e1ac4712e470124d0b",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/lib/accounts.js": {
|
||||
@@ -1260,8 +1260,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/report-persistence.mjs": {
|
||||
"mtime": 1791773638.010959,
|
||||
"ast_hash": "9876c659bb7c3513f871d576f0024e72",
|
||||
"mtime": 1791793231.819183,
|
||||
"ast_hash": "24636f96fd5cd576cde11ef9e911f3ff",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/ui-20261009.mjs": {
|
||||
@@ -1680,13 +1680,123 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/customer-feedback-second.mjs": {
|
||||
"mtime": 1791787113.9230983,
|
||||
"ast_hash": "2ee0f030571c4a0f6b897a8f9bd4b15d",
|
||||
"mtime": 1791792650.5151644,
|
||||
"ast_hash": "2e340ce46b90c142341cb1f9636cb2e7",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u5ba2\u6237\u53cd\u9988\u7b2c\u4e8c\u6279\u4fee\u6539-2026-10-12.md": {
|
||||
"mtime": 1791786653.9070883,
|
||||
"ast_hash": "0cb17a2e1c909816642fa31d3a416f1e",
|
||||
"mtime": 1791787225.4750996,
|
||||
"ast_hash": "fc8acdbebaa37e806d6f21b24c9c9b90",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"design/ui-20261012/manifest.json": {
|
||||
"mtime": 1791793758.3191924,
|
||||
"ast_hash": "0f1746d6457878cd4ac5f5c4019d7524",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/customer-feedback-third.mjs": {
|
||||
"mtime": 1791793748.2471912,
|
||||
"ast_hash": "60694d94b13194da8aa13f796c8be196",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"tools/export_third_feedback_assets.py": {
|
||||
"mtime": 1791793748.2471912,
|
||||
"ast_hash": "d33276ea29ab7592e99cf5dc28113065",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"tools/test_export_psd_assets.py": {
|
||||
"mtime": 1791793090.1911738,
|
||||
"ast_hash": "47ab953b1fa980034d3822257b4173f1",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"new_feedback/\u7b2c\u4e09\u8f6e\u53cd\u998820261012/\u5085\u8273NEW\u4e0e\u9879\u601d\u70bc\u7684\u4f1a\u8bdd 2026\u5e7410\u670812\u65e5.md": {
|
||||
"mtime": 1791790056.0,
|
||||
"ast_hash": "c916f80fdb198de25ee09f0f1d1192cf",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"design/ui-20261012/page-0.webp": {
|
||||
"mtime": 1791793754.4191914,
|
||||
"ast_hash": "35fd7cc24654f4aa29de6633e4fbf461",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"design/ui-20261012/page-1.webp": {
|
||||
"mtime": 1791793755.7511919,
|
||||
"ast_hash": "e975a47848bbdae8d58b9d5c1ae564e2",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"design/ui-20261012/page-4.webp": {
|
||||
"mtime": 1791793757.1511922,
|
||||
"ast_hash": "59a5db7c76b4674dc6cf70c82c4862d7",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"design/ui-20261012/page-9.webp": {
|
||||
"mtime": 1791793758.3191924,
|
||||
"ast_hash": "5f1e8bf0a41e26e135cd0e5f65abe51e",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/img/ui-20261012/brand-lockup.png": {
|
||||
"mtime": 1791793750.415191,
|
||||
"ast_hash": "558555cc40714b849da78a5179bfa7f0",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/img/ui-20261012/button-history-label.png": {
|
||||
"mtime": 1791793750.4231908,
|
||||
"ast_hash": "a9d03f5781d34b6372cfa7c520987c51",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/img/ui-20261012/button-start-label.png": {
|
||||
"mtime": 1791793750.419191,
|
||||
"ast_hash": "5c23895cde756eb5d5389858394d4315",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/img/ui-20261012/product-25.webp": {
|
||||
"mtime": 1791793750.747191,
|
||||
"ast_hash": "e7c712741ad00090e432abf74679a71b",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/img/ui-20261012/product-35.webp": {
|
||||
"mtime": 1791793751.0871909,
|
||||
"ast_hash": "cb4bc6170163813432e2420abeda87e0",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/img/ui-20261012/product-50.webp": {
|
||||
"mtime": 1791793751.4831908,
|
||||
"ast_hash": "5a9c0bde70efd5f50022a48def483580",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/img/ui-20261012/scan-waiting.webp": {
|
||||
"mtime": 1791793752.751191,
|
||||
"ast_hash": "f389fa035ff64a894a7e2242c9e0341b",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/img/ui-20261012/splash-slogan.png": {
|
||||
"mtime": 1791793750.399191,
|
||||
"ast_hash": "5730cf293b59cf7c8c49ccfa5bcc6699",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"new_feedback/\u7b2c\u4e09\u8f6e\u53cd\u998820261012/\u56fe\u7247\u548c\u9644\u4ef6/exported_img_v3_0216d_0fc10635-b27d-4d3e-b92e-ff01cc4dfa1g.png": {
|
||||
"mtime": 1791790056.0,
|
||||
"ast_hash": "cad3834df44b1d38feb172955030e96c",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"new_feedback/\u7b2c\u4e09\u8f6e\u53cd\u998820261012/\u56fe\u7247\u548c\u9644\u4ef6/exported_img_v3_0216d_2586d3cb-ffdb-4792-893b-cc831952a2cg.png": {
|
||||
"mtime": 1791790056.0,
|
||||
"ast_hash": "8cf8984413f91c3ad3d454e1723edfb3",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"new_feedback/\u7b2c\u4e09\u8f6e\u53cd\u998820261012/\u56fe\u7247\u548c\u9644\u4ef6/exported_img_v3_0216d_69cd3338-bd06-47cb-979c-375e1cb5028g.png": {
|
||||
"mtime": 1791790056.0,
|
||||
"ast_hash": "88614b7df04765e246a550c641da1479",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"new_feedback/\u7b2c\u4e09\u8f6e\u53cd\u998820261012/\u56fe\u7247\u548c\u9644\u4ef6/exported_img_v3_0216d_bd67ae20-e4ad-4ecd-8b0c-6ca58494d57g.png": {
|
||||
"mtime": 1791790056.0,
|
||||
"ast_hash": "d2c6770ba56a6f33b67aa3cb9b609229",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u5ba2\u6237\u53cd\u9988\u7b2c\u4e09\u8f6e\u4fee\u6539-2026-10-12.md": {
|
||||
"mtime": 1791793748.2471912,
|
||||
"ast_hash": "ae9f2b73434be30167f778fa974d950b",
|
||||
"semantic_hash": ""
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 270 KiB |
|
After Width: | Height: | Size: 261 KiB |
|
After Width: | Height: | Size: 324 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,89 @@
|
||||
/* Third-round PSD, 2026-10-12. Measurements include text transforms.
|
||||
Native 248px WeChat header is excluded. Fixed text sizes/line breaks are
|
||||
intentional; flexible photo backgrounds and safe areas accommodate phones. */
|
||||
.splash-slogan {
|
||||
left: calc(50% - 1.041667px); right: auto;
|
||||
width: 249.305556px; height: 75.347222px;
|
||||
transform: translateX(-50%); text-shadow: none;
|
||||
top: max(6.838833%, calc(env(safe-area-inset-top) + 48px));
|
||||
}
|
||||
#s-splash .splash-slogan { top: max(6.838833%, calc(env(safe-area-inset-top) + 48px)); }
|
||||
.splash-bottom { bottom: max(5.882353%, env(safe-area-inset-bottom)); gap: calc(7.291667 * var(--u)); padding-bottom: 0; }
|
||||
.brand-row { margin-bottom: calc(10.416667 * var(--u)); gap: 0; }
|
||||
.brand-lockup { width: 179.513889px; height: 20.486111px; }
|
||||
#s-splash .btn-primary { display: flex; align-items: center; justify-content: center; width: calc(181.25 * var(--u)); height: calc(38.888889 * var(--u)); line-height: calc(38.888889 * var(--u)); border-radius: calc(6.944444 * var(--u)); }
|
||||
.splash-button-label { width: 55.902778px; height: 13.888889px; }
|
||||
.btn-history .splash-button-label { width: 56.597222px; height: 13.541667px; }
|
||||
|
||||
/* These two fixed-content dialogs retain the PSD text column on every phone.
|
||||
308px fits a 320px viewport without shrinking fonts or changing line breaks. */
|
||||
#p-privacy .popup, #p-photoguide .popup {
|
||||
width: 307.986111px; height: 313.194444px; min-height: 313.194444px;
|
||||
padding: 0; top: -42.881944px; border-radius: 8.680556px;
|
||||
text-rendering: geometricPrecision;
|
||||
}
|
||||
#p-privacy .popup-title, #p-photoguide .popup-title {
|
||||
position: absolute; margin: 0; font-size: 23.148148px; line-height: 28.935185px;
|
||||
letter-spacing: 0; white-space: nowrap;
|
||||
}
|
||||
#p-privacy .popup-title { left: 31.944444px; top: 35px; }
|
||||
#p-photoguide .popup-title { left: 37.5px; top: 61.388889px; text-align: left; }
|
||||
#p-privacy .popup-body, .guide-description {
|
||||
position: absolute; margin: 0;
|
||||
font-size: 11.574074px; line-height: 17.361111px; font-weight: 400; letter-spacing: 0;
|
||||
}
|
||||
#p-privacy .popup-body { left: 31.944444px; top: 109.027778px; width: 243.75px; }
|
||||
/* Text-frame advance includes side bearings beyond the PSD visible-ink bbox. */
|
||||
.guide-description { left: 37.847222px; top: 135.069444px; width: 237px; }
|
||||
.guide-description span { white-space: nowrap; }
|
||||
#p-privacy .privacy-row { position: absolute; left: 29.861111px; top: 184.375px; gap: 10.069444px; }
|
||||
#p-privacy .privacy-text { font-size: 10.127315px; line-height: 18.807870px; white-space: nowrap; }
|
||||
#p-privacy .privacy-link { font-family: "MiSans Demibold", var(--font); font-weight: 700; }
|
||||
#p-privacy .checkbox { width: 11.805556px; height: 12.152778px; }
|
||||
#p-privacy .popup-btn, #p-photoguide .popup-btn {
|
||||
position: absolute; left: 63.194444px; margin: 0;
|
||||
width: 181.25px; height: 38.888889px; line-height: 38.888889px;
|
||||
font-family: "MiSans Demibold", var(--font); font-size: 14.467593px; font-weight: 700;
|
||||
letter-spacing: 0; border-radius: 6.944444px;
|
||||
}
|
||||
#p-privacy .popup-btn { top: 239.930556px; }
|
||||
#p-photoguide .popup-btn { top: 219.791667px; }
|
||||
#p-privacy .popup-close, #p-photoguide .popup-close { top: 10.055556px; right: 10.055556px; width: 32px; height: 32px; }
|
||||
#p-privacy .popup-close img, #p-photoguide .popup-close img { width: 17.708333px; height: 17.361111px; }
|
||||
@media (max-height: 440px) {
|
||||
#p-privacy .popup, #p-photoguide .popup { top: 0; }
|
||||
}
|
||||
|
||||
/* PSD 984x503 card; text starts 77/78px in, image starts at x480.
|
||||
Fixed typography is preserved even at 320px; never center-crop the artwork. */
|
||||
.prod-title { font-size: 23.148148px; line-height: 31.828704px; margin-top: calc(19.583333 * var(--u)); letter-spacing: 0; }
|
||||
.card-product { width: calc(341.666667 * var(--u)); margin-left: calc(-0.333333 * var(--u)); border-radius: calc(5.208333 * var(--u)); }
|
||||
.prod-card { position: relative; height: calc(174.652778 * var(--u)); min-height: 0; text-rendering: geometricPrecision; }
|
||||
.prod-info { flex: 0 0 48.780488%; padding: 0; }
|
||||
#product-title {
|
||||
position: absolute; left: 26.736111px; top: calc(34.027778 * var(--u));
|
||||
margin: 0; font-size: 14.467593px; line-height: 15.071903px; font-weight: 600;
|
||||
}
|
||||
.prod-name {
|
||||
position: absolute; left: 26.736111px; top: calc(57.638889 * var(--u));
|
||||
margin: 0; font-size: 11.574074px; line-height: 17.361111px; font-weight: 600;
|
||||
}
|
||||
.prod-badge { position: absolute; left: 27.430556px; top: calc(115.277778 * var(--u)); width: calc(35.069444 * var(--u)); margin: 0; }
|
||||
.prod-scroll { flex: 0 0 51.219512%; }
|
||||
.prod-scroll img { width: 100%; height: 100%; aspect-ratio: auto; object-fit: fill; }
|
||||
|
||||
/* Exact static PSD waiting image. No fake progress or added scanning animation.
|
||||
Contain the original composition instead of stretching/cropping faces/icons. */
|
||||
#p-analyzing { background: #000; overflow: hidden; }
|
||||
.scan-artwork { width: 100%; height: 100%; object-fit: contain; }
|
||||
#p-analyzing:not(.scan-artwork-ready) .scan-artwork { visibility: hidden; }
|
||||
/* A failed/stalled image must not leave an unexplained black screen. The live
|
||||
status is visually hidden only once the exact PSD artwork has loaded. */
|
||||
.scan-status { position: absolute; top: 10.5%; left: 0; right: 0; color: #fff; text-align: center; font: 700 23.148148px/1.4 "MiSans Demibold",var(--font); }
|
||||
.scan-artwork-ready .scan-status { width: 1px; height: 1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
||||
.scan-cancel {
|
||||
position: absolute; left: 50%; bottom: max(10px, env(safe-area-inset-bottom));
|
||||
transform: translateX(-50%); padding: 10px 24px;
|
||||
color: #bbb; font: 12px/1.5 "PingFang SC","Microsoft YaHei",sans-serif;
|
||||
}
|
||||
.scan-cancel:focus-visible { outline: 1px solid var(--yellow); border-radius: 4px; }
|
||||
@@ -15,6 +15,7 @@
|
||||
<link rel="stylesheet" href="vendor/mobile-select/mobile-select.css">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/ui-20261009.css">
|
||||
<link rel="stylesheet" href="css/ui-20261012.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -42,14 +43,13 @@
|
||||
<section class="screen" id="s-splash">
|
||||
<div class="splash-bg"></div>
|
||||
<button class="account-status account-status-home" data-action="switch-login" aria-label="切换登录方式"><span data-auth-status></span><small>切换 ›</small></button>
|
||||
<p class="splash-slogan"><span class="splash-slogan-en">ONE ME,<br>FROM THE INSIDE OUT</span><br><span class="splash-slogan-zh">以我为始 自有表现</span></p>
|
||||
<img class="splash-slogan" src="assets/img/ui-20261012/splash-slogan.png" width="718" height="217" loading="lazy" alt="ONE ME,FROM THE INSIDE OUT;以我为始 自有表现">
|
||||
<div class="splash-bottom">
|
||||
<div class="brand-row">
|
||||
<img src="assets/img/ui-20261009/brand-logo.png" alt="1'me" class="brand-logo">
|
||||
<span class="brand-text">肌肤状态检测</span>
|
||||
<img src="assets/img/ui-20261012/brand-lockup.png" width="517" height="59" loading="lazy" alt="1'me 肌肤状态检测" class="brand-lockup">
|
||||
</div>
|
||||
<button class="btn btn-primary" data-action="open-privacy">开始检测</button>
|
||||
<button class="btn btn-primary btn-history" data-action="history">我的报告</button>
|
||||
<button class="btn btn-primary" data-action="open-privacy"><img class="splash-button-label" src="assets/img/ui-20261012/button-start-label.png" width="161" height="40" loading="lazy" alt="开始检测"></button>
|
||||
<button class="btn btn-primary btn-history" data-action="history"><img class="splash-button-label" src="assets/img/ui-20261012/button-history-label.png" width="163" height="39" loading="lazy" alt="我的报告"></button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -289,7 +289,7 @@
|
||||
<img src="assets/img/ui-20261009/icon-close.png" alt="关闭">
|
||||
</button>
|
||||
<h2 class="popup-title">欢迎体验1'me<br>肌肤状态评估服务!</h2>
|
||||
<p class="popup-body">我们将采集您的面部影像,用于本次肌肤分析,并将报告保存至服务端,供您在“我的报告”中回看。</p>
|
||||
<p class="popup-body">我们将采集您的面部影像,为您生成专属肌肤分析与状态管理方案。所有数据严格保密,仅用于本次检测分析。</p>
|
||||
<label class="privacy-row" id="privacy-row">
|
||||
<span class="checkbox" id="privacy-check"></span>
|
||||
<span class="privacy-text">我已阅读并同意<a class="privacy-link" id="privacy-policy-link">相关隐私协议</a></span>
|
||||
@@ -301,13 +301,13 @@
|
||||
<!-- ═══════════ 弹窗:拍照引导 ═══════════ -->
|
||||
<!-- 背景只保留启动页底图 + Slogan(不叠档案表单),对齐设计稿 04 -->
|
||||
<div class="overlay overlay-guide" id="p-photoguide">
|
||||
<div class="guide-bg"><p class="splash-slogan">ONE ME,<br>FROM THE INSIDE OUT<br>以我为始 自有表现</p></div>
|
||||
<div class="guide-bg"><img class="splash-slogan" src="assets/img/ui-20261012/splash-slogan.png" width="718" height="217" loading="lazy" alt="ONE ME,FROM THE INSIDE OUT;以我为始 自有表现"></div>
|
||||
<div class="popup">
|
||||
<button class="popup-close" data-action="close-photoguide" aria-label="关闭">
|
||||
<img src="assets/img/ui-20261009/icon-close.png" alt="关闭">
|
||||
</button>
|
||||
<h2 class="popup-title popup-title-center">请拍摄或上传<br>正面及左右侧面照片</h2>
|
||||
<p class="guide-description">为使测试结果更准确,请关闭美颜/滤镜、并确保光线充足均匀。</p>
|
||||
<p class="guide-description"><span>为使测试结果更准确,请关闭美颜/滤镜、并确</span><br><span>保光线充足均匀。</span></p>
|
||||
<button class="btn btn-primary popup-btn" data-action="start-camera">开始</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -452,12 +452,12 @@
|
||||
</div>
|
||||
|
||||
<!-- ═══════════ 分析中过渡 ═══════════ -->
|
||||
<div class="overlay overlay-dark" id="p-analyzing">
|
||||
<div class="analyzing">
|
||||
<div class="spinner"></div>
|
||||
<p>正在生成您的肌肤报告…</p>
|
||||
<p class="analyzing-tip">点击空白处取消</p>
|
||||
</div>
|
||||
<div class="overlay" id="p-analyzing" role="dialog" aria-modal="true" aria-label="正在生成肌肤报告">
|
||||
<!-- Original visible PSD layers, including decorative album/flip icons.
|
||||
Not camera controls: no action attributes, focus targets or file inputs. -->
|
||||
<img id="scan-artwork" class="scan-artwork" data-src="assets/img/ui-20261012/scan-waiting.webp" width="1080" height="2091" alt="" aria-hidden="true" draggable="false">
|
||||
<p class="scan-status" role="status">检测中…</p>
|
||||
<button class="scan-cancel" type="button" data-action="cancel-analysis">取消</button>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════ Toast ═══════════ -->
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
function showScreen(id, nav) { // nav: 'push' 新增历史 | 'replace' 替换当前历史 | 缺省不动历史
|
||||
if (id !== 's-login' && !AccountClient.user() && !(/[?&]mock=1/.test(location.search))) id = 's-login';
|
||||
if (['s-profile', 's-cam', 's-report'].includes(id)) loadAppFont();
|
||||
if (id === 's-profile' || id === 's-cam') prepareScanArtwork();
|
||||
document.querySelectorAll('.screen').forEach(function (s) {
|
||||
s.classList.toggle('active', s.id === id);
|
||||
});
|
||||
@@ -140,9 +141,19 @@
|
||||
}
|
||||
|
||||
/* ─── 弹窗管理 ─── */
|
||||
function prepareScanArtwork() {
|
||||
var image = $('#scan-artwork');
|
||||
if (!image.getAttribute('src') || (image.complete && !image.naturalWidth)) {
|
||||
image.onload = function () { $('#p-analyzing').classList.add('scan-artwork-ready'); };
|
||||
image.onerror = function () { $('#p-analyzing').classList.remove('scan-artwork-ready'); };
|
||||
image.src = image.dataset.src;
|
||||
}
|
||||
}
|
||||
function openOverlay(id) {
|
||||
if (id === 'p-privacy' || id === 'p-photoguide') loadAppFont();
|
||||
if (id === 'p-analyzing') prepareScanArtwork();
|
||||
$('#' + id).classList.add('show');
|
||||
if (id === 'p-analyzing') $('.scan-cancel').focus({ preventScroll: true });
|
||||
}
|
||||
function closeOverlay(id) { $('#' + id).classList.remove('show'); }
|
||||
|
||||
@@ -164,6 +175,10 @@
|
||||
if (e.key === 'Tab') { e.preventDefault(); $('[data-action="login-confirm"]').focus(); }
|
||||
return;
|
||||
}
|
||||
if ($('#p-analyzing').classList.contains('show')) {
|
||||
if (e.key === 'Tab') { e.preventDefault(); $('.scan-cancel').focus(); }
|
||||
return; // Preserve existing Escape behavior; do not focus camera controls.
|
||||
}
|
||||
if (e.key !== 'Escape' && e.key !== 'Esc') return;
|
||||
var order = ['p-zoom', 'p-policy', 'p-privacy', 'p-photoguide'];
|
||||
for (var i = 0; i < order.length; i++) {
|
||||
@@ -181,7 +196,7 @@
|
||||
}
|
||||
// 分析中弹层点遮罩取消:回档案页(mock 路径同样经 seq 丢弃)
|
||||
$('#p-analyzing').addEventListener('click', function (e) {
|
||||
if (e.target !== this) return; // 只响应遮罩空白处,不误伤卡片
|
||||
if (e.target !== this && !e.target.closest('[data-action="cancel-analysis"]')) return;
|
||||
leaveCamera();
|
||||
showScreen('s-profile');
|
||||
popHistorySilently();
|
||||
@@ -1352,7 +1367,10 @@
|
||||
var span = document.createElement('span'); span.className = 'product-line'; span.textContent = line;
|
||||
$('#product-name').appendChild(span);
|
||||
});
|
||||
$('#product-image').src = product.image;
|
||||
// Presentation snapshots retain historical copy/recommendations. Only the
|
||||
// artwork crop is refreshed by tier, without mutating stored report data.
|
||||
var productArt = ReportUI.products.filter(function (p) { return p.tier === product.tier; })[0];
|
||||
$('#product-image').src = productArt ? productArt.image : product.image;
|
||||
$('#product-image').alt = product.name + '双仓精华液';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
['sens', '泛红', 'stab', 'sens'], ['acne', '痘痘', 'stab', 'acne']
|
||||
];
|
||||
var products = [
|
||||
{ tier: 1, match: '25+束紧精华', title: "1'me 主场精华 I", name: '以我本然有光蓄活', image: 'assets/img/ui-20261009/product-25.png' },
|
||||
{ tier: 2, match: '35+支撑精华', title: "1'me 主场精华 II", name: '以我自在有势挺括', image: 'assets/img/ui-20261009/product-35.png' },
|
||||
{ tier: 3, match: '50+重塑精华', title: "1'me 主场精华 III", name: '以我从容有态构塑', image: 'assets/img/ui-20261009/product-50.png' }
|
||||
{ tier: 1, match: '25+束紧精华', title: "1'me 主场精华 I", name: '以我本然有光蓄活', image: 'assets/img/ui-20261012/product-25.webp' },
|
||||
{ tier: 2, match: '35+支撑精华', title: "1'me 主场精华 II", name: '以我自在有势挺括', image: 'assets/img/ui-20261012/product-35.webp' },
|
||||
{ tier: 3, match: '50+重塑精华', title: "1'me 主场精华 III", name: '以我从容有态构塑', image: 'assets/img/ui-20261012/product-50.webp' }
|
||||
];
|
||||
var levelMap = { '预警': '改善', '改善': '良好', '良好': '优秀' };
|
||||
var dimNames = { firm: '紧塑力', vita: '透亮感', stab: '稳韧力' };
|
||||
|
||||
@@ -21,31 +21,27 @@ try {
|
||||
for (const width of [320, 375, 390, 414, 480]) {
|
||||
await page.setViewportSize({ width, height: 726 });
|
||||
const home = await page.evaluate(() => {
|
||||
const en = getComputedStyle(document.querySelector('.splash-slogan-en'));
|
||||
const zh = getComputedStyle(document.querySelector('.splash-slogan-zh'));
|
||||
const text = document.querySelector('#s-splash .brand-text');
|
||||
const style = getComputedStyle(text);
|
||||
const logo = document.querySelector('#s-splash .brand-logo').getBoundingClientRect();
|
||||
const lockup = document.querySelector('#s-splash .brand-lockup');
|
||||
const logo = lockup.getBoundingClientRect();
|
||||
const row = document.querySelector('#s-splash .brand-row').getBoundingClientRect();
|
||||
const slogan = document.querySelector('.splash-slogan').getBoundingClientRect();
|
||||
const sloganImage = document.querySelector('#s-splash .splash-slogan');
|
||||
const slogan = sloganImage.getBoundingClientRect();
|
||||
const badge = document.querySelector('.account-status-home').getBoundingClientRect();
|
||||
const ctx = document.createElement('canvas').getContext('2d');
|
||||
ctx.font = `${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
|
||||
const metrics = ctx.measureText(text.textContent);
|
||||
return { en: en.fontWeight, zh: zh.fontWeight, size: style.fontSize, weight: style.fontWeight,
|
||||
logoHeight: logo.height, inkHeight: metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent,
|
||||
return { logoHeight: logo.height, source: lockup.getAttribute('src'), alt: lockup.alt,
|
||||
logoRatio: logo.width / logo.height, rowX: row.x, rowRight: row.right,
|
||||
badgeBottom: badge.bottom, sloganTop: slogan.top,
|
||||
sloganLines: slogan.height / parseFloat(zh.lineHeight), overflow: document.documentElement.scrollWidth > innerWidth };
|
||||
sloganRatio: slogan.width / slogan.height, sloganAlt: sloganImage.alt,
|
||||
overflow: document.documentElement.scrollWidth > innerWidth };
|
||||
});
|
||||
assert.equal(home.en, '400'); assert.equal(home.zh, '700');
|
||||
assert.equal(home.size, '24px'); assert.equal(home.weight, '700');
|
||||
assert.equal(home.logoHeight, 26);
|
||||
assert(Math.abs(home.inkHeight - home.logoHeight) <= 4, 'logo/text optical height mismatch: ' + JSON.stringify(home));
|
||||
assert(Math.abs(home.logoRatio - 132 / 59) < .01, 'logo must retain original aspect ratio');
|
||||
// Third-round PSD replaces platform-dependent glyphs with the exact lockup.
|
||||
assert(home.source.endsWith('ui-20261012/brand-lockup.png'));
|
||||
assert.equal(home.alt, "1'me 肌肤状态检测");
|
||||
assert(Math.abs(home.logoHeight - 59 / 2.88) < .02);
|
||||
assert(Math.abs(home.logoRatio - 517 / 59) < .01, 'lockup must retain PSD aspect ratio');
|
||||
assert(Math.abs(home.sloganRatio - 718 / 217) < .01, 'three-line PSD slogan must retain its aspect ratio');
|
||||
assert(home.sloganAlt.includes('以我为始 自有表现'));
|
||||
assert(home.rowX >= 0 && home.rowRight <= width && !home.overflow, 'brand row overflows at ' + width);
|
||||
assert(home.badgeBottom <= home.sloganTop, 'status badge overlaps slogan at ' + width);
|
||||
assert(Math.abs(home.sloganLines - 3) < .1, 'slogan must retain three lines at ' + width);
|
||||
}
|
||||
await page.setViewportSize({ width: 375, height: 726 });
|
||||
if (artifacts) {
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
// Local UI + in-memory API fixtures only. No account/report writes or paid calls.
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
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 root = fileURLToPath(new URL('../../', import.meta.url));
|
||||
const artifacts = process.env.H5_ARTIFACT_DIR;
|
||||
const widths = [320, 375, 390, 414, 480];
|
||||
const consent = '我们将采集您的面部影像,为您生成专属肌肤分析与状态管理方案。所有数据严格保密,仅用于本次检测分析。';
|
||||
const guideLines = ['为使测试结果更准确,请关闭美颜/滤镜、并确', '保光线充足均匀。'];
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'design/ui-20261012/manifest.json')));
|
||||
for (const asset of manifest.assets) {
|
||||
const bytes = fs.readFileSync(path.join(root, asset.file));
|
||||
assert.equal(crypto.createHash('sha256').update(bytes).digest('hex'), asset.sha256, asset.file);
|
||||
}
|
||||
const styleOf = key => manifest.text_styles.find(row => row.path === key);
|
||||
assert.equal(styleOf('0/4').runs[0].font, 'MiSans-Demibold');
|
||||
assert.equal(styleOf('0/5').runs[0].font, 'MiSans-Regular');
|
||||
assert.equal(styleOf('8/1/0/5').runs[0].font, 'MiSans-Semibold');
|
||||
assert.equal(styleOf('1/3/1').text, consent);
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage({ viewport: { width: 375, height: 726 }, isMobile: true, hasTouch: true });
|
||||
page.setDefaultTimeout(60000);
|
||||
const errors = [], requests = [], writes = [];
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
page.on('request', request => { requests.push(new URL(request.url()).pathname); if (request.method() === 'POST') writes.push(request.url()); });
|
||||
await page.route('**/api/auth/session', route => route.fulfill({ json: { ok: true, user: { id: 'third-batch-fixture', kind: 'wechat' } } }));
|
||||
await page.route('**/vendor/faceapi/face-api.js', route => route.abort());
|
||||
await page.addInitScript(() => {
|
||||
navigator.mediaDevices.getUserMedia = () => Promise.reject(new DOMException('fixture denial', 'NotAllowedError'));
|
||||
});
|
||||
async function screenshot(name, selector) {
|
||||
if (!artifacts) return;
|
||||
fs.mkdirSync(artifacts, { recursive: true });
|
||||
await (selector ? page.locator(selector) : page).screenshot({ path: path.join(artifacts, name + '.png'), animations: 'disabled' });
|
||||
}
|
||||
async function loadFonts() {
|
||||
await page.waitForFunction(() => [...document.fonts].some(font => /MiSans Demibold/.test(font.family)));
|
||||
await page.evaluate(async () => { await Promise.all([document.fonts.load('400 12px MiSans'), document.fonts.load('600 14px MiSans'), document.fonts.load('700 23px "MiSans Demibold"')]); });
|
||||
}
|
||||
async function rect(selector) { return page.locator(selector).evaluate(e => { const r = e.getBoundingClientRect(); return { x: r.x, y: r.y, width: r.width, height: r.height, right: r.right, bottom: r.bottom }; }); }
|
||||
async function actualLines(selector) {
|
||||
return page.locator(selector).evaluate(el => {
|
||||
const lines = [], walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
||||
let node;
|
||||
while ((node = walker.nextNode())) for (let i = 0; i < node.length; i++) {
|
||||
const range = document.createRange(); range.setStart(node, i); range.setEnd(node, i + 1);
|
||||
const r = range.getBoundingClientRect(); if (!r.width || !r.height) continue;
|
||||
let row = lines.find(row => Math.abs(row.y - r.y) < 1);
|
||||
if (!row) { row = { y: r.y, text: '' }; lines.push(row); }
|
||||
row.text += node.textContent[i];
|
||||
}
|
||||
return lines.map(row => row.text);
|
||||
});
|
||||
}
|
||||
try {
|
||||
await page.goto(base);
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
await page.waitForLoadState('networkidle');
|
||||
assert(!requests.some(p => /\.woff2$|faceapi|scan-waiting|product-\d+\.webp/.test(p)), 'heavy/scanning assets must not compete with entry');
|
||||
await page.waitForFunction(() => document.querySelector('#s-splash .splash-slogan').naturalWidth === 718 && document.querySelector('.brand-lockup').naturalWidth === 517);
|
||||
await screenshot('01-home');
|
||||
await page.click('[data-action="open-privacy"]');
|
||||
await loadFonts();
|
||||
await page.waitForTimeout(350);
|
||||
assert.equal(await page.locator('#p-privacy .popup-body').textContent(), consent);
|
||||
const bodyLines = await actualLines('#p-privacy .popup-body');
|
||||
assert.deepEqual(bodyLines, ['我们将采集您的面部影像,为您生成专属肌肤分', '析与状态管理方案。所有数据严格保密,仅用于', '本次检测分析。']);
|
||||
for (const width of widths) {
|
||||
await page.setViewportSize({ width, height: 726 });
|
||||
const popup = await rect('#p-privacy .popup');
|
||||
assert(popup.x >= 0 && popup.right <= width);
|
||||
assert.deepEqual(await actualLines('#p-privacy .popup-body'), bodyLines, 'consent wrapping at ' + width);
|
||||
}
|
||||
const dialogRadius = await page.locator('#p-privacy .popup').evaluate(e => parseFloat(getComputedStyle(e).borderRadius));
|
||||
assert(Math.abs(dialogRadius - manifest.shape_metrics.find(row => row.path === '1/1').corner_radius_psd_px / 2.88) < .001);
|
||||
await page.setViewportSize({ width: 375, height: 726 });
|
||||
await screenshot('02-consent');
|
||||
await page.click('#privacy-check'); await page.click('[data-action="agree-continue"]');
|
||||
await page.waitForSelector('#s-profile.active');
|
||||
assert(requests.some(p => p.endsWith('scan-waiting.webp')), 'preload waiting image only after profile entry');
|
||||
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(400);
|
||||
}
|
||||
await page.click('[data-action="submit-profile"]');
|
||||
await page.waitForTimeout(350);
|
||||
for (const width of widths) {
|
||||
await page.setViewportSize({ width, height: 726 });
|
||||
assert.deepEqual(await actualLines('.guide-description'), guideLines, 'exact PSD guide lines at ' + width);
|
||||
const popup = await rect('#p-photoguide .popup'), description = await rect('.guide-description');
|
||||
assert(popup.x >= 0 && popup.right <= width && description.right <= popup.right);
|
||||
assert.equal(await page.locator('.guide-description').evaluate(e => e.scrollWidth <= e.clientWidth), true);
|
||||
await screenshot('03-guide-' + width);
|
||||
}
|
||||
// The real capture/upload progression with held, intercepted analysis response.
|
||||
await page.setViewportSize({ width: 375, height: 726 });
|
||||
let pending;
|
||||
await page.route('**/api/skin-analyze', route => { pending = route; });
|
||||
async function captureThree() {
|
||||
await page.click('[data-action="start-camera"]');
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
await page.waitForFunction(n => document.querySelector('#cam-step').textContent.includes(n + '/3'), i);
|
||||
await page.waitForTimeout(1600);
|
||||
await page.setInputFiles('#cam-file', path.join(root, 'reference/facepp-skinanalyze/examples/images/C3_180.jpg'));
|
||||
}
|
||||
await page.waitForSelector('#p-analyzing.show');
|
||||
await page.waitForFunction(() => document.querySelector('#scan-artwork').naturalWidth === 1080);
|
||||
const deadline = Date.now() + 5000;
|
||||
while (!pending && Date.now() < deadline) await page.waitForTimeout(20);
|
||||
assert(pending, 'analysis request must be intercepted within 5 seconds');
|
||||
}
|
||||
await captureThree();
|
||||
assert.equal(await page.locator('.scan-status').evaluate(e => getComputedStyle(e).clipPath), 'inset(50%)', 'loaded PSD text must not be duplicated');
|
||||
const scanPixels = await page.locator('#scan-artwork').evaluate(image => {
|
||||
const canvas = document.createElement('canvas'); canvas.width = image.naturalWidth; canvas.height = image.naturalHeight;
|
||||
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][0] > 220 && scanPixels[1][1] > 220 && scanPixels[1][2] < 150, 'yellow lower face must be visible');
|
||||
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]]) {
|
||||
await page.setViewportSize({ width, height });
|
||||
const art = await rect('#scan-artwork');
|
||||
assert.equal(art.width, width); assert.equal(art.height, height);
|
||||
assert.equal(await page.locator('#scan-artwork').evaluate(e => getComputedStyle(e).objectFit), 'contain');
|
||||
assert(await page.locator('.scan-cancel').isVisible());
|
||||
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
||||
}
|
||||
await page.setViewportSize({ width: 375, height: 726 });
|
||||
await screenshot('04-waiting');
|
||||
for (const point of [[70, 655], [306, 655], [180, 350]]) await page.mouse.click(...point);
|
||||
assert(await page.locator('#p-analyzing').isVisible(), 'icons and face must not cancel/open camera');
|
||||
await page.keyboard.press('Tab');
|
||||
assert.equal(await page.evaluate(() => document.activeElement.dataset.action), 'cancel-analysis');
|
||||
await page.keyboard.press('Escape');
|
||||
assert(await page.locator('#p-analyzing').isVisible(), 'Escape must retain existing waiting behavior');
|
||||
await page.click('[data-action="cancel-analysis"]');
|
||||
await page.waitForSelector('#s-profile.active');
|
||||
await pending.fulfill({ json: { ok: false, message: '迟到的失败不应影响页面' } }); pending = null;
|
||||
await page.waitForTimeout(1400);
|
||||
assert.equal(await page.locator('#p-analyzing.show').count(), 0);
|
||||
assert(!(await page.locator('#toast').textContent()).includes('迟到'));
|
||||
assert(await page.locator('#f-birth').inputValue());
|
||||
// Re-entry/failure must still dismiss the new artwork and retain the profile.
|
||||
await page.click('[data-action="submit-profile"]'); await captureThree();
|
||||
await pending.fulfill({ json: { ok: false, message: '未检测到人脸' } }); pending = null;
|
||||
await page.waitForSelector('#s-profile.active');
|
||||
assert.equal(await page.locator('#p-analyzing.show').count(), 0);
|
||||
assert((await page.locator('#toast').textContent()).includes('未检测到人脸'));
|
||||
// Successful completion follows the same request callback into the report.
|
||||
await page.click('[data-action="submit-profile"]'); await captureThree();
|
||||
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');
|
||||
assert.equal(await page.locator('#p-analyzing.show').count(), 0);
|
||||
assert.notEqual(await page.locator('#stat-age b').textContent(), '--');
|
||||
await page.unroute('**/api/skin-analyze');
|
||||
assert.equal(writes.length, 3, 'only three intercepted analysis POSTs; no auth/report mutations');
|
||||
|
||||
for (const [age, tier] of [[24, 1], [40, 2], [60, 3]]) {
|
||||
await page.route('**/assets/mock/report-fixture.js', async route => {
|
||||
const response = await route.fetch();
|
||||
await route.fulfill({ response, body: await response.text() + '\nMOCK_FIXTURE.result.skin_age = { value: ' + age + ' };' });
|
||||
});
|
||||
await page.goto(base + '?mock=1&screen=report'); await loadFonts();
|
||||
await page.waitForFunction(() => document.querySelector('#product-image').naturalWidth === 504);
|
||||
assert((await page.locator('#product-image').getAttribute('src')).endsWith('product-' + ({ 1: 25, 2: 35, 3: 50 }[tier]) + '.webp'));
|
||||
for (const width of widths) {
|
||||
await page.setViewportSize({ width, height: 844 });
|
||||
await page.locator('.card-product').scrollIntoViewIfNeeded();
|
||||
const info = await rect('.prod-info'), name = await rect('#product-name'), title = await rect('#product-title');
|
||||
assert(title.right <= info.right && name.right <= info.right, 'product text overlaps art at ' + width + ': ' + JSON.stringify({ info, name, title }));
|
||||
assert(Math.abs(title.x - info.x - 26.736111) < .1);
|
||||
assert.equal(await page.locator('#product-name').evaluate(e => getComputedStyle(e).fontWeight), '600');
|
||||
assert.equal((await actualLines('#product-name')).length, 2);
|
||||
const card = await rect('.card-product');
|
||||
assert(Math.abs(card.width - 984 / 1080 * width) < .1, 'exact PSD card width');
|
||||
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
||||
}
|
||||
await page.setViewportSize({ width: 375, height: 726 }); await screenshot('05-product-' + tier, '.card-product');
|
||||
await page.unroute('**/assets/mock/report-fixture.js');
|
||||
}
|
||||
// Font downloads may fail on a phone: explicit guide lines still must not
|
||||
// reflow or escape the dialog, and short viewports must keep controls visible.
|
||||
const fallback = await browser.newPage({ viewport: { width: 375, height: 726 } });
|
||||
await fallback.route('**/*.woff2', route => route.abort());
|
||||
await fallback.route('**/scan-waiting.webp', route => route.abort());
|
||||
await fallback.route('**/api/auth/session', route => route.fulfill({ json: { ok: true, user: { id: 'font-fallback-fixture', kind: 'browser' } } }));
|
||||
await fallback.goto(base); await fallback.click('[data-action="open-privacy"]');
|
||||
await fallback.waitForLoadState('networkidle');
|
||||
await fallback.evaluate(async () => {
|
||||
await document.fonts.ready;
|
||||
document.querySelector('#p-privacy').classList.remove('show');
|
||||
document.querySelector('#p-photoguide').classList.add('show');
|
||||
});
|
||||
for (const width of widths) {
|
||||
await fallback.setViewportSize({ width, height: 440 });
|
||||
assert(await fallback.locator('#p-photoguide').evaluate(el => {
|
||||
const box = el.querySelector('.popup').getBoundingClientRect();
|
||||
return box.top >= 0 && box.bottom <= innerHeight && box.left >= 0 && box.right <= innerWidth &&
|
||||
[...el.querySelectorAll('.guide-description span')].every(span => span.getBoundingClientRect().right <= box.right);
|
||||
}), 'font fallback/short viewport overflow at ' + width);
|
||||
assert(await fallback.locator('[data-action="start-camera"]').isVisible());
|
||||
}
|
||||
await fallback.evaluate(() => { document.querySelector('#p-photoguide').classList.remove('show'); document.querySelector('#p-privacy').classList.add('show'); });
|
||||
await fallback.click('#privacy-check'); await fallback.click('[data-action="agree-continue"]');
|
||||
await fallback.waitForLoadState('networkidle');
|
||||
await fallback.evaluate(() => document.querySelector('#p-analyzing').classList.add('show'));
|
||||
assert.equal(await fallback.locator('#p-analyzing.scan-artwork-ready').count(), 0);
|
||||
assert.equal(await fallback.locator('.scan-status').evaluate(e => getComputedStyle(e).clipPath), 'none', 'failed image must leave a visible text status');
|
||||
assert.equal(await fallback.locator('#scan-artwork').evaluate(e => getComputedStyle(e).visibility), 'hidden');
|
||||
await fallback.click('[data-action="cancel-analysis"]');
|
||||
assert.equal(await fallback.locator('#p-analyzing.show').count(), 0, 'image failure must not disable cancel');
|
||||
await fallback.close();
|
||||
assert.deepEqual(errors, []);
|
||||
console.log('PASS: PSD source hashes/fonts/copy; no heavy entry assets; fixed consent/guide wrapping in five widths; exact product crops/padding/Semibold/all tiers; actual three-photo waiting transition; visible clipped face; decorative icons/focus; cancel/stale/failure/retry/success; six viewport sizes; no backend writes or JS errors');
|
||||
} finally { await browser.close(); }
|
||||
@@ -146,7 +146,7 @@ try {
|
||||
const record = JSON.parse(fs.readFileSync(recordFile, 'utf8').trim());
|
||||
const snapshotFile = path.join(dataRoot, 'accounts/reports', record.owner_id, result.record_id + '.json');
|
||||
const legacy = JSON.parse(fs.readFileSync(snapshotFile, 'utf8'));
|
||||
legacy.presentation.product = ReportUI.products[1];
|
||||
legacy.presentation.product = { ...ReportUI.products[1], image: 'assets/img/ui-20261009/product-35.png' };
|
||||
legacy.presentation.views[0].text = '历史报告原文,不应按新文案重新计算。';
|
||||
legacy.vm.essence = { name: '35+支撑精华', text: '旧版推荐', triggers: ['最差项约束'] };
|
||||
delete legacy.vm.recommendation;
|
||||
@@ -162,6 +162,7 @@ try {
|
||||
await page.click('[data-report-id]');
|
||||
await page.waitForSelector('#s-report.active');
|
||||
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((await page.locator('.ditem-para').allTextContents()).includes(legacy.presentation.views[0].text));
|
||||
await desktop.goto(base + 'admin.html');
|
||||
await desktop.locator('#tbl-records tbody tr').first().click();
|
||||
|
||||
@@ -19,18 +19,18 @@ ASSETS = ROOT / "h5/assets/img/ui-20261009"
|
||||
ASSET_SPECS = [
|
||||
("bg-splash.jpg", "0/0", "0"),
|
||||
("bg-profile.jpg", "3/0/0", "3"),
|
||||
("bg-login.jpg", "9/0/0", "9"),
|
||||
("bg-login.jpg", "login/0/0", "login"),
|
||||
("bg-report.jpg", "8/0", "8"),
|
||||
("bg-history.jpg", "2/0", "2"),
|
||||
("brand-logo.png", "0/1/0", None),
|
||||
("brand-login.png", "9/7/1", None),
|
||||
("brand-login.png", "login/7/1", None),
|
||||
("icon-close.png", "1/6", None),
|
||||
("icon-dropdown.png", "3/1/2", None),
|
||||
("icon-back.png", "3/6/0", None),
|
||||
("icon-album.png", "5/1/1", None),
|
||||
("icon-switch-camera.png", "5/1/0", None),
|
||||
("icon-wechat.png", "9/7/6", None),
|
||||
("icon-guest.png", "9/7/7", None),
|
||||
("icon-wechat.png", "login/7/6", None),
|
||||
("icon-guest.png", "login/7/7", None),
|
||||
("icon-shutter.png", "5/1/2", None),
|
||||
("skin-0-dry.png", "3/3/1/0/2", None),
|
||||
("skin-1-mixeddry.png", "3/3/1/1/2", None),
|
||||
@@ -59,7 +59,13 @@ ASSET_SPECS = [
|
||||
def layer_at(psd, indices):
|
||||
layer = psd
|
||||
for part in indices.split("/"):
|
||||
layer = layer[int(part)]
|
||||
if part == "login":
|
||||
matches = [page for page in psd if page.name == "登录"]
|
||||
if len(matches) != 1:
|
||||
raise ValueError("Expected exactly one 登录 page; refusing incorrect asset mapping")
|
||||
layer = matches[0]
|
||||
else:
|
||||
layer = layer[int(part)]
|
||||
return layer
|
||||
|
||||
|
||||
@@ -111,7 +117,7 @@ def main():
|
||||
print(f"Page {index}: {page.name}", flush=True)
|
||||
# Cached layer pixels preserve Photoshop text appearance without local fonts.
|
||||
target = DESIGN / "pages" / f"{index + 1:02d}-{page.name}.png"
|
||||
if args.assets_only:
|
||||
if args.assets_only and target.exists():
|
||||
image = Image.open(target)
|
||||
else:
|
||||
image = page.composite(viewport=page.bbox)
|
||||
@@ -160,7 +166,7 @@ def main():
|
||||
thumb.thumbnail((180, 190))
|
||||
thumbs.append((filename, thumb))
|
||||
|
||||
sheet = Image.new("RGB", (5 * 236, 2 * 1620), "#eeeeee")
|
||||
sheet = Image.new("RGB", (5 * 236, ((len(previews)+4)//5) * 1620), "#eeeeee")
|
||||
draw = ImageDraw.Draw(sheet)
|
||||
for i, (_, preview) in enumerate(previews):
|
||||
x, y = (i % 5) * 236 + 10, (i // 5) * 1620 + 35
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Deterministic, selective exports from the 2026-10-12 third-feedback PSD.
|
||||
|
||||
Does not overwrite the original 10.09 exports, modify the PSD, substitute fonts,
|
||||
or reveal hidden Photoshop layers. Requires psd-tools, Pillow, aggdraw.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
from psd_tools import PSDImage
|
||||
from export_psd_assets import ROOT, layer_at
|
||||
|
||||
ASSETS = ROOT / "h5/assets/img/ui-20261012"
|
||||
DESIGN = ROOT / "design/ui-20261012"
|
||||
|
||||
|
||||
def union(layers):
|
||||
boxes = [layer.bbox for layer in layers]
|
||||
return (min(b[0] for b in boxes), min(b[1] for b in boxes),
|
||||
max(b[2] for b in boxes), max(b[3] for b in boxes))
|
||||
|
||||
|
||||
def sha256(path):
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as source:
|
||||
for chunk in iter(lambda: source.read(8 * 1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def composite(layers, viewport):
|
||||
image = Image.new("RGBA", (viewport[2] - viewport[0], viewport[3] - viewport[1]))
|
||||
for layer in layers:
|
||||
# Cached Photoshop text pixels, with original internal visibility intact.
|
||||
part = layer.composite(viewport=viewport)
|
||||
if part is None:
|
||||
raise ValueError(f"Empty layer: {layer.name}")
|
||||
image = Image.alpha_composite(image, part.convert("RGBA"))
|
||||
return image
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("psd", type=Path)
|
||||
args = parser.parse_args()
|
||||
psd = PSDImage.open(args.psd)
|
||||
expected = {0: "启动页", 1: "启动页弹窗", 4: "拍照弹窗", 8: "报告页", 9: "扫描", 10: "登录"}
|
||||
for index, name in expected.items():
|
||||
if len(psd) <= index or psd[index].name != name:
|
||||
raise ValueError(f"Unexpected PSD layout at {index}: expected {name}; no files written")
|
||||
ASSETS.mkdir(parents=True, exist_ok=True)
|
||||
DESIGN.mkdir(parents=True, exist_ok=True)
|
||||
manifest = {"source": args.psd.name, "source_sha256": sha256(args.psd),
|
||||
"document_size": list(psd.size), "native_header_px": 248,
|
||||
"design_scale": 2.88, "assets": []}
|
||||
|
||||
def export(name, paths, viewport=None):
|
||||
layers = [layer_at(psd, key) for key in paths]
|
||||
viewport = viewport or union(layers)
|
||||
image = composite(layers, viewport)
|
||||
if image.getchannel("A").getbbox() is None:
|
||||
raise ValueError(f"Empty transparent export: {name}")
|
||||
target = ASSETS / name
|
||||
if target.suffix == ".webp":
|
||||
image.save(target, lossless=True, method=6)
|
||||
else:
|
||||
image.save(target, optimize=True)
|
||||
manifest["assets"].append({"file": str(target.relative_to(ROOT)), "source_layers": paths,
|
||||
"bbox": list(viewport), "size": list(image.size),
|
||||
"bytes": target.stat().st_size,
|
||||
"sha256": sha256(target)})
|
||||
print(f"Exported {name}: {image.size}", flush=True)
|
||||
return image
|
||||
|
||||
export("splash-slogan.png", ["0/5", "0/4"])
|
||||
export("brand-lockup.png", ["0/1"])
|
||||
export("button-start-label.png", ["0/3/1"])
|
||||
export("button-history-label.png", ["0/2/2/2"])
|
||||
# Preserve the exact clipped right half of each card (not a new center crop).
|
||||
for tier, key in [(25, "8/1/0"), (35, "8/1/1"), (50, "8/1/2")]:
|
||||
card = layer_at(psd, key)
|
||||
left, top, right, bottom = card.bbox
|
||||
export(f"product-{tier}.webp", [key], (left + 480, top, right, bottom))
|
||||
page = psd[9]
|
||||
left, top, right, bottom = page.bbox
|
||||
# Composite the whole page: the face uses cross-layer clipping dependencies.
|
||||
# Exporting the icon group alone silently loses the face. The viewport crops
|
||||
# out the native header/back; default visibility keeps hidden controls hidden.
|
||||
scan = export("scan-waiting.webp", ["9"], (left, top + 248, right, bottom))
|
||||
if min(scan.getpixel((540, 720))[:3]) < 245:
|
||||
raise ValueError("Scanning face missing from composite")
|
||||
yellow = scan.getpixel((540, 1200))
|
||||
if not (yellow[0] > 220 and yellow[1] > 220 and yellow[2] < 150):
|
||||
raise ValueError("Scanning yellow face region missing from composite")
|
||||
|
||||
manifest["text_styles"] = []
|
||||
for key in ["0/4", "0/5", "0/1/1", "1/3/0", "1/3/1", "4/7", "4/8", "8/1/0/5", "8/1/0/6"]:
|
||||
layer = layer_at(psd, key)
|
||||
fonts = [font.get("Name").value for font in layer.resource_dict["FontSet"]]
|
||||
runs = []
|
||||
for run in layer.engine_dict["StyleRun"]["RunArray"]:
|
||||
style = run["StyleSheet"]["StyleSheetData"]
|
||||
font = fonts[int(style["Font"])]
|
||||
runs.append({"font": font, "font_size": float(style["FontSize"]),
|
||||
"leading": float(style["Leading"]) if "Leading" in style else None})
|
||||
manifest["text_styles"].append({"path": key, "text": layer.text, "bbox": list(layer.bbox),
|
||||
"transform": list(layer.transform), "runs": runs})
|
||||
manifest["shape_metrics"] = []
|
||||
for key in ["0/3/0", "0/2/0", "1/2/0", "4/2/0", "8/1/0/0", "1/1", "4/1"]:
|
||||
layer = layer_at(psd, key)
|
||||
anchors = [(knot.anchor[1] * psd.width, knot.anchor[0] * psd.height)
|
||||
for knot in layer.vector_mask.paths[0]]
|
||||
# These PSD rounded rectangles start on the top edge, one radius in.
|
||||
radius = anchors[0][0] - min(x for x, _ in anchors)
|
||||
manifest["shape_metrics"].append({"path": key, "bbox": list(layer.bbox),
|
||||
"corner_radius_psd_px": round(radius, 6)})
|
||||
# Review-only page previews; never use them as full-page H5 replacements.
|
||||
for index in [0, 1, 4, 9]:
|
||||
page = psd[index]
|
||||
left, top, right, bottom = page.bbox
|
||||
preview = page.composite(viewport=(left, top + 248, right, bottom))
|
||||
preview.thumbnail((540, 1400))
|
||||
preview.save(DESIGN / f"page-{index}.webp", quality=94, method=6)
|
||||
(DESIGN / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,35 @@
|
||||
"""The third PSD inserts 扫描 before 登录; never silently use its index."""
|
||||
import unittest
|
||||
from export_psd_assets import layer_at, ASSET_SPECS
|
||||
|
||||
|
||||
class Page(list):
|
||||
def __init__(self, name):
|
||||
super().__init__([["background"], [], [], [], [], [], [], [None, "brand", None, None, None, None, "wechat", "guest"]])
|
||||
self.name = name
|
||||
|
||||
|
||||
class LoginMappingTest(unittest.TestCase):
|
||||
def test_old_and_new_layout(self):
|
||||
for index in (9, 10):
|
||||
pages = [Page("other") for _ in range(index)] + [Page("登录")]
|
||||
if index == 10:
|
||||
pages[9].name = "扫描"
|
||||
self.assertEqual(layer_at(pages, "login/0/0"), "background")
|
||||
self.assertEqual(layer_at(pages, "login/7/1"), "brand")
|
||||
self.assertEqual(layer_at(pages, "login/7/6"), "wechat")
|
||||
|
||||
def test_missing_or_ambiguous_login(self):
|
||||
for pages in ([], [Page("扫描")], [Page("登录"), Page("登录")]):
|
||||
with self.assertRaises(ValueError):
|
||||
layer_at(pages, "login/0/0")
|
||||
|
||||
def test_every_login_asset_uses_semantic_mapping(self):
|
||||
names = {"bg-login.jpg", "brand-login.png", "icon-wechat.png", "icon-guest.png"}
|
||||
for filename, key, _ in ASSET_SPECS:
|
||||
if filename in names:
|
||||
self.assertTrue(key.startswith("login/"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||