scam-detector / app.py
carolinembithe's picture
Update app.py
a25b5c4 verified
Raw
History Blame Contribute Delete
8.53 kB
from flask import Flask, render_template, request, jsonify
import torch
from transformers import RobertaTokenizer, RobertaForSequenceClassification
import os
from werkzeug.utils import secure_filename
from audio_transcriber import AudioTranscriber
from flask_socketio import SocketIO, emit
import threading
from company_verifier import CompanyVerifier
from pattern_analyzer import get_analyzer
app = Flask(__name__)
app.config['SECRET_KEY'] = 'callshield-secret'
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
ALLOWED_EXTENSIONS = {'wav', 'mp3', 'ogg', 'm4a', 'flac', 'webm', 'opus', 'mp4'}
if not os.path.exists(app.config['UPLOAD_FOLDER']):
os.makedirs(app.config['UPLOAD_FOLDER'])
class SpamClassifier:
def __init__(self, model_path='./spam_classifier_model'):
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
self.tokenizer = RobertaTokenizer.from_pretrained(model_path)
self.model = RobertaForSequenceClassification.from_pretrained(model_path)
self.model.to(self.device)
self.model.eval()
self.pattern_analyzer = get_analyzer()
def predict(self, text):
# ML score
inputs = self.tokenizer(text, return_tensors='pt', truncation=True, padding=True, max_length=256)
inputs = {key: val.to(self.device) for key, val in inputs.items()}
with torch.no_grad():
outputs = self.model(**inputs)
probabilities = torch.softmax(outputs.logits, dim=-1)[0]
ml_score = round(probabilities[1].item() * 100, 1)
# Rule-based pattern score
pattern_result = self.pattern_analyzer.analyze(text)
pattern_score = pattern_result['pattern_score']
# Hybrid scoring logic:
# - Pattern rules are high-precision (Kenyan-specific keywords).
# When they fire, trust them.
# - The ML model was trained on limited/generic data and tends to
# over-flag legitimate professional communications (loans, payslips,
# dividends) that mention money. When no patterns match, dampen the
# ML score significantly to avoid false positives.
if pattern_score == 0:
# No rule-based evidence found — cap ML contribution at 45%
# so it can still flag borderline cases but won't alarm on
# legitimate SACCO/bank/payroll messages
final_score = round(min(ml_score * 0.5, 45.0), 1)
elif pattern_score >= 40:
# Strong pattern evidence — take the higher of the two
final_score = round(max(ml_score, pattern_score), 1)
else:
# Weak pattern signal — blend, slightly discounting ML
final_score = round(max(ml_score * 0.7, pattern_score), 1)
return {
'scam_risk': final_score,
'is_spam': final_score >= 50.0,
'ml_score': ml_score,
'pattern_score': pattern_score,
'matched_flags': pattern_result['matched_flags'],
'scam_type': pattern_result['scam_type'],
}
# Initialize models globally at startup
print("Initializing models...")
classifier = None
transcriber = None
verifier = None
if os.path.exists('./spam_classifier_model'):
print("Loading spam classifier...")
classifier = SpamClassifier()
print("Spam classifier loaded!")
print("Loading Whisper small model...")
transcriber = AudioTranscriber()
print("Whisper model loaded!")
print("Loading company verifier...")
verifier = CompanyVerifier()
print("Company verifier loaded!")
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/')
def home():
return render_template('index.html')
@app.route('/predict', methods=['POST'])
def predict():
if classifier is None:
return jsonify({'error': 'Model not found. Train the model first.'}), 400
data = request.get_json()
text = data.get('text', '')
if not text:
return jsonify({'error': 'No text provided'}), 400
result = classifier.predict(text)
return jsonify(result)
@app.route('/analyze-audio', methods=['POST'])
def analyze_audio():
if 'audio' not in request.files:
return jsonify({'error': 'No audio file provided'}), 400
file = request.files['audio']
if file.filename == '':
return jsonify({'error': 'No file selected'}), 400
# Accept webm/opus from browser mic even without extension
fname = file.filename or 'audio.webm'
if not allowed_file(fname):
# Try by content-type
ct = file.content_type or ''
if not any(x in ct for x in ['audio', 'video/webm']):
return jsonify({'error': 'Invalid file type'}), 400
if classifier is None:
return jsonify({'error': 'Model not found. Train the model first.'}), 400
# Get caller number if provided
caller_number = request.form.get('caller_number', '')
# Save file
fname = file.filename if file.filename else 'audio.webm'
filename = secure_filename(fname) or 'audio.webm'
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(filepath)
try:
# Transcribe
text = transcriber.transcribe(filepath)
if not text.strip():
return jsonify({'error': 'No speech detected'}), 400
# Classify
result = classifier.predict(text)
result['transcribed_text'] = text
# Verify company if caller number provided
if caller_number and verifier:
verification = verifier.verify_call(caller_number, text)
if verification['is_impersonation']:
result['is_spam'] = True
result['scam_risk'] = max(result['scam_risk'], 90.0)
result['warning'] = verification['warning']
return jsonify(result)
except Exception as e:
return jsonify({'error': str(e)}), 500
finally:
if os.path.exists(filepath):
os.remove(filepath)
# Real-time text analysis
@socketio.on('analyze_text')
def handle_realtime_text(data):
text = data.get('text', '')
if text and len(text) > 10 and classifier:
result = classifier.predict(text)
emit('analysis_result', {
'scam_risk': result['scam_risk'],
'is_spam': result['is_spam'],
'matched_flags': result.get('matched_flags', []),
'scam_type': result.get('scam_type'),
})
# Real-time microphone
from realtime_analyzer import RealtimeAnalyzer
analyzer = None
print("Initializing real-time analyzer...")
try:
analyzer = RealtimeAnalyzer(transcriber=transcriber, classifier=classifier)
print("Real-time analyzer ready!")
except Exception as e:
print(f"Warning: Could not initialize microphone: {e}")
@socketio.on('start_microphone')
def handle_start_mic():
global analyzer
if not analyzer:
try:
analyzer = RealtimeAnalyzer(transcriber=transcriber, classifier=classifier)
except Exception as e:
emit('mic_error', {'error': str(e)})
return
def callback(result):
# Include transcribed text and verify company
text = result.get('transcribed_text', '')
caller_number = '' # Get from call metadata if available
response = {
'scam_risk': result['scam_risk'],
'is_spam': result['is_spam'],
'transcribed_text': text
}
if verifier and text:
verification = verifier.verify_call(caller_number, text)
if verification['is_impersonation']:
response['is_spam'] = True
response['scam_risk'] = max(result['scam_risk'], 90.0)
response['warning'] = verification['warning']
socketio.emit('mic_result', response)
analyzer.start_recording(callback)
emit('mic_status', {'status': 'recording'})
@socketio.on('stop_microphone')
def handle_stop_mic():
if analyzer:
analyzer.stop_recording()
emit('mic_status', {'status': 'stopped'})
if __name__ == '__main__':
port = int(os.environ.get('PORT', 7860))
debug = os.environ.get('FLASK_DEBUG', 'false').lower() == 'true'
socketio.run(app, host='0.0.0.0', port=port, debug=debug, allow_unsafe_werkzeug=True)