Spaces:
Runtime error
Runtime error
| import Foundation | |
| import UIKit | |
| class HuggingFaceService { | |
| static let shared = HuggingFaceService() | |
| // Updated API endpoints for Gradio 5.x | |
| private let baseURL = "https://kylanoconnor-plonk-geolocation.hf.space" | |
| private let session = URLSession.shared | |
| private init() {} | |
| // MARK: - PLONK Location Analysis | |
| /// Analyze location from image using PLONK model on Hugging Face Space | |
| func analyzeLocationWithPlonk( | |
| image: UIImage, | |
| additionalContext: String? = nil | |
| ) async throws -> LocationAnalysisData { | |
| // First, discover the working endpoint | |
| guard let workingEndpoint = await discoverWorkingEndpoint() else { | |
| print("β PLONK: No working endpoint found") | |
| throw HuggingFaceError.spaceUnavailable | |
| } | |
| print("β PLONK: Using endpoint: \(workingEndpoint)") | |
| // Convert image to base64 with compression | |
| guard let imageData = image.jpegData(compressionQuality: 0.6) else { | |
| throw HuggingFaceError.imageProcessingFailed | |
| } | |
| // If image is too large, compress further | |
| let finalImageData: Data | |
| if imageData.count > 1_000_000 { // If larger than 1MB | |
| guard let smallerImageData = image.jpegData(compressionQuality: 0.3) else { | |
| throw HuggingFaceError.imageProcessingFailed | |
| } | |
| finalImageData = smallerImageData | |
| print("ποΈ Compressed image to \(finalImageData.count) bytes") | |
| } else { | |
| finalImageData = imageData | |
| } | |
| let base64Image = finalImageData.base64EncodedString() | |
| // Try the request | |
| let plonkResult = try await makeRequest(to: workingEndpoint, with: base64Image) | |
| let plonkCoordinates = plonkResult.coordinates | |
| print("β PLONK found coordinates: \(plonkCoordinates.formattedString)") | |
| // Enhance with OpenAI analysis | |
| return try await enhanceWithOpenAI( | |
| image: image, | |
| plonkCoordinates: plonkCoordinates, | |
| additionalContext: additionalContext | |
| ) | |
| } | |
| /// Discover the working API endpoint for Gradio 5.x | |
| private func discoverWorkingEndpoint() async -> String? { | |
| print("π Discovering working PLONK endpoint...") | |
| // Check if the space is running | |
| guard let url = URL(string: baseURL) else { return nil } | |
| do { | |
| let (data, response) = try await session.data(from: url) | |
| guard let httpResponse = response as? HTTPURLResponse, | |
| httpResponse.statusCode == 200 else { | |
| print("β Space is not accessible") | |
| return nil | |
| } | |
| // Check if it's a Gradio app | |
| let responseString = String(data: data, encoding: .utf8) ?? "" | |
| if responseString.contains("gradio") || responseString.contains("Gradio") { | |
| print("β Gradio space is running") | |
| // For Gradio 5.x, try these endpoints in order of preference: | |
| let possibleEndpoints = [ | |
| "\(baseURL)/api/predict", // Standard API endpoint | |
| "\(baseURL)/run/predict", // Alternative endpoint | |
| "\(baseURL)/gradio_api/predict" // Legacy endpoint | |
| ] | |
| // Test each endpoint with a simple POST request | |
| for endpoint in possibleEndpoints { | |
| if let endpointURL = URL(string: endpoint) { | |
| var request = URLRequest(url: endpointURL) | |
| request.httpMethod = "POST" | |
| request.setValue("application/json", forHTTPHeaderField: "Content-Type") | |
| // Simple test payload | |
| let testPayload = ["data": ["test"]] | |
| do { | |
| request.httpBody = try JSONSerialization.data(withJSONObject: testPayload) | |
| let (_, response) = try await session.data(for: request) | |
| if let httpResponse = response as? HTTPURLResponse { | |
| print("π‘ Testing \(endpoint): \(httpResponse.statusCode)") | |
| // 200 = working, 422 = validation error but endpoint exists | |
| if httpResponse.statusCode == 200 || httpResponse.statusCode == 422 { | |
| print("β Found working endpoint: \(endpoint)") | |
| return endpoint | |
| } | |
| } | |
| } catch { | |
| // Continue trying other endpoints | |
| continue | |
| } | |
| } | |
| } | |
| // If no specific endpoint works, return the default | |
| return "\(baseURL)/api/predict" | |
| } else { | |
| print("β οΈ Space might be loading or not a Gradio app") | |
| return nil | |
| } | |
| } catch { | |
| print("β Error checking space: \(error)") | |
| return nil | |
| } | |
| } | |
| /// Make a request to the PLONK API with proper Gradio 5.x format | |
| private func makeRequest(to endpoint: String, with base64Image: String) async throws -> LocationAnalysisData { | |
| guard let url = URL(string: endpoint) else { | |
| throw HuggingFaceError.invalidResponse | |
| } | |
| // Gradio 5.x API format for gr.Interface | |
| let payload: [String: Any] = [ | |
| "data": [base64Image] // Simple array format for gr.Interface | |
| ] | |
| var request = URLRequest(url: url) | |
| request.httpMethod = "POST" | |
| request.setValue("application/json", forHTTPHeaderField: "Content-Type") | |
| request.timeoutInterval = 60.0 // Longer timeout for model inference | |
| print("π PLONK: Submitting to \(endpoint)") | |
| print("π¦ PLONK: Payload keys: \(payload.keys.joined(separator: ", "))") | |
| do { | |
| request.httpBody = try JSONSerialization.data(withJSONObject: payload) | |
| } catch { | |
| print("β PLONK: Failed to serialize request payload") | |
| throw HuggingFaceError.requestSerializationFailed | |
| } | |
| let (data, response) = try await session.data(for: request) | |
| guard let httpResponse = response as? HTTPURLResponse else { | |
| throw HuggingFaceError.invalidResponse | |
| } | |
| print("π‘ PLONK: Response status code: \(httpResponse.statusCode)") | |
| // Log the raw response for debugging | |
| if let responseString = String(data: data, encoding: .utf8) { | |
| print("π PLONK: Raw response: \(responseString.prefix(500))...") | |
| } | |
| switch httpResponse.statusCode { | |
| case 200: | |
| return try parseResponse(data) | |
| case 422: | |
| // Validation error - try alternative format | |
| print("β οΈ Validation error, trying alternative format...") | |
| return try await makeRequestAlternativeFormat(to: endpoint, with: base64Image) | |
| case 404: | |
| throw HuggingFaceError.serverError(404) | |
| case 413: | |
| throw HuggingFaceError.networkError("Image too large") | |
| case 503: | |
| throw HuggingFaceError.spaceUnavailable | |
| default: | |
| throw HuggingFaceError.serverError(httpResponse.statusCode) | |
| } | |
| } | |
| /// Try alternative request format for Gradio 5.x | |
| private func makeRequestAlternativeFormat(to endpoint: String, with base64Image: String) async throws -> LocationAnalysisData { | |
| guard let url = URL(string: endpoint) else { | |
| throw HuggingFaceError.invalidResponse | |
| } | |
| // Try different formats that Gradio 5.x might expect | |
| let alternativePayloads: [[String: Any]] = [ | |
| // Format 1: With data URI prefix | |
| [ | |
| "data": ["data:image/jpeg;base64,\(base64Image)"] | |
| ], | |
| // Format 2: With inputs key | |
| [ | |
| "inputs": [base64Image] | |
| ], | |
| // Format 3: Direct base64 with PIL type hint | |
| [ | |
| "data": [base64Image], | |
| "type": ["PIL"] | |
| ], | |
| // Format 4: Legacy format with data URI | |
| [ | |
| "data": ["data:image/jpeg;base64,\(base64Image)"], | |
| "fn_index": 0 | |
| ] | |
| ] | |
| for (index, payload) in alternativePayloads.enumerated() { | |
| print("π Trying alternative format \(index + 1)") | |
| var request = URLRequest(url: url) | |
| request.httpMethod = "POST" | |
| request.setValue("application/json", forHTTPHeaderField: "Content-Type") | |
| request.timeoutInterval = 60.0 | |
| do { | |
| request.httpBody = try JSONSerialization.data(withJSONObject: payload) | |
| let (data, response) = try await session.data(for: request) | |
| guard let httpResponse = response as? HTTPURLResponse else { | |
| continue | |
| } | |
| print("π‘ Alternative format \(index + 1) status: \(httpResponse.statusCode)") | |
| // Log response for debugging | |
| if let responseString = String(data: data, encoding: .utf8) { | |
| print("π Alternative format \(index + 1) response: \(responseString.prefix(200))...") | |
| } | |
| if httpResponse.statusCode == 200 { | |
| return try parseResponse(data) | |
| } | |
| } catch { | |
| print("β Alternative format \(index + 1) failed: \(error)") | |
| continue | |
| } | |
| } | |
| throw HuggingFaceError.invalidResponseFormat | |
| } | |
| // MARK: - Response Parsing | |
| private func parseResponse(_ data: Data) throws -> LocationAnalysisData { | |
| // Log the response for debugging | |
| if let responseString = String(data: data, encoding: .utf8) { | |
| print("π PLONK: Full response: \(responseString)") | |
| } | |
| // Try to parse as Gradio 5.x response format | |
| do { | |
| let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] | |
| // Gradio 5.x typically returns {"data": [result], ...} | |
| if let dataArray = json?["data"] as? [Any], | |
| let locationString = dataArray.first as? String { | |
| print("β PLONK: Parsed location string: \(locationString)") | |
| return try parseLocationString(locationString) | |
| } | |
| // Try other possible formats | |
| if let result = json?["result"] as? [Any], | |
| let locationString = result.first as? String { | |
| return try parseLocationString(locationString) | |
| } | |
| if let output = json?["output"] as? String { | |
| return try parseLocationString(output) | |
| } | |
| } catch { | |
| print("β JSON parsing failed: \(error)") | |
| } | |
| // Try to parse as direct string | |
| if let responseString = String(data: data, encoding: .utf8) { | |
| // Sometimes the response might be a direct string | |
| if responseString.contains("Latitude:") && responseString.contains("Longitude:") { | |
| return try parseLocationString(responseString) | |
| } | |
| } | |
| throw HuggingFaceError.responseParsingFailed | |
| } | |
| private func parseLocationString(_ locationString: String) throws -> LocationAnalysisData { | |
| print("π Parsing location string: \(locationString)") | |
| // Parse the format: "Predicted Location:\nLatitude: 40.748817\nLongitude: -73.985428" | |
| let lines = locationString.components(separatedBy: .newlines) | |
| var latitude: Double = 0 | |
| var longitude: Double = 0 | |
| var foundLat = false | |
| var foundLon = false | |
| for line in lines { | |
| let trimmedLine = line.trimmingCharacters(in: .whitespaces) | |
| if trimmedLine.contains("Latitude:") { | |
| let parts = trimmedLine.components(separatedBy: ":") | |
| if parts.count > 1 { | |
| let latString = parts[1].trimmingCharacters(in: .whitespaces) | |
| if let parsedLat = Double(latString) { | |
| latitude = parsedLat | |
| foundLat = true | |
| print("β Found latitude: \(latitude)") | |
| } | |
| } | |
| } else if trimmedLine.contains("Longitude:") { | |
| let parts = trimmedLine.components(separatedBy: ":") | |
| if parts.count > 1 { | |
| let lonString = parts[1].trimmingCharacters(in: .whitespaces) | |
| if let parsedLon = Double(lonString) { | |
| longitude = parsedLon | |
| foundLon = true | |
| print("β Found longitude: \(longitude)") | |
| } | |
| } | |
| } | |
| } | |
| guard foundLat && foundLon else { | |
| print("β Could not parse coordinates from: \(locationString)") | |
| throw HuggingFaceError.invalidResponseFormat | |
| } | |
| // Validate coordinates are in reasonable range | |
| guard latitude >= -90 && latitude <= 90 && longitude >= -180 && longitude <= 180 else { | |
| print("β Invalid coordinates: lat=\(latitude), lon=\(longitude)") | |
| throw HuggingFaceError.invalidResponseFormat | |
| } | |
| let locationName = generateLocationName(latitude: latitude, longitude: longitude) | |
| return LocationAnalysisData( | |
| locationName: locationName, | |
| coordinates: LocationCoordinates(latitude: latitude, longitude: longitude), | |
| confidenceRadius: 5.0, | |
| confidencePercentage: 85.0 | |
| ) | |
| } | |
| /// Enhance PLONK coordinates with OpenAI contextual analysis | |
| private func enhanceWithOpenAI( | |
| image: UIImage, | |
| plonkCoordinates: LocationCoordinates, | |
| additionalContext: String? | |
| ) async throws -> LocationAnalysisData { | |
| print("π§ Enhancing PLONK coordinates with OpenAI analysis...") | |
| // Create enhanced prompt with PLONK coordinates | |
| let enhancedPrompt = """ | |
| I have used a specialized geolocation AI model (PLONK) to analyze this image and it predicted these coordinates: | |
| **PLONK Prediction:** | |
| - Latitude: \(plonkCoordinates.latitude) | |
| - Longitude: \(plonkCoordinates.longitude) | |
| - Location: \(plonkCoordinates.formattedString) | |
| Now, please analyze this image in detail and provide: | |
| 1. **Location Verification**: Does this coordinate prediction seem accurate? | |
| 2. **Specific Location**: What is the specific landmark or location name? | |
| 3. **Context & Details**: Describe what confirms this location | |
| 4. **Confidence Assessment**: How confident are you? (1-100%) | |
| Additional context: \(additionalContext ?? "None provided") | |
| """ | |
| do { | |
| let openaiResult = try await OpenAIService.shared.analyzeLocationFromImage( | |
| image: image, | |
| additionalContext: enhancedPrompt | |
| ) | |
| return LocationAnalysisData( | |
| locationName: openaiResult.locationName, | |
| coordinates: plonkCoordinates, // Use PLONK's precise coordinates | |
| confidenceRadius: openaiResult.confidenceRadius, | |
| confidencePercentage: openaiResult.confidencePercentage | |
| ) | |
| } catch { | |
| print("β οΈ OpenAI enhancement failed: \(error)") | |
| // Fall back to PLONK-only result | |
| let fallbackLocationName = generateLocationName( | |
| latitude: plonkCoordinates.latitude, | |
| longitude: plonkCoordinates.longitude | |
| ) | |
| return LocationAnalysisData( | |
| locationName: fallbackLocationName, | |
| coordinates: plonkCoordinates, | |
| confidenceRadius: 5.0, | |
| confidencePercentage: 85.0 | |
| ) | |
| } | |
| } | |
| private func generateLocationName(latitude: Double, longitude: Double) -> String { | |
| let latDirection = latitude >= 0 ? "N" : "S" | |
| let lonDirection = longitude >= 0 ? "E" : "W" | |
| let region = determineRegion(latitude: latitude, longitude: longitude) | |
| return "\(region) (\(String(format: "%.4f", abs(latitude)))Β°\(latDirection), \(String(format: "%.4f", abs(longitude)))Β°\(lonDirection))" | |
| } | |
| private func determineRegion(latitude: Double, longitude: Double) -> String { | |
| if latitude >= 24 && latitude <= 50 && longitude >= -125 && longitude <= -65 { | |
| return "North America" | |
| } else if latitude >= 35 && latitude <= 70 && longitude >= -10 && longitude <= 40 { | |
| return "Europe" | |
| } else if latitude >= -35 && latitude <= 35 && longitude >= -20 && longitude <= 50 { | |
| return "Africa" | |
| } else if latitude >= 5 && latitude <= 55 && longitude >= 60 && longitude <= 150 { | |
| return "Asia" | |
| } else if latitude >= -45 && latitude <= -10 && longitude >= 110 && longitude <= 180 { | |
| return "Australia/Oceania" | |
| } else if latitude >= -60 && latitude <= 15 && longitude >= -85 && longitude <= -30 { | |
| return "South America" | |
| } else { | |
| return "Unknown Region" | |
| } | |
| } | |
| // MARK: - Testing & Health Check | |
| /// Test the PLONK API with a simple request | |
| func testPlonkAPI() async { | |
| print("π§ͺ Testing PLONK API...") | |
| guard let workingEndpoint = await discoverWorkingEndpoint() else { | |
| print("β No working endpoint found during test") | |
| return | |
| } | |
| print("β Found working endpoint for test: \(workingEndpoint)") | |
| // Create a simple test image (red square) | |
| let size = CGSize(width: 224, height: 224) | |
| UIGraphicsBeginImageContext(size) | |
| UIColor.red.setFill() | |
| UIRectFill(CGRect(origin: .zero, size: size)) | |
| let testImage = UIGraphicsGetImageFromCurrentImageContext()! | |
| UIGraphicsEndImageContext() | |
| do { | |
| guard let imageData = testImage.jpegData(compressionQuality: 0.8) else { | |
| print("β Failed to create test image data") | |
| return | |
| } | |
| let base64Image = imageData.base64EncodedString() | |
| let result = try await makeRequest(to: workingEndpoint, with: base64Image) | |
| print("β Test successful! Predicted: \(result.locationName)") | |
| } catch { | |
| print("β Test failed: \(error)") | |
| } | |
| } | |
| /// Check if the Hugging Face Space is available | |
| func checkSpaceAvailability() async -> Bool { | |
| guard let url = URL(string: baseURL) else { return false } | |
| do { | |
| let (_, response) = try await session.data(from: url) | |
| if let httpResponse = response as? HTTPURLResponse { | |
| print("π PLONK Space status: \(httpResponse.statusCode)") | |
| return httpResponse.statusCode == 200 | |
| } | |
| return false | |
| } catch { | |
| print("β PLONK Space check failed: \(error)") | |
| return false | |
| } | |
| } | |
| } | |
| // MARK: - Extensions | |
| extension HuggingFaceService { | |
| /// Quick location analysis with error handling | |
| func quickLocationAnalysis(image: UIImage) async -> LocationAnalysisData? { | |
| do { | |
| return try await analyzeLocationWithPlonk(image: image) | |
| } catch { | |
| print("π« PLONK analysis failed: \(error.localizedDescription)") | |
| return await plonkInspiredAnalysis(image: image) | |
| } | |
| } | |
| /// PLONK-inspired analysis using OpenAI (fallback) | |
| private func plonkInspiredAnalysis(image: UIImage) async -> LocationAnalysisData? { | |
| print("π§ Using PLONK-inspired analysis with OpenAI...") | |
| let plonkStylePrompt = """ | |
| Analyze this image for precise geolocation using visual clues. Act like a state-of-the-art visual geolocation model. | |
| Look for: | |
| - Architecture styles and building materials | |
| - Vegetation and landscape features | |
| - Street signs, license plates, or text | |
| - Cultural markers and local customs | |
| - Geographic features | |
| - Climate indicators | |
| - Infrastructure patterns | |
| Provide your best GPS coordinate estimate and explain your reasoning. | |
| """ | |
| do { | |
| return try await OpenAIService.shared.analyzeLocationFromImage( | |
| image: image, | |
| additionalContext: plonkStylePrompt | |
| ) | |
| } catch { | |
| print("β PLONK-inspired analysis failed: \(error)") | |
| return nil | |
| } | |
| } | |
| } | |
| // MARK: - Error Handling | |
| enum HuggingFaceError: Error, LocalizedError { | |
| case imageProcessingFailed | |
| case requestSerializationFailed | |
| case invalidResponse | |
| case invalidResponseFormat | |
| case responseParsingFailed | |
| case serverError(Int) | |
| case spaceUnavailable | |
| case networkError(String) | |
| case quotaExceeded | |
| var errorDescription: String? { | |
| switch self { | |
| case .imageProcessingFailed: | |
| return "Failed to process the image for analysis" | |
| case .requestSerializationFailed: | |
| return "Failed to prepare the request" | |
| case .invalidResponse: | |
| return "Invalid response from Hugging Face Space" | |
| case .invalidResponseFormat: | |
| return "Unexpected response format from PLONK model" | |
| case .responseParsingFailed: | |
| return "Failed to parse the location analysis results" | |
| case .serverError(let code): | |
| return "Server error (\(code)). The Hugging Face Space may be unavailable" | |
| case .spaceUnavailable: | |
| return "The PLONK model space is currently unavailable" | |
| case .networkError(let message): | |
| return "Network error: \(message)" | |
| case .quotaExceeded: | |
| return "API quota exceeded. Please try again later" | |
| } | |
| } | |
| } |