From 213cf781a09ae9b5f72805f03e694180b9714e45 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 9 Sep 2022 12:42:51 +0800 Subject: [PATCH] Allow voice changing --- .../App/Views/AudioPlayer/MiniPlayer.swift | 17 ++++-- .../AudioSession/AudioController.swift | 57 +++++++++++++++++-- .../AudioSession/SpeechSynthesizer.swift | 3 +- apple/OmnivoreKit/Sources/Utils/Keys.swift | 1 + 4 files changed, 69 insertions(+), 9 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayer.swift b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayer.swift index 10f468f75..74fa96bfb 100644 --- a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayer.swift @@ -333,11 +333,20 @@ public struct MiniPlayer: View { NavigationView { VStack { List { - ForEach(["Jenny", "Guy"], id: \.self) { name in - Button(action: {}) { - Text(name) + ForEach(audioSession.voiceList ?? [], id: \.key.self) { voice in + HStack { + Button(action: { + audioSession.currentVoice = voice.key + }) { + Text(voice.name) + } + .buttonStyle(PlainButtonStyle()) + .frame(maxWidth: .infinity) + + if voice.selected { + Image(systemName: "checkmark") + } } - .buttonStyle(PlainButtonStyle()) } } .padding(.top, 32) diff --git a/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift b/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift index 5bade892a..ebc474793 100644 --- a/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift +++ b/apple/OmnivoreKit/Sources/Services/AudioSession/AudioController.swift @@ -35,6 +35,24 @@ enum DownloadPriority: String { case high } +struct VoicePair { + let firstKey: String + let secondKey: String + + let firstName: String + let secondName: String +} + +let VOICES = [ + VoicePair(firstKey: "en-US-JennyNeural", secondKey: "en-US-BrandonNeural", firstName: "Jenny (USA)", secondName: "Brandon (USA)"), + VoicePair(firstKey: "en-US-CoraNeural", secondKey: "en-US-ChristopherNeural", firstName: "Cora (USA)", secondName: "Christopher (USA)"), + VoicePair(firstKey: "en-US-ElizabethNeural", secondKey: "en-US-EricNeural", firstName: "Elizabeth (USA)", secondName: "Eric (USA)"), + VoicePair(firstKey: "en-CA-ClaraNeural", secondKey: "en-CA-LiamNeural", firstName: "Clara (Canada)", secondName: "Liam (Canada)"), + VoicePair(firstKey: "en-UK-LibbyNeural", secondKey: "en-UK-EthanNeural", firstName: "Libby (UK)", secondName: "Ethan (UK)"), + VoicePair(firstKey: "en-AU-NatashaNeural", secondKey: "en-AU-WilliamNeural", firstName: "Natasha (Australia)", secondName: "William (Australia)"), + VoicePair(firstKey: "en-ID-NeerjaNeural", secondKey: "en-ID-PrabhatNeural", firstName: "Neerja (India)", secondName: "Prabhat (India)") +] + class SpeechPlayerItem: AVPlayerItem { let session: AudioController let speechItem: SpeechItem @@ -64,6 +82,7 @@ public class AudioController: NSObject, ObservableObject, AVAudioPlayerDelegate, @Published public var duration: TimeInterval = 0 @Published public var timeElapsedString: String? @Published public var durationString: String? + @Published public var voiceList: [(name: String, key: String, selected: Bool)]? let appEnvironment: AppEnvironment let networker: Networker @@ -79,6 +98,9 @@ public class AudioController: NSObject, ObservableObject, AVAudioPlayerDelegate, public init(appEnvironment: AppEnvironment, networker: Networker) { self.appEnvironment = appEnvironment self.networker = networker + + super.init() + self.voiceList = generateVoiceList() } public func play(item: LinkedItem) { @@ -109,6 +131,15 @@ public class AudioController: NSObject, ObservableObject, AVAudioPlayerDelegate, durations = nil } + public func generateVoiceList() -> [(name: String, key: String, selected: Bool)] { + VOICES.flatMap { voicePair in + [ + (name: voicePair.firstName, key: voicePair.firstKey, selected: voicePair.firstKey == currentVoice), + (name: voicePair.secondName, key: voicePair.secondKey, selected: voicePair.secondKey == currentVoice) + ] + } + } + public func preload(itemIDs _: [String], retryCount _: Int = 0) async -> Bool { // var pendingList = [String]() // @@ -166,7 +197,6 @@ public class AudioController: NSObject, ObservableObject, AVAudioPlayerDelegate, func updateDuration(forItem item: SpeechItem, newDuration: TimeInterval) { if let durations = self.durations, item.audioIdx < durations.count { - print("UPDATE DURATION: ", newDuration, "OLD", self.durations?[item.audioIdx]) self.durations?[item.audioIdx] = newDuration } } @@ -206,7 +236,26 @@ public class AudioController: NSObject, ObservableObject, AVAudioPlayerDelegate, } public var currentVoice: String { - "en-US-JennyNeural" + get { + UserDefaults.standard.string(forKey: Keys.textToSpeechVoice) ?? "en-US-JennyNeural" + } + set { + UserDefaults.standard.set(newValue, forKey: Keys.textToSpeechVoice) + voiceList = generateVoiceList() + } + } + + public var secondaryVoice: String { + let pair = VOICES.first { $0.firstKey == currentVoice || $0.secondKey == currentVoice } + if let pair = pair { + if pair.firstKey == currentVoice { + return pair.secondKey + } + if pair.secondKey == currentVoice { + return pair.firstKey + } + } + return "en-US-EricNeural" } public func isLoadingItem(item: LinkedItem) -> Bool { @@ -440,14 +489,14 @@ public class AudioController: NSObject, ObservableObject, AVAudioPlayerDelegate, } } - func downloadAudioFile(pageId: String, type _: DownloadType, priority _: DownloadPriority) async throws -> SpeechDocument { + func downloadAudioFile(pageId: String, type _: DownloadType, priority: DownloadPriority) async throws -> SpeechDocument { // let audioUrl = pathForAudioFile(pageId: pageId) // // if FileManager.default.fileExists(atPath: audioUrl.path) { // return (pending: false, url: audioUrl) // } - let path = "/api/article/\(pageId)/speech" + let path = "/api/article/\(pageId)/speech?voice=\(currentVoice)&secondaryVoice=\(secondaryVoice)priority=\(priority)" guard let url = URL(string: path, relativeTo: appEnvironment.serverBaseURL) else { throw BasicError.message(messageText: "Invalid audio URL") } diff --git a/apple/OmnivoreKit/Sources/Services/AudioSession/SpeechSynthesizer.swift b/apple/OmnivoreKit/Sources/Services/AudioSession/SpeechSynthesizer.swift index 468334cee..a4c53fe0a 100644 --- a/apple/OmnivoreKit/Sources/Services/AudioSession/SpeechSynthesizer.swift +++ b/apple/OmnivoreKit/Sources/Services/AudioSession/SpeechSynthesizer.swift @@ -174,8 +174,9 @@ func fetchUtterance(networker: Networker, segmentIdx: Int, utterance: Utterance) async throws -> URL { + let voiceStr = utterance.voice ?? document.defaultVoice let segmentStr = String(format: "%04d", arguments: [segmentIdx]) - let audioPath = document.audioDirectory.appendingPathComponent("audio-\(segmentStr).mp3") + let audioPath = document.audioDirectory.appendingPathComponent("audio-\(voiceStr)-\(segmentStr).mp3") let url = URL(string: "https://text-to-speech-streaming-bryle2uxwq-wl.a.run.app/")! if FileManager.default.fileExists(atPath: audioPath.path) { diff --git a/apple/OmnivoreKit/Sources/Utils/Keys.swift b/apple/OmnivoreKit/Sources/Utils/Keys.swift index 89cc6ea73..18ff9f1e6 100644 --- a/apple/OmnivoreKit/Sources/Utils/Keys.swift +++ b/apple/OmnivoreKit/Sources/Utils/Keys.swift @@ -2,4 +2,5 @@ import Foundation public enum Keys { public static let userIdKey = "omnivoreUserIdKey" + public static let textToSpeechVoice = "omnivoreTextToSpeechVoice" }