Tuning
Adjust wake sensitivity, timing, voice and models, and fix what feels wrong.
Everything on this page is a key in ~/.config/parlour/config.json.
parlour config edit opens the file. parlour config show prints what is in
force, with the defaults filled in. parlour restart (or a restart from the
app) picks up your changes.
For the wake word, the voice, the microphone and the local model, running
parlour init again is often quicker. Every question starts at what is set
now, Escape goes back one question, and a review at the end lets you change
any part before anything is downloaded or written.
When something is not quite right
| What you notice | What to try |
|---|---|
| It wakes up for the television | Raise wake.threshold towards 0.7. |
| It never wakes up | Lower wake.threshold towards 0.4, and check that audio.inputDevice is the microphone you think it is. |
| It cuts you off mid-sentence | Raise audio.silenceMs, or lower audio.silenceThreshold. |
| It waits too long before answering | Lower audio.silenceMs to about 600. |
| It gives up before you start talking | You have 2.5 s after the wake word to begin. Try saying the wake word and the request in one breath. |
| It answers slowly | Try a smaller local model (parlour models suggest lists them), or a smaller whisper model: parlour models fetch --whisper ggml-base.en.bin, then parlour service install to rewrite the whisper service. It loads the first model file in ~/Library/Caches/parlour/models/whisper/, and base sorts before small. |
| It asks the cloud too often, or not often enough | By default the local model decides, guided by its prompt (packages/parlour/src/core/prompt.ts). For a threshold you can tune, add the decision model. |
| It says nothing at all | Run parlour doctor, or press Check in the app. It names the part that is down. |
| You cannot interrupt it | audio.bargeIn lets the wake word cut a reply short. It is off by default, because with one box in one room the microphone hears the speaker. |
| It fires twice on one wake word | Raise wake.refractoryMs. |
| The voice is not the one you wanted | Set tts.voice and tts.speed. Kokoro downloads itself the first time Parlour starts after an install, so that first start is slow and the fallback voice may speak until it is ready. |
| It can see the lights but not the blinds | Expose the blinds. Home Assistant only publishes what is exposed under Settings > Voice assistants > Expose. |
Audio
{
"audio": {
"inputDevice": ":0",
"silenceMs": 800,
"maxUtteranceMs": 15000,
"silenceThreshold": 0.012,
"bargeIn": false
}
}inputDeviceis an avfoundation index, and":0"is the default microphone.ffmpeg -f avfoundation -list_devices true -i ""prints the list, andparlour initshows it to you when it asks.silenceMsis how much quiet ends a request once you have started talking.silenceThresholdis the RMS level, from 0 to 1, below which a frame counts as silence. Raise it for a noisy room. Lower it for a quiet speaker.maxUtteranceMscaps a single request. It guards against a microphone that never goes quiet. It is not a way to allow long questions.
The wake word
{ "wake": { "provider": "openwakeword", "words": ["hey_jarvis"], "threshold": 0.5, "refractoryMs": 1500 } }The stock words are hey_jarvis, alexa and hey_mycroft.
parlour models fetch --wake hey_jarvis,alexa fetches the ones you name into
~/Library/Caches/parlour/models/openwakeword/. Any openWakeWord model works.
Drop <word>.onnx into that directory and add <word> to words. You can
have more than one word live at once, for a small cost in CPU. To train your
own word, see Your own wake word.
The voice
{ "tts": { "provider": "kokoro", "voice": "bf_emma", "speed": 1.0, "fallback": "macos-say" } }parlour init offers four British voices: bf_emma (the default),
bf_isabella, bm_george and bm_lewis. Kokoro has more, among them
bf_alice, bf_lily, bm_daniel and bm_fable, and American voices whose
ids start af_ and am_, such as af_heart. If you name a voice Kokoro does
not have, the reply falls through to the fallback and the log lists every
voice Kokoro knows.
fallback names the voice used when the first one fails (null turns the
fallback off). macos-say ignores a Kokoro voice id and uses the system
voice. You can also give it a macOS voice name of its own ("voice": "Daniel"
when provider is macos-say).
Speech to text
whisper.cpp is the default, kept warm as a service. On macOS 26 you can use Apple's own on-device recogniser instead, through the yap command line tool: nothing to download and no server to run.
brew install yap{ "stt": { "provider": "yap", "locale": "en-GB" } }locale is optional and defaults to the Mac's own. parlour init installs
yap for you once the config names it. The whisper service stays installed
until you remove it with parlour service uninstall. If launchd rather than
the app runs the agent, follow it with parlour service install, which puts
back only what the config still wants. parlour doctor checks yap is found.
On Apple silicon there is also NVIDIA's Parakeet, through parakeet-mlx. It is fast and accurate for English and uses about 2 GB of memory.
pip install parakeet-mlx{ "stt": { "provider": "parakeet-mlx" } }Parlour keeps the model loaded in a Python worker that starts on the first
utterance, so that one waits for the load (and, the first time, the download
into the Hugging Face cache). model defaults to
mlx-community/parakeet-tdt-0.6b-v2; mlx-community/parakeet-tdt-0.6b-v3
trades a little English for about 25 languages. python defaults to
python3; point it at a venv's own Python if parakeet-mlx lives there, for
example ~/.local/share/uv/tools/parakeet-mlx/bin/python after
uv tool install parakeet-mlx. parlour doctor checks that Python can import
it and that ffmpeg is found.
The models
This is what parlour init writes when you let Parlour run the local model,
on a Mac with 16 GB, with the defaults filled in:
{
"llm": {
"local": { "provider": "openai-compatible", "managed": true, "baseUrl": "http://127.0.0.1:8920/v1", "model": "qwen3.5-9b", "temperature": 0.3, "timeoutMs": 30000 },
"cloud": { "provider": "anthropic", "enabled": true, "model": "claude-opus-5", "maxTokens": 1024, "onLocalFailure": true },
"maxToolRounds": 6
}
}The catalogue Parlour can run, and how much memory it suggests each for:
| Model id | Download | Suggested from |
|---|---|---|
qwen3.5-4b | 2.7 GB | 8 GB |
qwen3.5-9b | 5.7 GB | 16 GB |
gemma-4-26b-a4b | 17 GB | 32 GB |
qwen3.6-35b-a3b | 20.4 GB | 64 GB |
An Intel Mac runs the model on the CPU, so it is counted as having half its memory and is suggested a smaller model.
local.managedmeans Parlour runs the model itself: llama.cpp from Homebrew and a GGUF from its own catalogue, kept warm by launchd on port 8920, just like whisper. At a terminal,initoffers this first and suggests the largest model in the catalogue above that the Mac can hold.parlour models suggestprints the same list with this Mac's pick marked. To switch, runinitagain and pick another: it downloads the file and points the service at it. By hand, that isparlour models fetch --llm <id>,local.modelset to the id, andparlour service install. For a script,parlour init --local-model auto|none|<id>answers the question without a terminal.- Without
managed, Parlour talks to whatever serverbaseUrlnames and starts none of its own. The provider's defaults are LM Studio's:http://127.0.0.1:1234/v1andqwen3-8b-mlx. That is also whatinit --yes, orinitwith no terminal, writes on a fresh setup: a download of gigabytes is said yes to, never assumed. local.modelis the model id exactly as the server reports it.parlour doctorlists what is being served next to what is configured. A mismatch there is common after loading a different model in LM Studio. A managed server answers to the catalogue id, so the two only differ when the named model was never fetched.local.timeoutMsapplies to each completion. A model that often runs past it is too big for the machine. Withcloud.onLocalFailureon, every timeout becomes a cloud answer, which is slower and not private.cloud.enabled: false, or noANTHROPIC_API_KEY, keeps everything local. The escalation tool is not offered, and the persona stops telling the model to hand over. The local model answers everything itself, with the house tools still available, including questions it probably should not.maxToolRoundsstops a model that keeps calling tools. Six is enough for "turn the kitchen and hall lights off and set a timer".- A hosted OpenAI-compatible endpoint also works as the local model. Add
"apiKeyEnv": "SOME_NAME"naming a variable insecrets.env.
Through the AI SDK instead
You can fill either slot with "provider": "ai-sdk". It reaches the same two
back ends through Vercel's AI SDK instead of Parlour's own clients:
{
"llm": {
"local": { "provider": "ai-sdk", "backend": "openai-compatible", "baseUrl": "http://127.0.0.1:1234/v1", "model": "qwen3-8b-mlx" },
"cloud": { "provider": "ai-sdk", "backend": "anthropic", "model": "claude-opus-5", "effort": "low", "webSearch": true }
}
}The loop does not move. Tools are handed to the SDK without an execute,
so the SDK returns the calls instead of running them. The tool rounds, the
escalation and the request deadline all stay in Parlour. The only change is
who keeps up with the two APIs.
Parlour only runs its own model server for the openai-compatible provider,
so an ai-sdk local slot needs a server of its own, such as LM Studio.
The built-in clients remain the default, because they need nothing but
fetch. Choose the AI SDK if you would rather track one library than two
APIs, or want to put the SDK's own middleware in front of a model.
Decision model (optional)
{
"llm": {
"decision": {
"provider": "laya-mlx",
"mode": "shadow",
"escalateThreshold": 0.85,
"localConfidence": 0.75,
"model": "aac6fef/laya-mlx"
}
}
}Laya MLX scores each task on your
Mac and decides whether it needs the cloud. It runs after triage has split
the request and before the local model takes its turn. It needs Apple silicon
and pip install laya-mlx. The first load downloads the checkpoint into the
Hugging Face cache.
- Start with
"mode": "shadow". The log showsneeds_cloud, the intent and the confidence, butask_the_clever_onestill decides. - Switch to
"mode": "triage"once the scores look right. Scores aboveescalateThresholdgo straight to the cloud model. A clear house or timer intent keeps escalation off, so house commands stay local. "provider": "none", the default, skips the decision model entirely.- Multilingual house? Set
"model": "aac6fef/laya-multilingual-mlx".
The pipeline
{
"pipeline": {
"concurrency": 2,
"queueDepth": 2,
"triage": "auto",
"maxTasks": 4,
"timeoutMs": 45000
}
}concurrencyis how many requests are answered at once across the whole house. Each client gets one request at a time either way, so this sets how many rooms can be answered at once, not how hard one room can push. Two suits one machine with one local model. Raise it if the model is hosted elsewhere and can take the load. The queue still keeps each room in turn.queueDepthis how many requests may wait in one client's lane. Past that, the client's oldest waiting request is dropped silently. Someone who asks twice wants the second answer.triagecontrols when a request is repaired and split before Parlour acts on it.autoreads anything compound, long or ambiguous, and lets a plain instruction straight through.alwayscosts a local round trip on every request, which is worth it in a noisy room or with a small speech model.neversends the words to the action agent exactly as heard.maxTaskscaps how many tasks one request may become. Four covers "turn the kitchen and hall lights off, set a timer and tell me the forecast".timeoutMsis the ceiling on one whole request. It is checked between tool rounds and between tasks, not mid-request, so nothing already with a model is abandoned. It stops a request that is still running long after the person has left the room.
You can change these from a client too: parlour remote pipeline set or the
iPhone app's Server tab, within tighter limits than the file allows. See
Managing the server.
Keeping the Mac responsive
The local model and whisper share the Mac with whoever is using it. On Apple silicon they also share its memory with the GPU. A model that does not fit does not fail cleanly: the whole machine slows to a crawl. So Parlour holds them back:
- A model whose weights would take more than 70% of the Mac's memory is not
started, and
parlour doctorsays so.parlour models suggestnames one that fits. - Both servers leave two cores free, and use at most six. The local model keeps one conversation's worth of context rather than several.
- Both run at a lower priority than anything you are using. launchd waits a minute before starting a crashed one again, rather than ten seconds.
- Inside the App Store app, where Parlour keeps them running itself, a server that crashes five times in ten minutes is left stopped until you start it from a client.
Search
{ "search": { "provider": "searxng", "url": "http://searxng.local:8080", "maxResults": 5 } }Only the local model uses this. The cloud model brings its own search.
SearXNG needs - json under search.formats in its settings.yml.
For a hosted alternative, use "provider": "brave" with BRAVE_API_KEY in
secrets.env. "provider": "none" leaves the local model without a search
tool.
Logging
Set LOG_LEVEL in secrets.env (or the environment) to debug, info,
warn or error. debug prints every transcription time and every tool call
with its arguments. Use it when you want to know why the model did what it
did. Service logs go to ~/Library/Logs/parlour/agent.log, whisper.log
and, when Parlour runs the local model, llm.log.
parlour service logs --lines 200 prints the tail of each.