Skip to content

Commit

Permalink
Merge branch 'MacPaw:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
rawnly authored Nov 14, 2023
2 parents 9bd5370 + d9192df commit fcccad4
Show file tree
Hide file tree
Showing 12 changed files with 327 additions and 13 deletions.
8 changes: 7 additions & 1 deletion Demo/DemoChat/Sources/UI/DetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -176,14 +176,20 @@ struct DetailView: View {
.frame(width: 24, height: 24)
.padding(.trailing)
}
.disabled(inputText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}
.padding(.bottom)
}

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 {
Expand Down
81 changes: 80 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ This repository contains Swift community-maintained implementation over [OpenAI]
- [Chats](#chats)
- [Chats Streaming](#chats-streaming)
- [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)
Expand Down Expand Up @@ -129,7 +132,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
}
Expand Down Expand Up @@ -385,6 +388,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
Expand All @@ -409,6 +414,7 @@ struct ImagesResult: Codable, Equatable {
public let data: [URLResult]
}
```

**Example**

```swift
Expand All @@ -433,6 +439,79 @@ 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: 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: 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.
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 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)
```

### 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: 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?
}
```

**Response**

Uses the ImagesResult response similarly to ImagesQuery.

**Example**

```swift
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)
```

Review [Images Documentation](https://platform.openai.com/docs/api-reference/images) for more info.

### Audio
Expand Down
13 changes: 12 additions & 1 deletion Sources/OpenAI/OpenAI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ final public class OpenAI: OpenAIProtocol {
performRequest(request: JSONRequest<ImagesResult>(body: query, url: buildURL(path: .images)), completion: completion)
}

public func imageEdits(query: ImageEditsQuery, completion: @escaping (Result<ImagesResult, Error>) -> Void) {
performRequest(request: MultipartFormDataRequest<ImagesResult>(body: query, url: buildURL(path: .imageEdits)), completion: completion)
}

public func imageVariations(query: ImageVariationsQuery, completion: @escaping (Result<ImagesResult, Error>) -> Void) {
performRequest(request: MultipartFormDataRequest<ImagesResult>(body: query, url: buildURL(path: .imageVariations)), completion: completion)
}

public func embeddings(query: EmbeddingsQuery, completion: @escaping (Result<EmbeddingsResult, Error>) -> Void) {
performRequest(request: JSONRequest<EmbeddingsResult>(body: query, url: buildURL(path: .embeddings)), completion: completion)
}
Expand Down Expand Up @@ -180,7 +188,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"
Expand All @@ -190,6 +197,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
}
Expand Down
12 changes: 7 additions & 5 deletions Sources/OpenAI/Private/MultipartFormDataBodyBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion Sources/OpenAI/Private/MultipartFormDataEntry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?)
}
24 changes: 21 additions & 3 deletions Sources/OpenAI/Private/StreamingSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ final class StreamingSession<ResultType: Codable>: NSObject, Identifiable, URLSe
return session
}()

private var previousChunkBuffer = ""

init(urlRequest: URLRequest) {
self.urlRequest = urlRequest
}
Expand All @@ -47,14 +49,25 @@ final class StreamingSession<ResultType: Codable>: NSObject, Identifiable, URLSe
onProcessingError?(self, StreamingError.unknownContent)
return
}
let jsonObjects = 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) }

previousChunkBuffer = ""

guard jsonObjects.isEmpty == false, jsonObjects.first != streamingCompletionMarker else {
return
}
jsonObjects.forEach { jsonContent in
jsonObjects.enumerated().forEach { (index, jsonContent) in
guard jsonContent != streamingCompletionMarker else {
return
}
Expand All @@ -77,9 +90,14 @@ final class StreamingSession<ResultType: Codable>: NSObject, Identifiable, URLSe
let decoded = try JSONDecoder().decode(APIErrorResponse.self, from: jsonData)
onProcessingError?(self, decoded)
} catch {
onProcessingError?(self, apiError)
if index == jsonObjects.count - 1 {
previousChunkBuffer = "data: \(jsonContent)" // Chunk ends in a partial JSON
} else {
onProcessingError?(self, apiError)
}
}
}
}
}

}
46 changes: 46 additions & 0 deletions Sources/OpenAI/Public/Models/ImageEditsQuery.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// 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: 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: 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.
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: 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()
}
}
36 changes: 36 additions & 0 deletions Sources/OpenAI/Public/Models/ImageVariationsQuery.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// 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.
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: 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()
}
}
30 changes: 30 additions & 0 deletions Sources/OpenAI/Public/Protocols/OpenAIProtocol+Async.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,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
Expand Down
14 changes: 14 additions & 0 deletions Sources/OpenAI/Public/Protocols/OpenAIProtocol+Combine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,20 @@ public extension OpenAIProtocol {
}
.eraseToAnyPublisher()
}

func imageEdits(query: ImageEditsQuery) -> AnyPublisher<ImagesResult, Error> {
Future<ImagesResult, Error> {
imageEdits(query: query, completion: $0)
}
.eraseToAnyPublisher()
}

func imageVariations(query: ImageVariationsQuery) -> AnyPublisher<ImagesResult, Error> {
Future<ImagesResult, Error> {
imageVariations(query: query, completion: $0)
}
.eraseToAnyPublisher()
}

func embeddings(query: EmbeddingsQuery) -> AnyPublisher<EmbeddingsResult, Error> {
Future<EmbeddingsResult, Error> {
Expand Down
Loading

0 comments on commit fcccad4

Please sign in to comment.