Here’s a minimal Chrome extension server example that hides tweets containing “Bounty” chocolate images using a self‑hosted FastVLM classifier.
Folder structure:
extension/
manifest.json
content.js
background.js
server/
app.py
requirements.txt
manifest.json:
{
"manifest_version": 3,
"name": "Mute Bounty Images on X",
"version": "1.0.0",
"permissions": ["scripting", "activeTab"],
"host_permissions": ["
x.com/*", "
twitter.com/*", "http://localhost:8000/*"],
"background": { "service_worker": "background.js" },
"content_scripts": [
{
"matches": ["
x.com/*", "
twitter.com/*"],
"js": ["content.js"],
"run_at": "document_idle"
}
]
}
background.js:
chrome.runtime.onMessage.addListener(async (msg, _sender, sendResponse) => {
if (msg.type "CLASSIFY_URLS") {
try {
const res = await fetch("http://localhost:8000/classify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ urls: msg.urls })
});
const data = await res.json();
sendResponse({ ok: true, result: data });
} catch (e) {
sendResponse({ ok: false, error: String(e) });
}
return true; // keep channel open
}
});
content.js:
const seen = new WeakSet();
function extractImageUrls(tweet) {
// X uses <img src="...name=smalllargeorig">; prefer src
return [...tweet.querySelectorAll('img[src*="
twimg.com/media/"]')].map(img => img.src);
}
// Batch classify unique URLs to reduce calls
let queue = new Set();
let pending = false;
async function flush() {
if (pending queue.size 0) return;
pending = true;
const urls = [...queue];
queue.clear();
chrome.runtime.sendMessage({ type: "CLASSIFY_URLS", urls }, (resp) => {
pending = false;
if (!resp?.ok) return;
const verdicts = resp.result; // { [url]: {is_bounty:boolean, score:number} }
document.querySelectorAll('article[data-testid="tweet"]').forEach(t => {
if (seen.has(t)) return;
const imgs = extractImageUrls(t);
if (imgs.some(u => verdicts[u]?.is_bounty)) {
t.style.display = "none"; // remove from DOM
}
seen.add(t);
});
// in case more arrived during call
flush();
});
}
const observer = new MutationObserver(() => {
const tweets = document.querySelectorAll('article[data-testid="tweet"]');
tweets.forEach(t => {
if (seen.has(t)) return;
const urls = extractImageUrls(t);
urls.forEach(u => queue.add(u));
});
flush();
});
observer.observe(document.body, { subtree: true, childList: true });
Server (self-hosted FastVLM, Python FastAPI example):
server/requirements.txt:
fastapi
uvicorn
torch
Pillow
transformers
server/app.py:
from fastapi import FastAPI
from pydantic import BaseModel
import requests
from io import BytesIO
from PIL import Image
import torch
class DummyModel:
def init(self):
pass
def score(self, image: Image.Image) -> float:
# return pseudo score; in real use, run FastVLM and produce a probability
return 0.0
model = DummyModel()
app = FastAPI()
class Req(BaseModel):
urls: list[str]
@app.post("/classify")
def classify(req: Req):
out = {}
for url in req.urls:
try:
img =
Image.open(BytesIO(requests.get(url, timeout=10).content)).convert("RGB")
score = model.score(img)
out[url] = {"is_bounty": score > 0.5, "score": float(score)}
except Exception:
out[url] = {"is_bounty": False, "score": 0.0}
return out
Run the server: pip install -r requirements.txt && uvicorn app:app --host 0.0.0.0 --port 8000, then load the extension in Chrome (Developer Mode > Load unpacked > select extension folder).