Upload 3 files
Browse files
DEPLOY.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Deploying the Face Attribute Classifier (Gradio) to Hugging Face Spaces
|
| 2 |
+
|
| 3 |
+
## Why Gradio instead of Docker
|
| 4 |
+
Hugging Face changed its free-tier policy: creating a Docker Space now
|
| 5 |
+
requires a paid plan. The one remaining free path for personal accounts
|
| 6 |
+
is **up to 2 Gradio Spaces on ZeroGPU hardware** β free, no card needed.
|
| 7 |
+
|
| 8 |
+
"ZeroGPU hardware" sounds GPU-specific, but it isn't mandatory to use one.
|
| 9 |
+
A GPU is only allocated to functions you explicitly wrap with
|
| 10 |
+
`@spaces.GPU`. Since `app.py` in this package doesn't use that decorator
|
| 11 |
+
anywhere, the app just runs on the container's CPU β which is exactly
|
| 12 |
+
what three small classification models need.
|
| 13 |
+
|
| 14 |
+
## What this app does differently from your original Flask repo
|
| 15 |
+
Same logic as before β face-crop via Roboflow, then classify age/gender/
|
| 16 |
+
race β but:
|
| 17 |
+
- **Rebuilt as a Gradio app** (`gr.Interface`) instead of Flask routes +
|
| 18 |
+
HTML templates, since that's what the free tier now requires.
|
| 19 |
+
- **Age/gender/race run locally** via `transformers.pipeline(...)`
|
| 20 |
+
instead of calling Hugging Face's shared Inference API β that's the
|
| 21 |
+
service that stopped serving the gender/race models, so this sidesteps
|
| 22 |
+
the problem entirely rather than depending on it.
|
| 23 |
+
- **Roboflow key read from an environment variable** (a Space secret),
|
| 24 |
+
not hardcoded in the source.
|
| 25 |
+
|
| 26 |
+
## 1. Create the Space
|
| 27 |
+
1. Go to https://huggingface.co/new-space (free account is fine).
|
| 28 |
+
2. Space name: `face-attribute-classifier` (or whatever you like).
|
| 29 |
+
3. License: anything (MIT is fine).
|
| 30 |
+
4. SDK: **Gradio**.
|
| 31 |
+
5. Hardware: it will likely default to or only offer **ZeroGPU** on a
|
| 32 |
+
free account β that's fine, leave it. We're not using the GPU.
|
| 33 |
+
6. Visibility: **Public**.
|
| 34 |
+
7. Click **Create Space**.
|
| 35 |
+
|
| 36 |
+
## 2. Add the files
|
| 37 |
+
```bash
|
| 38 |
+
git clone https://huggingface.co/spaces/<your-username>/face-attribute-classifier
|
| 39 |
+
cd face-attribute-classifier
|
| 40 |
+
|
| 41 |
+
cp /path/to/face-classifier-deploy/app.py .
|
| 42 |
+
cp /path/to/face-classifier-deploy/README.md .
|
| 43 |
+
```
|
| 44 |
+
You also need a `requirements.txt` with at minimum:
|
| 45 |
+
```
|
| 46 |
+
gradio
|
| 47 |
+
transformers
|
| 48 |
+
torch --index-url https://download.pytorch.org/whl/cpu
|
| 49 |
+
pillow
|
| 50 |
+
requests
|
| 51 |
+
```
|
| 52 |
+
(Gradio Spaces install `requirements.txt` automatically β no Dockerfile
|
| 53 |
+
needed this time.)
|
| 54 |
+
|
| 55 |
+
## 3. Set the Roboflow key as a secret
|
| 56 |
+
Space **Settings** tab β **Repository secrets** β add:
|
| 57 |
+
- Name: `ROBOFLOW_API_KEY`
|
| 58 |
+
- Value: your Roboflow key (worth regenerating it first, since the
|
| 59 |
+
original was sitting in plain text in a public GitHub repo)
|
| 60 |
+
|
| 61 |
+
## 4. Push
|
| 62 |
+
```bash
|
| 63 |
+
git add .
|
| 64 |
+
git commit -m "Deploy face attribute classifier (Gradio, local inference)"
|
| 65 |
+
git push
|
| 66 |
+
```
|
| 67 |
+
First build will take a few minutes β downloading PyTorch plus three
|
| 68 |
+
model checkpoints. Watch the **Building** then **Logs** tab; you should
|
| 69 |
+
see "Loading race/age/gender models" on startup.
|
| 70 |
+
|
| 71 |
+
## 5. Test it
|
| 72 |
+
Once **Running**, open:
|
| 73 |
+
```
|
| 74 |
+
https://<your-username>-face-attribute-classifier.hf.space
|
| 75 |
+
```
|
| 76 |
+
You'll see Gradio's own UI (an upload box + three output fields) β
|
| 77 |
+
upload a face photo and confirm age/gender/ethnicity all come back
|
| 78 |
+
correctly.
|
| 79 |
+
|
| 80 |
+
## 6. Calling it programmatically (for the Cloudflare Worker)
|
| 81 |
+
Gradio apps expose a REST API automatically. Because `api_name="classify"`
|
| 82 |
+
is set in `app.py`, the calling pattern is:
|
| 83 |
+
|
| 84 |
+
1. **POST** to `https://<your-space-url>/gradio_api/call/classify`
|
| 85 |
+
with a JSON body containing the image (as a data URI) in Gradio's
|
| 86 |
+
expected input format. This returns an `event_id`.
|
| 87 |
+
2. **GET** `https://<your-space-url>/gradio_api/call/classify/<event_id>`
|
| 88 |
+
to stream the result back (Server-Sent Events).
|
| 89 |
+
|
| 90 |
+
This two-step flow is a bit more involved than the single POST/JSON
|
| 91 |
+
round-trip Roboflow used. Rather than guess the exact payload shape
|
| 92 |
+
sight-unseen, once your Space is live:
|
| 93 |
+
- Open it in a browser, use the built-in UI to classify a photo once
|
| 94 |
+
- Open DevTools β Network tab, find the two calls above, and copy their
|
| 95 |
+
exact request/response bodies
|
| 96 |
+
|
| 97 |
+
Send those to me and I'll write the Worker's `/face` route to match
|
| 98 |
+
exactly, and re-enable the "Try It" button on the portfolio.
|
| 99 |
+
|
| 100 |
+
## Notes
|
| 101 |
+
- Free tier sleeps after inactivity, cold start on next visit.
|
| 102 |
+
- Model weights get cached after the first load, so cold starts are
|
| 103 |
+
"reload from cache," not "re-download from the internet."
|
| 104 |
+
- **Heads up on sketchful.AI**: that Docker Space deployment we set up
|
| 105 |
+
earlier β if you haven't actually created and pushed that Space yet,
|
| 106 |
+
it's now blocked by this same policy change (creating a *new* Docker
|
| 107 |
+
Space requires a paid plan). If you already created it before the
|
| 108 |
+
policy changed, it should keep running fine; only new creation is
|
| 109 |
+
affected. Worth checking its status β if it needs to move too, that
|
| 110 |
+
one could go Gradio as well, or we could weigh a $9/mo Pro plan if
|
| 111 |
+
you end up wanting more than 2 Spaces total.
|
README.md
CHANGED
|
@@ -1,13 +1,18 @@
|
|
| 1 |
---
|
| 2 |
title: Face Attribute Classifier
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.25.0
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Face Attribute Classifier
|
| 3 |
+
emoji: π§
|
| 4 |
+
colorFrom: gray
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: gradio
|
|
|
|
|
|
|
| 7 |
app_file: app.py
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# Face Attribute Classifier
|
| 12 |
+
|
| 13 |
+
Predicts age, gender, and ethnicity from a face image. Runs entirely
|
| 14 |
+
locally in this Space (via `transformers`) β no dependency on Hugging
|
| 15 |
+
Face's shared serverless Inference API.
|
| 16 |
+
|
| 17 |
+
Built with Gradio. See the original Flask version for reference:
|
| 18 |
+
https://github.com/bUZgy/race-age-gender-classifier
|
app.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import base64
|
| 3 |
+
import requests
|
| 4 |
+
import gradio as gr
|
| 5 |
+
from PIL import Image
|
| 6 |
+
from transformers import pipeline
|
| 7 |
+
|
| 8 |
+
# --- Roboflow face-detection key, read from a Space secret (not hardcoded) ---
|
| 9 |
+
ROBOFLOW_API_KEY = os.environ.get("ROBOFLOW_API_KEY", "")
|
| 10 |
+
|
| 11 |
+
# --- Load all three classifiers once at startup ---
|
| 12 |
+
# These run locally in this container via `transformers`, instead of calling
|
| 13 |
+
# Hugging Face's shared serverless Inference API (which no longer serves the
|
| 14 |
+
# gender/race models β only age currently has an attached provider).
|
| 15 |
+
# No @spaces.GPU decorator needed β these are small, CPU-friendly models,
|
| 16 |
+
# so they just run on the free CPU container as normal.
|
| 17 |
+
print("Loading race/age/gender models β this can take a bit on first boot...")
|
| 18 |
+
race_pipe = pipeline("image-classification", model="crangana/trained-race")
|
| 19 |
+
age_pipe = pipeline("image-classification", model="crangana/trained-age")
|
| 20 |
+
gender_pipe = pipeline("image-classification", model="crangana/trained-gender")
|
| 21 |
+
print("Models loaded.")
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def crop_face(image_path):
|
| 25 |
+
"""Detect and crop the face using Roboflow's hosted face detector."""
|
| 26 |
+
with open(image_path, "rb") as image_file:
|
| 27 |
+
image_base64 = base64.b64encode(image_file.read()).decode("utf-8")
|
| 28 |
+
|
| 29 |
+
url = "https://detect.roboflow.com/face-det-npvna/1"
|
| 30 |
+
params = {"api_key": ROBOFLOW_API_KEY}
|
| 31 |
+
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
| 32 |
+
|
| 33 |
+
response = requests.post(url, params=params, data=image_base64, headers=headers)
|
| 34 |
+
crop_data = response.json()
|
| 35 |
+
|
| 36 |
+
if not crop_data.get("predictions"):
|
| 37 |
+
raise ValueError("No face detected in the image.")
|
| 38 |
+
|
| 39 |
+
pred = crop_data["predictions"][0]
|
| 40 |
+
width, height = pred["width"], pred["height"]
|
| 41 |
+
x = pred["x"] - width / 2
|
| 42 |
+
y = pred["y"] - height / 2
|
| 43 |
+
|
| 44 |
+
image = Image.open(image_path)
|
| 45 |
+
cropped = image.crop((x, y, x + width, y + height))
|
| 46 |
+
|
| 47 |
+
cropped_path = "/tmp/cropped_face.jpg"
|
| 48 |
+
cropped.save(cropped_path)
|
| 49 |
+
return cropped_path
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def get_top(results):
|
| 53 |
+
"""Pick the highest-confidence label from a list of {label, score} dicts."""
|
| 54 |
+
best = max(results, key=lambda r: r["score"])
|
| 55 |
+
return best["label"], best["score"]
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def classify(image):
|
| 59 |
+
if image is None:
|
| 60 |
+
return "No image provided.", "", ""
|
| 61 |
+
|
| 62 |
+
tmp_path = "/tmp/input_face.jpg"
|
| 63 |
+
image.convert("RGB").save(tmp_path)
|
| 64 |
+
|
| 65 |
+
try:
|
| 66 |
+
cropped_path = crop_face(tmp_path)
|
| 67 |
+
except Exception as e:
|
| 68 |
+
return f"Face detection failed: {e}", "β", "β"
|
| 69 |
+
|
| 70 |
+
try:
|
| 71 |
+
age_label, age_score = get_top(age_pipe(cropped_path))
|
| 72 |
+
gender_label, gender_score = get_top(gender_pipe(cropped_path))
|
| 73 |
+
race_label, race_score = get_top(race_pipe(cropped_path))
|
| 74 |
+
except Exception as e:
|
| 75 |
+
return f"Classification failed: {e}", "β", "β"
|
| 76 |
+
|
| 77 |
+
return (
|
| 78 |
+
f"{age_label} ({age_score:.0%})",
|
| 79 |
+
f"{gender_label} ({gender_score:.0%})",
|
| 80 |
+
f"{race_label} ({race_score:.0%})",
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
demo = gr.Interface(
|
| 85 |
+
fn=classify,
|
| 86 |
+
inputs=gr.Image(type="pil", label="Upload a face photo"),
|
| 87 |
+
outputs=[
|
| 88 |
+
gr.Textbox(label="Age"),
|
| 89 |
+
gr.Textbox(label="Gender"),
|
| 90 |
+
gr.Textbox(label="Ethnicity"),
|
| 91 |
+
],
|
| 92 |
+
title="Face Attribute Classifier",
|
| 93 |
+
description="Predicts age, gender, and ethnicity from a face photo.",
|
| 94 |
+
api_name="classify", # names the programmatic API endpoint /gradio_api/call/classify
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
if __name__ == "__main__":
|
| 98 |
+
demo.launch()
|