@@ -30,14 +30,6 @@ logger = logging.getLogger("hair.worker")
BASE_URL = " https://hair.xiangsilian.com "
SAMPLE_IMAGE_URL = f " { BASE_URL } /static/sample.jpg "
# 分辨率门槛(短边/长边,方向无关),可由环境变量覆盖(技术方案 §8.3)
MIN_SHORT_SIDE = int ( os . getenv ( " MIN_SHORT_SIDE " , " 600 " ) )
MIN_LONG_SIDE = int ( os . getenv ( " MIN_LONG_SIDE " , " 800 " ) )
# 文件大小上限(字节),可由环境变量覆盖;设为 0 表示不限制
MAX_FILE_BYTES = int ( os . getenv ( " MAX_FILE_BYTES " , " 1000000 " ) ) # 默认 1 MB
if MAX_FILE_BYTES < = 0 :
MAX_FILE_BYTES = float ( " inf " )
# 运行期状态:模型就绪标志(/health 据此返回 200/503)
_STATE = { " ready " : False }
@@ -103,7 +95,7 @@ app = FastAPI(
| 方式 | 字段名 | 说明 |
|------|--------|------|
| 文件上传 | `image_file` | `multipart/form-data`,单文件 ≤ 1 MB |
| 文件上传 | `image_file` | `multipart/form-data`,单文件上传 |
| URL | `image_url` | 图片的完整 HTTP/HTTPS 地址 |
| base64 | `image_base64` | 需携带前缀,如 `data:image/jpeg;base64,xxxx` |
@@ -112,9 +104,9 @@ app = FastAPI(
## 图片要求
- 格式:**JPG / PNG**
- 分辨率:最低 1080× 1920,最大 4000× 5000
- 分辨率:不限制
- 人脸数量:仅支持**单人**,多人返回错误码 `1005`
- 文件大小:≤ 1 MB(文件上传方式)
- 文件大小:不限制
## 统一响应结构
@@ -134,11 +126,9 @@ app = FastAPI(
| code | 说明 |
|------|------|
| 1001 | 无法识别人像 |
| 1002 | 人像分辨率过低 |
| 1003 | 非正面照 / 角度过大 |
| 1004 | 性别标签无法判定 |
| 1005 | 检测到多张人脸(仅支持单人)|
| 1006 | 文件超出 1 MB 限制 |
| 1007 | 图片参数错误(未传或同时传多个)|
| 1008 | 图片格式不支持(仅 JPG / PNG)|
""" ,
@@ -202,7 +192,7 @@ async def resolve_image_bytes(image_file, image_url, image_base64):
""" 三选一取图,返回 (raw_bytes, error_response)。
严格互斥:传 0 个或多个 → 1007;URL 下载失败/base64 解码失败 → 1008。
大小校验放到上层(统一 1006) ,此处只负责取到字节。
不限制文件 大小,此处只负责取到字节。
"""
provided = [ x for x in ( image_file , image_url , image_base64 ) if x ]
if len ( provided ) != 1 :
@@ -280,7 +270,7 @@ def _parse_hair_styles(raw: Optional[str], max_styles: int) -> Optional[list[int
_image_fields_desc = (
" 图片传参方式严格互斥,必须且只能选其一: \n "
" - **image_file**( multipart/form-data 上传,≤ 1 MB ) \n "
" - **image_file**( multipart/form-data 上传) \n "
" - **image_url**(完整 HTTP/HTTPS 地址) \n "
" - **image_base64**(需携带前缀,如 `data:image/jpeg;base64,xxxx`) \n \n "
" 同时传多个或一个都不传,均返回错误码 `1007`。 "
@@ -331,20 +321,12 @@ async def _face_measure_impl(image_file, image_url, image_base64, variant="v1"):
if e is not None :
return None , e
# 2. 大小校验(≤ 1MB )
if len ( raw ) > MAX_FILE_BYTES :
return None , err ( 1006 , " 文件超出 1 MB 限制 " )
# 3. 解码
# 2. 解码(不限制文件大小 / 分辨率 )
image = cv2 . imdecode ( np . frombuffer ( raw , np . uint8 ) , cv2 . IMREAD_COLOR )
if image is None :
return None , err ( 1008 , " 图片格式不支持(仅 JPG / PNG) " )
# 4. 分辨率(短边/长边,方向无关,可配置门槛)
h , w = image . shape [ : 2 ]
short_side , long_side = min ( w , h ) , max ( w , h )
if short_side < MIN_SHORT_SIDE or long_side < MIN_LONG_SIDE :
return None , err ( 1002 , " 人像分辨率过低 " )
try :
from face_analysis . detector import detector
@@ -488,7 +470,7 @@ async def _face_measure_impl(image_file, image_url, image_base64, variant="v1"):
} ,
)
async def face_measure (
image_file : Optional [ UploadFile ] = File ( default = None , description = " 上传图片文件(JPG/PNG,≤ 1 MB ) " ) ,
image_file : Optional [ UploadFile ] = File ( default = None , description = " 上传图片文件(JPG/PNG) " ) ,
image_url : Optional [ str ] = Form ( default = None , description = " 图片 URL " ) ,
image_base64 : Optional [ str ] = Form ( default = None , description = " 图片 base64(需带 data:image/...;base64, 前缀) " ) ,
) :
@@ -587,7 +569,7 @@ async def face_measure(
} ,
)
async def face_measure_v2 (
image_file : Optional [ UploadFile ] = File ( default = None , description = " 上传图片文件(JPG/PNG,≤ 1 MB ) " ) ,
image_file : Optional [ UploadFile ] = File ( default = None , description = " 上传图片文件(JPG/PNG) " ) ,
image_url : Optional [ str ] = Form ( default = None , description = " 图片 URL " ) ,
image_base64 : Optional [ str ] = Form ( default = None , description = " 图片 base64(需带 data:image/...;base64, 前缀) " ) ,
) :
@@ -660,7 +642,7 @@ async def face_measure_v2(
} ,
)
async def hair_grow (
image_file : Optional [ UploadFile ] = File ( default = None , description = " 上传图片文件(JPG/PNG,≤ 1 MB ) " ) ,
image_file : Optional [ UploadFile ] = File ( default = None , description = " 上传图片文件(JPG/PNG) " ) ,
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(必填) " ) ,
@@ -683,18 +665,11 @@ async def hair_grow(
raw , e = await resolve_image_bytes ( image_file , image_url , image_base64 )
if e is not None :
return e
if len ( raw ) > MAX_FILE_BYTES :
return err ( 1006 , " 文件超出 1 MB 限制 " )
image = cv2 . imdecode ( np . frombuffer ( raw , np . uint8 ) , cv2 . IMREAD_COLOR )
if image is None :
return err ( 1008 , " 图片格式不支持(仅 JPG / PNG) " )
h , w = image . shape [ : 2 ]
short_side , long_side = min ( w , h ) , max ( w , h )
if short_side < MIN_SHORT_SIDE or long_side < MIN_LONG_SIDE :
return err ( 1002 , " 人像分辨率过低 " )
try :
from fastapi . concurrency import run_in_threadpool
from hairline . service import generate_grow_results
@@ -781,7 +756,7 @@ _WORKFLOW2_PATH = os.path.join(os.path.dirname(__file__), "add_hair2.json")
} ,
)
async def hair_grow_v2 (
image_file : Optional [ UploadFile ] = File ( default = None , description = " 上传图片文件(JPG/PNG,≤ 1 MB ) " ) ,
image_file : Optional [ UploadFile ] = File ( default = None , description = " 上传图片文件(JPG/PNG) " ) ,
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(必填) " ) ,
@@ -804,18 +779,11 @@ async def hair_grow_v2(
raw , e = await resolve_image_bytes ( image_file , image_url , image_base64 )
if e is not None :
return e
if len ( raw ) > MAX_FILE_BYTES :
return err ( 1006 , " 文件超出 1 MB 限制 " )
image = cv2 . imdecode ( np . frombuffer ( raw , np . uint8 ) , cv2 . IMREAD_COLOR )
if image is None :
return err ( 1008 , " 图片格式不支持(仅 JPG / PNG) " )
h , w = image . shape [ : 2 ]
short_side , long_side = min ( w , h ) , max ( w , h )
if short_side < MIN_SHORT_SIDE or long_side < MIN_LONG_SIDE :
return err ( 1002 , " 人像分辨率过低 " )
try :
from fastapi . concurrency import run_in_threadpool
from hairline . service import generate_grow_results
@@ -883,7 +851,7 @@ async def hair_grow_v2(
} ,
)
async def hair_grow_b (
marked_image_file : Optional [ UploadFile ] = File ( default = None , description = " 划线图片文件(JPG/PNG,≤ 1 MB ) " ) ,
marked_image_file : Optional [ UploadFile ] = File ( default = None , description = " 划线图片文件(JPG/PNG) " ) ,
marked_image_url : Optional [ str ] = Form ( default = None , description = " 划线图片 URL " ) ,
marked_image_base64 : Optional [ str ] = Form ( default = None , description = " 划线图片 base64 " ) ,
use_mask : bool = Form ( default = True , description = " 是否画发际线(测试对比用)。false 时跳过划线检测、直接送划线图 " ) ,
@@ -893,18 +861,11 @@ async def hair_grow_b(
marked_raw , e = await resolve_image_bytes ( marked_image_file , marked_image_url , marked_image_base64 )
if e is not None :
return e
if len ( marked_raw ) > MAX_FILE_BYTES :
return err ( 1006 , " 文件超出 1 MB 限制 " )
marked = cv2 . imdecode ( np . frombuffer ( marked_raw , np . uint8 ) , cv2 . IMREAD_COLOR )
if marked is None :
return err ( 1008 , " 图片格式不支持(仅 JPG / PNG) " )
h , w = marked . shape [ : 2 ]
short_side , long_side = min ( w , h ) , max ( w , h )
if short_side < MIN_SHORT_SIDE or long_side < MIN_LONG_SIDE :
return err ( 1002 , " 人像分辨率过低 " )
try :
from fastapi . concurrency import run_in_threadpool
from hairline . service import generate_grow_b
@@ -985,7 +946,7 @@ async def hair_grow_b(
} ,
)
async def face_features (
image_file : Optional [ UploadFile ] = File ( default = None , description = " 上传图片文件(JPG/PNG,≤ 1 MB ) " ) ,
image_file : Optional [ UploadFile ] = File ( default = None , description = " 上传图片文件(JPG/PNG) " ) ,
image_url : Optional [ str ] = Form ( default = None , description = " 图片 URL " ) ,
image_base64 : Optional [ str ] = Form ( default = None , description = " 图片 base64(需带 data:image/...;base64, 前缀) " ) ,
) :
@@ -1052,14 +1013,14 @@ async def face_features(
" description " : " 参数错误 / 图片识别失败 " ,
" content " : {
" application/json " : {
" example " : { " code " : 1002 , " message " : " 人像分辨率过低 " , " request_id " : " x " , " data " : None }
" example " : { " code " : 1001 , " message " : " 无法识别 人像" , " request_id " : " x " , " data " : None }
}
} ,
} ,
} ,
)
async def hairline_generate (
image_file : Optional [ UploadFile ] = File ( default = None , description = " 上传图片文件(JPG/PNG,≤ 1 MB ) " ) ,
image_file : Optional [ UploadFile ] = File ( default = None , description = " 上传图片文件(JPG/PNG) " ) ,
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(必填) " ) ,
@@ -1070,18 +1031,11 @@ async def hairline_generate(
raw , e = await resolve_image_bytes ( image_file , image_url , image_base64 )
if e is not None :
return e
if len ( raw ) > MAX_FILE_BYTES :
return err ( 1006 , " 文件超出 1 MB 限制 " )
image = cv2 . imdecode ( np . frombuffer ( raw , np . uint8 ) , cv2 . IMREAD_COLOR )
if image is None :
return err ( 1008 , " 图片格式不支持(仅 JPG / PNG) " )
h , w = image . shape [ : 2 ]
short_side , long_side = min ( w , h ) , max ( w , h )
if short_side < MIN_SHORT_SIDE or long_side < MIN_LONG_SIDE :
return err ( 1002 , " 人像分辨率过低 " )
try :
from fastapi . concurrency import run_in_threadpool
from hairline . service import generate_hairline_pngs