I Built a Free Local AI Art Pipeline on My Mac — Here's What Broke #4
yha9806
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
What if you could run a complete AI art creation pipeline — 13 cultural traditions, 5-dimension scoring, structured layer generation — entirely on your MacBook, for free?
No cloud API key. No GPU server. Just
pip install vulca.Three traditions, one SDK — generated locally via ComfyUI/SDXL on Apple Silicon, zero cloud API cost.
These images were generated on an Apple Silicon Mac running ComfyUI locally. No Midjourney subscription. No Replicate credits. No DALL-E API calls. The evaluation scores below come from a VLM (Gemma 4 via Ollama) running on the same machine:
This post is not a product announcement. It is a technical deep dive into what it took to build VULCA — the bugs we hit, the architectural decisions we made, and the code that holds it together.
1. What is VULCA + The Local Stack
VULCA is an AI-native cultural art creation SDK. It generates, evaluates, decomposes, and evolves visual art across 13 cultural traditions. It runs locally (ComfyUI + Ollama) or in the cloud (Gemini).
Not another Midjourney wrapper or ComfyUI plugin — a standalone SDK for cultural art intelligence.
The project started as academic research. The VULCA Framework was published at EMNLP 2025 Findings, and VULCA-Bench provides 7,410 annotated samples with L1-L5 cultural scoring definitions. The SDK implements this research as a production tool.
Architecture
Quickstart
Provider Architecture: Pluggable, Not Locked In
VULCA does not depend on any single backend. Image providers are pluggable classes. ComfyUI is one provider. Gemini is another. You can add your own.
The key design insight: providers declare their capabilities as a frozen set. VULCA uses these capabilities to decide how to format prompts, whether to pass CJK text directly, and whether RGBA output is available.
The
multilingual_promptcapability is the difference between a 120-token structured prompt (Gemini can handle it) and a compressed 60-token flat prompt (CLIP will truncate anything beyond 77 tokens). More on this in section 5.When you ask ComfyUI to generate an image, VULCA constructs a complete ComfyUI workflow as a JSON dict and submits it via the REST API. No ComfyUI nodes to install. No custom workflows to import. The entire workflow is built programmatically:
That is from
src/vulca/providers/comfyui.pylines 42-62. It constructs a standard SDXL pipeline: checkpoint loader, empty latent, two CLIP text encoders (positive + negative), KSampler, VAE decode, save. The workflow is submitted as a single POST to/prompt, and VULCA polls/history/{prompt_id}until the image is ready.After the image comes back, VULCA validates it is actually a valid PNG before accepting it:
That validation was added in commit
fdc0e45after we discovered that certain PyTorch MPS bugs cause ComfyUI to return 4KB files with valid PNG headers but all-zero pixel data.2. L1-L5 Cultural Evaluation
Most AI art tools generate. VULCA evaluates.
The evaluation framework scores artwork across five dimensions, each measuring a different aspect of cultural and artistic quality:
These are not arbitrary categories. They come from the VULCA-Bench paper, which defines L1-L5 across 7,410 annotated samples.
13 Traditions, Custom Weights
Each tradition is defined as a YAML file with its own L1-L5 weight distribution. Chinese freehand ink painting (xieyi) weights philosophical aesthetics (L5) at 30% and cultural context (L3) at 25%, because the tradition values spiritual resonance and canonical motifs above raw technical rendering. A brand design tradition would weight L2 (technical execution) much higher.
The 13 supported traditions are:
chinese_xieyi,chinese_gongbi,japanese_traditional,western_academic,islamic_geometric,watercolor,african_traditional,south_asian,brand_design,photography,contemporary_art,ui_ux_design, anddefault.Three Evaluation Modes
The API: Three Lines to Score Any Image
The full
aevaluate()signature fromsrc/vulca/evaluate.py:The
sparseparameter is worth calling out. Whensparse=True, VULCA runs aBriefIndexerthat determines which L1-L5 dimensions are most relevant to the given intent. All five dimensions are still scored (consistency matters), but thesparse_activationmetadata tells callers which dimensions were most salient. This is useful in pipeline mode where you want to focus review on the dimensions that matter for a specific prompt.3. Deep Dive: Structured Layer Generation
VULCA does not generate images. It generates layers.
The pipeline works like this:
Layer decomposition: paper, distant mountains, forest, calligraphy, composite
Serial-First Style Anchoring
The first layer generates serially as a style anchor. Its raw RGB output becomes the visual reference (
style_ref) for all subsequent layers, which generate in parallel. This is Defense 3 from v0.14 — without it, each layer would independently interpret "Chinese xieyi style" and you would get five different visual interpretations in the same artwork.The Prompt Builder
The core of layer generation is
build_anchored_layer_prompt()insrc/vulca/layers/layered_prompt.py. This function wraps the plan's regeneration prompt in four mandatory anchor blocks: canvas, content (with negative list), spatial, style.The function has two code paths, controlled by
english_only:When
english_only=False(Gemini path): Returns a structured multi-section string with[CANVAS],[CONTENT],[SPATIAL],[STYLE], and[USER INTENT]blocks. Gemini's LLM-based encoder can parse these sections and follow the instructions.When
english_only=True(ComfyUI/SDXL path): Returns aLayerPromptResultwith a flat, CLIP-friendly prompt under 70 tokens and a separatenegative_prompt. This is the path that took the most engineering to get right. More on why in section 5.CJK-Aware Prompt Handling
VULCA accepts prompts in Chinese, Japanese, and Korean. When the target provider has the
multilingual_promptcapability (Gemini), CJK text passes through natively. When the provider does not have that capability (ComfyUI/SDXL with CLIP), VULCA strips CJK characters and falls back to English equivalents:So
vulca create "水墨山水" -t chinese_xieyi --provider comfyuiworks — VULCA translates the prompt for CLIP behind the scenes.4. Deep Dive: Making SDXL Work Locally
This is where things got interesting. Two traps nearly derailed the local ComfyUI path.
Trap 1: The ANCHOR Hallucination
Our structured layer prompts originally used section headers like
[CANVAS ANCHOR],[STYLE ANCHOR], and[CONTENT ANCHOR]. The word "ANCHOR" was there to signal to the LLM that these were fixed constraints, not suggestions.SDXL's CLIP encoder is not an LLM. It is a text encoder that treats every token as content. When it saw "ANCHOR", it interpreted it as a request to paint an anchor — the nautical kind.
The result: literal ship anchors appearing on rice paper backgrounds in Chinese ink wash paintings. Misty mountains with a ship anchor in the corner. Bamboo forests with an anchor hovering over them.
The fix was trivial once diagnosed. Rename the headers to
[CANVAS],[STYLE],[CONTENT],[SPATIAL]. No word that could be interpreted as visual content.Commit:
b168178—fix(layers): remove ANCHOR from prompt headers — SDXL paints literal anchorsThe lesson: CLIP-based models do not have a concept of "metadata" or "instructions" in a prompt. Every token is content. If your prompt engineering uses structured headers, every header word will influence the generated image.
Trap 1b: The 77-Token CLIP Ceiling
Fixing the anchor hallucination revealed a second, subtler problem. Our structured prompt — even without "ANCHOR" — was 120+ tokens. CLIP truncates at 77 tokens. The actual subject description ("misty mountains after spring rain") was buried past the 77-token boundary and never reached the encoder.
Gallery images (simple prompts, ~30 tokens) worked perfectly. Layered generation (structured prompts, 120+ tokens) produced generic, unfocused results. The debugging was confusing because the same code path worked for simple creates but failed for layered creates.
The fix: the
english_onlybranch inbuild_anchored_layer_prompt(). Instead of a structured multi-section prompt, VULCA builds a flat, subject-first prompt under 70 tokens:Plus a separate
negative_promptfield (other layer roles to avoid). The subject comes first so it is guaranteed to be within CLIP's 77-token window.Commit:
74f9952—fix(layers): CLIP-aware prompt compression for SDXL — flat <70 token promptThe
LayerPromptResultdataclass was added specifically for this:The structured string (Gemini path) returns a single
str. The CLIP path returns aLayerPromptResultwith both positive and negative prompts separated. The caller checksisinstance(result, LayerPromptResult)to decide which ComfyUI workflow nodes to populate.Trap 2: PyTorch MPS — A Version Minefield
With prompt engineering fixed, we hit the hardware layer. SDXL generation via ComfyUI on Apple Silicon (MPS backend) with PyTorch 2.11.0 produces black (all-zero, ~4KB) or noise (~2MB random pixels) images.
Key observations that made this hard to diagnose:
--force-fp32does NOT fix it — this is a correctness bug, not a precision issueThree compounding PyTorch MPS bugs cause the failure:
Bug 1: SDPA Non-Contiguous Tensor Regression (pytorch/pytorch#163597)
Introduced in PyTorch 2.8.0. MPS SDPA kernels produce wildly incorrect results when given non-contiguous tensors. SDXL's cross-attention performs transpose operations that create non-contiguous views, feeding garbage embeddings into the U-Net. Error magnitude: ~34.0 vs normal ~0.000006.
Bug 2: Conv2d Chunk Correctness Bug (pytorch/pytorch#169342)
Affects PyTorch 2.9.0+. The
chunk() -> conv()pattern produces correct results only for the first batch element. Single-image generation (batch=1) is unaffected. Multi-image batch workflows will hit it.Bug 3: Metal Kernel Migration Regressions (pytorch/pytorch#155797)
PyTorch 2.10-2.11 introduced additional MPS regressions during internal operator migrations. Identical symptoms reported on M3 Ultra via ComfyUI#10681.
Why VAEDecode Is the Failure Point
The VAE decoder is uniquely vulnerable:
The Version Matrix
--force-fp32can helpThe Fix
Pin
torch==2.9.0. That is the entire fix. We wrote a complete Apple Silicon MPS + ComfyUI/SDXL Compatibility Guide that covers diagnosis, workarounds (CPU VAE, force-fp32), environment variables, and verification steps.The guide is at
docs/apple-silicon-mps-comfyui-guide.mdin the repo.5. Inpainting and Layer Editing
Once you have layers, you can edit them individually without regenerating the entire artwork.
The inpaint path uses the same provider architecture. ComfyUI receives an inpaint workflow with a mask, Gemini receives the image + mask + instruction as a multipart prompt. The same
capabilitiessystem determines prompt formatting.6. What's Working, What's Next
Current State (v0.15.1)
The Commit Trail
The local provider path was stabilized across these commits:
b168178— remove ANCHOR from prompt headers42e0e3d— skip keying for background layersfdc0e45— validate ComfyUI PNG response74f9952— CLIP-aware prompt compressione840496— MPS compatibility guide485067e— v0.15.1 releaseRoadmap
limit: 0). Text + VLM vision work. Once billing is enabled, Gemini becomes the zero-setup cloud alternative.7. Get Started
5-Minute Local Setup
Python API
What VULCA Is
VULCA is an open-source SDK for AI-native cultural art creation. It brings cultural intelligence to AI art generation. 13 traditions, each with its own L1-L5 scoring rubric, terminology, and taboos.
It is built on peer-reviewed research (EMNLP 2025 Findings), tested against 7,410 annotated samples (VULCA-Bench), and runs entirely on your local machine if you want it to.
What VULCA Is Not
Links
docs/apple-silicon-mps-comfyui-guide.mdIf this resonates, star us on GitHub. Try it, break it, tell us what failed — issues welcome.
If you use VULCA in research, please cite:
13 traditions. One SDK. Your machine.
All reactions