feat: initial commit - face-service
This commit is contained in:
commit
7971c6b426
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
logs/
|
||||
*.log
|
||||
nohup.out
|
||||
nohup_gpu*.out
|
||||
py3/
|
||||
*.egg-info/
|
||||
*.pid
|
||||
6
ah.py
Normal file
6
ah.py
Normal file
@ -0,0 +1,6 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
from ahserver.webapp import webapp
|
||||
from init import load_face_service
|
||||
|
||||
if __name__ == '__main__':
|
||||
webapp(load_face_service)
|
||||
57
build.sh
Executable file
57
build.sh
Executable file
@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
# Face Service (InsightFace)
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
SERVICE_NAME="face-service"
|
||||
PORT=9091
|
||||
GPU=5
|
||||
PY=/data/ymq/wan22-service/py3/bin/python
|
||||
action="${1:-status}"
|
||||
|
||||
case "$action" in
|
||||
deploy|update)
|
||||
echo "=== $SERVICE_NAME Deploy (GPU $GPU, port $PORT) ==="
|
||||
if [ -f ah.pid ] && kill -0 $(cat ah.pid) 2>/dev/null; then
|
||||
kill $(cat ah.pid) 2>/dev/null || true; sleep 2
|
||||
fi
|
||||
if [ -d .git ] && [ -f .git/HEAD ]; then
|
||||
git pull origin master 2>/dev/null || true
|
||||
fi
|
||||
mkdir -p logs files wwwroot
|
||||
export PYTHONPATH="$(pwd)"
|
||||
export CUDA_VISIBLE_DEVICES=$GPU
|
||||
nohup $PY ah.py > nohup.out 2>&1 &
|
||||
echo $! > ah.pid
|
||||
echo "Started PID $(cat ah.pid) on port $PORT (GPU $GPU)"
|
||||
sleep 5
|
||||
if curl -s http://localhost:$PORT/api/status > /dev/null 2>&1; then
|
||||
echo "Service healthy"
|
||||
else
|
||||
echo "WARNING: not responding, check nohup.out"
|
||||
tail -20 nohup.out
|
||||
fi
|
||||
;;
|
||||
stop)
|
||||
if [ -f ah.pid ]; then
|
||||
kill $(cat ah.pid) 2>/dev/null || true; rm -f ah.pid; echo "Stopped"
|
||||
else echo "Not running"; fi
|
||||
;;
|
||||
start)
|
||||
mkdir -p logs files wwwroot
|
||||
export PYTHONPATH="$(pwd)"; export CUDA_VISIBLE_DEVICES=$GPU
|
||||
nohup $PY ah.py > nohup.out 2>&1 &
|
||||
echo $! > ah.pid; echo "Started PID $(cat ah.pid)"
|
||||
;;
|
||||
status)
|
||||
echo "=== $SERVICE_NAME Status ==="
|
||||
if [ -f ah.pid ] && kill -0 $(cat ah.pid) 2>/dev/null; then
|
||||
echo "Process: running (PID $(cat ah.pid))"
|
||||
else echo "Process: not running"; fi
|
||||
echo "Port: $PORT, GPU: $GPU"
|
||||
if curl -s --max-time 3 http://localhost:$PORT/api/status > /dev/null 2>&1; then
|
||||
echo "HTTP: OK"
|
||||
else echo "HTTP: not responding"; fi
|
||||
;;
|
||||
*) echo "Usage: $0 {deploy|update|stop|start|status}"; exit 1 ;;
|
||||
esac
|
||||
27
conf/config.json
Normal file
27
conf/config.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"password_key": "FaceService2026Key",
|
||||
"filesroot": "$[workdir]$/files",
|
||||
"logger": {
|
||||
"name": "face-service",
|
||||
"levelname": "info",
|
||||
"logfile": "$[workdir]$/logs/face-service.log"
|
||||
},
|
||||
"website": {
|
||||
"paths": [["$[workdir]$/wwwroot", ""]],
|
||||
"client_max_size": 52428800,
|
||||
"host": "0.0.0.0",
|
||||
"port": 9091,
|
||||
"coding": "utf-8",
|
||||
"indexes": ["index.html"],
|
||||
"startswiths": [
|
||||
{"leading": "/api/status", "registerfunction": "status"},
|
||||
{"leading": "/api/detect", "registerfunction": "detect"},
|
||||
{"leading": "/api/recognize", "registerfunction": "recognize"},
|
||||
{"leading": "/api/compare", "registerfunction": "compare"}
|
||||
],
|
||||
"processors": [
|
||||
[".tmpl", "tmpl"], [".app", "app"], [".ui", "bui"],
|
||||
[".dspy", "dspy"], [".md", "md"]
|
||||
]
|
||||
}
|
||||
}
|
||||
104
init.py
Normal file
104
init.py
Normal file
@ -0,0 +1,104 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
from traceback import format_exc
|
||||
from ahserver.serverenv import ServerEnv
|
||||
from appPublic.registerfunction import RegisterFunction
|
||||
from appPublic.log import exception
|
||||
import json
|
||||
|
||||
|
||||
async def status_handler(request, params_kw, *args, **kwargs):
|
||||
import sys, os
|
||||
sys.path.insert(0, os.getcwd())
|
||||
from workers.face_model import health_check
|
||||
health = health_check()
|
||||
return json.dumps({
|
||||
"service": "face-service",
|
||||
"model": health["model"],
|
||||
"model_loaded": health["loaded"],
|
||||
"det_size": health["det_size"],
|
||||
"endpoints": ["/api/status", "/api/detect", "/api/recognize", "/api/compare"]
|
||||
}, indent=2, ensure_ascii=False)
|
||||
|
||||
|
||||
async def detect_handler(request, params_kw, *args, **kwargs):
|
||||
import sys, os, time
|
||||
sys.path.insert(0, os.getcwd())
|
||||
from workers.face_model import detect
|
||||
try:
|
||||
images = params_kw.get("images", [])
|
||||
if not images:
|
||||
return json.dumps({"error": "images list required"})
|
||||
|
||||
start = time.time()
|
||||
all_results = []
|
||||
for img in images:
|
||||
result = detect(img)
|
||||
all_results.append(result)
|
||||
elapsed = round(time.time() - start, 4)
|
||||
|
||||
return json.dumps({
|
||||
"status": "SUCCEEDED",
|
||||
"results": all_results,
|
||||
"elapsed": elapsed
|
||||
}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
exception(f"{e}, {format_exc()}")
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
|
||||
async def recognize_handler(request, params_kw, *args, **kwargs):
|
||||
import sys, os, time
|
||||
sys.path.insert(0, os.getcwd())
|
||||
from workers.face_model import recognize
|
||||
try:
|
||||
images = params_kw.get("images", [])
|
||||
if not images:
|
||||
return json.dumps({"error": "images list required"})
|
||||
|
||||
start = time.time()
|
||||
all_results = []
|
||||
for img in images:
|
||||
result = recognize(img)
|
||||
all_results.append(result)
|
||||
elapsed = round(time.time() - start, 4)
|
||||
|
||||
return json.dumps({
|
||||
"status": "SUCCEEDED",
|
||||
"results": all_results,
|
||||
"elapsed": elapsed
|
||||
}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
exception(f"{e}, {format_exc()}")
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
|
||||
async def compare_handler(request, params_kw, *args, **kwargs):
|
||||
import sys, os
|
||||
sys.path.insert(0, os.getcwd())
|
||||
from workers.face_model import compare
|
||||
try:
|
||||
embedding1 = params_kw.get("embedding1")
|
||||
embedding2 = params_kw.get("embedding2")
|
||||
|
||||
if not embedding1 or not embedding2:
|
||||
return json.dumps({"error": "embedding1 and embedding2 required"})
|
||||
|
||||
result = compare(embedding1, embedding2)
|
||||
|
||||
return json.dumps({
|
||||
"status": "SUCCEEDED",
|
||||
**result
|
||||
}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
exception(f"{e}, {format_exc()}")
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
|
||||
def load_face_service():
|
||||
"""Register API handlers"""
|
||||
env = ServerEnv()
|
||||
rf = RegisterFunction()
|
||||
rf.register("status", status_handler)
|
||||
rf.register("detect", detect_handler)
|
||||
rf.register("recognize", recognize_handler)
|
||||
rf.register("compare", compare_handler)
|
||||
0
workers/__init__.py
Normal file
0
workers/__init__.py
Normal file
109
workers/face_model.py
Normal file
109
workers/face_model.py
Normal file
@ -0,0 +1,109 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
"""InsightFace buffalo_l model wrapper."""
|
||||
import os
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
_model = None
|
||||
_lock = False
|
||||
|
||||
MODEL_NAME = "buffalo_l"
|
||||
|
||||
def get_model():
|
||||
global _model, _lock
|
||||
if _model is None and not _lock:
|
||||
_lock = True
|
||||
try:
|
||||
import insightface
|
||||
from insightface.app import FaceAnalysis
|
||||
_model = FaceAnalysis(name=MODEL_NAME, providers=['CUDAExecutionProvider'])
|
||||
_model.prepare(ctx_id=0, det_size=(640, 640))
|
||||
except Exception as e:
|
||||
_lock = False
|
||||
raise RuntimeError(f"Failed to load InsightFace: {e}")
|
||||
return _model
|
||||
|
||||
|
||||
def load_image(image_input):
|
||||
"""Load image from path, URL, or base64."""
|
||||
if isinstance(image_input, str):
|
||||
if image_input.startswith("/") and os.path.exists(image_input):
|
||||
return Image.open(image_input).convert("RGB")
|
||||
elif image_input.startswith("http"):
|
||||
import urllib.request
|
||||
import io
|
||||
with urllib.request.urlopen(image_input, timeout=10) as resp:
|
||||
return Image.open(io.BytesIO(resp.read())).convert("RGB")
|
||||
raise ValueError(f"Cannot load image: {image_input[:50] if isinstance(image_input, str) else 'unknown'}")
|
||||
|
||||
|
||||
def detect(image_input):
|
||||
"""Detect faces in an image. Returns list of face info."""
|
||||
model = get_model()
|
||||
img = load_image(image_input)
|
||||
img_np = np.array(img)
|
||||
|
||||
faces = model.get(img_np)
|
||||
|
||||
results = []
|
||||
for i, face in enumerate(faces):
|
||||
results.append({
|
||||
"face_id": f"face_{i}",
|
||||
"bbox": face.bbox.tolist(),
|
||||
"det_score": round(float(face.det_score), 4),
|
||||
"age": int(face.age) if face.age > 0 else None,
|
||||
"gender": "M" if face.gender == 1 else "F",
|
||||
"embedding_dim": len(face.embedding)
|
||||
})
|
||||
|
||||
return {"faces": results, "count": len(results)}
|
||||
|
||||
|
||||
def recognize(image_input):
|
||||
"""Get face embeddings from an image."""
|
||||
model = get_model()
|
||||
img = load_image(image_input)
|
||||
img_np = np.array(img)
|
||||
|
||||
faces = model.get(img_np)
|
||||
|
||||
results = []
|
||||
for i, face in enumerate(faces):
|
||||
# Normalize embedding to unit vector
|
||||
embedding = face.embedding / np.linalg.norm(face.embedding)
|
||||
results.append({
|
||||
"face_id": f"face_{i}",
|
||||
"bbox": face.bbox.tolist(),
|
||||
"det_score": round(float(face.det_score), 4),
|
||||
"embedding": embedding.tolist()
|
||||
})
|
||||
|
||||
return {"faces": results, "count": len(results)}
|
||||
|
||||
|
||||
def compare(embed1, embed2):
|
||||
"""Compare two face embeddings. Returns cosine similarity."""
|
||||
e1 = np.array(embed1)
|
||||
e2 = np.array(embed2)
|
||||
|
||||
# Normalize
|
||||
e1 = e1 / np.linalg.norm(e1)
|
||||
e2 = e2 / np.linalg.norm(e2)
|
||||
|
||||
similarity = float(np.dot(e1, e2))
|
||||
|
||||
return {
|
||||
"similarity": round(similarity, 6),
|
||||
"is_same": similarity > 0.4,
|
||||
"confidence": "high" if similarity > 0.6 else ("medium" if similarity > 0.4 else "low")
|
||||
}
|
||||
|
||||
|
||||
def health_check():
|
||||
"""Check model status."""
|
||||
model = get_model()
|
||||
return {
|
||||
"model": MODEL_NAME,
|
||||
"loaded": model is not None,
|
||||
"det_size": [640, 640]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user