1. 城市下拉浮层 z-index 提至 50,不再被肤质轮播圆点压盖 2. 倒计时音频已更新(用户重生成,解码验证 2.62s) 3. 侧脸步蒙版鼻向修正:2/3 右脸鼻朝右、3/3 左脸鼻朝左(原与设计稿 06/07 相反) 4. 无进展提示上移至 bottom:24%(引导 chips 上方,不再压快门) 5. 相册选择器「拍照」项为 iOS/安卓系统行为,web 无法移除(详见回复) 6. 报告页顶栏移除,返回/历史改悬浮 7. 肌龄小字按产品要求不再展示(钳制留痕仍随数据返回) 8. 关注行对齐设计稿:前缀灰 + 维度名深色 500 9. 三维度排版对齐设计稿:去掉档位徽章,最低分标红 10. 肤色水润图改用 water_area(接口水分图),REPORT_MAPS 与 mock fixture 同步更新 11. 报告「综合分析」「生活方式建议」通栏移除,新增「护理建议」卡(V2.3 §8:肤质定制/最差2分类/精华/生活方式 五段,§8.5.1 无内部标注;skin-score 实现 8.6/8.7 全量文案库,回归新增 §8.8 两组样例逐字断言 86/86 绿) e2e 全量绿(含真实 API:水分图/红区合成图挂载、护理建议五段)
105 lines
4.5 KiB
Python
105 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
||
# ─── 报告页可视化调试 fixture 生成器(仅本目录工具,需 Pillow) ───
|
||
# 数据来源:reference/facepp-skinanalyze/examples/ref_3view_raw.json(案例1 完整原始返回)
|
||
# + reference/facepp-skinanalyze/examples/images/C3_180.jpg(案例1 正脸 4000×3000)
|
||
# 产物:report-fixture.js → window.MOCK_FIXTURE = { result, face_rectangle, side, photos:[正脸dataURL,null,null] }
|
||
# 生产不变量:照片与 face_maps 统一缩到宽 1280,marks 坐标同比例缩放(坐标空间 = 照片空间)
|
||
# 用法:python3 build-fixture.py
|
||
import base64
|
||
import io
|
||
import json
|
||
import os
|
||
|
||
from PIL import Image
|
||
|
||
ROOT = os.path.dirname(os.path.abspath(__file__))
|
||
REF = os.path.join(ROOT, '..', '..', '..', 'reference', 'facepp-skinanalyze', 'examples')
|
||
TARGET_W = 1280
|
||
|
||
# 与服务端 REPORT_MAPS 白名单一致(h5/server.js)
|
||
MAP_KEYS = ['red_area', 'brown_area', 'water_area', 'texture_enhanced_lines', 'texture_enhanced_pores', 'texture_enhanced_blackheads']
|
||
|
||
|
||
def to_data_url(img, fmt):
|
||
buf = io.BytesIO()
|
||
if fmt == 'JPEG':
|
||
img.convert('RGB').save(buf, 'JPEG', quality=85)
|
||
mime = 'image/jpeg'
|
||
else:
|
||
img.save(buf, 'PNG', optimize=True)
|
||
mime = 'image/png'
|
||
return 'data:%s;base64,%s' % (mime, base64.b64encode(buf.getvalue()).decode('ascii'))
|
||
|
||
|
||
def scale_coords(node, s):
|
||
"""递归缩放 result 内全部坐标:{left,top,width,height} 矩形与 {x,y} 点,与原数据同为整数。"""
|
||
if isinstance(node, dict):
|
||
ks = node.keys()
|
||
if {'left', 'top', 'width', 'height'} <= ks and all(isinstance(node[k], (int, float)) for k in ('left', 'top', 'width', 'height')):
|
||
node['left'] = round(node['left'] * s)
|
||
node['top'] = round(node['top'] * s)
|
||
node['width'] = max(1, round(node['width'] * s))
|
||
node['height'] = max(1, round(node['height'] * s))
|
||
return
|
||
if {'x', 'y'} <= ks and isinstance(node['x'], (int, float)) and isinstance(node['y'], (int, float)):
|
||
node['x'] = round(node['x'] * s)
|
||
node['y'] = round(node['y'] * s)
|
||
return
|
||
for v in node.values():
|
||
scale_coords(v, s)
|
||
elif isinstance(node, list):
|
||
for v in node:
|
||
scale_coords(v, s)
|
||
|
||
|
||
def main():
|
||
raw = json.load(open(os.path.join(REF, 'ref_3view_raw.json'), encoding='utf-8'))
|
||
result = raw['result']
|
||
src_w, src_h = Image.open(os.path.join(REF, 'images', 'C3_180.jpg')).size
|
||
s = TARGET_W / src_w # 4000 → 1280:0.32
|
||
target_h = round(src_h * s)
|
||
|
||
# 正脸照片
|
||
photo = Image.open(os.path.join(REF, 'images', 'C3_180.jpg')).convert('RGB')
|
||
photo = photo.resize((TARGET_W, target_h), Image.LANCZOS)
|
||
photo_url = to_data_url(photo, 'JPEG')
|
||
|
||
# face_maps:白名单图层,同比例缩放;格式保持原样(jpeg 图层出 jpeg,透明叠加层出 png,与服务端魔数判定一致)
|
||
maps = {}
|
||
for k in MAP_KEYS:
|
||
b64 = result.get('face_maps', {}).get(k)
|
||
if not b64:
|
||
continue
|
||
im = Image.open(io.BytesIO(base64.b64decode(b64)))
|
||
fmt = im.format # resize 后 format 丢失,先取
|
||
im = im.resize((TARGET_W, target_h), Image.LANCZOS)
|
||
maps[k] = to_data_url(im, 'JPEG' if fmt == 'JPEG' else 'PNG')
|
||
|
||
# marks 坐标随照片同比例缩放(face_maps 已替换为缩放后 dataURL,不参与坐标缩放)
|
||
result['face_maps'] = maps
|
||
scale_coords(result, s)
|
||
face_rect = raw.get('face_rectangle')
|
||
if face_rect:
|
||
scale_coords(face_rect, s)
|
||
|
||
fixture = {
|
||
'result': result,
|
||
'face_rectangle': face_rect,
|
||
'side': {'left': raw.get('left_side_result'), 'right': raw.get('right_side_result')},
|
||
'photos': [photo_url, None, None] # 与 CAM_STEPS 同序:[0]正脸;侧脸图未纳入 fixture
|
||
}
|
||
out = os.path.join(ROOT, 'report-fixture.js')
|
||
with open(out, 'w', encoding='utf-8') as f:
|
||
f.write('/* 报告页可视化 mock fixture — 由 build-fixture.py 生成,勿手改\n')
|
||
f.write(' 来源:ref_3view_raw.json(案例1)+ C3_180.jpg;照片与 face_maps 统一宽 %d,marks 坐标同比例缩放(×%.2f)\n' % (TARGET_W, s))
|
||
f.write(' 仅在 ?mock=1 时由 index.html 按需注入,生产零额外请求 */\n')
|
||
f.write('window.MOCK_FIXTURE = ')
|
||
json.dump(fixture, f, ensure_ascii=False, separators=(',', ':'))
|
||
f.write(';\n')
|
||
print('[build-fixture] %s %.1f KB(照片 %d×%d,图层 %d 张)' % (
|
||
out, os.path.getsize(out) / 1024, TARGET_W, target_h, len(maps)))
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|