diff --git a/.gitignore b/.gitignore
index 585ad25..1eabaa0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -132,3 +132,7 @@ Temporary Items
*.bak
*.backup
+# Build logs
+build_log.txt
+*.log
+
diff --git a/Makefile b/Makefile
index 21cf5cf..4e2cd9a 100644
--- a/Makefile
+++ b/Makefile
@@ -1,17 +1,23 @@
# Makefile for SpeakType
-.PHONY: help build clean test lint format run setup
+.PHONY: help build clean test lint format run setup logs logs-live logs-errors logs-export
# Default target
help:
@echo "SpeakType - Available commands:"
- @echo " make setup - Initial project setup"
- @echo " make build - Build the project"
- @echo " make run - Run the application"
- @echo " make test - Run all tests"
- @echo " make lint - Run SwiftLint"
- @echo " make format - Format code with SwiftLint"
- @echo " make clean - Clean build artifacts"
+ @echo " make setup - Initial project setup"
+ @echo " make build - Build the project"
+ @echo " make run - Run the application"
+ @echo " make test - Run all tests"
+ @echo " make lint - Run SwiftLint"
+ @echo " make format - Format code with SwiftLint"
+ @echo " make clean - Clean build artifacts"
+ @echo ""
+ @echo "Logging:"
+ @echo " make logs - View live logs"
+ @echo " make logs-live - Stream live logs (alias)"
+ @echo " make logs-errors - View recent errors"
+ @echo " make logs-export - Export last 24h logs to Desktop"
# Project setup
setup:
@@ -82,3 +88,40 @@ xcode:
@echo "Opening in Xcode..."
open speaktype.xcodeproj
+# LOGGING COMMANDS
+
+# View live logs
+logs:
+ @echo "π± Streaming SpeakType logs (Ctrl+C to stop)..."
+ @echo "Tip: Run 'make run' in another terminal first"
+ @echo ""
+ log stream --predicate 'process == "speaktype"' --level debug --style compact
+
+# Alias for logs
+logs-live: logs
+
+# View recent errors
+logs-errors:
+ @echo "β Recent SpeakType errors (last hour)..."
+ @log show --predicate 'process == "speaktype" AND messageType == error' --last 1h --style compact || echo "No errors found"
+
+# View recent logs (last 30 minutes)
+logs-recent:
+ @echo "π Recent SpeakType logs (last 30 minutes)..."
+ @log show --predicate 'process == "speaktype"' --last 30m --style compact
+
+# Export logs to Desktop
+logs-export:
+ @echo "πΎ Exporting logs to Desktop..."
+ @mkdir -p ~/Desktop/SpeakType_Logs
+ @log show --predicate 'process == "speaktype"' --last 1d > ~/Desktop/SpeakType_Logs/app_logs_$(shell date +%Y%m%d_%H%M%S).txt
+ @echo "System: $(shell sw_vers -productVersion)" > ~/Desktop/SpeakType_Logs/system_info.txt
+ @echo "Date: $(shell date)" >> ~/Desktop/SpeakType_Logs/system_info.txt
+ @echo "β
Logs exported to ~/Desktop/SpeakType_Logs/"
+ @open ~/Desktop/SpeakType_Logs/
+
+# Open Console.app filtered to SpeakType
+logs-console:
+ @echo "Opening Console.app..."
+ @open -a Console
+
diff --git a/README.md b/README.md
index 5bcc57c..7e3a156 100644
--- a/README.md
+++ b/README.md
@@ -1,103 +1,371 @@
# SpeakType
-A modern macOS application built with SwiftUI.
+
-## Project Structure
+
-```
-speaktype/
-βββ speaktype/ # Main application target
-β βββ App/ # Application entry point and configuration
-β βββ Views/ # SwiftUI views and UI components
-β βββ ViewModels/ # View models and business logic
-β βββ Models/ # Data models and entities
-β βββ Services/ # Business logic and API services
-β βββ Utilities/ # Helper functions and extensions
-β βββ Resources/ # Non-code resources
-β β βββ Assets.xcassets/ # Images, colors, and other assets
-β βββ Supporting Files/ # Info.plist, entitlements, etc.
-βββ speaktypeTests/ # Unit tests
-βββ speaktypeUITests/ # UI tests
-βββ docs/ # Documentation
-```
+**Fast, Offline Voice-to-Text for macOS**
+
+[](https://swift.org)
+[](https://www.apple.com/macos/)
+[](https://developer.apple.com/xcode/swiftui/)
+[](LICENSE)
+
+*Press a hotkey, speak, and instantly paste text anywhere on your Mac.*
+
+[Features](#features) β’ [Installation](#installation) β’ [Usage](#usage) β’ [Development](#development) β’ [Contributing](#contributing)
+
+
+
+---
+
+## π― What is SpeakType?
+
+SpeakType is a **privacy-first, offline voice dictation tool** for macOS that lets you type with your voice anywhereβin any app, at any time. Unlike online dictation services, everything runs **100% locally** on your Mac using OpenAI's Whisper AI model.
+
+### Why SpeakType?
+
+- π **Privacy First**: Zero data leaves your Macβever
+- β‘ **Lightning Fast**: Optimized for Apple Silicon
+- π **Works Everywhere**: Any app, any text field
+- π― **Accurate**: Powered by OpenAI Whisper AI
+- π° **Free & Open Source**: No subscriptions, no limits
-## Requirements
+---
-- macOS 13.0+
-- Xcode 15.0+
-- Swift 5.9+
+##β¨ Features
-## Getting Started
+### Core Features
+- **π€ System-Wide Dictation** - Works in Notes, Slack, Chrome, Terminalβanywhere
+- **β¨οΈ Global Hotkey** - Press, speak, release = instant text (default: `β₯ Space`)
+- **π 100% Offline** - All processing happens locally with WhisperKit
+- **π Smart Paste** - Auto-copies to clipboard or pastes directly
+- **π¨ Native macOS UI** - Beautiful SwiftUI interface with dark mode
-### Installation
+### AI & Models
+- **Multiple Models** - Choose between Base (fast) or Large (accurate)
+- **Auto-Download** - One-click model downloads with progress tracking
+- **Model Management** - Easy switching and cache cleanup
+- **Optimized for Apple Silicon** - Best performance on M1/M2/M3 Macs
+
+### User Experience
+- **Mini Recorder** - Elegant floating recorder window
+- **Visual Feedback** - Real-time audio waveforms and indicators
+- **History** - Review past transcriptions
+- **Customizable** - Configure hotkeys, audio devices, and behavior
+
+---
+
+## π Installation
+
+### Requirements
+
+| Component | Requirement |
+|-----------|-------------|
+| **OS** | macOS 13.0+ (Ventura or newer) |
+| **Architecture** | Apple Silicon (M1+) recommended |
+| **Storage** | 2GB free (for AI models) |
+| **Permissions** | Microphone & Accessibility |
+
+### Quick Start
-1. Clone the repository:
```bash
-git clone
+# 1. Clone the repository
+git clone https://github.com/yourusername/speaktype.git
cd speaktype
-```
-2. Open the project in Xcode:
-```bash
+# 2. Build & run
+make build
+make run
+
+# Or open in Xcode
open speaktype.xcodeproj
```
-3. Build and run the project (βR)
+### First Time Setup
-### Development
+1. **Grant Permissions** (you'll be prompted):
+ - π€ Microphone access for recording
+ - βΏ Accessibility access for auto-paste
-- Follow the [Contributing Guidelines](CONTRIBUTING.md) when making changes
-- Use SwiftUI for all UI components
-- Follow Swift naming conventions and style guide
-- Write unit tests for business logic
-- Write UI tests for critical user flows
+2. **Download AI Model**:
+ - Open SpeakType β AI Models
+ - Click "Download" on Whisper Base (550MB)
+ - Wait for download to complete
-## Architecture
+3. **Configure Hotkey** (optional):
+ - Go to Settings
+ - Default is `β₯ Space` (Option + Space)
+ - Customize to your preference
-This project follows the MVVM (Model-View-ViewModel) architecture pattern:
+4. **Start Using**:
+ - Press hotkey anywhere
+ - Speak naturally
+ - Release = text appears!
-- **Models**: Data structures and business entities
-- **Views**: SwiftUI views that display data
-- **ViewModels**: Manage view state and handle user interactions
-- **Services**: Handle data persistence, networking, and other business logic
+---
-## Building
+## π‘ Usage
-### Debug Build
-```bash
-xcodebuild -scheme speaktype -configuration Debug
+### Basic Usage
+
+```
+1. Press hotkey (β₯ Space)
+2. Speak your text
+3. Release hotkey
+4. β¨ Text appears!
```
-### Release Build
-```bash
-xcodebuild -scheme speaktype -configuration Release
+### Examples
+
+**Writing an Email**
+```
+Press hotkey β "Hi team comma I wanted to share the latest updates period"
+β "Hi team, I wanted to share the latest updates."
+```
+
+**Coding**
+```
+Press hotkey β "function calculate total open paren items close paren"
+β "function calculateTotal(items)"
```
-## Testing
+**Quick Notes**
+```
+Press hotkey β "Remember to buy milk and eggs"
+β "Remember to buy milk and eggs"
+```
+
+### Pro Tips
+
+- π¬ **Speak naturally** - Whisper handles accents and casual speech
+- β±οΈ **Shorter is better** - Best results with 3-10 second clips
+- π― **Say punctuation** - "comma", "period", "question mark"
+- π **Try again** - If transcription is off, just press hotkey again
+
+---
+
+## π οΈ Development
+
+### Project Structure
+
+```
+speaktype/
+βββ App/ # Application entry point
+β βββ speaktypeApp.swift
+β βββ AppDelegate.swift
+βββ Views/ # SwiftUI user interface
+β βββ Screens/ # Full-screen views
+β βββ Components/ # Reusable UI components
+β βββ Overlays/ # Floating overlays
+βββ ViewModels/ # MVVM view models
+βββ Models/ # Data models
+β βββ AIModel.swift
+β βββ TranscriptionResult.swift
+β βββ ...
+βββ Services/ # Business logic
+β βββ AudioRecordingService.swift
+β βββ WhisperService.swift
+β βββ ModelDownloadService.swift
+β βββ ...
+βββ Controllers/ # Window management
+βββ Utilities/ # Helpers & extensions
+β βββ AppLogger.swift
+β βββ Constants.swift
+β βββ Extensions/
+βββ Resources/ # Assets & config
+ βββ Assets.xcassets/
+ βββ Info.plist
+ βββ speaktype.entitlements
+```
+
+### Architecture
+
+SpeakType follows **MVVM (Model-View-ViewModel)** pattern with clean separation:
+
+- **Models**: Data structures (`AIModel`, `TranscriptionResult`)
+- **Views**: SwiftUI UI components
+- **ViewModels**: Business logic and state management
+- **Services**: Core functionality (audio, transcription, clipboard)
+
+### Tech Stack
+
+| Layer | Technology |
+|-------|------------|
+| **Language** | Swift 5.9+ |
+| **UI** | SwiftUI + AppKit |
+| **AI** | [WhisperKit](https://github.com/argmaxinc/WhisperKit) 0.9.4 |
+| **Audio** | AVFoundation |
+| **Hotkeys** | [KeyboardShortcuts](https://github.com/sindresorhus/KeyboardShortcuts) |
+| **State** | Combine + SwiftUI |
+
+### Quick Commands
-### Run Unit Tests
```bash
-xcodebuild test -scheme speaktype -destination 'platform=macOS'
+# Build & run
+make build # Build debug
+make run # Run app
+make clean # Clean build artifacts
+
+# Testing
+make test # Run all tests
+make test-unit # Unit tests only
+make test-ui # UI tests only
+
+# Logging
+make logs # View live logs
+make logs-errors # Recent errors only
+make logs-export # Export to Desktop
+
+# Code quality
+make lint # Run SwiftLint
+make format # Auto-fix issues
+```
+
+### Logging & Debugging
+
+SpeakType uses Apple's **unified logging system** with categories:
+
+```swift
+// In your code
+import Foundation
+
+AppLogger.info("Started recording", category: .audio)
+AppLogger.success("Transcription complete", category: .transcription)
+AppLogger.error("Download failed", error: error, category: .models)
```
-### Run UI Tests
+**View logs in terminal:**
```bash
-xcodebuild test -scheme speaktype -destination 'platform=macOS' -only-testing:speaktypeUITests
+# Live stream
+make logs
+
+# Filter by category
+log stream --predicate 'process == "speaktype" AND category == "Transcription"'
+
+# Export for debugging
+make logs-export
+```
+
+---
+
+## π€ Contributing
+
+We welcome contributions! Here's how to get started:
+
+### Quick Start for Contributors
+
+1. **Fork & clone**
+ ```bash
+ git clone https://github.com/yourusername/speaktype.git
+ cd speaktype
+ ```
+
+2. **Create a branch**
+ ```bash
+ git checkout -b feature/amazing-feature
+ ```
+
+3. **Make changes**
+ - Follow SwiftUI best practices
+ - Write tests for new features
+ - Run `make lint` before committing
+
+4. **Submit PR**
+ ```bash
+ git commit -m "Add amazing feature"
+ git push origin feature/amazing-feature
+ ```
+
+### Code Guidelines
+
+- β
Use MVVM pattern
+- β
Write SwiftUI views, not UIKit
+- β
Add unit tests for services
+- β
Follow [Swift API Design Guidelines](https://swift.org/documentation/api-design-guidelines/)
+- β
Use `AppLogger` instead of `print()`
+
+See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
+
+---
+
+## π Known Issues
+
+### WhisperKit Dependency Warnings
+
+You may see these build warnings:
```
+warning: 'WhisperKit' is missing a dependency on 'TensorUtils'
+warning: 'WhisperKit' is missing a dependency on 'Hub'
+warning: 'WhisperKit' is missing a dependency on 'Tokenizers'
+```
+
+**These are harmless** and come from Xcode's dependency scanner analyzing WhisperKit's internal structure. They don't affect functionalityβyour app builds and runs perfectly. This is a limitation of WhisperKit 0.9.4, not your code.
+
+---
+
+## πΊοΈ Roadmap
+
+### Version 1.1 (Next)
+- [ ] Streaming transcription (real-time)
+- [ ] Multiple language support
+- [ ] Custom vocabulary/terms
+- [ ] Improved punctuation handling
+
+### Version 1.2 (Future)
+- [ ] Transcription history with search
+- [ ] Text formatting commands
+- [ ] Per-app custom hotkeys
+- [ ] iCloud sync (optional)
+
+### Version 2.0 (Vision)
+- [ ] iOS companion app
+- [ ] App Store release
+- [ ] Advanced settings
+- [ ] Plugin system
+
+---
+
+## π License
+
+MIT License - see [LICENSE](LICENSE) file for details.
+
+---
+
+## π Credits & Acknowledgments
+
+SpeakType is built with amazing open-source projects:
+
+- **[WhisperKit](https://github.com/argmaxinc/WhisperKit)** by Argmax - Whisper models for macOS
+- **[KeyboardShortcuts](https://github.com/sindresorhus/KeyboardShortcuts)** by Sindre Sorhus - Global hotkey management
+- **[OpenAI Whisper](https://github.com/openai/whisper)** - State-of-the-art speech recognition
+
+**Inspiration:**
+- [VoiceInk](https://voices.ink/) - Original inspiration
+- macOS Speech Framework - System dictation
+
+---
+
+## π Support & Community
+
+- **π Bug Reports**: [GitHub Issues](https://github.com/yourusername/speaktype/issues)
+- **π¬ Discussions**: [GitHub Discussions](https://github.com/yourusername/speaktype/discussions)
+- **π§ Email**: [your.email@example.com](mailto:your.email@example.com)
+
+---
+
+## β Star History
-## Code Style
+If you find SpeakType useful, please consider starring the repo!
-- Use SwiftLint for code linting (configuration in `.swiftlint.yml`)
-- Follow Apple's Swift API Design Guidelines
-- Use meaningful variable and function names
-- Add comments for complex logic
-- Keep functions small and focused
+---
-## License
+
-[Add your license here]
+**Made with β€οΈ by developers, for developers**
-## Contact
+*Privacy-first β’ Open Source β’ Forever Free*
-[Add contact information here]
+[β¬ Back to Top](#speaktype)
+
diff --git a/TESTING.md b/TESTING.md
deleted file mode 100644
index ceac42c..0000000
--- a/TESTING.md
+++ /dev/null
@@ -1,360 +0,0 @@
-# SpeakType Phase Testing Guide
-
-Quick tests to verify each phase works before moving to the next one.
-
----
-
-## β
Phase 1: Foundation (DONE)
-
-### Test 1: Project Builds
-```bash
-cd /Users/karansingh/projects/iosapps/speaktype
-xcodebuild -scheme speaktype -destination 'platform=macOS' clean build
-```
-**Pass**: Build succeeds
-**Fail**: Build errors
-
-### Test 2: Models Import
-Add to `ContentView.swift`:
-```swift
-let state: TranscriptionState = .idle
-let settings = AppSettings.default
-```
-**Pass**: No compile errors
-**Fail**: "Cannot find type" errors
-
-### Manual Setup Needed:
-1. Open `speaktype.xcodeproj` in Xcode
-2. Add WhisperKit package: `https://github.com/argmaxinc/WhisperKit`
-3. Add all model files to Xcode project (right-click Models folder β Add Files)
-4. Set Info.plist path in Build Settings
-5. Set entitlements path in Build Settings
-
----
-
-## π€ Phase 2: Core Services
-
-### Test 1: Microphone Permission
-Run app β Should request microphone permission
-
-**Pass**: Permission dialog appears
-**Fail**: No dialog or crash
-
-### Test 2: Audio Recording
-Add test button to ContentView:
-```swift
-Button("Test Record") {
- let recorder = AudioRecordingService()
- recorder.startRecording()
- DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
- recorder.stopRecording()
- print("β
Recording works")
- }
-}
-```
-**Pass**: Prints "β
Recording works"
-**Fail**: Crash or error
-
-### Test 3: Whisper Transcription
-Add test button:
-```swift
-Button("Test Whisper") {
- Task {
- let service = WhisperService()
- try? await service.initialize()
- print("β
Whisper loaded")
- }
-}
-```
-**Pass**: Prints "β
Whisper loaded" after 3-5 seconds
-**Fail**: Crash or never loads
-
-### Test 4: Clipboard Copy
-```swift
-Button("Test Clipboard") {
- ClipboardService.shared.copy(text: "Hello World")
- let result = ClipboardService.shared.paste()
- print("Clipboard: \(result)")
-}
-```
-**Pass**: Prints "Clipboard: Hello World"
-**Fail**: Empty or wrong text
-
----
-
-## π₯οΈ Phase 3: UI Components
-
-### Test 1: Menu Bar Icon
-Run app β Check menu bar (top right)
-
-**Pass**: App icon appears in menu bar
-**Fail**: No icon or dock icon instead
-
-### Test 2: Menu Bar Popover
-Click menu bar icon
-
-**Pass**: Popover appears with status
-**Fail**: Nothing happens or crash
-
-### Test 3: Listening Overlay
-```swift
-Button("Show Listening") {
- // Should show floating "Listening..." panel
- WindowManager.shared.showListeningOverlay()
-}
-```
-**Pass**: Centered floating panel appears
-**Fail**: No panel or crashes
-
-### Test 4: Settings Window
-```swift
-Button("Open Settings") {
- WindowManager.shared.showSettings()
-}
-```
-**Pass**: Settings window opens with tabs
-**Fail**: No window or crash
-
----
-
-## π Phase 4: ViewModels
-
-### Test 1: AppViewModel State
-```swift
-let viewModel = AppViewModel()
-viewModel.startListening()
-print("State: \(viewModel.state)")
-```
-**Pass**: State changes from .idle to .listening
-**Fail**: State doesn't change
-
-### Test 2: Recording β Transcribing Flow
-```swift
-let viewModel = AppViewModel()
-viewModel.startListening()
-// Wait 2 seconds
-viewModel.stopListening()
-// State should go: idle β listening β transcribing β ready
-```
-**Pass**: State transitions correctly
-**Fail**: Gets stuck or crashes
-
----
-
-## πΎ Phase 5: Storage
-
-### Test 1: Settings Persistence
-```swift
-var settings = AppSettings.default
-settings.autoPaste = true
-SettingsStorageService.shared.save(settings)
-
-let loaded = SettingsStorageService.shared.load()
-print("Auto-paste: \(loaded.autoPaste)")
-```
-**Pass**: Prints "Auto-paste: true"
-**Fail**: Prints false or crashes
-
-### Test 2: Hotkey Storage
-```swift
-let hotkey = HotkeyConfiguration.default
-HotkeyStorageService.shared.save(hotkey)
-let loaded = HotkeyStorageService.shared.load()
-print("Hotkey: \(loaded.description)")
-```
-**Pass**: Prints hotkey description
-**Fail**: Nil or crash
-
----
-
-## π― Phase 6: Integration
-
-### Test 1: Complete App Launch
-Run app from Xcode (βR)
-
-**Pass**:
-- App icon in menu bar
-- Click icon β popover appears
-- No crash for 1 minute
-
-**Fail**: Crash or no menu bar icon
-
-### Test 2: Hotkey Registration
-Run app β Open Settings β Try changing hotkey
-
-**Pass**: Can press new key combo and it registers
-**Fail**: Can't capture hotkey or crash
-
----
-
-## π¬ Phase 7: End-to-End Flow
-
-### THE BIG TEST: Full Recording Flow
-
-1. Run app
-2. Press hotkey (default: ββ§Space)
-3. Speak: "Hello world this is a test"
-4. Release hotkey
-5. Wait for transcription
-6. Check clipboard
-
-**Pass**:
-- Listening overlay appeared
-- Transcribing overlay appeared
-- Result panel shows "hello world this is a test"
-- Text is in clipboard
-
-**Fail**: Any step breaks or wrong output
-
-### Test 2: Auto-Paste
-1. Open Notes app
-2. Click in text area
-3. Press hotkey
-4. Speak: "This should paste automatically"
-5. Release hotkey
-
-**Pass**: Text appears in Notes
-**Fail**: Nothing happens or clipboard only
-
----
-
-## β¨ Phase 8: Polish
-
-### Test 1: Dark Mode
-1. Run app
-2. System Preferences β Appearance β Dark
-3. Check all UI
-
-**Pass**: UI looks good in dark mode
-**Fail**: White text on white, wrong colors
-
-### Test 2: Animations
-Watch overlay appear/disappear
-
-**Pass**: Smooth fade in/out
-**Fail**: Instant pop or glitchy
-
----
-
-## π§ͺ Phase 9: Testing
-
-### Test 1: Short Recording
-Speak for 2 seconds β Should transcribe in <3 seconds
-
-**Pass**: Fast and accurate
-**Fail**: Slow or wrong text
-
-### Test 2: Long Recording
-Speak for 30 seconds β Should transcribe in <10 seconds
-
-**Pass**: Completes without crash
-**Fail**: Crash or timeout
-
-### Test 3: No Speech
-Press hotkey, don't speak, release
-
-**Pass**: Shows "No audio detected" or empty result
-**Fail**: Crash
-
-### Test 4: Multiple Apps
-Test pasting in: Notes, TextEdit, Safari, Messages
-
-**Pass**: Works in all apps
-**Fail**: Works in some but not others
-
----
-
-## π¦ Phase 10: Release
-
-### Test 1: Archive Build
-```bash
-xcodebuild archive -scheme speaktype -archivePath ./build/SpeakType.xcarchive
-```
-**Pass**: Archive succeeds
-**Fail**: Build errors
-
-### Test 2: Fresh Install Test
-1. Delete app from Applications
-2. Clear cache: `rm -rf ~/Library/Caches/com.yourcompany.speaktype`
-3. Install and run
-4. Complete onboarding
-
-**Pass**: Works on fresh install
-**Fail**: Crashes on first run
-
----
-
-## π¨ Common Issues & Fixes
-
-### "Microphone permission denied"
-Fix: System Preferences β Privacy β Microphone β Check speaktype
-
-### "Accessibility permission denied"
-Fix: System Preferences β Privacy β Accessibility β Check speaktype
-
-### "Whisper model not found"
-Fix: Check `~/Library/Caches/whisperkit/models/` or re-download
-
-### "Hotkey doesn't work"
-Fix: Check no conflicts in System Preferences β Keyboard β Shortcuts
-
-### "Build fails with SwiftUI errors"
-Fix: Clean build folder (ββ§K) and rebuild
-
----
-
-## β±οΈ Expected Times
-
-| Phase | Implementation | Testing |
-|-------|---------------|---------|
-| 1 | 2 hours | 10 min |
-| 2 | 3 hours | 15 min |
-| 3 | 4 hours | 20 min |
-| 4 | 2 hours | 15 min |
-| 5 | 1 hour | 10 min |
-| 6 | 2 hours | 15 min |
-| 7 | 3 hours | 30 min |
-| 8 | 2 hours | 15 min |
-| 9 | 2 hours | 1 hour |
-| 10 | 2 hours | 30 min |
-| **Total** | **23 hours** | **3 hours** |
-
----
-
-## π Intern Checklist
-
-```
-[ ] Phase 1 - All tests pass
-[ ] Phase 2 - Can record audio + Whisper loads
-[ ] Phase 3 - All UI appears correctly
-[ ] Phase 4 - State changes work
-[ ] Phase 5 - Settings save/load
-[ ] Phase 6 - App launches without crash
-[ ] Phase 7 - Full flow works (MOST IMPORTANT)
-[ ] Phase 8 - Looks good in light/dark
-[ ] Phase 9 - All edge cases handled
-[ ] Phase 10 - Can build for release
-```
-
----
-
-## π― Definition of Done (Each Phase)
-
-β
All tests pass
-β
No compiler warnings
-β
No crashes in normal use
-β
Code committed to git
-β
Brief note of what works/doesn't work
-
----
-
-## π When Stuck
-
-1. Check Issue Navigator (β5) for errors
-2. Clean build (ββ§K)
-3. Check IMPLEMENTATION_PLAN.md for code examples
-4. Google the error message
-5. Check [architecture docs](docs/ARCHITECTURE.md)
-
-**Phase 7 is the critical milestone** - if the full recording flow works, you're 80% done!
-
diff --git a/build_log.txt b/build_log.txt
deleted file mode 100644
index 095dbe0..0000000
--- a/build_log.txt
+++ /dev/null
@@ -1,21 +0,0 @@
-2026-01-08 00:49:15.446 xcodebuild[53214:2330497] [MT] IDERunDestination: Supported platforms for the buildables in the current scheme is empty.
---- xcodebuild: WARNING: Using the first of multiple matching destinations:
-{ platform:macOS, arch:arm64, id:00008122-000104641ADA001C, name:My Mac }
-{ platform:macOS, arch:x86_64, id:00008122-000104641ADA001C, name:My Mac }
-/Users/orm/speaktype/speaktype/Views/HistoryView.swift:9:13: error: referencing initializer 'init(_:selection:rowContent:)' on 'List' requires that 'HistoryItem' conform to 'Hashable'
- List(historyService.items, selection: $selectedItem) { item in
- ^
-SwiftUI.List:2:11: note: where 'SelectionValue' = 'HistoryItem'
-extension List {
- ^
-/Users/orm/speaktype/speaktype/Views/HistoryView.swift:10:17: error: initializer 'init(value:label:)' requires that 'HistoryItem' conform to 'Hashable'
- NavigationLink(value: item) {
- ^
-SwiftUI.NavigationLink.init:2:20: note: where 'P' = 'HistoryItem'
-nonisolated public init(value: P?, @ViewBuilder label: () -> Label) where P : Decodable, P : Encodable, P : Hashable}
- ^
-Failed frontend command:
-/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-frontend -frontend -c /Users/orm/speaktype/speaktype/App/speaktypeApp.swift /Users/orm/speaktype/speaktype/Models/AppSettings.swift /Users/orm/speaktype/speaktype/Models/AudioDevice.swift /Users/orm/speaktype/speaktype/Models/HotkeyConfiguration.swift /Users/orm/speaktype/speaktype/Models/RecordingSession.swift /Users/orm/speaktype/speaktype/Models/TranscriptionResult.swift /Users/orm/speaktype/speaktype/Models/TranscriptionState.swift /Users/orm/speaktype/speaktype/Services/AudioRecordingService.swift /Users/orm/speaktype/speaktype/Services/ClipboardService.swift /Users/orm/speaktype/speaktype/Services/HistoryService.swift /Users/orm/speaktype/speaktype/Services/HotkeyService.swift /Users/orm/speaktype/speaktype/Services/WhisperService.swift /Users/orm/speaktype/speaktype/Utilities/Extensions/Color+Extensions.swift /Users/orm/speaktype/speaktype/Utilities/Extensions/View+Extensions.swift /Users/orm/speaktype/speaktype/Utilities/AppColors.swift /Users/orm/speaktype/speaktype/Utilities/Constants.swift /Users/orm/speaktype/speaktype/Views/AIModelsView.swift /Users/orm/speaktype/speaktype/Views/AudioInputView.swift /Users/orm/speaktype/speaktype/Views/ContentView.swift /Users/orm/speaktype/speaktype/Views/DashboardView.swift -primary-file /Users/orm/speaktype/speaktype/Views/HistoryView.swift /Users/orm/speaktype/speaktype/Views/MainView.swift /Users/orm/speaktype/speaktype/Views/MiniRecorderView.swift /Users/orm/speaktype/speaktype/Views/OnboardingView.swift /Users/orm/speaktype/speaktype/Views/PermissionsView.swift /Users/orm/speaktype/speaktype/Views/SettingsView.swift /Users/orm/speaktype/speaktype/Views/ShortcutRecorderView.swift /Users/orm/speaktype/speaktype/Views/SidebarView.swift /Users/orm/speaktype/speaktype/Views/TranscribeAudioView.swift /Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Intermediates.noindex/speaktype.build/Debug/speaktype.build/DerivedSources/GeneratedAssetSymbols.swift -emit-dependencies-path /Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Intermediates.noindex/speaktype.build/Debug/speaktype.build/Objects-normal/arm64/HistoryView.d -emit-const-values-path /Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Intermediates.noindex/speaktype.build/Debug/speaktype.build/Objects-normal/arm64/HistoryView.swiftconstvalues -emit-reference-dependencies-path /Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Intermediates.noindex/speaktype.build/Debug/speaktype.build/Objects-normal/arm64/HistoryView.swiftdeps -serialize-diagnostics-path /Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Intermediates.noindex/speaktype.build/Debug/speaktype.build/Objects-normal/arm64/HistoryView.dia -emit-localized-strings -emit-localized-strings-path /Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Intermediates.noindex/speaktype.build/Debug/speaktype.build/Objects-normal/arm64 -target arm64-apple-macos26.1 -module-can-import-version AppKit 2685.20.119 2685.20.119 -module-can-import-version DeveloperToolsSupport 23.0.4 23.0.4 -module-can-import-version SwiftUI 7.1.13.1 7.1.13 -load-resolved-plugin /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib/swift/host/plugins/libFoundationMacros.dylib\#/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/bin/swift-plugin-server\#FoundationMacros -load-resolved-plugin /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib/swift/host/plugins/libObservationMacros.dylib\#/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/bin/swift-plugin-server\#ObservationMacros -load-resolved-plugin /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib/swift/host/plugins/libPreviewsMacros.dylib\#/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/bin/swift-plugin-server\#PreviewsMacros -load-resolved-plugin /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib/swift/host/plugins/libSwiftMacros.dylib\#/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/bin/swift-plugin-server\#SwiftMacros -load-resolved-plugin /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib/swift/host/plugins/libSwiftUIMacros.dylib\#/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/bin/swift-plugin-server\#SwiftUIMacros -disable-implicit-swift-modules -Xcc -fno-implicit-modules -Xcc -fno-implicit-module-maps -explicit-swift-module-map-file /Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Intermediates.noindex/speaktype.build/Debug/speaktype.build/Objects-normal/arm64/speaktype-dependencies-1.json -Xllvm -aarch64-use-tbi -enable-objc-interop -stack-check -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX26.1.sdk -I /Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Products/Debug -F /Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Products/Debug/PackageFrameworks -F /Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Products/Debug/PackageFrameworks -F /Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Products/Debug/PackageFrameworks -F /Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Products/Debug/PackageFrameworks -F /Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Products/Debug/PackageFrameworks -F /Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Products/Debug/PackageFrameworks -F /Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Products/Debug -no-color-diagnostics -Xcc -fno-color-diagnostics -enable-testing -g -debug-info-format\=dwarf -dwarf-version\=5 -module-cache-path /Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Intermediates.noindex/SwiftExplicitPrecompiledModules -profile-generate -profile-coverage-mapping -swift-version 5 -enforce-exclusivity\=checked -Onone -D DEBUG -serialize-debugging-options -const-gather-protocols-file /Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Intermediates.noindex/speaktype.build/Debug/speaktype.build/Objects-normal/arm64/speaktype_const_extract_protocols.json -enable-upcoming-feature DisableOutwardActorInference -enable-upcoming-feature InferSendableFromCaptures -enable-upcoming-feature GlobalActorIsolatedTypesUsability -enable-upcoming-feature MemberImportVisibility -enable-upcoming-feature InferIsolatedConformances -enable-upcoming-feature NonisolatedNonsendingByDefault -enable-experimental-feature DebugDescriptionMacro -enable-bare-slash-regex -default-isolation\=MainActor -empty-abi-descriptor -validate-clang-modules-once -clang-build-session-file /Users/orm/Library/Developer/Xcode/DerivedData/ModuleCache.noindex/Session.modulevalidation -Xcc -working-directory -Xcc /Users/orm/speaktype -enable-anonymous-context-mangled-names -file-compilation-dir /Users/orm/speaktype -Xcc -D_LIBCPP_HARDENING_MODE\=_LIBCPP_HARDENING_MODE_DEBUG -Xcc -ivfsstatcache -Xcc /Users/orm/Library/Developer/Xcode/DerivedData/SDKStatCaches.noindex/macosx26.1-25B74-3ae1261907bdadc328169391a47051a6.sdkstatcache -Xcc -I/Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Intermediates.noindex/speaktype.build/Debug/speaktype.build/swift-overrides.hmap -Xcc -iquote -Xcc /Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Intermediates.noindex/speaktype.build/Debug/speaktype.build/speaktype-generated-files.hmap -Xcc -I/Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Intermediates.noindex/speaktype.build/Debug/speaktype.build/speaktype-own-target-headers.hmap -Xcc -I/Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Intermediates.noindex/speaktype.build/Debug/speaktype.build/speaktype-all-non-framework-target-headers.hmap -Xcc -ivfsoverlay -Xcc /Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Intermediates.noindex/speaktype.build/Debug/speaktype-a09c45879bccb08166620f197fcee798-VFS/all-product-headers.yaml -Xcc -iquote -Xcc /Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Intermediates.noindex/speaktype.build/Debug/speaktype.build/speaktype-project-headers.hmap -Xcc -I/Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Products/Debug/include -Xcc -I/Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Intermediates.noindex/speaktype.build/Debug/speaktype.build/DerivedSources-normal/arm64 -Xcc -I/Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Intermediates.noindex/speaktype.build/Debug/speaktype.build/DerivedSources/arm64 -Xcc -I/Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Intermediates.noindex/speaktype.build/Debug/speaktype.build/DerivedSources -Xcc -DDEBUG\=1 -no-auto-bridging-header-chaining -module-name speaktype -frontend-parseable-output -disable-clang-spi -target-sdk-version 26.1 -target-sdk-name macosx26.1 -clang-target arm64-apple-macos26.1 -in-process-plugin-server-path /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/host/libSwiftInProcPluginServer.dylib -o /Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Build/Intermediates.noindex/speaktype.build/Debug/speaktype.build/Objects-normal/arm64/HistoryView.o -index-unit-output-path /speaktype.build/Debug/speaktype.build/Objects-normal/arm64/HistoryView.o -index-store-path /Users/orm/Library/Developer/Xcode/DerivedData/speaktype-dhvydfokwnwfendhtkesaktuqgcq/Index.noindex/DataStore -index-system-modules
-/Users/orm/speaktype/speaktype.xcodeproj: warning: The Copy Bundle Resources build phase contains this target's Info.plist file '/Users/orm/speaktype/speaktype/Resources/Info.plist'. (in target 'speaktype' from project 'speaktype')
-** BUILD FAILED **
-
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
deleted file mode 100644
index 8030cf7..0000000
--- a/docs/ARCHITECTURE.md
+++ /dev/null
@@ -1,297 +0,0 @@
-# Architecture
-
-## Overview
-
-SpeakType follows the MVVM (Model-View-ViewModel) architecture pattern, which provides clear separation of concerns and makes the codebase maintainable and testable.
-
-## Architecture Layers
-
-### 1. Models (`Models/`)
-
-Models represent the data structures and business entities in the application.
-
-**Responsibilities:**
-- Define data structures
-- Implement Codable for serialization
-- Business logic related to data validation
-- Computed properties for derived data
-
-**Example:**
-```swift
-struct User: Codable, Identifiable {
- let id: UUID
- var name: String
- var email: String
-
- var displayName: String {
- name.isEmpty ? email : name
- }
-}
-```
-
-### 2. Views (`Views/`)
-
-Views are SwiftUI components that display data and handle user interactions.
-
-**Responsibilities:**
-- Display UI elements
-- Handle user input
-- Observe ViewModel state
-- Navigate between screens
-
-**Structure:**
-- `Views/Screens/` - Full screen views
-- `Views/Components/` - Reusable UI components
-
-**Example:**
-```swift
-struct UserProfileView: View {
- @StateObject private var viewModel = UserProfileViewModel()
-
- var body: some View {
- VStack {
- Text(viewModel.user.displayName)
- Button("Update") {
- viewModel.updateProfile()
- }
- }
- }
-}
-```
-
-### 3. ViewModels (`ViewModels/`)
-
-ViewModels manage view state and handle business logic.
-
-**Responsibilities:**
-- Manage view state
-- Handle user actions
-- Coordinate with Services
-- Transform data for display
-- Handle validation
-
-**Guidelines:**
-- Conform to `ObservableObject`
-- Use `@Published` for state that views observe
-- Keep UI-independent (no SwiftUI imports except for simple types)
-- Use dependency injection for services
-
-**Example:**
-```swift
-class UserProfileViewModel: ObservableObject {
- @Published var user: User
- @Published var isLoading = false
- @Published var errorMessage: String?
-
- private let userService: UserServiceProtocol
-
- init(userService: UserServiceProtocol = UserService()) {
- self.userService = userService
- }
-
- func updateProfile() {
- isLoading = true
- Task {
- do {
- user = try await userService.updateUser(user)
- isLoading = false
- } catch {
- errorMessage = error.localizedDescription
- isLoading = false
- }
- }
- }
-}
-```
-
-### 4. Services (`Services/`)
-
-Services handle business logic, data persistence, and external communication.
-
-**Responsibilities:**
-- Network requests
-- Data persistence
-- Business logic
-- External API integration
-
-**Structure:**
-- `Services/Network/` - Networking layer
-- `Services/Storage/` - Data persistence (UserDefaults, CoreData, etc.)
-
-**Guidelines:**
-- Define protocols for testability
-- Use async/await for asynchronous operations
-- Handle errors appropriately
-- Keep services focused on a single responsibility
-
-**Example:**
-```swift
-protocol UserServiceProtocol {
- func fetchUser(id: UUID) async throws -> User
- func updateUser(_ user: User) async throws -> User
-}
-
-class UserService: UserServiceProtocol {
- private let networkManager: NetworkManager
-
- init(networkManager: NetworkManager = .shared) {
- self.networkManager = networkManager
- }
-
- func fetchUser(id: UUID) async throws -> User {
- try await networkManager.request(endpoint: .user(id))
- }
-
- func updateUser(_ user: User) async throws -> User {
- try await networkManager.request(endpoint: .updateUser(user))
- }
-}
-```
-
-### 5. Utilities (`Utilities/`)
-
-Helper functions, extensions, and constants.
-
-**Structure:**
-- `Utilities/Extensions/` - Swift and SwiftUI extensions
-- `Utilities/Helpers/` - Helper functions and utilities
-- `Utilities/Constants.swift` - App-wide constants
-
-## Data Flow
-
-1. **User Interaction** β View receives user input
-2. **View β ViewModel** β View calls ViewModel method
-3. **ViewModel β Service** β ViewModel requests data from Service
-4. **Service β External** β Service fetches/saves data
-5. **Service β ViewModel** β Service returns data to ViewModel
-6. **ViewModel β View** β ViewModel updates @Published properties
-7. **View Update** β SwiftUI automatically re-renders view
-
-## Dependency Injection
-
-Use protocol-based dependency injection for better testability:
-
-```swift
-// Define protocol
-protocol DataServiceProtocol {
- func fetchData() async throws -> [Item]
-}
-
-// Implement service
-class DataService: DataServiceProtocol {
- func fetchData() async throws -> [Item] {
- // Implementation
- }
-}
-
-// Inject in ViewModel
-class MyViewModel: ObservableObject {
- private let dataService: DataServiceProtocol
-
- init(dataService: DataServiceProtocol = DataService()) {
- self.dataService = dataService
- }
-}
-
-// Easy to mock for testing
-class MockDataService: DataServiceProtocol {
- func fetchData() async throws -> [Item] {
- return [Item(id: 1, name: "Test")]
- }
-}
-```
-
-## State Management
-
-### Local State
-Use `@State` for view-local state:
-```swift
-@State private var isExpanded = false
-```
-
-### Shared State
-Use `@StateObject` for view-owned objects:
-```swift
-@StateObject private var viewModel = MyViewModel()
-```
-
-Use `@ObservedObject` for passed-in objects:
-```swift
-@ObservedObject var viewModel: MyViewModel
-```
-
-### App-Wide State
-Use `@EnvironmentObject` for app-wide shared state:
-```swift
-@EnvironmentObject var appState: AppState
-```
-
-## Error Handling
-
-1. **Service Layer**: Throw specific errors
-2. **ViewModel Layer**: Catch and transform errors for display
-3. **View Layer**: Display errors to user
-
-```swift
-// Custom errors
-enum NetworkError: LocalizedError {
- case invalidURL
- case noData
- case decodingError
-
- var errorDescription: String? {
- switch self {
- case .invalidURL: return "Invalid URL"
- case .noData: return "No data received"
- case .decodingError: return "Failed to decode data"
- }
- }
-}
-
-// ViewModel handles errors
-@Published var errorMessage: String?
-
-func loadData() {
- Task {
- do {
- data = try await service.fetchData()
- } catch {
- errorMessage = error.localizedDescription
- }
- }
-}
-```
-
-## Testing Strategy
-
-### Unit Tests
-- Test ViewModels with mocked services
-- Test Models for validation logic
-- Test Services with mocked network layer
-
-### UI Tests
-- Test critical user flows
-- Test navigation
-- Test error states
-
-### Integration Tests
-- Test ViewModel + Service integration
-- Test data persistence
-
-## Best Practices
-
-1. **Single Responsibility**: Each component should have one clear purpose
-2. **Dependency Injection**: Use protocols for testability
-3. **Async/Await**: Use modern concurrency for asynchronous operations
-4. **Error Handling**: Handle errors at appropriate layers
-5. **Immutability**: Prefer `let` over `var` when possible
-6. **Type Safety**: Use Swift's type system to prevent errors
-7. **Documentation**: Document complex logic and public APIs
-8. **Testing**: Write tests for business logic and critical flows
-
-## Resources
-
-- [Swift API Design Guidelines](https://swift.org/documentation/api-design-guidelines/)
-- [SwiftUI Documentation](https://developer.apple.com/documentation/swiftui/)
-- [Swift Concurrency](https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html)
-
diff --git a/docs/FOLDER_STRUCTURE.md b/docs/FOLDER_STRUCTURE.md
deleted file mode 100644
index 95f3150..0000000
--- a/docs/FOLDER_STRUCTURE.md
+++ /dev/null
@@ -1,357 +0,0 @@
-# Folder Structure Guidelines
-
-This document outlines the folder structure and organization guidelines for the SpeakType macOS application.
-
-## Project Root Structure
-
-```
-speaktype/
-βββ .git/ # Git repository
-βββ .gitignore # Git ignore rules
-βββ .swiftlint.yml # SwiftLint configuration
-βββ README.md # Project overview
-βββ CONTRIBUTING.md # Contribution guidelines
-βββ speaktype.xcodeproj/ # Xcode project file
-βββ speaktype/ # Main application source
-βββ speaktypeTests/ # Unit tests
-βββ speaktypeUITests/ # UI tests
-βββ docs/ # Documentation
-```
-
-## Main Application Structure (`speaktype/`)
-
-```
-speaktype/
-βββ App/
-β βββ speaktypeApp.swift # App entry point
-β βββ AppDelegate.swift # App delegate (if needed for AppKit)
-β
-βββ Views/
-β βββ ContentView.swift # Main content view
-β βββ Screens/ # Full-screen views
-β β βββ HomeScreen.swift
-β β βββ SettingsScreen.swift
-β β βββ ...
-β βββ Components/ # Reusable UI components
-β βββ Buttons/
-β β βββ PrimaryButton.swift
-β β βββ SecondaryButton.swift
-β βββ Cards/
-β β βββ InfoCard.swift
-β βββ ...
-β
-βββ ViewModels/
-β βββ HomeViewModel.swift
-β βββ SettingsViewModel.swift
-β βββ ...
-β
-βββ Models/
-β βββ User.swift
-β βββ Settings.swift
-β βββ ...
-β
-βββ Services/
-β βββ Network/
-β β βββ NetworkManager.swift
-β β βββ APIEndpoint.swift
-β β βββ NetworkError.swift
-β βββ Storage/
-β β βββ UserDefaultsManager.swift
-β β βββ KeychainManager.swift
-β β βββ FileManager+Extensions.swift
-β βββ ...
-β
-βββ Utilities/
-β βββ Constants.swift
-β βββ Extensions/
-β β βββ View+Extensions.swift
-β β βββ Color+Extensions.swift
-β β βββ String+Extensions.swift
-β β βββ ...
-β βββ Helpers/
-β βββ DateFormatter+Helpers.swift
-β βββ ValidationHelpers.swift
-β βββ ...
-β
-βββ Resources/
- βββ Assets.xcassets/ # Images, colors, icons
- β βββ AppIcon.appiconset/
- β βββ AccentColor.colorset/
- β βββ Images/
- β βββ Colors/
- βββ Localizable.strings # Localization strings
- βββ Info.plist # App configuration
- βββ speaktype.entitlements # App entitlements
-```
-
-## Testing Structure
-
-### Unit Tests (`speaktypeTests/`)
-
-```
-speaktypeTests/
-βββ ViewModelTests/
-β βββ HomeViewModelTests.swift
-β βββ SettingsViewModelTests.swift
-βββ ModelTests/
-β βββ UserTests.swift
-β βββ SettingsTests.swift
-βββ ServiceTests/
-β βββ NetworkManagerTests.swift
-β βββ StorageManagerTests.swift
-βββ Mocks/
-β βββ MockNetworkManager.swift
-β βββ MockStorageManager.swift
-βββ Helpers/
- βββ TestHelpers.swift
-```
-
-### UI Tests (`speaktypeUITests/`)
-
-```
-speaktypeUITests/
-βββ Screens/
-β βββ HomeScreenUITests.swift
-β βββ SettingsScreenUITests.swift
-βββ Flows/
-β βββ OnboardingFlowTests.swift
-β βββ UserFlowTests.swift
-βββ Helpers/
- βββ UITestHelpers.swift
-```
-
-## Documentation Structure (`docs/`)
-
-```
-docs/
-βββ ARCHITECTURE.md # Architecture overview
-βββ FOLDER_STRUCTURE.md # This file
-βββ API.md # API documentation
-βββ SETUP.md # Setup instructions
-βββ images/ # Documentation images
- βββ architecture-diagram.png
-```
-
-## File Naming Conventions
-
-### Swift Files
-
-1. **Views**: Use descriptive names ending with `View`
- - `HomeView.swift`
- - `SettingsView.swift`
- - `UserProfileView.swift`
-
-2. **ViewModels**: Match view name + `ViewModel`
- - `HomeViewModel.swift`
- - `SettingsViewModel.swift`
- - `UserProfileViewModel.swift`
-
-3. **Models**: Use singular nouns
- - `User.swift`
- - `Settings.swift`
- - `Article.swift`
-
-4. **Services**: Descriptive name + `Manager` or `Service`
- - `NetworkManager.swift`
- - `AuthenticationService.swift`
- - `StorageManager.swift`
-
-5. **Extensions**: Type name + `+Extensions`
- - `View+Extensions.swift`
- - `Color+Extensions.swift`
- - `String+Extensions.swift`
-
-6. **Protocols**: Descriptive name + `Protocol` or `-able` suffix
- - `NetworkServiceProtocol.swift`
- - `Cacheable.swift`
- - `Validatable.swift`
-
-### Test Files
-
-Test files should mirror the structure of the main app with `Tests` suffix:
-- `HomeViewModel.swift` β `HomeViewModelTests.swift`
-- `User.swift` β `UserTests.swift`
-
-## Organization Guidelines
-
-### 1. Group Related Files
-
-Use Xcode groups (folders) to organize related files:
-- Group views by feature or screen
-- Group models by domain
-- Group services by functionality
-
-### 2. Keep Files Focused
-
-- One primary type per file
-- Related extensions can be in the same file
-- Keep files under 400 lines when possible
-
-### 3. Use MARK Comments
-
-Organize code within files using `// MARK:` comments:
-
-```swift
-// MARK: - Properties
-private let service: DataService
-@Published var items: [Item] = []
-
-// MARK: - Initialization
-init(service: DataService = DataService()) {
- self.service = service
-}
-
-// MARK: - Public Methods
-func loadData() {
- // ...
-}
-
-// MARK: - Private Methods
-private func processData() {
- // ...
-}
-```
-
-### 4. Alphabetical Ordering
-
-Within groups, order files alphabetically for easy navigation.
-
-### 5. Shared Components
-
-Place reusable components in appropriate shared folders:
-- `Views/Components/` for UI components
-- `Utilities/Extensions/` for extensions
-- `Utilities/Helpers/` for helper functions
-
-## Asset Organization
-
-### Assets.xcassets Structure
-
-```
-Assets.xcassets/
-βββ AppIcon.appiconset/
-βββ AccentColor.colorset/
-βββ Colors/
-β βββ PrimaryColor.colorset/
-β βββ SecondaryColor.colorset/
-β βββ BackgroundColor.colorset/
-βββ Images/
-β βββ Icons/
-β β βββ HomeIcon.imageset/
-β β βββ SettingsIcon.imageset/
-β βββ Illustrations/
-β βββ EmptyState.imageset/
-βββ Symbols/
- βββ CustomSymbol.symbolset/
-```
-
-### Asset Naming
-
-- Use descriptive, lowercase names with hyphens
-- Group related assets with prefixes
-- Examples:
- - `icon-home`
- - `button-primary-background`
- - `illustration-empty-state`
-
-## Configuration Files
-
-### Root Level Configuration
-
-- `.gitignore` - Git ignore patterns
-- `.swiftlint.yml` - SwiftLint rules
-- `README.md` - Project overview
-- `CONTRIBUTING.md` - Contribution guidelines
-
-### Xcode Configuration
-
-- `Info.plist` - App metadata and configuration
-- `*.entitlements` - App capabilities and permissions
-- `*.xcconfig` - Build configuration (optional)
-
-## Best Practices
-
-### 1. Consistent Structure
-
-Maintain consistent folder structure across features:
-```
-Feature/
-βββ FeatureView.swift
-βββ FeatureViewModel.swift
-βββ FeatureModel.swift
-βββ Components/
- βββ FeatureComponent.swift
-```
-
-### 2. Avoid Deep Nesting
-
-Keep folder hierarchy shallow (max 3-4 levels deep).
-
-### 3. Clear Boundaries
-
-Maintain clear separation between:
-- UI (Views)
-- Logic (ViewModels)
-- Data (Models)
-- Services
-
-### 4. Scalability
-
-Structure should scale as the app grows:
-- Easy to add new features
-- Easy to find existing code
-- Easy to refactor
-
-### 5. Team Collaboration
-
-- Clear structure helps team members find code
-- Consistent naming reduces confusion
-- Documented conventions prevent debates
-
-## Adding New Features
-
-When adding a new feature, follow this checklist:
-
-1. **Create folder structure**:
- ```
- Views/Screens/NewFeature/
- ViewModels/
- Models/
- ```
-
-2. **Add necessary files**:
- - View file(s)
- - ViewModel
- - Model (if needed)
- - Service (if needed)
-
-3. **Add tests**:
- - Unit tests for ViewModel
- - UI tests for critical flows
-
-4. **Update documentation**:
- - Update README if needed
- - Add API documentation
- - Update architecture docs
-
-## Migration Guide
-
-If you're reorganizing existing code:
-
-1. **Plan the structure** first
-2. **Move files** in Xcode (not Finder) to maintain references
-3. **Update imports** if needed
-4. **Run tests** to ensure nothing broke
-5. **Update documentation**
-6. **Commit changes** with clear message
-
-## Questions?
-
-If you're unsure where to place a file:
-1. Check existing similar files
-2. Refer to this document
-3. Ask the team
-4. Document your decision
-
-Remember: Consistency is more important than perfection!
-
diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md
deleted file mode 100644
index 9d1c8ff..0000000
--- a/docs/IMPLEMENTATION_PLAN.md
+++ /dev/null
@@ -1,752 +0,0 @@
-# SpeakType Implementation Plan
-
-## Project Overview
-A macOS system-wide voice-to-text utility that enables fast, offline voice dictation across all applications. Inspired by VoiceInk, with a goal to ship an MVP within 3-4 days.
-
-## Timeline: 3-4 Days MVP
-
----
-
-## Phase 1: Foundation & Setup (Day 1 - Morning)
-**Goal**: Set up core project structure and dependencies
-
-### 1.1 Project Configuration & Dependencies
-- [ ] Update `Info.plist` with required permissions
- - Microphone usage description
- - Accessibility API usage description
-- [ ] Add entitlements
- - `com.apple.security.device.microphone`
- - `com.apple.security.automation.apple-events`
- - App Sandbox with necessary permissions
-- [ ] Research and integrate Whisper model
- - Evaluate `whisper.cpp` Swift bindings
- - Alternative: `WhisperKit` framework
- - Download base/small model files
-- [ ] Update Xcode project settings
- - Set minimum macOS version to 13.0+
- - Configure build settings for menu bar app
- - Add LSUIElement = YES for menu bar only app
-
-### 1.2 Core Models
-**Files to create in `Models/`:**
-- [ ] `TranscriptionState.swift` - Enum for app states (idle, listening, transcribing, ready)
-- [ ] `TranscriptionResult.swift` - Model for transcription data
-- [ ] `AppSettings.swift` - User preferences model
-- [ ] `RecordingSession.swift` - Audio recording session data
-- [ ] `HotkeyConfiguration.swift` - Keyboard shortcut configuration
-
----
-
-## Phase 2: Core Services (Day 1 - Afternoon)
-**Goal**: Implement essential services for audio, transcription, and system integration
-
-### 2.1 Audio Service
-**Create `Services/Audio/`:**
-- [ ] `AudioRecordingService.swift`
- - Initialize AVAudioEngine
- - Configure audio session
- - Start/stop recording
- - Buffer audio data
- - Handle microphone permissions
-- [ ] `AudioBufferManager.swift`
- - Manage audio buffer
- - Convert audio format for Whisper
- - Handle memory efficiently
-- [ ] `AudioDeviceManager.swift`
- - Enumerate available audio input devices
- - Select/switch between devices
- - Monitor device changes (connect/disconnect)
- - Get device metadata (name, sample rate, channels)
-- [ ] `MicrophonePermissionManager.swift`
- - Request microphone access
- - Handle permission states
- - Provide user-friendly error messages
-
-### 2.2 Transcription Service
-**Create `Services/Transcription/`:**
-- [ ] `WhisperService.swift`
- - Initialize Whisper model
- - Load model on app launch
- - Transcribe audio buffer
- - Handle model lifecycle
- - Support base/small model selection
-- [ ] `TranscriptionQueue.swift`
- - Queue transcription requests
- - Handle concurrent requests
- - Cancel pending requests
-- [ ] `TranscriptionCache.swift` (optional)
- - Cache recent transcriptions
- - Improve performance
-
-### 2.3 System Integration Services
-**Create `Services/System/`:**
-- [ ] `HotkeyService.swift`
- - Register global keyboard shortcut
- - Handle press and release events
- - Support customizable hotkeys
- - Use Carbon or modern Swift alternatives
-- [ ] `ClipboardService.swift`
- - Copy text to clipboard
- - Read current clipboard (if needed)
- - Handle clipboard history
-- [ ] `AccessibilityService.swift`
- - Request accessibility permissions
- - Implement auto-paste functionality
- - Get active application
- - Paste text programmatically
-- [ ] `PermissionCoordinator.swift`
- - Centralize permission management
- - Check all required permissions
- - Guide user through permission setup
-
----
-
-## Phase 3: UI Components (Day 2 - Morning)
-**Goal**: Build all UI components following macOS design guidelines
-
-### 3.1 Menu Bar Integration
-**Create `Views/MenuBar/`:**
-- [ ] `MenuBarView.swift`
- - Menu bar icon with SF Symbol
- - Status indicator overlay
- - Popover presentation
-- [ ] `MenuBarPopoverView.swift`
- - App status display
- - Quick action buttons
- - Settings navigation
- - Quit button
-
-### 3.2 Floating Overlays
-**Create `Views/Overlays/`:**
-- [ ] `ListeningOverlayView.swift`
- - Centered floating panel
- - "Listening..." title
- - Recording indicator (animated)
- - Waveform visualization (optional)
- - Instructions text
- - Blur/vibrancy background
-- [ ] `TranscribingOverlayView.swift`
- - "Transcribing..." message
- - Loading spinner
- - Cancel button (optional)
-
-### 3.3 Result Display
-**Create `Views/Results/`:**
-- [ ] `TranscriptionResultView.swift`
- - Scrollable text display
- - Editable text field
- - Action buttons (Copy, Paste, Save, Dismiss)
- - Keyboard shortcuts
- - Auto-dismiss timer option
-
-### 3.4 Settings Screen
-**Create `Views/Settings/`:**
-- [ ] `SettingsView.swift`
- - Tab-based navigation (SwiftUI native)
- - Organized sections
-- [ ] `GeneralSettingsView.swift`
- - Hotkey customization
- - Launch at login toggle
- - Status in menu bar preferences
-- [ ] `AudioInputSettingsView.swift`
- - Audio device selection
- - Input mode (System Default, Custom Device, Prioritized)
- - Device list with active indicator
- - Refresh devices button
-- [ ] `TranscriptionSettingsView.swift`
- - Model selection (Base/Small)
- - Model details (size, speed, accuracy indicators)
- - Download/manage models
- - Language (English - fixed for MVP)
- - Show confidence scores (optional)
-- [ ] `ClipboardSettingsView.swift`
- - Auto-paste toggle
- - Auto-dismiss result panel
- - Clipboard history (optional)
-- [ ] `AboutSettingsView.swift`
- - App version
- - Open-source notice
- - GitHub link
- - Privacy policy
- - Model information
-
-### 3.5 Permission Screens
-**Create `Views/Permissions/`:**
-- [ ] `PermissionOnboardingView.swift`
- - First-launch wizard
- - Explain required permissions
- - Guide to system preferences
-- [ ] `PermissionRequestView.swift`
- - Individual permission cards
- - Open System Preferences buttons
- - Status indicators
-
-### 3.6 Reusable Components
-**Create `Views/Components/`:**
-- [ ] `RecordingIndicator.swift`
- - Animated recording dot
- - Customizable colors
-- [ ] `WaveformView.swift` (optional)
- - Real-time waveform visualization
- - Level meter
-- [ ] `StatusBadge.swift`
- - Color-coded status indicators
-- [ ] `ActionButton.swift`
- - Consistent button styling
- - Hover effects
-- [ ] `FloatingPanel.swift`
- - Reusable floating window wrapper
- - Blur/vibrancy materials
-
----
-
-## Phase 4: ViewModels & State Management (Day 2 - Afternoon)
-**Goal**: Connect UI to services with proper state management
-
-### 4.1 Core ViewModels
-**Create `ViewModels/`:**
-- [ ] `AppViewModel.swift`
- - Central app state coordinator
- - Manage app lifecycle
- - Coordinate between services
- - Handle global hotkey events
- - Publish transcription states
-- [ ] `RecordingViewModel.swift`
- - Control recording session
- - Monitor audio levels
- - Handle start/stop events
- - Publish recording duration
-- [ ] `TranscriptionViewModel.swift`
- - Manage transcription process
- - Handle results
- - Error handling
- - Retry logic
-- [ ] `SettingsViewModel.swift`
- - Manage user preferences
- - Persist settings
- - Handle hotkey changes
- - Model selection
-- [ ] `PermissionViewModel.swift`
- - Check permission status
- - Request permissions
- - Guide user to settings
- - Track permission states
-
-### 4.2 State Management
-**Create `State/`:**
-- [ ] `AppState.swift`
- - Global app state (Environment Object)
- - Current transcription state
- - Active recording session
- - Settings
-- [ ] `NotificationCenter+App.swift`
- - Custom notifications for app events
- - Hotkey pressed/released
- - Transcription complete
- - Error notifications
-
----
-
-## Phase 5: Storage & Persistence (Day 2 - Evening)
-**Goal**: Implement data persistence
-
-### 5.1 Settings Storage
-**Create `Services/Storage/`:**
-- [ ] `SettingsStorageService.swift`
- - UserDefaults wrapper
- - Save/load app settings
- - Default values
- - Migration support
-- [ ] `HotkeyStorageService.swift`
- - Persist hotkey configuration
- - Validate hotkey combinations
-
-### 5.2 Optional History
-**Create if time permits:**
-- [ ] `TranscriptionHistoryService.swift`
- - Store recent transcriptions
- - Limit storage size
- - Clear history option
-- [ ] `TranscriptionHistory.swift` model
-
----
-
-## Phase 6: Integration & App Flow (Day 3 - Morning)
-**Goal**: Wire everything together into complete user flows
-
-### 6.1 App Entry Point
-**Update `App/`:**
-- [ ] `speaktypeApp.swift`
- - Initialize as menu bar app
- - Set up environment objects
- - Load Whisper model on launch
- - Register global hotkey
- - Show permissions if needed
-- [ ] `AppDelegate.swift`
- - Handle app lifecycle events
- - Terminate behavior
- - Dock icon management
-
-### 6.2 Main Coordinator
-**Create `Coordinators/`:**
-- [ ] `AppCoordinator.swift`
- - Coordinate user flows
- - Manage window presentation
- - Handle hotkey events
- - Show/hide overlays
- - Navigate to settings
-
-### 6.3 Window Management
-**Create `Utilities/Windows/`:**
-- [ ] `WindowManager.swift`
- - Manage floating windows
- - Position windows (center, etc.)
- - Window levels (floating on top)
- - Focus management
-- [ ] `PanelWindowController.swift`
- - Custom NSPanel for overlays
- - Transparency support
- - Click-through behavior
-
----
-
-## Phase 7: Complete User Flows (Day 3 - Afternoon)
-**Goal**: Implement end-to-end functionality
-
-### 7.1 Recording Flow
-**Implementation sequence:**
-1. [ ] User presses hotkey
-2. [ ] HotkeyService detects press
-3. [ ] AppViewModel updates state to `.listening`
-4. [ ] ListeningOverlayView appears
-5. [ ] AudioRecordingService starts recording
-6. [ ] Waveform/indicator shows activity
-7. [ ] User releases hotkey
-8. [ ] AudioRecordingService stops recording
-9. [ ] AppViewModel updates state to `.transcribing`
-10. [ ] TranscribingOverlayView appears
-11. [ ] WhisperService transcribes audio
-12. [ ] AppViewModel updates state to `.ready`
-13. [ ] TranscriptionResultView appears with text
-14. [ ] ClipboardService copies text
-15. [ ] (Optional) AccessibilityService pastes text
-16. [ ] Auto-dismiss or wait for user action
-
-### 7.2 Settings Flow
-**Implementation:**
-- [ ] Click settings in menu bar popover
-- [ ] Open settings window
-- [ ] Update preferences
-- [ ] Save changes
-- [ ] Apply changes immediately
-
-### 7.3 Error Flows
-**Handle edge cases:**
-- [ ] Microphone permission denied
-- [ ] Accessibility permission denied
-- [ ] Model loading failure
-- [ ] Transcription error
-- [ ] No audio detected
-- [ ] Empty transcription result
-
----
-
-## Phase 8: Polish & Refinement (Day 3 - Evening)
-**Goal**: Improve UX and fix issues
-
-### 8.1 Visual Polish
-- [ ] Implement blur/vibrancy effects
-- [ ] Add smooth animations
- - Overlay fade in/out
- - Recording indicator pulse
- - Button hover effects
-- [ ] Support light/dark mode
-- [ ] Adjust spacing and sizing
-- [ ] Icon refinement
-
-### 8.2 UX Improvements
-- [ ] Keyboard shortcuts for result panel
-- [ ] Escape to dismiss overlays
-- [ ] Sound feedback (optional)
-- [ ] Haptic feedback if supported
-- [ ] Loading states
-- [ ] Empty states
-- [ ] Error messages user-friendly
-
-### 8.3 Performance Optimization
-- [ ] Lazy load Whisper model
-- [ ] Optimize audio buffer size
-- [ ] Reduce memory footprint
-- [ ] Test with long recordings
-- [ ] Profile with Instruments
-
----
-
-## Phase 9: Testing & Bug Fixes (Day 4 - Morning)
-**Goal**: Ensure stability and reliability
-
-### 9.1 Unit Tests
-**Create in `speaktypeTests/`:**
-- [ ] `AudioRecordingServiceTests.swift`
-- [ ] `ClipboardServiceTests.swift`
-- [ ] `SettingsStorageServiceTests.swift`
-- [ ] `AppViewModelTests.swift`
-- [ ] `TranscriptionViewModelTests.swift`
-
-### 9.2 Integration Tests
-- [ ] Test full recording β transcription β paste flow
-- [ ] Test permission flows
-- [ ] Test settings persistence
-- [ ] Test hotkey registration
-
-### 9.3 Manual Testing
-**Test scenarios:**
-- [ ] First launch experience
-- [ ] Permission requests
-- [ ] Record short utterance (3-5 seconds)
-- [ ] Record long utterance (30+ seconds)
-- [ ] Test in various apps (Notes, Safari, Messages, etc.)
-- [ ] Change hotkey
-- [ ] Change model
-- [ ] Test with no microphone
-- [ ] Test with denied permissions
-- [ ] Light/dark mode switching
-- [ ] System sleep/wake
-
-### 9.4 Bug Fixes
-- [ ] Fix crashes
-- [ ] Fix memory leaks
-- [ ] Fix UI glitches
-- [ ] Fix permission issues
-- [ ] Fix transcription errors
-
----
-
-## Phase 10: Documentation & Release Prep (Day 4 - Afternoon)
-**Goal**: Prepare for release
-
-### 10.1 Documentation
-- [ ] Update `README.md`
- - Feature list
- - Installation instructions
- - Usage guide
- - Screenshots/GIFs
- - Permissions explanation
-- [ ] Create `docs/USER_GUIDE.md`
- - Getting started
- - Keyboard shortcuts
- - Settings explanation
- - Troubleshooting
-- [ ] Create `docs/PRIVACY.md`
- - Local-only processing
- - No data collection
- - Permissions explanation
-- [ ] Update `LICENSE` (MIT or Apache 2.0)
-- [ ] Create `CHANGELOG.md`
-
-### 10.2 Code Quality
-- [ ] Run SwiftLint and fix warnings
-- [ ] Add code documentation
-- [ ] Clean up commented code
-- [ ] Remove debug logs
-- [ ] Code review
-
-### 10.3 Build & Package
-- [ ] Create Release build configuration
-- [ ] Archive for distribution
-- [ ] Code signing setup
-- [ ] Notarization (if distributing outside App Store)
-- [ ] Create DMG installer (optional)
-
-### 10.4 Repository Setup
-- [ ] Create GitHub repository
-- [ ] Push code
-- [ ] Add README with badges
-- [ ] Create release notes
-- [ ] Tag v1.0.0
-
----
-
-## Technical Architecture
-
-### Key Technologies
-- **Language**: Swift 5.9+
-- **UI Framework**: SwiftUI + AppKit (for menu bar)
-- **Speech Recognition**: whisper.cpp or WhisperKit
-- **Audio**: AVFoundation
-- **Hotkeys**: Carbon API or modern alternative
-- **Persistence**: UserDefaults
-
-### Third-Party Dependencies
-1. **Whisper Integration** (Choose one):
- - `WhisperKit` - Swift package by Argmax
- - `whisper.cpp` - C++ implementation with Swift bindings
-
-2. **Global Hotkeys** (Choose one):
- - `KeyboardShortcuts` - Swift package by Sindre Sorhus
- - Custom Carbon API wrapper
-
-3. **Optional**:
- - `LaunchAtLogin` - Swift package for launch at login
-
-### Data Flow
-```
-User β Hotkey Press
- β
-Audio Recording Service
- β
-Audio Buffer
- β
-Whisper Service
- β
-Transcription Result
- β
-Clipboard Service
- β
-(Optional) Accessibility Service β Paste
-```
-
-### State Machine
-```
-Idle β Listening β Transcribing β Ready β Idle
- β β β
- (Error) β (Error) β (Error) β Idle
-```
-
----
-
-## File Structure Overview
-
-```
-speaktype/
-βββ App/
-β βββ speaktypeApp.swift
-β βββ AppDelegate.swift
-βββ Models/
-β βββ TranscriptionState.swift
-β βββ TranscriptionResult.swift
-β βββ AppSettings.swift
-β βββ RecordingSession.swift
-β βββ HotkeyConfiguration.swift
-β βββ AudioDevice.swift
-βββ Views/
-β βββ MenuBar/
-β β βββ MenuBarView.swift
-β β βββ MenuBarPopoverView.swift
-β βββ Overlays/
-β β βββ ListeningOverlayView.swift
-β β βββ TranscribingOverlayView.swift
-β βββ Results/
-β β βββ TranscriptionResultView.swift
-β βββ Settings/
-β β βββ SettingsView.swift
-β β βββ GeneralSettingsView.swift
-β β βββ TranscriptionSettingsView.swift
-β β βββ ClipboardSettingsView.swift
-β β βββ AboutSettingsView.swift
-β βββ Permissions/
-β β βββ PermissionOnboardingView.swift
-β β βββ PermissionRequestView.swift
-β βββ Components/
-β βββ RecordingIndicator.swift
-β βββ WaveformView.swift
-β βββ StatusBadge.swift
-β βββ ActionButton.swift
-β βββ FloatingPanel.swift
-βββ ViewModels/
-β βββ AppViewModel.swift
-β βββ RecordingViewModel.swift
-β βββ TranscriptionViewModel.swift
-β βββ SettingsViewModel.swift
-β βββ PermissionViewModel.swift
-βββ Services/
-β βββ Audio/
-β β βββ AudioRecordingService.swift
-β β βββ AudioBufferManager.swift
-β β βββ AudioDeviceManager.swift
-β β βββ MicrophonePermissionManager.swift
-β βββ Transcription/
-β β βββ WhisperService.swift
-β β βββ TranscriptionQueue.swift
-β β βββ TranscriptionCache.swift
-β βββ System/
-β β βββ HotkeyService.swift
-β β βββ ClipboardService.swift
-β β βββ AccessibilityService.swift
-β β βββ PermissionCoordinator.swift
-β βββ Storage/
-β βββ SettingsStorageService.swift
-β βββ HotkeyStorageService.swift
-β βββ TranscriptionHistoryService.swift
-βββ Coordinators/
-β βββ AppCoordinator.swift
-βββ State/
-β βββ AppState.swift
-β βββ NotificationCenter+App.swift
-βββ Utilities/
-β βββ Windows/
-β β βββ WindowManager.swift
-β β βββ PanelWindowController.swift
-β βββ Extensions/
-β β βββ View+Extensions.swift
-β β βββ Color+Extensions.swift
-β β βββ NSApplication+Extensions.swift
-β βββ Constants.swift
-βββ Resources/
- βββ Assets.xcassets/
- βββ Info.plist
- βββ speaktype.entitlements
-```
-
----
-
-## Risk Mitigation
-
-### High-Risk Items
-1. **Whisper Integration** (HIGH PRIORITY - Day 1)
- - Risk: Complex integration, large model files
- - Mitigation: Research on Day 1 morning, have fallback option
- - Fallback: Use system Speech framework temporarily
-
-2. **Global Hotkeys** (HIGH PRIORITY - Day 1)
- - Risk: macOS permissions, deprecated APIs
- - Mitigation: Use proven Swift package (KeyboardShortcuts)
- - Fallback: Manual activation from menu bar
-
-3. **Auto-paste Functionality** (MEDIUM PRIORITY)
- - Risk: Accessibility permissions complex
- - Mitigation: Make it optional, default to clipboard only
- - Fallback: Copy to clipboard only
-
-4. **Performance** (MEDIUM PRIORITY)
- - Risk: Model loading slow, transcription lag
- - Mitigation: Load model on app launch, optimize audio format
- - Test: Profile early and often
-
-### Contingency Plans
-- If Whisper integration takes too long β Use macOS Speech framework
-- If auto-paste is problematic β Focus on clipboard functionality
-- If custom UI is complex β Simplify to basic SwiftUI components
-- If 3-4 days is tight β Cut optional features (waveform, history)
-
----
-
-## Success Metrics (MVP)
-
-### Functional Requirements
-- β
Press hotkey β speak β release β text in clipboard
-- β
Works offline
-- β
Works in any text field
-- β
Transcription accuracy >80% for clear speech
-- β
Latency <5 seconds for short clips (<10 sec speech)
-
-### Quality Requirements
-- β
No crashes during basic usage
-- β
Proper error messages
-- β
Permissions clearly explained
-- β
Works on macOS 13.0+
-
-### Polish Requirements (Nice to have)
-- β
Smooth animations
-- β
Light/dark mode support
-- β
Keyboard shortcuts
-- β
Clean, minimal UI
-
----
-
-## Post-MVP Roadmap
-
-### Version 1.1 (Week 2)
-- Real-time streaming transcription
-- Multiple language support
-- Improved model selection
-- Transcription history
-- Custom vocabulary
-
-### Version 1.2 (Week 3-4)
-- Text formatting commands
-- Punctuation auto-formatting
-- Custom hotkeys per app
-- Profiles/presets
-- Cloud sync (optional)
-
-### Version 2.0 (Month 2)
-- iOS standalone app
-- macOS App Store release
-- Pro features
-- Advanced settings
-- Analytics dashboard
-
----
-
-## Daily Checkpoints
-
-### Day 1 End
-- [ ] Project configured with dependencies
-- [ ] Core models defined
-- [ ] Audio recording working
-- [ ] Whisper integration functional
-- [ ] Can record and transcribe (even if UI is basic)
-
-### Day 2 End
-- [ ] All UI components built
-- [ ] ViewModels connected
-- [ ] Settings working
-- [ ] Can use app end-to-end (rough but functional)
-
-### Day 3 End
-- [ ] Complete user flows working
-- [ ] Permissions handled
-- [ ] UI polished
-- [ ] Major bugs fixed
-
-### Day 4 End
-- [ ] Tests written and passing
-- [ ] Documentation complete
-- [ ] Release build created
-- [ ] GitHub repository ready
-
----
-
-## Resources
-
-### Documentation
-- [SwiftUI Documentation](https://developer.apple.com/documentation/swiftui/)
-- [AVFoundation Audio](https://developer.apple.com/documentation/avfoundation/audio/)
-- [Accessibility Programming Guide](https://developer.apple.com/accessibility/)
-- [Whisper Documentation](https://github.com/openai/whisper)
-
-### Swift Packages
-- [WhisperKit](https://github.com/argmaxinc/WhisperKit)
-- [KeyboardShortcuts](https://github.com/sindresorhus/KeyboardShortcuts)
-- [LaunchAtLogin](https://github.com/sindresorhus/LaunchAtLogin-Modern)
-
-### Inspiration
-- [VoiceInk](https://voices.ink/)
-- [Whisper UI Examples](https://github.com/topics/whisper-ui)
-
----
-
-## Notes
-
-- Prioritize functionality over polish for MVP
-- Test on real devices early and often
-- Keep UI simple and focused
-- Err on the side of fewer features done well
-- Document decisions and assumptions
-- Commit code frequently with clear messages
-
----
-
-## Getting Started
-
-To begin implementation:
-1. Read this plan thoroughly
-2. Set up development environment
-3. Start with Phase 1, Task 1.1
-4. Check off items as completed
-5. Update this document with learnings
-6. Adjust timeline if needed
-
-Good luck! π
-
diff --git a/docs/SETUP.md b/docs/SETUP.md
deleted file mode 100644
index 7deecf4..0000000
--- a/docs/SETUP.md
+++ /dev/null
@@ -1,313 +0,0 @@
-# Setup Guide
-
-This guide will help you set up the SpeakType development environment.
-
-## Prerequisites
-
-### Required
-
-- **macOS**: 13.0 (Ventura) or later
-- **Xcode**: 15.0 or later
-- **Swift**: 5.9 or later (included with Xcode)
-- **Git**: For version control
-
-### Recommended
-
-- **Homebrew**: Package manager for macOS
-- **SwiftLint**: Code linting tool
-- **Xcode Command Line Tools**: For terminal-based builds
-
-## Initial Setup
-
-### 1. Install Xcode
-
-Download and install Xcode from the Mac App Store or Apple Developer website.
-
-```bash
-# Verify Xcode installation
-xcode-select --version
-
-# Install command line tools if needed
-xcode-select --install
-```
-
-### 2. Install Homebrew (if not already installed)
-
-```bash
-/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
-```
-
-### 3. Install SwiftLint
-
-```bash
-brew install swiftlint
-```
-
-### 4. Clone the Repository
-
-```bash
-git clone
-cd speaktype
-```
-
-### 5. Open the Project
-
-```bash
-# Open in Xcode
-open speaktype.xcodeproj
-
-# Or use the Makefile
-make xcode
-```
-
-### 6. Build the Project
-
-In Xcode:
-- Select the `speaktype` scheme
-- Choose your Mac as the destination
-- Press `βB` to build
-
-Or from the terminal:
-```bash
-make build
-```
-
-## Development Workflow
-
-### Building
-
-```bash
-# Debug build
-make build
-
-# Release build
-make build-release
-
-# Clean build
-make clean
-```
-
-### Running
-
-```bash
-# Run from terminal
-make run
-
-# Or press βR in Xcode
-```
-
-### Testing
-
-```bash
-# Run all tests
-make test
-
-# Run unit tests only
-make test-unit
-
-# Run UI tests only
-make test-ui
-
-# Or press βU in Xcode
-```
-
-### Linting
-
-```bash
-# Check for linting issues
-make lint
-
-# Auto-fix linting issues
-make format
-```
-
-## Project Structure
-
-After setup, your project structure should look like this:
-
-```
-speaktype/
-βββ .git/ # Git repository
-βββ .gitignore # Git ignore rules
-βββ .swiftlint.yml # SwiftLint configuration
-βββ .editorconfig # Editor configuration
-βββ .gitattributes # Git attributes
-βββ Makefile # Build automation
-βββ README.md # Project overview
-βββ CONTRIBUTING.md # Contribution guidelines
-βββ speaktype.xcodeproj/ # Xcode project
-βββ speaktype/ # Main source code
-β βββ App/ # App entry point
-β βββ Views/ # UI views
-β βββ ViewModels/ # View models
-β βββ Models/ # Data models
-β βββ Services/ # Business logic
-β βββ Utilities/ # Helpers and extensions
-β βββ Resources/ # Assets and resources
-βββ speaktypeTests/ # Unit tests
-βββ speaktypeUITests/ # UI tests
-βββ docs/ # Documentation
-```
-
-## Configuration
-
-### Xcode Settings
-
-Recommended Xcode settings for development:
-
-1. **Text Editing**:
- - Enable: "Automatically trim trailing whitespace"
- - Enable: "Including whitespace-only lines"
- - Tab width: 4 spaces
- - Indent width: 4 spaces
- - Use spaces instead of tabs
-
-2. **Source Control**:
- - Enable: "Enable source control"
- - Enable: "Refresh local status automatically"
-
-3. **Build Settings**:
- - Swift Language Version: Swift 5
- - Optimization Level (Debug): -Onone
- - Optimization Level (Release): -O
-
-### SwiftLint Integration
-
-SwiftLint is configured via `.swiftlint.yml`. To integrate with Xcode:
-
-1. Open Xcode project
-2. Select the target
-3. Go to Build Phases
-4. Add a new "Run Script Phase"
-5. Add this script:
-
-```bash
-if which swiftlint >/dev/null; then
- swiftlint
-else
- echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
-fi
-```
-
-## Troubleshooting
-
-### Build Errors
-
-**Problem**: "No such module" errors
-
-**Solution**:
-```bash
-# Clean build folder
-make clean
-
-# Or in Xcode: Product > Clean Build Folder (β§βK)
-```
-
-**Problem**: SwiftLint warnings/errors
-
-**Solution**:
-```bash
-# Auto-fix issues
-make format
-
-# Or manually fix based on warnings
-```
-
-### Git Issues
-
-**Problem**: Line ending issues
-
-**Solution**: The project uses `.gitattributes` to normalize line endings. Ensure you have:
-```bash
-git config --global core.autocrlf input
-```
-
-**Problem**: Merge conflicts in `.pbxproj`
-
-**Solution**: The `.gitattributes` file is configured to use `merge=union` for project files, which helps reduce conflicts.
-
-### Xcode Issues
-
-**Problem**: Xcode not finding files
-
-**Solution**:
-1. Clean build folder (β§βK)
-2. Close Xcode
-3. Delete DerivedData:
- ```bash
- rm -rf ~/Library/Developer/Xcode/DerivedData
- ```
-4. Reopen project
-
-**Problem**: Simulator issues
-
-**Solution**:
-```bash
-# Reset simulator
-xcrun simctl erase all
-
-# Or use Xcode: Device > Erase All Content and Settings
-```
-
-## IDE Setup
-
-### Xcode Extensions
-
-Recommended Xcode extensions:
-- **SwiftLint for Xcode**: Real-time linting
-- **Xcode Themes**: Better color schemes
-
-### VS Code (Optional)
-
-If you prefer VS Code for editing:
-
-1. Install Swift extension
-2. Install SwiftLint extension
-3. Open workspace: `code .`
-
-`.editorconfig` is already configured for consistent formatting.
-
-## Environment Variables
-
-If your app requires environment variables:
-
-1. Create a `.env` file (ignored by git):
- ```bash
- API_KEY=your_api_key_here
- BASE_URL=https://api.example.com
- ```
-
-2. Add to Xcode scheme:
- - Edit Scheme > Run > Arguments
- - Add environment variables
-
-## Next Steps
-
-After setup:
-
-1. Read [ARCHITECTURE.md](ARCHITECTURE.md) to understand the project structure
-2. Read [FOLDER_STRUCTURE.md](FOLDER_STRUCTURE.md) for organization guidelines
-3. Read [CONTRIBUTING.md](../CONTRIBUTING.md) before making changes
-4. Run tests to ensure everything works: `make test`
-5. Start coding! π
-
-## Additional Resources
-
-- [Swift Documentation](https://swift.org/documentation/)
-- [SwiftUI Documentation](https://developer.apple.com/documentation/swiftui/)
-- [Xcode Documentation](https://developer.apple.com/documentation/xcode)
-- [Git Documentation](https://git-scm.com/doc)
-
-## Getting Help
-
-If you encounter issues:
-
-1. Check this documentation
-2. Search existing issues on GitHub
-3. Ask in team chat/Slack
-4. Create a new issue with details
-
-## License
-
-[Add your license information here]
-
diff --git a/speaktype.xcodeproj/project.pbxproj b/speaktype.xcodeproj/project.pbxproj
index 5483be6..5ad6273 100644
--- a/speaktype.xcodeproj/project.pbxproj
+++ b/speaktype.xcodeproj/project.pbxproj
@@ -35,9 +35,22 @@
362947392F0DBC5100BE895D /* speaktypeUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = speaktypeUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
+/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
+ 362947242F0DBC4E00BE895E /* Exceptions for "speaktype" folder in "speaktype" target */ = {
+ isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
+ membershipExceptions = (
+ Resources/Info.plist,
+ );
+ target = 362947212F0DBC4E00BE895D /* speaktype */;
+ };
+/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
+
/* Begin PBXFileSystemSynchronizedRootGroup section */
362947242F0DBC4E00BE895D /* speaktype */ = {
isa = PBXFileSystemSynchronizedRootGroup;
+ exceptions = (
+ 362947242F0DBC4E00BE895E /* Exceptions for "speaktype" folder in "speaktype" target */,
+ );
path = speaktype;
sourceTree = "";
};
diff --git a/speaktype/Assets.xcassets/AppIcon.appiconset/Contents.json b/speaktype/Assets.xcassets/AppIcon.appiconset/Contents.json
index 0386aff..fb38217 100644
--- a/speaktype/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/speaktype/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -59,12 +59,6 @@
"idiom": "mac",
"filename": "icon_1024x1024.png",
"scale": "2x"
- },
- {
- "size": "1024x1024",
- "idiom": "mac",
- "filename": "icon_1024x1024.png",
- "scale": "1x"
}
],
"info": {
diff --git a/speaktype/Controllers/MiniRecorderWindowController.swift b/speaktype/Controllers/MiniRecorderWindowController.swift
index b2937b9..a31cdbd 100644
--- a/speaktype/Controllers/MiniRecorderWindowController.swift
+++ b/speaktype/Controllers/MiniRecorderWindowController.swift
@@ -19,31 +19,14 @@ class MiniRecorderWindowController: NSObject {
panel.orderOut(nil)
} else {
print("Showing Mini Recorder Panel")
- // Center closely to mouse or screen center? Center of screen is safer.
- // But let's respect previous floating behavior.
- panel.center()
- // panel.makeKeyAndOrderFront(nil) -> Activates app. We want specific behavior.
- // We want it to float but NOT activate the app if possible?
- // Actually, for a recorder, we usually want it visible.
+ panel.center()
- // To prevent activating the Main App (and thus showing Main Dashboard if it was key),
- // we show the panel without activating the app?
- // "panel.orderFront(nil)" shows it.
-
- // However, to type into it (if needed) or interact, we might need activation.
- // But our Mini Recorder is button-based.
-
- // Critical: If we use NSApp.activate, it might switch to the main dashboard scene if that's "primary".
- // By managing this panel separately, we avoid SwiftUI triggering the main scene.
-
- // orderFrontRegardless allows showing the window without activating the app
+ // Show without activating to avoid pulling main app focus unnecessarily
panel.orderFrontRegardless()
- // panel.orderFrontDuringApplicationMakeKey() // Does not exist
- // panel.makeKeyAndOrderFront(nil)
- NSApp.activate(ignoringOtherApps: true)
+ NSApp.activate(ignoringOtherApps: true)
// Trigger instant recording
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
NotificationCenter.default.post(name: .hotkeyTriggered, object: nil)
}
}
diff --git a/speaktype/Models/AIModel.swift b/speaktype/Models/AIModel.swift
index 890c1cb..a9a495c 100644
--- a/speaktype/Models/AIModel.swift
+++ b/speaktype/Models/AIModel.swift
@@ -10,43 +10,29 @@ struct AIModel: Identifiable, Equatable {
static let availableModels: [AIModel] = [
AIModel(
- name: "Distil-Whisper Large v3",
- variant: "distil-whisper/distil-large-v3",
- details: "English β’ High Quality β’ Fast",
+ name: "Whisper Large v3 Turbo",
+ variant: "openai_whisper-large-v3_turbo",
+ details: "Multilingual β’ High Accuracy β’ 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"
+ size: "1.6 GB"
),
AIModel(
name: "Whisper Medium",
- variant: "openai/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",
+ variant: "openai_whisper-base.en",
details: "English-only β’ Optimized for Apple Silicon",
rating: "Standard",
size: "74 MB"
),
AIModel(
name: "Whisper Small",
- variant: "openai_whisper-small",
+ variant: "openai_whisper-small.en",
details: "English-only β’ Higher accuracy",
rating: "Good",
size: "244 MB"
diff --git a/speaktype/Services/ModelDownloadService.swift b/speaktype/Services/ModelDownloadService.swift
index b5acf63..61c3405 100644
--- a/speaktype/Services/ModelDownloadService.swift
+++ b/speaktype/Services/ModelDownloadService.swift
@@ -57,13 +57,13 @@ class ModelDownloadService: ObservableObject {
// try await WhisperKit.download(variant: variant) { progress in ... }
// likely: download(variant:progressCallback:) - 'from' usually has a default
- let path = try await WhisperKit.download(variant: variant, progressCallback: { progress in
+ let _ = try await WhisperKit.download(variant: variant, progressCallback: { progress in
DispatchQueue.main.async {
self.downloadProgress[variant] = progress.fractionCompleted
}
})
- print("Model downloaded to: \(path)")
+ print("Model downloaded successfully")
DispatchQueue.main.async {
self.isDownloading[variant] = false
@@ -74,37 +74,43 @@ class ModelDownloadService: ObservableObject {
// Auto-Repair: If duplicate models found, delete and retry ONCE
if error.localizedDescription.contains("Multiple models found") {
- print("Corruption detected. cleaning cache and retrying...")
- Task { @MainActor in
- self.downloadError[variant] = "Repairing..."
+ print("β οΈ Multiple models detected. Cleaning cache and retrying...")
+
+ await MainActor.run {
+ self.downloadError[variant] = "Cleaning duplicates..."
}
let log = await self.deleteModel(variant: variant)
+ print("π§Ή Cleanup result: \(log)")
+
+ // Give filesystem time to settle
+ try? await Task.sleep(nanoseconds: 2_000_000_000)
- Task { @MainActor in
- self.downloadError[variant] = "Repairing... \(log)"
- try? await Task.sleep(nanoseconds: 1_000_000_000) // Sleep 1s to let FS settle
+ await MainActor.run {
+ self.downloadError[variant] = "Retrying download..."
}
- // Retry download
- Task {
- do {
- let path = try await WhisperKit.download(variant: variant, progressCallback: { progress in
- DispatchQueue.main.async {
- self.downloadProgress[variant] = progress.fractionCompleted
- }
- })
+ // Retry download once
+ do {
+ let _ = try await WhisperKit.download(variant: variant, progressCallback: { progress in
DispatchQueue.main.async {
- self.isDownloading[variant] = false
- self.downloadProgress[variant] = 1.0
- self.downloadError[variant] = nil
- }
- } catch {
- DispatchQueue.main.async {
- self.isDownloading[variant] = false
- self.downloadProgress[variant] = 0.0
- self.downloadError[variant] = error.localizedDescription
+ self.downloadProgress[variant] = progress.fractionCompleted
}
+ })
+
+ print("β
Model downloaded successfully after cleanup")
+
+ DispatchQueue.main.async {
+ self.isDownloading[variant] = false
+ self.downloadProgress[variant] = 1.0
+ self.downloadError[variant] = nil
+ }
+ } catch {
+ print("β Retry failed: \(error)")
+ DispatchQueue.main.async {
+ self.isDownloading[variant] = false
+ self.downloadProgress[variant] = 0.0
+ self.downloadError[variant] = "Error: \(error.localizedDescription)\n\nTry clicking the trash icon to manually clean cache."
}
}
return
@@ -113,8 +119,7 @@ class ModelDownloadService: ObservableObject {
DispatchQueue.main.async {
self.isDownloading[variant] = false
self.downloadProgress[variant] = 0.0
- // Append hint
- self.downloadError[variant] = error.localizedDescription + "\n(Try Trash icon)"
+ self.downloadError[variant] = error.localizedDescription + "\n\n(Try Trash icon to clean cache)"
}
}
}
@@ -125,70 +130,98 @@ class ModelDownloadService: ObservableObject {
let fileManager = FileManager.default
let searchDirs: [FileManager.SearchPathDirectory] = [.documentDirectory, .applicationSupportDirectory, .cachesDirectory]
- // "openai/whisper-large-v3" -> "whisper-large-v3"
+ // Parse variant: "openai/whisper-medium" or "openai_whisper-medium"
let variantParts = variant.split(separator: "/")
let modelName = variantParts.last ?? Substring(variant)
+ // Also search for underscore version: openai_whisper-medium
+ let underscoreVariant = variant.replacingOccurrences(of: "/", with: "_")
+
var deletedCount = 0
var checkedPaths: [String] = []
+ print("ποΈ Searching for model caches matching: '\(modelName)' or '\(underscoreVariant)'")
+
// 1. Check Standard macOS Paths
for searchDir in searchDirs {
guard let baseDir = fileManager.urls(for: searchDir, in: .userDomainMask).first else { continue }
- // Check ./huggingface/models
- let hfDir = baseDir.appendingPathComponent("huggingface/models")
- checkedPaths.append(hfDir.path) // Checks: ~/Library/Application Support/huggingface/models
- deletedCount += cleanupDirectory(hfDir, match: modelName)
+ // Check ./huggingface/models (HuggingFace cache)
+ let hfModelsDir = baseDir.appendingPathComponent("huggingface/models")
+ checkedPaths.append(hfModelsDir.path)
+ deletedCount += cleanupDirectory(hfModelsDir, matchAny: [String(modelName), underscoreVariant])
+
+ // Check ./huggingface/hub (Alternative HF structure)
+ let hfHubDir = baseDir.appendingPathComponent("huggingface/hub")
+ checkedPaths.append(hfHubDir.path)
+ deletedCount += cleanupDirectory(hfHubDir, matchAny: [String(modelName), underscoreVariant])
+
+ // Check SpeakType-specific directory
+ let speaktypeDir = baseDir.appendingPathComponent("SpeakType/huggingface")
+ checkedPaths.append(speaktypeDir.path)
+ deletedCount += cleanupDirectory(speaktypeDir, matchAny: [String(modelName), underscoreVariant])
- // Check ./ (root of Documents/Support, sometimes WhisperKit dumps here)
- // check baseDir.path
- deletedCount += cleanupDirectory(baseDir, match: modelName)
+ // Check root directory (sometimes models are here)
+ deletedCount += cleanupDirectory(baseDir, matchAny: [String(modelName), underscoreVariant])
}
// 2. Check ~/.cache (Common for Python/Unix HF tools)
let homeDir = fileManager.homeDirectoryForCurrentUser
- let dotCache = homeDir.appendingPathComponent(".cache/huggingface/models")
- checkedPaths.append(dotCache.path)
- deletedCount += cleanupDirectory(dotCache, match: modelName)
+ let dotCacheModels = homeDir.appendingPathComponent(".cache/huggingface/models")
+ checkedPaths.append(dotCacheModels.path)
+ deletedCount += cleanupDirectory(dotCacheModels, matchAny: [String(modelName), underscoreVariant])
- // 3. Explicit Container Check (Fallback)
- let container = homeDir.appendingPathComponent("Library/Containers/org.speaktype.speaktype/Data/Documents/huggingface/models")
- checkedPaths.append(container.path)
- deletedCount += cleanupDirectory(container, match: modelName)
+ let dotCacheHub = homeDir.appendingPathComponent(".cache/huggingface/hub")
+ checkedPaths.append(dotCacheHub.path)
+ deletedCount += cleanupDirectory(dotCacheHub, matchAny: [String(modelName), underscoreVariant])
- // 4. Check Temporary Directory
+ // 3. Check Temporary Directory
let tempDir = fileManager.temporaryDirectory
- // check tempDir/huggingface/models and just tempDir
- let tempHf = tempDir.appendingPathComponent("huggingface/models")
- checkedPaths.append(tempDir.path)
+ let tempHf = tempDir.appendingPathComponent("huggingface")
checkedPaths.append(tempHf.path)
- deletedCount += cleanupDirectory(tempDir, match: modelName)
- deletedCount += cleanupDirectory(tempHf, match: modelName)
+ deletedCount += cleanupDirectory(tempHf, matchAny: [String(modelName), underscoreVariant])
+ deletedCount += cleanupDirectory(tempDir, matchAny: [String(modelName), underscoreVariant])
+
+ // 4. Check for models--* pattern in Application Support (WhisperKit style)
+ if let appSupport = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first {
+ let modelsPattern = "models--" + variant.replacingOccurrences(of: "/", with: "--")
+ deletedCount += cleanupDirectory(appSupport, matchAny: [modelsPattern])
+
+ // Also check nested huggingface/hub for models-- pattern
+ let hfHub = appSupport.appendingPathComponent("huggingface/hub")
+ deletedCount += cleanupDirectory(hfHub, matchAny: [modelsPattern])
+ }
+
+ print("ποΈ Cleanup complete. Deleted \(deletedCount) items from \(checkedPaths.count) locations")
if deletedCount > 0 {
- return "Deleted \(deletedCount) items"
+ return "Deleted \(deletedCount) items. Retry download."
} else {
- // Return only first 2 checked paths for brevity if they exist, or just summary
- return "No match for '\(modelName)' in \(checkedPaths.count) locations. checked: \(checkedPaths.map { $0.replacingOccurrences(of: homeDir.path, with: "~") }.joined(separator: ", "))"
+ return "No cached models found matching '\(modelName)'. This error may be in a different location."
}
}
- private func cleanupDirectory(_ dir: URL, match: Substring) -> Int {
+ private func cleanupDirectory(_ dir: URL, matchAny patterns: [String]) -> Int {
let fileManager = FileManager.default
guard let contents = try? fileManager.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil) else { return 0 }
var count = 0
for url in contents {
- if url.lastPathComponent.contains(match) {
- do {
- try fileManager.removeItem(at: url)
- print("Deleted corrupted cache: \(url.path)")
- count += 1
- } catch {
- print("Failed to delete \(url.path): \(error)")
- }
- }
+ let fileName = url.lastPathComponent
+ // Check if any pattern matches
+ let matches = patterns.contains { pattern in
+ fileName.contains(pattern) || fileName.contains(pattern.replacingOccurrences(of: "/", with: "--"))
+ }
+
+ if matches {
+ do {
+ try fileManager.removeItem(at: url)
+ print("β
Deleted cache: \(url.lastPathComponent)")
+ count += 1
+ } catch {
+ print("β Failed to delete \(url.lastPathComponent): \(error)")
+ }
+ }
}
return count
}
diff --git a/speaktype/Utilities/AppLogger.swift b/speaktype/Utilities/AppLogger.swift
new file mode 100644
index 0000000..bff3ef0
--- /dev/null
+++ b/speaktype/Utilities/AppLogger.swift
@@ -0,0 +1,67 @@
+import Foundation
+import OSLog
+
+/// Unified logging for SpeakType
+/// Usage: AppLogger.service.info("Model downloaded")
+enum AppLogger {
+ /// General app lifecycle events
+ static let app = Logger(subsystem: subsystem, category: "App")
+
+ /// Audio recording events
+ static let audio = Logger(subsystem: subsystem, category: "Audio")
+
+ /// Transcription and WhisperKit events
+ static let transcription = Logger(subsystem: subsystem, category: "Transcription")
+
+ /// Model download and management
+ static let models = Logger(subsystem: subsystem, category: "Models")
+
+ /// Clipboard and pasteboard operations
+ static let clipboard = Logger(subsystem: subsystem, category: "Clipboard")
+
+ /// Hotkey and keyboard shortcuts
+ static let hotkeys = Logger(subsystem: subsystem, category: "Hotkeys")
+
+ /// UI and window management
+ static let ui = Logger(subsystem: subsystem, category: "UI")
+
+ /// General service operations
+ static let service = Logger(subsystem: subsystem, category: "Service")
+
+ /// History and persistence
+ static let history = Logger(subsystem: subsystem, category: "History")
+
+ /// Permissions and system access
+ static let permissions = Logger(subsystem: subsystem, category: "Permissions")
+
+ private static let subsystem = "com.2048labs.speaktype"
+}
+
+// MARK: - Convenience Methods
+extension AppLogger {
+ /// Log with emoji prefix for better visual scanning
+ static func info(_ message: String, category: Logger = AppLogger.service) {
+ category.info("βΉοΈ \(message)")
+ }
+
+ static func debug(_ message: String, category: Logger = AppLogger.service) {
+ category.debug("π \(message)")
+ }
+
+ static func error(_ message: String, error: Error? = nil, category: Logger = AppLogger.service) {
+ if let error = error {
+ category.error("β \(message): \(error.localizedDescription)")
+ } else {
+ category.error("β \(message)")
+ }
+ }
+
+ static func warning(_ message: String, category: Logger = AppLogger.service) {
+ category.warning("β οΈ \(message)")
+ }
+
+ static func success(_ message: String, category: Logger = AppLogger.service) {
+ category.info("β
\(message)")
+ }
+}
+
diff --git a/speaktype/Views/AIModelsView.swift b/speaktype/Views/AIModelsView.swift
deleted file mode 100644
index 2a8441b..0000000
--- a/speaktype/Views/AIModelsView.swift
+++ /dev/null
@@ -1,236 +0,0 @@
-import SwiftUI
-
-struct AIModelsView: View {
- @StateObject private var downloadService = ModelDownloadService.shared
- @AppStorage("selectedModelVariant") private var selectedModel: String = "openai_whisper-base"
-
- // Using standard WhisperKit model variants
- @State private var models = AIModel.availableModels
-
- // Helper to get name of selected model
- var selectedModelName: String {
- models.first(where: { $0.variant == selectedModel })?.name ?? "Unknown Model"
- }
-
- var body: some View {
- ScrollView {
- VStack(alignment: .leading, spacing: 30) {
- // Header (Redesigned)
- VStack(alignment: .leading, spacing: 10) {
- Text("AI Models")
- .font(.largeTitle)
- .fontWeight(.bold)
- .foregroundStyle(.white)
- Text("Manage your local transcription models powered by WhisperKit.")
- .font(.body)
- .foregroundStyle(.gray)
- }
-
- VStack(alignment: .leading, spacing: 20) {
- VStack(alignment: .leading, spacing: 8) {
- Text("Default Model")
- .font(.headline)
- .foregroundStyle(.gray)
- Text(selectedModelName)
- .font(.title2)
- .fontWeight(.bold)
- .foregroundStyle(.white)
- }
- .padding()
- .frame(maxWidth: .infinity, alignment: .leading)
- .background(Color.contentBackground.opacity(0.5))
- .cornerRadius(12)
- .overlay(
- RoundedRectangle(cornerRadius: 12)
- .stroke(Color.gray.opacity(0.3), lineWidth: 1)
- )
- }
-
- HStack {
- Text("Available Models")
- .font(.title2)
- .fontWeight(.bold)
- .foregroundStyle(.white)
- Spacer()
- }
-
- // Models List
- VStack(spacing: 16) {
- ForEach($models) { $model in
- ModelRow(model: $model, selectedModel: $selectedModel)
- }
- }
- }
- .padding(30)
- }
- .background(Color.contentBackground)
- }
-}
-
-
-
-struct ModelRow: View {
- @Binding var model: AIModel
- @Binding var selectedModel: String // Bind to parent selection
- @ObservedObject var downloadService = ModelDownloadService.shared
-
- var progress: Double {
- downloadService.downloadProgress[model.variant] ?? 0.0
- }
-
- var isDownloading: Bool {
- downloadService.isDownloading[model.variant] ?? false
- }
-
- var isDownloaded: Bool {
- // Placeholder check. In real app, check file existence or WhisperKit.isModelAvailable(variant)
- return progress >= 1.0
- }
-
- var isActive: Bool {
- selectedModel == model.variant
- }
-
- var downloadError: String? {
- downloadService.downloadError[model.variant]
- }
-
- var body: some View {
- HStack {
- VStack(alignment: .leading, spacing: 4) {
- HStack {
- Text(model.name)
- .font(.headline)
- .foregroundStyle(.white)
-
- // Rating Badge
- Text(model.rating)
- .font(.caption2)
- .fontWeight(.bold)
- .padding(.horizontal, 6)
- .padding(.vertical, 2)
- .background(ratingColor(for: model.rating).opacity(0.2))
- .foregroundStyle(ratingColor(for: model.rating))
- .clipShape(Capsule())
-
- if isActive {
- Text("(Active)")
- .font(.caption)
- .foregroundStyle(Color.green)
- }
- }
- HStack(spacing: 6) {
- Text(model.details)
- .font(.caption)
- .foregroundStyle(.gray)
-
- Text("β’")
- .font(.caption)
- .foregroundStyle(.gray)
-
- Text(model.size)
- .font(.caption)
- .foregroundStyle(.gray.opacity(0.8))
- }
- }
-
- Spacer()
-
- if isDownloaded {
- // If downloaded, allow selection
- if isActive {
- HStack(spacing: 4) {
- Image(systemName: "checkmark.circle.fill")
- Text("Selected")
- }
- .padding(.horizontal, 12)
- .padding(.vertical, 6)
- .foregroundStyle(Color.green)
- } else {
- Button("Use") {
- selectedModel = model.variant
- }
- .padding(.horizontal, 12)
- .padding(.vertical, 6)
- .background(Color.white.opacity(0.1))
- .foregroundStyle(.white)
- .cornerRadius(16)
- .buttonStyle(.plain)
- }
- } else if isDownloading {
- HStack {
- ProgressView(value: progress)
- .progressViewStyle(.linear)
- .frame(width: 80)
- .tint(Color.appRed)
- Text("\(Int(progress * 100))%")
- .font(.caption)
- .foregroundStyle(.gray)
- .frame(width: 35, alignment: .trailing)
- }
- .padding(.horizontal, 12)
- .padding(.vertical, 6)
- .background(Color.black.opacity(0.2))
- .cornerRadius(16)
- } else {
- VStack(alignment: .trailing) {
- Button(action: {
- downloadService.downloadModel(variant: model.variant)
- }) {
- HStack(spacing: 4) {
- Text("Download")
- Image(systemName: "arrow.down.circle")
- }
- .padding(.horizontal, 12)
- .padding(.vertical, 6)
- .background(Color.appRed)
- .foregroundStyle(.white)
- .cornerRadius(16)
- }
- .buttonStyle(.plain)
-
- // Debug: Manual Cache Clear
- Button(action: {
- Task {
- let result = await downloadService.deleteModel(variant: model.variant)
- await MainActor.run {
- downloadService.downloadError[model.variant] = "Manual Delete: \(result)"
- }
- }
- }) {
- Image(systemName: "trash")
- .foregroundStyle(.gray)
- .padding(6)
- }
- .buttonStyle(.plain)
- .help("Force Delete Cache")
-
- if let error = downloadError {
- Text(error)
- .font(.caption2)
- .foregroundStyle(.red)
- .multilineTextAlignment(.trailing)
- .frame(maxWidth: 150)
- }
- }
- }
- }
- .padding()
- .background(Color.white.opacity(0.05))
- .cornerRadius(12)
- .overlay(
- RoundedRectangle(cornerRadius: 12)
- .stroke(isActive ? Color.green.opacity(0.5) : Color.white.opacity(0.05), lineWidth: 1)
- )
- }
-
- func ratingColor(for rating: String) -> Color {
- switch rating {
- case "Best", "Excellent": return .purple
- case "Great", "Good": return .green
- case "Standard": return .blue
- case "Basic": return .gray
- default: return .white
- }
- }
-}
diff --git a/speaktype/Views/Components/ModelRow.swift b/speaktype/Views/Components/ModelRow.swift
new file mode 100644
index 0000000..b4131b1
--- /dev/null
+++ b/speaktype/Views/Components/ModelRow.swift
@@ -0,0 +1,205 @@
+import SwiftUI
+
+/// Reusable component for displaying a single AI model in the models list
+struct ModelRow: View {
+ @Binding var model: AIModel
+ @Binding var selectedModel: String
+ @ObservedObject var downloadService = ModelDownloadService.shared
+
+ // MARK: - Computed Properties
+
+ var progress: Double {
+ downloadService.downloadProgress[model.variant] ?? 0.0
+ }
+
+ var isDownloading: Bool {
+ downloadService.isDownloading[model.variant] ?? false
+ }
+
+ var isDownloaded: Bool {
+ progress >= 1.0
+ }
+
+ var isActive: Bool {
+ selectedModel == model.variant
+ }
+
+ var downloadError: String? {
+ downloadService.downloadError[model.variant]
+ }
+
+ var ratingColor: Color {
+ switch model.rating {
+ case "Best", "Excellent": return .purple
+ case "Great", "Good": return .green
+ case "Standard": return .blue
+ case "Basic": return .gray
+ default: return .white
+ }
+ }
+
+ // MARK: - Body
+
+ var body: some View {
+ HStack {
+ // Model Info
+ VStack(alignment: .leading, spacing: 4) {
+ HStack {
+ Text(model.name)
+ .font(.headline)
+ .foregroundStyle(.white)
+
+ // Rating Badge
+ Text(model.rating)
+ .font(.caption2)
+ .fontWeight(.bold)
+ .padding(.horizontal, 6)
+ .padding(.vertical, 2)
+ .background(ratingColor.opacity(0.2))
+ .foregroundStyle(ratingColor)
+ .clipShape(Capsule())
+
+ if isActive {
+ Text("(Active)")
+ .font(.caption)
+ .foregroundStyle(Color.green)
+ }
+ }
+
+ HStack(spacing: 6) {
+ Text(model.details)
+ .font(.caption)
+ .foregroundStyle(.gray)
+
+ Text("β’")
+ .font(.caption)
+ .foregroundStyle(.gray)
+
+ Text(model.size)
+ .font(.caption)
+ .foregroundStyle(.gray.opacity(0.8))
+ }
+ }
+
+ Spacer()
+
+ // Actions
+ actionButton
+ }
+ .padding()
+ .background(Color.white.opacity(0.05))
+ .cornerRadius(12)
+ .overlay(
+ RoundedRectangle(cornerRadius: 12)
+ .stroke(isActive ? Color.green.opacity(0.5) : Color.white.opacity(0.05), lineWidth: 1)
+ )
+ }
+
+ // MARK: - Subviews
+
+ @ViewBuilder
+ private var actionButton: some View {
+ if isDownloaded {
+ downloadedActions
+ } else if isDownloading {
+ downloadingProgress
+ } else {
+ downloadButton
+ }
+ }
+
+ private var downloadedActions: some View {
+ HStack(spacing: 12) {
+ if isActive {
+ HStack(spacing: 4) {
+ Image(systemName: "checkmark.circle.fill")
+ Text("Selected")
+ }
+ .padding(.horizontal, 12)
+ .padding(.vertical, 6)
+ .foregroundStyle(Color.green)
+ } else {
+ Button("Use") {
+ selectedModel = model.variant
+ }
+ .padding(.horizontal, 12)
+ .padding(.vertical, 6)
+ .background(Color.white.opacity(0.1))
+ .foregroundStyle(.white)
+ .cornerRadius(16)
+ .buttonStyle(.plain)
+ }
+ }
+ }
+
+ private var downloadingProgress: some View {
+ HStack {
+ ProgressView(value: progress)
+ .progressViewStyle(.linear)
+ .frame(width: 80)
+ .tint(Color.appRed)
+
+ Text("\(Int(progress * 100))%")
+ .font(.caption)
+ .foregroundStyle(.gray)
+ .frame(width: 35, alignment: .trailing)
+ }
+ .padding(.horizontal, 12)
+ .padding(.vertical, 6)
+ .background(Color.black.opacity(0.2))
+ .cornerRadius(16)
+ }
+
+ private var downloadButton: some View {
+ VStack(alignment: .trailing, spacing: 8) {
+ Button(action: {
+ downloadService.downloadModel(variant: model.variant)
+ }) {
+ HStack(spacing: 4) {
+ Text("Download")
+ Image(systemName: "arrow.down.circle")
+ }
+ .padding(.horizontal, 12)
+ .padding(.vertical, 6)
+ .background(Color.appRed)
+ .foregroundStyle(.white)
+ .cornerRadius(16)
+ }
+ .buttonStyle(.plain)
+
+ Button(action: {
+ Task {
+ let result = await downloadService.deleteModel(variant: model.variant)
+ await MainActor.run {
+ downloadService.downloadError[model.variant] = "Manual Delete: \(result)"
+ }
+ }
+ }) {
+ Image(systemName: "trash")
+ .foregroundStyle(.gray)
+ .padding(6)
+ }
+ .buttonStyle(.plain)
+ .help("Force Delete Cache")
+
+ if let error = downloadError {
+ Text(error)
+ .font(.caption2)
+ .foregroundStyle(.red)
+ .multilineTextAlignment(.trailing)
+ .frame(maxWidth: 150)
+ }
+ }
+ }
+}
+
+// MARK: - Preview
+
+#Preview {
+ ModelRow(
+ model: .constant(AIModel.availableModels[0]),
+ selectedModel: .constant("openai_whisper-base")
+ )
+ .padding()
+ .background(Color.black)
+}
diff --git a/speaktype/Views/ContentView.swift b/speaktype/Views/ContentView.swift
deleted file mode 100644
index d7414b2..0000000
--- a/speaktype/Views/ContentView.swift
+++ /dev/null
@@ -1,97 +0,0 @@
-import SwiftUI
-
-struct ContentView: View {
- @StateObject private var audioRecorder = AudioRecordingService()
- @State private var whisperService = WhisperService()
- @State private var transcribedText: String = ""
- @State private var isTranscribing = false
-
- var body: some View {
- VStack(spacing: 20) {
- Text("SpeakType Integration Test")
- .font(.headline)
-
- // Audio Recording Section
- if audioRecorder.isRecording {
- Button("Stop & Transcribe") {
- if let url = audioRecorder.stopRecording() {
- print("Recorded to: \(url)")
- Task {
- isTranscribing = true
- do {
- transcribedText = try await whisperService.transcribe(audioFile: url)
- } catch {
- transcribedText = "Error: \(error.localizedDescription)"
- }
- isTranscribing = false
- }
- }
- }
- .buttonStyle(.borderedProminent)
- .tint(.red)
- } else {
- Button("Start Recording") {
- transcribedText = "" // Clear previous
- audioRecorder.requestPermission()
- audioRecorder.startRecording()
- }
- .buttonStyle(.bordered)
- }
-
- // Whisper Initialization
- if !whisperService.isInitialized {
- Button("Initialize Whisper") {
- Task {
- try? await whisperService.initialize()
- }
- }
- .buttonStyle(.borderedProminent)
- }
-
- Divider()
-
- // Transcription Output
- VStack(alignment: .leading) {
- Text("Transcription:")
- .font(.caption)
- .foregroundStyle(.secondary)
-
- if isTranscribing {
- ProgressView("Transcribing...")
- } else {
- Text(transcribedText.isEmpty ? "No text yet" : transcribedText)
- .padding()
- .frame(maxWidth: .infinity, alignment: .leading)
- .background(Color.gray.opacity(0.1))
- .cornerRadius(8)
- }
- }
-
- // Clipboard Actions
- HStack {
- Button("Copy Text") {
- ClipboardService.shared.copy(text: transcribedText)
- }
- .disabled(transcribedText.isEmpty)
-
- Button("Paste (Simulate Cmd+V)") {
- ClipboardService.shared.paste()
- }
- .disabled(transcribedText.isEmpty)
- }
-
- Text("Status: \(whisperService.isInitialized ? "Whisper Ready" : "Whisper Idle")")
- .font(.caption)
- .foregroundStyle(whisperService.isInitialized ? .green : .orange)
- }
- .padding()
- .frame(width: 400, height: 500)
- .onAppear {
- // HotkeyService removed in favor of KeyboardShortcuts defined in App
- }
- }
-}
-
-#Preview {
- ContentView()
-}
diff --git a/speaktype/Views/HistoryView.swift b/speaktype/Views/HistoryView.swift
deleted file mode 100644
index a458b42..0000000
--- a/speaktype/Views/HistoryView.swift
+++ /dev/null
@@ -1,101 +0,0 @@
-import SwiftUI
-
-struct HistoryView: View {
- @StateObject private var historyService = HistoryService.shared
- @State private var selectedItem: HistoryItem?
- @State private var showCopyAlert = false
-
- var body: some View {
- NavigationSplitView {
- List(historyService.items, selection: $selectedItem) { item in
- NavigationLink(value: item) {
- VStack(alignment: .leading, spacing: 4) {
- Text(item.transcript.prefix(50) + (item.transcript.count > 50 ? "..." : ""))
- .font(.headline)
- .foregroundStyle(.white)
- .frame(maxWidth: .infinity, alignment: .leading)
-
- HStack {
- Text(item.date.formatted(date: .abbreviated, time: .shortened))
- Spacer()
- Text(formatDuration(item.duration))
- }
- .font(.caption)
- .foregroundStyle(.gray)
- }
- .padding(.vertical, 4)
- .frame(maxWidth: .infinity, alignment: .leading)
- .contextMenu {
- Button("Delete", role: .destructive) {
- historyService.deleteItem(id: item.id)
- if selectedItem?.id == item.id {
- selectedItem = nil
- }
- }
- }
- }
- }
- .navigationTitle("History")
- .listStyle(.sidebar)
- } detail: {
- if let item = selectedItem {
- VStack(spacing: 0) {
- // Header Bar
- HStack {
- Text(item.date.formatted(date: .long, time: .standard))
- .font(.subheadline)
- .foregroundStyle(.gray)
-
- Spacer()
-
- Button(action: {
- NSPasteboard.general.clearContents()
- NSPasteboard.general.setString(item.transcript, forType: .string)
- showCopyAlert = true
- }) {
- Label("Copy", systemImage: "doc.on.doc")
- }
- .buttonStyle(.bordered)
- .controlSize(.small)
- .alert("Text Copied", isPresented: $showCopyAlert) {
- Button("OK", role: .cancel) { }
- } message: {
- Text("Transcript has been copied to clipboard.")
- }
- }
- .padding(.horizontal, 16)
- .padding(.vertical, 12)
- .background(Color(nsColor: .controlBackgroundColor))
-
- Divider()
-
- // Edge-to-Edge Transcript
- ScrollView {
- Text(item.transcript)
- .font(.body)
- .foregroundStyle(.white)
- .textSelection(.enabled)
- .lineSpacing(4)
- .frame(maxWidth: .infinity, alignment: .leading)
- .padding(.horizontal, 16)
- .padding(.vertical, 20)
- .multilineTextAlignment(.leading)
- }
- .frame(maxWidth: .infinity, alignment: .leading)
- .background(Color.contentBackground)
- }
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
- } else {
- Text("Select a recording to view transcript")
- .foregroundStyle(.gray)
- }
- }
- }
-
- func formatDuration(_ duration: TimeInterval) -> String {
- let formatter = DateComponentsFormatter()
- formatter.allowedUnits = [.minute, .second]
- formatter.unitsStyle = .abbreviated
- return formatter.string(from: duration) ?? "0s"
- }
-}
diff --git a/speaktype/Views/MiniRecorderView.swift b/speaktype/Views/Overlays/MiniRecorderView.swift
similarity index 95%
rename from speaktype/Views/MiniRecorderView.swift
rename to speaktype/Views/Overlays/MiniRecorderView.swift
index fe458c6..0a8bb44 100644
--- a/speaktype/Views/MiniRecorderView.swift
+++ b/speaktype/Views/Overlays/MiniRecorderView.swift
@@ -170,7 +170,7 @@ struct MiniRecorderView: View {
}
// Save to History
- let duration = getAudioDuration(url: url)
+ let duration = await getAudioDuration(url: url)
HistoryService.shared.addItem(transcript: text, duration: duration)
// Delegate Commit to Controller (Copy, Hide, Paste happens there)
@@ -189,8 +189,14 @@ struct MiniRecorderView: View {
}
}
- private func getAudioDuration(url: URL) -> TimeInterval {
+ private func getAudioDuration(url: URL) async -> TimeInterval {
let asset = AVURLAsset(url: url)
- return CMTimeGetSeconds(asset.duration)
+ do {
+ let duration = try await asset.load(.duration)
+ return duration.seconds
+ } catch {
+ print("Error loading audio duration: \(error)")
+ return 0
+ }
}
}
diff --git a/speaktype/Views/DashboardView.swift b/speaktype/Views/Screens/Dashboard/DashboardView.swift
similarity index 100%
rename from speaktype/Views/DashboardView.swift
rename to speaktype/Views/Screens/Dashboard/DashboardView.swift
diff --git a/speaktype/Views/Screens/History/HistoryView.swift b/speaktype/Views/Screens/History/HistoryView.swift
new file mode 100644
index 0000000..d2d102d
--- /dev/null
+++ b/speaktype/Views/Screens/History/HistoryView.swift
@@ -0,0 +1,100 @@
+import SwiftUI
+
+struct HistoryView: View {
+ @StateObject private var historyService = HistoryService.shared
+ @State private var showCopyAlert = false
+
+ // We don't need selectedItem anymore for a simple list
+ // We can track which item gets copied to show the alert using a simpler mechanism or just the bool
+
+ var body: some View {
+ VStack(spacing: 0) {
+ Text("History")
+ .font(.largeTitle)
+ .fontWeight(.bold)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding()
+
+ if historyService.items.isEmpty {
+ ContentUnavailableView(
+ "No History Yet",
+ systemImage: "clock",
+ description: Text("Your transcriptions will appear here.")
+ )
+ } else {
+ List {
+ ForEach(historyService.items) { item in
+ DisclosureGroup {
+ // Expanded Content
+ VStack(alignment: .leading, spacing: 12) {
+ Divider()
+
+ Text(item.transcript)
+ .font(.body)
+ .textSelection(.enabled)
+ .fixedSize(horizontal: false, vertical: true) // Allow multiline growth
+
+ Button(action: {
+ copyToClipboard(text: item.transcript)
+ }) {
+ Label("Copy Transcript", systemImage: "doc.on.doc")
+ }
+ .buttonStyle(.bordered)
+ .controlSize(.small)
+ .padding(.top, 4)
+ }
+ .padding(.vertical, 8)
+ } label: {
+ // Collapsed Header
+ HStack {
+ VStack(alignment: .leading) {
+ Text(item.date.formatted(date: .abbreviated, time: .shortened))
+ .font(.headline)
+ Text(item.transcript.prefix(60) + (item.transcript.count > 60 ? "..." : ""))
+ .font(.caption)
+ .foregroundStyle(.gray)
+ .lineLimit(1)
+ }
+
+ Spacer()
+
+ Text(formatDuration(item.duration))
+ .font(.caption)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 4)
+ .background(Color.gray.opacity(0.2))
+ .cornerRadius(4)
+ }
+ .padding(.vertical, 4)
+ }
+ }
+ .onDelete(perform: deleteItems)
+ }
+ .listStyle(.inset)
+ }
+ }
+ .alert("Copied", isPresented: $showCopyAlert) {
+ Button("OK", role: .cancel) { }
+ } message: {
+ Text("Transcript copied to clipboard.")
+ }
+ }
+
+ private func copyToClipboard(text: String) {
+ let pasteboard = NSPasteboard.general
+ pasteboard.clearContents()
+ pasteboard.setString(text, forType: .string)
+ showCopyAlert = true
+ }
+
+ private func deleteItems(at offsets: IndexSet) {
+ historyService.deleteItem(at: offsets)
+ }
+
+ func formatDuration(_ duration: TimeInterval) -> String {
+ let formatter = DateComponentsFormatter()
+ formatter.allowedUnits = [.minute, .second]
+ formatter.unitsStyle = .abbreviated
+ return formatter.string(from: duration) ?? "0s"
+ }
+}
diff --git a/speaktype/Views/OnboardingView.swift b/speaktype/Views/Screens/Onboarding/OnboardingView.swift
similarity index 100%
rename from speaktype/Views/OnboardingView.swift
rename to speaktype/Views/Screens/Onboarding/OnboardingView.swift
diff --git a/speaktype/Views/PermissionsView.swift b/speaktype/Views/Screens/Onboarding/PermissionsView.swift
similarity index 100%
rename from speaktype/Views/PermissionsView.swift
rename to speaktype/Views/Screens/Onboarding/PermissionsView.swift
diff --git a/speaktype/Views/Screens/Settings/AIModelsView.swift b/speaktype/Views/Screens/Settings/AIModelsView.swift
new file mode 100644
index 0000000..5777ef6
--- /dev/null
+++ b/speaktype/Views/Screens/Settings/AIModelsView.swift
@@ -0,0 +1,91 @@
+import SwiftUI
+
+/// Screen for managing AI transcription models
+struct AIModelsView: View {
+ // MARK: - Properties
+
+ @StateObject private var downloadService = ModelDownloadService.shared
+ @AppStorage("selectedModelVariant") private var selectedModel: String = "openai_whisper-base"
+ @State private var models = AIModel.availableModels
+
+ // MARK: - Computed Properties
+
+ var selectedModelName: String {
+ models.first(where: { $0.variant == selectedModel })?.name ?? "Unknown Model"
+ }
+
+ // MARK: - Body
+
+ var body: some View {
+ ScrollView {
+ VStack(alignment: .leading, spacing: 30) {
+ headerSection
+ currentModelCard
+ modelsListSection
+ }
+ .padding(30)
+ }
+ .background(Color.contentBackground)
+ }
+
+ // MARK: - Subviews
+
+ private var headerSection: some View {
+ VStack(alignment: .leading, spacing: 10) {
+ Text("AI Models")
+ .font(.largeTitle)
+ .fontWeight(.bold)
+ .foregroundStyle(.white)
+
+ Text("Manage your local transcription models powered by WhisperKit.")
+ .font(.body)
+ .foregroundStyle(.gray)
+ }
+ }
+
+ private var currentModelCard: some View {
+ VStack(alignment: .leading, spacing: 8) {
+ Text("Default Model")
+ .font(.headline)
+ .foregroundStyle(.gray)
+
+ Text(selectedModelName)
+ .font(.title2)
+ .fontWeight(.bold)
+ .foregroundStyle(.white)
+ }
+ .padding()
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background(Color.contentBackground.opacity(0.5))
+ .cornerRadius(12)
+ .overlay(
+ RoundedRectangle(cornerRadius: 12)
+ .stroke(Color.gray.opacity(0.3), lineWidth: 1)
+ )
+ }
+
+ private var modelsListSection: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ HStack {
+ Text("Available Models")
+ .font(.title2)
+ .fontWeight(.bold)
+ .foregroundStyle(.white)
+ Spacer()
+ }
+
+ VStack(spacing: 16) {
+ ForEach($models) { $model in
+ ModelRow(model: $model, selectedModel: $selectedModel)
+ }
+ }
+ }
+ }
+}
+
+// MARK: - Preview
+
+#Preview {
+ AIModelsView()
+ .background(Color.black)
+}
diff --git a/speaktype/Views/AudioInputView.swift b/speaktype/Views/Screens/Settings/AudioInputView.swift
similarity index 100%
rename from speaktype/Views/AudioInputView.swift
rename to speaktype/Views/Screens/Settings/AudioInputView.swift
diff --git a/speaktype/Views/SettingsView.swift b/speaktype/Views/Screens/Settings/SettingsView.swift
similarity index 100%
rename from speaktype/Views/SettingsView.swift
rename to speaktype/Views/Screens/Settings/SettingsView.swift
diff --git a/speaktype/Views/TranscribeAudioView.swift b/speaktype/Views/TranscribeAudioView.swift
index de2ad59..c4a77e2 100644
--- a/speaktype/Views/TranscribeAudioView.swift
+++ b/speaktype/Views/TranscribeAudioView.swift
@@ -1,12 +1,14 @@
import SwiftUI
import AVFoundation
import CoreMedia
+import UniformTypeIdentifiers
struct TranscribeAudioView: View {
@StateObject private var audioRecorder = AudioRecordingService()
@State private var whisperService = WhisperService()
@State private var transcribedText: String = ""
@State private var isTranscribing = false
+ @State private var showFileImporter = false
var body: some View {
VStack(spacing: 30) {
@@ -19,6 +21,15 @@ struct TranscribeAudioView: View {
.stroke(style: StrokeStyle(lineWidth: 2, dash: [10]))
.foregroundStyle(Color.gray.opacity(0.3))
.frame(maxWidth: .infinity, maxHeight: 400)
+ // Make the entire area tappable for file picker
+ .contentShape(Rectangle())
+ .onTapGesture {
+ showFileImporter = true
+ }
+ .onDrop(of: [.audio, .movie, .fileURL], isTargeted: nil) { providers in
+ validateAndTranscribe(providers: providers)
+ return true
+ }
VStack(spacing: 20) {
Image(systemName: "arrow.down.doc")
@@ -99,6 +110,20 @@ struct TranscribeAudioView: View {
Spacer()
}
.background(Color.contentBackground)
+ .fileImporter(
+ isPresented: $showFileImporter,
+ allowedContentTypes: [.audio, .movie],
+ allowsMultipleSelection: false
+ ) { result in
+ switch result {
+ case .success(let urls):
+ if let url = urls.first {
+ handleFileSelection(url: url)
+ }
+ case .failure(let error):
+ print("File selection error: \(error.localizedDescription)")
+ }
+ }
.onAppear {
Task {
if !whisperService.isInitialized {
@@ -108,6 +133,63 @@ struct TranscribeAudioView: View {
}
}
+ private func handleFileSelection(url: URL) {
+ // Access security scoped resource if needed (for file picker)
+ let didStartAccessing = url.startAccessingSecurityScopedResource()
+
+ // Create a copy or use the URL directly.
+ // For simplicity in this context, we'll try to use it directly but ensure we stop accessing later if needed.
+ // However, since startTranscription is async, we might lose access.
+ // Better pattern: Copy to temp directory if possible, or keep access open during transcription.
+ // Given WhisperKit might need file access, let's copy to a temp location to be safe and avoid scope issues.
+
+ do {
+ let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(url.lastPathComponent)
+ try? FileManager.default.removeItem(at: tempURL) // Clean up if exists
+ try FileManager.default.copyItem(at: url, to: tempURL)
+
+ if didStartAccessing {
+ url.stopAccessingSecurityScopedResource()
+ }
+
+ startTranscription(url: tempURL)
+ } catch {
+ print("Error copying file: \(error)")
+ if didStartAccessing {
+ url.stopAccessingSecurityScopedResource()
+ }
+ // Fallback: try original URL if copy fails
+ startTranscription(url: url)
+ }
+ }
+
+ private func validateAndTranscribe(providers: [NSItemProvider]) {
+ for provider in providers {
+ if provider.hasItemConformingToTypeIdentifier(UTType.audio.identifier) ||
+ provider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
+
+ provider.loadFileRepresentation(forTypeIdentifier: UTType.content.identifier) { url, error in
+ if let url = url {
+ // LoadFileRepresentation gives us a temporary URL that might not persist.
+ // We should copy it immediately.
+ let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(url.lastPathComponent)
+ do {
+ try? FileManager.default.removeItem(at: tempURL)
+ try FileManager.default.copyItem(at: url, to: tempURL)
+
+ DispatchQueue.main.async {
+ startTranscription(url: tempURL)
+ }
+ } catch {
+ print("Error copying dropped file: \(error)")
+ }
+ }
+ }
+ return // Only handle the first valid file
+ }
+ }
+ }
+
private func startTranscription(url: URL) {
Task {
isTranscribing = true
diff --git a/speaktypeTests/ClipboardServiceTests.swift b/speaktypeTests/ClipboardServiceTests.swift
new file mode 100644
index 0000000..f0bdd4d
--- /dev/null
+++ b/speaktypeTests/ClipboardServiceTests.swift
@@ -0,0 +1,18 @@
+import XCTest
+@testable import speaktype
+
+final class ClipboardServiceTests: XCTestCase {
+
+ func testCopy() {
+ let text = "Copied Text Check"
+ ClipboardService.shared.copy(text: text)
+
+ let pasteboard = NSPasteboard.general
+ let copied = pasteboard.string(forType: .string)
+
+ XCTAssertEqual(copied, text, "Clipboard content should match copied text")
+ }
+
+ // Testing paste() is difficult in unit tests as it requires active application focus and AX permissions.
+ // We primarily verify the write operation here.
+}
diff --git a/speaktypeTests/HistoryServiceTests.swift b/speaktypeTests/HistoryServiceTests.swift
new file mode 100644
index 0000000..4df3b5a
--- /dev/null
+++ b/speaktypeTests/HistoryServiceTests.swift
@@ -0,0 +1,65 @@
+import XCTest
+@testable import speaktype
+
+final class HistoryServiceTests: XCTestCase {
+
+ var service: HistoryService!
+
+ override func setUp() {
+ super.setUp()
+ service = HistoryService.shared
+ // Clear existing history for test isolation
+ service.items = []
+ UserDefaults.standard.removeObject(forKey: "history_items")
+ }
+
+ override func tearDown() {
+ service.items = []
+ UserDefaults.standard.removeObject(forKey: "history_items")
+ super.tearDown()
+ }
+
+ func testAddItem() {
+ XCTAssertTrue(service.items.isEmpty)
+
+ let transcript = "Test Transcript"
+ let duration: TimeInterval = 10.0
+
+ service.addItem(transcript: transcript, duration: duration)
+
+ XCTAssertEqual(service.items.count, 1)
+ XCTAssertEqual(service.items.first?.transcript, transcript)
+ XCTAssertEqual(service.items.first?.duration, duration)
+ }
+
+ func testPersistence() {
+ let transcript = "Persistent Item"
+ service.addItem(transcript: transcript, duration: 5.0)
+
+ // Simulate app restart by re-initializing (or checking UserDefaults directly)
+ // Since 'init' loads from UserDefaults, creating a new instance isn't easy with singleton,
+ // but we can check if UserDefaults has the data.
+
+ guard let data = UserDefaults.standard.data(forKey: "history_items"),
+ let decoded = try? JSONDecoder().decode([HistoryItem].self, from: data) else {
+ XCTFail("Failed to load from UserDefaults")
+ return
+ }
+
+ XCTAssertEqual(decoded.count, 1)
+ XCTAssertEqual(decoded.first?.transcript, transcript)
+ }
+
+ func testDeleteItem() {
+ service.addItem(transcript: "Item 1", duration: 1.0)
+ service.addItem(transcript: "Item 2", duration: 2.0)
+
+ XCTAssertEqual(service.items.count, 2)
+
+ let itemToDelete = service.items.last! // "Item 1" (since newest is first)
+ service.deleteItem(id: itemToDelete.id)
+
+ XCTAssertEqual(service.items.count, 1)
+ XCTAssertEqual(service.items.first?.transcript, "Item 2")
+ }
+}
diff --git a/speaktypeTests/ModelDownloadServiceTests.swift b/speaktypeTests/ModelDownloadServiceTests.swift
new file mode 100644
index 0000000..5e5b92a
--- /dev/null
+++ b/speaktypeTests/ModelDownloadServiceTests.swift
@@ -0,0 +1,19 @@
+import XCTest
+@testable import speaktype
+
+final class ModelDownloadServiceTests: XCTestCase {
+
+ func testInitialState() {
+ let service = ModelDownloadService.shared
+
+ // Ensure no lingering downloads from other runs
+ // (Note: Shared singleton might have state if tests run in parallel or sequence without clearing)
+ // We can't easily clear private vars, but we can check types.
+
+ XCTAssertNotNil(service.downloadProgress)
+ XCTAssertNotNil(service.isDownloading)
+ }
+
+ // Real download tests require mocking backend connections or WhisperKit, which is out of scope for basic unit tests without dependency injection.
+ // We verified the model IDs in the previous verification step.
+}
diff --git a/speaktypeTests/speaktypeTests.swift b/speaktypeTests/speaktypeTests.swift
deleted file mode 100644
index 1febd54..0000000
--- a/speaktypeTests/speaktypeTests.swift
+++ /dev/null
@@ -1,17 +0,0 @@
-//
-// speaktypeTests.swift
-// speaktypeTests
-//
-// Created by Karan Singh on 7/1/26.
-//
-
-import Testing
-@testable import speaktype
-
-struct speaktypeTests {
-
- @Test func example() async throws {
- // Write your test here and use APIs like `#expect(...)` to check expected conditions.
- }
-
-}
diff --git a/speaktypeUITests/speaktypeUITests.swift b/speaktypeUITests/speaktypeUITests.swift
index d6105ea..40f7820 100644
--- a/speaktypeUITests/speaktypeUITests.swift
+++ b/speaktypeUITests/speaktypeUITests.swift
@@ -1,43 +1,33 @@
-//
-// speaktypeUITests.swift
-// speaktypeUITests
-//
-// Created by Karan Singh on 7/1/26.
-//
-
import XCTest
final class speaktypeUITests: XCTestCase {
override func setUpWithError() throws {
- // Put setup code here. This method is called before the invocation of each test method in the class.
-
- // In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
-
- // In UI tests itβs important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
- }
-
- override func tearDownWithError() throws {
- // Put teardown code here. This method is called after the invocation of each test method in the class.
}
- @MainActor
- func testExample() throws {
- // UI tests must launch the application that they test.
+ func testAppLaunchAndNavigation() throws {
let app = XCUIApplication()
app.launch()
-
- // Use XCTAssert and related functions to verify your tests produce the correct results.
- let window = app.windows["SpeakType"]
- XCTAssertTrue(window.exists, "The SpeakType window should exist")
- }
-
- @MainActor
- func testLaunchPerformance() throws {
- // This measures how long it takes to launch your application.
- measure(metrics: [XCTApplicationLaunchMetric()]) {
- XCUIApplication().launch()
+
+ // Verify Sidebar exists (macOS List maps to Outline)
+ let sidebar = app.outlines.firstMatch
+ // Wait up to 10s for UI to stabilize (increased for robustness)
+ if !sidebar.waitForExistence(timeout: 10.0) {
+ print("π¨ Sidebar NOT found! App Hierarchy:\n\(app.debugDescription)")
+ XCTFail("Sidebar should exist (Outline)")
+ }
+
+ // Navigate to Settings
+ // On macOS, text is often exposed directly. We look for "Settings" static text.
+ let settingsText = app.staticTexts["Settings"]
+
+ if settingsText.waitForExistence(timeout: 5.0) {
+ settingsText.click()
+
+ // Verify we are on Settings View
+ let settingsContent = app.staticTexts["Startup Behavior"]
+ XCTAssertTrue(settingsContent.waitForExistence(timeout: 5.0), "Should find Settings content")
}
}
}
diff --git a/speaktypeUITests/speaktypeUITestsLaunchTests.swift b/speaktypeUITests/speaktypeUITestsLaunchTests.swift
deleted file mode 100644
index b817767..0000000
--- a/speaktypeUITests/speaktypeUITestsLaunchTests.swift
+++ /dev/null
@@ -1,33 +0,0 @@
-//
-// speaktypeUITestsLaunchTests.swift
-// speaktypeUITests
-//
-// Created by Karan Singh on 7/1/26.
-//
-
-import XCTest
-
-final class speaktypeUITestsLaunchTests: XCTestCase {
-
- override class var runsForEachTargetApplicationUIConfiguration: Bool {
- true
- }
-
- override func setUpWithError() throws {
- continueAfterFailure = false
- }
-
- @MainActor
- func testLaunch() throws {
- let app = XCUIApplication()
- app.launch()
-
- // Insert steps here to perform after app launch but before taking a screenshot,
- // such as logging into a test account or navigating somewhere in the app
-
- let attachment = XCTAttachment(screenshot: app.screenshot())
- attachment.name = "Launch Screen"
- attachment.lifetime = .keepAlways
- add(attachment)
- }
-}