Text Classification
Transformers
Safetensors
Chinese
spam-detection
advertising-detection
ad-classification
chinese
modernbert
mmbert
multi-task
four-head
Instructions to use x6nux/laya-ad with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use x6nux/laya-ad with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="x6nux/laya-ad")# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("x6nux/laya-ad", device_map="auto") - Notebooks
- Google Colab
- Kaggle
File size: 10,841 Bytes
e2492ec | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 | #!/usr/bin/env python3
"""Real-time Jev cleaning for gmeow ban records.
Maps each ban record to the jev request state schema and runs the four
questions (is_ad / ad_kind / ad_scope / severity). Unique texts are judged
once; results land in an append-only JSONL that doubles as the resume state.
Record semantics:
Reason=UserName -> the ban was triggered by nickname/bio: text goes to
sender.first_name + sender.bio, message.text stays empty
Reason=SpamGP -> a group message: text goes to message.text
Reason=ExReply -> reply under an external channel post ("Source:"/"Msg:"):
Msg goes to message.text, Source becomes chat.title
Usage:
python scripts/jev_clean_gmeow.py --once
python scripts/jev_clean_gmeow.py --watch --interval 20
"""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import sys
import threading
import time
import urllib.error
import urllib.request
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from clean_gmeow_ads import ( # noqa: E402
classify, normalize, parse_source_msg, quality_flags, strip_contacts)
TEMPLATE = Path(".work/teacher/jev_request_template.json")
LINK_RE = re.compile(r"https?://|t\.me/|www\.", re.I)
HANDLE_RE = re.compile(r"@([A-Za-z0-9_]{3,})")
def load_env(path: str = ".work/teacher/.env") -> dict:
cfg = {}
for line in Path(path).read_text(encoding="utf-8").splitlines():
if "=" in line:
k, v = line.split("=", 1)
cfg[k.strip()] = v.strip()
return cfg
def text_key(text: str) -> str:
return hashlib.sha1(text.encode("utf-8")).hexdigest()[:16]
def make_state(group: dict, template_state: dict) -> dict:
"""Build the jev state for one deduped text group."""
text = group["text"]
reason = group["reason"]
channel = group.get("channel")
base = json.loads(json.dumps(template_state)) # deep copy
base["chat"] = {"id": next(iter(group["chat_ids"]), None),
"title": channel or ""}
base["message"].update({
"text": "",
"has_link": bool(LINK_RE.search(text)),
"has_mention": bool(HANDLE_RE.search(text)),
"has_media": reason == "Ad Image",
"is_forwarded": reason in ("ExReply", "SpamGP"),
"length": len(text),
})
sender = base["sender"]
sender.update({
"user_id": next(iter(group["tgids"]), None),
"username": (HANDLE_RE.search(text).group(1)
if reason == "UserName" and HANDLE_RE.search(text) else ""),
"first_name": "",
"bio": "",
"bio_links": [],
"is_premium": False,
"age_hours": 0,
"age_known": False,
"msgs_in_group": 0,
"prior_ad_hits": group["n"],
})
if reason == "UserName":
nick, sep, bio = text.partition("||")
sender["first_name"] = nick.strip()
sender["bio"] = bio.strip() if sep else text
else:
base["message"]["text"] = text
sender["first_name"] = "群成员"
# the ban list itself is evidence: tell the model what the bot flagged
base["known_ad_patterns"] = f"反广告机器人已按 reason={reason} 封禁该账号"
base["matched_rules"] = [
{"id": i + 1, "name": kw, "category": group["rule_kind"], "note": text[:200]}
for i, kw in enumerate(group["rule_hits"][:5])
]
return base
def request_teacher(cfg: dict, questions: dict, state: dict, retries: int = 8):
url = cfg["TEACHER_URL"]
payload = json.dumps(
{"model": cfg.get("TEACHER_MODEL", "jev-1.13"),
"questions": questions, "state": state},
ensure_ascii=False).encode("utf-8")
last = None
for attempt in range(retries):
req = urllib.request.Request(url, data=payload, method="POST", headers={
"Authorization": "Bearer " + cfg["TEACHER_KEY"],
"Content-Type": "application/json",
"Accept": "application/json",
"Accept-Encoding": "identity",
"User-Agent": "laya-ad-gmeow/1.0"})
try:
with urllib.request.urlopen(req, timeout=180) as resp:
return json.loads(resp.read().decode("utf-8", "replace"))
except urllib.error.HTTPError as exc:
body = exc.read().decode("utf-8", "replace")[:300]
last = f"HTTP {exc.code}: {body}"
if exc.code == 429:
continue
if exc.code in (500, 502, 503, 504):
time.sleep(2 * (attempt + 1))
continue
break
except Exception as exc: # noqa: BLE001
last = f"{type(exc).__name__}: {exc}"
time.sleep(2 * (attempt + 1))
raise RuntimeError(last)
def parse_answers(resp: dict) -> dict:
a = resp.get("answers") or {}
out = {}
for qid in ("is_ad", "ad_kind", "ad_scope"):
v = a.get(qid) or {}
out[qid] = v.get("choice")
out[qid + "_conf"] = v.get("confidence")
sev = a.get("severity") or {}
out["severity"] = sev.get("score")
out["severity_conf"] = sev.get("confidence")
return out
def build_groups(store: Path) -> dict[str, dict]:
groups: dict[str, dict] = {}
for line in store.open(encoding="utf-8"):
line = line.strip()
if not line:
continue
rec = json.loads(line)
channel, msg = parse_source_msg(rec["info"])
text = normalize(msg)
if not text:
continue
flags = quality_flags(text, *strip_contacts(text)[1:])
if "placeholder" in flags:
continue
key = text_key(text)
g = groups.get(key)
if g is None:
kind, score, hits = classify(text, channel)
g = groups[key] = {
"key": key, "text": text, "channel": channel,
"reason": rec["reason"], "reasons": {},
"tgids": set(), "chat_ids": set(), "n": 0,
"first_seen": rec["recorded_at"], "last_seen": rec["recorded_at"],
"flags": flags, "rule_kind": kind, "rule_score": score,
"rule_hits": hits,
}
g["n"] += 1
g["reasons"][rec["reason"]] = g["reasons"].get(rec["reason"], 0) + 1
g["reason"] = max(g["reasons"], key=g["reasons"].get)
g["tgids"].add(rec["tgid"])
if rec["chat_id"] is not None:
g["chat_ids"].add(rec["chat_id"])
g["first_seen"] = min(g["first_seen"], rec["recorded_at"])
g["last_seen"] = max(g["last_seen"], rec["recorded_at"])
return groups
def load_done(out: Path) -> set[str]:
done: set[str] = set()
if out.exists():
for line in out.open(encoding="utf-8"):
try:
done.add(json.loads(line)["key"])
except Exception: # noqa: BLE001
continue
return done
def judge_one(cfg, questions, template_state, group) -> dict:
state = make_state(group, template_state)
t0 = time.time()
resp = request_teacher(cfg, questions, state)
ans = parse_answers(resp)
return {
"key": group["key"],
"text": group["text"],
"label": ans["is_ad"],
"kind": ans["ad_kind"] if ans["ad_kind"] != "none" else "none",
"scope": ans["ad_scope"],
"severity": ans["severity"],
"jev_conf": {"is_ad": ans["is_ad_conf"], "ad_kind": ans["ad_kind_conf"],
"ad_scope": ans["ad_scope_conf"], "severity": ans["severity_conf"]},
"rule_kind": group["rule_kind"],
"rule_score": group["rule_score"],
"rule_hits": group["rule_hits"],
"agrees": (ans["ad_kind"] == group["rule_kind"]),
"quality": group["flags"],
"reason": group["reason"],
"n_records": group["n"],
"n_accounts": len(group["tgids"]),
"n_chats": len(group["chat_ids"]),
"channel": group["channel"],
"first_seen": group["first_seen"],
"last_seen": group["last_seen"],
"model": resp.get("model"),
"latency_s": round(time.time() - t0, 2),
"judged_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
}
def run_pass(args, cfg, questions, template_state, groups, done, out, lock) -> int:
pending = [g for k, g in groups.items() if k not in done]
if args.limit:
pending = pending[:args.limit]
if not pending:
return 0
written = 0
with ThreadPoolExecutor(max_workers=args.workers) as ex:
futs = {ex.submit(judge_one, cfg, questions, template_state, g): g
for g in pending}
for fut in as_completed(futs):
g = futs[fut]
try:
row = fut.result()
except Exception as exc: # noqa: BLE001
print(f"[err] key={g['key']} {exc}", flush=True)
continue
with lock:
out.parent.mkdir(parents=True, exist_ok=True)
with out.open("a", encoding="utf-8") as fh:
fh.write(json.dumps(row, ensure_ascii=False) + "\n")
done.add(row["key"])
written += 1
if written % 20 == 0:
print(f"[jev] judged {written}/{len(pending)}", flush=True)
return written
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--store", type=Path, default=Path("data/raw/gmeow/bans.jsonl"))
ap.add_argument("--out", type=Path, default=Path("data/cleaned/gmeow/ads_jev.jsonl"))
ap.add_argument("--template", type=Path, default=TEMPLATE)
ap.add_argument("--workers", type=int, default=8)
ap.add_argument("--limit", type=int, default=0)
ap.add_argument("--once", action="store_true")
ap.add_argument("--watch", action="store_true")
ap.add_argument("--interval", type=int, default=20)
ap.add_argument("--duration", type=int, default=0)
args = ap.parse_args()
cfg = load_env()
tpl = json.loads(args.template.read_text(encoding="utf-8"))
questions, template_state = tpl["questions"], tpl["state"]
lock = threading.Lock()
started = time.time()
total = 0
while True:
done = load_done(args.out)
groups = build_groups(args.store)
new = sum(1 for k in groups if k not in done)
print(f"[scan] store_groups={len(groups)} done={len(done)} pending={new}",
flush=True)
total += run_pass(args, cfg, questions, template_state, groups, done, args.out, lock)
if args.once or not args.watch:
break
if args.duration and time.time() - started >= args.duration:
break
time.sleep(args.interval)
print(f"done judged={total} out={args.out}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
|