Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Whisper #415

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions voice/voice-ai/Voice AI.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@
B97E04B72B3C14BC000C9FCA /* TwitterManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B97E04B52B3C14BC000C9FCA /* TwitterManager.swift */; };
B97E04B82B3C14BC000C9FCA /* TwitterManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B97E04B52B3C14BC000C9FCA /* TwitterManager.swift */; };
B97E04B92B3C14BC000C9FCA /* TwitterManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B97E04B52B3C14BC000C9FCA /* TwitterManager.swift */; };
B98288E92B4C132C00AA43C4 /* Hey.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = B98288E82B4C132C00AA43C4 /* Hey.mp3 */; };
B98288EA2B4C132C00AA43C4 /* Hey.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = B98288E82B4C132C00AA43C4 /* Hey.mp3 */; };
B9A3ADEE2B15CCCF00C5FC66 /* ShareLinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9A3ADED2B15CCCE00C5FC66 /* ShareLinkTests.swift */; };
B9A3ADF02B15CD3500C5FC66 /* ActivityViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9A3ADEF2B15CD3500C5FC66 /* ActivityViewTests.swift */; };
B9A3AE182B18CC7900C5FC66 /* TimerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9A3AE102B18CC7900C5FC66 /* TimerManager.swift */; };
Expand Down Expand Up @@ -241,6 +243,10 @@
B9B331A32AFB849000F6A9C9 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9B331A22AFB849000F6A9C9 /* StoreKit.framework */; };
B9BA337E2AE683EF00D7756D /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9BA337D2AE683EF00D7756D /* AudioPlayer.swift */; };
B9BB0FE02B3C69FC00E663F6 /* TwitterUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9BB0FDF2B3C69FC00E663F6 /* TwitterUI.swift */; };
B9BB0FE22B459FB100E663F6 /* OpenAITextToSpeech.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9BB0FE12B459FB100E663F6 /* OpenAITextToSpeech.swift */; };
B9BB0FE32B459FB100E663F6 /* OpenAITextToSpeech.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9BB0FE12B459FB100E663F6 /* OpenAITextToSpeech.swift */; };
B9BB0FE42B459FB100E663F6 /* OpenAITextToSpeech.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9BB0FE12B459FB100E663F6 /* OpenAITextToSpeech.swift */; };
B9BB0FE52B459FB100E663F6 /* OpenAITextToSpeech.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9BB0FE12B459FB100E663F6 /* OpenAITextToSpeech.swift */; };
B9C4A81F2AEE594900327529 /* MockSpeechRecognition.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9C4A81E2AEE594900327529 /* MockSpeechRecognition.swift */; };
B9C4A8282AEE867A00327529 /* ActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9C4A8262AEE861C00327529 /* ActionHandler.swift */; };
B9C4A8292AEE867E00327529 /* ActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9C4A8262AEE861C00327529 /* ActionHandler.swift */; };
Expand Down Expand Up @@ -461,6 +467,7 @@
B97E04AD2B358175000C9FCA /* TwitterAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterAPI.swift; sourceTree = "<group>"; };
B97E04B32B3582F0000C9FCA /* TwitterModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterModel.swift; sourceTree = "<group>"; };
B97E04B52B3C14BC000C9FCA /* TwitterManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterManager.swift; sourceTree = "<group>"; };
B98288E82B4C132C00AA43C4 /* Hey.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = Hey.mp3; sourceTree = "<group>"; };
B9A3ADED2B15CCCE00C5FC66 /* ShareLinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareLinkTests.swift; sourceTree = "<group>"; };
B9A3ADEF2B15CD3500C5FC66 /* ActivityViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityViewTests.swift; sourceTree = "<group>"; };
B9A3AE102B18CC7900C5FC66 /* TimerManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimerManager.swift; sourceTree = "<group>"; };
Expand All @@ -484,6 +491,7 @@
B9B331A22AFB849000F6A9C9 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS17.0.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; };
B9BA337D2AE683EF00D7756D /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = "<group>"; };
B9BB0FDF2B3C69FC00E663F6 /* TwitterUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterUI.swift; sourceTree = "<group>"; };
B9BB0FE12B459FB100E663F6 /* OpenAITextToSpeech.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAITextToSpeech.swift; sourceTree = "<group>"; };
B9C4A81E2AEE594900327529 /* MockSpeechRecognition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSpeechRecognition.swift; sourceTree = "<group>"; };
B9C4A8262AEE861C00327529 /* ActionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionHandler.swift; sourceTree = "<group>"; };
CD0D13342ADA73B300031EDD /* Voice AI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Voice AI.app"; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -737,6 +745,7 @@
B930AACE2ADD4BAF009F9F8C /* Resources */ = {
isa = PBXGroup;
children = (
B98288E82B4C132C00AA43C4 /* Hey.mp3 */,
A4680D9D2AE9C8A600F5F8ED /* beep.mp3 */,
B930AACF2ADD4BAF009F9F8C /* Font */,
);
Expand Down Expand Up @@ -823,6 +832,7 @@
isa = PBXGroup;
children = (
B9AD50532ADFE8A0006F18A1 /* SpeechRecognition.swift */,
B9BB0FE12B459FB100E663F6 /* OpenAITextToSpeech.swift */,
);
path = SpeechRecognition;
sourceTree = "<group>";
Expand Down Expand Up @@ -1238,6 +1248,7 @@
buildActionMask = 2147483647;
files = (
A44CFFB12B0D7200003D6822 /* AppConfig.plist in Resources */,
B98288EA2B4C132C00AA43C4 /* Hey.mp3 in Resources */,
B3E8D1552B201E450041B82B /* Localizable.xcstrings in Resources */,
E47F1A992B07F94F00455617 /* Dotrice-Regular.otf in Resources */,
F67EDE382B07C67400FDEA80 /* logo.png in Resources */,
Expand Down Expand Up @@ -1280,6 +1291,7 @@
files = (
CD0D13422ADA73B400031EDD /* Preview Assets.xcassets in Resources */,
22A9883B2AF8FE0D00A32B1A /* SyncedProducts.storekit in Resources */,
B98288E92B4C132C00AA43C4 /* Hey.mp3 in Resources */,
A44CFFB02B0D7200003D6822 /* AppConfig.plist in Resources */,
CD0D133E2ADA73B400031EDD /* Assets.xcassets in Resources */,
B9AD506D2ADFEFDB006F18A1 /* Dotrice-Bold-Expanded.otf in Resources */,
Expand Down Expand Up @@ -1372,6 +1384,7 @@
CDC137162B11B129003386E9 /* TimeLogger.swift in Sources */,
B91F05B12B049F720029A32D /* AppleSignInManager.swift in Sources */,
E4D0D8E42B2384F800F717A2 /* AlertManager.swift in Sources */,
B9BB0FE52B459FB100E663F6 /* OpenAITextToSpeech.swift in Sources */,
B91F05B72B04B3E00029A32D /* KeychainService.swift in Sources */,
B91F05BB2B04BA250029A32D /* NetworkManager.swift in Sources */,
A46B5A7C2AE73CE600C874ED /* AudioPlayer.swift in Sources */,
Expand Down Expand Up @@ -1441,6 +1454,7 @@
B9AD50542ADFE8A0006F18A1 /* SpeechRecognition.swift in Sources */,
224BECD72B20AC9100C84602 /* LogStore.swift in Sources */,
B91F05C22B04C0D50029A32D /* CreateUser.swift in Sources */,
B9BB0FE22B459FB100E663F6 /* OpenAITextToSpeech.swift in Sources */,
B9AD50612ADFE9B7006F18A1 /* Usage.swift in Sources */,
B9BA337E2AE683EF00D7756D /* AudioPlayer.swift in Sources */,
AC66DF302B2706E100DDC802 /* RandomTrivia.swift in Sources */,
Expand Down Expand Up @@ -1469,6 +1483,7 @@
buildActionMask = 2147483647;
files = (
F614BECA2B18E0FB00C71E32 /* AppleSignInManagerTests.swift in Sources */,
B9BB0FE32B459FB100E663F6 /* OpenAITextToSpeech.swift in Sources */,
B3D0A3442AF29B1B00E8B0DA /* MockNetworkService.swift in Sources */,
B919B7BF2AF3C3F7006335D1 /* AudioEngineAndSessionTests.swift in Sources */,
6EC2F6292B067E91002EFADD /* KeychainService.swift in Sources */,
Expand Down Expand Up @@ -1607,6 +1622,7 @@
B3BC2CB42B05F4AF00A58477 /* NetworkManager.swift in Sources */,
B9A3AE1E2B18CC7900C5FC66 /* RelayAuth.swift in Sources */,
6E53AF4A2AF0126E0022A8F2 /* VibrationManager.swift in Sources */,
B9BB0FE42B459FB100E663F6 /* OpenAITextToSpeech.swift in Sources */,
F65054FF2B05269200FFEA07 /* xUIDebounceTests.swift in Sources */,
6E56C7C02B067AF100ED2296 /* AppleSignInManager.swift in Sources */,
6E53AF472AF0121E0022A8F2 /* AudioPlayer.swift in Sources */,
Expand Down
34 changes: 27 additions & 7 deletions voice/voice-ai/x/Actions/AudioPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,32 +29,33 @@ class AVAudioSessionWrapper: AVAudioSessionProtocol {
}
}

class AudioPlayer: NSObject {
class AudioPlayer: NSObject, AVAudioPlayerDelegate {
var logger = Logger(
subsystem: Bundle.main.bundleIdentifier!,
category: String(describing: "AVAudioSessionWrapper")
)
var audioPlayer: AVAudioPlayer?
var timer: Timer?

var completion: (() -> Void)?

func playSound(_ isLoop: Bool = true, _ resource: String = "beep") {
playSoundWithSettings(isLoop, resource)
}

func playSoundWithSettings(_ loop: Bool = true, _ resource: String = "beep") {
guard let soundURL = Bundle.main.url(forResource: resource, withExtension: "mp3") else {
self.logger.log("Sound file not found")

SentrySDK.capture(message: "Sound file not found")

return
}
do {
audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
audioPlayer?.prepareToPlay()
audioPlayer?.numberOfLoops = 0 // Play once, as the loop will be handled by the Timer
audioPlayer?.play()

// Schedule a Timer to play the sound every 2 seconds
if loop {
timer = Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(playSoundWithDelay), userInfo: nil, repeats: true)
Expand All @@ -64,16 +65,35 @@ class AudioPlayer: NSObject {
SentrySDK.capture(message: "Error playing sound: \(error.localizedDescription)")
}
}

func playSoundTTS(fromData data: Data, completion: @escaping () -> Void) {
do {
audioPlayer = try AVAudioPlayer(data: data)
audioPlayer?.delegate = self
audioPlayer?.prepareToPlay()
audioPlayer?.numberOfLoops = 0
audioPlayer?.play()
self.completion = completion
} catch {
logger.log("Error playing sound from data: \(error.localizedDescription)")
SentrySDK.capture(message: "Error playing sound from data: \(error.localizedDescription)")
completion() // Ensure to call completion even in case of an error
}
}

func stopSound() {
audioPlayer?.stop()
timer?.invalidate() // Stop the timer when stopping the sound
}

@objc func playSoundWithDelay() {
if audioPlayer?.isPlaying == false {
audioPlayer?.currentTime = 0
audioPlayer?.play()
}
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
completion?()
completion = nil
}
}
9 changes: 8 additions & 1 deletion voice/voice-ai/x/AppConfiguration/AppConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ class AppConfig {
private var paymentMode: String?
var themeName: String?
private var mixpanelToken: String?

private var textToSpeechKey: String?

init(dic: [String: Any]? = nil, relay: RelayAuthProtocol? = nil) {
loadConfiguration(dic: dic)

Expand Down Expand Up @@ -143,6 +144,7 @@ class AppConfig {
serverAPIKey = dictionary["SERVER_API_KEY"] as? String
paymentMode = (dictionary["PAYMENT_MODE"] as? String) ?? "sandbox"
mixpanelToken = (dictionary["MIXPANEL_TOKEN"] as? String)
textToSpeechKey = (dictionary["TEXT_TO_SPEECH_API_KEY"] as? String)

// Convert the string values to Int
if let eventsString = dictionary["MINIMUM_SIGNIFICANT_EVENTS"] as? String,
Expand Down Expand Up @@ -301,6 +303,11 @@ class AppConfig {
func getMixpanelToken() -> String? {
return mixpanelToken
}

func getTextToSpeechKey() -> String? {
return textToSpeechKey
}

}

extension AppConfig {
Expand Down
28 changes: 0 additions & 28 deletions voice/voice-ai/x/Converter/TextToSpeechConverter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ class TextToSpeechConverter: NSObject, TextToSpeechConverterProtocol {
var isSpeaking: Bool {
return synthesizer.isSpeaking
}
var showDownloadVoicePromptCalled: Bool = false

private(set) var isDefaultVoiceUsed = false
let alertManager = AlertManager(viewControllerProvider: {
Expand Down Expand Up @@ -120,33 +119,6 @@ class TextToSpeechConverter: NSObject, TextToSpeechConverterProtocol {
synthesizer.continueSpeaking()
}
}

func isPremiumOrEnhancedVoice(voiceIdentifier: String) -> Bool {
let lowercasedIdentifier = voiceIdentifier.lowercased()
return lowercasedIdentifier.contains("premium")
}

func checkAndPromptForPremiumVoice(voiceIdentifier: String? = nil) {
guard let currentVoiceIdentifier = voiceIdentifier ?? AVSpeechSynthesisVoice(language: getLanguageCode())?.identifier else {
return
}

print("currentVoice: \(currentVoiceIdentifier)")
print("Is the voice premium? \(isPremiumOrEnhancedVoice(voiceIdentifier: currentVoiceIdentifier))")

if !isPremiumOrEnhancedVoice(voiceIdentifier: currentVoiceIdentifier) {
showDownloadVoicePrompt()
}
}

func showDownloadVoicePrompt() {
// The prompt should guide the user on how to download a premium voice
DispatchQueue.main.asyncAfter(deadline: .now() + 10.0) {
let okAction = UIAlertAction(title: String(localized: "button.ok"), style: .default)
self.alertManager.showAlertForSettings(title: "Enhance Your Experience", message: "Download a premium voice for a better experience. Go to Settings > Accessibility > Spoken Content > Voices to choose and download a premium voice.", actions: [okAction])
}
showDownloadVoicePromptCalled = true
}
}

extension TextToSpeechConverter: AVSpeechSynthesizerDelegate {}
19 changes: 8 additions & 11 deletions voice/voice-ai/x/NetworkManager/Twitter/TwitterManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,22 +60,22 @@ class TwitterManager: ObservableObject {
}
}

func getAllTwitterListDetails(completion: @escaping (String) -> Void) {
func getAllTwitterListDetails(completion: @escaping ([String]) -> Void) {
var details: [String] = []

// Create a DispatchGroup
let dispatchGroup = DispatchGroup()

for list in twitterLists {
// Enter the DispatchGroup before starting each asynchronous task
dispatchGroup.enter()

TwitterAPI().getTwitterListBy(name: list.name ?? "") { result in
switch result {
case .success(let fetchedLists):
let combinedText = fetchedLists.data.compactMap { $0.text }.joined(separator: "\n")
details.append(combinedText)

let texts = fetchedLists.data.compactMap { $0.text }
details.append(contentsOf: texts)
case .failure(let error):
print("Error fetching lists: \(error)")
// Handle the error appropriately
Expand All @@ -85,14 +85,11 @@ class TwitterManager: ObservableObject {
dispatchGroup.leave()
}
}

// Notify when all asynchronous tasks are completed
dispatchGroup.notify(queue: .main) {
// Combine all the details into a single string
let finalDetails = details.joined(separator: "\n")

// Call the completion handler with the final string
completion(finalDetails)
// Call the completion handler with the final tweets array
completion(details)
}
}
}
Binary file added voice/voice-ai/x/Resources/Hey.mp3
Binary file not shown.
63 changes: 63 additions & 0 deletions voice/voice-ai/x/SpeechRecognition/OpenAITextToSpeech.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@

import Foundation
import Sentry

class OpenAITextToSpeech: NSObject {

var currentDataTask: URLSessionDataTask?

func fetchAudioData(text: String, completion: @escaping (Result<Data, Error>) -> Void) {
guard let url = URL(string: "https://api.openai.com/v1/audio/speech") else {
completion(.failure(NSError(domain: "OpenAITextToSpeechError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])))
return
}

guard let apiKey = AppConfig.shared.getTextToSpeechKey() else {
SentrySDK.capture(message: "OpenAI API key is nil")
return
}

var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")

let body: [String: Any] = [
"model": "tts-1",
"input": text,
"voice": "nova"
]

do {
request.httpBody = try JSONSerialization.data(withJSONObject: body)
} catch {
completion(.failure(error))
return
}

currentDataTask = URLSession.shared.dataTask(with: request) { data, response, error in
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can stream the response in chunks and play in real time without waiting for the whole file https://platform.openai.com/docs/guides/text-to-speech/streaming-real-time-audio

guard let data = data else {
completion(.failure(error ?? NSError(domain: "OpenAITextToSpeechError", code: 0, userInfo: nil)))
return
}
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
httpResponse.mimeType == "audio/mpeg" {
completion(.success(data))
} else {
self.handleError(nil, message: "Received non-audio response or error from API")
}
}
currentDataTask?.resume()
}

func cancelAudioDataFetch() {
currentDataTask?.cancel()
currentDataTask = nil
}
private func handleError(_ error: Error?, message: String) {
// Implement user-friendly error handling
print(message, error as Any)
// Additional error handling logic can be added here
}
}
Loading
Loading