# LingoTuner > ๐Ÿ‡ฌ๐Ÿ‡ง English ยท [๐Ÿ‡ฉ๐Ÿ‡ช Deutsch](https://github.com/johannes-kaindl/lingotuner/blob/main/README.de.md) **Retune the communication style of a text with four dials and a local LLM โ€” direct or diplomatic, explicit or implicit, factual or social, literal or figurative. Nothing leaves your machine.** [![License: AGPL-3.0](https://img.shields.io/badge/license-AGPL--3.0-blue.svg)](https://github.com/johannes-kaindl/lingotuner/blob/main/LICENSE) [![Docs: CC BY-SA 4.0](https://img.shields.io/badge/docs-CC%20BY--SA%204.0-lightgrey.svg)](https://github.com/johannes-kaindl/lingotuner/blob/main/LICENSE-DOCS) [![Release](https://img.shields.io/github/v/release/johannes-kaindl/lingotuner?label=release)](https://github.com/johannes-kaindl/lingotuner/releases) ![Platform](https://img.shields.io/badge/platform-Obsidian%201.11.4%2B%20ยท%20desktop%20%26%20mobile-7c3aed) Neurodivergent and neurotypical communication styles differ along a few well-described dimensions: how direct a request is, how much context is spelled out, how much social framing surrounds the content, and how literal the language is. LingoTuner is an interpreter between those styles. Select a text, set four dials, add a note if you like, and a local model rewrites the text โ€” same information, different style. A sidebar panel next to your note holds the four sliders, the note field, the streamed result and the four ways out of it.

Obsidian with a note open in the editor and the LingoTuner panel on the right: the source line reads Selection, 175 characters, the preset Maximum clarity is active, the streamed result reads Could you send me the report by Friday?, and four buttons โ€” Replace selection, Replace note, Copy, New note โ€” sit below

## What it does - **Four dials, five steps each.** Directness, context, social nuance and semantics, each from โˆ’2 to +2. The middle means "leave this dimension alone"; only what you move is instructed, and each step carries its own before/after example for the model. The model never sees a number, only the name of the step. - **A note with priority.** "Use first names", "shorten the second paragraph" โ€” free text that is put into the prompt *after* the dials and explicitly outranks them. A note alone is enough for a run, without touching a single dial. - **Three sources, four outputs.** Take the selection, the body of the active note, or text pasted into the panel; then replace the selection, replace the note body, copy, or create a new note. Replacing goes through the editor, so Cmd+Z undoes it in one step, and replacing a note leaves its frontmatter untouched. - **Streaming preview, round history.** The result grows paragraph by paragraph instead of appearing after a blind wait. Every run is kept as a round: go back to an earlier one, refine it further, or start again from the source โ€” later rounds stay. - **Presets.** Four are built in (Neutral, Maximum clarity, Collegial, Extremely polite); any dial combination can be saved under your own name. A combination that matches no preset is shown as "(adjusted)". - **Prompts you can override.** An optional vault folder may replace the shipped system prompt (`system.md`) or single example pairs (`_.md`). Empty means the shipped text โ€” so improvements still reach you. One button writes the shipped texts into the folder as a starting point; existing files are never overwritten. - **Local by design.** Any OpenAI-compatible server: LM Studio, Ollama, MLX. Several endpoints can be listed and are tried in order. A hosted provider is possible, but only if you add its address and key yourself โ€” and a row carrying a key says so. - **Model-agnostic.** No model name is hard-coded. The list is read live from the endpoint's `GET /v1/models`; leaving the setting empty lets the server use whatever it has loaded. Reasoning models are handled: `` output is separated from the result, and reasoning is suppressed where the server supports it. - **For other plugins.** `app.plugins.plugins["lingotuner"].api.tune(text, "clarity")` runs the same path as the panel and returns `{ ok: true, text, truncated }` or a named reason. Every call is also reported to LLM Lab if that plugin is installed. - **Bilingual interface.** English is canonical; the UI and the example bank follow Obsidian's display language and ship a full German translation. ## Requirements - **Obsidian 1.11.4+** (desktop or mobile). - **An OpenAI-compatible local server** with a chat model loaded โ€” [LM Studio](https://lmstudio.ai), [Ollama](https://ollama.com) or MLX. New to local LLMs? The **[local LLM setup guide](https://uplink.jkaindl.de/llm-setup)** walks you through server, model and mobile access end to end. - **CORS, if the connection test is green but the stream is blocked.** The result is streamed over XHR from the origin `app://obsidian.md`, and a server that answers a plain request may still refuse that. In LM Studio enable CORS in the server settings (`lms server start --bind 0.0.0.0 --cors` sets both at once โ€” a missing flag is reset to its default); for Ollama set `OLLAMA_ORIGINS=app://obsidian.md`. If the server refuses the stream, the plugin retries once without streaming, so the answer still arrives โ€” all at once instead of token by token. Only when that fails too does it name the case instead of reporting a generic network error. - **A mid-size model, if you care about the result.** The rewrite has to keep every piece of information while changing the style; measured against a local LM Studio, a 35B mixture-of-experts model invented no information, never switched language and never returned the text unchanged, across all 16 dial steps in both languages. Two of those steps (`context:-2`, `social:2`) hit the intended tone but not the concreteness of the reference text. A very small model tends to shorten instead of rephrase. ## Install ### Catalog (recommended) **Via [AnySource Sideloader](https://github.com/johannes-kaindl/anysource-sideloader)**, which installs and updates plugins from any git forge. Subscribe to the Order from Traces catalog once under **Settings โ†’ AnySource Sideloader โ†’ Catalogs โ†’ Add**: ``` https://git.jkaindl.de/jkaindl/obsidian-catalog/raw/branch/main/catalog.json ``` LingoTuner then appears in the sideloader's plugin list and updates like any other plugin; every download is checksum-verified. To install just this one plugin without the catalog, add its repository URL as a source instead: `https://github.com/johannes-kaindl/lingotuner`. ### Manual Download `main.js`, `manifest.json` and `styles.css` from the [latest release](https://github.com/johannes-kaindl/lingotuner/releases) (or the `lingotuner.zip` bundle, which contains exactly those three files) into `/.obsidian/plugins/lingotuner/`, then enable the plugin under **Settings โ†’ Community plugins**. ### From source ```bash git clone https://github.com/johannes-kaindl/lingotuner cd lingotuner npm install npm run build # produces main.js ``` Copy `main.js`, `manifest.json` and `styles.css` into `/.obsidian/plugins/lingotuner/`. ## Usage 1. Open the panel โ€” ribbon icon (sliders) or the command **Open panel**. With text selected, **Tune selection** opens the panel and takes the selection with it. 2. Pick a source: **Selection**, **Active note** or **Text field**. The status line says what was picked up ("Selection: 412 characters in Mail to X") or why nothing can run ("Select some text in a note first", "The note is in reading mode"). 3. Move the dials or pick a preset, and add a note if something specific should change. 4. Press **Tune**. The result streams in; **Cancel** stops it and keeps what has arrived. From the second round on, the panel lists the rounds and one click returns to any of them. 5. Not quite right? **Refine this result** applies a new note to the result you are looking at; **Tune again from source** starts over from the original text. 6. Take the result out: **Replace selection**, **Replace note** (asks first, keeps the frontmatter), **Copy**, or **New note**. The replace buttons stay disabled unless the source is still live โ€” same note, still in editing mode, text unchanged since it was read. Copy and New note always work.

The panel after two rounds: a note reading Keep the offer of another day, the refined result, and the Rounds list with Round 1 from source and Round 2 refined from round 1

The panel with the preset Collegial: four dials at different steps, each with its step name, an empty note field, the Tune button and an empty preview

Click the preview for full size โ€” the four dials with their step names

## Configuration **Settings โ†’ Community plugins โ†’ LingoTuner**, grouped into connection, style and output: | Setting | What it does | |---|---| | Endpoints | An ordered list of OpenAI-compatible servers, each with an optional API key and model override. The first reachable one is used; every row shows its own reachability status. API keys are kept in Obsidian's keychain, not in the plugin's data file. | | Request | Shows the sampling values, thinking level and token budget actually sent for the current model family and backend, with per-family overrides, the last request and any deviations for the running session. The thinking control in the panel switches between off and the model's preferred level; a per-family toggle in this section turns it into a four-level dropdown. | | Override folder | Optional vault folder with `system.md` and `_.md` files that replace shipped prompt text. Empty means shipped text. | | Saved presets | Your own dial combinations, saved from the panel. | | Logbook | Off by default. When on, every run is appended to a monthly note (time, model, dial values, note, original, result). | | Folder for new notes | Where **New note** puts the tuned text. Empty means the vault root. |

The LingoTuner settings tab: the Connection group with one reachable endpoint marked active, the collapsed Request section and the Model row, then the Style group with the override folder

## How it works The prompt is assembled, not templated. A system part states the invariants โ€” keep the language of the text, add nothing, drop nothing, keep the Markdown structure, return only the rewritten text. Each dial that is *not* in the middle adds one instruction block with the name of its step plus one before/after example pair for exactly that step; a dial in the middle adds nothing at all, which is why "all dials centred and no note" is reported as nothing to do rather than sent to a model. The note, if present, is appended last with an explicit precedence over everything above it. The answer is streamed over XHR (server-sent events), reasoning output is separated from the result before it is displayed, and a stream cut short by the token limit is shown with a warning rather than silently kept. The whole path โ€” prompt building, dial model, session history, source guards โ€” lives in `src/core/` without importing `obsidian`, and a gate keeps it that way, which is why it can be tested in plain Node. ## Documentation - [Documentation index](https://github.com/johannes-kaindl/lingotuner/blob/main/docs/README.md) โ€” all guides in one place. - [Getting started](https://github.com/johannes-kaindl/lingotuner/blob/main/docs/getting-started.md) โ€” from the install to your first tuned text. - [Use the Apple Intelligence endpoint](https://github.com/johannes-kaindl/lingotuner/blob/main/docs/use-apple-intelligence.md) โ€” tune on the device through an Apple Shortcut, with its limits. - [Troubleshooting](https://github.com/johannes-kaindl/lingotuner/blob/main/docs/troubleshooting.md) โ€” the exact message, its cause and the fix. ## Contributing - [`AGENTS.md`](https://github.com/johannes-kaindl/lingotuner/blob/main/AGENTS.md) โ€” architecture, conventions and gotchas for contributors and AI agents. - [`docs/internal/SMOKE.md`](https://github.com/johannes-kaindl/lingotuner/blob/main/docs/internal/SMOKE.md) โ€” the GUI smoke test: what is checked against a running Obsidian, and how to reproduce the run. - [`docs/internal/LAB.md`](https://github.com/johannes-kaindl/lingotuner/blob/main/docs/internal/LAB.md) โ€” the measured run of all 16 example pairs against a local model, with the findings that shaped the shipped prompt text. - [`CHANGELOG.md`](https://github.com/johannes-kaindl/lingotuner/blob/main/CHANGELOG.md) โ€” what changed per release. ## License Code AGPL-3.0-or-later ([`LICENSE`](LICENSE)), documentation CC BY-SA 4.0 ([`LICENSE-DOCS`](LICENSE-DOCS)).