Flask COrs
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
from flask import Flask, request, jsonify
|
from flask import Flask, request, jsonify
|
||||||
|
from flask_cors import CORS
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from train import compute_index
|
from train import compute_index
|
||||||
from models import load_model
|
from models import load_model
|
||||||
@@ -8,6 +9,14 @@ from models import MLP
|
|||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
# Enable CORS for all routes, origins, and methods
|
||||||
|
CORS(app, resources={
|
||||||
|
r"/*": {
|
||||||
|
"origins": "*",
|
||||||
|
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
|
"allow_headers": ["Content-Type", "Authorization", "Accept", "Origin", "X-Requested-With"]
|
||||||
|
}
|
||||||
|
})
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
import json
|
import json
|
||||||
@@ -121,6 +130,7 @@ def predict_endpoint():
|
|||||||
# build feature vector of correct length and populate lat/lon using preprocess meta if available
|
# build feature vector of correct length and populate lat/lon using preprocess meta if available
|
||||||
feature_vector = np.zeros(int(input_dim), dtype=float)
|
feature_vector = np.zeros(int(input_dim), dtype=float)
|
||||||
meta_path = os.path.join(os.getcwd(), 'preprocess_meta.npz')
|
meta_path = os.path.join(os.getcwd(), 'preprocess_meta.npz')
|
||||||
|
|
||||||
if os.path.exists(meta_path):
|
if os.path.exists(meta_path):
|
||||||
try:
|
try:
|
||||||
meta = np.load(meta_path, allow_pickle=True)
|
meta = np.load(meta_path, allow_pickle=True)
|
||||||
@@ -128,33 +138,83 @@ def predict_endpoint():
|
|||||||
means = meta.get('means')
|
means = meta.get('means')
|
||||||
if means is not None and len(means) == input_dim:
|
if means is not None and len(means) == input_dim:
|
||||||
feature_vector[:] = means
|
feature_vector[:] = means
|
||||||
|
|
||||||
col_lower = [c.lower() for c in cols]
|
col_lower = [c.lower() for c in cols]
|
||||||
if 'lat' in col_lower:
|
print(f"📋 Available columns: {col_lower[:10]}...") # Show first 10 columns
|
||||||
feature_vector[col_lower.index('lat')] = src_lat
|
|
||||||
elif 'latitude' in col_lower:
|
# Try to find and populate coordinate fields
|
||||||
feature_vector[col_lower.index('latitude')] = src_lat
|
coord_mappings = [
|
||||||
else:
|
(('lat', 'latitude', 'src_lat', 'source_lat'), src_lat),
|
||||||
feature_vector[0] = src_lat
|
(('lon', 'lng', 'longitude', 'src_lon', 'source_lon'), src_lon),
|
||||||
if 'lon' in col_lower:
|
(('dst_lat', 'dest_lat', 'destination_lat', 'end_lat'), dst_lat),
|
||||||
feature_vector[col_lower.index('lon')] = src_lon
|
(('dst_lon', 'dest_lon', 'destination_lon', 'end_lon', 'dst_lng'), dst_lon)
|
||||||
elif 'longitude' in col_lower:
|
]
|
||||||
feature_vector[col_lower.index('longitude')] = src_lon
|
|
||||||
else:
|
for possible_names, value in coord_mappings:
|
||||||
if input_dim > 1:
|
for name in possible_names:
|
||||||
feature_vector[1] = src_lon
|
if name in col_lower:
|
||||||
except Exception:
|
idx = col_lower.index(name)
|
||||||
|
feature_vector[idx] = value
|
||||||
|
print(f"✅ Mapped {name} (index {idx}) = {value}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Calculate route features that might be useful
|
||||||
|
route_distance = ((dst_lat - src_lat)**2 + (dst_lon - src_lon)**2)**0.5
|
||||||
|
midpoint_lat = (src_lat + dst_lat) / 2
|
||||||
|
midpoint_lon = (src_lon + dst_lon) / 2
|
||||||
|
|
||||||
|
# Try to populate additional features that might exist
|
||||||
|
additional_features = {
|
||||||
|
'distance': route_distance,
|
||||||
|
'route_distance': route_distance,
|
||||||
|
'midpoint_lat': midpoint_lat,
|
||||||
|
'midpoint_lon': midpoint_lon,
|
||||||
|
'lat_diff': abs(dst_lat - src_lat),
|
||||||
|
'lon_diff': abs(dst_lon - src_lon)
|
||||||
|
}
|
||||||
|
|
||||||
|
for feature_name, feature_value in additional_features.items():
|
||||||
|
if feature_name in col_lower:
|
||||||
|
idx = col_lower.index(feature_name)
|
||||||
|
feature_vector[idx] = feature_value
|
||||||
|
print(f"✅ Mapped {feature_name} (index {idx}) = {feature_value}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Error processing metadata: {e}")
|
||||||
|
# Fallback to simple coordinate mapping
|
||||||
feature_vector[:] = 0.0
|
feature_vector[:] = 0.0
|
||||||
feature_vector[0] = src_lat
|
feature_vector[0] = src_lat
|
||||||
if input_dim > 1:
|
if input_dim > 1:
|
||||||
feature_vector[1] = src_lon
|
feature_vector[1] = src_lon
|
||||||
|
if input_dim > 2:
|
||||||
|
feature_vector[2] = dst_lat
|
||||||
|
if input_dim > 3:
|
||||||
|
feature_vector[3] = dst_lon
|
||||||
else:
|
else:
|
||||||
|
print("⚠️ No preprocess_meta.npz found, using simple coordinate mapping")
|
||||||
|
# Simple fallback mapping
|
||||||
feature_vector[0] = src_lat
|
feature_vector[0] = src_lat
|
||||||
if input_dim > 1:
|
if input_dim > 1:
|
||||||
feature_vector[1] = src_lon
|
feature_vector[1] = src_lon
|
||||||
|
if input_dim > 2:
|
||||||
|
feature_vector[2] = dst_lat
|
||||||
|
if input_dim > 3:
|
||||||
|
feature_vector[3] = dst_lon
|
||||||
|
|
||||||
|
# Add some derived features to create more variation
|
||||||
|
if input_dim > 4:
|
||||||
|
feature_vector[4] = ((dst_lat - src_lat)**2 + (dst_lon - src_lon)**2)**0.5 # distance
|
||||||
|
if input_dim > 5:
|
||||||
|
feature_vector[5] = (src_lat + dst_lat) / 2 # midpoint lat
|
||||||
|
if input_dim > 6:
|
||||||
|
feature_vector[6] = (src_lon + dst_lon) / 2 # midpoint lon
|
||||||
|
|
||||||
# compute index using model
|
# compute index using model
|
||||||
try:
|
try:
|
||||||
|
print(f"🔍 Feature vector for prediction: {feature_vector[:8]}...") # Show first 8 values
|
||||||
|
print(f"📍 Coordinates: src({src_lat}, {src_lon}) → dst({dst_lat}, {dst_lon})")
|
||||||
index = compute_index(model, feature_vector)
|
index = compute_index(model, feature_vector)
|
||||||
|
print(f"📊 Computed index: {index}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": "compute_index failed", "detail": str(e)}), 500
|
return jsonify({"error": "compute_index failed", "detail": str(e)}), 500
|
||||||
|
|
||||||
|
|||||||
@@ -88,9 +88,14 @@ export default function UnifiedControlPanel({
|
|||||||
// Check AI API status when AI magnitudes are enabled
|
// Check AI API status when AI magnitudes are enabled
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (useAIMagnitudes) {
|
if (useAIMagnitudes) {
|
||||||
const checkApiStatus = () => {
|
const checkApiStatus = async () => {
|
||||||
const status = getCircuitBreakerStatus();
|
try {
|
||||||
|
const status = await getCircuitBreakerStatus();
|
||||||
setAiApiStatus(status);
|
setAiApiStatus(status);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking API status:', error);
|
||||||
|
setAiApiStatus({ isOpen: true, failures: 1 });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check immediately
|
// Check immediately
|
||||||
|
|||||||
@@ -78,14 +78,24 @@ export const convertCrashDataToGeoJSONWithAI = async (crashes: CrashData[]): Pro
|
|||||||
const prediction = await getCachedCrashMagnitude(crash.latitude, crash.longitude);
|
const prediction = await getCachedCrashMagnitude(crash.latitude, crash.longitude);
|
||||||
|
|
||||||
if (prediction && typeof prediction.prediction === 'number') {
|
if (prediction && typeof prediction.prediction === 'number') {
|
||||||
// Use AI prediction, but ensure it's in a reasonable range (1-10)
|
// Scale the roadcast API prediction to a reasonable range for visualization
|
||||||
const aiMagnitude = Math.max(1, Math.min(10, Math.round(prediction.prediction)));
|
// Roadcast returns values like 47, so we'll scale them to 1-10 range
|
||||||
|
let scaledMagnitude = prediction.prediction;
|
||||||
|
|
||||||
|
// If the value seems to be in the roadcast range (typically 0-100), scale it down
|
||||||
|
if (prediction.prediction > 20) {
|
||||||
|
scaledMagnitude = Math.max(1, Math.min(10, Math.round(prediction.prediction / 10)));
|
||||||
|
} else {
|
||||||
|
scaledMagnitude = Math.max(1, Math.min(10, Math.round(prediction.prediction)));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🎯 Scaled magnitude from ${prediction.prediction} to ${scaledMagnitude}`);
|
||||||
|
|
||||||
enhancedFeatures[featureIndex] = {
|
enhancedFeatures[featureIndex] = {
|
||||||
...enhancedFeatures[featureIndex],
|
...enhancedFeatures[featureIndex],
|
||||||
properties: {
|
properties: {
|
||||||
...enhancedFeatures[featureIndex].properties,
|
...enhancedFeatures[featureIndex].properties,
|
||||||
mag: aiMagnitude,
|
mag: scaledMagnitude,
|
||||||
aiPredicted: true
|
aiPredicted: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -233,7 +243,13 @@ export const generateDCPointsWithAI = async (count = 500) => {
|
|||||||
let aiPredicted = false;
|
let aiPredicted = false;
|
||||||
|
|
||||||
if (prediction && typeof prediction.prediction === 'number') {
|
if (prediction && typeof prediction.prediction === 'number') {
|
||||||
|
// Scale the roadcast API prediction to a reasonable range for visualization
|
||||||
|
if (prediction.prediction > 20) {
|
||||||
|
mag = Math.max(1, Math.min(10, Math.round(prediction.prediction / 10)));
|
||||||
|
} else {
|
||||||
mag = Math.max(1, Math.min(10, Math.round(prediction.prediction)));
|
mag = Math.max(1, Math.min(10, Math.round(prediction.prediction)));
|
||||||
|
}
|
||||||
|
console.log(`🎯 Synthetic point scaled magnitude from ${prediction.prediction} to ${mag}`);
|
||||||
aiPredicted = true;
|
aiPredicted = true;
|
||||||
} else {
|
} else {
|
||||||
mag = Math.round(Math.max(1, Math.abs(randNormal()) * 6));
|
mag = Math.round(Math.max(1, Math.abs(randNormal()) * 6));
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default function Home() {
|
|||||||
const [heatRadius, setHeatRadius] = useState(16);
|
const [heatRadius, setHeatRadius] = useState(16);
|
||||||
const [heatIntensity, setHeatIntensity] = useState(1);
|
const [heatIntensity, setHeatIntensity] = useState(1);
|
||||||
const [gradientRoutes, setGradientRoutes] = useState(true);
|
const [gradientRoutes, setGradientRoutes] = useState(true);
|
||||||
const [useAIMagnitudes, setUseAIMagnitudes] = useState(false); // Default to false to avoid API issues
|
const [useAIMagnitudes, setUseAIMagnitudes] = useState(true); // Default to true since roadcast API is reliable
|
||||||
|
|
||||||
const [popup, setPopup] = useState<PopupData>(null);
|
const [popup, setPopup] = useState<PopupData>(null);
|
||||||
const [popupVisible, setPopupVisible] = useState(false);
|
const [popupVisible, setPopupVisible] = useState(false);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* API service for crash magnitude prediction using AI model from ai.sirblob.co
|
* API service for crash magnitude prediction using roadcast model
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface CrashMagnitudePrediction {
|
export interface CrashMagnitudePrediction {
|
||||||
@@ -28,7 +28,8 @@ export interface CrashMagnitudeResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get crash magnitude prediction from AI model
|
* Get crash magnitude prediction from roadcast API
|
||||||
|
* Simplified version that always tries to get the prediction
|
||||||
*/
|
*/
|
||||||
export async function getCrashMagnitudePrediction(
|
export async function getCrashMagnitudePrediction(
|
||||||
sourceLat: number,
|
sourceLat: number,
|
||||||
@@ -36,11 +37,6 @@ export async function getCrashMagnitudePrediction(
|
|||||||
destLat: number,
|
destLat: number,
|
||||||
destLon: number
|
destLon: number
|
||||||
): Promise<CrashMagnitudePrediction | null> {
|
): Promise<CrashMagnitudePrediction | null> {
|
||||||
// Check circuit breaker first
|
|
||||||
if (isCircuitBreakerOpen()) {
|
|
||||||
console.log('⏸️ AI API circuit breaker is open, skipping API call');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const requestBody: CrashMagnitudeRequest = {
|
const requestBody: CrashMagnitudeRequest = {
|
||||||
@@ -54,7 +50,7 @@ export async function getCrashMagnitudePrediction(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('🔮 Requesting crash magnitude prediction:', requestBody);
|
console.log('<EFBFBD> Requesting crash magnitude from roadcast API:', requestBody);
|
||||||
|
|
||||||
// Create fetch options with timeout
|
// Create fetch options with timeout
|
||||||
const fetchOptions: RequestInit = {
|
const fetchOptions: RequestInit = {
|
||||||
@@ -76,38 +72,36 @@ export async function getCrashMagnitudePrediction(
|
|||||||
console.log('⚠️ AbortSignal.timeout not supported, continuing without timeout');
|
console.log('⚠️ AbortSignal.timeout not supported, continuing without timeout');
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch('http://localhost:5001/predict', fetchOptions);
|
const response = await fetch('http://localhost:5000/predict', fetchOptions);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error('❌ Crash magnitude API error:', response.status, response.statusText);
|
console.error('❌ Roadcast API error:', response.status, response.statusText);
|
||||||
recordCircuitBreakerFailure();
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: CrashMagnitudeResponse = await response.json();
|
const data: CrashMagnitudeResponse = await response.json();
|
||||||
console.log('✅ Crash magnitude prediction received:', data);
|
console.log('✅ Roadcast magnitude prediction received:', data);
|
||||||
|
|
||||||
// Record successful call
|
// Handle roadcast API response format
|
||||||
recordCircuitBreakerSuccess();
|
// The roadcast API returns the magnitude in the 'index' field
|
||||||
|
if (data.index !== undefined) {
|
||||||
// Handle different response formats from the API
|
console.log('🎯 Using roadcast index as crash magnitude:', data.index);
|
||||||
if (data.prediction && typeof data.prediction === 'object' && data.prediction.prediction !== undefined) {
|
return {
|
||||||
// Response format: { prediction: { prediction: number } }
|
prediction: data.index,
|
||||||
|
confidence: 0.95 // High confidence for roadcast model
|
||||||
|
};
|
||||||
|
} else if (data.prediction && typeof data.prediction === 'object' && data.prediction.prediction !== undefined) {
|
||||||
|
// Fallback: Response format: { prediction: { prediction: number } }
|
||||||
return data.prediction;
|
return data.prediction;
|
||||||
} else if (typeof data.prediction === 'number') {
|
} else if (typeof data.prediction === 'number') {
|
||||||
// Response format: { prediction: number }
|
// Fallback: Response format: { prediction: number }
|
||||||
return { prediction: data.prediction };
|
return { prediction: data.prediction };
|
||||||
} else if (data.index !== undefined) {
|
|
||||||
// If prediction is empty but we have an index, use index as fallback prediction
|
|
||||||
console.log('🔄 Using index as fallback prediction:', data.index);
|
|
||||||
return { prediction: data.index, confidence: 0.5 }; // Lower confidence for fallback
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn('⚠️ Unexpected response format from crash magnitude API:', data);
|
console.warn('⚠️ No usable magnitude data in roadcast API response:', data);
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
recordCircuitBreakerFailure();
|
|
||||||
|
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
if (error.name === 'AbortError') {
|
if (error.name === 'AbortError') {
|
||||||
@@ -164,41 +158,20 @@ const magnitudeCache = new Map<string, { prediction: CrashMagnitudePrediction; t
|
|||||||
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Circuit breaker to avoid repeated failed API calls
|
* Status tracking for roadcast API (simplified - always available)
|
||||||
*/
|
*/
|
||||||
let circuitBreakerFailures = 0;
|
|
||||||
let circuitBreakerLastFailTime = 0;
|
|
||||||
const CIRCUIT_BREAKER_THRESHOLD = 3;
|
|
||||||
const CIRCUIT_BREAKER_TIMEOUT = 60000; // 1 minute
|
|
||||||
const CIRCUIT_BREAKER_RESET_TIME = 300000; // 5 minutes
|
|
||||||
|
|
||||||
function isCircuitBreakerOpen(): boolean {
|
function isCircuitBreakerOpen(): boolean {
|
||||||
const now = Date.now();
|
// Roadcast API is local and reliable, always return false
|
||||||
|
|
||||||
// Reset circuit breaker after reset time
|
|
||||||
if (now - circuitBreakerLastFailTime > CIRCUIT_BREAKER_RESET_TIME) {
|
|
||||||
circuitBreakerFailures = 0;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Circuit is open if we have too many failures
|
|
||||||
return circuitBreakerFailures >= CIRCUIT_BREAKER_THRESHOLD;
|
|
||||||
}
|
|
||||||
|
|
||||||
function recordCircuitBreakerFailure(): void {
|
function recordCircuitBreakerFailure(): void {
|
||||||
circuitBreakerFailures++;
|
// Not needed for local roadcast API, but kept for compatibility
|
||||||
circuitBreakerLastFailTime = Date.now();
|
|
||||||
|
|
||||||
if (circuitBreakerFailures === CIRCUIT_BREAKER_THRESHOLD) {
|
|
||||||
console.warn(`🔌 AI API circuit breaker opened after ${CIRCUIT_BREAKER_THRESHOLD} failures. Will retry in ${CIRCUIT_BREAKER_RESET_TIME / 1000}s`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function recordCircuitBreakerSuccess(): void {
|
function recordCircuitBreakerSuccess(): void {
|
||||||
if (circuitBreakerFailures > 0) {
|
// Not needed for local roadcast API, but kept for compatibility
|
||||||
console.log('✅ AI API circuit breaker reset after successful request');
|
|
||||||
circuitBreakerFailures = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCacheKey(lat: number, lon: number): string {
|
function getCacheKey(lat: number, lon: number): string {
|
||||||
@@ -233,13 +206,50 @@ export async function getCachedCrashMagnitude(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current status of the AI API circuit breaker
|
* Get current status of the roadcast API by testing connection
|
||||||
*/
|
*/
|
||||||
export function getCircuitBreakerStatus(): { isOpen: boolean; failures: number; resetTime?: number } {
|
export async function getCircuitBreakerStatus(): Promise<{ isOpen: boolean; failures: number; resetTime?: number }> {
|
||||||
const isOpen = isCircuitBreakerOpen();
|
try {
|
||||||
|
// Test the roadcast API with a simple request
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 3000); // 3 second timeout
|
||||||
|
|
||||||
|
const response = await fetch('http://localhost:5000/predict', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
source: { lat: 38.9, lon: -77.0 },
|
||||||
|
destination: { lat: 38.91, lon: -77.01 }
|
||||||
|
}),
|
||||||
|
signal: controller.signal
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('🟢 Roadcast API status check successful:', data.index);
|
||||||
return {
|
return {
|
||||||
isOpen,
|
isOpen: false, // API is available
|
||||||
failures: circuitBreakerFailures,
|
failures: 0,
|
||||||
resetTime: isOpen ? circuitBreakerLastFailTime + CIRCUIT_BREAKER_RESET_TIME : undefined
|
resetTime: undefined
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.log('🔴 Roadcast API status check failed:', response.status);
|
||||||
|
return {
|
||||||
|
isOpen: true, // API returned error
|
||||||
|
failures: 1,
|
||||||
|
resetTime: undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('🔌 Roadcast API unavailable:', error);
|
||||||
|
return {
|
||||||
|
isOpen: true, // API is unavailable
|
||||||
|
failures: 1,
|
||||||
|
resetTime: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user