hackerbyhobby commited on
Commit
aaede28
·
unverified ·
1 Parent(s): 2a76f84

added text to voice

Browse files
Files changed (3) hide show
  1. app.py +76 -62
  2. app.py.bestoftues +380 -0
  3. requirements.txt +1 -0
app.py CHANGED
@@ -7,6 +7,12 @@ from langdetect import detect
7
  from deep_translator import GoogleTranslator
8
  import openai
9
  import os
 
 
 
 
 
 
10
 
11
  # Set your OpenAI API key
12
  openai.api_key = os.getenv("OPENAI_API_KEY")
@@ -26,10 +32,39 @@ model_name = "joeddav/xlm-roberta-large-xnli"
26
  classifier = pipeline("zero-shot-classification", model=model_name)
27
  CANDIDATE_LABELS = ["SMiShing", "Other Scam", "Legitimate"]
28
 
29
- def get_keywords_by_language(text: str):
30
  """
31
- Detect language using langdetect and translate keywords if needed.
 
 
 
 
 
 
 
32
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  snippet = text[:200]
34
  try:
35
  detected_lang = detect(snippet)
@@ -48,9 +83,6 @@ def get_keywords_by_language(text: str):
48
  return SMISHING_KEYWORDS, OTHER_SCAM_KEYWORDS, "en"
49
 
50
  def boost_probabilities(probabilities: dict, text: str):
51
- """
52
- Boost probabilities based on keyword matches and presence of URLs.
53
- """
54
  lower_text = text.lower()
55
  smishing_keywords, other_scam_keywords, detected_lang = get_keywords_by_language(text)
56
 
@@ -60,7 +92,10 @@ def boost_probabilities(probabilities: dict, text: str):
60
  smishing_boost = 0.30 * smishing_count
61
  other_scam_boost = 0.30 * other_scam_count
62
 
63
- found_urls = re.findall(r"(https?://[^\s]+|\b(?:[a-zA-Z0-9.-]+\.(?:com|net|org|edu|gov|mil|io|ai|co|info|biz|us|uk|de|fr|es|ru|jp|cn|in|au|ca|br|mx|it|nl|se|no|fi|ch|pl|kr|vn|id|tw|sg|hk))\b)", lower_text)
 
 
 
64
  if found_urls:
65
  smishing_boost += 0.35
66
 
@@ -77,7 +112,6 @@ def boost_probabilities(probabilities: dict, text: str):
77
  p_other_scam = max(p_other_scam, 0.0)
78
  p_legit = max(p_legit, 0.0)
79
 
80
- # Re-normalize
81
  total = p_smishing + p_other_scam + p_legit
82
  if total > 0:
83
  p_smishing /= total
@@ -94,10 +128,6 @@ def boost_probabilities(probabilities: dict, text: str):
94
  }
95
 
96
  def query_llm_for_classification(raw_message: str) -> dict:
97
- """
98
- First LLM call: asks for a classification (SMiShing, Other Scam, or Legitimate)
99
- acting as a cybersecurity expert. Returns label and short reason.
100
- """
101
  if not raw_message.strip():
102
  return {"label": "Unknown", "reason": "No message provided to the LLM."}
103
 
@@ -119,7 +149,6 @@ def query_llm_for_classification(raw_message: str) -> dict:
119
  )
120
  raw_reply = response["choices"][0]["message"]["content"].strip()
121
 
122
- import json
123
  llm_result = json.loads(raw_reply)
124
  if "label" not in llm_result or "reason" not in llm_result:
125
  return {"label": "Unknown", "reason": f"Unexpected format: {raw_reply}"}
@@ -130,19 +159,13 @@ def query_llm_for_classification(raw_message: str) -> dict:
130
  return {"label": "Unknown", "reason": f"LLM error: {e}"}
131
 
132
  def incorporate_llm_label(boosted: dict, llm_label: str) -> dict:
133
- """
134
- Adjust the final probabilities based on the LLM's classification.
135
- If LLM says SMiShing, add +0.2 to SMiShing, etc. Then clamp & re-normalize.
136
- """
137
  if llm_label == "SMiShing":
138
  boosted["SMiShing"] += 0.2
139
  elif llm_label == "Other Scam":
140
  boosted["Other Scam"] += 0.2
141
  elif llm_label == "Legitimate":
142
  boosted["Legitimate"] += 0.2
143
- # else "Unknown" => do nothing
144
 
145
- # clamp
146
  for k in boosted:
147
  if boosted[k] < 0:
148
  boosted[k] = 0.0
@@ -152,7 +175,6 @@ def incorporate_llm_label(boosted: dict, llm_label: str) -> dict:
152
  for k in boosted:
153
  boosted[k] /= total
154
  else:
155
- # fallback
156
  boosted["Legitimate"] = 1.0
157
  boosted["SMiShing"] = 0.0
158
  boosted["Other Scam"] = 0.0
@@ -172,21 +194,14 @@ def query_llm_for_explanation(
172
  found_urls: list,
173
  detected_lang: str
174
  ) -> str:
175
- """
176
- Second LLM call: provides a holistic explanation of the final classification
177
- in the same language as detected_lang (English or Spanish).
178
- """
179
- # Decide the language for final explanation
180
  if detected_lang == "es":
181
- # Spanish
182
  system_prompt = (
183
  "Eres un experto en ciberseguridad. Proporciona una explicación final al usuario en español. "
184
  "Combina la clasificación local, la clasificación LLM y la etiqueta final en una sola explicación breve. "
185
  "No reveles el código interno ni el JSON bruto; simplemente da una breve explicación fácil de entender. "
186
- "Termina con la etiqueta final. "
187
  )
188
  else:
189
- # Default to English
190
  system_prompt = (
191
  "You are a cybersecurity expert providing a final explanation to the user in English. "
192
  "Combine the local classification, the LLM classification, and the final label "
@@ -222,12 +237,6 @@ URLs => {found_urls}
222
  return f"Could not generate final explanation due to error: {e}"
223
 
224
  def smishing_detector(input_type, text, image):
225
- """
226
- Main detection function combining text (if 'Text') & OCR (if 'Screenshot'),
227
- plus two LLM calls:
228
- 1) classification to adjust final probabilities,
229
- 2) a final explanation summarizing the outcome in the detected language.
230
- """
231
  if input_type == "Text":
232
  combined_text = text.strip() if text else ""
233
  else:
@@ -247,7 +256,6 @@ def smishing_detector(input_type, text, image):
247
  "final_explanation": "No text provided"
248
  }
249
 
250
- # 1. Local zero-shot classification
251
  local_result = classifier(
252
  sequences=combined_text,
253
  candidate_labels=CANDIDATE_LABELS,
@@ -255,38 +263,33 @@ def smishing_detector(input_type, text, image):
255
  )
256
  original_probs = {k: float(v) for k, v in zip(local_result["labels"], local_result["scores"])}
257
 
258
- # 2. Basic boosting from keywords & URLs
259
  boosted = boost_probabilities(original_probs, combined_text)
260
  detected_lang = boosted.pop("detected_lang", "en")
261
 
262
- # Convert to float only
263
  for k in boosted:
264
  boosted[k] = float(boosted[k])
265
 
266
  local_label = max(boosted, key=boosted.get)
267
  local_conf = round(boosted[local_label], 3)
268
 
269
- # 3. LLM Classification
270
  llm_classification = query_llm_for_classification(combined_text)
271
  llm_label = llm_classification.get("label", "Unknown")
272
  llm_reason = llm_classification.get("reason", "No reason provided")
273
 
274
- # 4. Incorporate LLM’s label into final probabilities
275
  boosted = incorporate_llm_label(boosted, llm_label)
276
 
277
- # Now we have updated probabilities
278
  final_label = max(boosted, key=boosted.get)
279
  final_confidence = round(boosted[final_label], 3)
280
 
281
- # 5. Gather found keywords & URLs
282
  lower_text = combined_text.lower()
283
  smishing_keys, scam_keys, _ = get_keywords_by_language(combined_text)
284
-
285
- found_urls = re.findall(r"(https?://[^\s]+|\b(?:[a-zA-Z0-9.-]+\.(?:com|net|org|edu|gov|mil|io|ai|co|info|biz|us|uk|de|fr|es|ru|jp|cn|in|au|ca|br|mx|it|nl|se|no|fi|ch|pl|kr|vn|id|tw|sg|hk))\b)", lower_text)
 
 
286
  found_smishing = [kw for kw in smishing_keys if kw in lower_text]
287
  found_other_scam = [kw for kw in scam_keys if kw in lower_text]
288
 
289
- # 6. Final LLM explanation (in detected_lang)
290
  final_explanation = query_llm_for_explanation(
291
  text=combined_text,
292
  final_label=final_label,
@@ -317,24 +320,35 @@ def smishing_detector(input_type, text, image):
317
  "final_explanation": final_explanation,
318
  }
319
 
320
- #
321
- # Gradio interface with dynamic visibility
322
- #
323
- def toggle_inputs(choice):
324
  """
325
- Return updates for (text_input, image_input) based on the radio selection.
 
 
326
  """
 
 
 
 
 
 
 
 
 
 
 
327
  if choice == "Text":
328
- # Show text input, hide image
329
  return gr.update(visible=True), gr.update(visible=False)
330
  else:
331
- # choice == "Screenshot"
332
- # Hide text input, show image
333
  return gr.update(visible=False), gr.update(visible=True)
334
 
 
335
  with gr.Blocks() as demo:
336
- gr.Markdown("## SMiShing & Scam Detector with LLM-Enhanced Logic (Multilingual Explanation)")
337
-
338
  with gr.Row():
339
  input_type = gr.Radio(
340
  choices=["Text", "Screenshot"],
@@ -346,16 +360,14 @@ with gr.Blocks() as demo:
346
  lines=3,
347
  label="Paste Suspicious SMS Text",
348
  placeholder="Type or paste the message here...",
349
- visible=True # default
350
  )
351
-
352
  image_input = gr.Image(
353
  type="pil",
354
  label="Upload Screenshot",
355
- visible=False # hidden by default
356
  )
357
 
358
- # Whenever input_type changes, toggle which input is visible
359
  input_type.change(
360
  fn=toggle_inputs,
361
  inputs=input_type,
@@ -363,15 +375,17 @@ with gr.Blocks() as demo:
363
  queue=False
364
  )
365
 
366
- # Button to run classification
367
  analyze_btn = gr.Button("Classify")
368
- output_json = gr.JSON(label="Result")
369
 
370
- # On button click, call the smishing_detector
 
 
 
 
371
  analyze_btn.click(
372
- fn=smishing_detector,
373
  inputs=[input_type, text_input, image_input],
374
- outputs=output_json
375
  )
376
 
377
  if __name__ == "__main__":
 
7
  from deep_translator import GoogleTranslator
8
  import openai
9
  import os
10
+ import io
11
+ import requests
12
+ import json
13
+
14
+ # For text-to-speech
15
+ from gtts import gTTS
16
 
17
  # Set your OpenAI API key
18
  openai.api_key = os.getenv("OPENAI_API_KEY")
 
32
  classifier = pipeline("zero-shot-classification", model=model_name)
33
  CANDIDATE_LABELS = ["SMiShing", "Other Scam", "Legitimate"]
34
 
35
+ def tts_explanation(explanation: str, detected_lang: str):
36
  """
37
+ Generate TTS audio from the final explanation text.
38
+ We'll choose English or Spanish voices in gTTS, but cannot guarantee
39
+ a specific "female" voice. We'll do a best approximation.
40
+
41
+ - If text is Spanish: set lang="es"
42
+ - If text is English (or other): set lang="en"
43
+ - We'll set tld="co.uk" for a British accent that might sound female.
44
+ Adjust if needed or switch to a more advanced TTS service.
45
  """
46
+ # Choose language for gTTS
47
+ if detected_lang == "es":
48
+ lang_code = "es"
49
+ tld = "com"
50
+ else:
51
+ lang_code = "en"
52
+ # Attempt a 'comforting female' accent:
53
+ # gTTS doesn't let you pick male/female directly, but you can pick a TLD for a different accent
54
+ tld = "co.uk"
55
+
56
+ try:
57
+ tts = gTTS(text=explanation, lang=lang_code, tld=tld, slow=False)
58
+ mp3_bytes = io.BytesIO()
59
+ tts.write_to_fp(mp3_bytes)
60
+ mp3_bytes.seek(0)
61
+ return mp3_bytes
62
+ except Exception as e:
63
+ print("TTS generation error:", e)
64
+ # If TTS fails, return an empty buffer
65
+ return io.BytesIO()
66
+
67
+ def get_keywords_by_language(text: str):
68
  snippet = text[:200]
69
  try:
70
  detected_lang = detect(snippet)
 
83
  return SMISHING_KEYWORDS, OTHER_SCAM_KEYWORDS, "en"
84
 
85
  def boost_probabilities(probabilities: dict, text: str):
 
 
 
86
  lower_text = text.lower()
87
  smishing_keywords, other_scam_keywords, detected_lang = get_keywords_by_language(text)
88
 
 
92
  smishing_boost = 0.30 * smishing_count
93
  other_scam_boost = 0.30 * other_scam_count
94
 
95
+ found_urls = re.findall(
96
+ r"(https?://[^\s]+|\b[a-zA-Z0-9.-]+\.(?:com|net|org|edu|gov|mil|io|ai|co|info|biz|us|uk|de|fr|es|ru|jp|cn|in|au|ca|br|mx|it|nl|se|no|fi|ch|pl|kr|vn|id|tw|sg|hk)\b)",
97
+ lower_text
98
+ )
99
  if found_urls:
100
  smishing_boost += 0.35
101
 
 
112
  p_other_scam = max(p_other_scam, 0.0)
113
  p_legit = max(p_legit, 0.0)
114
 
 
115
  total = p_smishing + p_other_scam + p_legit
116
  if total > 0:
117
  p_smishing /= total
 
128
  }
129
 
130
  def query_llm_for_classification(raw_message: str) -> dict:
 
 
 
 
131
  if not raw_message.strip():
132
  return {"label": "Unknown", "reason": "No message provided to the LLM."}
133
 
 
149
  )
150
  raw_reply = response["choices"][0]["message"]["content"].strip()
151
 
 
152
  llm_result = json.loads(raw_reply)
153
  if "label" not in llm_result or "reason" not in llm_result:
154
  return {"label": "Unknown", "reason": f"Unexpected format: {raw_reply}"}
 
159
  return {"label": "Unknown", "reason": f"LLM error: {e}"}
160
 
161
  def incorporate_llm_label(boosted: dict, llm_label: str) -> dict:
 
 
 
 
162
  if llm_label == "SMiShing":
163
  boosted["SMiShing"] += 0.2
164
  elif llm_label == "Other Scam":
165
  boosted["Other Scam"] += 0.2
166
  elif llm_label == "Legitimate":
167
  boosted["Legitimate"] += 0.2
 
168
 
 
169
  for k in boosted:
170
  if boosted[k] < 0:
171
  boosted[k] = 0.0
 
175
  for k in boosted:
176
  boosted[k] /= total
177
  else:
 
178
  boosted["Legitimate"] = 1.0
179
  boosted["SMiShing"] = 0.0
180
  boosted["Other Scam"] = 0.0
 
194
  found_urls: list,
195
  detected_lang: str
196
  ) -> str:
 
 
 
 
 
197
  if detected_lang == "es":
 
198
  system_prompt = (
199
  "Eres un experto en ciberseguridad. Proporciona una explicación final al usuario en español. "
200
  "Combina la clasificación local, la clasificación LLM y la etiqueta final en una sola explicación breve. "
201
  "No reveles el código interno ni el JSON bruto; simplemente da una breve explicación fácil de entender. "
202
+ "Termina con la etiqueta final."
203
  )
204
  else:
 
205
  system_prompt = (
206
  "You are a cybersecurity expert providing a final explanation to the user in English. "
207
  "Combine the local classification, the LLM classification, and the final label "
 
237
  return f"Could not generate final explanation due to error: {e}"
238
 
239
  def smishing_detector(input_type, text, image):
 
 
 
 
 
 
240
  if input_type == "Text":
241
  combined_text = text.strip() if text else ""
242
  else:
 
256
  "final_explanation": "No text provided"
257
  }
258
 
 
259
  local_result = classifier(
260
  sequences=combined_text,
261
  candidate_labels=CANDIDATE_LABELS,
 
263
  )
264
  original_probs = {k: float(v) for k, v in zip(local_result["labels"], local_result["scores"])}
265
 
 
266
  boosted = boost_probabilities(original_probs, combined_text)
267
  detected_lang = boosted.pop("detected_lang", "en")
268
 
 
269
  for k in boosted:
270
  boosted[k] = float(boosted[k])
271
 
272
  local_label = max(boosted, key=boosted.get)
273
  local_conf = round(boosted[local_label], 3)
274
 
 
275
  llm_classification = query_llm_for_classification(combined_text)
276
  llm_label = llm_classification.get("label", "Unknown")
277
  llm_reason = llm_classification.get("reason", "No reason provided")
278
 
 
279
  boosted = incorporate_llm_label(boosted, llm_label)
280
 
 
281
  final_label = max(boosted, key=boosted.get)
282
  final_confidence = round(boosted[final_label], 3)
283
 
 
284
  lower_text = combined_text.lower()
285
  smishing_keys, scam_keys, _ = get_keywords_by_language(combined_text)
286
+ found_urls = re.findall(
287
+ r"(https?://[^\s]+|\b[a-zA-Z0-9.-]+\.(?:com|net|org|edu|gov|mil|io|ai|co|info|biz|us|uk|de|fr|es|ru|jp|cn|in|au|ca|br|mx|it|nl|se|no|fi|ch|pl|kr|vn|id|tw|sg|hk)\b)",
288
+ lower_text
289
+ )
290
  found_smishing = [kw for kw in smishing_keys if kw in lower_text]
291
  found_other_scam = [kw for kw in scam_keys if kw in lower_text]
292
 
 
293
  final_explanation = query_llm_for_explanation(
294
  text=combined_text,
295
  final_label=final_label,
 
320
  "final_explanation": final_explanation,
321
  }
322
 
323
+ ###
324
+ # Combined function to produce both text (JSON) and TTS audio
325
+ ###
326
+ def classify_and_tts(input_type, text, image):
327
  """
328
+ 1. Perform the classification logic (smishing_detector).
329
+ 2. Generate TTS audio from the final explanation in a comforting female voice.
330
+ 3. Return both the JSON result & the audio bytes.
331
  """
332
+ result = smishing_detector(input_type, text, image)
333
+ final_explanation = result["final_explanation"]
334
+ detected_lang = result.get("detected_language", "en")
335
+
336
+ # Generate TTS from final_explanation
337
+ audio_data = tts_explanation(final_explanation, detected_lang)
338
+ # Return both
339
+ return result, audio_data
340
+
341
+
342
+ def toggle_inputs(choice):
343
  if choice == "Text":
 
344
  return gr.update(visible=True), gr.update(visible=False)
345
  else:
 
 
346
  return gr.update(visible=False), gr.update(visible=True)
347
 
348
+
349
  with gr.Blocks() as demo:
350
+ gr.Markdown("## SMiShing & Scam Detector with LLM-Enhanced Logic + TTS Explanation")
351
+
352
  with gr.Row():
353
  input_type = gr.Radio(
354
  choices=["Text", "Screenshot"],
 
360
  lines=3,
361
  label="Paste Suspicious SMS Text",
362
  placeholder="Type or paste the message here...",
363
+ visible=True
364
  )
 
365
  image_input = gr.Image(
366
  type="pil",
367
  label="Upload Screenshot",
368
+ visible=False
369
  )
370
 
 
371
  input_type.change(
372
  fn=toggle_inputs,
373
  inputs=input_type,
 
375
  queue=False
376
  )
377
 
 
378
  analyze_btn = gr.Button("Classify")
 
379
 
380
+ # We'll show the classification JSON + TTS audio
381
+ output_json = gr.JSON(label="Classification Result")
382
+ audio_output = gr.Audio(label="TTS Explanation")
383
+
384
+ # We call classify_and_tts, which returns (dict_result, audio_data)
385
  analyze_btn.click(
386
+ fn=classify_and_tts,
387
  inputs=[input_type, text_input, image_input],
388
+ outputs=[output_json, audio_output]
389
  )
390
 
391
  if __name__ == "__main__":
app.py.bestoftues ADDED
@@ -0,0 +1,380 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import pytesseract
3
+ from PIL import Image
4
+ from transformers import pipeline
5
+ import re
6
+ from langdetect import detect
7
+ from deep_translator import GoogleTranslator
8
+ import openai
9
+ import os
10
+
11
+ # Set your OpenAI API key
12
+ openai.api_key = os.getenv("OPENAI_API_KEY")
13
+
14
+ # Translator instance
15
+ translator = GoogleTranslator(source="auto", target="es")
16
+
17
+ # 1. Load separate keywords for SMiShing and Other Scam (assumed in English)
18
+ with open("smishing_keywords.txt", "r", encoding="utf-8") as f:
19
+ SMISHING_KEYWORDS = [line.strip().lower() for line in f if line.strip()]
20
+
21
+ with open("other_scam_keywords.txt", "r", encoding="utf-8") as f:
22
+ OTHER_SCAM_KEYWORDS = [line.strip().lower() for line in f if line.strip()]
23
+
24
+ # 2. Zero-Shot Classification Pipeline
25
+ model_name = "joeddav/xlm-roberta-large-xnli"
26
+ classifier = pipeline("zero-shot-classification", model=model_name)
27
+ CANDIDATE_LABELS = ["SMiShing", "Other Scam", "Legitimate"]
28
+
29
+ def get_keywords_by_language(text: str):
30
+ """
31
+ Detect language using langdetect and translate keywords if needed.
32
+ """
33
+ snippet = text[:200]
34
+ try:
35
+ detected_lang = detect(snippet)
36
+ except Exception:
37
+ detected_lang = "en"
38
+
39
+ if detected_lang == "es":
40
+ smishing_in_spanish = [
41
+ translator.translate(kw).lower() for kw in SMISHING_KEYWORDS
42
+ ]
43
+ other_scam_in_spanish = [
44
+ translator.translate(kw).lower() for kw in OTHER_SCAM_KEYWORDS
45
+ ]
46
+ return smishing_in_spanish, other_scam_in_spanish, "es"
47
+ else:
48
+ return SMISHING_KEYWORDS, OTHER_SCAM_KEYWORDS, "en"
49
+
50
+ def boost_probabilities(probabilities: dict, text: str):
51
+ """
52
+ Boost probabilities based on keyword matches and presence of URLs.
53
+ """
54
+ lower_text = text.lower()
55
+ smishing_keywords, other_scam_keywords, detected_lang = get_keywords_by_language(text)
56
+
57
+ smishing_count = sum(1 for kw in smishing_keywords if kw in lower_text)
58
+ other_scam_count = sum(1 for kw in other_scam_keywords if kw in lower_text)
59
+
60
+ smishing_boost = 0.30 * smishing_count
61
+ other_scam_boost = 0.30 * other_scam_count
62
+
63
+ found_urls = re.findall(r"(https?://[^\s]+|\b(?:[a-zA-Z0-9.-]+\.(?:com|net|org|edu|gov|mil|io|ai|co|info|biz|us|uk|de|fr|es|ru|jp|cn|in|au|ca|br|mx|it|nl|se|no|fi|ch|pl|kr|vn|id|tw|sg|hk))\b)", lower_text)
64
+ if found_urls:
65
+ smishing_boost += 0.35
66
+
67
+ p_smishing = probabilities.get("SMiShing", 0.0)
68
+ p_other_scam = probabilities.get("Other Scam", 0.0)
69
+ p_legit = probabilities.get("Legitimate", 1.0)
70
+
71
+ p_smishing += smishing_boost
72
+ p_other_scam += other_scam_boost
73
+ p_legit -= (smishing_boost + other_scam_boost)
74
+
75
+ # Clamp
76
+ p_smishing = max(p_smishing, 0.0)
77
+ p_other_scam = max(p_other_scam, 0.0)
78
+ p_legit = max(p_legit, 0.0)
79
+
80
+ # Re-normalize
81
+ total = p_smishing + p_other_scam + p_legit
82
+ if total > 0:
83
+ p_smishing /= total
84
+ p_other_scam /= total
85
+ p_legit /= total
86
+ else:
87
+ p_smishing, p_other_scam, p_legit = 0.0, 0.0, 1.0
88
+
89
+ return {
90
+ "SMiShing": p_smishing,
91
+ "Other Scam": p_other_scam,
92
+ "Legitimate": p_legit,
93
+ "detected_lang": detected_lang
94
+ }
95
+
96
+ def query_llm_for_classification(raw_message: str) -> dict:
97
+ """
98
+ First LLM call: asks for a classification (SMiShing, Other Scam, or Legitimate)
99
+ acting as a cybersecurity expert. Returns label and short reason.
100
+ """
101
+ if not raw_message.strip():
102
+ return {"label": "Unknown", "reason": "No message provided to the LLM."}
103
+
104
+ system_prompt = (
105
+ "You are a cybersecurity expert. You will classify the user's message "
106
+ "as one of: SMiShing, Other Scam, or Legitimate. Provide a short reason. "
107
+ "Return only JSON with keys: label, reason."
108
+ )
109
+ user_prompt = f"Message: {raw_message}\nClassify it as SMiShing, Other Scam, or Legitimate."
110
+
111
+ try:
112
+ response = openai.ChatCompletion.create(
113
+ model="gpt-3.5-turbo",
114
+ messages=[
115
+ {"role": "system", "content": system_prompt},
116
+ {"role": "user", "content": user_prompt}
117
+ ],
118
+ temperature=0.2
119
+ )
120
+ raw_reply = response["choices"][0]["message"]["content"].strip()
121
+
122
+ import json
123
+ llm_result = json.loads(raw_reply)
124
+ if "label" not in llm_result or "reason" not in llm_result:
125
+ return {"label": "Unknown", "reason": f"Unexpected format: {raw_reply}"}
126
+
127
+ return llm_result
128
+
129
+ except Exception as e:
130
+ return {"label": "Unknown", "reason": f"LLM error: {e}"}
131
+
132
+ def incorporate_llm_label(boosted: dict, llm_label: str) -> dict:
133
+ """
134
+ Adjust the final probabilities based on the LLM's classification.
135
+ If LLM says SMiShing, add +0.2 to SMiShing, etc. Then clamp & re-normalize.
136
+ """
137
+ if llm_label == "SMiShing":
138
+ boosted["SMiShing"] += 0.2
139
+ elif llm_label == "Other Scam":
140
+ boosted["Other Scam"] += 0.2
141
+ elif llm_label == "Legitimate":
142
+ boosted["Legitimate"] += 0.2
143
+ # else "Unknown" => do nothing
144
+
145
+ # clamp
146
+ for k in boosted:
147
+ if boosted[k] < 0:
148
+ boosted[k] = 0.0
149
+
150
+ total = sum(boosted.values())
151
+ if total > 0:
152
+ for k in boosted:
153
+ boosted[k] /= total
154
+ else:
155
+ # fallback
156
+ boosted["Legitimate"] = 1.0
157
+ boosted["SMiShing"] = 0.0
158
+ boosted["Other Scam"] = 0.0
159
+
160
+ return boosted
161
+
162
+ def query_llm_for_explanation(
163
+ text: str,
164
+ final_label: str,
165
+ final_conf: float,
166
+ local_label: str,
167
+ local_conf: float,
168
+ llm_label: str,
169
+ llm_reason: str,
170
+ found_smishing: list,
171
+ found_other_scam: list,
172
+ found_urls: list,
173
+ detected_lang: str
174
+ ) -> str:
175
+ """
176
+ Second LLM call: provides a holistic explanation of the final classification
177
+ in the same language as detected_lang (English or Spanish).
178
+ """
179
+ # Decide the language for final explanation
180
+ if detected_lang == "es":
181
+ # Spanish
182
+ system_prompt = (
183
+ "Eres un experto en ciberseguridad. Proporciona una explicación final al usuario en español. "
184
+ "Combina la clasificación local, la clasificación LLM y la etiqueta final en una sola explicación breve. "
185
+ "No reveles el código interno ni el JSON bruto; simplemente da una breve explicación fácil de entender. "
186
+ "Termina con la etiqueta final. "
187
+ )
188
+ else:
189
+ # Default to English
190
+ system_prompt = (
191
+ "You are a cybersecurity expert providing a final explanation to the user in English. "
192
+ "Combine the local classification, the LLM classification, and the final label "
193
+ "into one concise explanation. Do not reveal internal code or raw JSON. "
194
+ "End with a final statement of the final label."
195
+ )
196
+
197
+ user_context = f"""
198
+ User Message:
199
+ {text}
200
+
201
+ Local Classification => Label: {local_label}, Confidence: {local_conf}
202
+ LLM Classification => Label: {llm_label}, Reason: {llm_reason}
203
+ Final Overall Label => {final_label} (confidence {final_conf})
204
+
205
+ Suspicious SMiShing Keywords => {found_smishing}
206
+ Suspicious Other Scam Keywords => {found_other_scam}
207
+ URLs => {found_urls}
208
+ """
209
+
210
+ try:
211
+ response = openai.ChatCompletion.create(
212
+ model="gpt-3.5-turbo",
213
+ messages=[
214
+ {"role": "system", "content": system_prompt},
215
+ {"role": "user", "content": user_context}
216
+ ],
217
+ temperature=0.2
218
+ )
219
+ final_explanation = response["choices"][0]["message"]["content"].strip()
220
+ return final_explanation
221
+ except Exception as e:
222
+ return f"Could not generate final explanation due to error: {e}"
223
+
224
+ def smishing_detector(input_type, text, image):
225
+ """
226
+ Main detection function combining text (if 'Text') & OCR (if 'Screenshot'),
227
+ plus two LLM calls:
228
+ 1) classification to adjust final probabilities,
229
+ 2) a final explanation summarizing the outcome in the detected language.
230
+ """
231
+ if input_type == "Text":
232
+ combined_text = text.strip() if text else ""
233
+ else:
234
+ combined_text = ""
235
+ if image is not None:
236
+ combined_text = pytesseract.image_to_string(image, lang="spa+eng").strip()
237
+
238
+ if not combined_text:
239
+ return {
240
+ "text_used_for_classification": "(none)",
241
+ "label": "No text provided",
242
+ "confidence": 0.0,
243
+ "keywords_found": [],
244
+ "urls_found": [],
245
+ "llm_label": "Unknown",
246
+ "llm_reason": "No text to analyze",
247
+ "final_explanation": "No text provided"
248
+ }
249
+
250
+ # 1. Local zero-shot classification
251
+ local_result = classifier(
252
+ sequences=combined_text,
253
+ candidate_labels=CANDIDATE_LABELS,
254
+ hypothesis_template="This message is {}."
255
+ )
256
+ original_probs = {k: float(v) for k, v in zip(local_result["labels"], local_result["scores"])}
257
+
258
+ # 2. Basic boosting from keywords & URLs
259
+ boosted = boost_probabilities(original_probs, combined_text)
260
+ detected_lang = boosted.pop("detected_lang", "en")
261
+
262
+ # Convert to float only
263
+ for k in boosted:
264
+ boosted[k] = float(boosted[k])
265
+
266
+ local_label = max(boosted, key=boosted.get)
267
+ local_conf = round(boosted[local_label], 3)
268
+
269
+ # 3. LLM Classification
270
+ llm_classification = query_llm_for_classification(combined_text)
271
+ llm_label = llm_classification.get("label", "Unknown")
272
+ llm_reason = llm_classification.get("reason", "No reason provided")
273
+
274
+ # 4. Incorporate LLM’s label into final probabilities
275
+ boosted = incorporate_llm_label(boosted, llm_label)
276
+
277
+ # Now we have updated probabilities
278
+ final_label = max(boosted, key=boosted.get)
279
+ final_confidence = round(boosted[final_label], 3)
280
+
281
+ # 5. Gather found keywords & URLs
282
+ lower_text = combined_text.lower()
283
+ smishing_keys, scam_keys, _ = get_keywords_by_language(combined_text)
284
+
285
+ found_urls = re.findall(r"(https?://[^\s]+|\b(?:[a-zA-Z0-9.-]+\.(?:com|net|org|edu|gov|mil|io|ai|co|info|biz|us|uk|de|fr|es|ru|jp|cn|in|au|ca|br|mx|it|nl|se|no|fi|ch|pl|kr|vn|id|tw|sg|hk))\b)", lower_text)
286
+ found_smishing = [kw for kw in smishing_keys if kw in lower_text]
287
+ found_other_scam = [kw for kw in scam_keys if kw in lower_text]
288
+
289
+ # 6. Final LLM explanation (in detected_lang)
290
+ final_explanation = query_llm_for_explanation(
291
+ text=combined_text,
292
+ final_label=final_label,
293
+ final_conf=final_confidence,
294
+ local_label=local_label,
295
+ local_conf=local_conf,
296
+ llm_label=llm_label,
297
+ llm_reason=llm_reason,
298
+ found_smishing=found_smishing,
299
+ found_other_scam=found_other_scam,
300
+ found_urls=found_urls,
301
+ detected_lang=detected_lang
302
+ )
303
+
304
+ return {
305
+ "detected_language": detected_lang,
306
+ "text_used_for_classification": combined_text,
307
+ "original_probabilities": {k: round(v, 3) for k, v in original_probs.items()},
308
+ "boosted_probabilities_before_llm": {local_label: local_conf},
309
+ "llm_label": llm_label,
310
+ "llm_reason": llm_reason,
311
+ "boosted_probabilities_after_llm": {k: round(v, 3) for k, v in boosted.items()},
312
+ "label": final_label,
313
+ "confidence": final_confidence,
314
+ "smishing_keywords_found": found_smishing,
315
+ "other_scam_keywords_found": found_other_scam,
316
+ "urls_found": found_urls,
317
+ "final_explanation": final_explanation,
318
+ }
319
+
320
+ #
321
+ # Gradio interface with dynamic visibility
322
+ #
323
+ def toggle_inputs(choice):
324
+ """
325
+ Return updates for (text_input, image_input) based on the radio selection.
326
+ """
327
+ if choice == "Text":
328
+ # Show text input, hide image
329
+ return gr.update(visible=True), gr.update(visible=False)
330
+ else:
331
+ # choice == "Screenshot"
332
+ # Hide text input, show image
333
+ return gr.update(visible=False), gr.update(visible=True)
334
+
335
+ with gr.Blocks() as demo:
336
+ gr.Markdown("## SMiShing & Scam Detector with LLM-Enhanced Logic (Multilingual Explanation)")
337
+
338
+ with gr.Row():
339
+ input_type = gr.Radio(
340
+ choices=["Text", "Screenshot"],
341
+ value="Text",
342
+ label="Choose Input Type"
343
+ )
344
+
345
+ text_input = gr.Textbox(
346
+ lines=3,
347
+ label="Paste Suspicious SMS Text",
348
+ placeholder="Type or paste the message here...",
349
+ visible=True # default
350
+ )
351
+
352
+ image_input = gr.Image(
353
+ type="pil",
354
+ label="Upload Screenshot",
355
+ visible=False # hidden by default
356
+ )
357
+
358
+ # Whenever input_type changes, toggle which input is visible
359
+ input_type.change(
360
+ fn=toggle_inputs,
361
+ inputs=input_type,
362
+ outputs=[text_input, image_input],
363
+ queue=False
364
+ )
365
+
366
+ # Button to run classification
367
+ analyze_btn = gr.Button("Classify")
368
+ output_json = gr.JSON(label="Result")
369
+
370
+ # On button click, call the smishing_detector
371
+ analyze_btn.click(
372
+ fn=smishing_detector,
373
+ inputs=[input_type, text_input, image_input],
374
+ outputs=output_json
375
+ )
376
+
377
+ if __name__ == "__main__":
378
+ if not openai.api_key:
379
+ print("WARNING: OPENAI_API_KEY not set. LLM calls will fail or be skipped.")
380
+ demo.launch()
requirements.txt CHANGED
@@ -10,3 +10,4 @@ sentencepiece==0.1.99
10
  numpy==1.25.0
11
  shap==0.41.0
12
  openai
 
 
10
  numpy==1.25.0
11
  shap==0.41.0
12
  openai
13
+ gTTS