Spaces:
Runtime error
Runtime error
File size: 22,942 Bytes
5c11fb8 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 |
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"
}
}
} |