diff --git a/Sources/FalClient/Client.swift b/Sources/FalClient/Client.swift index b28c847..b97a060 100644 --- a/Sources/FalClient/Client.swift +++ b/Sources/FalClient/Client.swift @@ -41,6 +41,7 @@ public protocol Client { func run(_ id: String, input: Payload?, options: RunOptions) async throws -> Payload + @available(*, deprecated, message: "Pass the path as part of the app identifier instead") func subscribe( to app: String, path: String?, @@ -50,6 +51,15 @@ public protocol Client { includeLogs: Bool, onQueueUpdate: OnQueueUpdate? ) async throws -> Payload + + func subscribe( + to app: String, + input: Payload?, + pollInterval: DispatchTimeInterval, + timeout: DispatchTimeInterval, + includeLogs: Bool, + onQueueUpdate: OnQueueUpdate? + ) async throws -> Payload } public extension Client { @@ -68,8 +78,8 @@ public extension Client { try await run(app, input: input, options: options) } - /// Submits a request to the given [app], an optional [path]. This method - /// uses the [queue] API to submit the request and poll for the result. + /// Submits a request to the given [app]. This method uses the [queue] API + /// to submit the request and poll for the result. /// /// This is useful for long running requests, and it's the preffered way /// to interact with the model APIs. @@ -83,7 +93,6 @@ public extension Client { /// - onQueueUpdate: A callback to be called when the queue status is updated. func subscribe( to app: String, - path: String? = nil, input: Payload? = nil, pollInterval: DispatchTimeInterval = .seconds(1), timeout: DispatchTimeInterval = .minutes(3), @@ -91,11 +100,29 @@ public extension Client { onQueueUpdate: OnQueueUpdate? = nil ) async throws -> Payload { try await subscribe(to: app, - path: path, input: input, pollInterval: pollInterval, timeout: timeout, includeLogs: includeLogs, onQueueUpdate: onQueueUpdate) } + + @available(*, deprecated, message: "Pass the path as part of the app identifier instead") + func subscribe( + to app: String, + path: String? = nil, + input: Payload? = nil, + pollInterval: DispatchTimeInterval = .seconds(1), + timeout: DispatchTimeInterval = .minutes(3), + includeLogs: Bool = false, + onQueueUpdate: OnQueueUpdate? = nil + ) async throws -> Payload { + let appId = path.map { "\(app)\($0)" } ?? app + return try await subscribe(to: appId, + input: input, + pollInterval: pollInterval, + timeout: timeout, + includeLogs: includeLogs, + onQueueUpdate: onQueueUpdate) + } } diff --git a/Sources/FalClient/Realtime.swift b/Sources/FalClient/Realtime.swift index dee0fcd..a60d94c 100644 --- a/Sources/FalClient/Realtime.swift +++ b/Sources/FalClient/Realtime.swift @@ -60,6 +60,9 @@ func hasBinaryField(_ type: Encodable) -> Bool { if child.value is Data { return true } + if case FalImageContent.raw = child.value { + return true + } } return false } @@ -112,8 +115,23 @@ public class RealtimeConnection: BaseRealtimeConnection {} /// Connection implementation that can be used to send messages using a custom `Encodable` type. public class TypedRealtimeConnection: BaseRealtimeConnection {} +/// This is a list of apps deployed before formal realtime support. Their URLs follow +/// a different pattern and will be kept here until we fully sunset them. +let LegacyApps = [ + "lcm-sd15-i2i", + "lcm", + "sdxl-turbo-realtime", + "sd-turbo-real-time-high-fps-msgpack-a10g", + "lcm-plexed-sd15-i2i", + "sd-turbo-real-time-high-fps-msgpack", +] + func buildRealtimeUrl(forApp app: String, token: String? = nil) -> URL { - guard var components = URLComponents(string: buildUrl(fromId: app, path: "/ws")) else { + // Some basic support for old ids, this should be removed during 1.0.0 release + // For full-support of old ids, users can point to version 0.4.x + let appAlias = (try? appAlias(fromId: app)) ?? app + let path = LegacyApps.contains(appAlias) || !app.contains("/") ? "/ws" : "/realtime" + guard var components = URLComponents(string: buildUrl(fromId: app, path: path)) else { preconditionFailure("Invalid URL. This is unexpected and likely a problem in the client library.") } components.scheme = "wss" @@ -121,8 +139,6 @@ func buildRealtimeUrl(forApp app: String, token: String? = nil) -> URL { if let token { components.queryItems = [URLQueryItem(name: "fal_jwt_token", value: token)] } - - print(components.url!) // swiftlint:disable:next force_unwrapping return components.url! } diff --git a/Sources/FalClient/Utility.swift b/Sources/FalClient/Utility.swift index 71170e8..02b3c83 100644 --- a/Sources/FalClient/Utility.swift +++ b/Sources/FalClient/Utility.swift @@ -8,7 +8,7 @@ func buildUrl(fromId id: String, path: String? = nil, subdomain: String? = nil) func ensureAppIdFormat(_ id: String) throws -> String { let parts = id.split(separator: "/") - if parts.count == 2 { + if parts.count > 1 { return id } let regex = try NSRegularExpression(pattern: "^([0-9]+)-([a-zA-Z0-9-]+)$") @@ -26,7 +26,7 @@ func ensureAppIdFormat(_ id: String) throws -> String { func appAlias(fromId id: String) throws -> String { let appId = try ensureAppIdFormat(id) - guard let alias = appId.split(separator: "/").last else { + guard let alias = appId.split(separator: "/").dropFirst().first else { throw FalError.invalidUrl(url: id) } return String(describing: alias) diff --git a/Sources/Samples/FalRealtimeSampleApp/FalRealtimeSampleApp/ViewModel.swift b/Sources/Samples/FalRealtimeSampleApp/FalRealtimeSampleApp/ViewModel.swift index 91ba70f..0b860b6 100644 --- a/Sources/Samples/FalRealtimeSampleApp/FalRealtimeSampleApp/ViewModel.swift +++ b/Sources/Samples/FalRealtimeSampleApp/FalRealtimeSampleApp/ViewModel.swift @@ -1,10 +1,6 @@ import FalClient import SwiftUI -// See https://www.fal.ai/models/latent-consistency-sd/api for API documentation - -let OptimizedLatentConsistency = "fal-ai/lcm-sd15-i2i" - struct LcmInput: Encodable { let prompt: String let image: FalImageContent @@ -28,12 +24,13 @@ class LiveImage: ObservableObject { // This example demonstrates the support to Codable types, but // RealtimeConnection can also be used for untyped input / output - // using dictionary-like ObjectValue + // using dictionary-like Payload private var connection: TypedRealtimeConnection? init() { connection = try? fal.realtime.connect( - to: OptimizedLatentConsistency, + // See https://fal.ai/models/latent-consistency-sd/api + to: "fal-ai/lcm-sd15-i2i", connectionKey: "PencilKitDemo", throttleInterval: .milliseconds(128) ) { (result: Result) in