import os import time # ← for latency / inference-time measurement os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" import tensorflow as tf from tensorflow import keras from tensorflow.keras import layers import numpy as np import pickle import gradio as gr # ============================================================ # CUSTOM LAYERS # ============================================================ @tf.keras.utils.register_keras_serializable() class TransformerBlock(layers.Layer): def __init__(self, embed_dim, num_heads, ff_dim, rate=0.1, **kwargs): super().__init__(**kwargs) self.embed_dim = embed_dim self.num_heads = num_heads self.ff_dim = ff_dim self.rate = rate self.att = layers.MultiHeadAttention(num_heads=num_heads, key_dim=embed_dim) self.ffn = keras.Sequential([layers.Dense(ff_dim, activation="relu"), layers.Dense(embed_dim)]) self.layernorm1 = layers.LayerNormalization(epsilon=1e-6) self.layernorm2 = layers.LayerNormalization(epsilon=1e-6) self.dropout1 = layers.Dropout(rate) self.dropout2 = layers.Dropout(rate) def build(self, input_shape): super().build(input_shape) def call(self, inputs, training=None): attn_output = self.att(inputs, inputs) attn_output = self.dropout1(attn_output, training=training) out1 = self.layernorm1(inputs + attn_output) ffn_output = self.ffn(out1) ffn_output = self.dropout2(ffn_output, training=training) return self.layernorm2(out1 + ffn_output) def get_config(self): config = super().get_config() config.update({"embed_dim": self.embed_dim, "num_heads": self.num_heads, "ff_dim": self.ff_dim, "rate": self.rate}) return config @tf.keras.utils.register_keras_serializable() class TokenAndPositionEmbedding(layers.Layer): def __init__(self, maxlen, vocab_size, embed_dim, **kwargs): super().__init__(**kwargs) self.maxlen = maxlen self.vocab_size = vocab_size self.embed_dim = embed_dim self.token_emb = layers.Embedding(input_dim=vocab_size, output_dim=embed_dim) self.pos_emb = layers.Embedding(input_dim=maxlen, output_dim=embed_dim) def build(self, input_shape): super().build(input_shape) def call(self, x): seq_len = tf.shape(x)[-1] positions = tf.range(start=0, limit=seq_len, delta=1) positions = self.pos_emb(positions) x = self.token_emb(x) return x + positions def get_config(self): config = super().get_config() config.update({"maxlen": self.maxlen, "vocab_size": self.vocab_size, "embed_dim": self.embed_dim}) return config # ============================================================ # LOAD TOKENIZER & MODEL # ============================================================ with open("improved_tokenizer.pkl", "rb") as f: tokenizer = pickle.load(f) MAX_LEN = 80 model = tf.keras.models.load_model( "improved_bangla_sentiment.keras", custom_objects={ "TransformerBlock": TransformerBlock, "TokenAndPositionEmbedding": TokenAndPositionEmbedding }, compile=False ) # ============================================================ # PREDICTION # ============================================================ def preprocess_text(text): seq = tokenizer.texts_to_sequences([text]) padded = keras.preprocessing.sequence.pad_sequences(seq, maxlen=MAX_LEN) return padded EMPTY_RESULT = """
šŸ’¬

কিছু ą¦ą¦•ą¦Ÿą¦¾ লিখুন ą¦¬ą¦æą¦¶ą§ą¦²ą§‡ą¦·ą¦£ করতে...

ENTER BANGLA TEXT ABOVE AND CLICK ANALYZE

""" EXAMPLES = [ "ą¦ą¦‡ ą¦Ŗą¦£ą§ą¦Æą¦Ÿą¦æ অসাধারণ! আমি খুব ą¦øą¦Øą§ą¦¤ą§ą¦·ą§ą¦Ÿą„¤", "ঔেলিভারি অনেক দেরিতে ą¦ą¦øą§‡ą¦›ą§‡, খুব খারাপ ą¦…ą¦­ą¦æą¦œą§ą¦žą¦¤ą¦¾ą„¤", "ą¦•ą§‹ą¦Æą¦¼ą¦¾ą¦²ą¦æą¦Ÿą¦æ ą¦®ą§‹ą¦Ÿą¦¾ą¦®ą§ą¦Ÿą¦æ ভালো, দাম ą¦ą¦•ą¦Ÿą§ ą¦¬ą§‡ą¦¶ą¦æą„¤", "আমি ą¦ą¦•ą¦¦ą¦®ą¦‡ খুশি নই, ą¦Ŗą¦£ą§ą¦Æą¦Ÿą¦æ ą¦Ŗą§ą¦°ą¦¤ą§ą¦Æą¦¾ą¦¶ą¦¾ অনুযায়ী ą¦Øą¦Æą¦¼ą„¤", "ą¦šą¦®ą§Žą¦•ą¦¾ą¦° ą¦øą¦¾ą¦°ą§ą¦­ą¦æą¦ø ą¦ą¦¬ą¦‚ ą¦¦ą§ą¦°ą§ą¦¤ ą¦”ą§‡ą¦²ą¦æą¦­ą¦¾ą¦°ą¦æą„¤", ] def predict_sentiment(text): if not text or not text.strip(): return EMPTY_RESULT # ── Timing: total wall-clock time (latency) ────────────── t_start = time.perf_counter() # ── Preprocessing ──────────────────────────────────────── t_pre0 = time.perf_counter() processed = preprocess_text(text) t_pre1 = time.perf_counter() preprocess_ms = (t_pre1 - t_pre0) * 1000 # ── Inference (model.predict) ───────────────────────────── t_inf0 = time.perf_counter() prediction = model.predict(processed, verbose=0)[0] t_inf1 = time.perf_counter() inference_ms = (t_inf1 - t_inf0) * 1000 # ── Total latency ───────────────────────────────────────── latency_ms = (time.perf_counter() - t_start) * 1000 # ── Decode prediction ───────────────────────────────────── if np.ndim(prediction) == 0 or len(np.atleast_1d(prediction)) == 1: score = float(prediction) is_positive = score >= 0.5 confidence = score if is_positive else 1 - score else: class_id = int(np.argmax(prediction)) confidence = float(np.max(prediction)) is_positive = class_id == 1 pct = int(confidence * 100) word_count = len(text.split()) char_count = len(text.strip()) # ── Uncertainty flag (confidence near 0.5) ─────────────── is_uncertain = confidence < 0.65 radius = 52 circ = 2 * 3.14159 * radius offset = circ * (1 - confidence) if is_positive: emoji = "😊" label_en = "Positive" label_bn = "ą¦‡ą¦¤ą¦æą¦¬ą¦¾ą¦šą¦•" color = "#00e5a0" glow = "rgba(0,229,160,0.28)" card_bg = "rgba(0,229,160,0.06)" card_bdr = "rgba(0,229,160,0.22)" bar_grad = "linear-gradient(90deg,#00b87844,#00e5a0)" intensity = "ą¦‰ą¦šą§ą¦š" if pct >= 80 else ("ą¦®ą¦¾ą¦ą¦¾ą¦°ą¦æ" if pct >= 60 else "হালকা") else: emoji = "😠" label_en = "Negative" label_bn = "ą¦Øą§‡ą¦¤ą¦æą¦¬ą¦¾ą¦šą¦•" color = "#f87171" glow = "rgba(248,113,113,0.28)" card_bg = "rgba(248,113,113,0.06)" card_bdr = "rgba(248,113,113,0.22)" bar_grad = "linear-gradient(90deg,#f8717144,#f87171)" intensity = "ą¦‰ą¦šą§ą¦š" if pct >= 80 else ("ą¦®ą¦¾ą¦ą¦¾ą¦°ą¦æ" if pct >= 60 else "হালকা") # ── Uncertainty banner (shown only when confidence < 65%) ─ uncertain_banner = "" if is_uncertain: uncertain_banner = f"""
āš ļø LOW CONFIDENCE ({pct}%) — PREDICTION MAY BE UNRELIABLE
""" # ── Performance stats row ────────────────────────────────── stats_row = f"""
ā± PERFORMANCE METRICS
TOTAL LATENCY {latency_ms:.1f} ms wall-clock end-to-end
INFERENCE TIME {inference_ms:.1f} ms model.predict only
PREPROCESS {preprocess_ms:.1f} ms tokenize + pad
CHARS / TOKENS {char_count} / {MAX_LEN} input / max seq len
""" return f"""
{uncertain_banner}
{emoji}
{intensity} Ā· {pct}%
{label_en}
{label_bn}
{pct} %
CONFIDENCE SCORE
0 25 50 75 100
šŸ”¤ {word_count} WORDS
šŸŽÆ {pct}% CONF
⚔ Transformer
{stats_row}
""" # ============================================================ # CSS # ============================================================ css = """ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;700;900&family=Hind+Siliguri:wght@400;500;600;700&display=swap'); @keyframes bsa-fadein { from{opacity:0;transform:translateY(14px);}to{opacity:1;transform:translateY(0);} } @keyframes bsa-pulse { 0%{transform:scale(1);opacity:0.8;}100%{transform:scale(2.4);opacity:0;} } @keyframes bsa-shim { 0%{background-position:200% 0;}100%{background-position:-200% 0;} } @keyframes bsa-blink { 0%,100%{opacity:1;}50%{opacity:0.15;} } @keyframes bsa-aurora { 0%{opacity:0.6;}50%{opacity:1;}100%{opacity:0.7;} } body { background: #070a14 !important; } .gradio-container { background: #070a14 !important; font-family: 'Outfit', 'Hind Siliguri', sans-serif !important; min-height: 100vh !important; } .gradio-container::before { content:''; position:fixed;inset:0;pointer-events:none;z-index:0; background: radial-gradient(ellipse 70% 50% at 15% 25%, rgba(124,58,237,0.17) 0%, transparent 55%), radial-gradient(ellipse 55% 55% at 85% 75%, rgba(34,211,238,0.12) 0%, transparent 55%), radial-gradient(ellipse 45% 35% at 50% 50%, rgba(0,229,160,0.06) 0%, transparent 55%); animation: bsa-aurora 10s ease-in-out infinite alternate; } .gradio-container > .main, .gradio-container > .main > .wrap, .contain { background: transparent !important; max-width: 100% !important; } #bsa-wrap { max-width: 800px !important; margin: 0 auto !important; padding: 0 24px 80px !important; position: relative !important; z-index: 1 !important; background: transparent !important; } #bsa-wrap .block, #bsa-wrap .form, #bsa-wrap .gap, #bsa-card .block, #bsa-card .form, #bsa-card .gap { background: transparent !important; border: none !important; box-shadow: none !important; padding: 0 !important; gap: 0 !important; } #bsa-card { background: rgba(13,16,35,0.88) !important; border: 1px solid rgba(255,255,255,0.09) !important; border-radius: 24px !important; padding: 36px 36px 28px !important; position: relative !important; overflow: hidden !important; backdrop-filter: blur(20px) !important; -webkit-backdrop-filter: blur(20px) !important; box-shadow: 0 30px 80px rgba(0,0,0,0.55), 0 0 0 1px rgba(255,255,255,0.04) !important; } #bsa-card::before { content:''; position:absolute;top:0;left:0;right:0;height:1px; background:linear-gradient(90deg,transparent 0%,rgba(124,58,237,0.8) 30%,rgba(34,211,238,1) 50%,rgba(124,58,237,0.8) 70%,transparent 100%); background-size:200% 100%; animation:bsa-shim 4s linear infinite; } #bsa-info { background: rgba(13,16,35,0.88) !important; border: 1px solid rgba(255,255,255,0.09) !important; border-radius: 24px !important; padding: 32px 36px !important; margin-top: 20px !important; backdrop-filter: blur(20px) !important; -webkit-backdrop-filter: blur(20px) !important; box-shadow: 0 20px 60px rgba(0,0,0,0.4) !important; } /* ── TEXTAREA ── */ .gradio-container textarea, #bsa-card textarea, #bsa-wrap textarea, textarea { background-color: rgba(15,18,40,0.8) !important; background: rgba(15,18,40,0.8) !important; border: 1.5px solid rgba(255,255,255,0.1) !important; border-radius: 14px !important; color: #e2e8f0 !important; -webkit-text-fill-color: #e2e8f0 !important; font-family: 'Hind Siliguri', sans-serif !important; font-size: 16px !important; line-height: 1.8 !important; padding: 16px 18px !important; resize: none !important; caret-color: #22d3ee !important; transition: border-color 0.3s, box-shadow 0.3s !important; box-shadow: none !important; } .gradio-container textarea:focus, textarea:focus { background-color: rgba(20,24,55,0.9) !important; background: rgba(20,24,55,0.9) !important; border-color: rgba(124,58,237,0.6) !important; box-shadow: 0 0 0 3px rgba(124,58,237,0.14) !important; outline: none !important; color: #e2e8f0 !important; -webkit-text-fill-color: #e2e8f0 !important; } textarea::placeholder { color: navajowhite !important; -webkit-text-fill-color: navajowhite !important; } label > span, .gradio-container label > span { display: none !important; } /* ── PRIMARY BUTTON ── */ .gradio-container button.primary, button.primary, button[variant="primary"] { background: linear-gradient(135deg, #7c3aed 0%, #5b21b6 100%) !important; border: 1px solid rgba(139,92,246,0.45) !important; border-radius: 13px !important; color: #ffffff !important; -webkit-text-fill-color: #ffffff !important; font-family: 'Outfit', sans-serif !important; font-weight: 700 !important; font-size: 15px !important; padding: 14px 24px !important; cursor: pointer !important; transition: all 0.25s !important; box-shadow: 0 4px 20px rgba(124,58,237,0.38) !important; text-shadow: none !important; } button.primary:hover, button[variant="primary"]:hover { transform: translateY(-2px) !important; box-shadow: 0 10px 35px rgba(124,58,237,0.58) !important; background: linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%) !important; } /* ── SECONDARY BUTTON ── */ .gradio-container button.secondary, button.secondary, button[variant="secondary"] { background: rgba(255,255,255,0.04) !important; border: 1.5px solid rgba(255,255,255,0.1) !important; border-radius: 13px !important; color: rgba(148,163,184,0.85) !important; -webkit-text-fill-color: rgba(148,163,184,0.85) !important; font-family: 'Outfit', sans-serif !important; font-size: 14px !important; padding: 14px 20px !important; cursor: pointer !important; transition: all 0.2s !important; box-shadow: none !important; text-shadow: none !important; } button.secondary:hover, button[variant="secondary"]:hover { background: rgba(255,255,255,0.08) !important; border-color: rgba(255,255,255,0.18) !important; color: #e2e8f0 !important; -webkit-text-fill-color: #e2e8f0 !important; transform: translateY(-1px) !important; } /* ── EXAMPLE BUTTONS ── */ .ex-btn button, .gradio-container .ex-btn button { background: #ffffff !important; border: 1px solid rgba(255,255,255,0.1) !important; border-radius: 10px !important; color: navajowhite !important; -webkit-text-fill-color: navajowhite !important; font-family: 'Hind Siliguri', sans-serif !important; font-size: 14px !important; font-weight: 500 !important; padding: 10px 16px !important; text-align: left !important; cursor: pointer !important; transition: all 0.2s !important; box-shadow: none !important; text-shadow: none !important; line-height: 1.6 !important; white-space: normal !important; height: auto !important; min-height: unset !important; } .ex-btn button:hover, .gradio-container .ex-btn button:hover { background: rgba(124,58,237,0.12) !important; border-color: rgba(124,58,237,0.35) !important; color: #ffffff !important; -webkit-text-fill-color: #ffffff !important; transform: translateX(3px) !important; box-shadow: none !important; } /* ── EXAMPLE BUTTON ROW ── */ .ex-btn-row { gap: 8px !important; flex-wrap: wrap !important; margin: 0 !important; } .ex-btn-row .block, .ex-btn-row .form { background: transparent !important; border: none !important; box-shadow: none !important; padding: 0 !important; min-width: 0 !important; } /* ── BUTTON ROW ── */ .bsa-btnrow { gap: 12px !important; margin: 18px 0 28px !important; } .bsa-btnrow .block, .bsa-btnrow .form { background: transparent !important; border: none !important; box-shadow: none !important; } /* ── RESULT ── */ .bsa-result .block, .bsa-result .prose, .bsa-result .wrap, .bsa-result > div { background: transparent !important; border: none !important; box-shadow: none !important; padding: 0 !important; } footer, .svelte-footer, .gr-footer { display: none !important; } """ # ============================================================ # HTML BLOCKS # ============================================================ HERO = """
transformer Ā· nlp Ā· bengali
Bangla Sentiment Analyzer

বাংলা রিভিউ বা ą¦®ą¦Øą§ą¦¤ą¦¬ą§ą¦Æ লিখুন — ą¦¤ą¦¾ą§Žą¦•ą§ą¦·ą¦£ą¦æą¦• ą¦¬ą¦æą¦¶ą§ą¦²ą§‡ą¦·ą¦£ পান

⚔ TensorFlow šŸ”¤ Transformer BD Bengali NLP šŸŽÆ Classification
""" SEC_INPUT = """
Input
""" SEC_RESULT = """
Result
""" DIVIDER = """
""" EX_LABEL = """
✦ Example inputs — click to try
""" FOOTER = """

Powered by Custom Transformer Built with TensorFlow & Gradio Bengali NLP

""" # ── NEW: Static information panel ───────────────────────────────────────────── INFO_PANEL = """
Model & Performance Reference
šŸ’» 1 Ā· Computational Cost
This model is a single-head Transformer trained for binary text classification. Its computational footprint is intentionally lightweight:
PARAMETERS
~1–5 M
typical small Transformer
MEMORY (RAM)
~50–200 MB
model weights + TF runtime
HARDWARE
CPU only
GPU not required at inference
MAX SEQ LEN
80 tokens
O(n²) attention applies
ā„¹ļø  Self-attention complexity scales as O(n²·d) where n = sequence length and d = embedding dimension. At n=80 this is negligible — the dominant cost is the first inference call due to TF graph compilation (TensorFlow's XLA warm-up). Subsequent calls are significantly faster.
🌐 2 · Latency
Latency is the total wall-clock time from receiving user input to returning the result — it includes tokenization, padding, model forward-pass, and HTML rendering.
1st REQUEST (cold)
500 ms – 3 s
TF graph compilation
WARM REQUESTS
20 – 80 ms
CPU inference, typical
WITH GPU
< 10 ms
if CUDA is available
ā„¹ļø  Network latency (Gradio UI ↔ Python backend) adds ~5–15 ms on localhost. On Hugging Face Spaces or remote servers, add 50–150 ms round-trip depending on geography.
⚔ 3 · Inference Time
Inference time measures only the model.predict() call — the neural network forward pass itself. This is the pure compute cost and excludes tokenization, Gradio overhead, and network round-trips. Each run shows the live measured value above in the result card.
BATCH SIZE
1 sample
single-text inference
TYPICAL RANGE
5 – 50 ms
warm, CPU, seq=80
VARIABILITY
±10–30%
OS scheduling, cache state
ā„¹ļø  First-call inference is slower because TensorFlow traces and compiles the computation graph (XLA/JIT). After the first call the compiled graph is cached in memory, making all subsequent inferences significantly faster and more consistent.
āš ļø 4 Ā· When Predictions May Vary or Be Unreliable
šŸ”€
MIXED SENTIMENT
Text containing both positive and negative signals (e.g., "ą¦Ŗą¦£ą§ą¦Æą¦Ÿą¦æ ą¦øą§ą¦Øą§ą¦¦ą¦° ą¦•ą¦æą¦Øą§ą¦¤ą§ দাম বেশি") confuses the binary classifier. Confidence will hover near 50 % and may flip with small edits.
šŸ”¤
OOV / RARE VOCABULARY
Words not seen during training are mapped to an unknown token <UNK>. Heavy use of slang, dialect, transliteration, or technical jargon will degrade accuracy.
🌐
CODE-SWITCHING (BANGLISH)
Mixing Bangla script with English words or Roman-script Bangla causes many tokens to be OOV, making predictions unreliable. Use pure Unicode Bangla for best results.
šŸ“
VERY SHORT OR VERY LONG TEXT
Single-word inputs lack context. Texts exceeding 80 tokens are silently truncated — sentiment expressed only in the trailing portion will be ignored entirely.
šŸ˜
SARCASM & IRONY
The model has no pragmatic understanding. Ironic phrases like "ą¦¹ą§ą¦Æą¦¾ą¦, অসাধারণ ą¦øą¦¾ą¦°ą§ą¦­ą¦æą¦ø!" (sarcastic) are likely classified as Positive because the surface tokens are positive.
šŸŽ­
DOMAIN SHIFT
If the training data consisted mainly of product reviews, the model may perform poorly on political commentary, news text, or social media posts with different linguistic patterns.
šŸŽ²
DROPOUT AT INFERENCE (if training=True)
Dropout layers are disabled during inference (training=False), so results are deterministic. If you call the model with training=True, outputs will differ randomly on each run.
""" # ============================================================ # GRADIO UI # ============================================================ with gr.Blocks(css=css, title="Bangla Sentiment Analyzer") as demo: with gr.Column(elem_id="bsa-wrap"): gr.HTML(HERO) with gr.Column(elem_id="bsa-card"): gr.HTML(SEC_INPUT) text_input = gr.Textbox( lines=4, placeholder="ą¦ą¦–ą¦¾ą¦Øą§‡ আপনার বাংলা রিভিউ বা ą¦®ą¦Øą§ą¦¤ą¦¬ą§ą¦Æ লিখুন...", label="", show_label=False, ) with gr.Row(elem_classes="bsa-btnrow"): submit_btn = gr.Button("šŸ” Analyze Sentiment", variant="primary", scale=3) clear_btn = gr.Button("āœ• Clear", variant="secondary", scale=1) gr.HTML(SEC_RESULT) with gr.Column(elem_classes="bsa-result"): output_html = gr.HTML(value=EMPTY_RESULT) gr.HTML(DIVIDER) gr.HTML(EX_LABEL) # ── EXAMPLE BUTTONS ── with gr.Row(elem_classes="ex-btn-row"): ex1 = gr.Button(EXAMPLES[0], elem_classes="ex-btn") ex2 = gr.Button(EXAMPLES[1], elem_classes="ex-btn") with gr.Row(elem_classes="ex-btn-row"): ex3 = gr.Button(EXAMPLES[2], elem_classes="ex-btn") ex4 = gr.Button(EXAMPLES[3], elem_classes="ex-btn") with gr.Row(elem_classes="ex-btn-row"): ex5 = gr.Button(EXAMPLES[4], elem_classes="ex-btn") # ── Static info panel (always visible below the main card) ── gr.HTML(INFO_PANEL) gr.HTML(FOOTER) # ── Main events ── submit_btn.click(fn=predict_sentiment, inputs=text_input, outputs=output_html) text_input.submit(fn=predict_sentiment, inputs=text_input, outputs=output_html) clear_btn.click(fn=lambda: ("", EMPTY_RESULT), inputs=None, outputs=[text_input, output_html]) # ── Example button events ── ex1.click(fn=lambda: EXAMPLES[0], inputs=None, outputs=text_input) ex2.click(fn=lambda: EXAMPLES[1], inputs=None, outputs=text_input) ex3.click(fn=lambda: EXAMPLES[2], inputs=None, outputs=text_input) ex4.click(fn=lambda: EXAMPLES[3], inputs=None, outputs=text_input) ex5.click(fn=lambda: EXAMPLES[4], inputs=None, outputs=text_input) if __name__ == "__main__": demo.launch(share=True)