makeitfr commited on
Commit
1fd6a84
·
verified ·
1 Parent(s): df57942

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +285 -0
app.py ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import time
4
+ import subprocess
5
+ import numpy as np
6
+ from PIL import Image
7
+ from io import BytesIO
8
+ import requests
9
+ import threading
10
+
11
+ # FastAPI imports
12
+ from fastapi import FastAPI, UploadFile, File, HTTPException, Form
13
+ from fastapi.responses import JSONResponse
14
+ import uvicorn
15
+
16
+ # 1. Environment Setup & Dependency Installation
17
+ def setup_environment():
18
+ print("--- Setting up environment ---")
19
+ dependencies = ["huggingface_hub", "onnxruntime", "transformers", "pillow", "numpy"]
20
+ try:
21
+ import huggingface_hub
22
+ import onnxruntime
23
+ import transformers
24
+ print("Dependencies already satisfied.")
25
+ except ImportError:
26
+ print("Installing dependencies...")
27
+ subprocess.check_call([sys.executable, "-m", "pip", "install"] + dependencies)
28
+
29
+ # 2. Model Download
30
+ def download_model(repo_id="Heliosoph/florence-2-base-ft-quantized-onnx", local_dir="florence2_quantized"):
31
+ from huggingface_hub import snapshot_download
32
+ if not os.path.exists(local_dir):
33
+ print(f"--- Downloading model from {repo_id} ---")
34
+ snapshot_download(repo_id=repo_id, local_dir=local_dir)
35
+ print("Download complete.")
36
+ else:
37
+ print(f"Model directory '{local_dir}' already exists.")
38
+
39
+ # 3. Inference Engine
40
+ class Florence2ONNXEngine:
41
+ def __init__(self, model_dir="florence2_quantized"):
42
+ import onnxruntime as ort
43
+ from transformers import CLIPImageProcessor, AutoTokenizer
44
+
45
+ self.model_dir = model_dir
46
+ print("--- Initializing ONNX Engine ---")
47
+
48
+ # Load processors
49
+ self.image_processor = CLIPImageProcessor.from_pretrained("microsoft/Florence-2-base-ft")
50
+ self.tokenizer = AutoTokenizer.from_pretrained("facebook/bart-base")
51
+
52
+ # Load ONNX sessions
53
+ providers = ['CPUExecutionProvider']
54
+ self.vision_session = ort.InferenceSession(os.path.join(model_dir, 'vision_encoder_quantized.onnx'), providers=providers)
55
+ self.embed_session = ort.InferenceSession(os.path.join(model_dir, 'embed_tokens_quantized.onnx'), providers=providers)
56
+ self.encoder_session = ort.InferenceSession(os.path.join(model_dir, 'encoder_model_quantized.onnx'), providers=providers)
57
+ self.decoder_session = ort.InferenceSession(os.path.join(model_dir, 'decoder_model_quantized.onnx'), providers=providers)
58
+ print("✓ Florence-2 ONNX Engine initialized successfully")
59
+
60
+ def generate_caption(self, image_path=None, image_array=None, task_prompt="<MORE_DETAILED_CAPTION>", max_new_tokens=1024):
61
+ """Generate caption from image path or PIL Image object"""
62
+ if image_path:
63
+ image = Image.open(image_path).convert("RGB")
64
+ elif image_array is not None and isinstance(image_array, Image.Image):
65
+ image = image_array.convert("RGB")
66
+ else:
67
+ raise ValueError("Either image_path or image_array must be provided")
68
+
69
+ print(f"--- Running Inference (Max Tokens: {max_new_tokens}) ---")
70
+ pixel_values = self.image_processor(images=image, return_tensors="np")['pixel_values']
71
+
72
+ # Map specific prompts to descriptive strings if needed
73
+ prompt_map = {
74
+ "<CAPTION>": "What does the image describe?",
75
+ "<DETAILED_CAPTION>": "Describe this image in detail.",
76
+ "<MORE_DETAILED_CAPTION>": "Describe this image in great detail with every object and background."
77
+ }
78
+ text_prompt = prompt_map.get(task_prompt, task_prompt)
79
+ input_ids = self.tokenizer(text_prompt, return_tensors="np")['input_ids']
80
+
81
+ # 1. Vision Features
82
+ start_time = time.time()
83
+ image_features = self.vision_session.run(None, {'pixel_values': pixel_values})[0]
84
+
85
+ # 2. Text Embeddings
86
+ text_embeds = self.embed_session.run(None, {'input_ids': input_ids})[0]
87
+
88
+ # 3. Encoder Fusion
89
+ combined_embeds = np.concatenate([image_features, text_embeds], axis=1)
90
+ attention_mask = np.ones((1, combined_embeds.shape[1]), dtype=np.int64)
91
+ encoder_outputs = self.encoder_session.run(None, {
92
+ 'inputs_embeds': combined_embeds,
93
+ 'attention_mask': attention_mask
94
+ })
95
+ last_hidden_state = encoder_outputs[0]
96
+
97
+ # 4. Autoregressive Decoding with Repetition Penalty
98
+ generated_ids = [2] # BART Start Token
99
+ min_new_tokens = 250 # Enforce minimum generation
100
+ repetition_penalty = 1.5 # Penalize repeated tokens
101
+
102
+ for i in range(max_new_tokens):
103
+ decoder_input_ids = np.array([generated_ids], dtype=np.int64)
104
+ decoder_embeds = self.embed_session.run(None, {'input_ids': decoder_input_ids})[0]
105
+
106
+ logits = self.decoder_session.run(None, {
107
+ 'inputs_embeds': decoder_embeds,
108
+ 'encoder_hidden_states': last_hidden_state,
109
+ 'encoder_attention_mask': attention_mask
110
+ })[0]
111
+
112
+ # Apply repetition penalty to recently generated tokens
113
+ current_logits = logits[0, -1, :].copy()
114
+ for prev_token in set(generated_ids[-50:]): # Check last 50 tokens
115
+ if current_logits[prev_token] > 0:
116
+ current_logits[prev_token] /= repetition_penalty
117
+ else:
118
+ current_logits[prev_token] *= repetition_penalty
119
+
120
+ next_token = np.argmax(current_logits)
121
+ # Only allow EOS token after minimum generation
122
+ if next_token == 2 and i < min_new_tokens:
123
+ # Force a different token by reducing EOS probability
124
+ current_logits[2] = -1e9
125
+ next_token = np.argmax(current_logits)
126
+ if next_token == 2: break # EOS Token
127
+ generated_ids.append(next_token)
128
+
129
+ if (i + 1) % 50 == 0:
130
+ print(f"Generated {i+1} tokens...")
131
+
132
+ end_time = time.time()
133
+ caption = self.tokenizer.decode(generated_ids, skip_special_tokens=True)
134
+
135
+ print(f"Inference complete in {end_time - start_time:.2f}s")
136
+ return caption
137
+
138
+
139
+ # Global engine instance
140
+ engine = None
141
+
142
+ def initialize_engine():
143
+ """Initialize the Florence2 ONNX engine"""
144
+ global engine
145
+ setup_environment()
146
+ download_model()
147
+ engine = Florence2ONNXEngine()
148
+
149
+ # FastAPI app setup
150
+ app = FastAPI(
151
+ title="Florence-2 ONNX Image Captioning Server",
152
+ description="Auto-captions images using Florence-2 ONNX models"
153
+ )
154
+
155
+ def load_image_from_url(image_url: str) -> Image.Image:
156
+ """Load an image from a URL."""
157
+ try:
158
+ response = requests.get(image_url, timeout=30)
159
+ response.raise_for_status()
160
+ image = Image.open(BytesIO(response.content))
161
+ return image.convert('RGB')
162
+ except Exception as e:
163
+ raise ValueError(f"Error loading image from URL: {e}")
164
+
165
+ def load_image_from_bytes(image_bytes: bytes) -> Image.Image:
166
+ """Load an image from bytes."""
167
+ try:
168
+ image = Image.open(BytesIO(image_bytes))
169
+ return image.convert('RGB')
170
+ except Exception as e:
171
+ raise ValueError(f"Error loading image from bytes: {e}")
172
+
173
+ # API Endpoints
174
+
175
+ @app.get("/")
176
+ async def root():
177
+ """Root endpoint - shows server status"""
178
+ return {
179
+ "name": "Florence-2 ONNX Image Captioning Server",
180
+ "status": "running",
181
+ "model": "Florence-2-base-ft-quantized-onnx",
182
+ "model_loaded": engine is not None,
183
+ "endpoints": {
184
+ "GET /health": "Health check",
185
+ "GET /analyze": "Analyze image from URL",
186
+ "POST /analyze": "Analyze uploaded image",
187
+ }
188
+ }
189
+
190
+ @app.get("/health")
191
+ async def health():
192
+ """Health check endpoint"""
193
+ return {
194
+ "status": "healthy" if engine is not None else "initializing",
195
+ "model": "Florence-2-base-ft-quantized-onnx",
196
+ "model_loaded": engine is not None,
197
+ }
198
+
199
+ @app.get("/analyze")
200
+ async def analyze_get(image_url: str = None):
201
+ """Analyze an image by URL.
202
+
203
+ Usage: /analyze?image_url=https://example.com/image.jpg
204
+ """
205
+ try:
206
+ if engine is None:
207
+ raise HTTPException(status_code=503, detail="Model not initialized")
208
+
209
+ if not image_url:
210
+ raise HTTPException(status_code=400, detail="image_url query parameter is required")
211
+
212
+ # Load image from URL
213
+ image = load_image_from_url(image_url)
214
+
215
+ # Generate caption
216
+ caption = engine.generate_caption(image_array=image)
217
+
218
+ return JSONResponse(content={
219
+ "success": True,
220
+ "caption": caption,
221
+ "image_size": {"width": image.width, "height": image.height},
222
+ "model": "Florence-2-base-ft-quantized-onnx"
223
+ })
224
+
225
+ except HTTPException:
226
+ raise
227
+ except Exception as e:
228
+ return JSONResponse(
229
+ status_code=500,
230
+ content={"success": False, "error": str(e)}
231
+ )
232
+
233
+ @app.post("/analyze")
234
+ async def analyze_post(file: UploadFile = File(None)):
235
+ """Analyze an uploaded image (multipart/form-data).
236
+
237
+ Returns: JSON with caption and metadata
238
+ """
239
+ try:
240
+ if engine is None:
241
+ raise HTTPException(status_code=503, detail="Model not initialized")
242
+
243
+ if file is None:
244
+ raise HTTPException(status_code=400, detail="file is required")
245
+
246
+ # Read uploaded file
247
+ content = await file.read()
248
+
249
+ # Load image from bytes
250
+ try:
251
+ image = load_image_from_bytes(content)
252
+ except Exception as e:
253
+ raise HTTPException(status_code=400, detail=f"Failed to read uploaded image: {e}")
254
+
255
+ # Generate caption
256
+ caption = engine.generate_caption(image_array=image)
257
+
258
+ return JSONResponse(content={
259
+ "success": True,
260
+ "caption": caption,
261
+ "filename": file.filename,
262
+ "image_size": {"width": image.width, "height": image.height},
263
+ "model": "Florence-2-base-ft-quantized-onnx"
264
+ })
265
+
266
+ except HTTPException:
267
+ raise
268
+ except Exception as e:
269
+ return JSONResponse(
270
+ status_code=500,
271
+ content={"success": False, "error": str(e)}
272
+ )
273
+
274
+ # Get the port from environment variable
275
+ port = int(os.environ.get("PORT", 7860))
276
+
277
+ # Launch server
278
+ if __name__ == "__main__":
279
+ print("Initializing Florence-2 ONNX Engine...")
280
+ initialize_engine()
281
+
282
+ print(f"\n✓ Server ready! Starting on 0.0.0.0:{port}")
283
+ print(f"API Documentation: http://localhost:{port}/docs")
284
+
285
+ uvicorn.run(app, host="0.0.0.0", port=port)