|
|
|
|
@@ -15,6 +15,35 @@ struct Defaults {
|
|
|
|
|
static let livePreview = "livePreview"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Icons
|
|
|
|
|
// SF Symbols: vector icons that stay sharp at any scale. Template symbols are
|
|
|
|
|
// tinted by macOS (menu bar, menus, buttons) and follow light/dark appearance.
|
|
|
|
|
enum Icons {
|
|
|
|
|
static func symbol(_ name: String, pointSize: CGFloat = 13, weight: NSFont.Weight = .regular, description: String? = nil) -> NSImage? {
|
|
|
|
|
let image = NSImage(systemSymbolName: name, accessibilityDescription: description)?
|
|
|
|
|
.withSymbolConfiguration(.init(pointSize: pointSize, weight: weight))
|
|
|
|
|
image?.isTemplate = true
|
|
|
|
|
return image
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fixed colour, for states that need it (recording) and for custom drawing (HUD).
|
|
|
|
|
// Tints the monochrome symbol so knockouts stay transparent (the tick in
|
|
|
|
|
// checkmark.circle.fill); a one-colour palette would fill them in. Drawn
|
|
|
|
|
// lazily, so it stays sharp on any display scale.
|
|
|
|
|
static func coloredSymbol(_ name: String, pointSize: CGFloat, weight: NSFont.Weight = .regular, color: NSColor, description: String? = nil) -> NSImage? {
|
|
|
|
|
guard let symbol = symbol(name, pointSize: pointSize, weight: weight, description: description) else { return nil }
|
|
|
|
|
let image = NSImage(size: symbol.size, flipped: false) { rect in
|
|
|
|
|
symbol.draw(in: rect)
|
|
|
|
|
color.set()
|
|
|
|
|
rect.fill(using: .sourceAtop)
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
image.accessibilityDescription = description
|
|
|
|
|
image.isTemplate = false
|
|
|
|
|
return image
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Whisper Models
|
|
|
|
|
struct WhisperModels {
|
|
|
|
|
struct Model {
|
|
|
|
|
@@ -681,24 +710,40 @@ final class MeterView: NSView {
|
|
|
|
|
}
|
|
|
|
|
func reset() { level = 0; silent = false; mode = .recording; notice = nil; previewText = nil; needsDisplay = true }
|
|
|
|
|
|
|
|
|
|
// Draws a symbol centred on a text line drawn at `origin`; returns the x where the text starts
|
|
|
|
|
private static func drawSymbol(_ name: String, color: NSColor, font: NSFont, at origin: NSPoint) -> CGFloat {
|
|
|
|
|
guard let image = Icons.coloredSymbol(name, pointSize: font.pointSize, weight: .medium, color: color) else { return origin.x }
|
|
|
|
|
let lineHeight = ceil(font.ascender - font.descender)
|
|
|
|
|
image.draw(in: NSRect(x: origin.x, y: origin.y + (lineHeight - image.size.height) / 2,
|
|
|
|
|
width: image.size.width, height: image.size.height))
|
|
|
|
|
return origin.x + image.size.width + 5
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override func draw(_ dirtyRect: NSRect) {
|
|
|
|
|
let bg = NSBezierPath(roundedRect: bounds, xRadius: 12, yRadius: 12)
|
|
|
|
|
NSColor(calibratedWhite: 0.10, alpha: 0.93).setFill()
|
|
|
|
|
bg.fill()
|
|
|
|
|
|
|
|
|
|
let warn = mode == .recording && silent
|
|
|
|
|
let label = mode == .transcribing ? "⏳ Transcribing…" : warn ? "🎤 No audio input?" : "🎤 Recording…"
|
|
|
|
|
let titleFont = NSFont.systemFont(ofSize: 12, weight: .medium)
|
|
|
|
|
let (symbol, symbolColor, label): (String, NSColor, String) =
|
|
|
|
|
mode == .transcribing ? ("hourglass", .white, "Transcribing…")
|
|
|
|
|
: warn ? ("mic.slash.fill", .systemRed, "No audio input?")
|
|
|
|
|
: ("mic.fill", .systemRed, "Recording…")
|
|
|
|
|
var textY = bounds.height - 26
|
|
|
|
|
label.draw(at: NSPoint(x: 16, y: textY), withAttributes: [
|
|
|
|
|
.font: NSFont.systemFont(ofSize: 12, weight: .medium),
|
|
|
|
|
let titleX = MeterView.drawSymbol(symbol, color: symbolColor, font: titleFont, at: NSPoint(x: 16, y: textY))
|
|
|
|
|
label.draw(at: NSPoint(x: titleX, y: textY), withAttributes: [
|
|
|
|
|
.font: titleFont,
|
|
|
|
|
.foregroundColor: warn ? NSColor.systemRed : NSColor.white
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
switch notice {
|
|
|
|
|
case .loadingModel?:
|
|
|
|
|
textY -= 19
|
|
|
|
|
"Loading model into memory…".draw(at: NSPoint(x: 16, y: textY), withAttributes: [
|
|
|
|
|
.font: NSFont.systemFont(ofSize: 11, weight: .semibold),
|
|
|
|
|
let font = NSFont.systemFont(ofSize: 11, weight: .semibold)
|
|
|
|
|
let x = MeterView.drawSymbol("memorychip", color: .systemYellow, font: font, at: NSPoint(x: 16, y: textY))
|
|
|
|
|
"Loading model into memory…".draw(at: NSPoint(x: x, y: textY), withAttributes: [
|
|
|
|
|
.font: font,
|
|
|
|
|
.foregroundColor: NSColor.systemYellow
|
|
|
|
|
])
|
|
|
|
|
textY -= 16
|
|
|
|
|
@@ -708,8 +753,10 @@ final class MeterView: NSView {
|
|
|
|
|
])
|
|
|
|
|
case .modelLoaded?:
|
|
|
|
|
textY -= 19
|
|
|
|
|
"✓ Model loaded".draw(at: NSPoint(x: 16, y: textY), withAttributes: [
|
|
|
|
|
.font: NSFont.systemFont(ofSize: 11, weight: .semibold),
|
|
|
|
|
let font = NSFont.systemFont(ofSize: 11, weight: .semibold)
|
|
|
|
|
let x = MeterView.drawSymbol("checkmark.circle.fill", color: .systemGreen, font: font, at: NSPoint(x: 16, y: textY))
|
|
|
|
|
"Model loaded".draw(at: NSPoint(x: x, y: textY), withAttributes: [
|
|
|
|
|
.font: font,
|
|
|
|
|
.foregroundColor: NSColor.systemGreen
|
|
|
|
|
])
|
|
|
|
|
case nil:
|
|
|
|
|
@@ -818,6 +865,7 @@ final class DownloadProgressWindow: NSObject {
|
|
|
|
|
private var phase = Phase.downloading
|
|
|
|
|
|
|
|
|
|
private var window: NSWindow?
|
|
|
|
|
private let titleIcon = NSImageView()
|
|
|
|
|
private let titleLabel = NSTextField(labelWithString: "")
|
|
|
|
|
// Two indicators instead of toggling isIndeterminate on one: after an
|
|
|
|
|
// indeterminate phase, NSProgressIndicator stops drawing determinate values.
|
|
|
|
|
@@ -871,6 +919,7 @@ final class DownloadProgressWindow: NSObject {
|
|
|
|
|
bytesPerSecond = 0
|
|
|
|
|
lastRender = .distantPast
|
|
|
|
|
titleLabel.stringValue = "Downloading \(modelName)"
|
|
|
|
|
setTitleIcon("arrow.down.circle.fill", color: .controlAccentColor)
|
|
|
|
|
setBar(indeterminate: true)
|
|
|
|
|
setDetail("Connecting…")
|
|
|
|
|
hintLabel.isHidden = false
|
|
|
|
|
@@ -916,6 +965,7 @@ final class DownloadProgressWindow: NSObject {
|
|
|
|
|
func showLoading(modelName: String) {
|
|
|
|
|
phase = .loading
|
|
|
|
|
titleLabel.stringValue = "Loading \(modelName)…"
|
|
|
|
|
setTitleIcon("memorychip.fill", color: .controlAccentColor)
|
|
|
|
|
setBar(indeterminate: true)
|
|
|
|
|
setDetail("Download complete. Loading the model into memory…")
|
|
|
|
|
hintLabel.isHidden = true
|
|
|
|
|
@@ -924,7 +974,8 @@ final class DownloadProgressWindow: NSObject {
|
|
|
|
|
|
|
|
|
|
func showReady(modelName: String) {
|
|
|
|
|
phase = .ready
|
|
|
|
|
titleLabel.stringValue = "✓ \(modelName) is ready"
|
|
|
|
|
titleLabel.stringValue = "\(modelName) is ready"
|
|
|
|
|
setTitleIcon("checkmark.circle.fill", color: .systemGreen)
|
|
|
|
|
setBar(indeterminate: false, value: 1)
|
|
|
|
|
setDetail("It is now the active model. Press ⌃⌥D to dictate.")
|
|
|
|
|
hintLabel.isHidden = true
|
|
|
|
|
@@ -933,7 +984,8 @@ final class DownloadProgressWindow: NSObject {
|
|
|
|
|
|
|
|
|
|
func showFailed(modelName: String, message: String) {
|
|
|
|
|
phase = .failed
|
|
|
|
|
titleLabel.stringValue = "⚠️ Could not download \(modelName)"
|
|
|
|
|
titleLabel.stringValue = "Could not download \(modelName)"
|
|
|
|
|
setTitleIcon("exclamationmark.triangle.fill", color: .systemYellow)
|
|
|
|
|
busyBar.stopAnimation(nil)
|
|
|
|
|
busyBar.isHidden = true
|
|
|
|
|
bar.isHidden = true
|
|
|
|
|
@@ -951,7 +1003,11 @@ final class DownloadProgressWindow: NSObject {
|
|
|
|
|
|
|
|
|
|
let v = NSView(frame: NSRect(x: 0, y: 0, width: 440, height: 170))
|
|
|
|
|
|
|
|
|
|
titleLabel.frame = NSRect(x: 20, y: 130, width: 400, height: 20)
|
|
|
|
|
titleIcon.frame = NSRect(x: 20, y: 131, width: 18, height: 18)
|
|
|
|
|
titleIcon.symbolConfiguration = .init(pointSize: 14, weight: .semibold)
|
|
|
|
|
v.addSubview(titleIcon)
|
|
|
|
|
|
|
|
|
|
titleLabel.frame = NSRect(x: 44, y: 130, width: 376, height: 20)
|
|
|
|
|
titleLabel.font = .boldSystemFont(ofSize: 13)
|
|
|
|
|
titleLabel.lineBreakMode = .byTruncatingTail
|
|
|
|
|
v.addSubview(titleLabel)
|
|
|
|
|
@@ -995,6 +1051,11 @@ final class DownloadProgressWindow: NSObject {
|
|
|
|
|
return w
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func setTitleIcon(_ name: String, color: NSColor) {
|
|
|
|
|
titleIcon.image = NSImage(systemSymbolName: name, accessibilityDescription: nil)
|
|
|
|
|
titleIcon.contentTintColor = color
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func setBar(indeterminate: Bool, value: Double = 0) {
|
|
|
|
|
bar.isHidden = indeterminate
|
|
|
|
|
busyBar.isHidden = !indeterminate
|
|
|
|
|
@@ -1162,7 +1223,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
|
|
|
|
|
if alert.runModal() == .alertFirstButtonReturn {
|
|
|
|
|
downloadModel(WhisperModels.available[popup.indexOfSelectedItem])
|
|
|
|
|
} else {
|
|
|
|
|
updateStatus("⚠️ No model selected")
|
|
|
|
|
updateStatus("No model selected", warning: true)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -1180,7 +1241,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
guard let url = URL(string: model.url) else {
|
|
|
|
|
updateStatus("⚠️ Invalid URL")
|
|
|
|
|
updateStatus("Invalid URL", warning: true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -1224,7 +1285,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
|
|
|
|
|
func failDownload(_ message: String) {
|
|
|
|
|
let name = currentDownloadModel?.name ?? "model"
|
|
|
|
|
clearDownloadState()
|
|
|
|
|
updateStatus("⚠️ Download failed")
|
|
|
|
|
updateStatus("Download failed", warning: true)
|
|
|
|
|
if playSounds { NSSound(named: "Basso")?.play() }
|
|
|
|
|
downloadWindow.showFailed(modelName: name, message: message)
|
|
|
|
|
downloadWindow.show()
|
|
|
|
|
@@ -1237,24 +1298,43 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
|
|
|
|
|
updateDownloadIndicators()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Menu bar icon when idle: download progress while a model downloads, else the mic
|
|
|
|
|
var idleIcon: String {
|
|
|
|
|
downloadTask != nil ? "⬇️ \(downloadPercent)%" : "🎤"
|
|
|
|
|
// Menu bar icon. While idle it shows the download progress if a model is downloading.
|
|
|
|
|
enum MenuBarIcon { case idle, recording, busy }
|
|
|
|
|
var menuBarIcon = MenuBarIcon.idle { didSet { refreshMenuBarIcon() } }
|
|
|
|
|
|
|
|
|
|
func refreshMenuBarIcon() {
|
|
|
|
|
guard let button = statusItem?.button else { return }
|
|
|
|
|
let downloading = menuBarIcon == .idle && downloadTask != nil
|
|
|
|
|
switch menuBarIcon {
|
|
|
|
|
case .idle:
|
|
|
|
|
button.image = downloading
|
|
|
|
|
? Icons.symbol("arrow.down.circle", pointSize: 15, description: "WhisperDictate: downloading a model")
|
|
|
|
|
: Icons.symbol("mic", pointSize: 15, description: "WhisperDictate")
|
|
|
|
|
case .recording:
|
|
|
|
|
button.image = Icons.coloredSymbol("mic.fill", pointSize: 15, color: .systemRed, description: "WhisperDictate: recording")
|
|
|
|
|
case .busy:
|
|
|
|
|
button.image = Icons.symbol("hourglass", pointSize: 15, description: "WhisperDictate: working")
|
|
|
|
|
}
|
|
|
|
|
button.imagePosition = .imageLeading
|
|
|
|
|
button.title = downloading ? "\(downloadPercent)%" : ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sync the menu bar icon, the menu's download line and the Settings button
|
|
|
|
|
// with the download state, without overriding the recording/transcribing icon.
|
|
|
|
|
func updateDownloadIndicators() {
|
|
|
|
|
if let title = statusItem.button?.title, title == "🎤" || title.hasPrefix("⬇️") {
|
|
|
|
|
statusItem.button?.title = idleIcon
|
|
|
|
|
}
|
|
|
|
|
refreshMenuBarIcon()
|
|
|
|
|
if let item = statusItem.menu?.item(withTag: 101) {
|
|
|
|
|
item.isHidden = currentDownloadModel == nil
|
|
|
|
|
if let model = currentDownloadModel {
|
|
|
|
|
item.title = "⬇️ Downloading \(model.name): \(downloadPercent)% — Show Progress…"
|
|
|
|
|
item.title = "Downloading \(model.name): \(downloadPercent)% — Show Progress…"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
settingsDownloadButton?.title = downloadTask != nil ? "⬇️ \(downloadPercent)%" : "Download..."
|
|
|
|
|
if let button = settingsDownloadButton {
|
|
|
|
|
let downloading = downloadTask != nil
|
|
|
|
|
button.image = downloading ? Icons.symbol("arrow.down.circle", pointSize: 11) : nil
|
|
|
|
|
button.imagePosition = .imageLeading
|
|
|
|
|
button.title = downloading ? "\(downloadPercent)%" : "Download..."
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - URLSessionDownloadDelegate
|
|
|
|
|
@@ -1324,7 +1404,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
|
|
|
|
|
// MARK: - Status Item
|
|
|
|
|
func setupStatusItem() {
|
|
|
|
|
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
|
|
|
|
statusItem.button?.title = "🎤"
|
|
|
|
|
refreshMenuBarIcon()
|
|
|
|
|
|
|
|
|
|
let menu = NSMenu()
|
|
|
|
|
|
|
|
|
|
@@ -1336,6 +1416,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
|
|
|
|
|
menu.addItem(statusMenuItem)
|
|
|
|
|
|
|
|
|
|
let downloadMenuItem = NSMenuItem(title: "", action: #selector(showDownloadWindow), keyEquivalent: "")
|
|
|
|
|
downloadMenuItem.image = Icons.symbol("arrow.down.circle")
|
|
|
|
|
downloadMenuItem.target = self
|
|
|
|
|
downloadMenuItem.tag = 101
|
|
|
|
|
downloadMenuItem.isHidden = true
|
|
|
|
|
@@ -1734,20 +1815,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
|
|
|
|
|
func checkModelExists() {
|
|
|
|
|
let validation = isValidModelPath(modelPath)
|
|
|
|
|
if !validation.valid {
|
|
|
|
|
updateStatus("⚠️ \(validation.error ?? "Invalid model")")
|
|
|
|
|
updateStatus(validation.error ?? "Invalid model", warning: true)
|
|
|
|
|
} else {
|
|
|
|
|
updateStatus("Ready")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func updateStatus(_ status: String) {
|
|
|
|
|
if let menu = statusItem.menu {
|
|
|
|
|
for item in menu.items {
|
|
|
|
|
if item.tag == 100 {
|
|
|
|
|
item.title = "Status: \(status)"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
func updateStatus(_ status: String, warning: Bool = false) {
|
|
|
|
|
guard let item = statusItem.menu?.item(withTag: 100) else { return }
|
|
|
|
|
item.title = "Status: \(status)"
|
|
|
|
|
item.image = warning ? Icons.coloredSymbol("exclamationmark.triangle.fill", pointSize: 13, color: .systemYellow) : nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Permissions
|
|
|
|
|
@@ -1840,7 +1917,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
|
|
|
|
|
audioRecorder?.isMeteringEnabled = true
|
|
|
|
|
audioRecorder?.record()
|
|
|
|
|
isRecording = true
|
|
|
|
|
statusItem.button?.title = "🔴"
|
|
|
|
|
menuBarIcon = .recording
|
|
|
|
|
updateStatus("Recording...")
|
|
|
|
|
if playSounds { NSSound(named: "Tink")?.play() }
|
|
|
|
|
NSLog("Recording started")
|
|
|
|
|
@@ -1961,7 +2038,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
|
|
|
|
|
let keepHUD = explainLoading || recordingHUD.hasPreview
|
|
|
|
|
stopMeter(hideHUD: !keepHUD)
|
|
|
|
|
if keepHUD { recordingHUD.showTranscribing(notice: explainLoading ? .loadingModel : nil) }
|
|
|
|
|
statusItem.button?.title = "⏳"
|
|
|
|
|
menuBarIcon = .busy
|
|
|
|
|
updateStatus("Transcribing...")
|
|
|
|
|
if playSounds { NSSound(named: "Pop")?.play() }
|
|
|
|
|
NSLog("Recording stopped, transcribing...")
|
|
|
|
|
@@ -2031,7 +2108,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !isRecording && !isTranscribing {
|
|
|
|
|
statusItem.button?.title = "⏳"
|
|
|
|
|
menuBarIcon = .busy
|
|
|
|
|
updateStatus("Loading model…")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -2043,7 +2120,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
|
|
|
|
|
defer { completion?() }
|
|
|
|
|
guard let self = self, self.whisperServer === server else { return }
|
|
|
|
|
let busy = self.isRecording || self.isTranscribing
|
|
|
|
|
if !busy { self.statusItem.button?.title = self.idleIcon }
|
|
|
|
|
if !busy { self.menuBarIcon = .idle }
|
|
|
|
|
if ok {
|
|
|
|
|
if !busy { self.updateStatus("Ready") }
|
|
|
|
|
NSLog("whisper-server ready on port \(server.port)")
|
|
|
|
|
@@ -2061,8 +2138,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
|
|
|
|
|
let modelValidation = isValidModelPath(modelPath)
|
|
|
|
|
guard modelValidation.valid else {
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
self.statusItem.button?.title = self.idleIcon
|
|
|
|
|
self.updateStatus("⚠️ \(modelValidation.error ?? "Invalid model")")
|
|
|
|
|
self.menuBarIcon = .idle
|
|
|
|
|
self.updateStatus(modelValidation.error ?? "Invalid model", warning: true)
|
|
|
|
|
if self.playSounds { NSSound(named: "Basso")?.play() }
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
@@ -2070,8 +2147,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
|
|
|
|
|
|
|
|
|
|
guard SupportedLanguages.isValid(language) else {
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
self.statusItem.button?.title = self.idleIcon
|
|
|
|
|
self.updateStatus("⚠️ Invalid language")
|
|
|
|
|
self.menuBarIcon = .idle
|
|
|
|
|
self.updateStatus("Invalid language", warning: true)
|
|
|
|
|
if self.playSounds { NSSound(named: "Basso")?.play() }
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
@@ -2098,7 +2175,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
|
|
|
|
|
if let finalText = finalText, !finalText.isEmpty {
|
|
|
|
|
self.pasteText(finalText)
|
|
|
|
|
} else {
|
|
|
|
|
self.statusItem.button?.title = self.idleIcon
|
|
|
|
|
self.menuBarIcon = .idle
|
|
|
|
|
self.updateStatus("Ready")
|
|
|
|
|
if self.playSounds { NSSound(named: "Basso")?.play() }
|
|
|
|
|
NSLog("No speech recognized")
|
|
|
|
|
@@ -2111,8 +2188,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
|
|
|
|
|
func transcribeWithCLI(prompt: String? = nil) -> String? {
|
|
|
|
|
guard let whisperPath = findWhisperCLI() else {
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
self.statusItem.button?.title = self.idleIcon
|
|
|
|
|
self.updateStatus("⚠️ whisper-cli not found")
|
|
|
|
|
self.menuBarIcon = .idle
|
|
|
|
|
self.updateStatus("whisper-cli not found", warning: true)
|
|
|
|
|
if self.playSounds { NSSound(named: "Basso")?.play() }
|
|
|
|
|
NSLog("whisper-cli not found in PATH")
|
|
|
|
|
}
|
|
|
|
|
@@ -2187,7 +2264,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.statusItem.button?.title = self.idleIcon
|
|
|
|
|
self.menuBarIcon = .idle
|
|
|
|
|
self.updateStatus("Ready")
|
|
|
|
|
if self.playSounds { NSSound(named: "Glass")?.play() }
|
|
|
|
|
}
|
|
|
|
|
|