| --- |
| license: mit |
| language: |
| - fa |
| - en |
| tags: |
| - embedded |
| - esp32 |
| - iot |
| - constrained-decoding |
| - intent-parsing |
| - text-generation |
| - persian |
| - edge-ai |
| - tinyml |
| pipeline_tag: text-generation |
| inference: false |
| --- |
| |
|  |
|
|
| # llmOS — intent compiler |
|
|
| **A 2.85M-parameter language model that runs on an ESP32-S3 microcontroller and |
| turns a spoken sentence into a hardware automation rule.** |
|
|
| Say *"if the sensor on pin 1 goes high, turn off pin 2"* and the board does that |
| from then on. No code, no recompiling, no internet, no cloud account. The model |
| runs on the chip itself, from a microSD card. |
|
|
| | | | |
| |---|---| |
| | **Firmware + installer** | [github.com/AliAkrami1375/llmos-firmware](https://github.com/AliAkrami1375/llmos-firmware) | |
| | **Try it in the browser** | [the Space](https://huggingface.co/spaces/DibaAi/llmOS) | |
| | **Languages** | Persian (primary) and English | |
| | **Hardware** | ESP32-S3 with 8 MB PSRAM | |
|
|
| [**فارسی — پایین صفحه**](#فارسی) |
|
|
| --- |
|
|
| ## What it does |
|
|
| | You say | What it produces | |
| |---|---| |
| | if pin 1 goes high, turn off pin 2 | `ON GPIO1=1 DO GPIO2=0` | |
| | toggle pin 5 every 30 seconds | `ON EVERY 30s DO GPIO5=T` | |
| | when the light sensor on pin 4 drops below 800, turn on pin 7 | `ON ADC4<800 DO GPIO7=1` | |
| | at 07:00 turn on pin 9 for 30 seconds | `ON AT 07:00 DO GPIO9=1; WAIT 30s; GPIO9=0` | |
| | turn on pin 16 now | `DO GPIO16=1` | |
| | connect to wifi MyHome with password 1a2b3c | `CALL wifi.connect "MyHome" "1a2b3c"` | |
| | read pin 5 | `CALL pin.read "5"` | |
| | hello, how are you? | `ERR` — correctly refused | |
|
|
| The output is not free text. It is a small formal language a deterministic rule |
| engine executes; the model's only job is the translation. |
|
|
| ### Persian is the primary language |
|
|
| The training distribution is mostly Persian, and the gap is not cosmetic. Two |
| capabilities are reliable in Persian and are not reached in English: |
|
|
| | Persian | Result | |
| |---|---| |
| | پایه ۷ رو بذار اتاق علی | `NAME GPIO7 "اتاق علی"` | |
| | اتاق علی رو روشن کن | `DO GPIO7=1` | |
| | ساعت ۳ بامداد بازر پایه ۹ را ۳۰ ثانیه بزن | `ON AT 03:00 DO GPIO9=1; WAIT 30s; GPIO9=0` | |
|
|
| Pin **naming** is Persian-only in practice, and so are clock times written as |
| words. In English, give the time in figures — `at 07:00`, not *"at 7am"*, which |
| is read unreliably. Everything in the first table works in both languages. |
|
|
| --- |
|
|
| ## Why invalid output is impossible, not merely unlikely |
|
|
| This is the part worth reading even if you never touch an ESP32. |
|
|
| A 2.85M-parameter model left to itself produces nonsense some of the time. This |
| one cannot, because **the grammar is enforced during decoding rather than |
| learned**. |
|
|
| The rule language is compiled into a prefix automaton. At every generation step |
| each candidate token is tested: *would accepting this token leave the output a |
| valid prefix of some complete rule?* Tokens that fail are removed before the |
| sample. The end-of-output token only becomes reachable once the text is a |
| complete, valid rule. |
|
|
| The same test enforces **numeric range**, not just syntax. `GPIO99` is |
| unreachable because 99 is not an allowed pin; `ADC4<9999` is unreachable |
| because the ADC is 12-bit. Pins that carry flash, PSRAM or the SD card are |
| excluded from the automaton entirely, so no sentence — however phrased — can |
| produce a rule that unmounts the card the model is running from. |
|
|
| ### Quoted values are copied, not spelled |
|
|
| A model this small cannot spell a random WiFi password, and it never has to. |
|
|
| While the decoder is inside a quoted argument it stops asking *"is this valid |
| syntax?"* and starts asking **"does this keep the argument a verbatim substring |
| of what the user typed?"** Only tokens preserving that property survive. The |
| model never spells anything — it decides **where to start copying and where to |
| stop**, which is what attention is naturally good at. |
|
|
| A password full of strange characters therefore comes out byte-exact by |
| construction rather than by luck. Range checking deliberately does *not* look |
| inside quotes: a network named `GPIO99 ADC9999` is a legal SSID and is never |
| read as a pin. |
|
|
| --- |
|
|
| ## Accuracy |
|
|
| Measured on 5,000 held-out requests, through the same C engine that runs on the |
| board, against these exact quantized weights: |
|
|
| ``` |
| exact match : 99.92% 4 wrong out of 5,000 |
| syntactically ok : 100.00% |
| parses + in range: 100.00% |
| mean confidence : 1.000 |
| ``` |
|
|
| Broken down by intent class, because one number hides which half of the |
| behaviour is weak: |
|
|
| | Intent | n | Accuracy | |
| |---|---|---| |
| | digital rule | 1,496 | 99.9% | |
| | immediate action | 530 | 100% | |
| | analog threshold | 828 | 100% | |
| | periodic | 611 | 100% | |
| | scheduled (`at 3am`) | 580 | 99.8% | |
| | pin naming | 527 | 99.8% | |
| | tool call | 317 | 99.7% | |
| | out of domain | 111 | 100% | |
|
|
| The last row matters as much as the first: the model recognises when a request |
| is *not* an automation rule and says so, instead of forcing it into one. |
|
|
| The two 100% rows above are **by construction, not by training** — see the |
| previous section. |
|
|
| --- |
|
|
| ## Architecture |
|
|
| | | | |
| |---|---| |
| | Type | decoder-only transformer, Llama-style | |
| | Parameters | 2,853,312 | |
| | Dimension / layers / heads | 192 / 6 / 4 (4 KV heads) | |
| | Feed-forward | SwiGLU, hidden 512 | |
| | Positional | RoPE | |
| | Normalisation | RMSNorm | |
| | Context | 128 tokens | |
| | Vocabulary | 1,024 | |
| | Quantization | int8 symmetric, group size 32 | |
| | On disk | 3,142 KB | |
| | Working memory on device | 1,166 KB (KV cache + buffers) | |
|
|
| ### Tokenizer |
|
|
| Byte-level BPE, 1,024 tokens, with **atomic digits** — no digit-digit merge |
| exists in the vocabulary. |
|
|
| That last detail was not cosmetic. An earlier tokenizer split `500` as |
| `['500']`, `800` as `['8','00']` and `4095` as `['40','9','5']`. The model had |
| to learn a different spelling strategy per number, and scheduled rules sat at |
| 89.2% accuracy. Making digits atomic moved that class to 99.8% with no |
| architecture change. |
|
|
| Tokenization is greedy longest-match, implemented identically in C and Python |
| and verified byte-for-byte over 740,000 strings — because a tokenizer that |
| disagrees with itself across the training/inference boundary produces a model |
| that looks trained and behaves broken. |
|
|
| --- |
|
|
| ## Files |
|
|
| | File | What it is | |
| |---|---| |
| | `model.llm` | the quantized weights, custom binary format | |
| | `tokenizer.bin` | the vocabulary — **must match the model** | |
| | `tools.json` | the shipped HTTP tool definitions, as reference | |
| | `LICENSE` | MIT | |
|
|
| > `model.llm` and `tokenizer.bin` are a **matched pair**. Replacing one without |
| > the other gives confident nonsense rather than an error. Always take both |
| > from the same revision. |
|
|
| ### This is not a `transformers` checkpoint |
|
|
| `model.llm` is a flat int8 format read by a C99 inference engine with no |
| dependencies, designed to be `mmap`-friendly on a microcontroller. There is no |
| `config.json`, no `safetensors`, and `AutoModel.from_pretrained` will not work. |
|
|
| To run it you either flash the firmware, or use the Space, which runs that same |
| C engine on a CPU. |
|
|
| --- |
|
|
| ## How to run it |
|
|
| ### On hardware |
|
|
| ```bash |
| git clone https://github.com/AliAkrami1375/llmos-firmware.git |
| cd llmos-firmware |
| ./install.sh # Linux, macOS |
| ``` |
|
|
| ```powershell |
| powershell -ExecutionPolicy Bypass -File install.ps1 # Windows |
| ``` |
|
|
| The installer finds the board, flashes it and copies the model to a FAT32 |
| microSD card. You need an ESP32-S3 with **8 MB PSRAM** (N8R8 or N16R8) and a |
| USB **data** cable. |
|
|
| ### In a browser |
|
|
| [**huggingface.co/spaces/DibaAi/llmOS**](https://huggingface.co/spaces/DibaAi/llmOS) |
| — the same C engine, the same weights, on a CPU. |
|
|
| --- |
|
|
| ## The rule language |
|
|
| ``` |
| rule := "ON " trigger " DO " actions |
| | "DO " actions |
| | "CALL " tool arg* |
| | "NAME GPIO" pin " " label |
| | "ERR" |
| |
| trigger := "GPIO" pin "=" ("0"|"1") |
| | "ADC" pin ("<"|">") value |
| | "EVERY " n unit |
| | "AT " hh ":" mm |
| |
| action := "GPIO" pin "=" ("0"|"1"|"T") T = toggle |
| | "WAIT " n unit |
| | "CALL " tool arg* |
| |
| unit := "ms" | "s" | "m" | "h" |
| ``` |
|
|
| Allowed pins: **1–9, 14–18, 21, 38–44, 47, 48**. ADC: **1–9**, threshold |
| 0–4095. Durations up to 24 hours. Everything else is unreachable in the |
| automaton, not merely rejected afterwards. |
|
|
| --- |
|
|
| ## Tool calls and notifications |
|
|
| Rules control pins. A **tool** is something the device does that is not a pin |
| rule — joining a WiFi network, reading an API, and most usefully **telling you |
| when something happened**. |
|
|
| `CALL` is an action like any other, so a rule may contain one. These are valid, |
| and the parser and engine both have tests for them: |
|
|
| ``` |
| ON GPIO7=1 DO CALL notify.send "door opened" |
| ON ADC3>2400 DO GPIO5=1; CALL notify.send "too hot" |
| ON AT 07:00 DO CALL notify.send "good morning" |
| ``` |
|
|
| > **The model will not write those lines for you.** It only ever emits the five |
| > tool names it was trained on — `wifi.connect`, `wifi.scan`, `wifi.status`, |
| > `sys.info`, `pin.read`. `notify.send` is a *user* tool, defined in a file on |
| > the SD card long after training, so no sentence produces it. Ask *"if pin 7 |
| > goes high, notify me"* and you get a plain pin rule or `ERR`. |
| > |
| > To get a rule like the ones above onto the device, write the line into |
| > `rules.txt` on the card; it is parsed and stored at boot. |
|
|
| ### `notify.send` |
|
|
| Every other tool answers a question you asked. `notify.send` is the one that |
| speaks **without being asked** — the one that tells you the door opened at 3 a.m. |
| while you were asleep. |
|
|
| It posts to a webhook on **your own network**, so it needs no internet, no TLS, |
| no API key and no third party that can be blocked or shut down: |
|
|
| ```json |
| { |
| "name": "notify.send", |
| "kind": "http", |
| "parameters": [{ "name": "text", "required": true }], |
| "phrases": ["پیام بده", "خبرم کن", "اطلاع بده", "notify me"], |
| "execute": { |
| "method": "POST", |
| "url": "http://192.168.1.50:8123/api/webhook/llmos", |
| "body": "{\"message\":\"{text}\"}", |
| "timeout_ms": 5000 |
| }, |
| "reply": { "fa": "پیام فرستاده شد.", "on_error": "پیام فرستاده نشد." } |
| } |
| ``` |
|
|
| Point the URL at Home Assistant, Node-RED, or a five-line Python receiver. |
| Until you do, it correctly reports that the message was not sent — nothing is |
| listening at the placeholder address. |
|
|
| There are exactly two ways to reach it: |
|
|
| | What you want | How | |
| |---|---| |
| | A message **now** | say *"خبرم کن …"* / *"notify me …"* — the phrase router matches it, and whatever follows the phrase becomes the message | |
| | A message **when something happens** | write `ON GPIO7=1 DO CALL notify.send "door opened"` into `rules.txt` on the card | |
|
|
| **A rule containing a `CALL` fires on the edge.** One message on the low→high |
| transition, not one every 20 ms tick while the pin stays high; a 30 ms debounce |
| means a bouncing contact still counts as one event. That is the difference |
| between a useful notification and three thousand messages. |
|
|
| **A failed call does not stop the rule.** The remaining actions still run — a |
| dead webhook must not stop the light from switching on. |
|
|
| ### The two routes into a tool |
|
|
| ``` |
| your sentence |
| │ |
| ├─► phrase router ──── matched a tool's trigger phrase? ──► run it now |
| │ (typo-tolerant) │ no |
| │ ▼ |
| └───────────────────────────► the model ──► ON … DO … │ CALL … │ ERR |
| |
| rules.txt on the card ────────────► parsed at boot ──► stored as a rule |
| ``` |
|
|
| The router normalises Persian text (ی/ي, ک/ك, zero-width non-joiners, Persian, |
| Arabic and Latin digits, diacritics) and tolerates typos. It runs the tool |
| immediately rather than creating a rule. |
|
|
| > **Adding a tool to `tools.json` teaches the model nothing.** The model was |
| > trained on a fixed set of tool names; a tool added afterwards is reached by |
| > its own trigger phrases, not by the model writing `CALL yourtool`. This is |
| > not a bug — it is what a closed vocabulary means. It is also exactly why |
| > every tool carries its own phrases, and why the third route above exists. |
|
|
| Seven HTTP tools ship in `tools.json` — notifications, weather, air quality, |
| sunrise/sunset, time, earthquakes, connectivity. Every URL was actually called |
| and every extraction path checked against the real response. |
|
|
| Five more are **compiled into the firmware and cannot be deleted** — |
| `wifi.scan`, `wifi.connect`, `wifi.status`, `sys.info`, `pin.read`. Not because |
| of a flag in the file, but because they are not in the file at all; they are |
| rebuilt from a table at every boot. Pull the SD card out and `wifi.connect` is |
| still there. |
|
|
| **→ [Full tool and notification documentation](https://github.com/AliAkrami1375/llmos-firmware/blob/main/docs/TOOLS.md)** |
| ([Persian](https://github.com/AliAkrami1375/llmos-firmware/blob/main/docs/TOOLS.fa.md)) |
|
|
| --- |
|
|
| ## Pin names |
|
|
| You can call pin 7 *"Ali's room"* and then say *"turn on Ali's room"*. |
|
|
| **The model never learns your names.** They are created long after training. |
| A name is substituted with its pin number *before* the sentence reaches the |
| model and substituted back *afterwards*, entirely outside it. That is why a |
| name you invented one minute ago works immediately, and why 32 names cost the |
| model nothing. |
|
|
| --- |
|
|
| ## Training |
|
|
| Trained from scratch on a synthetic Persian/English corpus of automation |
| requests paired with their target rules, on a single T4. No pretrained |
| checkpoint; the vocabulary and the task are both narrow enough that |
| pretraining on general text would have spent capacity on nothing useful. |
|
|
| The loss is applied only to the rule span, not to the input sentence, so all |
| capacity goes to the translation rather than to modelling how people write. |
|
|
| ### What the numbers actually taught us |
|
|
| Overall accuracy said almost nothing useful. **Breaking it down by intent class |
| exposed every real bug.** Five separate apparent "model weaknesses" turned out |
| to be data or tooling defects: |
|
|
| - 10% of quoted values were corrupted by the typo-noise augmentation, teaching |
| the model to copy text that was no longer in the input; |
| - 4.1% of the test set asked for durations above the 24-hour cap, which the |
| grammar makes unreachable — the model was being scored on impossible targets; |
| - the decoder's own range check quietly made ADC thresholds 410–999 unreachable, |
| so it emitted `55` when asked for `550`. This one accounted for 123 of the |
| last 127 failures and had misled three training runs before it was found; |
| - the output token limit was below what 15% of targets needed, showing up as a |
| 12-point accuracy drop while the training loss reported 0.0001; |
| - the tokenizer split numbers inconsistently, as described above. |
|
|
| The dataset generator now refuses to emit an example it can prove is |
| impossible, and the test suite sweeps **every** legal value — all 4,096 ADC |
| thresholds and every duration — to assert each one is reachable. |
|
|
| --- |
|
|
| ## Limits, stated plainly |
|
|
| - **Narrow by design.** Pins, sensors, timers and the listed tools. *"Make the |
| house warm"* is refused, and that is intended behaviour, not a gap. |
| - **No compound conditions.** `if pin 1 AND pin 2` is not supported yet. |
| - **128-token context.** Long, rambling sentences are truncated. |
| - **Scheduling needs the network once.** There is no RTC, so nothing |
| time-based fires until the clock has synchronised over SNTP. |
| - **24 rules and 32 pin names** at a time on the device. |
| - **Persian first.** English works, but the training distribution is mostly |
| Persian and English coverage is correspondingly thinner. |
| - **No authentication** on the device's HTTP API. Anyone on the network can |
| control it. |
|
|
| ## What is not in this repository |
|
|
| The training code, the dataset generator, the model architecture and the |
| firmware sources are not published here. This repository is the **release**: |
| weights, tokenizer and documentation. Ready-to-flash binaries and the installer |
| are at |
| [github.com/AliAkrami1375/llmos-firmware](https://github.com/AliAkrami1375/llmos-firmware). |
|
|
| ## License |
|
|
| MIT. |
|
|
| <br> |
|
|
| --- |
|
|
| <br> |
|
|
| <a name="فارسی"></a> |
|
|
|  |
|
|
| <div dir="rtl"> |
|
|
| # llmOS — کامپایلر نیت |
|
|
| **یک مدل زبانی ۲٫۸۵ میلیون پارامتری که روی میکروکنترلر ESP32-S3 اجرا میشود و یک جملهی معمولی را به قانون اتوماسیون سختافزاری تبدیل میکند.** |
|
|
| میگویید «اگر سنسور پایه ۱ روشن شد پایه ۲ را خاموش کن» و برد از همان لحظه این کار را میکند. بدون کد، بدون کامپایل مجدد، بدون اینترنت و بدون هیچ سرویس ابری. مدل روی خود چیپ اجرا میشود، از روی کارت microSD. |
|
|
| | | | |
| |---|---| |
| | **فرمور و نصبکننده** | [github.com/AliAkrami1375/llmos-firmware](https://github.com/AliAkrami1375/llmos-firmware) | |
| | **تست در مرورگر** | [اسپیس](https://huggingface.co/spaces/DibaAi/llmOS) | |
| | **زبان** | فارسی (اصلی) و انگلیسی | |
| | **سختافزار** | ESP32-S3 با ۸ مگابایت PSRAM | |
|
|
| --- |
|
|
| ## چه کار میکند |
|
|
| | میگویید | تولید میکند | |
| |---|---| |
| | اگر سنسور پایه ۱ روشن شد پایه ۲ را خاموش کن | `ON GPIO1=1 DO GPIO2=0` | |
| | هر ۳۰ ثانیه پایه ۵ را چشمک بزن | `ON EVERY 30s DO GPIO5=T` | |
| | وقتی سنسور نور پایه ۴ کمتر از ۸۰۰ شد چراغ پایه ۷ را روشن کن | `ON ADC4<800 DO GPIO7=1` | |
| | ساعت ۳ صبح بازر پایه ۹ را ۳۰ ثانیه بزن | `ON AT 03:00 DO GPIO9=1; WAIT 30s; GPIO9=0` | |
| | پایه ۷ را بگذار «اتاق علی» | `NAME GPIO7 "اتاق علی"` | |
| | اتاق علی را روشن کن | `DO GPIO7=1` | |
| | به وایفای MyHome با رمز 1a2b3c وصل شو | `CALL wifi.connect "MyHome" "1a2b3c"` | |
| | شبکههای وایفای رو اسکن کن | `CALL wifi.scan` | |
| | سلام خوبی؟ | `ERR` — درست رد شد | |
|
|
| خروجی متن آزاد نیست. یک زبان صوری کوچک است که یک موتور قوانین قطعی اجرایش میکند؛ تنها کار مدل ترجمه است. |
|
|
| --- |
|
|
| ## چرا خروجی نامعتبر ناممکن است، نه فقط بعید |
|
|
| این بخش حتی اگر هیچوقت به ESP32 دست نزنید ارزش خواندن دارد. |
|
|
| مدل ۲٫۸۵ میلیون پارامتری اگر رها شود گاهی چرت میگوید. این یکی نمیتواند، چون **گرامر هنگام دیکود کردن اعمال میشود نه اینکه یاد گرفته شده باشد**. |
|
|
| زبان قانون به یک اتوماتای پیشوندی کامپایل میشود. در هر گام تولید، هر توکن کاندید تست میشود: *اگر این توکن را بپذیریم، خروجی هنوز پیشوند معتبری از یک قانون کامل میماند؟* توکنهایی که رد شوند قبل از نمونهبرداری حذف میشوند. توکن پایان تنها وقتی قابل دسترس میشود که متن یک قانون کامل و معتبر باشد. |
|
|
| همین تست **بازهی عددی** را هم اعمال میکند، نه فقط نحو را. `GPIO99` غیرقابلدسترس است چون ۹۹ پایهی مجاز نیست؛ `ADC4<9999` غیرقابلدسترس است چون ADC دوازدهبیتی است. پایههایی که فلش، PSRAM یا کارت حافظه رویشان است کلاً از اتوماتا بیروناند، پس هیچ جملهای — با هر عبارتی — نمیتواند قانونی بسازد که کارتی را که مدل از رویش اجرا میشود unmount کند. |
|
|
| ### مقدار نقلقولی کپی میشود، هجی نمیشود |
|
|
| مدلی به این کوچکی نمیتواند رمز تصادفی وایفای را هجی کند، و هیچوقت لازم نیست بکند. |
|
|
| وقتی دیکودر داخل یک آرگومان نقلقولی است، از پرسیدن «آیا این نحو معتبری است؟» به پرسیدن **«آیا این آرگومان را زیررشتهای از متن کاربر نگه میدارد؟»** سوئیچ میکند. فقط توکنهایی که این شرط را حفظ کنند زنده میمانند. مدل هیچوقت چیزی را هجی نمیکند — فقط تصمیم میگیرد **از کجا شروع به کپی کند و کجا بایستد**، و این دقیقاً کاری است که مکانیزم توجه بهطور طبیعی خوب انجام میدهد. |
|
|
| رمزی پر از کاراکتر عجیب بنا به ساختار بایتبهبایت درست درمیآید، نه به شانس. بررسی بازهها عمداً داخل نقلقول نگاه نمیکند: شبکهای به نام `GPIO99 ADC9999` یک SSID مجاز است و هیچوقت بهعنوان پایه خوانده نمیشود. |
|
|
| --- |
|
|
| ## دقت |
|
|
| روی ۵۰۰۰ درخواست کنارگذاشتهشده، از مسیر همان موتور C که روی برد اجرا میشود و با همین وزنهای کوانتیزه: |
|
|
| </div> |
|
|
| ``` |
| exact match : 99.92% 4 wrong out of 5,000 |
| syntactically ok : 100.00% |
| parses + in range: 100.00% |
| mean confidence : 1.000 |
| ``` |
|
|
| <div dir="rtl"> |
|
|
| و به تفکیک نوع نیت، چون یک عدد کلی پنهان میکند کدام نیمهی رفتار ضعیف است: |
|
|
| | نیت | تعداد | دقت | |
| |---|---|---| |
| | قانون دیجیتال | ۱۴۹۶ | ۹۹٫۹٪ | |
| | اقدام فوری | ۵۳۰ | ۱۰۰٪ | |
| | آستانهی آنالوگ | ۸۲۸ | ۱۰۰٪ | |
| | تناوبی | ۶۱۱ | ۱۰۰٪ | |
| | زمانبندیشده (`ساعت ۳`) | ۵۸۰ | ۹۹٫۸٪ | |
| | نامگذاری پایه | ۵۲۷ | ۹۹٫۸٪ | |
| | فراخوانی ابزار | ۳۱۷ | ۹۹٫۷٪ | |
| | خارج از دامنه | ۱۱۱ | ۱۰۰٪ | |
|
|
| سطر آخر به اندازهی سطر اول مهم است: مدل تشخیص میدهد که یک درخواست **قانون اتوماسیون نیست** و همین را میگوید، بهجای اینکه به زور در قالبی بچپاندش. |
|
|
| دو سطر ۱۰۰٪ بالا **بنا به ساختار است، نه بنا به آموزش** — بخش قبل را ببینید. |
|
|
| --- |
|
|
| ## معماری |
|
|
| | | | |
| |---|---| |
| | نوع | ترنسفورمر فقط-دیکودر، سبک Llama | |
| | پارامتر | ۲٬۸۵۳٬۳۱۲ | |
| | بعد / لایه / هد | ۱۹۲ / ۶ / ۴ (۴ هد KV) | |
| | فیدفوروارد | SwiGLU، پنهان ۵۱۲ | |
| | موقعیتی | RoPE | |
| | نرمالسازی | RMSNorm | |
| | کانتکست | ۱۲۸ توکن | |
| | واژگان | ۱۰۲۴ | |
| | کوانتیزاسیون | int8 متقارن، گروه ۳۲ | |
| | روی دیسک | ۳۱۴۲ کیلوبایت | |
| | حافظهی کاری روی دستگاه | ۱۱۶۶ کیلوبایت (KV cache و بافرها) | |
|
|
| ### توکنایزر |
|
|
| BPE سطح-بایت با ۱۰۲۴ توکن و **ارقام اتمی** — هیچ ادغام رقم-رقمی در واژگان وجود ندارد. |
|
|
| این جزئیات تزئینی نبود. توکنایزر قبلی `500` را `['500']`، `800` را `['8','00']` و `4095` را `['40','9','5']` میشکست. مدل مجبور بود برای هر عدد راهبرد هجی متفاوتی یاد بگیرد و قوانین زمانبندیشده روی ۸۹٫۲٪ گیر کرده بودند. اتمی کردن ارقام همان کلاس را بدون هیچ تغییری در معماری به ۹۹٫۸٪ رساند. |
|
|
| توکنایزیشن حریصانهی طولانیترین-تطبیق است، در C و پایتون یکسان پیاده شده و روی ۷۴۰٬۰۰۰ رشته بایتبهبایت بررسی شده — چون توکنایزری که در مرز آموزش/استنتاج با خودش اختلاف داشته باشد مدلی میسازد که آموزشدیده به نظر میرسد و خراب رفتار میکند. |
|
|
| --- |
|
|
| ## فایلها |
|
|
| | فایل | چیست | |
| |---|---| |
| | `model.llm` | وزنهای کوانتیزه، فرمت باینری اختصاصی | |
| | `tokenizer.bin` | واژگان — **باید با مدل جفت باشد** | |
| | `tools.json` | تعریف ابزارهای HTTP همراه، بهعنوان مرجع | |
| | `LICENSE` | MIT | |
|
|
| > `model.llm` و `tokenizer.bin` یک **جفت هماهنگاند**. عوض کردن یکی بدون دیگری بهجای خطا، جواب اشتباه با اطمینان کامل میدهد. همیشه هر دو را از یک نسخه بردارید. |
|
|
| ### این یک چکپوینت `transformers` نیست |
|
|
| `model.llm` یک فرمت تخت int8 است که یک موتور استنتاج C99 بدون هیچ وابستگی میخواندش، و طوری طراحی شده که روی میکروکنترلر `mmap`-پذیر باشد. نه `config.json` دارد، نه `safetensors`، و `AutoModel.from_pretrained` کار نمیکند. |
|
|
| برای اجرا یا فرمور را فلش کنید، یا از اسپیس استفاده کنید که همان موتور C را روی CPU اجرا میکند. |
|
|
| --- |
|
|
| ## چطور اجرایش کنیم |
|
|
| ### روی سختافزار |
|
|
| </div> |
|
|
| ```bash |
| git clone https://github.com/AliAkrami1375/llmos-firmware.git |
| cd llmos-firmware |
| ./install.sh # لینوکس، مک |
| ``` |
|
|
| ```powershell |
| powershell -ExecutionPolicy Bypass -File install.ps1 # ویندوز |
| ``` |
|
|
| <div dir="rtl"> |
|
|
| نصبکننده برد را پیدا میکند، فلش میکند و مدل را روی کارت microSD با فرمت FAT32 کپی میکند. به یک ESP32-S3 با **۸ مگابایت PSRAM** (مدل N8R8 یا N16R8) و یک کابل USB **دیتا** نیاز دارید. |
|
|
| ### در مرورگر |
|
|
| [**huggingface.co/spaces/DibaAi/llmOS**](https://huggingface.co/spaces/DibaAi/llmOS) — همان موتور C، همان وزنها، روی CPU. |
|
|
| --- |
|
|
| ## زبان قانونها |
|
|
| </div> |
|
|
| ``` |
| rule := "ON " trigger " DO " actions |
| | "DO " actions |
| | "CALL " tool arg* |
| | "NAME GPIO" pin " " label |
| | "ERR" |
| |
| trigger := "GPIO" pin "=" ("0"|"1") |
| | "ADC" pin ("<"|">") value |
| | "EVERY " n unit |
| | "AT " hh ":" mm |
| |
| action := "GPIO" pin "=" ("0"|"1"|"T") T = toggle |
| | "WAIT " n unit |
| | "CALL " tool arg* |
| |
| unit := "ms" | "s" | "m" | "h" |
| ``` |
|
|
| <div dir="rtl"> |
|
|
| پایههای مجاز: **۱ تا ۹، ۱۴ تا ۱۸، ۲۱، ۳۸ تا ۴۴، ۴۷، ۴۸**. ADC: **۱ تا ۹**، آستانه ۰ تا ۴۰۹۵. مدتها تا ۲۴ ساعت. هر چیز دیگری در اتوماتا **غیرقابلدسترس** است، نه اینکه بعداً رد شود. |
|
|
| --- |
|
|
| ## فراخوانی ابزار و اطلاعرسانی |
|
|
| قانونها پایهها را کنترل میکنند. **ابزار** کاری است که دستگاه انجام میدهد ولی قانون GPIO نیست — وصل شدن به وایفای، خواندن یک API، و از همه کاربردیتر **خبر دادن وقتی اتفاقی افتاد**. |
|
|
| `CALL` هم یک اکشن است مثل بقیه، پس قانون میتواند شاملش شود. اینها معتبرند و پارسر و موتور هر دو برایشان تست دارند: |
|
|
| </div> |
|
|
| ``` |
| ON GPIO7=1 DO CALL notify.send "درب باز شد" |
| ON ADC3>2400 DO GPIO5=1; CALL notify.send "خیلی گرم شد" |
| ON AT 07:00 DO CALL notify.send "صبح بخیر" |
| ``` |
|
|
| <div dir="rtl"> |
|
|
| > **ولی مدل این خطها را برایتان نمینویسد.** مدل فقط پنج نام ابزاری را تولید میکند که رویشان آموزش دیده — `wifi.connect`، `wifi.scan`، `wifi.status`، `sys.info`، `pin.read`. `notify.send` یک ابزار *کاربر* است که خیلی بعد از آموزش در فایلی روی کارت حافظه تعریف میشود، پس هیچ جملهای تولیدش نمیکند. اگر بگویید «اگر پایه ۷ روشن شد بهم خبر بده»، یک قانون پایهی ساده یا `ERR` میگیرید. |
| > |
| > برای رساندن چنین قانونی به دستگاه، خط را در `rules.txt` روی کارت بنویسید؛ هنگام بوت پارس و ذخیره میشود. |
|
|
| </div> |
|
|
| <div dir="rtl"> |
|
|
| ### `notify.send` |
|
|
| بقیهی ابزارها به سؤالی که پرسیدهاید جواب میدهند. `notify.send` تنها ابزاری است که **بدون پرسیده شدن حرف میزند** — همان که میگوید ساعت ۳ بامداد که خواب بودید درب باز شد. |
|
|
| روی یک وبهوک در **شبکهی خودتان** POST میکند، پس نه اینترنت میخواهد، نه TLS، نه کلید API و نه هیچ طرف سومی که بشود فیلترش کرد یا تعطیل شود: |
|
|
| </div> |
|
|
| ```json |
| { |
| "name": "notify.send", |
| "kind": "http", |
| "parameters": [{ "name": "text", "required": true }], |
| "phrases": ["پیام بده", "خبرم کن", "اطلاع بده", "notify me"], |
| "execute": { |
| "method": "POST", |
| "url": "http://192.168.1.50:8123/api/webhook/llmos", |
| "body": "{\"message\":\"{text}\"}", |
| "timeout_ms": 5000 |
| }, |
| "reply": { "fa": "پیام فرستاده شد.", "on_error": "پیام فرستاده نشد." } |
| } |
| ``` |
|
|
| <div dir="rtl"> |
|
|
| آدرس را به Home Assistant، Node-RED یا یک گیرندهی پنجخطی پایتون وصل کنید. تا وقتی این کار را نکردهاید درست میگوید که پیام فرستاده نشد — چیزی روی آدرس نمونه گوش نمیدهد. |
|
|
| دقیقاً دو راه برای رسیدن به آن هست: |
|
|
| | چه میخواهید | چطور | |
| |---|---| |
| | **همین حالا** پیام | بگویید «خبرم کن …» — مسیریاب عبارت تطبیقش میدهد و هر چه بعد از عبارت بیاید متن پیام میشود | |
| | **وقتی اتفاقی افتاد** پیام | خط `ON GPIO7=1 DO CALL notify.send "درب باز شد"` را در `rules.txt` روی کارت بنویسید | |
|
|
| **قانونی که `CALL` داشته باشد روی لبه شلیک میکند.** یک پیام روی گذار صفر→یک، نه یک پیام در هر تیک ۲۰ میلیثانیهای تا وقتی پایه روشن مانده؛ و ۳۰ میلیثانیه debounce باعث میشود کنتاکت لرزان هم یک رویداد شمرده شود. تفاوت یک اطلاعرسانی مفید با سه هزار پیام همین است. |
|
|
| **شکست یک فراخوانی، قانون را متوقف نمیکند.** اکشنهای بعدی همچنان اجرا میشوند — یک وبهوک مرده نباید جلوی روشن شدن چراغ را بگیرد. |
|
|
| ### دو مسیری که به ابزار میرسند |
|
|
| </div> |
|
|
| ``` |
| جملهی شما |
| │ |
| ├─► مسیریاب عبارت ──── با عبارت تریگر ابزاری خواند؟ ──► همین حالا اجرا |
| │ (با تحمل غلط املایی) │ نه |
| │ ▼ |
| └───────────────────────────► مدل ──► ON … DO … │ CALL … │ ERR |
| |
| rules.txt روی کارت ───────────────► پارس در بوت ──► ذخیره بهعنوان قانون |
| ``` |
|
|
| <div dir="rtl"> |
|
|
| مسیریاب متن فارسی را نرمال میکند (ی/ي، ک/ك، نیمفاصله، ارقام فارسی و عربی و لاتین، اعراب) و غلط تایپی را تحمل میکند. ابزار را بلافاصله اجرا میکند، نه اینکه قانون بسازد. |
|
|
| > **اضافه کردن ابزار به `tools.json` به مدل چیزی یاد نمیدهد.** مدل روی مجموعهی ثابتی از نام ابزارها آموزش دیده؛ ابزاری که بعداً اضافه شود از راه عبارتهای تریگر خودش صدا زده میشود، نه با اینکه مدل `CALL yourtool` بنویسد. این باگ نیست — معنای «واژگان بسته» همین است. و دقیقاً به همین دلیل هر ابزار عبارتهای خودش را حمل میکند. |
|
|
| هفت ابزار HTTP در `tools.json` آمادهاند — اطلاعرسانی، آب و هوا، کیفیت هوا، طلوع و غروب، ساعت، زلزله، وضعیت اینترنت. هر آدرس واقعاً صدا زده شده و هر مسیر استخراج با پاسخ واقعی بررسی شده. |
|
|
| پنج ابزار دیگر **داخل فرمور کامپایل شدهاند و حذف نمیشوند** — `wifi.scan`، `wifi.connect`، `wifi.status`، `sys.info`، `pin.read`. نه به خاطر فلگی در فایل، بلکه چون اصلاً داخل فایل نیستند؛ در هر بوت از یک جدول ساخته میشوند. کارت را در بیاورید، `wifi.connect` سر جایش است. |
|
|
| **← [مستندات کامل ابزار و اطلاعرسانی](https://github.com/AliAkrami1375/llmos-firmware/blob/main/docs/TOOLS.fa.md)** ([انگلیسی](https://github.com/AliAkrami1375/llmos-firmware/blob/main/docs/TOOLS.md)) |
|
|
| --- |
|
|
| ## نامگذاری پایهها |
|
|
| میتوانید پایه ۷ را «اتاق علی» بنامید و بعد بگویید «اتاق علی را روشن کن». |
|
|
| **مدل هیچوقت اسمهای شما را یاد نمیگیرد.** این اسمها خیلی بعد از آموزش ساخته میشوند. اسم **قبل** از رسیدن جمله به مدل با شمارهی پایه جایگزین میشود و **بعد** از آن برمیگردد، کاملاً بیرون از مدل. به همین دلیل اسمی که یک دقیقه پیش ساختهاید بلافاصله کار میکند، و ۳۲ اسم هیچ هزینهای به مدل تحمیل نمیکند. |
|
|
| --- |
|
|
| ## آموزش |
|
|
| از صفر روی یک پیکرهی مصنوعی فارسی/انگلیسی از درخواستهای اتوماسیون همراه با قانون هدفشان، روی یک T4 آموزش دیده. بدون چکپوینت از پیش آموزشدیده؛ واژگان و خود مسئله آنقدر باریکاند که پیشآموزش روی متن عمومی ظرفیت را صرف چیز بیفایده میکرد. |
|
|
| خطا فقط روی بازهی قانون اعمال میشود نه روی جملهی ورودی، پس تمام ظرفیت صرف ترجمه میشود نه مدل کردن اینکه آدمها چطور مینویسند. |
|
|
| ### چیزی که اعداد واقعاً یادمان دادند |
|
|
| دقت کلی تقریباً هیچ چیز مفیدی نمیگفت. **تفکیک آن به کلاسهای نیت، هر باگ واقعی را بیرون کشید.** پنج «ضعف مدل» ظاهری در واقع نقص داده یا ابزار بودند: |
|
|
| - ۱۰٪ از مقادیر نقلقولی توسط نویز غلط تایپی خراب میشدند و به مدل یاد میدادند متنی را کپی کند که دیگر در ورودی نبود؛ |
| - ۴٫۱٪ از مجموعهی تست مدتهایی بالاتر از سقف ۲۴ ساعت میخواستند که گرامر غیرقابلدسترسشان میکند — مدل داشت روی هدفهای ناممکن نمره میگرفت؛ |
| - بررسی بازهی خود دیکودر بیسروصدا آستانههای ADC از ۴۱۰ تا ۹۹۹ را غیرقابلدسترس کرده بود، پس وقتی ۵۵۰ خواسته میشد `55` تولید میکرد. همین یکی مسئول ۱۲۳ تا از ۱۲۷ شکست باقیمانده بود و سه اجرای آموزش را گمراه کرده بود؛ |
| - سقف توکن خروجی کمتر از چیزی بود که ۱۵٪ از هدفها لازم داشتند، و به شکل ۱۲ واحد افت دقت ظاهر میشد در حالی که خطای آموزش ۰٫۰۰۰۱ گزارش میکرد؛ |
| - توکنایزر اعداد را ناهماهنگ میشکست، همانطور که بالا توضیح داده شد. |
|
|
| حالا مولد دیتاست از تولید نمونهای که بتواند ناممکن بودنش را ثابت کند خودداری میکند، و مجموعهی تست **همهی** مقادیر مجاز را جارو میکند — هر ۴۰۹۶ آستانهی ADC و هر مدت — تا مطمئن شود همه قابل دسترساند. |
|
|
| --- |
|
|
| ## محدودیتها، صادقانه |
|
|
| - **دامنه عمداً باریک است.** پایهها، سنسورها، تایمرها و ابزارهای فهرستشده. «خونه رو گرم کن» رد میشود و این رفتار درست است نه کمبود. |
| - **بدون شرط ترکیبی.** `اگر پایه ۱ و پایه ۲` هنوز پشتیبانی نمیشود. |
| - **کانتکست ۱۲۸ توکن.** جملههای خیلی طولانی بریده میشوند. |
| - **زمانبندی یک بار به شبکه نیاز دارد.** RTC روی برد نیست، پس تا وقتی ساعت با SNTP همگام نشده هیچ قانون زمانی شلیک نمیکند. |
| - **۲۴ قانون و ۳۲ نام پایه** در هر لحظه روی دستگاه. |
| - **اول فارسی.** انگلیسی کار میکند، ولی توزیع آموزش عمدتاً فارسی است و پوشش انگلیسی به همان نسبت نازکتر. |
| - **بدون احراز هویت** روی API دستگاه. هر کسی روی شبکه میتواند کنترلش کند. |
|
|
| ## چه چیزی در این مخزن نیست |
|
|
| کد آموزش، مولد دیتاست، معماری مدل و سورس فرمور اینجا منتشر نشدهاند. این مخزن **ریلیز** است: وزنها، توکنایزر و مستندات. باینریهای آمادهی فلش و نصبکننده در [github.com/AliAkrami1375/llmos-firmware](https://github.com/AliAkrami1375/llmos-firmware) هستند. |
|
|
| ## مجوز |
|
|
| MIT |
|
|
| </div> |
|
|