Files
2026-10-10 14:23:43 +08:00
..
2026-10-10 14:23:43 +08:00
2026-10-10 14:23:43 +08:00
2026-10-10 14:23:43 +08:00
2026-10-10 14:23:43 +08:00
2026-09-28 15:27:31 +08:00
2026-10-10 14:23:43 +08:00
2026-10-10 14:23:43 +08:00
2026-10-09 23:22:28 +08:00
2026-10-10 14:23:43 +08:00
2026-10-10 14:23:43 +08:00
2026-10-09 23:22:28 +08:00

1'me 在线肌肤测试 H5

引导式自动三连拍(正脸/右脸/左脸),调用 Face++ 皮肤分析(专业版 skinanalyze_pro) 三视图真实检测,即时生成三维度×6分类肌肤状态分析与个性化护理建议的移动端 H5。

  • 前端:原生 HTML/CSS/JS,零依赖、零构建(face-api.js 已本地化到 vendor/,运行时零外部请求)
  • 后端:server.js(Node ≥ 18,零依赖)静态托管 + Face++ API 中转,api_secret 只存在服务端

目录结构

h5/
├── index.html          页面结构(启动 / 档案 / 拍照 / 报告 + 弹层 + 内联 SVG 取景蒙版)
├── admin.html          后台查看页(记录列表 / 失败与异常 / 统计概览,零依赖,见下文「后台查看」)
├── server.js           后端:静态托管 + POST /api/skin-analyze(Face++ skinanalyze_pro 三视图中转)+ /api/admin/*(记录查询/重试/统计/导出)
├── css/style.css       C 端全部样式(375px 基准)
├── css/admin.css       后台查看页样式
├── js/app.js           全部交互(屏幕切换、肤质轮播、引导式三连拍、报告渲染)
├── js/admin.js         后台查看页脚本(纯原生 ES5)
├── js/skin-score.js    三维度×6分类评分引擎(纯逻辑 UMD,window.SkinScore,可 node 自测;服务端评分复用同一份)
├── js/head-pose.js     头部姿态估算(纯逻辑 UMD,window.HeadPose,可 node 自测)
├── data/records.jsonl  检测记录库(一行一条 JSON,零依赖嵌入式存储,已 .gitignore)
├── data/photos/        检测照片 data/photos/<record_id>/{front,left,right}.jpg(本地磁盘代替对象存储)
├── data/seed.js        演示种子数据(一次性:node h5/data/seed.js,覆盖重写 records.jsonl)
├── data/results/       每次检测的 Face++ 原始返回落盘(复核用,已 .gitignore)
├── js/head-pose.js     头部姿态估算(纯逻辑 UMD,window.HeadPose,可 node 自测)
├── test/regression.js  真值回归(node h5/test/regression.js,对照 docs xlsx 两组三视图真值)
├── vendor/faceapi/     face-api.js 0.22.2 + 模型(本地化,见该目录 README)
├── assets/
│   ├── favicon.svg     站点图标
│   ├── img/            背景图、肤质插画、拍照底图、报告检测/产品图
│   ├── mock/           mock 报告页数据源(report-fixture.js,真实案例1数据)
│   └── svg/            品牌 Logo 与控件图标
└── preview/            开发期整页截图(仅存档,部署时可整目录剔除)

../apikey.bash          Face++ 密钥(export SKIN_API_KEY=... / SKIN_API_SECRET=...,已 .gitignore)

密钥配置

二选一:

  1. 环境变量:export SKIN_API_KEY=xxx SKIN_API_SECRET=xxx 后启动 server.js
  2. 密钥文件:项目根目录放 apikey.bash(内容为 export SKIN_API_KEY=... 等,server.js 会自动解析)

密钥文件已被 .gitignore 排除,严禁提交到版本库。

本地运行

cd h5
node server.js              # 默认 8080,可用 PORT=xxx 修改
# 打开 http://127.0.0.1:8080

调试:

  • http://127.0.0.1:8080/?mock=1 使用内置样本(SkinScore.MOCK_RESULT/MOCK_SIDE)跳过真实检测;不加载 face-api、不跑姿态检测,显示快门手动逐步推进(离线/自动化验证用)。
  • http://127.0.0.1:8080/?mock=1&screen=report 直渲 mock 报告页(仅调试用,供无头浏览器截图验证布局;档案缺省按 28 岁/干性肌肤);mock 数据源为 assets/mock/report-fixture.js(真实案例1三视图检测返回,含 face_maps 图层 data URL 与 marks 坐标),改该文件即可换样本。

拍照交互(动态捕捉自动三连拍)

三步严格按顺序,本步拍到合格照片才进入下一步(左右一律以用户本人为准):1/3 正对屏幕(target front)→ 2/3 右脸出镜(用户头向自己的左侧转,target right,产物作 right_image)→ 3/3 左脸出镜(头向自己的右侧转,target left,产物作 left_image)。送检前 photosByView 按 target 重排为 [正, 左, 右]。页面内没有返回按钮,微信/浏览器返回键经 History API 回到档案页并释放摄像头。

  • 取景蒙版:内联 SVG(#cam-mask)按步骤切换轮廓——正脸步为正脸剪影(含耳部),侧脸步为对应方向的侧脸剪影;轮廓路径由 Figma 拍照示意图(design/assets/images/*-拍照示意.png 的 alpha 镂空区)逐像素描摹生成,坐标为 Figma 375×812 整帧,viewBox 只取 y 86–812;框外 85% 黑,轮廓线 2.5px 半透明白。检测运行时轮廓内有自上而下的扫描带(#cam-clip 由 app.js 复制轮廓生成);未检测到人脸时轮廓呼吸动画;条件合格时轮廓变绿。
  • 预览缩小 15%:#cam-video 按 scale(.85) 显示。距离判定使用 object-fit:cover 和缩放后的屏幕占比(人脸高度占屏高 <26% 提示靠近、>58% 提示远一点),避免横竖视频比例不同导致距离误判。
  • 动态提示(一次一条,优先级:无人脸 > 距离 > 居中 > 角度 > 光线):「未检测到人脸」「请靠近一点 / 请远一点」「请将脸部移入取景框」(人脸中心偏离中线 >18% 屏宽)、角度类「请向左转头 / 请向右转头 / 请略微抬头 / 请略微低头」(正脸阈值 |yaw|≤15°、|pitch|≤18°,超出时按超出比例更大的一项给方向;侧脸 yaw 28°–75°、|pitch|≤20°)、光线类「请到光线充足处 / 光线过强,请避开强光」(脸部平均亮度 <65 或 >235)。
  • 合格即拍:单人、距离、横纵居中、姿态和光线连续合格 450ms 后立即抓取当前帧,再闪白 180ms 给出反馈。无倒计时;任何丢帧或检测异常都会清空合格时长,防止误拍。
  • 实时检测:本地 face-api 模型,约每 150ms 用 detectAllFaces + landmarks 判断单人及姿态。连续漏检 3 帧才更换文案,但每次漏检均中断可拍摄状态。按原始帧判断真实左右,持续转错方向也不会自动交换左右照片。切换镜头及退出页面作废旧的检测回调。
  • 镜像策略:前置镜头预览镜像(照镜子习惯);后置预览不镜像,侧脸轮廓随之翻转(.cam-mask.flip)。抓帧一律不镜像,三张照片都是摄像头原始视角。
  • 抓拍反馈:先抓取已校验的当前帧(原始视角、最长边 1280 JPEG),再显示 180ms 闪白和短振动;不在延迟后抓取可能已经偏离姿态的画面。
  • 正式页面无手动快门:相机权限或模型加载失败时显示重试按钮;仅 ?mock=1 调试模式保留手动快门。拍摄进度明确显示正脸→右脸→左脸,完成三张后只送检一次。
  • 相册选图:按当前步骤选择照片,压缩后等待模型加载并校验单人、左右朝向和光线;不合格或检查失败均停留本步,不再跳过校验。读取、校验和抓拍期间锁定操作,离开页面使未完成读取失效。
  • 语音播报:步骤引导/姿态提示/拍摄完成/分析中/失败,文案与文件名契约见 ../docs/语音播报内容设计文档_V1.0.md。新增的提示(居中、抬头/低头、光线)暂无音频,清单缺失的条目静默跳过,只显示文字。
  • 无进展提示:单步 12s 仍未拍到,底部浮出「识别不到?请正对镜头,并到光线充足处」(不阻塞流程)。
  • 资源管理:离开拍照页停止检测循环、清全部定时器、释放摄像头;camSeq(流程)/camSeqCam(相机)双令牌防竞态;face-api 模型只加载一次。

档案页与报告页

  • 档案页:出生年月、性别、省市用统一 mobile-select 滚轮,提供占位提示;按出生月份计算并显示年龄,限制选择结果为 1–99 岁,提交时重新计算以适应跨月。省市联动数据见 js/region-data.js;插件行点击已适配 44px 行高与安全区,见 vendor README。
  • 报告页:无返回按钮与页头标题;总分环形图 + 肌肤年龄与「比实际年龄年轻/大 N 岁」;三维度分数 + 进度条(最低分标红并挂「优先关注」);紧塑力/活力感/稳定力各一张卡,卡内子 Tab 切换;点检测图全屏放大,再点或按返回键回到报告。

检测流程与评分渲染

  1. 三视图送检:POST /api/skin-analyze,请求 { front_image, left_image, right_image, profile? }(base64 可带 dataURL 前缀;正脸=photos[0],左=photos[2],右=photos[1];profile 为可选档案 {real_age,gender,region,daily_skin_type},后台记录用)。
  2. 服务端转发 Face++ 专业版 skinanalyze_pro(三视图必须走 image_file 文件形式),请求 return_side_results=jawline_info 取左右下颌线角度,return_maps/return_marks 取叠加图与标注;响应 { ok:true, result, face_rectangle, side:{ left, right } },失败 { ok:false, code, message }(message 已是用户可读文案)。result.face_maps 为对象,含报告页所需图层的完整 data URL(red_area/brown_area/texture_enhanced_lines/texture_enhanced_pores/texture_enhanced_blackheads,由 server.js 的 REPORT_MAPS 常量控制);marks 坐标(acne/brown_spot/mole/closed_comedones/acne_pustule/acne_nodule/wrinkle_mark/melanin_mark/dark_circle_mark)随 result 原样透传,与上传图片同一像素空间。
  3. 评分由 js/skin-score.js 引擎完成,公式/锁值/话术/产品推荐以 docs/三维度评分与话术生成需求文档_V2.3.docx 为最高契约(V2.3 口径,含 §8.5 护理建议五段):
    • 6 分类:轮廓 = min(眼袋映射分, 下颌线角度分);皱纹(12 分区);光滑(毛孔/黑头/纹理);肤色水润(色素×水润);敏感(泛红面积取 red_spot.red_spot_area/红区分);痘痘(分级映射×丘疹 acne.count 计数扣分);分组按实际年龄;
    • 三维度木桶 min:紧塑力(轮廓+皱纹)、活力感(光滑+肤色水润)、稳定力(敏感+痘痘);
    • S = 7 + 3×P/100;档位:良好(P≥90)/改善(80–90)/预警(<80);肤龄钳制到实际年龄 ±3 并留痕;三视图版 18 条话术;
    • 精华推荐三重判据(均分档/最差项约束/年龄组档)取最强档,triggers 记录命中判据。
  4. 前端 SkinScore.buildViewModel(data.result, data.side, { realAge, skinType }) → viewModel 直渲报告页:
    • 头部:综合评分(vm.total,缺失回退三维度 S 均分×10)、肤龄 vm.skinAgeDisplay + 钳制标记 vm.ageClampNote;三维度 S(1 位小数)+ 档位配色(良好绿/改善琥珀/预警红);
    • 三个 Tab 面板各展示所辖两分类:分类名、S + 档位徽章、话术正文(vm.cats.*.text);P 值不直接展示,P=null 的分类显示"本次未检出有效数据";
    • 报告底部:降级标注(vm.notes 中与侧脸/下颌线/补拍相关的条目,去掉接口质量码后展示)、护理建议五段(vm.careAdvice)、精华推荐卡(vm.essence.name + 命中判据;推荐正文已在护理建议第 4 段,卡内不重复);
    • 近期优先关注 = 最差维度(vm.focus);6 分类各配检测可视化图:轮廓=正脸实拍、皱纹=正脸压暗+texture_enhanced_lines 皱纹增强线(线图层几乎全透明,直挂白底不可读,故与压暗沉脸合成)、光滑=正脸+色斑/痣/痤疮标记叠加、肤色=brown_area 色沉图、敏感=red_area 红区图、痘痘=正脸+闭口粉刺/痤疮标记叠加(marks 按原像素坐标叠加到正脸实拍)。
  5. 浏览器冒烟:node h5/test/e2e-browser.mjs(需全局 playwright + npx playwright install chromium,服务跑在 8080)——mock 三连拍到报告页,并核对 14 条语音可拉取、流程语音完整播完不被截断。
  6. 真值回归:node h5/test/regression.js 以 docs/皮肤检测测试结果_三视图_20260926.xlsx(sheet「三视图检测汇总」,数据第 3/4 行=案例1/案例2)两组三视图真值对照评分引擎输出。
  7. 报告历史:检测成功后按「检测」为单位存入 localStorage(juzi.reports.v1,上限 5 条,超配额逐条淘汰)——含三张照片、档案、全部评分数据与 marks 坐标,不含 face_maps 整幅图层(体积原因;回看时皱纹/肤色/敏感图降级为占位,光滑/痘痘标记叠加仍可用)。当前无 UI 入口(第9轮移除),数据仅作本地追溯。
  8. 原始返回落盘:服务端每次检测成功后把 Face++ 完整原始返回写入 h5/data/results/<时间戳>-<request_id>.json(复核/真值用,已 .gitignore;写盘失败不影响响应)。
  9. 检测失败按服务端 message 提示并退回档案页。

后台查看(admin.html)

本地查看版后台,浏览器打开 http://127.0.0.1:8080/admin.html(无登录鉴权,仅内网/本地使用;上线前需加网关或鉴权)。请求体除三张照片外还带可选档案 profile:{real_age,gender,region,daily_skin_type}(前端 js/app.js 自动附带,年龄非法按 null),服务端据此落库并在检测成功时用 SkinScore.buildViewModel 现场评分(与 C 端同一份公式)。

  • 数据落库:每次有效调用(正脸照片校验通过起)生成 record_id,照片写 data/photos/<record_id>/,记录(档案/照片地址/拍摄与上传状态/检测状态与失败原因/评分/文案/精华/耗时/raw 文件名/重试关系)追加到 data/records.jsonl。C 端响应结构不变。
  • 记录列表:多条件筛选(时间区间/检测状态/失败原因/年龄/产品组别/维度+档位/记录ID),行点击开详情抽屉(照片大图与下载、状态、六分类与三维度评分表、文案全文、护理建议、精华与触发判据、计算备注、接口原始 JSON 折叠查看、手动重试)。
  • 失败与异常:失败原因分布条 + 失败列表(每条可一键重试:用原照片重新检测,生成新记录并挂版本链)。
  • 统计概览:今日/累计/成功率/平均耗时卡片,三维度档位分布,产品组别分布(纯 CSS 饼图),近 14 天每日检测量柱状图,按日期区间导出 CSV(UTF-8 BOM,不含 raw_json)。
  • 演示数据:node h5/data/seed.js 生成约 10 条演示记录(覆盖重写 records.jsonl,照片复制自 reference/facepp-skinanalyze/examples/images/ 真实三视图)。

Admin API(统一返回 {ok:false,message} 错误)

接口 说明
GET /api/admin/records 分页查询。参数:from/to(ISO 日期)、detect_status、fail_code、age_min/age_max、group、dim(firm/vita/stab)+dim_level、q(record_id 前缀)、page、page_size(默认 20)、only_fail=1。返回 {total,page,page_size,items}(倒序,含照片 URL,不含 raw_json 与文案全文)
GET /api/admin/records/:id 完整记录 + raw_json 全文(读失败为 null)+ photos URL + versions(版本链:父记录与重试子记录)
POST /api/admin/records/:id/retry 手动重试:原照片重新检测,生成新记录(retry_of=原 id),原记录 retry_count+1,返回新记录
GET /api/admin/photos/:id/:which 照片回显(which=front/left/right),image/jpeg inline;文件不存在 404
GET /api/admin/stats 今日/累计检测量、成功率、平均耗时、失败原因分布、三维度档位分布、产品组别分布、近 14 天每日检测量/成功数
GET /api/admin/export?from=&to= CSV 导出(UTF-8 BOM,Content-Disposition attachment 带日期区间文件名);禁止 raw_json 进单元格(Excel 单格 32767 字符截断)

部署

需要 Node ≥ 18 运行 server.js(负责静态与 API 中转),不能用纯静态托管(密钥不能进前端)。

PORT=8080 node server.js    # 建议 pm2 / systemd 常驻

线上部署(juzi.xiangsilian.com)

  • Node 只绑回环(HOST 默认 127.0.0.1,公网入口仅 nginx;需局域网调试才 HOST=0.0.0.0),nginx 反代 + certbot HTTPS,见 /etc/nginx/sites-available/juzi。
  • 后台 Basic Auth:nginx 对 admin.html、css/admin.css、js/admin.js、/api/admin/ 做 auth_basic(账密 /etc/nginx/.htpasswd-juzi),/data/(照片/原始 JSON/记录库)由 nginx 404 + server.js 静态层 403 双重封锁,照片只能走鉴权后的 /api/admin/photos。上线正式版前应替换为文档要求的后台账号体系。
  • 服务常驻建议 systemd:
[Unit]
Description=juzi skin h5
After=network.target
[Service]
WorkingDirectory=/home/ubuntu/juzi/h5
ExecStart=/usr/bin/node server.js
Restart=always
[Install]
WantedBy=multi-user.target

Windows 本机部署(提测用)

h5\deploy-local.bat      :: 默认 PORT=8088,日志写 h5\server.log

登记开机自启(用户级计划任务,需系统允许;若提示"拒绝访问",直接双击 deploy-local.bat 或手动运行即可):

schtasks /create /tn "JuziSkinH5" /tr "\"C:\Windows\System32\cmd.exe\" /c \"D:\juzi\h5\deploy-local.bat\"" /sc onlogon /f
:: 移除: schtasks /delete /tn "JuziSkinH5" /f

局域网提测需设置 HOST=0.0.0.0 并允许对应防火墙端口。手机通过局域网 HTTP 访问时不能使用 getUserMedia,需使用 HTTPS 才能测试动态自动拍照;相册选图入口仍可用且逐步校验。

nginx 反代示例(静态与 API 都交给 server.js,nginx 只做 HTTPS 终结):

server {
    listen 443 ssl;
    server_name example.com;
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        client_max_body_size 20m;   # 与 server.js 请求上限一致
    }
}

HTTPS 必需:手机自动拍摄要求安全上下文,正式域名必须使用 HTTPS;本机 localhost 例外。相机不可用时页面显示重试与相册入口。

已知占位清单

  • 产品图(prod-1.jpg)为设计稿素材;检测图已替换为用户实拍。
  • 语音 14 条已由 tools/gen_voice.py 生成;改文案后重跑脚本并同步 manifest.json。
  • 「了解更多」为开发中占位(Toast 提示);拍照页右下按钮为切换前后镜头(系统相机降级模式下提示在系统相机内切换)。
  • 产品推荐卡(COREⅡ 双仓精华液)为设计稿固定素材,不随精华推荐档位(25+/35+/50+)变化,待产品提供三档对应素材。
  • 真机复核:头部姿态系数沿用既有校准,新增多人、屏幕坐标距离/居中与亮度闸门已通过可控浏览器输入测试。前后镜头、不同手机曝光及微信相机权限仍需用真实设备复核;自动化测试不替代硬件实拍。

技术说明

  • 尺寸换算:设计稿 1080px 基准 ÷ 2.88 = 375px 落地基准。
  • 图片性能:首屏背景 preload;首屏外 loading="lazy"。
  • 快门防连点/双流程令牌(camSeq/camSeqCam)防竞态;拍照页取景框外纯黑(对齐 Figma),白字直接落在黑色上。
  • face-api.js 0.22.2 与模型全部本地化(vendor/faceapi/,约 1.4MB),运行时零外部请求,微信 webview / iOS Safari 弱网与内网部署可用;许可证(MIT / Apache-2.0)见 vendor/faceapi/README.md。

UI 反馈验收

node h5/test/e2e-feedback.mjs 验证出生年月、省市联动、自动三连拍的质量闸门与顺序、模型重试、相册校验、图片放大及 320/375/430px 布局。测试拦截分析请求,不调用 Face++。

两个浏览器测试都支持 BASE_URL(例如 http://127.0.0.1:8088)、PLAYWRIGHT_MODULE(本机 Playwright 的 index.mjs 绝对路径)和 E2E_OUTPUT_DIR(截图目录),Windows 与 Linux 均可执行。未指定 Playwright 路径时使用全局安装。