NeelakshSaxena commited on
Commit
5ec6ff8
Β·
1 Parent(s): 6cc5e1f

Add TMOS deepfake Gradio Space

Browse files
Files changed (3) hide show
  1. app.py +187 -0
  2. requirements.txt +36 -0
  3. scripts/tmos_classifier.py +208 -0
app.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import time
4
+ from contextlib import nullcontext
5
+ from pathlib import Path
6
+
7
+ import gradio as gr
8
+ import torch
9
+ from dotenv import load_dotenv
10
+ from peft import PeftModel
11
+ from PIL import Image, ImageOps
12
+ from transformers import AutoProcessor
13
+
14
+ ROOT_DIR = Path(__file__).resolve().parent
15
+ SCRIPTS_DIR = ROOT_DIR / "scripts"
16
+
17
+ if str(SCRIPTS_DIR) not in sys.path:
18
+ sys.path.insert(0, str(SCRIPTS_DIR))
19
+
20
+ from tmos_classifier import TMOSClassifier
21
+
22
+ load_dotenv()
23
+ HF_TOKEN = os.getenv("HF_TOKEN")
24
+
25
+ BASE_MODEL_ID = "llava-hf/llava-1.5-7b-hf"
26
+ ADAPTER_PATH = ROOT_DIR / "final-production-weights" / "best_model"
27
+ ADAPTER_REPO_ID = "Werrewulf/tmos-dd-weights"
28
+ PROMPT = "USER: <image>\nIs this image real or produced by AI?\nASSISTANT:"
29
+ TARGET_IMAGE_SIZE = 336
30
+ THRESHOLD = 0.5
31
+
32
+ model = None
33
+ processor = None
34
+ inference_device = None
35
+
36
+
37
+ def select_torch_dtype() -> torch.dtype:
38
+ if torch.cuda.is_available():
39
+ return torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
40
+ return torch.float16
41
+
42
+
43
+ def load_model_and_processor():
44
+ global model, processor, inference_device
45
+
46
+ if model is not None and processor is not None and inference_device is not None:
47
+ return model, processor, inference_device
48
+
49
+ adapter_source = None
50
+ local_adapter_file = next(
51
+ (
52
+ candidate
53
+ for candidate in (
54
+ ADAPTER_PATH / "adapter_model.safetensors",
55
+ ADAPTER_PATH / "adapter_model.bin",
56
+ )
57
+ if candidate.exists()
58
+ ),
59
+ None,
60
+ )
61
+
62
+ if local_adapter_file is not None:
63
+ adapter_source = str(ADAPTER_PATH)
64
+ else:
65
+ adapter_source = ADAPTER_REPO_ID
66
+
67
+ dtype = select_torch_dtype()
68
+ print(f"Loading TMOS-DD model from {adapter_source} with dtype={dtype}...")
69
+
70
+ base_model = TMOSClassifier(
71
+ base_model_id=BASE_MODEL_ID,
72
+ torch_dtype=dtype,
73
+ device_map="auto",
74
+ token=HF_TOKEN,
75
+ )
76
+ model = PeftModel.from_pretrained(base_model, adapter_source, is_trainable=False, token=HF_TOKEN)
77
+ model.eval()
78
+
79
+ processor = AutoProcessor.from_pretrained(BASE_MODEL_ID, token=HF_TOKEN)
80
+ processor.patch_size = 14
81
+ processor.vision_feature_select_strategy = "default"
82
+
83
+ inference_device = next(model.parameters()).device
84
+ print(f"TMOS-DD ready on {inference_device}.")
85
+ return model, processor, inference_device
86
+
87
+
88
+ def preprocess_image(image: Image.Image) -> Image.Image:
89
+ image = image.convert("RGB")
90
+ return ImageOps.contain(image, (TARGET_IMAGE_SIZE, TARGET_IMAGE_SIZE), method=Image.Resampling.BICUBIC)
91
+
92
+
93
+ def confidence_card(prob_fake: float, label: str) -> str:
94
+ confidence = prob_fake if label == "Fake" else 1.0 - prob_fake
95
+ confidence_pct = confidence * 100.0
96
+ fake_pct = prob_fake * 100.0
97
+ real_pct = (1.0 - prob_fake) * 100.0
98
+ accent = "#ef4444" if label == "Fake" else "#10b981"
99
+
100
+ return f"""
101
+ <div style="border:1px solid rgba(255,255,255,0.12); border-radius:16px; padding:16px; background:linear-gradient(135deg, rgba(17,24,39,0.92), rgba(15,23,42,0.96)); color:white;">
102
+ <div style="font-size:0.85rem; opacity:0.8; letter-spacing:0.04em; text-transform:uppercase; margin-bottom:8px;">Confidence</div>
103
+ <div style="display:flex; align-items:baseline; gap:10px; margin-bottom:12px;">
104
+ <div style="font-size:2rem; font-weight:700; color:{accent};">{confidence_pct:.2f}%</div>
105
+ <div style="font-size:1rem; opacity:0.9;">for <strong>{label}</strong></div>
106
+ </div>
107
+ <div style="height:12px; width:100%; background:rgba(255,255,255,0.08); border-radius:999px; overflow:hidden; margin-bottom:10px;">
108
+ <div style="height:100%; width:{fake_pct:.2f}%; background:linear-gradient(90deg, #f87171, #ef4444);"></div>
109
+ </div>
110
+ <div style="display:flex; justify-content:space-between; font-size:0.9rem; opacity:0.95;">
111
+ <span>Real: {real_pct:.2f}%</span>
112
+ <span>Fake: {fake_pct:.2f}%</span>
113
+ </div>
114
+ </div>
115
+ """
116
+
117
+
118
+ def infer_image(image: Image.Image):
119
+ if image is None:
120
+ raise gr.Error("Please upload an image before running detection.")
121
+
122
+ model_obj, processor_obj, device = load_model_and_processor()
123
+
124
+ prepared_image = preprocess_image(image)
125
+ inputs = processor_obj(text=PROMPT, images=prepared_image, return_tensors="pt", padding=True)
126
+ inputs = {name: tensor.to(device) for name, tensor in inputs.items()}
127
+
128
+ autocast_context = (
129
+ torch.autocast(device_type="cuda", dtype=select_torch_dtype())
130
+ if device.type == "cuda"
131
+ else nullcontext()
132
+ )
133
+
134
+ start_time = time.perf_counter()
135
+ with torch.inference_mode(), autocast_context:
136
+ outputs = model_obj(
137
+ input_ids=inputs["input_ids"],
138
+ pixel_values=inputs["pixel_values"],
139
+ attention_mask=inputs["attention_mask"],
140
+ )
141
+
142
+ if device.type == "cuda":
143
+ torch.cuda.synchronize()
144
+
145
+ elapsed_ms = (time.perf_counter() - start_time) * 1000.0
146
+
147
+ logit = float(outputs["logit"].squeeze().detach().float().cpu().item())
148
+ prob_fake = float(torch.sigmoid(torch.tensor(logit)).item())
149
+ label = "Fake" if prob_fake >= THRESHOLD else "Real"
150
+ confidence = prob_fake if label == "Fake" else 1.0 - prob_fake
151
+
152
+ return prepared_image, label, round(prob_fake, 6), round(confidence * 100.0, 2), round(elapsed_ms, 2), confidence_card(prob_fake, label)
153
+
154
+
155
+ load_model_and_processor()
156
+
157
+ with gr.Blocks(title="TMOS Deepfake Detector", theme=gr.themes.Soft()) as demo:
158
+ gr.Markdown(
159
+ "# TMOS Deepfake Detector\n"
160
+ "Research demo for image-based deepfake detection with a deterministic classification head on top of LLaVA-1.5-7B.\n\n"
161
+ "> Warning: runs on free infrastructure, so startup and inference may take time."
162
+ )
163
+
164
+ with gr.Row():
165
+ image_input = gr.Image(type="pil", label="Upload image")
166
+ with gr.Column():
167
+ prediction_output = gr.Textbox(label="Prediction", interactive=False)
168
+ probability_output = gr.Number(label="P(fake)", interactive=False, precision=6)
169
+ confidence_output = gr.Number(label="Confidence (%)", interactive=False, precision=2)
170
+ latency_output = gr.Number(label="Latency (ms)", interactive=False, precision=2)
171
+
172
+ preview_output = gr.Image(label="Processed image passed to the model", interactive=False)
173
+ confidence_html = gr.HTML()
174
+
175
+ detect_button = gr.Button("Run detection", variant="primary")
176
+
177
+ detect_button.click(
178
+ fn=infer_image,
179
+ inputs=image_input,
180
+ outputs=[preview_output, prediction_output, probability_output, confidence_output, latency_output, confidence_html],
181
+ )
182
+
183
+ demo.queue(default_concurrency_limit=1, max_size=8)
184
+
185
+
186
+ if __name__ == "__main__":
187
+ demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", "7860")))
requirements.txt ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core ML
2
+ numpy>=1.24.0
3
+ python-dotenv
4
+ gradio
5
+ torch>=2.0.0
6
+ torchvision>=0.15.0
7
+ torchaudio>=2.0.0
8
+ torchcodec
9
+ Pillow
10
+
11
+ # Dependencies
12
+ albumentations>=0.5.2
13
+ datasets
14
+ huggingface_hub
15
+ scikit-learn>=1.3.0
16
+ scikit-image>=0.21.0
17
+ pandas>=2.0.0
18
+ matplotlib>=3.7.0
19
+ seaborn
20
+ transformers
21
+ peft
22
+ accelerate
23
+ diffusers
24
+ opencv-python
25
+
26
+ # Optional fallback for lower-memory GPU execution
27
+ bitsandbytes
28
+
29
+ # M2TR specific
30
+ yacs==0.1.8
31
+ nbconvert
32
+ tensorboard==2.20.0
33
+ tqdm==4.67.1
34
+ PyYAML==6.0.3
35
+ simplejson==3.20.2
36
+ fvcore
scripts/tmos_classifier.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ TMOS_Classifier: Binary classification head on top of LLaVA's transformer backbone.
3
+
4
+ Strips the autoregressive lm_head and replaces it with a single nn.Linear(hidden_size, 1)
5
+ for binary deepfake detection (0 = Real, 1 = Fake).
6
+
7
+ Usage:
8
+ from tmos_classifier import TMOSClassifier, TMOS_LORA_CONFIG
9
+
10
+ classifier = TMOSClassifier(base_model_id="llava-hf/llava-1.5-7b-hf")
11
+ classifier = get_peft_model(classifier, TMOS_LORA_CONFIG)
12
+
13
+ logit = classifier(input_ids=..., pixel_values=..., attention_mask=...)
14
+ loss = nn.BCEWithLogitsLoss()(logit, label)
15
+ """
16
+
17
+ import torch
18
+ import torch.nn as nn
19
+ from transformers import LlavaForConditionalGeneration
20
+ from peft import LoraConfig
21
+
22
+
23
+ # ─── LoRA Configuration ──────────────────────────────────────────────
24
+ # Massive expansion: r=64 across ALL linear layers in the LLM backbone.
25
+ # We exclude lm_head (we discard it), fc1/fc2/out_proj (CLIP vision),
26
+ # and linear_1/linear_2 (multi-modal projector) from LoRA to keep
27
+ # the vision encoder frozen and only adapt the language transformer.
28
+
29
+ TMOS_LORA_CONFIG = LoraConfig(
30
+ r=64,
31
+ lora_alpha=128, # 2x rank as a common heuristic
32
+ target_modules=[
33
+ "q_proj", "k_proj", "v_proj", "o_proj",
34
+ "gate_proj", "up_proj", "down_proj",
35
+ ],
36
+ lora_dropout=0.1,
37
+ bias="none",
38
+ task_type=None, # Custom classifier β€” not a causal LM
39
+ modules_to_save=["classifier"], # Always train the classification head
40
+ )
41
+
42
+
43
+ class TMOSClassifier(nn.Module):
44
+ """
45
+ Binary classifier built on the LLaVA transformer backbone.
46
+
47
+ Architecture:
48
+ pixel_values ──► CLIP Vision Tower ──► Multi-Modal Projector ──┐
49
+ β”œβ”€β”€β–Ί LLaMA Transformer ──► last_hidden_state[:, -1, :] ──► classifier ──► logit
50
+ input_ids ──► Token Embedding β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
51
+
52
+ The lm_head is never used. We extract the final token's hidden state
53
+ and pass it through a learned nn.Linear(hidden_size, 1) head.
54
+ """
55
+
56
+ def __init__(self, base_model_id, torch_dtype=torch.float16, device_map="auto", token=None):
57
+ super().__init__()
58
+
59
+ # Load the full LLaVA model (we need vision tower + projector + LLM)
60
+ self.base = LlavaForConditionalGeneration.from_pretrained(
61
+ base_model_id,
62
+ torch_dtype=torch_dtype,
63
+ low_cpu_mem_usage=True,
64
+ device_map=device_map,
65
+ token=token,
66
+ )
67
+
68
+ hidden_size = self.base.config.text_config.hidden_size # 4096 for 7B
69
+
70
+ # Freeze the lm_head β€” we won't use it, but freezing prevents
71
+ # wasted gradient computation if PEFT accidentally wraps it.
72
+ for param in self.base.lm_head.parameters():
73
+ param.requires_grad = False
74
+
75
+ # Binary classification head: hidden_size β†’ 1 logit
76
+ self.classifier = nn.Linear(hidden_size, 1, dtype=torch_dtype)
77
+ nn.init.xavier_uniform_(self.classifier.weight)
78
+ nn.init.zeros_(self.classifier.bias)
79
+
80
+ def forward(
81
+ self,
82
+ input_ids=None,
83
+ pixel_values=None,
84
+ attention_mask=None,
85
+ labels=None, # float tensor of shape (B,) β€” 0.0=real, 1.0=fake
86
+ **kwargs, # absorb extra keys from data collator
87
+ ):
88
+ """
89
+ Single deterministic forward pass β†’ logit + optional BCE loss.
90
+
91
+ Returns:
92
+ dict with keys:
93
+ "logit": (B, 1) raw logit
94
+ "loss": scalar BCE loss (only if labels provided)
95
+ """
96
+ # ── 1. Forward through the LLaVA backbone ──
97
+ # We call the internal model (vision + projector + LLM) directly,
98
+ # asking for hidden states, NOT for language-model logits.
99
+ outputs = self.base.model(
100
+ input_ids=input_ids,
101
+ pixel_values=pixel_values,
102
+ attention_mask=attention_mask,
103
+ return_dict=True,
104
+ )
105
+
106
+ # last_hidden_state: (B, seq_len, hidden_size)
107
+ last_hidden_state = outputs.last_hidden_state
108
+
109
+ # ── 2. Pool: extract the final non-padded token per sequence ──
110
+ if attention_mask is not None:
111
+ # Sum of mask gives the sequence length (excluding padding)
112
+ # Index of the last real token = seq_lengths - 1
113
+ seq_lengths = attention_mask.sum(dim=1).long() - 1
114
+ # Clamp to valid range
115
+ seq_lengths = seq_lengths.clamp(min=0, max=last_hidden_state.size(1) - 1)
116
+ # Gather the hidden state at each sequence's last real token
117
+ pooled = last_hidden_state[
118
+ torch.arange(last_hidden_state.size(0), device=last_hidden_state.device),
119
+ seq_lengths,
120
+ ]
121
+ else:
122
+ # No mask β†’ just take the last position
123
+ pooled = last_hidden_state[:, -1, :]
124
+
125
+ # ── 3. Classify ──
126
+ logit = self.classifier(pooled) # (B, 1)
127
+
128
+ result = {"logit": logit}
129
+
130
+ # ── 4. Loss ──
131
+ if labels is not None:
132
+ labels = labels.to(logit.dtype).to(logit.device)
133
+ if labels.dim() == 1:
134
+ labels = labels.unsqueeze(1) # (B,) β†’ (B, 1)
135
+ loss_fn = nn.BCEWithLogitsLoss()
136
+ result["loss"] = loss_fn(logit, labels)
137
+
138
+ return result
139
+
140
+ def prepare_inputs_for_generation(self, *args, **kwargs):
141
+ """Stub required by PEFT β€” we never generate text."""
142
+ raise NotImplementedError("TMOSClassifier does not support generation.")
143
+
144
+ def gradient_checkpointing_enable(self, **kwargs):
145
+ """Delegate to the base model for HF Trainer compatibility."""
146
+ self.base.model.gradient_checkpointing_enable(**kwargs)
147
+
148
+ @property
149
+ def config(self):
150
+ """Expose the base model config for PEFT."""
151
+ return self.base.config
152
+
153
+ @property
154
+ def device(self):
155
+ return next(self.parameters()).device
156
+
157
+ @property
158
+ def dtype(self):
159
+ return next(self.parameters()).dtype
160
+
161
+
162
+ # ─── Standalone Test ──────────────────────────────────────────────────
163
+ if __name__ == "__main__":
164
+ import os
165
+ from dotenv import load_dotenv
166
+ load_dotenv()
167
+ HF_TOKEN = os.getenv("HF_TOKEN")
168
+
169
+ print("Testing TMOSClassifier...")
170
+ device = "cuda" if torch.cuda.is_available() else "cpu"
171
+
172
+ clf = TMOSClassifier(
173
+ base_model_id="llava-hf/llava-1.5-7b-hf",
174
+ torch_dtype=torch.float16,
175
+ token=HF_TOKEN,
176
+ )
177
+ clf.to(device)
178
+
179
+ # Print parameter counts
180
+ total = sum(p.numel() for p in clf.parameters())
181
+ trainable = sum(p.numel() for p in clf.parameters() if p.requires_grad)
182
+ print(f"Total params: {total:>12,}")
183
+ print(f"Trainable params: {trainable:>12,}")
184
+ print(f"Classifier head: {sum(p.numel() for p in clf.classifier.parameters()):,}")
185
+
186
+ # Smoke test with dummy input
187
+ from transformers import AutoProcessor
188
+ processor = AutoProcessor.from_pretrained("llava-hf/llava-1.5-7b-hf", token=HF_TOKEN)
189
+ processor.patch_size = 14
190
+ processor.vision_feature_select_strategy = "default"
191
+
192
+ from PIL import Image
193
+ dummy_img = Image.new("RGB", (336, 336), color=(128, 128, 128))
194
+ inputs = processor(
195
+ text="USER: <image>\nIs this real?\nASSISTANT:",
196
+ images=dummy_img,
197
+ return_tensors="pt",
198
+ ).to(device)
199
+
200
+ labels = torch.tensor([1.0], device=device) # fake
201
+
202
+ with torch.no_grad():
203
+ out = clf(**inputs, labels=labels)
204
+
205
+ print(f"Logit: {out['logit'].item():.4f}")
206
+ print(f"Loss: {out['loss'].item():.4f}")
207
+ print(f"Prob: {torch.sigmoid(out['logit']).item():.4f}")
208
+ print("Test passed.")