├── .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 |
--------------------------------------------------------------------------------