feat: complete local integration of AI mind codebase (brain, memory, etc.) in text2video, supporting both Direct Codebase execution and Remote HTTP fallback
3d7a63c | # core/searcher.py | |
| import re | |
| import time | |
| import hashlib | |
| import threading | |
| import requests | |
| from bs4 import BeautifulSoup | |
| from ddgs import DDGS | |
| from concurrent.futures import ThreadPoolExecutor, as_completed | |
| _USER_AGENTS = [ | |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", | |
| "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", | |
| "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", | |
| ] | |
| _ua_index = 0 | |
| _ua_lock = threading.Lock() | |
| def _next_ua(): | |
| global _ua_index | |
| with _ua_lock: | |
| ua = _USER_AGENTS[_ua_index % len(_USER_AGENTS)] | |
| _ua_index += 1 | |
| return ua | |
| class WebSearcher: | |
| def __init__(self): | |
| self.ddgs = DDGS() | |
| self.scraper_pool = ThreadPoolExecutor(max_workers=3) | |
| self._cache = {} | |
| self._cache_ttl = 300 # 5 minutes | |
| self._cache_lock = threading.Lock() | |
| # ββ Cache helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _cache_key(self, query): | |
| return hashlib.md5(query.lower().strip().encode()).hexdigest() | |
| def _cache_get(self, key): | |
| with self._cache_lock: | |
| entry = self._cache.get(key) | |
| if entry and time.time() - entry["ts"] < self._cache_ttl: | |
| return entry["value"] | |
| return None | |
| def _cache_set(self, key, value): | |
| with self._cache_lock: | |
| self._cache[key] = {"value": value, "ts": time.time()} | |
| # Evict old entries if cache is too large | |
| if len(self._cache) > 100: | |
| oldest = min(self._cache.items(), key=lambda x: x[1]["ts"]) | |
| del self._cache[oldest[0]] | |
| # ββ Search βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def search(self, query, max_results=6): | |
| cache_key = self._cache_key(f"search:{query}") | |
| cached = self._cache_get(cache_key) | |
| if cached: | |
| return cached | |
| results = [] | |
| seen_urls = set() | |
| try: | |
| for r in self.ddgs.text(query, max_results=max_results + 2): | |
| url = r.get("href", "") | |
| if url in seen_urls: | |
| continue | |
| # Skip low-quality sources | |
| if any(skip in url for skip in ["reddit.com/r/", "quora.com", "pinterest.com"]): | |
| continue | |
| seen_urls.add(url) | |
| results.append({ | |
| "title": r.get("title", ""), | |
| "url": url, | |
| "snippet": r.get("body", ""), | |
| }) | |
| if len(results) >= max_results: | |
| break | |
| except Exception as e: | |
| print(f"β οΈ Search error: {e}") | |
| self._cache_set(cache_key, results) | |
| return results | |
| # ββ Scrape βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def scrape_page(self, url): | |
| try: | |
| headers = { | |
| "User-Agent": _next_ua(), | |
| "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", | |
| "Accept-Language": "en-US,en;q=0.5", | |
| } | |
| res = requests.get(url, headers=headers, timeout=6, allow_redirects=True) | |
| res.raise_for_status() | |
| soup = BeautifulSoup(res.text, "html.parser") | |
| # Remove boilerplate | |
| for tag in soup(["script", "style", "nav", "footer", "header", | |
| "aside", "form", "noscript", "iframe", "ads", | |
| "[class*='cookie']", "[class*='banner']", | |
| "[id*='sidebar']", "[id*='nav']"]): | |
| tag.decompose() | |
| # Try to get the main content area | |
| main = ( | |
| soup.find("main") or | |
| soup.find("article") or | |
| soup.find(id="content") or | |
| soup.find(class_="content") or | |
| soup.find(class_="post-content") or | |
| soup.body | |
| ) | |
| if main: | |
| text = main.get_text(separator=" ", strip=True) | |
| else: | |
| text = soup.get_text(separator=" ", strip=True) | |
| # Clean up whitespace and repeated chars | |
| text = re.sub(r"\s{3,}", " ", text) | |
| text = re.sub(r"\.{4,}", "...", text) | |
| return text[:5000] | |
| except requests.exceptions.Timeout: | |
| return None | |
| except requests.exceptions.HTTPError: | |
| return None | |
| except Exception as e: | |
| print(f"β οΈ Scrape error ({url[:40]}): {e}") | |
| return None | |
| # ββ Parallel search + scrape βββββββββββββββββββββββββββββββββββββββββββββ | |
| def search_and_read_parallel(self, query, max_scrape=3, max_results=8): | |
| """Enhanced search with deeper content extraction.""" | |
| results = self.search(query, max_results=max_results) | |
| if not results: | |
| return [] | |
| future_to_idx = {} | |
| for i, r in enumerate(results[:max_scrape]): | |
| future = self.scraper_pool.submit(self.scrape_page, r["url"]) | |
| future_to_idx[future] = i | |
| for future in as_completed(future_to_idx, timeout=15): | |
| idx = future_to_idx[future] | |
| try: | |
| content = future.result() | |
| if content: | |
| results[idx]["full_content"] = content | |
| # Extract key facts | |
| sentences = content.split('.')[:20] | |
| results[idx]["key_points"] = [s.strip() for s in sentences if len(s) > 40 and len(s) < 200] | |
| except Exception: | |
| pass | |
| return results | |
| def search_and_read(self, query, deep=True): | |
| """Legacy sequential method for compatibility.""" | |
| results = self.search(query) | |
| if deep: | |
| for r in results[:2]: | |
| content = self.scrape_page(r["url"]) | |
| if content: | |
| r["full_content"] = content | |
| return results |