Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/swift-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ on:
jobs:
test:
name: Build and Test Swift App
runs-on: macos-15
runs-on: macos-26
defaults:
run:
working-directory: ZeroGSwift
Expand Down
150 changes: 114 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,64 +1,142 @@
# ZeroG
<p align="center">
<img src="ZeroG_Logo.png" alt="ZeroG logo" width="140">
</p>

Privacy-focused voice typing for macOS.
# 🧑‍🚀 ZeroG

Hold **Left Control** to record, release to transcribe and paste into the active app. Hold **Control + Q** while recording to optionally polish the transcription with Gemini.
**Open Source Voice Typing for macOS**

## Requirements
> **"The voice typing tool so good, you'll forget how to type."**

- macOS 14+
- Xcode with Swift 5.9+ toolchain
- Apple Silicon recommended for WhisperKit performance
---

## Development
## 🛰️ The Manifesto

```bash
cd ZeroGSwift
swift test
swift run
```
**We don't type. We transmit.**

ZeroG was born from the realization that typing is a bottleneck. It is a terrestrial limitation. We spent decades training our fingers to hit 100 Words Per Minute (WPM), only to realize that the speed of thought is infinite.

ZeroG is not just a dictation tool. It is an evolutionary step. Just as an astronaut in orbit unlearns the physics of gravity and expects a pen to float rather than fall, ZeroG users unlearn the friction of the keyboard.

We are building the "Air Gap" for your thoughts: **Private. Local. Weightless.**

---

## 🚀 Flight Systems (Features)

## Build
- **Zero Friction**: Native Swift running NVIDIA Parakeet on the **Apple Neural Engine** for near-instant transcription. 0 WPM. 100% Output.
- **Vacuum Sealed**: In space, sound doesn't travel. In ZeroG, your voice doesn't travel either. Audio and text are processed entirely on your Mac. No data leaves the ship.
- **Universal Comms**: Hold `Left Control` to transmit thought into *any* application. Release to paste.
- **Gravity Assist** (Optional): Press `⌃⌥P` to polish your last transmission with **Apple Foundation Models**, fully on-device (macOS 26 with Apple Intelligence).
- **Debris Filter**: Strips the "um"s and "uh"s from the feed before payload delivery.
- **Auto-Cut**: A silence sensor cuts the feed if the mic is left open on dead air.
- **Flight Recorder**: A native "Glass" HUD that floats above your dock.

![ZeroG HUD floating above the macOS dock](assets/zerog-hud.png)

---

## 🛠️ Pre-Flight Check (Installation)

### Prerequisites
- macOS 14+ (Apple Silicon recommended for optimal thrust)
- Xcode with a Swift 5.9+ toolchain
- [Git](https://git-scm.com/)

### 1. Board the Ship
```bash
cd ZeroGSwift
swift build -c release
git clone https://github.com/antonynjoro/ZeroG.git
cd ZeroG/ZeroGSwift
./build_app.sh
```

The packaged app is written to:
The packaged app lands at `ZeroGSwift/build/ZeroG.app`. Move it to `/Applications` and launch.

```text
ZeroGSwift/build/ZeroG.app
```
First takeoff downloads the transcription model (roughly 460 MB, one time). After that, every flight is fully offline.

### 2. Clearance Codes (Permissions)
First launch runs a guided pre-flight sequence that requests exactly two clearances:

- **Microphone**: Audio input feed.
- **Accessibility**: Detects the trigger key and injects the payload (text) into target fields.

That is the full list. No Input Monitoring. No network clearance. Nothing else.

## Configuration
### 3. Takeoff
Look for the microphone icon in your status bar.

ZeroG reads configuration from environment variables and `UserDefaults`.
- **Transmit**: Hold `Left Control`. The HUD appears. Speak. Release to paste.
- **Polished Transmission**: Press `⌃⌥P` after a transmission to clean it up and paste the refined version.

- `DEBUG=true` enables debug logging.
- `GOOGLE_API_KEY=...` enables optional Gemini polishing.
---

In packaged app use, the Gemini key can be set from the menu bar item.
## 🕹️ Flight Controls (Configuration)

## Permissions
Everything lives in the status bar menu:

Grant these in **System Settings -> Privacy & Security**:
- **Trigger Key**: Default is `Left Control`. Swappable.
- **Polish Shortcut**: Default is `⌃⌥P`. Swappable.

- Microphone
- Accessibility
- Input Monitoring
### Flight Recorder (Logging)
The Black Box is **OFF** by default for maximum privacy. For development builds, create a `.env` with `DEBUG=true` and reboot systems.

## Testing and CI
---

The active implementation is Swift-only. CI runs on macOS and executes:
## ⚠️ Turbulence (Troubleshooting)

### Payload Failure (Not Pasting)
- Check **System Settings > Privacy & Security > Accessibility**.
- If ZeroG is listed but pasting fails, remove it (-) and re-add it. Old clearance codes expire, especially after rebuilding the app.

### Dead Air (No Audio)
- Check **System Settings > Privacy & Security > Microphone**.
- Ensure we have a lock on your comms.

---

## 🧪 R&D (Development)

### Run From Source
```bash
cd ZeroGSwift
swift run
```

### Run Diagnostics
```bash
cd ZeroGSwift
swift package resolve
swift test
swift build -c release
./build_app.sh
```

The legacy Python implementation and Python tests have been removed from the active tree.
### Blueprint
- `ZeroGSwift/ZeroG/Core`: Core physics engine (state machine, key monitor, recorder, engines, polish).
- `ZeroGSwift/ZeroG/GUI`: Visual interface (HUD, status bar, onboarding).
- `ZeroGSwift/ZeroGTests`: Simulation scenarios.

---

## 📅 Captain's Log

ZeroG's first prototype flew on Python. In 2026 the whole ship was rebuilt in native Swift.

### [The Swift Era] - 2026
- **Full Rewrite**: Python systems decommissioned. ZeroG is now a native Swift bird: lighter, faster, silent on the pad.
- **New Engine**: NVIDIA Parakeet TDT running on the Apple Neural Engine for near-instant transcription.
- **Gravity Assist 2.0**: Cloud thrusters removed. Polish now runs on-device via Apple Foundation Models. Nothing leaves the ship, ever.
- **Guided Pre-Flight**: New onboarding sequence walks both permission grants on first launch.

### [v0.10.0] - 2026-02-20
- **Parallel Chunk Transcription**: Audio chunked and transcribed in the background while you speak, cutting wait-time for long dictations.

### [v0.9.0] - 2026-01-03
- **Rebrand**: Initiated "ZeroG" protocol.
- **Brand Guide**: Published `BRANDING.md` for all contributors.

### [v0.8.1] - 2026-01-02
- **Hands-Free**: Auto-stop on silence.

### [v0.8.0] - 2026-01-02
- **Universal Injection**: Compatibility across all sectors.

---
*ZeroG: Don't let gravity hold back your thoughts.*
6 changes: 6 additions & 0 deletions ZeroGSwift/ZeroG/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ enum Config {
/// Interacts with the trailing-audio knobs — see `TranscriptionQuality`.
static let silenceDuration: TimeInterval = 12.0

/// Last-resort cap for a recording whose trigger key appears to stay held.
/// The release watchdog and silence detector normally stop abandoned sessions
/// much sooner; this only bounds audio memory if both safeguards miss one.
/// Ten minutes of 16 kHz mono Float audio is roughly 38 MB.
static let recordingSafetyTimeout: TimeInterval = 10 * 60

/// Seconds to keep recording after key release to capture trailing speech.
/// Sourced from `TranscriptionQuality.recordingTailSeconds`.
static let recordingTailDuration: TimeInterval = TranscriptionQuality.recordingTailSeconds
Expand Down
72 changes: 72 additions & 0 deletions ZeroGSwift/ZeroG/Core/AVAudioCaptureSession.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import AudioToolbox
import AVFoundation

/// A fresh AVAudioEngine configured for one recording and one concrete input device.
/// Recreating this object avoids retaining a dead audio route after calls or device changes.
final class AVAudioCaptureSession: AudioCaptureSession {
private enum CaptureError: LocalizedError {
case audioUnitUnavailable
case deviceSelectionFailed(OSStatus)

var errorDescription: String? {
switch self {
case .audioUnitUnavailable:
"The microphone audio unit is unavailable."
case .deviceSelectionFailed(let status):
"The microphone could not be selected (Core Audio error \(status))."
}
}
}

private let audioEngine: AVAudioEngine
private let inputNode: AVAudioInputNode

let recordingFormat: AVAudioFormat

init(device: AudioInputDevice) throws {
let audioEngine = AVAudioEngine()
let inputNode = audioEngine.inputNode
guard let audioUnit = inputNode.audioUnit else {
throw CaptureError.audioUnitUnavailable
}

var deviceID = device.deviceID
let status = AudioUnitSetProperty(
audioUnit,
kAudioOutputUnitProperty_CurrentDevice,
kAudioUnitScope_Global,
0,
&deviceID,
UInt32(MemoryLayout<AudioDeviceID>.size)
)
guard status == noErr else {
throw CaptureError.deviceSelectionFailed(status)
}

self.audioEngine = audioEngine
self.inputNode = inputNode
self.recordingFormat = inputNode.inputFormat(forBus: 0)
}

func installTap(
bufferSize: AVAudioFrameCount,
handler: @escaping (AVAudioPCMBuffer, AVAudioFormat) -> Void
) {
let format = recordingFormat
inputNode.installTap(onBus: 0, bufferSize: bufferSize, format: format) { buffer, _ in
handler(buffer, format)
}
}

func start() throws {
try audioEngine.start()
}

func removeTap() {
inputNode.removeTap(onBus: 0)
}

func stop() {
audioEngine.stop()
}
}
11 changes: 9 additions & 2 deletions ZeroGSwift/ZeroG/Core/AppStateMachine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,17 @@ final class AppStateMachine: ObservableObject {
Log.debug("StateMachine", "\(previous) → \(newState)")
}

/// Convenience: transition to `.idle` after a delay.
/// Convenience: transition to `.idle` after a delay — but only if the state
/// hasn't moved on in the meantime. An unconditional delayed reset stomped
/// a recording that started within the delay window (success → recording →
/// timer fires → idle), which left the mic running: the release handler's
/// "are we recording?" check failed and the session never stopped, merging
/// into the next one (field-captured 2026-07-03 15:53).
func resetToIdle(after delay: TimeInterval = Config.Timing.successReset) {
let scheduledFrom = currentState
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
self?.transition(to: .idle)
guard let self, self.currentState == scheduledFrom else { return }
self.transition(to: .idle)
}
}
}
14 changes: 14 additions & 0 deletions ZeroGSwift/ZeroG/Core/AudioCaptureSession.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import AVFoundation

/// Testable boundary around the AVAudioEngine capture operations used per recording.
protocol AudioCaptureSession: AnyObject {
var recordingFormat: AVAudioFormat { get }

func installTap(
bufferSize: AVAudioFrameCount,
handler: @escaping (AVAudioPCMBuffer, AVAudioFormat) -> Void
)
func start() throws
func removeTap()
func stop()
}
11 changes: 11 additions & 0 deletions ZeroGSwift/ZeroG/Core/AudioInputDevice.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import CoreAudio

/// A physical or virtual macOS audio input that ZeroG can record from.
struct AudioInputDevice: Equatable, Identifiable {
let deviceID: AudioDeviceID
let uid: String
let name: String
let isBuiltIn: Bool

var id: String { uid }
}
Loading
Loading