DearthAI commited on
Commit
f086d7e
·
verified ·
1 Parent(s): b6ac386

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +42 -6
app.py CHANGED
@@ -19,11 +19,13 @@ embedding pipeline.
19
  import io
20
  import os
21
  from urllib.parse import urlsplit
 
22
 
23
  import numpy as np
24
  import open_clip
25
  import requests
26
  import torch
 
27
  from fastapi import FastAPI, Header, HTTPException
28
  from PIL import Image
29
  from pydantic import BaseModel
@@ -37,6 +39,17 @@ FETCH_UA = (
37
  "(KHTML, like Gecko) Chrome/120 Safari/537.36"
38
  )
39
 
 
 
 
 
 
 
 
 
 
 
 
40
  app = FastAPI(title="Weaver OpenCLIP encoder")
41
 
42
  device = "cuda" if torch.cuda.is_available() else "cpu"
@@ -45,6 +58,24 @@ model = model.to(device).eval()
45
  tokenizer = open_clip.get_tokenizer(MODEL_NAME)
46
 
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  def _check_auth(authorization: str | None) -> None:
49
  if EMBED_TOKEN and authorization != f"Bearer {EMBED_TOKEN}":
50
  raise HTTPException(status_code=401, detail="unauthorized")
@@ -60,7 +91,7 @@ class ImageRequest(BaseModel):
60
 
61
  @app.get("/health")
62
  def health():
63
- return {"ok": True, "device": device}
64
 
65
 
66
  @app.post("/embed")
@@ -107,13 +138,18 @@ def embed_image(req: ImageRequest, authorization: str | None = Header(default=No
107
  pass
108
 
109
  out: list[list[float] | None] = [None] * len(urls)
 
110
  if tensors:
111
  batch = torch.stack(tensors).to(device)
112
  with torch.no_grad():
113
  feats = model.encode_image(batch)
114
  feats = feats / feats.norm(dim=-1, keepdim=True)
115
- feats = feats.cpu().numpy().astype(np.float32)
116
- for slot, vec in zip(slots, feats):
117
- out[slot] = vec.tolist()
118
-
119
- return {"embeddings": out, "dims": dims}
 
 
 
 
 
19
  import io
20
  import os
21
  from urllib.parse import urlsplit
22
+ from urllib.request import urlopen
23
 
24
  import numpy as np
25
  import open_clip
26
  import requests
27
  import torch
28
+ import torch.nn as nn
29
  from fastapi import FastAPI, Header, HTTPException
30
  from PIL import Image
31
  from pydantic import BaseModel
 
39
  "(KHTML, like Gecko) Chrome/120 Safari/537.36"
40
  )
41
 
42
+ # LAION aesthetic predictor — a single Linear(512, 1) head trained on top of
43
+ # CLIP ViT-B/32 embeddings (the SAME vectors we already compute), so quality
44
+ # scoring is nearly free: no second model, no extra image fetch/decode. Output is
45
+ # roughly the AVA 1..10 aesthetic scale. Weights are the original LAION linear
46
+ # predictor for vit_b_32. If the download fails at startup, aesthetic scoring is
47
+ # simply disabled (the API returns null and the feed treats it as neutral).
48
+ AESTHETIC_URLS = [
49
+ "https://raw.githubusercontent.com/LAION-AI/aesthetic-predictor/main/sa_0_4_vit_b_32_linear.pth",
50
+ "https://github.com/LAION-AI/aesthetic-predictor/raw/main/sa_0_4_vit_b_32_linear.pth",
51
+ ]
52
+
53
  app = FastAPI(title="Weaver OpenCLIP encoder")
54
 
55
  device = "cuda" if torch.cuda.is_available() else "cpu"
 
58
  tokenizer = open_clip.get_tokenizer(MODEL_NAME)
59
 
60
 
61
+ def _load_aesthetic_head() -> nn.Module | None:
62
+ """Load the LAION linear aesthetic head for ViT-B/32 (512-dim). None on failure."""
63
+ head = nn.Linear(512, 1)
64
+ for url in AESTHETIC_URLS:
65
+ try:
66
+ with urlopen(url, timeout=30) as resp: # noqa: S310 — fixed trusted URLs
67
+ state = torch.load(io.BytesIO(resp.read()), map_location="cpu")
68
+ head.load_state_dict(state)
69
+ head = head.to(device).eval()
70
+ return head
71
+ except Exception: # noqa: BLE001 — any failure → scoring disabled
72
+ continue
73
+ return None
74
+
75
+
76
+ aesthetic_head = _load_aesthetic_head()
77
+
78
+
79
  def _check_auth(authorization: str | None) -> None:
80
  if EMBED_TOKEN and authorization != f"Bearer {EMBED_TOKEN}":
81
  raise HTTPException(status_code=401, detail="unauthorized")
 
91
 
92
  @app.get("/health")
93
  def health():
94
+ return {"ok": True, "device": device, "aesthetic": aesthetic_head is not None}
95
 
96
 
97
  @app.post("/embed")
 
138
  pass
139
 
140
  out: list[list[float] | None] = [None] * len(urls)
141
+ aesthetics: list[float | None] = [None] * len(urls)
142
  if tensors:
143
  batch = torch.stack(tensors).to(device)
144
  with torch.no_grad():
145
  feats = model.encode_image(batch)
146
  feats = feats / feats.norm(dim=-1, keepdim=True)
147
+ # Aesthetic score off the SAME normalized embedding (near-free).
148
+ aes = aesthetic_head(feats).squeeze(-1).cpu().numpy() if aesthetic_head else None
149
+ feats_np = feats.cpu().numpy().astype(np.float32)
150
+ for k, slot in enumerate(slots):
151
+ out[slot] = feats_np[k].tolist()
152
+ if aes is not None:
153
+ aesthetics[slot] = float(aes[k])
154
+
155
+ return {"embeddings": out, "dims": dims, "aesthetics": aesthetics}