""" 100% local server - makes no external network calls. Runs on your machine only (127.0.0.1) to serve the UI and analyze files locally. """ import os import sys os.environ.setdefault("HF_HUB_DISABLE_SYMLINKS_WARNING", "1") # Hidden "app" mode (launched via TruScan.vbs with pythonw.exe): there is no # console, so send every print/log line to cache\logs\truescan.log instead. if sys.stdout is None or os.environ.get("TRUESCAN_HIDDEN"): _log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cache", "logs") os.makedirs(_log_dir, exist_ok=True) _log_path = os.path.join(_log_dir, "truescan.log") try: if os.path.getsize(_log_path) > 5 * 1024 * 1024: # keep it small os.replace(_log_path, _log_path + ".old") except OSError: pass sys.stdout = sys.stderr = open(_log_path, "a", encoding="utf-8", buffering=1) # Keep the last lines of output in memory so the app window can show them # (the "black screen" moved inside the app: click the status pill). import collections as _collections _RECENT_LINES = _collections.deque(maxlen=200) class _Tee: def __init__(self, stream): self._stream = stream self._buf = "" def write(self, text): try: if self._stream is not None: self._stream.write(text) except Exception: pass self._buf += text while "\n" in self._buf: line, self._buf = self._buf.split("\n", 1) line = line.rstrip("\r") if line.strip(): _RECENT_LINES.append(line[-400:]) # tqdm progress bars use \r without \n - keep the latest one visible if "\r" in self._buf: parts = [p.strip() for p in self._buf.split("\r") if p.strip()] last = parts[-1] if parts else "" self._buf = "" if last: _RECENT_LINES.append(last[-400:]) return len(text) def flush(self): try: if self._stream is not None: self._stream.flush() except Exception: pass def __getattr__(self, name): return getattr(self._stream, name) sys.stdout = _Tee(sys.stdout) sys.stderr = _Tee(sys.stderr) import json import hashlib import tempfile import webbrowser import traceback import threading import time from concurrent.futures import ThreadPoolExecutor # Project layout: this file lives at the project root; the actual app code # lives in route/, and model weights/packages live in models/. Both need # to be importable — route/ for `from analysis_engine import ...` and for # analysis_engine's own sibling imports (ai_detector, face_detector, ...), # and models/ because it holds Python PACKAGES (aidetector/, bioverify_engine/) # that get imported directly (e.g. `import aidetector`), not just data files. _PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) _ROUTE_DIR = os.path.join(_PROJECT_ROOT, "route") _MODELS_DIR = os.path.join(_PROJECT_ROOT, "models") for _p in (_ROUTE_DIR, _MODELS_DIR): if _p not in sys.path: sys.path.insert(0, _p) from flask import Flask, request, jsonify, render_template from werkzeug.utils import secure_filename # --------------------------------------------------------------------------- # Optional detector files. If one is missing from this installation (e.g. it # was never committed to GitHub), the app still starts: that one signal # reports "unavailable" (the same value the detector itself returns when its # model can't load) and the status pill lists it as missing. # --------------------------------------------------------------------------- _OPTIONAL_MODULES = { "nonescape_bridge": {"classify_image": None}, "steganographia_detector": {"classify_image": None}, "pixel_forensics_detector": {"classify_image": None}, "sdxl_face_detector": {"classify_image": None}, "video_metadata_detector": {"scan_video_metadata": (0, [], False)}, "voice_deepfake_detector": {"classify_audio": (0, None, False)}, "voice_descriptive_stats": {}, "speaker_verification": {}, } MISSING_MODULES = [] def _install_missing_stubs(): import importlib.util import types for name, funcs in _OPTIONAL_MODULES.items(): if importlib.util.find_spec(name) is not None: continue mod = types.ModuleType(name) mod.__truescan_stub__ = True for fn, ret in funcs.items(): setattr(mod, fn, (lambda ret=ret: (lambda *a, **k: ret))()) def __getattr__(attr, _name=name): if attr.startswith("__"): raise AttributeError(attr) def _missing(*a, **k): raise RuntimeError(f"{_name}.py is missing from this installation") return _missing mod.__getattr__ = __getattr__ sys.modules[name] = mod MISSING_MODULES.append(name) if MISSING_MODULES: print(f"[app] Missing detector files (those signals are skipped): {', '.join(MISSING_MODULES)}", flush=True) _install_missing_stubs() from analysis_engine import analyze_image_bytes, analyze_video_bytes, r import forensic_scope_api import document_forensics import voice_deepfake_detector import voice_descriptive_stats import speaker_verification import plate_reader import vehicle_classifier import image_enhancer import video_enhancer import audio_extractor import ai_detector import deepfake_face_detector import face_detector import efficientnetb3_detector import sdxl_face_detector import noise_residual_detector import clone_detection_module import pixel_forensics_detector import community_forensics_detector import steganographia_detector import insightface_detector app = Flask(__name__) import logging class _NoPolling(logging.Filter): """Hides the UI's every-few-seconds counter polling from the console so real messages (warm-up, errors, timings) stay readable.""" _QUIET = ("/api/calibration-log/count", "/api/correct-plate/count", "/api/ready", "/api/activity", "/favicon.ico") def filter(self, record): msg = record.getMessage() return not any(p in msg for p in self._QUIET) logging.getLogger("werkzeug").addFilter(_NoPolling()) app.config["MAX_CONTENT_LENGTH"] = 300 * 1024 * 1024 # 300MB max _SHARE_PW_FILE = os.path.join(_PROJECT_ROOT, "cache", "share_password.txt") _share_pw_cache = {"mtime": None, "pw": ""} def _share_password(): """Password for remote visitors: APP_PASSWORD (server/Docker) or the one set by "Share Online.bat" (Windows PC shared through Tailscale).""" if os.environ.get("APP_PASSWORD"): return os.environ["APP_PASSWORD"] try: m = os.path.getmtime(_SHARE_PW_FILE) except OSError: return "" if m != _share_pw_cache["mtime"]: with open(_SHARE_PW_FILE, "r", encoding="utf-8-sig") as f: _share_pw_cache.update(mtime=m, pw=f.read().strip()) return _share_pw_cache["pw"] _PROXY_HEADERS = ("X-Forwarded-For", "X-Forwarded-Host", "X-Forwarded-Proto", "Tailscale-User-Login", "Tailscale-Funnel-Request") def _is_local_user(): """The person sitting at this PC (app window) — not a visitor arriving through Tailscale serve/funnel, which also connects from 127.0.0.1 but adds proxy headers.""" return (request.remote_addr in ("127.0.0.1", "::1") and not any(h in request.headers for h in _PROXY_HEADERS) and not os.environ.get("TRUESCAN_SERVER")) _AUTH_COOKIE = "ts_auth" _OPEN_PATHS = ("/login", "/logout", "/favicon.ico") def _session_token(pw): import hashlib import hmac return hmac.new(pw.encode(), b"truscan-session-v1", hashlib.sha256).hexdigest() def _password_ok(given, pw): import hmac return given is not None and hmac.compare_digest(str(given).encode(), pw.encode()) _LOGIN_PAGE = """