Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions src-tauri/build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,128 @@
fn main() {
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
build_apple_intelligence_bridge();

tauri_build::build()
}

#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
fn build_apple_intelligence_bridge() {
use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;

const REAL_SWIFT_FILE: &str = "swift/apple_intelligence.swift";
const STUB_SWIFT_FILE: &str = "swift/apple_intelligence_stub.swift";
const BRIDGE_HEADER: &str = "swift/apple_intelligence_bridge.h";

println!("cargo:rerun-if-changed={REAL_SWIFT_FILE}");
println!("cargo:rerun-if-changed={STUB_SWIFT_FILE}");
println!("cargo:rerun-if-changed={BRIDGE_HEADER}");

let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
let object_path = out_dir.join("apple_intelligence.o");
let static_lib_path = out_dir.join("libapple_intelligence.a");

let sdk_path = String::from_utf8(
Command::new("xcrun")
.args(["--sdk", "macosx", "--show-sdk-path"])
.output()
.expect("Failed to locate macOS SDK")
.stdout,
)
.expect("SDK path is not valid UTF-8")
.trim()
.to_string();

// Check if the SDK supports FoundationModels (required for Apple Intelligence)
let framework_path =
Path::new(&sdk_path).join("System/Library/Frameworks/FoundationModels.framework");
let has_foundation_models = framework_path.exists();

let source_file = if has_foundation_models {
println!("cargo:warning=Building with Apple Intelligence support.");
REAL_SWIFT_FILE
} else {
println!("cargo:warning=Apple Intelligence SDK not found. Building with stubs.");
STUB_SWIFT_FILE
};

if !Path::new(source_file).exists() {
panic!("Source file {} is missing!", source_file);
}

let swiftc_path = String::from_utf8(
Command::new("xcrun")
.args(["--find", "swiftc"])
.output()
.expect("Failed to locate swiftc")
.stdout,
)
.expect("swiftc path is not valid UTF-8")
.trim()
.to_string();

let toolchain_swift_lib = Path::new(&swiftc_path)
.parent()
.and_then(|p| p.parent())
.map(|root| root.join("lib/swift/macosx"))
.expect("Unable to determine Swift toolchain lib directory");
let sdk_swift_lib = Path::new(&sdk_path).join("usr/lib/swift");

let status = Command::new("xcrun")
.args([
"swiftc",
"-target",
"arm64-apple-macosx26.0",
"-sdk",
&sdk_path,
"-O",
"-import-objc-header",
BRIDGE_HEADER,
"-c",
source_file,
"-o",
object_path
.to_str()
.expect("Failed to convert object path to string"),
])
.status()
.expect("Failed to invoke swiftc for Apple Intelligence bridge");

if !status.success() {
panic!("swiftc failed to compile {source_file}");
}

let status = Command::new("libtool")
.args([
"-static",
"-o",
static_lib_path
.to_str()
.expect("Failed to convert static lib path to string"),
object_path
.to_str()
.expect("Failed to convert object path to string"),
])
.status()
.expect("Failed to create static library for Apple Intelligence bridge");

if !status.success() {
panic!("libtool failed for Apple Intelligence bridge");
}

println!("cargo:rustc-link-search=native={}", out_dir.display());
println!("cargo:rustc-link-lib=static=apple_intelligence");
println!(
"cargo:rustc-link-search=native={}",
toolchain_swift_lib.display()
);
println!("cargo:rustc-link-search=native={}", sdk_swift_lib.display());
println!("cargo:rustc-link-lib=framework=Foundation");

if has_foundation_models {
println!("cargo:rustc-link-lib=framework=FoundationModels");
}

println!("cargo:rustc-link-arg=-Wl,-rpath,/usr/lib/swift");
}
52 changes: 45 additions & 7 deletions src-tauri/src/actions.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
use crate::apple_intelligence;
use crate::audio_feedback::{play_feedback_sound, play_feedback_sound_blocking, SoundType};
use crate::managers::audio::AudioRecordingManager;
use crate::managers::history::HistoryManager;
use crate::managers::transcription::TranscriptionManager;
use crate::settings::{get_settings, AppSettings};
use crate::settings::{get_settings, AppSettings, APPLE_INTELLIGENCE_PROVIDER_ID};
use crate::shortcut;
use crate::tray::{change_tray_icon, TrayIconState};
use crate::utils::{self, show_recording_overlay, show_transcribing_overlay};
Expand Down Expand Up @@ -86,12 +88,6 @@ async fn maybe_post_process_transcription(
return None;
}

let api_key = settings
.post_process_api_keys
.get(&provider.id)
.cloned()
.unwrap_or_default();

debug!(
"Starting LLM post-processing with provider '{}' (model: {})",
provider.id, model
Expand All @@ -101,6 +97,48 @@ async fn maybe_post_process_transcription(
let processed_prompt = prompt.replace("${output}", transcription);
debug!("Processed prompt length: {} chars", processed_prompt.len());

if provider.id == APPLE_INTELLIGENCE_PROVIDER_ID {
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
{
if !apple_intelligence::check_apple_intelligence_availability() {
debug!("Apple Intelligence selected but not currently available on this device");
return None;
}

let token_limit = model.trim().parse::<i32>().unwrap_or(0);
return match apple_intelligence::process_text(&processed_prompt, token_limit) {
Ok(result) => {
if result.trim().is_empty() {
debug!("Apple Intelligence returned an empty response");
None
} else {
debug!(
"Apple Intelligence post-processing succeeded. Output length: {} chars",
result.len()
);
Some(result)
}
}
Err(err) => {
error!("Apple Intelligence post-processing failed: {}", err);
None
}
};
}

#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
{
debug!("Apple Intelligence provider selected on unsupported platform");
return None;
}
}

let api_key = settings
.post_process_api_keys
.get(&provider.id)
.cloned()
.unwrap_or_default();

// Create OpenAI-compatible client
let client = match crate::llm_client::create_client(&provider, api_key) {
Ok(client) => client,
Expand Down
71 changes: 71 additions & 0 deletions src-tauri/src/apple_intelligence.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int};

// Define the response structure from Swift
#[repr(C)]
pub struct AppleLLMResponse {
pub response: *mut c_char,
pub success: c_int,
pub error_message: *mut c_char,
}

// Link to the Swift functions
extern "C" {
pub fn is_apple_intelligence_available() -> c_int;
pub fn process_text_with_apple_llm(
prompt: *const c_char,
max_tokens: i32,
) -> *mut AppleLLMResponse;
pub fn free_apple_llm_response(response: *mut AppleLLMResponse);
}

// Safe wrapper functions
pub fn check_apple_intelligence_availability() -> bool {
unsafe { is_apple_intelligence_available() == 1 }
}

pub fn process_text(prompt: &str, max_tokens: i32) -> Result<String, String> {
let prompt_cstr = CString::new(prompt).map_err(|e| e.to_string())?;

let response_ptr = unsafe { process_text_with_apple_llm(prompt_cstr.as_ptr(), max_tokens) };

if response_ptr.is_null() {
return Err("Null response from Apple LLM".to_string());
}

let response = unsafe { &*response_ptr };

let result = if response.success == 1 {
if response.response.is_null() {
Ok(String::new())
} else {
let c_str = unsafe { CStr::from_ptr(response.response) };
let rust_str = c_str.to_string_lossy().into_owned();
Ok(rust_str)
}
} else {
let error_c_str = if !response.error_message.is_null() {
unsafe { CStr::from_ptr(response.error_message) }
} else {
CStr::from_bytes_with_nul(b"Unknown error\0").unwrap()
};
let error_msg = error_c_str.to_string_lossy().into_owned();
Err(error_msg)
};

// Clean up the response
unsafe { free_apple_llm_response(response_ptr) };

result
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_availability() {
let available = check_apple_intelligence_availability();
println!("Apple Intelligence available: {}", available);
}
}
2 changes: 2 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
mod actions;
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
mod apple_intelligence;
mod audio_feedback;
pub mod audio_toolkit;
mod clipboard;
Expand Down
Loading