3v324v23 commited on
Commit
c25654b
·
0 Parent(s):
Files changed (7) hide show
  1. .gitignore +8 -0
  2. Dockerfile +16 -0
  3. app/core.py +21 -0
  4. app/main.py +48 -0
  5. app/utils.py +189 -0
  6. docker-compose.yml +13 -0
  7. requirements.txt +10 -0
.gitignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # Ignore environment files
2
+ .env
3
+ __pycache__/
4
+ *.pyc
5
+ *.tar
6
+ *.zip
7
+ /tmp
8
+ /.venv
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ RUN pip install --upgrade pip
6
+
7
+ RUN pip install torch torchvision --extra-index-url https://download.pytorch.org/whl/cpu
8
+
9
+ COPY requirements.txt .
10
+ RUN pip install --no-cache-dir -r requirements.txt
11
+
12
+ COPY app ./app
13
+ COPY .env .
14
+
15
+ EXPOSE 8002
16
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8002"]
app/core.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.utils import extract_keyframes, detect_timestamp
2
+ from pathlib import Path
3
+
4
+ def process_input_file(filepath, metadata):
5
+ path = Path(filepath)
6
+ keyframes = extract_keyframes(path) # Extract frames from the video (if needed), or simply return the original image
7
+
8
+ for frame in keyframes:
9
+ result = detect_timestamp(
10
+ image_path=frame,
11
+ metadata=metadata
12
+ )
13
+ if result and result.get("timestamp"):
14
+ return result
15
+
16
+ return {
17
+ "timestamp": None,
18
+ "source": None,
19
+ "confidence": 0.0,
20
+ "keyframe_file": None
21
+ }
app/main.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, File, UploadFile
2
+ from typing import List
3
+ from app.core import process_input_file
4
+ import shutil, os, json, tempfile
5
+ import time
6
+
7
+ app = FastAPI()
8
+
9
+ @app.post("/analyze/")
10
+ async def analyze_media(
11
+ files: List[UploadFile] = File(...),
12
+ metadata_file: UploadFile = File(...)
13
+ ):
14
+ # Save metadata file to a temporary location
15
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".json") as tmp:
16
+ shutil.copyfileobj(metadata_file.file, tmp)
17
+ metadata_path = tmp.name
18
+
19
+ # Read metadata from the temporary file
20
+ with open(metadata_path, "r", encoding="utf-8") as f:
21
+ metadata = json.load(f)
22
+
23
+ # Remove the temporary metadata file
24
+ os.remove(metadata_path)
25
+
26
+ results = []
27
+
28
+ for file in files:
29
+ # Save each uploaded file to a temporary location
30
+ with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(file.filename)[1]) as tmp:
31
+ shutil.copyfileobj(file.file, tmp)
32
+ temp_path = tmp.name
33
+
34
+ start_time = time.perf_counter()
35
+
36
+ output = process_input_file(
37
+ filepath=temp_path,
38
+ metadata=metadata
39
+ )
40
+
41
+ elapsed = time.perf_counter() - start_time
42
+ print(f"⏱️ [analyze_media] Service call took {elapsed:.2f} seconds")
43
+
44
+ results.append(output)
45
+
46
+ os.remove(temp_path)
47
+
48
+ return {"results": results}
app/utils.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import cv2
3
+ import time
4
+ import torch
5
+ import requests
6
+ import tempfile
7
+ import torchvision.transforms as T
8
+ from pathlib import Path
9
+ from datetime import datetime
10
+ from dotenv import load_dotenv
11
+ from difflib import SequenceMatcher
12
+ from serpapi import GoogleSearch
13
+ from open_clip import create_model_and_transforms
14
+
15
+ # Load model
16
+ model, _, preprocess = create_model_and_transforms('ViT-B-32', pretrained='openai')
17
+ device = "cuda" if torch.cuda.is_available() else "cpu"
18
+ model = model.to(device).eval()
19
+
20
+ # Load environment variables
21
+ load_dotenv()
22
+ IMGBB_API_KEY = os.getenv("IMGBB_API_KEY")
23
+ SERPAPI_API_KEY = os.getenv("SERPAPI_API_KEY")
24
+
25
+ def upload_to_imgbb(image_path):
26
+ with open(image_path, "rb") as f:
27
+ res = requests.post(
28
+ "https://api.imgbb.com/1/upload",
29
+ params={"key": IMGBB_API_KEY},
30
+ files={"image": f}
31
+ )
32
+ return res.json()["data"]["url"]
33
+
34
+ def extract_keyframes(video_path, frame_interval=5, threshold=0.92):
35
+ keyframe_paths = []
36
+
37
+ cap = cv2.VideoCapture(str(video_path))
38
+ frame_id = 0
39
+ saved_id = 0
40
+ prev_feat = None
41
+
42
+ # Create a temporary directory for keyframes
43
+ keyframe_dir = tempfile.mkdtemp(prefix="keyframes_")
44
+
45
+ while cap.isOpened():
46
+ ret, frame = cap.read()
47
+ if not ret:
48
+ break
49
+
50
+ if frame_id % frame_interval == 0:
51
+ # Convert frame → tensor (CLIP)
52
+ image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
53
+ image_pil = T.ToPILImage()(image)
54
+ image_tensor = preprocess(image_pil).unsqueeze(0).to(device)
55
+
56
+ with torch.no_grad():
57
+ feat = model.encode_image(image_tensor)
58
+ feat = feat / feat.norm(dim=-1, keepdim=True)
59
+
60
+ # Save keyframe if it's significantly different from the previous one
61
+ if prev_feat is None or (feat @ prev_feat.T).item() < threshold:
62
+ save_path = os.path.join(keyframe_dir, f"keyframe_{saved_id:03}.jpg")
63
+ cv2.imwrite(save_path, frame)
64
+ keyframe_paths.append(save_path)
65
+ saved_id += 1
66
+ prev_feat = feat
67
+
68
+ frame_id += 1
69
+
70
+ cap.release()
71
+ return keyframe_paths
72
+
73
+ def parse_date_from_string(s):
74
+ formats = [
75
+ "%b %d, %Y, %H:%M", # Oct 17, 2023, 14:25
76
+ "%B %d, %Y, %H:%M", # October 17, 2023, 14:25
77
+ "%b %d, %Y", # Oct 17, 2023
78
+ "%B %d, %Y", # October 17, 2023
79
+ "%Y-%m-%d %H:%M", # 2023-10-17 14:25
80
+ "%Y-%m-%d", # 2023-10-17
81
+ "%d/%m/%Y %H:%M", # 17/10/2023 14:25
82
+ "%d/%m/%Y", # 17/10/2023
83
+ ]
84
+ for fmt in formats:
85
+ try:
86
+ return datetime.strptime(s.strip(), fmt)
87
+ except:
88
+ continue
89
+ return None
90
+
91
+ def simple_similarity(a, b):
92
+ return SequenceMatcher(None, a.lower(), b.lower()).ratio()
93
+
94
+ def detect_timestamp(image_path, metadata):
95
+ text_query = f"{metadata['location']} {metadata['title']} {metadata['description']}"
96
+
97
+ def search_by_text():
98
+ search = GoogleSearch({
99
+ "engine": "google",
100
+ "q": text_query,
101
+ "api_key": SERPAPI_API_KEY,
102
+ "num": 20,
103
+ "tbs": "sbd:1"
104
+ })
105
+ results = search.get_dict()
106
+ return results.get("organic_results", [])
107
+
108
+ text_results = search_by_text()
109
+ print(f"Retrieved {len(text_results)} results from text search")
110
+
111
+ print(f"\nProcessing image: {os.path.basename(image_path)}")
112
+
113
+ # Upload image
114
+ with open(image_path, "rb") as f:
115
+ upload_response = requests.post(
116
+ "https://api.imgbb.com/1/upload",
117
+ params={"key": IMGBB_API_KEY},
118
+ files={"image": f}
119
+ )
120
+ image_url = upload_response.json()["data"]["url"]
121
+ print(f"Uploaded to imgbb: {image_url}")
122
+
123
+ # Reverse image search
124
+ search = GoogleSearch({
125
+ "engine": "google_reverse_image",
126
+ "image_url": image_url,
127
+ "api_key": SERPAPI_API_KEY
128
+ })
129
+ results = search.get_dict()
130
+
131
+ image_results = []
132
+ for key, value in results.items():
133
+ if isinstance(value, list) and all(isinstance(item, dict) for item in value):
134
+ print(f"Added {len(value)} results from field '{key}'")
135
+ image_results.extend(value)
136
+
137
+ print(f"Total of {len(image_results)} image search results")
138
+
139
+ # Merge and score
140
+ merged = text_results + image_results
141
+ scored = []
142
+ for res in merged:
143
+ title = res.get("title", "")
144
+ link = res.get("link", "")
145
+ snippet = res.get("snippet", "")
146
+ date = parse_date_from_string(res.get("date", ""))
147
+ text = f"{title} {snippet}"
148
+ sim = simple_similarity(text, text_query)
149
+ scored.append({
150
+ "title": title,
151
+ "link": link,
152
+ "date": date,
153
+ "similarity": sim,
154
+ "from_image": res in image_results
155
+ })
156
+
157
+ scored = sorted(scored, key=lambda x: (-x["similarity"], x["date"] or datetime.max))
158
+
159
+ for item in scored:
160
+ if item["date"]:
161
+ date_str = item["date"].strftime("%Y-%m-%d %H:%M") if item["date"].hour or item["date"].minute else item["date"].strftime("%Y-%m-%d")
162
+ print(f"\nMatch found:")
163
+ print(f"Link: {item['link']}")
164
+ print(f"Title: {item['title']}")
165
+ print(f"Similarity: {item['similarity']:.2f}")
166
+ print(f"Published date: {date_str}")
167
+
168
+ result = {
169
+ "timestamp": date_str,
170
+ "source": item["link"],
171
+ "confidence": item["similarity"]
172
+ }
173
+
174
+ if item["from_image"]:
175
+ result["keyframe_file"] = image_url
176
+
177
+ return result
178
+
179
+ print("No reliable timestamp found.")
180
+ return {
181
+ "timestamp": None,
182
+ "source": None,
183
+ "confidence": 0.0
184
+ }
185
+
186
+
187
+
188
+
189
+
docker-compose.yml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ web:
3
+ build: .
4
+ ports:
5
+ - "8002:8002"
6
+ volumes:
7
+ - ./.env:/code/.env
8
+
9
+ env_file:
10
+ - ./.env
11
+
12
+ restart: unless-stopped
13
+ runtime: nvidia
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ requests
4
+ opencv-python-headless
5
+ python-multipart
6
+ python-dotenv
7
+ open_clip_torch
8
+ torch
9
+ torchvision
10
+ google-search-results