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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ temp/
# IDE
.vscode/
.idea/

# FoundationModels backend — compiled binary (build with tools/build_fm.sh)
tools/fm-generate
*.o
4 changes: 4 additions & 0 deletions comfyui_coreai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from .nodes.embedding import CoreAIImageTextSimilarity
from .nodes.instance_seg import CoreAIInstanceSegmentation
from .nodes.loader import CoreAIModelLoader, CoreAIHealthCheck
from .nodes.apple_text import CoreAIAppleText

# --- ComfyUI node registration ---

Expand All @@ -39,6 +40,8 @@
"CoreAIImageTextSimilarity": CoreAIImageTextSimilarity,
# Generation
"CoreAIImageGeneration": CoreAIImageGeneration,
# Apple on-device (FoundationModels, macOS 26+ — no runner / macOS 27 needed)
"CoreAIAppleText": CoreAIAppleText,
# Utils
"CoreAIModelLoader": CoreAIModelLoader,
"CoreAIHealthCheck": CoreAIHealthCheck,
Expand All @@ -52,6 +55,7 @@
"CoreAIInstanceSegmentation": "CoreAI Instance Segmentation",
"CoreAIImageTextSimilarity": "CoreAI CLIP Similarity",
"CoreAIImageGeneration": "CoreAI Image Generation (FLUX.2)",
"CoreAIAppleText": "CoreAI Apple Text (FoundationModels)",
"CoreAIModelLoader": "CoreAI Model Loader",
"CoreAIHealthCheck": "CoreAI Health Check",
}
Expand Down
91 changes: 91 additions & 0 deletions comfyui_coreai/nodes/apple_text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""
nodes/apple_text.py — CoreAI Apple Text (FoundationModels).

REAL on-device text generation using Apple's SYSTEM language model
(FoundationModels, macOS 26+, Apple Intelligence enabled), via the
tools/fm-generate Swift CLI.

This is distinct from the coreai-catalog `.aimodel` vision models (depth, SAM,
detection, VLM, image-gen) — those run on the coreai-runner over Core AI and need
macOS 27's `CoreAI` framework. FoundationModels ships with macOS 26 and is
text-only, so this node runs TODAY without the runner or macOS 27. Useful for
generating / expanding prompts for the diffusion node, on-device and private.
"""

from __future__ import annotations

import logging
import os
import subprocess
from pathlib import Path
from typing import Any

logger = logging.getLogger("ComfyUI-CoreAI")


def _fm_binary() -> str | None:
"""Locate the compiled fm-generate CLI (env override, then repo tools/)."""
env = os.environ.get("COREAI_FM_PATH")
if env and Path(env).exists():
return env
# comfyui_coreai/nodes/apple_text.py -> repo root -> tools/fm-generate
cand = Path(__file__).resolve().parents[2] / "tools" / "fm-generate"
if cand.exists() and os.access(cand, os.X_OK):
return str(cand)
return None


class CoreAIAppleText:
"""On-device text generation with Apple's FoundationModels (macOS 26+).

Text-only (FoundationModels has no image input on macOS 26). Requires Apple
Intelligence enabled; the system model downloads on first use.
"""

@classmethod
def INPUT_TYPES(cls) -> dict[str, Any]:
return {
"required": {
"prompt": (
"STRING",
{
"default": "Write a vivid one-line image prompt: a serene mountain lake at golden hour.",
"multiline": True,
"tooltip": "Apple's on-device text model (FoundationModels). "
"Runs on macOS 26+ with Apple Intelligence on — no coreai-runner needed.",
},
),
}
}

RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
FUNCTION = "generate"
CATEGORY = "CoreAI/Apple"

def generate(self, prompt: str):
binary = _fm_binary()
if not binary:
return (
"[FoundationModels backend not built — run tools/build_fm.sh "
"(macOS 26+, Apple Silicon).]",
)
try:
result = subprocess.run(
[binary, "--prompt", prompt],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass prompts via stdin instead of argv

When a workflow prompt contains private text, passing it as --prompt exposes the full prompt in the helper process's command-line arguments for the duration of generation, where local process-listing tools can read it. This undermines the node's on-device/private use case; send the prompt over stdin or a private temp file instead.

Useful? React with 👍 / 👎.

capture_output=True,
text=True,
timeout=120,
)
except Exception as e: # noqa: BLE001
logger.warning("fm-generate invocation failed: %s", e)
return (f"[fm-generate failed: {e}]",)

if result.returncode == 0:
return (result.stdout.strip(),)
if result.returncode == 3:
return (
f"[Apple model unavailable — {result.stderr.strip()}. Enable Apple "
"Intelligence in System Settings; the model downloads on first use.]",
)
return (f"[Apple model error: {result.stderr.strip() or result.returncode}]",)
6 changes: 6 additions & 0 deletions tools/build_fm.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
# Build the FoundationModels backend CLI (macOS 26+, Apple Silicon).
set -euo pipefail
cd "$(dirname "$0")"
swiftc -O -parse-as-library fm-generate.swift -o fm-generate
echo "✓ built tools/fm-generate — test: ./fm-generate --prompt 'hi'"
41 changes: 41 additions & 0 deletions tools/fm-generate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// fm-generate.swift — REAL on-device text generation via Apple's FoundationModels
// (macOS 26+, Apple Intelligence enabled). This is Apple's SYSTEM language model —
// NOT a coreai-catalog .aimodel (those are vision models needing macOS 27's CoreAI
// framework). Text-only: FoundationModels on macOS 26 has no image input.
//
// Build: swiftc -O -parse-as-library tools/fm-generate.swift -o tools/fm-generate
// Run: tools/fm-generate --prompt "Describe the Apple Neural Engine in one sentence."
//
// Exit codes: 0 ok · 1 generation error · 3 model unavailable (stderr: FM_UNAVAILABLE:<reason>)
import Foundation
import FoundationModels

@main
struct FMGenerate {
static func main() async {
var prompt = ""
let args = Array(CommandLine.arguments.dropFirst())
var i = 0
while i < args.count {
if args[i] == "--prompt", i + 1 < args.count { prompt = args[i + 1]; i += 2; continue }
i += 1
}
if prompt.isEmpty {
FileHandle.standardError.write("usage: fm-generate --prompt <text>\n".data(using: .utf8)!)
exit(2)
}
let model = SystemLanguageModel.default
guard case .available = model.availability else {
FileHandle.standardError.write("FM_UNAVAILABLE: \(model.availability)\n".data(using: .utf8)!)
exit(3)
}
do {
let session = LanguageModelSession()
let reply = try await session.respond(to: prompt)
print(reply.content)
} catch {
FileHandle.standardError.write("FM_ERROR: \(error)\n".data(using: .utf8)!)
exit(1)
}
}
}
Loading