abhisheksan commited on
Commit
6064a78
·
1 Parent(s): e516e1a
.gitignore ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Created by https://www.toptal.com/developers/gitignore/api/python
2
+ # Edit at https://www.toptal.com/developers/gitignore?templates=python
3
+
4
+ ### Python ###
5
+ # Byte-compiled / optimized / DLL files
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+
10
+ # C extensions
11
+ *.so
12
+
13
+ # Distribution / packaging
14
+ .Python
15
+ build/
16
+ develop-eggs/
17
+ dist/
18
+ downloads/
19
+ eggs/
20
+ .eggs/
21
+ lib/
22
+ lib64/
23
+ parts/
24
+ sdist/
25
+ var/
26
+ wheels/
27
+ share/python-wheels/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+ MANIFEST
32
+
33
+ # PyInstaller
34
+ # Usually these files are written by a python script from a template
35
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
36
+ *.manifest
37
+ *.spec
38
+
39
+ # Installer logs
40
+ pip-log.txt
41
+ pip-delete-this-directory.txt
42
+
43
+ # Unit test / coverage reports
44
+ htmlcov/
45
+ .tox/
46
+ .nox/
47
+ .coverage
48
+ .coverage.*
49
+ .cache
50
+ nosetests.xml
51
+ coverage.xml
52
+ *.cover
53
+ *.py,cover
54
+ .hypothesis/
55
+ .pytest_cache/
56
+ cover/
57
+
58
+ # Translations
59
+ *.mo
60
+ *.pot
61
+
62
+ # Django stuff:
63
+ *.log
64
+ local_settings.py
65
+ db.sqlite3
66
+ db.sqlite3-journal
67
+
68
+ # Flask stuff:
69
+ instance/
70
+ .webassets-cache
71
+
72
+ # Scrapy stuff:
73
+ .scrapy
74
+
75
+ # Sphinx documentation
76
+ docs/_build/
77
+
78
+ # PyBuilder
79
+ .pybuilder/
80
+ target/
81
+
82
+ # Jupyter Notebook
83
+ .ipynb_checkpoints
84
+
85
+ # IPython
86
+ profile_default/
87
+ ipython_config.py
88
+
89
+ # pyenv
90
+ # For a library or package, you might want to ignore these files since the code is
91
+ # intended to run in multiple environments; otherwise, check them in:
92
+ # .python-version
93
+
94
+ # pipenv
95
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
97
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
98
+ # install all needed dependencies.
99
+ #Pipfile.lock
100
+
101
+ # poetry
102
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
103
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
104
+ # commonly ignored for libraries.
105
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
106
+ #poetry.lock
107
+
108
+ # pdm
109
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
110
+ #pdm.lock
111
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
112
+ # in version control.
113
+ # https://pdm.fming.dev/#use-with-ide
114
+ .pdm.toml
115
+
116
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
117
+ __pypackages__/
118
+
119
+ # Celery stuff
120
+ celerybeat-schedule
121
+ celerybeat.pid
122
+
123
+ # SageMath parsed files
124
+ *.sage.py
125
+
126
+ # Environments
127
+ .env
128
+ .venv
129
+ env/
130
+ venv/
131
+ ENV/
132
+ env.bak/
133
+ venv.bak/
134
+
135
+ # Spyder project settings
136
+ .spyderproject
137
+ .spyproject
138
+
139
+ # Rope project settings
140
+ .ropeproject
141
+
142
+ # mkdocs documentation
143
+ /site
144
+
145
+ # mypy
146
+ .mypy_cache/
147
+ .dmypy.json
148
+ dmypy.json
149
+
150
+ # Pyre type checker
151
+ .pyre/
152
+
153
+ # pytype static type analyzer
154
+ .pytype/
155
+
156
+ # Cython debug symbols
157
+ cython_debug/
158
+
159
+ # PyCharm
160
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
161
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
162
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
163
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
164
+ #.idea/
165
+
166
+ ### Python Patch ###
167
+ # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
168
+ poetry.toml
169
+
170
+ # ruff
171
+ .ruff_cache/
172
+
173
+ # LSP config files
174
+ pyrightconfig.json
175
+
176
+ # End of https://www.toptal.com/developers/gitignore/api/python
Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10.9
2
+
3
+ WORKDIR /code
4
+
5
+ COPY ./requirements.txt /code/requirements.txt
6
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
7
+
8
+ COPY . /code/
9
+
10
+ # Expose the secret FIREBASE_KEY_BASE64 at buildtime and save it as a file
11
+ RUN --mount=type=secret,id=FIREBASE_KEY_BASE64,mode=0444,required=true \
12
+ cat /run/secrets/FIREBASE_KEY_BASE64 | base64 -d > /code/firebase_key.json
13
+
14
+ # Set the Google Application Credentials environment variable
15
+ ENV GOOGLE_APPLICATION_CREDENTIALS="/code/firebase_key.json"
16
+
17
+ # The FIREBASE_STORAGE_BUCKET will be available at runtime as an environment variable
18
+
19
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
README copy.md ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use python 3.10 for this project as the audio extraction library can work with this version only
2
+
3
+ create a virtual environment : .\venv\Scripts\activate
4
+
5
+ install the required libraries using : pip install -r requirements.txt
6
+
7
+ run the app using : uvicorn app.main:app --reload
8
+
9
+ The issue you are facing regarding while re uploading the same image or audio the path error shows up is die to mssing ffmpeg installation on your device the solution for the same :
10
+ - open cmd and type : winget install ffmpeg
11
+ - after the installation has been sucessfully done add the bin path to the System environment variables
12
+ - if u fail to find the path just type : where ffmpeg , in cmd and you will get the path
app/api/__init__.py ADDED
File without changes
app/api/routes.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, Response
2
+ from pydantic import BaseModel
3
+ from app.services import video_service, image_service, antispoof_service
4
+ from app.services.antispoof_service import antispoof_service
5
+ from app.services.image_service import compare_images
6
+ import logging
7
+
8
+ router = APIRouter()
9
+
10
+ class ContentRequest(BaseModel):
11
+ url: str
12
+
13
+ class CompareRequest(BaseModel):
14
+ url1: str
15
+ url2: str
16
+
17
+ @router.get("/health")
18
+ @router.head("/health")
19
+ async def health_check():
20
+ """
21
+ Health check endpoint that responds to both GET and HEAD requests.
22
+ """
23
+ return Response(content="OK", media_type="text/plain")
24
+
25
+ @router.post("/fingerprint")
26
+ async def create_fingerprint(request: ContentRequest):
27
+ try:
28
+ result = await video_service.fingerprint_video(request.url)
29
+ return {"message": "Fingerprint processing completed", "result": result}
30
+ except Exception as e:
31
+ logging.error(f"Error in fingerprint processing: {str(e)}")
32
+ raise HTTPException(status_code=500, detail=f"Error in fingerprint processing: {str(e)}")
33
+
34
+ @router.post("/verify_video_only")
35
+ async def verify_video_only(request: ContentRequest):
36
+ try:
37
+ result = await video_service.fingerprint_video(request.url)
38
+ return {"message": "Video verification completed", "result": result}
39
+ except Exception as e:
40
+ logging.error(f"Error in video verification: {str(e)}")
41
+ raise HTTPException(status_code=500, detail=f"Error in video verification: {str(e)}")
42
+
43
+ @router.post("/verify_liveness")
44
+ async def verify_liveness(request: ContentRequest):
45
+ try:
46
+ result = await antispoof_service.verify_liveness(request.url)
47
+ return {"message": "Liveness verification completed", "result": result}
48
+ except Exception as e:
49
+ logging.error(f"Error in liveness verification: {str(e)}")
50
+ raise HTTPException(status_code=500, detail=f"Error in liveness verification: {str(e)}")
51
+
52
+ @router.post("/compare_videos")
53
+ async def compare_videos_route(request: CompareRequest):
54
+ try:
55
+ result = await video_service.compare_videos(request.url1, request.url2)
56
+ return {"message": "Video comparison completed", "result": result}
57
+ except Exception as e:
58
+ logging.error(f"Error in video comparison: {str(e)}")
59
+ raise HTTPException(status_code=500, detail=f"Error in video comparison: {str(e)}")
60
+
61
+ @router.post("/verify_image")
62
+ async def verify_image_route(request: ContentRequest):
63
+ try:
64
+ result = await image_service.verify_image(request.url)
65
+ return {"message": "Image verification completed", "result": result}
66
+ except Exception as e:
67
+ logging.error(f"Error in image verification: {str(e)}")
68
+ raise HTTPException(status_code=500, detail=f"Error in image verification: {str(e)}")
69
+
70
+ @router.post("/compare_images")
71
+ async def compare_images_route(request: CompareRequest):
72
+ try:
73
+ # Call the image comparison service with the URLs from the request body
74
+ result = await compare_images(request.url1, request.url2)
75
+ return {"message": "Image comparison completed", "result": result}
76
+ except Exception as e:
77
+ logging.error(f"Error in image comparison: {str(e)}")
78
+ raise HTTPException(status_code=500, detail=f"Error in image comparison: {str(e)}")
app/core/__init__.py ADDED
File without changes
app/core/config.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from pathlib import Path
3
+
4
+ # Base directory of the project
5
+ BASE_DIR = Path(__file__).resolve().parent.parent.parent
6
+
7
+
8
+ # Anti-spoofing model and config paths
9
+ MODEL_PATH = os.path.join(BASE_DIR, "models", "mobilenetv2_spoof_model.h5")
10
+ CONFIG_PATH = os.path.join(BASE_DIR, "models", "model_config.json")
11
+
app/core/firebase_config.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import base64
3
+ import json
4
+ from firebase_admin import credentials, initialize_app, storage
5
+ from dotenv import load_dotenv
6
+
7
+ load_dotenv()
8
+
9
+ _firebase_app = None
10
+ _firebase_bucket = None
11
+
12
+ def initialize_firebase():
13
+ global _firebase_app, _firebase_bucket
14
+
15
+ if _firebase_app is not None:
16
+ return _firebase_bucket
17
+
18
+ firebase_key_base64 = os.getenv('FIREBASE_KEY_BASE64')
19
+ if not firebase_key_base64:
20
+ raise ValueError("Environment variable FIREBASE_KEY_BASE64 is not set or is empty")
21
+
22
+ try:
23
+ print("Decoding Firebase key...")
24
+ firebase_key_json = base64.b64decode(firebase_key_base64).decode('utf-8')
25
+ firebase_key_dict = json.loads(firebase_key_json)
26
+
27
+ print("Initializing Firebase...")
28
+ cred = credentials.Certificate(firebase_key_dict)
29
+
30
+ # Initialize Firebase with a unique name if needed
31
+ _firebase_app = initialize_app(cred, {
32
+ 'storageBucket': os.getenv('FIREBASE_STORAGE_BUCKET')
33
+ })
34
+
35
+ _firebase_bucket = storage.bucket(app=_firebase_app)
36
+ print("Firebase initialized successfully.")
37
+ return _firebase_bucket
38
+ except Exception as e:
39
+ raise RuntimeError(f"Error initializing Firebase: {e}")
40
+
41
+ firebase_bucket = initialize_firebase()
app/core/logging_config.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import logging
2
+
3
+ def configure_logging():
4
+ logging.basicConfig(level=logging.INFO,
5
+ format='%(asctime)s - %(levelname)s - %(message)s')
app/main.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from fastapi import FastAPI, Request
3
+ from fastapi.responses import JSONResponse
4
+ from app.api.routes import router
5
+ from app.core.logging_config import configure_logging
6
+ from app.core.firebase_config import initialize_firebase
7
+ import logging
8
+
9
+ app = FastAPI()
10
+
11
+ @app.on_event("startup")
12
+ async def startup_event():
13
+ configure_logging()
14
+ initialize_firebase()
15
+
16
+ app.include_router(router)
17
+
18
+ @app.exception_handler(Exception)
19
+ async def global_exception_handler(request: Request, exc: Exception):
20
+ logging.error(f"Unhandled exception: {str(exc)}", exc_info=True)
21
+ return JSONResponse(
22
+ status_code=500,
23
+ content={"message": "An unexpected error occurred. Please try again later."}
24
+ )
25
+
26
+ if __name__ == "__main__":
27
+ import uvicorn
28
+ uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info")
app/services/__init__.py ADDED
File without changes
app/services/antispoof_service.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from app.core.config import MODEL_PATH, CONFIG_PATH
3
+ from app.utils.file_utils import download_file, remove_temp_file
4
+ from fastapi import HTTPException
5
+ from app.core.firebase_config import firebase_bucket
6
+ from io import BytesIO
7
+ from PIL import Image
8
+ import tensorflow as tf
9
+ import json
10
+ import numpy as np
11
+
12
+ class AntispoofService:
13
+ def __init__(self):
14
+ self.model = None
15
+ self.config = None
16
+ self.load_model_and_config()
17
+
18
+ def load_model_and_config(self):
19
+ try:
20
+ self.model = tf.keras.models.load_model(MODEL_PATH)
21
+ with open(CONFIG_PATH, "r") as config_file:
22
+ self.config = json.load(config_file)
23
+ logging.info("Model and configuration loaded successfully.")
24
+ except Exception as e:
25
+ logging.error(f"Error loading model or config: {str(e)}")
26
+ raise
27
+
28
+ async def verify_liveness(self, image_url: str):
29
+ if self.model is None or self.config is None:
30
+ raise ValueError("Model or configuration not loaded")
31
+
32
+ firebase_filename = None
33
+ try:
34
+ firebase_filename = await download_file(image_url)
35
+
36
+ blob = firebase_bucket.blob(firebase_filename)
37
+ image_bytes = blob.download_as_bytes()
38
+
39
+ image = Image.open(BytesIO(image_bytes))
40
+ image = image.resize((self.config["img_width"], self.config["img_height"]))
41
+ image_array = np.array(image) / 255.0
42
+ image_array = np.expand_dims(image_array, axis=0)
43
+
44
+ prediction = self.model.predict(image_array)[0][0]
45
+
46
+ is_real = bool(prediction <= self.config["threshold"])
47
+ result = "Real" if is_real else "Spoof"
48
+
49
+ return {
50
+ "image_url": image_url,
51
+ "prediction_score": float(prediction),
52
+ "is_real": is_real,
53
+ "result": result
54
+ }
55
+ except Exception as e:
56
+ logging.error(f"Error processing image: {str(e)}")
57
+ raise HTTPException(status_code=500, detail=f"Error processing image: {str(e)}")
58
+ finally:
59
+ if firebase_filename:
60
+ await remove_temp_file(firebase_filename)
61
+
62
+ antispoof_service = AntispoofService()
app/services/audio_service.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import numpy as np
3
+ import librosa
4
+ import imagehash
5
+ from PIL import Image
6
+ import logging
7
+ import io
8
+ from pydub import AudioSegment
9
+
10
+ import ffmpeg
11
+ import numpy as np
12
+ logging.basicConfig(level=logging.DEBUG)
13
+ logger = logging.getLogger(__name__)
14
+ def extract_audio_features(video_bytes):
15
+ logger.info("Extracting audio features")
16
+ try:
17
+ # Attempt to extract audio using pydub
18
+ try:
19
+ logger.info("Attempting to extract audio using pydub")
20
+ audio = AudioSegment.from_file(io.BytesIO(video_bytes), format="mp4")
21
+ audio = audio.set_channels(1).set_frame_rate(44100)
22
+ samples = audio.get_array_of_samples()
23
+ audio_array = np.array(samples).astype(np.float32) / 32768.0
24
+ except Exception as pydub_error:
25
+ logger.warning(f"Pydub extraction failed: {str(pydub_error)}. Attempting ffmpeg extraction.")
26
+
27
+ # Fallback to ffmpeg if pydub fails
28
+ try:
29
+ out, _ = (
30
+ ffmpeg
31
+ .input('pipe:0')
32
+ .output('pipe:1', format='f32le', acodec='pcm_f32le', ac=1, ar='44100')
33
+ .run(input=video_bytes, capture_stdout=True, capture_stderr=True)
34
+ )
35
+ audio_array = np.frombuffer(out, np.float32)
36
+ except ffmpeg.Error as ffmpeg_error:
37
+ logger.error(f"FFmpeg extraction failed: {ffmpeg_error.stderr.decode()}")
38
+ raise
39
+
40
+ # Extract MFCC features
41
+ mfcc = librosa.feature.mfcc(y=audio_array, sr=44100, n_mfcc=13)
42
+
43
+ logger.info("Audio features extracted successfully")
44
+ return mfcc
45
+
46
+ except Exception as e:
47
+ logger.error(f"Error extracting audio features: {str(e)}", exc_info=True)
48
+ raise
49
+
50
+ def compute_audio_hash(features):
51
+ logging.info("Computing audio hash.")
52
+ if features is None:
53
+ return None
54
+ # Ensure the features are properly shaped for hashing
55
+ features_2d = features.reshape(features.shape[0], -1)
56
+ features_2d = (features_2d - np.min(features_2d)) / (np.max(features_2d) - np.min(features_2d))
57
+ features_2d = (features_2d * 255).astype(np.uint8)
58
+ return imagehash.phash(Image.fromarray(features_2d))
59
+
60
+ def compute_audio_hashes(video_bytes):
61
+ logger.info("Computing audio hashes")
62
+ try:
63
+ out, _ = (
64
+ ffmpeg
65
+ .input('pipe:0')
66
+ .output('pipe:1', format='f32le', acodec='pcm_f32le', ac=1, ar='44100')
67
+ .run(input=video_bytes, capture_stdout=True, capture_stderr=True)
68
+ )
69
+
70
+ audio_array = np.frombuffer(out, np.float32)
71
+ mfccs = librosa.feature.mfcc(y=audio_array, sr=44100, n_mfcc=13)
72
+
73
+ audio_hashes = []
74
+ for mfcc in mfccs.T:
75
+ audio_hash = imagehash.average_hash(Image.fromarray(mfcc.reshape(13, 1)))
76
+ audio_hashes.append(str(audio_hash))
77
+
78
+ logger.info("Finished computing audio hashes.")
79
+ return audio_hashes
80
+ except ffmpeg.Error as e:
81
+ logger.error(f"FFmpeg error in compute_audio_hashes: {e.stderr.decode()}")
82
+ raise
83
+ except Exception as e:
84
+ logger.error(f"Error in compute_audio_hashes: {str(e)}", exc_info=True)
85
+ raise
app/services/image_service.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.utils.image_utils import verify_image_format, process_image, compare_images as compare_images_util
2
+ from fastapi import HTTPException
3
+ import logging
4
+ from app.utils.file_utils import download_file, remove_temp_file
5
+
6
+ async def verify_image(image_url: str):
7
+ firebase_filename = None
8
+ try:
9
+ firebase_filename = await download_file(image_url)
10
+ verify_image_format(firebase_filename)
11
+
12
+ image_hash = process_image(firebase_filename)
13
+ return {"image_hash": image_hash}
14
+ except Exception as e:
15
+ logging.error(f"Error verifying image: {str(e)}", exc_info=True)
16
+ raise HTTPException(status_code=500, detail=f"Error verifying image: {str(e)}")
17
+ finally:
18
+ if firebase_filename:
19
+ await remove_temp_file(firebase_filename)
20
+
21
+ async def compare_images(image_url1: str, image_url2: str):
22
+ firebase_filename1 = None
23
+ firebase_filename2 = None
24
+ try:
25
+ # Download the files from their URLs and store them in Firebase
26
+ firebase_filename1 = await download_file(image_url1)
27
+ firebase_filename2 = await download_file(image_url2)
28
+
29
+ # Verify the image format for both images
30
+ verify_image_format(firebase_filename1)
31
+ verify_image_format(firebase_filename2)
32
+
33
+ # Compare the images using the utility function
34
+ comparison_result = compare_images_util(firebase_filename1, firebase_filename2)
35
+
36
+ # Return the comparison result
37
+ return comparison_result
38
+ except Exception as e:
39
+ logging.error(f"Error comparing images: {str(e)}", exc_info=True)
40
+ raise HTTPException(status_code=500, detail=f"Error comparing images: {str(e)}")
41
+ finally:
42
+ # Remove temporary files from Firebase after processing
43
+ if firebase_filename1:
44
+ await remove_temp_file(firebase_filename1)
45
+ if firebase_filename2:
46
+ await remove_temp_file(firebase_filename2)
app/services/video_service.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import ffmpeg
3
+ import numpy as np
4
+ from scipy.fftpack import dct
5
+ import imagehash
6
+ from PIL import Image
7
+ import logging
8
+ import logging
9
+ from app.utils.hash_utils import compute_video_hash, compute_frame_hashes
10
+ from app.services.audio_service import extract_audio_features, compute_audio_hash, compute_audio_hashes
11
+ from app.utils.file_utils import download_file, remove_temp_file, get_file_stream
12
+ import io
13
+
14
+ import tempfile
15
+ import os
16
+ logging.basicConfig(level=logging.DEBUG)
17
+ logger = logging.getLogger(__name__)
18
+ def validate_video_bytes(video_bytes):
19
+ try:
20
+ # If video_bytes is already a BytesIO object, use it directly
21
+ # Otherwise, create a new BytesIO object from the bytes
22
+ if not isinstance(video_bytes, io.BytesIO):
23
+ video_bytes = io.BytesIO(video_bytes)
24
+
25
+ # Reset the BytesIO object to the beginning
26
+ video_bytes.seek(0)
27
+
28
+ # Create a temporary file to store the video data
29
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as temp_file:
30
+ temp_file.write(video_bytes.read())
31
+ temp_file_path = temp_file.name
32
+
33
+ # Use ffprobe to get video information
34
+ probe = ffmpeg.probe(temp_file_path)
35
+
36
+ # Clean up the temporary file
37
+ os.unlink(temp_file_path)
38
+
39
+ # Check for audio stream
40
+ audio_stream = next((stream for stream in probe['streams'] if stream['codec_type'] == 'audio'), None)
41
+
42
+ if audio_stream is None:
43
+ logger.warning("No audio stream found in the file")
44
+ return False
45
+ return True
46
+ except ffmpeg.Error as e:
47
+ logger.error(f"Error validating video bytes: {e.stderr.decode()}")
48
+ return False
49
+ except Exception as e:
50
+ logger.error(f"Unexpected error in validate_video_bytes: {str(e)}")
51
+ return False
52
+
53
+ async def extract_video_features(firebase_filename):
54
+ logging.info("Extracting video features")
55
+ video_stream = get_file_stream(firebase_filename)
56
+ video_bytes = video_stream.getvalue()
57
+
58
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as temp_file:
59
+ temp_file.write(video_bytes)
60
+ temp_file_path = temp_file.name
61
+
62
+ cap = cv2.VideoCapture(temp_file_path)
63
+
64
+ features = []
65
+ while True:
66
+ ret, frame = cap.read()
67
+ if not ret:
68
+ break
69
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
70
+ resized = cv2.resize(gray, (32, 32))
71
+ dct_frame = dct(dct(resized.T, norm='ortho').T, norm='ortho')
72
+ features.append(dct_frame[:8, :8].flatten())
73
+
74
+ cap.release()
75
+ os.unlink(temp_file_path)
76
+
77
+ logging.info("Finished extracting video features.")
78
+ return np.array(features), video_bytes
79
+
80
+ async def fingerprint_video(video_url):
81
+ logging.info(f"Fingerprinting video: {video_url}")
82
+ firebase_filename = None
83
+ try:
84
+ firebase_filename = await download_file(video_url)
85
+ video_stream = get_file_stream(firebase_filename)
86
+ video_bytes = video_stream.getvalue()
87
+
88
+ video_features, _ = await extract_video_features(firebase_filename)
89
+
90
+ if validate_video_bytes(io.BytesIO(video_bytes)):
91
+ audio_features = extract_audio_features(video_bytes)
92
+ audio_hashes = compute_audio_hashes(video_bytes)
93
+ collective_audio_hash = compute_audio_hash(audio_features)
94
+ else:
95
+ logging.warning("No audio stream found or invalid video. Skipping audio feature extraction.")
96
+ audio_hashes = []
97
+ collective_audio_hash = None
98
+
99
+ video_hash = compute_video_hash(video_features)
100
+ frame_hashes = compute_frame_hashes(firebase_filename)
101
+
102
+ logging.info("Finished fingerprinting video.")
103
+
104
+ return {
105
+ 'frame_hashes': frame_hashes,
106
+ 'audio_hashes': audio_hashes,
107
+ 'robust_audio_hash': str(collective_audio_hash) if collective_audio_hash else None,
108
+ 'robust_video_hash': str(video_hash),
109
+ }
110
+ finally:
111
+ if firebase_filename:
112
+ await remove_temp_file(firebase_filename)
113
+
114
+ async def compare_videos(video_url1, video_url2):
115
+ fp1 = await fingerprint_video(video_url1)
116
+ fp2 = await fingerprint_video(video_url2)
117
+
118
+ video_similarity = 1 - (imagehash.hex_to_hash(fp1['robust_video_hash']) - imagehash.hex_to_hash(fp2['robust_video_hash'])) / 64.0
119
+ audio_similarity = 1 - (imagehash.hex_to_hash(fp1['robust_audio_hash']) - imagehash.hex_to_hash(fp2['robust_audio_hash'])) / 64.0
120
+
121
+ overall_similarity = (video_similarity + audio_similarity) / 2
122
+ is_same_content = overall_similarity > 0.9 # You can adjust this threshold
123
+
124
+ logging.info(f"Comparison result - Video Similarity: {video_similarity}, Audio Similarity: {audio_similarity}, Overall Similarity: {overall_similarity}, Is Same Content: {is_same_content}")
125
+
126
+ return {
127
+ "video_similarity": video_similarity,
128
+ "audio_similarity": audio_similarity,
129
+ "overall_similarity": overall_similarity,
130
+ "is_same_content": is_same_content
131
+ }
app/utils/__init__.py ADDED
File without changes
app/utils/file_utils.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import aiohttp
2
+ import uuid
3
+ import logging
4
+ from urllib.parse import urlparse
5
+ from app.core.firebase_config import firebase_bucket
6
+ import io
7
+
8
+ async def download_file(url: str) -> str:
9
+ parsed_url = urlparse(url)
10
+ file_extension = parsed_url.path.split('.')[-1]
11
+
12
+ if not file_extension:
13
+ file_extension = 'tmp'
14
+
15
+ filename = f"{uuid.uuid4()}.{file_extension}"
16
+
17
+ try:
18
+ async with aiohttp.ClientSession() as session:
19
+ async with session.get(url) as response:
20
+ if response.status != 200:
21
+ raise Exception(f"Failed to download file: HTTP {response.status}")
22
+
23
+ content = await response.read()
24
+ blob = firebase_bucket.blob(filename)
25
+ blob.upload_from_string(content, content_type=response.headers.get('content-type'))
26
+
27
+ logging.info(f"File downloaded and saved to Firebase: {filename}")
28
+ return filename
29
+ except Exception as e:
30
+ logging.error(f"Error downloading file: {str(e)}")
31
+ raise
32
+
33
+ async def remove_temp_file(filename: str):
34
+ try:
35
+ blob = firebase_bucket.blob(filename)
36
+ blob.delete()
37
+ logging.info(f"Temporary file deleted from Firebase: {filename}")
38
+ except Exception as e:
39
+ logging.error(f"Error deleting temporary file from Firebase: {str(e)}")
40
+
41
+ def get_file_content(filename: str) -> bytes:
42
+ try:
43
+ blob = firebase_bucket.blob(filename)
44
+ return blob.download_as_bytes()
45
+ except Exception as e:
46
+ logging.error(f"Error getting file content from Firebase: {str(e)}")
47
+ raise
48
+
49
+ def get_file_stream(filename: str) -> io.BytesIO:
50
+ try:
51
+ content = get_file_content(filename)
52
+ return io.BytesIO(content)
53
+ except Exception as e:
54
+ logging.error(f"Error getting file stream from Firebase: {str(e)}")
55
+ raise
app/utils/hash_utils.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ import imagehash
4
+ from PIL import Image
5
+ import logging
6
+ from app.utils.file_utils import get_file_stream
7
+
8
+ def compute_video_hash(features):
9
+ logging.info("Computing video hash.")
10
+ return imagehash.phash(Image.fromarray(np.mean(features, axis=0).reshape(8, 8)))
11
+
12
+ import tempfile
13
+ import os
14
+
15
+ def compute_frame_hashes(firebase_filename):
16
+ logging.info("Computing frame hashes")
17
+ video_stream = get_file_stream(firebase_filename)
18
+ video_bytes = video_stream.getvalue()
19
+
20
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as temp_file:
21
+ temp_file.write(video_bytes)
22
+ temp_file_path = temp_file.name
23
+
24
+ cap = cv2.VideoCapture(temp_file_path)
25
+
26
+ frame_hashes = []
27
+ while True:
28
+ ret, frame = cap.read()
29
+ if not ret:
30
+ break
31
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
32
+ img_hash = imagehash.average_hash(Image.fromarray(gray))
33
+ frame_hashes.append(str(img_hash))
34
+
35
+ cap.release()
36
+ os.unlink(temp_file_path)
37
+
38
+ logging.info("Finished computing frame hashes.")
39
+ return frame_hashes
app/utils/image_utils.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ from typing import Union
4
+ from PIL import Image
5
+ from io import BytesIO
6
+ import imghdr
7
+ from fastapi import HTTPException
8
+ from app.utils.file_utils import get_file_content
9
+
10
+ SUPPORTED_IMAGE_FORMATS = ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.webp']
11
+
12
+ def verify_image_format(filename: str):
13
+ content = get_file_content(filename)
14
+ file_ext = '.' + (imghdr.what(BytesIO(content)) or '')
15
+
16
+ if file_ext not in SUPPORTED_IMAGE_FORMATS:
17
+ raise HTTPException(status_code=400, detail=f"Unsupported image format. Supported formats are: {', '.join(SUPPORTED_IMAGE_FORMATS)}")
18
+
19
+ def preprocess_image(image: Union[str, np.ndarray, Image.Image], hash_size: int = 32) -> np.ndarray:
20
+ if isinstance(image, str):
21
+ content = get_file_content(image)
22
+ img = Image.open(BytesIO(content))
23
+ img = strip_metadata(img)
24
+ image = np.array(img)
25
+ elif isinstance(image, Image.Image):
26
+ image = strip_metadata(image)
27
+ image = np.array(image)
28
+
29
+ if len(image.shape) == 3:
30
+ image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
31
+
32
+ image = cv2.resize(image, (hash_size, hash_size), interpolation=cv2.INTER_AREA)
33
+ image = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX)
34
+
35
+ return image
36
+
37
+ def strip_metadata(img: Image.Image) -> Image.Image:
38
+ data = list(img.getdata())
39
+ img_without_exif = Image.new(img.mode, img.size)
40
+ img_without_exif.putdata(data)
41
+ return img_without_exif
42
+
43
+ def perceptual_image_hash(image: Union[str, np.ndarray, Image.Image], hash_size: int = 32) -> str:
44
+ processed_image = preprocess_image(image, hash_size)
45
+ dct = cv2.dct(np.float32(processed_image))
46
+ dct_low = dct[:8, :8]
47
+ median = np.median(dct_low[1:])
48
+
49
+ hash_value = ''
50
+ for i in range(8):
51
+ for j in range(8):
52
+ hash_value += '1' if dct_low[i, j] > median else '0'
53
+
54
+ return hash_value
55
+
56
+ def hamming_distance(hash1: str, hash2: str) -> int:
57
+ return sum(c1 != c2 for c1, c2 in zip(hash1, hash2))
58
+
59
+ def are_images_similar(hash1: str, hash2: str, threshold: int = 5) -> bool:
60
+ distance = hamming_distance(hash1, hash2)
61
+ return distance <= threshold
62
+
63
+ def process_image(filename: str):
64
+ try:
65
+ content = get_file_content(filename)
66
+ img = Image.open(BytesIO(content))
67
+ image_hash = perceptual_image_hash(img)
68
+
69
+ return image_hash
70
+ except Exception as e:
71
+ raise HTTPException(status_code=500, detail=f"Error processing image: {str(e)}")
72
+
73
+ def compare_images(filename1: str, filename2: str):
74
+ try:
75
+ content1 = get_file_content(filename1)
76
+ content2 = get_file_content(filename2)
77
+ img1 = Image.open(BytesIO(content1))
78
+ img2 = Image.open(BytesIO(content2))
79
+ hash1 = perceptual_image_hash(img1)
80
+ hash2 = perceptual_image_hash(img2)
81
+
82
+ are_similar = are_images_similar(hash1, hash2)
83
+ distance = hamming_distance(hash1, hash2)
84
+
85
+ return {
86
+ "image1_hash": hash1,
87
+ "image2_hash": hash2,
88
+ "are_similar": are_similar,
89
+ "hamming_distance": distance
90
+ }
91
+ except Exception as e:
92
+ raise HTTPException(status_code=500, detail=f"Error comparing images: {str(e)}")
models/mobilenetv2_spoof_model.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:59a8eb471e37ffa98ca5984c18e8ab3c0fa38e1ccf407c6114b327b824a1839e
3
+ size 32110960
models/model_config.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"img_width": 224, "img_height": 224, "class_labels": {"real": 0, "spoof": 1}, "threshold": 0.5}
render.yaml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ - type: web
3
+ name: credify
4
+ env: python
5
+ buildCommand: |
6
+ apt-get update && apt-get install -y ffmpeg
7
+ chmod +x $(which ffmpeg)
8
+ ffmpeg -version
9
+ pip install --upgrade pip
10
+ pip install --use-pep517 -r requirements.txt
11
+ startCommand: uvicorn app.main:app --host 0.0.0.0 --port $PORT
requirements.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiohttp==3.10.5
2
+ fastapi==0.109.2
3
+ # ffmpeg==1.4
4
+ ffmpeg-python==0.2.0
5
+ firebase_admin==6.5.0
6
+ imagehash==4.3.1
7
+ librosa==0.10.2.post1
8
+ numpy>=1.23.5,<2.0.0
9
+ Pillow==10.4.0
10
+ pydantic==2.6.1
11
+ pydub==0.25.1
12
+ python-dotenv==1.0.1
13
+ scipy==1.14.1
14
+ tensorflow==2.15.0
15
+ uvicorn==0.30.6
16
+ opencv-python==4.8.0.74
17
+ python-magic==0.4.27