import streamlit as st import numpy as np import pickle, json, re, requests, torch, os import open_clip from PIL import Image from io import BytesIO from transformers import AutoTokenizer, AutoModel from sklearn.preprocessing import normalize st.set_page_config( page_title="Catch-Bait โ Indian YouTube Clickbait Detector", page_icon="๐ฏ", layout="wide", initial_sidebar_state="collapsed", ) st.markdown(""" """, unsafe_allow_html=True) # โโ Constants โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ HINDI_BAIT = [ "เคธเคเฅเคเคพเค","เคเฅเคฒเคพเคธเคพ","เคงเคฎเคพเคเคพ","เคคเคฌเคพเคนเฅ","เคเฅเคเคเคพเคจเฅ","เคนเฅเคฐเคพเคจ","เคเฅเคฏเคพ เคนเฅเค", "เคเคช เคจเคนเฅเค เคเคพเคจเคคเฅ","เคธเคพเคฎเคจเฅ เคเค","เคฌเคกเคผเคพ เคเฅเคฒเคพเคธเคพ","เคธเคจเคธเคจเฅ","เคตเคฟเคธเฅเคซเฅเค", "เคธเค","เคเฅเค ","เค เคธเคฒเฅ","เคชเคฐเฅเคฆเคพเคซเคพเคถ","shocking","exposed","leaked", "you won't believe","watch till end","must watch","viral", "mind-blowing","breaking","exclusive","secret","truth","reveals", "biggest","never seen","unbelievable","crazy","insane", ] # โโ Load models โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ @st.cache_resource(show_spinner=False) def load_all(): clf = pickle.load(open("src/clickbait_xgb_v1.pkl", "rb")) cfg = json.load(open("src/feature_config.json")) tok = AutoTokenizer.from_pretrained("google/muril-base-cased") txt = AutoModel.from_pretrained("google/muril-base-cased").eval() cm, _, cp = open_clip.create_model_and_transforms("ViT-B-32", pretrained="openai") cm.eval() return clf, cfg, tok, txt, cm, cp clf, cfg, tokenizer, text_model, clip_model, clip_prep = load_all() YT_KEY = os.environ.get("YOUTUBE_API_KEY", "") # โโ Helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ def mean_pool(out, mask): t = out.last_hidden_state m = mask.unsqueeze(-1).float() return (t * m).sum(1) / m.sum(1).clamp(min=1e-9) def text_embed(text): enc = tokenizer(text, return_tensors="pt", truncation=True, padding=True, max_length=128) with torch.no_grad(): out = text_model(**enc) return mean_pool(out, enc["attention_mask"]).squeeze().numpy() def img_embed(pil): t = clip_prep(pil).unsqueeze(0) with torch.no_grad(): e = clip_model.encode_image(t) return e.squeeze().numpy() def meta_vec(title, views=0, likes=0, comments=0, dur=0, tags="", h=5): t = str(title); v = max(float(views), 1) return np.array([ np.log1p(v), float(likes)/v, float(comments)/v, min(dur/3600, 24), len(t)/100, sum(1 for c in t if c.isupper())/max(len(t), 1), min(len(re.findall(r'[\U00010000-\U0010ffff]', t))/5, 1), min(sum(1 for w in HINDI_BAIT if w in t.lower())/5, 1), int("?" in t), int("!" in t), min(len([x for x in str(tags).split(",") if x.strip()])/20, 1), float(h)/10, ], dtype=np.float32) def h_score(title): t = str(title).lower(); s = 0 s += min(sum(1 for w in HINDI_BAIT if w in t)*1.5, 4) eng = re.sub(r'[^\x00-\x7F]+', '', title) if len(eng) > 5 and sum(1 for c in eng if c.isupper())/len(eng) > 0.35: s += 2 if re.search(r'\?.*!|!.*\?', title): s += 1.5 ec = len(re.findall(r'[\U00010000-\U0010ffff]', title)) if ec > 2: s += min(ec*0.5, 2) return min(s, 10) def parse_dur(d): m = re.match(r'PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?', str(d)) return (int(m.group(1) or 0)*3600 + int(m.group(2) or 0)*60 + int(m.group(3) or 0)) if m else 0 def run_predict(title, pil, views, likes, comments, dur, tags, hs): te = normalize(text_embed(title).reshape(1,-1))[0] ie = normalize(img_embed(pil).reshape(1,-1))[0] me = meta_vec(title, views, likes, comments, dur, tags, hs) X = np.concatenate([te, ie, me]).reshape(1,-1) prob = float(clf.predict_proba(X)[0][1]) return prob def fetch_meta(vid_id): r = requests.get("https://www.googleapis.com/youtube/v3/videos", params={"part":"snippet,statistics,contentDetails","id":vid_id,"key":YT_KEY}, timeout=12).json() if not r.get("items"): return None item = r["items"][0] s, st, cd = item["snippet"], item["statistics"], item["contentDetails"] return { "title": s.get("title",""), "channel": s.get("channelTitle",""), "date": s.get("publishedAt","")[:10], "views": int(st.get("viewCount",0) or 0), "likes": int(st.get("likeCount",0) or 0), "comments": int(st.get("commentCount",0) or 0), "duration": parse_dur(cd.get("duration","")), "tags": ",".join(s.get("tags",[])), "thumb": (s.get("thumbnails",{}).get("maxres") or s.get("thumbnails",{}).get("high",{})).get("url",""), } def extract_id(url): m = re.search(r"(?:v=|youtu\.be/|embed/)([a-zA-Z0-9_-]{11})", url) return m.group(1) if m else None def fmt_n(n): if n >= 1_000_000: return f"{n/1e6:.1f}M" if n >= 1_000: return f"{n/1e3:.1f}K" return str(n) def fmt_d(s): h, r = divmod(int(s), 3600); m, sc = divmod(r, 60) return f"{h}h {m}m" if h else f"{m}m {sc}s" def highlight(title): out = title for w in sorted(HINDI_BAIT, key=len, reverse=True): if w.lower() in out.lower(): out = re.compile(re.escape(w), re.I).sub(f'{w}', out, count=1) return out def sig_top(raw, hi, md): return "st-red" if raw >= hi else "st-amber" if raw >= md else "st-green" # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ # NAV # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ st.markdown("""
""", unsafe_allow_html=True) # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ # HERO + INPUT # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ st.markdown("""