"""根据 results.json 生成发际线生发对比报告 HTML。 报告布局(原图 vs 结果对比): - 顶部:总览统计(成功/失败数、耗时、参数) - 按脸分组,每张脸一个区块: - 左:原图 - 右:5 发型 × {高清, 非高清} 网格(共 10 张),失败的格子标注原因 - 失败用例汇总表 输出:static/report_hairline_v2/index.html """ import html import json import os from urllib.parse import quote OUT_DIR = "/home/xsl/hair/static/report_hairline_v2" RESULTS = os.path.join(OUT_DIR, "results.json") TARGET = os.path.join(OUT_DIR, "index.html") def url(path): """把相对路径里的中文做 URL 编码,分隔符 '/' 保留。 Starlette StaticFiles 对未编码中文路径返回 400,编码后所有浏览器稳定可加载。 """ return "/".join(quote(seg) for seg in path.split("/")) def main(): with open(RESULTS, encoding="utf-8") as fh: data = json.load(fh) meta = data["meta"] results = data["results"] hairstyles = meta["hairstyles"] # [{id, cn}] faces = meta["faces"] # [stem, ...] hr_opts = meta["hr_options"] # [{is_hr, tag}] # 索引:(stem, hair_id, hr_tag) -> result idx = {} for r in results: idx[(r["stem"], r["hair_id"], r["hr_tag"])] = r # 统计 ok = sum(1 for r in results if r["ok"]) fail = len(results) - ok parts = [] parts.append(f""" 发际线生发对比报告 · 接口12 grow_v2

发际线生发对比报告 接口12 · grow_v2

固定:pushed 遮罩 + multiband 融合 · mb_levels=5 · erode_cm=0.6 · 非高清
生成时间:{html.escape(meta['generated_at'])} · 并发 {meta['concurrency']} · 耗时 {meta['elapsed_sec']}s
{meta['total']}
总调用
{ok}
成功
{fail}
失败
{len(faces)}
人脸数
{len(hairstyles)}
发型数
""") # 图例 parts.append('
每张脸:第一列为原图,其余 5 列为发际线生发结果(非高清)。点击任意图片可放大。
') # 每张脸一个区块:原图 + 5 发型横向并排 for stem in faces: orig_rel = f"orig/{stem}.jpg" parts.append('
') parts.append(f'

{html.escape(stem)}

') # 网格:第一列原图,其余 5 列发型结果 parts.append('
') # 表头 parts.append('
原图
') for h in hairstyles: parts.append(f'
{html.escape(h["cn"])}
{html.escape(h["id"])}
') # 第一列:原图大图(可点击放大) parts.append( f'
原图 {html.escape(stem)}' f'
原图
') # 5 发型结果(hr_opts 现在只有 nohr 一档,直接取) hr_tag = hr_opts[0]["tag"] if hr_opts else "nohr" for h in hairstyles: r = idx.get((stem, h["id"], hr_tag)) if r and r["ok"]: ms = r["ms"] parts.append( f'
' f'
{ms}ms
') else: err = (r or {}).get("error", "未执行") parts.append( f'
{html.escape(err)}
') parts.append('
') # grid parts.append('
') # face-block # 失败汇总 fails = [r for r in results if not r["ok"]] if fails: parts.append('
') parts.append(f'

失败用例({len(fails)})

') parts.append('' '' '') for r in fails: parts.append( f'' f'' f'' f'') parts.append('
人脸发型耗时(ms)原因
{html.escape(r["stem"])}{html.escape(r["hair_cn"])}{r["ms"]}{html.escape(r["error"] or "")}
') parts.append(f"""
""") with open(TARGET, "w", encoding="utf-8") as fh: fh.write("".join(parts)) print(f"报告已生成:{TARGET}") print(f"成功 {ok}/{meta['total']},失败 {fail}") if __name__ == "__main__": main()