From c24a1bf144b6a4e095ba2b22f3506219d4651810 Mon Sep 17 00:00:00 2001 From: Aled Samuel Date: Mon, 24 Apr 2023 13:17:50 +0100 Subject: [PATCH 1/9] Extend Images API to support Edits and Variations --- README.md | 74 +++++++++++++++++++ Sources/OpenAI/OpenAI.swift | 13 +++- .../Public/Models/ImageEditsQuery.swift | 29 ++++++++ .../Public/Models/ImageVariationsQuery.swift | 23 ++++++ .../Protocols/OpenAIProtocol+Async.swift | 30 ++++++++ .../Protocols/OpenAIProtocol+Combine.swift | 14 ++++ .../Public/Protocols/OpenAIProtocol.swift | 36 ++++++++- Tests/OpenAITests/OpenAITests.swift | 38 ++++++++++ 8 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 Sources/OpenAI/Public/Models/ImageEditsQuery.swift create mode 100644 Sources/OpenAI/Public/Models/ImageVariationsQuery.swift diff --git a/README.md b/README.md index 23dfc1fe..26c0d091 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ This repository contains Swift community-maintained implementation over [OpenAI] - [Completions](#completions) - [Chats](#chats) - [Images](#images) + - [Create Image](#create-image) + - [Create Image Edit](#create-image-edit) + - [Create Image Variation](#create-image-variation) - [Audio](#audio) - [Audio Transcriptions](#audio-transcriptions) - [Audio Translations](#audio-translations) @@ -252,6 +255,8 @@ Given a prompt and/or an input image, the model will generate a new image. As Artificial Intelligence continues to develop, so too does the intriguing concept of Dall-E. Developed by OpenAI, a research lab for artificial intelligence purposes, Dall-E has been classified as an AI system that can generate images based on descriptions provided by humans. With its potential applications spanning from animation and illustration to design and engineering - not to mention the endless possibilities in between - it's easy to see why there is such excitement over this new technology. +### Create Image + **Request** ```swift @@ -276,6 +281,7 @@ struct ImagesResult: Codable, Equatable { public let data: [URLResult] } ``` + **Example** ```swift @@ -300,6 +306,74 @@ let result = try await openAI.images(query: query) ![Generated Image](https://user-images.githubusercontent.com/1411778/213134082-ba988a72-fca0-4213-8805-63e5f8324cab.png) +### Create Image Edit + +Creates an edited or extended image given an original image and a prompt. + +**Request** + +```swift +public struct ImageEditsQuery: Codable { + /// The image to edit. Must be a valid PNG file, less than 4MB, and square. If mask is not provided, image must have transparency, which will be used as the mask. + public let image: String + /// An additional image whose fully transparent areas (e.g. where alpha is zero) indicate where image should be edited. Must be a valid PNG file, less than 4MB, and have the same dimensions as image. + public let mask: String? + /// A text description of the desired image(s). The maximum length is 1000 characters. + public let prompt: String + /// The number of images to generate. Must be between 1 and 10. + public let n: Int? + /// The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024. + public let size: String? +} +``` + +**Response** + +Uses the ImagesResult response similarly to ImagesQuery. + +**Example** + +```swift + let query = ImagesEditQuery(image: "@whitecat.png", prompt: "White cat with heterochromia sitting on the kitchen table with a bowl of food", n: 1, size: "1024x1024") + openAI.imageEdits(query: query) { result in + //Handle result here + } +//or +let result = try await openAI.imageEdits(query: query) +``` + +### Create Image Variation + +Creates a variation of a given image. + +**Request** + +```swift +public struct ImageVariationsQuery: Codable { + /// The image to edit. Must be a valid PNG file, less than 4MB, and square. If mask is not provided, image must have transparency, which will be used as the mask. + public let image: String + /// The number of images to generate. Must be between 1 and 10. + public let n: Int? + /// The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024. + public let size: String? +} +``` + +**Response** + +Uses the ImagesResult response similarly to ImagesQuery. + +**Example** + +```swift + let query = ImagesVariationQuery(image: "@whitecat.png", n: 1, size: "1024x1024") + openAI.imageVariations(query: query) { result in + //Handle result here + } +//or +let result = try await openAI.imageVariations(query: query) +``` + Review [Images Documentation](https://platform.openai.com/docs/api-reference/images) for more info. ### Audio diff --git a/Sources/OpenAI/OpenAI.swift b/Sources/OpenAI/OpenAI.swift index 6d8fde68..8371f5ec 100644 --- a/Sources/OpenAI/OpenAI.swift +++ b/Sources/OpenAI/OpenAI.swift @@ -63,6 +63,14 @@ final public class OpenAI: OpenAIProtocol { performRequest(request: JSONRequest(body: query, url: buildURL(path: .images)), completion: completion) } + public func imageEdits(query: ImageEditsQuery, completion: @escaping (Result) -> Void) { + performRequest(request: JSONRequest(body: query, url: buildURL(path: .imageEdits)), completion: completion) + } + + public func imageVariations(query: ImageVariationsQuery, completion: @escaping (Result) -> Void) { + performRequest(request: JSONRequest(body: query, url: buildURL(path: .imageVariations)), completion: completion) + } + public func embeddings(query: EmbeddingsQuery, completion: @escaping (Result) -> Void) { performRequest(request: JSONRequest(body: query, url: buildURL(path: .embeddings)), completion: completion) } @@ -151,7 +159,6 @@ typealias APIPath = String extension APIPath { static let completions = "/v1/completions" - static let images = "/v1/images/generations" static let embeddings = "/v1/embeddings" static let chats = "/v1/chat/completions" static let edits = "/v1/edits" @@ -161,6 +168,10 @@ extension APIPath { static let audioTranscriptions = "/v1/audio/transcriptions" static let audioTranslations = "/v1/audio/translations" + static let images = "/v1/images/generations" + static let imageEdits = "/v1/images/edits" + static let imageVariations = "/v1/images/variations" + func withPath(_ path: String) -> String { self + "/" + path } diff --git a/Sources/OpenAI/Public/Models/ImageEditsQuery.swift b/Sources/OpenAI/Public/Models/ImageEditsQuery.swift new file mode 100644 index 00000000..db7ca147 --- /dev/null +++ b/Sources/OpenAI/Public/Models/ImageEditsQuery.swift @@ -0,0 +1,29 @@ +// +// ImageEditsQuery.swift +// +// +// Created by Aled Samuel on 24/04/2023. +// + +import Foundation + +public struct ImageEditsQuery: Codable { + /// The image to edit. Must be a valid PNG file, less than 4MB, and square. If mask is not provided, image must have transparency, which will be used as the mask. + public let image: String + /// An additional image whose fully transparent areas (e.g. where alpha is zero) indicate where image should be edited. Must be a valid PNG file, less than 4MB, and have the same dimensions as image. + public let mask: String? + /// A text description of the desired image(s). The maximum length is 1000 characters. + public let prompt: String + /// The number of images to generate. Must be between 1 and 10. + public let n: Int? + /// The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024. + public let size: String? + + public init(image: String, mask: String? = nil, prompt: String, n: Int? = nil, size: String? = nil) { + self.image = image + self.mask = mask + self.prompt = prompt + self.n = n + self.size = size + } +} diff --git a/Sources/OpenAI/Public/Models/ImageVariationsQuery.swift b/Sources/OpenAI/Public/Models/ImageVariationsQuery.swift new file mode 100644 index 00000000..3b787e5d --- /dev/null +++ b/Sources/OpenAI/Public/Models/ImageVariationsQuery.swift @@ -0,0 +1,23 @@ +// +// ImageVariationsQuery.swift +// +// +// Created by Aled Samuel on 24/04/2023. +// + +import Foundation + +public struct ImageVariationsQuery: Codable { + /// The image to edit. Must be a valid PNG file, less than 4MB, and square. If mask is not provided, image must have transparency, which will be used as the mask. + public let image: String + /// The number of images to generate. Must be between 1 and 10. + public let n: Int? + /// The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024. + public let size: String? + + public init(image: String, n: Int? = nil, size: String? = nil) { + self.image = image + self.n = n + self.size = size + } +} diff --git a/Sources/OpenAI/Public/Protocols/OpenAIProtocol+Async.swift b/Sources/OpenAI/Public/Protocols/OpenAIProtocol+Async.swift index da20a6f7..0c3137d4 100644 --- a/Sources/OpenAI/Public/Protocols/OpenAIProtocol+Async.swift +++ b/Sources/OpenAI/Public/Protocols/OpenAIProtocol+Async.swift @@ -41,6 +41,36 @@ public extension OpenAIProtocol { } } } + + func imageEdits( + query: ImageEditsQuery + ) async throws -> ImagesResult { + try await withCheckedThrowingContinuation { continuation in + imageEdits(query: query) { result in + switch result { + case let .success(success): + return continuation.resume(returning: success) + case let .failure(failure): + return continuation.resume(throwing: failure) + } + } + } + } + + func imageVariations( + query: ImageVariationsQuery + ) async throws -> ImagesResult { + try await withCheckedThrowingContinuation { continuation in + imageVariations(query: query) { result in + switch result { + case let .success(success): + return continuation.resume(returning: success) + case let .failure(failure): + return continuation.resume(throwing: failure) + } + } + } + } func embeddings( query: EmbeddingsQuery diff --git a/Sources/OpenAI/Public/Protocols/OpenAIProtocol+Combine.swift b/Sources/OpenAI/Public/Protocols/OpenAIProtocol+Combine.swift index 65d403bd..2550ea4b 100644 --- a/Sources/OpenAI/Public/Protocols/OpenAIProtocol+Combine.swift +++ b/Sources/OpenAI/Public/Protocols/OpenAIProtocol+Combine.swift @@ -28,6 +28,20 @@ public extension OpenAIProtocol { } .eraseToAnyPublisher() } + + func imageEdits(query: ImageEditsQuery) -> AnyPublisher { + Future { + imageEdits(query: query, completion: $0) + } + .eraseToAnyPublisher() + } + + func imageVariations(query: ImageVariationsQuery) -> AnyPublisher { + Future { + imageVariations(query: query, completion: $0) + } + .eraseToAnyPublisher() + } func embeddings(query: EmbeddingsQuery) -> AnyPublisher { Future { diff --git a/Sources/OpenAI/Public/Protocols/OpenAIProtocol.swift b/Sources/OpenAI/Public/Protocols/OpenAIProtocol.swift index 67245fc7..5e5f2a95 100644 --- a/Sources/OpenAI/Public/Protocols/OpenAIProtocol.swift +++ b/Sources/OpenAI/Public/Protocols/OpenAIProtocol.swift @@ -38,11 +38,45 @@ public protocol OpenAIProtocol { ``` - Parameters: - - query: An `ImagesQuery` object containing the input parameters for the API request. This includes the query parameters such as the model, text prompt, image size, and other settings. + - query: An `ImagesQuery` object containing the input parameters for the API request. This includes the query parameters such as the text prompt, image size, and other settings. - completion: A closure which receives the result when the API request finishes. The closure's parameter, `Result`, will contain either the `ImagesResult` object with the generated images, or an error if the request failed. **/ func images(query: ImagesQuery, completion: @escaping (Result) -> Void) + /** + This function sends an image edit query to the OpenAI API and retrieves generated images in response. The Images Edit API enables you to edit images or graphics using OpenAI's powerful deep learning models. + + Example: + ``` + let query = ImagesEditQuery(image: "@whitecat.png", prompt: "White cat with heterochromia sitting on the kitchen table with a bowl of food", n: 1, size: "1024x1024") + openAI.imageEdits(query: query) { result in + //Handle result here + } + ``` + + - Parameters: + - query: An `ImagesEditQuery` object containing the input parameters for the API request. This includes the query parameters such as the image to be edited, an image to be used a mask if applicable, text prompt, image size, and other settings. + - completion: A closure which receives the result when the API request finishes. The closure's parameter, `Result`, will contain either the `ImagesResult` object with the generated images, or an error if the request failed. + **/ + func imageEdits(query: ImageEditsQuery, completion: @escaping (Result) -> Void) + + /** + This function sends an image variation query to the OpenAI API and retrieves generated images in response. The Images Variations API enables you to create a variation of a given image using OpenAI's powerful deep learning models. + + Example: + ``` + let query = ImagesVariationQuery(image: "@whitecat.png", n: 1, size: "1024x1024") + openAI.imageVariations(query: query) { result in + //Handle result here + } + ``` + + - Parameters: + - query: An `ImagesVariationQuery` object containing the input parameters for the API request. This includes the query parameters such as the image to use as a basis for the variation(s), image size, and other settings. + - completion: A closure which receives the result when the API request finishes. The closure's parameter, `Result`, will contain either the `ImagesResult` object with the generated images, or an error if the request failed. + **/ + func imageVariations(query: ImageVariationsQuery, completion: @escaping (Result) -> Void) + /** This function sends an embeddings query to the OpenAI API and retrieves embeddings in response. The Embeddings API enables you to generate high-dimensional vector representations of texts, which can be used for various natural language processing tasks such as semantic similarity, clustering, and classification. diff --git a/Tests/OpenAITests/OpenAITests.swift b/Tests/OpenAITests/OpenAITests.swift index f077866b..44a626ca 100644 --- a/Tests/OpenAITests/OpenAITests.swift +++ b/Tests/OpenAITests/OpenAITests.swift @@ -62,6 +62,44 @@ class OpenAITests: XCTestCase { XCTAssertEqual(inError, apiError) } + func testImageEdit() async throws { + let query = ImageEditsQuery(image: "@whitecat.png", prompt: "White cat with heterochromia sitting on the kitchen table with a bowl of food", n: 1, size: "1024x1024") + let imagesResult = ImagesResult(created: 100, data: [ + .init(url: "http://foo.bar") + ]) + try self.stub(result: imagesResult) + let result = try await openAI.imageEdits(query: query) + XCTAssertEqual(result, imagesResult) + } + + func testImageEditError() async throws { + let query = ImageEditsQuery(image: "@whitecat.png", prompt: "White cat with heterochromia sitting on the kitchen table with a bowl of food", n: 1, size: "1024x1024") + let inError = APIError(message: "foo", type: "bar", param: "baz", code: "100") + self.stub(error: inError) + + let apiError: APIError = try await XCTExpectError { try await openAI.imageEdits(query: query) } + XCTAssertEqual(inError, apiError) + } + + func testImageVariation() async throws { + let query = ImageVariationsQuery(image: "@whitecat.png", n: 1, size: "1024x1024") + let imagesResult = ImagesResult(created: 100, data: [ + .init(url: "http://foo.bar") + ]) + try self.stub(result: imagesResult) + let result = try await openAI.imageVariations(query: query) + XCTAssertEqual(result, imagesResult) + } + + func testImageVariationError() async throws { + let query = ImageVariationsQuery(image: "@whitecat.png", n: 1, size: "1024x1024") + let inError = APIError(message: "foo", type: "bar", param: "baz", code: "100") + self.stub(error: inError) + + let apiError: APIError = try await XCTExpectError { try await openAI.imageVariations(query: query) } + XCTAssertEqual(inError, apiError) + } + func testChats() async throws { let query = ChatQuery(model: .gpt4, messages: [ .init(role: .system, content: "You are Librarian-GPT. You know everything about the books."), From 39ae798580e5029e4f972223be7293e5f9dba7a9 Mon Sep 17 00:00:00 2001 From: Aled Samuel Date: Tue, 25 Apr 2023 18:09:21 +0100 Subject: [PATCH 2/9] Image Edits and Variations now use MultipartFormDataBody --- Sources/OpenAI/OpenAI.swift | 4 ++-- .../MultipartFormDataBodyBuilder.swift | 12 ++++++---- .../Private/MultipartFormDataEntry.swift | 2 +- .../Public/Models/ImageEditsQuery.swift | 23 ++++++++++++++++--- .../Public/Models/ImageVariationsQuery.swift | 19 ++++++++++++--- 5 files changed, 46 insertions(+), 14 deletions(-) diff --git a/Sources/OpenAI/OpenAI.swift b/Sources/OpenAI/OpenAI.swift index 8371f5ec..e4ec5a62 100644 --- a/Sources/OpenAI/OpenAI.swift +++ b/Sources/OpenAI/OpenAI.swift @@ -64,11 +64,11 @@ final public class OpenAI: OpenAIProtocol { } public func imageEdits(query: ImageEditsQuery, completion: @escaping (Result) -> Void) { - performRequest(request: JSONRequest(body: query, url: buildURL(path: .imageEdits)), completion: completion) + performRequest(request: MultipartFormDataRequest(body: query, url: buildURL(path: .imageEdits)), completion: completion) } public func imageVariations(query: ImageVariationsQuery, completion: @escaping (Result) -> Void) { - performRequest(request: JSONRequest(body: query, url: buildURL(path: .imageVariations)), completion: completion) + performRequest(request: MultipartFormDataRequest(body: query, url: buildURL(path: .imageVariations)), completion: completion) } public func embeddings(query: EmbeddingsQuery, completion: @escaping (Result) -> Void) { diff --git a/Sources/OpenAI/Private/MultipartFormDataBodyBuilder.swift b/Sources/OpenAI/Private/MultipartFormDataBodyBuilder.swift index 741968e7..73832b70 100644 --- a/Sources/OpenAI/Private/MultipartFormDataBodyBuilder.swift +++ b/Sources/OpenAI/Private/MultipartFormDataBodyBuilder.swift @@ -32,11 +32,13 @@ private extension MultipartFormDataEntry { var body = Data() switch self { case .file(let paramName, let fileName, let fileData, let contentType): - body.append("--\(boundary)\r\n") - body.append("Content-Disposition: form-data; name=\"\(paramName)\"; filename=\"\(fileName)\"\r\n") - body.append("Content-Type: \(contentType)\r\n\r\n") - body.append(fileData) - body.append("\r\n") + if let fileName, let fileData { + body.append("--\(boundary)\r\n") + body.append("Content-Disposition: form-data; name=\"\(paramName)\"; filename=\"\(fileName)\"\r\n") + body.append("Content-Type: \(contentType)\r\n\r\n") + body.append(fileData) + body.append("\r\n") + } case .string(let paramName, let value): if let value { body.append("--\(boundary)\r\n") diff --git a/Sources/OpenAI/Private/MultipartFormDataEntry.swift b/Sources/OpenAI/Private/MultipartFormDataEntry.swift index 3e1f7de1..93e9120c 100644 --- a/Sources/OpenAI/Private/MultipartFormDataEntry.swift +++ b/Sources/OpenAI/Private/MultipartFormDataEntry.swift @@ -9,6 +9,6 @@ import Foundation enum MultipartFormDataEntry { - case file(paramName: String, fileName: String, fileData: Data, contentType: String), + case file(paramName: String, fileName: String?, fileData: Data?, contentType: String), string(paramName: String, value: Any?) } diff --git a/Sources/OpenAI/Public/Models/ImageEditsQuery.swift b/Sources/OpenAI/Public/Models/ImageEditsQuery.swift index db7ca147..a208c8d7 100644 --- a/Sources/OpenAI/Public/Models/ImageEditsQuery.swift +++ b/Sources/OpenAI/Public/Models/ImageEditsQuery.swift @@ -9,9 +9,11 @@ import Foundation public struct ImageEditsQuery: Codable { /// The image to edit. Must be a valid PNG file, less than 4MB, and square. If mask is not provided, image must have transparency, which will be used as the mask. - public let image: String + public let image: Data + public let fileName: String /// An additional image whose fully transparent areas (e.g. where alpha is zero) indicate where image should be edited. Must be a valid PNG file, less than 4MB, and have the same dimensions as image. - public let mask: String? + public let mask: Data? + public let maskFileName: String? /// A text description of the desired image(s). The maximum length is 1000 characters. public let prompt: String /// The number of images to generate. Must be between 1 and 10. @@ -19,11 +21,26 @@ public struct ImageEditsQuery: Codable { /// The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024. public let size: String? - public init(image: String, mask: String? = nil, prompt: String, n: Int? = nil, size: String? = nil) { + public init(image: Data, fileName: String, mask: Data? = nil, maskFileName: String? = nil, prompt: String, n: Int? = nil, size: String? = nil) { self.image = image + self.fileName = fileName self.mask = mask + self.maskFileName = maskFileName self.prompt = prompt self.n = n self.size = size } } + +extension ImageEditsQuery: MultipartFormDataBodyEncodable { + func encode(boundary: String) -> Data { + let bodyBuilder = MultipartFormDataBodyBuilder(boundary: boundary, entries: [ + .file(paramName: "image", fileName: fileName, fileData: image, contentType: "image/png"), + .file(paramName: "mask", fileName: maskFileName, fileData: mask, contentType: "image/png"), + .string(paramName: "prompt", value: prompt), + .string(paramName: "n", value: n), + .string(paramName: "size", value: size) + ]) + return bodyBuilder.build() + } +} diff --git a/Sources/OpenAI/Public/Models/ImageVariationsQuery.swift b/Sources/OpenAI/Public/Models/ImageVariationsQuery.swift index 3b787e5d..35276a06 100644 --- a/Sources/OpenAI/Public/Models/ImageVariationsQuery.swift +++ b/Sources/OpenAI/Public/Models/ImageVariationsQuery.swift @@ -8,16 +8,29 @@ import Foundation public struct ImageVariationsQuery: Codable { - /// The image to edit. Must be a valid PNG file, less than 4MB, and square. If mask is not provided, image must have transparency, which will be used as the mask. - public let image: String + /// The image to edit. Must be a valid PNG file, less than 4MB, and square. + public let image: Data + public let fileName: String /// The number of images to generate. Must be between 1 and 10. public let n: Int? /// The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024. public let size: String? - public init(image: String, n: Int? = nil, size: String? = nil) { + public init(image: Data, fileName: String, n: Int? = nil, size: String? = nil) { self.image = image + self.fileName = fileName self.n = n self.size = size } } + +extension ImageVariationsQuery: MultipartFormDataBodyEncodable { + func encode(boundary: String) -> Data { + let bodyBuilder = MultipartFormDataBodyBuilder(boundary: boundary, entries: [ + .file(paramName: "image", fileName: fileName, fileData: image, contentType: "image/png"), + .string(paramName: "n", value: n), + .string(paramName: "size", value: size) + ]) + return bodyBuilder.build() + } +} From 6805dd48388150e1dacb2df412e39384e47d9dbc Mon Sep 17 00:00:00 2001 From: Aled Samuel Date: Tue, 25 Apr 2023 18:15:31 +0100 Subject: [PATCH 3/9] Fixed broken Image related unit tests --- Tests/OpenAITests/OpenAITests.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Tests/OpenAITests/OpenAITests.swift b/Tests/OpenAITests/OpenAITests.swift index 44a626ca..929c7baa 100644 --- a/Tests/OpenAITests/OpenAITests.swift +++ b/Tests/OpenAITests/OpenAITests.swift @@ -63,7 +63,8 @@ class OpenAITests: XCTestCase { } func testImageEdit() async throws { - let query = ImageEditsQuery(image: "@whitecat.png", prompt: "White cat with heterochromia sitting on the kitchen table with a bowl of food", n: 1, size: "1024x1024") +// let imageData = Data() + let query = ImageEditsQuery(image: Data(), fileName: "whitecat.png", prompt: "White cat with heterochromia sitting on the kitchen table with a bowl of food", n: 1, size: "1024x1024") let imagesResult = ImagesResult(created: 100, data: [ .init(url: "http://foo.bar") ]) @@ -73,7 +74,7 @@ class OpenAITests: XCTestCase { } func testImageEditError() async throws { - let query = ImageEditsQuery(image: "@whitecat.png", prompt: "White cat with heterochromia sitting on the kitchen table with a bowl of food", n: 1, size: "1024x1024") + let query = ImageEditsQuery(image: Data(), fileName: "whitecat.png", prompt: "White cat with heterochromia sitting on the kitchen table with a bowl of food", n: 1, size: "1024x1024") let inError = APIError(message: "foo", type: "bar", param: "baz", code: "100") self.stub(error: inError) @@ -82,7 +83,7 @@ class OpenAITests: XCTestCase { } func testImageVariation() async throws { - let query = ImageVariationsQuery(image: "@whitecat.png", n: 1, size: "1024x1024") + let query = ImageVariationsQuery(image: Data(), fileName: "whitecat.png", n: 1, size: "1024x1024") let imagesResult = ImagesResult(created: 100, data: [ .init(url: "http://foo.bar") ]) @@ -92,7 +93,7 @@ class OpenAITests: XCTestCase { } func testImageVariationError() async throws { - let query = ImageVariationsQuery(image: "@whitecat.png", n: 1, size: "1024x1024") + let query = ImageVariationsQuery(image: Data(), fileName: "whitecat.png", n: 1, size: "1024x1024") let inError = APIError(message: "foo", type: "bar", param: "baz", code: "100") self.stub(error: inError) From 9a18df4e4bd5384db8587c27ad5f911139f38d5a Mon Sep 17 00:00:00 2001 From: Aled Samuel Date: Tue, 25 Apr 2023 18:58:17 +0100 Subject: [PATCH 4/9] Updated readme --- README.md | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 26c0d091..af61f76b 100644 --- a/README.md +++ b/README.md @@ -315,9 +315,11 @@ Creates an edited or extended image given an original image and a prompt. ```swift public struct ImageEditsQuery: Codable { /// The image to edit. Must be a valid PNG file, less than 4MB, and square. If mask is not provided, image must have transparency, which will be used as the mask. - public let image: String + public let image: Data + public let fileName: String /// An additional image whose fully transparent areas (e.g. where alpha is zero) indicate where image should be edited. Must be a valid PNG file, less than 4MB, and have the same dimensions as image. - public let mask: String? + public let mask: Data? + public let maskFileName: String? /// A text description of the desired image(s). The maximum length is 1000 characters. public let prompt: String /// The number of images to generate. Must be between 1 and 10. @@ -334,10 +336,11 @@ Uses the ImagesResult response similarly to ImagesQuery. **Example** ```swift - let query = ImagesEditQuery(image: "@whitecat.png", prompt: "White cat with heterochromia sitting on the kitchen table with a bowl of food", n: 1, size: "1024x1024") - openAI.imageEdits(query: query) { result in - //Handle result here - } +let data = image.pngData() +let query = ImagesEditQuery(image: data, fileName: "whitecat.png", prompt: "White cat with heterochromia sitting on the kitchen table with a bowl of food", n: 1, size: "1024x1024") +openAI.imageEdits(query: query) { result in + //Handle result here +} //or let result = try await openAI.imageEdits(query: query) ``` @@ -351,7 +354,8 @@ Creates a variation of a given image. ```swift public struct ImageVariationsQuery: Codable { /// The image to edit. Must be a valid PNG file, less than 4MB, and square. If mask is not provided, image must have transparency, which will be used as the mask. - public let image: String + public let image: Data + public let fileName: String /// The number of images to generate. Must be between 1 and 10. public let n: Int? /// The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024. @@ -366,10 +370,11 @@ Uses the ImagesResult response similarly to ImagesQuery. **Example** ```swift - let query = ImagesVariationQuery(image: "@whitecat.png", n: 1, size: "1024x1024") - openAI.imageVariations(query: query) { result in - //Handle result here - } +let data = image.pngData() +let query = ImagesVariationQuery(image: data, fileName: "whitecat.png", n: 1, size: "1024x1024") +openAI.imageVariations(query: query) { result in + //Handle result here +} //or let result = try await openAI.imageVariations(query: query) ``` From 689283eca564f781738a747bcc0ddc6dc12befd5 Mon Sep 17 00:00:00 2001 From: ssasika Date: Tue, 3 Oct 2023 13:44:37 +0100 Subject: [PATCH 5/9] Added text chat input validation, enabling 'Send' button only for valid input. --- Demo/DemoChat/Sources/UI/DetailView.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Demo/DemoChat/Sources/UI/DetailView.swift b/Demo/DemoChat/Sources/UI/DetailView.swift index 55ff60af..9e2a07e9 100644 --- a/Demo/DemoChat/Sources/UI/DetailView.swift +++ b/Demo/DemoChat/Sources/UI/DetailView.swift @@ -176,6 +176,7 @@ struct DetailView: View { .frame(width: 24, height: 24) .padding(.trailing) } + .disabled(inputText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) } .padding(.bottom) } @@ -183,7 +184,12 @@ struct DetailView: View { private func tapSendMessage( scrollViewProxy: ScrollViewProxy ) { - sendMessage(inputText, selectedChatModel) + let message = inputText.trimmingCharacters(in: .whitespacesAndNewlines) + if message.isEmpty { + return + } + + sendMessage(message, selectedChatModel) inputText = "" // if let lastMessage = conversation.messages.last { From 80e916097b36281b68ab579e9e08cb39519bd472 Mon Sep 17 00:00:00 2001 From: Pallav Agarwal Date: Fri, 13 Oct 2023 00:26:22 -0400 Subject: [PATCH 6/9] Correctly handle partial JSON at the end of the chunk --- Sources/OpenAI/Private/StreamingSession.swift | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Sources/OpenAI/Private/StreamingSession.swift b/Sources/OpenAI/Private/StreamingSession.swift index 55902f09..e2d52e57 100644 --- a/Sources/OpenAI/Private/StreamingSession.swift +++ b/Sources/OpenAI/Private/StreamingSession.swift @@ -28,6 +28,8 @@ final class StreamingSession: NSObject, Identifiable, URLSe return session }() + private var prevChunkBuffer = "" + init(urlRequest: URLRequest) { self.urlRequest = urlRequest } @@ -47,14 +49,16 @@ final class StreamingSession: NSObject, Identifiable, URLSe onProcessingError?(self, StreamingError.unknownContent) return } - let jsonObjects = stringContent + let jsonObjects = "\(prevChunkBuffer)\(stringContent)" .components(separatedBy: "data:") .filter { $0.isEmpty == false } .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + prevChunkBuffer = "" + guard jsonObjects.isEmpty == false, jsonObjects.first != streamingCompletionMarker else { return } - jsonObjects.forEach { jsonContent in + jsonObjects.enumerated().forEach { (index, jsonContent) in guard jsonContent != streamingCompletionMarker else { return } @@ -77,7 +81,12 @@ final class StreamingSession: NSObject, Identifiable, URLSe let decoded = try JSONDecoder().decode(APIErrorResponse.self, from: jsonData) onProcessingError?(self, decoded) } catch { - onProcessingError?(self, apiError) + if index == jsonObjects.count - 1 { + // Chunk ends in a partial JSON + prevChunkBuffer = "data: \(jsonContent)" + } else { + onProcessingError?(self, apiError) + } } } } From e1a6d0b1641b78e924fb0b94969ab758787a0b9b Mon Sep 17 00:00:00 2001 From: Mason Dierkes Date: Tue, 24 Oct 2023 17:01:32 -0500 Subject: [PATCH 7/9] Typo in ReadME max_tokens should reflect the new name maxTokens --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a71debbb..c52bee5a 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,7 @@ struct CompletionsResult: Codable, Equatable { **Example** ```swift -let query = CompletionsQuery(model: .textDavinci_003, prompt: "What is 42?", temperature: 0, max_tokens: 100, top_p: 1, frequency_penalty: 0, presence_penalty: 0, stop: ["\\n"]) +let query = CompletionsQuery(model: .textDavinci_003, prompt: "What is 42?", temperature: 0, maxTokens: 100, topP: 1, frequencyPenalty: 0, presencePenalty: 0, stop: ["\\n"]) openAI.completions(query: query) { result in //Handle result here } From 7e22461629f6767894a7f3a253e150c332d451dc Mon Sep 17 00:00:00 2001 From: Ihor Makhnyk <146889658+ingvarus-bc@users.noreply.github.com> Date: Thu, 9 Nov 2023 18:56:18 +0200 Subject: [PATCH 8/9] Remove comment in OpenAITests.swift --- Tests/OpenAITests/OpenAITests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/OpenAITests/OpenAITests.swift b/Tests/OpenAITests/OpenAITests.swift index 929c7baa..2dce76f6 100644 --- a/Tests/OpenAITests/OpenAITests.swift +++ b/Tests/OpenAITests/OpenAITests.swift @@ -63,7 +63,6 @@ class OpenAITests: XCTestCase { } func testImageEdit() async throws { -// let imageData = Data() let query = ImageEditsQuery(image: Data(), fileName: "whitecat.png", prompt: "White cat with heterochromia sitting on the kitchen table with a bowl of food", n: 1, size: "1024x1024") let imagesResult = ImagesResult(created: 100, data: [ .init(url: "http://foo.bar") From 8f08006ae8391d8778a8228740d4616031f99762 Mon Sep 17 00:00:00 2001 From: Ihor Makhnyk Date: Mon, 13 Nov 2023 12:18:00 +0200 Subject: [PATCH 9/9] Move json processing to separate method --- Sources/OpenAI/Private/StreamingSession.swift | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/Sources/OpenAI/Private/StreamingSession.swift b/Sources/OpenAI/Private/StreamingSession.swift index e2d52e57..a69e46cf 100644 --- a/Sources/OpenAI/Private/StreamingSession.swift +++ b/Sources/OpenAI/Private/StreamingSession.swift @@ -28,7 +28,7 @@ final class StreamingSession: NSObject, Identifiable, URLSe return session }() - private var prevChunkBuffer = "" + private var previousChunkBuffer = "" init(urlRequest: URLRequest) { self.urlRequest = urlRequest @@ -49,11 +49,20 @@ final class StreamingSession: NSObject, Identifiable, URLSe onProcessingError?(self, StreamingError.unknownContent) return } - let jsonObjects = "\(prevChunkBuffer)\(stringContent)" + processJSON(from: stringContent) + } + +} + +extension StreamingSession { + + private func processJSON(from stringContent: String) { + let jsonObjects = "\(previousChunkBuffer)\(stringContent)" .components(separatedBy: "data:") .filter { $0.isEmpty == false } .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } - prevChunkBuffer = "" + + previousChunkBuffer = "" guard jsonObjects.isEmpty == false, jsonObjects.first != streamingCompletionMarker else { return @@ -82,8 +91,7 @@ final class StreamingSession: NSObject, Identifiable, URLSe onProcessingError?(self, decoded) } catch { if index == jsonObjects.count - 1 { - // Chunk ends in a partial JSON - prevChunkBuffer = "data: \(jsonContent)" + previousChunkBuffer = "data: \(jsonContent)" // Chunk ends in a partial JSON } else { onProcessingError?(self, apiError) } @@ -91,4 +99,5 @@ final class StreamingSession: NSObject, Identifiable, URLSe } } } + }