接口5:新增 hairline_level 档位(middle/high/low)贴图 + 集成接口2生发能力
- hairline_level: 可选 middle(默认)/high/low,选用不同高度档位发际线贴图; 新增 hairline_texture_high/low 两套贴图,get_texture_map 改为按档位缓存 - hair_style: 可选逗号分隔序号,对选中发际线类型同步生发(ComfyUI), 结果合并进 hairline_images[].grown_image_base64(未选中为 null); 生发黑模板固定取 middle(hairline_texture_black/),与档位无关 - 新增 use_mask/prompt 生发控制参数(同接口2) - 测试页/接口文档同步更新 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@@ -979,13 +979,21 @@ async def face_features(
|
||||
|
||||
---
|
||||
|
||||
**入参**:新增必填 `gender`(`male`/`female`),决定返回的发际线集合(female 5 / male 4)。
|
||||
**入参**:
|
||||
- 必填 `gender`(`male`/`female`),决定返回的发际线集合(female 5 / male 4)。
|
||||
- 可选 `hairline_level`(`middle`(默认) / `high` / `low`),选用不同档位的发际线贴图。
|
||||
- 可选 `hair_style`(发型序号,逗号分隔如 `1,2,3`):对选中的发际线类型额外**生发**(同接口2)。
|
||||
留空则只返回发际线叠图、不生发。越界/非法返回 `1007`。
|
||||
`female`:1=ellipse,2=flower,3=heart,4=straight,5=wave;`male`:1=ellipse,2=inverse_arc,3=m,4=straight。
|
||||
- 可选 `use_mask` / `prompt`:同接口2 的生发控制参数。
|
||||
注:生发的黑模板固定取 `hairline_texture_black/`(middle),与 `hairline_level` 无关。
|
||||
|
||||
**返回说明**:
|
||||
|
||||
- `hairline_images`:发际线叠加图列表(发际线曲线叠加在用户照片上,同接口2预览),
|
||||
数量 = 该性别的发际线类型数,本期按贴图顺序 `order=1..N`(暂不计算合适度)。
|
||||
worker 返回 `image_base64`,网关落盘后改写为 `image_url`。
|
||||
每项还含 `grown_image_base64`:当 `order` 在 `hair_style` 中时为该类型的**生发图**,否则为 `null`。
|
||||
- `best_hairline_center_point`:最佳(`order=1`)发际线曲线的**面部中间点**坐标,
|
||||
以**原图像素**为基准(左上角为原点,x 向右,y 向下)。
|
||||
""",
|
||||
@@ -1000,8 +1008,8 @@ async def face_features(
|
||||
"request_id": "mock-request-id",
|
||||
"data": {
|
||||
"hairline_images": [
|
||||
{"image_base64": "iVBORw0KGgo...", "order": 1},
|
||||
{"image_base64": "iVBORw0KGgo...", "order": 2},
|
||||
{"image_base64": "iVBORw0KGgo...", "grown_image_base64": "iVBORw0KGgo...", "order": 1},
|
||||
{"image_base64": "iVBORw0KGgo...", "grown_image_base64": None, "order": 2},
|
||||
],
|
||||
"best_hairline_center_point": {"x": 540, "y": 430},
|
||||
},
|
||||
@@ -1024,9 +1032,23 @@ async def hairline_generate(
|
||||
image_url: Optional[str] = Form(default=None, description="图片 URL"),
|
||||
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
|
||||
gender: Optional[str] = Form(default=None, description="性别 male/female(必填)"),
|
||||
hairline_level: str = Form(default="middle", description="发际线贴图档位 middle(默认)/high/low"),
|
||||
hair_style: Optional[str] = Form(default=None, description="生发发型序号逗号分隔(可选,如 1,2,3)。留空则只返回发际线叠图不生发。female:1-5 male:1-4"),
|
||||
use_mask: bool = Form(default=True, description="生发是否启用 inpaint 遮罩(同接口2,测试对比用)"),
|
||||
prompt: str = Form(default="补充遮罩区域的头发", description="ComfyUI 提示词(同接口2),会替换工作流节点60的文本"),
|
||||
):
|
||||
if gender not in ("male", "female"):
|
||||
return err(1004, "gender 必填且只能为 male / female")
|
||||
if hairline_level not in ("middle", "high", "low"):
|
||||
return err(1004, "hairline_level 只能为 middle / high / low")
|
||||
|
||||
# hair_style 可选:留空 → 不生发;非空但非法 → 1007
|
||||
hair_styles = None
|
||||
if hair_style and hair_style.strip():
|
||||
max_styles = {"female": 5, "male": 4}[gender]
|
||||
hair_styles = _parse_hair_styles(hair_style, max_styles)
|
||||
if hair_styles is None:
|
||||
return err(1007, f"hair_style 需为 1..{max_styles} 的整数(逗号分隔),收到 {hair_style!r}")
|
||||
|
||||
raw, e = await resolve_image_bytes(image_file, image_url, image_base64)
|
||||
if e is not None:
|
||||
@@ -1040,14 +1062,17 @@ async def hairline_generate(
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
from hairline.service import generate_hairline_pngs
|
||||
|
||||
res = await run_in_threadpool(generate_hairline_pngs, image, gender)
|
||||
res = await run_in_threadpool(
|
||||
generate_hairline_pngs, image, gender, hairline_level, hair_styles, use_mask, prompt)
|
||||
if res is None:
|
||||
return err(1001, "无法识别人像")
|
||||
|
||||
hairline_images = []
|
||||
for it in res["images"]:
|
||||
hairline_images.append({
|
||||
"image_base64": _jpg_b64(it["image_bgr"]), # 发际线叠图 JPG
|
||||
"image_base64": _jpg_b64(it["image_bgr"]), # 发际线叠图 JPG
|
||||
"grown_image_base64": (_png_to_jpg_b64(it["grown_png"]) # 生发图 JPG(未生发为 null)
|
||||
if it.get("grown_png") else None),
|
||||
"order": it["order"],
|
||||
})
|
||||
c = res["best_center"]
|
||||
|
||||
@@ -373,7 +373,7 @@
|
||||
|
||||
## 接口 5:发际线 PNG 生成接口
|
||||
|
||||
**说明**:输入用户照片,返回 N 张用户发际线的 PNG 图片,并返回「最合适发际线」的面部中间点坐标。
|
||||
**说明**:输入用户照片,返回 N 张用户发际线的 PNG 图片,并返回「最合适发际线」的面部中间点坐标。可选地对指定发际线类型**同步生发**(同接口2)。
|
||||
|
||||
**请求**:`POST /api/v1/hairline/generate`
|
||||
|
||||
@@ -384,6 +384,12 @@
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| gender | string | **是** | 性别:`male` / `female`。决定返回的发际线集合(female 5 / male 4) |
|
||||
| hairline_level | string | 否 | 发际线贴图档位:`middle`(默认)/ `high` / `low`,选用不同高度档位的发际线贴图(仅影响发际线叠图)。非法返回 `1004` |
|
||||
| hair_style | string | 否 | 生发发型序号,**逗号分隔多选**(如 `1,2,3`)。**留空则只返回发际线叠图、不生发**。female:1=ellipse, 2=flower, 3=heart, 4=straight, 5=wave;male:1=ellipse, 2=inverse_arc, 3=m, 4=straight。越界/非法返回 `1007` |
|
||||
| use_mask | bool | 否 | 生发是否启用 inpaint 遮罩,默认 `true`。`false` 时用干净原图生成(空遮罩、不烧模板黑线),供测试对比 |
|
||||
| prompt | string | 否 | ComfyUI 提示词,默认「补充遮罩区域的头发」,会替换工作流节点 60 的文本 |
|
||||
|
||||
> ⚠️ 生发的黑模板固定取自 `hairline_texture_black/`(middle 档),与 `hairline_level` 无关:即 `high`/`low` 档下发际线叠图用对应档位贴图,但生发目标仍压到 middle 档。
|
||||
|
||||
### 输出(data)
|
||||
|
||||
@@ -397,6 +403,7 @@
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| image_url | string | 发际线叠加图 URL(worker 返回 `image_base64`,网关落盘后改写为 url) |
|
||||
| grown_image_url | string \| null | **生发后图片** URL(ComfyUI「植发」效果图)。仅当该项 `order` 在 `hair_style` 中时有值,否则为 `null`(worker 返回 `grown_image_base64`,网关落盘后改写为 url) |
|
||||
| order | int | 排序序号(本期固定 `1..N`,暂不计算合适度) |
|
||||
|
||||
### 响应示例(当前 Mock 返回值)
|
||||
@@ -408,14 +415,16 @@
|
||||
"request_id": "mock-request-id",
|
||||
"data": {
|
||||
"hairline_images": [
|
||||
{ "image_url": "https://hair.xiangsilian.com/static/sample.jpg", "order": 1 },
|
||||
{ "image_url": "https://hair.xiangsilian.com/static/sample.jpg", "order": 2 }
|
||||
{ "image_url": "https://hair.xiangsilian.com/static/annotations/uuid1.png", "grown_image_url": "https://hair.xiangsilian.com/static/annotations/grown1.png", "order": 1 },
|
||||
{ "image_url": "https://hair.xiangsilian.com/static/annotations/uuid2.png", "grown_image_base64": null, "order": 2 }
|
||||
],
|
||||
"best_hairline_center_point": { "x": 540, "y": 430 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 说明:未生发的元素中,网关不改写 `null` 值,故字段名保持为 `grown_image_base64: null`(有值时才改写为 `grown_image_url`),与接口2生发失败项一致。
|
||||
|
||||
---
|
||||
|
||||
## 接口 7:C 端生发 v2 接口
|
||||
@@ -480,7 +489,7 @@
|
||||
| 2 C 端生发 | 用户照片 | 生发后图片 + 指定发际线预览(单/多张) |
|
||||
| 3 B 端生发 | 划线图片 | 最合适发际线图片 + 生发后图片 |
|
||||
| 4 用户特征 | 用户照片 | 6 个用户特征字段(脸形/眉形/年龄/动静/性别/基因风格) |
|
||||
| 5 发际线 PNG | 用户照片 | N 张发际线 PNG + 最合适发际线面部中间点坐标 |
|
||||
| 5 发际线 PNG | 用户照片 + gender(+ hairline_level 档位 / hair_style 生发) | N 张发际线 PNG(可含生发图)+ 最合适发际线面部中间点坐标 |
|
||||
| 7 C 端生发 v2 | 用户照片 + gender + hair_style | 同接口2,使用 add_hair2.json 工作流 |
|
||||
|
||||
---
|
||||
|
||||
@@ -29,13 +29,20 @@ _REPO = os.path.dirname(os.path.dirname(__file__))
|
||||
_TEXTURE_DIR = os.path.join(_REPO, "hairline_texture")
|
||||
_BLACK_TEXTURE_DIR = os.path.join(_REPO, "hairline_texture_black")
|
||||
|
||||
# 发际线贴图档位:middle=默认(hairline_texture/),high/low 各自独立文件夹。
|
||||
_TEXTURE_DIRS = {
|
||||
"middle": _TEXTURE_DIR,
|
||||
"high": os.path.join(_REPO, "hairline_texture_high"),
|
||||
"low": os.path.join(_REPO, "hairline_texture_low"),
|
||||
}
|
||||
|
||||
# ⚠️ 本 worker 是 RTX 5090(sm_120),torch 2.2.2(cu121) 只编到 sm_90,CUDA 跑算子会报
|
||||
# "no kernel image"。SegFormer 默认走 CPU(~2.5s/张)。换 torch cu128 后可设 SEG_DEVICE=cuda。
|
||||
_SEG_DEVICE = os.getenv("SEG_DEVICE", "cpu")
|
||||
|
||||
_landmarker = None
|
||||
_parser = None
|
||||
_texture_map = None
|
||||
_texture_maps: dict = {} # {level: {gender: [(key, path)]}},按档位缓存
|
||||
|
||||
|
||||
def get_landmarker() -> FaceLandmarker:
|
||||
@@ -61,24 +68,27 @@ def _gender_key(stem: str):
|
||||
return None, None
|
||||
|
||||
|
||||
def get_texture_map() -> dict:
|
||||
"""扫描 hairline_texture/ 建 {gender: [(key, path)]},按 key 排序、缓存。
|
||||
def get_texture_map(level: str = "middle") -> dict:
|
||||
"""扫描指定档位贴图目录建 {gender: [(key, path)]},按 key 排序、按档位缓存。
|
||||
|
||||
level:middle(默认) / high / low,分别对应 hairline_texture[/_high|/_low]。
|
||||
文件名规范化去空格(如 `man_ inverse_arc.png` → key `inverse_arc`)。
|
||||
"""
|
||||
global _texture_map
|
||||
if _texture_map is not None:
|
||||
return _texture_map
|
||||
if level not in _TEXTURE_DIRS:
|
||||
raise ValueError(f"hairline_level 必须是 middle/high/low,收到 {level!r}")
|
||||
cached = _texture_maps.get(level)
|
||||
if cached is not None:
|
||||
return cached
|
||||
mapping: dict[str, list] = {"female": [], "male": []}
|
||||
for path in sorted(glob.glob(os.path.join(_TEXTURE_DIR, "*.png"))):
|
||||
for path in sorted(glob.glob(os.path.join(_TEXTURE_DIRS[level], "*.png"))):
|
||||
stem = os.path.splitext(os.path.basename(path))[0]
|
||||
gender, key = _gender_key(stem)
|
||||
if gender:
|
||||
mapping[gender].append((key, path))
|
||||
for g in mapping:
|
||||
mapping[g].sort(key=lambda kp: kp[0])
|
||||
_texture_map = mapping
|
||||
return _texture_map
|
||||
_texture_maps[level] = mapping
|
||||
return mapping
|
||||
|
||||
|
||||
def extract_502(image_bgr: np.ndarray):
|
||||
@@ -190,10 +200,40 @@ def generate_grow_results(image_bgr: np.ndarray, gender: str, use_mask: bool = T
|
||||
return results
|
||||
|
||||
|
||||
def generate_hairline_pngs(image_bgr: np.ndarray, gender: str):
|
||||
"""接口5:该性别全部发际线叠图(同接口2预览) + 最佳(order1)发际线曲线的面部中间点。
|
||||
def _grow_from_texture(image_bgr: np.ndarray, ctx: dict, white_path: str | None,
|
||||
use_mask: bool, prompt: str | None):
|
||||
"""对单个发际线做生发(ComfyUI)。黑模板固定取 hairline_texture_black/(middle),
|
||||
与 hairline_level 无关(high/low 贴图与 middle 同名,basename 映射即落回 middle 黑模板)。
|
||||
use_mask=False 时用干净原图+空遮罩(与贴图无关,white_path 可为 None)。
|
||||
失败返回 None,不抛异常。
|
||||
"""
|
||||
try:
|
||||
if use_mask:
|
||||
black = load_texture_rgba(_black_texture_path(white_path))
|
||||
marked, mask = build_inpaint_mask(
|
||||
image_bgr, ctx["landmarks"], ctx["parse_map"], ctx["points"], black)
|
||||
else:
|
||||
h, w = image_bgr.shape[:2]
|
||||
marked, mask = image_bgr, np.zeros((h, w), np.uint8)
|
||||
buf = io.BytesIO()
|
||||
compose_comfy_rgba(marked, mask).save(buf, format="PNG")
|
||||
return comfyui.run(buf.getvalue(), prompt=prompt)
|
||||
except Exception as e: # noqa: BLE001 单张失败不拖垮整请求
|
||||
logger.warning("接口5 生发图失败:%s", e)
|
||||
return None
|
||||
|
||||
Returns: {"images":[{hairline_type,order,image_bgr}], "best_center":(x,y)};无人脸 None。
|
||||
|
||||
def generate_hairline_pngs(image_bgr: np.ndarray, gender: str, hairline_level: str = "middle",
|
||||
hair_styles: list[int] | None = None, use_mask: bool = True,
|
||||
prompt: str | None = None):
|
||||
"""接口5:该性别全部发际线叠图(同接口2预览) + 最佳(order1)发际线曲线的面部中间点,
|
||||
并对 hair_styles 选中的类型生发(同接口2)。
|
||||
|
||||
hairline_level:middle(默认)/high/low,选用不同档位的发际线贴图(仅影响叠图)。
|
||||
hair_styles(1-indexed 列表,按贴图排序):指定对哪些类型生发;None/[] 时不生发。
|
||||
生发黑模板固定取自 hairline_texture_black/(middle),与 hairline_level 无关。
|
||||
use_mask/prompt:同接口2 的生发参数。
|
||||
Returns: {"images":[{hairline_type,order,image_bgr,grown_png}], "best_center":(x,y)};无人脸 None。
|
||||
"""
|
||||
if gender not in ("male", "female"):
|
||||
raise ValueError(f"gender 必须是 male/female,收到 {gender!r}")
|
||||
@@ -206,12 +246,24 @@ def generate_hairline_pngs(image_bgr: np.ndarray, gender: str):
|
||||
# 面部中轴 x = 眉心(9/151 中点)
|
||||
face_cx = float((lm[9, 0] + lm[151, 0]) / 2 * w)
|
||||
|
||||
textures = get_texture_map()[gender]
|
||||
textures = get_texture_map(hairline_level)[gender]
|
||||
grow_set = set(hair_styles or [])
|
||||
|
||||
# use_mask=False:干净原图+空遮罩与贴图无关,只跑一次 ComfyUI,选中项复用
|
||||
shared_grown = None
|
||||
if grow_set and not use_mask:
|
||||
shared_grown = _grow_from_texture(image_bgr, ctx, None, use_mask=False, prompt=prompt)
|
||||
|
||||
images, best_center = [], None
|
||||
for order, (key, path) in enumerate(textures, start=1):
|
||||
white = load_texture_rgba(path)
|
||||
preview = render_hairline_overlay(image_bgr, ctx["points"], ext_faces, uv, white)
|
||||
images.append({"hairline_type": key, "order": order, "image_bgr": preview})
|
||||
grown_png = None
|
||||
if order in grow_set:
|
||||
grown_png = shared_grown if not use_mask else \
|
||||
_grow_from_texture(image_bgr, ctx, path, use_mask=True, prompt=prompt)
|
||||
images.append({"hairline_type": key, "order": order,
|
||||
"image_bgr": preview, "grown_png": grown_png})
|
||||
if order == 1: # 最佳发际线曲线的中点(面部中轴处的发际线 y)
|
||||
overlay = build_overlay_layer(h, w, ctx["points"], ext_faces, uv, white)
|
||||
ys, xs = np.where(overlay[:, :, 3] > 40)
|
||||
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
@@ -21,6 +21,7 @@
|
||||
.form-group { display: flex; align-items: center; gap: 6px; white-space: nowrap; }
|
||||
.form-group label { font-size: 13px; font-weight: 600; color: #374151; }
|
||||
.form-group select { padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; }
|
||||
.form-group input[type=text] { padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; width: 150px; }
|
||||
.btn { padding: 10px 28px; border: none; border-radius: 8px; font-size: 15px; cursor: pointer; font-weight: 600; transition: .2s; }
|
||||
.btn-primary { background: #2563eb; color: #fff; }
|
||||
.btn-primary:hover { background: #1d4ed8; }
|
||||
@@ -72,7 +73,7 @@
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>💈 接口5 — 发际线 PNG 生成 测试</h1>
|
||||
<p class="subtitle">POST /api/v1/hairline/generate | 上传正面照 + 性别 → N 张发际线叠加图 + 最佳中心点坐标</p>
|
||||
<p class="subtitle">POST /api/v1/hairline/generate | 上传正面照 + 性别(+档位/生发序号)→ N 张发际线叠加图(含生发图)+ 最佳中心点坐标</p>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
@@ -82,6 +83,14 @@
|
||||
<label>性别</label>
|
||||
<select id="gender"><option value="female" selected>👩 Female(5种)</option><option value="male">👨 Male(4种)</option></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>发际线档位</label>
|
||||
<select id="hairlineLevel"><option value="middle" selected>middle(默认)</option><option value="high">high</option><option value="low">low</option></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label title="逗号分隔如 1,2,3;留空则只出发际线不生发">生发序号</label>
|
||||
<input type="text" id="hairStyle" placeholder="如 1,2 留空不生发">
|
||||
</div>
|
||||
<button class="btn btn-primary" id="submitBtn" onclick="submitTest()">🚀 提交</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="clearResults()">清除</button>
|
||||
</div>
|
||||
@@ -140,7 +149,8 @@ async function submitTest() {
|
||||
$('submitBtn').disabled = true; $('submitBtn').textContent = '⏳ ...';
|
||||
setStatus('请求中...', 'info'); $('resultsArea').classList.add('hidden');
|
||||
|
||||
const fd = new FormData(); fd.append('image_file', f); fd.append('gender', $('gender').value);
|
||||
const fd = new FormData(); fd.append('image_file', f); fd.append('gender', $('gender').value); fd.append('hairline_level', $('hairlineLevel').value);
|
||||
const _hs = $('hairStyle').value.trim(); if (_hs) fd.append('hair_style', _hs);
|
||||
const _reqStart = performance.now();
|
||||
try {
|
||||
const r = await fetch(API_BASE + '/api/v1/hairline/generate', { method:'POST', body:fd });
|
||||
@@ -169,7 +179,9 @@ function renderGrid() {
|
||||
_images.forEach((img, i) => {
|
||||
h += '<div class="result-card'+(i===0?' selected':'')+'" onclick="selectCard('+i+',this)">'+
|
||||
'<div class="img-wrap"><img src="'+img.image_url+'" alt="#'+img.order+'" loading="lazy"></div>'+
|
||||
'<div class="info"><span class="badge">#'+(img.order||'—')+'</span>'+(i===0?'<span style="font-size:11px;color:#059669">⭐ 最佳</span>':'')+'</div></div>';
|
||||
'<div class="info"><span class="badge">#'+(img.order||'—')+'</span>'+
|
||||
(img.grown_image_url?'<span style="font-size:11px;color:#7c3aed">🌱 生发</span>':'')+
|
||||
(i===0?'<span style="font-size:11px;color:#059669">⭐ 最佳</span>':'')+'</div></div>';
|
||||
});
|
||||
$('resultsGrid').innerHTML = h;
|
||||
}
|
||||
@@ -177,11 +189,13 @@ function renderGrid() {
|
||||
function selectCard(idx, el) {
|
||||
document.querySelectorAll('.result-card').forEach(c => c.classList.remove('selected'));
|
||||
if (el) el.classList.add('selected');
|
||||
const g = _images[idx].grown_image_url;
|
||||
$('previewArea').innerHTML =
|
||||
'<div class="preview-stack">'+
|
||||
'<img class="layer-base5" src="'+_origUrl+'" alt="原图">'+
|
||||
'<img class="layer-over5" src="'+_images[idx].image_url+'" alt="#'+_images[idx].order+'">'+
|
||||
'</div>';
|
||||
'</div>'+
|
||||
(g?'<div style="margin-top:10px"><div style="font-size:12px;color:#7c3aed;margin-bottom:4px">🌱 生发图</div><img src="'+g+'" alt="生发 #'+_images[idx].order+'" style="max-width:100%;border-radius:8px"></div>':'');
|
||||
syncOverlay();
|
||||
}
|
||||
|
||||
|
||||