3 changed files with 341 additions and 2 deletions
-
8kplayer.xcodeproj/project.pbxproj
-
306kplayer/core/FaceManager.swift
-
29kplayer/photo/SPhotoAlbumView.swift
@ -0,0 +1,306 @@ |
|||
// |
|||
// Created by Marco Schmickler on 08.12.25. |
|||
// Copyright (c) 2025 Marco Schmickler. All rights reserved. |
|||
// |
|||
|
|||
import Foundation |
|||
|
|||
// MARK: - Error Types |
|||
enum FaceAPIError: Error, LocalizedError { |
|||
case invalidURL |
|||
case networkError(Error) |
|||
case invalidResponse |
|||
case decodingError(Error) |
|||
case validationError(String) |
|||
case serverError(String) |
|||
|
|||
var errorDescription: String? { |
|||
switch self { |
|||
case .invalidURL: |
|||
return "Invalid URL" |
|||
case .networkError(let error): |
|||
return "Network error: \(error.localizedDescription)" |
|||
case .invalidResponse: |
|||
return "Invalid response from server" |
|||
case .decodingError(let error): |
|||
return "Failed to decode response: \(error.localizedDescription)" |
|||
case .validationError(let message): |
|||
return "Validation error: \(message)" |
|||
case .serverError(let message): |
|||
return "Server error: \(message)" |
|||
} |
|||
} |
|||
} |
|||
|
|||
// MARK: - Request Models |
|||
struct ProcessImageRequest: Codable { |
|||
let inputImagePath: String |
|||
let sourceFacePath: String |
|||
let outputPath: String? |
|||
|
|||
enum CodingKeys: String, CodingKey { |
|||
case inputImagePath = "input_image_path" |
|||
case sourceFacePath = "source_face_path" |
|||
case outputPath = "output_path" |
|||
} |
|||
} |
|||
|
|||
struct ProcessFolderRequest: Codable { |
|||
let inputFolderPath: String |
|||
let sourceFacePath: String |
|||
let outputFolderPath: String? |
|||
|
|||
enum CodingKeys: String, CodingKey { |
|||
case inputFolderPath = "input_folder_path" |
|||
case sourceFacePath = "source_face_path" |
|||
case outputFolderPath = "output_folder_path" |
|||
} |
|||
} |
|||
|
|||
struct ProcessVideoRequest: Codable { |
|||
let inputVideoPath: String |
|||
let sourceFacePath: String |
|||
let outputPath: String? |
|||
|
|||
enum CodingKeys: String, CodingKey { |
|||
case inputVideoPath = "input_video_path" |
|||
case sourceFacePath = "source_face_path" |
|||
case outputPath = "output_path" |
|||
} |
|||
} |
|||
|
|||
// MARK: - Response Models |
|||
struct ProcessResponse: Codable { |
|||
let success: Bool |
|||
let message: String |
|||
let outputPath: String? |
|||
let processingTime: Double? |
|||
|
|||
enum CodingKeys: String, CodingKey { |
|||
case success |
|||
case message |
|||
case outputPath = "output_path" |
|||
case processingTime = "processing_time" |
|||
} |
|||
} |
|||
|
|||
struct FolderProcessResponse: Codable { |
|||
let success: Bool |
|||
let message: String |
|||
let outputFolder: String |
|||
let totalImages: Int |
|||
let successCount: Int |
|||
let failedCount: Int |
|||
let processingTime: Double |
|||
|
|||
enum CodingKeys: String, CodingKey { |
|||
case success |
|||
case message |
|||
case outputFolder = "output_folder" |
|||
case totalImages = "total_images" |
|||
case successCount = "success_count" |
|||
case failedCount = "failed_count" |
|||
case processingTime = "processing_time" |
|||
} |
|||
} |
|||
|
|||
struct HealthResponse: Codable { |
|||
let status: String |
|||
let version: String |
|||
let cacheStats: [String: AnyCodable] |
|||
|
|||
enum CodingKeys: String, CodingKey { |
|||
case status |
|||
case version |
|||
case cacheStats = "cache_stats" |
|||
} |
|||
} |
|||
|
|||
struct ClearCacheResponse: Codable { |
|||
let message: String? |
|||
} |
|||
|
|||
struct CacheStatsResponse: Codable { |
|||
let stats: [String: AnyCodable] |
|||
} |
|||
|
|||
// Helper for dynamic JSON values |
|||
struct AnyCodable: Codable { |
|||
let value: Any |
|||
|
|||
init(_ value: Any) { |
|||
self.value = value |
|||
} |
|||
|
|||
init(from decoder: Decoder) throws { |
|||
let container = try decoder.singleValueContainer() |
|||
if let int = try? container.decode(Int.self) { |
|||
value = int |
|||
} else if let double = try? container.decode(Double.self) { |
|||
value = double |
|||
} else if let string = try? container.decode(String.self) { |
|||
value = string |
|||
} else if let bool = try? container.decode(Bool.self) { |
|||
value = bool |
|||
} else if let array = try? container.decode([AnyCodable].self) { |
|||
value = array.map { $0.value } |
|||
} else if let dict = try? container.decode([String: AnyCodable].self) { |
|||
value = dict.mapValues { $0.value } |
|||
} else { |
|||
value = NSNull() |
|||
} |
|||
} |
|||
|
|||
func encode(to encoder: Encoder) throws { |
|||
var container = encoder.singleValueContainer() |
|||
switch value { |
|||
case let int as Int: |
|||
try container.encode(int) |
|||
case let double as Double: |
|||
try container.encode(double) |
|||
case let string as String: |
|||
try container.encode(string) |
|||
case let bool as Bool: |
|||
try container.encode(bool) |
|||
case let array as [Any]: |
|||
try container.encode(array.map { AnyCodable($0) }) |
|||
case let dict as [String: Any]: |
|||
try container.encode(dict.mapValues { AnyCodable($0) }) |
|||
default: |
|||
try container.encodeNil() |
|||
} |
|||
} |
|||
} |
|||
|
|||
// MARK: - API Client |
|||
class FaceManager { |
|||
static let sharedInstance = FaceManager() |
|||
|
|||
let faceUrl = "http://win11marco:5013" |
|||
private let session: URLSession |
|||
|
|||
private init() { |
|||
let config = URLSessionConfiguration.default |
|||
config.timeoutIntervalForRequest = 300 |
|||
config.timeoutIntervalForResource = 600 |
|||
self.session = URLSession(configuration: config) |
|||
} |
|||
|
|||
// MARK: - Private Helper Methods |
|||
private func createRequest(endpoint: String, method: String = "GET", body: Codable? = nil) throws -> URLRequest { |
|||
guard let url = URL(string: faceUrl + endpoint) else { |
|||
throw FaceAPIError.invalidURL |
|||
} |
|||
|
|||
var request = URLRequest(url: url) |
|||
request.httpMethod = method |
|||
request.setValue("application/json", forHTTPHeaderField: "Content-Type") |
|||
|
|||
if let body = body { |
|||
let encoder = JSONEncoder() |
|||
encoder.keyEncodingStrategy = .convertToSnakeCase |
|||
request.httpBody = try encoder.encode(body) |
|||
} |
|||
|
|||
return request |
|||
} |
|||
|
|||
private func executeRequest<T: Decodable>(_ request: URLRequest) async throws -> T { |
|||
let (data, response) = try await session.data(for: request) |
|||
|
|||
guard let httpResponse = response as? HTTPURLResponse else { |
|||
throw FaceAPIError.invalidResponse |
|||
} |
|||
|
|||
guard (200...299).contains(httpResponse.statusCode) else { |
|||
if let errorMessage = try? JSONDecoder().decode([String: String].self, from: data), |
|||
let detail = errorMessage["detail"] { |
|||
throw FaceAPIError.serverError(detail) |
|||
} |
|||
throw FaceAPIError.serverError("HTTP \(httpResponse.statusCode)") |
|||
} |
|||
|
|||
let decoder = JSONDecoder() |
|||
decoder.keyDecodingStrategy = .convertFromSnakeCase |
|||
|
|||
do { |
|||
return try decoder.decode(T.self, from: data) |
|||
} catch { |
|||
throw FaceAPIError.decodingError(error) |
|||
} |
|||
} |
|||
|
|||
// MARK: - API Methods |
|||
|
|||
/// Get API information and documentation |
|||
func getRoot() async throws -> [String: Any] { |
|||
let request = try createRequest(endpoint: "/") |
|||
let response: [String: AnyCodable] = try await executeRequest(request) |
|||
return response.mapValues { $0.value } |
|||
} |
|||
|
|||
/// Health check endpoint with cache statistics |
|||
func healthCheck() async throws -> HealthResponse { |
|||
let request = try createRequest(endpoint: "/health") |
|||
return try await executeRequest(request) |
|||
} |
|||
|
|||
/// Process a single image with face swapping and GFPGAN restoration |
|||
/// - Parameters: |
|||
/// - inputImagePath: Path to the image to process |
|||
/// - sourceFacePath: Path to the source face image |
|||
/// - outputPath: Optional path for output (default: output.jpg) |
|||
func processImage(inputImagePath: String, sourceFacePath: String, outputPath: String? = nil) async throws -> ProcessResponse { |
|||
let body = ProcessImageRequest( |
|||
inputImagePath: inputImagePath, |
|||
sourceFacePath: sourceFacePath, |
|||
outputPath: outputPath |
|||
) |
|||
let request = try createRequest(endpoint: "/process-image", method: "POST", body: body) |
|||
return try await executeRequest(request) |
|||
} |
|||
|
|||
/// Process all images in a folder with face swapping and GFPGAN restoration |
|||
/// - Parameters: |
|||
/// - inputFolderPath: Path to folder containing images |
|||
/// - sourceFacePath: Path to the source face image |
|||
/// - outputFolderPath: Optional path to output folder (default: <input_folder>/swap) |
|||
func processFolder(inputFolderPath: String, sourceFacePath: String, outputFolderPath: String? = nil) async throws -> FolderProcessResponse { |
|||
let body = ProcessFolderRequest( |
|||
inputFolderPath: inputFolderPath, |
|||
sourceFacePath: sourceFacePath, |
|||
outputFolderPath: outputFolderPath |
|||
) |
|||
let request = try createRequest(endpoint: "/process-folder", method: "POST", body: body) |
|||
return try await executeRequest(request) |
|||
} |
|||
|
|||
/// Process a video file with face swapping and GFPGAN restoration |
|||
/// - Parameters: |
|||
/// - inputVideoPath: Path to the video to process |
|||
/// - sourceFacePath: Path to the source face image |
|||
/// - outputPath: Optional path for output video (default: output.mp4) |
|||
func processVideo(inputVideoPath: String, sourceFacePath: String, outputPath: String? = nil) async throws -> ProcessResponse { |
|||
let body = ProcessVideoRequest( |
|||
inputVideoPath: inputVideoPath, |
|||
sourceFacePath: sourceFacePath, |
|||
outputPath: outputPath |
|||
) |
|||
let request = try createRequest(endpoint: "/process-video", method: "POST", body: body) |
|||
return try await executeRequest(request) |
|||
} |
|||
|
|||
/// Clear the model cache |
|||
func clearCache() async throws -> [String: Any] { |
|||
let request = try createRequest(endpoint: "/cache/clear", method: "POST") |
|||
let response: [String: AnyCodable] = try await executeRequest(request) |
|||
return response.mapValues { $0.value } |
|||
} |
|||
|
|||
/// Get detailed cache statistics |
|||
func getCacheStats() async throws -> [String: Any] { |
|||
let request = try createRequest(endpoint: "/cache/stats") |
|||
let response: [String: AnyCodable] = try await executeRequest(request) |
|||
return response.mapValues { $0.value } |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue