save code

This commit is contained in:
Ubuntu
2026-06-13 12:11:31 +08:00
parent 1233952571
commit d8a6e53979
14 changed files with 1074 additions and 6 deletions
+547 -6
View File
@@ -1,13 +1,554 @@
from fastapi import FastAPI
"""旷视五接口 - 第一版(Mock 实现)
app = FastAPI(title="hair")
当前为 Mock 服务,所有接口返回固定示例值,不做真实算法计算。
图片字段统一返回 https://hair.xiangsilian.com/static/sample.jpg。
"""
import json
from typing import Any, List, Optional
from fastapi import FastAPI, File, Form, UploadFile
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, Field
# ---------------------------------------------------------------------------
# App & 全局常量
# ---------------------------------------------------------------------------
BASE_URL = "https://hair.xiangsilian.com"
SAMPLE_IMAGE_URL = f"{BASE_URL}/static/sample.jpg"
app = FastAPI(
title="旷视五接口",
version="0.1.0",
description="""
## 概述
本服务提供五个人像分析接口,当前为 **Mock 第一版**:
- 接口已全部上线,传任意合法参数均可正常响应
- 所有字段返回固定示例值,图片字段统一指向示例图片
- 真实算法逻辑后续接入,字段结构不变
## 图片传参说明
每个接口的图片参数均支持三种方式,**严格互斥,必须且只能选其一**:
| 方式 | 字段名 | 说明 |
|------|--------|------|
| 文件上传 | `image_file` | `multipart/form-data`,单文件 ≤ 1 MB |
| URL | `image_url` | 图片的完整 HTTP/HTTPS 地址 |
| base64 | `image_base64` | 需携带前缀,如 `data:image/jpeg;base64,xxxx` |
> 传 0 个或同时传多个,均返回错误码 `1007`。
## 图片要求
- 格式:**JPG / PNG**
- 分辨率:最低 1080×1920,最大 4000×5000
- 人脸数量:仅支持**单人**,多人返回错误码 `1005`
- 文件大小:≤ 1 MB(文件上传方式)
## 统一响应结构
```json
{
"code": 0,
"message": "success",
"request_id": "唯一请求标识",
"data": {}
}
```
`code = 0` 表示成功,非 0 表示失败,`message` 为具体原因。
## 错误码一览
| code | 说明 |
|------|------|
| 1001 | 无法识别人像 |
| 1002 | 人像分辨率过低 |
| 1003 | 非正面照 / 角度过大 |
| 1004 | 性别标签无法判定 |
| 1005 | 检测到多张人脸(仅支持单人)|
| 1006 | 文件超出 1 MB 限制 |
| 1007 | 图片参数错误(未传或同时传多个)|
| 1008 | 图片格式不支持(仅 JPG / PNG)|
""",
)
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/")
async def hello():
return {"message": "Hello, World!"}
# ---------------------------------------------------------------------------
# 通用响应模型
# ---------------------------------------------------------------------------
def ok(data: Any) -> dict:
return {
"code": 0,
"message": "success",
"request_id": "mock-request-id",
"data": data,
}
@app.get("/health")
def err(code: int, message: str) -> dict:
return {
"code": code,
"message": message,
"request_id": "mock-request-id",
"data": None,
}
# ---------------------------------------------------------------------------
# 通用图片请求 BodyJSON 方式,用于 url / base64
# ---------------------------------------------------------------------------
_image_fields_desc = (
"图片传参方式严格互斥,必须且只能选其一:\n"
"- **image_file**multipart/form-data 上传,≤ 1 MB\n"
"- **image_url**(完整 HTTP/HTTPS 地址)\n"
"- **image_base64**(需携带前缀,如 `data:image/jpeg;base64,xxxx`\n\n"
"同时传多个或一个都不传,均返回错误码 `1007`。"
)
class ImageJsonBody(BaseModel):
image_url: Optional[str] = Field(
default=None,
description="图片 URL(与 image_base64 二选一,不可同时传)",
examples=["https://hair.xiangsilian.com/static/sample.jpg"],
)
image_base64: Optional[str] = Field(
default=None,
description="图片 base64,需带前缀,如 `data:image/jpeg;base64,xxxx`",
examples=["data:image/jpeg;base64,/9j/4AAQSkZJRgAB..."],
)
model_config = {
"json_schema_extra": {
"examples": [
{
"summary": "使用 URL 传图",
"value": {"image_url": "https://hair.xiangsilian.com/static/sample.jpg"},
},
{
"summary": "使用 base64 传图",
"value": {"image_base64": "data:image/jpeg;base64,/9j/4AAQSkZJRgAB..."},
},
]
}
}
# ---------------------------------------------------------------------------
# 接口 1:四庭七眼测量标注
# ---------------------------------------------------------------------------
@app.post(
"/api/v1/face/measure",
summary="接口1 四庭七眼测量标注",
tags=["人脸分析"],
description=f"""
输入用户正面照,返回:
- 标注好四庭七眼数据的 **PNG 图片**(仅标注图层,不含人物)
- 四庭(顶庭/上庭/中庭/下庭)各段**厘米数值及占比**
- 七眼(眼宽/脸宽/两眼间距)**厘米数值及占比**
- 五个关键分界点的**原图像素坐标**(头顶/发际线/眉心/鼻翼下缘/下巴尖)
{_image_fields_desc}
图片同时支持 `multipart/form-data` 文件上传(字段名 `image_file`),或 JSON Body 传 `image_url` / `image_base64`。
---
**坐标说明**:所有坐标以原图像素为基准,原点为图片左上角,x 向右,y 向下。
**标注图片 UI 规范**(真实版本生效):
- 字体/线条颜色:`#FFFFFF 100%`
- 四庭数值在图片**左侧**呈现,七眼间距**上下穿插**展示
- 字体:PingFangSC-Regular 10pt,线宽 1pt
- 横线/竖线渐变消失,虚线两侧带箭头
""",
responses={
200: {
"description": "成功",
"content": {
"application/json": {
"example": {
"code": 0,
"message": "success",
"request_id": "mock-request-id",
"data": {
"annotated_image_url": SAMPLE_IMAGE_URL,
"face_total_height_cm": 13.76,
"four_courts": {
"top_court_cm": 3.44,
"upper_court_cm": 3.44,
"middle_court_cm": 3.44,
"lower_court_cm": 3.44,
"ratios": {
"top_court": 0.25,
"upper_court": 0.25,
"middle_court": 0.25,
"lower_court": 0.25,
},
},
"seven_eyes": {
"eye_width_cm": 3.44,
"face_width_cm": 24.08,
"inter_eye_distance_cm": 3.44,
"ratios": {"eye_width": 0.143, "inter_eye_distance": 0.143},
},
"landmarks": {
"hair_top": {"x": 540, "y": 120},
"hairline": {"x": 540, "y": 430},
"brow_center": {"x": 540, "y": 740},
"nose_bottom": {"x": 540, "y": 1050},
"chin_tip": {"x": 540, "y": 1360},
},
},
}
}
},
},
400: {
"description": "参数错误 / 图片识别失败",
"content": {
"application/json": {
"examples": {
"图片参数错误": {"value": {"code": 1007, "message": "图片参数错误:必须且只能传 image_file / image_url / image_base64 其中一个", "request_id": "x", "data": None}},
"无法识别人像": {"value": {"code": 1001, "message": "无法识别人像", "request_id": "x", "data": None}},
"多张人脸": {"value": {"code": 1005, "message": "检测到多张人脸,仅支持单人照片", "request_id": "x", "data": None}},
}
}
},
},
},
)
async def face_measure(
image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG,≤ 1 MB"),
image_url: Optional[str] = Form(default=None, description="图片 URL"),
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
):
data = {
"annotated_image_url": SAMPLE_IMAGE_URL,
"face_total_height_cm": 13.76,
"four_courts": {
"top_court_cm": 3.44,
"upper_court_cm": 3.44,
"middle_court_cm": 3.44,
"lower_court_cm": 3.44,
"ratios": {
"top_court": 0.25,
"upper_court": 0.25,
"middle_court": 0.25,
"lower_court": 0.25,
},
},
"seven_eyes": {
"eye_width_cm": 3.44,
"face_width_cm": 24.08,
"inter_eye_distance_cm": 3.44,
"ratios": {"eye_width": 0.143, "inter_eye_distance": 0.143},
},
"landmarks": {
"hair_top": {"x": 540, "y": 120},
"hairline": {"x": 540, "y": 430},
"brow_center": {"x": 540, "y": 740},
"nose_bottom": {"x": 540, "y": 1050},
"chin_tip": {"x": 540, "y": 1360},
},
}
return ok(data)
# ---------------------------------------------------------------------------
# 接口 2C 端生发
# ---------------------------------------------------------------------------
@app.post(
"/api/v1/hair/grow",
summary="接口2 C端生发",
tags=["生发"],
description=f"""
输入用户正面照,返回多个生发方案,每个方案包含:
- 生发后效果图 URL
- 对应的发际线形(如花瓣形、波浪形)
- 合适度排序(order=1 最优)
{_image_fields_desc}
图片同时支持 `multipart/form-data` 文件上传(字段名 `image_file`)。
---
**beauty_enabled**:是否对生发后图片开启美颜,默认 `false`。
""",
responses={
200: {
"description": "成功",
"content": {
"application/json": {
"example": {
"code": 0,
"message": "success",
"request_id": "mock-request-id",
"data": {
"results": [
{"image_url": SAMPLE_IMAGE_URL, "hairline_type": "花瓣形", "order": 1},
{"image_url": SAMPLE_IMAGE_URL, "hairline_type": "波浪形", "order": 2},
]
},
}
}
},
},
400: {
"description": "参数错误 / 图片识别失败",
"content": {
"application/json": {
"examples": {
"图片参数错误": {"value": {"code": 1007, "message": "图片参数错误:必须且只能传 image_file / image_url / image_base64 其中一个", "request_id": "x", "data": None}},
"非正面照": {"value": {"code": 1003, "message": "角度问题,请上传正面照", "request_id": "x", "data": None}},
}
}
},
},
},
)
async def hair_grow(
image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG,≤ 1 MB"),
image_url: Optional[str] = Form(default=None, description="图片 URL"),
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
beauty_enabled: bool = Form(default=False, description="是否开启美颜效果,默认 false"),
):
data = {
"results": [
{"image_url": SAMPLE_IMAGE_URL, "hairline_type": "花瓣形", "order": 1},
{"image_url": SAMPLE_IMAGE_URL, "hairline_type": "波浪形", "order": 2},
]
}
return ok(data)
# ---------------------------------------------------------------------------
# 接口 3B 端生发
# ---------------------------------------------------------------------------
@app.post(
"/api/v1/hair/grow-b",
summary="接口3 B端生发(医生/操作端)",
tags=["生发"],
description="""
医生/操作端在用户原图上**手动划线标注**目标发际线后,上传划线图与原图,返回:
- 最合适的发际线图片 URL
- 生发后效果图 URL
- 发际线形
**划线图片**:字段名前缀为 `marked_image_`,同样支持文件/URL/base64 三选一。
**原始用户照片**:字段名前缀为 `original_image_`,同样支持文件/URL/base64 三选一,**必填**。
> 两组图片的传参方式可以不同,例如划线图用 URL,原图用 base64,但各自组内严格互斥。
""",
responses={
200: {
"description": "成功",
"content": {
"application/json": {
"example": {
"code": 0,
"message": "success",
"request_id": "mock-request-id",
"data": {
"best_hairline_image_url": SAMPLE_IMAGE_URL,
"hair_growth_image_url": SAMPLE_IMAGE_URL,
"hairline_type": "花瓣形",
},
}
}
},
},
400: {
"description": "参数错误",
"content": {
"application/json": {
"example": {"code": 1007, "message": "图片参数错误", "request_id": "x", "data": None}
}
},
},
},
)
async def hair_grow_b(
marked_image_file: Optional[UploadFile] = File(default=None, description="划线图片文件(JPG/PNG,≤ 1 MB"),
marked_image_url: Optional[str] = Form(default=None, description="划线图片 URL"),
marked_image_base64: Optional[str] = Form(default=None, description="划线图片 base64"),
original_image_file: Optional[UploadFile] = File(default=None, description="原始用户照片文件(JPG/PNG,≤ 1 MB"),
original_image_url: Optional[str] = Form(default=None, description="原始用户照片 URL"),
original_image_base64: Optional[str] = Form(default=None, description="原始用户照片 base64"),
):
data = {
"best_hairline_image_url": SAMPLE_IMAGE_URL,
"hair_growth_image_url": SAMPLE_IMAGE_URL,
"hairline_type": "花瓣形",
}
return ok(data)
# ---------------------------------------------------------------------------
# 接口 4:用户特征
# ---------------------------------------------------------------------------
@app.post(
"/api/v1/face/features",
summary="接口4 用户特征分析",
tags=["人脸分析"],
description=f"""
输入用户照片,返回 N 个用户面部特征字段。
{_image_fields_desc}
图片同时支持 `multipart/form-data` 文件上传(字段名 `image_file`)。
---
**返回格式**`data.features` 为一个 **JSON 字符串**(不是对象),需要在客户端 `JSON.parse()` 后使用。
当前优先返回字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| face_shape | string | 脸形,如"鹅蛋脸" |
| eyebrow_shape | string | 眉形,如"柳叶眉" |
| facial_age | int | 面部年龄 |
| dynamic_static_type | string | 动静类型,如"静态" |
| gender | string | 性别,如"" |
| gene_style | object | 面部特征对应的基因风格 |
> 字段后续可随时增删,不固定,客户端按需取用即可。
""",
responses={
200: {
"description": "成功",
"content": {
"application/json": {
"example": {
"code": 0,
"message": "success",
"request_id": "mock-request-id",
"data": {
"features": '{"face_shape": "鹅蛋脸", "eyebrow_shape": "柳叶眉", "facial_age": 26, "dynamic_static_type": "静态", "gender": "", "gene_style": {"label": "面部特征标签", "style": "基因风格示例"}}',
},
}
}
},
},
400: {
"description": "参数错误 / 图片识别失败",
"content": {
"application/json": {
"example": {"code": 1001, "message": "无法识别人像", "request_id": "x", "data": None}
}
},
},
},
)
async def face_features(
image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG,≤ 1 MB"),
image_url: Optional[str] = Form(default=None, description="图片 URL"),
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
):
features = json.dumps(
{
"face_shape": "鹅蛋脸",
"eyebrow_shape": "柳叶眉",
"facial_age": 26,
"dynamic_static_type": "静态",
"gender": "",
"gene_style": {"label": "面部特征标签", "style": "基因风格示例"},
},
ensure_ascii=False,
)
return ok({"features": features})
# ---------------------------------------------------------------------------
# 接口 5:发际线 PNG 生成
# ---------------------------------------------------------------------------
@app.post(
"/api/v1/hairline/generate",
summary="接口5 发际线PNG生成",
tags=["人脸分析"],
description=f"""
输入用户照片,返回 N 张用户发际线的 PNG 图片,并标注最合适发际线的面部中间点坐标。
{_image_fields_desc}
图片同时支持 `multipart/form-data` 文件上传(字段名 `image_file`)。
---
**返回说明**
- `hairline_images`:发际线 PNG 列表,数量 N 不固定,已按合适度**从高到低排序**(`order=1` 最合适)
- `best_hairline_center_point`:最合适发际线的面部中间点坐标,以**原图像素**为基准(左上角为原点,x 向右,y 向下)
""",
responses={
200: {
"description": "成功",
"content": {
"application/json": {
"example": {
"code": 0,
"message": "success",
"request_id": "mock-request-id",
"data": {
"hairline_images": [
{"image_url": SAMPLE_IMAGE_URL, "order": 1},
{"image_url": SAMPLE_IMAGE_URL, "order": 2},
],
"best_hairline_center_point": {"x": 540, "y": 430},
},
}
}
},
},
400: {
"description": "参数错误 / 图片识别失败",
"content": {
"application/json": {
"example": {"code": 1002, "message": "人像分辨率过低", "request_id": "x", "data": None}
}
},
},
},
)
async def hairline_generate(
image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG,≤ 1 MB"),
image_url: Optional[str] = Form(default=None, description="图片 URL"),
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
):
data = {
"hairline_images": [
{"image_url": SAMPLE_IMAGE_URL, "order": 1},
{"image_url": SAMPLE_IMAGE_URL, "order": 2},
],
"best_hairline_center_point": {"x": 540, "y": 430},
}
return ok(data)
# ---------------------------------------------------------------------------
# 健康检查
# ---------------------------------------------------------------------------
@app.get("/health", include_in_schema=False)
async def health():
return {"status": "ok"}
@app.get("/", include_in_schema=False)
async def index():
return {"service": "旷视五接口", "version": "0.1.0", "docs": f"{BASE_URL}/docs"}