Tyler Garrett
Screenshot 2026-06-08 at 8.11.03 PM

Tyler Garrett

Founder, dev3lop

Screenshot 2026-06-06 at 9.01.20 AMScreenshot 2026-06-04 at 10.34.36 PMScreenshot 2026-06-09 at 6.55.41 AMIMG_0001Screenshot 2026-06-07 at 8.42.47 AM

Recent

general Tree Chop, Make Wood, USA Hand Made Nunchucks Sep 3, 2026

I've always wanted to do this and here I am. Cutting down wood to be turned into wooden nunchucks. I know that sounds wild but that's what I'm doing. Cutting down a small tree, so that I can turn it into something. Being that I am eager to go from tree to object, and my client Heritage Beam and Board in Wisconsin has given me this opportunity. Of course their old barn wood route is much cooler overall, and the source of my nunchucks, the owner said I could give this a swing! So barn wood hand crafted nunchucks they created thus far are from old barn wood, this is just a small dead tree I am wacking!... So yea! This, it's the history of our country. Also, what they created - barn wood to nunchucks, likely the oldest nunchucks in america, and the only ones being hand crafted and verified by a real human! What I learned when shopping for american made nunchucks, it's just people buying things not created in america and they re-string them. Very inexpensive wood, made with a machine, and claimed to be american made. Not something I'm going to sink any of my dollars into.. So much of wood is just machine crafted, using new wood that was pumped with fertilizer, and automatically built. Not like what I'm chopping, although what I'm chopping is dead, it was fertilized. Much like the barn wood used in the creation of the nunchucks on BJJ.AC... This tree was grown by itself. It grew, it died, I found, I cut the bottom of this dead tree down. The wood is light. Much lighter than the barn wood nunchucks. What's cool is my software engineering engagement turned into building the first pair of American made nunchucks, and priced appropriately for being built in America. Not some junk you buy online, a real product, hand made in a time where automation wins.

I traveled north, visited the makers of the hand made nunchucks, Heritage Beam and Board, and learned about the entire process. The first two are finished, and beautiful. They are the heaviest nunchucks I've ever used, one of the makers has claimed the first elbow injury, and I'm sure I'm in for a fair share of clunks. I do not recommend purchasing these nunchucks without some really solid experience. They work great as wood chimes if you're feeling cheeky, or a laptop stand that is worthy of a convo in a public space. Alright back to living. Chat more soon.

IMG_0001 hand made nunchucks.
Open →
general Wordpress is the new Excel Online Aug 19, 2026

Wordpress has become the internets new excel file. The place where you paint yourself into a corner, manually. Although AI is being added to excel and wordpress themes, it's dominated by manual effort to keep a bloated and old system alive. Wordpress is a dying artwork, a lot like excel. It was great at prototyping, but today wordpress is now the axe in a good companies knee.

Wordpress is now the new skill gap for every business under the sun. With the rise of AI hacking, simple scripts are now becoming weaponized in enterprise methods. Meaning anyone is able to now generate the most powerful and diverse hacking software, known to the earth, simply due to having access to LLMs. Now, wordpress is simply a target, in world of LLMs being able to create you a static website.

Why do static websites beat wordpress?

LLMs connected to code editors, a github repo, and attached to netlify will dunk every wordpress website in 24 to 48 hours. I speak from experience having moved small wordpress and big wordpress websites (1k+ pages) to static web pages with great ease. Each time, creating a more sophisticated means of migrating. One being the sitemap, next being simple web scrapes, and beyond that it's a wordpress export, redirect export, and google search console scraped by an ai agent and poof. A few prompts later you're done? Then you can ask it to be faster than your original website, and you're at $20/month spend? So to access an app that can rug your wordpress website, you're spending less per month than your hosting. I know hosting is expensive on wordpress when the site has a ton of activity, it's expensive to keep it safe, and now LLM access is more affordable than ever, enabling users to quickly go from wordpress to astro/react/vite/tailwind.

My suggestion, drop wordpress while you have time to be competitive.

Open →
general I'm back at BJJ Aug 15, 2026

Took a long break from BJJ, sadly I hurt my knee sparing a super heavy weight at the ATOS gym in bee cave during a gi session. Was something very strange, a noise I feel like that was more helpful adjustment than anything, turned out to be a possible tear in my knee. But I don't really know, first thing I did when I got home, I attacked it with a massage tool for hours, then my knee was swollen. Makes me wonder, perhaps I did that with the massage hero day or I just need to admit to myself that I am just dealing with an injury, this is just how it goes.

Now, while in BJJ, I'm realizing that I can't just spar anyone, and I have to respect weight classes more appropriately. Just because I can scrap with heavy weights or super heavy weights, doesn't mean my body enjoys nor do I enjoy the recovery timer. That's one thing I need to realize, if I want to play in the mud, I should expect to get very dirty.

Fortunately my instructor at Gracie Barra has not given up on me during my injury, and keeps inviting me to visit. So I've made it to now 3 no gi sessions, each extremely helpful and although I'm sparring less, I'm learning a lot and getting to come back more often. My theory is my body is just not ready to spar at the level most people are at in these gyms, and it will take time to get there. It's not just strength, conditioning, but it's also the nervous system. My nervous system is improving, but it's still an impactful situation that I must learn to evolve with.

Regardless of the injury, I'm glad to find no gi is really good for me, and I think knowing this element of grappling is key to being better at bjj with a gi. Excited to see how it translates.

Open →
general localVideo — findings Aug 5, 2026

Notes from building a local image/video generation app on Apple Silicon. Everything here was measured on one machine unless stated otherwise:

M5 Max · 128 GB unified memory · macOS 26.4 · ComfyUI 0.29.0 · torch 2.13.0 · Python 3.12.13 Models: LTX 2.3 22B (BF16) for video, Krea 2 for images.

Numbers from a single machine and a single library. They are real measurements, not benchmarks — treat them as orders of magnitude, not league tables.


1. The FP8 wall on Apple Silicon

Almost every curated model weight you can download is FP8. Metal has no float8 kernels. An FP8 tensor on the GPU cannot be converted, copied off, or even reinterpreted with .view(). It fails at load, not at inference:

RuntimeError: Undefined type Float8_e4m3fn

The same tensors work fine on CPU. There is no userspace workaround for the GPU path — you cannot cast your way out, because the cast itself is the unsupported operation.

This is why so many "local generation" tools hand a Mac user a 30 GB download that crashes on load.

The inversion that fixes it

FP8 exists to squeeze large models into 24 GB consumer NVIDIA cards. That constraint does not apply to a Mac with unified memory.

  • torch.mps.recommended_max_memory() on this machine: 115.4 GB
  • LTX 2.3 22B, uncompressed BF16: ~46 GB

So the fix is to stop compressing. Use the BF16 build the quantised one was derived from. The "compatibility" format is the problem; the "too big" format is the solution.

Blog angle: the counterintuitive one — on a big-memory Mac, the larger download is the one that works.

ComfyUI flags that matter

PYTORCH_ENABLE_MPS_FALLBACK=1 PYTORCH_MPS_HIGH_WATERMARK_RATIO=0.0 \
  python main.py --use-split-cross-attention --fp32-vae
  • Never --bf16-vae — breaks the LTX audio VAE.
  • Never --force-fp16 — black frames on recent macOS.
  • PYTORCH_MPS_HIGH_WATERMARK_RATIO=0.0 lifts the allocator's default ~0.75×RAM

cap, which you need for a 46 GB model.


2. Measured generation cost

JobTime
Image 512² · 1 step~17 s
Image 512² · 4 steps~20 s
Image 512² · 8 steps~42 s
Image 1024² · 8 steps~60 s
Video 2 s · draft · 640²~87 s
LTX 2.3 22B BF16 sampling~7.4 s/step @ 49 frames · ~36 s/step @ 201 frames

Fixed overhead dominates short image jobs

1 step costs 17 s and 4 steps costs 20 s. Three extra sampling steps cost 3 seconds; the job costs 17 seconds before it samples anything. Model load, text encode and VAE decode swamp the actual diffusion at low step counts.

Practical consequence: dropping from 4 steps to 1 saves ~15%, not ~75%. People optimise the wrong number.

The attention ceiling is a hard wall, not a slowdown

Refined (2× upscale) video is bounded by attention memory:

(W/32) · (H/32) · (frames/8 + 1)  >  ~45,000 tokens  →  allocation aborts

This is worth computing before submitting rather than discovering as a crash. It also means "longer" and "bigger" trade against each other on a fixed budget — you can have a long small clip or a short large one.


3. Making an extension not look like a cut

Generating a continuation from the last frame of a clip produces a visible seam. The interesting finding is why.

A single still tells the model where things are, but not where they were going. Motion restarts from rest at the join. It is not a colour or detail problem — it is a velocity problem.

LTXVAddGuide accepts a multi-frame video guide, not just one image. Feeding the last 9 frames instead of the last 1 carries velocity across the join.

Measured on the same source clip:

worst frame-to-frame jumpratio vs. clip average
single still1.632.35×
9-frame motion guide1.082.10×

Honest caveat: this did not reach the ~1.5× ratio where a cut becomes genuinely invisible. The residual difference is tone, not motion — the re-rendered frames land about 1.2/255 darker. Dissolving across the overlapping frames hides that step, and it measures better than either a hard cut or a blind crossfade, because you are blending two takes of the same instants rather than two different moments.

Guides are cropped to 8n+1 frames, so 1, 9, and 17 are the usable depths.

The "it only zooms out" mystery

Early clips all drifted backwards regardless of prompt. The cause was not prompting — it was LTXVPreprocess(img_compression), which is the motion knob. ComfyUI's default is 35; this app had shipped 18. Lower compression produced less motion, which read as a slow zoom-out.

Blog angle: a parameter named "image compression" is the motion control. The name tells you nothing.


4. Drawing a lineage graph correctly

The app draws generation history as a left-to-right DAG: an image spawns clips, a clip spawns extensions, an extension gets mended back onto its source.

The bug that makes chains "fall upward"

A tidy-tree pass placed each node, then pushed it down to clear its column — but left the descendants it had already placed behind. This is Reingold–Tilford missing its modifier/shift step. The deeper a chain ran, the further it lagged, so every chain visibly climbed away from the origin it came from.

Measured over a real 68-node library, in row units:

mean parent→child driftworst edgegroup spreadgroup off-centre
before2.6510.56.363.68
after0.411.500

The off-centre and spread going to exactly 0 is not tuning. Placing each node at the centre of the bounding block it produced — rather than the midpoint of its first and last child — makes every subtree symmetric about its own root by induction. "Centred in its grouping" falls out of the algorithm.

Recursion depth is the number of columns

A textbook recursive contour implementation blew the stack at ~5,300 columns. The non-recursive version (pre-order flatten, then read backwards for post-order) does:

  • a 5,000-column chain in 5 ms
  • an 8,191-node binary tree in under 1 ms

A generation history grows without bound, so this is not hypothetical.

Hang nodes off their deepest parent, not their first

A "mend" joins a clip and that clip's own extension — it has two parents. Hanging it off the deepest one makes the chain read left-to-right instead of doubling back. Mend edge length dropped from 1.39 → 0.32 rows mean.


5. Export formats: measure, don't estimate

For a 20.2 s clip at 896 px:

FormatSizeTime
MP4 (stream copy)0.95 MB81 ms
MP4 silent0.81 MB155 ms
WebM (VP9)0.68 MB1.8 s
WebM silent0.45 MB1.8 s
GIF @ 480px/12fps10.83 MB0.7 s
GIF ping-ponged21.61 MB1.3 s

GIF is ~15× the size of VP9 for the same seconds. A full-resolution GIF of a 20 s clip is tens of megabytes and shareable nowhere, which is why hard fps and width ceilings are the only honest option.

Why not estimate sizes from bitrate

Because GIF size depends almost entirely on how much the frames move, and generated clips range from a static lantern to a whip pan. Bitrate maths would be instant and wrong exactly where it matters. Producing all three formats and reporting real bytes costs ~1.9 s and is then cached — a confident "about 3 MB" that arrives as 11 MB is worse than a two-second wait.

VP9 is unusable at defaults

-deadline good -cpu-used 4 -row-mt 1 is the difference between 0.9 s for a 10 s clip and minutes. Worth knowing before concluding VP9 is too slow to offer.

GIF needs a generated palette

Without palettegen/paletteuse, the encoder falls back to a generic 256-colour table and gradients band badly — which is most of what generated clips are.

One ffmpeg pass, not N

For a 12-frame filmstrip, tile=12x1 in a single pass takes 97 ms. Pulling twelve frames individually is twelve process spawns and twelve full decodes — a GIF has no keyframe index, so every seek re-decodes from the start.


6. What a real library looks like

Composition of a working 137-item library, which shaped several design decisions:

OriginCount
text → video68
extensions24
mends24
image → video (source still in library)13
image → video (external upload, not stored)7
loop exports1

"Re-run this from the same source frame" is a minority path. Most items have no source frame at all. A feature that promises it universally is lying for the majority — it has to route by origin and disable itself with a reason otherwise.

Most settings were never chosen

116 of the items never recorded a motion value. The server default simply applied. Any UI that shows "Motion: 55" for those is presenting an accident as a decision.

This forced a three-state model rather than the obvious two:

  • set — recorded, differs from the default: the user chose it
  • default — recorded, matches the default
  • assumed — never recorded: the default applied and nobody agreed to it

A default that had never once run

video.js declared baseStrength: 0.95. But the request validator materialised strength: 1 before the builder could ever reach its own fallback — making the 0.95 unreachable dead code. The library proved it: 74 of 82 clips recorded strength 1, and not one recorded 0.95, while the UI slider sat at "0.95" and called it the default.

Blog angle: a default value that has never executed, in a UI that advertises it, is invisible until you count what actually landed on disk.

Payload weight is mostly repetition

/api/lineage for this library: 110,310 bytes. Adding raw generation params per node would add +76% — and models{} alone accounts for 32,674 bytes: six identical filename strings repeated once per node. A whitelist plus a payload-level dedupe table costs +19–27% instead.

Most items are unconnected

68 nodes with 29 apparent origins collapsed to 50 nodes and 11 origins once items with no parent and no children were hidden. Roughly a quarter of a real library is one-offs that bury the lineage the graph exists to show.


7. Bugs worth writing up

A collection of failures that were invisible rather than loud — the interesting category.

Number(null) is 0, and 0 is finite. URLSearchParams.get() returns null for an absent parameter. A naive Number.isFinite guard therefore accepts it and clamps an absent speed to the 0.25 floor — silently returning slow motion for a request that never mentioned speed.

A silent no-op from a lookup that returned the wrong map. Graph node clicks resolved through the raw fetched data, while the DOM was built from the rendered view. Nodes that existed only in the view (in-flight jobs) returned null and the click quietly did nothing.

A deliberate design choice becoming a bug one feature later. In-flight "ghost" nodes deliberately never wrote themselves onto their parent's children array — correct, because a finished job would otherwise leave a dangling id forever. Two features later, chain-selection walked children and silently skipped every running job. The original comment explaining why is what made the second bug findable.

A graph walk that overshoots its own filter. A reachability helper marked ids as visited before checking whether they resolved, so under a filter it returned nodes one hop outside the visible set. In a "file these into a group" feature, that means filing items the user cannot see.

A fast path that fired too eagerly. A stream-copy shortcut triggered on any "repeat once at 1× speed" — including requests that changed container or dropped audio. Asking for WebM would have handed back an untouched MP4.

Audio reintroduced after being declined. A ping-pong filter chain rebuilt its audio branch from "does the source have audio" rather than "did we resolve an audio label", re-adding a track the caller had just asked to drop.

ffmpeg rejects an empty -filter_complex. A plain transcode pushes no filters, so the flag must be omitted entirely rather than passed empty.

An extension guard that corrupted by coercing. An upload path forced any unknown extension to .png — so a GIF was storable, just stored as GIF bytes under a name nothing could decode.

A whitelist that silently dropped new fields. A normaliser copied a fixed list of keys; twice, a new field was added upstream and simply never arrived, with no error anywhere.

A positional index that retargeted itself. A lightbox tracked the open item by list position. When a newly generated item shifted the newest-first list, actions silently applied to a different item — including downloads and "extend this".

Common thread: none of these threw. They returned undefined, 0, null, or the wrong-but-valid thing. The ones that surfaced did so because a number was counted against real data, not because anything crashed.


8. Design positions worth defending

Things that came out of the above and are arguable enough to be interesting.

Measure, don't estimate, when the estimate can be wrong in the direction that matters. Two seconds of waiting beats a confident wrong number someone plans around.

Three states beat two whenever "unset" is possible. Anywhere a default can apply silently, a UI that shows only "changed / unchanged" fabricates intent.

A format conversion is not a new artifact. A loop of a clip is something you can extend again and earns a place in the history graph. A WebM of a clip that already exists is a delivery copy — filing it would add an identical-looking node per platform someone exports for.

Raw material is not the user's work. A pasted GIF stays out of the gallery. Putting someone else's image among a user's own output is a category error.

Frame zero is almost never the interesting frame. Given an animation as input, where you continue from should be the user's decision, not a silent default.

An explicit statement beats a heuristic. When a user has manually filed items into a group, a "hide unconnected items" heuristic must not then hide them. Grey the heuristic out and say why, rather than silently overriding either one.


Reproducing any of this

The app is MIT, zero runtime npm dependencies, no build step: <https://github.com/MassiveNoobie/localVideo>

Requirements: macOS on Apple Silicon (36 GB+ recommended, 64 GB+ for comfortable video), Node.js 22+, a running ComfyUI, and BF16 model weights.

Open →
general Local video generation on Apple Silicon Aug 2, 2026

Why every tool fails on a Mac, and what we did instead

If you own an Apple Silicon Mac with real memory and you've tried to generate video locally, you've hit the same wall everyone does: you download 30 GB of curated weights, hit generate, and get

RuntimeError: Undefined type Float8_e4m3fn

This is a field report on why that happens, why it's not a Mac limitation the way it looks, and what we built once we understood it.


The wall: FP8 is a roach motel on Metal

Nearly every curated model bundle ships FP8 weights. FP8 exists for a good reason — it halves an FP16 model so a 22B video model fits in a 24 GB consumer NVIDIA card. It's the compression that makes local generation possible on gaming hardware.

PyTorch's Metal backend has no float8 kernels. That much is documented. What isn't documented is how completely closed the trap is. We tested every escape:

x = torch.zeros(8, dtype=torch.float8_e4m3fn, device="mps")

x.to(torch.bfloat16)     # RuntimeError: Undefined type Float8_e4m3fn
x.to("cpu")              # RuntimeError: Undefined type Float8_e4m3fn
x.view(torch.uint8)      # RuntimeError: Undefined type Float8_e4m3fn

You can put an FP8 tensor onto the GPU. You cannot convert it, copy it off, or even reinterpret its raw bytes. There is no userspace workaround — not a shim, not a monkeypatch, not a clever cast. We wrote all three before accepting it.

The failure is also path-dependent, which is why the situation looks so confusing from the outside:

PathResult
FP8 image checkpoints that dequantize on the CPU at load✅ works, just slower to load
FP8 video checkpoints that requantize on-device when applying a LoRA❌ hard fail

So FP8 images work and FP8 video dies, on the same machine, in the same app. That inconsistency is why "does this run on a Mac?" gets contradictory answers.

The unlock: stop compressing

Here's the part that reframes everything.

FP8 is a workaround for not having enough memory. A Mac with 64–128 GB of unified memory doesn't have that problem. It can hold the model uncompressed.

The BF16 build of LTX 2.3 22B is a 46 GB file. On a 24 GB NVIDIA card that's a non-starter — hence FP8. On a 128 GB M5 Max, torch.mps.recommended_max_memory() reports 115 GB allocatable. It just… fits.

Requested to load LTXAV
loaded completely; 40053.34 MB loaded, full load: True
8/8 [01:00<00:00, 7.53s/it]

A 22-billion-parameter video model, full precision, resident in a laptop's memory, sampling at 7.4 s/step. No quantization, no shim, no compromise. The BF16 file is a drop-in for the FP8 one — the loader doesn't care.

The weights that don't work on Apple Silicon are the ones built for cards smaller than your Mac.

The second wall: attention doesn't care how much memory you have

Fixing precision got video generating. Then a large request killed the backend:

RuntimeError: Invalid buffer size: 456.35 GiB

The two-stage LTX pipeline refines at 2×, and the refine pass attends over roughly (W/32) · (H/32) · (frames/8 + 1) tokens. That's quadratic in resolution and linear in duration, so 1600 px × 11 s asks for a single 456 GiB allocation. No GPU satisfies that; it just fails later and uglier on CUDA.

Past about 45k tokens the allocation aborts. That's computable before you submit, so the right move is to check and refuse with an explanation, not to crash mid-queue.

Which leads to the most useful feature we built. The refine pass is ~60% of generation time, so making it optional gives you two modes:

2 s clipLong/large clips
Draft (decode stage 1)~87 sfine at any size
Refined (+2× refine)~4 minbounded by the token budget

Draft is ~2.7× faster and handles the 1600 px × 11 s request that crashes in Refined. Iterate in Draft, commit in Refined.

Flags that matter

PYTORCH_ENABLE_MPS_FALLBACK=1 \
PYTORCH_MPS_HIGH_WATERMARK_RATIO=0.0 \
python main.py --use-split-cross-attention --fp32-vae
  • HIGH_WATERMARK_RATIO=0.0 lifts the allocator cap — required for a 46 GB model.
  • --fp32-vae is the first-line fix for MPS decode NaNs.
  • Never --bf16-vae — it breaks the LTX audio VAE.
  • Never --force-fp16 — black frames on recent macOS.
  • SageAttention and FlashAttention are CUDA-only. torch.compile doesn't work on MPS.

Measured (M5 Max, 128 GB, macOS 26.4, torch 2.13, ComfyUI 0.29)

JobTime
Image 512² · 4 steps~13 s
Image 1024² · 8 steps~45 s
Video 2 s · Draft~87 s
Video 2 s · Refined · 1280²~4 min warm
Video 8 s · Refined · 1280²~24 min
LTX 2.3 22B BF16 sampling7.4 s/step @ 49 frames · 36 s/step @ 201 frames

Published M1/M2/M3 benchmarks are a pessimistic floor for M4+: those chips emulate BF16, while M4 and later do it in hardware.


What we built

localVideo — a local image and video studio designed around these findings instead of fighting them.

  • Picks BF16 weights automatically, and greys out anything your backend can't

run with the actual technical reason

  • Draft / Refined and S / M / L, so speed is a dial rather than a surprise
  • Computes the attention token budget before submitting and tells you the max

duration for your size instead of aborting the backend

  • Offline by default: binds loopback only, zero telemetry, no CDN assets. Pull

the ethernet cable and it still works.

On phoning home

"Local" and "private" get used interchangeably in this space, and they aren't the same thing. A tool can run every model on your GPU and still narrate your session to a server somewhere.

So we audited what actually leaves the machine, and it's worth knowing what's normal in local-generation tools:

CallTriggerTypical default
Product analytics to a hosted endpointapp launch, every generation (with a model label)on, opt-out
Update check against a release APIsign-in, then every few hourson
Model/registry fetchesinstalling weights or browsing a catalogon demand
Font/script CDNsevery page loadinvisible, always

None of that is malicious — analytics answer real product questions, and update checks are a courtesy. But an opt-out beacon that fires on every generation is a strange default for software whose pitch is that your prompts never leave your computer, and a CDN font request means the UI doesn't fully work offline.

localVideo makes exactly zero outbound connections. Not fewer — zero:

  • no analytics, telemetry, crash reporting, or usage beacons; there is no SDK to disable
  • no update checks, no release pings, no license or entitlement calls
  • no CDN fonts, scripts, or styles — every asset is served from your own machine
  • binds 127.0.0.1 (LAN access is an explicit --lan flag, never a default)
  • the only socket it ever opens is to your local ComfyUI

There's no privacy toggle in the settings because there's nothing to toggle. The verification is a one-liner: run it with the network down and nothing changes. Or put it on a firewalled network and watch it stay silent — we did both.

Don't take our word for it — grep the source. Every URL in the codebase:

$ grep -rhoE "https?://[a-zA-Z0-9.:_-]+" server.js lib/ public/ | sort | uniq -c
   5 http://127.0.0.1:8188      # your ComfyUI
   1 http://www.w3.org          # SVG xmlns namespace — an identifier, never fetched

That's the whole list.

On dependencies

We kept exactly one: ComfyUI, as the generation engine. It's genuinely good at being a graph executor, its node API is stable and introspectable, and reimplementing diffusion pipelines to avoid it would be vanity, not engineering.

Everything else is gone:

localVideo
npm packages0 — Node built-ins only, no node_modules
Build stepnone — the frontend is plain HTML/CSS/JS
ComfyUI custom-node packs0 — all 33 node classes we emit are core ComfyUI
Analytics / telemetry SDKs0
Platform installersnone — it's a Node script

That third row is the one we're proudest of. Comparable tools require five or more third-party node packs, each an independent Python project that can fail to build — one of them (numba-based) simply doesn't compile on macOS + Python 3.12, which silently breaks an entire workflow family. Sticking to core nodes means the install is "clone ComfyUI, download weights, run."

Graphs are built against ComfyUI's own /object_info signatures, which is the authoritative description of what each node accepts. That also caught a real bug class: ComfyUI 0.29 types some inputs as unions ("FLOAT,INT"), and a builder that only recognizes single scalar types silently drops them — producing graphs the server rejects with a confusing required_input_missing.

A native MLX backend that removes the ComfyUI dependency entirely is interesting and plausible on this hardware. It is not what this is today.

Credit

Mix Studio by BlackMixture is what convinced us a local generation front end could feel genuinely good rather than like a node graph with a hat on. It's Windows/NVIDIA-only and doesn't run this hardware, so localVideo is an independent implementation written from ComfyUI's public API — no shared code — but the inspiration is real and worth naming.

Thanks also to the ComfyUI team, Lightricks (LTX-2.3), and Krea, whose models and published reference workflows made the rest possible.

Open →

Building j7 — a minimal place to chat, blog, and keep a portfolio.

j7, by dev3lopcom, llc