Use Apple's on-device ML from any language. DarwinKit is a Swift CLI that exposes Apple's NaturalLanguage and Vision frameworks via JSON-RPC over stdio. Spawn it as a subprocess, send JSON, get results. No Swift knowledge required.
Zero API keys. Zero cloud costs. Runs entirely on-device.
Your App (any language) DarwinKit (Swift) Apple Frameworks
| | |
|-- stdin: JSON-RPC request -->| |
| |-- NLEmbedding.vector() -->|
| |<-- [0.03, -0.08, ...] --|
|<-- stdout: JSON-RPC resp. ---| |
| Method | Description | Apple Framework |
|---|---|---|
nlp.embed |
Text embeddings (512-dim vectors) | NLEmbedding |
nlp.distance |
Semantic distance between texts | NLEmbedding |
nlp.neighbors |
Find similar words/sentences | NLEmbedding |
nlp.tag |
POS tagging, NER, lemmatization | NLTagger |
nlp.sentiment |
Sentiment analysis | NLTagger |
nlp.language |
Language detection | NLLanguageRecognizer |
vision.ocr |
Text extraction from images | VNRecognizeTextRequest |
system.capabilities |
Query available methods + OS info | — |
- macOS 13+ (Ventura)
- Sentence embeddings require macOS 11+ (Big Sur)
brew tap 0xMassi/darwinkit
brew install darwinkitcurl -L https://github.com/0xMassi/darwinkit/releases/latest/download/darwinkit-macos-universal.tar.gz | tar xz
sudo mv darwinkit /usr/local/bin/git clone https://github.com/0xMassi/darwinkit.git
cd darwinkit
swift build -c release
# Binary at .build/release/darwinkit# Start the server — it reads from stdin and writes to stdout
echo '{"jsonrpc":"2.0","id":"1","method":"nlp.sentiment","params":{"text":"I love this product"}}' \
| darwinkit serve 2>/dev/null{"id":"1","jsonrpc":"2.0","result":{"label":"positive","score":1.0}}darwinkit query '{"jsonrpc":"2.0","id":"1","method":"nlp.language","params":{"text":"Bonjour le monde"}}'{
"id": "1",
"jsonrpc": "2.0",
"result": {
"confidence": 0.9990198612213135,
"language": "fr"
}
}DarwinKit uses JSON-RPC 2.0 over NDJSON (one JSON object per line). Same pattern as MCP.
Request (you send):
{"jsonrpc":"2.0","id":"1","method":"nlp.embed","params":{"text":"hello","language":"en"}}Response (you receive):
{"jsonrpc":"2.0","id":"1","result":{"vector":[0.031,-0.089,...],"dimension":512}}Error:
{"jsonrpc":"2.0","id":"1","error":{"code":-32602,"message":"Missing required param: text"}}- Spawn
darwinkit serveas a subprocess - Read the
readynotification from stdout (contains version + available methods) - Write requests to stdin, read responses from stdout
- Close stdin when done — DarwinKit exits cleanly
| Code | Meaning |
|---|---|
| -32700 | Parse error (malformed JSON) |
| -32600 | Invalid request |
| -32601 | Method not found |
| -32602 | Invalid params |
| -32603 | Internal error |
| -32001 | Framework unavailable |
| -32002 | Permission denied |
| -32003 | OS version too old |
| -32004 | Operation cancelled |
Compute semantic vectors using Apple's built-in embeddings.
{"jsonrpc":"2.0","id":"1","method":"nlp.embed","params":{
"text": "quarterly meeting notes",
"language": "en",
"type": "sentence"
}}| Param | Type | Required | Default | Description |
|---|---|---|---|---|
text |
string | yes | — | Text to embed |
language |
string | yes | — | Language code: en, es, fr, de, it, pt, zh |
type |
string | no | "sentence" |
"word" or "sentence" |
Returns { "vector": [...], "dimension": 512 }.
Cosine distance between two texts (0 = identical, 2 = opposite).
{"jsonrpc":"2.0","id":"1","method":"nlp.distance","params":{
"text1": "cat", "text2": "dog", "language": "en", "type": "word"
}}Returns { "distance": 0.312, "type": "cosine" }.
Find semantically similar words or sentences.
{"jsonrpc":"2.0","id":"1","method":"nlp.neighbors","params":{
"text": "programming", "language": "en", "type": "word", "count": 5
}}Returns { "neighbors": [{"text": "coding", "distance": 0.21}, ...] }.
Part-of-speech tagging and named entity recognition.
{"jsonrpc":"2.0","id":"1","method":"nlp.tag","params":{
"text": "Steve Jobs founded Apple in Cupertino",
"schemes": ["nameType", "lexicalClass"]
}}| Param | Type | Required | Default |
|---|---|---|---|
text |
string | yes | — |
language |
string | no | auto-detect |
schemes |
string[] | no | ["lexicalClass"] |
Available schemes: lexicalClass, nameType, lemma, sentimentScore, language.
Sentiment analysis with score and label.
{"jsonrpc":"2.0","id":"1","method":"nlp.sentiment","params":{
"text": "This is absolutely fantastic"
}}Returns { "score": 0.9, "label": "positive" }. Labels: positive (>0.1), negative (<-0.1), neutral.
Detect the language of a text.
{"jsonrpc":"2.0","id":"1","method":"nlp.language","params":{
"text": "Bonjour, comment allez-vous?"
}}Returns { "language": "fr", "confidence": 0.99 }.
Extract text from images using Apple Vision.
{"jsonrpc":"2.0","id":"1","method":"vision.ocr","params":{
"path": "/tmp/screenshot.png",
"languages": ["en-US"],
"level": "accurate"
}}| Param | Type | Required | Default |
|---|---|---|---|
path |
string | yes | — |
languages |
string[] | no | ["en-US"] |
level |
string | no | "accurate" |
Returns { "text": "...", "blocks": [{"text": "...", "confidence": 0.99, "bounds": {"x":0.1,"y":0.8,"width":0.3,"height":0.05}}] }.
Bounds are normalized (0-1), origin at bottom-left. Supports JPEG, PNG, TIFF, HEIC, PDF.
Query version, OS info, and available methods.
{"jsonrpc":"2.0","id":"1","method":"system.capabilities","params":{}}DarwinKit works with any language that can spawn a subprocess and read/write its stdio. Below are working examples.
import { spawn } from "child_process";
import * as readline from "readline";
class DarwinKit {
private process;
private rl;
private pending = new Map<string, { resolve: Function; reject: Function }>();
private nextId = 1;
constructor() {
this.process = spawn("darwinkit", ["serve"], {
stdio: ["pipe", "pipe", "pipe"],
});
this.rl = readline.createInterface({ input: this.process.stdout });
this.rl.on("line", (line) => {
const msg = JSON.parse(line);
// Skip notifications (no id)
if (!msg.id) return;
const pending = this.pending.get(msg.id);
if (!pending) return;
this.pending.delete(msg.id);
if (msg.error) pending.reject(new Error(msg.error.message));
else pending.resolve(msg.result);
});
}
async call(method: string, params: Record<string, any> = {}): Promise<any> {
const id = String(this.nextId++);
const request = { jsonrpc: "2.0", id, method, params };
this.process.stdin.write(JSON.stringify(request) + "\n");
return new Promise((resolve, reject) => {
this.pending.set(id, { resolve, reject });
});
}
close() {
this.process.stdin.end();
}
}
// Usage
const dk = new DarwinKit();
const sentiment = await dk.call("nlp.sentiment", { text: "I love this" });
console.log(sentiment); // { score: 1.0, label: "positive" }
const ocr = await dk.call("vision.ocr", { path: "/tmp/photo.png" });
console.log(ocr.text);
dk.close();import json
import subprocess
import threading
class DarwinKit:
def __init__(self):
self.process = subprocess.Popen(
["darwinkit", "serve"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
)
self._id = 0
self._lock = threading.Lock()
# Read and discard the 'ready' notification
self.process.stdout.readline()
def call(self, method: str, params: dict = None) -> dict:
with self._lock:
self._id += 1
request = {"jsonrpc": "2.0", "id": str(self._id), "method": method, "params": params or {}}
self.process.stdin.write(json.dumps(request) + "\n")
self.process.stdin.flush()
line = self.process.stdout.readline()
response = json.loads(line)
if "error" in response and response["error"]:
raise Exception(f"DarwinKit error {response['error']['code']}: {response['error']['message']}")
return response["result"]
def close(self):
self.process.stdin.close()
self.process.wait()
# Usage
dk = DarwinKit()
embedding = dk.call("nlp.embed", {"text": "hello world", "language": "en", "type": "sentence"})
print(f"Vector dimension: {embedding['dimension']}") # 512
lang = dk.call("nlp.language", {"text": "Ciao, come stai?"})
print(f"Detected: {lang['language']} ({lang['confidence']:.0%})") # it (99%)
dk.close()use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::io::{BufRead, BufReader, Write};
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
pub struct DarwinKit {
child: std::process::Child,
reader: BufReader<std::process::ChildStdout>,
next_id: AtomicU64,
}
#[derive(Serialize)]
struct Request {
jsonrpc: &'static str,
id: String,
method: String,
params: Value,
}
#[derive(Deserialize)]
struct Response {
id: Option<String>,
result: Option<Value>,
error: Option<RpcError>,
}
#[derive(Deserialize)]
struct RpcError {
code: i32,
message: String,
}
impl DarwinKit {
pub fn new() -> std::io::Result<Self> {
let mut child = Command::new("darwinkit")
.args(["serve"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()?;
let stdout = child.stdout.take().unwrap();
let mut reader = BufReader::new(stdout);
// Read and discard ready notification
let mut ready = String::new();
reader.read_line(&mut ready)?;
Ok(Self { child, reader, next_id: AtomicU64::new(1) })
}
pub fn call(&mut self, method: &str, params: Value) -> Result<Value, String> {
let id = self.next_id.fetch_add(1, Ordering::Relaxed).to_string();
let request = Request { jsonrpc: "2.0", id: id.clone(), method: method.to_string(), params };
let stdin = self.child.stdin.as_mut().unwrap();
serde_json::to_writer(&mut *stdin, &request).map_err(|e| e.to_string())?;
stdin.write_all(b"\n").map_err(|e| e.to_string())?;
stdin.flush().map_err(|e| e.to_string())?;
let mut line = String::new();
self.reader.read_line(&mut line).map_err(|e| e.to_string())?;
let response: Response = serde_json::from_str(&line).map_err(|e| e.to_string())?;
if let Some(err) = response.error {
return Err(format!("DarwinKit error {}: {}", err.code, err.message));
}
response.result.ok_or_else(|| "No result".to_string())
}
}
impl Drop for DarwinKit {
fn drop(&mut self) {
drop(self.child.stdin.take()); // close stdin -> darwinkit exits
let _ = self.child.wait();
}
}
// Usage
fn main() -> Result<(), String> {
let mut dk = DarwinKit::new().map_err(|e| e.to_string())?;
let result = dk.call("nlp.sentiment", serde_json::json!({"text": "Rust is great"}))?;
println!("Score: {}", result["score"]); // 1.0
let ocr = dk.call("vision.ocr", serde_json::json!({"path": "/tmp/image.png"}))?;
println!("Text: {}", ocr["text"]);
Ok(())
}package main
import (
"bufio"
"encoding/json"
"fmt"
"os/exec"
"sync"
"sync/atomic"
)
type DarwinKit struct {
cmd *exec.Cmd
stdin *json.Encoder
reader *bufio.Reader
nextID atomic.Int64
mu sync.Mutex
}
type rpcRequest struct {
JSONRPC string `json:"jsonrpc"`
ID string `json:"id"`
Method string `json:"method"`
Params interface{} `json:"params"`
}
type rpcResponse struct {
ID *string `json:"id"`
Result json.RawMessage `json:"result"`
Error *struct {
Code int `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func NewDarwinKit() (*DarwinKit, error) {
cmd := exec.Command("darwinkit", "serve")
stdin, _ := cmd.StdinPipe()
stdout, _ := cmd.StdoutPipe()
if err := cmd.Start(); err != nil {
return nil, err
}
reader := bufio.NewReader(stdout)
reader.ReadString('\n') // skip ready notification
return &DarwinKit{
cmd: cmd,
stdin: json.NewEncoder(stdin),
reader: reader,
}, nil
}
func (dk *DarwinKit) Call(method string, params interface{}) (json.RawMessage, error) {
dk.mu.Lock()
defer dk.mu.Unlock()
id := fmt.Sprintf("%d", dk.nextID.Add(1))
req := rpcRequest{JSONRPC: "2.0", ID: id, Method: method, Params: params}
if err := dk.stdin.Encode(req); err != nil {
return nil, err
}
line, err := dk.reader.ReadString('\n')
if err != nil {
return nil, err
}
var resp rpcResponse
if err := json.Unmarshal([]byte(line), &resp); err != nil {
return nil, err
}
if resp.Error != nil {
return nil, fmt.Errorf("darwinkit error %d: %s", resp.Error.Code, resp.Error.Message)
}
return resp.Result, nil
}
func (dk *DarwinKit) Close() {
dk.cmd.Process.Kill()
dk.cmd.Wait()
}
func main() {
dk, err := NewDarwinKit()
if err != nil {
panic(err)
}
defer dk.Close()
result, _ := dk.Call("nlp.language", map[string]string{"text": "Hola mundo"})
fmt.Println(string(result)) // {"language":"es","confidence":0.99}
}require 'json'
require 'open3'
class DarwinKit
def initialize
@stdin, @stdout, @stderr, @wait = Open3.popen3("darwinkit", "serve")
@id = 0
@stdout.gets # skip ready notification
end
def call(method, params = {})
@id += 1
request = { jsonrpc: "2.0", id: @id.to_s, method: method, params: params }
@stdin.puts(request.to_json)
@stdin.flush
line = @stdout.gets
response = JSON.parse(line)
if response["error"]
raise "DarwinKit error #{response['error']['code']}: #{response['error']['message']}"
end
response["result"]
end
def close
@stdin.close
@wait.value
end
end
# Usage
dk = DarwinKit.new
result = dk.call("nlp.sentiment", { text: "Ruby is elegant" })
puts "#{result['label']}: #{result['score']}"
ocr = dk.call("vision.ocr", { path: "/tmp/receipt.png" })
puts ocr["text"]
dk.close# One-shot query (simplest usage)
darwinkit query '{"jsonrpc":"2.0","id":"1","method":"nlp.language","params":{"text":"Guten Tag"}}'
# Pipe multiple requests in server mode
{
echo '{"jsonrpc":"2.0","id":"1","method":"nlp.sentiment","params":{"text":"Great product"}}'
echo '{"jsonrpc":"2.0","id":"2","method":"nlp.language","params":{"text":"Bonjour"}}'
} | darwinkit serve 2>/dev/null
# Extract text from screenshot with jq
darwinkit query '{"jsonrpc":"2.0","id":"1","method":"vision.ocr","params":{"path":"/tmp/screenshot.png"}}' \
| jq -r '.result.text'Bundle DarwinKit inside your Tauri app:
1. Place binaries in src-tauri/binaries/:
src-tauri/binaries/
darwinkit-aarch64-apple-darwin # Apple Silicon
darwinkit-x86_64-apple-darwin # Intel
2. Configure tauri.conf.json:
{
"bundle": {
"externalBin": ["binaries/darwinkit"]
}
}3. Spawn from Rust:
use tauri_plugin_shell::ShellExt;
let sidecar = app.shell().sidecar("darwinkit").unwrap().args(["serve"]);
let (mut rx, child) = sidecar.spawn().unwrap();
// Send request
child.write(b"{\"jsonrpc\":\"2.0\",\"id\":\"1\",\"method\":\"nlp.sentiment\",\"params\":{\"text\":\"hello\"}}\n").unwrap();
// Read response
while let Some(event) = rx.recv().await {
if let tauri_plugin_shell::process::CommandEvent::Stdout(line) = event {
let response: serde_json::Value = serde_json::from_slice(&line).unwrap();
println!("{}", response);
break;
}
}darwinkit/
Package.swift # Swift 5.9, macOS 13+
Sources/
DarwinKit/ # Thin CLI entry point
DarwinKit.swift # @main, serve + query subcommands
DarwinKitCore/ # All logic (importable by tests)
Server/
Protocol.swift # JSON-RPC types, AnyCodable
JsonRpcServer.swift # stdin/stdout NDJSON loop
MethodRouter.swift # Method dispatch + capabilities
Handlers/
SystemHandler.swift # system.capabilities
NLPHandler.swift # nlp.* methods
VisionHandler.swift # vision.ocr
Providers/
NLPProvider.swift # Protocol + Apple NaturalLanguage impl
VisionProvider.swift # Protocol + Apple Vision impl
Tests/
DarwinKitCoreTests/
ProtocolTests.swift # JSON-RPC encoding/decoding
NLPHandlerTests.swift # Mock provider tests
VisionHandlerTests.swift # Mock provider tests
All Apple framework calls are behind provider protocols. Tests use mock providers for deterministic, fast unit tests without requiring specific OS versions.
swift build # Debug build
swift build -c release # Release build
swift test # Run all 43 tests
swift test --filter NLP # Run NLP tests onlyswift build -c release --arch arm64 --arch x86_64- v0.1.0 (current) — NLP + Vision + JSON-RPC server
- v0.2.0 —
speech.transcribevia SFSpeechRecognizer - v0.3.0 —
llm.generatevia Apple Foundation Models (macOS 26+)
MIT