diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3d85c64..716f889 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,15 +8,34 @@ on: jobs: build: + name: Build runs-on: macos-latest steps: - - uses: actions/checkout@v4 - - uses: swift-actions/setup-swift@v1 + - name: Checkout project + uses: actions/checkout@v4 + - name: Setup Swift + uses: swift-actions/setup-swift@v1 with: swift-version: "5.9" - - name: Check format - run: swift package plugin --allow-writing-to-package-directory swiftformat . - - name: Build Library + # - name: Test library + # run: swift test + - name: Build library run: swift build --target FalClient --configuration release - # - name: Build Sample App - # run: xcodebuild -project Sources/FalSampleApp/FalSampleApp.xcodeproj -scheme FalSampleApp + # samples: + # name: Build samples + # needs: build + # runs-on: macos-latest + # steps: + # - name: Checkout project + # uses: actions/checkout@v4 + # - name: Setup Swift + # uses: swift-actions/setup-swift@v1 + # with: + # swift-version: "5.9" + # - name: Build basic app + # uses: sersoft-gmbh/xcodebuild-action@v3 + # with: + # project: Sources/Samples/FalSampleApp/FalSampleApp.xcodeproj + # scheme: FalSampleApp + # destination: platform=iOS Simulator,name=iPhone 13,OS=16.2 + # action: build diff --git a/Package.resolved b/Package.resolved index 0d1d81f..141e85f 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,21 +1,48 @@ { "pins" : [ { - "identity" : "msgpack-swift", + "identity" : "cwlcatchexception", "kind" : "remoteSourceControl", - "location" : "https://github.com/fumoboy007/msgpack-swift.git", + "location" : "https://github.com/mattgallagher/CwlCatchException.git", "state" : { - "revision" : "1e3124367973f45955f27f49a617862605e55288", - "version" : "2.0.0" + "revision" : "3b123999de19bf04905bc1dfdb76f817b0f2cc00", + "version" : "2.1.2" } }, { - "identity" : "swiftformat", + "identity" : "cwlpreconditiontesting", "kind" : "remoteSourceControl", - "location" : "https://github.com/nicklockwood/SwiftFormat", + "location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git", "state" : { - "revision" : "cac06079ce883170ab44cb021faad298daeec2a5", - "version" : "0.52.10" + "revision" : "dc9af4781f2afdd1e68e90f80b8603be73ea7abc", + "version" : "2.2.0" + } + }, + { + "identity" : "nimble", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Quick/Nimble.git", + "state" : { + "revision" : "d616f15123bfb36db1b1075153f73cf40605b39d", + "version" : "13.0.0" + } + }, + { + "identity" : "quick", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Quick/Quick.git", + "state" : { + "revision" : "ef9aaf3f634b3a1ab6f54f1173fe2400b36e7cb8", + "version" : "7.3.0" + } + }, + { + "identity" : "swift-msgpack", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nnabeyang/swift-msgpack.git", + "state" : { + "revision" : "01a4324add1dbcba63dc74a8febb02291622b532", + "version" : "0.3.3" } } ], diff --git a/Package.swift b/Package.swift index e6409b6..aec66b4 100644 --- a/Package.swift +++ b/Package.swift @@ -6,37 +6,39 @@ import PackageDescription let package = Package( name: "FalClient", platforms: [ - .iOS(.v16), - .macOS(.v13), - .macCatalyst(.v16), - .tvOS(.v16), - .watchOS(.v9), + .iOS(.v15), + .macOS(.v12), + .macCatalyst(.v15), + .tvOS(.v15), + .watchOS(.v8), ], products: [ - // Products define the executables and libraries a package produces, making them visible to other packages. .library( name: "FalClient", targets: ["FalClient"] ), ], dependencies: [ - .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.52.10"), - .package(url: "https://github.com/fumoboy007/msgpack-swift.git", from: "2.0.0") + .package(url: "https://github.com/nnabeyang/swift-msgpack.git", from: "0.3.3"), + .package(url: "https://github.com/Quick/Quick.git", from: "7.3.0"), + .package(url: "https://github.com/Quick/Nimble.git", from: "13.0.0"), ], targets: [ - // Targets are the basic building blocks of a package, defining a module or a test suite. - // Targets can depend on other targets in this package and products from dependencies. .target( name: "FalClient", dependencies: [ - .product(name: "DMMessagePack", package: "msgpack-swift") + .product(name: "SwiftMsgpack", package: "swift-msgpack"), ], path: "Sources/FalClient" ), .testTarget( name: "FalClientTests", - dependencies: ["FalClient"], + dependencies: [ + "FalClient", + .product(name: "Quick", package: "quick"), + .product(name: "Nimble", package: "nimble"), + ], path: "Tests/FalClientTests" - ) + ), ] ) diff --git a/Sources/FalClient/Client+Request.swift b/Sources/FalClient/Client+Request.swift index d134522..11f41d6 100644 --- a/Sources/FalClient/Client+Request.swift +++ b/Sources/FalClient/Client+Request.swift @@ -56,7 +56,7 @@ extension Client { } func checkResponseStatus(for response: URLResponse, withData data: Data) throws { - guard let httpResponse = response as? HTTPURLResponse else { + guard response is HTTPURLResponse else { throw FalError.invalidResultFormat } if let httpResponse = response as? HTTPURLResponse, !httpResponse.isSuccessful { diff --git a/Sources/FalClient/FalClient.swift b/Sources/FalClient/FalClient.swift index 931ce48..aa4e966 100644 --- a/Sources/FalClient/FalClient.swift +++ b/Sources/FalClient/FalClient.swift @@ -1,10 +1,6 @@ import Dispatch import Foundation -func buildUrl(fromId id: String, path: String? = nil) -> String { - "https://\(id).gateway.alpha.fal.ai" + (path ?? "") -} - /// The main client class that provides access to simple API model usage, /// as well as access to the `queue` and `storage` APIs. /// diff --git a/Sources/FalClient/FalImage.swift b/Sources/FalClient/FalImage.swift new file mode 100644 index 0000000..64adce7 --- /dev/null +++ b/Sources/FalClient/FalImage.swift @@ -0,0 +1,102 @@ +import Foundation + +public enum FalImageContent: Codable { + case url(String) + case raw(Data) + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let url = try? container.decode(String.self) { + self = .url(url) + } else if let data = try? container.decode(Data.self) { + self = .raw(data) + } else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "FalImageContent must be either URL, Base64 or Binary") + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case let .url(url): + try container.encode(url) + case let .raw(data): + try container.encode(data) + } + } + + public var data: Data { + switch self { + case let .url(url): + let url = URL(string: url)! + return try! Data(contentsOf: url) + case let .raw(data): + return data + } + } +} + +extension FalImageContent: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self = .url(value) + } +} + +extension FalImageContent: ExpressibleByStringInterpolation { + public init(stringInterpolation: StringInterpolation) { + self = .url(stringInterpolation.string) + } + + public struct StringInterpolation: StringInterpolationProtocol { + var string: String = "" + + public init(literalCapacity _: Int, interpolationCount _: Int) {} + + public mutating func appendLiteral(_ literal: String) { + string.append(literal) + } + + public mutating func appendInterpolation(_ value: String) { + string.append(value) + } + } +} + +public struct FalImage: Codable { + public let content: FalImageContent + public let contentType: String + public let width: Int + public let height: Int + + // The following exist so we support payloads with both `url` and `content` keys + // This should no longer be necessary once the Server API is consolidated + enum UrlCodingKeys: String, CodingKey { + case content = "url" + case contentType = "content_type" + case width + case height + } + + enum RawDataCodingKeys: String, CodingKey { + case content + case contentType = "content_type" + case width + case height + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: UrlCodingKeys.self) + if let url = try? container.decode(String.self, forKey: .content) { + content = .url(url) + contentType = try container.decode(String.self, forKey: .contentType) + width = try container.decode(Int.self, forKey: .width) + height = try container.decode(Int.self, forKey: .height) + } else { + let container = try decoder.container(keyedBy: RawDataCodingKeys.self) + content = try .raw(container.decode(Data.self, forKey: .content)) + contentType = try container.decode(String.self, forKey: .contentType) + width = try container.decode(Int.self, forKey: .width) + height = try container.decode(Int.self, forKey: .height) + } + } +} diff --git a/Sources/FalClient/Payload.swift b/Sources/FalClient/Payload.swift index c57b2b4..c348d1c 100644 --- a/Sources/FalClient/Payload.swift +++ b/Sources/FalClient/Payload.swift @@ -1,5 +1,5 @@ import Foundation -import MessagePack +import SwiftMsgpack /// Represents a value that can be encoded and decoded. This data structure /// is used to represent the input and output of the model API and closely @@ -240,7 +240,7 @@ public extension Payload { } static func create(fromBinary data: Data) throws -> Payload { - try MessagePackDecoder().decode(Payload.self, from: data) + try MsgPackDecoder().decode(Payload.self, from: data) } func json() throws -> Data { @@ -248,7 +248,7 @@ public extension Payload { } func binary() throws -> Data { - try MessagePackEncoder().encode(self) + try MsgPackEncoder().encode(self) } } diff --git a/Sources/FalClient/Queue.swift b/Sources/FalClient/Queue.swift index 8022c89..07d0dd2 100644 --- a/Sources/FalClient/Queue.swift +++ b/Sources/FalClient/Queue.swift @@ -49,8 +49,23 @@ public struct QueueStatusInput: Encodable { public struct QueueClient: Queue { public let client: Client + func runOnQueue(_ app: String, input: Payload?, options: RunOptions) async throws -> Payload { + var requestInput = input + if let storage = client.storage as? StorageClient, + let input, + options.httpMethod != .get, + input.hasBinaryData + { + requestInput = try await storage.autoUpload(input: input) + } + let queryParams = options.httpMethod == .get ? input : nil + let url = buildUrl(fromId: app, path: options.path, subdomain: "queue") + let data = try await client.sendRequest(to: url, input: requestInput?.json(), queryParams: queryParams?.asDictionary, options: options) + return try .create(fromJSON: data) + } + public func submit(_ id: String, input: Payload?, webhookUrl _: String?) async throws -> String { - let result = try await client.run(id, input: input, options: .route("/fal/queue/submit")) + let result = try await runOnQueue(id, input: input, options: .withMethod(.post)) guard case let .string(requestId) = result["request_id"] else { throw FalError.invalidResultFormat } @@ -58,18 +73,20 @@ public struct QueueClient: Queue { } public func status(_ id: String, of requestId: String, includeLogs: Bool) async throws -> QueueStatus { - try await client.run( + let result = try await runOnQueue( id, - input: QueueStatusInput(logs: includeLogs), - options: .route("/fal/queue/requests/\(requestId)/status", withMethod: .get) + input: ["logs": .bool(includeLogs)], + options: .route("/requests/\(requestId)/status", withMethod: .get) ) + let json = try result.json() + return try JSONDecoder().decode(QueueStatus.self, from: json) } public func response(_ id: String, of requestId: String) async throws -> Payload { - try await client.run( + try await runOnQueue( id, input: nil as Payload?, - options: .route("/fal/queue/requests/\(requestId)/response", withMethod: .get) + options: .route("/requests/\(requestId)", withMethod: .get) ) } } diff --git a/Sources/FalClient/Realtime.swift b/Sources/FalClient/Realtime.swift index 21b5835..dee0fcd 100644 --- a/Sources/FalClient/Realtime.swift +++ b/Sources/FalClient/Realtime.swift @@ -1,7 +1,7 @@ import Dispatch import Foundation -import MessagePack +import SwiftMsgpack func throttle(_ function: @escaping (T) -> Void, throttleInterval: DispatchTimeInterval) -> ((T) -> Void) { var lastExecution = DispatchTime.now() @@ -20,7 +20,7 @@ public enum FalRealtimeError: Error { case connectionError(code: Int? = nil) case unauthorized case invalidInput - case invalidResult + case invalidResult(requestId: String? = nil, causedBy: Error? = nil) case serviceError(type: String, reason: String) } @@ -95,13 +95,13 @@ public class BaseRealtimeConnection { func sendJSON(_ data: Input) throws { let jsonData = try JSONEncoder().encode(data) guard let json = String(data: jsonData, encoding: .utf8) else { - throw FalRealtimeError.invalidResult + throw FalRealtimeError.invalidInput } try sendReference(.string(json)) } func sendBinary(_ data: Input) throws { - let payload = try MessagePackEncoder().encode(data) + let payload = try MsgPackEncoder().encode(data) try sendReference(.data(payload)) } } @@ -112,16 +112,17 @@ public class RealtimeConnection: BaseRealtimeConnection {} /// Connection implementation that can be used to send messages using a custom `Encodable` type. public class TypedRealtimeConnection: BaseRealtimeConnection {} -func buildRealtimeUrl(forApp app: String, host: String, token: String? = nil) -> URL { - var components = URLComponents() +func buildRealtimeUrl(forApp app: String, token: String? = nil) -> URL { + guard var components = URLComponents(string: buildUrl(fromId: app, path: "/ws")) else { + preconditionFailure("Invalid URL. This is unexpected and likely a problem in the client library.") + } components.scheme = "wss" - components.host = "\(app).\(host)" - components.path = "/ws" if let token { components.queryItems = [URLQueryItem(name: "fal_jwt_token", value: token)] } + print(components.url!) // swiftlint:disable:next force_unwrapping return components.url! } @@ -188,10 +189,8 @@ class WebSocketConnection: NSObject, URLSessionWebSocketDelegate { return } - // TODO: get host from config let url = buildRealtimeUrl( forApp: app, - host: "gateway.alpha.fal.ai", token: token ) let webSocketTask = session.webSocketTask(with: url) @@ -206,11 +205,9 @@ class WebSocketConnection: NSObject, URLSessionWebSocketDelegate { func refreshToken(_ app: String, completion: @escaping (Result) -> Void) { Task { - // TODO: improve app alias resolution - let appAlias = app.split(separator: "-").dropFirst().joined(separator: "-") let url = "https://rest.alpha.fal.ai/tokens/" - let body: Payload = [ - "allowed_apps": [.string(appAlias)], + let body: Payload = try [ + "allowed_apps": [.string(appAlias(fromId: app))], "token_expiration": 300, ] do { @@ -237,7 +234,7 @@ class WebSocketConnection: NSObject, URLSessionWebSocketDelegate { do { self?.receiveMessage() - var object = try message.decode(to: Payload.self) + let object = try message.decode(to: Payload.self) if isSuccessResult(object) { self?.onMessage(message) return @@ -350,7 +347,7 @@ extension WebSocketMessage { return data case let .string(string): guard let data = string.data(using: .utf8) else { - throw FalRealtimeError.invalidResult + throw FalRealtimeError.invalidResult() } return data @unknown default: @@ -361,7 +358,7 @@ extension WebSocketMessage { func decode(to type: Type.Type) throws -> Type { switch self { case let .data(data): - return try MessagePackDecoder().decode(type, from: data) + return try MsgPackDecoder().decode(type, from: data) case .string: return try JSONDecoder().decode(type, from: data()) @unknown default: diff --git a/Sources/FalClient/Utility.swift b/Sources/FalClient/Utility.swift new file mode 100644 index 0000000..71170e8 --- /dev/null +++ b/Sources/FalClient/Utility.swift @@ -0,0 +1,33 @@ +import Foundation + +func buildUrl(fromId id: String, path: String? = nil, subdomain: String? = nil) -> String { + let appId = (try? ensureAppIdFormat(id)) ?? id + let sub = subdomain != nil ? "\(subdomain!)." : "" + return "https://\(sub)fal.run/\(appId)" + (path ?? "") +} + +func ensureAppIdFormat(_ id: String) throws -> String { + let parts = id.split(separator: "/") + if parts.count == 2 { + return id + } + let regex = try NSRegularExpression(pattern: "^([0-9]+)-([a-zA-Z0-9-]+)$") + let matches = regex.matches(in: id, options: [], range: NSRange(location: 0, length: id.utf16.count)) + if let match = matches.first, match.numberOfRanges == 3, + let appOwnerRange = Range(match.range(at: 1), in: id), + let appIdRange = Range(match.range(at: 2), in: id) + { + let appOwner = String(id[appOwnerRange]) + let appId = String(id[appIdRange]) + return "\(appOwner)/\(appId)" + } + return id +} + +func appAlias(fromId id: String) throws -> String { + let appId = try ensureAppIdFormat(id) + guard let alias = appId.split(separator: "/").last else { + throw FalError.invalidUrl(url: id) + } + return String(describing: alias) +} diff --git a/Sources/Samples/FalCameraSampleApp/FalCameraSampleApp.xcodeproj/project.pbxproj b/Sources/Samples/FalCameraSampleApp/FalCameraSampleApp.xcodeproj/project.pbxproj index 44015ab..303998e 100644 --- a/Sources/Samples/FalCameraSampleApp/FalCameraSampleApp.xcodeproj/project.pbxproj +++ b/Sources/Samples/FalCameraSampleApp/FalCameraSampleApp.xcodeproj/project.pbxproj @@ -310,7 +310,7 @@ "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 14.0; @@ -350,7 +350,7 @@ "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 14.0; diff --git a/Sources/Samples/FalCameraSampleApp/FalCameraSampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Sources/Samples/FalCameraSampleApp/FalCameraSampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 54c8337..e0bc10d 100644 --- a/Sources/Samples/FalCameraSampleApp/FalCameraSampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Sources/Samples/FalCameraSampleApp/FalCameraSampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,12 +1,12 @@ { "pins" : [ { - "identity" : "msgpack-swift", + "identity" : "swift-msgpack", "kind" : "remoteSourceControl", - "location" : "https://github.com/fumoboy007/msgpack-swift.git", + "location" : "https://github.com/nnabeyang/swift-msgpack.git", "state" : { - "revision" : "1e3124367973f45955f27f49a617862605e55288", - "version" : "2.0.0" + "revision" : "01a4324add1dbcba63dc74a8febb02291622b532", + "version" : "0.3.3" } } ], diff --git a/Sources/Samples/FalCameraSampleApp/FalCameraSampleApp/ImageStreamingModel.swift b/Sources/Samples/FalCameraSampleApp/FalCameraSampleApp/ImageStreamingModel.swift index 9cd8ca7..4d15b14 100644 --- a/Sources/Samples/FalCameraSampleApp/FalCameraSampleApp/ImageStreamingModel.swift +++ b/Sources/Samples/FalCameraSampleApp/FalCameraSampleApp/ImageStreamingModel.swift @@ -1,9 +1,7 @@ import FalClient import SwiftUI -let TurboApp = "110602490-sd-turbo-real-time-high-fps-msgpack" -// let TurboApp = "110602490-sdxl-turbo-realtime-high-fps" -// let TurboApp = "110602490-sd-turbo-realtime-high-fps" +let TurboApp = "fal-ai/sd-turbo-real-time-high-fps-msgpack-a10g" class ImageStreamingModel: ObservableObject, ImageStreamingDelegate { @Published var currentCapturedFrame: UIImage? { diff --git a/Sources/Samples/FalCameraSampleApp/FalCameraSampleApp/fal.swift b/Sources/Samples/FalCameraSampleApp/FalCameraSampleApp/fal.swift index 6bc6f47..85c408d 100644 --- a/Sources/Samples/FalCameraSampleApp/FalCameraSampleApp/fal.swift +++ b/Sources/Samples/FalCameraSampleApp/FalCameraSampleApp/fal.swift @@ -1,4 +1,4 @@ import FalClient -// let fal = FalClient.withProxy("http://localhost:3333/api/fal/proxy") -let fal = FalClient.withCredentials(.keyPair("")) +let fal = FalClient.withProxy("http://localhost:3333/api/fal/proxy") +// let fal = FalClient.withCredentials(.keyPair("")) diff --git a/Sources/Samples/FalRealtimeSampleApp/FalRealtimeSampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Sources/Samples/FalRealtimeSampleApp/FalRealtimeSampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 073d75d..2ec9333 100644 --- a/Sources/Samples/FalRealtimeSampleApp/FalRealtimeSampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Sources/Samples/FalRealtimeSampleApp/FalRealtimeSampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -10,12 +10,12 @@ } }, { - "identity" : "msgpack-swift", + "identity" : "swift-msgpack", "kind" : "remoteSourceControl", - "location" : "https://github.com/fumoboy007/msgpack-swift.git", + "location" : "https://github.com/nnabeyang/swift-msgpack.git", "state" : { - "revision" : "1e3124367973f45955f27f49a617862605e55288", - "version" : "2.0.0" + "revision" : "01a4324add1dbcba63dc74a8febb02291622b532", + "version" : "0.3.3" } } ], diff --git a/Sources/Samples/FalRealtimeSampleApp/FalRealtimeSampleApp/ViewModel.swift b/Sources/Samples/FalRealtimeSampleApp/FalRealtimeSampleApp/ViewModel.swift index 378d13b..91ba70f 100644 --- a/Sources/Samples/FalRealtimeSampleApp/FalRealtimeSampleApp/ViewModel.swift +++ b/Sources/Samples/FalRealtimeSampleApp/FalRealtimeSampleApp/ViewModel.swift @@ -3,30 +3,24 @@ import SwiftUI // See https://www.fal.ai/models/latent-consistency-sd/api for API documentation -let OptimizedLatentConsistency = "110602490-lcm-sd15-i2i" +let OptimizedLatentConsistency = "fal-ai/lcm-sd15-i2i" struct LcmInput: Encodable { let prompt: String - let imageUrl: String + let image: FalImageContent let seed: Int let syncMode: Bool enum CodingKeys: String, CodingKey { case prompt - case imageUrl = "image_url" + case image = "image_url" case seed case syncMode = "sync_mode" } } -struct LcmImage: Decodable { - let url: String - let width: Int - let height: Int -} - struct LcmResponse: Decodable { - let images: [LcmImage] + let images: [FalImage] } class LiveImage: ObservableObject { @@ -46,9 +40,8 @@ class LiveImage: ObservableObject { if case let .success(data) = result, let image = data.images.first { - let data = try? Data(contentsOf: URL(string: image.url)!) DispatchQueue.main.async { - self.currentImage = data + self.currentImage = image.content.data } } if case let .failure(error) = result { @@ -61,7 +54,7 @@ class LiveImage: ObservableObject { if let connection { try connection.send(LcmInput( prompt: prompt, - imageUrl: "data:image/jpeg;base64,\(drawing.base64EncodedString())", + image: "data:image/jpeg;base64,\(drawing.base64EncodedString())", seed: 6_252_023, syncMode: true )) diff --git a/Sources/Samples/FalSampleApp/FalSampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Sources/Samples/FalSampleApp/FalSampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 073d75d..2ec9333 100644 --- a/Sources/Samples/FalSampleApp/FalSampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Sources/Samples/FalSampleApp/FalSampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -10,12 +10,12 @@ } }, { - "identity" : "msgpack-swift", + "identity" : "swift-msgpack", "kind" : "remoteSourceControl", - "location" : "https://github.com/fumoboy007/msgpack-swift.git", + "location" : "https://github.com/nnabeyang/swift-msgpack.git", "state" : { - "revision" : "1e3124367973f45955f27f49a617862605e55288", - "version" : "2.0.0" + "revision" : "01a4324add1dbcba63dc74a8febb02291622b532", + "version" : "0.3.3" } } ], diff --git a/Tests/FalClientTests/FalClientTests.swift b/Tests/FalClientTests/FalClientTests.swift deleted file mode 100644 index 9443d3c..0000000 --- a/Tests/FalClientTests/FalClientTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -@testable import FalClient -import XCTest - -final class FalClientTests: XCTestCase { - func testExample() throws { - // XCTest Documentation - // https://developer.apple.com/documentation/xctest - - // Defining Test Cases and Test Methods - // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods - } -} diff --git a/Tests/FalClientTests/UtilitySpec.swift b/Tests/FalClientTests/UtilitySpec.swift new file mode 100644 index 0000000..ee360d8 --- /dev/null +++ b/Tests/FalClientTests/UtilitySpec.swift @@ -0,0 +1,20 @@ +@testable import FalClient +import Nimble +import Quick + +class UtilitySpec: QuickSpec { + override static func spec() { + describe("Utility.buildUrl") { + it("should create a url to gateway fal.ai from a legacy app alias") { + let id = "1234-app-alias" + let url = buildUrl(fromId: id) + expect(url).to(equal("https://\(id).gateway.alpha.fal.ai")) + } + it("should create a url to fal.run from an app alias") { + let id = "user/app-alias" + let url = buildUrl(fromId: id) + expect(url).to(equal("https://fal.run/user/app-alias")) + } + } + } +}