commit 7971c6b4267105424e11186b22d9efa928759ab4 Author: yumoqing Date: Sat Jul 4 20:11:17 2026 +0800 feat: initial commit - face-service diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5cbdaeb --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +__pycache__/ +*.pyc +*.pyo +logs/ +*.log +nohup.out +nohup_gpu*.out +py3/ +*.egg-info/ +*.pid diff --git a/ah.py b/ah.py new file mode 100644 index 0000000..4c45e22 --- /dev/null +++ b/ah.py @@ -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) diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..2eef18d --- /dev/null +++ b/build.sh @@ -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 diff --git a/conf/config.json b/conf/config.json new file mode 100644 index 0000000..a204dee --- /dev/null +++ b/conf/config.json @@ -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"] + ] + } +} diff --git a/init.py b/init.py new file mode 100644 index 0000000..954adb2 --- /dev/null +++ b/init.py @@ -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) diff --git a/workers/__init__.py b/workers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/workers/face_model.py b/workers/face_model.py new file mode 100644 index 0000000..4e606f3 --- /dev/null +++ b/workers/face_model.py @@ -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] + }