CTk Sentiment Analyzer – Python + AI + CustomTkinter GUI | FuzzuTech
Demo :
Click Video πππ
Features :
-
Python + CTkinter GUI
-
Real-time Sentiment Detection (VADER + Naive Bayes)
-
Dark/Light Themes
-
Emoji-powered results
-
Export CSV History
-
Train/Load ML Models
Code :
import os
import json
import time
import tkinter as tk
from tkinter import filedialog, messagebox
import customtkinter as ctk
# Data / ML
import nltk
from nltk.sentiment import SentimentIntensityAnalyzer
import pandas as pd
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from joblib import dump, load
APP_TITLE = "CTk Sentiment Analyzer — FuzzuTech"
ctk.set_appearance_mode("Dark")
ctk.set_default_color_theme("blue")
# Ensure data/model dirs
os.makedirs("data", exist_ok=True)
os.makedirs("assets", exist_ok=True)
os.makedirs("models", exist_ok=True)
VADER_READY = False
SIA = None
HISTORY = [] # list of dicts {ts, text, model, label, score}
ACCENT_COLORS = ["blue","green","dark-blue","sweetkind","sunburst","emerald"]
# ----------------------- Helpers -----------------------
def ensure_vader():
global VADER_READY, SIA
try:
nltk.data.find('sentiment/vader_lexicon.zip')
except LookupError:
try:
nltk.download('vader_lexicon')
except Exception as e:
messagebox.showerror("NLTK Error", f"Failed to download VADER: {e}")
return False
try:
SIA = SentimentIntensityAnalyzer()
VADER_READY = True
return True
except Exception as e:
messagebox.showerror("VADER Init", str(e))
return False
NB_PATH = os.path.join("models","nb_model.pkl")
LE_PATH = os.path.join("models","label_encoder.npy")
def load_or_none(path):
try:
return load(path)
except Exception:
return None
def label_from_vader_scores(scores):
# compound in [-1,1]
c = scores.get('compound', 0)
if c >= 0.2:
return 'pos', c
elif c <= -0.2:
return 'neg', abs(c)
else:
return 'neu', 1-abs(c)
class GlassCard(ctk.CTkFrame):
def __init__(self, master, **kwargs):
super().__init__(master, corner_radius=20, **kwargs)
self.configure(fg_color=("#111418","#0b0e12"))
class App(ctk.CTk):
def __init__(self):
super().__init__()
self.title(APP_TITLE)
self.geometry("1080x700")
self.minsize(960, 620)
# Top bar
top = GlassCard(self)
top.pack(fill="x", padx=14, pady=(14,8))
self.model_var = tk.StringVar(value="VADER (Rule-based)")
self.realtime_var = tk.BooleanVar(value=False)
ctk.CTkLabel(top, text=APP_TITLE, font=("Segoe UI Semibold", 20)).pack(side="left", padx=12)
self.model_menu = ctk.CTkOptionMenu(top, values=["VADER (Rule-based)", "Naive Bayes (Train Quick)"], variable=self.model_var, width=240)
self.model_menu.pack(side="left", padx=8)
self.theme_switch = ctk.CTkSwitch(top, text="Realtime", variable=self.realtime_var)
self.theme_switch.pack(side="left", padx=8)
self.appearance = ctk.CTkOptionMenu(top, values=["Dark","Light"], command=lambda m: ctk.set_appearance_mode(m), width=110)
self.appearance.pack(side="right", padx=8)
self.accent = ctk.CTkOptionMenu(top, values=ACCENT_COLORS, command=lambda c: ctk.set_default_color_theme(c) if c in ["blue","green","dark-blue"] else None, width=140)
self.accent.set("blue")
self.accent.pack(side="right", padx=8)
# Main layout
body = ctk.CTkFrame(self, fg_color="transparent")
body.pack(fill="both", expand=True, padx=14, pady=(0,14))
body.grid_columnconfigure(0, weight=3)
body.grid_columnconfigure(1, weight=2)
body.grid_rowconfigure(0, weight=1)
# Left card: input / actions
left = GlassCard(body)
left.grid(row=0, column=0, sticky="nsew", padx=(0,8), pady=0)
self.input = ctk.CTkTextbox(left, corner_radius=16, height=220, font=("Inter", 14))
self.input.pack(fill="both", expand=True, padx=16, pady=(16,8))
self.input.bind("<Control-Return>", lambda e: self.analyze())
self.input.bind("<KeyRelease>", lambda e: self.analyze() if self.realtime_var.get() else None)
btns = ctk.CTkFrame(left, fg_color="transparent")
btns.pack(fill="x", padx=16, pady=(0,12))
ctk.CTkButton(btns, text="Analyze (Ctrl+Enter)", command=self.analyze).pack(side="left", padx=6)
ctk.CTkButton(btns, text="Clear (Ctrl+L)", command=self.clear).pack(side="left", padx=6)
self.bind("<Control-l>", lambda e: self.clear())
ctk.CTkButton(btns, text="Export CSV (Ctrl+S)", command=self.export_csv).pack(side="right", padx=6)
self.bind("<Control-s>", lambda e: self.export_csv())
# Right card: result / history / training
right = GlassCard(body)
right.grid(row=0, column=1, sticky="nsew", padx=(8,0), pady=0)
right.grid_rowconfigure(3, weight=1)
self.result_label = ctk.CTkLabel(right, text="Result: —", font=("Segoe UI", 18))
self.result_label.grid(row=0, column=0, sticky="w", padx=16, pady=(16,6))
self.emoji_label = ctk.CTkLabel(right, text="π", font=("Segoe UI Emoji", 42))
self.emoji_label.grid(row=0, column=1, sticky="e", padx=16, pady=(16,6))
self.bar = ctk.CTkProgressBar(right)
self.bar.set(0)
self.bar.grid(row=1, column=0, columnspan=2, sticky="ew", padx=16)
# Train/Load
tools = ctk.CTkFrame(right, fg_color="transparent")
tools.grid(row=2, column=0, columnspan=2, sticky="ew", padx=12, pady=(8,8))
ctk.CTkButton(tools, text="Download/Init VADER", command=self.init_vader).pack(side="left", padx=6)
ctk.CTkButton(tools, text="Train NB", command=self.train_nb).pack(side="left", padx=6)
ctk.CTkButton(tools, text="Load NB", command=self.load_nb).pack(side="left", padx=6)
# History table
self.table = ctk.CTkTextbox(right, corner_radius=14, height=280)
self.table.grid(row=3, column=0, columnspan=2, sticky="nsew", padx=12, pady=(0,12))
self.table.insert("end", "Time, Model, Label, Score, Text\n")
self.table.configure(state="disabled")
# State for NB model
self.nb_pipe = None
self.le = None
# Prepare VADER on startup
self.after(200, self.init_vader)
# ---- actions ----
def init_vader(self):
ok = ensure_vader()
if ok:
messagebox.showinfo("VADER", "VADER is ready ✅")
def analyze(self):
text = self.input.get("1.0", "end").strip()
if not text:
return
model = self.model_var.get()
label, conf = "neu", 0.0
if model.startswith("VADER"):
if not VADER_READY:
if not ensure_vader():
return
scores = SIA.polarity_scores(text)
label, conf = label_from_vader_scores(scores)
else:
if self.nb_pipe is None:
messagebox.showwarning("Naive Bayes", "Train or load the NB model first.")
return
proba = self.nb_pipe.predict_proba([text])[0]
idx = int(np.argmax(proba))
label = self.le.inverse_transform([idx])[0]
conf = float(np.max(proba))
# Update UI
self.result_label.configure(text=f"Result: {label.upper()} ({conf:.2f})")
self.bar.set(conf)
emoji = {"pos":"π","neg":"π ","neu":"π"}.get(label, "π")
self.emoji_label.configure(text=emoji)
# History
row = {
"ts": time.strftime("%Y-%m-%d %H:%M:%S"),
"model": model.split(" ")[0],
"label": label,
"score": round(conf, 3),
"text": text.replace("\n"," ")[:1200]
}
HISTORY.append(row)
self._append_table(row)
def _append_table(self, row):
self.table.configure(state="normal")
line = f"{row['ts']}, {row['model']}, {row['label']}, {row['score']}, {row['text']}\n"
self.table.insert("end", line)
self.table.see("end")
self.table.configure(state="disabled")
def clear(self):
self.input.delete("1.0", "end")
self.result_label.configure(text="Result: —")
self.bar.set(0)
self.emoji_label.configure(text="π")
def export_csv(self):
if not HISTORY:
messagebox.showinfo("Export", "No history to export.")
return
df = pd.DataFrame(HISTORY)
path = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV","*.csv")])
if path:
df.to_csv(path, index=False)
messagebox.showinfo("Export", f"Saved to {path}")
def train_nb(self):
# choose CSV
path = filedialog.askopenfilename(initialdir="data", filetypes=[("CSV","*.csv")])
if not path:
return
try:
df = pd.read_csv(path)
if not set(["text","label"]).issubset(df.columns):
messagebox.showerror("CSV", "CSV must have 'text' and 'label' columns")
return
X = df["text"].astype(str).values
y = df["label"].astype(str).values
self.le = LabelEncoder()
y_enc = self.le.fit_transform(y)
Xtr, Xte, ytr, yte = train_test_split(X, y_enc, test_size=0.2, random_state=42)
self.nb_pipe = Pipeline([
("tfidf", TfidfVectorizer(max_features=5000, ngram_range=(1,2))),
("nb", MultinomialNB())
])
self.nb_pipe.fit(Xtr, ytr)
dump(self.nb_pipe, NB_PATH)
np.save(LE_PATH, self.le.classes_)
# quick report
ypred = self.nb_pipe.predict(Xte)
rep = classification_report(yte, ypred, target_names=list(self.le.classes_), zero_division=0)
messagebox.showinfo("Training done", rep)
except Exception as e:
messagebox.showerror("Train NB", str(e))
def load_nb(self):
try:
self.nb_pipe = load(NB_PATH)
classes = np.load(LE_PATH, allow_pickle=True)
self.le = LabelEncoder()
self.le.classes_ = classes
messagebox.showinfo("Load NB", "Loaded models/nb_model.pkl ✅")
except Exception as e:
messagebox.showerror("Load NB", f"Failed: {e}\nTrain the model first.")
if __name__ == "__main__":
app = App()
app.mainloop()
Comments
Post a Comment