abdebug2003 commited on
Commit
74ce024
·
verified ·
1 Parent(s): ea57de7

Upload handler.py

Browse files
Files changed (1) hide show
  1. handler.py +121 -0
handler.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Custom Hugging Face Inference Endpoint handler for Qwen3-VL-Embedding-8B.
3
+
4
+ WHERE THIS FILE GOES:
5
+ Hugging Face Inference Endpoints look for a `handler.py` file living in the
6
+ ROOT of the MODEL REPO you deploy (not in your own project repo). So:
7
+
8
+ 1. Duplicate Qwen/Qwen3-VL-Embedding-8B into your own namespace on the Hub
9
+ (huggingface.co -> the model page -> "..." menu -> "Duplicate this model"),
10
+ e.g. your-username/qwen3-vl-embedding-endpoint. This copies the weights
11
+ without you having to re-upload ~16GB yourself.
12
+ 2. Add this file to that new repo, named exactly `handler.py`, in the repo root.
13
+ 3. Add the accompanying `requirements.txt` (see endpoint_requirements.txt)
14
+ to that same repo root.
15
+ 4. Deploy an Inference Endpoint from that repo. Because it contains a
16
+ handler.py, Endpoints will use it automatically instead of a default
17
+ pipeline.
18
+
19
+ WHAT IT DOES:
20
+ Loads the model once when the endpoint starts, then on every request:
21
+ builds the same system-instruction + text/image conversation your old
22
+ local code used, runs a forward pass, takes last-token pooling, L2-normalizes,
23
+ and returns the embedding as JSON.
24
+
25
+ REQUEST FORMAT (what your client should POST):
26
+ {"inputs": {"text": "some product text", "image_base64": "<optional b64>"}}
27
+
28
+ RESPONSE FORMAT:
29
+ {"embedding": [0.01, -0.02, ...], "dimension": 4096}
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import base64
34
+ from io import BytesIO
35
+ from typing import Any, Dict, List, Optional
36
+
37
+ import torch
38
+ import torch.nn.functional as F
39
+ from PIL import Image
40
+ from transformers import AutoModel, AutoProcessor
41
+
42
+ INSTRUCTION = (
43
+ "Represent this retail product for multimodal fashion, beauty, and home catalog retrieval. "
44
+ "Preserve product identity, category, materials, visible design details, structure, color nuance, and style-relevant attributes."
45
+ )
46
+
47
+ MAX_IMAGE_SIDE = 768
48
+
49
+
50
+ def _format_as_conversation(text: str, has_image: bool) -> List[Dict[str, Any]]:
51
+ content: List[Dict[str, Any]] = []
52
+ if has_image:
53
+ content.append({"type": "image"})
54
+ if text:
55
+ content.append({"type": "text", "text": text})
56
+ if not content:
57
+ content.append({"type": "text", "text": ""})
58
+ return [
59
+ {"role": "system", "content": [{"type": "text", "text": INSTRUCTION}]},
60
+ {"role": "user", "content": content},
61
+ ]
62
+
63
+
64
+ class EndpointHandler:
65
+ def __init__(self, path: str = ""):
66
+ # `path` is filled in by the Endpoints runtime with the local
67
+ # directory the repo (including model weights) was downloaded into.
68
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
69
+ self.dtype = torch.bfloat16 if self.device == "cuda" else torch.float32
70
+
71
+ self.processor = AutoProcessor.from_pretrained(
72
+ path, trust_remote_code=True, local_files_only=True
73
+ )
74
+ self.model = AutoModel.from_pretrained(
75
+ path, trust_remote_code=True, local_files_only=True, torch_dtype=self.dtype
76
+ )
77
+ self.model = self.model.to(self.device).eval()
78
+
79
+ def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
80
+ payload = data.get("inputs", data) or {}
81
+ text = payload.get("text") or ""
82
+ image_b64 = payload.get("image_base64")
83
+
84
+ image = self._decode_image(image_b64) if image_b64 else None
85
+ _validate(text, image)
86
+
87
+ vector = self._compute(text, image)
88
+ return {"embedding": vector, "dimension": len(vector)}
89
+
90
+ def _compute(self, text: str, image: Optional[Image.Image]) -> List[float]:
91
+ images = [image] if image is not None else None
92
+ conversation = _format_as_conversation(text, image is not None)
93
+ prompt_text = self.processor.apply_chat_template(
94
+ conversation, tokenize=False, add_generation_prompt=True
95
+ )
96
+ inputs = self.processor(text=[prompt_text], images=images, padding=True, return_tensors="pt")
97
+ inputs = {key: value.to(self.device) for key, value in inputs.items()}
98
+
99
+ with torch.inference_mode():
100
+ outputs = self.model(**inputs)
101
+ hidden = outputs.last_hidden_state
102
+ attention_mask = inputs["attention_mask"]
103
+ last_token_index = attention_mask.sum(dim=1) - 1
104
+ embedding = hidden[0, last_token_index[0]]
105
+ embedding = F.normalize(embedding, p=2, dim=0)
106
+
107
+ return embedding.detach().cpu().float().tolist()
108
+
109
+ @staticmethod
110
+ def _decode_image(image_b64: str) -> Image.Image:
111
+ try:
112
+ image = Image.open(BytesIO(base64.b64decode(image_b64))).convert("RGB")
113
+ except Exception as exc:
114
+ raise ValueError(f"Invalid image_base64: {exc}") from exc
115
+ image.thumbnail((MAX_IMAGE_SIDE, MAX_IMAGE_SIDE), Image.Resampling.BICUBIC)
116
+ return image
117
+
118
+
119
+ def _validate(text: str, image: Optional[Image.Image]) -> None:
120
+ if not text and image is None:
121
+ raise ValueError("Either 'text' or 'image_base64' must be provided")