From 650142d56e480048efbfce410a182ae9cfae8b9c Mon Sep 17 00:00:00 2001 From: Tejaswani Date: Mon, 8 Sep 2025 12:27:09 +0530 Subject: [PATCH] Added voice-to-SMS input feature --- __pycache__/app.cpython-312.pyc | Bin 0 -> 93269 bytes ...ensemble_classifier_method.cpython-312.pyc | Bin 0 -> 29392 bytes __pycache__/export_feature.cpython-312.pyc | Bin 0 -> 3168 bytes __pycache__/smart_preprocess.cpython-312.pyc | Bin 0 -> 3010 bytes __pycache__/speech_handler.cpython-312.pyc | Bin 0 -> 7308 bytes __pycache__/speech_to_text.cpython-312.pyc | Bin 0 -> 14711 bytes app.py | 363 +++++++++++------- requirements.txt | 21 +- speech_handler.py | 151 ++++++++ speech_to_text.py | 313 +++++++++++++++ types.py | 53 +++ 11 files changed, 752 insertions(+), 149 deletions(-) create mode 100644 __pycache__/app.cpython-312.pyc create mode 100644 __pycache__/ensemble_classifier_method.cpython-312.pyc create mode 100644 __pycache__/export_feature.cpython-312.pyc create mode 100644 __pycache__/smart_preprocess.cpython-312.pyc create mode 100644 __pycache__/speech_handler.cpython-312.pyc create mode 100644 __pycache__/speech_to_text.cpython-312.pyc create mode 100644 speech_handler.py create mode 100644 speech_to_text.py create mode 100644 types.py diff --git a/__pycache__/app.cpython-312.pyc b/__pycache__/app.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..307461f685488466787ebdb43809aa4b6fabc083 GIT binary patch literal 93269 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 G_5TM_Ld_ij literal 0 HcmV?d00001 diff --git a/__pycache__/smart_preprocess.cpython-312.pyc b/__pycache__/smart_preprocess.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3e6c8308991c3808894ab19e32794758ae29168d GIT binary patch literal 3010 zcmbtWO>7&-6`t8$k|ITl)Q_#mwydS2+M?~yk|o=cqRNpiCARF)jpg>B(qNXHwY1W5 zm)TjGCPM;5kpdEGz%`Oq3MwE9a`=d-%z(9MbZ|msN_)zo> zN&Tg5fT9ELH{W~nW@hKTH*fj7K){EfMQ;9jw&X_WuUyz_o;un5Cy*sX5WzZ{=Dncf zv@k7Tq#+^_>0L}*#0}^n4S-(a18gLIzyN6i43ZFFGid>AC2fHFsy6#cJMb4s2jBs6 z5bzM`1UyW-0A&(>7f*}i2zilolW<Q z)u_#ImWg>Y%do{n#ZsA&wU`775HmVu?u@P~RApkC&M;3}rBs;#qvQZ{C)HafOb}wQuGlI}w`cD#w>_&E^OUbuV1kw|NS9?L&BUagEBKOm*`8J9oRVjv zM&=9t%Q7*IW41hRm^mhz>6wDpoR!mgdB!A6vhBQHXtou7UbamcUQw=XBq>zM6j~K3 zQ`0nL=rbmzT2faTHW}6!2BI)b8MY&ChI<+AW4NE;0fx^od=6Gt@7T<3sdm;fQg!!L zoyZeMglF?Hz!EB=x!t2x6NDbs`w}kVadb~e2{R}q+&qd%pAl@nlm*cB3ThwkhyZEMZj4()}Y?WC*s(r&Bqah1;7W+pzBQPO%I4B|AcSau>~sTs>mLEc0&dDd22Frg%q zmU>%LY|S*Nbru%yf`?uLD5G_M=-%Yw~cN@M4m)cNHXA_~2NDfjn}PK>omdm;*@p%zgC z$P%&vYi*9l`73QJvg7DxCqi+EMhQbqw<9^nSNlpU;-8?RP(+XF>y^;mE(tPc3Po@% zA~EmPiHRFi-kRa)Ib~Xwnz9qRs@gQ8 zD)ZJT=)2*e+@Op$LLE!N`@u>m`~||Uo<(T^FZ68$oA2FTyuBP=Ze4zBwYw5LwkSCw z3_ocMmalBWM6|~_JhiInUaOP#cCr)v4p08Cb%$5FqE^)0${2D{B+_62b;TNDEp3OG_t{_~k2Gu(b=3iG8D8AcfZ#%Atw~k2A z{5V7p-+htbALAvA5x?Vk;b&qFN5rSxAy2vgV-bOQG@8w1cumz+17soEYw?$%+^U`< z&%GlvssZ=Q8ZDemnOVb5&@9bpDb38%gsQ7))v#%FL^o54PDi8L)+hKvM*#kg{!vDs zwa1o~$9=yX`uWf=Uj9x0$8UXd_P1l746oo%+hbo+KbU=QUlcDlMezeq6c<{; zL?&ybphjh8O+!r>*>nl&=XOCEg_pp5?3)()p!9e6PO;qXTzzi*FK+F~0A53g7s6y7ha9e?HH?n>8d zm4lZyIu5N||6uz4>4#0fk}93wtaOZSgj(Oburzspa;>ZH@j#_*uo4=2=5@8W%ae{D z`S-6$`>gY@$)23zSy8=iF(6Qjwyzr`W}TPtf65)uZr7llj4BC5T!b&giU8>=RP!bU zA8$h?7T*BM3oo?Q!cjYUt=?E~a4omkTXR}=N_V3E-M6lT)oE_o3jjr@>B#CprKzX< z+D22?s;9=9k3T$DY3?siK50F$zF&Uk68&xmi7t250jwdBNGqC=NED*4TK)a<>%B&Q zJ{%6qS=o|vnqjC`SiT5xm4^n;ojo(qf4Z+%hW`WMNU#=nx1G;u#*D>1XGJ*-0zit` z*3JQ$M6{G$J!bW#`L>3VRuc(}=P>`+)2t5Pzl^0B_D@mu-Kw?rdBaHBBw1Y@wZ03M zJPtGt2z3+VKO^s7P`e{FHw){MXJMZs0((+rE?|xuxx5QU9S`RX$mLrYbiADVI3IEv zIrnp$9w)$g6W5<}f}DqHyqWWs+T2#o+iH9t=lg5xw{!ji*N-?IoFCwP)H%rcAgM>pnEvJ$vdaTgWbzhT;*Q9!BuW}eaXL2_)P3r_YIdXejeyr Y6)S=6@~fXq;HmGw?s$-Npz8g90bu*nZ~y=R literal 0 HcmV?d00001 diff --git a/__pycache__/speech_handler.cpython-312.pyc b/__pycache__/speech_handler.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3083616d0c1eea5013c2f4934c4623c92e1cc8f2 GIT binary patch literal 7308 zcmbtZYitx(mcG?h)o;7oxG{d>0z+`~Fob|j8WLmL5XcTN#0*(H%y!CM)pnbHgNW>7blZQYw`WXB8u*dc6Zuf+kfM%S%9!Xm-TVbV;+AsT$9YW|Dd`n~}!siTaqV zOYv;xj7oK_%&2E2T|KAwcrEdolupKJHaC{ds8TYMGxW_;CY#h$Nmj-|ep=1I2(Fhl zQhG9%Ql;9Z)XZoyqiQ|gM`)fSVTUOmH-O3H2QvA1t@S+i%iJ%3`0sP}tkLpyt2}cXePdF9N1##H7Y}BE6VU`~x z`QW!MFNEvOI?I~2F(*FuO3(%*LsRLNiao#Ch&^65;rLimlj12^)1(~Do=GaI21b!o zS<7$Hvs*wK7*AJeLXN9aB1_9=Pi97!8J>wdo7Bf7+0e6TSx?3#l~S0(whbqswG(nG zH6q7POKETgIg`}VU}RK1Zl4ckPCP|C{98N$kE=wl(8lfa#klN8*>ejdR-qQixT}`O z>X$je$GNa$qW2IwmYhmXsFFUWmOqH;3;xMICY#ffX<$$!P1SYy3a#fzSTG$LHN9_H zzB!>n-*z7CH^hL|$i=EdA91Fjsi}m9-yuDXL?fqu0Q;n|=c%I^b&w{{sHX;1?X;fF zof^u?=~NyVPFedGvo-3;z8$8aF-*>LLIeC5ZgCAcM zE(I3c{(1N6S@-G(ZfOzCVLB5SY7}MvwR6UwU!5qR;-KDt)_70h(sF{$BTKY zyHef97jr6njWprKJTcUcwQrSHiNxAm@R;aa<{Qf$5@U__ zXYe>r9y^J@@yd>?{Xgw5uIrui^(}1OesS>f(f5ze`8O{izCP#cU+CLiChk~5+&Sm# zUFdn)B0lhMD)GeMM}x3Lyw+_h?v(+`RZ`%nzh)b`syk_1-l12vy1d|0YdQ1$K@Ox0yy5Xp(L{sL_^I;>hcKS z0fsvWig8ub;N16^K21&o1jHx^I5280^sxVUSfG$P5R*@-8Pl7fa$1eS8ZvE$(XfNo zAyH)FH8h1Jv5hhVu`{+dF-Dx|Shi_P$s1s*)(4OCP9$&A1PN zi^hDQb2iZVsZYAKbG~cGEd1}=F{S;^7Xg-sfkw6$rVFB(nl*IOpO(+X6!2F>)_BFx zARprAMw$)a%(F5`zyoW`?$!XC zGl~{oGQHO4t1`-`K*}b{(?KFeeyT`SK@~}FG1y$D8Osh)LbqnbF=Rxfk!&`#8RTe? zU`8ZOr&VQ)ZLo`_n}7W`(g4dwF*`!-<@5+-!n&2I_Vn~f-%mj*rn1lLd4%CE%$e@8 z!Uo0!CdD!?Mycu?u!MXXaywO~seBIvB24?c~JS8208CJ%wsJa^0Rb|NprVM_i zHS>Uh8451ApQVVFu^Al|h)4#WtJ2M%{jC(GkfX%`a#=DNk-|Re2Ab(TT*>cEF-IXw zW$pYWvvhP|2!!*t@iBxeM{TTj_VyCbY6V(VMAMqgz64)*Uo$%Tv=>7I2Szh zFc`WT_-SB1xNA1JYr5&TYl^{LbHVSGgh0a~?(Pfpa-?nwOZR?ac*VenxdGbjou4S!QYA5Y|PdeB@ z4+%BTH-={$!?)Le+PJOg-}d$6I?~)aziQ*`s*QgnT*KSkLfeTa>#NfrzeJkXAmtEH zq~f~%`~1fPGtTLA#kB{EZ3pK<2cLwI`z!W&&q^;L=hG(}suG?(-cEuoAi}d7K(5&S z@;`M=$^ZD$RCBR4Qf!Po@JF7NcpyI23ShvWG)4Q!wQzJ3|A`~APjY>kdWbF>99PqW314SqRrVK5?{@M!0i*xDmhN?d0-v2`{_c!a6_W`bA6ku(f)o{vh z)^80J6Xazr;Q&lsGs)LjLDU0frb4BLvb1i(5SD+BzjO(Z7Jh5-R9{h^4U+4*nw&}T z+_BSCMBBVi$JCv*=XCIXcOCM+7wAwcxpk6j4ldMc zFLd;IGOWmO^~(#t!qt{>!2KS;S(oH9uq56RjIOF?%XpWlhFJMJvflW)6wMkbg^4jT z3X}nKER5h~gxv!wxXCb6N@w9JT1N2$2Eq>FrftAlD^vk;9WYsF7_=EC)YvGqyeR^< zC36;rHvAxSI+Hz{foqYWS}5p$OMTZ+HJ!cDFbFbhWN5juWagadJCx2rc3|;z0}tGR zFrl`v59$h-^3W|XqT#iz0)ccRyghe0T@JH9!g+@+uoGH6gIq~))$QjPx*eJezPZrS zakKM!=X}envn{Vqum7}V?-l1~jV;#_H&fSBA7&8W+PBeQB>73`huqhXJITsb^P%q9Q1{)n z|FHF=tpGcKN4C1nk>9*B{qC=Or@ME=he)UsIR`9t`X2=MmfR@w zv5SN{kYXv?y!$uNkGWqTnBG-v8k`UAF9!ENY#e@8av{UB$6gx%2RVj-)dgFl#m49Z zf0QBMAoo;DgIWJ#z`L)Fd=eP&M%R*iZC%k${@&}WqOJT4pwv1t(<;KqOec^1wV}Z^ z{7f&0{atS8-zTdE+xh#e1Q@yBF5<`<$7Q zHJ08g3wTqsagNp($U9ukjjyI|ofq-~#Qn-WlEu$jUS6-FFON#E_@6s(b*;#X>xx87L#bWcvb; zFjA?!fkgm2a53pXKP%G=#3l$=VUd*%thx?6w0M}` z^WCAThC3&2A1toeIq&bC_4hLG>3ganwEU!{gWT{8xcI+YHQ<08N`x+`i9s9g5RWh` zd4y05#c8NgLZwTM#Y}H3mS%N+==)=_cMUmZQ(Uo_l8wh=6oHqOtxN&0Ojdr*V?vJ? zc4iIQgFTEM^i_C#L|8S$DtMtkP<*gTm<*i@3yTDP zzG@1KyKfJb2>g5;Wk1DZCyHzAyHG5~cA8zr?9gl{^wOZ3&Q5H9?f=|p1V3yyJnXw0yK*pu^TIms zOGDO0bB$DmYJd^=8wfxc#S5l^G1-e~ali&hV-*{@%%E)uYRx%t*^xZ^t3fzG`+;h% z0NjDve7Ss$C}yFBQ_ttXZ>?RyxXvs<_h1j57weSlFFS0vOqYEQOaXN=8~#Mik7Q-4 z9KydRP$Q?at)%;KI@UVaZa#JIvE1c#`X-EF=A`{AJRoXv9PBD5EY=fF`Y+P)#Ce9} z`o1FgEJ3AE@L!zx9pCbq;3<07%?azFbFurv(J$)T{wjO^jgsIKTNj!;=bLxVHt(F4 ze>+la-uY?svG?mPx~_!2@U8d^{l9Nom^w3kthl28fq(B8{)Q_@Ztb}BQgKCi&cCtb zC7$5r!25v*?v_#)5n8{7>yIA;wYc#!cU`f*`#;@bP*dFatmLe+KGiTm_?K-1yT~WI a>h`($d!l2X4{jYC_T4=8eJl2L@c#p}cON(a literal 0 HcmV?d00001 diff --git a/__pycache__/speech_to_text.cpython-312.pyc b/__pycache__/speech_to_text.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..316575586917702c0f2b14ce8217804e4162fe77 GIT binary patch literal 14711 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 literal 0 HcmV?d00001 diff --git a/app.py b/app.py index 41f7417..e90ad6b 100644 --- a/app.py +++ b/app.py @@ -1,76 +1,48 @@ +from typing import Dict, Any, Optional, List, Union, TypedDict, Callable import streamlit as st import pandas as pd import plotly.express as px import plotly.graph_objects as go +import numpy as np +import json +from datetime import datetime +import torch from transformers import pipeline, AutoTokenizer, AutoModelForSequenceClassification -from datetime import datetime, timedelta - -# Assuming these files exist in the same directory -from export_feature import export_results_button -# from ensemble_classifier_method import EnsembleSpamClassifier, ModelPerformanceTracker, PredictionResult -# Dummy classes if you don't have the actual files for testing -try: - from ensemble_classifier_method import EnsembleSpamClassifier, ModelPerformanceTracker, PredictionResult -except ImportError: - st.warning("`ensemble_classifier_method.py` not found. Using dummy classes. Please provide the actual file for full functionality.") - class PredictionResult: - def __init__(self, label, score, spam_probability=None): - self.label = label - self.score = score - self.spam_probability = spam_probability - class ModelPerformanceTracker: - def __init__(self): - self.stats = {} - def update_performance(self, model_name, correct): pass - def get_all_stats(self): return {} - def save_to_file(self, filename): pass - class EnsembleSpamClassifier: - def __init__(self, performance_tracker): - self.performance_tracker = performance_tracker - self.default_weights = {"DistilBERT": 0.25, "BERT": 0.25, "RoBERTa": 0.25, "ALBERT": 0.25} - self.model_weights = self.default_weights.copy() - def update_model_weights(self, weights): self.model_weights.update(weights) - def get_model_weights(self): return self.model_weights - def get_ensemble_prediction(self, predictions, method): - # Dummy implementation for ensemble prediction - if not predictions: - return {'label': 'UNKNOWN', 'confidence': 0.0, 'spam_probability': 0.0, 'method': method, 'details': 'No model predictions'} - - # Simple majority voting for dummy - spam_votes = sum(1 for p in predictions.values() if p['label'] == 'SPAM') - ham_votes = sum(1 for p in predictions.values() if p['label'] == 'HAM') - - if spam_votes > ham_votes: - label = 'SPAM' - score = sum(p['score'] for p in predictions.values() if p['label'] == 'SPAM') / spam_votes if spam_votes else 0 - spam_prob = score # Simplified - elif ham_votes > spam_votes: - label = 'HAM' - score = sum(p['score'] for p in predictions.values() if p['label'] == 'HAM') / ham_votes if ham_votes else 0 - spam_prob = 1 - score # Simplified - else: # Tie or no clear majority, default to HAM for safety or SPAM for caution - label = 'HAM' - score = 0.5 - spam_prob = 0.5 - return {'label': label, 'confidence': score, 'spam_probability': spam_prob, 'method': method, 'details': f'Dummy {method} applied'} - - def get_all_predictions(self, predictions): - # Dummy method to return results for all ensemble methods - dummy_results = {} - for method_key in ["majority_voting", "weighted_average", "confidence_weighted", "adaptive_threshold", "meta_ensemble"]: - dummy_results[method_key] = self.get_ensemble_prediction(predictions, method_key) - return dummy_results - -# Core Python imports +from collections import defaultdict +from io import StringIO import time import re -from datetime import datetime -from pathlib import Path -import numpy as np -from typing import Dict, List, Tuple, Any, Optional -from io import StringIO -import torch -from collections import defaultdict # Added for easier analytics data aggregation + +# Custom type definitions +class MethodStats(TypedDict): + count: int + spam: int + confidences: List[float] + +class ModelStats(TypedDict): + spam: int + ham: int + total: int + +# Session state type definition +class SpamlyserSessionState: + """Type-safe session state container for Spamlyser""" + def __init__(self): + self.classification_history: List[Dict[str, Any]] = [] + self.ensemble_history: List[Dict[str, Any]] = [] + self.model_stats: Dict[str, ModelStats] = {} + self.ensemble_tracker = None + self.ensemble_classifier = None + self.loaded_models: Dict[str, Any] = {} + +# Import app core and ensemble functionality +import ensemble_classifier_method as ecm +from ensemble_classifier_method import ModelPerformanceTracker, EnsembleSpamClassifier + +# Import other local modules +from export_feature import export_results_button +from speech_handler import SpeechHandler +from speech_to_text import SpeechToText # --- Streamlit Page Configuration --- st.set_page_config( @@ -193,20 +165,16 @@ def get_all_predictions(self, predictions): ] }) -# --- Session State Initialization --- -if 'classification_history' not in st.session_state: - st.session_state.classification_history = [] -if 'model_stats' not in st.session_state: - st.session_state.model_stats = {model: {'spam': 0, 'ham': 0, 'total': 0} for model in ["DistilBERT", "BERT", "RoBERTa", "ALBERT"]} -if 'ensemble_tracker' not in st.session_state: - st.session_state.ensemble_tracker = ModelPerformanceTracker() -if 'ensemble_classifier' not in st.session_state: - st.session_state.ensemble_classifier = EnsembleSpamClassifier(performance_tracker=st.session_state.ensemble_tracker) -if 'ensemble_history' not in st.session_state: - st.session_state.ensemble_history = [] -if 'loaded_models' not in st.session_state: - st.session_state.loaded_models = {model_name: None for model_name in ["DistilBERT", "BERT", "RoBERTa", "ALBERT"]} - +# Custom type for session state +class SessionState: + def __init__(self): + self.classification_history: List[Dict[str, Any]] = [] + self.model_stats: Dict[str, ModelStats] = {} + self.ensemble_tracker: ModelPerformanceTracker = ModelPerformanceTracker() + self.ensemble_classifier: EnsembleSpamClassifier = EnsembleSpamClassifier(performance_tracker=self.ensemble_tracker) + self.ensemble_history: List[Dict[str, Any]] = [] + self.loaded_models: Dict[str, Any] = {} + self.speech_handler: SpeechHandler = SpeechHandler() # --- Model Configurations --- MODEL_OPTIONS = { @@ -269,6 +237,33 @@ def get_all_predictions(self, predictions): } } +# --- Session State Initialization --- +def init_session_state(): + """Initialize session state with proper typing""" + if 'classification_history' not in st.session_state: + st.session_state.classification_history = [] + if 'ensemble_history' not in st.session_state: + st.session_state.ensemble_history = [] + if 'model_stats' not in st.session_state: + st.session_state.model_stats = { + model: {'spam': 0, 'ham': 0, 'total': 0} + for model in MODEL_OPTIONS.keys() + } + if 'ensemble_tracker' not in st.session_state: + st.session_state.ensemble_tracker = ecm.ModelPerformanceTracker() + if 'ensemble_classifier' not in st.session_state: + st.session_state.ensemble_classifier = ecm.EnsembleSpamClassifier( + performance_tracker=st.session_state.ensemble_tracker + ) + if 'loaded_models' not in st.session_state: + st.session_state.loaded_models = { + model_name: None for model_name in MODEL_OPTIONS.keys() + } + if 'speech_handler' not in st.session_state: + st.session_state.speech_handler = SpeechHandler() + +init_session_state() + # --- Header --- st.markdown("""
@@ -284,9 +279,18 @@ def get_all_predictions(self, predictions): # --- Sidebar --- with st.sidebar: - # --- Dark Mode Toggle --- + # --- Initialize Session State --- if 'dark_mode' not in st.session_state: st.session_state.dark_mode = False + st.session_state.classification_history = [] + st.session_state.ensemble_history = [] + st.session_state.model_stats = {} + tracker = ecm.ModelPerformanceTracker() + st.session_state.ensemble_tracker = tracker + st.session_state.ensemble_classifier = ecm.EnsembleSpamClassifier( + performance_tracker=tracker + ) + st.session_state.loaded_models = {} st.markdown("""

Analysis Mode

@@ -355,37 +359,79 @@ def get_all_predictions(self, predictions): # Sidebar Overall Stats st.markdown("### ๐Ÿ“Š Overall Statistics") - total_single_predictions = sum(st.session_state.model_stats[model]['total'] for model in MODEL_OPTIONS) - total_ensemble_predictions = len(st.session_state.ensemble_history) - total_predictions_overall = total_single_predictions + total_ensemble_predictions - st.markdown(f""" -
-

Total Predictions

-

{total_predictions_overall}

-
- """, unsafe_allow_html=True) + # Safely calculate statistics with proper error handling + try: + # Ensure session state is properly initialized + if not hasattr(st.session_state, 'model_stats'): + st.session_state.model_stats = {model: {'spam': 0, 'ham': 0, 'total': 0} for model in MODEL_OPTIONS.keys()} + if not hasattr(st.session_state, 'ensemble_history'): + st.session_state.ensemble_history = [] + + total_single_predictions = sum( + st.session_state.model_stats.get(model, {}).get('total', 0) + for model in MODEL_OPTIONS.keys() + ) + total_ensemble_predictions = len(st.session_state.ensemble_history) + total_predictions_overall = total_single_predictions + total_ensemble_predictions - overall_spam_count = sum(st.session_state.model_stats[model]['spam'] for model in MODEL_OPTIONS) + \ - sum(1 for entry in st.session_state.ensemble_history if entry['prediction'] == 'SPAM') - overall_ham_count = sum(st.session_state.model_stats[model]['ham'] for model in MODEL_OPTIONS) + \ - sum(1 for entry in st.session_state.ensemble_history if entry['prediction'] == 'HAM') - col_spam, col_ham = st.columns(2) - with col_spam: st.markdown(f""" -
-

Spam Count

-

{overall_spam_count}

+
+

Total Predictions

+

{total_predictions_overall}

""", unsafe_allow_html=True) - with col_ham: - st.markdown(f""" -
-

Ham Count

-

{overall_ham_count}

+ + overall_spam_count = sum( + st.session_state.model_stats.get(model, {}).get('spam', 0) + for model in MODEL_OPTIONS.keys() + ) + sum(1 for entry in st.session_state.ensemble_history if entry['prediction'] == 'SPAM') + + overall_ham_count = sum( + st.session_state.model_stats.get(model, {}).get('ham', 0) + for model in MODEL_OPTIONS.keys() + ) + sum(1 for entry in st.session_state.ensemble_history if entry['prediction'] == 'HAM') + + col_spam, col_ham = st.columns(2) + with col_spam: + st.markdown(f""" +
+

Spam Count

+

{overall_spam_count}

+
+ """, unsafe_allow_html=True) + with col_ham: + st.markdown(f""" +
+

Ham Count

+

{overall_ham_count}

+
+ """, unsafe_allow_html=True) + except (AttributeError, KeyError, TypeError): + # Handle case where session state is not properly initialized + st.markdown(""" +
+

Total Predictions

+

0

""", unsafe_allow_html=True) + col_spam, col_ham = st.columns(2) + with col_spam: + st.markdown(""" +
+

Spam Count

+

0

+
+ """, unsafe_allow_html=True) + with col_ham: + st.markdown(""" +
+

Ham Count

+

0

+
+ """, unsafe_allow_html=True) + # --- Model Loading Helpers --- @st.cache_resource @@ -414,8 +460,8 @@ def _load_model_cached(model_id): if tokenizer is None or model is None: return None pipe = pipeline( - "text-classification", - model=model, + "text-classification", + model=model, tokenizer=tokenizer, device=0 if torch.cuda.is_available() else -1 ) @@ -424,35 +470,23 @@ def _load_model_cached(model_id): st.error(f"โŒ Error creating pipeline for {model_id}: {str(e)}") return None + def load_model_if_needed(model_name, _progress_callback=None): - if st.session_state.loaded_models[model_name] is None: + """Load a model if not already loaded, with optional progress callback.""" + if model_name not in st.session_state.loaded_models or st.session_state.loaded_models[model_name] is None: + if _progress_callback: + _progress_callback(f"Loading {model_name} model...") + model_id = MODEL_OPTIONS[model_name]["id"] - status_container = st.empty() - def update_status(message): - if status_container: - status_container.info(message) - if _progress_callback: - _progress_callback(message) - try: - update_status(f"Starting to load {model_name}...") - update_status(f"๐Ÿ”„ Loading tokenizer for {model_name}...") - update_status(f"๐Ÿค– Loading {model_name} model... (This may take a few minutes)") - model = _load_model_cached(model_id) - if model is not None: - update_status(f"โœ… Successfully loaded {model_name}") - st.session_state.loaded_models[model_name] = model - else: - update_status(f"โŒ Failed to load {model_name}") - return None - time.sleep(1) - except Exception as e: - update_status(f"โŒ Error loading {model_name}: {str(e)}") - return None - finally: - time.sleep(1) - status_container.empty() + st.session_state.loaded_models[model_name] = _load_model_cached(model_id) + + if _progress_callback: + _progress_callback(f"โœ… {model_name} model loaded successfully!") + return st.session_state.loaded_models[model_name] + + def get_loaded_models(): models = {} progress_bar = st.progress(0) @@ -943,14 +977,21 @@ def render_ensemble_dashboard(): st.markdown("### ๐Ÿง  Ensemble Method Analytics") # Analyze ensemble history - method_stats = defaultdict(lambda: {'count': 0, 'spam': 0, 'confidences': []}) + method_stats: Dict[str, MethodStats] = {} + for method in ENSEMBLE_METHODS.keys(): + method_stats[method] = MethodStats( + count=0, + spam=0, + confidences=[] + ) for item in st.session_state.ensemble_history: method = item['method'] - method_stats[method]['count'] += 1 - method_stats[method]['confidences'].append(item['confidence']) - if item['prediction'] == 'SPAM': - method_stats[method]['spam'] += 1 + if method in method_stats: + method_stats[method]['count'] += 1 + method_stats[method]['confidences'].append(item['confidence']) + if item['prediction'] == 'SPAM': + method_stats[method]['spam'] += 1 if method_stats: col1, col2 = st.columns(2) @@ -1252,7 +1293,7 @@ def render_realtime_monitor(): 'total_messages': len(st.session_state.classification_history) + len(st.session_state.ensemble_history) } - json_data = st.json.dumps(dashboard_data, indent=2, default=str) + json_data = json.dumps(dashboard_data, indent=2, default=str) st.download_button( label="๐Ÿ“ฅ Download Dashboard Data (JSON)", @@ -1323,13 +1364,40 @@ def get_ensemble_predictions(message, models): # Set initial value of text_area based on sample_selector or previous user input user_sms_initial_value = selected_message if selected_message else st.session_state.get('user_sms_input_value', "") - user_sms = st.text_area( - "Enter SMS message to analyse", - value=user_sms_initial_value, - height=120, - placeholder="Type or paste your SMS message here...", - help="Enter the SMS message you want to classify as spam or ham (legitimate)" - ) + col_text, col_record = st.columns([4, 1]) + + with col_text: + user_sms = st.text_area( + "Enter SMS message to analyse", + value=user_sms_initial_value, + height=120, + placeholder="Type or paste your SMS message here...", + help="Enter the SMS message you want to classify as spam or ham (legitimate)", + key="sms_input" + ) + + with col_record: + if st.button("๐ŸŽค Record Voice", type="primary", key="record_btn"): + with st.spinner("๐ŸŽค Listening... Speak clearly"): + try: + success, result = st.session_state.speech_handler.listen_and_transcribe() + if success: + # Get existing text and append new text + current_text = user_sms # Use the widget value directly + if current_text and current_text.strip(): + new_text = f"{current_text.strip()} {result}" + else: + new_text = result + # Update session state for persistence + st.session_state.user_sms_input_value = new_text + st.success("โœ… Speech recognized!") + st.rerun() + else: + st.error(f"โŒ {result}") + except Exception as e: + st.error(f"Error: {str(e)}") + time.sleep(2) + # Store current text_area value in session state for persistence st.session_state.user_sms_input_value = user_sms @@ -1364,6 +1432,10 @@ def get_ensemble_predictions(message, models): result = classifier(cleaned_sms)[0] label = result['label'].upper() confidence = result['score'] + # Safely update model statistics with proper error handling + if selected_model_name not in st.session_state.model_stats: + st.session_state.model_stats[selected_model_name] = {'spam': 0, 'ham': 0, 'total': 0} + st.session_state.model_stats[selected_model_name][label.lower()] += 1 st.session_state.model_stats[selected_model_name]['total'] += 1 st.session_state.classification_history.append({ @@ -1515,7 +1587,7 @@ def get_ensemble_predictions(message, models): st.markdown("#### ๐Ÿ“Š Single Model Performance") # Check if there's any data for any model - if any(st.session_state.model_stats[model]['total'] > 0 for model in MODEL_OPTIONS): + if any(st.session_state.model_stats.get(model, {}).get('total', 0) > 0 for model in MODEL_OPTIONS.keys()): # Pie Chart for Spam/Ham Distribution of the SELECTED model current_model_stats = st.session_state.model_stats[selected_model_name] if current_model_stats['total'] > 0: @@ -1975,4 +2047,3 @@ def classify_csv(file, ensemble_mode, selected_models_for_bulk, selected_ensembl
""", unsafe_allow_html=True) - diff --git a/requirements.txt b/requirements.txt index 1e05383..827b476 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,6 +23,21 @@ python-dotenv>=1.0.0,<2.0.0 pytest>=7.4.0,<8.0.0 pytest-cov>=4.1.0,<5.0.0 -#UI -streamlit -fpdf \ No newline at end of file +# UI Components +streamlit>=1.30.0 +fpdf2>=2.7.0 + +# Speech Recognition +SpeechRecognition>=3.10.0 +PyAudio>=0.2.13 +pocketsphinx>=5.0.0 + +# Type Checking +mypy>=1.5.0 +types-requests>=2.31.0 +types-setuptools>=68.0.0 +types-protobuf>=4.24.0 +typing-extensions>=4.7.0 +pandas-stubs>=2.0.0 +types-tqdm>=4.65.0 +types-PyYAML>=6.0.0 \ No newline at end of file diff --git a/speech_handler.py b/speech_handler.py new file mode 100644 index 0000000..19ff284 --- /dev/null +++ b/speech_handler.py @@ -0,0 +1,151 @@ +""" +Speech Handler Module + +A friendly and robust speech recognition handler that converts speech to text. +Handles microphone input, noise adjustment, and multiple recognition engines. +""" + +import speech_recognition as sr +from typing import Tuple, Any + + +class SpeechHandler: + """ + A user-friendly speech recognition handler. + + This class provides an easy-to-use interface for converting speech to text + with automatic error handling and fallback mechanisms. + """ + + def __init__(self): + """Initialize the speech handler with optimized settings.""" + self.recognizer: Any = sr.Recognizer() + self._setup_recognizer() + + def _setup_recognizer(self) -> None: + """Configure the recognizer with optimal settings for better accuracy.""" + # Adjust energy threshold for better speech detection + if hasattr(self.recognizer, "energy_threshold"): + self.recognizer.energy_threshold = 300 + + # Enable dynamic energy threshold for adaptive listening + if hasattr(self.recognizer, "dynamic_energy_threshold"): + self.recognizer.dynamic_energy_threshold = True + + # Set pause threshold for natural speech breaks + if hasattr(self.recognizer, "pause_threshold"): + self.recognizer.pause_threshold = 0.8 + + def _validate_audio(self, audio) -> bool: + """Validate that the captured audio is suitable for processing.""" + if not audio or not hasattr(audio, 'sample_rate'): + return False + + # Check if audio is too short (less than 0.5 seconds) + if len(audio.frame_data) / audio.sample_rate < 0.5: + return False + + return True + + def listen_and_transcribe(self, timeout: int = 5, max_duration: int = 10) -> Tuple[bool, str]: + """ + Listen for speech and convert it to text. + + Args: + timeout: Maximum time to wait for speech (seconds) + max_duration: Maximum duration of speech to record (seconds) + + Returns: + Tuple of (success: bool, result: str) + """ + try: + # Create a new microphone instance for fresh capture + microphone = sr.Microphone() + + with microphone as source: + print("๐ŸŽค Adjusting for ambient noise... Please wait.") + # Adjust for ambient noise to improve recognition + self.recognizer.adjust_for_ambient_noise(source, duration=1) + + try: + print("๐ŸŽค Listening... Please speak clearly.") + audio = self.recognizer.listen( + source, + timeout=timeout, + phrase_time_limit=max_duration + ) + + # Validate the captured audio + if not self._validate_audio(audio): + return False, "โŒ Audio too short or invalid. Please try again." + + except sr.WaitTimeoutError: + return False, "โฐ No speech detected. Please try speaking again." + except Exception as e: + return False, f"โŒ Error recording audio: {str(e)}" + + # Convert speech to text with fallback mechanisms + return self._transcribe_audio(audio) + + except Exception as e: + return False, f"๐Ÿ’ฅ Critical error: {str(e)}" + + def _transcribe_audio(self, audio) -> Tuple[bool, str]: + """Transcribe the captured audio to text using multiple engines.""" + try: + # Primary: Try Google's speech recognition (most accurate) + try: + print("๐Ÿ”„ Processing with Google Speech Recognition...") + result = self.recognizer.recognize_google(audio) + if result and result.strip(): + print(f"โœ… Recognized: '{result}'") + return True, result + except (sr.UnknownValueError, sr.RequestError) as e: + print(f"โš ๏ธ Google recognition failed: {str(e)}") + + # Fallback: Try local Sphinx recognizer if available + try: + print("๐Ÿ”„ Trying offline recognition...") + result = self.recognizer.recognize_sphinx(audio) + if result and result.strip(): + print(f"โœ… Offline recognition: '{result}'") + return True, result + except ImportError: + print("โ„น๏ธ Offline recognizer not available") + except Exception as e: + print(f"โš ๏ธ Offline recognition failed: {str(e)}") + + return False, "โ“ Could not understand the audio. Please speak more clearly." + + except Exception as e: + return False, f"โŒ Speech recognition error: {str(e)}" + + def cleanup(self) -> None: + """Clean up resources when done.""" + try: + if hasattr(self, 'recognizer'): + del self.recognizer + print("๐Ÿงน Speech handler cleaned up successfully") + except Exception as e: + print(f"โš ๏ธ Cleanup warning: {str(e)}") + + +# Example usage +if __name__ == "__main__": + print("๐ŸŽค Speech Handler Demo") + print("=" * 50) + + handler = SpeechHandler() + + try: + success, text = handler.listen_and_transcribe(timeout=5, max_duration=10) + + if success: + print(f"\n๐ŸŽ‰ Success! Transcribed text: '{text}'") + else: + print(f"\nโŒ Failed: {text}") + + except KeyboardInterrupt: + print("\nโน๏ธ Stopped by user") + finally: + handler.cleanup() diff --git a/speech_to_text.py b/speech_to_text.py new file mode 100644 index 0000000..5b9399a --- /dev/null +++ b/speech_to_text.py @@ -0,0 +1,313 @@ +""" +๐ŸŽค Speech to Text Module + +A friendly and robust speech recognition system with real-time processing capabilities. +Supports both continuous listening and one-time transcription with callback support. + +Features: +- ๐ŸŽฏ Real-time continuous listening +- ๐Ÿ”„ One-time transcription +- ๐Ÿ›ก๏ธ Comprehensive error handling +- ๐ŸŽจ User-friendly interface +- โšก Multi-threaded processing +""" + +import speech_recognition as sr +import threading +import queue +import time +from typing import Optional, Callable, Dict, Any + + +class SpeechToText: + """ + ๐ŸŽค Friendly Speech-to-Text Converter + + This class provides an easy-to-use interface for converting speech to text + with both real-time continuous listening and one-time transcription capabilities. + + Features: + - Real-time speech recognition with callbacks + - Single utterance transcription + - Robust error handling with user-friendly messages + - Multi-threaded processing for smooth performance + - Automatic microphone calibration + """ + + def __init__(self): + """Initialize the speech-to-text system with optimal settings.""" + print("๐Ÿš€ Initializing Speech-to-Text system...") + + self.recognizer = sr.Recognizer() + self.microphone = sr.Microphone() + self.is_listening = False + self.audio_queue = queue.Queue() + self.error_callback: Optional[Callable] = None + self.text_callback: Optional[Callable] = None + self.processing_thread: Optional[threading.Thread] = None + + # Configure optimal settings + self._setup_recognizer() + print("โœ… Speech-to-Text system ready!") + + def _setup_recognizer(self) -> None: + """Configure the recognizer with optimal settings for better accuracy.""" + try: + print("๐ŸŽค Calibrating microphone for ambient noise...") + with self.microphone as source: + self.recognizer.adjust_for_ambient_noise(source, duration=1) + print("โœ… Microphone calibrated successfully!") + + # Set optimal recognition parameters + self.recognizer.energy_threshold = 300 + self.recognizer.dynamic_energy_threshold = True + self.recognizer.pause_threshold = 0.8 + + except OSError as e: + print(f"โš ๏ธ Microphone access error: {str(e)}") + print("๐Ÿ’ก Please check your microphone permissions and try again.") + except Exception as e: + print(f"โš ๏ธ Microphone setup warning: {str(e)}") + print("๐Ÿ”ง Using default microphone settings.") + + def set_callbacks(self, text_callback: Callable, error_callback: Callable) -> None: + """ + Set callback functions for handling transcription results and errors. + + Args: + text_callback: Function to call when text is recognized + error_callback: Function to call when an error occurs + """ + self.text_callback = text_callback + self.error_callback = error_callback + print("๐Ÿ“ž Callbacks configured successfully!") + + def start_listening(self) -> None: + """Start continuous listening in a background thread.""" + if not self.is_listening: + self.is_listening = True + print("๐ŸŽค Starting continuous listening...") + threading.Thread(target=self._listen_loop, daemon=True).start() + else: + print("โ„น๏ธ Already listening!") + + def stop_listening(self) -> None: + """Stop the continuous listening loop.""" + if self.is_listening: + self.is_listening = False + print("โน๏ธ Stopped listening.") + else: + print("โ„น๏ธ Not currently listening.") + + def _listen_loop(self) -> None: + """Main listening loop that runs in a separate thread.""" + while self.is_listening: + try: + with self.microphone as source: + audio = self.recognizer.listen( + source, + timeout=5, + phrase_time_limit=10 + ) + self.audio_queue.put(audio) + + # Process audio in a separate thread to avoid blocking + threading.Thread( + target=self._process_audio, + args=(audio,), + daemon=True + ).start() + + except sr.WaitTimeoutError: + continue # No speech detected, continue listening + except Exception as e: + if self.error_callback: + self.error_callback(f"โŒ Error capturing audio: {str(e)}") + time.sleep(1) # Prevent tight loop on error + + def _process_audio(self, audio) -> None: + """Process captured audio and convert to text with fallback options.""" + try: + print("๐Ÿ”„ Processing audio...") + # Try Google Speech Recognition first (most accurate) + text = self.recognizer.recognize_google(audio) # type: ignore + + if text and text.strip(): + print(f"โœ… Recognized: '{text}'") + if self.text_callback: + self.text_callback(text) + else: + print("โš ๏ธ Empty recognition result") + + except sr.UnknownValueError: + error_msg = "โ“ Could not understand the audio. Please speak more clearly." + print(error_msg) + if self.error_callback: + self.error_callback(error_msg) + + except sr.RequestError as e: + error_msg = f"โŒ Could not connect to Google Speech Recognition: {str(e)}" + print(error_msg) + print("๐Ÿ’ก Trying offline recognition...") + + # Fallback to offline recognition if available + try: + text = self.recognizer.recognize_sphinx(audio) # type: ignore + if text and text.strip(): + print(f"โœ… Offline recognition: '{text}'") + if self.text_callback: + self.text_callback(text) + else: + error_msg = "โŒ Offline recognition also failed" + print(error_msg) + if self.error_callback: + self.error_callback(error_msg) + except ImportError: + error_msg = "โŒ Offline recognizer not available. Please install PocketSphinx." + print(error_msg) + if self.error_callback: + self.error_callback(error_msg) + except Exception as offline_error: + error_msg = f"โŒ Offline recognition failed: {str(offline_error)}" + print(error_msg) + if self.error_callback: + self.error_callback(error_msg) + + except Exception as e: + error_msg = f"๐Ÿ’ฅ Unexpected error processing audio: {str(e)}" + print(error_msg) + if self.error_callback: + self.error_callback(error_msg) + + def transcribe_once(self, timeout: int = 5, max_duration: int = 10) -> Optional[str]: + """ + Capture and transcribe a single utterance with user-friendly feedback. + + Args: + timeout: Maximum time to wait for speech (seconds) + max_duration: Maximum duration of speech to record (seconds) + + Returns: + Transcribed text or None if failed + """ + try: + print("๐ŸŽค Listening for single utterance... Speak now!") + print("(Press Ctrl+C to cancel)") + + with self.microphone as source: + audio = self.recognizer.listen( + source, + timeout=timeout, + phrase_time_limit=max_duration + ) + + print("๐Ÿ”„ Processing your speech...") + # Try Google Speech Recognition first + text = self.recognizer.recognize_google(audio) # type: ignore + + if text and text.strip(): + print(f"๐ŸŽ‰ Success! Transcribed: '{text}'") + return text + else: + print("โš ๏ธ No speech content detected") + return None + + except sr.UnknownValueError: + print("โ“ Could not understand the audio. Please speak more clearly.") + except sr.RequestError as e: + print(f"โŒ Could not connect to Google Speech Recognition: {str(e)}") + print("๐Ÿ’ก Trying offline recognition...") + + # Fallback to offline recognition + try: + text = self.recognizer.recognize_sphinx(audio) # type: ignore + if text and text.strip(): + print(f"๐ŸŽ‰ Offline recognition successful: '{text}'") + return text + else: + print("โŒ Offline recognition also failed") + except ImportError: + print("โŒ Offline recognizer not available. Please install PocketSphinx for offline support.") + except Exception as offline_error: + print(f"โŒ Offline recognition failed: {str(offline_error)}") + except sr.WaitTimeoutError: + print("โฐ No speech detected within timeout period. Please try again.") + except KeyboardInterrupt: + print("\nโน๏ธ Cancelled by user") + except Exception as e: + print(f"๐Ÿ’ฅ Unexpected error: {str(e)}") + + return None + + def get_status(self) -> dict: + """Get the current status of the speech recognition system.""" + return { + "is_listening": self.is_listening, + "queue_size": self.audio_queue.qsize(), + "has_text_callback": self.text_callback is not None, + "has_error_callback": self.error_callback is not None + } + + +# Example usage and demo +def demo_text_callback(text: str) -> None: + """Demo callback for successful text recognition.""" + print(f"๐Ÿ“ Text callback: {text}") + + +def demo_error_callback(error: str) -> None: + """Demo callback for errors.""" + print(f"๐Ÿšจ Error callback: {error}") + + +if __name__ == "__main__": + print("๐ŸŽค Speech to Text Demo") + print("=" * 50) + + # Create instance + stt = SpeechToText() + + print("\nChoose an option:") + print("1. Single transcription") + print("2. Continuous listening demo") + print("3. Status check") + + try: + choice = input("\nEnter your choice (1-3): ").strip() + + if choice == "1": + print("\n--- Single Transcription Mode ---") + result = stt.transcribe_once() + if result: + print(f"\nFinal result: {result}") + else: + print("\nNo result obtained.") + + elif choice == "2": + print("\n--- Continuous Listening Mode ---") + stt.set_callbacks(demo_text_callback, demo_error_callback) + stt.start_listening() + + print("Listening... Say something! (Press Ctrl+C to stop)") + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + stt.stop_listening() + print("\nStopped listening.") + + elif choice == "3": + print("\n--- Status Check ---") + status = stt.get_status() + for key, value in status.items(): + print(f"{key}: {value}") + + else: + print("โŒ Invalid choice. Please run again.") + + except KeyboardInterrupt: + print("\nโน๏ธ Demo interrupted by user") + except Exception as e: + print(f"๐Ÿ’ฅ Demo error: {str(e)}") + + print("\n๐Ÿ‘‹ Demo completed!") diff --git a/types.py b/types.py new file mode 100644 index 0000000..4ab188b --- /dev/null +++ b/types.py @@ -0,0 +1,53 @@ +"""Type definitions for the Spamlyser application.""" +from typing import TypeVar, Dict, Any, List, Union, Optional +from typing_extensions import TypedDict, Protocol + +# Type for model predictions +class PredictionDict(TypedDict): + label: str + score: float + spam_probability: Optional[float] + +# Type for model statistics +class ModelStats(TypedDict): + spam: int + ham: int + total: int + +# Type for method statistics +class MethodStats(TypedDict): + count: int + spam: int + confidences: List[float] + +# Type for ensemble predictions +class EnsemblePrediction(TypedDict): + label: str + confidence: float + method: str + spam_probability: float + details: str + +# Type for model options +class ModelOption(TypedDict): + id: str + description: str + icon: str + color: str + +# Protocol for models +class Model(Protocol): + def __call__(self, text: Union[str, List[str]]) -> List[PredictionDict]: ... + +# Type variables +T = TypeVar('T') +ModelName = str +ModelDict = Dict[ModelName, Model] +OptionsDict = Dict[ModelName, ModelOption] +StatsDict = Dict[ModelName, ModelStats] +MethodStatsDict = Dict[str, MethodStats] + +# Type aliases +JsonDict = Dict[str, Any] +Confidence = float +Label = str