Skip to content
Merged
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
1 change: 1 addition & 0 deletions speaktype/App/speaktypeApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ struct speaktypeApp: App {
.preferredColorScheme(.dark)
.tint(.appRed)
}
.defaultSize(width: 1200, height: 800)
.handlesExternalEvents(matching: ["main-dashboard", "open"]) // Only open for matching IDs
.commands {
SidebarCommands()
Expand Down
86 changes: 51 additions & 35 deletions speaktype/Assets.xcassets/AppIcon.appiconset/Contents.json
Original file line number Diff line number Diff line change
@@ -1,58 +1,74 @@
{
"images" : [
"images": [
{
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
"size": "16x16",
"idiom": "mac",
"filename": "icon_16x16.png",
"scale": "1x"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
"size": "16x16",
"idiom": "mac",
"filename": "icon_32x32.png",
"scale": "2x"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
"size": "32x32",
"idiom": "mac",
"filename": "icon_32x32.png",
"scale": "1x"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
"size": "32x32",
"idiom": "mac",
"filename": "icon_64x64.png",
"scale": "2x"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
"size": "128x128",
"idiom": "mac",
"filename": "icon_128x128.png",
"scale": "1x"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
"size": "128x128",
"idiom": "mac",
"filename": "icon_256x256.png",
"scale": "2x"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
"size": "256x256",
"idiom": "mac",
"filename": "icon_256x256.png",
"scale": "1x"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
"size": "256x256",
"idiom": "mac",
"filename": "icon_512x512.png",
"scale": "2x"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
"size": "512x512",
"idiom": "mac",
"filename": "icon_512x512.png",
"scale": "1x"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
"size": "512x512",
"idiom": "mac",
"filename": "icon_1024x1024.png",
"scale": "2x"
},
{
"size": "1024x1024",
"idiom": "mac",
"filename": "icon_1024x1024.png",
"scale": "1x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
"info": {
"version": 1,
"author": "xcode"
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 31 additions & 13 deletions speaktype/Controllers/MiniRecorderWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import SwiftUI

class MiniRecorderWindowController: NSObject {
private var panel: NSPanel?
private var hostingController: NSHostingController<MiniRecorderView>?
private var hostingController: NSHostingController<AnyView>?

// Toggle visibility
func toggle() {
Expand Down Expand Up @@ -41,6 +41,11 @@ class MiniRecorderWindowController: NSObject {
// panel.orderFrontDuringApplicationMakeKey() // Does not exist
// panel.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)

// Trigger instant recording
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
NotificationCenter.default.post(name: .hotkeyTriggered, object: nil)
}
}
}

Expand All @@ -55,25 +60,31 @@ class MiniRecorderWindowController: NSObject {
}
)

hostingController = NSHostingController(rootView: recorderView)
// Initialize hosting controller with transparent background view
// Wrap in AnyView because .background() changes the type from MiniRecorderView to some View
hostingController = NSHostingController(rootView: AnyView(recorderView.background(Color.clear)))

let p = NSPanel(
contentRect: NSRect(x: 0, y: 0, width: 300, height: 60),
styleMask: [.nonactivatingPanel, .titled, .fullSizeContentView, .hudWindow],
styleMask: [.nonactivatingPanel, .fullSizeContentView, .borderless],
backing: .buffered,
defer: false
)

p.isOpaque = false
p.backgroundColor = .clear

p.contentViewController = hostingController
p.titleVisibility = .hidden
p.titlebarAppearsTransparent = true
p.isMovableByWindowBackground = true
p.hasShadow = false // Remove shadow to kill "boundary" artifact

// Window Behavior
p.level = .floating
p.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
p.isReleasedWhenClosed = false
p.hidesOnDeactivate = false // Keep floating even if focus lost? Yes.
p.hidesOnDeactivate = false // Keep floating even if focus lost
p.standardWindowButton(.closeButton)?.isHidden = true
p.standardWindowButton(.miniaturizeButton)?.isHidden = true
p.standardWindowButton(.zoomButton)?.isHidden = true
Expand All @@ -96,19 +107,26 @@ class MiniRecorderWindowController: NSObject {

// 3. Wait for focus switch
print("Waiting for focus switch...")
try? await Task.sleep(nanoseconds: 800_000_000) // 0.8s
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1.0s wait for stability

// 4. Robust Paste Routine
print("Attempting Paste Routine...")

// 4. Paste
if ClipboardService.shared.isAccessibilityTrusted {
print("Simulating Paste (Accessibility Trusted)...")
ClipboardService.shared.paste()
print("Paste command sent.")
} else {
print("⚠️ Accessibility Permission Missing! Cannot paste. Requesting system prompt...")
// Trigger native system prompt which is more reliable for re-association
// User has permissions, but CGEvent is failing for them.
// Switch to AppleScript (System Events) as PRIMARY method. It's slower but 100% reliable.
print("✅ Accessibility Trusted. Using robust AppleScript paste.")

// Small delay to ensure 'System Events' is ready
try? await Task.sleep(nanoseconds: 100_000_000) // 0.1s

await MainActor.run {
ClipboardService.shared.requestAccessibilityPermission()
ClipboardService.shared.appleScriptPaste()
}
} else {
// No permissions? Try CGEvent as a distinct "Hail Mary" that sometimes slips through
print("⚠️ Accessibility Untrusted. Trying CGEvent fallback.")
ClipboardService.shared.paste()
}
}
}
Expand Down
62 changes: 62 additions & 0 deletions speaktype/Models/AIModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import SwiftUI

struct AIModel: Identifiable, Equatable {
let id = UUID()
let name: String
let variant: String
let details: String
let rating: String
let size: String

static let availableModels: [AIModel] = [
AIModel(
name: "Distil-Whisper Large v3",
variant: "distil-whisper/distil-large-v3",
details: "English • High Quality • Fast",
rating: "Excellent",
size: "756 MB"
),
AIModel(
name: "Whisper Large v3",
variant: "openai/whisper-large-v3",
details: "Multilingual • Best Accuracy • Slower",
rating: "Best",
size: "3.1 GB"
),
AIModel(
name: "Whisper Medium",
variant: "openai/whisper-medium",
details: "Multilingual • Balanced",
rating: "Great",
size: "1.5 GB"
),
AIModel(
name: "Distil-Whisper Medium",
variant: "distil-whisper/distil-medium.en",
details: "English-only • Fast • Good Accuracy",
rating: "Good",
size: "396 MB"
),
AIModel(
name: "Whisper Base",
variant: "openai_whisper-base",
details: "English-only • Optimized for Apple Silicon",
rating: "Standard",
size: "74 MB"
),
AIModel(
name: "Whisper Small",
variant: "openai_whisper-small",
details: "English-only • Higher accuracy",
rating: "Good",
size: "244 MB"
),
AIModel(
name: "Whisper Tiny",
variant: "openai_whisper-tiny",
details: "Multilingual • Fastest",
rating: "Basic",
size: "39 MB"
)
]
}
2 changes: 1 addition & 1 deletion speaktype/Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
</dict>
</array>
<key>LSUIElement</key>
<true/>
<false/>
<key>NSMicrophoneUsageDescription</key>
<string>SpeakType needs access to your microphone to record your voice for transcription. All processing is done locally on your device.</string>
<key>NSAppleEventsUsageDescription</key>
Expand Down
3 changes: 2 additions & 1 deletion speaktype/Services/AudioRecordingService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ class AudioRecordingService: NSObject, ObservableObject {
let power = recorder.averagePower(forChannel: 0) // -160 to 0 dB
// Normalize: -60dB to 0dB -> 0.0 to 1.0
let normalized = max(0.0, (power + 60) / 60)
// Noise Gate: Ignore background noise below ~10% (approx -54dB)
DispatchQueue.main.async {
self.audioLevel = normalized
self.audioLevel = normalized < 0.15 ? 0.0 : normalized
}
}
}
Expand Down
23 changes: 21 additions & 2 deletions speaktype/Services/ClipboardService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ class ClipboardService {
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(text, forType: .string)
print("Copied to clipboard: \(text.prefix(20))...")

// Verify write
if let check = pasteboard.string(forType: .string), check == text {
print("✅ Clipboard Write Verified: '\(check.prefix(20))...'")
} else {
print("❌ Clipboard Write FAILED!")
}
}

// Paste content (Simulate Cmd+V)
Expand Down Expand Up @@ -43,7 +49,20 @@ class ClipboardService {
cmdUp?.post(tap: .cghidEventTap)

print("Simulated Cmd+V")
print("Simulated Cmd+V")
}
}

// Fallback using AppleScript (more robust for some apps)
func appleScriptPaste() {
let script = "tell application \"System Events\" to keystroke \"v\" using command down"
if let appleScript = NSAppleScript(source: script) {
var error: NSDictionary?
appleScript.executeAndReturnError(&error)
if let error = error {
print("AppleScript Paste Error: \(error)")
} else {
print("Executed AppleScript Paste")
}
}
}

Expand Down
8 changes: 8 additions & 0 deletions speaktype/Services/HistoryService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ struct HistoryItem: Identifiable, Codable, Hashable {
let date: Date
let transcript: String
let duration: TimeInterval

func hash(into hasher: inout Hasher) {
hasher.combine(id)
}

static func == (lhs: HistoryItem, rhs: HistoryItem) -> Bool {
lhs.id == rhs.id
}
}

class HistoryService: ObservableObject {
Expand Down
Loading