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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
name: Bug report
about: Something isn't working
title: ''
labels: bug
assignees: ''
---

**What happened**
A clear description of the bug and what you expected instead.

**Steps to reproduce**
1.
2.
3.

**Environment**
- macOS version:
- OpenWhisp version (or commit):
- Mac chip (Apple Silicon / Intel):
- Whisper model (tiny / base / small / …):
- Engine: Whisper **CLI** / Whisper **server** / Apple Speech
- AI post-processing: off / OpenAI / local (llama.cpp / Ollama)
- Output mode: Preview / Final paste / Live chunks

**Logs**
If relevant, paste the last lines of
`~/Library/Caches/com.openwhisp.app/whisper-engine.log`
(redact any transcript text you don't want to share).

**Additional context**
Screenshots, the target app you were dictating into, etc.
8 changes: 8 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: Security or privacy issue
url: https://github.com/initcore0/openwhisp/security/advisories/new
about: Please report privately — see SECURITY.md. Do not open a public issue.
- name: Question / idea / preset to share
url: https://github.com/initcore0/openwhisp/discussions
about: Use Discussions for questions, prompt/vocab presets, and open-ended ideas.
23 changes: 23 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
name: Feature request
about: Suggest an idea or improvement
title: ''
labels: enhancement
assignees: ''
---

**The problem / use case**
What are you trying to do, and what's missing today?

**Proposed solution**
What you'd like to happen.

**Privacy note**
Does this involve sending data off-device? OpenWhisp is local-first, so any
cloud/network feature must be opt-in and clearly surfaced. (See SECURITY.md.)

**Alternatives considered**
Other approaches, or how other tools do it.

**Roadmap fit (optional)**
Does this map to anything in docs/ROADMAP.md?
19 changes: 19 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!-- Thanks for contributing to OpenWhisp! -->

## What & why
<!-- What does this change and why? Link any related issue (#123). -->

## How it was tested
- [ ] `swift test` passes
- [ ] `./build.sh` builds cleanly
- [ ] Added/updated tests for logic changes (pure logic in an
`import Foundation`-only file under `OpenWhisp/Services/`, added to
`Package.swift` sources, tested in `Tests/OpenWhispCoreTests/`)
- [ ] Manually verified in the running app (if UI/behavior changed)

## Privacy checklist
- [ ] This change does **not** send audio or text off-device, **or** any new
network egress is opt-in, surfaced in the UI, and documented (see SECURITY.md).

## Notes
<!-- Anything reviewers should know: trade-offs, follow-ups, screenshots. -->
50 changes: 50 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Contributing to OpenWhisp

Thanks for your interest! OpenWhisp is a local-first, MIT-licensed macOS
dictation app, and contributions are welcome.

## Quick start

```bash
git clone --recursive https://github.com/initcore0/openwhisp.git
cd openwhisp
./scripts/build-whisper.sh # build the vendored whisper.cpp submodule
./build.sh && ./package.sh # build + bundle the app
swift test # run the unit tests
```

See the [README](README.md#building) for full build/permissions details and
[docs/ROADMAP.md](docs/ROADMAP.md) for direction and priorities.

## Before you open a PR

- **Run `swift test` and `./build.sh`** — both must pass.
- **Add tests for logic changes.** The `OpenWhispCore` SwiftPM target compiles
only Foundation-only files, so put new pure logic (parsing, formatting,
decisions) in a `import Foundation`-only file under `OpenWhisp/Services/`, add
it to `Package.swift`'s `sources`, and test it under `Tests/OpenWhispCoreTests/`.
This keeps the bug-prone text/decision logic covered without needing the GUI.
- **Keep platform-bound code (AppKit/AVFoundation/Accessibility) thin** and
delegate to testable pure helpers where possible.
- **Match the surrounding style** and keep diffs focused.

## What makes a good contribution

- Bug fixes (with a regression test).
- Items from the roadmap (open an issue first for larger ones so we can align).
- New **voice commands / prompt presets / vocabulary** — these are easy wins and
exactly the kind of thing the community can share.
- Docs and onboarding improvements.

## Privacy is a hard requirement

OpenWhisp's promise is that **audio and text never leave the device** unless the
user explicitly opts into a cloud provider. Any change that could send data
off-device must be opt-in, clearly surfaced in the UI, and documented. See
[SECURITY.md](SECURITY.md).

## Reporting bugs

Use the issue templates. Include your macOS version, the engine (Whisper CLI vs
server, or Apple Speech) and AI provider in use, and relevant lines from the log
(`~/Library/Caches/com.openwhisp.app/whisper-engine.log`).
13 changes: 13 additions & 0 deletions OpenWhisp/Models/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,19 @@ class AppState: ObservableObject {
return "\(trigger) to insert - Esc to cancel"
}

/// Whether dictation can send any text off this machine to the internet.
/// Transcription is always on-device; the only egress is AI post-processing
/// with the OpenAI (cloud) provider. The local provider stays on machine/LAN.
/// (One-time model downloads aren't counted — they're not your dictated text.)
var sendsTextToCloud: Bool {
PrivacyStatus.sendsTextToCloud(enhancementEnabled: openAIEnhancementEnabled, provider: llmProvider)
}

/// Short, user-facing privacy statement for the current configuration.
var privacyStatusText: String {
PrivacyStatus.statusText(enhancementEnabled: openAIEnhancementEnabled, provider: llmProvider)
}

var languageDisplayName: String {
Self.languageDisplayName(for: language)
}
Expand Down
26 changes: 26 additions & 0 deletions OpenWhisp/Services/PrivacyStatus.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Foundation

/// Pure decision for the "where does my text go" privacy indicator, so it can be
/// unit-tested independently of the SwiftUI/AppState layer.
///
/// Transcription is always on-device. The only way dictated text leaves the
/// machine is AI post-processing with the cloud (OpenAI) provider; the local
/// provider stays on machine/LAN. One-time model downloads are not dictated text
/// and are not considered here.
enum PrivacyStatus {
/// True only when AI cleanup is on AND the provider is the cloud one.
static func sendsTextToCloud(enhancementEnabled: Bool, provider: String) -> Bool {
enhancementEnabled && provider == "openai"
}

/// Short, user-facing statement for the current configuration.
static func statusText(enhancementEnabled: Bool, provider: String) -> String {
if sendsTextToCloud(enhancementEnabled: enhancementEnabled, provider: provider) {
return "Sends final text to OpenAI for cleanup"
}
if enhancementEnabled && provider == "local" {
return "On-device + your local LLM — nothing goes to the cloud"
}
return "Fully on-device — no network used"
}
}
12 changes: 11 additions & 1 deletion OpenWhisp/Views/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -808,14 +808,24 @@ struct SettingsView: View {

private var statusSection: some View {
settingsSection("Status") {
// Privacy indicator — makes "local-first" visible at a glance.
HStack(spacing: 6) {
Image(systemName: appState.sendsTextToCloud ? "wifi" : "lock.shield.fill")
.foregroundColor(appState.sendsTextToCloud ? .orange : .green)
Text(appState.privacyStatusText)
.font(.callout)
.foregroundColor(appState.sendsTextToCloud ? .orange : .green)
Spacer()
}

HStack {
Text("Status:")
Spacer()
Text(appState.statusMessage)
.foregroundColor(statusColor)
.frame(width: 180, alignment: .trailing)
}

if let error = appState.error, !error.isEmpty {
Text(error)
.foregroundColor(.red)
Expand Down
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ let package = Package(
"AppProfile.swift",
"TranscriptionHistory.swift",
"SecureFieldPolicy.swift",
"DownloadProgressFormatter.swift"
"DownloadProgressFormatter.swift",
"PrivacyStatus.swift"
]
),
.testTarget(
Expand Down
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# OpenWhisp — Local‑First Dictation for macOS

> **Wispr Flow‑style dictation, but fully on‑device, free, and open source.**
> No cloud, no subscription, no telemetry — your voice never leaves your Mac.

OpenWhisp is a menu‑bar dictation app for macOS. Hold a key, speak, release — your words are transcribed **on‑device** and typed into whatever app is focused. No cloud account, no subscription, no audio leaving your machine (unless you explicitly opt into a cloud LLM for cleanup).

Transcription runs locally with [whisper.cpp](https://github.com/ggerganov/whisper.cpp); optional text cleanup can run **fully locally** against your own LLM server, or via OpenAI if you choose.
Expand Down Expand Up @@ -208,6 +211,17 @@ Recognized leads include "make this/it…", "rewrite/rephrase this…", "transla
- History and settings are stored locally (`~/Library/Application Support/OpenWhisp/`, UserDefaults, Keychain).
- The **only** time text leaves your machine is if you turn on AI post‑processing with the **OpenAI** provider. The **local** provider keeps everything on your machine/LAN.
- Transcript text is **not** written to the app's log files.
- **Password / secure fields** are detected and skipped — OpenWhisp won't dictate into, insert, or store their contents.
- Settings → Status shows a live **privacy indicator** ("Fully on‑device" vs "Sends text to OpenAI") for your current configuration.

**Verify it yourself** — you don't have to take our word for it:

```bash
# Should stay silent while you dictate, unless you enabled the OpenAI cloud provider:
nettop -p "$(pgrep -x OpenWhisp)"
```

See [SECURITY.md](SECURITY.md) for the full privacy model and how to report issues.

---

Expand Down Expand Up @@ -282,7 +296,7 @@ swift test # run the unit tests for core logic

## Contributing

Issues and PRs welcome. Please run `swift test` and `./build.sh` before submitting. Keep new pure‑logic in Foundation‑only files where possible so it can be unit‑tested.
Issues and PRs welcome — see **[CONTRIBUTING.md](CONTRIBUTING.md)** for the dev setup, test conventions, and the privacy requirement. Run `swift test` and `./build.sh` before submitting.

Direction and priorities (competitive analysis, feature gaps, plugin plan) live in **[docs/ROADMAP.md](docs/ROADMAP.md)**.

Expand Down
45 changes: 45 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Security & Privacy

Privacy is OpenWhisp's whole point, so we treat it as a security property.

## The privacy model

- **Transcription is 100% on-device.** Audio is captured locally, written to a
temporary WAV in `~/Library/Caches/com.openwhisp.app/`, transcribed by
whisper.cpp on your Mac, and the WAV is **deleted after each transcription**.
- **No telemetry.** OpenWhisp sends no analytics, crash reports, or usage data.
- **The only network egress** is optional AI post-processing **with the OpenAI
(cloud) provider**. If you use the **local** provider (llama.cpp / Ollama),
text stays on your machine / LAN. If AI post-processing is off, nothing leaves
the device at all. The Settings → Status panel shows the current state.
- **Secrets** (the OpenAI API key) are stored in the macOS **Keychain**, never in
plain text.
- **Secure fields** (password fields) are detected and OpenWhisp refuses to
dictate into, insert, or store their contents.

### Verify it yourself
Because OpenWhisp is open source you don't have to take our word for it:

```bash
# Watch for any network activity by OpenWhisp while you dictate (should be silent
# unless you enabled the OpenAI cloud provider):
nettop -p "$(pgrep -x OpenWhisp)"
```

You can also read the relevant code: audio capture (`OpenWhisp/Services/AudioRecorder.swift`),
the WAV deletion and HTTP calls (`OpenWhisp/Services/WhisperEngine.swift`), and
the only outbound endpoint (`OpenWhisp/Services/OpenAITranslationService.swift`).

## Reporting a vulnerability

If you find a security or privacy issue (e.g. a path where audio/text could leak
off-device unexpectedly), please **do not open a public issue**. Instead, report
it privately via GitHub's **"Report a vulnerability"** (Security → Advisories) on
the repository, or open a minimal private channel with the maintainer.

Please include: macOS version, OpenWhisp version, the engine/provider in use, and
clear reproduction steps. We aim to acknowledge reports promptly.

## Supported versions

OpenWhisp is pre-1.0; security fixes target the latest `main` / latest release.
28 changes: 28 additions & 0 deletions Tests/OpenWhispCoreTests/PrivacyStatusTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import XCTest
@testable import OpenWhispCore

final class PrivacyStatusTests: XCTestCase {

func testFullyLocalWhenEnhancementOff() {
XCTAssertFalse(PrivacyStatus.sendsTextToCloud(enhancementEnabled: false, provider: "openai"))
XCTAssertEqual(PrivacyStatus.statusText(enhancementEnabled: false, provider: "openai"),
"Fully on-device — no network used")
}

func testLocalProviderNeverCloud() {
XCTAssertFalse(PrivacyStatus.sendsTextToCloud(enhancementEnabled: true, provider: "local"))
XCTAssertEqual(PrivacyStatus.statusText(enhancementEnabled: true, provider: "local"),
"On-device + your local LLM — nothing goes to the cloud")
}

func testOpenAIEnabledSendsToCloud() {
XCTAssertTrue(PrivacyStatus.sendsTextToCloud(enhancementEnabled: true, provider: "openai"))
XCTAssertEqual(PrivacyStatus.statusText(enhancementEnabled: true, provider: "openai"),
"Sends final text to OpenAI for cleanup")
}

func testOpenAIProviderButEnhancementOffIsLocal() {
// Provider set to openai but cleanup off → nothing is sent.
XCTAssertFalse(PrivacyStatus.sendsTextToCloud(enhancementEnabled: false, provider: "openai"))
}
}
10 changes: 5 additions & 5 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,11 +212,11 @@ dictation — fall back to the raw text), and **declare network use** per plugin
- ✅ `didWriteData` progress + Retry; first-run defaults to **tiny** (#36).
- ✅ Gatekeeper install docs fixed in README + `release.yml` (#34).

### Phase 1 — Positioning & proof
- Lock the tagline (vs Wispr / vs VoiceInk: *MIT, zero-egress, hackable*).
- Add a demo GIF.
- Add the network indicator + "verify it yourself" note.
- Add `SECURITY` / `ISSUE` / `PR` / `CONTRIBUTING` templates.
### Phase 1 — Positioning & proof *(mostly ✅)*
- ✅ Tagline locked (Wispr-style, fully on-device, free, open source).
- ⬜ Demo GIF *(deferred — needs a screen recording)*.
- ✅ Network/privacy indicator in Settings → Status + "verify it yourself" note.
- `SECURITY` / `CONTRIBUTING` / issue + PR templates added.

### Phase 2 — Wire the chain *(plugin prerequisite + tech-debt paydown)*
- Replace the hardcoded stages in `postProcess()` and the separate
Expand Down
Loading