abhisheksan commited on
Commit
e4b2f2c
·
1 Parent(s): 6c31b41

Refactor main.py and add NUMBA_DISABLE_JIT environment variable

Browse files
README.md CHANGED
@@ -6,15 +6,77 @@ colorTo: green
6
  sdk: docker
7
  app_port: 7860
8
  ---
9
- use python 3.10 for this project as the audio extraction library can work with this version only
10
 
11
- create a virtual environment : .\venv\Scripts\activate
12
 
13
- install the required libraries using : pip install -r requirements.txt
14
 
15
- run the app using : uvicorn app.main:app --reload
16
 
17
- 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 :
18
- - open cmd and type : winget install ffmpeg
19
- - after the installation has been sucessfully done add the bin path to the System environment variables
20
- - if u fail to find the path just type : where ffmpeg , in cmd and you will get the path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  sdk: docker
7
  app_port: 7860
8
  ---
 
9
 
10
+ # Credify 🐳
11
 
12
+ [![Hugging Face Spaces](https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Spaces-blue)](https://huggingface.co/spaces/abhisheksan/credify)
13
 
14
+ Credify is a Docker-based application designed to detect tampered media and assign unique fingerprints to them.
15
 
16
+ ## 🚀 Quick Setup
17
+
18
+ ### Prerequisites
19
+
20
+ - Python 3.10
21
+ - Docker
22
+ - FFmpeg
23
+
24
+ ### Installation
25
+
26
+ 1. Clone the repository:
27
+ ```
28
+ git clone https://github.com/abhisheksharm-3/credify.git
29
+ cd credify/server
30
+ ```
31
+
32
+ 2. Create and activate a virtual environment:
33
+ ```
34
+ python -m venv venv
35
+ .\venv\Scripts\activate
36
+ ```
37
+
38
+ 3. Install required dependencies:
39
+ ```
40
+ pip install -r requirements.txt
41
+ ```
42
+
43
+ 4. Download models:
44
+ The `models` folder is not included in the repository due to its large size. Download the models from [this Google Drive link](https://drive.google.com/drive/folders/13ekurrSgQo6d99PCv708vQVInfWpKsno?usp=sharing) and place them in a folder named `models` within the `server` directory.
45
+
46
+ 5. Install FFmpeg:
47
+ - Open CMD and run: `winget install ffmpeg`
48
+ - Add the FFmpeg bin path to System Environment Variables
49
+ - To find the path, run: `where ffmpeg` in CMD
50
+
51
+ ## 🏃‍♂️ Running the Application
52
+
53
+ Start the application using:
54
+ ```
55
+ uvicorn app.main:app --reload
56
+ ```
57
+
58
+ ## 🐋 Docker Deployment
59
+
60
+ The application is configured for Docker deployment with the following specifications:
61
+ - App Port: 7860
62
+ - SDK: Docker
63
+
64
+ ## 🎨 Theme
65
+
66
+ - Color Scheme: Blue to Green
67
+
68
+ ## 🛠️ Troubleshooting
69
+
70
+ If you encounter a path error when re-uploading the same image or audio, ensure that FFmpeg is properly installed and configured on your system as described in the installation steps.
71
+
72
+ <!-- ## 📄 License
73
+
74
+ [Include license information here]
75
+
76
+ ## 🤝 Contributing
77
+
78
+ [Include contribution guidelines here]
79
+
80
+ ## 📞 Contact
81
+
82
+ [Include contact information or support channels here] -->
app/api/routes.py CHANGED
@@ -1,4 +1,4 @@
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
@@ -14,15 +14,6 @@ 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
-
26
  @router.post("/fingerprint")
27
  async def create_fingerprint(request: ContentRequest):
28
  try:
@@ -71,9 +62,8 @@ async def verify_image_route(request: ContentRequest):
71
  @router.post("/compare_images")
72
  async def compare_images_route(request: CompareRequest):
73
  try:
74
- # Call the image comparison service with the URLs from the request body
75
  result = await compare_images(request.url1, request.url2)
76
  return {"message": "Image comparison completed", "result": result}
77
  except Exception as e:
78
  logging.error(f"Error in image comparison: {str(e)}")
79
- raise HTTPException(status_code=500, detail=f"Error in image comparison: {str(e)}")
 
1
+ from fastapi import APIRouter, HTTPException
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
 
14
  url1: str
15
  url2: str
16
 
 
 
 
 
 
 
 
 
 
17
  @router.post("/fingerprint")
18
  async def create_fingerprint(request: ContentRequest):
19
  try:
 
62
  @router.post("/compare_images")
63
  async def compare_images_route(request: CompareRequest):
64
  try:
 
65
  result = await compare_images(request.url1, request.url2)
66
  return {"message": "Image comparison completed", "result": result}
67
  except Exception as e:
68
  logging.error(f"Error in image comparison: {str(e)}")
69
+ raise HTTPException(status_code=500, detail=f"Error in image comparison: {str(e)}")
app/main.py CHANGED
@@ -1,5 +1,4 @@
1
 
2
- import os
3
  from fastapi import FastAPI, Request
4
  from fastapi.responses import JSONResponse
5
  from app.api.routes import router
@@ -9,7 +8,6 @@ from app.api.forgery_routes import router as forgery_router
9
  import logging
10
 
11
  app = FastAPI()
12
- os.environ['NUMBA_DISABLE_JIT'] = '1'
13
 
14
  @app.on_event("startup")
15
  async def startup_event():
 
1
 
 
2
  from fastapi import FastAPI, Request
3
  from fastapi.responses import JSONResponse
4
  from app.api.routes import router
 
8
  import logging
9
 
10
  app = FastAPI()
 
11
 
12
  @app.on_event("startup")
13
  async def startup_event():
app/services/video_service.py CHANGED
@@ -1,95 +1,68 @@
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.")
@@ -97,15 +70,15 @@ async def fingerprint_video(video_url):
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
- 'audio_hash': str(collective_audio_hash) if collective_audio_hash else None,
108
- 'video_hash': str(video_hash),
109
  }
110
  finally:
111
  if firebase_filename:
@@ -115,8 +88,8 @@ 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['video_hash']) - imagehash.hex_to_hash(fp2['video_hash'])) / 64.0
119
- audio_similarity = 1 - (imagehash.hex_to_hash(fp1['audio_hash']) - imagehash.hex_to_hash(fp2['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
 
 
 
1
  import numpy as np
2
  from scipy.fftpack import dct
3
  import imagehash
4
  from PIL import Image
5
  import logging
6
+ import io
7
+ import av
8
  from app.utils.hash_utils import compute_video_hash, compute_frame_hashes
9
  from app.services.audio_service import extract_audio_features, compute_audio_hash, compute_audio_hashes
10
+ from app.utils.file_utils import download_file, remove_temp_file, get_file_content
11
+ from app.core.firebase_config import firebase_bucket
12
 
 
 
13
  logging.basicConfig(level=logging.DEBUG)
14
  logger = logging.getLogger(__name__)
15
+
16
  def validate_video_bytes(video_bytes):
17
  try:
18
+ with av.open(io.BytesIO(video_bytes)) as container:
19
+ has_video = any(stream.type == 'video' for stream in container.streams)
20
+ has_audio = any(stream.type == 'audio' for stream in container.streams)
21
+
22
+ if not has_video:
23
+ raise ValueError("No video stream found in the file")
24
+
25
+ if not has_audio:
26
+ logger.warning("No audio stream found in the file")
27
+
28
+ return has_audio
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  except Exception as e:
30
+ logger.error(f"Error validating video bytes: {str(e)}")
31
  return False
32
 
33
+ async def extract_video_features(video_content):
34
  logging.info("Extracting video features")
 
 
 
 
 
 
 
 
35
 
36
+ try:
37
+ with av.open(io.BytesIO(video_content)) as container:
38
+ video_stream = next(s for s in container.streams if s.type == 'video')
39
+ features = []
40
+
41
+ for frame in container.decode(video=0):
42
+ img = frame.to_image().convert('L') # Convert to grayscale
43
+ resized = np.array(img.resize((32, 32)))
44
+ dct_frame = dct(dct(resized.T, norm='ortho').T, norm='ortho')
45
+ features.append(dct_frame[:8, :8].flatten())
46
+
47
+ except Exception as e:
48
+ logger.error(f"Error extracting video features: {str(e)}")
49
+ raise
50
 
51
  logging.info("Finished extracting video features.")
52
+ return np.array(features)
53
 
54
  async def fingerprint_video(video_url):
55
  logging.info(f"Fingerprinting video: {video_url}")
56
  firebase_filename = None
57
  try:
58
  firebase_filename = await download_file(video_url)
59
+ video_content = get_file_content(firebase_filename)
 
60
 
61
+ video_features = await extract_video_features(video_content)
62
 
63
+ if validate_video_bytes(video_content):
64
+ audio_features = extract_audio_features(video_content)
65
+ audio_hashes = compute_audio_hashes(video_content)
66
  collective_audio_hash = compute_audio_hash(audio_features)
67
  else:
68
  logging.warning("No audio stream found or invalid video. Skipping audio feature extraction.")
 
70
  collective_audio_hash = None
71
 
72
  video_hash = compute_video_hash(video_features)
73
+ frame_hashes = compute_frame_hashes(video_content)
74
 
75
  logging.info("Finished fingerprinting video.")
76
 
77
  return {
78
  'frame_hashes': frame_hashes,
79
  'audio_hashes': audio_hashes,
80
+ 'robust_audio_hash': str(collective_audio_hash) if collective_audio_hash else None,
81
+ 'robust_video_hash': str(video_hash),
82
  }
83
  finally:
84
  if firebase_filename:
 
88
  fp1 = await fingerprint_video(video_url1)
89
  fp2 = await fingerprint_video(video_url2)
90
 
91
+ video_similarity = 1 - (imagehash.hex_to_hash(fp1['robust_video_hash']) - imagehash.hex_to_hash(fp2['robust_video_hash'])) / 64.0
92
+ audio_similarity = 1 - (imagehash.hex_to_hash(fp1['robust_audio_hash']) - imagehash.hex_to_hash(fp2['robust_audio_hash'])) / 64.0 if fp1['robust_audio_hash'] and fp2['robust_audio_hash'] else 0
93
 
94
  overall_similarity = (video_similarity + audio_similarity) / 2
95
  is_same_content = overall_similarity > 0.9 # You can adjust this threshold
app/utils/forgery_video_utils.py CHANGED
@@ -1,106 +1,105 @@
1
- import cv2
2
  import numpy as np
3
- from moviepy.editor import VideoFileClip
4
  from PIL import Image
5
  import io
6
  from app.utils.file_utils import get_file_content, upload_file_to_firebase, remove_temp_file
7
- import subprocess
8
- import tempfile
9
- import os
10
  import logging
 
 
11
 
12
- async def extract_audio(firebase_filename):
13
  try:
14
  video_content = get_file_content(firebase_filename)
15
- with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as temp_video:
16
- temp_video.write(video_content)
17
- temp_video_path = temp_video.name
18
-
19
- with VideoFileClip(temp_video_path) as video:
20
- if video.audio is not None:
21
- audio_filename = f"{firebase_filename}_audio.wav"
22
- with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as temp_audio:
23
- video.audio.write_audiofile(temp_audio.name, logger=None)
24
- temp_audio_path = temp_audio.name
25
-
26
- with open(temp_audio_path, 'rb') as audio_file:
27
- audio_content = audio_file.read()
28
-
29
- await upload_file_to_firebase(audio_content, audio_filename)
30
- os.remove(temp_audio_path)
31
- os.remove(temp_video_path)
32
- return audio_filename
33
 
34
- os.remove(temp_video_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  except Exception as e:
36
  logging.error(f"Error extracting audio: {str(e)}")
37
  return None
38
 
39
- async def extract_frames(firebase_filename, max_frames=10):
40
  frames = []
41
  video_content = get_file_content(firebase_filename)
42
 
43
- with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as temp_video:
44
- temp_video.write(video_content)
45
- temp_video_path = temp_video.name
46
-
47
  try:
48
- with VideoFileClip(temp_video_path) as video:
49
- duration = video.duration
 
50
  frame_interval = duration / max_frames
51
 
52
  for i in range(max_frames):
53
- t = i * frame_interval
54
- frame = video.get_frame(t)
55
- frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
56
- frame_image = Image.fromarray(frame_rgb)
57
-
58
- frame_filename = f"{firebase_filename}_frame_{i}.jpg"
59
- frame_byte_arr = io.BytesIO()
60
- frame_image.save(frame_byte_arr, format='JPEG')
61
- frame_byte_arr = frame_byte_arr.getvalue()
62
-
63
- await upload_file_to_firebase(frame_byte_arr, frame_filename)
64
- frames.append(frame_filename)
65
-
66
- finally:
67
- os.remove(temp_video_path)
 
68
 
69
  return frames
70
 
71
- async def compress_and_process_video(firebase_filename, target_size_mb=50, max_duration=60):
72
- video_content = get_file_content(firebase_filename)
73
-
74
- with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as temp_video:
75
- temp_video.write(video_content)
76
- input_path = temp_video.name
 
 
77
 
78
- output_filename = f"{firebase_filename}_compressed.mp4"
79
- with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as temp_output:
80
- output_path = temp_output.name
81
 
 
 
 
82
  try:
83
- probe_cmd = ['ffprobe', '-v', 'error', '-select_streams', 'v:0',
84
- '-show_entries', 'stream=width,height,duration,bit_rate',
85
- '-of', 'json', input_path]
86
-
87
- result = subprocess.run(probe_cmd, capture_output=True, text=True)
88
- video_info = eval(result.stdout)['streams'][0]
89
-
90
- width = video_info.get('width', 1280)
91
- height = video_info.get('height', 720)
92
- duration = float(video_info.get('duration', '0'))
93
- original_bitrate = int(video_info.get('bit_rate', '0'))
94
-
95
- if duration <= 0:
96
- logging.warning(f"Invalid video duration ({duration}). Using 1 second as default.")
97
- duration = 1
98
-
99
  duration = min(duration, max_duration)
 
100
 
 
101
  target_size_bits = target_size_mb * 8 * 1024 * 1024
102
  target_bitrate = int(target_size_bits / duration)
103
 
 
104
  if width > height:
105
  new_width = min(width, 1280)
106
  new_height = int((new_width / width) * height)
@@ -111,28 +110,49 @@ async def compress_and_process_video(firebase_filename, target_size_mb=50, max_d
111
  new_width = new_width - (new_width % 2)
112
  new_height = new_height - (new_height % 2)
113
 
114
- cmd = [
115
- 'ffmpeg', '-y', '-i', input_path,
116
- '-c:v', 'libx264', '-preset', 'faster',
117
- '-crf', '23',
118
- '-b:v', f'{target_bitrate}',
119
- '-maxrate', f'{int(1.5*target_bitrate)}',
120
- '-bufsize', f'{2*target_bitrate}',
121
- '-vf', f'scale={new_width}:{new_height}',
122
- '-t', str(duration),
123
- '-c:a', 'aac', '-b:a', '128k',
124
- output_path
125
- ]
126
-
127
- subprocess.run(cmd, check=True, capture_output=True)
128
-
129
- with open(output_path, 'rb') as compressed_video:
130
- compressed_content = compressed_video.read()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
 
 
 
132
  await upload_file_to_firebase(compressed_content, output_filename)
133
 
134
- finally:
135
- os.remove(input_path)
136
- os.remove(output_path)
137
 
138
- return output_filename
 
 
 
1
+ import av
2
  import numpy as np
 
3
  from PIL import Image
4
  import io
5
  from app.utils.file_utils import get_file_content, upload_file_to_firebase, remove_temp_file
 
 
 
6
  import logging
7
+ import uuid
8
+ from typing import List, Tuple
9
 
10
+ async def extract_audio(firebase_filename: str) -> str:
11
  try:
12
  video_content = get_file_content(firebase_filename)
13
+ input_container = av.open(io.BytesIO(video_content))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
+ audio_stream = next((s for s in input_container.streams if s.type == 'audio'), None)
16
+ if audio_stream is None:
17
+ logging.warning(f"No audio stream found in {firebase_filename}")
18
+ return None
19
+
20
+ output_container = av.open(io.BytesIO(), mode='w', format='wav')
21
+ output_stream = output_container.add_stream('pcm_s16le', rate=audio_stream.rate)
22
+
23
+ for frame in input_container.decode(audio_stream):
24
+ for packet in output_stream.encode(frame):
25
+ output_container.mux(packet)
26
+
27
+ # Flush the stream
28
+ for packet in output_stream.encode(None):
29
+ output_container.mux(packet)
30
+
31
+ output_container.close()
32
+
33
+ audio_content = output_container.data.getvalue()
34
+ audio_filename = f"{firebase_filename}_audio.wav"
35
+ await upload_file_to_firebase(audio_content, audio_filename)
36
+
37
+ return audio_filename
38
  except Exception as e:
39
  logging.error(f"Error extracting audio: {str(e)}")
40
  return None
41
 
42
+ async def extract_frames(firebase_filename: str, max_frames: int = 10) -> List[str]:
43
  frames = []
44
  video_content = get_file_content(firebase_filename)
45
 
 
 
 
 
46
  try:
47
+ with av.open(io.BytesIO(video_content)) as container:
48
+ video_stream = container.streams.video[0]
49
+ duration = float(video_stream.duration * video_stream.time_base)
50
  frame_interval = duration / max_frames
51
 
52
  for i in range(max_frames):
53
+ container.seek(int(i * frame_interval * av.time_base))
54
+ for frame in container.decode(video=0):
55
+ frame_rgb = frame.to_rgb().to_ndarray()
56
+ frame_image = Image.fromarray(frame_rgb)
57
+
58
+ frame_filename = f"{firebase_filename}_frame_{i}.jpg"
59
+ frame_byte_arr = io.BytesIO()
60
+ frame_image.save(frame_byte_arr, format='JPEG')
61
+ frame_byte_arr = frame_byte_arr.getvalue()
62
+
63
+ await upload_file_to_firebase(frame_byte_arr, frame_filename)
64
+ frames.append(frame_filename)
65
+ break # Only take the first frame after seeking
66
+
67
+ except Exception as e:
68
+ logging.error(f"Error extracting frames: {str(e)}")
69
 
70
  return frames
71
 
72
+ import av
73
+ import numpy as np
74
+ from PIL import Image
75
+ import io
76
+ from app.utils.file_utils import get_file_content, upload_file_to_firebase, remove_temp_file
77
+ import logging
78
+ import uuid
79
+ from typing import List, Tuple
80
 
81
+ # ... (previous functions remain unchanged)
 
 
82
 
83
+ async def compress_and_process_video(firebase_filename: str, target_size_mb: int = 50, max_duration: int = 60) -> str:
84
+ video_content = get_file_content(firebase_filename)
85
+
86
  try:
87
+ input_container = av.open(io.BytesIO(video_content))
88
+ video_stream = input_container.streams.video[0]
89
+ audio_stream = next((s for s in input_container.streams if s.type == 'audio'), None)
90
+
91
+ # Get video information
92
+ width = video_stream.width
93
+ height = video_stream.height
94
+ duration = float(video_stream.duration * video_stream.time_base)
 
 
 
 
 
 
 
 
95
  duration = min(duration, max_duration)
96
+ frame_rate = video_stream.average_rate
97
 
98
+ # Calculate target bitrate
99
  target_size_bits = target_size_mb * 8 * 1024 * 1024
100
  target_bitrate = int(target_size_bits / duration)
101
 
102
+ # Adjust dimensions
103
  if width > height:
104
  new_width = min(width, 1280)
105
  new_height = int((new_width / width) * height)
 
110
  new_width = new_width - (new_width % 2)
111
  new_height = new_height - (new_height % 2)
112
 
113
+ output_buffer = io.BytesIO()
114
+ output_container = av.open(output_buffer, mode='w', format='mp4')
115
+ output_video_stream = output_container.add_stream('libx264', rate=frame_rate)
116
+ output_video_stream.width = new_width
117
+ output_video_stream.height = new_height
118
+ output_video_stream.pix_fmt = 'yuv420p'
119
+ output_video_stream.bit_rate = target_bitrate
120
+
121
+ if audio_stream:
122
+ output_audio_stream = output_container.add_stream('aac', rate=audio_stream.rate)
123
+ output_audio_stream.bit_rate = 128000 # 128k bitrate for audio
124
+
125
+ for frame in input_container.decode(video=0):
126
+ if frame.time > duration:
127
+ break
128
+ new_frame = frame.reformat(width=new_width, height=new_height, format='yuv420p')
129
+ for packet in output_video_stream.encode(new_frame):
130
+ output_container.mux(packet)
131
+
132
+ if audio_stream:
133
+ for frame in input_container.decode(audio=0):
134
+ if frame.time > duration:
135
+ break
136
+ for packet in output_audio_stream.encode(frame):
137
+ output_container.mux(packet)
138
+
139
+ # Flush streams
140
+ for packet in output_video_stream.encode(None):
141
+ output_container.mux(packet)
142
+ if audio_stream:
143
+ for packet in output_audio_stream.encode(None):
144
+ output_container.mux(packet)
145
+
146
+ # Close the output container
147
+ output_container.close()
148
 
149
+ # Get the compressed content
150
+ compressed_content = output_buffer.getvalue()
151
+ output_filename = f"{firebase_filename}_compressed.mp4"
152
  await upload_file_to_firebase(compressed_content, output_filename)
153
 
154
+ return output_filename
 
 
155
 
156
+ except Exception as e:
157
+ logging.error(f"Error compressing and processing video: {str(e)}")
158
+ raise
app/utils/hash_utils.py CHANGED
@@ -1,39 +1,30 @@
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
 
 
1
  import numpy as np
2
  import imagehash
3
  from PIL import Image
4
  import logging
5
+ import io
6
+ import av
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
+ def compute_frame_hashes(video_content):
 
 
 
13
  logging.info("Computing frame hashes")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
+ try:
16
+ with av.open(io.BytesIO(video_content)) as container:
17
+ video_stream = next(s for s in container.streams if s.type == 'video')
18
+ frame_hashes = []
19
+
20
+ for frame in container.decode(video=0):
21
+ img = frame.to_image().convert('L') # Convert to grayscale
22
+ img_hash = imagehash.average_hash(img)
23
+ frame_hashes.append(str(img_hash))
24
+
25
+ except Exception as e:
26
+ logging.error(f"Error computing frame hashes: {str(e)}")
27
+ raise
28
 
29
  logging.info("Finished computing frame hashes.")
30
  return frame_hashes
requirements.txt CHANGED
@@ -1,12 +1,12 @@
1
  aiohttp==3.10.5
 
2
  fastapi==0.115.0
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
- moviepy==1.0.3
9
- numpy>=1.23.5,<2.0.0
10
  opencv_python==4.10.0.84
11
  opencv_python_headless==4.10.0.84
12
  Pillow==10.4.0
 
1
  aiohttp==3.10.5
2
+ av==13.0.0
3
  fastapi==0.115.0
4
+ #ffmpeg==1.4
5
  ffmpeg_python==0.2.0
6
  firebase_admin==6.5.0
7
  ImageHash==4.3.1
8
  librosa==0.10.2.post1
9
+ numpy==2.1.1
 
10
  opencv_python==4.10.0.84
11
  opencv_python_headless==4.10.0.84
12
  Pillow==10.4.0