diff --git a/apple/Omnivore.xcodeproj/project.pbxproj b/apple/Omnivore.xcodeproj/project.pbxproj index 64de79df1..2ae3158cb 100644 --- a/apple/Omnivore.xcodeproj/project.pbxproj +++ b/apple/Omnivore.xcodeproj/project.pbxproj @@ -1400,7 +1400,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.36.0; + MARKETING_VERSION = 1.37.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; @@ -1435,7 +1435,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.36.0; + MARKETING_VERSION = 1.37.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1490,7 +1490,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.36.0; + MARKETING_VERSION = 1.37.0; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; PRODUCT_NAME = Omnivore; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1831,7 +1831,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.36.0; + MARKETING_VERSION = 1.37.0; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; PRODUCT_NAME = Omnivore; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedPlayer.swift b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedPlayer.swift index f4e3188ab..e540601bb 100644 --- a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedPlayer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedPlayer.swift @@ -28,6 +28,9 @@ } var playPauseButtonItem: some View { + if audioController.playbackError { + return AnyView(Color.clear) + } if let itemID = audioController.itemAudioProperties?.itemID, audioController.isLoadingItem(itemID: itemID) { return AnyView(ProgressView()) } else { @@ -349,6 +352,9 @@ func playerContent(_: LinkedItemAudioProperties) -> some View { ZStack { + if audioController.playbackError { + Text("There was an error playing back your audio.").foregroundColor(Color.red).font(.footnote) + } audioCards .frame(maxHeight: .infinity) @@ -422,12 +428,10 @@ public var innerBody: some View { if let itemAudioProperties = self.audioController.itemAudioProperties { - return AnyView(playerContent(itemAudioProperties) - .tint(.appGrayTextContrast) - .alert("There was an error playing back your audio.", - isPresented: $audioController.playbackError) { - Button(LocalText.dismissButton, role: .none) {} - }) + return AnyView( + playerContent(itemAudioProperties) + .tint(.appGrayTextContrast) + ) } else { return AnyView(EmptyView()) } diff --git a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayer.swift b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayer.swift index b719cd8f8..861da9bdb 100644 --- a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayer.swift @@ -34,6 +34,9 @@ } var playPauseButtonItem: some View { + if audioController.playbackError { + return AnyView(Color.clear) + } if let itemID = audioController.itemAudioProperties?.itemID, audioController.isLoadingItem(itemID: itemID) { return AnyView(ProgressView()) } else { @@ -121,21 +124,25 @@ func playerContent(_ itemAudioProperties: LinkedItemAudioProperties) -> some View { VStack(spacing: 0) { HStack(alignment: .center, spacing: 15) { - artwork(itemAudioProperties, forDimensions: 50) + if audioController.playbackError { + Text("There was an error playing back your audio.").foregroundColor(Color.red).font(.footnote) + Spacer(minLength: 0) + } else { + artwork(itemAudioProperties, forDimensions: 50) - Text(itemAudioProperties.title) - .font(Font.system(size: 17, weight: .medium)) - .fixedSize(horizontal: false, vertical: true) - .lineLimit(2) - .foregroundColor(.appGrayTextContrast) - .frame(maxHeight: 40, alignment: .leading) + Text(itemAudioProperties.title) + .font(Font.system(size: 17, weight: .medium)) + .fixedSize(horizontal: false, vertical: true) + .lineLimit(2) + .foregroundColor(.appGrayTextContrast) + .frame(maxHeight: 40, alignment: .leading) - Spacer(minLength: 0) - - playPauseButtonItem - .frame(width: 40, height: 40) - .foregroundColor(.themeAudioPlayerGray) + Spacer(minLength: 0) + playPauseButtonItem + .frame(width: 40, height: 40) + .foregroundColor(.themeAudioPlayerGray) + } stopButton .frame(width: 40, height: 40) .foregroundColor(.themeAudioPlayerGray) @@ -169,14 +176,9 @@ Spacer(minLength: 0) playerContent(itemAudioProperties) .frame(maxHeight: expanded ? 0 : 110) - // .tint(.appGrayTextContrast) - // .background(Color.systemBackground) } } } - }.alert("There was an error playing back your audio.", - isPresented: $audioController.playbackError) { - Button(LocalText.dismissButton, role: .none) {} } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 9a1499d38..aba8bf1ca 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -126,7 +126,7 @@ struct WebReaderContainerView: View { #if os(iOS) var audioNavbarItem: some View { - if audioController.isLoadingItem(itemID: item.unwrappedID) { + if !audioController.playbackError, audioController.isLoadingItem(itemID: item.unwrappedID) { return AnyView(ProgressView() .padding(.horizontal)) } else { @@ -157,7 +157,7 @@ struct WebReaderContainerView: View { } var textToSpeechButtonImage: some View { - if audioController.state == .stopped || audioController.itemAudioProperties?.itemID != self.item.id { + if audioController.playbackError || audioController.state == .stopped || audioController.itemAudioProperties?.itemID != self.item.id { return AnyView(Image.headphones) } let name = audioController.isPlayingItem(itemID: item.unwrappedID) ? "pause.circle" : "play.circle" diff --git a/apple/OmnivoreKit/Sources/Models/AppEnvironment.swift b/apple/OmnivoreKit/Sources/Models/AppEnvironment.swift index 92016510b..854059f41 100644 --- a/apple/OmnivoreKit/Sources/Models/AppEnvironment.swift +++ b/apple/OmnivoreKit/Sources/Models/AppEnvironment.swift @@ -103,7 +103,7 @@ public extension AppEnvironment { case .prod: return URL(string: prodTtsURL)! case .test, .local: - return URL(string: "http://localhost:4000")! + return URL(string: "http://localhost:8080")! case .custom: guard let str = UserDefaults.standard.string(forKey: AppEnvironmentUserDefaultKey.ttsBaseURL.rawValue), diff --git a/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift b/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift index 5fff16478..3694a7f8b 100644 --- a/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift +++ b/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift @@ -56,7 +56,7 @@ var durations: [Double]? var lastReadUpdate = 0.0 - var samplePlayer: AVAudioPlayer? + var samplePlayer: AVPlayer? public init(dataService: DataService) { self.dataService = dataService @@ -134,6 +134,12 @@ public func stopWithError() { pause() playbackError = true + + timer?.invalidate() + timer = nil + if let player = player { + player.removeAllItems() + } } public func generateVoiceList() -> [VoiceItem] { @@ -475,36 +481,36 @@ return "en-US-CoraNeural" } + func previewVoiceURL(_ voice: String) -> URL? { + URL(string: "https://storage.googleapis.com/omnivore_preview_bucket/tts-voice-previews/\(voice).mp3") + } + public func playVoiceSample(voice: String) { - do { - pause() + pause() - if let url = Bundle(url: UtilsPackage.bundleURL)?.url(forResource: voice, withExtension: "mp3") { - try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: []) - - samplePlayer = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.mp3.rawValue) - if !(samplePlayer?.play() ?? false) { - throw BasicError.message(messageText: "Unable to playback audio") - } - } else { - NSNotification.operationFailed(message: "Error playing voice sample.") + if let url = previewVoiceURL(voice) { + samplePlayer = AVPlayer(playerItem: AVPlayerItem(url: url)) + if let samplePlayer = samplePlayer { + samplePlayer.play() } - } catch { - print("ERROR", error) + } else { NSNotification.operationFailed(message: "Error playing voice sample.") } } public func isPlayingSample(voice: String) -> Bool { - if let samplePlayer = self.samplePlayer, let url = Bundle(url: UtilsPackage.bundleURL)?.url(forResource: voice, withExtension: "mp3") { - return samplePlayer.url == url && samplePlayer.isPlaying + if let samplePlayer = self.samplePlayer, let url = previewVoiceURL(voice) { + if let urlAsset = samplePlayer.currentItem?.asset as? AVURLAsset { + return urlAsset.url == url && samplePlayer.timeControlStatus == .playing + } + return false } return false } public func stopVoiceSample() { if let samplePlayer = self.samplePlayer { - samplePlayer.stop() + samplePlayer.pause() self.samplePlayer = nil } } @@ -576,8 +582,7 @@ let startOffset = index < document.utterances.count ? offset : 0.0 self.startStreamingAudio(itemID: itemID, document: document, atIndex: startIndex, andOffset: startOffset) } else { - print("unable to load speech document") - // TODO: Post error to SnackBar + self.stopWithError() } } } diff --git a/apple/OmnivoreKit/Sources/Services/AudioSession/SpeechSynthesizer.swift b/apple/OmnivoreKit/Sources/Services/AudioSession/SpeechSynthesizer.swift index d9e14490f..2939c2cf4 100644 --- a/apple/OmnivoreKit/Sources/Services/AudioSession/SpeechSynthesizer.swift +++ b/apple/OmnivoreKit/Sources/Services/AudioSession/SpeechSynthesizer.swift @@ -17,6 +17,7 @@ struct UtteranceRequest: Codable { let language: String let rate: String let isUltraRealisticVoice: Bool + let isOpenAIVoice: Bool } struct Utterance: Decodable { @@ -32,7 +33,8 @@ struct Utterance: Decodable { voice: usedVoice, language: document.language, rate: "1.1", - isUltraRealisticVoice: Voices.isUltraRealisticVoice(usedVoice)) + isUltraRealisticVoice: Voices.isUltraRealisticVoice(usedVoice), + isOpenAIVoice: Voices.isOpenAIVoice(usedVoice)) return try JSONEncoder().encode(request) } } diff --git a/apple/OmnivoreKit/Sources/Services/AudioSession/Voices.swift b/apple/OmnivoreKit/Sources/Services/AudioSession/Voices.swift index fc18983a2..e524cb08e 100644 --- a/apple/OmnivoreKit/Sources/Services/AudioSession/Voices.swift +++ b/apple/OmnivoreKit/Sources/Services/AudioSession/Voices.swift @@ -62,6 +62,10 @@ public enum Voices { }) } + public static func isOpenAIVoice(_ voiceKey: String) -> Bool { + voiceKey.starts(with: "openai-") + } + public static let English = VoiceLanguage(key: "en", name: "English", defaultVoice: "en-US-ChristopherNeural", @@ -81,6 +85,10 @@ public enum Voices { ] public static let Pairs = [ + VoicePair(firstKey: "openai-alloy", secondKey: "openai-echo", firstName: "Alloy", secondName: "Echo", language: "en-US", category: .enUS), + VoicePair(firstKey: "openai-fable", secondKey: "openai-onyx", firstName: "Fable", secondName: "Onyx", language: "en-US", category: .enUS), + VoicePair(firstKey: "openai-nova", secondKey: "openai-shimmer", firstName: "Nova", secondName: "Shimmer", language: "en-US", category: .enUS), + VoicePair(firstKey: "en-US-JennyNeural", secondKey: "en-US-BrandonNeural", firstName: "Jenny", secondName: "Brandon", language: "en-US", category: .enUS), VoicePair(firstKey: "en-US-CoraNeural", secondKey: "en-US-ChristopherNeural", firstName: "Cora", secondName: "Christopher", language: "en-US", category: .enUS), VoicePair(firstKey: "en-US-ElizabethNeural", secondKey: "en-US-EricNeural", firstName: "Elizabeth", secondName: "Eric", language: "en-US", category: .enUS), diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/Adam.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/Adam.mp3 deleted file mode 100644 index ba996326f..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/Adam.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/Arnold.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/Arnold.mp3 deleted file mode 100644 index eab05dc9d..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/Arnold.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/Bella.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/Bella.mp3 deleted file mode 100644 index c379de344..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/Bella.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/Elli.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/Elli.mp3 deleted file mode 100644 index 5291da7f2..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/Elli.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/Josh.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/Josh.mp3 deleted file mode 100644 index 3f5687061..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/Josh.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/Rachel.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/Rachel.mp3 deleted file mode 100644 index d66775fa8..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/Rachel.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/de-CH-LeniNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/de-CH-LeniNeural.mp3 deleted file mode 100644 index 165d8354a..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/de-CH-LeniNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/de-DE-AmalaNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/de-DE-AmalaNeural.mp3 deleted file mode 100644 index ed406cc94..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/de-DE-AmalaNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/de-DE-BerndNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/de-DE-BerndNeural.mp3 deleted file mode 100644 index 10abc327e..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/de-DE-BerndNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/de-DE-ChristophNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/de-DE-ChristophNeural.mp3 deleted file mode 100644 index 7344e1945..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/de-DE-ChristophNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/de-DE-KatjaNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/de-DE-KatjaNeural.mp3 deleted file mode 100644 index 9c8a630c1..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/de-DE-KatjaNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/de-DE-LouisaNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/de-DE-LouisaNeural.mp3 deleted file mode 100644 index 976b98d5a..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/de-DE-LouisaNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-AU-NatashaNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-AU-NatashaNeural.mp3 deleted file mode 100644 index 3e950e298..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-AU-NatashaNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-AU-WilliamNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-AU-WilliamNeural.mp3 deleted file mode 100644 index 1ed498f63..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-AU-WilliamNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-CA-ClaraNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-CA-ClaraNeural.mp3 deleted file mode 100644 index 7d7963d15..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-CA-ClaraNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-CA-LiamNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-CA-LiamNeural.mp3 deleted file mode 100644 index d1387c1c6..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-CA-LiamNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-GB-EthanNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-GB-EthanNeural.mp3 deleted file mode 100644 index fdb55cb03..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-GB-EthanNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-GB-LibbyNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-GB-LibbyNeural.mp3 deleted file mode 100644 index b4db2eb6d..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-GB-LibbyNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-IE-ConnorNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-IE-ConnorNeural.mp3 deleted file mode 100644 index 652050533..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-IE-ConnorNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-IE-EmilyNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-IE-EmilyNeural.mp3 deleted file mode 100644 index 0213ab9db..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-IE-EmilyNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-IN-NeerjaNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-IN-NeerjaNeural.mp3 deleted file mode 100644 index 0bfe98954..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-IN-NeerjaNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-IN-PrabhatNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-IN-PrabhatNeural.mp3 deleted file mode 100644 index c7c034521..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-IN-PrabhatNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-SG-LunaNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-SG-LunaNeural.mp3 deleted file mode 100644 index d4e09c122..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-SG-LunaNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-SG-WayneNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-SG-WayneNeural.mp3 deleted file mode 100644 index fbcd8f1e4..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-SG-WayneNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-US-BrandonNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-US-BrandonNeural.mp3 deleted file mode 100644 index 894cc48d1..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-US-BrandonNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-US-ChristopherNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-US-ChristopherNeural.mp3 deleted file mode 100644 index 019580fc4..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-US-ChristopherNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-US-CoraNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-US-CoraNeural.mp3 deleted file mode 100644 index 34278e3e2..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-US-CoraNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-US-ElizabethNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-US-ElizabethNeural.mp3 deleted file mode 100644 index 61f071abe..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-US-ElizabethNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-US-EricNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-US-EricNeural.mp3 deleted file mode 100644 index 518ff4ddf..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-US-EricNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-US-JennyNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-US-JennyNeural.mp3 deleted file mode 100644 index 168bac663..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/en-US-JennyNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/es-ES-AlvaroNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/es-ES-AlvaroNeural.mp3 deleted file mode 100644 index a68103765..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/es-ES-AlvaroNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/es-ES-ElviraNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/es-ES-ElviraNeural.mp3 deleted file mode 100644 index 7cce30ad7..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/es-ES-ElviraNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/fr-FR-DeniseNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/fr-FR-DeniseNeural.mp3 deleted file mode 100644 index a7d2bfcf6..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/fr-FR-DeniseNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/fr-FR-HenriNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/fr-FR-HenriNeural.mp3 deleted file mode 100644 index e8602e018..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/fr-FR-HenriNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/hi-IN-MadhurNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/hi-IN-MadhurNeural.mp3 deleted file mode 100644 index 7b82d1001..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/hi-IN-MadhurNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/hi-IN-SwaraNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/hi-IN-SwaraNeural.mp3 deleted file mode 100644 index eeca65a5a..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/hi-IN-SwaraNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/it-IT-BenignoNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/it-IT-BenignoNeural.mp3 deleted file mode 100644 index a4a7d2fc1..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/it-IT-BenignoNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/it-IT-IsabellaNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/it-IT-IsabellaNeural.mp3 deleted file mode 100644 index 78658d476..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/it-IT-IsabellaNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ja-JP-KeitaNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ja-JP-KeitaNeural.mp3 deleted file mode 100644 index c49d90c15..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ja-JP-KeitaNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ja-JP-NanamiNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ja-JP-NanamiNeural.mp3 deleted file mode 100644 index 53f81a660..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ja-JP-NanamiNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/pt-BR-AntonioNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/pt-BR-AntonioNeural.mp3 deleted file mode 100644 index a71b654bf..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/pt-BR-AntonioNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/pt-BR-BrendaNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/pt-BR-BrendaNeural.mp3 deleted file mode 100644 index 69ac03670..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/pt-BR-BrendaNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ta-IN-PallaviNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ta-IN-PallaviNeural.mp3 deleted file mode 100644 index e0f366c9a..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ta-IN-PallaviNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ta-IN-ValluvarNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ta-IN-ValluvarNeural.mp3 deleted file mode 100644 index a22f6ea99..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ta-IN-ValluvarNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ta-LK-KumarNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ta-LK-KumarNeural.mp3 deleted file mode 100644 index 3455cb916..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ta-LK-KumarNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ta-LK-SaranyaNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ta-LK-SaranyaNeural.mp3 deleted file mode 100644 index 1bc08e940..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ta-LK-SaranyaNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ta-MY-KaniNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ta-MY-KaniNeural.mp3 deleted file mode 100644 index 7e06b8f45..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ta-MY-KaniNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ta-MY-SuryaNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ta-MY-SuryaNeural.mp3 deleted file mode 100644 index 80fd1136e..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ta-MY-SuryaNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ta-SG-AnbuNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ta-SG-AnbuNeural.mp3 deleted file mode 100644 index 3830fba4f..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ta-SG-AnbuNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ta-SG-VenbaNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ta-SG-VenbaNeural.mp3 deleted file mode 100644 index aaa243378..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/ta-SG-VenbaNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/zh-CN-XiaochenNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/zh-CN-XiaochenNeural.mp3 deleted file mode 100644 index 031ce0ae4..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/zh-CN-XiaochenNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/zh-CN-XiaohanNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/zh-CN-XiaohanNeural.mp3 deleted file mode 100644 index 7b746a4a6..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/zh-CN-XiaohanNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/zh-CN-XiaoxiaoNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/zh-CN-XiaoxiaoNeural.mp3 deleted file mode 100644 index 868d0f757..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/zh-CN-XiaoxiaoNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/zh-CN-YunyangNeural.mp3 b/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/zh-CN-YunyangNeural.mp3 deleted file mode 100644 index a6dd6e2ad..000000000 Binary files a/apple/OmnivoreKit/Sources/Utils/Resources/VoiceSamples/zh-CN-YunyangNeural.mp3 and /dev/null differ diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift index a8652b8c5..ebbdae6ef 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift @@ -74,7 +74,7 @@ public struct GridCard: View { ZStack(alignment: .bottomLeading) { if let imageURL = item.imageURL { - AsyncImage(url: imageURL) { phase in + CachedAsyncImage(url: imageURL) { phase in switch phase { case .empty: Color.systemBackground diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryFeatureCard.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryFeatureCard.swift index 9df2f06a8..97ee23227 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryFeatureCard.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryFeatureCard.swift @@ -34,7 +34,7 @@ public struct LibraryFeatureCard: View { var imageBox: some View { ZStack(alignment: .bottomLeading) { if let imageURL = item.imageURL { - AsyncImage(url: imageURL) { phase in + CachedAsyncImage(url: imageURL) { phase in switch phase { case .empty: Color.systemBackground diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift index e718126cb..2034eea85 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift @@ -241,7 +241,7 @@ public struct LibraryItemCard: View { var imageBox: some View { ZStack(alignment: .bottomLeading) { if let imageURL = item.imageURL { - AsyncImage(url: imageURL) { phase in + CachedAsyncImage(url: imageURL) { phase in if let image = phase.image { image .resizable() diff --git a/apple/OmnivoreKit/Sources/Views/Images/CachedAsyncImage.swift b/apple/OmnivoreKit/Sources/Views/Images/CachedAsyncImage.swift new file mode 100644 index 000000000..f92709b4f --- /dev/null +++ b/apple/OmnivoreKit/Sources/Views/Images/CachedAsyncImage.swift @@ -0,0 +1,66 @@ +// +// CachedAsyncImage.swift +// +// +// Created by Jackson Harper on 11/8/23. +// + +// from: https://github.com/pitt500/Pokedex + +import SwiftUI + +struct CachedAsyncImage: View where Content: View { + private let url: URL + private let scale: CGFloat + private let transaction: Transaction + private let content: (AsyncImagePhase) -> Content + + init( + url: URL, + scale: CGFloat = 1.0, + transaction: Transaction = Transaction(), + @ViewBuilder content: @escaping (AsyncImagePhase) -> Content + ) { + self.url = url + self.scale = scale + self.transaction = transaction + self.content = content + } + + var body: some View { + if let cached = ImageCache[url] { + // _ = print("cached \(url.absoluteString)") + content(.success(cached)) + } else { + // _ = print("request \(url.absoluteString)") + AsyncImage( + url: url, + scale: scale, + transaction: transaction + ) { phase in + cacheAndRender(phase: phase) + } + } + } + + func cacheAndRender(phase: AsyncImagePhase) -> some View { + if case let .success(image) = phase { + ImageCache[url] = image + } + + return content(phase) + } +} + +private enum ImageCache { + private static var cache: [URL: Image] = [:] + + static subscript(url: URL) -> Image? { + get { + ImageCache.cache[url] + } + set { + ImageCache.cache[url] = newValue + } + } +} diff --git a/packages/text-to-speech/src/azureTextToSpeech.ts b/packages/text-to-speech/src/azureTextToSpeech.ts index 1d972ec1c..fe43b826f 100644 --- a/packages/text-to-speech/src/azureTextToSpeech.ts +++ b/packages/text-to-speech/src/azureTextToSpeech.ts @@ -18,7 +18,7 @@ import { export class AzureTextToSpeech implements TextToSpeech { use(input: TextToSpeechInput): boolean { - return !input.isUltraRealisticVoice + return !input.isUltraRealisticVoice && !input.isOpenAIVoice } synthesizeTextToSpeech = async ( diff --git a/packages/text-to-speech/src/index.ts b/packages/text-to-speech/src/index.ts index 992111ef7..9ff7abf4a 100644 --- a/packages/text-to-speech/src/index.ts +++ b/packages/text-to-speech/src/index.ts @@ -19,6 +19,7 @@ import { } from './textToSpeech' import { createClient } from 'redis' import { RealisticTextToSpeech } from './realisticTextToSpeech' +import { OpenAITextToSpeech } from './openaiTextToSpeech' // explicitly create the return type of RedisClient type RedisClient = ReturnType @@ -63,6 +64,7 @@ const MAX_CHARACTER_COUNT = 50000 const storage = new Storage() const textToSpeechHandlers = [ + new OpenAITextToSpeech(), new AzureTextToSpeech(), new RealisticTextToSpeech(), ] diff --git a/packages/text-to-speech/src/openaiTextToSpeech.ts b/packages/text-to-speech/src/openaiTextToSpeech.ts new file mode 100644 index 000000000..0c4a42225 --- /dev/null +++ b/packages/text-to-speech/src/openaiTextToSpeech.ts @@ -0,0 +1,56 @@ +import { + TextToSpeech, + TextToSpeechInput, + TextToSpeechOutput, +} from './textToSpeech' +import axios from 'axios' +import { stripEmojis } from './htmlToSsml' + +const OPEN_AI_VOICES = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'] + +export class OpenAITextToSpeech implements TextToSpeech { + synthesizeTextToSpeech = async ( + input: TextToSpeechInput + ): Promise => { + const apiKey = process.env.OPENAI_API_KEY + const voice = input.voice?.substring('openai-'.length) + + if (!apiKey) { + throw new Error('API credentials not set') + } + + const HEADERS = { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + } + + const payload = { + model: 'tts-1', + voice: voice, + input: stripEmojis(input.text), + } + + const requestUrl = `https://api.openai.com/v1/audio/speech` + const response = await axios.post(requestUrl, payload, { + headers: HEADERS, + responseType: 'arraybuffer', + }) + + if (response.data.length === 0) { + console.log('No payload returned: ', response) + throw new Error('No payload returned') + } + + return { + speechMarks: [], + audioData: response.data, + } + } + + use(input: TextToSpeechInput): boolean { + if (input.voice?.startsWith('openai-')) { + return true + } + return false + } +} diff --git a/packages/text-to-speech/src/textToSpeech.ts b/packages/text-to-speech/src/textToSpeech.ts index a446b18b3..ed385a29e 100644 --- a/packages/text-to-speech/src/textToSpeech.ts +++ b/packages/text-to-speech/src/textToSpeech.ts @@ -7,6 +7,7 @@ export interface TextToSpeechInput { rate?: string secondaryVoice?: string audioStream?: NodeJS.ReadWriteStream + isOpenAIVoice?: boolean isUltraRealisticVoice?: boolean }