Jawbreaker
Private scam defense for someone you love.
Is this message safe?
Paste a text, email, or DM. Jawbreaker will show the risk, the warning signs, and the safest next step before you reply, click, or pay.
import os import json from dataclasses import asdict from functools import lru_cache from html import escape from pathlib import Path from time import perf_counter, sleep from typing import Any import gradio as gr from fastapi.responses import HTMLResponse from jawbreaker.analyzers import ( build_llama_cpp_analyzer, build_transformers_analyzer, heuristic_analyzer, prediction_to_analysis, repair_prediction, should_apply_heuristic_guard, validate_prediction, ) from jawbreaker.render import render_analysis_html, render_memory_html, render_scanning_html from jawbreaker.schema import ScamAnalysis from jawbreaker.trust import ( build_trusted_note, confidence_metadata, is_low_context_message, low_context_analysis, ) try: import spaces except ImportError: spaces = None EXAMPLES = [ "USPS: Your package is held due to an unpaid fee. Verify now: http://usps-track-secure.example", "Hi Grandma, I lost my phone. This is my new number. Can you send $800 for rent today? Please don't tell Mom.", "Coinbase alert: We received a request to update the phone number on your account. If this wasn't you, call support immediately at [callback number].", "Hi! I'm a recruiter from TikTok Shop. We are looking for a part-time assistant. Flexible remote work, 60 minutes per day, $330-$750 per day. Contact me on WhatsApp at [phone number].", "Your dentist appointment is confirmed for Tuesday at 2:00 PM. Reply C to confirm or R to reschedule.", "Your verification code is 482913. Do not share this code with anyone. We will never ask for it by phone.", ] DEFAULT_TRANSFORMERS_MODEL_ID = "openbmb/MiniCPM5-1B" DEFAULT_ADAPTER_ID = "build-small-hackathon/jawbreaker-minicpm5-1b-lora-v8" DEFAULT_TRANSFORMERS_MAX_TOKENS = 512 LOGO_PATH = Path("jawbreaker_logo.png") FORCE_LIGHT_JS = """() => { const forceLight = () => document.body.classList.remove('dark'); forceLight(); new MutationObserver(forceLight).observe(document.body, { attributes: true, attributeFilter: ['class'], }); }""" FORCE_LIGHT_HEAD = """ """ def app_theme() -> gr.Theme: theme = gr.themes.Soft( primary_hue="red", secondary_hue="slate", neutral_hue="zinc", radius_size="sm", ) theme.set( block_label_background_fill="transparent", block_label_border_color="transparent", block_label_text_color="#6b5144", block_label_text_size="13px", block_label_text_weight="700", ) return theme def app_css() -> str: return Path("style.css").read_text(encoding="utf-8") def gpu_callback(fn): if spaces is None: return fn return spaces.GPU(fn) def _env_int(name: str, default: int | None = None) -> int | None: value = os.getenv(name) if value is None or value == "": return default return int(value) def _env_bool(name: str, default: bool | None = None) -> bool | None: value = os.getenv(name) if value is None or value == "": return default return value.strip().lower() in {"1", "true", "yes", "on"} def default_adapter_id(model_id: str) -> str | None: configured = os.getenv("JAWBREAKER_ADAPTER_ID") if configured is not None: return configured or None if model_id == DEFAULT_TRANSFORMERS_MODEL_ID: return DEFAULT_ADAPTER_ID return None @lru_cache(maxsize=1) def get_analyzer(): backend = current_backend() if backend == "heuristic": return heuristic_analyzer if backend == "llama-cpp": model_path = resolve_model_path() return build_llama_cpp_analyzer( model_path, chat_format=os.getenv("JAWBREAKER_CHAT_FORMAT") or None, n_ctx=_env_int("JAWBREAKER_N_CTX", 1024) or 1024, n_threads=_env_int("JAWBREAKER_N_THREADS", 2), n_gpu_layers=_env_int("JAWBREAKER_N_GPU_LAYERS", 0) or 0, n_batch=_env_int("JAWBREAKER_N_BATCH", 128) or 128, n_ubatch=_env_int("JAWBREAKER_N_UBATCH", 128) or 128, offload_kqv=_env_bool("JAWBREAKER_OFFLOAD_KQV", True), op_offload=_env_bool("JAWBREAKER_OP_OFFLOAD"), max_tokens=_env_int("JAWBREAKER_MAX_TOKENS", 256) or 256, temperature=float(os.getenv("JAWBREAKER_TEMPERATURE", "0")), ) if backend in {"transformers", "zerogpu"}: model_id = os.getenv("JAWBREAKER_TRANSFORMERS_MODEL_ID", DEFAULT_TRANSFORMERS_MODEL_ID) return build_transformers_analyzer( model_id, adapter_id=default_adapter_id(model_id), max_new_tokens=_env_int("JAWBREAKER_MAX_TOKENS", DEFAULT_TRANSFORMERS_MAX_TOKENS) or DEFAULT_TRANSFORMERS_MAX_TOKENS, temperature=float(os.getenv("JAWBREAKER_TEMPERATURE", "0")), device_map=os.getenv("JAWBREAKER_DEVICE_MAP", "auto"), dtype=os.getenv("JAWBREAKER_TORCH_DTYPE", "auto"), trust_remote_code=_env_bool("JAWBREAKER_TRUST_REMOTE_CODE", True) or False, attn_implementation=os.getenv("JAWBREAKER_ATTENTION_IMPLEMENTATION", "eager") or None, ) raise ValueError(f"Unsupported JAWBREAKER_BACKEND: {backend}") def current_backend() -> str: return os.getenv("JAWBREAKER_BACKEND", "llama-cpp").strip().lower() def should_warm_model() -> bool: return _env_bool("JAWBREAKER_WARM_MODEL", True) and current_backend() in {"transformers", "zerogpu"} def resolve_model_path() -> Path: model_path = Path(os.getenv("JAWBREAKER_MODEL_PATH", "models/qwen3-0.6b-gguf/Qwen3-0.6B-Q4_K_M.gguf")) if model_path.exists(): return model_path repo_id = os.getenv("JAWBREAKER_MODEL_REPO", "unsloth/Qwen3-0.6B-GGUF") filename = os.getenv("JAWBREAKER_MODEL_FILE", "Qwen3-0.6B-Q4_K_M.gguf") cache_dir = os.getenv("JAWBREAKER_MODEL_CACHE") try: from huggingface_hub import hf_hub_download except ImportError as exc: raise RuntimeError("huggingface_hub is required to download the configured model file.") from exc return Path(hf_hub_download(repo_id=repo_id, filename=filename, cache_dir=cache_dir)) def run_analysis(message: str, memory: list[dict] | None) -> ScamAnalysis: memory = memory or [] heuristic = ScamAnalysis.from_heuristics(message, memory) started = perf_counter() try: prediction = get_analyzer()(message) except Exception as exc: print( "jawbreaker analyzer_error fallback=heuristic " f"elapsed={perf_counter() - started:.2f}s error={exc!r}", flush=True, ) heuristic.summary = f"{heuristic.summary} Jawbreaker used its safety fallback because the model response was not usable." return heuristic raw_validation_errors = validate_prediction(prediction) prediction = repair_prediction(prediction) validation_errors = validate_prediction(prediction) model_analysis = prediction_to_analysis(prediction, similar_memory=heuristic.similar_memory) print(f"jawbreaker analyze elapsed={perf_counter() - started:.2f}s", flush=True) if should_use_heuristic_guard(model_analysis, heuristic, validation_errors, message): print( "jawbreaker guard=heuristic " f"model_risk={model_analysis.risk_level} heuristic_risk={heuristic.risk_level}", flush=True, ) return heuristic if raw_validation_errors: print(f"jawbreaker schema_repaired errors={raw_validation_errors}", flush=True) return model_analysis def warm_model_impl() -> None: backend = current_backend() if not should_warm_model(): print(f"jawbreaker warm_model skip backend={backend}", flush=True) return None started = perf_counter() print(f"jawbreaker warm_model start backend={backend}", flush=True) get_analyzer() print(f"jawbreaker warm_model ready elapsed={perf_counter() - started:.2f}s", flush=True) return None @gpu_callback def warm_model() -> None: return warm_model_impl() def memory_entry_from_analysis(message: str, analysis: ScamAnalysis) -> dict[str, Any]: return { "summary": analysis.summary, "scam_type": analysis.scam_type, "risk_level": analysis.risk_level, "fingerprint": analysis.scam_dna, "text": message[:240], } def render_current_memory(memory: list[dict]) -> str: return render_memory_html(ScamAnalysis.from_heuristics("", memory), memory) def render_save_status(message: str = "") -> str: if not message: return "
" return f"Private scam defense for someone you love.
Paste a text, email, or DM. Jawbreaker will show the risk, the warning signs, and the safest next step before you reply, click, or pay.
When something feels off, check it here.
Paste a text, email, or DM that feels off. Jawbreaker turns it into a simple safety note: the risk, the warning signs, and the safest next step before you click, reply, or pay.
MESSAGE TO CHECK
""" ) message = gr.Textbox( label=None, show_label=False, placeholder="Paste a text, email, or DM here.", lines=10, max_lines=16, ) analyze = gr.Button("Check message", variant="primary", elem_classes=["check-btn"]) gr.Examples(examples=EXAMPLES, inputs=message, label="Try a sample") memory = gr.HTML(render_current_memory([])) with gr.Column(scale=7): result = gr.HTML( """SYSTEM STANDING BY
Paste any text message, email, or DM on the left. The local model will evaluate risk factors, unpack the scam strategy, and deliver a plain-English protection plan.
1. Copy a text message from your phone or an email that feels off.
2. Paste it into the input area on the left of this screen.
3. Click RUN SCAM DETECTOR to analyze it with private local AI.