gkc55 commited on
Commit
adce462
·
1 Parent(s): 6d7e3e4

Initial Docker deployment of LSB Steganography Flask app

Browse files
Dockerfile ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10
2
+
3
+ WORKDIR /app
4
+
5
+ COPY . /app
6
+
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
+
9
+ EXPOSE 7860
10
+
11
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, jsonify
2
+ import torch
3
+ import torch.nn as nn
4
+ from PIL import Image
5
+ import numpy as np
6
+ import io
7
+ import pickle
8
+ from scipy.stats import entropy
9
+
10
+ app = Flask(__name__)
11
+ app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024
12
+
13
+ # --------------------
14
+ # Model definitions
15
+ # --------------------
16
+ class BinaryDNN(nn.Module):
17
+ def __init__(self):
18
+ super().__init__()
19
+ self.net = nn.Sequential(
20
+ nn.Linear(196614, 384), nn.BatchNorm1d(384), nn.ReLU(), nn.Dropout(0.45),
21
+ nn.Linear(384, 192), nn.BatchNorm1d(192), nn.ReLU(), nn.Dropout(0.35),
22
+ nn.Linear(192, 96), nn.BatchNorm1d(96), nn.ReLU(), nn.Dropout(0.25),
23
+ nn.Linear(96, 2))
24
+ def forward(self, x): return self.net(x)
25
+
26
+ class MultiDNN(nn.Module):
27
+ def __init__(self):
28
+ super().__init__()
29
+ self.net = nn.Sequential(
30
+ nn.Linear(196614, 384), nn.BatchNorm1d(384), nn.ReLU(), nn.Dropout(0.45),
31
+ nn.Linear(384, 192), nn.BatchNorm1d(192), nn.ReLU(), nn.Dropout(0.35),
32
+ nn.Linear(192, 96), nn.BatchNorm1d(96), nn.ReLU(), nn.Dropout(0.25),
33
+ nn.Linear(96, 4))
34
+ def forward(self, x): return self.net(x)
35
+
36
+ # --------------------
37
+ # Load artifacts ONCE
38
+ # --------------------
39
+ with open('final_scaler.pkl', 'rb') as f:
40
+ scaler = pickle.load(f)
41
+ with open('final_bin_le.pkl', 'rb') as f:
42
+ binary_le = pickle.load(f)
43
+ with open('final_multi_le.pkl', 'rb') as f:
44
+ multi_le = pickle.load(f)
45
+
46
+ device = torch.device('cpu')
47
+ binary_model = BinaryDNN()
48
+ binary_model.load_state_dict(torch.load('final_bin_best.pth', map_location=device))
49
+ binary_model.eval()
50
+ multi_model = MultiDNN()
51
+ multi_model.load_state_dict(torch.load('final_multi_best.pth', map_location=device))
52
+ multi_model.eval()
53
+
54
+ # --------------------
55
+ # V3 Feature extraction
56
+ # --------------------
57
+ def extract_lsb_feature(img, k=2):
58
+ mask = (1 << k) - 1
59
+ return (img & mask).flatten()
60
+
61
+ def extract_randomness_features(lsb_feat):
62
+ chunk_size = 512
63
+ lsb_reshaped = lsb_feat.reshape(-1, chunk_size)
64
+ ent_values = []
65
+ for block in lsb_reshaped[::10]:
66
+ hist = np.bincount(block.astype(np.int32), minlength=4)
67
+ hist = hist / max(hist.sum(), 1)
68
+ ent = entropy(hist + 1e-10)
69
+ ent_values.append(ent)
70
+ mean_entropy = np.mean(ent_values)
71
+ sample = lsb_feat[:5000].astype(np.int32)
72
+ transitions = np.sum(np.abs(np.diff(sample)))
73
+ transition_rate = transitions / max(len(sample) - 1, 1)
74
+ hist_full = np.bincount(lsb_feat.astype(np.int32), minlength=4)
75
+ expected = len(lsb_feat) / 4
76
+ chi2_stat = np.sum((hist_full - expected) ** 2 / expected) if expected > 0 else 0
77
+ return np.array([mean_entropy, transition_rate, chi2_stat], dtype=np.float32)
78
+
79
+ def extract_url_pattern_features(lsb_feat):
80
+ chunk_size = 4
81
+ num_chunks = len(lsb_feat) // chunk_size
82
+ lsb_trimmed = lsb_feat[:num_chunks * chunk_size].astype(np.int32)
83
+ byte_values = []
84
+ for i in range(0, len(lsb_trimmed), chunk_size):
85
+ val = (lsb_trimmed[i] << 6) | (lsb_trimmed[i+1] << 4) | (lsb_trimmed[i+2] << 2) | lsb_trimmed[i+3]
86
+ byte_values.append(val)
87
+ byte_values = np.array(byte_values)
88
+ url_char_count = np.sum((byte_values >= 10) & (byte_values <= 15))
89
+ html_char_count = np.sum((byte_values >= 0) & (byte_values <= 5))
90
+ unique_ratio = len(np.unique(byte_values)) / max(len(byte_values), 1)
91
+ return np.array([
92
+ url_char_count / max(len(byte_values), 1),
93
+ html_char_count / max(len(byte_values), 1),
94
+ unique_ratio
95
+ ], dtype=np.float32)
96
+
97
+ def extract_features(image, k=2):
98
+ image = image.resize((256, 256), Image.LANCZOS)
99
+ if image.mode != 'RGB':
100
+ image = image.convert('RGB')
101
+ img_array = np.array(image)
102
+ lsb_feat = extract_lsb_feature(img_array, k=k)
103
+ rand_feat = extract_randomness_features(lsb_feat)
104
+ url_feat = extract_url_pattern_features(lsb_feat)
105
+ combined = np.concatenate([lsb_feat.astype(np.float32), rand_feat, url_feat])
106
+ return combined
107
+
108
+ # --------------------
109
+ # Flask endpoints
110
+ # --------------------
111
+ @app.route('/')
112
+ def index():
113
+ return render_template('index.html')
114
+
115
+ @app.route('/predict', methods=['POST'])
116
+ def predict():
117
+ try:
118
+ file = request.files['file']
119
+ image = Image.open(io.BytesIO(file.read()))
120
+ features = extract_features(image)
121
+ features_normalized = scaler.transform(features.reshape(1, -1)).astype(np.float32)
122
+ features_tensor = torch.FloatTensor(features_normalized)
123
+
124
+ with torch.no_grad():
125
+ binary_output = binary_model(features_tensor)
126
+ binary_probs = torch.softmax(binary_output, dim=1).cpu().numpy()[0]
127
+ binary_pred_idx = int(np.argmax(binary_probs))
128
+ binary_class = binary_le.classes_[binary_pred_idx]
129
+
130
+ if binary_class == 'stego':
131
+ with torch.no_grad():
132
+ multi_output = multi_model(features_tensor)
133
+ multi_probs = torch.softmax(multi_output, dim=1).cpu().numpy()[0]
134
+ multi_pred_idx = int(np.argmax(multi_probs))
135
+ payload_type = multi_le.classes_[multi_pred_idx]
136
+ else:
137
+ payload_type = 'clean'
138
+ multi_probs = np.zeros(len(multi_le.classes_))
139
+ multi_probs[multi_le.transform(['clean'])[0]] = 1.0
140
+
141
+ result = {
142
+ 'binary_prediction': {
143
+ 'class': binary_class,
144
+ 'confidence': float(binary_probs[binary_pred_idx]),
145
+ 'probabilities': {c: float(binary_probs[binary_le.transform([c])[0]]) for c in binary_le.classes_}
146
+ },
147
+ 'multiclass_prediction': {
148
+ 'payload_type': payload_type,
149
+ 'probabilities': {c: float(multi_probs[i]) for i, c in enumerate(multi_le.classes_)}
150
+ }
151
+ }
152
+ return jsonify(result)
153
+ except Exception as e:
154
+ import traceback
155
+ print('Error:', str(e))
156
+ traceback.print_exc()
157
+ return jsonify({'error': str(e)}), 500
158
+
159
+ @app.errorhandler(413)
160
+ def request_entity_too_large(error):
161
+ return jsonify({'error': 'File size exceeds 5MB'}), 413
162
+
163
+
164
+ if __name__ == "__main__":
165
+ port = int(os.environ.get("PORT", 7860))
166
+ app.run(debug=True, host="0.0.0.0", port=port)
final_bin_best.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:937140babfa952b004688c3bb98665434afd2b901658e5e8ff4de0438c107d90
3
+ size 302389710
final_bin_le.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:90b3bd41467e805a270b7d2b1ca04eb1ea9d31670d16bf5c4022f81f5c3c6d9f
3
+ size 276
final_multi_best.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2d10330083813ade2f03ff8365b24abdc732f5728f1470372390863f748073ee
3
+ size 302390532
final_multi_le.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b16da8d351473e04c4cf7ebcac502f4f7410e7832ec44db0f2e2f1c8d6ddf93b
3
+ size 316
final_scaler.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0a115a9cf586486270825e41369e3d3e6f2367f6f49c16f1d583d6ac626015a2
3
+ size 4719234
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ flask
2
+ torch
3
+ numpy
4
+ scipy
5
+ pillow
6
+ scikit-learn
templates/index.html ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Multi-Payload LSB Steganalysis Demo</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link href="https://fonts.googleapis.com/css?family=Inter:600,700&amp;display=swap" rel="stylesheet">
9
+ <style>
10
+ body {
11
+ min-height: 100vh;
12
+ background: linear-gradient(120deg,#090e1a 0%,#1e293b 75%,#64748b 100%);
13
+ font-family: 'Inter', Arial, sans-serif;
14
+ margin:0;padding:0;
15
+ overflow-x: hidden;
16
+ }
17
+ .main-card {
18
+ background: rgba(255,255,255,0.92);
19
+ border-radius: 2rem;
20
+ box-shadow: 0 12px 40px rgba(20,32,64,0.21);
21
+ margin: 2.2rem auto;
22
+ max-width: 790px;
23
+ width: 98vw;
24
+ padding: 2.7rem 2.2rem 2.7rem 2.2rem;
25
+ transition: box-shadow 0.18s;
26
+ position: relative;
27
+ backdrop-filter: blur(2px);
28
+ }
29
+ .header-title {
30
+ font-size: 2.4rem;
31
+ font-weight: 900;
32
+ color: #2563eb;
33
+ text-align: center;
34
+ letter-spacing: .016em;
35
+ margin-bottom: 0.18em;
36
+ text-shadow: 0 3px 12px #64748b26;
37
+ }
38
+ .header-desc {
39
+ text-align: center;
40
+ color: #334155;
41
+ font-size: 1.22rem;
42
+ font-weight: 600;
43
+ margin-bottom: 1.5em;
44
+ letter-spacing: 0.07em;
45
+ }
46
+ /* Glass info panel */
47
+ .info-glass {
48
+ background: rgba(229,240,255,0.82);
49
+ border-radius: 1.1rem;
50
+ box-shadow: 0 2px 18px #0797c522, 0 0.5px 3.5px #ffd70033;
51
+ padding: 1.3em 1.3em 0.9em 1.3em;
52
+ margin-bottom: 1.45em;
53
+ }
54
+ .info-title { color: #2563eb; font-size: 1.12em; font-weight: 700; margin-bottom: 0.7em;}
55
+ .info-snip { color: #23304f; font-size: 0.97em; margin-bottom: 0.3em;}
56
+ .info-payloads td, .info-payloads th {padding: 0.26em .8em;}
57
+ .info-collapsible { cursor: pointer; font-size: 1.07rem; color: #334155; background:none;border:none;font-weight:700;}
58
+ .info-collapsible:after {content: "▼";font-size:.9em;margin-left:.5em;}
59
+ .info-collapsible.collapsed:after {content:"►";}
60
+ .info-content {display:block;transition: max-height .3s cubic-bezier(.32,1.8,.75,1);}
61
+ .info-content.collapsed {display:none;}
62
+ .dropzone { border: 3px dashed #2563eb; border-radius: 1.2rem; text-align: center; background: #f5faff; padding: 2.3rem 1.1rem; cursor: pointer; transition: border-color 0.22s, background 0.2s, box-shadow .21s;}
63
+ .dropzone:hover { background: #dbeafe; border-color: #3b82f6; box-shadow: 0 3px 26px #2563eb33; }
64
+ .preview-img { max-width: 250px; border-radius: 1rem; margin-top: 1rem; box-shadow: 0 0 16px #3b82f655; background: #fff;}
65
+ .result-badge { font-size: 1.2em; font-weight: 800; padding: 0.45em 1.6em; border-radius: 2em; letter-spacing: 0.03em; display:inline-block; margin-bottom:0.3em; box-shadow: 0 3px 10px #0002;}
66
+ .prob-bar-wrap { display: flex; align-items: center; gap:0.45em; margin-bottom: 0.6em;}
67
+ .prob-bar-label { width:80px; font-weight:700; font-size:1.09em; color:#222b3a; letter-spacing:.01em; text-transform:uppercase; text-align:right;}
68
+ .prob-bar { flex: 1; height: 29px; border-radius: 1em; background: #e0e7ef; overflow: hidden; position:relative; margin-right:6px;}
69
+ .prob-bar-inner { height:100%;display:flex;align-items:center; font-weight:700;color:#fff;font-size:1.17em;padding-left:1em; border-radius:1em;transition:width 0.55s cubic-bezier(.32,1.2,.69,1);}
70
+ .prob-bar-value { min-width:52px;text-align:right;font-weight:600;font-size:1.05em;margin-left:4px; color:#1e293b;}
71
+ .icon-big { font-size: 2.2em; vertical-align: sub; margin-right: 0.23em;}
72
+ .icon-clean { color: #1dd8a2;}
73
+ .icon-stego { color: #ef4444;}
74
+ /* Subtle watermark background icon */
75
+ .bg-watermark {
76
+ position: absolute; bottom: 14px; right: 18px; z-index:0; opacity:.12; font-size:5.5em; pointer-events:none;
77
+ }
78
+ @media(max-width: 900px){ .main-card{ max-width:99vw; padding:1.3rem}}
79
+ @media(max-width: 520px){ .prob-bar-label{width:54px;font-size:0.99em}}
80
+ </style>
81
+ </head>
82
+ <body>
83
+ <div class="container-fluid">
84
+ <div class="main-card">
85
+ <!-- Watermark icon -->
86
+ <div class="bg-watermark"><span class="icon-big icon-stego">🔒</span></div>
87
+ <!-- Header section -->
88
+ <div class="header-title">Multi-Payload LSB Steganalysis Demo</div>
89
+ <div class="header-desc">
90
+ Analyze PNG images for hidden data using advanced<br>Least Significant Bit detection and AI.<br>
91
+ Upload an image and instantly detect steganographic payloads!
92
+ </div>
93
+ <!-- Info section -->
94
+ <div class="info-glass mb-4">
95
+ <button class="info-collapsible" onclick="toggleInfo()">About & Knowledge  </button>
96
+ <div id="infoPanel" class="info-content collapsed">
97
+ <div class="info-title">What is Steganography?</div>
98
+ <div class="info-snip">Steganography is the science of hiding information within digital media so it is invisible to regular inspection. Images, audio, and video files are common carriers.</div>
99
+ <div class="info-title mt-2">What are LSBs and Payloads?</div>
100
+ <div class="info-snip">LSB (Least Significant Bit) steganography modifies the lowest-value bits in an image (usually the last 1 or 2 of each pixel) to embed secret data called the payload.</div>
101
+ <div class="info-title mt-2">What Payload Types Are Detected?</div>
102
+ <table class="info-payloads mb-1 small">
103
+ <tr><th>Payload</th><th>Meaning</th></tr>
104
+ <tr><td>HTML</td><td>Hidden HTML code</td></tr>
105
+ <tr><td>JS</td><td>Hidden JavaScript code</td></tr>
106
+ <tr><td>PS</td><td>Hidden Powershell script</td></tr>
107
+ <tr><td>Clean</td><td>No payload / genuine image</td></tr>
108
+ </table>
109
+ <div class="info-title mt-2">How does the model decide?</div>
110
+ <div class="info-snip mb-2">The neural network analyzes subtle changes in the 2 least significant bits of every image pixel, plus additional statistics, to predict if a payload is present and its type. Confidence is shown for each class.</div>
111
+ </div>
112
+ </div>
113
+ <!-- Upload & results UI -->
114
+ <div class="dropzone" id="dropzone">
115
+ <span class="fs-3">🖼️</span>
116
+ <div class="mt-2 mb-1 fw-semibold">Upload PNG image or drag & drop</div>
117
+ <div class="text-muted small mb-0">Max 5MB &nbsp;|&nbsp; Secure PyTorch Model</div>
118
+ <input type="file" id="fileInput" accept="image/png" style="display:none;">
119
+ </div>
120
+ <div id="preview" style="display:none;" class="text-center mt-3">
121
+ <img id="previewImg" class="preview-img mb-2" alt="Preview"/>
122
+ <div id="filename" class="small text-muted mt-1"></div>
123
+ </div>
124
+ <button id="analyzeBtn" class="btn btn-primary w-100 mt-3 fw-bold" disabled>Analyze Image</button>
125
+ <div id="loading" style="display:none;" class="text-center mt-3">
126
+ <div class="spinner-border text-primary" style="width:2.1rem;height:2.1rem;"></div>
127
+ <p class="mt-2">Analyzing image...</p>
128
+ </div>
129
+ <div id="results" style="display:none;" class="mt-4"></div>
130
+ </div>
131
+ </div>
132
+ <script>
133
+ let selectedFile = null;
134
+ const dropzone = document.getElementById('dropzone');
135
+ const fileInput = document.getElementById('fileInput');
136
+ const preview = document.getElementById('preview');
137
+ const previewImg = document.getElementById('previewImg');
138
+ const filenameDiv = document.getElementById('filename');
139
+ const analyzeBtn = document.getElementById('analyzeBtn');
140
+ const loading = document.getElementById('loading');
141
+ const results = document.getElementById('results');
142
+ const infoPanel = document.getElementById('infoPanel');
143
+
144
+ dropzone.onclick = () => fileInput.click();
145
+ dropzone.ondragover = (e) => { e.preventDefault(); dropzone.style.background = "#dbeafe"; };
146
+ dropzone.ondragleave = () => dropzone.style.background = "#f5faff";
147
+ dropzone.ondrop = (e) => {
148
+ e.preventDefault(); dropzone.style.background = "#dbeafe";
149
+ if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]);
150
+ };
151
+ fileInput.onchange = (e) => { if (e.target.files.length) handleFile(e.target.files[0]); };
152
+ function handleFile(file) {
153
+ if (!file.name.toLowerCase().endsWith('.png')) { alert('Please select a PNG image.'); return; }
154
+ selectedFile = file; analyzeBtn.disabled = false;
155
+ preview.style.display = 'block';
156
+ filenameDiv.textContent = `${file.name} (${(file.size/1024).toFixed(1)} KB)`;
157
+ const reader = new FileReader();
158
+ reader.onload = e => { previewImg.src = e.target.result; }; reader.readAsDataURL(file);
159
+ results.style.display = 'none';
160
+ }
161
+ analyzeBtn.onclick = async () => {
162
+ if (!selectedFile) return;
163
+ loading.style.display = 'block'; results.style.display = 'none'; analyzeBtn.disabled = true;
164
+ const formData = new FormData(); formData.append('file', selectedFile);
165
+ try {
166
+ const resp = await fetch('/predict', { method:'POST', body:formData });
167
+ const data = await resp.json();
168
+ if (resp.ok) displayResults(data); else alert(data.error || 'Prediction failed');
169
+ } catch (e) { alert('Network error: ' + e.message); }
170
+ loading.style.display = 'none'; analyzeBtn.disabled = false;
171
+ };
172
+ // Collapsible info
173
+ function toggleInfo(){
174
+ infoPanel.classList.toggle('collapsed');
175
+ document.querySelector('.info-collapsible').classList.toggle('collapsed');
176
+ }
177
+ // Custom icons for clean/stego
178
+ const CLEAN_EMOJI = '<span class="icon-big icon-clean">🔓</span>';
179
+ const STEGO_EMOJI = '<span class="icon-big icon-stego">🔒</span>';
180
+ function displayResults(data) {
181
+ let html = '';
182
+ const b = data.binary_prediction, m = data.multiclass_prediction;
183
+ html += `<div class="mb-2 text-center">
184
+ <span class="result-badge"
185
+ style="background:${b.class==='clean'?'#14b8a6':'#ef4444'};color:#fff">
186
+ ${b.class==='clean' ? CLEAN_EMOJI+'CLEAN' : STEGO_EMOJI+'STEGO'}
187
+ </span>
188
+ <div class="small mt-1 mb-2" style="color:#34425a;">Binary prediction (Clean vs. Stego)</div>
189
+ <div class="prob-bar-wrap">
190
+ <span class="prob-bar-label">CLEAN</span>
191
+ <div class="prob-bar"><div class="prob-bar-inner" style="width:${(b.probabilities.clean*100).toFixed(1)}%;background:#10b981;"></div></div>
192
+ <span class="prob-bar-value">${(b.probabilities.clean*100).toFixed(1)}%</span>
193
+ </div>
194
+ <div class="prob-bar-wrap">
195
+ <span class="prob-bar-label">STEGO</span>
196
+ <div class="prob-bar"><div class="prob-bar-inner" style="width:${(b.probabilities.stego*100).toFixed(1)}%;background:#ef4444;"></div></div>
197
+ <span class="prob-bar-value">${(b.probabilities.stego*100).toFixed(1)}%</span>
198
+ </div>
199
+ </div>`;
200
+ if (b.class === 'stego') {
201
+ html += `<div class="mb-2 text-center">
202
+ <span class="result-badge" style="background:#3b82f6;color:#fff">Payload: ${m.payload_type.toUpperCase()}</span>
203
+ <div class="small mb-2" style="color:#2a3140;">Multi-class payload classification</div>`;
204
+ const colors = {html:'#6366f1',js:'#f97316',ps:'#14b8a6',clean:'#06b6d4'};
205
+ Object.entries(m.probabilities)
206
+ .sort((a,b)=>b[1]-a[1])
207
+ .forEach(([k,v]) => {
208
+ let color = colors[k] || '#818cf8';
209
+ html += `<div class="prob-bar-wrap">
210
+ <span class="prob-bar-label">${k.toUpperCase()}</span>
211
+ <div class="prob-bar">
212
+ <div class="prob-bar-inner" style="width:${(v*100).toFixed(1)}%;background:${color};"></div>
213
+ </div>
214
+ <span class="prob-bar-value">${(v*100).toFixed(1)}%</span>
215
+ </div>`;
216
+ });
217
+ html += `</div>`;
218
+ }
219
+ results.innerHTML = html; results.style.display = 'block';
220
+ }
221
+ </script>
222
+ </body>
223
+ </html>