pediot commited on
Commit
f6627f1
·
0 Parent(s):

Initial commit: Setup FastAPI application for HF Spaces

Browse files
Files changed (10) hide show
  1. .gitignore +63 -0
  2. Dockerfile +16 -0
  3. README.md +46 -0
  4. app.py +52 -0
  5. requirements.txt +10 -0
  6. runtime.txt +1 -0
  7. src/auth.py +17 -0
  8. src/encoder.py +60 -0
  9. src/models.py +40 -0
  10. src/utils.py +90 -0
.gitignore ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python-related files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.swp
5
+ .DS_Store
6
+ *.egg-info/
7
+
8
+ # Virtual environment
9
+ venv/
10
+ env/
11
+ *.venv/
12
+
13
+ # Jupyter Notebooks checkpoints
14
+ .ipynb_checkpoints/
15
+
16
+ # Logs
17
+ logs/
18
+ *.log
19
+
20
+ # Hugging Face Transformers cache
21
+ ~/.cache/huggingface/
22
+
23
+ # Docker-related files
24
+ *.dockerignore
25
+
26
+ # Ignore compiled code
27
+ *.so
28
+ *.o
29
+ *.out
30
+ *.a
31
+
32
+ # Ignore OS-specific files
33
+ Thumbs.db
34
+ ehthumbs.db
35
+
36
+ # Ignore FastAPI auto-generated files
37
+ *.db
38
+ instance/
39
+ .env
40
+ .env.local
41
+ .env.*.local
42
+
43
+ # VS Code settings
44
+ .vscode/
45
+ .history/
46
+
47
+ # Ignore dependency files
48
+ pip-log.txt
49
+ pip-delete-this-directory.txt
50
+
51
+ # Ignore coverage files
52
+ .coverage
53
+ htmlcov/
54
+ coverage.xml
55
+
56
+ # Ignore test-related files
57
+ .tox/
58
+ .pytest_cache/
59
+ nosetests.xml
60
+ test-reports/
61
+
62
+ # Ignore Hugging Face Spaces cache
63
+ space_runtime/
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9
2
+
3
+ WORKDIR /app
4
+
5
+ ENV HF_HOME=/app/hf_cache
6
+ ENV HF_TOKEN=${HF_TOKEN}
7
+ RUN mkdir -p /app/hf_cache && chmod 777 /app/hf_cache
8
+
9
+ COPY requirements.txt .
10
+ RUN pip install --no-cache-dir -r requirements.txt
11
+
12
+ COPY . .
13
+
14
+ EXPOSE 7860
15
+
16
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
README.md ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: fclip_back2
3
+ emoji: 🌖
4
+ colorFrom: purple
5
+ colorTo: yellow
6
+ sdk: docker
7
+ pinned: false
8
+ license: cc-by-nc-4.0
9
+ short_description: Generate text and image embeddings for clothing items
10
+ ---
11
+
12
+ # Install
13
+
14
+ ### Create SSH key
15
+
16
+ ```
17
+ ls ~/.ssh/id_rsa.pub
18
+ ssh-keygen -t rsa -b 4096 -C "your-email@example.com"
19
+ cat ~/.ssh/id_rsa.pub
20
+ ```
21
+
22
+ ### Add key to HF SSH key settings
23
+
24
+ ### Clone project
25
+
26
+ ```
27
+ git clone https://huggingface.co/spaces/precove/fclip_back2
28
+ python -m venv venv
29
+ source venv/bin/activate
30
+ pip install -r requirements.txt
31
+ ```
32
+
33
+ # Usage
34
+
35
+ ### FastAPI
36
+
37
+ ```
38
+ uvicorn app:app --host 0.0.0.0 --port 8080 --reload
39
+ ```
40
+
41
+ ### Docker
42
+
43
+ ```
44
+ docker build -t fclip .
45
+ docker run -p 8080:7860 fclip
46
+ ```
app.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gc
2
+ from fastapi import FastAPI, Depends
3
+
4
+ from src.encoder import FashionCLIPEncoder
5
+ from src.models import TextRequest, ImageRequest, Response
6
+ from src.auth import verify_token
7
+ from src.utils import delete_images
8
+
9
+
10
+ encoder = FashionCLIPEncoder(normalize=True)
11
+ app = FastAPI()
12
+ app.state.req_count = 0
13
+ COLLECT_GC_EVERY = 20
14
+
15
+
16
+ @app.get("/")
17
+ async def root():
18
+ return {
19
+ "status": "ok",
20
+ }
21
+
22
+
23
+ @app.post("/encode_texts")
24
+ async def encode_texts(
25
+ request: TextRequest,
26
+ token: str = Depends(verify_token),
27
+ ) -> Response:
28
+ embeddings = encoder.encode_text(request.texts)
29
+ response = Response(embeddings=embeddings)
30
+
31
+ return response
32
+
33
+
34
+ @app.post("/encode_images")
35
+ async def encode_images(
36
+ request: ImageRequest,
37
+ token: str = Depends(verify_token),
38
+ ) -> Response:
39
+ try:
40
+ images = request.download()
41
+ embeddings = encoder.encode_images(images)
42
+ return Response(embeddings=embeddings)
43
+
44
+ finally:
45
+ success = delete_images(images)
46
+ if not success:
47
+ print("Failed to delete images")
48
+
49
+ app.state.req_count += 1
50
+
51
+ if app.state.req_count % COLLECT_GC_EVERY == 0:
52
+ gc.collect()
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ torch==2.6.0
2
+ transformers==4.37.2
3
+ datasets==2.16.1
4
+ open-clip-torch>=2.23.0
5
+ huggingface-hub>=0.20.3
6
+ fastapi==0.110.0
7
+ uvicorn==0.27.1
8
+ pydantic==2.6.3
9
+ python-decouple
10
+ numpy==1.24.3
runtime.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ python-3.9.6
src/auth.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import HTTPException, Security
2
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
3
+ from decouple import config
4
+
5
+
6
+ API_TOKEN = config("API_TOKEN")
7
+ security = HTTPBearer()
8
+
9
+
10
+ def verify_token(credentials: HTTPAuthorizationCredentials = Security(security)):
11
+ if credentials.credentials != API_TOKEN:
12
+ raise HTTPException(
13
+ status_code=401,
14
+ detail="Invalid authentication credentials",
15
+ headers={"WWW-Authenticate": "Bearer"},
16
+ )
17
+ return credentials.credentials
src/encoder.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+ from PIL.Image import Image
3
+
4
+ import torch
5
+ from transformers import AutoModel, AutoProcessor
6
+
7
+ from .utils import normalize_vectors
8
+
9
+
10
+ MODEL_NAME = "Marqo/marqo-fashionCLIP"
11
+
12
+
13
+ class FashionCLIPEncoder:
14
+ def __init__(self, normalize: bool = False):
15
+ self.normalize = normalize
16
+
17
+ self.device = torch.device("cpu")
18
+
19
+ self.processor = AutoProcessor.from_pretrained(
20
+ MODEL_NAME,
21
+ trust_remote_code=True,
22
+ )
23
+
24
+ self.model = AutoModel.from_pretrained(
25
+ MODEL_NAME,
26
+ trust_remote_code=True,
27
+ )
28
+
29
+ self.model.to(self.device)
30
+ self.model.eval()
31
+
32
+ def encode_text(self, texts: List[str]) -> List[List[float]]:
33
+ kwargs = {
34
+ "padding": "max_length",
35
+ "return_tensors": "pt",
36
+ "truncation": True,
37
+ }
38
+
39
+ inputs = self.processor(text=texts, **kwargs)
40
+
41
+ with torch.no_grad():
42
+ batch = {k: v.to(self.device) for k, v in inputs.items()}
43
+ vectors = self.model.get_text_features(**batch)
44
+
45
+ return self._postprocess_vectors(vectors)
46
+
47
+ def encode_images(self, images: List[Image]) -> List[List[float]]:
48
+ inputs = self.processor(images=images, return_tensors="pt")
49
+
50
+ with torch.no_grad():
51
+ batch = {k: v.to(self.device) for k, v in inputs.items()}
52
+ vectors = self.model.get_image_features(**batch)
53
+
54
+ return self._postprocess_vectors(vectors)
55
+
56
+ def _postprocess_vectors(self, vectors: torch.Tensor) -> List[List[float]]:
57
+ if self.normalize:
58
+ vectors = normalize_vectors(vectors)
59
+
60
+ return vectors.detach().cpu().numpy().tolist()
src/models.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, validator
2
+ from typing import List
3
+ from PIL.Image import Image
4
+
5
+ from .utils import download_image_as_pil
6
+
7
+
8
+ BATCH_SIZE_TEXT: int = 128
9
+ BATCH_SIZE_IMAGE: int = 64
10
+
11
+
12
+ class TextRequest(BaseModel):
13
+ texts: List[str]
14
+
15
+ @validator("texts")
16
+ def validate_texts_batch_size(cls, v):
17
+ if len(v) > BATCH_SIZE_TEXT:
18
+ raise ValueError(f"Maximum batch size for texts is {BATCH_SIZE_TEXT}")
19
+ if len(v) == 0:
20
+ raise ValueError("At least one text is required")
21
+ return v
22
+
23
+
24
+ class ImageRequest(BaseModel):
25
+ urls: List[str]
26
+
27
+ @validator("urls")
28
+ def validate_images_batch_size(cls, v):
29
+ if len(v) > BATCH_SIZE_IMAGE:
30
+ raise ValueError(f"Maximum batch size for images is {BATCH_SIZE_IMAGE}")
31
+ if len(v) == 0:
32
+ raise ValueError("At least one image URL is required")
33
+ return v
34
+
35
+ def download(self) -> List[Image]:
36
+ return [download_image_as_pil(url) for url in self.urls]
37
+
38
+
39
+ class Response(BaseModel):
40
+ embeddings: List[List[float]]
src/utils.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, List
2
+
3
+ import requests, torch
4
+ from PIL import Image
5
+
6
+
7
+ REQUESTS_HEADERS = {
8
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
9
+ }
10
+
11
+
12
+ def download_image_as_pil(url: str, timeout: int = 10) -> Image.Image:
13
+ try:
14
+ response = requests.get(
15
+ url, stream=True, headers=REQUESTS_HEADERS, timeout=timeout
16
+ )
17
+
18
+ if response.status_code == 200:
19
+ return Image.open(response.raw)
20
+
21
+ except Exception as e:
22
+ return
23
+
24
+
25
+ def delete_images(images: List[Image.Image]) -> bool:
26
+ try:
27
+ for image in images:
28
+ if hasattr(image, "close"):
29
+ image.close()
30
+ success = True
31
+
32
+ except Exception:
33
+ success = False
34
+
35
+ del images
36
+
37
+ return success
38
+
39
+
40
+ def normalize_vectors(vectors: torch.Tensor) -> torch.Tensor:
41
+ norms = torch.norm(vectors, p=2, dim=1, keepdim=True)
42
+ norms = torch.norm(vectors, p=2, dim=1, keepdim=True)
43
+ norms = torch.where(norms > 1e-8, norms, torch.ones_like(norms))
44
+ normalized_vectors = vectors / norms
45
+
46
+ return normalized_vectors
47
+
48
+
49
+ def analyze_model_parameters(model: torch.nn.Module) -> Dict:
50
+ total_params = 0
51
+ param_types = set()
52
+ param_type_counts = {}
53
+
54
+ for param in model.parameters():
55
+ total_params += param.numel()
56
+ dtype = param.dtype
57
+ param_types.add(dtype)
58
+ param_type_counts[dtype] = param_type_counts.get(dtype, 0) + param.numel()
59
+
60
+ results = {
61
+ "total_params": total_params,
62
+ "param_types": {},
63
+ "device_info": {
64
+ "device": next(model.parameters()).device,
65
+ "cuda_available": torch.cuda.is_available(),
66
+ },
67
+ }
68
+
69
+ for dtype in param_types:
70
+ count = param_type_counts[dtype]
71
+ percentage = (count / total_params) * 100
72
+ memory_bytes = count * torch.finfo(dtype).bits // 8
73
+ memory_mb = memory_bytes / (1024 * 1024)
74
+
75
+ results["param_types"][str(dtype)] = {
76
+ "count": count,
77
+ "percentage": percentage,
78
+ "memory_mb": memory_mb,
79
+ }
80
+
81
+ if torch.cuda.is_available():
82
+ results["device_info"].update(
83
+ {
84
+ "cuda_device": torch.cuda.get_device_name(0),
85
+ "cuda_memory_allocated_mb": torch.cuda.memory_allocated(0) / 1024**2,
86
+ "cuda_memory_cached_mb": torch.cuda.memory_reserved(0) / 1024**2,
87
+ }
88
+ )
89
+
90
+ return results