A local-first, cross-platform writing assistant. Run a model on-device, or connect any OpenAI-compatible endpoint (local or cloud).
Grammar, synonyms, advice, summaries, free-form rewrites and translation, with inline accept/reject on each suggestion. No login, no accounts, no backend.
⚠️ Alpha. Glossa is usable but young: it has not been released yet, and its behavior may change between versions. Bug reports and feedback are welcome.
Glossa is a writing editor that sends your text to a Large Language Model and
turns its feedback into discrete, reviewable suggestions you can accept or
reject one by one. It can run a model directly inside the app (on-device, via
llama.cpp) with no endpoint and no API key, or point the same code at any
OpenAI-compatible endpoint — a local server (llama.cpp / Ollama) or a cloud
provider (OpenAI / Claude) — through the /v1/chat/completions API.
It is a pure client: there is no server to run, and no state is stored anywhere but on your device.
- Two interchangeable backends:
- In-app model (on-device, via llama.cpp): no endpoint, no API key. Download one from a small catalog or add any Hugging Face GGUF repo and pick a quantization; downloads are managed by the app and resume if interrupted.
- Remote endpoint: any OpenAI-compatible server, local or cloud.
- The choice is live and mutually exclusive; the editor shows the active model.
- Five writing modes plus a translator:
- Grammar & syntax: discrete error items, accept or reject each one.
- Synonyms & repetitions: variety suggestions.
- Advice: clarity, tone and structure recommendations (not errors).
- Summary: a concise summary in a side sheet.
- Free prompt: transform the text from an instruction (preview, then apply).
- Translate: source (auto-detected or chosen) to target, in a two-sheet layout.
- Inline underlines colored by category, styled by severity (solid, wavy or dotted), clickable.
- Deterministic writing score computed app-side from the open items, not produced by the model.
- Smart re-check: debounced re-analysis as you type, per paragraph with content-addressed caching, so only the paragraph you edited is re-sent.
- Decision memory: dismissed items don't reappear during auto re-check; a manual re-analysis starts fresh, so a mistaken dismissal can resurface.
- Free language list: add any language by name, used by both corrections and the translator.
- Editable prompts: customize the per-action instructions from Settings.
- Light and dark themes with a centralized design system.
The design follows a few deliberate choices:
- Structured JSON, not diffs. Instead of asking the model to rewrite the text
and diffing the result (noisy), Glossa asks it to list the issues as a JSON
array, each item already a discrete unit with
original,replacement,category,severityandexplanation. - Offsets are recomputed app-side. Model character offsets are unreliable, so
Glossa never trusts them: it re-locates each
originalstring inside the current text (handling multiple occurrences). SeeSuggestionLocalizer. - Prompts are in English, output follows the text. Base prompts are written in English, which behaves more consistently across models; a centralized language rule asks the model to answer in the user's language (or the one chosen in Settings).
- No backend. The app calls the endpoint directly over HTTP. Cloud keys, if used, live only in local storage on the user's own machine.
┌──────────────────────────────────────────────┐
│ Glossa (Flutter client) │
│ editor + overlay · panels · score · settings │
└─────────────┬───────────────────────┬─────────┘
in-process │ │ HTTP POST /v1/chat/completions
(FFI) │ ┌─────────┴─────────┐
▼ ▼ ▼
┌────────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ In-app model │ │ Local endpoint │ │ Cloud endpoint │
│ llama.cpp on-device│ │ llama.cpp/Ollama│ │ OpenAI / Claude │
└────────────────────┘ └─────────────────┘ └─────────────────┘
| Platform | Status | Notes |
|---|---|---|
| iOS / iPadOS | ✅ | universal app |
| Android | ✅ | |
| macOS | ✅ | |
| Windows | ✅ | desktop |
| Linux | ✅ | desktop (secure storage via libsecret) |
| Web | ✅ | local endpoint only (cloud blocked by browser CORS) |
- Flutter 3.44+ (Dart 3.12+)
with native assets enabled (
flutter config --enable-native-assets, needed by the in-app model backend). - A model to use, either:
- In-app: nothing to set up — download one from Settings, no endpoint or key.
- Remote endpoint: a llama.cpp server
or Ollama locally, or a cloud provider exposing
/v1/chat/completions(e.g. OpenAI).
git clone https://github.com/pacio5/glossa-language-tool.git
cd glossa-language-tool
flutter pub get
flutter run -d macos # or: chrome, <ios-simulator>, <android-emulator>Then open Settings and choose a backend:
- In-app: pick a model and download it (or add a Hugging Face GGUF repo), then select it. No endpoint or key needed.
- Remote: set the endpoint URL (e.g.
http://127.0.0.1:8080), an optional API key, and the model name. "Test connection" verifies and saves it.
Switch the backend to In-app in Settings and download a model from the
catalog, or paste a Hugging Face GGUF repo (e.g. unsloth/Qwen3.5-2B-GGUF) and
pick a quantization. The model runs on-device via llama.cpp (GPU-accelerated
where available); nothing leaves your machine. Larger models need more RAM and a
longer first download.
The endpoint URL is normalized for you: /v1 and /chat/completions are
optional and added automatically.
Run llama.cpp or Ollama and point Glossa at it. Network reachability differs by platform; the project is already configured, so you only need the right URL:
| Running on | Endpoint to use | Why |
|---|---|---|
| macOS / iOS simulator | http://127.0.0.1:8080 |
shares the host loopback |
| Android emulator | http://127.0.0.1:8080 after adb reverse tcp:8080 tcp:8080 |
forwards the device's localhost to the host |
| Physical phone (Wi-Fi) | http://<your-mac-ip>:8080 |
bind the server to 0.0.0.0 |
For the web build against a local endpoint, open CORS on the server
(OLLAMA_ORIGINS for Ollama, the corresponding flag for llama.cpp).
Use the provider's base URL and your API key. Cloud-from-web is blocked by the provider's CORS policy, so use the desktop or mobile build for cloud endpoints.
OpenAI: endpoint https://api.openai.com, your OpenAI key, a model name (e.g.
gpt-4o-mini).
Claude (Anthropic): Glossa reaches Claude through Anthropic's OpenAI-compatible endpoint, so the same client works with no special handling. Set:
- Endpoint URL:
https://api.anthropic.com - API key: your Anthropic key
- Model: a Claude model name (e.g.
claude-opus-4-8)
The compatibility layer ignores the JSON-mode hint Glossa sends, but this is harmless: the prompts already require a JSON-only reply and the parser extracts it, so the writing modes work unchanged.
Everything lives in Settings (persisted locally, no account). The API key is
kept in the platform secure storage (Keychain on iOS/macOS, Keystore on Android,
Credential Locker on Windows); the rest of the settings live in
shared_preferences. On the web there is no OS keystore, so prefer a short-lived
or scoped key there.
- Backend: an in-app on-device model, or a remote OpenAI-compatible endpoint (mutually exclusive, switched live)
- In-app models: download from a catalog or add a Hugging Face GGUF repo; managed and persisted on device
- Endpoint URL, API key (optional), model name (remote backend)
- Correction language: automatic (follow the text) or a specific language
- Languages: a free list you can add to; feeds corrections and the translator
- Theme: light or dark
- Prompts: edit the per-action instructions sent to the model
The project follows a small set of conventions:
- Architecture: MVVM, feature-first (
lib/features/<feature>/withmodel · data · cubit · view; cross-cutting code inlib/core/). - State management: flutter_bloc (Cubits), immutable
state with
equatable. - Dependency injection via
RepositoryProvider/BlocProvider. - Static analysis:
very_good_analysis;flutter analyzemust stay at zero issues. - Language: English everywhere in code, comments and UI strings.
flutter analyze # zero issues expected
dart format lib test # formatting
flutter test # unit + widget tests (live integration tests skip)The live integration tests are gated by env vars. A remote one hits a real endpoint; the embedded one downloads a small model and runs it on-device:
LLM_LIVE_ENDPOINT=http://127.0.0.1:8080 \
LLM_LIVE_MODEL=<your-model> \
flutter test test/integration/
RUN_EMBEDDED_LIVE=1 flutter test \
test/integration/embedded_live_test.dart --timeout=nonelib/
├─ main.dart # composition root (DI wiring)
├─ app.dart # MaterialApp + providers + theming
├─ core/
│ ├─ llm/ # providers (remote + in-app), model manager/catalog
│ └─ ui/ # theme + breakpoints
└─ features/
├─ editor/ # the editor: cubit, analysis, localizer, views
└─ settings/ # endpoint/model/language/theme/prompts
Core functionality is complete. Remaining items (see plan/):
- Embedded inference: bundle llama.cpp directly into the app (via Dart FFI or a native plugin) so it can run a local model on-device, with no external endpoint to set up. The current OpenAI-compatible client stays as an option for cloud or shared models.
- Model benchmarking across IT / EN / ES
- Release packaging per platform
- Optional richer iPad two-column layout
Issues and pull requests are welcome. Please keep flutter analyze clean, all
tests passing, and code/comments in English.
MIT © Elia Pacioni