File size: 9,089 Bytes
6396e6b
c8f4e3f
df1b2ad
c8f4e3f
8d28be7
6396e6b
df1b2ad
 
 
 
 
c8f4e3f
 
 
 
 
 
 
6396e6b
c8f4e3f
 
 
 
 
6396e6b
 
c8f4e3f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6396e6b
c8f4e3f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6396e6b
df1b2ad
c8f4e3f
 
 
6396e6b
c8f4e3f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6396e6b
 
8d28be7
6396e6b
c8f4e3f
6396e6b
 
 
 
 
 
c8f4e3f
6396e6b
 
c8f4e3f
 
 
 
 
 
 
 
 
df1b2ad
c8f4e3f
df1b2ad
c8f4e3f
 
 
 
df1b2ad
 
 
c8f4e3f
 
 
 
df1b2ad
 
 
 
 
c8f4e3f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
df1b2ad
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c8f4e3f
6396e6b
4d6298c
 
c8f4e3f
 
6396e6b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
from pathlib import Path
from typing import Any
import shutil
from .model import FFTCNN # Import the FFT CNN architecture (package-relative)
from config import Config

try:
    from huggingface_hub import hf_hub_download
except Exception:
    hf_hub_download = None


# NOTE: EfficientNet/nn imports are done lazily when torch is available.
ELAForgeryNet = None  # will be constructed dynamically when needed
torch = None
TORCH_AVAILABLE = False


class ModelLoader:
    """A class to load and hold PyTorch models used by this feature.

    It loads:
      - an FFT-based CNN (downloaded from Hugging Face Hub)
      - an ELA-based document forgery detector (local .pth by default)
    """

    def __init__(self, model_repo_id: str, model_filename: str, doc_model_path: str = None):
        # Try to import torch once and expose module-level variables
        global torch, TORCH_AVAILABLE
        try:
            import torch as _torch
            torch = _torch
            TORCH_AVAILABLE = True
        except Exception:
            torch = None
            TORCH_AVAILABLE = False
            print("[WARN] PyTorch not available; model loading will be skipped until torch is installed.")
        if TORCH_AVAILABLE:
            self.device = "cuda" if torch.cuda.is_available() else "cpu"
        else:
            self.device = "cpu"
        print(f"Using device: {self.device} (torch available: {TORCH_AVAILABLE})")

        # Load FFT CNN from HF Hub
        self.fft_model = None
        if TORCH_AVAILABLE:
            try:
                self.fft_model = self._load_fft_model(repo_id=model_repo_id, filename=model_filename)
                print("FFT CNN model loaded successfully from Hub.")
            except Exception:
                # Try local fallback path (if provided in config)
                self.fft_model = None
                local_path = Path(getattr(Config, 'REAL_FORGED_MODEL_LOCAL_PATH', ''))
                if local_path and local_path.exists():
                    try:
                        print(f"Attempting to load FFT model from local path: {local_path}")
                        model = FFTCNN()
                        state = torch.load(str(local_path), map_location=torch.device(self.device))
                        state_dict = state.get('state_dict', state) if isinstance(state, dict) else state
                        model.load_state_dict(state_dict, strict=False)
                        model.to(self.device)
                        model.eval()
                        self.fft_model = model
                        print("FFT CNN model loaded successfully from local path.")
                    except Exception as e:
                        print(f"Failed to load local FFT model: {e}")
                else:
                    print("No local FFT model path configured or file missing; FFT model not loaded.")
        else:
            print("Skipping FFT model load because PyTorch is not installed.")

        # Load document forgery model (ELA CNN), downloading the checkpoint if needed.
        self.doc_model = None
        if doc_model_path is None:
            doc_model_path = Config.DOCUMENT_FORGERY_MODEL_PATH

        self.doc_model = None
        if TORCH_AVAILABLE:
            try:
                self.doc_model = self._load_document_forgery_model(Path(doc_model_path))
                if self.doc_model is not None:
                    print("Document forgery (ELA) model loaded successfully.")
            except Exception as e:
                print(f"Warning: failed to load document forgery model: {e}")
        else:
            print("Skipping document forgery model load because PyTorch is not installed.")

    def _load_fft_model(self, repo_id: str, filename: str):
        """Downloads and loads the FFT CNN model from a Hugging Face Hub repository."""
        print(f"Attempting to download FFT CNN model from Hugging Face repo: {repo_id}")
        try:
            from huggingface_hub import hf_hub_download
        except Exception as e:
            raise RuntimeError(f"huggingface_hub not available: {e}")

        try:
            model_path = hf_hub_download(repo_id=repo_id, filename=filename, token=Config.HF_TOKEN)
            print(f"Model downloaded to: {model_path}")

            model = FFTCNN()
            model.load_state_dict(torch.load(model_path, map_location=torch.device(self.device)))
            model.to(self.device)
            model.eval()
            return model
        except Exception as e:
            print(f"Error downloading or loading FFT model from Hugging Face: {e}")
            raise

    def _load_document_forgery_model(self, path: Path):
        """Load the ELA-based document forgery model from a local .pth checkpoint.

        Returns the model instance or None if the file does not exist.
        """
        # If the configured path doesn't exist, try sensible fallbacks in the repo.
        if not path.exists():
            print(f"Document forgery model file not found at configured path: {path}")

            # 1) Try the configured document forgery checkpoint path relative to repo root
            repo_root = Path(__file__).resolve().parents[2]
            candidate = repo_root / 'features' / 'Model' / 'document_forgery' / path.name
            if candidate.exists():
                path = candidate
                print(f"Found document forgery model at fallback path: {path}")
            else:
                # 2) Search the repo for any file with the configured checkpoint name
                print(f"Searching repository for '{path.name}'...")
                matches = list(repo_root.rglob(path.name))
                if matches:
                    path = matches[0]
                    print(f"Found document forgery model at: {path}")
                else:
                    try:
                        path = self._download_document_forgery_model(path)
                    except Exception as exc:
                        print(f"Document forgery model not found in repository and download failed: {exc}")
                        return None

        print(f"Loading document forgery model from: {path}")
        # Build the ELA model architecture lazily (requires torchvision & torch.nn)
        try:
            import torchvision.models as tv_models
            import torch.nn as nn
        except Exception as e:
            raise RuntimeError(f"Required packages for ELA model not available: {e}")

        backbone = tv_models.efficientnet_b0(weights='IMAGENET1K_V1')
        in_features = backbone.classifier[1].in_features
        backbone.classifier = nn.Sequential(
            nn.Dropout(p=0.4),
            nn.Linear(in_features, 256),
            nn.ReLU(inplace=True),
            nn.Dropout(p=0.2),
            nn.Linear(256, 2),
        )
        model = backbone
        state = torch.load(str(path), map_location=torch.device(self.device))

        # The checkpoint might be either a state_dict or a full checkpoint dict
        if isinstance(state, dict) and 'state_dict' in state:
            state_dict = state['state_dict']
        else:
            state_dict = state

        # Attempt to load state dict; allow strict=False to be tolerant to minor key name differences
        model.load_state_dict(state_dict, strict=False)
        model.to(self.device)
        model.eval()
        return model

    def _download_document_forgery_model(self, target_path: Path) -> Path:
        """Download the document forgery checkpoint into the configured local path."""
        if hf_hub_download is None:
            raise RuntimeError("huggingface_hub not available")

        repo_id = getattr(Config, "DOCUMENT_FORGERY_MODEL_REPO_ID", Config.REAL_FORGED_MODEL_REPO_ID)
        configured_name = getattr(Config, "DOCUMENT_FORGERY_MODEL_FILENAME", str(target_path))
        candidate_filenames = []
        for candidate in (configured_name, str(target_path), target_path.name):
            if candidate and candidate not in candidate_filenames:
                candidate_filenames.append(candidate)

        last_error = None
        for filename in candidate_filenames:
            try:
                print(f"Downloading document forgery model from Hugging Face repo: {repo_id} ({filename})")
                downloaded_path = hf_hub_download(repo_id=repo_id, filename=filename, token=Config.HF_TOKEN)
                target_path.parent.mkdir(parents=True, exist_ok=True)
                shutil.copy2(downloaded_path, target_path)
                print(f"Document forgery model downloaded to: {target_path}")
                return target_path
            except Exception as exc:
                last_error = exc

        raise RuntimeError(f"unable to download document forgery model: {last_error}")


# --- Global Model Instance ---
MODEL_REPO_ID = Config.REAL_FORGED_MODEL_REPO_ID
MODEL_FILENAME = Config.REAL_FORGED_MODEL_FILENAME
DOC_MODEL_PATH = Config.DOCUMENT_FORGERY_MODEL_PATH
models = ModelLoader(model_repo_id=MODEL_REPO_ID, model_filename=MODEL_FILENAME, doc_model_path=DOC_MODEL_PATH)