I’m embedding a small UI with pywebview and want Python to JS live updates. I created a GPSSpoofingDetector class that loads a pickled sklearn model and a pandas test CSV. I want a JavaScript “Start” button to call a Python method that runs random_sample_testing, and during that test the Python code calls window.evaluate_js(...) to push JSON updates to the page.
This works if I run the test as the webview.start() callback (i.e. webview.start(detector.random_sample_testing, args=(window, data, feature_names), http_server=True)), but when I expose a Python API object to JS using js_api=api in webview.create_window() I immediately get errors before any button is clicked.
GPStest.py:
import time import json import webview import pickle import pandas as pd import numpy as np import warnings from pathlib import Path warnings.filterwarnings('ignore') class GPSSpoofingDetector: """Wrap original script into a class with small functions while preserving exact prints, returns and behavior. Usage: run this file directly. It will perform the same prints and return values as the original script. """ def __init__(self): # constants kept exactly as in the original script self.PROJECT_ROOT = Path(__file__).resolve().parents[2] # -> GPS_Spoofing_Detection self.MODEL_PATH = self.PROJECT_ROOT / "model" / "DT_model.pkl" self.TEST_CSV = self.PROJECT_ROOT / "dataset" / "testing" / "test_data.csv" self.FEATURE_COLUMNS = [2, 7, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 26] self.clf = None self.scaler = None print('initializing') def load_model(self): """Load the trained model and scaler from pickle. Prints same messages.""" print("Loading model...") with open(self.MODEL_PATH, 'rb') as f: self.clf, self.scaler = pickle.load(f) print("✓ Model loaded successfully\n") def predict_spoofing(self, gps_features_df, show_details=True): """ Predict GPS spoofing from features Parameters: - gps_features_df: DataFrame with proper column names - show_details: show prediction probabilities Returns: prediction label and numeric class """ features_scaled = self.scaler.transform(gps_features_df) prediction = self.clf.predict(features_scaled) labels = {0: 'Clean', 1: 'Static Spoofing', 2: 'Dynamic Spoofing'} result = labels[prediction[0]] if show_details and hasattr(self.clf, 'predict_proba'): probas = self.clf.predict_proba(features_scaled)[0] print(f"Prediction: {result}") print(f"Confidence:") for i, label in labels.items(): print(f" {label}: {probas[i]*100:.2f}%") return result, prediction[0] def load_data(self): """Load test CSV and return DataFrame. Keeps same variable names.""" data = pd.read_csv(self.TEST_CSV) return data def print_features_used(self, data): """Print feature names used exactly as original script.""" feature_names = data.columns[self.FEATURE_COLUMNS].tolist() print(f"\nFeatures used: {feature_names}\n") return feature_names def random_sample_testing(self, window, data, feature_names): """Perform the same random testing section and prints identical output.""" print("="*60) print("RANDOM SAMPLE TESTING (10 samples)") print("="*60) np.random.seed(42) random_indices = np.random.choice(len(data), 20, replace=False) correct = 0 labels = {0: 'Clean', 1: 'Static Spoofing', 2: 'Dynamic Spoofing'} for i, idx in enumerate(random_indices, 1): sample = data.iloc[[idx]][feature_names] actual = int(data.iloc[idx, 0]) prediction, pred_num = self.predict_spoofing(sample, show_details=False) match = "✓" if pred_num == actual else "✗" if pred_num == actual: correct += 1 if window: extra_cols = ['Label', 'lat', 'lon', 'alt', 'satellites_used'] # customize this list available = [c for c in extra_cols if c in data.columns] meta = data.loc[idx, available].to_dict() if available else {} print('meta: ', meta) # convert to JSON string so JS can parse it safely js_cmd = f'addData({json.dumps(meta)})' # Push to the webview. Use the window object returned by create_window. # This runs from a background thread and is allowed. window.evaluate_js(js_cmd) # simulate work time.sleep(1) print(f"{i:2d}. Actual: {labels[actual]:20s} | Predicted: {prediction:20s} {match}") if window: # notify JS that we're done window.evaluate_js("addData('Done producing data.')") print(f"\nAccuracy: {correct}/10 = {correct*10}%") app.py:
from pathlib import Path import time import json from functools import partial import threading import webview from GPStest import GPSSpoofingDetector class Api: def __init__(self, detector, data, feature_names, window:None|webview.Window): self.detector = detector self.data = data self.feature_names = feature_names self.window = window def run_test(self): # This will be called from JS # Start a thread so UI doesn’t freeze import threading t = threading.Thread(target=detector.random_sample_testing, args=(window, data, feature_names)) t.start() return "Started" if __name__ == '__main__': detector = GPSSpoofingDetector() detector.load_model() data = detector.load_data() feature_names = detector.print_features_used(data) api = Api(detector, data, feature_names, None) window = webview.create_window('Live updates demo', url="app2.html", js_api=api) api.window = window print('starting webview') webview.start(http_server=True) app2.html:
<head> <script> document.addEventListener('DOMContentLoaded', function() { document.getElementById('pybutn').addEventListener('click', function() { if (window.pywebview && window.pywebview.api) { window.pywebview.api.run_test(); } else { console.warn('pywebview API not ready yet'); setTimeout(()=> window.pywebview?.api?.run_test(), 300); } }); // called by Python via window.evaluate_js("addData(...);") window.addData = function(obj) { const out = document.getElementById('out'); const d = document.createElement('div'); d.className = 'item'; if (typeof obj === 'object' && obj !== null) { // You can access individual fields: d.innerHTML = ` <b>Label:</b> ${obj.Label ?? 'N/A'}<br> <b>Lat:</b> ${obj.lat ?? 'N/A'}<br> <b>Lon:</b> ${obj.lon ?? 'N/A'}<br> <b>Alt:</b> ${obj.alt ?? 'N/A'}<br> <b>Sats:</b> ${obj.satellites_used ?? 'N/A'} `; } else { // obj might already be an object if evaluate_js passes it, but we send JSON string d.textContent = typeof obj === 'string' ? obj : JSON.stringify(obj); } out.appendChild(d); out.scrollTop = out.scrollHeight; // auto-scroll } }); </script> </head> error:
initializing Loading model... ✓ Model loaded successfully Features used: ['time_utc_usec', 's_variance_m_s', 'c_variance_rad', 'epv', 'hdop', 'vdop', 'noise_per_ms', 'jamming_indicator', 'vel_m_s', 'vel_n_m_s', 'vel_e_m_s', 'vel_d_m_s', 'cog_rad', 'satellites_used'] starting webview [pywebview] Error while processing data.Label.array.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T.T ... What I expect: GPSSpoofingDetectorloads model & CSV at script start (console prints show that).The web UI starts and waits for me to click "Start".
When clicking "Start", JS calls
window.pywebview.api.run_test().run_test()starts a background thread that runsdetector.random_sample_testing(...). That method pushes JSON to the page viawindow.evaluate_js(...)and I see live updates in the page.
What actually happens:
The console output shows the model is loaded and features printed, then starting webview, but immediately I get long pywebview errors before I press Start