Textify

Textify desktop preview

Textify’s desktop application for macOS, Windows, and Linux. This directory contains the Electron interface, desktop integration, and local GPU workers.

Run locally

Use Node.js 24, npm, CMake 3.20+, and a C++17 toolchain. On macOS install Xcode Command Line Tools; on Windows use Visual Studio 2022 or newer with Desktop Development with C++; on Linux use GCC or Clang and the desktop libraries listed in .github/workflows/electron.yml. Build on the OS and architecture you package.

cd electron
npm ci
npm run native:prepare
npm run native:build
npm start

native:prepare explicitly downloads and verifies a pinned upstream whisper.cpp archive and the pinned Silero speech detector. Compilation does not download source. JavaScript dependencies are locked in package-lock.json.

Open Transcription models to search, download or import an exact signed model artifact. Each model shows its supported languages, versions and sizes, installation status, and an upstream model link. Search, language and installed filters narrow the list. A version’s options button reveals Import and Remove. All platforms support Whisper small.en, large-v2, large-v3, and large-v3-turbo. The first three expose English; turbo exposes English and Hindi according to the bundled signed catalog. Import copies into this app’s own storage and checks size and SHA-256. Downloads and imports are hashed once; launching Textify or switching models checks file sizes and layout without rereading the weights. Remove and download a model again if its file is damaged.

All platforms also support these GPU-only versions:

Checkpoint Versions Runtime
Parakeet TDT 0.6B V3 GGUF F16, Q8_0, Q5_K_M transcribe.cpp
Qwen3-ASR 0.6B GGUF BF16, Q8_0, Q5_K_M transcribe.cpp
Qwen3-ASR 1.7B GGUF BF16, Q8_0, Q5_K_M transcribe.cpp
Confucius4-R2T2 1.7B GGUF Q8_0, F16; original BF16 safetensors audio.cpp

Confucius shows live English or Chinese text in the compact floating bar while recording. All models perform final recognition on release and insert once. The bar shows Textify’s icon, waveform, colored border and three-line transcript, with compact Copy and dismissal controls for recovery. Qwen and Parakeet offer automatic language detection. MLX/CoreML variants are not included. Custom vocabulary prompts apply to Whisper; replacement pairs apply to every engine. Windows and Linux run these versions on Vulkan; their physical-GPU checks are listed in MANUAL_QA.md. Published catalog benchmark ratings are not claimed as measurements of these desktop workers. A version that does not fit in free GPU memory fails with a message asking for a smaller version; weights are never moved to system memory. The largest versions are about 4.1 GB before working buffers. Removing or replacing the active model waits for its worker to exit, because Windows keeps a memory-mapped model file locked until then.

BF16 · Original downloads the publisher’s 11 original files (4.09 GB) from netease-youdao/Confucius4-R2T2 revision 185ce639118ad1362d049ca0d8ed04b6ec5cd6c9. Import folder accepts an existing copy of that exact revision. Files are staged, individually hashed and installed as a complete directory; partial transfers can resume. Extra source files are not imported. The entry is in models/manifest.json inside this Electron directory, signed with the existing trusted catalog key. Both catalogs are verified and all files remain subject to sticky revocations, including canonical directory digests. The shared root catalog is unchanged. Model weights remain BF16; there is no Python service, runtime conversion to GGUF, cloud ASR or CPU inference fallback.

The extra source archives are checksum-pinned. They build offline as separate executables with embedded Metal or Vulkan shaders. Mac workers use no non-system dylibs; Windows and Linux workers use the system Vulkan loader, as the Whisper worker does. audio.cpp’s deployment build also embeds model specifications required by original HF folders. Parakeet’s predictor/joint graphs are moved to the GPU, each ggml copy rejects CPU graph execution, and software Vulkan devices are rejected. CI on all three OSes checks that both additional workers refuse startup without a supported GPU.

Streaming uses 320 ms chunks and bounded 25-second decoder windows, independent of the complete five-minute capture retained for final recognition. Cancel and release drain in-flight preview work before reset; late text cannot enter another session. If inference falls behind, only stale preview audio is discarded. Audio and preview text are held in memory, cleared after the session and never logged.

Hold the microphone button to record and release it to transcribe. Choose Copy dictation, then paste into your app. Microphone permission is requested when you record or use Check microphone.

Desktop behavior

Desktop Global trigger Text delivery
macOS Right Command by default, after Accessibility permission Native paste with target revalidation and clipboard restoration
Windows Right Control by default Native paste with target revalidation and clipboard restoration
Linux X11 Right Control by default Explicit Copy, then manual paste
Linux Wayland Desktop GlobalShortcuts portal, when available Explicit Copy, then manual paste

The microphone button always offers Copy. On Wayland, Enable global trigger opens the desktop’s shortcut consent flow; the desktop chooses the binding. If the portal is unavailable or consent is denied, use the microphone button. Wayland cancellation uses the recording indicator’s Cancel button; Escape also works while the settings window is focused.

Automatic paste checks that the originally captured app is still active and rejects positively identified password fields. It does not restore app focus. After a paste event has been sent, Textify does not retry an uncertain result. It restores the previous clipboard only when the clipboard has not changed. Explicit Copy replaces the clipboard normally.

The preview includes custom words, replacement pairs, English spoken punctuation, a five-minute recording limit, tray/menu-bar operation, adjustable recording indicator, language selection, model switching/deletion, resumable downloads, and persistent signed revocation rules. Non-English dictation skips English text rewriting. Restored model artifacts require explicit verification.

General → Floating Icon provides a live position preview, X/Y sliders and point values, 50–200% scale, arrow steppers, and Reset Position & Scale. Positive Y moves the bar up. The bar remains inside the active display’s visible area; dragging previews immediately and saves on release.

General combines dictation preferences, Floating Icon and launch at login after installation. Privacy offers app exclusions on macOS, Windows, and X11; Wayland cannot reliably identify foreground apps and shows exclusions as unavailable. On any desktop, the in-app microphone button is an explicit Copy workflow and does not inspect another app.

Audio and pending dictation stay in memory. Activity stores only daily numeric totals for completed dictations, with day, week, and month views derived from those totals. There is no transcript history, telemetry, or speech upload. Model downloads require an explicit action.

Privacy → Save recordings for training is off by default. When it is on, each completed dictation is saved in the data directory’s Recordings folder as a 16 kHz mono 16-bit WAV of the audio the model received, plus a JSON record: modelText (raw model output), finalText (after replacement pairs and spoken punctuation), modelID, language, seconds, and correctedText, which stays null until you fill it in during review. Cancelled, empty, secure-field and excluded-app dictations are not saved. Nothing is uploaded.

While saving is on and you are not dictating, Textify also transcribes each saved clip locally with the largest installed Whisper, Parakeet, or Qwen3-ASR model from each family other than the clip’s own, and stores that text in the record’s comparisons, keyed by model ID.

Recordings, in the sidebar while saving is on (or Privacy → Review), lists clips that need review first, ranked by how much the dictation model’s words differ from the other models’ (case and punctuation are ignored). Opening a clip plays it and shows each model’s text with the disputed words marked; Use copies a model’s text into the correction. Save (⌘↩, or Ctrl+Enter) writes the text to correctedText and opens the highest-ranked clip that still needs review. Keep spoken commands such as “comma” as words, because the correction is the training target for the model’s raw output. Delete removes the clip’s WAV and JSON files, and Show folder opens the folder. Custom words, preferences, model files and trust records remain in the existing Textify Electron data directory so preview updates retain them.

Verify and package

npm run check
npm run smoke
npm run package

package creates a local application directory under release/. dist creates the configured DMG, NSIS, or AppImage/deb distribution for the current OS. Packaging refuses native helpers built for a different OS or architecture. On macOS these commands require a valid Developer ID Application certificate and its private key in the local Keychain. If there are several, set CSC_NAME to the intended certificate name. Keep the same signing identity and bundle ID across updates so macOS can recognize existing permission grants. The signing path does not silently fall back to ad-hoc signing. It retains library validation and signs the app and its embedded code with the selected identity.

Until a certificate is available, npm run package:preview and npm run dist:preview explicitly produce local ad-hoc previews. These can require permissions to be renewed after an update. Electron CI uses these preview commands on macOS. On Windows and Linux it runs npm run dist, which adds the update feed; Windows installers remain unsigned. No packaging command publishes. For notarization, configure electron-builder’s APPLE_KEYCHAIN_PROFILE with an existing local notarytool credential profile before packaging. Production Mac packaging refuses to run without notarization credentials. Verify the resulting app’s ticket and Gatekeeper assessment before distribution; signing alone does not claim a notarized release, and credentials are never bundled.

Automatic updates

Releases update themselves from GitHub Releases. At launch and every six hours, Textify checks the newest v*-preview.* release for its platform’s feed: latest-mac.yml, latest.yml on Windows, or latest-linux.yml on Linux. A newer version shows a notification and a Settings notice. Choosing Update downloads the new version and checks its SHA-512 against the feed. It installs the next time Textify quits. General settings can turn the checks off.

Windows and Linux installers are not signed, so the feed’s SHA-512 is their only check.

Packaging with --preview and development builds have no update feed, so they never check. A Mac App Store build must not include this updater. Install the first updater-enabled release manually; later releases arrive through the app.

Accessibility setup on macOS

General shows Enable Accessibility when permission is missing. The button requests the native prompt and opens the matching settings pane. Turn on the exact app name shown in the card; Textify checks once per second for up to five minutes, then also checks when its window regains focus. Approval automatically activates the global shortcut, while Privacy shows Enabled. No extra trigger activation click is required in the normal path. Revocation never prompts on its own; it stops the shortcut and any active recording when detected.

If Textify is absent from the list, a small floating helper appears over Settings. Drag its Textify icon into the Accessibility app list, then turn on the new switch. The helper drags the exact running .app bundle and closes when approval is detected; it can also be dismissed manually.

App missing, or already switched on? reveals the exact running .app path, a Finder shortcut and instructions for adding that copy through the Settings + button if dragging is unavailable. The app never edits TCC databases, grants itself access, resets permissions or changes other apps’ grants. Permission status is read from macOS, not saved as a preference. node scripts/accessibility-smoke.mjs checks this flow with substituted OS boundaries and isolated app data; it does not change actual Mac permissions.

To exercise actual offline recognition with the public speech fixture without using your microphone or system clipboard:

TEXTIFY_MODEL_FIXTURE='/absolute/path/to/ggml-small.en-q5_1.bin' npm run smoke
node scripts/native-smoke.mjs '/absolute/path/to/ggml-small.en-q5_1.bin'

In PowerShell, set $env:TEXTIFY_MODEL_FIXTURE before running npm run smoke. The optional smoke test copies the model into temporary isolated app storage, feeds a fixture MediaStream through the real AudioWorklet and native worker, checks Copy, and removes the temporary storage. Screenshots are written only to the ignored artifacts/ directory.

Current verification and remaining work

The baseline passed native builds, 47 tests, real fixture transcription, Electron smoke, packaged launch and installer creation on all three CI runners: baseline run. The expanded preview passes 101 tests locally; its workflow repeats those checks and adds installation/launch checks for NSIS, Debian, extracted AppImage and DMG. See the migration record for the latest run, tested revision and downloadable artifacts. The desktop release procedure lists the checks required before a public cross-platform download is created.

On macOS, node scripts/insertion-smoke.mjs verified native insertion into an owned test window, original clipboard restoration, target mismatch rejection, and password-field rejection. The script uses public test text and no microphone; Windows CI also runs it. Physical microphone, real app, login startup and Linux GNOME/KDE Wayland checks remain in MANUAL_QA.md. The owner can test Windows; a Linux desktop tester is still needed. Xvfb is not a Wayland desktop.

The preview does not include speech enhancement or diagnostics export. Confucius4-R2T2 provides live transcription previews. Outstanding physical-device checks are recorded in the desktop QA guide.

For additional local model checks, scripts/runtime-smoke.mjs verifies the signed model hash and exercises language and custom-word configuration with public test audio. It accepts MODEL [en|hi] [mono-16khz-f32-file]; English defaults to the public JFK sample. It prints GPU worker startup and final transcription times, or how long a failed startup ran. No recognized text is logged or saved.

GPU-required preview

0.2.0-preview.2 supersedes the CPU-based Windows/Linux preview. Metal is required on macOS; Windows and Linux build with GGML_VULKAN=ON. For Windows builds install the Vulkan SDK (headers, libraries and glslc); Ubuntu builds need libvulkan-dev glslc spirv-headers. End users need their hardware vendor’s GPU driver, not the SDK. Linux also requires the system Vulkan loader (libvulkan1). Workers start with DISABLE_LAYER_AMD_SWITCHABLE_GRAPHICS_1=1. On a Windows PC with an NVIDIA GPU, an older AMD driver’s switchable-graphics layer returned an incomplete device list on every call, and ggml retried until model loading timed out.

native/require-gpu.mjs applies exact edits to the checksum-pinned whisper source: GPU backend initialization must succeed, model weights must use GPU buffers, and the graph scheduler refuses CPU computation. Software/virtual Vulkan devices are rejected. Workers start with implicit Vulkan layers disabled (VK_LOADER_LAYERS_DISABLE=~implicit~), so overlays such as Steam, OBS or RivaTuner are not loaded into GPU startup; loaders older than 1.3.234 ignore the setting. Audio preprocessing, token sampling, memory transfers and graph bookkeeping still use the CPU. The native build runs a regression test that attempts a CPU matrix graph and requires refusal.

The one exception is the Silero VAD v5.1.2 speech detector (885 KB, MIT, ggml-silero-v5.1.2.bin from ggml-org/whisper-vad). textify-whisper --speech runs it on the CPU, on one thread, before every transcription. The patch lets only its scheduler compute on the CPU, and it takes the CPU backend directly, so it never starts Metal or Vulkan. A second native test requires it to detect speech in the JFK sample and none in silence, then requires refusal of an ordinary CPU graph in the same process.

On all hosted CI runners, no-GPU startup, disabled recording, and the CPU speech check are tested. The hosted Mac exposes a paravirtual Metal device without Apple7 compute features. These checks are not evidence of NVIDIA/AMD/Intel GPU recognition or performance; those require a real GPU. The local Mac fixture uses real Metal.

Holds without usable audio and rejected or empty recognition return quietly to idle. So do recordings in which the speech detector finds under 0.25 s of speech (checked on the whole recording, made louder when quiet); if the detector cannot run, transcription proceeds. Quiet recordings, whose loudest frames stay below -26 dBFS, are trimmed at 10 dB above their noise floor (between -60 and -45 dBFS) with a 400 ms margin; other recordings keep the -45 dBFS threshold and 150 ms margin. Raise quiet speech (General settings, on by default) raises quiet recordings by up to 30 dB before transcription. A transcribe.cpp decode that reaches its output limit returns no text instead of a GPU error. Capture, model, and insertion failures still show actionable errors. The Mac package now explicitly includes the Audio Input entitlement on the app and helpers. mac-signature-smoke.mjs verifies the signed output; the earlier unsigned package lacked this capability even though it declared a usage string.

Parakeet GPU regression check (Apple Silicon, exact signed Q8_0 file):

node scripts/runtime-smoke.mjs /path/to/parakeet-tdt-0.6b-v3-Q8_0.gguf
TEXTIFY_MODEL_ID=parakeet-tdt-0.6b-v3-q8-0 TEXTIFY_MODEL_FIXTURE=/path/to/parakeet-tdt-0.6b-v3-Q8_0.gguf npm run smoke

The pinned Parakeet encoder otherwise selects CONV_2D_DW, unsupported by its Metal backend, and silently schedules it on CPU upstream. The preparation patch selects the existing im2col/matmul implementation for both depthwise sites; Textify’s CPU refusal remains enabled. This is separate from moving its TDT decoder graphs onto the GPU. Recognition is tested with the public JFK fixture, without saving the transcript.