mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Allow voice changing
This commit is contained in:
parent
f7de80f87d
commit
213cf781a0
4 changed files with 69 additions and 9 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -2,4 +2,5 @@ import Foundation
|
|||
|
||||
public enum Keys {
|
||||
public static let userIdKey = "omnivoreUserIdKey"
|
||||
public static let textToSpeechVoice = "omnivoreTextToSpeechVoice"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue