diff --git a/Sources/ShipinKit/LumaAI/LumaAI.swift b/Sources/ShipinKit/LumaAI/LumaAI.swift index 0b8d1ea..52218c1 100644 --- a/Sources/ShipinKit/LumaAI/LumaAI.swift +++ b/Sources/ShipinKit/LumaAI/LumaAI.swift @@ -51,10 +51,13 @@ public actor LumaAI { let bodyData = try encoder.encode(requestBody) request.httpBody = bodyData + print("Request Body: \(String(data: bodyData, encoding: .utf8) ?? "")") + let (data, response) = try await URLSession.shared.data(for: request) if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) { - throw LumaAIError.httpError(statusCode: httpResponse.statusCode) + print("Error Response: \(String(data: data, encoding: .utf8) ?? "")") + throw LumaAIError.httpError(statusCode: httpResponse.statusCode) } let decoder = JSONDecoder() @@ -106,7 +109,9 @@ public actor LumaAI { /// - Returns: An array of `LumaAIGenerationResponse` objects representing the list of generations. /// /// - Throws: `LumaAIError.httpError` if the API request fails, or `LumaAIError.decodingError` if the response cannot be decoded. - public func listGenerations(limit: Int = 10, offset: Int = 0) async throws -> [LumaAIGenerationResponse] { + public func listGenerations(limit: Int = 10, offset: Int = 0) async throws -> LumaAIGenerationResponse { + debugPrint("Entering listGenerations with limit: \(limit), offset: \(offset)") + var components = URLComponents(url: baseURL.appendingPathComponent("/dream-machine/v1/generations"), resolvingAgainstBaseURL: true) components?.queryItems = [ URLQueryItem(name: "limit", value: String(limit)), @@ -114,26 +119,45 @@ public actor LumaAI { ] guard let url = components?.url else { + debugPrint("Error: Failed to construct URL") throw URLError(.badURL) } + debugPrint("Constructed URL: \(url)") var request = URLRequest(url: url) request.httpMethod = "GET" request.addValue("application/json", forHTTPHeaderField: "accept") request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "authorization") + debugPrint("Request headers: \(request.allHTTPHeaderFields ?? [:])") + debugPrint("Sending request...") let (data, response) = try await URLSession.shared.data(for: request) + debugPrint("Received response") - if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) { - throw LumaAIError.httpError(statusCode: httpResponse.statusCode) + if let httpResponse = response as? HTTPURLResponse { + debugPrint("HTTP Status Code: \(httpResponse.statusCode)") + if !(200...299).contains(httpResponse.statusCode) { + debugPrint("Error: HTTP request failed") + throw LumaAIError.httpError(statusCode: httpResponse.statusCode) + } + } + + debugPrint("Response data size: \(data.count) bytes") + if let responseString = String(data: data, encoding: .utf8) { + debugPrint("Response body: \(responseString)") } let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase do { - let generationResponses = try decoder.decode([LumaAIGenerationResponse].self, from: data) + debugPrint("Attempting to decode response...") + let generationResponses = try decoder.decode(LumaAIGenerationResponse.self, from: data) + debugPrint("Successfully decoded response") + debugPrint("Number of generations: \(generationResponses.generations.count)") return generationResponses } catch { + debugPrint("Error: Failed to decode response") + debugPrint("Decoding error: \(error)") throw LumaAIError.decodingError(underlying: error) } } @@ -159,9 +183,23 @@ public actor LumaAI { /// Retrieves a list of supported camera motions from the Luma AI API. /// + /// This method fetches an array of strings representing various camera motion options + /// that can be used in video generation. These motions define how the virtual camera + /// moves during the generated video sequence. + /// + /// Possible camera motions include: + /// - Static: No camera movement + /// - Move Left/Right/Up/Down: Camera translates in the specified direction + /// - Push In/Pull Out: Camera moves forward or backward + /// - Zoom In/Out: Camera lens zooms in or out + /// - Pan Left/Right: Camera rotates horizontally + /// - Orbit Left/Right: Camera circles around the subject + /// - Crane Up/Down: Camera moves vertically, typically on a crane or jib + /// /// - Returns: An array of strings representing supported camera motions. /// - /// - Throws: `LumaAIError.httpError` if the API request fails, or `LumaAIError.decodingError` if the response cannot be decoded. + /// - Throws: `LumaAIError.httpError` if the API request fails, or + /// `LumaAIError.decodingError` if the response cannot be decoded. public func listCameraMotions() async throws -> [String] { let url = baseURL.appendingPathComponent("/dream-machine/v1/generations/camera_motion/list") var request = URLRequest(url: url) @@ -190,22 +228,22 @@ public actor LumaAI { let task = Task { var currentResponse = initialResponse - while currentResponse.state != "completed" && currentResponse.state != "failed" { + while currentResponse.generations.first?.state != "completed" && currentResponse.generations.first?.state != "failed" { try await Task.sleep(for: .seconds(5)) - currentResponse = try await self.checkGenerationStatus(id: currentResponse.id) + currentResponse = try await self.checkGenerationStatus(id: currentResponse.generations.first?.id ?? "") } } - self.generationTasks[initialResponse.id] = task + self.generationTasks[initialResponse.generations.first?.id ?? ""] = task do { try await task.value } catch { - self.generationTasks.removeValue(forKey: initialResponse.id) + self.generationTasks.removeValue(forKey: initialResponse.generations.first?.id ?? "") throw error } - self.generationTasks.removeValue(forKey: initialResponse.id) + self.generationTasks.removeValue(forKey: initialResponse.generations.first?.id ?? "") } private func checkGenerationStatus(id: String) async throws -> LumaAIGenerationResponse { diff --git a/Sources/ShipinKit/LumaAI/LumaAIGenerationResponse.swift b/Sources/ShipinKit/LumaAI/LumaAIGenerationResponse.swift index 9847e84..ed6c510 100644 --- a/Sources/ShipinKit/LumaAI/LumaAIGenerationResponse.swift +++ b/Sources/ShipinKit/LumaAI/LumaAIGenerationResponse.swift @@ -6,25 +6,56 @@ // import Foundation - /// Represents the response from the Luma AI generation API. +/// +/// This struct encapsulates the data returned by the Luma AI generation API, including +/// information about the generation process, its current state, and associated assets. public struct LumaAIGenerationResponse: Codable, Sendable { - public let id: String - public let state: String - public let failureReason: String? - public let createdAt: String - public let assets: LumaAIAssets - public let version: String - public let request: LumaAIGenerationRequest - + /// A Boolean value indicating whether there are more results available. + public let hasMore: Bool? + + /// The total count of generations in the response. + public let count: Int + + /// The maximum number of generations that can be returned in a single response. + public let limit: Int + + /// The number of generations skipped before the current set of results. + public let offset: Int + + /// An array of generation details. + public let generations: [LumaAIGeneration] + + /// Represents a single generation in the response. + public struct LumaAIGeneration: Codable, Sendable { + /// The unique identifier for the generation. + public let id: String + + /// The current state of the generation (e.g., "completed", "failed", "in_progress"). + public let state: String + + /// The reason for failure, if the generation failed. Otherwise, it's null. + public let failureReason: String? + + /// The timestamp when the generation was created. + public let createdAt: String + + /// The assets associated with the generation. + public let assets: LumaAIAssets + + /// The version of the Luma AI API used for this generation. + public let version: String + + /// The original request parameters used for this generation. + public let request: LumaAIGenerationRequest + } + enum CodingKeys: String, CodingKey { - case id - case state - case failureReason = "failure_reason" - case createdAt = "created_at" - case assets - case version - case request + case hasMore = "has_more" + case count + case limit + case offset + case generations } } @@ -54,7 +85,6 @@ public struct LumaAIGenerationRequest: Codable, Sendable { public struct LumaAIKeyframeData: Codable, Sendable { public let type: LumaAIKeyframeType public let url: String? - public let id: String? } /// Represents the type of keyframe in the generation request. diff --git a/Tests/ShipinKitTests.swift b/Tests/ShipinKitTests.swift index 0b4ff8f..3747053 100644 --- a/Tests/ShipinKitTests.swift +++ b/Tests/ShipinKitTests.swift @@ -9,11 +9,11 @@ import XCTest @testable import ShipinKit final class ShipinKitTests: XCTestCase { - var shipinKit: ShipinKit! + var shipinKit: RunwayML! override func setUp() { super.setUp() - shipinKit = ShipinKit(apiKey: "KEY_HERE") + shipinKit = RunwayML(apiKey: "KEY_HERE") } func testShipinKitInitialization() {