Support native macOS speech relocation and safe indexing
This commit is contained in:
1 parent
9a93ee6375
commit
5b93dd67a7
8 files changed
+287
-14
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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'))
|
||||
Reference in new issue
Block a user