Browse Source

Face

master
marcoschmickler 6 days ago
parent
commit
f5f90524b1
  1. 8
      kplayer.xcodeproj/project.pbxproj
  2. 306
      kplayer/core/FaceManager.swift
  3. 29
      kplayer/photo/SPhotoAlbumView.swift

8
kplayer.xcodeproj/project.pbxproj

@ -80,6 +80,7 @@
1C736F6A223D4ADB2E1BA733 /* ItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C736069C214E9522BB1BD97 /* ItemCell.swift */; };
1C736F7D29B76C7037CEF778 /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73647019E6C2E822127BA3 /* DatabaseManager.swift */; };
1C736FAE5D3E5D3BA3C1FAE5 /* links.html in Resources */ = {isa = PBXBuildFile; fileRef = 1C73645DBD6499A726D34973 /* links.html */; };
1C736FB6920008B2603D3415 /* openapi.json in Resources */ = {isa = PBXBuildFile; fileRef = 1C736DD5585DB81208FBF425 /* openapi.json */; };
1C736FB92B19FE17E4357C85 /* MediaItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C73688DAB88F9360FB62A49 /* MediaItem.swift */; };
AA74B07A01F0E99E6DEC7D1B /* Pods_kplayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B75159FFCD5A882E6F167FE /* Pods_kplayer.framework */; };
C91E05892795AC5C0003AB79 /* KTag+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C91E05832795AC5C0003AB79 /* KTag+CoreDataClass.swift */; };
@ -88,6 +89,7 @@
C91E058C2795AC5C0003AB79 /* KItem+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C91E05862795AC5C0003AB79 /* KItem+CoreDataProperties.swift */; };
C91E058D2795AC5C0003AB79 /* KSnapshot+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C91E05872795AC5C0003AB79 /* KSnapshot+CoreDataClass.swift */; };
C91E058E2795AC5C0003AB79 /* KSnapshot+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C91E05882795AC5C0003AB79 /* KSnapshot+CoreDataProperties.swift */; };
C92A39432EE783930013E899 /* FaceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C92A39422EE7838B0013E899 /* FaceManager.swift */; };
C98AF5D51B124D6A00D196CC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C98AF5D41B124D6A00D196CC /* AppDelegate.swift */; };
C98AF5D81B124D6A00D196CC /* kplayer.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = C98AF5D61B124D6A00D196CC /* kplayer.xcdatamodeld */; };
C98AF5DF1B124D6A00D196CC /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C98AF5DD1B124D6A00D196CC /* Main.storyboard */; };
@ -175,6 +177,7 @@
1C736DBB6986A8B62963FBB3 /* HtmlParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HtmlParser.swift; sourceTree = "<group>"; };
1C736DCCE3AA9993E15F7652 /* UIImageExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageExtension.swift; sourceTree = "<group>"; };
1C736DCD945ABAE984FF43EF /* KNetworkProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KNetworkProtocol.swift; sourceTree = "<group>"; };
1C736DD5585DB81208FBF425 /* openapi.json */ = {isa = PBXFileReference; lastKnownFileType = file.json; path = openapi.json; sourceTree = "<group>"; };
1C736E32C8574BFE3536F1C2 /* ImageCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
1C736E59A0C8DB9AC4D98F7B /* SRangeSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRangeSlider.swift; sourceTree = "<group>"; };
1C736EA15A11AF7D57F85824 /* ThumbnailCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailCache.swift; sourceTree = "<group>"; };
@ -191,6 +194,7 @@
C91E05862795AC5C0003AB79 /* KItem+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KItem+CoreDataProperties.swift"; sourceTree = "<group>"; };
C91E05872795AC5C0003AB79 /* KSnapshot+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KSnapshot+CoreDataClass.swift"; sourceTree = "<group>"; };
C91E05882795AC5C0003AB79 /* KSnapshot+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KSnapshot+CoreDataProperties.swift"; sourceTree = "<group>"; };
C92A39422EE7838B0013E899 /* FaceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaceManager.swift; sourceTree = "<group>"; };
C98AF5CF1B124D6A00D196CC /* kplayer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = kplayer.app; sourceTree = BUILT_PRODUCTS_DIR; };
C98AF5D31B124D6A00D196CC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C98AF5D41B124D6A00D196CC /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@ -311,6 +315,7 @@
1C736DC8C3AFB991541A2079 /* core */ = {
isa = PBXGroup;
children = (
C92A39422EE7838B0013E899 /* FaceManager.swift */,
C9D6764E291E73A00060179C /* Cached+CoreDataClass.swift */,
C9D6764F291E73A00060179C /* Cached+CoreDataProperties.swift */,
C91E05832795AC5C0003AB79 /* KTag+CoreDataClass.swift */,
@ -331,6 +336,7 @@
1C73659CC9B523B957E58DC6 /* LocalManager.swift */,
1C73647019E6C2E822127BA3 /* DatabaseManager.swift */,
1C7363743CD8120637AC1EDE /* KFrame.swift */,
1C736DD5585DB81208FBF425 /* openapi.json */,
);
path = core;
sourceTree = "<group>";
@ -545,6 +551,7 @@
1C73696E4C0353053BF98031 /* links.html in Resources */,
1C736FAE5D3E5D3BA3C1FAE5 /* links.html in Resources */,
1C736C9821DA743C2E3F3B07 /* kplayer.txt in Resources */,
1C736FB6920008B2603D3415 /* openapi.json in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -629,6 +636,7 @@
1C736821D6DF2237A3EABCC1 /* ViewControllerExtensions.swift in Sources */,
1C73671FC2CCCACAA2FFC153 /* ThumbnailCache.swift in Sources */,
C91E05892795AC5C0003AB79 /* KTag+CoreDataClass.swift in Sources */,
C92A39432EE783930013E899 /* FaceManager.swift in Sources */,
1C736E21B246C0BE7E123FD3 /* MediaModel.swift in Sources */,
1C736A7B6221A1D50FB3904C /* ItemType.swift in Sources */,
1C7360C0F2A4F0214FE353BD /* FileHelper.swift in Sources */,

306
kplayer/core/FaceManager.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 }
}
}

29
kplayer/photo/SPhotoAlbumView.swift

@ -69,7 +69,7 @@ struct SPhotoAlbumView: View {
if embedded && !more {
v.overlay(SEmbeddedVideo(embedded: $embedded, down: $embDown).offset(y: embDown ? 0: 70), alignment: embDown ? .bottomLeading : .topLeading)
} else if more {
v.overlay(VStack {
v.overlay(VStack (spacing: 15 ) {
KToggleButton(text: "spring", binding: $model.spring).frame(height: 30)
Button(action: {
saveSelectedItem()
@ -88,8 +88,17 @@ struct SPhotoAlbumView: View {
Text("copy")
})
.buttonStyle(BorderlessButtonStyle())
Button(action: {faceSelectedItem("sarah")}, label: {Text("sarah")}).buttonStyle(BorderlessButtonStyle())
Button(action: {faceSelectedItem("claudia")}, label: {Text("claudia")}).buttonStyle(BorderlessButtonStyle())
Button(action: {faceSelectedItem("jessica")}, label: {Text("jessica")}).buttonStyle(BorderlessButtonStyle())
Button(action: {faceSelectedItem("marleen")}, label: {Text("marleen")}).buttonStyle(BorderlessButtonStyle())
Button(action: {faceSelectedItem("renate")}, label: {Text("renate")}).buttonStyle(BorderlessButtonStyle())
Button(action: {faceSelectedItem("birgit")}, label: {Text("birgit")}).buttonStyle(BorderlessButtonStyle())
Button(action: {faceSelectedItem("barbara")}, label: {Text("barbara")}).buttonStyle(BorderlessButtonStyle())
Button(action: {faceSelectedItem("nina")}, label: {Text("nina")}).buttonStyle(BorderlessButtonStyle())
Button(action: {faceSelectedItem("amruta")}, label: {Text("amruta")}).buttonStyle(BorderlessButtonStyle())
}
.frame(width: 60, alignment: .top).offset(x: 0, y: 70), alignment: .topLeading)
.frame(width: 80, alignment: .top).offset(x: 0, y: 70), alignment: .topLeading)
.overlay(TagEditor(item: model.allItems[model.index], completionHandler: DatabaseManager.sharedInstance.saveItemMetaData)
.frame(width: 60, alignment: .top).offset(x: 0, y: 70),
alignment: .topTrailing)
@ -105,6 +114,22 @@ struct SPhotoAlbumView: View {
DatabaseManager.sharedInstance.saveItemMetaData(item)
}
func faceSelectedItem(_ name: String) {
let item = model.selectedItem
let path = item.fullPath.replacing("/srv/samba/ren", with: "z:")
let outpath1 = path.replacing("/", with: "")
let outpath = outpath1.replacing("z:", with: "z:/cut/"+name+"/")
print(path)
print(outpath1)
print(outpath)
Task {
try await FaceManager.sharedInstance.processFolder(inputFolderPath: path, sourceFacePath: "benchmark/"+name+".jpg", outputFolderPath: outpath)
//try! await FaceManager.sharedInstance.processImage(inputImagePath: "input", sourceFacePath: "benchmark/Renate.jpg")
}
}
func cleanup() {
for i in model.allItems {
i.thumbImage = nil

Loading…
Cancel
Save