├── .swiftlint.yml ├── ElevenSpeak ├── Application │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Secret.swift │ ├── AppDelegate.swift │ ├── SceneDelegate.swift │ └── Promt.swift ├── Models │ ├── Message.swift │ └── Conversation.swift ├── ElevenSpeak.entitlements ├── Environment │ ├── DateProvider.swift │ ├── IDProvider.swift │ └── ChatStore.swift ├── Presentation │ ├── ListView.swift │ ├── MainView.swift │ ├── ChatBubbleView.swift │ ├── ChatView.swift │ └── DetailView.swift ├── Info.plist └── Service │ ├── AudioPlayerService.swift │ ├── ElevenLabsService.swift │ ├── WhisperService.swift │ ├── AudioRecorderService.swift │ └── GPTService.swift ├── .swiftformat ├── ElevenSpeak.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── xcshareddata │ └── xcschemes │ │ └── ElevenSpeak.xcscheme └── project.pbxproj ├── README.md ├── LICENSE └── .gitignore /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ElevenSpeak/Application/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --header "{file}\nElevenSpeak. Created by Bogdan Bystritskiy." 2 | 3 | --disable unusedArguments, trailingCommas 4 | 5 | --swiftversion 5.8 -------------------------------------------------------------------------------- /ElevenSpeak.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ElevenSpeak/Application/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ElevenSpeak/Application/Secret.swift: -------------------------------------------------------------------------------- 1 | // Secret.swift 2 | // ElevenSpeak. Created by Bogdan Bystritskiy. 3 | 4 | enum Secret { 5 | static let whisperKey = "YOUR_TOKEN_HERE" 6 | static let gptKey = "YOUR_TOKEN_HERE" 7 | static let elevenlabsKey = "YOUR_TOKEN_HERE" 8 | } 9 | -------------------------------------------------------------------------------- /ElevenSpeak.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ElevenSpeak/Models/Message.swift: -------------------------------------------------------------------------------- 1 | // Message.swift 2 | // ElevenSpeak. Created by Bogdan Bystritskiy. 3 | 4 | import Foundation 5 | import OpenAI 6 | 7 | struct Message { 8 | var id: String 9 | var role: Chat.Role 10 | var content: String 11 | var createdAt: Date 12 | } 13 | 14 | extension Message: Equatable, Codable, Hashable, Identifiable {} 15 | -------------------------------------------------------------------------------- /ElevenSpeak/ElevenSpeak.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ElevenSpeak/Models/Conversation.swift: -------------------------------------------------------------------------------- 1 | // Conversation.swift 2 | // ElevenSpeak. Created by Bogdan Bystritskiy. 3 | 4 | import Foundation 5 | 6 | struct Conversation { 7 | init(id: String, messages: [Message] = []) { 8 | self.id = id 9 | self.messages = messages 10 | } 11 | 12 | typealias ID = String 13 | 14 | let id: String 15 | var messages: [Message] 16 | } 17 | 18 | extension Conversation: Equatable, Identifiable {} 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ElevenSpeak 2 | 3 | Your personal voice language customizable teacher. 4 | Based on: 5 | - Whisper (your speach-to-text) 6 | - Chat-GPT4 for semantic part 7 | - Eleven Labs (text-to-speach customizable) 8 | 9 | ### Key features: 10 | - choose your teacher's gender (male of female) 11 | - choose your teacher's age (young, middle aged, old) 12 | - choose your teacher's accent (American, British, African, Australian, Indian) 13 | - choose your teacher's accent strength (from 30% to 200%) 14 | -------------------------------------------------------------------------------- /ElevenSpeak/Environment/DateProvider.swift: -------------------------------------------------------------------------------- 1 | // DateProvider.swift 2 | // ElevenSpeak. Created by Bogdan Bystritskiy. 3 | 4 | import SwiftUI 5 | 6 | private struct DateProviderKey: EnvironmentKey { 7 | static let defaultValue: () -> Date = Date.init 8 | } 9 | 10 | public extension EnvironmentValues { 11 | var dateProviderValue: () -> Date { 12 | get { self[DateProviderKey.self] } 13 | set { self[DateProviderKey.self] = newValue } 14 | } 15 | } 16 | 17 | public extension View { 18 | func dateProviderValue(_ dateProviderValue: @escaping () -> Date) -> some View { 19 | environment(\.dateProviderValue, dateProviderValue) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ElevenSpeak/Environment/IDProvider.swift: -------------------------------------------------------------------------------- 1 | // IDProvider.swift 2 | // ElevenSpeak. Created by Bogdan Bystritskiy. 3 | 4 | import SwiftUI 5 | 6 | private struct IDProviderKey: EnvironmentKey { 7 | static let defaultValue: () -> String = { 8 | UUID().uuidString 9 | } 10 | } 11 | 12 | public extension EnvironmentValues { 13 | var idProviderValue: () -> String { 14 | get { self[IDProviderKey.self] } 15 | set { self[IDProviderKey.self] = newValue } 16 | } 17 | } 18 | 19 | public extension View { 20 | func idProviderValue(_ idProviderValue: @escaping () -> String) -> some View { 21 | environment(\.idProviderValue, idProviderValue) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ElevenSpeak/Application/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // AppDelegate.swift 2 | // ElevenSpeak. Created by Bogdan Bystritskiy. 3 | 4 | import UIKit 5 | 6 | @UIApplicationMain 7 | class AppDelegate: UIResponder, UIApplicationDelegate { 8 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 9 | true 10 | } 11 | 12 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 13 | UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ElevenSpeak/Presentation/ListView.swift: -------------------------------------------------------------------------------- 1 | // ListView.swift 2 | // ElevenSpeak. Created by Bogdan Bystritskiy. 3 | 4 | import SwiftUI 5 | 6 | struct ListView: View { 7 | @Binding var conversations: [Conversation] 8 | @Binding var selectedConversationId: Conversation.ID? 9 | 10 | var body: some View { 11 | List( 12 | $conversations, 13 | editActions: [.delete], 14 | selection: $selectedConversationId 15 | ) { $conversation in 16 | Text( 17 | conversation.messages.last?.content ?? "New Conversation" 18 | ) 19 | .lineLimit(2) 20 | } 21 | .navigationTitle("Conversations") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ElevenSpeak.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "elevenlabsswift", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/ArchieGoodwin/ElevenlabsSwift", 7 | "state" : { 8 | "revision" : "89edd5c3420dba7f1f7f63bb6239b7d275469343", 9 | "version" : "0.7.4" 10 | } 11 | }, 12 | { 13 | "identity" : "openai", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/MacPaw/OpenAI", 16 | "state" : { 17 | "revision" : "4da423fe637a628ebee8f214217a9e96060a5222", 18 | "version" : "0.2.1" 19 | } 20 | } 21 | ], 22 | "version" : 2 23 | } 24 | -------------------------------------------------------------------------------- /ElevenSpeak/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /ElevenSpeak/Service/AudioPlayerService.swift: -------------------------------------------------------------------------------- 1 | // AudioPlayerService.swift 2 | // ElevenSpeak. Created by Bogdan Bystritskiy. 3 | 4 | import AVFoundation 5 | 6 | class AudioPlayerService: NSObject, ObservableObject { 7 | private var audioPlayer: AVAudioPlayer? 8 | 9 | @Published var isPlaying = false 10 | @Published var audioFileURL: URL? 11 | 12 | func playRecording(url: URL) { 13 | do { 14 | audioPlayer = try AVAudioPlayer(contentsOf: url) 15 | audioPlayer?.play() 16 | isPlaying = true 17 | } catch { 18 | print("Failed to play recording") 19 | } 20 | } 21 | 22 | func stopPlaying() { 23 | audioPlayer?.stop() 24 | isPlaying = false 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ElevenSpeak/Presentation/MainView.swift: -------------------------------------------------------------------------------- 1 | // MainView.swift 2 | // ElevenSpeak. Created by Bogdan Bystritskiy. 3 | 4 | import OpenAI 5 | import SwiftUI 6 | 7 | struct MainView: View { 8 | @State private var selectedTab = 0 9 | 10 | @StateObject var chatStore: ChatStore 11 | 12 | @Environment(\.idProviderValue) var idProvider 13 | @Environment(\.dateProviderValue) var dateProvider 14 | 15 | init(idProvider: @escaping () -> String) { 16 | _chatStore = StateObject( 17 | wrappedValue: ChatStore( 18 | openAIClient: OpenAI(apiToken: Secret.gptKey), 19 | idProvider: idProvider 20 | ) 21 | ) 22 | } 23 | 24 | var body: some View { 25 | ChatView(store: chatStore) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ElevenSpeak/Service/ElevenLabsService.swift: -------------------------------------------------------------------------------- 1 | // ElevenLabsService.swift 2 | // ElevenSpeak. Created by Bogdan Bystritskiy. 3 | 4 | import ElevenlabsSwift 5 | import Foundation 6 | 7 | class ElevenLabsService: NSObject, ObservableObject { 8 | private let elevenApi: ElevenlabsSwift 9 | private let audioPlayerService = AudioPlayerService() 10 | 11 | override init() { 12 | elevenApi = ElevenlabsSwift(elevenLabsAPI: Secret.elevenlabsKey) 13 | } 14 | 15 | public func getAudio(from text: String) { 16 | Task { 17 | let voices = try await elevenApi.fetchVoices() 18 | if let voiceId = voices.first?.id { 19 | let url = try await elevenApi.textToSpeech(voice_id: voiceId, text: text) 20 | audioPlayerService.playRecording(url: url) 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ElevenSpeak/Application/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // SceneDelegate.swift 2 | // ElevenSpeak. Created by Bogdan Bystritskiy. 3 | 4 | import SwiftUI 5 | import UIKit 6 | 7 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 8 | var window: UIWindow? 9 | 10 | let idProvider: () -> String 11 | let dateProvider: () -> Date 12 | 13 | override init() { 14 | idProvider = { 15 | UUID().uuidString 16 | } 17 | dateProvider = Date.init 18 | } 19 | 20 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 21 | let rootView = MainView(idProvider: idProvider) 22 | 23 | if let windowScene = scene as? UIWindowScene { 24 | let window = UIWindow(windowScene: windowScene) 25 | window.rootViewController = UIHostingController(rootView: rootView) 26 | self.window = window 27 | window.makeKeyAndVisible() 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ElevenSpeak/Service/WhisperService.swift: -------------------------------------------------------------------------------- 1 | // WhisperService.swift 2 | // ElevenSpeak. Created by Bogdan Bystritskiy. 3 | 4 | import Foundation 5 | import OpenAI 6 | 7 | class WhisperService: NSObject, ObservableObject { 8 | @Published var isTranscribing: Bool = false 9 | 10 | private let openAI: OpenAI 11 | 12 | override init() { 13 | openAI = OpenAI(apiToken: Secret.whisperKey) 14 | } 15 | 16 | func transcribe(file: Data, fileName: String = "recording.m4a", completion: @escaping (_ answer: String) -> Void) { 17 | isTranscribing = true 18 | let query = AudioTranscriptionQuery(file: file, fileName: fileName, model: .whisper_1) 19 | openAI.audioTranscriptions(query: query) { result in 20 | switch result { 21 | case let .success(transcriptionResult): 22 | completion(transcriptionResult.text) 23 | case let .failure(error): 24 | completion(error.localizedDescription) 25 | } 26 | } 27 | isTranscribing = false 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Bogdan Bystritskiy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ElevenSpeak/Application/Promt.swift: -------------------------------------------------------------------------------- 1 | // Promt.swift 2 | // ElevenSpeak. Created by Bogdan Bystritskiy. 3 | 4 | // swiftlint:disable line_length 5 | enum Promt { 6 | static let teacher = 7 | """ 8 | You are my English teacher. Your main task is to teach me English and improve my level of English. Follow the following rules: 9 | 10 | - You ask questions in English, I answer in English. Use American English format. Build our communication in the format of a dialogue, questions can be either unrelated or in the format of a conversation on one topic. You can easily switch from topic to topic and change the format of the conversation. 11 | 12 | - Gradually, if I can cope with the answers, you need to increase the complexity of the questions. You should also make the topics of your questions varied and interesting, covering philosophy, politics, science and art. 13 | 14 | Let's start our English lesson! 15 | """ 16 | 17 | static let emoji = 18 | """ 19 | You a EmojiGPT. 20 | You receive a completely random message as input, your task is to characterize the meaning with one emoji. 21 | Just only one emoji, nothing else. 22 | """ 23 | } 24 | -------------------------------------------------------------------------------- /ElevenSpeak/Application/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ElevenSpeak/Service/AudioRecorderService.swift: -------------------------------------------------------------------------------- 1 | // AudioRecorderService.swift 2 | // ElevenSpeak. Created by Bogdan Bystritskiy. 3 | 4 | import AVFoundation 5 | 6 | // swiftlint:disable all 7 | class AudioRecorderService: NSObject, ObservableObject { 8 | var audioFileData: Data? 9 | @Published var isRecording = false 10 | 11 | private var audioRecorder: AVAudioRecorder? 12 | private var audioFilePath: String? 13 | 14 | func startRecording() { 15 | let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] 16 | let audioFileName = documentsDirectory.appendingPathComponent("recording.m4a") 17 | audioFilePath = audioFileName.path 18 | 19 | let settings = [ 20 | AVFormatIDKey: Int(kAudioFormatMPEG4AAC), 21 | AVSampleRateKey: 12000, 22 | AVNumberOfChannelsKey: 1, 23 | AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue, 24 | ] 25 | 26 | do { 27 | audioRecorder = try AVAudioRecorder(url: audioFileName, settings: settings) 28 | audioRecorder?.record() 29 | isRecording = true 30 | } catch { 31 | print("Failed to start recording") 32 | } 33 | } 34 | 35 | func stopRecording() { 36 | audioRecorder?.stop() 37 | isRecording = false 38 | 39 | let audioFileURL = URL(fileURLWithPath: audioFilePath!) 40 | audioFileData = try! Data(contentsOf: audioFileURL) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ElevenSpeak/Presentation/ChatBubbleView.swift: -------------------------------------------------------------------------------- 1 | // ChatBubbleView.swift 2 | // ElevenSpeak. Created by Bogdan Bystritskiy. 3 | 4 | import SwiftUI 5 | 6 | struct ChatBubbleView: View { 7 | let message: Message 8 | 9 | private var assistantBackgroundColor: Color { 10 | Color(uiColor: UIColor.systemGray5) 11 | } 12 | 13 | private var userForegroundColor: Color { 14 | Color(uiColor: .white) 15 | } 16 | 17 | private var userBackgroundColor: Color { 18 | Color(uiColor: .systemBlue) 19 | } 20 | 21 | var body: some View { 22 | HStack { 23 | switch message.role { 24 | case .assistant: 25 | Text(message.content) 26 | .padding(.horizontal, 16) 27 | .padding(.vertical, 12) 28 | .background(assistantBackgroundColor) 29 | .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) 30 | Spacer(minLength: 24) 31 | case .user: 32 | Spacer(minLength: 24) 33 | Text(message.content) 34 | .padding(.horizontal, 16) 35 | .padding(.vertical, 12) 36 | .foregroundColor(userForegroundColor) 37 | .background(userBackgroundColor) 38 | .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) 39 | case .system: 40 | EmptyView() 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ElevenSpeak/Service/GPTService.swift: -------------------------------------------------------------------------------- 1 | // GPTService.swift 2 | // ElevenSpeak. Created by Bogdan Bystritskiy. 3 | 4 | import Foundation 5 | import OpenAI 6 | 7 | class GPTService: NSObject, ObservableObject { 8 | private let openAI: OpenAI 9 | 10 | var messages: [Chat] = [.init(role: .system, content: Promt.teacher)] 11 | 12 | override init() { 13 | openAI = OpenAI(apiToken: Secret.gptKey) 14 | } 15 | 16 | public func getAnswer(prompt: String, completion: @escaping (_ answer: String) -> Void) { 17 | messages.append(.init(role: .user, content: prompt)) 18 | let query = ChatQuery( 19 | model: .gpt3_5Turbo, 20 | messages: messages 21 | ) 22 | Task { 23 | do { 24 | let result = try await openAI.chats(query: query) 25 | completion(result.choices.first?.message.content ?? "") 26 | } catch { 27 | completion("") 28 | } 29 | } 30 | } 31 | 32 | public func getEmoji(prompt: String, completion: @escaping (_ answer: String) -> Void) { 33 | let query = ChatQuery( 34 | model: .gpt3_5Turbo, 35 | messages: [ 36 | .init(role: .system, content: Promt.emoji), 37 | .init(role: .user, content: prompt) 38 | ] 39 | ) 40 | Task { 41 | do { 42 | let result = try await openAI.chats(query: query) 43 | completion(result.choices.first?.message.content ?? "") 44 | } catch { 45 | completion("") 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ElevenSpeak/Presentation/ChatView.swift: -------------------------------------------------------------------------------- 1 | // ChatView.swift 2 | // ElevenSpeak. Created by Bogdan Bystritskiy. 3 | 4 | import Combine 5 | import SwiftUI 6 | 7 | public struct ChatView: View { 8 | @ObservedObject var store: ChatStore 9 | 10 | @Environment(\.dateProviderValue) var dateProvider 11 | @Environment(\.idProviderValue) var idProvider 12 | 13 | public init(store: ChatStore) { 14 | self.store = store 15 | } 16 | 17 | public var body: some View { 18 | NavigationSplitView { 19 | ListView( 20 | conversations: $store.conversations, 21 | selectedConversationId: Binding( 22 | get: { 23 | store.selectedConversationID 24 | }, set: { newId in 25 | store.selectConversation(newId) 26 | } 27 | ) 28 | ) 29 | .toolbar { 30 | ToolbarItem( 31 | placement: .primaryAction 32 | ) { 33 | Button(action: { 34 | store.createConversation() 35 | }) { 36 | Image(systemName: "plus") 37 | } 38 | .buttonStyle(.borderedProminent) 39 | } 40 | } 41 | } detail: { 42 | if let conversation = store.selectedConversation { 43 | DetailView( 44 | conversation: conversation, 45 | error: store.conversationErrors[conversation.id], 46 | sendMessage: { message, selectedModel in 47 | Task { 48 | await store.sendMessage( 49 | Message( 50 | id: idProvider(), 51 | role: .user, 52 | content: message, 53 | createdAt: dateProvider() 54 | ), 55 | conversationId: conversation.id, 56 | model: selectedModel 57 | ) 58 | } 59 | } 60 | ) 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | ElevenSpeak/Application/Secret.swift 92 | -------------------------------------------------------------------------------- /ElevenSpeak.xcodeproj/xcshareddata/xcschemes/ElevenSpeak.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 44 | 50 | 51 | 52 | 53 | 59 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /ElevenSpeak/Presentation/DetailView.swift: -------------------------------------------------------------------------------- 1 | // DetailView.swift 2 | // ElevenSpeak. Created by Bogdan Bystritskiy. 3 | 4 | import OpenAI 5 | import SwiftUI 6 | 7 | struct DetailView: View { 8 | let conversation: Conversation 9 | let error: Error? 10 | let sendMessage: (String, Model) -> Void 11 | 12 | @ObservedObject var audioRecorderService = AudioRecorderService() 13 | @ObservedObject var whisperService = WhisperService() 14 | @ObservedObject var elevanLabsService = ElevenLabsService() 15 | 16 | private var fillColor: Color { 17 | Color(uiColor: UIColor.systemBackground) 18 | } 19 | 20 | private var strokeColor: Color { 21 | Color(uiColor: UIColor.systemGray5) 22 | } 23 | 24 | var body: some View { 25 | NavigationStack { 26 | ScrollViewReader { scrollViewProxy in 27 | VStack { 28 | List { 29 | ForEach(conversation.messages) { message in 30 | ChatBubbleView(message: message) 31 | } 32 | .listRowSeparator(.hidden) 33 | } 34 | .listStyle(.plain) 35 | .animation(.default, value: conversation.messages) 36 | .onChange(of: conversation) { newValue in 37 | if let lastMessage = newValue.messages.last { 38 | scrollViewProxy.scrollTo(lastMessage.id, anchor: .bottom) 39 | 40 | if lastMessage.role == .assistant { 41 | elevanLabsService.getAudio(from: lastMessage.content) 42 | } 43 | } 44 | } 45 | if let error { 46 | errorMessage(error: error) 47 | } 48 | inputBar(scrollViewProxy: scrollViewProxy) 49 | } 50 | .navigationTitle("Chat") 51 | } 52 | } 53 | } 54 | 55 | @ViewBuilder private func errorMessage(error: Error) -> some View { 56 | Text( 57 | error.localizedDescription 58 | ) 59 | .font(.caption) 60 | .foregroundColor(Color(uiColor: .systemRed)) 61 | .padding(.horizontal) 62 | } 63 | 64 | @ViewBuilder private func inputBar(scrollViewProxy: ScrollViewProxy) -> some View { 65 | HStack { 66 | Button(action: { 67 | withAnimation { 68 | didTapListeningButton() 69 | } 70 | }) { 71 | Image(systemName: "mic.fill") 72 | .resizable() 73 | .aspectRatio(contentMode: .fit) 74 | .frame(width: 24, height: 24) 75 | .foregroundColor(audioRecorderService.isRecording ? .red : .blue) 76 | } 77 | } 78 | .padding(.bottom) 79 | } 80 | 81 | private func didTapListeningButton() { 82 | if audioRecorderService.isRecording { 83 | stopListening() 84 | } else { 85 | startListening() 86 | } 87 | } 88 | 89 | private func startListening() { 90 | audioRecorderService.startRecording() 91 | } 92 | 93 | private func stopListening() { 94 | audioRecorderService.stopRecording() 95 | whisperService.transcribe(file: audioRecorderService.audioFileData!) { text in 96 | sendMessage(text, .gpt3_5Turbo) 97 | } 98 | } 99 | } 100 | 101 | struct DetailView_Previews: PreviewProvider { 102 | static var previews: some View { 103 | DetailView( 104 | conversation: Conversation( 105 | id: "1", 106 | messages: [ 107 | Message(id: "1", role: .assistant, content: "Hello, how can I help you today?", createdAt: Date(timeIntervalSinceReferenceDate: 0)), 108 | Message(id: "2", role: .user, content: "I need help with my subscription.", createdAt: Date(timeIntervalSinceReferenceDate: 100)), 109 | Message(id: "3", role: .assistant, content: "Sure, what seems to be the problem with your subscription?", createdAt: Date(timeIntervalSinceReferenceDate: 200)) 110 | ] 111 | ), 112 | error: nil, 113 | sendMessage: { _, _ in } 114 | ) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /ElevenSpeak/Environment/ChatStore.swift: -------------------------------------------------------------------------------- 1 | // ChatStore.swift 2 | // ElevenSpeak. Created by Bogdan Bystritskiy. 3 | 4 | import Combine 5 | import Foundation 6 | import OpenAI 7 | 8 | public final class ChatStore: ObservableObject { 9 | public var openAIClient: OpenAIProtocol 10 | let idProvider: () -> String 11 | 12 | @Published var conversations: [Conversation] = [] 13 | @Published var conversationErrors: [Conversation.ID: Error] = [:] 14 | @Published var selectedConversationID: Conversation.ID? 15 | 16 | var selectedConversation: Conversation? { 17 | selectedConversationID.flatMap { id in 18 | conversations.first { $0.id == id } 19 | } 20 | } 21 | 22 | var selectedConversationPublisher: AnyPublisher { 23 | $selectedConversationID.receive(on: RunLoop.main).map { id in 24 | self.conversations.first(where: { $0.id == id }) 25 | } 26 | .eraseToAnyPublisher() 27 | } 28 | 29 | public init( 30 | openAIClient: OpenAIProtocol, 31 | idProvider: @escaping () -> String 32 | ) { 33 | self.openAIClient = openAIClient 34 | self.idProvider = idProvider 35 | } 36 | 37 | // MARK: - Events 38 | 39 | func createConversation() { 40 | let conversation = Conversation(id: idProvider(), messages: []) 41 | conversations.append(conversation) 42 | } 43 | 44 | func selectConversation(_ conversationId: Conversation.ID?) { 45 | selectedConversationID = conversationId 46 | } 47 | 48 | func deleteConversation(_ conversationId: Conversation.ID) { 49 | conversations.removeAll(where: { $0.id == conversationId }) 50 | } 51 | 52 | @MainActor 53 | func sendMessage( 54 | _ message: Message, 55 | conversationId: Conversation.ID, 56 | model: Model 57 | ) async { 58 | guard let conversationIndex = conversations.firstIndex(where: { $0.id == conversationId }) else { 59 | return 60 | } 61 | conversations[conversationIndex].messages.append(message) 62 | 63 | await completeChat( 64 | conversationId: conversationId, 65 | model: model 66 | ) 67 | } 68 | 69 | @MainActor 70 | func completeChat( 71 | conversationId: Conversation.ID, 72 | model: Model 73 | ) async { 74 | guard let conversation = conversations.first(where: { $0.id == conversationId }) else { 75 | return 76 | } 77 | 78 | conversationErrors[conversationId] = nil 79 | 80 | do { 81 | guard let conversationIndex = conversations.firstIndex(where: { $0.id == conversationId }) else { 82 | return 83 | } 84 | 85 | let chatsStream: AsyncThrowingStream = openAIClient.chatsStream( 86 | query: ChatQuery( 87 | model: model, 88 | messages: conversation.messages.map { message in 89 | Chat(role: message.role, content: message.content) 90 | } 91 | ) 92 | ) 93 | 94 | for try await partialChatResult in chatsStream { 95 | for choice in partialChatResult.choices { 96 | let existingMessages = conversations[conversationIndex].messages 97 | let message = Message( 98 | id: partialChatResult.id, 99 | role: choice.delta.role ?? .assistant, 100 | content: choice.delta.content ?? "", 101 | createdAt: Date(timeIntervalSince1970: TimeInterval(partialChatResult.created)) 102 | ) 103 | if let existingMessageIndex = existingMessages.firstIndex(where: { $0.id == partialChatResult.id }) { 104 | // Meld into previous message 105 | let previousMessage = existingMessages[existingMessageIndex] 106 | let combinedMessage = Message( 107 | id: message.id, // id stays the same for different deltas 108 | role: message.role, 109 | content: previousMessage.content + message.content, 110 | createdAt: message.createdAt 111 | ) 112 | conversations[conversationIndex].messages[existingMessageIndex] = combinedMessage 113 | } else { 114 | conversations[conversationIndex].messages.append(message) 115 | } 116 | } 117 | } 118 | } catch { 119 | conversationErrors[conversationId] = error 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ElevenSpeak.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | FD541A022A2A0C7900F68C68 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FD541A012A2A0C7900F68C68 /* Assets.xcassets */; }; 11 | FD63869E2A2CABEA001EA164 /* DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6386902A2CABEA001EA164 /* DetailView.swift */; }; 12 | FD63869F2A2CABEA001EA164 /* ListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6386912A2CABEA001EA164 /* ListView.swift */; }; 13 | FD6386A12A2CABEA001EA164 /* DateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6386942A2CABEA001EA164 /* DateProvider.swift */; }; 14 | FD6386A22A2CABEA001EA164 /* IDProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6386952A2CABEA001EA164 /* IDProvider.swift */; }; 15 | FD6386A32A2CABEA001EA164 /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6386962A2CABEA001EA164 /* ChatView.swift */; }; 16 | FD6386A42A2CABEA001EA164 /* Conversation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6386982A2CABEA001EA164 /* Conversation.swift */; }; 17 | FD6386A52A2CABEA001EA164 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6386992A2CABEA001EA164 /* Message.swift */; }; 18 | FD6386A62A2CABEA001EA164 /* ChatStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD63869A2A2CABEA001EA164 /* ChatStore.swift */; }; 19 | FD6386A92A2CB15D001EA164 /* ChatBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6386A82A2CB15D001EA164 /* ChatBubbleView.swift */; }; 20 | FD80928F2A2B0958008EA8A2 /* AudioRecorderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD80928E2A2B0958008EA8A2 /* AudioRecorderService.swift */; }; 21 | FD81475F2A2B614800825C4A /* OpenAI in Frameworks */ = {isa = PBXBuildFile; productRef = FD81475E2A2B614800825C4A /* OpenAI */; }; 22 | FD8147612A2B62A500825C4A /* WhisperService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8147602A2B62A500825C4A /* WhisperService.swift */; }; 23 | FD8147632A2B6B3700825C4A /* Secret.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8147622A2B6B3700825C4A /* Secret.swift */; }; 24 | FD8147652A2B730E00825C4A /* GPTService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8147642A2B730E00825C4A /* GPTService.swift */; }; 25 | FD8147672A2B78F200825C4A /* Promt.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8147662A2B78F200825C4A /* Promt.swift */; }; 26 | FD81476A2A2B7C1000825C4A /* ElevenlabsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = FD8147692A2B7C1000825C4A /* ElevenlabsSwift */; }; 27 | FD81476C2A2B7C1900825C4A /* ElevenLabsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD81476B2A2B7C1900825C4A /* ElevenLabsService.swift */; }; 28 | FD81476E2A2BA2F800825C4A /* AudioPlayerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD81476D2A2BA2F800825C4A /* AudioPlayerService.swift */; }; 29 | FDAFAF472A2C7B3D005CE791 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAFAF462A2C7B3D005CE791 /* MainView.swift */; }; 30 | FDAFAF492A2C8197005CE791 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAFAF482A2C8197005CE791 /* AppDelegate.swift */; }; 31 | FDAFAF4B2A2C81A1005CE791 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAFAF4A2A2C81A1005CE791 /* SceneDelegate.swift */; }; 32 | /* End PBXBuildFile section */ 33 | 34 | /* Begin PBXFileReference section */ 35 | FD5419FA2A2A0C7800F68C68 /* ElevenSpeak.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ElevenSpeak.app; sourceTree = BUILT_PRODUCTS_DIR; }; 36 | FD541A012A2A0C7900F68C68 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 37 | FD541A062A2A0C7900F68C68 /* ElevenSpeak.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ElevenSpeak.entitlements; sourceTree = ""; }; 38 | FD6386902A2CABEA001EA164 /* DetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailView.swift; sourceTree = ""; }; 39 | FD6386912A2CABEA001EA164 /* ListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListView.swift; sourceTree = ""; }; 40 | FD6386942A2CABEA001EA164 /* DateProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateProvider.swift; sourceTree = ""; }; 41 | FD6386952A2CABEA001EA164 /* IDProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IDProvider.swift; sourceTree = ""; }; 42 | FD6386962A2CABEA001EA164 /* ChatView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = ""; }; 43 | FD6386982A2CABEA001EA164 /* Conversation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Conversation.swift; sourceTree = ""; }; 44 | FD6386992A2CABEA001EA164 /* Message.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; 45 | FD63869A2A2CABEA001EA164 /* ChatStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatStore.swift; sourceTree = ""; }; 46 | FD6386A82A2CB15D001EA164 /* ChatBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBubbleView.swift; sourceTree = ""; }; 47 | FD80928E2A2B0958008EA8A2 /* AudioRecorderService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderService.swift; sourceTree = ""; }; 48 | FD8147602A2B62A500825C4A /* WhisperService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhisperService.swift; sourceTree = ""; }; 49 | FD8147622A2B6B3700825C4A /* Secret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secret.swift; sourceTree = ""; }; 50 | FD8147642A2B730E00825C4A /* GPTService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GPTService.swift; sourceTree = ""; }; 51 | FD8147662A2B78F200825C4A /* Promt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Promt.swift; sourceTree = ""; }; 52 | FD81476B2A2B7C1900825C4A /* ElevenLabsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElevenLabsService.swift; sourceTree = ""; }; 53 | FD81476D2A2BA2F800825C4A /* AudioPlayerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerService.swift; sourceTree = ""; }; 54 | FDAFAF462A2C7B3D005CE791 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; 55 | FDAFAF482A2C8197005CE791 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 56 | FDAFAF4A2A2C81A1005CE791 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 57 | /* End PBXFileReference section */ 58 | 59 | /* Begin PBXFrameworksBuildPhase section */ 60 | FD5419F72A2A0C7800F68C68 /* Frameworks */ = { 61 | isa = PBXFrameworksBuildPhase; 62 | buildActionMask = 2147483647; 63 | files = ( 64 | FD81475F2A2B614800825C4A /* OpenAI in Frameworks */, 65 | FD81476A2A2B7C1000825C4A /* ElevenlabsSwift in Frameworks */, 66 | ); 67 | runOnlyForDeploymentPostprocessing = 0; 68 | }; 69 | /* End PBXFrameworksBuildPhase section */ 70 | 71 | /* Begin PBXGroup section */ 72 | FD0A0FB42A2A2B65004FDEF9 /* Frameworks */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | ); 76 | name = Frameworks; 77 | sourceTree = ""; 78 | }; 79 | FD5419F12A2A0C7800F68C68 = { 80 | isa = PBXGroup; 81 | children = ( 82 | FD5419FC2A2A0C7800F68C68 /* ElevenSpeak */, 83 | FD5419FB2A2A0C7800F68C68 /* Products */, 84 | FD0A0FB42A2A2B65004FDEF9 /* Frameworks */, 85 | ); 86 | sourceTree = ""; 87 | }; 88 | FD5419FB2A2A0C7800F68C68 /* Products */ = { 89 | isa = PBXGroup; 90 | children = ( 91 | FD5419FA2A2A0C7800F68C68 /* ElevenSpeak.app */, 92 | ); 93 | name = Products; 94 | sourceTree = ""; 95 | }; 96 | FD5419FC2A2A0C7800F68C68 /* ElevenSpeak */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | FD541A1E2A2A1C9400F68C68 /* Application */, 100 | FD541A222A2A1CB500F68C68 /* Service */, 101 | FD6386972A2CABEA001EA164 /* Models */, 102 | FD6386932A2CABEA001EA164 /* Environment */, 103 | FD541A212A2A1CAD00F68C68 /* Presentation */, 104 | FD541A062A2A0C7900F68C68 /* ElevenSpeak.entitlements */, 105 | ); 106 | path = ElevenSpeak; 107 | sourceTree = ""; 108 | }; 109 | FD541A1E2A2A1C9400F68C68 /* Application */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | FD541A012A2A0C7900F68C68 /* Assets.xcassets */, 113 | FD8147622A2B6B3700825C4A /* Secret.swift */, 114 | FD8147662A2B78F200825C4A /* Promt.swift */, 115 | FDAFAF482A2C8197005CE791 /* AppDelegate.swift */, 116 | FDAFAF4A2A2C81A1005CE791 /* SceneDelegate.swift */, 117 | ); 118 | path = Application; 119 | sourceTree = ""; 120 | }; 121 | FD541A212A2A1CAD00F68C68 /* Presentation */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | FDAFAF462A2C7B3D005CE791 /* MainView.swift */, 125 | FD6386962A2CABEA001EA164 /* ChatView.swift */, 126 | FD6386912A2CABEA001EA164 /* ListView.swift */, 127 | FD6386902A2CABEA001EA164 /* DetailView.swift */, 128 | FD6386A82A2CB15D001EA164 /* ChatBubbleView.swift */, 129 | ); 130 | path = Presentation; 131 | sourceTree = ""; 132 | }; 133 | FD541A222A2A1CB500F68C68 /* Service */ = { 134 | isa = PBXGroup; 135 | children = ( 136 | FD80928E2A2B0958008EA8A2 /* AudioRecorderService.swift */, 137 | FD81476D2A2BA2F800825C4A /* AudioPlayerService.swift */, 138 | FD8147602A2B62A500825C4A /* WhisperService.swift */, 139 | FD8147642A2B730E00825C4A /* GPTService.swift */, 140 | FD81476B2A2B7C1900825C4A /* ElevenLabsService.swift */, 141 | ); 142 | path = Service; 143 | sourceTree = ""; 144 | }; 145 | FD6386932A2CABEA001EA164 /* Environment */ = { 146 | isa = PBXGroup; 147 | children = ( 148 | FD63869A2A2CABEA001EA164 /* ChatStore.swift */, 149 | FD6386942A2CABEA001EA164 /* DateProvider.swift */, 150 | FD6386952A2CABEA001EA164 /* IDProvider.swift */, 151 | ); 152 | path = Environment; 153 | sourceTree = ""; 154 | }; 155 | FD6386972A2CABEA001EA164 /* Models */ = { 156 | isa = PBXGroup; 157 | children = ( 158 | FD6386982A2CABEA001EA164 /* Conversation.swift */, 159 | FD6386992A2CABEA001EA164 /* Message.swift */, 160 | ); 161 | path = Models; 162 | sourceTree = ""; 163 | }; 164 | /* End PBXGroup section */ 165 | 166 | /* Begin PBXNativeTarget section */ 167 | FD5419F92A2A0C7800F68C68 /* ElevenSpeak */ = { 168 | isa = PBXNativeTarget; 169 | buildConfigurationList = FD541A092A2A0C7900F68C68 /* Build configuration list for PBXNativeTarget "ElevenSpeak" */; 170 | buildPhases = ( 171 | FD5419F62A2A0C7800F68C68 /* Sources */, 172 | FD5419F72A2A0C7800F68C68 /* Frameworks */, 173 | FD5419F82A2A0C7800F68C68 /* Resources */, 174 | FD541A1C2A2A175B00F68C68 /* SwiftLint */, 175 | FD541A1D2A2A179200F68C68 /* SwiftFormat */, 176 | ); 177 | buildRules = ( 178 | ); 179 | dependencies = ( 180 | ); 181 | name = ElevenSpeak; 182 | packageProductDependencies = ( 183 | FD81475E2A2B614800825C4A /* OpenAI */, 184 | FD8147692A2B7C1000825C4A /* ElevenlabsSwift */, 185 | ); 186 | productName = ElevenSpeak; 187 | productReference = FD5419FA2A2A0C7800F68C68 /* ElevenSpeak.app */; 188 | productType = "com.apple.product-type.application"; 189 | }; 190 | /* End PBXNativeTarget section */ 191 | 192 | /* Begin PBXProject section */ 193 | FD5419F22A2A0C7800F68C68 /* Project object */ = { 194 | isa = PBXProject; 195 | attributes = { 196 | BuildIndependentTargetsInParallel = 1; 197 | LastSwiftUpdateCheck = 1430; 198 | LastUpgradeCheck = 1430; 199 | TargetAttributes = { 200 | FD5419F92A2A0C7800F68C68 = { 201 | CreatedOnToolsVersion = 14.3; 202 | }; 203 | }; 204 | }; 205 | buildConfigurationList = FD5419F52A2A0C7800F68C68 /* Build configuration list for PBXProject "ElevenSpeak" */; 206 | compatibilityVersion = "Xcode 14.0"; 207 | developmentRegion = en; 208 | hasScannedForEncodings = 0; 209 | knownRegions = ( 210 | en, 211 | Base, 212 | ); 213 | mainGroup = FD5419F12A2A0C7800F68C68; 214 | packageReferences = ( 215 | FD81475D2A2B614800825C4A /* XCRemoteSwiftPackageReference "OpenAI" */, 216 | FD8147682A2B7C1000825C4A /* XCRemoteSwiftPackageReference "ElevenlabsSwift" */, 217 | ); 218 | productRefGroup = FD5419FB2A2A0C7800F68C68 /* Products */; 219 | projectDirPath = ""; 220 | projectRoot = ""; 221 | targets = ( 222 | FD5419F92A2A0C7800F68C68 /* ElevenSpeak */, 223 | ); 224 | }; 225 | /* End PBXProject section */ 226 | 227 | /* Begin PBXResourcesBuildPhase section */ 228 | FD5419F82A2A0C7800F68C68 /* Resources */ = { 229 | isa = PBXResourcesBuildPhase; 230 | buildActionMask = 2147483647; 231 | files = ( 232 | FD541A022A2A0C7900F68C68 /* Assets.xcassets in Resources */, 233 | ); 234 | runOnlyForDeploymentPostprocessing = 0; 235 | }; 236 | /* End PBXResourcesBuildPhase section */ 237 | 238 | /* Begin PBXShellScriptBuildPhase section */ 239 | FD541A1C2A2A175B00F68C68 /* SwiftLint */ = { 240 | isa = PBXShellScriptBuildPhase; 241 | alwaysOutOfDate = 1; 242 | buildActionMask = 2147483647; 243 | files = ( 244 | ); 245 | inputFileListPaths = ( 246 | ); 247 | inputPaths = ( 248 | ); 249 | name = SwiftLint; 250 | outputFileListPaths = ( 251 | ); 252 | outputPaths = ( 253 | ); 254 | runOnlyForDeploymentPostprocessing = 0; 255 | shellPath = /bin/sh; 256 | shellScript = "if [[ \"$(uname -m)\" == arm64 ]]; then\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\n\nif which swiftlint > /dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; 257 | }; 258 | FD541A1D2A2A179200F68C68 /* SwiftFormat */ = { 259 | isa = PBXShellScriptBuildPhase; 260 | alwaysOutOfDate = 1; 261 | buildActionMask = 2147483647; 262 | files = ( 263 | ); 264 | inputFileListPaths = ( 265 | ); 266 | inputPaths = ( 267 | ); 268 | name = SwiftFormat; 269 | outputFileListPaths = ( 270 | ); 271 | outputPaths = ( 272 | ); 273 | runOnlyForDeploymentPostprocessing = 0; 274 | shellPath = /bin/sh; 275 | shellScript = "if which swiftformat >/dev/null; then\n swiftformat .\nelse\n echo \"warning: SwiftFormat not installed, download from https://github.com/nicklockwood/SwiftFormat\"\nfi\n"; 276 | }; 277 | /* End PBXShellScriptBuildPhase section */ 278 | 279 | /* Begin PBXSourcesBuildPhase section */ 280 | FD5419F62A2A0C7800F68C68 /* Sources */ = { 281 | isa = PBXSourcesBuildPhase; 282 | buildActionMask = 2147483647; 283 | files = ( 284 | FD6386A42A2CABEA001EA164 /* Conversation.swift in Sources */, 285 | FD8147672A2B78F200825C4A /* Promt.swift in Sources */, 286 | FD63869F2A2CABEA001EA164 /* ListView.swift in Sources */, 287 | FD6386A32A2CABEA001EA164 /* ChatView.swift in Sources */, 288 | FD63869E2A2CABEA001EA164 /* DetailView.swift in Sources */, 289 | FD6386A62A2CABEA001EA164 /* ChatStore.swift in Sources */, 290 | FDAFAF4B2A2C81A1005CE791 /* SceneDelegate.swift in Sources */, 291 | FDAFAF472A2C7B3D005CE791 /* MainView.swift in Sources */, 292 | FD6386A52A2CABEA001EA164 /* Message.swift in Sources */, 293 | FD80928F2A2B0958008EA8A2 /* AudioRecorderService.swift in Sources */, 294 | FD6386A12A2CABEA001EA164 /* DateProvider.swift in Sources */, 295 | FD8147632A2B6B3700825C4A /* Secret.swift in Sources */, 296 | FD8147612A2B62A500825C4A /* WhisperService.swift in Sources */, 297 | FD81476C2A2B7C1900825C4A /* ElevenLabsService.swift in Sources */, 298 | FDAFAF492A2C8197005CE791 /* AppDelegate.swift in Sources */, 299 | FD6386A92A2CB15D001EA164 /* ChatBubbleView.swift in Sources */, 300 | FD81476E2A2BA2F800825C4A /* AudioPlayerService.swift in Sources */, 301 | FD6386A22A2CABEA001EA164 /* IDProvider.swift in Sources */, 302 | FD8147652A2B730E00825C4A /* GPTService.swift in Sources */, 303 | ); 304 | runOnlyForDeploymentPostprocessing = 0; 305 | }; 306 | /* End PBXSourcesBuildPhase section */ 307 | 308 | /* Begin XCBuildConfiguration section */ 309 | FD541A072A2A0C7900F68C68 /* Debug */ = { 310 | isa = XCBuildConfiguration; 311 | buildSettings = { 312 | ALWAYS_SEARCH_USER_PATHS = NO; 313 | CLANG_ANALYZER_NONNULL = YES; 314 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 315 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 316 | CLANG_ENABLE_MODULES = YES; 317 | CLANG_ENABLE_OBJC_ARC = YES; 318 | CLANG_ENABLE_OBJC_WEAK = YES; 319 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 320 | CLANG_WARN_BOOL_CONVERSION = YES; 321 | CLANG_WARN_COMMA = YES; 322 | CLANG_WARN_CONSTANT_CONVERSION = YES; 323 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 324 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 325 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 326 | CLANG_WARN_EMPTY_BODY = YES; 327 | CLANG_WARN_ENUM_CONVERSION = YES; 328 | CLANG_WARN_INFINITE_RECURSION = YES; 329 | CLANG_WARN_INT_CONVERSION = YES; 330 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 331 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 332 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 333 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 334 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 335 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 336 | CLANG_WARN_STRICT_PROTOTYPES = YES; 337 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 338 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 339 | CLANG_WARN_UNREACHABLE_CODE = YES; 340 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 341 | COPY_PHASE_STRIP = NO; 342 | DEBUG_INFORMATION_FORMAT = dwarf; 343 | ENABLE_STRICT_OBJC_MSGSEND = YES; 344 | ENABLE_TESTABILITY = YES; 345 | GCC_C_LANGUAGE_STANDARD = gnu11; 346 | GCC_DYNAMIC_NO_PIC = NO; 347 | GCC_NO_COMMON_BLOCKS = YES; 348 | GCC_OPTIMIZATION_LEVEL = 0; 349 | GCC_PREPROCESSOR_DEFINITIONS = ( 350 | "DEBUG=1", 351 | "$(inherited)", 352 | ); 353 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 354 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 355 | GCC_WARN_UNDECLARED_SELECTOR = YES; 356 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 357 | GCC_WARN_UNUSED_FUNCTION = YES; 358 | GCC_WARN_UNUSED_VARIABLE = YES; 359 | MACOSX_DEPLOYMENT_TARGET = 13.3; 360 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 361 | MTL_FAST_MATH = YES; 362 | ONLY_ACTIVE_ARCH = YES; 363 | SDKROOT = macosx; 364 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 365 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 366 | }; 367 | name = Debug; 368 | }; 369 | FD541A082A2A0C7900F68C68 /* Release */ = { 370 | isa = XCBuildConfiguration; 371 | buildSettings = { 372 | ALWAYS_SEARCH_USER_PATHS = NO; 373 | CLANG_ANALYZER_NONNULL = YES; 374 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 375 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 376 | CLANG_ENABLE_MODULES = YES; 377 | CLANG_ENABLE_OBJC_ARC = YES; 378 | CLANG_ENABLE_OBJC_WEAK = YES; 379 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 380 | CLANG_WARN_BOOL_CONVERSION = YES; 381 | CLANG_WARN_COMMA = YES; 382 | CLANG_WARN_CONSTANT_CONVERSION = YES; 383 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 384 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 385 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 386 | CLANG_WARN_EMPTY_BODY = YES; 387 | CLANG_WARN_ENUM_CONVERSION = YES; 388 | CLANG_WARN_INFINITE_RECURSION = YES; 389 | CLANG_WARN_INT_CONVERSION = YES; 390 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 391 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 392 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 393 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 394 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 395 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 396 | CLANG_WARN_STRICT_PROTOTYPES = YES; 397 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 398 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 399 | CLANG_WARN_UNREACHABLE_CODE = YES; 400 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 401 | COPY_PHASE_STRIP = NO; 402 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 403 | ENABLE_NS_ASSERTIONS = NO; 404 | ENABLE_STRICT_OBJC_MSGSEND = YES; 405 | GCC_C_LANGUAGE_STANDARD = gnu11; 406 | GCC_NO_COMMON_BLOCKS = YES; 407 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 408 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 409 | GCC_WARN_UNDECLARED_SELECTOR = YES; 410 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 411 | GCC_WARN_UNUSED_FUNCTION = YES; 412 | GCC_WARN_UNUSED_VARIABLE = YES; 413 | MACOSX_DEPLOYMENT_TARGET = 13.3; 414 | MTL_ENABLE_DEBUG_INFO = NO; 415 | MTL_FAST_MATH = YES; 416 | SDKROOT = macosx; 417 | SWIFT_COMPILATION_MODE = wholemodule; 418 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 419 | }; 420 | name = Release; 421 | }; 422 | FD541A0A2A2A0C7900F68C68 /* Debug */ = { 423 | isa = XCBuildConfiguration; 424 | buildSettings = { 425 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 426 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 427 | CODE_SIGN_ENTITLEMENTS = ElevenSpeak/ElevenSpeak.entitlements; 428 | CODE_SIGN_STYLE = Automatic; 429 | COMBINE_HIDPI_IMAGES = YES; 430 | CURRENT_PROJECT_VERSION = 1; 431 | DEVELOPMENT_TEAM = F7AZ625QQ5; 432 | ENABLE_HARDENED_RUNTIME = YES; 433 | ENABLE_PREVIEWS = YES; 434 | GENERATE_INFOPLIST_FILE = YES; 435 | INFOPLIST_FILE = ElevenSpeak/Info.plist; 436 | INFOPLIST_KEY_CFBundleDisplayName = ElevenSpeak; 437 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; 438 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 439 | INFOPLIST_KEY_NSMicrophoneUsageDescription = "This app uses the microphone to transcribe your speech."; 440 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; 441 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 442 | LD_RUNPATH_SEARCH_PATHS = ( 443 | "$(inherited)", 444 | "@executable_path/../Frameworks", 445 | ); 446 | MACOSX_DEPLOYMENT_TARGET = 13.0; 447 | MARKETING_VERSION = 1.0; 448 | PRODUCT_BUNDLE_IDENTIFIER = com.bystritskiy.ElevenSpeak; 449 | PRODUCT_NAME = "$(TARGET_NAME)"; 450 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 451 | SUPPORTS_MACCATALYST = NO; 452 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 453 | SWIFT_EMIT_LOC_STRINGS = YES; 454 | SWIFT_VERSION = 5.0; 455 | TARGETED_DEVICE_FAMILY = "1,2"; 456 | }; 457 | name = Debug; 458 | }; 459 | FD541A0B2A2A0C7900F68C68 /* Release */ = { 460 | isa = XCBuildConfiguration; 461 | buildSettings = { 462 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 463 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 464 | CODE_SIGN_ENTITLEMENTS = ElevenSpeak/ElevenSpeak.entitlements; 465 | CODE_SIGN_STYLE = Automatic; 466 | COMBINE_HIDPI_IMAGES = YES; 467 | CURRENT_PROJECT_VERSION = 1; 468 | DEVELOPMENT_TEAM = F7AZ625QQ5; 469 | ENABLE_HARDENED_RUNTIME = YES; 470 | ENABLE_PREVIEWS = YES; 471 | GENERATE_INFOPLIST_FILE = YES; 472 | INFOPLIST_FILE = ElevenSpeak/Info.plist; 473 | INFOPLIST_KEY_CFBundleDisplayName = ElevenSpeak; 474 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; 475 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 476 | INFOPLIST_KEY_NSMicrophoneUsageDescription = "This app uses the microphone to transcribe your speech."; 477 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; 478 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 479 | LD_RUNPATH_SEARCH_PATHS = ( 480 | "$(inherited)", 481 | "@executable_path/../Frameworks", 482 | ); 483 | MACOSX_DEPLOYMENT_TARGET = 13.0; 484 | MARKETING_VERSION = 1.0; 485 | PRODUCT_BUNDLE_IDENTIFIER = com.bystritskiy.ElevenSpeak; 486 | PRODUCT_NAME = "$(TARGET_NAME)"; 487 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 488 | SUPPORTS_MACCATALYST = NO; 489 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 490 | SWIFT_EMIT_LOC_STRINGS = YES; 491 | SWIFT_VERSION = 5.0; 492 | TARGETED_DEVICE_FAMILY = "1,2"; 493 | }; 494 | name = Release; 495 | }; 496 | /* End XCBuildConfiguration section */ 497 | 498 | /* Begin XCConfigurationList section */ 499 | FD5419F52A2A0C7800F68C68 /* Build configuration list for PBXProject "ElevenSpeak" */ = { 500 | isa = XCConfigurationList; 501 | buildConfigurations = ( 502 | FD541A072A2A0C7900F68C68 /* Debug */, 503 | FD541A082A2A0C7900F68C68 /* Release */, 504 | ); 505 | defaultConfigurationIsVisible = 0; 506 | defaultConfigurationName = Release; 507 | }; 508 | FD541A092A2A0C7900F68C68 /* Build configuration list for PBXNativeTarget "ElevenSpeak" */ = { 509 | isa = XCConfigurationList; 510 | buildConfigurations = ( 511 | FD541A0A2A2A0C7900F68C68 /* Debug */, 512 | FD541A0B2A2A0C7900F68C68 /* Release */, 513 | ); 514 | defaultConfigurationIsVisible = 0; 515 | defaultConfigurationName = Release; 516 | }; 517 | /* End XCConfigurationList section */ 518 | 519 | /* Begin XCRemoteSwiftPackageReference section */ 520 | FD81475D2A2B614800825C4A /* XCRemoteSwiftPackageReference "OpenAI" */ = { 521 | isa = XCRemoteSwiftPackageReference; 522 | repositoryURL = "https://github.com/MacPaw/OpenAI"; 523 | requirement = { 524 | kind = upToNextMajorVersion; 525 | minimumVersion = 0.2.1; 526 | }; 527 | }; 528 | FD8147682A2B7C1000825C4A /* XCRemoteSwiftPackageReference "ElevenlabsSwift" */ = { 529 | isa = XCRemoteSwiftPackageReference; 530 | repositoryURL = "https://github.com/ArchieGoodwin/ElevenlabsSwift"; 531 | requirement = { 532 | kind = upToNextMajorVersion; 533 | minimumVersion = 0.7.4; 534 | }; 535 | }; 536 | /* End XCRemoteSwiftPackageReference section */ 537 | 538 | /* Begin XCSwiftPackageProductDependency section */ 539 | FD81475E2A2B614800825C4A /* OpenAI */ = { 540 | isa = XCSwiftPackageProductDependency; 541 | package = FD81475D2A2B614800825C4A /* XCRemoteSwiftPackageReference "OpenAI" */; 542 | productName = OpenAI; 543 | }; 544 | FD8147692A2B7C1000825C4A /* ElevenlabsSwift */ = { 545 | isa = XCSwiftPackageProductDependency; 546 | package = FD8147682A2B7C1000825C4A /* XCRemoteSwiftPackageReference "ElevenlabsSwift" */; 547 | productName = ElevenlabsSwift; 548 | }; 549 | /* End XCSwiftPackageProductDependency section */ 550 | }; 551 | rootObject = FD5419F22A2A0C7800F68C68 /* Project object */; 552 | } 553 | --------------------------------------------------------------------------------