--- url: https://suyiiyii.github.io/alldayrec/en/explanation/architecture.md --- # Architecture and data flow alldayrec is a Python server plus a few clients. Audio comes in from a recorder or a phone. The server runs speech detection and voiceprints locally, hands the ASR and the summaries to the cloud services you pick, stores the results in SQLite, and keeps the original audio on disk. ```text BLE recorder ──BLE──▶ phone app ──HTTP(S)──▶ server (FastAPI) ──▶ web UI, MCP, CLI (or the phone's mic) │ local: VAD, voiceprints (onnxruntime) │ cloud: ASR engine, OpenAI-compatible LLM ▼ SQLite + original audio ``` ## The parts | Directory | What | |---|---| | `server/` | The server: upload queue, VAD and voiceprints, ASR engines, conversations, summaries, MCP (Python 3.11, uv) | | `web/` | The web UI: today, conversations, people, calendar, live transcript, device pages (Vue, pnpm) | | `app/` | The phone app for iOS and Android: records the phone's mic or the BLE recorder, uploads, firmware updates (Flutter) | | `firmware/devkit/` | The BLE recorder firmware for nRF52840 boards (Zephyr / nRF Connect SDK), based on the Omi devkit | | `firmware/selftest/`, `firmware/blerelay/` | Board self-test firmware; an ESP32 BLE relay used for testing | | `hardware/lr-p3/` | The LR-P3 recorder's parts, wiring and 3D-printed case | | `cli/` | `lr.py`: read your conversations from a terminal or an agent | ## How audio comes in The recorder writes to its SD card all the time. While it is connected to the phone, it streams the same record to the phone too; for the time it was disconnected, the phone reads that part back from the card when it returns and fills the gap. The phone app cuts the audio into short Opus pieces and uploads them to `POST /api/chunks`; while offline, the pieces queue on the phone and go up later. The server first saves each piece to disk, then hands it to that device's pipeline in time order. So a client that was offline for a long time can upload its whole backlog at once when it comes back, and other devices are not affected. Anything that can POST an audio file can be an audio source; this is also how you [import recordings you already have](../how-to/import-recordings.md). ## Two transcription layers The server finds where people speak with FireRedVAD, locally, and then transcribes in two layers: * **The final layer** runs all the time. It cuts the audio at pauses into pieces of up to 5 minutes and sends each piece once to the ASR engine (`ENGINE`); the result is the final transcript. * **The draft layer** runs only while a web page is open. At each pause (or every `REFINE_EVERY_S` seconds while no pause comes) it sends the new audio to `DRAFT_ENGINE`, so words show within seconds; with `DRAFT_ENGINE=qwen-stream` it streams the audio to Qwen continuously instead, and words show as they are said. Drafts show only for the time the finals don't cover yet, and the finals replace them when they arrive. The text and the sentence splits come from the ASR engine; the server does speech detection and speaker identity locally, and the table below shows where each engine's speakers come from. Before audio goes to the engine, silences longer than 3 seconds are cut to 3 seconds (`SQUEEZE_KEEP_S`). The engines differ in where the speakers come from: | `ENGINE` | Provider | Speakers | |---|---|---| | `qwen` | Alibaba Cloud Bailian, Qwen-Audio file transcription | from the engine, refined by local voiceprints | | `volc` | Volcengine Doubao Seed-ASR 2.0 | local voiceprints | | `stepfun-sse` | StepFun streaming ASR | local voiceprints | | `stepfun`, `stepfun-file` | StepFun file mode | from the engine, refined by local voiceprints | | `local` | sherpa-onnx X-ASR zh-en in the server process, text only | local voiceprints | For the accuracy of the local engine against the cloud engines, see [ASR benchmark](./asr-benchmark.md). ## Knowing who spoke The voiceprint model ReDimNet2 runs on the local CPU. The server recognizes 「我」 (me: the person recording), and within a conversation it labels the other people S1, S2 and so on; sentences too short to recognize are labeled S?. Once you give a label a name in the web UI, the same voice is recognized in later conversations. ## Conversations, summaries and the recap Sentences are grouped into conversations by time. A conversation ends after 5 minutes with nobody speaking, when the recorder stops, or when it reaches 4 hours. A conversation with less than 10 seconds of speech is not kept as a conversation (its sentences stay in the transcript). When a conversation ends, the server writes a summary with the LLM you configured: a title, the summary, each person's points, to-dos (with due dates), decisions and tags. A conversation with under 60 seconds of speech doesn't go to the LLM; its first sentence becomes the title. Every day at `RECAP_AT` (default 10:00), the server writes the previous day's recap. 「问 AI」 (Ask AI) answers questions about your past with the same LLM: the summaries of the last few days go into the prompt, and older or more detailed content is found with tools that query the database. Every claim carries the id of the conversation it came from. ## Who reads this data The web UI, the web pages inside the app, the [MCP endpoint](../reference/mcp.md) and the [CLI](../reference/cli.md) all read the same tenant's database. The database is SQLite, so you can also read it directly yourself; the tables are in [Data layout](../reference/data.md). One server can serve several people, each with a token, a database and a recordings directory (`tenants.json`), isolated from each other in the same process. --- --- url: https://suyiiyii.github.io/alldayrec/en/explanation/asr-benchmark.md --- # ASR benchmark: local engines vs cloud > 中文摘要:2026-10-23 在 5 段真实录音(共 1.77 小时)上,比较了能在 CPU 上跑的开源语音识别模型和几家云端引擎。 > 最好的本地模型 X-ASR(`ENGINE=local` 用的就是它)和 SenseVoice-Small 句内字错率约 19.6,默认的云端千问是 13.5,差约 6 个点,和千问之前用的阶跃 SSE(18.2)相当。 > 速度不是问题:X-ASR 在 4 核上 RTF 0.024,一天 5 小时的语音约 7 分钟算完;N100、树莓派 5(推算)约 17 分钟、25–32 分钟。 > 带大模型解码器的模型(Qwen3-ASR-0.6B)在 CPU 上不划算:不更准,算力约 10 倍;sherpa-onnx 要用 2.0.0 及以上。 > 参考答案(GT)偏向部分厂商,数字适合排序、不宜当绝对值;方法、注意事项和在自己的录音上复跑的办法见下文。 Measured on 2026-10-23. The question: can an open-weight model on a CPU replace the cloud engine of the final transcript? Everything ran locally on five private recordings; no audio or ground truth is published, only the numbers. ## Key findings * **Local is about 6 CER points behind the default cloud engine.** Mean in-sentence CER over the 5 cases: X-ASR-zh-en int8 **19.7**, SenseVoice-Small 2024 int8 **19.5**, against Bailian qwen-audio-3.1 (`ENGINE=qwen`) 13.5, Volcengine 12.1 and StepFun SSE 18.2 (the cloud engine used before qwen). So local is roughly StepFun-SSE class. * **Speed is not the problem.** X-ASR runs at RTF 0.024 on 4 threads of a 16-core desktop (Zen 3) and 0.012 on an Apple M4: 7 min and 3.5 min of decoding for a day with 5 h of speech. An N100 or a Raspberry Pi 5 (extrapolated) needs about 17 min and 25–32 min. * **LLM-decoder models did not pay off on CPU.** Qwen3-ASR-0.6B: 20.9 CER at 10× the compute and 1.2 GB RAM after load. Fun-ASR-Nano-2512 could not be evaluated (setup failure, see the notes). * **The sherpa-onnx version matters.** On the home case 2.0.0 lifts Qwen3-ASR by about 2 points over 1.13.8 and partly fixes Fun-ASR-Nano; CTC and transducer models barely move. The server pins `sherpa-onnx>=2.0.0`. * **`ENGINE=local` uses X-ASR** (Apache-2.0, 168 MB on disk from a ~136 MB tarball, about 330 MB RAM, token times and cased punctuation). SenseVoice-Small ties on the mean and is best on the most trustworthy case, but has a custom licence and weaker punctuation. ## Method **Cases.** Five real-life recordings with ground truth, 1.77 h of audio in total (6358 s, about 21 min per case on average), of which 4789 s is speech: | Case | What it is | Where its GT came from | |---|---|---| | home | at home | human-adjudicated from Doubao, StepFun and Volcengine candidates (the most trustworthy) | | meeting | a meeting | Doubao | | dji | outdoor walk, DJI wireless mic | Doubao | | phone | the phone's own mic | Doubao | | evening | an evening recording | StepFun | **GT bias, read before the numbers.** Meeting, dji and phone GT came from Doubao, which favours Volcengine and Doubao. Evening GT came from StepFun, which favours StepFun SSE (its 7.6 there). Home was adjudicated by a person, but from Doubao, StepFun and Volcengine candidates. Every engine outside that set, which includes every local model, is slightly penalised everywhere. On home alone: SenseVoice 16.5, Paraformer 19.0, X-ASR 19.7, Qwen3-ASR 20.6, against qwen 11.6 and Volcengine 7.3. **What the model sees.** Each case is cut in pauses into blocks of at most 5 min, the same blocks as an earlier cloud comparison. Inside a block the server's own loose VAD (`segmenter.Segmenter`, `SQUEEZE_VAD` 0.3) finds the speech, padded 0.2 s, merged across gaps under 0.5 s and cut to at most 20 s at the quietest 100 ms. One decode per segment; that is what `ENGINE=local` does inside a window. **Runtime.** sherpa-onnx int8 builds on CPU, version 1.13.8 on the desktop (the newest wheel on the mirror used) and 2.0.0 on the M4. **CER.** Text is NFKC-normalised, lowercased, letters and digits only. * *In-sentence CER* counts hypothesis characters whose time falls inside a GT sentence ±0.5 s, against all GT text. It ignores text an engine invents in silence or in stretches the GT skipped. * *Whole CER* compares all text of the blocks with all GT text, no timing involved. * The scorer reproduces the earlier cloud comparison exactly (qwen in-sentence 11.6 / 5.6 / 14.9 / 23.0 / 12.2, StepFun SSE whole 14.4 / 14.1 / 21.8 / 39.9 / 7.9), so local and cloud rows are on the same scale. * Models without token times get their characters spread evenly over the segment, which costs them slightly in in-sentence CER; whole CER is unaffected. **Mixed rows.** The share of hypothesis rows spanning two GT speakers, rows split at 。?! and pauses over 0.5 s. A row that spans two people gets one speaker label, so this bounds speaker accuracy for text-only engines. **Speed and memory.** * Desktop: `nice -n 19`, pinned to 4 physical cores, `num_threads=4`; 1-thread runs on one core. At times two jobs ran at once on different CCDs. * M4: home case only, on a machine busy with other work (load 3–9). * RTF = decode seconds / speech seconds decoded. Per second of recorded audio it is ×0.75. * "RSS after load" is the process RSS right after creating the recognizer. Run peaks are higher: they include about 0.4 GB of decoded case audio held by the benchmark, plus decoder state (Qwen3-ASR 3.4 GB, Fun-ASR-Nano 3.2 GB, the others 1.3–2.0 GB). Load time is with a warm page cache. ## Results Per-case order: home / meeting / dji / phone / evening. CER and mixed rows in %. | Engine (int8, sherpa-onnx, CPU) | In-sentence CER per case | **Mean** | Whole CER | Mixed rows | RTF 4 thr, desktop | RTF 1 thr, desktop | RTF 4 / 1 thr, M4 | Load s | RSS after load | Disk | |---|---|---|---|---|---|---|---|---|---|---| | **X-ASR-zh-en** zipformer, punct, 2026-06-03 | 19.7 / 8.6 / 25.3 / 31.2 / 13.7 | **19.7** | **21.9** | 11.2 | 0.024 | 0.045 | 0.012 / 0.015 | 2.2 | 328 MB | 168 MB | | **SenseVoice-Small** 2024-07-17 | **16.5** / 9.8 / 24.9 / 30.0 / 16.1 | **19.5** | 24.3 | 18.2 | 0.023 | 0.074 | 0.012 / 0.032 | 1.1 | 356 MB | 229 MB | | Qwen3-ASR-0.6B (LLM decoder) | 20.6 / 9.8 / 28.2 / 29.9 / 15.8 | 20.9 | 24.1 | 9.3 | 0.229 | 0.587 | 0.133 / – | 3.8 | 1214 MB | 942 MB | | Fun-ASR-Nano encoder + CTC head | 20.3 / 11.4 / 27.0 / 33.7 / 17.5 | 22.0 | 27.2 | 30.9 (no punct) | 0.026 | – | – | 1.2 | 391 MB | 253 MB | | SenseVoice 2025-09-09 (Cantonese fine-tune, no punct) | 20.9 / 10.8 / 30.9 / 36.5 / 16.9 | 23.2 | 26.1 | 31.6 | 0.020 | – | – | 1.4 | – | 227 MB | | FireRedASR2 CTC | 20.3 / 13.8 / 27.8 / 38.3 / 17.5 | 23.5 | 28.2 | 30.6 (no punct) | 0.210 | – | – | 2.0 | 949 MB | 742 MB | | Paraformer-large zh 2024-03-09 (no punctuation model applied) | 19.0 / 11.1 / **46.9** / 30.3 / 18.0 | 25.1 | 27.4 | 33.9 | 0.017 | – | – | 1.7 | 452 MB | 217 MB | | Fun-ASR-Nano-2512 (LLM decoder): **setup failure, not a model result** | 845 / 132 / 231 / 354 / 301 | (372) | (427) | – | 0.345 | – | – | 6.0 | 1221 MB | 963 MB | | *FireRedASR2-AED fp32 on a **GPU** (earlier run, rescored)* | 18.2 / 8.8 / 24.7 / 33.7 / 17.8 | 20.6 | 23.7 | 9.5 | GPU: 5.6–30× real time, 5.5 GB VRAM | | | | | | Cloud references, same blocks and same scorer: | Cloud engine | In-sentence CER per case | **Mean** | Whole CER | Mixed rows | Price per hour of audio | |---|---|---|---|---|---| | Volcengine Seed-ASR 2.0 (earlier run; whole CER not computed) | 7.3 / 4.8 / 13.1 / 23.0 / 12.2 | 12.1 | – | – | – | | **Bailian qwen-audio-3.1 file transcription** (`ENGINE=qwen`, the default) | 11.6 / 5.6 / 14.9 / 23.0 / 12.2 | 13.5 | 16.7 | 0.9 | ~¥0.05 | | Tencent Cloud 16k_zh_en_2.0 | 11.5 / 6.3 / 16.8 / 19.0 / 14.9 | 13.7 | 16.8 | 2.0 | ~¥0.8 | | Bailian fun-asr | 18.0 / 7.6 / 20.5 / 17.2 / 12.8 | 15.2 | 19.0 | 1.6 | ~¥0.49 | | StepFun SSE (the cloud engine used before qwen) | 13.6 / 7.7 / 22.4 / 39.8 / 7.6 | 18.2 | 19.6 | 2.3 | ¥0.15 | | StepFun file mode, 16 kHz Opus upload | 19.8 / 10.5 / 24.6 / 43.7 / 9.7 | 21.7 | 23.1 | 1.7 | ¥0.15 | Notes: * **X-ASR and SenseVoice tie on in-sentence CER** (19.7 vs 19.5). X-ASR is better on whole CER (21.9 vs 24.3), meeting and evening; SenseVoice on home and phone. X-ASR's punctuation splits rows better (mixed 11.2 % vs 18.2 %), it is faster on one thread (0.045 vs 0.074) and it is Apache-2.0; SenseVoice has a custom licence. * **Qwen3-ASR-0.6B, the only LLM-decoder model that worked, is no better** (20.9 / 24.1) at 10× the compute and 3.4× the RAM. Its sherpa build gives no token times, which costs it a little in-sentence but not whole. * **Fun-ASR-Nano-2512** (sherpa-onnx 1.13.8, default prompt, greedy) is **a setup failure, not a model result.** On home it produced 16,306 characters against the GT's 1,406, mostly Latin letters and repeat loops. sherpa's own bundled clean Mandarin clips looped the same way ("to be used to be used …"), so the runtime or the file pairing is at fault, not the weights. Its CTC head alone is sane (22.0). Published results rank it top for far-field audio; worth a retry. * **sherpa-onnx 2.0.0 cross-check** (M4, home case only; the main table is 1.13.8): | Model | In-sentence CER, 1.13.8 → 2.0.0 | Whole CER, 1.13.8 → 2.0.0 | |---|---|---| | SenseVoice 2024 | 16.5 → 16.1 | 21.1 → 20.9 | | X-ASR | 19.7 → 19.3 | 23.2 → 22.2 | | Qwen3-ASR-0.6B | 20.6 → **18.4** | 24.1 → 22.4 | | Fun-ASR-Nano | 845 → **45.8** | → 55.9 | Fun-ASR-Nano still loops on 2.0.0 (2,368 characters for 1,406, of which 763 in repeat runs). The LLM decoders gain most from the newer runtime, so pin ≥ 2.0.0. * **Paraformer** is worst on the outdoor dji walk (46.9), and this build returns no token times. * **Package-name traps.** sherpa's `sense-voice … 2025-09-09` build is a Cantonese fine-tune with no punctuation (the row above); its `paraformer-zh … 2025-10-07` build is WSChuan, a Sichuanese fine-tune (30.9 mean, dropped). Use `sense-voice … 2024-07-17` and the 2024-03-09 Paraformer. * **Token times.** Real per-token times from X-ASR, SenseVoice 2024, FireRed CTC and the Nano CTC head; none from Qwen3-ASR or the Paraformer 2024-03-09 build. * No traditional or Cantonese characters appeared in any local output. * **`score_case.py`'s own CER** (60 s windows) ranks the engines the same way: qwen 29.4, StepFun SSE 31.7, X-ASR 34.6, FireRed-AED 35.7, SenseVoice 36.3, Qwen3-ASR 37.1, Paraformer 40.0. Its level is inflated (meeting is 48–55 for every engine because its GT drops unintelligible and overlapping stretches); use it for ranking only. ## Speakers with a text-only engine No open model that runs on CPU also returns speakers, so every local engine leaves speakers to the server's own voiceprint diarist. Measured on Volcengine sentences (same text, different labels; mean of the 5 cases): | Labels | 我 vs others | who is who | purity | |---|---|---|---| | online labels as the page first shows them | 84.3 | 75.8 | 91.6 | | relabel at close **without engine speaker ids** (what a local engine gets) | 86.7 | 77.4 | 94.7 | | relabel at close with engine speaker ids, guarded | 87.2 | 82.4 | 89.9 | Engine ids add 0.5 pt on "is it me" and 5 pt on who-is-who, at a cost of 4.8 pt purity: the diarist does nearly all the work, and a local engine only has to deliver text and sentence times. The open risk is sentence granularity: local rows span two speakers 9–18 % of the time when split at 。?! (cloud 0.9–2.3 %), and 2–5 % when also split at , and pauses over 0.3 s (cloud 0.1–0.3 %). That is why `ENGINE=local` cuts at both, using token times. End-to-end speaker accuracy with real local sentence cuts was not measured in this run (see the last section for how). ## Load on small hardware Assumption: one heavy user records 3–5 h of speech a day; a day that imports old recordings can reach ~21 h. On the desktop the server's own VAD and voiceprint work, with cloud ASR, averaged about 0.15 core and 1.0 GB RSS. CPU minutes per day = speech hours × 60 × RTF (4-thread wall time). Desktop and M4 are measured. **N100 and Pi 5 are extrapolated, not measured**: no published sherpa-onnx CPU RTF was found for either, so the desktop's 4-thread RTF was scaled by Geekbench 6 multi-core (desktop on 4 threads ≈ 3.2 × single-core 2226 ≈ 7100; N100 3129 → ×2.3; Pi 5 1604–2059 → ×3.4–4.4). The M4 beat the desktop per thread by 2.3–3.0×, more than Geekbench predicts, so the ARM numbers are probably pessimistic; treat them as ±50 %. LLM decoders also stream about 0.75 GB of weights per token, and on the N100 and Pi 5 memory bandwidth may bind first, so their Qwen3-ASR rows are optimistic. | Box | Engine | RTF (4 thr) | Per 5 h of speech | Per 21 h import day | Average load | RAM for ASR | |---|---|---|---|---|---|---| | 16-core desktop (Zen 3), 4 cores, measured | X-ASR / SenseVoice | 0.024 / 0.023 | 7 min | 30 min | < 0.03 core | ~0.35 GB | | Apple M4, measured | X-ASR / SenseVoice | 0.012 / 0.012 | 3.5 min | 15 min | < 0.02 core | ~0.35 GB | | Intel N100 mini PC, extrapolated | X-ASR / SenseVoice | ~0.055 | ~17 min | ~70 min | ~0.05 core | ~0.35 GB | | Raspberry Pi 5, extrapolated | X-ASR / SenseVoice | ~0.08–0.11 | ~25–32 min | ~1.7–2.3 h | ~0.1 core | ~0.35 GB | | 16-core desktop, 4 cores, measured | Qwen3-ASR-0.6B | 0.229 | 69 min | 4.8 h | 0.2 core | 1.2 GB (peak 3.4) | | Apple M4, measured | Qwen3-ASR-0.6B | 0.133 | 40 min | 2.8 h | 0.1 core | 1.2 GB (peak 2.5) | | Intel N100, extrapolated | Qwen3-ASR-0.6B | ~0.53 | ~2.6 h | ~11 h | ~0.45 core | 1.2–3.4 GB | | Raspberry Pi 5, extrapolated | Qwen3-ASR-0.6B | ~0.8–1.0 | ~4–5 h | 17–21 h, cannot catch up the same day | ~0.7–0.9 core | does not fit a 4 GB Pi beside the server | * With 1 thread for ASR (leaving the other cores to the server), X-ASR's RTF is 0.045 on the desktop and 0.015 on the M4, about 0.08 on an N100 and 0.10–0.13 on a Pi 5: still at least 8× real time. * A final piece is at most 300 s; X-ASR on an N100 needs about 15 s of CPU for it, so latency barely changes. * On small boxes the server's existing VAD and voiceprint work is likely to outweigh ASR; that baseline was not measured on ARM or the N100. LLM-decoder ASR only makes sense on M4- or desktop-class CPUs. **GPU** was not re-measured; one user doesn't need one. On an RTX 2060 Super 8 GB, FireRedASR2-AED fp32 ran 5.6–30× real time in about 5.5 GB VRAM (the GPU row above), and an earlier test on an 11.7 min podcast took 83–98 s with Fun-ASR-Nano (about 4 GB VRAM), 166 s with Qwen3-ASR-1.7B (degraded in its second half) and 8 s with SenseVoice. ## Models Benchmarked (release dates of the models; published CERs use different normalisation, so don't compare them here): * [X-ASR-zh-en](https://github.com/Gilgamesh-J/X-ASR) zipformer transducer, 2026-05-23, Apache-2.0, 0.16B; offline and streaming sherpa builds with punctuation. * [SenseVoice-Small](https://huggingface.co/FunAudioLLM/SenseVoiceSmall), 2024-07, custom [FunASR Model License](https://github.com/modelscope/FunASR/blob/main/MODEL_LICENSE) v1.1, 234M. * [Qwen3-ASR-0.6B](https://huggingface.co/Qwen/Qwen3-ASR-0.6B), 2026-01-29, Apache-2.0. * [Fun-ASR-Nano-2512](https://huggingface.co/FunAudioLLM/Fun-ASR-Nano-2512), 2025-12, Apache-2.0, 0.8B (and its CTC head, sherpa `sense-voice-funasr-nano-int8-2025-12-17`). * [FireRedASR2](https://github.com/FireRedTeam/FireRedASR2S), 2026-02, Apache-2.0 (CTC int8 on CPU, AED on GPU). * Paraformer-large zh, 2023, Apache-2.0 (sherpa build 2024-03-09). Skipped: Whisper large-v3-turbo (weak on Chinese, mixes simplified and traditional output, hallucinates on silence); Dolphin base/small (published KeSpeech 10.9 vs Qwen3-ASR-0.6B 7.1); GLM-ASR-Nano-2512 (4.5 GB; an earlier test collapsed on an 11.7 min file); MOSS-Transcribe-Diarize (gives speakers, GPU-only in practice); VibeVoice-ASR (gives speakers, published AISHELL-4 WER 21–27); 7–9B audio LLMs (GPU only). sherpa-onnx model pages: , . ## Run it on your own recordings The scorer and replay tools are in `server/` (run `uv sync` there first). Lay out your cases like this: ``` $CASES_DIR/ / meta.json # for replay_case.py: {"audio": "", "device": "", "pieces": [{"offset_s": 0, "wall": ""}, ...]} gt.jsonl # ground truth, one sentence per line: start, end (wall-clock ISO), speaker, text gt.until # optional: one ISO time; GT from there on is untrusted and dropped hyp/.jsonl # hypotheses, same shape as gt.jsonl ``` In `gt.jsonl` the wearer is `我`; give every other person their own label. Hypothesis speakers may be `我` / `他人` or anonymous labels (`S1`, `SPEAKER_00`, …). * `uv run python score_case.py --gt /gt.jsonl --hyp /hyp/.jsonl [--gt-until ]` scores one hypothesis: 我-vs-others accuracy, who-is-who accuracy after a one-to-one label mapping, coverage, mixed rows, count ratio, CER per 60 s window (a different, higher scale than the tables above) and latency when present. * `uv run python replay_case.py engine-pieces` replays a case's audio through the server's final layer as it runs now (squeeze, lift, `PROVIDERS[ENGINE]`, diarist) and writes `/hyp/engine-pieces.jsonl`; `HYP_NAME` renames it. Load the server's `.env` first (`set -a; . .env; set +a`); it needs your voiceprint bank (`OWNER_BANK`, an `owner-redimnet2.npz`). Run it with `ENGINE=local` to get the end-to-end speaker numbers this benchmark did not measure. Its case loader currently also requires `STEPFUN_API_KEY` to be set. * `CASES_DIR=/path/to/cases ./eval_all.sh` scores every `hyp/*.jsonl` of every case against its `gt.jsonl`, honouring `gt.until`. The in-sentence and whole CER of the tables came from a separate benchmark scorer that is not in the repository; `score_case.py`'s 60 s-window CER gives the same ranking. --- --- url: https://suyiiyii.github.io/alldayrec/explanation/asr-benchmark.md --- # ASR benchmark: local engines vs cloud > 中文摘要:2026-10-23 在 5 段真实录音(共 1.77 小时)上,比较了能在 CPU 上跑的开源语音识别模型和几家云端引擎。 > 最好的本地模型 X-ASR(`ENGINE=local` 用的就是它)和 SenseVoice-Small 句内字错率约 19.6,默认的云端千问是 13.5,差约 6 个点,和千问之前用的阶跃 SSE(18.2)相当。 > 速度不是问题:X-ASR 在 4 核上 RTF 0.024,一天 5 小时的语音约 7 分钟算完;N100、树莓派 5(推算)约 17 分钟、25–32 分钟。 > 带大模型解码器的模型(Qwen3-ASR-0.6B)在 CPU 上不划算:不更准,算力约 10 倍;sherpa-onnx 要用 2.0.0 及以上。 > 参考答案(GT)偏向部分厂商,数字适合排序、不宜当绝对值;方法、注意事项和在自己的录音上复跑的办法见下文。 Measured on 2026-10-23. The question: can an open-weight model on a CPU replace the cloud engine of the final transcript? Everything ran locally on five private recordings; no audio or ground truth is published, only the numbers. ## Key findings * **Local is about 6 CER points behind the default cloud engine.** Mean in-sentence CER over the 5 cases: X-ASR-zh-en int8 **19.7**, SenseVoice-Small 2024 int8 **19.5**, against Bailian qwen-audio-3.1 (`ENGINE=qwen`) 13.5, Volcengine 12.1 and StepFun SSE 18.2 (the cloud engine used before qwen). So local is roughly StepFun-SSE class. * **Speed is not the problem.** X-ASR runs at RTF 0.024 on 4 threads of a 16-core desktop (Zen 3) and 0.012 on an Apple M4: 7 min and 3.5 min of decoding for a day with 5 h of speech. An N100 or a Raspberry Pi 5 (extrapolated) needs about 17 min and 25–32 min. * **LLM-decoder models did not pay off on CPU.** Qwen3-ASR-0.6B: 20.9 CER at 10× the compute and 1.2 GB RAM after load. Fun-ASR-Nano-2512 could not be evaluated (setup failure, see the notes). * **The sherpa-onnx version matters.** On the home case 2.0.0 lifts Qwen3-ASR by about 2 points over 1.13.8 and partly fixes Fun-ASR-Nano; CTC and transducer models barely move. The server pins `sherpa-onnx>=2.0.0`. * **`ENGINE=local` uses X-ASR** (Apache-2.0, 168 MB on disk from a ~136 MB tarball, about 330 MB RAM, token times and cased punctuation). SenseVoice-Small ties on the mean and is best on the most trustworthy case, but has a custom licence and weaker punctuation. ## Method **Cases.** Five real-life recordings with ground truth, 1.77 h of audio in total (6358 s, about 21 min per case on average), of which 4789 s is speech: | Case | What it is | Where its GT came from | |---|---|---| | home | at home | human-adjudicated from Doubao, StepFun and Volcengine candidates (the most trustworthy) | | meeting | a meeting | Doubao | | dji | outdoor walk, DJI wireless mic | Doubao | | phone | the phone's own mic | Doubao | | evening | an evening recording | StepFun | **GT bias, read before the numbers.** Meeting, dji and phone GT came from Doubao, which favours Volcengine and Doubao. Evening GT came from StepFun, which favours StepFun SSE (its 7.6 there). Home was adjudicated by a person, but from Doubao, StepFun and Volcengine candidates. Every engine outside that set, which includes every local model, is slightly penalised everywhere. On home alone: SenseVoice 16.5, Paraformer 19.0, X-ASR 19.7, Qwen3-ASR 20.6, against qwen 11.6 and Volcengine 7.3. **What the model sees.** Each case is cut in pauses into blocks of at most 5 min, the same blocks as an earlier cloud comparison. Inside a block the server's own loose VAD (`segmenter.Segmenter`, `SQUEEZE_VAD` 0.3) finds the speech, padded 0.2 s, merged across gaps under 0.5 s and cut to at most 20 s at the quietest 100 ms. One decode per segment; that is what `ENGINE=local` does inside a window. **Runtime.** sherpa-onnx int8 builds on CPU, version 1.13.8 on the desktop (the newest wheel on the mirror used) and 2.0.0 on the M4. **CER.** Text is NFKC-normalised, lowercased, letters and digits only. * *In-sentence CER* counts hypothesis characters whose time falls inside a GT sentence ±0.5 s, against all GT text. It ignores text an engine invents in silence or in stretches the GT skipped. * *Whole CER* compares all text of the blocks with all GT text, no timing involved. * The scorer reproduces the earlier cloud comparison exactly (qwen in-sentence 11.6 / 5.6 / 14.9 / 23.0 / 12.2, StepFun SSE whole 14.4 / 14.1 / 21.8 / 39.9 / 7.9), so local and cloud rows are on the same scale. * Models without token times get their characters spread evenly over the segment, which costs them slightly in in-sentence CER; whole CER is unaffected. **Mixed rows.** The share of hypothesis rows spanning two GT speakers, rows split at 。?! and pauses over 0.5 s. A row that spans two people gets one speaker label, so this bounds speaker accuracy for text-only engines. **Speed and memory.** * Desktop: `nice -n 19`, pinned to 4 physical cores, `num_threads=4`; 1-thread runs on one core. At times two jobs ran at once on different CCDs. * M4: home case only, on a machine busy with other work (load 3–9). * RTF = decode seconds / speech seconds decoded. Per second of recorded audio it is ×0.75. * "RSS after load" is the process RSS right after creating the recognizer. Run peaks are higher: they include about 0.4 GB of decoded case audio held by the benchmark, plus decoder state (Qwen3-ASR 3.4 GB, Fun-ASR-Nano 3.2 GB, the others 1.3–2.0 GB). Load time is with a warm page cache. ## Results Per-case order: home / meeting / dji / phone / evening. CER and mixed rows in %. | Engine (int8, sherpa-onnx, CPU) | In-sentence CER per case | **Mean** | Whole CER | Mixed rows | RTF 4 thr, desktop | RTF 1 thr, desktop | RTF 4 / 1 thr, M4 | Load s | RSS after load | Disk | |---|---|---|---|---|---|---|---|---|---|---| | **X-ASR-zh-en** zipformer, punct, 2026-06-03 | 19.7 / 8.6 / 25.3 / 31.2 / 13.7 | **19.7** | **21.9** | 11.2 | 0.024 | 0.045 | 0.012 / 0.015 | 2.2 | 328 MB | 168 MB | | **SenseVoice-Small** 2024-07-17 | **16.5** / 9.8 / 24.9 / 30.0 / 16.1 | **19.5** | 24.3 | 18.2 | 0.023 | 0.074 | 0.012 / 0.032 | 1.1 | 356 MB | 229 MB | | Qwen3-ASR-0.6B (LLM decoder) | 20.6 / 9.8 / 28.2 / 29.9 / 15.8 | 20.9 | 24.1 | 9.3 | 0.229 | 0.587 | 0.133 / – | 3.8 | 1214 MB | 942 MB | | Fun-ASR-Nano encoder + CTC head | 20.3 / 11.4 / 27.0 / 33.7 / 17.5 | 22.0 | 27.2 | 30.9 (no punct) | 0.026 | – | – | 1.2 | 391 MB | 253 MB | | SenseVoice 2025-09-09 (Cantonese fine-tune, no punct) | 20.9 / 10.8 / 30.9 / 36.5 / 16.9 | 23.2 | 26.1 | 31.6 | 0.020 | – | – | 1.4 | – | 227 MB | | FireRedASR2 CTC | 20.3 / 13.8 / 27.8 / 38.3 / 17.5 | 23.5 | 28.2 | 30.6 (no punct) | 0.210 | – | – | 2.0 | 949 MB | 742 MB | | Paraformer-large zh 2024-03-09 (no punctuation model applied) | 19.0 / 11.1 / **46.9** / 30.3 / 18.0 | 25.1 | 27.4 | 33.9 | 0.017 | – | – | 1.7 | 452 MB | 217 MB | | Fun-ASR-Nano-2512 (LLM decoder): **setup failure, not a model result** | 845 / 132 / 231 / 354 / 301 | (372) | (427) | – | 0.345 | – | – | 6.0 | 1221 MB | 963 MB | | *FireRedASR2-AED fp32 on a **GPU** (earlier run, rescored)* | 18.2 / 8.8 / 24.7 / 33.7 / 17.8 | 20.6 | 23.7 | 9.5 | GPU: 5.6–30× real time, 5.5 GB VRAM | | | | | | Cloud references, same blocks and same scorer: | Cloud engine | In-sentence CER per case | **Mean** | Whole CER | Mixed rows | Price per hour of audio | |---|---|---|---|---|---| | Volcengine Seed-ASR 2.0 (earlier run; whole CER not computed) | 7.3 / 4.8 / 13.1 / 23.0 / 12.2 | 12.1 | – | – | – | | **Bailian qwen-audio-3.1 file transcription** (`ENGINE=qwen`, the default) | 11.6 / 5.6 / 14.9 / 23.0 / 12.2 | 13.5 | 16.7 | 0.9 | ~¥0.05 | | Tencent Cloud 16k_zh_en_2.0 | 11.5 / 6.3 / 16.8 / 19.0 / 14.9 | 13.7 | 16.8 | 2.0 | ~¥0.8 | | Bailian fun-asr | 18.0 / 7.6 / 20.5 / 17.2 / 12.8 | 15.2 | 19.0 | 1.6 | ~¥0.49 | | StepFun SSE (the cloud engine used before qwen) | 13.6 / 7.7 / 22.4 / 39.8 / 7.6 | 18.2 | 19.6 | 2.3 | ¥0.15 | | StepFun file mode, 16 kHz Opus upload | 19.8 / 10.5 / 24.6 / 43.7 / 9.7 | 21.7 | 23.1 | 1.7 | ¥0.15 | Notes: * **X-ASR and SenseVoice tie on in-sentence CER** (19.7 vs 19.5). X-ASR is better on whole CER (21.9 vs 24.3), meeting and evening; SenseVoice on home and phone. X-ASR's punctuation splits rows better (mixed 11.2 % vs 18.2 %), it is faster on one thread (0.045 vs 0.074) and it is Apache-2.0; SenseVoice has a custom licence. * **Qwen3-ASR-0.6B, the only LLM-decoder model that worked, is no better** (20.9 / 24.1) at 10× the compute and 3.4× the RAM. Its sherpa build gives no token times, which costs it a little in-sentence but not whole. * **Fun-ASR-Nano-2512** (sherpa-onnx 1.13.8, default prompt, greedy) is **a setup failure, not a model result.** On home it produced 16,306 characters against the GT's 1,406, mostly Latin letters and repeat loops. sherpa's own bundled clean Mandarin clips looped the same way ("to be used to be used …"), so the runtime or the file pairing is at fault, not the weights. Its CTC head alone is sane (22.0). Published results rank it top for far-field audio; worth a retry. * **sherpa-onnx 2.0.0 cross-check** (M4, home case only; the main table is 1.13.8): | Model | In-sentence CER, 1.13.8 → 2.0.0 | Whole CER, 1.13.8 → 2.0.0 | |---|---|---| | SenseVoice 2024 | 16.5 → 16.1 | 21.1 → 20.9 | | X-ASR | 19.7 → 19.3 | 23.2 → 22.2 | | Qwen3-ASR-0.6B | 20.6 → **18.4** | 24.1 → 22.4 | | Fun-ASR-Nano | 845 → **45.8** | → 55.9 | Fun-ASR-Nano still loops on 2.0.0 (2,368 characters for 1,406, of which 763 in repeat runs). The LLM decoders gain most from the newer runtime, so pin ≥ 2.0.0. * **Paraformer** is worst on the outdoor dji walk (46.9), and this build returns no token times. * **Package-name traps.** sherpa's `sense-voice … 2025-09-09` build is a Cantonese fine-tune with no punctuation (the row above); its `paraformer-zh … 2025-10-07` build is WSChuan, a Sichuanese fine-tune (30.9 mean, dropped). Use `sense-voice … 2024-07-17` and the 2024-03-09 Paraformer. * **Token times.** Real per-token times from X-ASR, SenseVoice 2024, FireRed CTC and the Nano CTC head; none from Qwen3-ASR or the Paraformer 2024-03-09 build. * No traditional or Cantonese characters appeared in any local output. * **`score_case.py`'s own CER** (60 s windows) ranks the engines the same way: qwen 29.4, StepFun SSE 31.7, X-ASR 34.6, FireRed-AED 35.7, SenseVoice 36.3, Qwen3-ASR 37.1, Paraformer 40.0. Its level is inflated (meeting is 48–55 for every engine because its GT drops unintelligible and overlapping stretches); use it for ranking only. ## Speakers with a text-only engine No open model that runs on CPU also returns speakers, so every local engine leaves speakers to the server's own voiceprint diarist. Measured on Volcengine sentences (same text, different labels; mean of the 5 cases): | Labels | 我 vs others | who is who | purity | |---|---|---|---| | online labels as the page first shows them | 84.3 | 75.8 | 91.6 | | relabel at close **without engine speaker ids** (what a local engine gets) | 86.7 | 77.4 | 94.7 | | relabel at close with engine speaker ids, guarded | 87.2 | 82.4 | 89.9 | Engine ids add 0.5 pt on "is it me" and 5 pt on who-is-who, at a cost of 4.8 pt purity: the diarist does nearly all the work, and a local engine only has to deliver text and sentence times. The open risk is sentence granularity: local rows span two speakers 9–18 % of the time when split at 。?! (cloud 0.9–2.3 %), and 2–5 % when also split at , and pauses over 0.3 s (cloud 0.1–0.3 %). That is why `ENGINE=local` cuts at both, using token times. End-to-end speaker accuracy with real local sentence cuts was not measured in this run (see the last section for how). ## Load on small hardware Assumption: one heavy user records 3–5 h of speech a day; a day that imports old recordings can reach ~21 h. On the desktop the server's own VAD and voiceprint work, with cloud ASR, averaged about 0.15 core and 1.0 GB RSS. CPU minutes per day = speech hours × 60 × RTF (4-thread wall time). Desktop and M4 are measured. **N100 and Pi 5 are extrapolated, not measured**: no published sherpa-onnx CPU RTF was found for either, so the desktop's 4-thread RTF was scaled by Geekbench 6 multi-core (desktop on 4 threads ≈ 3.2 × single-core 2226 ≈ 7100; N100 3129 → ×2.3; Pi 5 1604–2059 → ×3.4–4.4). The M4 beat the desktop per thread by 2.3–3.0×, more than Geekbench predicts, so the ARM numbers are probably pessimistic; treat them as ±50 %. LLM decoders also stream about 0.75 GB of weights per token, and on the N100 and Pi 5 memory bandwidth may bind first, so their Qwen3-ASR rows are optimistic. | Box | Engine | RTF (4 thr) | Per 5 h of speech | Per 21 h import day | Average load | RAM for ASR | |---|---|---|---|---|---|---| | 16-core desktop (Zen 3), 4 cores, measured | X-ASR / SenseVoice | 0.024 / 0.023 | 7 min | 30 min | < 0.03 core | ~0.35 GB | | Apple M4, measured | X-ASR / SenseVoice | 0.012 / 0.012 | 3.5 min | 15 min | < 0.02 core | ~0.35 GB | | Intel N100 mini PC, extrapolated | X-ASR / SenseVoice | ~0.055 | ~17 min | ~70 min | ~0.05 core | ~0.35 GB | | Raspberry Pi 5, extrapolated | X-ASR / SenseVoice | ~0.08–0.11 | ~25–32 min | ~1.7–2.3 h | ~0.1 core | ~0.35 GB | | 16-core desktop, 4 cores, measured | Qwen3-ASR-0.6B | 0.229 | 69 min | 4.8 h | 0.2 core | 1.2 GB (peak 3.4) | | Apple M4, measured | Qwen3-ASR-0.6B | 0.133 | 40 min | 2.8 h | 0.1 core | 1.2 GB (peak 2.5) | | Intel N100, extrapolated | Qwen3-ASR-0.6B | ~0.53 | ~2.6 h | ~11 h | ~0.45 core | 1.2–3.4 GB | | Raspberry Pi 5, extrapolated | Qwen3-ASR-0.6B | ~0.8–1.0 | ~4–5 h | 17–21 h, cannot catch up the same day | ~0.7–0.9 core | does not fit a 4 GB Pi beside the server | * With 1 thread for ASR (leaving the other cores to the server), X-ASR's RTF is 0.045 on the desktop and 0.015 on the M4, about 0.08 on an N100 and 0.10–0.13 on a Pi 5: still at least 8× real time. * A final piece is at most 300 s; X-ASR on an N100 needs about 15 s of CPU for it, so latency barely changes. * On small boxes the server's existing VAD and voiceprint work is likely to outweigh ASR; that baseline was not measured on ARM or the N100. LLM-decoder ASR only makes sense on M4- or desktop-class CPUs. **GPU** was not re-measured; one user doesn't need one. On an RTX 2060 Super 8 GB, FireRedASR2-AED fp32 ran 5.6–30× real time in about 5.5 GB VRAM (the GPU row above), and an earlier test on an 11.7 min podcast took 83–98 s with Fun-ASR-Nano (about 4 GB VRAM), 166 s with Qwen3-ASR-1.7B (degraded in its second half) and 8 s with SenseVoice. ## Models Benchmarked (release dates of the models; published CERs use different normalisation, so don't compare them here): * [X-ASR-zh-en](https://github.com/Gilgamesh-J/X-ASR) zipformer transducer, 2026-05-23, Apache-2.0, 0.16B; offline and streaming sherpa builds with punctuation. * [SenseVoice-Small](https://huggingface.co/FunAudioLLM/SenseVoiceSmall), 2024-07, custom [FunASR Model License](https://github.com/modelscope/FunASR/blob/main/MODEL_LICENSE) v1.1, 234M. * [Qwen3-ASR-0.6B](https://huggingface.co/Qwen/Qwen3-ASR-0.6B), 2026-01-29, Apache-2.0. * [Fun-ASR-Nano-2512](https://huggingface.co/FunAudioLLM/Fun-ASR-Nano-2512), 2025-12, Apache-2.0, 0.8B (and its CTC head, sherpa `sense-voice-funasr-nano-int8-2025-12-17`). * [FireRedASR2](https://github.com/FireRedTeam/FireRedASR2S), 2026-02, Apache-2.0 (CTC int8 on CPU, AED on GPU). * Paraformer-large zh, 2023, Apache-2.0 (sherpa build 2024-03-09). Skipped: Whisper large-v3-turbo (weak on Chinese, mixes simplified and traditional output, hallucinates on silence); Dolphin base/small (published KeSpeech 10.9 vs Qwen3-ASR-0.6B 7.1); GLM-ASR-Nano-2512 (4.5 GB; an earlier test collapsed on an 11.7 min file); MOSS-Transcribe-Diarize (gives speakers, GPU-only in practice); VibeVoice-ASR (gives speakers, published AISHELL-4 WER 21–27); 7–9B audio LLMs (GPU only). sherpa-onnx model pages: , . ## Run it on your own recordings The scorer and replay tools are in `server/` (run `uv sync` there first). Lay out your cases like this: ``` $CASES_DIR/ / meta.json # for replay_case.py: {"audio": "", "device": "", "pieces": [{"offset_s": 0, "wall": ""}, ...]} gt.jsonl # ground truth, one sentence per line: start, end (wall-clock ISO), speaker, text gt.until # optional: one ISO time; GT from there on is untrusted and dropped hyp/.jsonl # hypotheses, same shape as gt.jsonl ``` In `gt.jsonl` the wearer is `我`; give every other person their own label. Hypothesis speakers may be `我` / `他人` or anonymous labels (`S1`, `SPEAKER_00`, …). * `uv run python score_case.py --gt /gt.jsonl --hyp /hyp/.jsonl [--gt-until ]` scores one hypothesis: 我-vs-others accuracy, who-is-who accuracy after a one-to-one label mapping, coverage, mixed rows, count ratio, CER per 60 s window (a different, higher scale than the tables above) and latency when present. * `uv run python replay_case.py engine-pieces` replays a case's audio through the server's final layer as it runs now (squeeze, lift, `PROVIDERS[ENGINE]`, diarist) and writes `/hyp/engine-pieces.jsonl`; `HYP_NAME` renames it. Load the server's `.env` first (`set -a; . .env; set +a`); it needs your voiceprint bank (`OWNER_BANK`, an `owner-redimnet2.npz`). Run it with `ENGINE=local` to get the end-to-end speaker numbers this benchmark did not measure. Its case loader currently also requires `STEPFUN_API_KEY` to be set. * `CASES_DIR=/path/to/cases ./eval_all.sh` scores every `hyp/*.jsonl` of every case against its `gt.jsonl`, honouring `gt.until`. The in-sentence and whole CER of the tables came from a separate benchmark scorer that is not in the repository; `score_case.py`'s 60 s-window CER gives the same ranking. --- --- url: https://suyiiyii.github.io/alldayrec/en/reference/cli.md --- # CLI: lr.py [`cli/lr.py`](https://github.com/suyiiyii/alldayrec/blob/main/cli/lr.py) reads your conversations in a terminal or an agent. Its output is compact Markdown meant for agents; `--json` gives the raw JSON of the API. It only reads data and uses only the Python standard library. ## Run ```bash uv run --no-project cli/lr.py [args] ``` The file starts with `#!/usr/bin/env -S uv run --script`, so with uv installed you can also run `cli/lr.py ` directly. ## Server and token `lr.py` reads them in this order: 1. The environment variables `LIVE_RECORD_URL` (the server address) and `LIVE_RECORD_TOKEN` (the tenant token); 2. The lines with the same names in `live-record.env`, in the same directory as `lr.py`. ## Commands | Command | Arguments | Output | |---|---|---| | `day` | `[date]` | A day's conversations and summaries | | `search` | `[query] [--who WHO] [--tag TAG] [--since SINCE] [--until UNTIL] [--limit LIMIT]` | Finds conversations by word (titles, summaries, tags, to-dos, and the transcript when those have no match), participant, tag and date, newest first; `--who` can be `我` (me), `S1` or a recognized name | | `show` | `id [--text]` | One conversation: each person's points, topics, to-dos, decisions; `--text` adds the transcript with speakers | | `at` | `span [date]` | The transcript of a time span, with `span` written as `HH:MM-HH:MM` | | `todos` | `[--since SINCE] [--until UNTIL] [--owner OWNER]` | To-dos from the summaries of these days' conversations, newest first; the last 7 days by default; `--owner 我` shows only your own | | `names` | — | Recognized names, by number of conversations, most first | | `days` | — | Dates with recordings, newest first | You can write any date argument as `YYYY-MM-DD`, `MM-DD`, `today`, `yesterday` or `-N` (N days ago). Put `--json` before the command: `lr.py --json day today`. ## Example ```bash export LIVE_RECORD_URL=http://127.0.0.1:8770 LIVE_RECORD_TOKEN= uv run --no-project cli/lr.py at 09:30-09:32 2026-10-20 ``` ```text # 2026-10-20 09:30-09:32 7 句 09:30:59 S?:大家好, 09:31:00 S?:今天下午三点我们在三楼会议室开会, 09:31:04 S1:讨论下个月的发布计划, ... ``` --- --- url: https://suyiiyii.github.io/alldayrec/en/reference/configuration.md --- # Configuration The server's settings live in two places: `tenants.json` lists the people it serves, and environment variables (usually in `.env`) set everything else. When a required setting is missing, the server refuses to start and writes the missing ones to its log. ::: info Version This page follows the main branch. v0.1.0 has no `UI_LANG` and no `lang` field in `tenants.json`; up to v0.2.0, `LOOP_STALL_MS` is off by default. Everything else is the same. ::: ## tenants.json A JSON array with one entry per person (tenant). Its permissions must be 600. ```json [{"id": "main", "token": "", "data": "/data/main", "recordings": "/data/main/recordings"}] ``` | Field | Description | |---|---| | `id` | The tenant name, used in logs and metrics | | `token` | The token the app and the web pages send (`Authorization: Bearer` or a cookie) | | `data` | The data directory: `live.db` and more, see [Data layout](./data.md) | | `recordings` | The recordings directory | | `lang` | Optional. The language the LLM writes this person's content in (summaries, to-dos, live analysis, the daily recap, "ask AI" answers and more): `zh` or `en`. Without it, `UI_LANG` applies, and without `UI_LANG`, `zh`. Once the language is switched on the web page 「我的」 (Me) (`PUT /api/lang`), that choice wins: it is kept in this person's database and survives a restart | * Location: `DATA_DIR/tenants.json` (see `DATA_DIR` below). In the Docker image `DATA_DIR` is `/data`, which makes it `data/tenants.json` on the host. * When the file is missing, the server's first start makes one: one tenant `main` with a random token and the directories `DATA_DIR/main` and `DATA_DIR/main/recordings`, and logs the link that signs you in, `http://127.0.0.1:/connect?token=`. * `id`, `token`, `data` and `recordings` must each be unique across tenants. * With a single tenant the token can be empty. The server then skips authentication and must stay on `127.0.0.1`. With several tenants, each one needs a token. * The server reads this file only at startup. ## .env [`.env.example`](https://github.com/suyiiyii/alldayrec/blob/main/.env.example) lists every variable below, each with a comment. "Example value" is the value in `.env.example`, your starting point once you copy it to `.env`; "When unset" is the default in the code when the variable doesn't exist at all. Put each comment in `.env` on a line of its own: Docker Compose reads `VAR= # comment` as the value `# comment`. ### Required: ASR | Variable | Example value | When unset | Description | |---|---|---|---| | `ENGINE` | `qwen` | `qwen` | The ASR engine of the final layer: `qwen`, `volc`, `stepfun-sse`, `stepfun`, `stepfun-file`, `local`. See [Change the ASR engine](../how-to/change-asr-engine.md) | | `DASHSCOPE_API_KEY` | empty | — | Alibaba Cloud Bailian (DashScope) key, Beijing region. Used by `qwen` and `qwen-stream` | ### Required: LLM Used for conversation summaries, to-dos, "ask AI" and the daily recap. Any OpenAI-compatible `POST /chat/completions` endpoint that supports tool calls and `response_format` `json_object` works: a cloud service, or one you run yourself (vLLM, llama.cpp, Ollama). | Variable | Example value | When unset | Description | |---|---|---|---| | `LLM_BASE_URL` | `https://dashscope.aliyuncs.com/compatible-mode/v1` | required | The endpoint; the server appends `/chat/completions` | | `LLM_API_KEY` | empty | required | The endpoint's key; with the example's Bailian compatible mode it is the same key as `DASHSCOPE_API_KEY` | | `LLM_MODEL` | `qwen-plus` | required | The model name | ### Optional | Variable | Example value | When unset | Description | |---|---|---|---| | `DATA_DIR` | empty | the user data directory: `~/.local/share/alldayrec` on Linux (`alldayrec` under `XDG_DATA_HOME` when that is set), `~/Library/Application Support/alldayrec` on macOS | The directory `tenants.json` is in; without `MODELS_DIR` the model files go in its `models`, and the tenant a first start makes in its `main`. Fixed at `/data` in Docker | | `MODELS_DIR` | empty | `DATA_DIR/models` | The model files directory, an absolute, writable path; missing files are downloaded here at startup. Fixed at `/data/models` in Docker | | `PORT` | `8770` | `8770` | The port to listen on. Docker Compose fixes it at `8770` | | `HOST` | `127.0.0.1` | `127.0.0.1` | The address to listen on; set `0.0.0.0` when the reverse proxy runs on another machine. Docker Compose fixes it at `0.0.0.0` | | `DRAFT_ENGINE` | empty | same as `ENGINE` | The engine of the draft layer (while a page is open); it can also be `qwen-stream`: while a page is open, the audio streams to Qwen (Bailian qwen-audio-3.1-asr-flash-streaming, with `DASHSCOPE_API_KEY`), so words show as they are said | | `ENGINE_FALLBACK` | empty | none | The engine used when `ENGINE` or `DRAFT_ENGINE` fails on a window (402, 451, 5xx, timeout); empty means a retry later | | `VOLC_ASR_KEY` | empty | — | The API key of a Volcengine Ark Agent Plan, used by `volc` | | `STEPFUN_API_KEY` | empty | — | StepFun key, used by `stepfun` and `stepfun-sse` | | `STEPFUN_SSE_URL` | empty | `https://api.stepfun.com/v1/audio/asr/sse` | The StepFun SSE endpoint; the default is the pay-as-you-go address; for Step Plan use `.../step_plan/v1/audio/asr/sse` | | `LOCAL_ASR_URL` | empty | the k2-fsa/sherpa-onnx GitHub Release | Where `local` downloads its model tarball (about 136 MB) from; the same tarball on a mirror works too (sha256-checked) | | `UI_LANG` | `zh` | `zh` | The language of what the LLM writes (summaries, to-dos, live analysis, the daily recap, "ask AI" answers and more): `zh` or `en`. A `lang` in `tenants.json` wins. The web pages use it too in a browser that hasn't picked a language; the app's UI doesn't change | | `RECAP_AT` | `10:00` | `10:00` | The time of day the server writes yesterday's recap; set it empty for no recap | | `TZ` | `Asia/Shanghai` | the system time zone | The time zone for conversation dates and `RECAP_AT`. A Docker container's system time zone is UTC; an empty `TZ=` means UTC too | | `REFINE_EVERY_S` | `20` | `30` | Draft layer: run once after this many seconds without a pause | | `TURN_GAP_S` | `0` | `1.0` | Above 0, joins adjacent sentences of the same speaker less than this many seconds apart into one line; 0 keeps the engine's own sentence split | | `FIRMWARE_TOKEN` | empty | none | The token needed, besides the tenant token, to upload recorder firmware (`firmware/devkit/lr_release.py`); empty means no uploads | | `VM_INGEST_URL` | empty | none | A VictoriaMetrics import URL (e.g. `https://example.com/api/v1/import/prometheus`); the server pushes `lr_*` metrics to it every minute; empty means no push | | `VM_INGEST_USER`, `VM_INGEST_PASSWORD` | empty | empty | Basic auth for pushing metrics | ### Advanced: only for `stepfun` and `stepfun-file` When `stepfun` or `stepfun-file` appears in `ENGINE`, `DRAFT_ENGINE` or `ENGINE_FALLBACK`, all four `ASR_*` variables below are required. | Variable | Example value | When unset | Description | |---|---|---|---| | `ASR_UPLOAD_BRIDGE_URL` | empty | — | Your own upload bridge: `POST {filename, content_type}`, returns `{upload_url, download_url}` | | `ASR_UPLOAD_BRIDGE_TOKEN` | empty | — | The upload bridge's token | | `ASR_SERVICE_URL` | empty | — | Your own file-mode ASR service (with speakers and word times): `POST /audio/transcriptions {audio_url, model}`, `GET /jobs/{id}` | | `ASR_SERVICE_BASIC_AUTH` | empty | — | That service's basic auth, written as `user:password` | | `FILE_MODELS` | `stepaudio-2.5-asr-8006,doubao-seedasr-2.0` | same as the example value | The file-mode engines, in order; a window the first one rejects goes to the second | ### Only for offline scripts | Variable | Used in | Description | |---|---|---| | `LIVE_DATA`, `RECORDINGS_DIR` | `owner.py`, `replay_case.py` | A tenant's data directory and recordings directory | | `OWNER_LABELS` | `owner.py` | Windows known to be your own voice (jsonl) | | `CASES_DIR` | `eval_all.sh` | The directory of labeled evaluation cases | | `EMB_MODEL`, `SEG_MODEL`, `OWNER_SURE`, `LINK_MIN`, `OWNER_DEFAULT_DEVICE` | `replay_case.py`, `service_case.py` | See those two scripts | ## Other environment variables The server also reads the variables below, which `.env.example` doesn't list. They are for tuning, and their defaults are the values the author uses. | Variable | When unset | Description | Source | |---|---|---|---| | `SLICE_FOREGROUND_S`, `SLICE_BACKGROUND_S`, `SLICE_SCREEN_OFF_S` | `1`, `20`, `60` | Seconds of audio per piece while the app is in the foreground, in the background, or with the screen off (Android); the app reads them from `/api/client-config` | `routes_upload.py` | | `REFINE_WINDOW_S` | `240` | The most seconds of audio the draft layer handles at once | `device_pipeline.py` | | `LIVE_EVERY_S` | `120` | The live analysis is redone each time an ongoing conversation gains this many seconds of speech | `device_pipeline.py` | | `SQUEEZE_KEEP_S` | `3` | Before audio goes to the engine, silences longer than this many seconds are cut to this length; 0 sends the audio whole | `asr.py` | | `SQUEEZE_VAD` | `0.3` | The VAD threshold that finds those silences | `segmenter.py` | | `SUMMARY_WORKERS` | `4` | How many summaries are written at once | `summaries.py` | | `QWEN_ASR_MODEL` | `qwen-audio-3.1-asr-flash-filetrans` | The Bailian model the `qwen` engine uses | `qwen_asr.py` | | `ASR_CALLS`, `INFER_CALLS`, `DB_CALLS`, `MEDIA_CALLS` | `1`, `2`, `8`, `16` | How many local ASR runs, voiceprint computations, whole-database queries and audio encodes or decodes run at once | `threads.py` | | `INFER_THREADS` | `4` | onnxruntime threads per voiceprint computation | `infer.py` | | `MUSIC_DETECT` | `1` | At finalization, a sentence heard as music (a song played aloud, the voice about as loud as its accompaniment) gets no voiceprint: its speaker is `S?` and it never reaches the People page. It is still transcribed, with a music-note icon on its row. Fixing its speaker to someone makes it a voice: the icon goes and its voiceprint is learnt as usual. 0 turns it off | `music.py` | | `VAD_GAIN_MAX_DB` | `16` | The most dB the VAD's features are raised (this affects only speech detection, not the audio sent to the engine or played back); 0 turns it off | `infer.py` | | `ASR_GAIN_MAX_DB` | `16` | The most dB the audio sent to the engine is raised; 0 sends it as is | `squeeze.py` | | `DEHUM` | `1` | Removes the recorder's 10 ms periodic hum when its audio is read; 0 turns it off | `audio.py` | | `SPEAKER_LINK`, `SPEAKER_SUGGEST`, `MERGE_SUGGEST` | `0.5`, `0.6`, `0.45` | Voiceprint similarity thresholds of the 人物 (People) page: join an anonymous group, suggest a name, suggest merging two labels in the same conversation | `routes_speakers.py` | | `DIAR_TH`, `DIAR_JOIN`, `DIAR_GUARD`, `PEOPLE_MIN` | `0.5`, `0.35`, `0.6`, `0.7` | Thresholds for telling speakers apart within a conversation | `diarist.py` | | `SELFVOICE_POP_P` | `0.95` | The probability the top candidate needs before the 「哪个是你的声音」 (Which one is your voice) card pops up by itself | `selfvoice.py` | | `LOOP_STALL_MS` | `100` | When the event loop is held up longer than this many milliseconds, the log gets one line with how long and in which function (at most a line a second, the ones in between counted), and the `perf_stalls` table a row; it also measures the loop's lag and the thread pools' use each minute. About 0.6% of a core; `0` turns it off | `monitor.py` | --- --- url: https://suyiiyii.github.io/alldayrec/en/reference/data.md --- # Data layout Each tenant has two directories, both set in `tenants.json`: `data` (the database and derived data) and `recordings` (the original audio). With Docker deployed as in the quickstart, they are `data/main` and `data/main/recordings` on the host. ## DATA_DIR `DATA_DIR` is `./data` on the host with Docker; with uvx, pip or from source it is your user data directory by default (see [Configuration](./configuration.md)). The tenant `main` that a first start makes lives under it: ```text / ├── tenants.json ├── models/ model files (MODELS_DIR) └── main/ data directory of tenant main ├── live.db SQLite database (WAL mode, with live.db-wal and live.db-shm beside it) ├── .passes.jsonl the engine's raw output from each transcription pass └── recordings/ recordings directory of tenant main └── chunks///. ``` ## The data directory | Path | Content | |---|---| | `live.db` | The SQLite database, see the table below; its schema is defined in [`server/alldayrec/db.py`](https://github.com/suyiiyii/alldayrec/blob/main/server/alldayrec/db.py) | | `.passes.jsonl` | The engine's raw output from each transcription pass | | `owner-redimnet2.npz` | The voiceprint of 「我」 (me) | | `voices/clips/` | Voice clips of the speakers | | `firmware/` | Recorder firmware packages (only in the first tenant's directory) | | `live-record.apk` | The Android app the server offers at `/app.apk` (only in the first tenant's directory) | ## The recordings directory | Path | Content | |---|---| | `chunks///.` | One file per device per hour; each piece that arrives is appended to it as is, and its position and length in the file are recorded in the `pieces` table of `live.db`. The extension follows the upload's `Content-Type`: `ogg`, `webm`, `aac`, otherwise `bin` | | `rejected//.` | Uploads that failed to decode (the endpoint returned 400), kept as they are for inspection | ## The tables of live.db | Table | Content | |---|---| | `sentences` | Final sentences: time, speaker, text, source device, engine | | `drafts` | Draft-layer sentences (produced only while a page is open), kept after the finals replace them | | `conversations` | Conversations: start and end time, participants, seconds of speech, number of sentences, type, device | | `summaries` | Each conversation's summary (JSON) | | `timelines` | The live analysis as it stood when a conversation ended | | `voices` | The voiceprint and voice clips of each speaker label in each conversation | | `apart` | Voices marked 「不是这个人」 (Not this person) | | `people` | Voiceprints of people with recognized names | | `enrolled` | The sentences each person's voiceprint was learnt from, one row each; an undo or a split takes them back out | | `aliases` | Other names of a person | | `name_hints` | What each label was called in a conversation (used only to suggest names) | | `profiles` | People cards of people with recognized names, each row with its source | | `pieces` | The index of uploaded pieces, which is also the processing queue | | `feedback` | Users' corrections to summaries | | `todo_marks` | 「完成」 (Done) or 「不对」 (Wrong) marks on to-dos (new on main, not in v0.1.0) | | `kv` | Key-value pairs such as each device's processing progress | | `device_log` | Device events uploaded by the phones, including the recorders' own logs | | `device_status` | Recorder status read by the clients (battery, card, firmware) | | `calls` | Duration, result and usage of each ASR and LLM call, without content | | `balances` | Prepaid account balances, recorded hourly | | `perf_stalls` | Times the event loop was held up: when it ended, how many milliseconds, in which functions (only in the first tenant's store; new on main) | | `perf_samples` | The process, one row a minute: CPU seconds, memory, loop lag, thread pool use, queued pieces, open pages (only in the first tenant's store; new on main) | | `perf_requests` | HTTP requests: one row for each that took 50 ms or more, and each minute one row per method, route and status (new on main) | | `perf_pieces` | How long each step of each final piece and draft pass took: decode, VAD, ASR, voiceprints, store (new on main) | The `perf_` tables hold durations and resource use only, never content, and keep 90 days. `GET /api/perf?day=YYYY-MM-DD` reads one day; `python -m alldayrec.perf export ` exports one from a copy of the store. A voiceprint learnt before `enrolled` existed is one summary row per person, not tied to sentences. If it took in someone else's voice back then, `python -m alldayrec.bank relearn ` recomputes that row from the voices now under the person's name (from live recordings). It only prints, per person, the sentences before and after, the voices used and dropped, and the cosine between the old and the new voiceprint; `--apply` writes it, and then the server needs a restart (it loads the voiceprints at start). What was learnt sentence by sentence since stays. --- --- url: https://suyiiyii.github.io/alldayrec/en/how-to/add-person.md --- # How to add another person One server can serve several people. Each person is one entry (one tenant) in `tenants.json`, with their own token, database and recordings directory. Tenants can't see each other's data. ## Docker 1. Add an entry to the array in `data/tenants.json`. Give it a new name for `id` and the directories, and a newly generated token (`openssl rand -hex 16`): ```json [ {"id": "main", "token": "", "data": "/data/main", "recordings": "/data/main/recordings"}, {"id": "alice", "token": "", "data": "/data/alice", "recordings": "/data/alice/recordings"} ] ``` `data` and `recordings` are paths inside the container. `/data` is `./data` on the host. 2. Restart the server: ```bash docker compose restart ``` The `tenants:` line in the log lists every tenant, for example `tenants: main, alice`. 3. Give the new token to that person. They open the web UI at `http(s):///?token=`, then follow [Install and connect the phone app](./install-app.md) to connect their own phone. ## Instances deployed with deploy.sh Run `server/tenants.sh add `. It adds a tenant to the last instance and creates its token and directories. The details are in the comments at the top of the script. ## Notes * The server reads `tenants.json` only at startup. Restart it after a change. * No two tenants may share an `id`, `token`, `data` or `recordings`; otherwise the server refuses to start. * `tenants.json` must have mode 600 (readable only by you); otherwise the server refuses to start. * The server skips authentication only when there is a single tenant and its token is empty. Once there is a second person, everyone needs a token. * The first tenant's data directory also holds the APK the server offers phones for download (`live-record.apk`) and the recorders' firmware packages. --- --- url: https://suyiiyii.github.io/alldayrec/en/how-to/backup.md --- # How to back up and restore Everything you need to back up is in one place: `./data` for a Docker deployment; without Docker, the `tenants.json` in `DATA_DIR` plus each tenant's `data` and `recordings` directories (the tenant `main` a first start makes is in `DATA_DIR` too). What they hold is in [Data layout](../reference/data.md). You can skip the model files (`data/models`): the server downloads missing ones again at startup. ## Back up with the server stopped (simplest) ```bash docker compose stop tar czf alldayrec-backup-$(date +%Y%m%d).tar.gz data docker compose start ``` ## Back up the database without stopping Recording files are only appended to, so you can copy them as they are. The database `live.db` is written to while the server runs, so copy it with SQLite's backup API. This command, run in the container, copies the database of the quickstart tenant `main` to `data/main/live-backup.db`: ```bash docker compose exec alldayrec python -c "import sqlite3; sqlite3.connect('/data/main/live.db').backup(sqlite3.connect('/data/main/live-backup.db'))" ``` Do this for every tenant, with the path changed to its `data` directory in `tenants.json`. ## Restore 1. Stop the server: ```bash docker compose stop ``` 2. Put the backed-up `data` directory (or one tenant's `live.db` from it) back in place. 3. Start it: ```bash docker compose start ``` --- --- url: https://suyiiyii.github.io/alldayrec/en/how-to/bind-recorder.md --- # How to bind a recorder A BLE recorder (such as [LR-P3](../reference/hardware.md)) has to be bound to a phone first. Once bound, it starts recording: when the phone is nearby, it uploads in real time; when the phone is away, it stores the audio on its card and catches up in the background once the phone is back. While unbound, it doesn't record, and the app records with the phone's mic. ## Bind 1. In the app, open **我的 › 录音设备** (Me › Recorders) and tap **添加录音设备** (Add recorder). 2. The page lists the permissions you haven't granted yet (Bluetooth or "附近的设备" (Nearby devices), notifications, running in the background). Allow each one. 3. Turn the recorder on (plugging in the charging cable is enough). Nearby recorders show up in the list. The name is printed on the recorder; when several are nearby, the one with the strongest signal is marked "最近" (Nearest). 4. Tap **绑定** (Bind) next to it. When the list marks a recorder "已绑定其他手机" (Bound to another phone), you can tap **强制绑定** (Force bind): the recorder binds to this phone, and the previous phone can no longer connect to it. When it is marked "正连着其他手机" (Connected to another phone), first unbind it on that phone, or turn off that phone's Bluetooth. ## Unbind On the **录音设备** page, tap **解除绑定** (Unbind). * With the recorder nearby: it forgets all phones and stops recording; the app first fetches the recordings on its card. This phone switches to its own mic. * With the recorder away: only this phone drops the binding. The recorder still remembers it and keeps recording. You can force-bind it later when you add it. * iPhone: the app can't delete the system's own pairing record. After unbinding, go to **Settings › Bluetooth › device name › Forget This Device**. To bind the recorder to this iPhone again later, you must forget it there first. ## Old recordings on the card after a phone change To be added: whether the audio recorded on the card before the binding gets uploaded after the recorder is rebound to another phone. If you don't have a recorder, you can build an LR-P3 yourself by following [`hardware/lr-p3/README.md`](https://github.com/suyiiyii/alldayrec/blob/main/hardware/lr-p3/README.md). --- --- url: https://suyiiyii.github.io/alldayrec/en/how-to/change-asr-engine.md --- # How to change the ASR engine `ENGINE` in `.env` decides which ASR engine makes the final transcript; the default is `qwen` (Alibaba Cloud Bailian). For how the engines differ, see [Architecture and data flow](../explanation/architecture.md#two-transcription-layers) and [ASR benchmark](../explanation/asr-benchmark.md). ## Switch engines 1. In `.env`, set `ENGINE` to one of the values in the table, and fill in the key it needs: | `ENGINE` | Needs | |---|---| | `qwen` | `DASHSCOPE_API_KEY` | | `volc` | `VOLC_ASR_KEY` | | `stepfun-sse` | `STEPFUN_API_KEY` | | `stepfun`, `stepfun-file` | `STEPFUN_API_KEY`, plus your own upload bridge and file-mode ASR service: `ASR_UPLOAD_BRIDGE_URL`, `ASR_UPLOAD_BRIDGE_TOKEN`, `ASR_SERVICE_URL`, `ASR_SERVICE_BASIC_AUTH` | | `local` | No key | 2. Restart the server. With Docker, run `docker compose up -d`: Compose recreates the container with the new `.env`. When a key is missing, the server doesn't start, and the log says `not set: (see .env.example)`. When `ENGINE` is misspelled, the log lists the valid values. Whichever engine you use, you need the three LLM settings (`LLM_BASE_URL`, `LLM_API_KEY`, `LLM_MODEL`). ## No ASR key at all: `ENGINE=local` `local` runs the X-ASR zh-en model with sherpa-onnx in the server process. It outputs text only, and speakers come from the server's own voiceprints. On far-field audio, it does worse than `qwen`. When the first audio arrives, the server downloads a model tarball of about 136 MB, so the first transcript takes about a minute. If the download is slow, set `LOCAL_ASR_URL` to the same tarball's address on a mirror; the server checks the file against its sha256. ## Drafts and fallback * `DRAFT_ENGINE`: the engine that makes drafts while a page is open; empty means the same as `ENGINE`. With `qwen-stream`, the server streams the audio to Qwen while a page is open, so words show as they are said (needs `DASHSCOPE_API_KEY`). `qwen-stream` works only as `DRAFT_ENGINE`. * `ENGINE_FALLBACK`: the engine to use when the main engine fails on a window (402, 451, 5xx, a timeout); empty means retry later. All the related settings are in [Configuration](../reference/configuration.md). --- --- url: https://suyiiyii.github.io/alldayrec/en/how-to/connect-mcp.md --- # How to connect an AI agent over MCP The server offers a remote MCP endpoint at `/mcp`. AI agents such as Claude Code, Codex, Cursor and Cherry Studio use it to search your conversations, summaries and to-dos, and to mark a to-do as done or wrong. It uses the same token as the app. ## Before you start * Once connected, the transcripts and summaries the AI finds (including what other people said) go to the AI vendor you picked. * In sessions that use this connection, don't auto-approve web or shell tools: words in a recording can be taken as instructions and use those tools to send content out. * The token reads and writes all your data on this server. Keep it on your own computer only. The **接入 AI** (Connect AI) section of the web page **连接手机** (Connect a phone, `/connect`) lists the settings below with the address and token filled in. Click **复制** (Copy). ## Claude Code Run this once in a terminal: ```bash claude mcp add --transport http alldayrec https:///mcp --header "Authorization: Bearer " --scope user ``` ## Codex Add this to `~/.codex/config.toml`: ```toml [mcp_servers.alldayrec] url = "https:///mcp" http_headers = { "Authorization" = "Bearer " } ``` ## Cursor Add this to `~/.cursor/mcp.json`: ```json { "mcpServers": { "alldayrec": { "url": "https:///mcp", "headers": { "Authorization": "Bearer " } } } } ``` ## Cherry Studio Paste this in **Settings › MCP › Add › Import from JSON**: ```json { "mcpServers": { "alldayrec": { "type": "streamableHttp", "url": "https:///mcp", "headers": { "Authorization": "Bearer " } } } } ``` ## Check the connection List the tools with curl, without an AI agent: ```bash curl -s -H "Authorization: Bearer " -H "Content-Type: application/json" \ -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}' https:///mcp ``` In the JSON that comes back, `result.tools` is the list of tools. What each tool does is in [MCP tools](../reference/mcp.md). --- --- url: https://suyiiyii.github.io/alldayrec/en/how-to/contribute.md --- # How to contribute [Issues](https://github.com/suyiiyii/alldayrec/issues) and PRs are welcome. Code boundaries, the CI workflows, release builds and their secrets are in [CONTRIBUTING.md](https://github.com/suyiiyii/alldayrec/blob/main/CONTRIBUTING.md). ## Local checks The same checks CI runs: ```bash # server/ uv run --with pytest pytest -q # web/ pnpm install --frozen-lockfile && pnpm build for f in *.check.ts; do node --experimental-strip-types "$f" || exit 1; done # app/ (use the pub host recorded in pubspec.lock) PUB_HOSTED_URL=https://pub.flutter-io.cn flutter pub get --enforce-lockfile flutter analyze --no-pub && flutter test --no-pub -j 4 ``` The Android JVM and Swift checks are in `.github/workflows/app.yml`; the firmware host checks are in `.github/workflows/firmware-check.yml`. ## Docs site The docs site lives in `docs/` and is built with VitePress: ```bash cd docs pnpm install --frozen-lockfile pnpm docs:dev # preview locally pnpm docs:build # build to docs/.vitepress/dist and check for dead links ``` The tables in [HTTP API](../reference/api.md) and [MCP tools](../reference/mcp.md) are generated by a script. After you change an endpoint, regenerate them from the repository root and commit: ```bash uv run --project server python docs/gen-api.py ``` ## Commit messages * Format `[type][Area] subject`: in English, imperative, a subject line of at most 72 characters, details in the body. * type: `feat`, `fix`, `chore`, `ci`, `perf`, `doc`, `test`; Area: `Server`, `App`, `Firmware`, `Web`, `CI` and others, which you can stack (`[fix][Server][Web]`). * No `Co-authored-by`. Binary files such as screenshots don't go into `main`: push them to the `assets` branch first (add only, never change; name them `img/-.ext`), then link them as `https://raw.githubusercontent.com/suyiiyii/alldayrec/assets/img/`. --- --- url: https://suyiiyii.github.io/alldayrec/en/how-to/deploy-sh.md --- # How to deploy with deploy.sh (maintainers) Besides Docker, the server can run as systemd units on a Linux host, deployed over ssh from a clean checkout of `origin/main`. This page is for people who maintain one or a few instances themselves. ## Prerequisites * Your machine: a clean checkout of the repository with `HEAD` equal to `origin/main`; pnpm (the script builds the web app). * The target host: you can log in over ssh, and the remote shell can run as root (the script works on systemd and the instance directories). * Each instance is a directory with its `.env` and `tenants.json`. Its `.env` sets `DATA_DIR=` (and `MODELS_DIR` when the model files are elsewhere); its systemd unit runs `/server/.venv/bin/python -m alldayrec` with the working directory `/server` and `EnvironmentFile=/.env`. Before installing anything, the script checks that the new server finds and reads every instance's `tenants.json` the way its `.env` says, and installs nothing when one fails. ## Set the environment variables ```bash export LR_DEPLOY_HOST=user@host # required: the ssh / scp target (your ssh config and agent) export LR_DEPLOY_SHELL='sudo bash -s' # default `bash -s`: the remote shell the script is piped into, as root export LR_INSTANCES='/opt/alldayrec:alldayrec' # the default: : pairs, space separated, test first ``` scp puts the release and the APK in the login user's home directory on the host. When the remote shell's `$HOME` is another directory, `LR_TGZ` and `LR_APK` tell the script their paths there. ## Deploy, check, roll back ```bash server/deploy.sh # build the web app, back up, install (uv sync), check the model files, restart and check each instance server/deploy.sh check # only read what is running server/deploy.sh rollback # put back the code and web app the last deploy replaced, drop the database columns it added, restart and check ``` Each deploy leaves a backup in `/backup-deploy-