Spaces:
Running
Running
| import os | |
| import json | |
| import redis | |
| class RedisManager: | |
| """ | |
| Manages connection to Redis for caching deepfake detection results | |
| and tracking async task states. Falls back to in-memory dictionaries if Redis is offline. | |
| """ | |
| def __init__(self, host: str = "localhost", port: int = 6379, db: int = 0): | |
| self.host = host | |
| self.port = port | |
| self.db = db | |
| self.redis_client = None | |
| self.local_cache = {} | |
| self.local_tasks = {} | |
| # Render/managed Redis providers expose a single connection string (REDIS_URL) | |
| # instead of separate host/port; prefer it when present. | |
| redis_url = os.environ.get("REDIS_URL") | |
| if not self._is_redis_reachable(redis_url, host, port): | |
| print("Warning: Redis is not reachable. Falling back to in-memory cache.") | |
| self.redis_client = None | |
| return | |
| try: | |
| if redis_url: | |
| self.redis_client = redis.from_url( | |
| redis_url, socket_connect_timeout=2.0, decode_responses=True | |
| ) | |
| else: | |
| # Short timeout to avoid blocking startup if Redis is down | |
| self.redis_client = redis.Redis( | |
| host=host, | |
| port=port, | |
| db=db, | |
| socket_connect_timeout=2.0, | |
| decode_responses=True | |
| ) | |
| # Test connection | |
| self.redis_client.ping() | |
| print(f"Connected to Redis at {redis_url or f'{host}:{port}'}") | |
| except (redis.ConnectionError, redis.TimeoutError): | |
| print(f"Warning: Redis is not reachable. Falling back to in-memory cache.") | |
| self.redis_client = None | |
| def _is_redis_reachable(self, redis_url: str | None, host: str, port: int) -> bool: | |
| """ | |
| Quickly check if the Redis port is listening using a basic socket connection | |
| to avoid library-specific blocking hangs during DNS resolution or connect. | |
| """ | |
| import socket | |
| from urllib.parse import urlparse | |
| target_host = host | |
| target_port = port | |
| if redis_url: | |
| try: | |
| parsed = urlparse(redis_url) | |
| target_host = parsed.hostname or host | |
| target_port = parsed.port or port | |
| except Exception: | |
| pass | |
| try: | |
| # Use a quick 1.0 second connection timeout to see if port is open | |
| s = socket.create_connection((target_host, target_port), timeout=1.0) | |
| s.close() | |
| return True | |
| except Exception: | |
| return False | |
| def get_cached_result(self, file_hash: str) -> dict | None: | |
| """ | |
| Retrieves cached result by file hash. | |
| """ | |
| key = f"deepfake:result:{file_hash}" | |
| if self.redis_client: | |
| try: | |
| data = self.redis_client.get(key) | |
| if data: | |
| return json.loads(data) | |
| except Exception as e: | |
| print(f"Redis get cache error: {e}") | |
| return self.local_cache.get(file_hash) | |
| def cache_result(self, file_hash: str, result: dict, expire_sec: int = 86400) -> None: | |
| """ | |
| Caches detection result. | |
| """ | |
| key = f"deepfake:result:{file_hash}" | |
| if self.redis_client: | |
| try: | |
| self.redis_client.setex(key, expire_sec, json.dumps(result)) | |
| # Add to a history set for easy listing | |
| self.redis_client.sadd("deepfake:history:hashes", file_hash) | |
| except Exception as e: | |
| print(f"Redis set cache error: {e}") | |
| self.local_cache[file_hash] = result | |
| def get_task_status(self, task_id: str) -> dict | None: | |
| """ | |
| Retrieves task status. | |
| """ | |
| key = f"deepfake:task:{task_id}" | |
| if self.redis_client: | |
| try: | |
| data = self.redis_client.get(key) | |
| if data: | |
| return json.loads(data) | |
| except Exception as e: | |
| print(f"Redis get task error: {e}") | |
| return self.local_tasks.get(task_id) | |
| def update_task_status(self, task_id: str, status: dict, expire_sec: int = 3600) -> None: | |
| """ | |
| Updates background task status. | |
| """ | |
| key = f"deepfake:task:{task_id}" | |
| if self.redis_client: | |
| try: | |
| self.redis_client.setex(key, expire_sec, json.dumps(status)) | |
| except Exception as e: | |
| print(f"Redis set task error: {e}") | |
| self.local_tasks[task_id] = status | |
| def get_history(self) -> list[dict]: | |
| """ | |
| Gets a history list of all completed analyses. | |
| """ | |
| history = [] | |
| if self.redis_client: | |
| try: | |
| hashes = self.redis_client.smembers("deepfake:history:hashes") | |
| for file_hash in hashes: | |
| res = self.get_cached_result(file_hash) | |
| if res: | |
| # Strip raw base64 images from history list to keep response lightweight | |
| light_res = res.copy() | |
| if "frames" in light_res: | |
| for frame in light_res["frames"]: | |
| if "image" in frame: | |
| frame["image"] = None # Remove heavy image payload | |
| if "faces" in frame: | |
| for face in frame["faces"]: | |
| if "crop_b64" in face: | |
| face["crop_b64"] = None | |
| history.append(light_res) | |
| # Sort by timestamp, newest first | |
| history.sort(key=lambda x: x.get("timestamp", 0), reverse=True) | |
| return history | |
| except Exception as e: | |
| print(f"Redis get history error: {e}") | |
| # Fallback to local memory history | |
| for file_hash, res in self.local_cache.items(): | |
| light_res = res.copy() | |
| if "frames" in light_res: | |
| for frame in light_res["frames"]: | |
| if "image" in frame: | |
| frame["image"] = None | |
| if "faces" in frame: | |
| for face in frame["faces"]: | |
| if "crop_b64" in face: | |
| face["crop_b64"] = None | |
| history.append(light_res) | |
| history.sort(key=lambda x: x.get("timestamp", 0), reverse=True) | |
| return history | |