Nebulixlabs commited on
Commit
cda49ed
·
verified ·
1 Parent(s): eae3f53

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +261 -139
app.py CHANGED
@@ -3,178 +3,302 @@ import uuid
3
  import asyncio
4
  import os
5
  import uvicorn
 
 
 
 
6
  from contextlib import asynccontextmanager
7
- from datetime import datetime, timedelta
8
- from urllib.parse import urlparse
9
  from fastapi import FastAPI, Query, BackgroundTasks
10
- from fastapi.responses import JSONResponse, FileResponse # <--- FileResponse add kiya
11
- from rapidfuzz import process, fuzz
 
12
  from cryptography.fernet import Fernet
13
 
14
- # --- 1. SETUP & ENCRYPTION ---
15
  SECRET_KEY = Fernet.generate_key()
16
  cipher_suite = Fernet(SECRET_KEY)
17
 
18
- # Logs directory setup
19
  LOG_DIR = "logs"
 
20
  os.makedirs(LOG_DIR, exist_ok=True)
21
 
22
- # Global variables for dataset
23
  raw_data = []
24
  search_corpus = []
 
 
25
 
26
- # --- 2. OPTIMIZED CLEANUP TASK ---
27
- async def cleanup_old_logs():
28
- while True:
29
- try:
30
- if os.path.exists(LOG_DIR):
31
- thirty_days_ago = datetime.now() - timedelta(days=30)
32
-
33
- for filename in os.listdir(LOG_DIR):
34
- if filename.startswith("search_logs_") and filename.endswith(".jsonl"):
35
- date_part = filename.replace("search_logs_", "").replace(".jsonl", "")
36
- try:
37
- file_date = datetime.strptime(date_part, "%Y-%m-%d")
38
- if file_date < thirty_days_ago:
39
- os.remove(os.path.join(LOG_DIR, filename))
40
- print(f"🗑️ Deleted 1-month old log file: {filename}")
41
- except ValueError:
42
- pass
43
- except Exception as e:
44
- print("Cleanup Error:", e)
45
-
46
- # Har 24 ghante me ek baar check
47
- await asyncio.sleep(24 * 60 * 60)
48
 
49
- # --- 3. LIFESPAN (Replaces @app.on_event("startup")) ---
50
- @asynccontextmanager
51
- async def lifespan(app: FastAPI):
52
- # a. Dataset Loading during startup
53
- global raw_data, search_corpus
54
- try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  with open("dataset.jsonl", "r", encoding="utf-8") as f:
56
  for line in f:
57
  if line.strip():
58
- item = json.loads(line)
59
- raw_data.append(item)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
- domain = urlparse(item.get("url", "")).netloc
62
- text_to_search = f"{domain} {item.get('title', '')} {item.get('description', '')}"
63
- search_corpus.append(text_to_search.lower())
64
- print(f"✅ Successfully loaded {len(raw_data)} records from dataset.jsonl")
65
- except FileNotFoundError:
66
- print("❌ ERROR: dataset.jsonl file not found in the directory!")
67
-
68
- # b. Start background cleanup task
69
- cleanup_task = asyncio.create_task(cleanup_old_logs())
70
-
71
- yield # App jab tak chalegi, yaha yield rahega
72
-
73
- # c. App shutdown hone par task cancel karna
74
- cleanup_task.cancel()
75
 
76
- # Pass lifespan to FastAPI
77
- app = FastAPI(title="Fast Search API", lifespan=lifespan)
 
 
 
 
 
 
 
78
 
79
 
80
- # --- 4. DATE-BASED LOGGING & ENCRYPTION ---
81
  async def log_query_encrypted(req_id: str, query: str):
82
- encrypted_q = cipher_suite.encrypt(query.encode()).decode()
83
- now = datetime.now()
84
-
85
- date_str = now.strftime("%Y-%m-%d")
86
- log_file = os.path.join(LOG_DIR, f"search_logs_{date_str}.jsonl")
87
-
88
- log_entry = {
89
- "id": req_id,
90
- "query": encrypted_q,
91
- "timestamp": now.isoformat()
92
- }
93
-
94
- with open(log_file, "a", encoding="utf-8") as f:
95
- f.write(json.dumps(log_entry) + "\n")
96
-
97
-
98
- # --- 5. FAST AI / NLP LOGIC ---
99
- def check_self_harm(query: str) -> bool:
100
- harm_keywords = ["suicide", "kill myself", "die", "harm myself", "end my life", "suiside"]
101
- return any(word in query.lower() for word in harm_keywords)
102
-
103
- def check_adult_content(query: str) -> bool:
104
- adult_keywords = ["porn", "sex", "xxx", "nude", "adult"]
105
- return any(word in query.lower() for word in adult_keywords)
106
-
107
- def ai_fallback_search(query: str):
108
- return {
109
- "ai_fallback_triggered": True,
110
- "message": f"we dont find anything but we are trying to be the perfect sorry but our ai find this {query}",
111
- "results": [
112
- {
113
- "url": f"https://www.google.com/search?q={query.replace(' ', '+')}",
114
- "title": f"AI Web Search: {query}",
115
- "description": "Local DB me result na milne par ye AI generated fallback result hai.",
116
- "images": [],
117
- "videos": [],
118
- "docs": []
119
- }
120
- ]
121
- }
122
-
123
- # --- 0. SERVE HTML UI ---
124
- @app.get("/")
125
- async def serve_ui():
126
- # Ye root URL (e.g. https://nebulixlabs-nutral-sr-image-enhancer.hf.space/) par HTML load karega
127
- return FileResponse("index.html")
128
- # --- 6. SEARCH API ENDPOINT ---
129
  @app.get("/search")
130
- async def search(q: str = Query(..., description="Search query"), background_tasks: BackgroundTasks = BackgroundTasks()):
131
  req_id = str(uuid.uuid4())
132
-
133
- # Background Logging
134
  background_tasks.add_task(log_query_encrypted, req_id, q)
135
 
136
- # Self-Harm Check
137
- if check_self_harm(q):
138
- return JSONResponse(
139
- status_code=400,
140
- content={
141
- "request_id": req_id,
142
- "error": "Safety Alert",
143
- "message": "Please do not harm yourself. Help is available. You are valuable."
144
- }
145
- )
146
-
147
- is_adult = check_adult_content(q)
148
 
149
- final_results = []
150
-
151
- # RapidFuzz search algorithm
152
- if search_corpus:
153
- results = process.extract(
154
- q.lower(),
155
- search_corpus,
156
- limit=30,
157
- scorer=fuzz.WRatio
158
- )
 
 
 
 
159
 
160
- for match_str, score, index in results:
161
- if score > 40:
162
- result_item = raw_data[index].copy()
163
- result_item["match_score"] = round(score, 2)
164
- final_results.append(result_item)
165
 
166
- # Fallback to AI if no results found
167
  if len(final_results) == 0:
168
- response_data = ai_fallback_search(q)
169
- else:
170
  response_data = {
171
- "total_results": len(final_results),
172
- "results": final_results
 
173
  }
 
 
174
 
175
  response_data["request_id"] = req_id
176
 
177
- # Adult Content Safety Modification
178
  if is_adult:
179
  response_data["safety_message"] = "you are safe you can do anything"
180
  headers = {"X-Safety-Message": "you are safe you can do anything"}
@@ -182,7 +306,5 @@ async def search(q: str = Query(..., description="Search query"), background_tas
182
 
183
  return response_data
184
 
185
- # --- 7. START SERVER FOR GRADIO SPACE ---
186
- # Ye block Gradio Space ko bata dega ki port 7860 par API continuously chalani hai.
187
  if __name__ == "__main__":
188
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
3
  import asyncio
4
  import os
5
  import uvicorn
6
+ import aiohttp
7
+ import io
8
+ import zipfile
9
+ from bs4 import BeautifulSoup
10
  from contextlib import asynccontextmanager
11
+ from datetime import datetime
12
+ from urllib.parse import urlparse, urljoin
13
  from fastapi import FastAPI, Query, BackgroundTasks
14
+ from fastapi.responses import JSONResponse
15
+ from fastapi.middleware.cors import CORSMiddleware
16
+ from rapidfuzz import fuzz
17
  from cryptography.fernet import Fernet
18
 
19
+ # --- 1. SETUP & CONFIGURATION ---
20
  SECRET_KEY = Fernet.generate_key()
21
  cipher_suite = Fernet(SECRET_KEY)
22
 
 
23
  LOG_DIR = "logs"
24
+ DATA_DIR = "."
25
  os.makedirs(LOG_DIR, exist_ok=True)
26
 
 
27
  raw_data = []
28
  search_corpus = []
29
+ unique_domains = set()
30
+ visited_urls = set()
31
 
32
+ # Max queue size to protect Basic CPU RAM
33
+ scrape_queue = asyncio.Queue(maxsize=50000)
34
+ merge_lock = asyncio.Lock()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
+ # Threshold for merging
37
+ NEXT_THRESHOLD = 30000
38
+
39
+ # --- 2. DATASET MANAGEMENT & DEDUPLICATION ---
40
+ def get_domain(url):
41
+ return urlparse(url).netloc.lower()
42
+
43
+ def process_item_into_memory(item):
44
+ url = item.get("url", "")
45
+ if url in visited_urls:
46
+ return # Avoid duplicate URL in memory
47
+
48
+ visited_urls.add(url)
49
+ domain = get_domain(url)
50
+ unique_domains.add(domain)
51
+ raw_data.append(item)
52
+
53
+ search_corpus.append({
54
+ "url": url.lower(),
55
+ "domain": domain,
56
+ "title": item.get("title", "").lower(),
57
+ "description": item.get("description", "").lower()
58
+ })
59
+
60
+ def load_all_data():
61
+ global NEXT_THRESHOLD
62
+ print("🔄 Scanning existing datasets and date files...")
63
+
64
+ # 1. Load Main Dataset
65
+ if os.path.exists("dataset.jsonl"):
66
  with open("dataset.jsonl", "r", encoding="utf-8") as f:
67
  for line in f:
68
  if line.strip():
69
+ try:
70
+ item = json.loads(line)
71
+ process_item_into_memory(item)
72
+ # Add domain base URL to scraper to find NEW pages on old domains
73
+ base_url = f"https://{get_domain(item['url'])}/"
74
+ if base_url not in visited_urls:
75
+ try:
76
+ scrape_queue.put_nowait(base_url)
77
+ except asyncio.QueueFull:
78
+ pass
79
+ except: pass
80
+
81
+ # 2. Load ALL date-*.jsonl files
82
+ for filename in os.listdir(DATA_DIR):
83
+ if filename.startswith("date-") and filename.endswith(".jsonl"):
84
+ with open(filename, "r", encoding="utf-8") as f:
85
+ for line in f:
86
+ if line.strip():
87
+ try:
88
+ item = json.loads(line)
89
+ process_item_into_memory(item)
90
+ except: pass
91
+
92
+ # Set Threshold (e.g., if we have 65k domains, next is 90k)
93
+ current_domains = len(unique_domains)
94
+ NEXT_THRESHOLD = ((current_domains // 30000) + 1) * 30000
95
+ print(f"✅ Startup Complete! Unique URLs: {len(visited_urls)} | Unique Domains: {current_domains} | Next Merge at: {NEXT_THRESHOLD}")
96
+
97
+ async def check_and_merge_datasets():
98
+ global NEXT_THRESHOLD
99
+ if len(unique_domains) >= NEXT_THRESHOLD:
100
+ async with merge_lock:
101
+ print(f"🌟 Threshold {NEXT_THRESHOLD} Reached! Merging & Deduplicating Data...")
102
+
103
+ # Since raw_data is already perfectly deduplicated by URL in memory, we just dump it!
104
+ temp_file = "dataset.jsonl.tmp"
105
+ with open(temp_file, "w", encoding="utf-8") as f:
106
+ for item in raw_data:
107
+ f.write(json.dumps(item) + "\n")
108
+
109
+ os.replace(temp_file, "dataset.jsonl")
110
+
111
+ # Delete all date-*.jsonl files as they are merged
112
+ for filename in os.listdir(DATA_DIR):
113
+ if filename.startswith("date-") and filename.endswith(".jsonl"):
114
+ os.remove(filename)
115
+ print(f"🗑️ Deleted merged file: {filename}")
116
+
117
+ NEXT_THRESHOLD += 30000
118
+ print(f"✅ Merge Successful! Next threshold is {NEXT_THRESHOLD}")
119
+
120
+
121
+ # --- 3. AUTO TRANCO FETCHER & SPIDER CRAWLER ---
122
+ async def fetch_tranco_seeds(session):
123
+ print("🌍 Downloading Tranco Top Domains dataset...")
124
+ try:
125
+ # Download Tranco top 1 million CSV ZIP
126
+ async with session.get("https://tranco-list.eu/top-1m.csv.zip", timeout=30) as resp:
127
+ if resp.status == 200:
128
+ content = await resp.read()
129
+ with zipfile.ZipFile(io.BytesIO(content)) as z:
130
+ csv_filename = z.namelist()[0]
131
+ with z.open(csv_filename) as f:
132
+ lines = f.read().decode('utf-8').splitlines()
133
+ # Take top 5000 domains to start seeding
134
+ for line in lines[:5000]:
135
+ parts = line.split(",")
136
+ if len(parts) >= 2:
137
+ domain = parts[1].strip()
138
+ try:
139
+ scrape_queue.put_nowait(f"https://{domain}/")
140
+ except asyncio.QueueFull:
141
+ break
142
+ print("✅ Tranco dataset loaded into Scraper Queue!")
143
+ except Exception as e:
144
+ print("⚠️ Failed to download Tranco, using fallback seeds.", e)
145
+ fallback = ["https://wikipedia.org/", "https://github.com/", "https://bbc.com/"]
146
+ for url in fallback:
147
+ await scrape_queue.put(url)
148
+
149
+ async def extract_page_data(session, url):
150
+ headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) NebulixBot/1.0"}
151
+ try:
152
+ async with session.get(url, headers=headers, timeout=10) as response:
153
+ if response.status != 200: return None
154
+
155
+ html = await response.text()
156
+ soup = BeautifulSoup(html, 'html.parser')
157
+
158
+ title = soup.title.string if soup.title else ""
159
+ desc_tag = soup.find("meta", {"name": "description"})
160
+ description = desc_tag["content"] if desc_tag else ""
161
+
162
+ fav_tag = soup.find("link", {"rel": lambda x: x and 'icon' in x.lower()})
163
+ favicon = urljoin(url, fav_tag["href"]) if fav_tag else ""
164
+
165
+ images = [urljoin(url, img['src']) for img in soup.find_all('img', src=True)][:10]
166
+ videos = [urljoin(url, vid['src']) for vid in soup.find_all(['video', 'iframe'], src=True)][:5]
167
+
168
+ docs = []
169
+ new_links = []
170
+ for a in soup.find_all('a', href=True):
171
+ href = urljoin(url, a['href'])
172
+ if href.endswith(('.pdf', '.doc', '.docx')):
173
+ docs.append(href)
174
+ # Ensure it's a web link and user requirement: filter out 'tranco' keyword in URLs
175
+ elif href.startswith('http') and "tranco" not in href.lower():
176
+ new_links.append(href)
177
+
178
+ return {
179
+ "url": url,
180
+ "title": title.strip() if title else "",
181
+ "description": description.strip() if description else "",
182
+ "favicon": favicon,
183
+ "images": images,
184
+ "videos": videos,
185
+ "docs": docs[:5],
186
+ "new_links": new_links[:25] # Spider branching limit
187
+ }
188
+ except Exception: return None
189
+
190
+ async def scraper_worker(worker_id, session):
191
+ while True:
192
+ url = await scrape_queue.get()
193
+
194
+ if url not in visited_urls:
195
+ visited_urls.add(url)
196
+
197
+ data = await extract_page_data(session, url)
198
+ if data:
199
+ # 1. SPIDER EFFECT: Put new discovered links into the queue
200
+ for link in data.pop("new_links"):
201
+ if link not in visited_urls:
202
+ try:
203
+ scrape_queue.put_nowait(link)
204
+ except asyncio.QueueFull:
205
+ pass # If queue is full, ignore to save memory
206
+
207
+ # 2. Save data to TODAY's date file
208
+ date_str = datetime.now().strftime("%Y-%m-%d")
209
+ daily_file = f"date-{date_str}.jsonl"
210
+
211
+ async with merge_lock:
212
+ with open(daily_file, "a", encoding="utf-8") as f:
213
+ f.write(json.dumps(data) + "\n")
214
 
215
+ # Update Memory without duplicate (handled in function)
216
+ process_item_into_memory(data)
217
+
218
+ # 3. Check Threshold for Auto-Merge
219
+ await check_and_merge_datasets()
220
+
221
+ scrape_queue.task_done()
222
+ await asyncio.sleep(0.05) # Yield event loop
 
 
 
 
 
 
223
 
224
+ async def start_scraper():
225
+ async with aiohttp.ClientSession() as session:
226
+ # Fetch initial seeds if queue is empty
227
+ if scrape_queue.empty():
228
+ await fetch_tranco_seeds(session)
229
+
230
+ # 15 Workers for full CPU utilization without crashing HF Basic Space
231
+ workers = [asyncio.create_task(scraper_worker(i, session)) for i in range(15)]
232
+ await asyncio.gather(*workers)
233
 
234
 
235
+ # --- 4. FASTAPI & LOGGING LIFESPAN ---
236
  async def log_query_encrypted(req_id: str, query: str):
237
+ try:
238
+ encrypted_q = cipher_suite.encrypt(query.encode()).decode()
239
+ now = datetime.now()
240
+ date_str = now.strftime("%Y-%m-%d")
241
+ log_file = os.path.join(LOG_DIR, f"search_logs_{date_str}.jsonl")
242
+
243
+ log_entry = {"id": req_id, "query": encrypted_q, "timestamp": now.isoformat()}
244
+ with open(log_file, "a", encoding="utf-8") as f:
245
+ f.write(json.dumps(log_entry) + "\n")
246
+ f.flush()
247
+ except Exception as e: print("Log Error:", e)
248
+
249
+ @asynccontextmanager
250
+ async def lifespan(app: FastAPI):
251
+ load_all_data() # Load existing dataset + date files + deduplicate
252
+ scraper_task = asyncio.create_task(start_scraper()) # Start Spider Background
253
+ yield
254
+ scraper_task.cancel()
255
+
256
+ app = FastAPI(title="Spider Search Engine API", lifespan=lifespan)
257
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
258
+
259
+
260
+ # --- 5. SMART SEARCH ENDPOINT ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  @app.get("/search")
262
+ async def search(q: str = Query(..., description="Search Query"), background_tasks: BackgroundTasks = BackgroundTasks()):
263
  req_id = str(uuid.uuid4())
 
 
264
  background_tasks.add_task(log_query_encrypted, req_id, q)
265
 
266
+ is_adult = any(word in q.lower() for word in ["porn", "sex", "xxx", "nude", "adult"])
267
+ query_lower = q.lower()
268
+ scored_results = []
 
 
 
 
 
 
 
 
 
269
 
270
+ async with merge_lock:
271
+ for index, data in enumerate(search_corpus):
272
+ title_score = fuzz.token_set_ratio(query_lower, data["title"])
273
+ desc_score = fuzz.token_set_ratio(query_lower, data["description"])
274
+ domain_score = fuzz.token_set_ratio(query_lower, data["domain"])
275
+
276
+ # Weighted scoring mechanism
277
+ total_score = (title_score * 0.50) + (desc_score * 0.35) + (domain_score * 0.15)
278
+
279
+ if total_score > 40:
280
+ scored_results.append((total_score, index))
281
+
282
+ scored_results.sort(key=lambda x: x[0], reverse=True)
283
+ top_30 = scored_results[:30]
284
 
285
+ final_results = []
286
+ for score, index in top_30:
287
+ item = raw_data[index].copy()
288
+ item["match_score"] = round(score, 2)
289
+ final_results.append(item)
290
 
 
291
  if len(final_results) == 0:
 
 
292
  response_data = {
293
+ "ai_fallback_triggered": True,
294
+ "message": "AI generated fallback result.",
295
+ "results": [{"url": f"https://www.google.com/search?q={q.replace(' ', '+')}", "title": f"Web Search: {q}", "description": "Local DB empty for this query."}]
296
  }
297
+ else:
298
+ response_data = {"total_results": len(final_results), "results": final_results}
299
 
300
  response_data["request_id"] = req_id
301
 
 
302
  if is_adult:
303
  response_data["safety_message"] = "you are safe you can do anything"
304
  headers = {"X-Safety-Message": "you are safe you can do anything"}
 
306
 
307
  return response_data
308
 
 
 
309
  if __name__ == "__main__":
310
  uvicorn.run(app, host="0.0.0.0", port=7860)