Metadata-Version: 2.5
Name: flow-local
Version: 0.2.0
Summary: Push-to-talk voice dictation, fully offline: faster-whisper STT + local Ollama cleanup
Author: Toni Kiuru
License: MIT
License-File: LICENSE
Requires-Python: >=3.10
Requires-Dist: faster-whisper>=1.2
Requires-Dist: huggingface-hub>=0.24
Requires-Dist: numpy>=1.26
Requires-Dist: nvidia-cublas-cu12; sys_platform == 'linux'
Requires-Dist: nvidia-cudnn-cu12; sys_platform == 'linux'
Requires-Dist: pillow>=10
Requires-Dist: platformdirs>=4
Requires-Dist: pynput>=1.7
Requires-Dist: pyperclip>=1.8
Requires-Dist: pystray>=0.19
Requires-Dist: rapidfuzz>=3.5.0
Requires-Dist: requests>=2.31
Requires-Dist: sounddevice>=0.5
Description-Content-Type: text/markdown

# flow_local

[![CI](https://github.com/Tonzium/TyprFlow/actions/workflows/ci.yml/badge.svg)](https://github.com/Tonzium/TyprFlow/actions/workflows/ci.yml)

Push-to-talk voice dictation into any app - fully local, fully offline.

<!-- TODO: replace with a ~10s GIF of a dictate -> paste round trip.
     Suggested path: docs/dictate-demo.gif -->
<!-- ![flow_local demo](docs/dictate-demo.gif) -->

- **STT**: faster-whisper (`large-v3-turbo` for English, `Finnish-NLP/whisper-large-finnish-v3-ct2` for Finnish)
- **Cleanup**: local Ollama model rewrites the raw transcript - removes fillers, applies self-corrections, fixes punctuation
- **Output**: pasted into the focused window via clipboard + Ctrl+V / Cmd+V (old clipboard restored)

## Download

| Platform | Get it | Download size |
|---|---|---|
| Windows 10/11 | [Installer (.exe)](https://github.com/Tonzium/TyprFlow/releases/latest) | ~75 MB |
| Linux (X11 or Wayland) | [AppImage](https://github.com/Tonzium/TyprFlow/releases/latest) | ~1.5 GB - [why so large](#why-the-appimage-is-15-gb) |
| macOS | [.dmg](https://github.com/Tonzium/TyprFlow/releases/latest) | ~92 MB |
| Already have Python 3.10+ | `pipx install flow-local` | ~150 MB, ~1.5 GB on Linux |

Installers are unsigned. Windows shows a SmartScreen "unknown publisher"
warning (**More info** -> **Run anyway**); macOS needs right-click -> **Open**
the first time (Gatekeeper).

**Budget for the first run.** However you install it, the app downloads its
speech model the first time you launch it - about **1.6 GB**, once. If you
also install Ollama for the AI cleanup pass, it pulls the language model in
the background too, about **4.7 GB** for the default `qwen2.5:7b`. Both are
one-time and cached, and dictation works (raw, uncleaned) as soon as the
speech model is in.

Then read the section for your OS below. **Linux under Wayland needs two
one-time permission changes** before global hotkeys can work at all.

## Windows

1. Run the installer. It installs per-user into
   `%LOCALAPPDATA%\Programs\flow-local` and needs no administrator rights.
2. Launch **flow-local**. A tray icon appears immediately, blue while the
   speech model downloads, grey once it is ready.
3. Hold **Ctrl+Space**, speak, release. The text is pasted into whatever
   window has focus.

Optional: install [Ollama](https://ollama.com/download) for the AI cleanup
pass, command mode and compose mode. Without it, dictation still works and
gives you the raw transcript. The tray menu carries an **Install Ollama** item
until you have it; after installing, hit **Save** in the settings UI and the
app re-probes without a restart.

### Using an NVIDIA GPU on Windows

The installer build transcribes on the **CPU**. That is a deliberate trade:
the CUDA 12 runtime is ~1.4 GB of wheels, and bundling it would turn a 75 MB
installer into a 1.5 GB one for everybody, GPU or not.

A frozen build cannot be extended afterwards, so **the installer has no way to
add GPU support** - unless the machine already carries a system-wide CUDA 12
runtime (both cuBLAS and cuDNN) on `PATH`, which CTranslate2 then finds by
itself. If you have an NVIDIA card and want the speed, install with pipx
instead:

```
pipx install flow-local
pipx inject flow-local nvidia-cublas-cu12 nvidia-cudnn-cu12
```

Relaunch, and the console reports `[model] running on GPU` rather than
`[model] running on CPU`. Verified on an RTX 5090 with the CUDA 12.9 wheels.

### Notes for a pipx install on Windows

Start it with **`flow-localw`**, not `flow-local`. Both run the same app, but
`flow-local` keeps a console window open for as long as the app runs, and
closing that window kills dictation. `flow-localw` runs windowless, the way
the installer build does.

## Linux

Global hotkeys, the clipboard and desktop notifications each need a system
package that is not always present. Install those first:

```
# Debian / Ubuntu / Mint
sudo apt install libportaudio2 libnotify-bin wl-clipboard
# on an X11 session, xclip instead of wl-clipboard:
sudo apt install xclip
```

```
# Fedora
sudo dnf install portaudio libnotify wl-clipboard
# Arch
sudo pacman -S portaudio libnotify wl-clipboard
```

Running the **AppImage** additionally needs FUSE 2, which Ubuntu 22.04+ and
Fedora no longer install by default:

```
sudo apt install libfuse2        # Debian / Ubuntu
sudo dnf install fuse-libs       # Fedora
```

Then make it executable and run it:

```
chmod +x flow-local-x86_64.AppImage
./flow-local-x86_64.AppImage
```

Linux builds include the CUDA 12 runtime, so an NVIDIA GPU is used
automatically with no extra steps.

### Wayland: two one-time permission changes

Under Wayland an ordinary application cannot see global key presses or inject
keystrokes - by design. flow_local therefore reads the keyboard through
`/dev/input` and types through `/dev/uinput`, and **your account can do
neither by default**. Both failures are quiet ones: the hotkey simply does
nothing. The app raises a desktop notification when it hits them.

**1. Let it read the keyboard.** This is what makes hotkeys fire:

```
sudo usermod -aG input $USER
```

**2. Let it type.** This is what makes the transcript land in your window:

```
echo 'KERNEL=="uinput", GROUP="input", MODE="0660", OPTIONS+="static_node=uinput"' | sudo tee /etc/udev/rules.d/99-flow-local-uinput.rules
sudo udevadm control --reload-rules
sudo udevadm trigger
```

**Log out and back in** - group membership only applies to new sessions.

To confirm it worked, start the app from a terminal and look for:

```
[hotkeys] using evdev backend (Linux input devices)
```

A warning about falling back to pynput instead means step 1 has not taken
effect yet.

These permissions let the app read all keyboard input for your user, which is
inherent to global hotkeys on Wayland rather than specific to this app. It
reads the devices without grabbing them, so your keystrokes still reach the
focused window normally.

Under **X11** neither step is needed - the app falls back to pynput, which X11
supports directly. The evdev path is still preferred where it is available,
and `FLOW_HOTKEY_BACKEND=pynput` forces the fallback if you would rather not
grant device access.

### If there is no tray icon

Several Linux desktops ship no system tray. GNOME removed it and needs an
extension such as AppIndicator Support; a bare Wayland session may have none
at all. The app notices and keeps running without an icon - dictation, hotkeys
and notifications are unaffected - but the menu is then out of reach, so use
the command line for the two things it provided:

```
flow-local --settings    # open the settings UI in a browser
flow-local --quit        # stop the running instance
```

Both work with a tray present too. `flow-local --help` lists them.

### Why the AppImage is 1.5 GB

Because it bundles the CUDA 12 runtime (~1.4 GB) unconditionally, so NVIDIA
users get GPU transcription with no extra steps and everyone else carries it
for nothing. `pipx install flow-local` on Linux pulls the same wheels for the
same reason. Windows and macOS ship without them, which is the whole 20x size
difference. This is a known trade rather than a bug; `PLAN_INSTALL.md` weighs
the options.

## Platform support

| Platform | Status | Notes |
|---|---|---|
| Windows | Supported | Primary development platform, tested end-to-end. CPU transcription unless installed via pipx with the CUDA wheels |
| Linux / X11 | Should work | Needs PortAudio and a clipboard tool; hotkeys need no extra permissions |
| Linux / Wayland | Should work | Needs the two permission changes above; several desktops have no system tray |
| macOS | Should work | Grant Accessibility permission (System Settings -> Privacy) so global hotkeys work; paste uses Cmd+V |

Windows is tested end-to-end. The Linux and macOS paths are covered by CI
(lint plus unit tests on all three OSes, then a wheel install and launch) but
have not been hardware-tested on every desktop - reports welcome.

## First run

The tray icon appears right away and turns blue while the speech model
downloads (~1.6 GB, once) - the hotkeys answer with "still loading" until it
is ready. Three things are checked at startup and reported in a notification
rather than failing:

- **No Ollama?** Dictation still works, you just get the raw transcript;
  cleanup, command mode and compose mode stay off. An **Install Ollama
  (enables AI cleanup)** item appears in the tray menu until you have it.
- **No CUDA 12 runtime?** Transcription runs on the CPU, which is slower. See
  the Windows GPU section above.
- **No microphone?** Reported with a pointer to your OS sound settings.

## Hotkeys (config.json)

| Keys | Action |
|---|---|
| hold `Ctrl+Space` | dictate, release to transcribe & paste |
| hold `Ctrl+Alt+Space` | command mode: select text, hold, speak an instruction ("make this formal") - the selection is replaced with the result |
| hold `Ctrl+Alt+C` | compose mode: speak a brief ("email to john, meeting moved to thursday") - a draft is copied to the clipboard, never auto-pasted |
| hold `Ctrl+Alt+O` | open mode: speak a file you want opened ("the spreadsheet I was messing with yesterday") - a clear match opens directly, an ambiguous one lists numbered candidates (hold again and say the number) |
| `Ctrl+Shift+Space` | toggle English / Finnish |
| `Ctrl+Alt+R` | toggle raw mode (skip LLM cleanup) |

Edit `config.json` to change hotkeys, models, or the personal dictionary
(words fed to Whisper as spelling hints). Open mode is Windows-only for now:
it matches against the Windows Recent-files list plus the directories in
`open_roots`, and only opens documents/media (never executables or scripts).

## Tray menu

Right-click the tray icon (on Windows, bottom-right taskbar, maybe behind the
`^` chevron):

- **Settings** - opens the web-based settings UI
- **Language** - toggle English / Finnish
- **Wakeup Beep** - toggle the sound played when recording starts
- **Raw mode** - toggle the LLM cleanup pass
- **Start on login** - checkbox; the label and mechanism are per-OS (registry
  Run key on Windows, a LaunchAgent on macOS, an XDG `.desktop` entry on Linux)
- **Check for updates** - opens the GitHub Releases page in your browser
- **Quit**

## Command line

```
flow-local              # start dictating
flow-local --settings   # open the settings UI (works while the app runs)
flow-local --quit       # stop the running instance
flow-local --version
flow-local --help
```

## Development

Running from source instead of the installer - for contributing, or for a
platform without a packaged build yet.

Managed with [uv](https://docs.astral.sh/uv/). You also need
[Ollama](https://ollama.com/) for transcript cleanup and command mode - plain
dictation still works without it, falling back to the raw transcript.

```
# 1. Start Ollama and pull the cleanup model (default: qwen2.5:7b)
ollama serve                 # skip if it already runs as a background service
ollama pull qwen2.5:7b

# 2. Install and run
uv sync
uv run flow-local
```

No config-file setup required: on first run the app creates a `config.json`
(and `glossary.json`) for you, seeded from the committed `*.example.json`
files. It is written to your per-user config directory
(`%APPDATA%\flow-local\` on Windows, `~/Library/Application Support/flow-local/`
on macOS, `$XDG_CONFIG_HOME/flow-local/` on Linux) - edit it there, or through
the tray's **Settings** UI. If you are working from a repo checkout and want
your personal config to live in the repo instead, drop a `config.json` at the
repo root (copy `flow_local/config.example.json`) and it takes priority.
Either way `config.json` and `glossary.json` are gitignored. The first run
also downloads the Whisper STT model (~1.6 GB).

Dev tooling:

```
uv sync --dev
uv run ruff check .      # lint
uv run ruff format .     # format
uv run pytest            # unit tests (no mic/GPU/Ollama needed)
uv run python smoke_test.py  # loads Whisper + hits Ollama (needs Ollama; no mic)
uv run flow-local --settings # open the web-based settings UI
```

CI (GitHub Actions, `.github/workflows/ci.yml`) runs lint and tests on every
push and pull request across Ubuntu, macOS, and Windows, then builds the
wheel, installs it into a clean venv and launches the console script from
there.

Why no Dockerfile: the app is a desktop tool that needs the host microphone,
global keyboard hooks, the system tray, the clipboard, and the GPU - none of
which a container can reach sensibly. The Ollama dependency is already an
external service; if a server deployment is ever needed, the right split is a
containerized transcription API plus a thin native client.

### Releasing

1. Bump `version` in `pyproject.toml`.
2. Move the `CHANGELOG.md` "Unreleased" entries under a new `## [X.Y.Z]` heading.
3. `git tag vX.Y.Z && git push origin vX.Y.Z` - this triggers
   `.github/workflows/release.yml`, which builds the Windows/macOS/Linux
   installers and opens a **draft** GitHub Release with them attached.
4. Review the draft, then **publish it by hand**. Nothing goes live from a tag
   push alone - and an unpublished draft means the Download links above still
   resolve to the previous version, however green the build was.
5. The PyPI upload (`publish-pypi`) is gated on the `pypi` GitHub environment
   and waits for an approval, because a PyPI version is permanent - it can be
   yanked but never reused or truly withdrawn. Approve it once the artifacts
   look right. It authenticates via PyPI trusted publishing (OIDC), so there
   is no API token to rotate.
