fcitx5-ainput is a voice input addon for fcitx5 (Linux IME framework). The user holds CapsLock, speaks, releases, and the recorded audio is transcribed by an ASR backend and committed as text into the focused application. It serves users who want fast push-to-talk dictation without leaving the keyboard.
- Push-to-talk capture (CapsLock hold), status UI via fcitx5 input panel, consecutive-speech queueing (max 3 in-flight recognitions, FIFO order preserved per app/input context).
- Pluggable ASR backends behind
IAsrProvider/IAsrProviderFactoryand theAsrProviderRegistry:mock— deterministic test provider.zipformer,fire_red— local providers via externalsherpa-onnxbinaries.doubao(ByteDance),qwen(Alibaba DashScope) — cloud providers via libcurl, persistent serialized worker threads.
- Audio capture with optional denoising; cloud failures surfaced as actionable status errors (network / service / timeout / microphone / no-speech).
- Scope boundary: short-duration dictation only (≤ minutes). Long-audio file transcription (filetrans/async job APIs) is out of scope.
- Low latency and predictable UX first; diagnostics and timers are built in
(
Ainput Qwen [timer] encode/network/parselogs,diagnostic_logevents). - Every provider must degrade loudly and recoverably: cancellable requests, curl handle eviction on transport failure, actionable error strings.
- Configuration split (2026-10, Rust rewrite):
- Shared settings (provider, key timing, denoise, audio tuning) live in
fcitx5's own
Configuration, so they appear infcitx5-configtooland land in~/.config/fcitx5/conf/ainput.conf. fcitx5 writes only non-defaults. - Per-provider data (credentials, model ids, endpoints, paths, timeouts) is
~/.config/ainput/<provider>.json. Every file is seeded on first run with all fields, their defaults and English comments; a template that omits a field failstemplates_list_every_field. - There is no
config.jsonanymore. The pre-rewrite file is migrated once intoainput.confand renamed toconfig.json.migratedso migration cannot run twice and overwrite later UI changes. - No
/etclayer, and no pacnew dance: package upgrades never touch user files.
- Shared settings (provider, key timing, denoise, audio tuning) live in
fcitx5's own
- Generated config templates are English only.
README.zh-CN.mdis the only Chinese artefact in the repository. - Tuning knobs stay out of config unless a user has a real reason to turn
them: request_style, the doubao poll knobs and num_threads were retired
into code constants in 2026-10 (doubao keeps one whole-budget
timeout_sec). New knobs need a justification, not just "might be useful". - OS prerequisites are shipped, not assumed. CapsLock restore injects a
synthetic CapsLock press+release through
/dev/uinput; the package must therefore provide the udev rule that makes that node writable (udev/70-ainput-uinput.rules, installed by meson). It usesTAG+="uaccess"so every local user with an active session — including accounts created after installation — works with no group membership, and without granting read access to/dev/input/event*. Losing this rule makes CapsLock restore fail silently while voice input still works (seefound.md#9).
- Two halves, one boundary.
shell/is the C++ part that must be C++ (fcitx5AddonInstance,AinputConfig,KeyEvent,commitString).rust/owns capture, DSP, ASR, config files, uinput and diagnostics. The only contract isshell/ainput_core.h: integers, C strings and POD structs. Never pass a C++ type across it. - A new ASR backend is
rust/src/asr/<id>.rs+ one entry inDESCRIPTORS,create()anddocs/architecture.md. Keepmockas the interface spec. - One recognition worker for the process; the pending counter is reserved
before anything is created and released exactly once on every path.
Cancellation is cooperative via
std::sync::atomic::AtomicBooland must still produce an error, never silence. - JSON uses
serde_json; the old hand-written extraction is gone. Config files keep//comments, stripped byconfig::strip_commentsin a string-aware, offset-preserving way. - Dependencies: use maintained Rust crates where they exist and link the system
library (
libpulse-binding,curl); hand-write FFI only for libraries with no maintained crate (rust/src/ffi/dsp.rs: ebur128, speexdsp, soxr). - Failures are never swallowed. Every failure either becomes an
Errorwith aCategorythat the shell renders as one actionable sentence, or is reported to the user. Silently degrading (for example dropping denoising) is a bug. - Documentation is one file per topic, not one file per phase of work:
README.md(English) /README.zh-CN.md(Chinese) — project introduction, install, usage, provider table, quick config. Keep engineering detail out; keep both languages in sync.docs/configuration.md— every config key, default, validation, migration.docs/architecture.md— module boundaries, data flow, provider contract, how to add a backend.docs/development.md— build, tests, diagnostics, repository rules.docs/history.md— archive: dated failure analysis plus the chronological log that used to beFINDINGS.md. Never present it as the current design.docs/qwen-asr-api.md,docs/doubao-auc-api.md— vendor API notes.
- Do not reintroduce per-module readmes,
WhyThisArchWork.mdfiles, a generic fcitx5 tutorial set, or atools/directory of one-off probes; those were removed in 2026-10. Hardware checks are the ignored Rust test plus the diagnostic log. - Durable claims (archive name, download size, config path, default value,
test count) are verified against the code or the upstream API before they
ship. See
found.md#12. - Commit
rust/,shell/,docs/,udev/, build and packaging files,README*,LICENSE,AGENTS.md,found.md. Never commit build output,rust/target/,compile_commands.json, runtime config,~/.configor~/.cachecontents, models, diagnostic logs, or editor/IDE state. The full list is indocs/development.md. - Build: meson drives
cargo buildforrust/and links the static archive intoainput.so;meson testruns the Rust unit tests. Packaging: PKGBUILD (Arch), makedepends includecargo. Verification:ninja -C build,meson test -C build, and (for capture or queue changes)cargo test --lib -- --ignoredfor the microphone test. Meson install carries the addon files and the udev rule (lib/udev/rules.d);PKGBUILD.installreloads udev on install/upgrade. - Release chain: repo → GitHub (
origin) + Gitee (gitee, PKGBUILD source) → AUR. Two packages, two AUR repos:fcitx5-ainput-git(source atmaster) andfcitx5-ainput-bin(the GitHub release artifact,PKGBUILD.bin). They install identical files, so-bindeclaresconflicts=('fcitx5-ainput-git')andprovides=('fcitx5-ainput');replacesis inert because paru installs AUR packages withpacman -U. Full procedure indocs/development.md. For a-gitpackage AUR and paru can only display the staticpkgver=placeholder (they cannot runpkgver()), so keep that placeholder at the real current version and regenerate.SRCINFO(makepkg --printsrcinfo > /tmp/x && [ -s /tmp/x ] && cp /tmp/x .SRCINFO) before every push to AUR. AUR push: SSH asaurwith the full package name —aur@aur.archlinux.org:fcitx5-ainput-git.git(HTTPS push is rejected with 403). aurweb's RPC index lags a git push by ~2 minutes, so a staleparu -qaright after pushing is normal, not a packaging bug. The web page (/packages/<name>) flips over before the RPC index does, so when RPC still saysresultcount: 0check the page before assuming the push failed. - aarch64 is cross-compiled (
tools/cross-aarch64.sh), not tested: there is no arm machine. Only the object format, the dependency list and the exported symbols are verified. Say so in release notes rather than implying support. - The prebuilt artifacts require glibc 2.39+ (linked on current Arch). Keep
that in the README: a user on an older system must use
-git. - Renaming a package on AUR is not possible; the procedure is: publish the new
pkgbasefirst, then file a Deletion request for the old one and link the replacement (/pkgbase/<old>-> Package Actions -> Submit Request).Mergeis only worth it when the old package has votes/comments to carry over; with none, the guidelines state a deletion request linking the successor is identical. Only the maintainer can request it, and the reason must live in the request itself, not in package comments — once a TU acts, the request andaur-requestsmail are the only record. Deletion leaves the old git repo cloneable, so publish the successor first (2026-10 rename:fcitx5-vinput-git->fcitx5-ainput-git, PRQ#88425). replacesis inert for how AUR packages are actually consumed: paru builds a package and installs it withpacman -U, and-Uignoresreplaces(verified in a throwaway pacman root). Onlyconflictsmakes an install offer to remove the old package. Declaringprovides/conflictsagainst a name that has no package and no dependents is dead metadata — add the pair only when a stable non--gitpackage actually exists.- Docs are topic-based:
docs/configuration.md,docs/architecture.md,docs/development.md,docs/history.md, plus vendor notes (docs/qwen-asr-api.md,docs/doubao-auc-api.md).docs/history.mdis an archive (dated analysis + old engineering log) and must never be presented as the current design.README.mdandREADME.zh-CN.mdstay structurally identical.
qwenprovider is config-driven end to end (~/.config/ainput/qwen.json): model (defaultqwen-audio-3.1-asr-flash), endpoint, request style (auto-detected per model family), language hints, instant/precompiled hot words, dialect retention, speaker diarization. Config is re-read on every recognition, so model switches need no restart or rebuild. Legacyqwen3-asr-flashstill works by settingmodel(old wire format is kept).doubaoprovider likewise takesmodel_name/enable_itn/enable_puncfromdoubao.json.- Cross-vendor wire unification was evaluated and rejected: Doubao uses a
submit/query poll protocol with
X-Api-Keyheaders and header-based status codes, Qwen uses a synchronous Bearer-auth JSON call. Vendors stay one provider each behindProvider; only the config schema is unified. - When Alibaba deprecates the next model, the expected fix is a config/template
/doc update, not code — unless the wire protocol itself changes. The request
format is chosen from the model name in
rust/src/asr/qwen.rs(legacy_format).