Support native macOS speech relocation and safe indexing

This commit is contained in:
karl committed 2026-10-23 09:43:42 +02:00
1 parent 9a93ee6375
commit 5b93dd67a7
8 files changed
+287 -14

No files matched your search

+137 -3
View File
@@ -258,9 +258,143 @@ claim an untested platform, hardware backend or UI interaction works.
```
Building needs internet access, disk space for the toolchain and local models,
and permission to install dependencies. macOS native execution remains
unverified in this repository; your agent should verify its build on your Mac
and report any issue. See [platform build instructions](docs/BUILDING.md).
and permission to install dependencies. An Apple Silicon source installation
was tested on 23 October 2026; follow the Mac guide below. Other host/architecture
combinations still need their own checks. See [platform build instructions](docs/BUILDING.md).
### macOS: build and install from source
Tested with source commit `9a93ee6375d610d580ef16c5ec52218727fab187`
on an Apple M4 Mac with 16 GiB unified memory and macOS 26.6.2. The installed
0.1.9 app opened outside the checkout, used Metal for genuine Google embeddings,
and passed English/German document retrieval and code-only semantic search.
There is currently no downloadable Mac installer. Intel Macs, Developer ID
signing and notarization remain unverified.
**Prerequisites:** Xcode command-line tools (or full Xcode), stable Rust/Cargo,
Node **24 LTS**, Python 3.11+ and CMake. Check that `cargo`, `node`, `npm`,
`python3` and `cmake` are on your `PATH`; run `xcode-select -p` to check the
Apple toolchain. If it is missing, run `xcode-select --install` and complete
Apple's installation before continuing. Use the official [Rust installer](https://rustup.rs/)
if Cargo is missing. With Homebrew, `brew install node@24 cmake python` supplies
the other tools; select Node 24 with
`export PATH="$(brew --prefix node@24)/bin:$PATH"` and verify `node --version`.
Node 26 failed a Unicode-highlighting test on the tested machine; Node 24.19.0
passed all 66 frontend tests.
Allow space for native build outputs and the model assets. The pinned text
model and media projector download totals **864,676,480 bytes** (about 865 MB).
The steps below also copy those assets into the app, so the download size is
not the total disk-space requirement. Existing valid model files are reused
and verified by size and SHA-256.
```sh
git clone https://tea.karlcom.de/karl/searchi.git
cd searchi
# Record the current revision, including the Mac speech/scan fixes:
git rev-parse HEAD
python3 tools/embeddinggemma2/build_runtime.py --backend auto --jobs 2
python3 tools/embeddinggemma2/download.py
python3 tools/embeddinggemma2/stage_runtime.py --backend auto --bundle-model
python3 tools/stage_extraction_worker.py
```
Use the normal `auto` backend: on macOS it builds Metal support with CPU support
in the same worker. The builder embeds Metal kernels and checks the native
architecture and Apple framework dependencies. Do not substitute an explicit
CPU-only worker for this normal installation. See [macOS runtime details](docs/MACOS-RUNTIME.md).
**Optional media helpers:** the pinned decoder-provisioning manifest currently
covers Windows and Linux. For a local Mac installation with Homebrew FFmpeg,
stage the existing tools before packaging:
```sh
brew install ffmpeg
mkdir -p apps/desktop/src-tauri/resources/media-runtime/bin
install -m 755 "$(brew --prefix)/bin/ffmpeg" \
apps/desktop/src-tauri/resources/media-runtime/bin/ffmpeg
install -m 755 "$(brew --prefix)/bin/ffprobe" \
apps/desktop/src-tauri/resources/media-runtime/bin/ffprobe
```
These binaries retain dependencies on Homebrew libraries. Keep that Homebrew
installation available; this does not create a portable Mac media distribution.
Speech recognition uses the separate worker/models staged below. The setup
panel reports readiness and any missing capabilities.
**Local speech recognition:** build and stage the normal Metal/CPU speech runtime
before packaging the desktop. The app provisions about149MB of verified speech
models separately from the Google assets.
```sh
git clone https://github.com/ggml-org/whisper.cpp.git .searchi/references/whisper.cpp
git -C .searchi/references/whisper.cpp checkout d1be6fde11ac6e0407606b4e42fe72d34add8037
python3 tools/speech-runtime/build.py --source .searchi/references/whisper.cpp \
--output .searchi/speech-runtime/macos-auto --device auto
python3 tools/stage_speech_worker.py --runtime .searchi/speech-runtime/macos-auto/runtime \
--platform macos --architecture aarch64
```
Use a fresh speech build output directory. The Mac runtime uses loader-relative
library paths, so the installed app does not depend on the build tree. Speech
recognition, video-frame retrieval, timestamped audio/video previews and music
metadata lookup were exercised in the installed Mac app. A known spoken sentence
was found in both audio and video; an artist search found a piano recording.
The optional CLI command `searchi setup --speech --model-dir PATH` can provision
the same verified models explicitly. Normal Mac app profiles use
`~/Library/Application Support/dev.searchi.desktop/SpeechModels`.
Build the desktop bundle using Node 24:
```sh
cd apps/desktop
npm ci
npm test
npm run tauri -- build --features custom-protocol --bundles app \
--config '{"bundle":{"active":true}}'
cd ../..
```
Install the generated bundle in your user Applications folder. If searchi is
already running, choose Quit before replacing it. Retain an existing installed
copy separately if you need to roll back; the app's data is stored separately.
```sh
mkdir -p "$HOME/Applications"
ditto apps/desktop/src-tauri/target/release/bundle/macos/searchi.app \
"$HOME/Applications/searchi.app"
codesign --force --deep --sign - "$HOME/Applications/searchi.app"
codesign --verify --deep --strict "$HOME/Applications/searchi.app"
open "$HOME/Applications/searchi.app"
```
This applies a local ad-hoc signature, not Developer ID signing or notarization.
The installed bundle contains the embedding worker, extraction worker and model
files; it does not need a frontend development server or the original checkout
to serve search. Any staged Homebrew media helpers still need their libraries.
**First-run coverage:** settings and the index live in
`~/Library/Application Support/dev.searchi.desktop/`. Review your indexed folders
before starting a large scan. Automatic discovery starts at `/` on macOS but prunes `/System`, `/Volumes`,
`/private` and the user Library app-state directory. You can also use explicit
personal-folder roots to control coverage. A dedicated sample folder makes an initial
semantic check easy. Known credential directories, build caches and searchi's
own data/model directories are excluded; macOS permissions still determine
which files can be read. Adding a folder does not mean its semantic index is
immediately ready—wait for the setup status.
Try a filename, an exact phrase and a description using different words. Select
the result and inspect its original passage. The tested sample agreement said
“sixty days of written notice”; “ending the supplier partnership” and the German
“Kündigung beim Lieferanten” both found it. Code scope found a retry function
from a description of increasing delays. These were small fixture checks, not
a whole-library accuracy or performance benchmark.
The Mac launcher shortcut is **Shift + Command + Space**, as configured in the
app; shortcut/tray interaction was not verified during this installation test.
Optional external Ask/summarization remains unconfigured unless you choose an
endpoint explicitly.
### Your first search
@@ -59,6 +59,14 @@ pub fn exclusions() -> Vec<PathBuf> {
.iter()
.map(PathBuf::from)
.collect();
#[cfg(target_os = "macos")]
{
paths.extend(["/System", "/Volumes", "/private"].map(PathBuf::from));
// App state, VM disks and OS caches are not personal searchable documents.
if let Some(home) = std::env::var_os("HOME") {
paths.push(PathBuf::from(home).join("Library"));
}
}
#[cfg(windows)]
if let Some(local) = std::env::var_os("LOCALAPPDATA") {
paths.push(PathBuf::from(local).join("Programs"));
@@ -156,6 +164,27 @@ mod tests {
assert!(exclusions().iter().all(|path| path.is_absolute()));
}
#[cfg(target_os = "macos")]
#[test]
fn mac_scan_prunes_system_mounts_and_app_state_but_keeps_documents() {
let excluded = exclusions();
let home = PathBuf::from(std::env::var_os("HOME").expect("Mac user home"));
for path in [
PathBuf::from("/System"),
PathBuf::from("/Volumes"),
home.join("Library"),
] {
let canonical = super::super::data_directory::canonical_exclusion(&path).unwrap();
assert!(
excluded.contains(&canonical),
"unpruned Mac path: {}",
path.display()
);
}
let documents = home.join("Documents");
assert!(!excluded.iter().any(|p| documents.starts_with(p)));
}
#[test]
fn automatic_scan_keeps_documents_and_prunes_credential_stores() {
let dir = tempfile::tempdir().unwrap();
+7 -5
View File
@@ -522,11 +522,13 @@ mod tests {
.expect("watch reconciliation")
}
#[test]
fn native_windows_or_linux_create_update_rename_delete() {
let root = tempfile::tempdir().unwrap();
fn native_create_update_rename_delete() {
let directory = tempfile::tempdir().unwrap();
// Match canonical production roots; macOS FSEvents emits /private/var paths.
let root = std::fs::canonicalize(directory.path()).unwrap();
let (tx, rx) = mpsc::channel();
let service = WatchService::start_timed(
vec![root.path().to_owned()],
vec![root.clone()],
vec![],
move |r| {
tx.send(r).unwrap();
@@ -536,8 +538,8 @@ mod tests {
)
.unwrap();
assert!(receive(&rx).paths.is_none());
let first = root.path().join("first.txt");
let second = root.path().join("second.txt");
let first = root.join("first.txt");
let second = root.join("second.txt");
std::fs::write(&first, "one").unwrap();
assert!(receive(&rx).paths.unwrap().contains(&first));
std::fs::write(&first, "two").unwrap();
+3 -1
View File
@@ -11,7 +11,9 @@ Headless Linux CLI/worker builds do not need WebKitGTK or a display server.
macOS runtime preparation embeds Metal kernel sources and inspects the static
worker's Apple dependencies before staging. See [macOS runtime](MACOS-RUNTIME.md)
for the prepared workflow and explicitly unexecuted native checks.
for the Apple Silicon installation evidence and remaining native checks.
The [README Mac guide](../README.md#macos-build-and-install-from-source) includes
the tested build, local installation, signing and first-run steps.
Headless builds still need a C/C++ compiler, linker, CMake, and Rust: SQLite, USearch,
NumKong, and the Google worker contain native code.
+58 -4
View File
@@ -27,10 +27,10 @@ python3 tools/embeddinggemma2/build_runtime.py --backend auto --jobs 2
python3 tools/embeddinggemma2/stage_runtime.py --backend auto
```
These commands are prepared, **not executed on macOS**. Native compilation,
actual `lipo`/`otool` output, relocation outside the source tree, genuine Google
CPU/Metal inference, device fallback, app integration and signing/notarization
remain open. This change does not create a macOS release download.
The initial preparation was fixture-tested on 13 October; native Apple Silicon
execution was subsequently verified on 23 October as recorded below. This does
not create a macOS release download. Follow the [README installation guide](../README.md#macos-build-and-install-from-source)
for the tested source-build and local installation steps.
## Evidence, 2026-10-13
@@ -47,3 +47,57 @@ Worker hashes remain identical to their previously tested CPU deployments:
Windows `c01cb3ec13cfca722ee768e8350e1399a8b9a4d14a24a35adc3473a5cfc82a73`;
Linux `b52d50a1e676bd4bf64124daff1c4ce83d86a8bde01e4eaf09838cbb24a4c28f`.
No GPU inference or native macOS execution was performed.
## Native Apple Silicon installation, 2026-10-23
Source `9a93ee6375d610d580ef16c5ec52218727fab187`, Apple M4/16 GiB,
macOS26.6.2, stable Rust, CMake4.4.3 and Node24.19.0. Normal `auto` worker
build/staging passed: arm64, embedded Metal library, Apple system-framework
linkage and artifact provenance verified. Both bundled Google assets passed
size/SHA-256 checks. The genuine query-role worker reported `device: gpu`,
512-token context and normalized 768-dimensional English/German embeddings.
The release Tauri app was installed outside the checkout with both helpers and
the model files. Local ad-hoc signing and deep/strict signature verification
passed. Installed UI checks covered model readiness, English/German document
retrieval, code-only retrieval, source previews, explanations and filters.
Seven actual native screenshots were captured. All66frontend tests passed
under Node24; global Node26 failed one Unicode-highlighting test.
Homebrew FFmpeg supplied local media helpers with external Homebrew library
dependencies. Speech recognition was unavailable during this initial check; the full installation below supersedes that limitation. Intel/universal execution,
GPU failure recovery, shortcut/tray interaction, clean-machine distribution,
Developer ID signing and notarization remain unchecked. These results do not
replace those gates or establish broad media accuracy.
## Full media and speech installation, 2026-10-23
The installed native app now includes the auto Metal/CPU whisper.cpp runtime at
`d1be6fde11ac6e0407606b4e42fe72d34add8037`, with verified multilingual base
and Silero6.2 model assets. All speech shared libraries use loader-relative
paths. `tools/speech-runtime/macos_smoke.py` checked installed linkage and real
recognition of a public-domain recording; the observed backend was `MTL0`.
An owned13-file corpus produced38indexed content segments, including8recognized
speech segments, with no reported media/speech failures. Native UI checks found
and played a waterfall video at4seconds, a spoken sentence in audio and video
near2seconds, and a tagged piano track by artist. A semantic piano-description
query ranked the track first among related suggestions. These checks do not
establish general lyrics recognition or large-library retrieval quality.
CLI and stdio MCP use the same normal query worker. A three-file fixture passed
semantic search, code filtering, source excerpt reading and invalid-handle
rejection. The worker reported GPU,768dimensions and512query-context tokens.
Ten distinct queries through an already-running MCP process had median8.28ms
request wall time; ten repeated cached queries median0.29ms. Three fresh CLI
processes including worker startup had median1018ms. These are small-index local
measurements, not desktop UI timings or production-scale latency guarantees.
Commit validation: all57native desktop tests pass, including real filesystem
create/update/rename/delete events using canonical Mac paths;66frontend tests,
format/build checks,3speech builder tests and installed Metal recognition pass.
The broader workspace run exposes an existing stale permission-denial rollback
expectation in `targeted_reconciliation` (current engine skips unreadable inputs).
Strict current-toolchain Clippy also flags unchanged permission-literal and
ANN-cache boolean expressions. These failures are not qualified as passing gates.
+17
View File
@@ -1,5 +1,22 @@
# Production readiness roadmap
## Native Apple Silicon support — 2026-10-23
- [x] Build/install the normal Metal/CPU runtime and Tauri desktop on Apple M4,
verify bundled assets and native GPU query inference.
- [x] Relocate the speech worker and shared libraries using `@loader_path`;
verify installed linkage and actual Whisper recognition on Metal.
- [x] Verify native document/code search, media previews, timestamped audio/video
playback and artist metadata lookup; document the complete source installation.
- [x] Verify native watcher lifecycle using canonical Mac paths (57 desktop tests pass).
- [x] Exclude Mac system trees, mounted volumes and user Library app state from
automatic indexing, preserving personal document roots.
Evidence and limits: [Mac runtime](MACOS-RUNTIME.md). Homebrew FFmpeg remains a
local dependency. Intel/universal builds, clean-machine redistributable media,
Developer ID/notarization, hotkey/tray checks and broad CPU/GPU quality parity
remain open; these native M4 checks do not complete every platform release gate.
## Current bounded goal — 2026-10-14
### Final 0.1.9 correction — delivered
+6 -1
View File
@@ -33,6 +33,11 @@ else()
set(GGML_METAL OFF CACHE BOOL "" FORCE)
endif()
endif()
if(APPLE AND NOT SEARCHI_INTERNAL_CPU)
# Every dylib/backend must resolve beside the installed worker, without the build tree.
set(CMAKE_INSTALL_RPATH "@loader_path")
set(CMAKE_BUILD_WITH_INSTALL_RPATH ON)
endif()
if(UNIX AND NOT APPLE AND NOT SEARCHI_INTERNAL_CPU)
# The staged worker and every dynamic backend live together. RUNPATH is
# non-transitive, so each shared target needs its own relocatable lookup.
@@ -51,7 +56,7 @@ if(MSVC)
target_compile_options(searchi-speech-worker PRIVATE /utf-8)
endif()
if(APPLE)
set_target_properties(searchi-speech-worker PROPERTIES BUILD_RPATH "$ORIGIN;@loader_path" INSTALL_RPATH "$ORIGIN;@loader_path")
set_target_properties(searchi-speech-worker PROPERTIES BUILD_RPATH "@loader_path" INSTALL_RPATH "@loader_path")
elseif(UNIX)
set_target_properties(searchi-speech-worker PROPERTIES BUILD_RPATH "$ORIGIN" INSTALL_RPATH "$ORIGIN")
endif()
+30
View File
@@ -0,0 +1,30 @@
"""Check installed Mac speech linkage and real recognition of an owned PCM fixture."""
import argparse,json,struct,subprocess
from pathlib import Path
def check_runtime(runtime):
names={p.name for p in runtime.iterdir() if p.is_file()}
for p in runtime.iterdir():
if not p.is_file() or not (p.name=='searchi-speech-worker' or p.suffix in {'.dylib','.so'}):continue
loads=subprocess.check_output(['otool','-L',str(p)],text=True).splitlines()[1:]
for line in loads:
dep=line.strip().split(' (',1)[0]
assert dep.startswith(('/System/Library/','/usr/lib/')) or dep.startswith('@rpath/') and Path(dep).name in names,(p.name,dep)
meta=subprocess.check_output(['otool','-l',str(p)],text=True).splitlines()
paths=[meta[i+2].strip().split(' (',1)[0].removeprefix('path ')for i,l in enumerate(meta)if l.strip()=='cmd LC_RPATH']
assert paths and all(x=='@loader_path' for x in paths),(p.name,paths)
def frames(raw):
out=[]
while raw:
assert len(raw)>=4
n=struct.unpack('<I',raw[:4])[0];assert 0<n<=4*1024*1024 and len(raw)>=4+n
out.append(json.loads(raw[4:4+n]));raw=raw[4+n:]
return out
if __name__=='__main__':
a=argparse.ArgumentParser();a.add_argument('--runtime',type=Path,required=True);a.add_argument('--models',type=Path,required=True);a.add_argument('--pcm',type=Path,required=True);a.add_argument('--output',type=Path,required=True);args=a.parse_args();check_runtime(args.runtime)
pcm=args.pcm.read_bytes();assert len(pcm)%4==0 and 0<len(pcm)//4<=480000
command=[str(args.runtime/'searchi-speech-worker'),str(args.models/'ggml-base.bin'),str(args.models/'ggml-silero-v6.2.0.bin'),'auto']
result=subprocess.run(command,input=b'\x02'+struct.pack('<I',len(pcm)//4)+pcm,capture_output=True,timeout=180,check=True)
messages=frames(result.stdout);assert len(messages)==2 and messages[0]['ready'] and messages[0]['distribution_auto'];assert messages[1]['ok'];text=' '.join(x.get('text','')for x in messages[1].get('sentences',[])).lower();assert 'free' in text and 'equal' in text,text;args.output.write_text(json.dumps(messages,indent=2));print('PASS: installed @loader_path closure and actual recognition; backend',messages[1].get('backend_observed'))