Skip to content

Commit

Permalink
Merge branch 'main' into feat/assistants-api-3
Browse files Browse the repository at this point in the history
  • Loading branch information
ingvarus-bc authored Feb 14, 2024
2 parents b789eed + 4b7b666 commit 956ab9b
Show file tree
Hide file tree
Showing 12 changed files with 248 additions and 113 deletions.
65 changes: 65 additions & 0 deletions Demo/DemoChat/Sources/Extensions/View.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// View.swift
//
//
// Created by James J Kalafus on 2024-02-03.
//

import SwiftUI

extension View {

@inlinable public func navigationTitle(_ titleKey: LocalizedStringKey, selectedModel: Binding<String>) -> some View {
self
.navigationTitle(titleKey)
.safeAreaInset(edge: .top) {
HStack {
Text(
"Model: \(selectedModel.wrappedValue)"
)
.font(.caption)
.foregroundColor(.secondary)
Spacer()
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
}
}

@inlinable public func modelSelect(selectedModel: Binding<String>, models: [String], showsModelSelectionSheet: Binding<Bool>, help: String) -> some View {
self
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showsModelSelectionSheet.wrappedValue.toggle()
}) {
Image(systemName: "cpu")
}
}
}
.confirmationDialog(
"Select model",
isPresented: showsModelSelectionSheet,
titleVisibility: .visible,
actions: {
ForEach(models, id: \.self) { (model: String) in
Button {
selectedModel.wrappedValue = model
} label: {
Text(model)
}
}

Button("Cancel", role: .cancel) {
showsModelSelectionSheet.wrappedValue = false
}
},
message: {
Text(
"View \(help) for details"
)
.font(.caption)
}
)
}
}
2 changes: 1 addition & 1 deletion Demo/DemoChat/Sources/UI/DetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct DetailView: View {
@State private var selectedChatModel: Model = .gpt4_0613
var availableAssistants: [Assistant]

private let availableChatModels: [Model] = [.gpt3_5Turbo0613, .gpt4_0613]
private static let availableChatModels: [Model] = [.gpt3_5Turbo, .gpt4]

let conversation: Conversation
let error: Error?
Expand Down
15 changes: 10 additions & 5 deletions Demo/DemoChat/Sources/UI/TextToSpeechView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ public struct TextToSpeechView: View {

@State private var prompt: String = ""
@State private var voice: AudioSpeechQuery.AudioSpeechVoice = .alloy
@State private var speed: Double = 1
@State private var speed: Double = AudioSpeechQuery.Speed.normal.rawValue
@State private var responseFormat: AudioSpeechQuery.AudioSpeechResponseFormat = .mp3

@State private var showsModelSelectionSheet = false
@State private var selectedSpeechModel: String = Model.tts_1

private static let availableSpeechModels: [String] = [Model.tts_1, Model.tts_1_hd]

public init(store: SpeechStore) {
self.store = store
}
Expand Down Expand Up @@ -56,7 +60,7 @@ public struct TextToSpeechView: View {
HStack {
Text("Speed: ")
Spacer()
Stepper(value: $speed, in: 0.25...4, step: 0.25) {
Stepper(value: $speed, in: AudioSpeechQuery.Speed.min.rawValue...AudioSpeechQuery.Speed.max.rawValue, step: 0.25) {
HStack {
Spacer()
Text("**\(String(format: "%.2f", speed))**")
Expand All @@ -79,7 +83,7 @@ public struct TextToSpeechView: View {
Section {
HStack {
Button("Create Speech") {
let query = AudioSpeechQuery(model: .tts_1,
let query = AudioSpeechQuery(model: selectedSpeechModel,
input: prompt,
voice: voice,
responseFormat: responseFormat,
Expand All @@ -93,6 +97,7 @@ public struct TextToSpeechView: View {
.disabled(prompt.replacingOccurrences(of: " ", with: "").isEmpty)
Spacer()
}
.modelSelect(selectedModel: $selectedSpeechModel, models: Self.availableSpeechModels, showsModelSelectionSheet: $showsModelSelectionSheet, help: "https://platform.openai.com/docs/models/tts")
}
if !$store.audioObjects.wrappedValue.isEmpty {
Section("Click to play, swipe to save:") {
Expand Down Expand Up @@ -129,7 +134,7 @@ public struct TextToSpeechView: View {
}
.listStyle(.insetGrouped)
.scrollDismissesKeyboard(.interactively)
.navigationTitle("Create Speech")
.navigationTitle("Create Speech", selectedModel: $selectedSpeechModel)
}
}

Expand Down
39 changes: 26 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ public struct AudioSpeechQuery: Codable, Equatable {
public let model: Model // tts-1 or tts-1-hd
public let input: String
public let voice: AudioSpeechVoice
public let response_format: AudioSpeechResponseFormat
public let responseFormat: AudioSpeechResponseFormat
public let speed: String? // Initializes with Double?
//...
}
Expand All @@ -570,13 +570,13 @@ public let audioData: Data?
**Example:**

```swift
let query = AudioSpeechQuery(model: .tts_1, input: "Hello, world!", voice: .alloy, response_format: .mp3, speed: 1.0)
let query = AudioSpeechQuery(model: .tts_1, input: "Hello, world!", voice: .alloy, responseFormat: .mp3, speed: 1.0)

openAI.audioCreateSpeech(query: query) { result in
// Handle response here
}
//or
let result = try await openAI.audioTranscriptions(query: query)
let result = try await openAI.audioCreateSpeech(query: query)
```
[OpenAI Create Speech – Documentation](https://platform.openai.com/docs/api-reference/audio/createSpeech)

Expand Down Expand Up @@ -800,16 +800,24 @@ Models are represented as a typealias `typealias Model = String`.

```swift
public extension Model {
static let gpt4_1106_preview = "gpt-4-1106-preview"
static let gpt4_vision_preview = "gpt-4-vision-preview"
static let gpt4_turbo_preview = "gpt-4-turbo-preview"
static let gpt4_vision_preview = "gpt-4-vision-preview"
static let gpt4_0125_preview = "gpt-4-0125-preview"
static let gpt4_1106_preview = "gpt-4-1106-preview"
static let gpt4 = "gpt-4"
static let gpt4_0613 = "gpt-4-0613"
static let gpt4_0314 = "gpt-4-0314"
static let gpt4_32k = "gpt-4-32k"
static let gpt4_32k_0613 = "gpt-4-32k-0613"
static let gpt4_32k_0314 = "gpt-4-32k-0314"

static let gpt3_5Turbo = "gpt-3.5-turbo"
static let gpt3_5Turbo_0125 = "gpt-3.5-turbo-0125"
static let gpt3_5Turbo_1106 = "gpt-3.5-turbo-1106"
static let gpt3_5Turbo0301 = "gpt-3.5-turbo-0301"

static let gpt3_5Turbo_0613 = "gpt-3.5-turbo-0613"
static let gpt3_5Turbo_0301 = "gpt-3.5-turbo-0301"
static let gpt3_5Turbo_16k = "gpt-3.5-turbo-16k"
static let gpt3_5Turbo_16k_0613 = "gpt-3.5-turbo-16k-0613"

static let textDavinci_003 = "text-davinci-003"
static let textDavinci_002 = "text-davinci-002"
Expand All @@ -820,7 +828,13 @@ public extension Model {
static let textDavinci_001 = "text-davinci-001"
static let codeDavinciEdit_001 = "code-davinci-edit-001"

static let tts_1 = "tts-1"
static let tts_1_hd = "tts-1-hd"

static let whisper_1 = "whisper-1"

static let dall_e_2 = "dall-e-2"
static let dall_e_3 = "dall-e-3"

static let davinci = "davinci"
static let curie = "curie"
Expand All @@ -831,22 +845,21 @@ public extension Model {
static let textSearchAda = "text-search-ada-doc-001"
static let textSearchBabbageDoc = "text-search-babbage-doc-001"
static let textSearchBabbageQuery001 = "text-search-babbage-query-001"
static let textEmbedding3 = "text-embedding-3-small"
static let textEmbedding3Large = "text-embedding-3-large"

static let textModerationStable = "text-moderation-stable"
static let textModerationLatest = "text-moderation-latest"
static let moderation = "text-moderation-001"

static let dall_e_2 = "dall-e-2"
static let dall_e_3 = "dall-e-3"
static let moderation = "text-moderation-007"
}
```

GPT-4 models are supported.

For example to use basic GPT-4 8K model pass `.gpt4` as a parameter.
As an example: To use the `gpt-4-turbo-preview` model, pass `.gpt4_turbo_preview` as the parameter to the `ChatQuery` init.

```swift
let query = ChatQuery(model: .gpt4, messages: [
let query = ChatQuery(model: .gpt4_turbo_preview, messages: [
.init(role: .system, content: "You are Librarian-GPT. You know everything about the books."),
.init(role: .user, content: "Who wrote Harry Potter?")
])
Expand Down
41 changes: 8 additions & 33 deletions Sources/OpenAI/OpenAI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ final public class OpenAI: OpenAIProtocol {
}

private let session: URLSessionProtocol
private var streamingSessions: [NSObject] = []
private var streamingSessions = ArrayWithThreadSafety<NSObject>()

public let configuration: Configuration

Expand Down Expand Up @@ -165,29 +165,16 @@ extension OpenAI {
timeoutInterval: configuration.timeoutInterval)
let task = session.dataTask(with: request) { data, _, error in
if let error = error {
completion(.failure(error))
return
return completion(.failure(error))
}
guard let data = data else {
completion(.failure(OpenAIError.emptyData))
return
return completion(.failure(OpenAIError.emptyData))
}

var apiError: Error? = nil
let decoder = JSONDecoder()
do {
let decoded = try JSONDecoder().decode(ResultType.self, from: data)
completion(.success(decoded))
completion(.success(try decoder.decode(ResultType.self, from: data)))
} catch {
apiError = error
}

if let apiError = apiError {
do {
let decoded = try JSONDecoder().decode(APIErrorResponse.self, from: data)
completion(.failure(decoded))
} catch {
completion(.failure(apiError))
}
completion(.failure((try? decoder.decode(APIErrorResponse.self, from: data)) ?? error))
}
}
task.resume()
Expand Down Expand Up @@ -227,25 +214,13 @@ extension OpenAI {

let task = session.dataTask(with: request) { data, _, error in
if let error = error {
completion(.failure(error))
return
return completion(.failure(error))
}
guard let data = data else {
completion(.failure(OpenAIError.emptyData))
return
return completion(.failure(OpenAIError.emptyData))
}

completion(.success(AudioSpeechResult(audioData: data)))
let apiError: Error? = nil

if let apiError = apiError {
do {
let decoded = try JSONDecoder().decode(APIErrorResponse.self, from: data)
completion(.failure(decoded))
} catch {
completion(.failure(apiError))
}
}
}
task.resume()
} catch {
Expand Down
31 changes: 13 additions & 18 deletions Sources/OpenAI/Private/StreamingSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,44 +57,39 @@ final class StreamingSession<ResultType: Codable>: NSObject, Identifiable, URLSe
extension StreamingSession {

private func processJSON(from stringContent: String) {
if stringContent.isEmpty {
return
}
let jsonObjects = "\(previousChunkBuffer)\(stringContent)"
.trimmingCharacters(in: .whitespacesAndNewlines)
.components(separatedBy: "data:")
.filter { $0.isEmpty == false }
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }

.filter { $0.isEmpty == false }

previousChunkBuffer = ""

guard jsonObjects.isEmpty == false, jsonObjects.first != streamingCompletionMarker else {
return
}
jsonObjects.enumerated().forEach { (index, jsonContent) in
guard jsonContent != streamingCompletionMarker else {
guard jsonContent != streamingCompletionMarker && !jsonContent.isEmpty else {
return
}
guard let jsonData = jsonContent.data(using: .utf8) else {
onProcessingError?(self, StreamingError.unknownContent)
return
}

var apiError: Error? = nil
let decoder = JSONDecoder()
do {
let decoder = JSONDecoder()
let object = try decoder.decode(ResultType.self, from: jsonData)
onReceiveContent?(self, object)
} catch {
apiError = error
}

if let apiError = apiError {
do {
let decoded = try JSONDecoder().decode(APIErrorResponse.self, from: jsonData)
if let decoded = try? decoder.decode(APIErrorResponse.self, from: jsonData) {
onProcessingError?(self, decoded)
} catch {
if index == jsonObjects.count - 1 {
previousChunkBuffer = "data: \(jsonContent)" // Chunk ends in a partial JSON
} else {
onProcessingError?(self, apiError)
}
} else if index == jsonObjects.count - 1 {
previousChunkBuffer = "data: \(jsonContent)" // Chunk ends in a partial JSON
} else {
onProcessingError?(self, error)
}
}
}
Expand Down
25 changes: 14 additions & 11 deletions Sources/OpenAI/Public/Models/AudioSpeechQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,7 @@ public struct AudioSpeechQuery: Codable, Equatable {
case responseFormat = "response_format"
case speed
}

private enum Constants {
static let normalSpeed = 1.0
static let maxSpeed = 4.0
static let minSpeed = 0.25
}


public init(model: Model, input: String, voice: AudioSpeechVoice, responseFormat: AudioSpeechResponseFormat = .mp3, speed: Double?) {
self.model = AudioSpeechQuery.validateSpeechModel(model)
self.speed = AudioSpeechQuery.normalizeSpeechSpeed(speed)
Expand All @@ -80,13 +74,22 @@ private extension AudioSpeechQuery {
}
return inputModel
}

}

public extension AudioSpeechQuery {

enum Speed: Double {
case normal = 1.0
case max = 4.0
case min = 0.25
}

static func normalizeSpeechSpeed(_ inputSpeed: Double?) -> String {
guard let inputSpeed else { return "\(Constants.normalSpeed)" }
let isSpeedOutOfBounds = inputSpeed >= Constants.maxSpeed && inputSpeed <= Constants.minSpeed
guard let inputSpeed else { return "\(Self.Speed.normal.rawValue)" }
let isSpeedOutOfBounds = inputSpeed <= Self.Speed.min.rawValue || Self.Speed.max.rawValue <= inputSpeed
guard !isSpeedOutOfBounds else {
print("[AudioSpeech] Speed value must be between 0.25 and 4.0. Setting value to closest valid.")
return inputSpeed < Constants.minSpeed ? "\(Constants.minSpeed)" : "\(Constants.maxSpeed)"
return inputSpeed < Self.Speed.min.rawValue ? "\(Self.Speed.min.rawValue)" : "\(Self.Speed.max.rawValue)"
}
return "\(inputSpeed)"
}
Expand Down
Loading

0 comments on commit 956ab9b

Please sign in to comment.