Skip to content

Commit 6856538

Browse files
committed
Merge branch 'pr-391' into dannys-build
* pr-391: fix(ui): hide Apple Intelligence option when unavailable fix(build): add fallback stub for Apple Intelligence on older SDKs feat: guide apple intelligence output feat: add Apple Intelligence post-processing provider # Conflicts: # src-tauri/src/actions.rs
2 parents f8e8b87 + ac62f49 commit 6856538

File tree

11 files changed

+612
-58
lines changed

11 files changed

+612
-58
lines changed

src-tauri/apple_intelligence.swift

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import Dispatch
2+
import Foundation
3+
import FoundationModels
4+
5+
@available(macOS 26.0, *)
6+
@Generable
7+
private struct CleanedTranscript: Sendable {
8+
let cleanedText: String
9+
}
10+
11+
// MARK: - Swift implementation for Apple LLM integration
12+
// This file is compiled via Cargo build script for Apple Silicon targets
13+
14+
private typealias ResponsePointer = UnsafeMutablePointer<AppleLLMResponse>
15+
16+
private func duplicateCString(_ text: String) -> UnsafeMutablePointer<CChar>? {
17+
return text.withCString { basePointer in
18+
guard let duplicated = strdup(basePointer) else {
19+
return nil
20+
}
21+
return duplicated
22+
}
23+
}
24+
25+
private func truncatedText(_ text: String, limit: Int) -> String {
26+
guard limit > 0 else { return text }
27+
let words = text.split(
28+
maxSplits: .max,
29+
omittingEmptySubsequences: true,
30+
whereSeparator: { $0.isWhitespace || $0.isNewline }
31+
)
32+
if words.count <= limit {
33+
return text
34+
}
35+
return words.prefix(limit).joined(separator: " ")
36+
}
37+
38+
@_cdecl("is_apple_intelligence_available")
39+
public func isAppleIntelligenceAvailable() -> Int32 {
40+
guard #available(macOS 26.0, *) else {
41+
return 0
42+
}
43+
44+
let model = SystemLanguageModel.default
45+
switch model.availability {
46+
case .available:
47+
return 1
48+
case .unavailable:
49+
return 0
50+
}
51+
}
52+
53+
@_cdecl("process_text_with_apple_llm")
54+
public func processTextWithAppleLLM(
55+
_ prompt: UnsafePointer<CChar>,
56+
maxTokens: Int32
57+
) -> UnsafeMutablePointer<AppleLLMResponse> {
58+
let swiftPrompt = String(cString: prompt)
59+
let responsePtr = ResponsePointer.allocate(capacity: 1)
60+
responsePtr.initialize(to: AppleLLMResponse(response: nil, success: 0, error_message: nil))
61+
62+
guard #available(macOS 26.0, *) else {
63+
responsePtr.pointee.error_message = duplicateCString(
64+
"Apple Intelligence requires macOS 26 or newer."
65+
)
66+
return responsePtr
67+
}
68+
69+
let model = SystemLanguageModel.default
70+
guard model.availability == .available else {
71+
responsePtr.pointee.error_message = duplicateCString(
72+
"Apple Intelligence is not currently available on this device."
73+
)
74+
return responsePtr
75+
}
76+
77+
let tokenLimit = max(0, Int(maxTokens))
78+
let semaphore = DispatchSemaphore(value: 0)
79+
80+
Task.detached(priority: .userInitiated) {
81+
defer { semaphore.signal() }
82+
do {
83+
let session = LanguageModelSession(model: model)
84+
var output: String
85+
86+
do {
87+
let structured = try await session.respond(
88+
to: swiftPrompt,
89+
generating: CleanedTranscript.self
90+
)
91+
output = structured.content.cleanedText
92+
} catch {
93+
let fallbackGeneration = try await session.respond(to: swiftPrompt)
94+
output = fallbackGeneration.content
95+
}
96+
97+
if tokenLimit > 0 {
98+
output = truncatedText(output, limit: tokenLimit)
99+
}
100+
responsePtr.pointee.response = duplicateCString(output)
101+
responsePtr.pointee.success = 1
102+
responsePtr.pointee.error_message = nil
103+
} catch {
104+
responsePtr.pointee.response = nil
105+
responsePtr.pointee.success = 0
106+
responsePtr.pointee.error_message = duplicateCString(error.localizedDescription)
107+
}
108+
}
109+
110+
semaphore.wait()
111+
return responsePtr
112+
}
113+
114+
@_cdecl("free_apple_llm_response")
115+
public func freeAppleLLMResponse(_ response: UnsafeMutablePointer<AppleLLMResponse>?) {
116+
guard let response = response else { return }
117+
118+
if let responseStr = response.pointee.response {
119+
free(UnsafeMutablePointer(mutating: responseStr))
120+
}
121+
122+
if let errorStr = response.pointee.error_message {
123+
free(UnsafeMutablePointer(mutating: errorStr))
124+
}
125+
126+
response.deallocate()
127+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#ifndef apple_intelligence_bridge_h
2+
#define apple_intelligence_bridge_h
3+
4+
// C-compatible function declarations for Swift bridge
5+
6+
#ifdef __cplusplus
7+
extern "C" {
8+
#endif
9+
10+
typedef struct {
11+
char* response;
12+
int success; // 0 for failure, 1 for success
13+
char* error_message; // Only valid when success = 0
14+
} AppleLLMResponse;
15+
16+
// Check if Apple Intelligence is available on the device
17+
int is_apple_intelligence_available(void);
18+
19+
// Process text using Apple's on-device LLM
20+
AppleLLMResponse* process_text_with_apple_llm(const char* prompt, int max_tokens);
21+
22+
// Free memory allocated by the Apple LLM response
23+
void free_apple_llm_response(AppleLLMResponse* response);
24+
25+
#ifdef __cplusplus
26+
}
27+
#endif
28+
29+
#endif /* apple_intelligence_bridge_h */
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import Foundation
2+
3+
// Stub implementation when FoundationModels is not available
4+
// This file is compiled via Cargo build script when the build environment
5+
// does not support Apple Intelligence (e.g. older Xcode/SDK).
6+
7+
private typealias ResponsePointer = UnsafeMutablePointer<AppleLLMResponse>
8+
9+
@_cdecl("is_apple_intelligence_available")
10+
public func isAppleIntelligenceAvailable() -> Int32 {
11+
return 0
12+
}
13+
14+
@_cdecl("process_text_with_apple_llm")
15+
public func processTextWithAppleLLM(
16+
_ prompt: UnsafePointer<CChar>,
17+
maxTokens: Int32
18+
) -> UnsafeMutablePointer<AppleLLMResponse> {
19+
let responsePtr = ResponsePointer.allocate(capacity: 1)
20+
// Initialize with safe defaults
21+
responsePtr.initialize(to: AppleLLMResponse(response: nil, success: 0, error_message: nil))
22+
23+
let msg = "Apple Intelligence is not available in this build (SDK requirement not met)."
24+
25+
// Duplicate the string for the C caller to own
26+
responsePtr.pointee.error_message = strdup(msg)
27+
28+
return responsePtr
29+
}
30+
31+
@_cdecl("free_apple_llm_response")
32+
public func freeAppleLLMResponse(_ response: UnsafeMutablePointer<AppleLLMResponse>?) {
33+
guard let response = response else { return }
34+
35+
if let responseStr = response.pointee.response {
36+
free(UnsafeMutablePointer(mutating: responseStr))
37+
}
38+
39+
if let errorStr = response.pointee.error_message {
40+
free(UnsafeMutablePointer(mutating: errorStr))
41+
}
42+
43+
response.deallocate()
44+
}

src-tauri/build.rs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,128 @@
11
fn main() {
2+
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
3+
build_apple_intelligence_bridge();
4+
25
tauri_build::build()
36
}
7+
8+
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
9+
fn build_apple_intelligence_bridge() {
10+
use std::env;
11+
use std::path::{Path, PathBuf};
12+
use std::process::Command;
13+
14+
const REAL_SWIFT_FILE: &str = "apple_intelligence.swift";
15+
const STUB_SWIFT_FILE: &str = "apple_intelligence_stub.swift";
16+
const BRIDGE_HEADER: &str = "apple_intelligence_bridge.h";
17+
18+
println!("cargo:rerun-if-changed={REAL_SWIFT_FILE}");
19+
println!("cargo:rerun-if-changed={STUB_SWIFT_FILE}");
20+
println!("cargo:rerun-if-changed={BRIDGE_HEADER}");
21+
22+
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
23+
let object_path = out_dir.join("apple_intelligence.o");
24+
let static_lib_path = out_dir.join("libapple_intelligence.a");
25+
26+
let sdk_path = String::from_utf8(
27+
Command::new("xcrun")
28+
.args(["--sdk", "macosx", "--show-sdk-path"])
29+
.output()
30+
.expect("Failed to locate macOS SDK")
31+
.stdout,
32+
)
33+
.expect("SDK path is not valid UTF-8")
34+
.trim()
35+
.to_string();
36+
37+
// Check if the SDK supports FoundationModels (required for Apple Intelligence)
38+
let framework_path = Path::new(&sdk_path)
39+
.join("System/Library/Frameworks/FoundationModels.framework");
40+
let has_foundation_models = framework_path.exists();
41+
42+
let source_file = if has_foundation_models {
43+
println!("cargo:warning=Building with Apple Intelligence support.");
44+
REAL_SWIFT_FILE
45+
} else {
46+
println!("cargo:warning=Apple Intelligence SDK not found. Building with stubs.");
47+
STUB_SWIFT_FILE
48+
};
49+
50+
if !Path::new(source_file).exists() {
51+
panic!("Source file {} is missing!", source_file);
52+
}
53+
54+
let swiftc_path = String::from_utf8(
55+
Command::new("xcrun")
56+
.args(["--find", "swiftc"])
57+
.output()
58+
.expect("Failed to locate swiftc")
59+
.stdout,
60+
)
61+
.expect("swiftc path is not valid UTF-8")
62+
.trim()
63+
.to_string();
64+
65+
let toolchain_swift_lib = Path::new(&swiftc_path)
66+
.parent()
67+
.and_then(|p| p.parent())
68+
.map(|root| root.join("lib/swift/macosx"))
69+
.expect("Unable to determine Swift toolchain lib directory");
70+
let sdk_swift_lib = Path::new(&sdk_path).join("usr/lib/swift");
71+
72+
let status = Command::new("xcrun")
73+
.args([
74+
"swiftc",
75+
"-target",
76+
"arm64-apple-macosx15.0",
77+
"-sdk",
78+
&sdk_path,
79+
"-O",
80+
"-import-objc-header",
81+
BRIDGE_HEADER,
82+
"-c",
83+
source_file,
84+
"-o",
85+
object_path
86+
.to_str()
87+
.expect("Failed to convert object path to string"),
88+
])
89+
.status()
90+
.expect("Failed to invoke swiftc for Apple Intelligence bridge");
91+
92+
if !status.success() {
93+
panic!("swiftc failed to compile {source_file}");
94+
}
95+
96+
let status = Command::new("libtool")
97+
.args([
98+
"-static",
99+
"-o",
100+
static_lib_path
101+
.to_str()
102+
.expect("Failed to convert static lib path to string"),
103+
object_path
104+
.to_str()
105+
.expect("Failed to convert object path to string"),
106+
])
107+
.status()
108+
.expect("Failed to create static library for Apple Intelligence bridge");
109+
110+
if !status.success() {
111+
panic!("libtool failed for Apple Intelligence bridge");
112+
}
113+
114+
println!("cargo:rustc-link-search=native={}", out_dir.display());
115+
println!("cargo:rustc-link-lib=static=apple_intelligence");
116+
println!(
117+
"cargo:rustc-link-search=native={}",
118+
toolchain_swift_lib.display()
119+
);
120+
println!("cargo:rustc-link-search=native={}", sdk_swift_lib.display());
121+
println!("cargo:rustc-link-lib=framework=Foundation");
122+
123+
if has_foundation_models {
124+
println!("cargo:rustc-link-lib=framework=FoundationModels");
125+
}
126+
127+
println!("cargo:rustc-link-arg=-Wl,-rpath,/usr/lib/swift");
128+
}

0 commit comments

Comments
 (0)