Added Assistant Mode in Prompt + Cleaner Code seperation
This commit is contained in:
parent
4b79d200ff
commit
6a376dd5ad
@ -5,8 +5,7 @@ enum PredefinedPrompts {
|
||||
|
||||
// Static UUIDs for predefined prompts
|
||||
private static let defaultPromptId = UUID(uuidString: "00000000-0000-0000-0000-000000000001")!
|
||||
private static let chatStylePromptId = UUID(uuidString: "00000000-0000-0000-0000-000000000002")!
|
||||
private static let emailPromptId = UUID(uuidString: "00000000-0000-0000-0000-000000000003")!
|
||||
private static let assistantPromptId = UUID(uuidString: "00000000-0000-0000-0000-000000000002")!
|
||||
|
||||
static var all: [CustomPrompt] {
|
||||
// Always return the latest predefined prompts from source code
|
||||
@ -71,54 +70,38 @@ enum PredefinedPrompts {
|
||||
),
|
||||
|
||||
CustomPrompt(
|
||||
id: chatStylePromptId,
|
||||
title: "Chat",
|
||||
id: assistantPromptId,
|
||||
title: "Assistant",
|
||||
promptText: """
|
||||
Primary Rules:
|
||||
We are in a causual chat conversation.
|
||||
1. Focus on clarity while preserving the speaker's personality:
|
||||
- Keep personality markers that show intent or style (e.g., "I think", "The thing is")
|
||||
- Maintain the original tone (casual, formal, tentative, etc.)
|
||||
2. Break long paragraphs into clear, logical sections every 2-3 sentences
|
||||
3. Fix grammar and punctuation errors based on context
|
||||
4. Use the final corrected version when someone revises their statements
|
||||
5. Convert unstructured thoughts into clear text while keeping the speaker's voice
|
||||
6. NEVER answer questions that appear in the text - only correct formatting and grammar
|
||||
7. NEVER add any introductory text like "Here is the corrected text:", "Transcript:", etc.
|
||||
8. NEVER add content not present in the source text
|
||||
9. NEVER add sign-offs or acknowledgments
|
||||
10. Correct speech-to-text transcription errors based on context.
|
||||
Provide a direct clear, and concise reply to the user's query. Use the available context if directly related to the user's query.
|
||||
Remember to:
|
||||
1. Be helpful and informative
|
||||
2. Be accurate and precise
|
||||
3. Don't add meta commentary or anything extra other than the actual answer
|
||||
6. Maintain a friendly, casual tone
|
||||
|
||||
Examples:
|
||||
Use the following information if provided:
|
||||
1. Active Window Context:
|
||||
IMPORTANT: Only use window content when directly relevant to input
|
||||
- Use application name and window title for understanding the context
|
||||
- Reference captured text from the window
|
||||
- Preserve application-specific terms and formatting
|
||||
- Help resolve unclear terms or phrases
|
||||
|
||||
Input: "so like i tried this new restaurant yesterday you know the one near the mall and um the pasta was really good i think i'll go back there soon"
|
||||
2. Available Clipboard Content:
|
||||
IMPORTANT: Only use when directly relevant to input
|
||||
- Use for additional context
|
||||
- Help resolve unclear references
|
||||
- Ignore unrelated clipboard content
|
||||
|
||||
Output: "I tried this new restaurant near the mall yesterday! 🍽️
|
||||
|
||||
The pasta was really good. I think I'll go back there soon! 😊"
|
||||
|
||||
Input: "we need to finish the project by friday no wait thursday because the client meeting is on friday morning and we still need to test everything"
|
||||
|
||||
Output: "We need to finish the project by Thursday (not Friday) ⏰ because the client meeting is on Friday morning.
|
||||
|
||||
We still need to test everything! ✅"
|
||||
|
||||
Input: "my phone is like three years old now and the battery is terrible i have to charge it like twice a day i think i need a new one"
|
||||
|
||||
Output: "My phone is three years old now and the battery is terrible. 📱
|
||||
|
||||
I have to charge it twice a day. I think I need a new one! 🔋"
|
||||
|
||||
Input: "went for a run yesterday it was nice weather and i saw this cute dog in the park wish i took a picture"
|
||||
|
||||
Output: "Went for a run yesterday! 🏃♀️
|
||||
|
||||
It was nice weather and I saw this cute dog in the park. 🐶
|
||||
|
||||
Wish I took a picture! 📸"
|
||||
3. Examples:
|
||||
- Follow the correction patterns shown in examples
|
||||
- Match the formatting style of similar texts
|
||||
- Use consistent terminology with examples
|
||||
- Learn from previous corrections
|
||||
""",
|
||||
icon: .chatFill,
|
||||
description: "Casual chat-style formatting",
|
||||
description: "AI assistant that provides direct answers to queries",
|
||||
isPredefined: true
|
||||
)
|
||||
]
|
||||
|
||||
@ -26,6 +26,57 @@ enum PromptTemplates {
|
||||
|
||||
static func createTemplatePrompts() -> [TemplatePrompt] {
|
||||
[
|
||||
TemplatePrompt(
|
||||
id: UUID(),
|
||||
title: "AI Assistant",
|
||||
promptText: """
|
||||
Primary Rules:
|
||||
We are in a causual chat conversation.
|
||||
1. Focus on clarity while preserving the speaker's personality:
|
||||
- Keep personality markers that show intent or style (e.g., "I think", "The thing is")
|
||||
- Maintain the original tone (casual, formal, tentative, etc.)
|
||||
2. Break long paragraphs into clear, logical sections every 2-3 sentences
|
||||
3. Fix grammar and punctuation errors based on context
|
||||
4. Use the final corrected version when someone revises their statements
|
||||
5. Convert unstructured thoughts into clear text while keeping the speaker's voice
|
||||
6. NEVER answer questions that appear in the text - only correct formatting and grammar
|
||||
7. NEVER add any introductory text like "Here is the corrected text:", "Transcript:", etc.
|
||||
8. NEVER add content not present in the source text
|
||||
9. NEVER add sign-offs or acknowledgments
|
||||
10. Correct speech-to-text transcription errors based on context.
|
||||
|
||||
Examples:
|
||||
|
||||
Input: "so like i tried this new restaurant yesterday you know the one near the mall and um the pasta was really good i think i'll go back there soon"
|
||||
|
||||
Output: "I tried this new restaurant near the mall yesterday! 🍽️
|
||||
|
||||
The pasta was really good. I think I'll go back there soon! 😊"
|
||||
|
||||
Input: "we need to finish the project by friday no wait thursday because the client meeting is on friday morning and we still need to test everything"
|
||||
|
||||
Output: "We need to finish the project by Thursday (not Friday) ⏰ because the client meeting is on Friday morning.
|
||||
|
||||
We still need to test everything! ✅"
|
||||
|
||||
Input: "my phone is like three years old now and the battery is terrible i have to charge it like twice a day i think i need a new one"
|
||||
|
||||
Output: "My phone is three years old now and the battery is terrible. 📱
|
||||
|
||||
I have to charge it twice a day. I think I need a new one! 🔋"
|
||||
|
||||
Input: "went for a run yesterday it was nice weather and i saw this cute dog in the park wish i took a picture"
|
||||
|
||||
Output: "Went for a run yesterday! 🏃♀️
|
||||
|
||||
It was nice weather and I saw this cute dog in the park. 🐶
|
||||
|
||||
Wish I took a picture! 📸"
|
||||
""",
|
||||
icon: .chatFill,
|
||||
description: "Casual chat-style formatting"
|
||||
),
|
||||
|
||||
TemplatePrompt(
|
||||
id: UUID(),
|
||||
title: "Email",
|
||||
|
||||
160
VoiceInk/Whisper/WhisperState+ModelManager.swift
Normal file
160
VoiceInk/Whisper/WhisperState+ModelManager.swift
Normal file
@ -0,0 +1,160 @@
|
||||
import Foundation
|
||||
import os
|
||||
|
||||
// MARK: - Model Management Extension
|
||||
extension WhisperState {
|
||||
|
||||
// MARK: - Model Directory Management
|
||||
|
||||
func createModelsDirectoryIfNeeded() {
|
||||
do {
|
||||
try FileManager.default.createDirectory(at: modelsDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
} catch {
|
||||
messageLog += "Error creating models directory: \(error.localizedDescription)\n"
|
||||
}
|
||||
}
|
||||
|
||||
func loadAvailableModels() {
|
||||
do {
|
||||
let fileURLs = try FileManager.default.contentsOfDirectory(at: modelsDirectory, includingPropertiesForKeys: nil)
|
||||
availableModels = fileURLs.compactMap { url in
|
||||
guard url.pathExtension == "bin" else { return nil }
|
||||
return WhisperModel(name: url.deletingPathExtension().lastPathComponent, url: url)
|
||||
}
|
||||
} catch {
|
||||
messageLog += "Error loading available models: \(error.localizedDescription)\n"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Model Loading
|
||||
|
||||
func loadModel(_ model: WhisperModel) async throws {
|
||||
guard whisperContext == nil else { return }
|
||||
|
||||
logger.notice("🔄 Loading Whisper model: \(model.name)")
|
||||
isModelLoading = true
|
||||
defer { isModelLoading = false }
|
||||
|
||||
do {
|
||||
whisperContext = try await WhisperContext.createContext(path: model.url.path)
|
||||
isModelLoaded = true
|
||||
currentModel = model
|
||||
logger.notice("✅ Successfully loaded model: \(model.name)")
|
||||
} catch {
|
||||
logger.error("❌ Failed to load model: \(model.name) - \(error.localizedDescription)")
|
||||
throw WhisperStateError.modelLoadFailed
|
||||
}
|
||||
}
|
||||
|
||||
func setDefaultModel(_ model: WhisperModel) async {
|
||||
do {
|
||||
currentModel = model
|
||||
UserDefaults.standard.set(model.name, forKey: "CurrentModel")
|
||||
canTranscribe = true
|
||||
} catch {
|
||||
currentError = error as? WhisperStateError ?? .unknownError
|
||||
canTranscribe = false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Model Download & Management
|
||||
|
||||
func downloadModel(_ model: PredefinedModel) async {
|
||||
guard let url = URL(string: model.downloadURL) else { return }
|
||||
|
||||
logger.notice("🔽 Downloading model: \(model.name)")
|
||||
do {
|
||||
let (data, response) = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<(Data, URLResponse), Error>) in
|
||||
let task = URLSession.shared.dataTask(with: url) { data, response, error in
|
||||
if let error = error {
|
||||
continuation.resume(throwing: error)
|
||||
return
|
||||
}
|
||||
guard let httpResponse = response as? HTTPURLResponse,
|
||||
(200...299).contains(httpResponse.statusCode),
|
||||
let data = data else {
|
||||
continuation.resume(throwing: URLError(.badServerResponse))
|
||||
return
|
||||
}
|
||||
continuation.resume(returning: (data, httpResponse))
|
||||
}
|
||||
|
||||
task.resume()
|
||||
|
||||
let observation = task.progress.observe(\.fractionCompleted) { progress, _ in
|
||||
DispatchQueue.main.async {
|
||||
self.downloadProgress[model.name] = progress.fractionCompleted
|
||||
}
|
||||
}
|
||||
|
||||
Task {
|
||||
await withTaskCancellationHandler {
|
||||
observation.invalidate()
|
||||
} operation: {
|
||||
await withCheckedContinuation { (_: CheckedContinuation<Void, Never>) in }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let destinationURL = modelsDirectory.appendingPathComponent(model.filename)
|
||||
try data.write(to: destinationURL)
|
||||
|
||||
availableModels.append(WhisperModel(name: model.name, url: destinationURL))
|
||||
self.downloadProgress.removeValue(forKey: model.name)
|
||||
logger.notice("✅ Successfully downloaded model: \(model.name)")
|
||||
} catch {
|
||||
logger.error("❌ Failed to download model: \(model.name) - \(error.localizedDescription)")
|
||||
currentError = .modelDownloadFailed
|
||||
self.downloadProgress.removeValue(forKey: model.name)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteModel(_ model: WhisperModel) async {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: model.url)
|
||||
availableModels.removeAll { $0.id == model.id }
|
||||
if currentModel?.id == model.id {
|
||||
currentModel = nil
|
||||
canTranscribe = false
|
||||
}
|
||||
} catch {
|
||||
messageLog += "Error deleting model: \(error.localizedDescription)\n"
|
||||
currentError = .modelDeletionFailed
|
||||
}
|
||||
}
|
||||
|
||||
func unloadModel() {
|
||||
Task {
|
||||
await whisperContext?.releaseResources()
|
||||
whisperContext = nil
|
||||
isModelLoaded = false
|
||||
|
||||
if let recordedFile = recordedFile {
|
||||
try? FileManager.default.removeItem(at: recordedFile)
|
||||
self.recordedFile = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func clearDownloadedModels() async {
|
||||
for model in availableModels {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: model.url)
|
||||
} catch {
|
||||
messageLog += "Error deleting model: \(error.localizedDescription)\n"
|
||||
}
|
||||
}
|
||||
availableModels.removeAll()
|
||||
}
|
||||
|
||||
// MARK: - Resource Management
|
||||
|
||||
func cleanupModelResources() async {
|
||||
if !isRecording && !isProcessing {
|
||||
logger.notice("🧹 Cleaning up Whisper resources")
|
||||
await whisperContext?.releaseResources()
|
||||
whisperContext = nil
|
||||
isModelLoaded = false
|
||||
}
|
||||
}
|
||||
}
|
||||
120
VoiceInk/Whisper/WhisperState+UI.swift
Normal file
120
VoiceInk/Whisper/WhisperState+UI.swift
Normal file
@ -0,0 +1,120 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import os
|
||||
|
||||
// MARK: - UI Management Extension
|
||||
extension WhisperState {
|
||||
|
||||
// MARK: - Recorder Panel Management
|
||||
|
||||
func showRecorderPanel() {
|
||||
logger.notice("📱 Showing \(self.recorderType) recorder")
|
||||
if recorderType == "notch" {
|
||||
if notchWindowManager == nil {
|
||||
notchWindowManager = NotchWindowManager(whisperState: self, recorder: recorder)
|
||||
logger.info("Created new notch window manager")
|
||||
}
|
||||
notchWindowManager?.show()
|
||||
} else {
|
||||
if miniWindowManager == nil {
|
||||
miniWindowManager = MiniWindowManager(whisperState: self, recorder: recorder)
|
||||
logger.info("Created new mini window manager")
|
||||
}
|
||||
miniWindowManager?.show()
|
||||
}
|
||||
}
|
||||
|
||||
func hideRecorderPanel() {
|
||||
if isRecording {
|
||||
Task {
|
||||
await toggleRecord()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mini Recorder Management
|
||||
|
||||
func toggleMiniRecorder() async {
|
||||
if isMiniRecorderVisible {
|
||||
await dismissMiniRecorder()
|
||||
} else {
|
||||
Task {
|
||||
await toggleRecord()
|
||||
|
||||
SoundManager.shared.playStartSound()
|
||||
|
||||
await MainActor.run {
|
||||
showRecorderPanel()
|
||||
isMiniRecorderVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dismissMiniRecorder() async {
|
||||
logger.notice("📱 Dismissing \(self.recorderType) recorder")
|
||||
shouldCancelRecording = true
|
||||
if isRecording {
|
||||
await recorder.stopRecording()
|
||||
}
|
||||
|
||||
if recorderType == "notch" {
|
||||
notchWindowManager?.hide()
|
||||
} else {
|
||||
miniWindowManager?.hide()
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
isRecording = false
|
||||
isVisualizerActive = false
|
||||
isProcessing = false
|
||||
isTranscribing = false
|
||||
canTranscribe = true
|
||||
isMiniRecorderVisible = false
|
||||
shouldCancelRecording = false
|
||||
}
|
||||
|
||||
try? await Task.sleep(nanoseconds: 150_000_000)
|
||||
await cleanupModelResources()
|
||||
}
|
||||
|
||||
func cancelRecording() async {
|
||||
shouldCancelRecording = true
|
||||
SoundManager.shared.playEscSound()
|
||||
if isRecording {
|
||||
await recorder.stopRecording()
|
||||
}
|
||||
await dismissMiniRecorder()
|
||||
}
|
||||
|
||||
// MARK: - Notification Handling
|
||||
|
||||
func setupNotifications() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(handleToggleMiniRecorder), name: .toggleMiniRecorder, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(handleLicenseStatusChanged), name: .licenseStatusChanged, object: nil)
|
||||
}
|
||||
|
||||
@objc public func handleToggleMiniRecorder() {
|
||||
if isMiniRecorderVisible {
|
||||
Task {
|
||||
await toggleRecord()
|
||||
}
|
||||
} else {
|
||||
Task {
|
||||
await toggleRecord()
|
||||
|
||||
SoundManager.shared.playStartSound()
|
||||
|
||||
await MainActor.run {
|
||||
showRecorderPanel()
|
||||
isMiniRecorderVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handleLicenseStatusChanged() {
|
||||
// This will refresh the license state when it changes elsewhere in the app
|
||||
self.licenseViewModel = LicenseViewModel()
|
||||
}
|
||||
}
|
||||
@ -32,9 +32,21 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private var whisperContext: WhisperContext?
|
||||
private let recorder = Recorder()
|
||||
private var recordedFile: URL? = nil
|
||||
@Published var isVisualizerActive = false
|
||||
|
||||
@Published var isMiniRecorderVisible = false {
|
||||
didSet {
|
||||
if isMiniRecorderVisible {
|
||||
showRecorderPanel()
|
||||
} else {
|
||||
hideRecorderPanel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var whisperContext: WhisperContext?
|
||||
let recorder = Recorder()
|
||||
var recordedFile: URL? = nil
|
||||
let whisperPrompt = WhisperPrompt()
|
||||
|
||||
let modelContext: ModelContext
|
||||
@ -61,11 +73,14 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
|
||||
let modelsDirectory: URL
|
||||
let recordingsDirectory: URL
|
||||
let enhancementService: AIEnhancementService?
|
||||
private var licenseViewModel: LicenseViewModel
|
||||
private let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "WhisperState")
|
||||
var licenseViewModel: LicenseViewModel
|
||||
let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "WhisperState")
|
||||
private var transcriptionStartTime: Date?
|
||||
private var notchWindowManager: NotchWindowManager?
|
||||
private var miniWindowManager: MiniWindowManager?
|
||||
var notchWindowManager: NotchWindowManager?
|
||||
var miniWindowManager: MiniWindowManager?
|
||||
|
||||
// For model progress tracking
|
||||
@Published var downloadProgress: [String: Double] = [:]
|
||||
|
||||
init(modelContext: ModelContext, enhancementService: AIEnhancementService? = nil) {
|
||||
self.modelContext = modelContext
|
||||
@ -91,14 +106,6 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private func createModelsDirectoryIfNeeded() {
|
||||
do {
|
||||
try FileManager.default.createDirectory(at: modelsDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
} catch {
|
||||
messageLog += "Error creating models directory: \(error.localizedDescription)\n"
|
||||
}
|
||||
}
|
||||
|
||||
private func createRecordingsDirectoryIfNeeded() {
|
||||
do {
|
||||
try FileManager.default.createDirectory(at: recordingsDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
@ -107,47 +114,6 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private func loadAvailableModels() {
|
||||
do {
|
||||
let fileURLs = try FileManager.default.contentsOfDirectory(at: modelsDirectory, includingPropertiesForKeys: nil)
|
||||
availableModels = fileURLs.compactMap { url in
|
||||
guard url.pathExtension == "bin" else { return nil }
|
||||
return WhisperModel(name: url.deletingPathExtension().lastPathComponent, url: url)
|
||||
}
|
||||
} catch {
|
||||
messageLog += "Error loading available models: \(error.localizedDescription)\n"
|
||||
}
|
||||
}
|
||||
|
||||
private func loadModel(_ model: WhisperModel) async throws {
|
||||
guard whisperContext == nil else { return }
|
||||
|
||||
logger.notice("🔄 Loading Whisper model: \(model.name)")
|
||||
isModelLoading = true
|
||||
defer { isModelLoading = false }
|
||||
|
||||
do {
|
||||
whisperContext = try await WhisperContext.createContext(path: model.url.path)
|
||||
isModelLoaded = true
|
||||
currentModel = model
|
||||
logger.notice("✅ Successfully loaded model: \(model.name)")
|
||||
} catch {
|
||||
logger.error("❌ Failed to load model: \(model.name) - \(error.localizedDescription)")
|
||||
throw WhisperStateError.modelLoadFailed
|
||||
}
|
||||
}
|
||||
|
||||
func setDefaultModel(_ model: WhisperModel) async {
|
||||
do {
|
||||
currentModel = model
|
||||
UserDefaults.standard.set(model.name, forKey: "CurrentModel")
|
||||
canTranscribe = true
|
||||
} catch {
|
||||
currentError = error as? WhisperStateError ?? .unknownError
|
||||
canTranscribe = false
|
||||
}
|
||||
}
|
||||
|
||||
func toggleRecord() async {
|
||||
if isRecording {
|
||||
logger.notice("🛑 Stopping recording")
|
||||
@ -257,58 +223,6 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
|
||||
private func onDidFinishRecording(success: Bool) {
|
||||
isRecording = false
|
||||
}
|
||||
|
||||
@Published var downloadProgress: [String: Double] = [:]
|
||||
|
||||
func downloadModel(_ model: PredefinedModel) async {
|
||||
guard let url = URL(string: model.downloadURL) else { return }
|
||||
|
||||
logger.notice("🔽 Downloading model: \(model.name)")
|
||||
do {
|
||||
let (data, response) = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<(Data, URLResponse), Error>) in
|
||||
let task = URLSession.shared.dataTask(with: url) { data, response, error in
|
||||
if let error = error {
|
||||
continuation.resume(throwing: error)
|
||||
return
|
||||
}
|
||||
guard let httpResponse = response as? HTTPURLResponse,
|
||||
(200...299).contains(httpResponse.statusCode),
|
||||
let data = data else {
|
||||
continuation.resume(throwing: URLError(.badServerResponse))
|
||||
return
|
||||
}
|
||||
continuation.resume(returning: (data, httpResponse))
|
||||
}
|
||||
|
||||
task.resume()
|
||||
|
||||
let observation = task.progress.observe(\.fractionCompleted) { progress, _ in
|
||||
DispatchQueue.main.async {
|
||||
self.downloadProgress[model.name] = progress.fractionCompleted
|
||||
}
|
||||
}
|
||||
|
||||
Task {
|
||||
await withTaskCancellationHandler {
|
||||
observation.invalidate()
|
||||
} operation: {
|
||||
await withCheckedContinuation { (_: CheckedContinuation<Void, Never>) in }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let destinationURL = modelsDirectory.appendingPathComponent(model.filename)
|
||||
try data.write(to: destinationURL)
|
||||
|
||||
availableModels.append(WhisperModel(name: model.name, url: destinationURL))
|
||||
self.downloadProgress.removeValue(forKey: model.name)
|
||||
logger.notice("✅ Successfully downloaded model: \(model.name)")
|
||||
} catch {
|
||||
logger.error("❌ Failed to download model: \(model.name) - \(error.localizedDescription)")
|
||||
currentError = .modelDownloadFailed
|
||||
self.downloadProgress.removeValue(forKey: model.name)
|
||||
}
|
||||
}
|
||||
|
||||
private func transcribeAudio(_ url: URL, duration: TimeInterval) async {
|
||||
if shouldCancelRecording { return }
|
||||
@ -328,7 +242,7 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
|
||||
logger.error("❌ Failed to load model: \(currentModel.name) - \(error.localizedDescription)")
|
||||
messageLog += "Failed to load transcription model. Please try again.\n"
|
||||
currentError = .modelLoadFailed
|
||||
await cleanupResources()
|
||||
await cleanupModelResources()
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -350,7 +264,7 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
|
||||
let permanentURLString = permanentURL.absoluteString
|
||||
|
||||
if shouldCancelRecording {
|
||||
await cleanupResources()
|
||||
await cleanupModelResources()
|
||||
return
|
||||
}
|
||||
|
||||
@ -358,7 +272,7 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
|
||||
let data = try readAudioSamples(url)
|
||||
|
||||
if shouldCancelRecording {
|
||||
await cleanupResources()
|
||||
await cleanupModelResources()
|
||||
return
|
||||
}
|
||||
|
||||
@ -367,14 +281,14 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
|
||||
await whisperContext.setPrompt(whisperPrompt.transcriptionPrompt)
|
||||
|
||||
if shouldCancelRecording {
|
||||
await cleanupResources()
|
||||
await cleanupModelResources()
|
||||
return
|
||||
}
|
||||
|
||||
await whisperContext.fullTranscribe(samples: data)
|
||||
|
||||
if shouldCancelRecording {
|
||||
await cleanupResources()
|
||||
await cleanupModelResources()
|
||||
return
|
||||
}
|
||||
|
||||
@ -393,7 +307,7 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
|
||||
enhancementService.isConfigured {
|
||||
do {
|
||||
if shouldCancelRecording {
|
||||
await cleanupResources()
|
||||
await cleanupModelResources()
|
||||
return
|
||||
}
|
||||
|
||||
@ -461,14 +375,14 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
await cleanupResources()
|
||||
await cleanupModelResources()
|
||||
await dismissMiniRecorder()
|
||||
|
||||
} catch {
|
||||
messageLog += "\(error.localizedDescription)\n"
|
||||
currentError = .transcriptionFailed
|
||||
|
||||
await cleanupResources()
|
||||
await cleanupModelResources()
|
||||
await dismissMiniRecorder()
|
||||
}
|
||||
}
|
||||
@ -488,174 +402,8 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
|
||||
return floats
|
||||
}
|
||||
|
||||
func deleteModel(_ model: WhisperModel) async {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: model.url)
|
||||
availableModels.removeAll { $0.id == model.id }
|
||||
if currentModel?.id == model.id {
|
||||
currentModel = nil
|
||||
canTranscribe = false
|
||||
}
|
||||
} catch {
|
||||
messageLog += "Error deleting model: \(error.localizedDescription)\n"
|
||||
currentError = .modelDeletionFailed
|
||||
}
|
||||
}
|
||||
|
||||
@Published var isVisualizerActive = false
|
||||
|
||||
@Published var isMiniRecorderVisible = false {
|
||||
didSet {
|
||||
if isMiniRecorderVisible {
|
||||
showRecorderPanel()
|
||||
} else {
|
||||
hideRecorderPanel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupNotifications() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(handleToggleMiniRecorder), name: .toggleMiniRecorder, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(handleLicenseStatusChanged), name: .licenseStatusChanged, object: nil)
|
||||
}
|
||||
|
||||
@objc public func handleToggleMiniRecorder() {
|
||||
if isMiniRecorderVisible {
|
||||
Task {
|
||||
await toggleRecord()
|
||||
}
|
||||
} else {
|
||||
Task {
|
||||
await toggleRecord()
|
||||
|
||||
SoundManager.shared.playStartSound()
|
||||
|
||||
await MainActor.run {
|
||||
showRecorderPanel()
|
||||
isMiniRecorderVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleLicenseStatusChanged() {
|
||||
// This will refresh the license state when it changes elsewhere in the app
|
||||
self.licenseViewModel = LicenseViewModel()
|
||||
}
|
||||
|
||||
private func showRecorderPanel() {
|
||||
logger.notice("📱 Showing \(self.recorderType) recorder")
|
||||
if recorderType == "notch" {
|
||||
if notchWindowManager == nil {
|
||||
notchWindowManager = NotchWindowManager(whisperState: self, recorder: recorder)
|
||||
logger.info("Created new notch window manager")
|
||||
}
|
||||
notchWindowManager?.show()
|
||||
} else {
|
||||
if miniWindowManager == nil {
|
||||
miniWindowManager = MiniWindowManager(whisperState: self, recorder: recorder)
|
||||
logger.info("Created new mini window manager")
|
||||
}
|
||||
miniWindowManager?.show()
|
||||
}
|
||||
}
|
||||
|
||||
private func hideRecorderPanel() {
|
||||
if isRecording {
|
||||
Task {
|
||||
await toggleRecord()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toggleMiniRecorder() async {
|
||||
if isMiniRecorderVisible {
|
||||
await dismissMiniRecorder()
|
||||
} else {
|
||||
Task {
|
||||
await toggleRecord()
|
||||
|
||||
SoundManager.shared.playStartSound()
|
||||
|
||||
await MainActor.run {
|
||||
showRecorderPanel()
|
||||
isMiniRecorderVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func cleanupResources() async {
|
||||
if !isRecording && !isProcessing {
|
||||
logger.notice("🧹 Cleaning up Whisper resources")
|
||||
await whisperContext?.releaseResources()
|
||||
whisperContext = nil
|
||||
isModelLoaded = false
|
||||
}
|
||||
}
|
||||
|
||||
func dismissMiniRecorder() async {
|
||||
logger.notice("📱 Dismissing \(self.recorderType) recorder")
|
||||
shouldCancelRecording = true
|
||||
if isRecording {
|
||||
await recorder.stopRecording()
|
||||
}
|
||||
|
||||
if recorderType == "notch" {
|
||||
notchWindowManager?.hide()
|
||||
} else {
|
||||
miniWindowManager?.hide()
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
isRecording = false
|
||||
isVisualizerActive = false
|
||||
isProcessing = false
|
||||
isTranscribing = false
|
||||
canTranscribe = true
|
||||
isMiniRecorderVisible = false
|
||||
shouldCancelRecording = false
|
||||
}
|
||||
|
||||
try? await Task.sleep(nanoseconds: 150_000_000)
|
||||
await cleanupResources()
|
||||
}
|
||||
|
||||
func cancelRecording() async {
|
||||
shouldCancelRecording = true
|
||||
SoundManager.shared.playEscSound()
|
||||
if isRecording {
|
||||
await recorder.stopRecording()
|
||||
}
|
||||
await dismissMiniRecorder()
|
||||
}
|
||||
|
||||
@Published var currentError: WhisperStateError?
|
||||
|
||||
func unloadModel() {
|
||||
Task {
|
||||
await whisperContext?.releaseResources()
|
||||
whisperContext = nil
|
||||
isModelLoaded = false
|
||||
|
||||
if let recordedFile = recordedFile {
|
||||
try? FileManager.default.removeItem(at: recordedFile)
|
||||
self.recordedFile = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func clearDownloadedModels() async {
|
||||
for model in availableModels {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: model.url)
|
||||
} catch {
|
||||
messageLog += "Error deleting model: \(error.localizedDescription)\n"
|
||||
}
|
||||
}
|
||||
availableModels.removeAll()
|
||||
}
|
||||
|
||||
func getEnhancementService() -> AIEnhancementService? {
|
||||
return enhancementService
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user