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