<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: bigkijimon</title>
    <description>The latest articles on DEV Community by bigkijimon (@bigkijimon).</description>
    <link>https://dev.to/bigkijimon</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4023503%2F2924c7b5-d02d-4638-91bf-4d560e34ec3b.png</url>
      <title>DEV Community: bigkijimon</title>
      <link>https://dev.to/bigkijimon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bigkijimon"/>
    <language>en</language>
    <item>
      <title>graphifyがApple Siliconで全滅した — x86_64 tree-sitterの罠と、コード ドキュメントを丸ごと知識グラフ化するまでの実録</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Sun, 12 Jul 2026 02:56:07 +0000</pubDate>
      <link>https://dev.to/bigkijimon/graphifygaapple-silicondequan-mie-sita-x8664-tree-sitternomin-to-kododokiyumentowowan-gotozhi-shi-gurahuhua-surumadenoshi-lu-pak</link>
      <guid>https://dev.to/bigkijimon/graphifygaapple-silicondequan-mie-sita-x8664-tree-sitternomin-to-kododokiyumentowowan-gotozhi-shi-gurahuhua-surumadenoshi-lu-pak</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;🇯🇵 This post is written in Japanese. Originally published on &lt;a href="https://zenn.dev/umamon/articles/8e68ce4930f62b" rel="noopener noreferrer"&gt;Zenn&lt;/a&gt;（この記事はZennからの転載です）。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Claude Codeが毎回ファイルを読み直す問題
&lt;/h2&gt;

&lt;p&gt;Claude Codeで大きめのプロジェクトを触っていると、毎セッション同じことが起きる。「あのファイルどこだっけ」でgrep、全文Read、また別のファイルをRead……。&lt;strong&gt;コードベースはgrepには大きすぎ、コンテキストに丸ごと積むには高すぎる。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;これを解くのが &lt;a href="https://github.com/safishamsi/graphify" rel="noopener noreferrer"&gt;graphify&lt;/a&gt;（PyPIパッケージ名は&lt;code&gt;graphifyy&lt;/code&gt;で yを2つ重ねる／CLIコマンドは&lt;code&gt;graphify&lt;/code&gt;）。フォルダを丸ごと&lt;strong&gt;知識グラフ&lt;/strong&gt;にして、Claudeがファイルを読み直す代わりにグラフを引く。公称「クエリあたり最大71.5xのトークン削減」。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;知識グラフとは何か&lt;/strong&gt;を一言で。コードとドキュメントを解析して「関数・クラス・概念＝ノード」「呼ぶ・依存する・参照する＝有向エッジ」の&lt;strong&gt;有向グラフ&lt;/strong&gt;として抽出したもの。質問すると、全文を読み直す代わりに&lt;strong&gt;関連ノードだけを部分グラフで返す&lt;/strong&gt;。だから軽い。graphifyはコードを&lt;code&gt;tree-sitter&lt;/code&gt;でローカルAST解析（LLM不使用）し、ドキュメントだけをモデルで意味抽出する。&lt;/p&gt;

&lt;p&gt;M1 Max（Apple Silicon）で、自分のプロジェクト——シェルのオーケストレーション群、Swiftのメニューバーアプリ、設計ドキュメント——を丸ごとグラフ化しようとした。そして&lt;strong&gt;いきなり全滅した&lt;/strong&gt;。この記事はその実録。&lt;/p&gt;

&lt;h2&gt;
  
  
  トラブルシューティング
&lt;/h2&gt;

&lt;h3&gt;
  
  
  全滅：&lt;code&gt;incompatible architecture&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;uv tool install graphifyy&lt;/code&gt; で入れて、AST抽出を走らせた瞬間これが出た。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ImportError: dlopen(.../tree_sitter/_binding.cpython-313-darwin.so, 0x0002):
  tried: '.../tree_sitter/_binding.cpython-313-darwin.so'
  (mach-o file, but is an incompatible architecture
   (have 'x86_64', need 'arm64e' or 'arm64'))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tree_sitter&lt;/code&gt; のネイティブバインディング（&lt;code&gt;.so&lt;/code&gt;）が &lt;strong&gt;x86_64&lt;/strong&gt; で、arm64のPythonがロードできない。graphifyはコードをtree-sitterでAST解析するので、ここが死ぬと&lt;strong&gt;コード抽出が全滅&lt;/strong&gt;する。&lt;/p&gt;

&lt;p&gt;Apple Siliconあるあるだが、厄介なのは「graphify自体が抱えていたx86_64依存」だったこと。自分のコードでもモデルでもなく、ツールのネイティブ依存がx86_64前提でインストールされていた。原因はシンプルで、arm64のPythonがx86_64の&lt;code&gt;.so&lt;/code&gt;をdlopenできずに落ちている。&lt;code&gt;uv tool&lt;/code&gt;の解決時にx86_64ホイールを掴んでいた。&lt;/p&gt;

&lt;h3&gt;
  
  
  修正①：tree-sitterをarm64で入れ直す
&lt;/h3&gt;

&lt;p&gt;uv tool環境には&lt;code&gt;pip&lt;/code&gt;が無いので、&lt;code&gt;uv pip&lt;/code&gt;でその環境を指定して強制再インストールする。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/Users/you/.local/share/uv/tools/graphifyy/bin/python
uv pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--python&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--reinstall-package&lt;/span&gt; tree-sitter &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="s1"&gt;'tree-sitter&amp;gt;=0.23.0'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tree-sitter 0.26.0&lt;/code&gt;（arm64）が入り、&lt;code&gt;dlopen&lt;/code&gt;の悲鳴は止まった。……が、まだAST結果は&lt;strong&gt;0ノード&lt;/strong&gt;だった。&lt;/p&gt;

&lt;h3&gt;
  
  
  修正②：言語grammarが「そもそも」入っていなかった
&lt;/h3&gt;

&lt;p&gt;再試行で今度はこう出た。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;warning: 32 .swift file(s) contributed nothing to the graph
  because a dependency is missing: tree_sitter_swift not installed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;tree-sitterの&lt;strong&gt;コア&lt;/strong&gt;は直ったが、&lt;strong&gt;各言語のgrammar&lt;/strong&gt;（&lt;code&gt;tree_sitter_swift&lt;/code&gt;等）が別パッケージで、未導入だった。ここで一気に入れる。特に &lt;strong&gt;&lt;code&gt;tree-sitter-bash&lt;/code&gt;&lt;/strong&gt; を入れると、シェルスクリプトのオーケストレーション群もAST解析できるようになる——これが自分の環境では本命だった。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--python&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  tree-sitter-bash tree-sitter-swift tree-sitter-python &lt;span class="se"&gt;\&lt;/span&gt;
  tree-sitter-typescript tree-sitter-javascript tree-sitter-json tree-sitter-go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;7言語すべて&lt;code&gt;import&lt;/code&gt;OK。&lt;strong&gt;この&lt;code&gt;tree-sitter-bash&lt;/code&gt;導入が効いて、それまで0ノードだったシェル13本＋Swift32本が解析対象になった。&lt;/strong&gt; 再々試行：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Swift AST: 323 nodes, 759 edges
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;動いた。&lt;/strong&gt; archを直し、grammarを入れる——この2段で復活する。片方だけだと0ノードで沈黙するので、両方必須。全体の流れはこう。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnm37k65gs8qvz81ytme5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnm37k65gs8qvz81ytme5.png" alt="全滅から復活へ — Apple Siliconのgraphify、2段の修正（archエラー→arm64再インストール→まだ0ノード→言語grammar導入→0から332ノードへ）" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;段階&lt;/th&gt;
&lt;th&gt;状態&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;初期&lt;/td&gt;
&lt;td&gt;dlopen &lt;code&gt;incompatible architecture&lt;/code&gt; で全滅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;修正①後（arm64 tree-sitter）&lt;/td&gt;
&lt;td&gt;archエラーは消えるが &lt;strong&gt;0ノード&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;修正②後（言語grammar導入）&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;332ノード / 653エッジ&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  コードグラフ構築：bash＋Swiftを1グラフに
&lt;/h2&gt;

&lt;h3&gt;
  
  
  コード → 332ノード
&lt;/h3&gt;

&lt;p&gt;まずコード資産をASTで（LLM不使用・完全無料）。シェルのオーケストレーション13本（GPU調停・ディスパッチャ・監視系など）＋Swiftアプリ32本＝45本を1グラフに。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Graph: 332 nodes, 653 edges, 23 communities
God node: 中核ViewModel   ← アプリの中枢を正しく検出
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ここでベストプラクティスをひとつ。&lt;strong&gt;testは除外、infra/docsは含める。&lt;/strong&gt; testはノードを3-4倍に膨らませてクラスタリングを濁す（graphifyの公式ガイドでも指摘される経験則）。逆にインフラ設定やドキュメントは「システムがどう配線されているか（何が何に依存するか）」を教えてくれる。&lt;/p&gt;

&lt;h3&gt;
  
  
  ＋ドキュメント → 629ノード
&lt;/h3&gt;

&lt;p&gt;次に、CLAUDE.mdや設計ドキュメント60本を&lt;strong&gt;semantic抽出&lt;/strong&gt;（並列サブエージェント）でグラフに足した。結果：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ENRICHED Graph: 629 nodes, 986 edges, 76 communities
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;コード構造と設計知識が1つのグラフに同居&lt;/strong&gt;した。構築の流れを図にするとこう。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz6ada5awnjihjkquu48p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz6ada5awnjihjkquu48p.png" alt="コード＋ドキュメント → 1つの知識グラフ → 副産物（コード45本はローカルAST・無料、設計doc60本はsemantic抽出。629ノード/986エッジのグラフから graph.html・Obsidian・Wiki・MCPサーバ・graphify query が全部出る）" width="799" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  実際に引いてみる
&lt;/h3&gt;

&lt;p&gt;「GPU調停まわりは何と繋がってる？」を聞くと、BFSでこう返る（識別子は汎用名にしてある。形式は実出力のまま）。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ graphify query "gpu arbitration freeze thaw"
Traversal: BFS depth=2 | Start: [GpuMonitor] | 13 nodes found

NODE GpuMonitor       [src=app/services/gpu_monitor.swift  L9   community=8]
NODE MemSwitchScript  [src=scripts/mem_switch.sh           L1   community=29]
NODE thaw_ollama()    [src=scripts/mem_switch.sh           L38  community=29]
NODE LocalLLMRuntime  [src=docs/env_snapshot.md            —    community=6]

EDGE GpuMonitor      --shares_data_with--&amp;gt; CoreViewModel   [INFERRED  0.85]
EDGE MemSwitchScript --references--&amp;gt;       LocalLLMRuntime [EXTRACTED 1.0]
EDGE thaw_ollama()   --calls--&amp;gt;            LocalLLMRuntime [EXTRACTED 1.0]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swiftの型（コード）、シェルのスクリプトと関数（コード）、設計メモの「ローカルLLM基盤」（ドキュメント）が&lt;strong&gt;1つの答えに混ざって出てくる&lt;/strong&gt;。&lt;code&gt;shares_data_with&lt;/code&gt;（INFERRED 0.85）や&lt;code&gt;references&lt;/code&gt;（EXTRACTED 1.0）のように、&lt;strong&gt;関係の種類と確信度まで&lt;/strong&gt;付く。「実装」と「なぜ・どこに繋がるか」を一度に返す——これがコードとドキュメントを同居させた効果だ。&lt;code&gt;graphify explain "&amp;lt;ノード&amp;gt;"&lt;/code&gt;なら&lt;code&gt;Type / Community / Degree&lt;/code&gt;と接続先一覧を平易に出してくれる。&lt;/p&gt;

&lt;h2&gt;
  
  
  性能計測：約215xのトークン削減
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;graphify benchmark&lt;/code&gt;の実測がこれ。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Corpus:          31,450 words → ~41,933 tokens (naive)
Graph:           629 nodes, 986 edges
Avg query cost:  ~195 tokens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;表にするとこう。&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方式&lt;/th&gt;
&lt;th&gt;1回のコスト&lt;/th&gt;
&lt;th&gt;倍率&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;コーパス丸ごと積む（naive）&lt;/td&gt;
&lt;td&gt;~41,933 tokens&lt;/td&gt;
&lt;td&gt;1x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;graphify query&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~195 tokens&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;約215x削減&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;（コーパス 31,450 words / グラフ 629ノード・986エッジ／&lt;code&gt;graphify benchmark&lt;/code&gt;実測）&lt;/p&gt;

&lt;p&gt;数字は2つある。&lt;strong&gt;公称「最大71.5x」はgraphify公式のベンチ値&lt;/strong&gt;、&lt;strong&gt;この自分のコーパスでの自測値は約215x&lt;/strong&gt;（&lt;code&gt;graphify benchmark&lt;/code&gt;の出力）。コーパスの内容と質問で変わる——構造・依存を尋ねる質問ほど効き、編集そのものやグラフ外のファイルには効かない。ただ「毎回コードベースを積み直す税」が桁で消えるのは共通だ。&lt;/p&gt;

&lt;h2&gt;
  
  
  副産物も全部おいしい
&lt;/h2&gt;

&lt;p&gt;graphifyは1回のビルドで大量の成果物を吐く。全部使うと効く。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTML&lt;/strong&gt;（&lt;code&gt;graph.html&lt;/code&gt;）：ブラウザで開く対話グラフ。サーバ不要。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Obsidian vault&lt;/strong&gt;：1ノード1mdの双方向リンク。「第二の脳」化。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wiki&lt;/strong&gt;：コミュニティ別の記事。エージェントが&lt;code&gt;wiki/index.md&lt;/code&gt;から辿る。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCPサーバ&lt;/strong&gt;：&lt;code&gt;python -m graphify.serve graph.json&lt;/code&gt;。&lt;code&gt;get_node&lt;/code&gt;/&lt;code&gt;get_neighbors&lt;/code&gt;/&lt;code&gt;shortest_path&lt;/code&gt;をツール露出。&lt;strong&gt;エージェントがグラフをライブ照会&lt;/strong&gt;できる（要&lt;code&gt;mcp&lt;/code&gt;パッケージ）。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;常時ON統合&lt;/strong&gt;：&lt;code&gt;graphify claude install&lt;/code&gt;でCLAUDE.mdにルールとPreToolUseフックが入り、以後は明示&lt;code&gt;/graphify&lt;/code&gt;なしでコードベースの質問前に自動でグラフを参照する。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;生成されるObsidianノートは1ノード＝1メモで、frontmatterと&lt;code&gt;[[…]]&lt;/code&gt;のbacklinkが付く（識別子は汎用名）。まず&lt;code&gt;GpuMonitor&lt;/code&gt;のノート：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;source_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app/services/gpu_monitor.swift"&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code"&lt;/span&gt;
&lt;span class="na"&gt;community&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Community&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;8"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="gh"&gt;# GpuMonitor&lt;/span&gt;
&lt;span class="gu"&gt;## Connections&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [[CoreViewModel]] - &lt;span class="sb"&gt;`shares_data_with`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [[MemSwitchScript]] - &lt;span class="sb"&gt;`conceptually_related_to`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;繋がった先の&lt;code&gt;CoreViewModel&lt;/code&gt;ノートを開くと、&lt;strong&gt;逆向きのリンクが自動で張られている&lt;/strong&gt;（双方向）：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;source_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app/models/core_view_model.swift"&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code"&lt;/span&gt;
&lt;span class="na"&gt;community&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Community&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="gh"&gt;# CoreViewModel&lt;/span&gt;
&lt;span class="gu"&gt;## Connections&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [[GpuMonitor]] - &lt;span class="sb"&gt;`shares_data_with`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [[MetricsProbe]] - &lt;span class="sb"&gt;`calls`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [[SessionProbe]] - &lt;span class="sb"&gt;`calls`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Obsidianのグラフビューで開けば、この双方向リンクがそのままノード網になり、コードを1行も読まずに「どのモジュールが何と繋がるか」を辿れる。&lt;/p&gt;

&lt;h2&gt;
  
  
  教訓（Apple Siliconでgraphifyを動かす人へ）
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;archエラーは2段で直す&lt;/strong&gt;：&lt;code&gt;tree-sitter&lt;/code&gt;本体をarm64で再インストール → 言語grammar（&lt;code&gt;tree-sitter-bash&lt;/code&gt;/&lt;code&gt;swift&lt;/code&gt;/…）を導入。片方だけだと0ノードで沈黙する。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;bash grammarを入れる&lt;/strong&gt;：シェル中心のオーケストレーションこそグラフ化の価値が高い。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;testは切り、infra/docsは入れる&lt;/strong&gt;：グラフの主役はクラスタの純度と「配線」。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;docのsemanticはモデルAPIを使う&lt;/strong&gt;：コードはローカルAST（無料）、doc/画像はモデル呼び出しでトークンを食う。コスト意識するならスコープを絞る（画像はvisionで特に高い＝依存グラフには不要なので除外が正解）。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;archの罠を抜ければ、graphifyはApple Siliconで完全に動く。そして一度グラフにすると、&lt;strong&gt;AIが「プロジェクト全体の文脈を持った状態」で動き始める&lt;/strong&gt;——速度だけの話ではない、というのが一番の収穫だった。&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>applesilicon</category>
      <category>graphify</category>
      <category>knowledgegraph</category>
    </item>
    <item>
      <title>100% Free, Fully Local AI Music Generation on an M1 Mac — ACE-Step 1.5: Two 100-Second Vocal Songs in 58 Seconds (Field Log)</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Sat, 11 Jul 2026 03:35:52 +0000</pubDate>
      <link>https://dev.to/bigkijimon/100-free-fully-local-ai-music-generation-on-an-m1-mac-ace-step-15-two-100-second-vocal-songs-3h1b</link>
      <guid>https://dev.to/bigkijimon/100-free-fully-local-ai-music-generation-on-an-m1-mac-ace-step-15-two-100-second-vocal-songs-3h1b</guid>
      <description>&lt;p&gt;Zero cost. Zero external calls. On a single M1 Max, two 100-second vocal songs (two seed variants) come out in about 58 seconds (measured on this M1 Max 64GB setup — details below).&lt;/p&gt;

&lt;p&gt;The audience I have in mind: &lt;strong&gt;Apple Silicon folks running LLMs/image/video models locally, and musicians who want to avoid cloud billing.&lt;/strong&gt; "Suno is convenient, but I hate the billing and the cloud upload. Does vocal-song generation really work on just a Mac, and how fast is it actually?" — this post answers that with real measurements.&lt;/p&gt;

&lt;p&gt;This is the second entry in my "free local AI ___, the honest log" series. Last time it was video → &lt;a href="https://zenn.dev/umamon/articles/df7a862c4a944e" rel="noopener noreferrer"&gt;I Built a 100% Free, Fully Local AI Short-Video Pipeline on an M1 Mac&lt;/a&gt;. This time it's music. It's the real log of producing eight English learning songs for kids — and I'll start with where I got burned (the failures).&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;To verify "it's running locally," don't trust a passing health check — inspect the endpoint URL and &lt;code&gt;api_mode&lt;/code&gt;.&lt;/strong&gt; The CLI bundled in the repo points to the cloud (acemusic.ai) by default, and the health check keeps returning OK even while your local server sits there perfectly healthy (the biggest trap; details below).&lt;/li&gt;
&lt;li&gt;Measured on an M1 Max 64GB: &lt;strong&gt;two variants of a 100-second vocal song = 58.48s&lt;/strong&gt; (LM 15.70s + DiT 42.78s). Total generation for all 8 songs × 2 variants: about 9 minutes.&lt;/li&gt;
&lt;li&gt;Don't judge quality by ear alone: &lt;strong&gt;a 3-layer scoring stack — mlx-whisper transcription match rate × an independent rubric × your ears.&lt;/strong&gt; The final 8 keepers averaged an 83.9% match rate and 94.1 points on the independent rubric.&lt;/li&gt;
&lt;li&gt;The environment landmine is the same one as last time (the video post): &lt;strong&gt;arm64 Python&lt;/strong&gt;. This time it was uvx grabbing an anaconda-flavored x86 Python.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. The incident: "I thought it was local — it was the cloud." Never trust a passing health check
&lt;/h2&gt;

&lt;p&gt;Zero-cost, zero-external-calls AI music — that was the plan. &lt;strong&gt;In reality, my generation requests were flying off to the cloud.&lt;/strong&gt; It's the most valuable failure in this log, so it goes first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timeline
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Day one. &lt;code&gt;git clone&lt;/code&gt; + &lt;code&gt;uv sync&lt;/code&gt;, API server up on &lt;code&gt;localhost:8001&lt;/code&gt;. Smooth so far.&lt;/li&gt;
&lt;li&gt;The repo ships a CLI (&lt;code&gt;acestep.sh&lt;/code&gt;) as &lt;code&gt;.claude/skills&lt;/code&gt;, and as I started generating with it —&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The CLI's default endpoint was the cloud API (acemusic.ai).&lt;/strong&gt; Even with a perfectly healthy API server running on my machine, requests were configured to go external.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Why you won't notice: the cloud health check returns OK
&lt;/h3&gt;

&lt;p&gt;Here's the nasty part. The CLI's health check &lt;strong&gt;returns "OK" — as a health check of the cloud side.&lt;/strong&gt; Your local server also started fine. So "server up OK, health check OK, generation works" — from every angle it looks like "it works," while the reality is external transmission. The more strictly you require fully-local (zero external calls), the more this cosmetic OK will fool you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flpm155gom2egdeolemt4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flpm155gom2egdeolemt4.png" alt="Fig 2: The trap of the passing cloud health check (before/after)" width="800" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix is two lines
&lt;/h3&gt;

&lt;p&gt;Point the endpoint and the API mode explicitly at local:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash scripts/acestep.sh config &lt;span class="nt"&gt;--set&lt;/span&gt; api_url &lt;span class="s2"&gt;"http://127.0.0.1:8001"&lt;/span&gt;
bash scripts/acestep.sh config &lt;span class="nt"&gt;--set&lt;/span&gt; api_mode native
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: If your requirement is fully local (zero external calls, commercial-safe), don't accept "health check OK" — &lt;strong&gt;inspect the endpoint URL and &lt;code&gt;api_mode&lt;/code&gt; yourself.&lt;/strong&gt; A passing health check and requests staying local are two different questions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every measurement below was taken after this fix (&lt;code&gt;api_mode native&lt;/code&gt;, targeting 127.0.0.1).&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Why ACE-Step 1.5 — model selection from a Mac perspective
&lt;/h2&gt;

&lt;p&gt;I compared the usual "local AI music on a Mac" candidates along three axes: &lt;strong&gt;vocals (songs with singing), runs on a Mac, and license.&lt;/strong&gt; Every license claim below is primary-sourced from the official repo/model card (as of July 2026).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Why I passed&lt;/th&gt;
&lt;th&gt;License (primary-sourced)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MusicGen / AudioCraft&lt;/td&gt;
&lt;td&gt;Weights are CC-BY-NC 4.0 = &lt;strong&gt;no commercial use&lt;/strong&gt; (code is MIT). Also, vocals were removed from the training data via source separation, and the official card states "The model is not able to generate realistic vocals." = &lt;strong&gt;can't make songs&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/facebookresearch/audiocraft" rel="noopener noreferrer"&gt;Code=MIT / weights=CC-BY-NC 4.0&lt;/a&gt;, &lt;a href="https://huggingface.co/facebook/musicgen-large" rel="noopener noreferrer"&gt;model card&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YuE&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No Mac path.&lt;/strong&gt; The official repo states "FlashAttention 2 is mandatory" = CUDA-first, with no macOS/Apple Silicon support mentioned. (License itself is Apache 2.0, which is fine)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/multimodal-art-projection/YuE" rel="noopener noreferrer"&gt;Apache 2.0&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stable Audio Open&lt;/td&gt;
&lt;td&gt;The official model card states "The model is not able to generate realistic vocals." = no vocal songs&lt;/td&gt;
&lt;td&gt;&lt;a href="https://huggingface.co/stabilityai/stable-audio-open-1.0" rel="noopener noreferrer"&gt;Stability AI Community License&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DiffRhythm&lt;/td&gt;
&lt;td&gt;Code and DiT weights are Apache 2.0, but &lt;strong&gt;the VAE alone is under the Stability AI Community License&lt;/strong&gt; (officially: "DiffRhythm-VAE is subject to the Stability AI Community License Agreement") — &lt;strong&gt;licensing splits per component.&lt;/strong&gt; Messy to manage&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/ASLP-lab/DiffRhythm" rel="noopener noreferrer"&gt;Code/DiT=Apache 2.0&lt;/a&gt;, &lt;a href="https://huggingface.co/ASLP-lab/DiffRhythm-vae" rel="noopener noreferrer"&gt;VAE=Stability AI Community License&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;By the way, MusicGen — which I used for BGM in the &lt;a href="https://zenn.dev/umamon/articles/df7a862c4a944e" rel="noopener noreferrer"&gt;previous post&lt;/a&gt; — is, per the table, "non-commercial + can't sing," so it stopped at BGM. Needing a different model for actual songs is where this project started.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What sold me on &lt;a href="https://github.com/ace-step/ACE-Step-1.5" rel="noopener noreferrer"&gt;ACE-Step 1.5&lt;/a&gt;&lt;/strong&gt; — three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;MIT license&lt;/strong&gt; (no strings for commercial use; compared with the table above, that simplicity is precious)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Official macOS/Apple Silicon support.&lt;/strong&gt; A &lt;code&gt;start_api_server_macos.sh&lt;/code&gt; ships in the repo; the LM side runs on MLX and the DiT side on PyTorch MPS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A bundled REST API server&lt;/strong&gt;, easy to wire into batch production and automation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The LM comes in 0.6B/1.7B/4B. I &lt;strong&gt;started with 0.6B&lt;/strong&gt;. There's an &lt;strong&gt;external report&lt;/strong&gt; of 1.7B peaking at 42GB (not verified by me), so on a machine that also keeps other local LLMs resident, skipping it felt like the safe call.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Install and launch (M1 Max 64GB, ~7GB initial download)
&lt;/h2&gt;

&lt;p&gt;The install itself is straightforward, so I'll keep this short.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environment&lt;/strong&gt;: MacBook Pro M1 Max 64GB (unified memory). On startup the server detected "51.8GB unified memory, tier=unlimited". Models: LM = &lt;code&gt;acestep-5Hz-lm-0.6B&lt;/code&gt; + DiT = &lt;code&gt;acestep-v15-turbo&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Start the API server (port 8001, LM on the MLX backend)&lt;/span&gt;
&lt;span class="nv"&gt;ACESTEP_LM_BACKEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mlx &lt;span class="nv"&gt;TOKENIZERS_PARALLELISM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  uv run acestep-api &lt;span class="nt"&gt;--host&lt;/span&gt; 127.0.0.1 &lt;span class="nt"&gt;--port&lt;/span&gt; 8001 &lt;span class="nt"&gt;--lm-model-path&lt;/span&gt; acestep-5Hz-lm-0.6B

&lt;span class="c"&gt;# Confirm it's listening&lt;/span&gt;
lsof &lt;span class="nt"&gt;-nP&lt;/span&gt; &lt;span class="nt"&gt;-iTCP&lt;/span&gt;:8001 &lt;span class="nt"&gt;-sTCP&lt;/span&gt;:LISTEN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first run downloads the models. Measured: &lt;strong&gt;about 7GB&lt;/strong&gt; total under &lt;code&gt;checkpoints/&lt;/code&gt; (about 4 minutes on a 47MB/s connection).&lt;/p&gt;

&lt;p&gt;Three small traps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;start_api_server_macos.sh&lt;/code&gt; &lt;strong&gt;can pop an interactive prompt&lt;/strong&gt; during its git update check. Calling &lt;code&gt;uv run acestep-api&lt;/code&gt; directly is the reliable path.&lt;/li&gt;
&lt;li&gt;The bundled CLI &lt;code&gt;scripts/acestep.sh&lt;/code&gt; &lt;strong&gt;doesn't have its executable bit set.&lt;/strong&gt; Call it via &lt;code&gt;bash&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The initial download and long generations will time out if you run them synchronously. &lt;strong&gt;Run them in the background.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Measured: two 100-second vocal songs in 58.48s (LM 15.70s + DiT 42.78s)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3a12n1u8rrjxu818s5p9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3a12n1u8rrjxu818s5p9.png" alt="Fig 1: Full pipeline" width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The main event. Generation is just handing the CLI a caption (style tags), lyrics with structure tags, and parameters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash scripts/acestep.sh generate &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;caption&amp;gt;"&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;full lyrics with structure tags&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--duration&lt;/span&gt; 100 &lt;span class="nt"&gt;--bpm&lt;/span&gt; 100 &lt;span class="nt"&gt;--key-scale&lt;/span&gt; &lt;span class="s2"&gt;"C major"&lt;/span&gt; &lt;span class="nt"&gt;--language&lt;/span&gt; en
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the result metadata for song 1 (a 100-second ABC song). &lt;strong&gt;One request auto-generates two variants with different seeds&lt;/strong&gt;, and the recorded time is the total for both.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"generation_info"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"**🎵 Total generation time (2 songs): 58.48s**
  - 29.24s per song
  - LM phase (2 songs): 15.70s
  - DiT phase (2 songs): 42.78s"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"seed_value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1014051477,117013642"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Two finished 100-second songs in 58.48s.&lt;/strong&gt; Generation runs faster than the song's actual length — faster than real time. The breakdown: LM (lyrics → semantic tokens) 15.70s, DiT (audio synthesis) 42.78s. Having this breakdown tells you which side to lighten if you want more speed, so I recommend keeping every meta.json.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measurements across all 8 songs
&lt;/h3&gt;

&lt;p&gt;That pace wasn't first-song luck; it held across all eight.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Song&lt;/th&gt;
&lt;th&gt;Length&lt;/th&gt;
&lt;th&gt;Generation time (2 variants)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;01&lt;/td&gt;
&lt;td&gt;100s&lt;/td&gt;
&lt;td&gt;58.48s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02&lt;/td&gt;
&lt;td&gt;110s&lt;/td&gt;
&lt;td&gt;66.32s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;03&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;62.99s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;04&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;63.97s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;68.92s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;06&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;65.55s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;07&lt;/td&gt;
&lt;td&gt;120s&lt;/td&gt;
&lt;td&gt;74.91s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;08&lt;/td&gt;
&lt;td&gt;115s&lt;/td&gt;
&lt;td&gt;74.90s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Total generation time for all 8 songs × 2 variants (16 tracks): about 9 minutes.&lt;/strong&gt; The outputs are 100–125s mp3s at 48kHz/128kbps, roughly 1.6MB (100s) to 2.0MB (125s) each. The lyrics were synthesized word-for-word as written (verified by inspecting &lt;code&gt;metas.lyrics&lt;/code&gt; in meta.json).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;All numbers are &lt;strong&gt;measured on this M1 Max 64GB configuration.&lt;/strong&gt; I haven't measured M2/M3/M4, so I'm not extrapolating to other chips. This is not a promise of "58 seconds for everyone."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  5. Lyric design: the LMNOP problem is universal
&lt;/h2&gt;

&lt;p&gt;Fast generation is useless if the lyric design is off. From here on, this is operational field experience you won't find in the usual roundups.&lt;/p&gt;

&lt;h3&gt;
  
  
  The "elemenopee" problem
&lt;/h3&gt;

&lt;p&gt;In the traditional ABC-song melody, L-M-N-O-P gets crammed together and smears into "elemenopee" — a well-known issue. I avoided it by &lt;strong&gt;regrouping the letters into evenly spaced chunks&lt;/strong&gt;: &lt;code&gt;ABCD / EFG / HIJK / LMN / OPQ / RST / UVW / XYZ&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This isn't just about the alphabet — it's a principle for &lt;strong&gt;any enumeration lyric&lt;/strong&gt; (numbers, days of the week): place each item at even intervals and don't insert "and" into the enumeration ("twenty-nine and thirty" was actually flagged in scoring).&lt;/p&gt;

&lt;h3&gt;
  
  
  General techniques for clearer AI singing
&lt;/h3&gt;

&lt;p&gt;Lyric-side techniques that solidified in practice, and should apply beyond ACE-Step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write numbers as English words&lt;/strong&gt; ("30" → "thirty"; the transcription scoring needed number normalization too)&lt;/li&gt;
&lt;li&gt;Have letters sung one at a time with separators: &lt;code&gt;A - B - C&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;4–8 words and 6–10 syllables per line; keep parallel lines within ±1–2 syllables&lt;/li&gt;
&lt;li&gt;Repeat choruses with identical spelling, word for word. Use parentheses for backing vocals &lt;code&gt;(happy!)&lt;/code&gt; and structure tags like &lt;code&gt;[call and response]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Keep the caption to 4–7 consistent tags, and never let caption and lyric moods contradict. &lt;strong&gt;Don't put BPM/key in the caption — pass them as dedicated parameters&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Three prohibitions that came out of independent scoring: 1) no "and" inside enumerations; 2) don't contort English for the sake of rhyme ("but fun, all right!" was a real example); 3) at most one alteration per chorus, and keep the same enumeration consistent within a song (no mixing fall vs autumn)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The real thing: song 1's full lyrics and parameters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Parameters: duration 100s / BPM 100 / C major / vocal en&lt;/li&gt;
&lt;li&gt;caption: &lt;code&gt;children's song, cheerful, playful, simple melody, ukulele, glockenspiel, acoustic guitar, clear female vocals, warm, sing-along&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Intro]

[Verse]
A - B - C - D
E - F - G
H - I - J - K
L - M - N

[Verse]
O - P - Q
R - S - T
U - V - W
X - Y - Z

[Chorus]
Now I know my A - B - C (A - B - C!)
Sing the alphabet with me (sing with me!)
Happy letters, you and me
Come and sing the A - B - C

[Verse] (repeat)
[Chorus] (repeat)

[Outro]
A - B - C, you and me!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These lyrics, as-is, became the two variants in the 58.48s above (measured 48kHz, 100.03s each).&lt;/p&gt;




&lt;h2&gt;
  
  
  6. "Don't grade your own songs" — the 3-layer Whisper scoring stack
&lt;/h2&gt;

&lt;p&gt;Comparing 16 tracks entirely by ear to rank them is, honestly, brutal. So I inserted &lt;strong&gt;objective scoring via transcription&lt;/strong&gt; — and stepped on another landmine setting it up, so let's start there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure #2: uvx grabs an x86 Python
&lt;/h3&gt;

&lt;p&gt;I tried to install mlx-whisper (fully local transcription) for scoring via &lt;code&gt;uvx&lt;/code&gt;, and dependency resolution failed. The cause: this machine's default &lt;code&gt;python3&lt;/code&gt; is an &lt;strong&gt;anaconda-provided x86_64 (Rosetta) build&lt;/strong&gt;, and uvx grabbed it. mlx is arm64-only, so it won't install.&lt;/p&gt;

&lt;p&gt;It's the sequel to the "make it grab arm64 Python" landmine from the previous post. You can avoid it when you create venvs yourself — but &lt;strong&gt;it comes back through convenience wrappers like uvx.&lt;/strong&gt; Passing an arm64 CPython explicitly via &lt;code&gt;--python&lt;/code&gt; gets you through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvx &lt;span class="nt"&gt;--python&lt;/span&gt; cpython-3.12-macos-aarch64-none &lt;span class="nt"&gt;--from&lt;/span&gt; mlx-whisper &lt;span class="se"&gt;\&lt;/span&gt;
  mlx_whisper &amp;lt;mp3&amp;gt; &lt;span class="nt"&gt;--model&lt;/span&gt; mlx-community/whisper-large-v3-turbo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When an MLX tool fails mysteriously on Apple Silicon, first check your arch with &lt;code&gt;python3 -c "import platform; print(platform.machine())"&lt;/code&gt; — especially on machines that also have anaconda.&lt;/p&gt;

&lt;h3&gt;
  
  
  The three layers
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Objective layer&lt;/strong&gt;: transcribe each track with mlx-whisper (large-v3-turbo, Apache 2.0, fully local) and compute the &lt;strong&gt;word-sequence match rate&lt;/strong&gt; against the lyrics actually used for synthesis (verified from meta.json). Normalize numbers to English words before comparing (to correct false-low scores from "1,2,3"-style transcriptions)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independent layer&lt;/strong&gt;: have &lt;strong&gt;an agent in a separate context&lt;/strong&gt; score against a rubric (English correctness 25 / design-principle adherence 25 / educational value 25 / singability 25 = 100 points). If the author (the same-context session) grades it, the grades go soft — so separate them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Final layer&lt;/strong&gt;: human ears. A person covers Whisper's measurement limits (below)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What the data actually said
&lt;/h3&gt;

&lt;p&gt;Match rates &lt;strong&gt;spread far more than I expected.&lt;/strong&gt; Across the 16 v1 tracks: from a low of 0.0% (a bad take where Whisper kept hallucinating "Thank you") to a high of 97.1% (song 3, variant b — near-perfect intelligibility). Seeing that spread at all was the single biggest payoff of adding automated scoring.&lt;/p&gt;

&lt;p&gt;After regenerating 7 songs as v2 based on the independent rubric's feedback (one rejection-and-redo), the v1/v2 comparison is where it gets interesting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvu6fp1uwd4by455979qd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvu6fp1uwd4by455979qd.png" alt="Fig 3: v1/v2 scoring comparison chart" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A fix that worked&lt;/strong&gt;: song 8 went 92.5 → &lt;strong&gt;100.0&lt;/strong&gt; (perfect transcription)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A fix that backfired&lt;/strong&gt;: song 1 went 75.3 → &lt;strong&gt;53.2&lt;/strong&gt;. The lyrics got better, but intelligibility dropped. In other words, &lt;strong&gt;lyric fixes and seed luck are independent&lt;/strong&gt; — which is why I switched to "never discard old takes; adopt the best take per song"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A hands-off ruling&lt;/strong&gt;: song 3's variant b had a 97.1% match rate (highest of all 16 tracks) and an independent score of 97. Fix suggestions existed, but "risk of touching &amp;gt; benefit," so I &lt;strong&gt;declined them.&lt;/strong&gt; Having the nerve not to touch your highest-scoring take is part of operations too&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final 8 keepers averaged an &lt;strong&gt;83.9% match rate&lt;/strong&gt; (81.9% at the point of v1 adoption) and &lt;strong&gt;94.1 points on the independent rubric&lt;/strong&gt; (max 98, min 89 — and the 89-point song had all 4 of its flagged issues fixed).&lt;/p&gt;

&lt;h3&gt;
  
  
  Being honest about measurement limits
&lt;/h3&gt;

&lt;p&gt;The Whisper match rate is &lt;strong&gt;a proxy for pronunciation clarity, not a quality score itself.&lt;/strong&gt; "Letter singing" (A - B - C) in particular is easy for Whisper to mistranscribe, dragging the rate down (I confirmed mistranscriptions like "EBC" on song 1). Don't cut tracks on the number alone — the final judge is your ears. Bad takes like the 0.0% one do happen, but the sample size is small, so I won't generalize to a "failure rate of X%." &lt;strong&gt;The two-variants-per-request design worked as bad-take insurance&lt;/strong&gt; — this time it absorbed two bad takes at zero extra cost.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. The recipe (shortest path)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install&lt;/strong&gt;: &lt;code&gt;git clone&lt;/code&gt; → &lt;code&gt;uv sync&lt;/code&gt; → start the API server (&lt;code&gt;ACESTEP_LM_BACKEND=mlx&lt;/code&gt;, the command in §3; run the ~7GB initial download in the background)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspect the endpoint&lt;/strong&gt;: before generating, &lt;strong&gt;verify the endpoint URL and &lt;code&gt;api_mode&lt;/code&gt;&lt;/strong&gt; (the two &lt;code&gt;config --set&lt;/code&gt; lines in §1; don't trust a passing health check)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate&lt;/strong&gt;: caption / structure-tagged lyrics / duration, bpm, key-scale (the command in §4; one request yields two variants)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score&lt;/strong&gt;: transcribe with mlx-whisper → match rate against the lyrics (the command in §6; &lt;strong&gt;pass arm64 Python explicitly via &lt;code&gt;--python&lt;/code&gt;&lt;/strong&gt;; normalize numbers to English words before comparing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Final judgment by ear&lt;/strong&gt;: the match rate is a proxy. Letter-singing dips and the like are Whisper's limits, so decide by listening in the end&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  8. To my fellow local-AI folks (wrap-up)
&lt;/h2&gt;

&lt;p&gt;Zero cost, zero external calls: 16 vocal tracks plus 14 regenerated ones, all made on this one Mac. Two 100-second songs in about 58 seconds; 8 songs × 2 variants in about 9 minutes (measured on this M1 Max 64GB setup). My answer to "is vocal-song generation on a Mac realistic?" is: &lt;strong&gt;on this configuration, generation speed is no longer the problem.&lt;/strong&gt; What eats your time is lyric design and quality judgment — which is exactly why automating the scoring paid off.&lt;/p&gt;

&lt;p&gt;The lessons, in three lines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't trust a passing health check.&lt;/strong&gt; Inspect the endpoint URL and &lt;code&gt;api_mode&lt;/code&gt; before you say "it runs locally"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check for arm64 Python first.&lt;/strong&gt; The x86 landmine comes back even through wrappers like uvx&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't grade your own songs.&lt;/strong&gt; Whisper match rate × independent rubric × ears, in three layers. And lyric fixes and seed luck are independent — never discard old takes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The video installment is here → &lt;a href="https://zenn.dev/umamon/articles/df7a862c4a944e" rel="noopener noreferrer"&gt;I Built a 100% Free, Fully Local AI Short-Video Pipeline on an M1 Mac&lt;/a&gt;. The series continues, inside this same Mac. Tell me where your local music generation gets stuck sometime, too.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;(All numbers and timings are measured on this M1 Max 64GB setup; no speculative numbers, no guarantee wording. License and official-spec citations reflect each official repo/model card as of July 2026.)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>music</category>
      <category>ai</category>
      <category>applesilicon</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>I Built a 100% Free, Fully Local AI Short-Video Pipeline on an M1 Mac — For Fellow Local-Claude Folks</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Fri, 10 Jul 2026 06:35:51 +0000</pubDate>
      <link>https://dev.to/bigkijimon/i-built-a-100-free-fully-local-ai-short-video-pipeline-on-an-m1-mac-for-fellow-local-claude-8je</link>
      <guid>https://dev.to/bigkijimon/i-built-a-100-free-fully-local-ai-short-video-pipeline-on-an-m1-mac-for-fellow-local-claude-8je</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Zero cost. Zero external calls. Draw keyframes with SDXL, bring them to life with LTX-2.3 (MLX) image-to-video, narrate with Kokoro TTS, score it with MusicGen, and stitch it all together with ffmpeg — entirely inside one M1 Max (64GB). This is the real log, plus a recipe you can copy.&lt;/p&gt;

&lt;p&gt;Audience: &lt;strong&gt;Claude Code users and developers running Claude/LLMs locally on Apple Silicon.&lt;/strong&gt; I wrote down every place I got stuck, because the failures are the most useful part.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Panning/zooming a still image (Ken Burns) is not a video. It's a slideshow.&lt;/strong&gt; Real motion only comes from &lt;strong&gt;I2V (image-to-video)&lt;/strong&gt;. Not grasping this first was my biggest mistake.&lt;/li&gt;
&lt;li&gt;The whole stack is free and local: &lt;strong&gt;SDXL (ComfyUI) keyframes → LTX-2.3 (MLX) I2V → Kokoro TTS narration → MusicGen BGM → ffmpeg assembly.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The nastiest gotcha is the &lt;strong&gt;Metal GPU watchdog crash (MLX issue #3267)&lt;/strong&gt;. The old package died instantly while the display was on; the newer &lt;code&gt;dgrauet/ltx-2-mlx&lt;/code&gt; (q8) &lt;strong&gt;runs to completion with the screen on&lt;/strong&gt; — that reversal was the biggest lesson.&lt;/li&gt;
&lt;li&gt;Environment setup is where 99% of people get stuck: &lt;strong&gt;arm64 Python&lt;/strong&gt; and &lt;strong&gt;pinning transformers&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. How it started: "This is just a slideshow"
&lt;/h2&gt;

&lt;p&gt;My first delivery was rejected in one sentence.&lt;/p&gt;

&lt;p&gt;I generated ten nice stills with SDXL, faked motion with ffmpeg's &lt;code&gt;zoompan&lt;/code&gt; (the Ken Burns push/pull), laid narration and subtitles on top, and called it "a video." The response:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This isn't what I wanted. It's just a slideshow. I asked for a video, not a slideshow."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It stung. Here's what I burned into my bones:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A "video" (movie) means the characters, the water, the light actually move.&lt;/strong&gt; A zoom/pan over a still is not a video. Never conflate the two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin down the definition of the deliverable first.&lt;/strong&gt; Don't sacrifice what the client actually wants (real motion) for "cheap and fast." Cheap and fast, but wrong, is worth zero — it's a full redo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real motion requires I2V.&lt;/strong&gt; A stills-only pipeline can only get you to a "storyboard / preview."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;But don't throw Ken Burns away&lt;/strong&gt; — it's great for matching narration length in storyboard checks, and for closing shots that don't need to move. &lt;strong&gt;Just never call that "finished."&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That single rejection was the origin of everything below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpwoh7na2ffj6yrq8ogth.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpwoh7na2ffj6yrq8ogth.gif" alt="Left: the rejected " width="640" height="180"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Side-by-side, from the actual project. Left = the rejected slideshow (zoom on a still). Right = real I2V motion from the same keyframe.&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  2. The goal: run real motion (I2V) locally, for free
&lt;/h2&gt;

&lt;p&gt;The idea is simple. &lt;strong&gt;Make a still image (a keyframe) for each shot, then have an I2V model bring that keyframe to life.&lt;/strong&gt; Keyframes come from SDXL; the motion comes from the MLX port of LTX-2.3. All of it runs inside this Mac.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb3omkv148jcote6mtnsp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb3omkv148jcote6mtnsp.png" alt="Full pipeline of the free local AI video stack" width="799" height="306"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fig 1: The whole pipeline — I2V (LTX-2.3) is the body of the video&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;Storyboard + narration script&lt;/td&gt;
&lt;td&gt;Local LLM (gemma-3-12b-it via mlx_lm)&lt;/td&gt;
&lt;td&gt;Design&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;Keyframe stills&lt;/td&gt;
&lt;td&gt;ComfyUI / SDXL (realcartoon-xl)&lt;/td&gt;
&lt;td&gt;Starting image per shot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;Narration audio + real duration&lt;/td&gt;
&lt;td&gt;Kokoro TTS (or macOS &lt;code&gt;say&lt;/code&gt;) + &lt;code&gt;ffprobe&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Free; locks timing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;I2V — "move" the keyframe&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;LTX-2.3 MLX&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;This is the body of the "video." Non-skippable&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;Assembly (concat + narration + subs + BGM)&lt;/td&gt;
&lt;td&gt;ffmpeg&lt;/td&gt;
&lt;td&gt;Finishing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(preview only)&lt;/td&gt;
&lt;td&gt;Ken Burns&lt;/td&gt;
&lt;td&gt;ffmpeg zoompan&lt;/td&gt;
&lt;td&gt;For review only. Not the finished product&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h2&gt;
  
  
  3. The stack (all free, all local)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keyframes&lt;/strong&gt;: ComfyUI + &lt;code&gt;realcartoon-xl-v4&lt;/code&gt; (SDXL family). Batch-generate by hitting the REST API (&lt;code&gt;:8000&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I2V (the video body)&lt;/strong&gt;: MLX port of LTX-2.3, in two flavors by purpose (below).

&lt;ul&gt;
&lt;li&gt;Draft = &lt;code&gt;notapalindrome/ltx23-mlx-av-q4&lt;/code&gt; (20GB, distilled q4, fast)&lt;/li&gt;
&lt;li&gt;Production = &lt;code&gt;dgrauet/ltx-2.3-mlx-q8&lt;/code&gt; (21GB, &lt;code&gt;--two-stages-hq&lt;/code&gt; + STG, high fidelity)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text encoder / script LLM&lt;/strong&gt;: &lt;code&gt;mlx-community/gemma-3-12b-it-4bit&lt;/code&gt; (~7GB).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Narration&lt;/strong&gt;: Kokoro TTS (&lt;code&gt;mlx-audio&lt;/code&gt;). macOS's built-in &lt;code&gt;say&lt;/code&gt; works too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BGM&lt;/strong&gt;: MusicGen (&lt;code&gt;transformers&lt;/code&gt;' built-in &lt;code&gt;MusicgenForConditionalGeneration&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assembly&lt;/strong&gt;: ffmpeg (Homebrew build — with a caveat below).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hardware is &lt;strong&gt;M1 Max / 64GB&lt;/strong&gt;. That "64GB reality" constrains model choice.&lt;/p&gt;


&lt;h2&gt;
  
  
  4. Environment setup is where you'll get stuck (two real landmines)
&lt;/h2&gt;

&lt;p&gt;Honestly, setup is harder than the generation itself. &lt;strong&gt;99% of the pain is here.&lt;/strong&gt; Two landmines:&lt;/p&gt;
&lt;h3&gt;
  
  
  1) Use arm64 Python (Anaconda will kill you)
&lt;/h3&gt;

&lt;p&gt;Anaconda-style Python often runs as x86_64 (Rosetta), and there &lt;code&gt;mlx&lt;/code&gt; simply won't install (&lt;code&gt;no matching distribution&lt;/code&gt;). &lt;strong&gt;Always create your venv with native arm64 Python.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/Library/Frameworks/Python.framework/Versions/3.12/bin/python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv ~/ltx-mlx
~/ltx-mlx/bin/pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; mlx-video-with-audio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The newer package (&lt;code&gt;dgrauet/ltx-2-mlx&lt;/code&gt;) uses &lt;code&gt;uv&lt;/code&gt;, but &lt;strong&gt;&lt;code&gt;uv&lt;/code&gt; may also grab an x86_64 anaconda python at first and fail to install mlx&lt;/strong&gt;, so pass the arm64 python explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 https://github.com/dgrauet/ltx-2-mlx.git ~/ltx-2-mlx
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/ltx-2-mlx
uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--all-extras&lt;/span&gt; &lt;span class="nt"&gt;--python&lt;/span&gt; /Library/Frameworks/Python.framework/Versions/3.11/bin/python3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2) Pin transformers to &lt;code&gt;&amp;gt;=4.50,&amp;lt;5&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pip install mlx-video-with-audio&lt;/code&gt; drags in transformers 5.13, which crashes &lt;code&gt;mlx_lm&lt;/code&gt;'s tokenizer registration (&lt;code&gt;AttributeError: 'str' object has no attribute '__module__'&lt;/code&gt;). &lt;strong&gt;Pin it down&lt;/strong&gt; and it works (4.57.6 runs fine; pip warns about deps but it works):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/ltx-mlx/bin/pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"transformers&amp;gt;=4.50,&amp;lt;5"&lt;/span&gt;   &lt;span class="c"&gt;# ← the required pin&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Miss these two and you'll lose hours to "mlx won't install for some reason" and "a mysterious AttributeError on startup." Kill them first.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Model choice and the "64GB reality"
&lt;/h2&gt;

&lt;p&gt;The full-precision LTX-2.3 model is &lt;strong&gt;53.5GB as a single file&lt;/strong&gt;, which OOMs / thrashes on 64GB at load. &lt;strong&gt;Don't use it.&lt;/strong&gt; Run quantized versions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9qfl99d9nnv9p3lxiqyj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9qfl99d9nnv9p3lxiqyj.png" alt="Model choice: q4 draft / q8 production / motion LoRA" width="800" height="466"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fig 2: Which quantized build to use, inside the 64GB reality&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Model / settings&lt;/th&gt;
&lt;th&gt;Measured speed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Speed / draft&lt;/td&gt;
&lt;td&gt;distilled q4 (&lt;code&gt;ltx23-mlx-av-q4&lt;/code&gt; / &lt;code&gt;--distilled&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;~4 min/clip, peak ~14.7GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quality / production&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;q8 &lt;code&gt;--two-stages-hq&lt;/code&gt; + STG&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;~30 min/clip&lt;/strong&gt;, peak ~21GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fights / heavy motion&lt;/td&gt;
&lt;td&gt;q8 HQ + &lt;code&gt;--stg-scale 1.0&lt;/code&gt; + motion LoRA&lt;/td&gt;
&lt;td&gt;30 min+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For HQ at 1024×576, 97 frames, it's &lt;strong&gt;~30 min/clip (1774s)&lt;/strong&gt; — of which Stage 1 is 24 min (15 steps × ~95s). That's ~7.5× slower than distilled q4 (~4 min), but bricks, water, and edges come out clearly sharper. &lt;strong&gt;Iterate drafts on q4, then bake only the final on q8&lt;/strong&gt; — that was the practical answer.&lt;/p&gt;


&lt;h2&gt;
  
  
  6. The recipe (the actual commands I ran)
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1) Script (local LLM, fully autonomous)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;mlx_lm&lt;/code&gt; + &lt;code&gt;gemma-3-12b-it-4bit&lt;/code&gt; generates the shot list, per-shot visual prompts, and narration together.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PY&lt;/span&gt;&lt;span class="s2"&gt;/mlx_lm.generate"&lt;/span&gt; &lt;span class="nt"&gt;--model&lt;/span&gt; mlx-community/gemma-3-12b-it-4bit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-tokens&lt;/span&gt; 1100 &lt;span class="nt"&gt;--temp&lt;/span&gt; 0.75 &lt;span class="nt"&gt;--prompt&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROMPT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'/^==========/d'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; script.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt enforces "a strong hook in the first 3 seconds," "visual prompts in English describing people/place/light/motion," and "no speculative numbers, no exaggeration, no guarantee wording."&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Keyframes (ComfyUI / SDXL)
&lt;/h3&gt;

&lt;p&gt;POST a graph to ComfyUI's REST (&lt;code&gt;:8000&lt;/code&gt;) to get stills. Sampler: &lt;code&gt;dpmpp_2m&lt;/code&gt; / &lt;code&gt;karras&lt;/code&gt; / steps 30 / cfg 6.5.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;class_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;KSampler&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inputs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;steps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cfg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;6.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sampler_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dpmpp_2m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scheduler&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;karras&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;denoise&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Character consistency is the lifeline&lt;/strong&gt;, so reuse a fixed design description of the main characters across every prompt. SDXL loves to add extra people, so put &lt;code&gt;three people, group&lt;/code&gt; in the negative prompt to hold it back.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl35e2m9rldby55fq1e8c.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl35e2m9rldby55fq1e8c.jpg" alt="An actual SDXL keyframe (canal at dusk, the seed image for cut01)" width="800" height="457"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A real keyframe generated with ComfyUI/SDXL (the starting image of cut01)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4nln0r2wxdmxicof6feb.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4nln0r2wxdmxicof6feb.jpg" alt="Another keyframe (serving scene, take 3)" width="800" height="457"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Character consistency held via a fixed design description (cut07, third retake)&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  3) I2V (the video body — LTX-2.3)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Draft = q4&lt;/strong&gt; (~4 min/shot). Run it alone, with &lt;code&gt;caffeinate&lt;/code&gt; to block sleep.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;caffeinate &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; ~/ltx-mlx/bin/python &lt;span class="nt"&gt;-m&lt;/span&gt; mlx_video.generate_av &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model-repo&lt;/span&gt; notapalindrome/ltx23-mlx-av-q4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--text-encoder-repo&lt;/span&gt; mlx-community/gemma-3-12b-it-4bit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; kf.png &lt;span class="nt"&gt;--prompt&lt;/span&gt; &lt;span class="s2"&gt;"your motion, cinematic film footage, natural gentle motion, warm golden light"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--negative-prompt&lt;/span&gt; &lt;span class="s2"&gt;"static, morphing, warping, extra fingers, flicker, jitter, low quality"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-W&lt;/span&gt; 1024 &lt;span class="nt"&gt;-H&lt;/span&gt; 576 &lt;span class="nt"&gt;--num-frames&lt;/span&gt; 65 &lt;span class="nt"&gt;--fps&lt;/span&gt; 24 &lt;span class="nt"&gt;--no-audio&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output-path&lt;/span&gt; cut.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Production = q8 HQ&lt;/strong&gt; (~30 min/shot, high fidelity).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/Users/yuma/ltx-2-mlx/.venv/bin/ltx-2-mlx generate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; dgrauet/ltx-2.3-mlx-q8 &lt;span class="nt"&gt;--two-stages-hq&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; keyframe.png 0 1.0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--prompt&lt;/span&gt; &lt;span class="s2"&gt;"your motion, cinematic film, natural motion, warm golden light"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--frames&lt;/span&gt; 97 &lt;span class="nt"&gt;--frame-rate&lt;/span&gt; 24 &lt;span class="nt"&gt;-H&lt;/span&gt; 576 &lt;span class="nt"&gt;-W&lt;/span&gt; 1024 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stg-scale&lt;/span&gt; 1.0 &lt;span class="nt"&gt;--cfg-scale&lt;/span&gt; 4.0 &lt;span class="nt"&gt;--seed&lt;/span&gt; 42 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; cut.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffld9tlhfpo8lhngltmrm.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffld9tlhfpo8lhngltmrm.gif" alt="A cut actually generated with LTX-2.3 (converted to GIF)" width="480" height="270"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A real generated cut from this pipeline (GIF-compressed for the article; the original is 1024×576, 24fps, with audio)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Key notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Resolution must be a &lt;strong&gt;multiple of 64&lt;/strong&gt; (1024×576 recommended = true 16:9). For vertical Shorts, &lt;code&gt;-H 1024 -W 576&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;num-frames is &lt;strong&gt;8n+1&lt;/strong&gt; (max 97 on the q4 family). Distilled models ignore &lt;code&gt;--steps&lt;/code&gt; (fixed at 8+3).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--no-audio&lt;/code&gt; drops the model's audio so you can add your own narration afterward (the right call for educational content).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--image PATH FRAME STRENGTH&lt;/code&gt; lets you &lt;strong&gt;anchor both end frames&lt;/strong&gt;, interpolating longer continuous motion.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  4) Narration (Kokoro TTS)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/ltx-mlx/bin/python &lt;span class="nt"&gt;-m&lt;/span&gt; mlx_audio.tts.generate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; mlx-community/Kokoro-82M-bf16 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--text&lt;/span&gt; &lt;span class="s2"&gt;"English narration"&lt;/span&gt; &lt;span class="nt"&gt;--voice&lt;/span&gt; bm_george &lt;span class="nt"&gt;--lang_code&lt;/span&gt; b &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output_path&lt;/span&gt; seg_01.wav
&lt;span class="c"&gt;# Get real duration: ffprobe -v error -show_entries format=duration -of csv=p=0 seg_01.wav&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You'll need &lt;code&gt;pip install "misaki[en]"&lt;/code&gt;. If a particular sentence throws a shape error, tweak the wording and retry. &lt;strong&gt;Kokoro is light and doesn't trip the GPU watchdog&lt;/strong&gt;, so it's fine with the screen on. macOS's &lt;code&gt;say -v Daniel -r 165&lt;/code&gt; works in a pinch too (free, reliable).&lt;/p&gt;
&lt;h3&gt;
  
  
  5) BGM (MusicGen)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;audiocraft&lt;/code&gt; chokes on the &lt;code&gt;av&lt;/code&gt; / pkg-config build, so skip it — the &lt;strong&gt;built-in MusicGen in transformers&lt;/strong&gt; is the answer. &lt;code&gt;facebook/musicgen-small&lt;/code&gt; on CPU (~4.6 min for 20s).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AutoProcessor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MusicgenForConditionalGeneration&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MusicgenForConditionalGeneration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;facebook/musicgen-small&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warm gentle emotional cinematic film score, soft solo piano with tender strings, slow, heartfelt, in a major key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;audio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;do_sample&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;guidance_scale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_new_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# ~20s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Loop the 20s with &lt;code&gt;acrossfade&lt;/code&gt; to match length, and mix it in gently at &lt;code&gt;volume=0.30&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  6) Assembly (ffmpeg)
&lt;/h3&gt;

&lt;p&gt;Upscale each shot to 1080p (lanczos + a light grade), burn the subtitles, add narration, concat all shots, and &lt;code&gt;amix&lt;/code&gt; the BGM at the end. Normalize loudness with &lt;code&gt;loudnorm=I=-15&lt;/code&gt;. &lt;strong&gt;Burn subtitles per shot&lt;/strong&gt; so they never drift from the narration.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. The full gotcha list (the main event for developers)
&lt;/h2&gt;

&lt;p&gt;This is what I most want to share. I stepped on all of these so you don't have to.&lt;/p&gt;

&lt;h3&gt;
  
  
  ★ Metal GPU watchdog crash (MLX issue #3267)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzveceeup0cfy9wp48agn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzveceeup0cfy9wp48agn.png" alt="How the Metal GPU watchdog kills MLX — and the before/after" width="800" height="466"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fig 3: The watchdog mechanism, and the reversal with the new package&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The symptom:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[METAL] Command buffer execution failed: Impacting Interactivity
(kIOGPUCommandBufferCallbackErrorImpactingInteractivity)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The eerie behavior: &lt;strong&gt;only the first LTX run after a reboot succeeds; the second onward dies instantly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The root cause is confirmed in MLX issue #3267: &lt;strong&gt;while the display is on, WindowServer composites the screen on the GPU; when MLX's heavy command buffer blocks that compositing, macOS kills the process to "preserve interactivity."&lt;/strong&gt; Display on = 100% reproducible; off = 100% avoided.&lt;/p&gt;

&lt;p&gt;What did NOT work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;caffeinate -i -s&lt;/code&gt; — prevents system sleep, but &lt;strong&gt;doesn't turn off the display&lt;/strong&gt;, so it's useless here.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pmset displaysleepnow&lt;/code&gt; — if an external monitor stays alive, compositing continues; no good.&lt;/li&gt;
&lt;li&gt;Stopping ComfyUI / &lt;code&gt;/free&lt;/code&gt; — unrelated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the &lt;strong&gt;old package (&lt;code&gt;mlx-video-with-audio&lt;/code&gt; distilled q4)&lt;/strong&gt;, the reliable fix was: 1) reboot the Mac to clear the post-hang driver leak, 2) power off the external monitor and close the lid so nothing is displayed, 3) run the LTX batch in that state. That got all 10 shots through with zero crashes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But the newer package overturned this.&lt;/strong&gt; &lt;code&gt;dgrauet/ltx-2-mlx&lt;/code&gt; (q8 HQ) &lt;strong&gt;generated a shot with the screen left on → 30.8 min, zero crashes, no new crash report.&lt;/strong&gt; So you can run HQ batches in the background during the day while a human uses the screen. No more being chained to unattended overnight jobs. It was a nice moment to walk back the earlier "screen must be off" conclusion with real measurements (do keep ventilation, though — it runs hot).&lt;/p&gt;

&lt;h3&gt;
  
  
  ★ Homebrew ffmpeg has no libass → subtitles won't burn
&lt;/h3&gt;

&lt;p&gt;The Homebrew ffmpeg is built without libass/freetype, so &lt;strong&gt;neither the &lt;code&gt;subtitles&lt;/code&gt; nor the &lt;code&gt;drawtext&lt;/code&gt; filter exists&lt;/strong&gt; (if &lt;code&gt;ffmpeg -filters | grep subtitles&lt;/code&gt; is empty, that's you). You can make an SRT, but you can't burn it.&lt;/p&gt;

&lt;p&gt;The workaround: &lt;strong&gt;draw subtitles onto a transparent PNG with Pillow, then burn them with ffmpeg's &lt;code&gt;overlay&lt;/code&gt;.&lt;/strong&gt; Font: &lt;code&gt;/System/Library/Fonts/Supplemental/Arial Bold.ttf&lt;/code&gt;. Use a thick, dark stroke and lay a semi-transparent rounded plate behind the text for readability.&lt;/p&gt;

&lt;h3&gt;
  
  
  ★ Boomerang (reverse-loop) artifacts
&lt;/h3&gt;

&lt;p&gt;An LTX clip is ~2.7–4s (97 frames). If you pad a long-narration shot with "forward + reverse (boomerang)," you get &lt;strong&gt;reverse artifacts&lt;/strong&gt; — things move and then un-move (e.g. someone "un-sips" a drink). That's the single biggest deduction on motion quality.&lt;/p&gt;

&lt;p&gt;The fix: &lt;strong&gt;don't reverse.&lt;/strong&gt; Hold the last frame as a clone and add a gentle drift:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tpad=stop_mode=clone:stop_duration=X   # clone-hold the final frame out to the narration length
zoompan=z='min(1.0+0.0006*on,1.11)':d=1:fps=24   # imperceptible drift
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need genuinely longer continuous motion, &lt;strong&gt;chain LTX clips&lt;/strong&gt; (last frame of a clip → next keyframe → generate → concat).&lt;/p&gt;

&lt;h3&gt;
  
  
  ★ 4x-UltraSharp (ESRGAN) breaks on Apple MPS
&lt;/h3&gt;

&lt;p&gt;Using ESRGAN-family upscalers (4x-UltraSharp) produces &lt;strong&gt;scanline-style breakup&lt;/strong&gt; on Apple's MPS. Don't. If you need 4K, a &lt;strong&gt;clean ffmpeg upscale&lt;/strong&gt; (&lt;code&gt;scale=3840:2160:flags=lanczos&lt;/code&gt; + a light unsharp) looks perfectly fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  ★ The scratchpad (/tmp) is volatile
&lt;/h3&gt;

&lt;p&gt;Put working scripts in &lt;code&gt;/tmp&lt;/code&gt; and they vanish on reboot/cleanup. &lt;strong&gt;Keep important scripts in your deliverables folder.&lt;/strong&gt; A humble lesson that pays off.&lt;/p&gt;

&lt;h3&gt;
  
  
  ★ LoRA turned out to be possible (retraction)
&lt;/h3&gt;

&lt;p&gt;I originally concluded "LoRA is Wan-only, LTX doesn't support it, it's a dead end." &lt;strong&gt;That was wrong.&lt;/strong&gt; I confirmed support in &lt;code&gt;dgrauet/ltx-2-mlx generate --help&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--lora PATH STRENGTH&lt;/code&gt; — a normal LoRA (motion boost, speed LoRA, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--distilled-lora&lt;/code&gt; / &lt;code&gt;--distilled-lora-strength&lt;/code&gt; — LoRA for the distilled (fast) path&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--stg-scale&lt;/code&gt; composes with &lt;code&gt;--lora&lt;/code&gt; (per-block application)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the &lt;strong&gt;"LTX + multiple LoRAs" route is real.&lt;/strong&gt; You don't have to touch ComfyUI-PyTorch (high risk of breaking your torch install) — &lt;strong&gt;you can attach LoRAs straight from the MLX CLI.&lt;/strong&gt; For fight-level heavy motion, the main line is "q8 &lt;code&gt;--two-stages-hq&lt;/code&gt; + &lt;code&gt;--stg-scale 1.0&lt;/code&gt; + a motion LoRA."&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Quality-score progression (the honest log)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F940c68013ptyk537puwx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F940c68013ptyk537puwx.png" alt="Quality score progression from rejected slideshow to ~85" width="800" height="420"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fig 4: The honest climb — still short of the 95-point gate&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Internally, videos go through &lt;strong&gt;independent scoring (the author doesn't grade their own; a separate session does) with a 95-point gate.&lt;/strong&gt; Here's how the actual score moved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v1 slideshow → rejected
 → Wan (I2V) 70
 → LTX character-consistent 76 → 77
 → all shots real LTX 78
 → all boomerangs removed 81
 → + Kokoro / + MusicGen / subtitle plate / -15 LUFS ≈ 85 (estimated)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;From slideshow → real video → ~85, still free.&lt;/strong&gt; Not at 95 yet (motion length and timing are the ceiling). I'll be honest about that. The next moves are LTX chaining and "HQ + motion LoRA" to push toward 90. The accurate framing isn't "free and local is perfect" — it's &lt;strong&gt;"free and local got us this far."&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  9. The recipe (shortest path)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;venv with arm64 python&lt;/strong&gt; (don't grab Anaconda's x86_64) → install &lt;code&gt;mlx-video-with-audio&lt;/code&gt;, pin &lt;code&gt;transformers&amp;gt;=4.50,&amp;lt;5&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Script&lt;/strong&gt; — &lt;code&gt;mlx_lm.generate&lt;/code&gt; + &lt;code&gt;gemma-3-12b-it-4bit&lt;/code&gt; for shot list + visual prompts + narration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyframes&lt;/strong&gt; — ComfyUI (&lt;code&gt;:8000&lt;/code&gt;) + &lt;code&gt;realcartoon-xl-v4&lt;/code&gt;. Reuse a fixed character design across shots; suppress extra people via the negative prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I2V&lt;/strong&gt; — draft on &lt;strong&gt;q4&lt;/strong&gt; (&lt;code&gt;generate_av&lt;/code&gt;, ~4 min), production on &lt;strong&gt;q8 &lt;code&gt;--two-stages-hq&lt;/code&gt; + STG&lt;/strong&gt; (~30 min). Resolution a multiple of 64, frames 8n+1, &lt;code&gt;--no-audio&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Narration&lt;/strong&gt; — Kokoro TTS (&lt;code&gt;mlx_audio&lt;/code&gt; / &lt;code&gt;misaki[en]&lt;/code&gt;). Get real duration with &lt;code&gt;ffprobe&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BGM&lt;/strong&gt; — transformers' built-in MusicGen (&lt;code&gt;facebook/musicgen-small&lt;/code&gt;, CPU). Mix gently at &lt;code&gt;volume=0.30&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assembly&lt;/strong&gt; — ffmpeg to 1080p + grade, &lt;strong&gt;subtitles via Pillow transparent PNG → overlay&lt;/strong&gt; (libass workaround), &lt;strong&gt;no reverse — clone-hold + drift&lt;/strong&gt;, then &lt;code&gt;amix&lt;/code&gt; the BGM + &lt;code&gt;loudnorm=I=-15&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-shift heavy GPU work&lt;/strong&gt; (colliding with other work on the same Mac slows or crashes both). For long jobs, &lt;code&gt;caffeinate -i&lt;/code&gt; + good airflow.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  10. To everyone running Claude locally
&lt;/h2&gt;

&lt;p&gt;A message and some tips for developers running Claude/LLMs locally, like me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You don't have to pay — one Apple Silicon machine gets you this far.&lt;/strong&gt; Cloud I2V (Kling/Runway/Veo…) is gorgeous and fast, but it means external calls and billing. Keeping everything on your own Mac is, beyond cost, mostly about the calm of holding all the pieces yourself. When you get stuck, the cause is entirely inside your own environment.&lt;/p&gt;

&lt;p&gt;Tips to dodge the pitfalls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't melt hours on setup.&lt;/strong&gt; 80% of "mlx won't install" and "mysterious AttributeError on startup" is either "you grabbed an x86_64 python" or "transformers is 5.x." Kill those two first and the rest is the fun part.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep asking "is it actually moving?"&lt;/strong&gt; Don't settle for a zoom over a pretty still. It's not a video until it goes through I2V. Cheap and fast but wrong is worth zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn failure logs into assets.&lt;/strong&gt; Save every winning prompt + seed, every model's runtime, every landmine into &lt;code&gt;knowledge/&lt;/code&gt;. Reproducibility is the asset. Our score climbed 77→85 precisely because we turned each failure into one line of notes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-shift heavy GPU tasks.&lt;/strong&gt; Run them alongside other heavy work or your own session on the same Mac and both slow down or crash. There's one GPU. Batch before bed or while away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be proud of "this far, for free."&lt;/strong&gt; It doesn't have to be perfect. Slideshow → real motion, one step at a time. Getting to share that log with each other is the best part of being local-Claude folks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'll make another one, inside this same Mac. Tell me where your pipeline gets stuck sometime, too. 🌿&lt;/p&gt;




&lt;p&gt;&lt;em&gt;(All figures and timings are measured on this M1 Max 64GB setup. No speculative numbers, no guarantee wording.)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
    </item>
  </channel>
</rss>
