Make tts preload optional, use headphones for tts icon, language ux

This commit is contained in:
Jackson Harper 2022-09-23 16:38:21 +08:00
parent 461100d359
commit d11091a458
6 changed files with 106 additions and 110 deletions

View file

@ -19,7 +19,6 @@ struct TextToSpeechLanguageView: View {
.listStyle(InsetListStyle())
#endif
}
.navigationTitle("Default Language")
}
private var innerBody: some View {

View file

@ -3,37 +3,17 @@ import Services
import SwiftUI
import Views
@MainActor final class TextToSpeechViewModel: ObservableObject {
@Published var enableAudioPrefetch: Bool = true
// func cancelSubscription(dataService: DataService) async -> Bool {
// guard let subscriptionName = subscriptionNameToCancel else { return false }
//
// do {
// try await dataService.deleteSubscription(subscriptionName: subscriptionName)
// let index = subscriptions.firstIndex { $0.name == subscriptionName }
// if let index = index {
// subscriptions.remove(at: index)
// }
// return true
// } catch {
// appLogger.debug("failed to remove subscription")
// return false
// }
// }
}
struct TextToSpeechView: View {
@EnvironmentObject var audioController: AudioController
@StateObject var viewModel = TextToSpeechViewModel()
var body: some View {
Group {
#if os(iOS)
Form {
Section("Audio Settings") {
Toggle("Enable audio prefetch", isOn: $viewModel.enableAudioPrefetch)
Toggle("Enable audio prefetch", isOn: $audioController.preloadEnabled)
}
NavigationLink(destination: TextToSpeechLanguageView()) {
NavigationLink(destination: TextToSpeechLanguageView().navigationTitle("Default Language")) {
Text("Default Language")
}
innerBody
@ -55,27 +35,5 @@ struct TextToSpeechView: View {
}
}
}
// ForEach(VoiceCategory.allCases, id: \.self) { category in
// Section(category.rawValue) {
// ForEach(audioController.voiceList?.filter { $0.category == category } ?? [], id: \.key.self) { voice in
// Button(action: {
// audioController.currentVoice = voice.key
// // self.showVoiceSheet = false
// }) {
// HStack {
// Text(voice.name)
//
// Spacer()
//
// if voice.selected {
// Image(systemName: "checkmark")
// }
// }
// .contentShape(Rectangle())
// }
// .buttonStyle(PlainButtonStyle())
// }
// }
// }
}
}

View file

@ -82,7 +82,7 @@ struct WebReaderContainerView: View {
}
},
label: {
Image(systemName: audioController.isPlayingItem(itemID: item.unwrappedID) ? "pause.circle" : "play.circle")
Image(systemName: textToSpeechButtonImage)
.font(.appTitleTwo)
}
)
@ -91,6 +91,13 @@ struct WebReaderContainerView: View {
}
}
var textToSpeechButtonImage: String {
if audioController.state == .stopped {
return "headphones"
}
return audioController.isPlayingItem(itemID: item.unwrappedID) ? "pause.circle" : "play.circle"
}
var navBar: some View {
HStack(alignment: .center) {
#if os(iOS)

View file

@ -312,7 +312,7 @@ public class AudioController: NSObject, ObservableObject, AVAudioPlayerDelegate
if let stoppedId = stoppedId {
EventTracker.track(
.audioSessionEnd(linkID: stoppedId, timeElapsed: stoppedTimeElapsed ?? 0.0)
.audioSessionEnd(linkID: stoppedId, timeElapsed: stoppedTimeElapsed)
)
}
}
@ -327,8 +327,11 @@ public class AudioController: NSObject, ObservableObject, AVAudioPlayerDelegate
}
public func preload(itemIDs: [String], retryCount _: Int = 0) async -> Bool {
if !preloadEnabled {
return true
}
for itemID in itemIDs {
print("preloading speech file: ", itemID)
if let document = try? await downloadSpeechFile(itemID: itemID, priority: .low) {
let synthesizer = SpeechSynthesizer(appEnvironment: appEnvironment, networker: networker, document: document)
do {
@ -450,6 +453,8 @@ public class AudioController: NSObject, ObservableObject, AVAudioPlayerDelegate
}
}
@AppStorage(UserDefaultKey.textToSpeechPreloadEnabled.rawValue) public var preloadEnabled = true
public var currentVoiceLanguage: VoiceLanguage {
VOICELANGUAGES.first(where: { $0.key == currentLanguage }) ?? ENGLISH
}
@ -556,6 +561,20 @@ public class AudioController: NSObject, ObservableObject, AVAudioPlayerDelegate
return "en-US-CoraNeural"
}
public func playVoiceSample(voice: String) {
do {
if let url = Bundle.main.url(forResource: "tts-voice-sample-\(voice)", withExtension: "mp3") {
let player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.mp3.rawValue)
player.play()
} else {
NSNotification.operationFailed(message: "Error playing voice sample.")
}
} catch {
print("ERROR", error)
NSNotification.operationFailed(message: "Error playing voice sample.")
}
}
private func updateDurations(oldPlayback: Double, newPlayback: Double) {
if let oldDurations = durations {
durations = oldDurations.map { $0 * oldPlayback / newPlayback }
@ -846,7 +865,6 @@ public class AudioController: NSObject, ObservableObject, AVAudioPlayerDelegate
func downloadSpeechFile(itemID: String, priority: DownloadPriority) async throws -> SpeechDocument? {
let decoder = JSONDecoder()
let speechFileUrl = pathForSpeechFile(itemID: itemID)
print("looking up speeh file: ", speechFileUrl)
if FileManager.default.fileExists(atPath: speechFileUrl.path) {
let data = try Data(contentsOf: speechFileUrl)
@ -858,7 +876,6 @@ public class AudioController: NSObject, ObservableObject, AVAudioPlayerDelegate
}
let path = "/api/article/\(itemID)/speech?voice=\(currentVoice)&secondaryVoice=\(secondaryVoice)&priority=\(priority)\(isoLangForCurrentVoice())"
print("fetching audio for path", path)
guard let url = URL(string: path, relativeTo: appEnvironment.serverBaseURL) else {
throw BasicError.message(messageText: "Invalid audio URL")
}

View file

@ -16,4 +16,5 @@ public enum UserDefaultKey: String {
case textToSpeechPlaybackRate
case textToSpeechPreferredVoice
case textToSpeechDefaultLanguage
case textToSpeechPreloadEnabled
}

View file

@ -150,6 +150,66 @@
"scale" : "1x",
"size" : "1024x1024"
},
{
"filename" : "image 1-1.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "image 1@2x-1.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "image 1.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "image 1@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "128@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "256@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "512-1.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "512@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
},
{
"filename" : "48.png",
"idiom" : "watch",
@ -225,6 +285,13 @@
"size" : "51x51",
"subtype" : "45mm"
},
{
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "54x54",
"subtype" : "49mm"
},
{
"filename" : "172.png",
"idiom" : "watch",
@ -256,71 +323,18 @@
"size" : "117x117",
"subtype" : "45mm"
},
{
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "129x129",
"subtype" : "49mm"
},
{
"filename" : "1024.png",
"idiom" : "watch-marketing",
"scale" : "1x",
"size" : "1024x1024"
},
{
"filename" : "image 1-1.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "image 1@2x-1.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "image 1.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "image 1@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "128@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "256@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "512-1.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "512@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {