Implement Phase 2: Real offline speech-to-text with whisper.cpp

- Add SwiftWhisper integration for real whisper.cpp support with Metal acceleration
- Implement complete WhisperCPPEngine with audio transcription and text normalization
- Build ModelManager with curated catalog, downloads, and Core ML encoder support
- Create preferences window with model management UI (download, select, delete)
- Add NSStatusItem menu bar with model status display
- Integrate STT pipeline: hotkey → audio capture → whisper transcription
- Add model setup alerts when no model is loaded
- Support offline operation with performance targets met (<4s for 10s audio)
- Store models in ~/Library/Application Support/MenuWhisper/Models/

Phase 2 TECHSPEC requirements fully implemented and tested.
This commit is contained in:
Felipe M 2025-09-19 08:31:35 +02:00
parent 6e768a7753
commit 5663f3c3de
Signed by: fmartingr
GPG key ID: CCFBC5637D4000A8
12 changed files with 1500 additions and 100 deletions

View file

@ -1,64 +1,26 @@
import SwiftUI
import CoreUtils
@main
struct MenuWhisperApp: App {
@StateObject private var appController = AppController()
class AppDelegate: NSObject, NSApplicationDelegate {
private let appController = AppController()
var body: some Scene {
MenuBarExtra("Menu-Whisper", systemImage: "mic") {
MenuBarContentView()
.environmentObject(appController)
.onAppear {
appController.start()
}
}
func applicationDidFinishLaunching(_ notification: Notification) {
appController.start()
}
}
struct MenuBarContentView: View {
@EnvironmentObject var appController: AppController
@main
struct MenuWhisperApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text("Menu-Whisper")
.font(.headline)
Text(appController.currentState.displayName)
.font(.subheadline)
.foregroundColor(stateColor)
if appController.currentState == .listening {
Text("Press ⌘⇧V or Esc to stop")
.font(.caption)
.foregroundColor(.secondary)
}
Divider()
Button("Preferences...") {
// TODO: Open preferences window in Phase 4
}
Button("Quit") {
NSApplication.shared.terminate(nil)
}
var body: some Scene {
// Use a hidden window scene since we're using NSStatusItem for the menu bar
WindowGroup {
EmptyView()
}
.padding(.horizontal, 4)
.windowStyle(.hiddenTitleBar)
.windowResizability(.contentSize)
.defaultSize(width: 0, height: 0)
}
}
private var stateColor: Color {
switch appController.currentState {
case .idle:
return .primary
case .listening:
return .blue
case .processing:
return .orange
case .injecting:
return .green
case .error:
return .red
}
}
}