oldAhmed commited on
Commit
69f85cd
·
verified ·
1 Parent(s): 4cc7b45

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +242 -0
app.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ os.environ["HF_HOME"] = "/app/cache"
3
+ os.environ["TORCH_HOME"] = "/app/cache"
4
+
5
+ import torch
6
+ import sys
7
+ import torch.nn as nn
8
+ from torchvision import transforms
9
+ from torchvision.models import inception_v3, Inception_V3_Weights
10
+ from transformers import BertTokenizerFast, BertModel
11
+ from PIL import Image
12
+ from fastapi import FastAPI, File, UploadFile, Form
13
+ from fastapi.responses import JSONResponse
14
+ import traceback
15
+ from io import BytesIO
16
+
17
+ # ----------------------------
18
+ # Configurations
19
+ # ----------------------------
20
+ app = FastAPI(title="Multimodal Hate Speech Detection")
21
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
22
+
23
+ CACHE_DIR = "/app/cache"
24
+ MODEL_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "BERT_Inception_multimodal.pt")
25
+
26
+ # Image preprocessing
27
+ image_transform = transforms.Compose([
28
+ transforms.Resize((299, 299)),
29
+ transforms.ToTensor(),
30
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
31
+ ])
32
+
33
+ # ----------------------------
34
+ # Model Definition
35
+ # ----------------------------
36
+ class TensorFusionMultimodalModel(nn.Module):
37
+ def __init__(self, bert_model):
38
+ super(TensorFusionMultimodalModel, self).__init__()
39
+ self.bert = bert_model
40
+ self.text_fc = nn.Linear(self.bert.config.hidden_size, 256)
41
+
42
+ # Image branch (InceptionV3 backbone)
43
+ weights = Inception_V3_Weights.IMAGENET1K_V1
44
+ inception = inception_v3(weights=None, aux_logits=True) # Do NOT auto-download
45
+ state_dict = weights.get_state_dict(progress=False) # Load from local cache
46
+ inception.load_state_dict(state_dict)
47
+
48
+ for param in inception.parameters():
49
+ param.requires_grad = False
50
+
51
+ self.cnn_backbone = nn.Sequential(*list(inception.children())[:-1]) # Remove FC layer
52
+ self.image_fc = nn.Sequential(
53
+ nn.Flatten(),
54
+ nn.Linear(2048, 256),
55
+ nn.BatchNorm1d(256),
56
+ nn.ReLU(),
57
+ nn.Dropout(0.5)
58
+ )
59
+
60
+ # Fusion layer
61
+ self.fusion_dim = 256 * 256 + 256 + 256 + 1
62
+ self.fusion_fc = nn.Sequential(
63
+ nn.Linear(self.fusion_dim, 256),
64
+ nn.ReLU(),
65
+ nn.Dropout(0.4),
66
+ nn.Linear(256, 1),
67
+ nn.Sigmoid()
68
+ )
69
+
70
+ def tensor_fusion(self, text_feat, img_feat):
71
+ batch_size = text_feat.size(0)
72
+ outer = torch.bmm(text_feat.unsqueeze(2), img_feat.unsqueeze(1)).view(batch_size, -1)
73
+ fusion = torch.cat([outer, text_feat, img_feat, torch.ones(batch_size, 1).to(text_feat.device)], dim=1)
74
+ return fusion
75
+
76
+ def forward(self, input_ids, attention_mask, images):
77
+ text_output = self.bert(input_ids=input_ids, attention_mask=attention_mask)
78
+ text_feat = self.text_fc(text_output.pooler_output)
79
+
80
+ img_feat = self.cnn_backbone(images).flatten(1)
81
+ img_feat = self.image_fc(img_feat)
82
+
83
+ fused_feat = self.tensor_fusion(text_feat, img_feat)
84
+ return self.fusion_fc(fused_feat)
85
+
86
+ # ----------------------------
87
+ # Load Model on Startup
88
+ # ----------------------------
89
+ @app.on_event("startup")
90
+ def load_model():
91
+ global model, tokenizer
92
+
93
+ if not os.path.exists(MODEL_PATH):
94
+ raise FileNotFoundError(f"[ERROR] Model file not found: {MODEL_PATH}")
95
+
96
+ tokenizer = BertTokenizerFast.from_pretrained("bert-base-multilingual-cased", cache_dir=CACHE_DIR)
97
+
98
+ # Allow pickle resolution
99
+ sys.modules['__main__'] = sys.modules['app']
100
+
101
+ # Load the full model
102
+ loaded = torch.load(MODEL_PATH, map_location=device)
103
+
104
+ # ✅ If loaded object still has `forward` via DataParallel, unwrap forcibly
105
+ if hasattr(loaded, "module"): # Works even if type doesn't match
106
+ loaded = loaded.module
107
+
108
+ # ✅ Move all parameters and buffers to device
109
+ loaded = loaded.to(device)
110
+
111
+ # Set to eval
112
+ loaded.eval()
113
+
114
+ for p in loaded.parameters():
115
+ p.requires_grad = False
116
+
117
+ model = loaded
118
+ print(f"[INFO] Model loaded successfully on {device}")
119
+
120
+ # @app.on_event("startup")
121
+ # def load_model():
122
+ # global model, tokenizer
123
+
124
+ # if not os.path.exists(MODEL_PATH):
125
+ # raise FileNotFoundError(f"[ERROR] Model file not found: {MODEL_PATH}")
126
+
127
+ # # Load tokenizer & BERT
128
+ # tokenizer = BertTokenizerFast.from_pretrained("bert-base-multilingual-cased", cache_dir=CACHE_DIR)
129
+ # bert_model = BertModel.from_pretrained("bert-base-multilingual-cased", cache_dir=CACHE_DIR)
130
+
131
+ # # Trick for pickle to resolve class path
132
+ # import sys
133
+ # sys.modules['__main__'] = sys.modules['app']
134
+
135
+ # # Load the full model
136
+ # model = torch.load(MODEL_PATH, map_location=device)
137
+ # model.to(device)
138
+ # model.eval()
139
+
140
+ # print("[INFO] Model and tokenizer loaded successfully!")
141
+
142
+
143
+
144
+
145
+
146
+ # ----------------------------
147
+ # API Endpoints
148
+ # ----------------------------
149
+ @app.get("/")
150
+ async def root():
151
+ return {"message": "API is running. Use POST /predict"}
152
+
153
+
154
+ @app.post("/predict")
155
+ async def predict(text: str = Form(...), file: UploadFile = File(...)):
156
+ try:
157
+ if 'model' not in globals() or model is None:
158
+ return JSONResponse(content={"error": "Model not loaded"}, status_code=500)
159
+ if 'tokenizer' not in globals() or tokenizer is None:
160
+ return JSONResponse(content={"error": "Tokenizer not loaded"}, status_code=500)
161
+
162
+ # Tokenize text
163
+ encoding = tokenizer(
164
+ text,
165
+ add_special_tokens=True,
166
+ max_length=125,
167
+ padding='max_length',
168
+ truncation=True,
169
+ return_tensors='pt'
170
+ )
171
+ input_ids = encoding['input_ids'].to(device, non_blocking=True)
172
+ attention_mask = encoding['attention_mask'].to(device, non_blocking=True)
173
+
174
+ # Process image
175
+ contents = await file.read()
176
+ if not contents:
177
+ return JSONResponse(content={"error": "Empty file received"}, status_code=400)
178
+
179
+ try:
180
+ img = Image.open(BytesIO(contents)).convert("RGB")
181
+ except Exception:
182
+ return JSONResponse(content={"error": "Invalid image file"}, status_code=400)
183
+
184
+ img_tensor = image_transform(img).unsqueeze(0).to(device, non_blocking=True)
185
+
186
+ # Predict
187
+ with torch.inference_mode():
188
+ # (Optional) autocast for GPU float16 speedup; safe to leave on CPU too
189
+ # On CPU autocast('cpu') is available in newer torch; we keep it simple:
190
+ output = model(input_ids, attention_mask, img_tensor)
191
+
192
+ # output should be shape [1, 1]; get scalar
193
+ prob = float(output.squeeze().item())
194
+ label = "Hate Speech" if prob >= 0.5 else "Non-Hate Speech"
195
+
196
+ return {"prediction": label, "confidence": round(prob, 4)}
197
+
198
+ except Exception:
199
+ print("Prediction Error:", traceback.format_exc())
200
+ return JSONResponse(content={"error": "Internal server error"}, status_code=500)
201
+
202
+ # @app.post("/predict")
203
+ # async def predict(text: str = Form(...), file: UploadFile = File(...)):
204
+ # try:
205
+ # # Tokenize text
206
+ # encoding = tokenizer(
207
+ # text,
208
+ # add_special_tokens=True,
209
+ # max_length=125,
210
+ # padding='max_length',
211
+ # truncation=True,
212
+ # return_tensors='pt'
213
+ # )
214
+ # input_ids = encoding['input_ids'].to(device)
215
+ # attention_mask = encoding['attention_mask'].to(device)
216
+
217
+ # # Process image
218
+ # contents = await file.read()
219
+ # img = Image.open(BytesIO(contents)).convert("RGB")
220
+ # img_tensor = image_transform(img).unsqueeze(0).to(device)
221
+
222
+ # # Predict
223
+ # with torch.no_grad():
224
+ # output = model(input_ids, attention_mask, img_tensor)
225
+ # prob = output.item()
226
+ # label = "Hate" if prob >= 0.5 else "No Hate"
227
+
228
+ # return {"prediction": label, "confidence": round(prob, 4)}
229
+
230
+
231
+
232
+ # except Exception as e:
233
+ # print("Prediction Error:", traceback.format_exc())
234
+ # return JSONResponse(content={"error": str(e)}, status_code=500)
235
+
236
+
237
+ # ----------------------------
238
+ # Run server (for local dev)
239
+ # ----------------------------
240
+ if __name__ == "__main__":
241
+ import uvicorn
242
+ uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=False)