[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-80826":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":13,"subscribersCount":13,"size":13,"stars1d":13,"stars7d":13,"stars30d":14,"stars90d":13,"forks30d":13,"starsTrendScore":13,"compositeScore":13,"rankGlobal":10,"rankLanguage":10,"license":16,"archived":17,"fork":17,"defaultBranch":18,"hasWiki":19,"hasPages":17,"topics":20,"createdAt":10,"pushedAt":10,"updatedAt":28,"readmeContent":29,"aiSummary":30,"trendingCount":13,"starSnapshotCount":13,"syncStatus":31,"lastSyncTime":32,"discoverSource":33},80826,"hass-closest-intent","charludo\u002Fhass-closest-intent","charludo","Fuzzy intent matcher for HomeAssistant. Garbled STT output in, actual intent out.","",null,"Python",38,0,1,3,"GNU Affero General Public License v3.0",false,"main",true,[21,22,23,24,25,26,27],"conversation","fuzzy-matching","hacs","home-assistant","home-assistant-integration","intent","voice","2026-06-12 02:04:07","\u003Cdiv align=\"center\">\n  \u003Cimg src=\"https:\u002F\u002Fraw.githubusercontent.com\u002Fcharludo\u002Fhass-closest-intent\u002Frefs\u002Fheads\u002Fmain\u002Fcustom_components\u002Fclosest_intent\u002Fbrand\u002Flogo.png\" alt=\"hass-closest-intent\" height=\"512px\"\u002F>\n  \u003Ch1>hass-closest-intent\u003C\u002Fh1>\n\u003C\u002Fdiv>\n\n\u003Cp align=\"center\">\n\tFuzzy intent matcher for HomeAssistant.\u003Cbr\u002F>Garbled STT output in, actual intent out.\n\u003C\u002Fp>\n\n\u003Cp align=\"center\">\n\t\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fcharludo\u002Fhass-closest-intent\u002Fstargazers\">\n\t\t\u003Cimg alt=\"Stars\" src=\"https:\u002F\u002Fimg.shields.io\u002Fgithub\u002Fstars\u002Fcharludo\u002Fhass-closest-intent?style=for-the-badge&logo=starship&color=F3B562&logoColor=D9E0EE&labelColor=302D41\">\u003C\u002Fa>\n\t\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fcharludo\u002Fhass-closest-intent\u002Fissues\">\n\t\t\u003Cimg alt=\"Issues\" src=\"https:\u002F\u002Fimg.shields.io\u002Fgithub\u002Fissues\u002Fcharludo\u002Fhass-closest-intent?style=for-the-badge&logo=bilibili&color=F06060&logoColor=D9E0EE&labelColor=302D41\">\u003C\u002Fa>\n\t\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fcharludo\u002Fhass-closest-intent\">\n\t\t\u003Cimg alt=\"Size\" src=\"https:\u002F\u002Fimg.shields.io\u002Fgithub\u002Factions\u002Fworkflow\u002Fstatus\u002Fcharludo\u002Fhass-closest-intent\u002Fnix.yml?color=8CBEB2&label=TESTS&logo=githubactions&style=for-the-badge&logoColor=D9E0EE&labelColor=302D41\"\u002F>\u003C\u002Fa>\n\u003C\u002Fp>\n\n&#160;\n\n### 🌲 Problem statement and solution\n\nSpeech-To-Text (STT) output, especially fast and local STT output, is often simply *bad*.\nHomeAssistant's own [Hassil](https:\u002F\u002Fgithub.com\u002FOHF-Voice\u002Fhassil) is *incredibly* picky:\nyour STT output must match *exactly* to one of the configured intents.\n\nThere's two paths forward from this: Upgrade your hardware to support better STT, or\ntry to figure out what the speaker *probably meant* to say from the garbled output.\n\nThis project does the latter.\n\nWith this custom integration, \"Lights on in **live in room**\" will actually turn on the lights in your **living room**.\nSo will, for that matter, \"lighrts on inn livainriomm\".\n\nShort demo, first with `closest-intent`, then with bare Hassil:\n\n\u003Cimg src=\"https:\u002F\u002Fraw.githubusercontent.com\u002Fcharludo\u002Fhass-closest-intent\u002Frefs\u002Fheads\u002Fmain\u002Fcustom_components\u002Fclosest_intent\u002Fbrand\u002Fdemo.gif\" alt=\"demo gif\" width=\"360\"\u002F>\n\n&#160;\n\n### 📜 Highlights\n\n- Pattern expansion. Expanding `\u003Cexpansion_rules>`, `(alternatives|to)`, and `[optional|alternatives]` all work, including on HASS-defined lists like your home's areas and entities!\n- Slot extraction. Both for wildcard slots (like for adding something to the shopping list, where the `{item}` is a wildcard), and against slots like `{timer_hours:hours}` with a fixed set of possibilities.\n- Fuzzy slot resolution. For list-like slots and expansion rules (including your areas and entities!), fuzzy match the slot values to the available options. Allows \"livikroom\" to be corrected to \"living room\".\n- Actual intent handling still done by Hassil. `closest-intent` simply corrects your STT output or typos to the closest matching intent, and then forwards a nice, canonical sentence to Hassil, who then deals with the intent just like if you had spoken\u002Ftyped perfectly.\n- 100% LLM-free. Just uses relatively simple fuzzy matching of the input against your intents, plus some clever-ish (well... working, at least) tricks to improve the results.\n- Fallback agent support. OK, I said 100% LLM-free, but if you absolutely want to, you can use one as fallback. More on this below.\n- Is fast :) (as in: basically instant for a couple hundred configured custom intents).\n\n> **Note:** `closest-intent` is completely language-agnostic. All the examples in this `README` are in English, but you can use it with any language you like; personally, I use it in German.\n\n&#160;\n\n### 📋 Examples\n\nHere's some examples of things I said, what my STT (`wyoming-faster-whisper-base`) understood, what HomeAssistant was able to do\u002Fanswer after passing the STT output through `closest-intent`, and what the same STT output would have resulted in with just bare Hassil.\n\n> **Note:** These are actual results I got when speaking the \"what was said\" sentences in my phone.\n> I'm a native German speaker, and so I do have an accent, but this pretty closely matches my experience when using the German-language version of whisper.\n> The \"bare Hassil\" responses are what I got after 1:1 pasting the STT output into the voice assist chat window with `closest-intent` disabled.\n\n| what was said | STT output | with Closest Intent | bare Hassil |\n| --- | --- | --- | --- |\n| `start cleaning` | `Star cleaning.` | ✅ Cleaning started. | ❌ Sorry, I couldn't understand that |\n| `stop cleaning` | `Stop clenching!` | ✅ Cleaning stopped. | ❌ Sorry, I am not aware of any device called clenching |\n| `vacuum the living room` | `Vacuum Believing Room` | ✅ Cleaning the living room. | ❌ Sorry, I am unaware of any floor called Believing Room |\n| `clean the office` | `King the Office` | ✅ Cleaning the office. | ❌ Sorry, there are multiple devices called Office *(author's note: no there aren't, wtf?)* |\n| `vacuum the kitchen` | `Back here in the kitchen.` | ✅ Cleaning the kitchen. | ❌ Sorry, I couldn't understand that |\n| `how warm is it in the bedroom` | `Our all is in the best room.` | ✅ In the bedroom, the temperature is currently.... | ❌ Sorry, I am not aware of any area called best room |\n| `add milk to the shopping list` | `Add milk to the chauvinist.` | ✅ \"milk\" added. | ❌ Sorry, I am not aware of any device called chauvinist |\n| `put call dentist on my todo list` | `put call dentist on my tudu list` | ✅ \"call dentist\" added. | ❌ Sorry, I am not aware of any device called tudu |\n| `turn on the water pump` | `turn on the what her pump` | ✅ Turned on the water pump. | ❌ Sorry, I am not aware of any device called what her pump |\n| `play some music` | `Place on music` | ✅ Playing music. | ❌ Sorry, I am not aware of any area called music |\n| `resume the music` | `Renew Music` | ✅ Resuming. | ❌ Sorry, I couldn't understand that |\n| `pause the music` | `Post music` | ✅ Paused. | ❌ Sorry, I couldn't understand that |\n| `next track` | `next rack` | ✅ Next track. | ❌ Sorry, I am not aware of any device called rack |\n| `enable shuffle` | `an able shuffling` | ✅ Shuffle enabled. | ❌ Sorry, I couldn't understand that |\n| `disable shuffle` | `Disable to schaffen.` | ✅ Shuffle disabled. | ❌ Sorry, I am not aware of any device called Disable |\n| `restart the player` | `Reset the plan.` | ✅ Restarting the player. | ❌ Sorry, I am not aware of any area called Reset |\n| `play a random album` | `Player random album` | ✅ Playing a random album. | ❌ Sorry, I couldn't understand that |\n| `play a random artist` | `Player and Immartist.` | ✅ Playing a random artist. | ❌ Sorry, I couldn't understand that |\n| `play the latest tracks` | `Plan the ladder tracks.` | ✅ Playing recently added tracks. | ❌ Sorry, I am not aware of any area called Plan |\n| `play recently played songs` | `Player recently played so...` | ✅ Playing recently heard tracks. | ❌ Sorry, I couldn't understand that |\n| `play playlist NieR` | `Play playlist NEAR!` | ✅ Playing the playlist NieR. | ❌ Sorry, I couldn't understand that |\n| `play my daily briefing` | `and play my daily breathing` | ✅ Here is your daily briefing: ... | ❌ Sorry, I am not aware of any area called and play |\n| `what time is it` | `What the hell is it?` | ✅ It is 16:36. | ✅ It is 16:36. *(author's note: okay, know what? earned. did not expect that.)* |\n| `what day is it today` | `One day is today.` | ✅ Today is Friday. | ✅\u002F❌ May 8th, 2026 *(author's note: that's the output for \"What **date** is it?\", but, eh, close enough)* |\n| `make the tv brighter` | `Make that CV brighter.` | ✅ Screen is now bright. | ❌ Sorry, I couldn't understand that |\n| `set the screen darker` | `The screen doctor.` | ✅ Screen is now dark. | ❌ Sorry, I am not aware of any device called screen doctor |\n| `what's the weather today` | `What's the matter with you?` | ✅ Today, the weather is... | ❌ It is 16:36. *(author's note: wait, WHAT?)* |\n| `how's the weather tomorrow morning` | `How's the better tomorrow?` | ✅ Tomorrow morning, it will be... | ❌ Sorry, I am not aware of any area called How's |\n| `what's the weather this week` | `What's the matter this weak` | ✅ Monday:..., Tuesday:..., | ❌ It is 16:36. *(author's note: sigh...)* |\n| `how's the weather at 5 o'clock` | `cast the red there at 5 o'clock` | ✅ At 5 o'clock, it will be... | ❌ Sorry, I am not aware of any area called cast |\n| `how windy is it right now` | `how windy is IR low` | ✅ The wind is currently blowing with... | ❌ No timers. |\n| `how windy will it be tonight` | `How will you be tonight?` | ✅ Tonight, the wind speed will be around... | ❌ Sorry, I couldn't understand that |\n| `how hot will it get today` | `How hard will it get today?` | ✅ Today, temperatures will reach up to... | ❌ Sorry, I couldn't understand that |\n| `will it rain today` | `with it right today` | ✅ No rain is expected today. | ❌ Sorry, I couldn't understand that |\n\n...you get the idea.\n\n&#160;\n\n### 💡 How it works\n\n`closest-intent` is registered in HomeAssistant as a conversation agent.\nOn startup, it parses (by default) all user-defined intents (or optionally, also the builtins ones). In this process, it also expands all rules, like `\u003Cexpansion_rule>`, `(alternatives|to)`, and `[optionals]`, and notes where `{slots}` are located, and whether they are wildcards or belong to some list (like areas, entities, or the numbers 1-100).\n\nWhen a user request comes in (via voice command or the chat box), `closest-intent` fuzzy-matches that request against those expanded rules.\nIf the rule does not contain a slot, it is picked immediately.\nIf it does contain a slot, `closest-intent` performs a sequence of fancy magic steps to find the best-fitting slot value among a range of possible positions within the top-scoring matched sentences.\nIn practice, this often means \"smallest slot-value on a word-boundary\", but the extraction is not limited to that.\n\nWith the best match found, we then reconstruct the \"canonical form\", i.e. a sentence that Hassil will actually understand.\nIf in your configured intents, \"Play some music.\" exists, and `closest-intent` got \"Place on music\" and matched that to the intent,\nit will simply forward \"Play some music.\" to Hassil. If the intent contained a slot, the extracted value will be substituted.\n\nThis guarantees that the sentence passed to Hassil will actually be understood, and allows us to not have to worry at all about performing actions, running scripts,...\n\nIf *no* matching intent could be found, we pass the exact input we got to the configured fallback agent.\nBy default, that is simply Hassil (which again allows us to be lazy and not worry about proper error responses), or another agent, like a LLM.\n\nThe \"happy path\" compared between with and without `closest-intent` thus looks something like this:\n\n```mermaid\nflowchart TB\n    subgraph Right[\" \"]\n        direction TB\n        L1[\"\u003Ci>“lights on in living room”\u003C\u002Fi>\"]\n        L2[\"\u003Ci>“likeson in live in room”\u003C\u002Fi>\"]\n        L3[\"\u003Ci>n\u002Fa\u003Ci>\"]\n        L4[\"\u003Ci>match: false\u003C\u002Fi>\"]\n        L5[\"\u003Ci>“Sorry, I couldn't understand that”\u003C\u002Fi>\"]\n        L1 -.-> L2\n        L2 -.-> L3\n        L3 -.-> L4\n        L4 -.-> L5\n    end\n\n    subgraph Mid[\" \"]\n        direction TB\n        User[You]\n        STT[STT]\n        CI[Closest Intent]\n        Hassil[Hassil]\n        HA[HomeAssistant]\n        User --> STT\n        STT --> CI\n        CI --> Hassil\n        Hassil --> HA\n    end\n\n    subgraph Left[\" \"]\n        direction TB\n        R1[\"\u003Ci>“lights on in living room”\u003C\u002Fi>\"]\n        R2[\"\u003Ci>“likeson in live in room”\u003C\u002Fi>\"]\n        R3[\"\u003Ci>“Lights on in living room.”\u003C\u002Fi>\"]\n        R4[\"\u003Ci>match: true\u003C\u002Fi>\"]\n        R5[\"\u003Ci>“Turned on the lights in living room.”\u003C\u002Fi>\"]\n        R1 ==> R2\n        R2 ==> R3\n        R3 ==> R4\n        R4 ==> R5\n    end\n\n    classDef out fill:none,stroke:none,color:#555\n    class L1,L2,L3,L4,L5,R1,R2,R3,R4,R5 out\n    classDef pane fill:none,stroke:none\n    class Left,Mid,Right pane\n```\n\n&#160;\n\n### ⚠️ Limitations\n\nThere's two major limitations to consider.\n\n1. False-positives are a real possibility.\n   This is actually an error class that Hassil usually almost completely eliminates:\n   since the input needs to match so perfectly to the configured intents,\n   there's almost no way to trigger an intent unintentionally.\n   **This issue gets worse the more similar-looking intents you have configured.**\n   If you are experiencing issues with this, raise the configured `threshold` (see below).\n1. Not entirely unrelated: by default, the builtin intents are disabled for matching in `closest-intent`. HomeAssistant configures a good amount of intents by default, and does so with a shitton of expansion rules to cover more possible commands.\n   Expanding all those intents is simply not viable (see also: \"combinatorial explosion\").\n   This is why we expand to a maximum depth of 32 expansions even when you enable builtin intents in `closest intent`.\n   In general though, these expansions are not super useful either, since they often cover things like `(How is|How's)`, which just do not matter much with our fuzzy matcher.\n\nIn consequence, I highly recommend configuring your own intents for your personal usecases, if you haven't already.\nThe best and most flexible way is through [`custom_sentences`](https:\u002F\u002Fwww.home-assistant.io\u002Fvoice_control\u002Fcustom_sentences_yaml\u002F).\n\nHowever, please feel free to try enabling the built-in intents first - it may just work for your usecase!\n\n&#160;\n\n### 📦 Installation\n\n#### Via HACS, officially [WIP]\n\n> ⚠️ `custom-intents` is not yet in the official HACS repos. I will add this section once it's in.\n\n#### Via HACS, custom repository\n\n1. Open HACS, click the three-dot menu (top right) -> **Custom repositories**.\n1. Paste `https:\u002F\u002Fgithub.com\u002Fcharludo\u002Fhass-closest-intent`, set **Type** to *Integration*, click **Add**.\n1. Find **Closest Intent** in the HACS list, click **Download**.\n1. Restart HomeAssistant.\n1. Search for **Closest Intent** in the HACS search bar, then install.\n1. Follow the config flow.\n\n&#160;\n\n### ⚙️ Configuration\n\nThe integration can be set up entirely in the UI (**Settings** -> **Devices & services** -> **Add integration** -> **Closest Intent**) or via `configuration.yaml`. Both paths accept the same options. UI options override YAML on a per-key basis, clearing them in the UI falls back to YAML.\n\n#### Options\n\n| Option | Default | Meaning |\n| ------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `threshold` | `70` | Minimum fuzzy-match score (0–100) for a candidate to be considered. Higher = stricter. Below the threshold, the original text is forwarded unchanged to the `fallback_agent`. |\n| `slot_threshold` | `threshold` | Minimum fuzzy-match score (0–100) for resolving a captured slot value against the slot's known list values. Higher = stricter, lower = more aggressive correction. Useful when intents usually match, but e.g. entity names are frequently misunderstood. Defaults to `threshold` is. |\n| `expansion_cap` | `16` | Maximum number of surface forms generated per pattern. Bounds the alternation\u002Foptional explosion. `0` disables expansion entirely (first branch only). Builtin intent expansion is always capped at `32`.|\n| `denylist` | `[]` | Intent names to exclude from matching. Useful when a built-in or imported intent collides with your own. |\n| `include_builtins` | `false` | Also fuzzy-match against HomeAssistant's built-in intents (`HassTurnOn` etc.). Off by default, see section on limitations. |\n| `slot_extraction` | `true` | Extract slot values from the user's speech and substitute them into the canonical sentence. Disable to make the integration only correct slot-less phrases. (Why would you do this though, this is the best part!)|\n| `fallback_agent` | `conversation.home_assistant` | Agent consulted if no match for the canonical sentence is found. Default is Hassil itself, i.e. \"no fallback\". Set to an LLM agent if you want one. |\n\n#### YAML configuration\n\n```yaml\nclosest_intent:\n  threshold: 70\n  slot_threshold: 70\n  expansion_cap: 16\n  denylist: []\n  include_builtins: false\n  slot_extraction: true\n  fallback_agent: conversation.home_assistant\n```\n\n&#160;\n\n### 🐙 Recommended setup\n\nHere's some recommended setup ideas for different usecases. Mix and match :)\n\n#### You have already configured your own custom intents for pretty much everything.\n\nJust use the default config. The `threshold` should be fine, the builtins are not required.\nOnly thing to check is if you can get rid of some of your expansion rules that fall squarely within the realm of the fuzzy-matchable.\n\n#### You already have good results with bare Hassil on your STT.\n\nConsider enabling **Prefer handling commands locally** in the HomeAssistant voice pipeline settings.\nThe speed boost is minimal for mortal amounts of intents, but faster is faster 🤷🏼‍♀\n\n#### You have an LLM set up as fallback for Hassil.\n\nConsider enabling **Prefer handling commands locally** in the HomeAssistant voice pipeline settings,\nbut setting the **Conversation agent** to `closest intent`. In the `closest-intent` setting, choose your LLM as the `fallback_agent`.\n\nThe pipeline then becomes: Hassil -> success or `closest-intent` -> success or LLM -> success or failure.\n\nThe result is that usually, your commands will be handled by Hassil\u002F`closest-intent`, and only rarely is a fallback to a much, much slower LLM required. In other words, **you benefit from this integration even if you are using an LLM for intent recognition**!\n\n&#160;\n\n### 🔍 Diagnostics\n\n`closest-intent` provides two diagnostics tools, `parse_sentence` and `dump_candidates`. Both can be called from **Settings** -> **Developer tools** -> **Actions**, then search for `Closest Intent`. They also work from automations, scripts, and `hass.services.async_call`.\n\n#### `closest_intent.parse_sentence`\n\nRuns one sentence through the matcher and returns a structured response, including the matched intent, the candidate pattern that won, its score, the captured slots, the canonical sentence that (would have been) forwarded to Hassil.\nOptionally, you can actually forward it to Hassil to see what action this would trigger. (The action is not actually triggered though.)\n\nUseful when a sentence unexpectedly doesn't match, slot capture is wrong, and so on.\n\n#### `closest_intent.dump_candidates`\n\nDebug-logs and returns the full per-language state. This includes every expanded candidate, every expansion rule and its surface forms, every slot list and its values.\n\nUseful for \"fuck why is this STILL not working\".\n\n#### Filing an issue\n\nPlease use these tools before filing an issue. Often this already helps solve the problem.\nHowever, if you believe that you did indeed find a bug\n(for example, a wrong slot capture, or no match where one should have been found),\n**please do file an issue with the output from `parse_sentence`**!\n\nI will try to reproduce the issue, add a regression test, and then hopefully fix this.\nIn my humble opinion the project is already in a *very* useful state, but things can always be better, and examples of things going wrong are super useful for this!\n\nIf the issue is \"my intent isn't in the pool \u002F lists look wrong\", also attach `closest_intent.dump_candidates` for the affected language.\nThis is enough for almost every reproducer without needing your full HomeAssistant config.\n\n&#160;\n\n\u003Ca href=\"https:\u002F\u002Fwww.buymeacoffee.com\u002Fcharludo\" target=\"_blank\">\u003Cimg src=\"https:\u002F\u002Fcdn.buymeacoffee.com\u002Fbuttons\u002Fv2\u002Fdefault-violet.png\" alt=\"Buy Me a Coffee\" style=\"height: 60px !important;width: 217px !important;\" >\u003C\u002Fa>\n\n&#160;\n","hass-closest-intent 是一个为 HomeAssistant 设计的模糊意图匹配器，能够将混乱的语音转文字（STT）输出转换为实际意图。其核心功能包括模式扩展、槽位提取和模糊槽位解析，支持对用户定义的区域和实体进行模糊匹配，使得诸如“lighrts on inn livainriomm”这样的输入能够被正确理解为“living room lights on”。该项目特别适用于那些由于硬件限制或快速本地STT服务导致语音识别不准确的家庭自动化场景中，通过修正用户的输入错误来提高HomeAssistant响应命令的准确性。完全基于Python编写，并且不依赖于大型语言模型，确保了处理过程的简洁高效。",2,"2026-06-11 04:02:29","CREATED_QUERY"]