/horizon-speech
Check that a microphone is usable for Horizon dictation and report which layer is at fault — no signal, bad level, or a genuine model/accent limit. Use when dictation produces wrong, unrelated, or empty text.
Claude Code
$ npx -y skills add peters/horizon --skill horizon-speech --agent claude-code
Codex
$ npx skills add peters/horizon --skill horizon-speech -a codex -y
Cursor
$ npx skills add peters/horizon --skill horizon-speech -a cursor -y
Gemini CLI
$ npx skills add peters/horizon --skill horizon-speech -a gemini-cli -y
GitHub Copilot
$ npx skills add peters/horizon --skill horizon-speech -a github-copilot -y
OpenCode
$ npx skills add peters/horizon --skill horizon-speech -a opencode -y
Windsurf
$ npx skills add peters/horizon --skill horizon-speech -a windsurf -y
SKILL.md
horizon-speech.SKILL.mdname: horizon-speech
description: Check that a microphone is usable for Horizon dictation and report which layer is at fault — no signal, bad level, or a genuine model/accent limit. Use when dictation produces wrong, unrelated, or empty text.
Horizon speech input check
This skill uses local audio diagnostics. Horizon has no speech MCP tool. Do not treat dictation as an MCP-controlled capability.
Run this when a user says dictation "types the wrong thing", produces text they never said, or produces nothing.
Read this first
Whisper-family models **hallucinate fluent, plausible text when fed silence**. Given a digitally silent recording, NB-Whisper will confidently emit something like `Esther Smith, forfatter` — grammatical, idiomatic, and completely unrelated to the user. The output looks like a model, accent, or dialect problem. It is almost always a capture problem.
So: **measure the capture level before touching models, languages, or config.** Never conclude "the model is bad at your accent" until step 3 shows real signal.
A user cannot tell these apart from the transcript alone. That is the whole reason this skill exists.
1. Compare configured device against reality
Horizon keeps its settings in `settings.sqlite`, and a YAML file is an export that Horizon never reads back. Do not read `config.yaml` to find a setting: it may be absent, because the first start of a build with a store moves it to `config.yaml.imported-<time>`, or it may be a stale export.
Find the settings path, then export it:
- With `HORIZON_HOME` set and not empty, Horizon uses only that folder and
ignores every location below.
- Otherwise it uses `$HOME/.horizon/`, then `$XDG_CONFIG_HOME/horizon/`, then a
relative `horizon.yaml`/`horizon.yml` in the working directory. Resolution keys off `HOME`; `USERPROFILE` is never consulted.
The store sits beside that path and has the same name with the extension replaced by `.settings.sqlite` (`config.settings.sqlite`, or `horizon.settings.sqlite`). Read the settings with:
horizon --export-config /tmp/horizon-settings.yaml # --config <path> for another path
That command fails when the store does not exist yet, which means Horizon has not saved a setting on this computer.
Read `features.speech` from the export:
- `input_device: ""` means the system default input is used.
- A non-empty name is matched case-insensitively: exact, then substring, then a
normalized identity key.
**If the configured name matches no present device, Horizon falls back to the system default and only logs a warning.** The user never sees it. A config naming a microphone that is no longer plugged in therefore looks like it works. Check the name against the live device list before anything else.
List input devices:
- Linux (PipeWire/PulseAudio): `wpctl status`, or `pactl list short sources`
- Linux (ALSA only): `arecord -l`
- macOS: `system_profiler SPAudioDataType`, or
`ffmpeg -f avfoundation -list_devices true -i ""`
Also confirm the device is not a **Bluetooth headset in HSP/HFP mode**. That profile is narrowband and heavily compressed; it degrades accuracy badly. Prefer a wired or USB microphone, or force the A2DP-era high-quality input if the stack offers one.
2. Record a short sample
Ask the user to speak normally for about six seconds. Record mono at 16 kHz — that is what the models consume.
**Record from the device step 1 matched, never the system default.** When `input_device` names a non-default microphone, sampling the default measures a different device than Horizon uses and can yield the exact opposite diagnosis — a silent default while the real microphone works, or the reverse.
- Linux (ALSA): `arecord -D <device> -f S16_LE -r 16000 -c 1 -d 6 sample.wav`,
where `<device>` is a name from `arecord -L`
- Linux (PipeWire): `pw-record --target <node-name-or-id> --rate 16000 --channels 1 sample.wav`
- macOS: `ffmpeg -f avfoundation -i ":<device-index>" -ar 16000 -ac 1 -t 6 sample.wav`
Only fall back to the default device when `input_device` is empty, since that is what Horizon itself then uses.
Tell the user **when recording starts**. If you launch the recorder as a background task, add a visible countdown first — otherwise the window elapses while they are still reading your message, and you will measure an empty room and misdiagnose it as a dead microphone.
3. Measure the level
`level.py` sits next to this `SKILL.md`. Invoke it by its full path — the working directory is normally the user's workspace, not the installed skill directory, so a bare `level.py` will not be found:
python3 <dir containing this SKILL.md>/level.py sample.wav
It needs only the Python 3 standard library, and reports peak, RMS and clipped samples, each as a percentage of full scale so the numbers mean the same thing at every sample width:
| Reading | Meaning | Fix | | --- | --- | --- | | `peak` below 0.6% | No signal — capture muted, or no device | Unmute capture; confirm the right device is selected and present | | clipped samples > 20 | Too hot — distortion | Lower gain; turn microphone boost **off** first | | `rms` above 25% | Hotter than necessary | Reduce gain slightly | | `rms` 5–25% | Ideal for ASR | Nothing to do | | `rms` 2–5% | Usable | A little more gain would help | | `rms` below 2% | Too quiet | Raise gain, or move closer |
4. Transcribe the same file
Run the profile's model against `sample.wav` and compare with what the user actually said. Horizon's models are transcribe.cpp GGUFs; if a `transcribe-cli` build is available:
transcribe-cli -m <model>.gguf -l <lang> sample.wav
Otherwise have the user dictate the same sentence in Horizon and compare.
5. Verdict
Combine steps 3 and 4 — this is the part the user cannot do alone:
- **No signal + fluent unrelated text** → capture is dead. The text is
hallucinated. Fix the microphone; the model is fine.
- **Good level + empty transcript** → real audio, no speech de
Read more
name: horizon-speech description: Check that a microphone is usable for Horizon dictation and report which layer is at fault — no signal, bad level, or a genuine model/accent limit. Use when dictation produces wrong, unrelated, or empty text.
Horizon speech input check
This skill uses local audio diagnostics. Horizon has no speech MCP tool. Do not treat dictation as an MCP-controlled capability.
Run this when a user says dictation "types the wrong thing", produces text they never said, or produces nothing.
Read this first
Whisper-family models **hallucinate fluent, plausible text when fed silence**. Given a digitally silent recording, NB-Whisper will confidently emit something like `Esther Smith, forfatter` — grammatical, idiomatic, and completely unrelated to the user. The output looks like a model, accent, or dialect problem. It is almost always a capture problem.
So: **measure the capture level before touching models, languages, or config.** Never conclude "the model is bad at your accent" until step 3 shows real signal.
A user cannot tell these apart from the transcript alone. That is the whole reason this skill exists.
1. Compare configured device against reality
Horizon keeps its settings in `settings.sqlite`, and a YAML file is an export that Horizon never reads back. Do not read `config.yaml` to find a setting: it may be absent, because the first start of a build with a store moves it to `config.yaml.imported-<time>`, or it may be a stale export.
Find the settings path, then export it:
- With `HORIZON_HOME` set and not empty, Horizon uses only that folder and
ignores every location below.
- Otherwise it uses `$HOME/.horizon/`, then `$XDG_CONFIG_HOME/horizon/`, then a
relative `horizon.yaml`/`horizon.yml` in the working directory. Resolution keys off `HOME`; `USERPROFILE` is never consulted.
The store sits beside that path and has the same name with the extension replaced by `.settings.sqlite` (`config.settings.sqlite`, or `horizon.settings.sqlite`). Read the settings with:
horizon --export-config /tmp/horizon-settings.yaml # --config <path> for another path
That command fails when the store does not exist yet, which means Horizon has not saved a setting on this computer.
Read `features.speech` from the export:
- `input_device: ""` means the system default input is used.
- A non-empty name is matched case-insensitively: exact, then substring, then a
normalized identity key.
**If the configured name matches no present device, Horizon falls back to the system default and only logs a warning.** The user never sees it. A config naming a microphone that is no longer plugged in therefore looks like it works. Check the name against the live device list before anything else.
List input devices:
- Linux (PipeWire/PulseAudio): `wpctl status`, or `pactl list short sources`
- Linux (ALSA only): `arecord -l`
- macOS: `system_profiler SPAudioDataType`, or
`ffmpeg -f avfoundation -list_devices true -i ""`
Also confirm the device is not a **Bluetooth headset in HSP/HFP mode**. That profile is narrowband and heavily compressed; it degrades accuracy badly. Prefer a wired or USB microphone, or force the A2DP-era high-quality input if the stack offers one.
2. Record a short sample
Ask the user to speak normally for about six seconds. Record mono at 16 kHz — that is what the models consume.
**Record from the device step 1 matched, never the system default.** When `input_device` names a non-default microphone, sampling the default measures a different device than Horizon uses and can yield the exact opposite diagnosis — a silent default while the real microphone works, or the reverse.
- Linux (ALSA): `arecord -D <device> -f S16_LE -r 16000 -c 1 -d 6 sample.wav`,
where `<device>` is a name from `arecord -L`
- Linux (PipeWire): `pw-record --target <node-name-or-id> --rate 16000 --channels 1 sample.wav`
- macOS: `ffmpeg -f avfoundation -i ":<device-index>" -ar 16000 -ac 1 -t 6 sample.wav`
Only fall back to the default device when `input_device` is empty, since that is what Horizon itself then uses.
Tell the user **when recording starts**. If you launch the recorder as a background task, add a visible countdown first — otherwise the window elapses while they are still reading your message, and you will measure an empty room and misdiagnose it as a dead microphone.
3. Measure the level
`level.py` sits next to this `SKILL.md`. Invoke it by its full path — the working directory is normally the user's workspace, not the installed skill directory, so a bare `level.py` will not be found:
python3 <dir containing this SKILL.md>/level.py sample.wav
It needs only the Python 3 standard library, and reports peak, RMS and clipped samples, each as a percentage of full scale so the numbers mean the same thing at every sample width:
| Reading | Meaning | Fix | | --- | --- | --- | | `peak` below 0.6% | No signal — capture muted, or no device | Unmute capture; confirm the right device is selected and present | | clipped samples > 20 | Too hot — distortion | Lower gain; turn microphone boost **off** first | | `rms` above 25% | Hotter than necessary | Reduce gain slightly | | `rms` 5–25% | Ideal for ASR | Nothing to do | | `rms` 2–5% | Usable | A little more gain would help | | `rms` below 2% | Too quiet | Raise gain, or move closer |
4. Transcribe the same file
Run the profile's model against `sample.wav` and compare with what the user actually said. Horizon's models are transcribe.cpp GGUFs; if a `transcribe-cli` build is available:
transcribe-cli -m <model>.gguf -l <lang> sample.wav
Otherwise have the user dictate the same sentence in Horizon and compare.
5. Verdict
Combine steps 3 and 4 — this is the part the user cannot do alone:
- **No signal + fluent unrelated text** → capture is dead. The text is
hallucinated. Fix the microphone; the model is fine.
- **Good level + empty transcript** → real audio, no speech de
Other skills on horizon.
More skills from other plugins.
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…
ask-matt
Ask which skill or flow fits your situation. A router over the skills in this repo.
Questions about horizon-speech.
- Which coding agents can use horizon-speech?
- horizon-speech installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.

