<?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>Claude Codeも電子透かしの対象になった — 無人運転で144本公開してきた工場の生成コマンドを照合したら、確定できたのは「対象条件に一致すること」だけだった</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Fri, 04 Sep 2026 23:45:04 +0000</pubDate>
      <link>https://dev.to/bigkijimon/claude-codemodian-zi-tou-kasinodui-xiang-ninatuta-wu-ren-yun-zhuan-de144ben-gong-kai-sitekitagong-chang-nosheng-cheng-komandowozhao-he-sitara-que-ding-dekitanohadui-xiang-tiao-jian-ni-zhi-surukoto-dakedatuta-5fn9</link>
      <guid>https://dev.to/bigkijimon/claude-codemodian-zi-tou-kasinodui-xiang-ninatuta-wu-ren-yun-zhuan-de144ben-gong-kai-sitekitagong-chang-nosheng-cheng-komandowozhao-he-sitara-que-ding-dekitanohadui-xiang-tiao-jian-ni-zhi-surukoto-dakedatuta-5fn9</guid>
      <description>&lt;p&gt;2026年8月、Anthropicは「Claudeが生成したコンテンツに電子透かしを入れる」と公式に発表した。対象には &lt;strong&gt;Claude Code&lt;/strong&gt; も名指しされている。うちは無人運転でClaude Codeにブログ記事を書かせ続けている工場で、これまでに144本を公開してきた。では、この発表で自分たちの運用は何が変わるのか。公式文書を読むだけでは、自分のケースに当てはまるかどうかは分からない。公式文書と自社の運用ログを実際に照合して、確定できたことと、まだ確定できないことを分けて書く。&lt;/p&gt;

&lt;h2&gt;
  
  
  何が発表されたか
&lt;/h2&gt;

&lt;p&gt;公式サポート記事によると、対象は次のように書かれている。&lt;/p&gt;

&lt;p&gt;"Claude Platform (API), Claude, Claude Code, Claude Cowork, and Claude Tag"&lt;/p&gt;

&lt;p&gt;AWS・Google Cloud・Microsoft Foundry経由の利用も対象に含まれ、地域は全世界。方式はコンテンツの種類で分かれており、テキストは埋め込み型の透かし、画像などのファイル（.svg / .png / .jpg 等）はメタデータ方式になる。ただし「Some platforms or features may not support certain marking types」という但し書きがあり、全部が一律に対象というわけでもない。&lt;/p&gt;

&lt;p&gt;時期については「2026年8月2日以降に発表される新モデルは発表時点から対象、それ以前のモデルは移行期間で時期未定」とある。そして一番重要なのはここだ。&lt;/p&gt;

&lt;p&gt;検出の仕組みは「forthcoming technical documentation で共有する」とだけ書かれており、&lt;strong&gt;現時点ではまだ公開されていない&lt;/strong&gt;。オプトアウトに関する記載も無い。&lt;/p&gt;

&lt;p&gt;つまり「透かしが入る」という発表はあっても、「入っているかどうかを外から確かめる方法」はまだ存在しない。&lt;/p&gt;

&lt;h2&gt;
  
  
  自社の運用は対象条件に当たるか、実行コマンドで照合する
&lt;/h2&gt;

&lt;p&gt;公式文書の対象条件は抽象的な製品名の列挙なので、自分の運用がそこに含まれるかは、自分の生成コマンドを見て初めて分かる。うちの無人生成は、こういう形のワンショット実行になっている（実際に使っているスクリプトの実名・cron時刻・保存パスは伏せ、実行形態だけ示す）。&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; claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROMPT_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MODEL_ARGS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-turns&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MAX_TURNS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--dangerously-skip-permissions&lt;/span&gt; &amp;lt; /dev/null &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1 &amp;amp;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;claude -p&lt;/code&gt; はCLIとしてのClaude Codeそのものであり、公式文書が名指しした対象製品と一致する。&lt;code&gt;--dangerously-skip-permissions&lt;/code&gt; は生成区間に人間の承認が入らないワンショット実行を意味する。承認の有無は対象条件に関係しないので、ここは「対象に一致するかどうか」を確かめるための材料というより、「うちの運用の重さ」を示す補足情報として書いている。&lt;/p&gt;

&lt;h2&gt;
  
  
  公式の対象条件自社の実態
&lt;/h2&gt;

&lt;p&gt;Claude Code経由の生成が対象生成は &lt;code&gt;claude -p&lt;/code&gt; 経由＝一致&lt;br&gt;
テキストは埋め込み型の透かし出力は記事本文（テキスト）＝方式上は対象範囲に含まれる&lt;br&gt;
2026-08-02以降の新モデルが対象。既存モデルは時期未定使用モデルが新基準・移行期間のどちらに当たるかは公式文書からは判定できない＝&lt;strong&gt;不明&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;規模を数え直す — 144本、うち技術ブログは81本&lt;/p&gt;

&lt;p&gt;「無人運転で何本公開してきたか」は、公開記事レジストリを実際に数えれば出る数字なので、自分で数え直した。&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="n"&gt;cd&lt;/span&gt; &lt;span class="o"&gt;~/&lt;/span&gt;&lt;span class="n"&gt;Documents&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Blog&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;_studio&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;python3&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
import csv
from collections import Counter
c = Counter()
with open(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;公開記事レジストリ.csv&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;) as f:
    r = csv.reader(f)
    next(r)
    for row in r:
        if row:
            c.update([row[0]])
print(c)
print(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;total:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, sum(c.values()))
&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Counter({'hs': 63, 'devto': 25, 'qiita': 24, 'zenn': 23, 'note': 9})
total: 144
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;公開記事レジストリを媒体別に集計した内訳（2026-09-05実測）&lt;/p&gt;

&lt;p&gt;技術ブログ4媒体（zenn / qiita / devto / note）の合計は23+24+25+9で&lt;strong&gt;81本&lt;/strong&gt;。この技術4媒体の内訳は2026年8月6日以降まったく増えていない。この記事がその停止を破る側であり、他の停止そのものを分析する記事は別に書いているので、ここでは数字の出所だけ示すに留める。&lt;/p&gt;

&lt;h2&gt;
  
  
  検出器はまだ無い、という一番大事な事実
&lt;/h2&gt;

&lt;p&gt;「透かしを検出してみた」という記事は、いまの時点では書けない。公式サポート記事は検出機構を「forthcoming technical documentation」としか書いておらず、Zennで428いいねを集めていた解説記事も、追記で「検出APIを近く提供予定、実装の詳細は調整中」と書くに留まっている。つまり第三者が手元のテキストに対して検出を試す方法は、この記事を書いている時点で存在しない。&lt;/p&gt;

&lt;p&gt;ここを曖昧にしたまま「うちの記事にも透かしが入っているはずだ」と書けば、それは測っていないことを測ったかのように書く行為になる。うちのブログ運用では過去に、実測していない数字を断定して公開後に訂正した事故が複数回あった。同じ失敗をここでは繰り返さない。書けるのは「対象条件に一致すること」までで、「検出できること」ではない。&lt;/p&gt;

&lt;h2&gt;
  
  
  何が変わり、何が変わらないか
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;分かったこと&lt;/strong&gt;: うちの生成経路（&lt;code&gt;claude -p&lt;/code&gt; 経由のワンショット実行）は、公式が対象と明記した「Claude Code」に一致する。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;分からないこと&lt;/strong&gt;: いま使っているモデルが、2026-08-02以降の新基準の対象か、それ以前のモデルとして移行期間中なのかは、公式文書だけでは判定できない。問い合わせ窓口や技術文書の追加公開を待つしかない。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;変わらないこと&lt;/strong&gt;: 編集フロー・入稿判断・QAの基準はこの発表とは無関係。透かしは出力に埋め込まれるものであって、記事の中身や公開判断を変える性質のものではない。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  自分の環境でも同じ確認をする
&lt;/h2&gt;

&lt;p&gt;この記事でやったことは、公式文書を読むこと以外は全部「自分の環境を数えて、自分の環境をgrepする」だけである。同じことは、無人運転でAIに記事や成果物を作らせている人なら誰でも再現できる。&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;# 1. 自分の生成コマンドがClaude Code経由かを確認する&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"claude"&lt;/span&gt; 自分の生成スクリプト.sh

&lt;span class="c"&gt;# 2. 自分がこれまでに何本公開してきたかを数える&lt;/span&gt;
&lt;span class="c"&gt;#    （記事一覧やCSV台帳があれば、媒体列をCounterで集計するだけでよい）&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;公式文書の対象条件は抽象的だが、自分の生成コマンドを見れば「対象に当たるかどうか」はその場で確定できる。検出できるかどうかは別問題として、まずここだけは自分で確かめられる。&lt;/p&gt;

&lt;h2&gt;
  
  
  まとめ
&lt;/h2&gt;

&lt;p&gt;Anthropicが電子透かしの対象にClaude Codeを含めたことで、無人運転で144本（技術ブログは81本）を公開してきたこの工場の生成経路は、対象条件に一致することが確認できた。一方で、いま使っているモデルが新基準の対象か移行期間中かは公式文書からは分からず、検出の仕組みも未公開のままである。確定できることと、まだ確定できないことを分けて記録しておく。技術文書が公開され次第、同じ手順でもう一度照合する。&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>anthropic</category>
      <category>ai</category>
      <category>automation</category>
    </item>
    <item>
      <title>「コミットにセッションURLが既定で付く」がHN135pt — 自社412コミットで確認したら0本だった</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Mon, 31 Aug 2026 12:30:06 +0000</pubDate>
      <link>https://dev.to/bigkijimon/komitutonisetusiyonurlgaji-ding-defu-ku-gahn135pt-zi-she-412komitutodeque-ren-sitara0ben-datuta-2lgb</link>
      <guid>https://dev.to/bigkijimon/komitutonisetusiyonurlgaji-ding-defu-ku-gahn135pt-zi-she-412komitutodeque-ren-sitara0ben-datuta-2lgb</guid>
      <description>&lt;p&gt;「Claude Codeがコミットメッセージにセッションリンクを既定で付ける」という話がHacker Newsのフロントページで135pt・167コメントを集めていた。既定でON、しかも警告なし——そう聞いたら、自分のリポジトリがどうなっているか気になる。実際に自社の実運用リポジトリ2本・合計412コミットをgrepしてみたら、該当するリンクは1本も無かった。&lt;/p&gt;

&lt;h2&gt;
  
  
  話題になっていたこと
&lt;/h2&gt;

&lt;p&gt;発端は GitHub issue &lt;code&gt;anthropics/claude-code#66504&lt;/code&gt;「Claude Session URL appended to commit messages and PR descriptions by default」。2026-08-31時点でHacker Newsのフロントページに135pt・コメント167という規模で載っていた（Hacker Newsのfront_page APIで観測）。&lt;/p&gt;

&lt;p&gt;issue自体の本文は今回読んでいないため、ここでは「この話題がHNで大きく取り上げられていた」という観測事実だけを使う。中身の主張を孫引きすることはしない。&lt;/p&gt;

&lt;h2&gt;
  
  
  自分のリポジトリではどうなっているか確認した
&lt;/h2&gt;

&lt;p&gt;話を鵜呑みにする前に、手元の実運用リポジトリで確認した。使ったのはこの1行だけだ。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"%B"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"claude.ai/code"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;対象は日常的にClaude Codeで開発している2つのリポジトリ。結果は次の通り。&lt;/p&gt;

&lt;p&gt;リポジトリ総コミット数Co-Authored-By: Claude を含むclaude.ai/code を含む&lt;/p&gt;

&lt;p&gt;HS_Web1341100&lt;br&gt;
upclass-app2782460&lt;br&gt;
合計4123560&lt;/p&gt;

&lt;p&gt;AI著者つきコミットは356本ある。にもかかわらず、セッションURLを含むものは2リポジトリ合わせて0本だった。&lt;/p&gt;
&lt;h2&gt;
  
  
  抑止設定は触っていない、それでも0本だった
&lt;/h2&gt;

&lt;p&gt;issueによれば、この機能を止める設定は &lt;code&gt;.claude/settings.json&lt;/code&gt; の &lt;code&gt;attribution.commit&lt;/code&gt; だという（これはissueの要約から引用する事実であり、自分で検証した挙動ではない）。両リポジトリのローカル設定と、グローバルの &lt;code&gt;~/.claude/settings.json&lt;/code&gt; を確認したが、どちらにも &lt;code&gt;attribution&lt;/code&gt; というキーは存在しなかった。つまり「意図的に切っていたから付いていなかった」わけではない。&lt;/p&gt;

&lt;p&gt;さらに、HS_Web の &lt;code&gt;Co-Authored-By&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;Co-Authored-By: Claude Fable 5 &amp;lt;noreply@anthropic.com&amp;gt;
Co-Authored-By: Claude Opus 4.8 (1M context) &amp;lt;noreply@anthropic.com&amp;gt;
Co-Authored-By: Claude Opus 4.8 &amp;lt;noreply@anthropic.com&amp;gt;
Co-Authored-By: Claude Opus 5 (1M context) &amp;lt;noreply@anthropic.com&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fable 5からOpus 5まで、複数のモデルにまたがって一貫してセッションURLは0本だった。単一バージョンの一時的な状態ではなさそうに見える。&lt;/p&gt;

&lt;h2&gt;
  
  
  検証していないことは検証していないと書く
&lt;/h2&gt;

&lt;p&gt;ここで断定できることと、できないことを分けておく。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;分かったこと&lt;/strong&gt;: 抑止設定を一度も触っていない環境で、412コミット中セッションURLは0本だった。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;分からないこと&lt;/strong&gt;: なぜ0本なのかは特定していない。導入バージョンがissueの記載と一致しているか、headless実行（&lt;code&gt;claude -p&lt;/code&gt;）では挙動が異なるのか、プランや地域による差があるのか、あるいはissueがクローズされた後に既定の挙動が変わったのか——このどれも今回は検証していない。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;「既定でこうなる」という話をHNの点数だけで信じず、自分のリポジトリで同じコマンドを一度実行してみる価値はある。&lt;/p&gt;

&lt;h2&gt;
  
  
  読者が今すぐ試せること
&lt;/h2&gt;

&lt;p&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="nb"&gt;cd&lt;/span&gt; &amp;lt;対象リポジトリ&amp;gt;
git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"%B"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"claude.ai/code"&lt;/span&gt;
git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"%B"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"Co-Authored-By: Claude"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;前者が0で後者が0より大きければ、今回の自社リポジトリと同じ状況——AI著者つきコミットはあるのに、セッションURLは付いていない、ということになる。原因の特定はまだ済んでいないが、まずは自分の環境の実態を数字で持っておくことが、この手の話題を追いかけるときの最初の一歩になる。&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>git</category>
      <category>ai</category>
      <category>automation</category>
    </item>
    <item>
      <title>Qwen2.5-VL 7BはM1 Max 64GBで何tok/s出るか — OCR実測で見えた「画像の複雑さは速度に効かない」理由</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Thu, 06 Aug 2026 01:58:47 +0000</pubDate>
      <link>https://dev.to/bigkijimon/qwen25-vl-7bham1-max-64gbdehe-tokschu-ruka-ocrshi-ce-dejian-etahua-xiang-nofu-za-sahasu-du-nixiao-kanai-li-you-1ei6</link>
      <guid>https://dev.to/bigkijimon/qwen25-vl-7bham1-max-64gbdehe-tokschu-ruka-ocrshi-ce-dejian-etahua-xiang-nofu-za-sahasu-du-nixiao-kanai-li-you-1ei6</guid>
      <description>&lt;p&gt;ローカルVLM(視覚言語モデル)の速度比較記事はいくつもあるが、載っているMacはM4 ProやM4/M5 Maxばかりで、&lt;strong&gt;M1 Max 64GBの実測行は1本も見当たらない&lt;/strong&gt;。手元にちょうどQwen2.5-VL 7Bが入っていたので、OCR(画像→テキスト書き起こし)タスクで実際に測った。結論から書くと、&lt;strong&gt;出力速度は画像の内容にほぼ関係なく23.4〜26.0 tok/sの狭い帯に収まった&lt;/strong&gt;。速度を決めていたのは画像の複雑さではなく、書き起こす文章の長さだった。&lt;/p&gt;

&lt;h2&gt;
  
  
  検証環境と方法
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;ハード: M1 Max 64GB&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ollama: 0.30.8(&lt;code&gt;ollama --version&lt;/code&gt;実測)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;モデル: &lt;code&gt;qwen2.5vl:7b&lt;/code&gt;(6.0GB、2026-07-24に追加済)。&lt;code&gt;ollama show&lt;/code&gt;実測では architecture=qwen25vl / parameters=&lt;strong&gt;8.3B&lt;/strong&gt;(タグ名は7bだが、視覚エンコーダを含めた実パラメータは8.3B) / quantization=Q4_K_M&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;推論バックエンド: サーバーログの&lt;code&gt;srv&lt;/code&gt;/&lt;code&gt;slot&lt;/code&gt;/&lt;code&gt;clip_model_loader&lt;/code&gt;形式から llama.cpp系のGGUFランタイム(MLXではない)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;テスト画像は外部から拾わず、HTML→ヘッドレスChromeのスクリーンショットで自作した3種類。&lt;/p&gt;

&lt;p&gt;画像サイズ内容&lt;/p&gt;

&lt;p&gt;test_a900×620ターミナルのエラー画面(英語コード、612字相当)&lt;br&gt;
test_b900×620和文契約書調の密文(356字、句読点の多い長文)&lt;br&gt;
test_c420×420レシート(品目と金額、数字中心の構造化データ)&lt;/p&gt;

&lt;p&gt;実行前に自社の&lt;code&gt;gpucheck.sh&lt;/code&gt;でGPUがアイドルであることを確認し、Ollamaの&lt;code&gt;/api/generate&lt;/code&gt;(&lt;code&gt;stream:false&lt;/code&gt;)を4回叩いた。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;run1_cold_a  : 初回(モデル未ロードの状態からtest_a)
run2_warm_a  : 直後にtest_aを再送(画像・プロンプトとも完全一致)
run3_warm_b  : test_b(新規画像)
run4_warm_c  : test_c(新規画像)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;レスポンスJSONの&lt;code&gt;eval_count&lt;/code&gt;/&lt;code&gt;eval_duration&lt;/code&gt;/&lt;code&gt;prompt_eval_count&lt;/code&gt;/&lt;code&gt;prompt_eval_duration&lt;/code&gt;/&lt;code&gt;load_duration&lt;/code&gt;を取り、&lt;code&gt;~/.ollama/logs/server.log&lt;/code&gt;の&lt;code&gt;slot print_timing&lt;/code&gt;行と突合して一致を確認した(二重検証)。&lt;/p&gt;

&lt;h2&gt;
  
  
  結果: 出力速度は画像の内容にほぼ関係なく23.4〜26.0 tok/s
&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%2Fpctgas5y4t2286tflllv.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%2Fpctgas5y4t2286tflllv.png" alt="Qwen2.5-VL 7BのM1 Max実測4回の内訳、load/prompt_eval/evalの積み上げ棒グラフ" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;4回の実測内訳。loadはモデル読込、prompt_evalは画像エンコード+プリフィル、evalは出力生成にかかった時間。&lt;/p&gt;

&lt;p&gt;run画像totalloadprompt_evaleval(出力)tok/s&lt;/p&gt;

&lt;p&gt;run1(cold)test_a17.51s5.05s5.78s(1095tok)6.67s(173tok)25.95&lt;br&gt;
run2(warm・再送)test_a7.65s0.19s0.05s(キャッシュ)7.39s(173tok)23.41&lt;br&gt;
run3(warm)test_b17.11s0.20s5.51s(1095tok)11.37s(273tok)24.01&lt;br&gt;
run4(warm)test_c11.91s0.19s5.77s(1131tok)5.92s(143tok)24.16&lt;/p&gt;

&lt;p&gt;異なる画像3枚(run1・run3・run4)のtok/sを平均すると&lt;strong&gt;24.71 tok/s&lt;/strong&gt;((25.95+24.01+24.16)/3)。全4回でも最小23.41〜最大25.95tok/sの幅に収まっており、テキスト主体の英語コード画面でも、句読点の多い和文密文でも、数字が並ぶレシートでも、出力速度そのものはほとんど変わらなかった。&lt;/p&gt;

&lt;p&gt;今回の空白を埋めるために比較材料にした&lt;a href="https://insiderllm.com/guides/vision-models-locally/" rel="noopener noreferrer"&gt;insiderllm.comの速度表&lt;/a&gt;では、Mac上での最速行は「M4 Pro 24GB」でQwen3-VL 8B=40〜60 t/sとされている(2026-07-29時点、M1 Maxの実測行は無し)。モデルもチップ世代も違うので単純比較はできないが、4年前のM1 Max 64GBはこの数字の半分強という位置づけになる。&lt;/p&gt;

&lt;h2&gt;
  
  
  画像側の前処理コストは「複雑さ」でなく、ほぼ固定コスト
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;prompt_eval_count&lt;/code&gt;(画像+プロンプトのトークン数)は run1=1095, run3=1095, run4=1131 と、900×620の密文(run3)も420×420のレシート(run4)も、900×620のコード画面(run1)とほぼ同じ範囲に収まった。ピクセル数も内容の複雑さもまったく違う3枚が、トークン数ではほぼ横並びになる。なぜレシート(run4)がやや多いのかは正確な内訳を検証できていないので「不明」と書いておく(アスペクト比の丸め処理の影響と推測はできるが、断定はしない)。&lt;/p&gt;

&lt;p&gt;サーバーログを見ると、画像のエンコード自体は&lt;code&gt;image processed in 2410〜2667 ms&lt;/code&gt;とほぼ一定。prompt_eval全体(5.51〜5.78s、run2を除く)のうち約半分がこの画像エンコードで、残りはプリフィル(プロンプト全体をモデルに読ませる処理)にあたる。&lt;/p&gt;

&lt;p&gt;唯一の例外がrun2で、test_aと完全に同じ画像・同じプロンプトを直後に再送したところ、&lt;code&gt;prompt_eval_duration&lt;/code&gt;が5.78s→&lt;strong&gt;0.05s&lt;/strong&gt;まで落ちた。サーバーログにも&lt;code&gt;image processed&lt;/code&gt;の行自体が出ておらず、画像エンコードそのものがスキップされている。つまりOllamaは同一コンテキストの再送に対してキャッシュを効かせるが、&lt;strong&gt;1文字でも画像や内容が変われば、毎回この5.5〜5.8秒の前処理コストを払い直す&lt;/strong&gt;。&lt;/p&gt;

&lt;h2&gt;
  
  
  精度実測: レシート100%・コード実質100%・密文99.72%
&lt;/h2&gt;

&lt;p&gt;各レスポンスを元テキストと&lt;code&gt;difflib.SequenceMatcher&lt;/code&gt;で突合した(空白・改行の差は正規化して除外)。&lt;/p&gt;

&lt;p&gt;画像文字数一致率誤り&lt;/p&gt;

&lt;p&gt;test_c レシート(数字・構造化)123字&lt;strong&gt;100.0%&lt;/strong&gt;なし(区切り線の表記ゆれを除く)&lt;br&gt;
test_a コード画面612字実質100%HTMLエンティティの表記差(&amp;lt;→&amp;lt;)のみ、内容は完全一致&lt;br&gt;
test_b 和文密文356字99.72%1文字誤変換(「自己&lt;strong&gt;の&lt;/strong&gt;費用負担」→「自己&lt;strong&gt;的&lt;/strong&gt;費用負担」)&lt;/p&gt;

&lt;p&gt;金額9箇所を含むレシートは1文字も落とさず完全一致。長い和文の自由文だけ、356字中1字だけ助詞の「の」を「的」に取り違えた。3種類・計1,091字を通しての誤りはこの1文字のみで、実務で使う分には十分な精度だが、&lt;strong&gt;「絶対に間違えない」ものではない&lt;/strong&gt;という前提で使う必要がある。&lt;/p&gt;

&lt;h2&gt;
  
  
  まとめ: 実務でどう使うか
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;1枚あたり6〜17秒(モデルロード後)かかるため、チャットのような即応答ではなく、バッチ/非同期の書き起こし処理向き。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;速度のボトルネックは画像の複雑さではなく&lt;strong&gt;出力トークン数&lt;/strong&gt;。書き起こす文章が長い画像ほど遅くなる(run3の273tok出力は run4の143tok出力よりevalだけで約2倍時間がかかった)。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;同一画像への再送はキャッシュが効いて速いが、実運用でそれを当てにできる場面は少ない。基本は「新規画像=毎回5.5〜5.8秒の前処理コスト」で見積もる。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;精度は数字・構造化データほど安定し、長い自由文ほど低確率で1文字単位の誤りが出る。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;なお、insiderllm.comの速度表本体・huggingfaceのolmOCR-2-7B-1025-MLX-8bit(MLX量子化の専用OCR VLM)は、今回の検証(Ollama/GGUF経路)の対象外として実際には開いていない。前者は事前調査で内容を確認した要約を出典として引用したのみ、後者はローカルOCR特化VLMへの関心の高さを示す文脈として触れたのみで、自分では試していない。&lt;/p&gt;

</description>
      <category>ollama</category>
      <category>qwen</category>
      <category>llm</category>
      <category>ocr</category>
    </item>
    <item>
      <title>AIが無人で書き溜めた社内ドキュメント1,355本をDiátaxisで仕分けたら、3割が自社のドキュメントですらなかった</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Thu, 06 Aug 2026 01:55:12 +0000</pubDate>
      <link>https://dev.to/bigkijimon/aigawu-ren-deshu-kiliu-metashe-nei-dokiyumento1355ben-wodiataxisdeshi-fen-ketara-3ge-gazi-she-nodokiyumentodesuranakatuta-7d6</link>
      <guid>https://dev.to/bigkijimon/aigawu-ren-deshu-kiliu-metashe-nei-dokiyumento1355ben-wodiataxisdeshi-fen-ketara-3ge-gazi-she-nodokiyumentodesuranakatuta-7d6</guid>
      <description>&lt;p&gt;Claude Codeに無人でドキュメントを書かせ続けて数か月経つと、「うちのナレッジベースは今どんな型に偏っているのか」が気になった。答え合わせに &lt;a href="https://diataxis.fr/" rel="noopener noreferrer"&gt;Diátaxis&lt;/a&gt;（Tutorials / How-to guides / Technical reference / Explanation の4分類フレームワーク）を使い、自社リポジトリの Markdown をローカルLLMで全数分類した。結果は「tutorial型が0.0%」という綺麗な数字になったが、それより先に踏んだ罠のほうが再現性が高い教訓だった——&lt;strong&gt;「何本あるか」を数える前に、その母数が本当に自分たちが書いたものかを疑う必要があった。&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  母数を数える前に、母数が汚れていた
&lt;/h2&gt;

&lt;p&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;# $VAULT = 自社ドキュメントの置き場（各自の環境に読み替える）&lt;/span&gt;
find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VAULT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.md"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/graphify-out/*"&lt;/span&gt; &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/node_modules/*"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/.next*/*"&lt;/span&gt; &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/_archive/*"&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
→ 1355
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;1,355本。この数字をそのままDiátaxisに投げるところだった。だが内訳をディレクトリ単位で眺めると、同じルールファイルが違うパスに何度も出てくる。犯人は Claude Code 用スキルパッケージ &lt;code&gt;vercel-react-best-practices&lt;/code&gt; のファイル75本(ルール72本+README/SKILL/AGENTS各1)で、1つのアプリ配下に5箇所——&lt;code&gt;skills/&lt;/code&gt; &lt;code&gt;data/skills/&lt;/code&gt; &lt;code&gt;agent/skills/&lt;/code&gt; &lt;code&gt;.tabnine/agent/skills/&lt;/code&gt; &lt;code&gt;.agents/skills/&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;diff &lt;span class="nt"&gt;-q&lt;/span&gt; skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md &lt;span class="se"&gt;\&lt;/span&gt;
        agent/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md
diff &lt;span class="nt"&gt;-q&lt;/span&gt; skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md &lt;span class="se"&gt;\&lt;/span&gt;
        .tabnine/agent/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md
&lt;span class="c"&gt;# 差分なし。5箇所ともバイト単位で同一ファイル&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ベンダー配布のスキルファイルとElectronアプリのビルド同梱docsをまとめて弾くと、438本(1,355本の32.3%)が「自分たちが書いたのではなく、ツールが持ち込んだもの」だった。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VAULT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.md"&lt;/span&gt; ... | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"/skills/|/dist/|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;tabnine"&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
→ 438
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;残った&lt;strong&gt;917本&lt;/strong&gt;が、実際にBigKiji自身(主にClaude Code)が無人で書き溜めた自社コーパスの実体だ。ここから先はこの917本を対象にする。&lt;/p&gt;

&lt;h2&gt;
  
  
  Diátaxisという物差し
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://diataxis.fr/" rel="noopener noreferrer"&gt;diataxis.fr&lt;/a&gt; はドキュメントを4つの需要に分ける。原文の定義を借りると "Diátaxis identifies four distinct needs, and four corresponding forms of documentation"。4分類は Tutorials(学習)・How-to guides(特定タスクの遂行)・Technical reference(情報の参照)・Explanation(理解と背景)で、これを「実践的⇔理論的」「学習志向⇔参照志向」の2軸が分ける。&lt;/p&gt;

&lt;h2&gt;
  
  
  ローカルLLMで917本を仕分ける
&lt;/h2&gt;

&lt;p&gt;Claudeのトークンは使わず、Ollama上の &lt;code&gt;qwen3.5:latest&lt;/code&gt; に投げた。各ファイル冒頭500文字を抜粋し、5択(tutorial/howto/reference/explanation/other)を1単語で返させる。最初の実装は空応答しか返ってこなかった。&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="err"&gt;curl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;http://localhost:&lt;/span&gt;&lt;span class="mi"&gt;11434&lt;/span&gt;&lt;span class="err"&gt;/api/generate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"qwen3.5:latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Classify ... Answer with exactly one word:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"num_predict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"thinking"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Thinking Process:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;1. Analyze the Request..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"done_reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"length"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;qwen3.5はデフォルトで思考トークンを吐くhybrid-thinkingモデルで、&lt;code&gt;num_predict&lt;/code&gt;の予算をthinkingだけで使い切り、本題の1単語に到達する前に打ち切られていた。Ollama 0.30.8では &lt;code&gt;"think": false&lt;/code&gt; を渡すとthinkingを止めて即答させられる。&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="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"qwen3.5:latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"think"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"temperature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"num_predict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"explanation"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;917本の逐次分類にかかった時間は&lt;strong&gt;1373.9秒(約22.9分)、平均1.498秒/件&lt;/strong&gt;(スクリプトの実測タイマー)。GPUはOllama単独稼働でComfyUI等の他ジョブは走らせていない状態。&lt;/p&gt;

&lt;h2&gt;
  
  
  結果 — 欠けていたのは"tutorial"、ゼロ件
&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%2F8wv168y13w3z5q1oorko.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%2F8wv168y13w3z5q1oorko.png" alt="無人生成コーパス917本のDiátaxis内訳。reference 47.2%、explanation 26.9%、howto 21.4%、other 4.5%、tutorial 0.0%の横棒グラフ" width="800" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;n=917（全1,355本からベンダー配布物・ビルド成果物438本を除外）。分類=qwen3.5:latest(Ollama, think:false)。要点: 自社コーパスはreference/explanationに偏り、tutorialは0件——読者がAIエージェント自身であることの反映。&lt;/p&gt;

&lt;p&gt;分類件数割合&lt;/p&gt;

&lt;p&gt;reference43347.2%&lt;br&gt;
explanation24726.9%&lt;br&gt;
howto19621.4%&lt;br&gt;
other414.5%&lt;br&gt;
tutorial00.0%&lt;/p&gt;

&lt;p&gt;最大はreference(47.2%)、最小はtutorialでゼロ。念のため「はじめに」「Step 1」「チュートリアル」等のキーワードを含む36本を別途スキャンしたが、その中にもtutorial判定は1本も出なかった——分類器の癖ではなく、実際にそういう内容がなかったということだ。組織の「骨格」にあたるドキュメント(部署ごとのナレッジ地図&lt;code&gt;MOC_*&lt;/code&gt;6本・各部署の追記式ログ&lt;code&gt;INDEX.md&lt;/code&gt;20本・全体入口&lt;code&gt;000_START_HERE.md&lt;/code&gt;)も個別に確認したところ、MOC_*はreference4本/explanation2本、INDEX.mdは20本中18本がreferenceで残り2本がhowto——どちらにもtutorialは1本も無く、骨格そのものの設計意図がtutorial型を持っていなかった。&lt;/p&gt;

&lt;p&gt;理由は分かりやすい。この917本の主な読み手はAIエージェント自身であって、初心者の人間ではない。エージェントは「手順を追って学ぶ」必要がなく、「今必要な事実を1回で引く(reference)」か「なぜそうなっているかを把握する(explanation)」かのどちらかを求める。tutorialが生まれない構造は、無人ドキュメント生成が悪いのではなく、読者の性質を正しく反映した結果だった。&lt;/p&gt;
&lt;h2&gt;
  
  
  分類パイプライン自身にもバグがあった
&lt;/h2&gt;

&lt;p&gt;初回実行では917本中22本が問答無用で"other"になっていた。原因はPythonの定番の罠で、パスの先頭2文字("./")を剥がすつもりで書いた &lt;code&gt;path.lstrip("./")&lt;/code&gt; が、実際には「先頭から'.'か'/'に含まれる文字を続く限り削る」という文字集合ベースの処理だった。&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="c1"&gt;# 誤り: "./.pi-subagents/artifacts/x.md" → "pi-subagents/artifacts/x.md"
#       (先頭の "./" だけでなく、隠しディレクトリの "." まで一緒に剥がれる)
&lt;/span&gt;&lt;span class="n"&gt;full&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# 修正: プレフィックスの明示的な除去に置き換え
&lt;/span&gt;&lt;span class="n"&gt;full&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.pi-subagents/&lt;/code&gt; &lt;code&gt;.pi/&lt;/code&gt; &lt;code&gt;.claude/&lt;/code&gt; 配下の隠しディレクトリだけがこの罠を踏み、ファイルが見つからず例外→機械的に"other"へフォールバックしていた。修正して22本を再分類すると、"other"のまま残ったのは3本だけで、残り19本はreference/explanation/howtoへ再分配された。上記の表はこの修正後の最終値である。&lt;/p&gt;

&lt;h2&gt;
  
  
  学び
&lt;/h2&gt;

&lt;p&gt;◆ドキュメント型の分析結果 &lt;br&gt;
無人ドキュメント生成は「参照」と「説明」と「作業ログ」を量産するのは得意だが、「学習コンテンツ」を自然には生まない。読み手がAIエージェント自身である社内コーパスでは、それはむしろ健全な偏りだ。&lt;/p&gt;

&lt;p&gt;◆実務的な教訓(母数チェック) &lt;br&gt;
母数を数える段階で32.3%がベンダー配布物というノイズだったことのほうが、実務上はよほど効く教訓だった。再現する手順は3行に収まる。&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;# 1. 母数を数える(除外条件を明示)&lt;/span&gt;
find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.md"&lt;/span&gt; &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/node_modules/*"&lt;/span&gt; &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/.next*/*"&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="c"&gt;# 2. ベンダー/ビルド成果物を機械的に弾く&lt;/span&gt;
... | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"/skills/|/dist/|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;tabnine"&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="c"&gt;# 3. 残った母数だけをローカルLLMで仕分ける(think:false を忘れない)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;1,350本という数字を鵜呑みにして分類を始めていたら、Diátaxisの結果そのものより先に「ベンダーのReactルールが自社ドキュメントの3割を占めている」という誤った印象を記事にするところだった。数える前に、数える対象を疑う。ローカルLLMは無料で使い倒せるからこそ、その前段の母数チェックにも同じくらい時間をかける価値がある。&lt;/p&gt;

</description>
      <category>diataxis</category>
      <category>ollama</category>
      <category>llm</category>
    </item>
    <item>
      <title>無人運転のシェルスクリプトを1ヶ月書いて踏んだ3つの失敗 — 「人間用CLIのまま」だと何が壊れるか</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Thu, 06 Aug 2026 00:15:40 +0000</pubDate>
      <link>https://dev.to/bigkijimon/xiu-zheng-hou-xian-zai-nokodo-m9f</link>
      <guid>https://dev.to/bigkijimon/xiu-zheng-hou-xian-zai-nokodo-m9f</guid>
      <description>&lt;p&gt;AIエージェントにシェルスクリプトを叩かせて無人運転させ始めると、人間が使う前提で書いた既存のCLIやスクリプトが、思わぬところで壊れる。今回はうちのM1 Max 64GB機で「GPUを1つずつ順番に使う」ための調停システム（信号機スクリプト・監視デーモン・状態表示アプリの3層構成）を実際に約1ヶ月運用して踏んだ、性質の違う3つの失敗を実話ベースで書く。共通しているのは、どれも「人間なら気づけたはずの状態変化」を、無人で動くコードが黙って見逃していたという点だ。&lt;/p&gt;

&lt;h2&gt;
  
  
  背景 — GPUを奪い合う3つのツールを1つずつ回す仕組み
&lt;/h2&gt;

&lt;p&gt;うちのMacではOllama（ローカルLLM）・ComfyUI（画像・動画生成）・ACE-Step（音楽生成）が同じ統合メモリ上のGPUを取り合っている。2つ同時に走らせるとMetalのGPUエラーで落ちるため、調停用のシェルスクリプト（以下「信号機」）が「今どれかが生成中か」を確認してから順番に実行する設計にしている。これを補完するのが60秒おきに動く監視デーモン（以下「番人」）と、状態を常時表示するmacOSメニューバーアプリ（Swift製）だ。3層とも同じ「今GPUを使っているのは誰か」という状態を見ているはずなのに、実際には3つの独立した失敗を引き起こした。&lt;/p&gt;

&lt;h2&gt;
  
  
  失敗1 — 状態確認関数が「無言で失敗終了」した
&lt;/h2&gt;

&lt;p&gt;信号機の状態確認関数は、ComfyUIが生成中かどうかを内部APIで判定してから青信号・赤信号を返す。当初のコードは次のような形だった。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gen_running&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  pgrep &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"ltx_pipelines|mlx_video|generate_av|stable_diffusion"&lt;/span&gt; ...
  comfy_busy &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"comfyui-job"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;問題は &lt;code&gt;comfy_busy &amp;amp;&amp;amp; echo ...&lt;/code&gt; という書き方だ。ComfyUIがアイドル（生成していない）だと &lt;code&gt;comfy_busy&lt;/code&gt; はfalseを返し、&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;の右側は実行されず、関数全体がゼロ以外の終了コードで終わる。このスクリプトは &lt;code&gt;set -e&lt;/code&gt; 付きで書かれていたため、呼び出し元の &lt;code&gt;r=$(gen_running)&lt;/code&gt; がその瞬間に丸ごと落ちていた。結果として「ComfyUIが暇なとき」に限って信号機の状態確認コマンドが常に赤（混雑）を返し続けるという、原因の割に見つけにくいバグになった。人間が対話的に同じコマンドを打っていれば「あれ、動いてないのに落ちるぞ」とすぐ気づけたはずだが、無人で定期実行されるコードはエラーコードを見ていなかった。直し方は1行で済む。&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;# 修正後（現在のコード）&lt;/span&gt;
comfy_busy &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"comfyui-job"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;状態を返すだけの関数は、状態がどうであれ必ずゼロ終了させる。これが1つ目の教訓だ。&lt;/p&gt;

&lt;h2&gt;
  
  
  失敗2 — タイムアウトの無いシェル実行ヘルパーが129プロセスを溜め込んだ
&lt;/h2&gt;

&lt;p&gt;状態表示アプリは数秒おきに &lt;code&gt;ollama ps&lt;/code&gt; を呼んで「今LLMが常駐しているか」を確認していた。ところが信号機がGPUを他のツールに割り当てる際、Ollamaのプロセスを &lt;code&gt;kill -STOP&lt;/code&gt; で一時停止（凍結）させる設計になっている。凍結中のOllamaサーバに &lt;code&gt;ollama ps&lt;/code&gt; を投げると、応答が返らずハングする。当時のシェル実行ヘルパーにはタイムアウトの概念が無く、ポーリングのたびにハングしたプロセスが生まれ続け、誰にも気づかれないまま蓄積していった。実測でこの滞留が129個に達し、メモリを圧迫して「アプリが重い」「GPUが故障したのでは」という誤認を引き起こした。修正後の実装は次の2点になっている（Swiftのソースで確認済み）。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ShellRunner.swift&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;defaultTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;TimeInterval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;
&lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"-c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"pkill -TERM -P &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt; 2&amp;gt;/dev/null; kill -TERM &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt; 2&amp;gt;/dev/null"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;12秒でタイムアウトさせ、期限が来たら子プロセスも含めてまとめてkillする。さらに呼び出し側にも「ロックが立っている（＝Ollamaが凍結中と分かっている）間はそもそも&lt;code&gt;ollama ps&lt;/code&gt;を呼ばない」というガードを追加した。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// GpuSignal.swift&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lockName&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ロック中でなければ ollama ps で常駐モデルを確認&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;「外部コマンドを呼ぶ以上、応答が返らない可能性は常にある」という当たり前の前提が、対話的に使う分には気にならなくても、無人で秒単位に呼び続けるコードでは即座に実害になる。タイムアウトと子プロセスごとのkillは、AIに叩かせるCLI・ラッパーには例外なく必要だという2つ目の教訓になった。&lt;/p&gt;

&lt;h2&gt;
  
  
  失敗3 — 2つの独立したガードが「無言で異なる基準」を使っていた
&lt;/h2&gt;

&lt;p&gt;番人には「重い生成が進行中ならOllamaを解凍（再開）しない」というガードが最初から入っていた。ただしその判定はプロセス名の &lt;code&gt;pgrep&lt;/code&gt; だけに頼っており、ComfyUIのジョブはサーバ内部（&lt;code&gt;main.py&lt;/code&gt;）でキューとして処理されるため、&lt;code&gt;pgrep&lt;/code&gt;には一切映らない。一方で信号機側は先に「ComfyUIは内部APIの&lt;code&gt;/queue&lt;/code&gt;で見るべき」と直っていたのに、番人側は据え置きのままだった。結果、番人はComfyUIが実際に生成中でもそれを検知できず、Ollamaを解凍し続け、信号機がせっかく凍結させた状態を60秒以内に打ち消していた。同じ「今誰かがGPUを使っているか」という1つの問いに対して、2つのスクリプトが別々の答えの出し方をしていたのが根本原因だ。修正はロックファイルを両者共通の基準にすることだった。&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;# ollama-watchdog.sh に追加した1行&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /tmp/bigkiji_gpu.lock &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;「生成中かどうか」を判定する仕組みを2箇所以上に持たせるなら、検出方法を必ず一致させる。これが3つ目の教訓だ。&lt;/p&gt;

&lt;h2&gt;
  
  
  3件から言える共通パターン
&lt;/h2&gt;

&lt;p&gt;失敗壊れた前提直し方&lt;/p&gt;

&lt;p&gt;状態確認関数の暗黙の失敗終了「アイドル時は何も返さなくていい」&lt;code&gt;|| true&lt;/code&gt; で常にゼロ終了させる&lt;br&gt;
タイムアウト無しの外部コマンド呼び出し「コマンドはいつか応答が返る」タイムアウト12秒＋子プロセスごとkill&lt;br&gt;
2つの独立したガードの検出方式不一致「同じ状態を見ているはず」ロックファイルという単一の真実の基準に統一&lt;/p&gt;

&lt;p&gt;3件はいずれも、対話的に使っていれば人間がその場で違和感に気づけたはずの箇所だ。無人運転にした瞬間、そのフィードバックのループが消える。逆に言えば、直し方はどれも数行で済んでいる。壊れる深さと直す量が釣り合っていないというギャップこそ、AI駆動のCLI設計で持ち帰るべき点だと感じている。&lt;/p&gt;

&lt;h2&gt;
  
  
  まとめ — そして「1年」ではなく「26日」だった
&lt;/h2&gt;

&lt;p&gt;この記事は当初「1年書き続けて踏んだ失敗」という切り口で企画していたが、書く前に自社の記録を数え直したところ、この信号機スクリプトの設置は2026-07-11、今日は2026-08-06で、経過は26日だった。1年という前提は成立しない。誇張したまま出すより、実際の期間で書くほうが、短期間でもこれだけ壊れた・直したという密度の話として成立すると考え、タイトルと本文を訂正している。数字は確認してから出す、を自分自身の記事タイトルにも適用した形だ。&lt;/p&gt;

&lt;h2&gt;
  
  
  参考（観測のみ・本文の主張には使っていない）
&lt;/h2&gt;

&lt;p&gt;執筆時点でZennのdaily trendingに「AIフレンドリーなCLIを開発するテクニック」という記事が上位に観測できた（本文は未読のため主張の引用はしていない）。この関心の高さ自体が、今回の実話を記事にする動機になっている。&lt;/p&gt;

</description>
      <category>ai</category>
      <category>macos</category>
    </item>
    <item>
      <title>Approve the hash, not the button: sealing what an AI coding CLI is about to send</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Wed, 05 Aug 2026 17:25:33 +0000</pubDate>
      <link>https://dev.to/bigkijimon/approve-the-hash-not-the-button-sealing-what-an-ai-coding-cli-is-about-to-send-2c0d</link>
      <guid>https://dev.to/bigkijimon/approve-the-hash-not-the-button-sealing-what-an-ai-coding-cli-is-about-to-send-2c0d</guid>
      <description>&lt;p&gt;Can you say, precisely, what Claude Code or Codex carried off your machine on its last run?&lt;/p&gt;

&lt;p&gt;I couldn't. The logs name the files it read. They do not record &lt;strong&gt;which lines&lt;/strong&gt; of those files went out, &lt;strong&gt;to which model&lt;/strong&gt;, or &lt;strong&gt;what preprocessing&lt;/strong&gt; they went through on the way. That gap costs nothing while everything works. It costs everything exactly once.&lt;/p&gt;

&lt;p&gt;I built a desktop app around that problem and have just made it public under Apache-2.0:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/bigkijimon/bigkiji-universe" rel="noopener noreferrer"&gt;https://github.com/bigkijimon/bigkiji-universe&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post isn't a pitch. It's about one mechanism inside it — the &lt;strong&gt;disclosure manifest&lt;/strong&gt; — which I think is reusable outside this project, so here it is with the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;Handing work to an external AI CLI usually looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;you assemble a prompt&lt;/li&gt;
&lt;li&gt;the CLI reads files&lt;/li&gt;
&lt;li&gt;something goes to an API&lt;/li&gt;
&lt;li&gt;a result comes back&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is no seam between 2 and 3 that a human can inspect. "The thing about to be sent" does not exist after it has been sent.&lt;/p&gt;

&lt;p&gt;You might think logging solves it. Logging is a record of &lt;em&gt;what already left&lt;/em&gt;. To have any say, you need to stop before the send, look at the contents, and then permit it. A record you read afterwards and an object you approve beforehand are not the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the seam
&lt;/h2&gt;

&lt;p&gt;The idea is small: &lt;strong&gt;seal the exact thing you're about to send behind a hash, and make the human approve that hash.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The whole file is 46 lines (&lt;code&gt;src/domain/pi-core/security/disclosure-manifest.js&lt;/code&gt;); the sealing function is these twelve:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createDisclosureManifest&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                    &lt;span class="nx"&gt;redactions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;estimatedTokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                    &lt;span class="nx"&gt;externalTools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;slices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;absolute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vaultRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;ranges&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ranges&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
      &lt;span class="na"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fileHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;purpose&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;redactions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;redactions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
    &lt;span class="na"&gt;externalTools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;normalizeExternalTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;externalTools&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;estimatedTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;estimatedTokens&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;payloadHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="na"&gt;policyHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;security&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;policyHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;disclosureHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;))&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;What each field is doing there:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Why it is in the seal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;files[].sha256&lt;/code&gt; + &lt;code&gt;ranges&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;A filename is not enough. This pins &lt;strong&gt;which lines&lt;/strong&gt; went&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;model&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;So "let Opus read these files" cannot become approval for a different model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;payloadHash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A fingerprint of the assembled body itself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;policyHash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which sandbox rules it was built under&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redactions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;What was masked and how much — types and counts only, never contents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;externalTools&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;For anything leaving via the broker, the &lt;strong&gt;verbatim query&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All of it goes into one JSON object, and the SHA-256 of that object is the &lt;code&gt;disclosureHash&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That hash is what the owner approves.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a hash rather than a button
&lt;/h2&gt;

&lt;p&gt;This is the part I actually care about.&lt;/p&gt;

&lt;p&gt;A normal confirmation is "Run this? yes / no". That is consent to &lt;strong&gt;the screen at the moment you pressed it&lt;/strong&gt;. If anything changed between the press and the spawn, what did that consent cover?&lt;/p&gt;

&lt;p&gt;So the spawn side re-verifies before it starts anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;security&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;policyHash&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disclosure&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;policyHash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;STALE_SECURITY_POLICY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;verifyDisclosureManifest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disclosure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preparedPrompt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;STALE_DISCLOSURE_MANIFEST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disclosure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;         &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;STALE_MODEL_SELECTION&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verification re-reads the files and re-hashes them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyDisclosureManifest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;manifest&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;policyHash&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;security&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;policyHash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payloadHash&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payloadHash&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nf"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fileHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vaultRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sha256&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;If a single byte moved between approval and launch, the run is refused rather than started.&lt;/strong&gt; Nothing runs on "it's probably still the same". This is the one place I refused to be optimistic.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;STALE_MODEL_SELECTION&lt;/code&gt; is separate on purpose, because a model swap is the change that happens most quietly. Falling back to a cheaper model is often correct operationally — but it is an &lt;strong&gt;unapproved execution&lt;/strong&gt;. Refuse it and ask again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shrink the payload before sealing it
&lt;/h2&gt;

&lt;p&gt;A manifest is only honest if the thing it seals is small. Hashing everything you sent proves only that you sent everything.&lt;/p&gt;

&lt;p&gt;The pruner ahead of it defaults to (&lt;code&gt;src/domain/pi-agent/context-pruner.js&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;10 files&lt;/li&gt;
&lt;li&gt;48,000 characters&lt;/li&gt;
&lt;li&gt;12,000 tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and takes &lt;strong&gt;±24-line slices&lt;/strong&gt; around relevant regions — line ranges, not whole files. The manifest's &lt;code&gt;ranges&lt;/code&gt; come from there.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Being straight about this: &lt;strong&gt;there is no "we cut context by N%" number in this repository.&lt;/strong&gt; There is no benchmark in it, so the README states no percentage and neither does this post. For a tool like this, only claiming what you measured seems like the minimum.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What the spawned process does not get
&lt;/h2&gt;

&lt;p&gt;Once approved, the child gets a deliberately thin environment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a private, throwaway &lt;code&gt;0700&lt;/code&gt; HOME and TMPDIR&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;only that provider's key.&lt;/strong&gt; No other vendor's credentials&lt;/li&gt;
&lt;li&gt;Claude Code runs with &lt;code&gt;--strict-mcp-config&lt;/code&gt;, an empty MCP config and &lt;code&gt;--disallowed-tools WebSearch,WebFetch,mcp__.*&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Codex runs &lt;code&gt;--ephemeral --ignore-user-config&lt;/code&gt; with web search disabled&lt;/li&gt;
&lt;li&gt;work happens in an isolated git worktree that cannot merge, commit or push&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tool calls pass a &lt;code&gt;PreToolUse&lt;/code&gt; hook that denies every web tool and every &lt;code&gt;mcp__*&lt;/code&gt;, and allows only an allowlisted shell subset — no pipes, no redirection, no networking binaries. One brokered path out. One path is a thing you can watch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What building it taught me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A sandbox hides what you need as well as what you meant to hide.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After I gave every task a throwaway HOME, Claude Code started answering &lt;code&gt;Not logged in · Please run /login&lt;/code&gt;. On macOS, &lt;code&gt;security&lt;/code&gt; looks for the login keychain under &lt;code&gt;$HOME/Library/Keychains&lt;/code&gt; — so replacing HOME had hidden &lt;strong&gt;the store the credential lives in&lt;/strong&gt;, not just a file. Codex returned 401.&lt;/p&gt;

&lt;p&gt;That was the whole explanation for 27 assignments and zero paid completions. The providers were never broken. They were never authenticated.&lt;/p&gt;

&lt;p&gt;The fix was not to scatter secrets around. It was to lend, by absolute path, the one file that CLI cannot start without — read-only, dying with the task.&lt;/p&gt;

&lt;h2&gt;
  
  
  A bonus lesson from going public
&lt;/h2&gt;

&lt;p&gt;Fixing CI for the launch showed me it had &lt;strong&gt;never passed anywhere but macOS&lt;/strong&gt;. &lt;code&gt;npm ci&lt;/code&gt; was refusing an out-of-sync lock file, and behind that first red were several portability bugs stacked up.&lt;/p&gt;

&lt;p&gt;The best one: &lt;strong&gt;the sandbox check used a different resolver on each side of the comparison.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;allowed roots: &lt;code&gt;fs.realpathSync&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;candidate path: &lt;code&gt;fs.realpathSync.native&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Identical on macOS and Linux. Not on Windows, where the JS implementation leaves 8.3 short names alone and the native one expands them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;allowed:   C:\Users\RUNNER~1\AppData\Local\Temp\...\project
candidate: C:\Users\runneradmin\AppData\Local\Temp\...\project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Judged as different places, so &lt;strong&gt;every read inside the sandbox was refused&lt;/strong&gt;. It fails closed, so it was never a hole — but the app could not read its own working directory on Windows.&lt;/p&gt;

&lt;p&gt;Generalised: in any code that handles paths, it is worth asking whether you are &lt;strong&gt;comparing two spellings of one place&lt;/strong&gt;. And note that I did not find this. A CI job I had been ignoring found it — red for four days while still describing itself as a three-OS matrix. A check that does not pass is not a check, however briefly.&lt;/p&gt;

&lt;p&gt;The regression guard is a source-level assertion rather than a behavioural one, because a condition that only fires on platforms with short names cannot be pinned by behaviour:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;doesNotMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sandboxSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/fs&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;realpathSync&lt;/span&gt;&lt;span class="se"&gt;(?!\.&lt;/span&gt;&lt;span class="sr"&gt;native&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sandbox-policy must canonicalise through security-policy.canonical&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(It failed on its first run by matching my own comment. It strips comment lines now.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build a seam you can approve beforehand, not a log you read afterwards.&lt;/strong&gt; If that seam is one hash, it is small enough for a human&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Have the executor re-verify that what was approved is what is about to run.&lt;/strong&gt; Never "probably the same"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put model selection inside the approval.&lt;/strong&gt; It is the substitution that happens most quietly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sandboxes hide what you need too.&lt;/strong&gt; Assume credentials will vanish and plan for it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI that does not pass is not CI.&lt;/strong&gt; Fix it and a pile of hidden bugs surfaces at once&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code is all readable. Design history is in &lt;code&gt;docs/architecture.md&lt;/code&gt; and &lt;code&gt;docs/v3/&lt;/code&gt;; the checks that still fail are written down in &lt;code&gt;docs/known-issues.md&lt;/code&gt; rather than papered over.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/bigkijimon/bigkiji-universe" rel="noopener noreferrer"&gt;https://github.com/bigkijimon/bigkiji-universe&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Environment&lt;/strong&gt;: Apple Silicon Mac, Node 24, Electron 43. Apache-2.0, eight runtime dependencies, 61 selftests (Linux passes; Windows does not yet — see known-issues).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every snippet above is the real thing, at commit &lt;code&gt;a5b19be&lt;/code&gt;.&lt;/strong&gt; Links go straight to it, so you can check the line counts too.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/bigkijimon/bigkiji-universe/blob/a5b19bea0ad7c5003063082b4992ff58d6ab807a/src/domain/pi-core/security/disclosure-manifest.js" rel="noopener noreferrer"&gt;&lt;code&gt;src/domain/pi-core/security/disclosure-manifest.js&lt;/code&gt;&lt;/a&gt; — building and verifying the manifest&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/bigkijimon/bigkiji-universe/blob/a5b19bea0ad7c5003063082b4992ff58d6ab807a/src/domain/pi-agent/task-runner.js" rel="noopener noreferrer"&gt;&lt;code&gt;src/domain/pi-agent/task-runner.js&lt;/code&gt;&lt;/a&gt; — the three staleness checks before spawn&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/bigkijimon/bigkiji-universe/blob/a5b19bea0ad7c5003063082b4992ff58d6ab807a/src/domain/pi-agent/context-pruner.js" rel="noopener noreferrer"&gt;&lt;code&gt;src/domain/pi-agent/context-pruner.js&lt;/code&gt;&lt;/a&gt; — the defaults and the ±24-line slices&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/bigkijimon/bigkiji-universe/blob/a5b19bea0ad7c5003063082b4992ff58d6ab807a/docs/known-issues.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/known-issues.md&lt;/code&gt;&lt;/a&gt; — what still fails&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Written by &lt;strong&gt;Uma&lt;/strong&gt; (&lt;a href="https://github.com/bigkijimon" rel="noopener noreferrer"&gt;@bigkijimon&lt;/a&gt;), running unattended local-plus-external AI pipelines off a single Apple Silicon Mac.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>opensource</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Rebuilding Summer #1: I Generated 100+ Game-Ready 3D Assets for Free, Using Only Local AI on a Mac</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Wed, 05 Aug 2026 09:11:24 +0000</pubDate>
      <link>https://dev.to/bigkijimon/rebuilding-summer-1-i-generated-100-game-ready-3d-assets-for-free-using-only-local-ai-on-a-mac-1i3e</link>
      <guid>https://dev.to/bigkijimon/rebuilding-summer-1-i-generated-100-game-ready-3d-assets-for-free-using-only-local-ai-on-a-mac-1i3e</guid>
      <description>&lt;p&gt;Hello! I'm &lt;strong&gt;StudioMidori&lt;/strong&gt;. I'm currently working on a co-op game (tentatively titled "Engawa Summer") using Unreal Engine 5, aiming to recreate the &lt;em&gt;vibe&lt;/em&gt; of '90s summer vacation games.&lt;/p&gt;

&lt;p&gt;In this series, "&lt;strong&gt;Rebuilding Summer&lt;/strong&gt;," I plan to document the technical aspects of my development process—sharing everything from successes to failures, in a detailed yet sometimes brutally honest way.&lt;/p&gt;

&lt;p&gt;For the first installment, the topic is an eternal struggle for solo developers: &lt;strong&gt;"How do you get 3D assets?"&lt;/strong&gt; Let me give you the conclusion upfront. &lt;strong&gt;I generated over 100+ assets using only my Mac and local AI, with zero extra costs.&lt;/strong&gt; I'll be sharing the setup and the pitfalls I ran into.&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%2F9ga1glnapjtiyc9tghj7.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%2F9ga1glnapjtiyc9tghj7.png" alt="A sample of reference images generated by AI" width="800" height="580"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup: A Two-Stage Pipeline from Image Generation to "Image $\rightarrow$ 3D" Conversion
&lt;/h2&gt;

&lt;p&gt;I used entirely free tools that run locally.&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;Tool Used&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;① Reference Image Generation&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;ComfyUI + SDXL (RealVisXL)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Generated "product photos," like a "floating 1990s round mailbox on a plain white background."&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;② Image $\rightarrow$ 3D Conversion&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Hunyuan3D-2 (fp16, ~4.9GB)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Generates a 3D mesh (GLB) from a single image.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;③ Execution Environment&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Apple Silicon Mac / MPS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Did not use any cloud APIs whatsoever.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;While Hunyuan3D feels like it's heavily geared towards CUDA (NVIDIA), &lt;strong&gt;it actually runs on Apple Silicon's MPS!&lt;/strong&gt; This was the part I was most worried about, honestly. I have a history of other 3D models crashing spectacularly due to unimplemented operators on MPS... But this time, it completed successfully. Nice job!&lt;/p&gt;

&lt;p&gt;Here are my measured results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Generation Time&lt;/strong&gt;: Approximately &lt;strong&gt;6–8 minutes per item&lt;/strong&gt; (&lt;code&gt;octree_resolution&lt;/code&gt; 256–320).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU Utilization&lt;/strong&gt;: &lt;strong&gt;Device Utilization 100%&lt;/strong&gt; during generation (checked with &lt;code&gt;ioreg&lt;/code&gt;. Proof that it wasn't slacking off).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output Mesh&lt;/strong&gt;: For a single vending machine, approximately &lt;strong&gt;709,000 vertices / 350k triangles&lt;/strong&gt; (I'll explain why this is an issue later).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Quality is Determined by 80% of the "Input Image Creation"
&lt;/h2&gt;

&lt;p&gt;After trying it out, I really felt that &lt;strong&gt;the quality of the 3D output is almost entirely determined by the input image&lt;/strong&gt;. After a lot of trial and error, I settled on the following recipe.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For &lt;strong&gt;boxy objects, shoot from the front view / for cylinders or spheres, shoot at a 45-degree angle&lt;/strong&gt; (to help it correctly estimate the shape).&lt;/li&gt;
&lt;li&gt;Include the prompt &lt;strong&gt;"levitating in an empty white void, nothing underneath"&lt;/strong&gt;.
→ If you don't include this, &lt;strong&gt;the floor and shadow under the object get materialized along with the ground&lt;/strong&gt;, leading to things like vending machines with wings popping into existence.&lt;/li&gt;
&lt;li&gt;Strictly negative prompt for &lt;code&gt;podium, plinth, table, shadow&lt;/code&gt; (If you leave it up to its own devices, product photos will automatically put items on display stands).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not generate any text&lt;/strong&gt; (&lt;code&gt;no text&lt;/code&gt; + negative prompt for &lt;code&gt;chinese characters, garbled text&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The "text" part was the hardest nut to crack. When I tried to get Japanese characters on a sign, &lt;strong&gt;the image AI confidently fabricates something that looks like "Chinese characters."&lt;/strong&gt; So, I gave up on it early and decided to &lt;strong&gt;generate all signs as plain white surfaces&lt;/strong&gt;, and then apply the correct font myself later in the engine. ...I realized later that this decision was "absolutely correct from a copyright perspective," but that's another story for another time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;▼ Input Image (Left) vs. 3-View Silhouette of Generated 3D (Right)&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%2Fnhc5ix5q56iit6yy7k9u.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%2Fnhc5ix5q56iit6yy7k9u.png" alt="Input: Round post floating on white background" width="512" height="512"&gt;&lt;/a&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%2Fy4tpzstx84lxvp6jnuqd.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%2Fy4tpzstx84lxvp6jnuqd.png" alt="Result: Side view is perfectly circular = Success" width="799" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  My Epic Failures (This Part is the Most Useful)
&lt;/h2&gt;

&lt;p&gt;It wouldn't be honest for a technical article to only show perfect success stories, so I'm going to share some &lt;strong&gt;reproducible failures&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure ①: Thin or Small Objects Get "Pancake-fied"
&lt;/h3&gt;

&lt;p&gt;Take the folding phone that symbolized the Heisei era. The result was a &lt;strong&gt;thin, flat cracker&lt;/strong&gt; where the depth information had died. No matter how much I tweaked the prompt, I couldn't get any thickness.&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%2Fmuk5o0dt7opcomqb7lxq.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%2Fmuk5o0dt7opcomqb7lxq.png" alt="3 views of a pancake-ified phone" width="799" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause&lt;/strong&gt;: Single-view image-to-3D models tend to break down when dealing with thin or small objects because they have to guess the unseen sides.&lt;br&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: I decided to accept that for things like phones, pagers, or cassettes—things that are inherently thin—I shouldn't rely on the AI at all. Instead, I figured out it was better to create them later using simple box primitives in the engine. It’s about knowing what tool to use where.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure ②: Statues "Multiply" Without Permission
&lt;/h3&gt;

&lt;p&gt;When I asked for just one Jizo statue, &lt;strong&gt;two appeared side-by-side, and they somehow merged into some kind of two-bodied thing&lt;/strong&gt;.&lt;br&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: I explicitly added &lt;code&gt;one single ... , only one figure&lt;/code&gt; to the prompt and added &lt;code&gt;multiple, two&lt;/code&gt; to the negative prompts. This worked for the guardian dogs and the Inari foxes too; a simple phrase like that brought them back to being single units. If you leave it up to the AI, it tends to be &lt;em&gt;too&lt;/em&gt; helpful by adding extra things, so it's key to clearly stating, "Just one is fine."&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure ③: Code/Antennas "Scatter into Space"
&lt;/h3&gt;

&lt;p&gt;When I generated a game controller, &lt;strong&gt;the cable broke apart and floated around like spaghetti in mid-air&lt;/strong&gt;.&lt;br&gt;
&lt;strong&gt;Solution&lt;/strong&gt;: Exclude thin attachments (like cords, antennas, or strings) from the prompt. Just generate the main body, and add the wires later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Yield Rate is About 85%. But a Big Pitfall Awaited.
&lt;/h2&gt;

&lt;p&gt;When I limited my focus to solid objects (houses, appliances, statues, cars, buildings), the &lt;strong&gt;yield rate was generally around 85%&lt;/strong&gt;. For "wheeled" items like tricycles or tractors, the wheels seemed to be strong structural hints, resulting in surprisingly clean shapes.&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%2Fjg185qmyfeoi3rl33sc3.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%2Fjg185qmyfeoi3rl33sc3.png" alt="Wheeled items are strong (tricycle, tractor, Cub, etc)" width="799" height="753"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Even organic matter (animals) formed shapes better than I expected.&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%2F76pwuoz1k55phlyxgezz.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%2F76pwuoz1k55phlyxgezz.png" alt="Chicken, cat, frog, fishbowl, etc" width="760" height="1722"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, after inspecting the 66 generated items, one fact became clear.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The generated GLBs lack UVs, normals, and materials. They are all just flat gray blobs.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This means I can't do the common sense thing of "applying a single texture." &lt;strong&gt;This is the next mountain to climb.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At this point, I thought that I could just apply color collectively on the engine side. To get straight to the conclusion: &lt;strong&gt;this assumption will be overturned later.&lt;/strong&gt; What exactly went wrong, and how, I'll cover in the next post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary and Next Preview
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;We can mass-produce game 3D assets for free using just a Mac + local AI&lt;/strong&gt; (with an estimated yield of 85% for simple objects).&lt;/li&gt;
&lt;li&gt;The quality is 80% dependent on the input image. When prompting, I'll float it in empty space and ask for figures "one by one," avoiding text generation.&lt;/li&gt;
&lt;li&gt;The output is a &lt;strong&gt;gray mesh without UVs or materials&lt;/strong&gt;. &lt;strong&gt;The next challenge is figuring out how to colorize this.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next time, I plan to share everything—including Python scripts for any snags—on &lt;strong&gt;importing these 100 mass-produced assets into Unreal Engine and making them actually functional&lt;/strong&gt;. It will be titled "The episode where we breathe 'color' (not life) into the gray masses."&lt;/p&gt;

&lt;p&gt;...Though I really want to say that, I need to give you a heads-up honestly. &lt;strong&gt;In this next post, I will completely rebuild my coloring strategy.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'm updating the progress daily on X (&lt;code&gt;@StudioMidori&lt;/code&gt;). &lt;strong&gt;If you don't mind, I would be super grateful if you could follow along and watch as summer gets rebuilt one pixel at a time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;――StudioMidori&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>ai</category>
      <category>unrealengine</category>
      <category>macos</category>
    </item>
    <item>
      <title>自社Markdown 1,325本をbge-m3でローカル埋め込みしたら何分かかったか — M1 Max 64GBのRAG基盤コスト実測</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Sun, 02 Aug 2026 23:48:15 +0000</pubDate>
      <link>https://dev.to/bigkijimon/zi-she-markdown-1325ben-wobge-m3derokarumai-meip-misitarahe-fen-kakatutaka-m1-max-64gbnoragji-pan-kosutoshi-ce-17lg</link>
      <guid>https://dev.to/bigkijimon/zi-she-markdown-1325ben-wobge-m3derokarumai-meip-misitarahe-fen-kakatutaka-m1-max-64gbnoragji-pan-kosutoshi-ce-17lg</guid>
      <description>&lt;p&gt;「ローカルRAGを組みたいが、埋め込みバッチにどれくらい時間がかかるのか分からない」——ベンチマーク記事はたいてい他人のデータセットか、せいぜい数十件のサンプルで測っている。自社には無人運転で日々書き溜めてきたMarkdownが千本単位である。それをそのまま埋め込みに投げたら何分かかるのか、M1 Max 64GBで実測した。&lt;/p&gt;

&lt;p&gt;結論から言うと、&lt;strong&gt;1,325件のMarkdownを1件ずつ逐次で埋め込むと約5分46秒&lt;/strong&gt;で終わった。だが数字そのものより、測る過程で踏んだ2つの罠——&lt;strong&gt;GPU調停スクリプトを自分にかけて自滅した話&lt;/strong&gt;と、&lt;strong&gt;「文字数でトリムすれば安全」という思い込みが7件で崩れた話&lt;/strong&gt;——のほうが再現性のある学びだった。&lt;/p&gt;

&lt;h2&gt;
  
  
  何を測ったか
&lt;/h2&gt;

&lt;p&gt;対象は自社の業務用リポジトリ配下にある &lt;code&gt;.md&lt;/code&gt; ファイル全部。&lt;code&gt;node_modules&lt;/code&gt;・&lt;code&gt;graphify-out&lt;/code&gt;・&lt;code&gt;.next*&lt;/code&gt;・&lt;code&gt;_archive&lt;/code&gt; 系の生成物/依存物は除外し、実際に人とAIが書いた文書だけを数えた。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.md"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/node_modules/*"&lt;/span&gt; &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/graphify-out/*"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/.next*/*"&lt;/span&gt; &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/_archive/*"&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;実行するたびに &lt;strong&gt;1,325〜1,327件&lt;/strong&gt;と件数がわずかにブレた。ドキュメント生成が今この瞬間も進行中で、コーパスが静的でないことの証拠だ。&lt;/p&gt;

&lt;p&gt;モデルは &lt;code&gt;ollama list&lt;/code&gt; で確認した &lt;code&gt;bge-m3:latest&lt;/code&gt;（1.2GB、当日追加済み）。Ollamaのローカル埋め込みエンドポイント &lt;code&gt;/api/embed&lt;/code&gt; を直接POSTで叩き、各ファイルの先頭8,000文字を送る素朴な実装で測った。&lt;/p&gt;

&lt;h2&gt;
  
  
  結果 — 3回実測の実測値
&lt;/h2&gt;

&lt;p&gt;Run対象ファイル数成功失敗総時間docs/sec&lt;/p&gt;

&lt;p&gt;11,3251,3187346.16秒3.808&lt;br&gt;
21,3251,3187354.51秒3.718&lt;br&gt;
31,3271,3207344.17秒3.835&lt;/p&gt;

&lt;p&gt;総時間の中央値は&lt;strong&gt;346.16秒（約5分46秒）&lt;/strong&gt;。最速はRun3の344.17秒、最遅はRun2の354.51秒——3回の振れ幅は10秒程度で、平均だけでなく両端を見ても大きなブレは無かった。docs/secの中央値は&lt;strong&gt;3.808件/秒&lt;/strong&gt;。処理した総文字数は約394万文字。&lt;/p&gt;

&lt;p&gt;失敗は3回とも&lt;strong&gt;ちょうど7件&lt;/strong&gt;で一致していた。件数が毎回同じという時点で、単発のネットワーク不調ではなく構造的な原因があると見て、後述の通り切り分けた。&lt;/p&gt;
&lt;h2&gt;
  
  
  メモリはどこまで伸びたか — VRAMではなくRSSを代理指標に使った
&lt;/h2&gt;

&lt;p&gt;Apple Siliconはユニファイドメモリで、GPU専用VRAMを直接計測するコマンドが手元に無かった。手元にある&lt;code&gt;gpucheck.sh&lt;/code&gt;はGPU Device Utilization%（稼働率）を見るツールであって、メモリ使用量は測れない。そこで代理指標として、1秒間隔で&lt;code&gt;ps -axo rss,comm&lt;/code&gt;をサンプリングし、Ollamaプロセスの常駐メモリ(RSS)の最大値を追った。&lt;/p&gt;

&lt;p&gt;3回とも&lt;strong&gt;ピークRSS 7,839.3MB&lt;/strong&gt;で完全に一致した。これは埋め込みのたびにメモリが積み上がっているのではなく、常駐しているOllamaサーバプロセスがモデルロード後にそのメモリ量で安定していることを示す——3回とも同一プロセスを使い回した実行だったための一致であり、プロセスを再起動すれば変わりうる数字だという点は明記しておく。&lt;/p&gt;
&lt;h2&gt;
  
  
  詰まった所①: GPU調停スクリプトを自分にかけて自滅した
&lt;/h2&gt;

&lt;p&gt;全社共通のGPU調停ルールに従い、最初は生成ジョブを &lt;code&gt;gpu-signal.sh run &amp;lt;名前&amp;gt; "&amp;lt;コマンド&amp;gt;"&lt;/code&gt; でラップして実行した。結果は1件も進まずタイムアウトで終了した。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[freeze] llama-server PID=24908 凍結（GPU Compute解放）
[freeze] ollama serve PID=1148 凍結
✅ GPUをComfyUI/LTXに明け渡し完了。生成を開始できます。
...
TimeoutError: timed out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;原因は単純だった。この調停スクリプトは「ComfyUI/LTX/ACE-StepのようなGPUツールにGPUを明け渡すためにOllamaを凍結する」設計であり、&lt;strong&gt;今回のジョブはOllama(bge-m3)自身を叩くベンチ&lt;/strong&gt;だった。信号機を通した瞬間、自分が呼び出そうとしているサーバーを自分で止めてしまった。&lt;/p&gt;

&lt;p&gt;学びはシンプルで、「GPUを使うジョブは必ず調停スクリプトを通す」という運用ルールには例外がある。&lt;strong&gt;調停の対象は「Ollama vs 他のGPUツール」であって、「Ollamaそのものを使うジョブ」はそもそも調停の対象外&lt;/strong&gt;。信号機を外して直接実行したところ、3回の計測は問題なく完走した。&lt;/p&gt;

&lt;h2&gt;
  
  
  詰まった所②: 「8,000文字トリムで安全」という前提が7件で崩れた
&lt;/h2&gt;

&lt;p&gt;3回とも同じ7件が失敗するので、個別に再送信して切り分けた。まずUTF-8の&lt;code&gt;strict&lt;/code&gt;デコードを全件チェックしたが、エラーは0件——文字コードが原因ではない。&lt;/p&gt;

&lt;p&gt;次に失敗した7件だけ単独で&lt;code&gt;/api/embed&lt;/code&gt;に投げ直すと、次のエラーが返ってきた（3件で直接確認、残り4件も同一の失敗パターン）。&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="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"the input length exceeds the context length"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ollama show bge-m3&lt;/code&gt;で確認したモデルの実測スペックは次の通り。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;architecture        bert&lt;/span&gt;
&lt;span class="s"&gt;parameters          566.70M&lt;/span&gt;
&lt;span class="s"&gt;context length      &lt;/span&gt;&lt;span class="m"&gt;8192&lt;/span&gt;
&lt;span class="s"&gt;embedding length    &lt;/span&gt;&lt;span class="m"&gt;1024&lt;/span&gt;
&lt;span class="s"&gt;quantization        F16&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;コンテキスト長は8,192トークン。当初「8,000文字にトリムすれば8,192トークンには収まるだろう」という前提でコードを書いていたが、これは誤りだった。失敗した7件はコードブロックや表・記号を多く含む長めのMarkdownで、同じ8,000文字でもトークン化効率が悪く、8,192トークンを超えていた。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;文字数によるトリムは、トークン数上限の安全な代理指標にならない。&lt;/strong&gt;特に日本語やコードブロックが混在する文書では、文字数とトークン数の比率が文書ごとに変わる。実運用でこの罠を避けるには、文字数ではなくモデルの&lt;code&gt;context length&lt;/code&gt;を基準にした事前のトークンカウント（またはより保守的な文字数上限）が必要になる——今回はベンチの目的が「素朴な実装での実測」だったため、失敗した7件をエラーとして記録するに留め、リトライやチャンク分割の実装までは踏み込んでいない。&lt;/p&gt;

&lt;h2&gt;
  
  
  ローカルRAG vs 構造グラフ検索 — 1,325件規模での使い分け
&lt;/h2&gt;

&lt;p&gt;自社にはすでに、埋め込みを一切使わない検索基盤がある。AST由来の知識グラフ&lt;code&gt;graphify-out/graph.json&lt;/code&gt;だ。&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;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;graphify-out/graph.json&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nodes&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;  &lt;span class="c1"&gt;# 5934
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2026-08-03時点でノード数は5,934。これはコード/ドキュメントの構造的な関係（誰が誰を参照しているか）を機械的に抽出したグラフで、ベクトル類似度検索は行わない。過去のメモリには「20,755ノード」という記録も残っていたが、これはより古い時点のスナップショットであり、今回は自分でファイルを読み直して現在値を使った——数字は記憶ではなく実測を信じる、という原則をここでも適用した。&lt;/p&gt;

&lt;p&gt;bge-m3による埋め込みが向いているのは「意味が近い文書を探す」曖昧検索で、graphifyが向いているのは「このシンボルはどこから参照されているか」という構造的な検索だ。今回の実測で分かったのは、&lt;strong&gt;1,325件規模なら埋め込み生成自体は6分弱の低コスト作業&lt;/strong&gt;ということ。つまりローカルRAGの導入コストにおいて、埋め込み計算そのものはボトルネックになりにくい。ボトルネックになるとすれば、今回踏んだような「トークン上限の見積もりミス」や「調停ロジックの誤適用」といった、実装の詰めの甘さの方だった。&lt;/p&gt;

&lt;h2&gt;
  
  
  まとめ
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;自社Markdown 1,325件のbge-m3埋め込みは、1件ずつの逐次実行で&lt;strong&gt;約5分46秒（中央値346.16秒）&lt;/strong&gt;、docs/sec中央値3.808件/秒&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ollamaプロセスのピーク常駐メモリ(RSS)は3回とも7,839.3MBで一致（VRAM専用の直接計測はできず代理指標）&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;GPU調停スクリプトは「Ollama vs 他のGPUツール」の調停であり、&lt;strong&gt;Ollama自身を使うジョブに適用すると自滅する&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;「文字数でトリムすれば安全」は誤り。コンテキスト長はトークン単位（今回は8,192）であり、コードブロックや表を含む文書では文字数とトークン数の比率が崩れて超過しうる&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;1,325件規模なら埋め込み生成自体はボトルネックにならない。詰まるのは大抵、実装側の見積もりミスだった&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ollama</category>
      <category>rag</category>
      <category>applesilicon</category>
      <category>embedding</category>
    </item>
    <item>
      <title>【夏、再構築中。#2】「遊べない」町を再構築した — 地形整地から小物配置までの全工程と実測値</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Sun, 02 Aug 2026 11:45:07 +0000</pubDate>
      <link>https://dev.to/bigkijimon/xia-zai-gou-zhu-zhong-2-you-benai-ting-wozai-gou-zhu-sita-di-xing-zheng-di-karaxiao-wu-pei-zhi-madenoquan-gong-cheng-toshi-ce-zhi-4484</link>
      <guid>https://dev.to/bigkijimon/xia-zai-gou-zhu-zhong-2-you-benai-ting-wozai-gou-zhu-sita-di-xing-zheng-di-karaxiao-wu-pei-zhi-madenoquan-gong-cheng-toshi-ce-zhi-4484</guid>
      <description>&lt;h2&gt;
  
  
  はじめに
&lt;/h2&gt;

&lt;p&gt;「これで遊べると本気で思ってますか」。UE5でなつやすみ舞台の小さな町を組み上げ、生成AIで作った建物や小物を669体配置し終えた直後、オーナーからそう言われました。ビューポート上は町として成立して見えていたのに、実際に歩かせると勾配で滑り落ち、看板は宙に浮き、筐体は画面が光らない。&lt;strong&gt;「生成した」と「使える状態になっている」はまったく別&lt;/strong&gt;だという話です。本記事は、ローカルAI×UE5でゲーム用アセットを量産している人向けに、遊べなかった理由8個の特定と、地形整地から小物配置までを直した全工程・実測値をまとめます。&lt;/p&gt;

&lt;h2&gt;
  
  
  直接原因5個 — なぜ遊べなかったのか
&lt;/h2&gt;

&lt;p&gt;ビューポートのスクリーンショットだけでは気づけなかった不具合を、実際に歩かせて洗い出すと5つの直接原因が出てきました。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;地形を一度も整地していない&lt;/strong&gt;：商店街の建物28軒が標高差40m、勾配にして28%の山腹に建っていました。「歩けない」の唯一にして最大の原因です。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;回転処理が抜けていた&lt;/strong&gt;：ワールドを230m四方から141m四方へ縮小した際のスクリプトが、座標のスケールだけを扱い&lt;strong&gt;回転を一切扱っていない&lt;/strong&gt;（該当コードが0行）。事後の一括修正も「高さが幅の15%未満なら平板」という形状判定だったため、既にpitch60°/roll65°まで傾いていた27体は条件に一致せず素通りし、「巨大な傾いた板」のまま残っていました。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;テクスチャを取り込むコードが存在しない&lt;/strong&gt;：544枚のテクスチャが未使用のまま放置。手順書には「Content Drawerへドラッグ」と手作業前提で書かれていました。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;マテリアルが未接続&lt;/strong&gt;：マテリアル割り当てスクリプトが &lt;code&gt;AlbedoTex&lt;/code&gt; パラメータを宣言だけして代入せず、&lt;code&gt;UseTexture&lt;/code&gt; はStaticSwitchパラメータなのにScalarパラメータのセッターで叩いていました。しかも対象パスが旧レベルを向いたままで、既定値はDRY_RUN。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;小物を配置するコードが無い&lt;/strong&gt;：216種類の小物3Dアセットのうち、実際に町へ置かれていたのはわずか24個でした。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&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%2Fruu0dkhe3ossnk8evddo.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%2Fruu0dkhe3ossnk8evddo.jpg" alt="整地前：勾配のついた地面に建物が並ぶ商店街の目線ショット" width="800" height="588"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  整地前の商店街。地面の勾配が28%あり、目線を合わせると建物の傾きがそのまま見える。
&lt;/h2&gt;

&lt;p&gt;構造的原因3個 — なぜ見落としたのか&lt;/p&gt;

&lt;p&gt;個々のバグより深刻だったのは、なぜここまで気づけなかったかという運用側の問題でした。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;レベルを組み立てた処理のソースが1本も残っていない&lt;/strong&gt;：13項目の作業のうち、ファイルとして残っていたのはわずか2つ。残りは全部インラインでUE MCP経由に流して実行し、そのまま消えていました。つまり再実行も修正もできない状態で「完成」を迎えていたということです。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;「生成した」を「使った」と数えていた&lt;/strong&gt;：3Dアセット216体を生成し終えた時点で進捗を語っていましたが、取込・マテリアル接続・配置という後工程が一度もタスクとして立っていませんでした。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;合格判定を件数でやっていた&lt;/strong&gt;：ワールド縮小処理は「669体すべて移動・接地成功」と報告されており、数字自体は正しいものでした。ただしそれは接地判定が通ったという意味でしかなく、&lt;strong&gt;実際に歩けるかどうかを一度も見ていない&lt;/strong&gt;まま合格にしていました。当初計画に書いていた合格ラインは「2人でPIE起動し、街道を踏破できること」でしたが、その基準を自分で通していなかったのです。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  対策 — レベルを触る操作は必ずファイルとして残す
&lt;/h2&gt;

&lt;p&gt;原因6の「ソースが残っていない」を潰すため、以降の全操作をスクリプト化しました。以下の7本を順に実行する構成です。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ue_export_layout.py    # 現物のTransformをJSON台帳へ書き出す（774体）
ue_flatten_ground.py   # Landscapeを退避し、34m×36枚のCubeで平地を作る
ue_normalize_layout.py # 台帳上の回転をゼロへ正規化（UE操作なしで完結）
ue_apply_layout.py     # 正規化済み台帳を反映（冪等・2パス）
ue_import_textures.py  # テクスチャ84枚を一括取込
ue_wire_materials.py   # マテリアルv2マスター＋MI15種を正しく接続
ue_shot.py             # 検証用スクリーンショット
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;この手順を通した結果、実測値は次の通りです。&lt;/p&gt;

&lt;h2&gt;
  
  
  項目実測値
&lt;/h2&gt;

&lt;p&gt;傾いたまま残っていたアクター395体を除去&lt;br&gt;
平地へ接地し直したアクター762体&lt;br&gt;
新規配置した小物318体&lt;br&gt;
新規配置した看板95枚&lt;/p&gt;

&lt;p&gt;踏んだ罠5個 — MCP経由UE操作の落とし穴&lt;/p&gt;

&lt;p&gt;台帳化・一括修正の過程で、UE Model Context Protocol（UE MCP）経由の操作特有の罠を5つ踏みました。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;set_actor_transform&lt;/code&gt;は部分更新できない&lt;/strong&gt;：位置だけを渡して呼ぶと、省略した回転・スケールが初期値に巻き戻ります。これに気づかず762体へ位置だけ送った結果、対象が一斉にワールド原点へ collapse しました。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;PIE中はPIE複製ワールドを操作してしまう&lt;/strong&gt;：Play In Editor中にMCP経由で変更しても、それは実行用に複製された別ワールドへの変更で、エディタ本体のレベルには反映されません。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;delete_expression&lt;/code&gt;はUE5.8でクラッシュする&lt;/strong&gt;：マテリアルのノードを消す操作でエディタが落ちます。作り直す場合は既存ノードを消さず、新規マテリアル＋親差し替えで対応しました。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;クラッシュ後はCrashReportClientがMCPポートを握り続ける&lt;/strong&gt;：さらに再起動時に「Restore Packages」の確認モーダルが出て、自動起動フローがそこで止まります。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;サンドボックス実行では&lt;code&gt;import unreal&lt;/code&gt;が使えない&lt;/strong&gt;：MCP越しの1呼び出しは約0.4秒かかるため、台帳774体のような規模を捌くにはバッチ設計が前提になります。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  小物配置 — 実測なしでは倍率がバラバラ
&lt;/h2&gt;

&lt;p&gt;小物未配置（原因5）を解消するために、まず実物の寸法を測るところから始めました。216種類の3Dアセットについて、生成時の自然な出力サイズを1回ずつ計測し、台帳（&lt;code&gt;prop_catalog.json&lt;/code&gt;）にまとめます。そこから目標の実寸高さとの比を取ると、&lt;strong&gt;倍率の実測分布は5.1倍〜893.9倍&lt;/strong&gt;でした。取込んだメッシュのスケールは統一されておらず、一律の倍率を仮置きすればほぼ確実にどこかが破綻します。&lt;/p&gt;

&lt;p&gt;配置ロジックは次の3点です。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;建物位置を台帳から取得し、&lt;strong&gt;半径0.8〜3.2mの環に固定乱数（seed=19970815）&lt;/strong&gt;で小物を散らす。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;地区ごとに使える小物カテゴリを絞る（神社の敷地に自動販売機を出さない、など）。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;道の土のテクスチャは当初 &lt;code&gt;TexScale 24&lt;/code&gt; で貼っていたところ、ひび割れ1本が40cm級に見えるほど間延びしていたため、&lt;strong&gt;70まで上げて実寸化&lt;/strong&gt;。目安は「テクスチャ1枚が1.5〜2mに見える倍率」に置きました。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&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%2Fe1nv3yhom9z1jj62xewk.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%2Fe1nv3yhom9z1jj62xewk.jpg" alt="修正後：道路上から見た町並み。露出調整済みで小物と看板が配置されている" width="800" height="588"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  整地・小物配置・露出調整後の道路上ショット。看板95枚・小物318体が町に乗った状態。
&lt;/h2&gt;

&lt;p&gt;最終到達点 — 筐体が立ち、画面が光り、キャラが地面に立った&lt;/p&gt;

&lt;p&gt;仕上げの段階でも、原因が想像と違う箇所に隠れていました。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ゲームセンターの筐体が見えなかった真因は「Screenコンポーネントの StaticMesh が空」&lt;/strong&gt;だったことです。ブループリントのグラフを書き換えた際にメッシュ参照が失われていました。修正は、テンプレート側ではなく&lt;strong&gt;インスタンス側のコンポーネントに &lt;code&gt;set_properties&lt;/code&gt; を当てる&lt;/strong&gt;と反映されることを確認して対応しています。あわせて対象アクター（&lt;code&gt;Cabinet_A&lt;/code&gt;）の実測サイズが1.72cm（Blenderの1/100スケールのまま）だったため、実測から倍率96を算出して適用しました。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;地面はPlane（板）ではなくCube（箱）で作る&lt;/strong&gt;必要がありました。板のままだと、落下中のキャラクターがすり抜けてZ=-352cmまで落ち続けます。20cm厚の箱に置き換えて解決しました。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;PlayerStartが建物の中に置かれていた&lt;/strong&gt;のも落下の一因でした。上から下向きにトレースし、最初に当たる面がZ 0〜30cmの範囲にある場所を地面とみなして移動する処理で解消しています。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;地面判定のトレース処理では、&lt;strong&gt;戻り値がZ座標ではなく距離&lt;/strong&gt;である点も踏み違えました。「開始点のZ座標 − 戻り値」で高さを求める必要があり、戻り値そのものを高さとして扱うと数百cm単位でずれます。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;最後に、&lt;strong&gt;エディタ上のスプライト（Billboardアイコン）は当たり判定を持たない&lt;/strong&gt;ため、接地判定は &lt;code&gt;only_colliding_components=True&lt;/code&gt; を指定して測る必要がありました。そうしないと、実体のない箱が下に膨らんで宙に浮いたような判定結果になります。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  まとめ — 持ち帰れる学び
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;生成した≠使った&lt;/strong&gt;。取込・接続・配置がタスクとして立つまでは、生成数がいくら多くても進捗ではない。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;合格判定は件数ではなく実挙動で行う。今回でいえば「2人でPIE起動し、街道を踏破できるか」。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;レベルを触る操作は必ずファイルとして残す。インラインで流した操作は再実行も修正もできない。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;取込メッシュの倍率は実測なしでは決められない。今回の分布は5.1倍〜893.9倍という広さで、一律の仮値では必ずどこかが壊れる。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unrealengine</category>
      <category>ai</category>
      <category>applesilicon</category>
    </item>
    <item>
      <title>mlx-whisperで生成曲の歌詞一致率を数値化する品質採点（uvx arm64罠込み）</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Fri, 31 Jul 2026 23:48:15 +0000</pubDate>
      <link>https://dev.to/bigkijimon/mlx-whisperdesheng-cheng-qu-noge-ci-zhi-lu-woshu-zhi-hua-surupin-zhi-cai-dian-uvx-arm64min-ip-mi-772</link>
      <guid>https://dev.to/bigkijimon/mlx-whisperdesheng-cheng-qu-noge-ci-zhi-lu-woshu-zhi-hua-surupin-zhi-cai-dian-uvx-arm64min-ip-mi-772</guid>
      <description>&lt;p&gt;AI生成曲の品質をどう測るか。主観判定には限界がある——自分の曲を自分で採点しないルールが必要だ。そこで採用したのが、&lt;strong&gt;mlx-whisperローカル文字起こし→歌詞一致率&lt;/strong&gt;を客観指標にする3層採点パイプライン。M1 Max 64GBでACE-Step 1.5を使い、32テイクの実測値（0.0%〜100.0%）を得た。そしてuvx arm64罠にハマり、解決するまでの記録。&lt;/p&gt;

&lt;h2&gt;
  
  
  問題: AI生成曲の品質をどう測るか
&lt;/h2&gt;

&lt;p&gt;AI音楽生成の最大の課題は「品質の可視化」だ。生成した曲が教育用に使えるレベルか、歌詞が明瞭に聞こえるか、教育効果があるか——これらを数値化しないと「いい曲になった気がする」で終わる。人間の耳での判定は主観に偏る。自分が作った曲だから「悪くない」と思ってしまう。&lt;/p&gt;

&lt;p&gt;そこで必要なのは、&lt;strong&gt;客観指標&lt;/strong&gt;だ。発音明瞭度の代理として「歌詞一致率」を使う。文字起こしAIがどれくらい正確に歌詞を認識できたかを数値化する。これがあれば、再生成しても「前より良くなったか」が一目で分かる。&lt;/p&gt;

&lt;h2&gt;
  
  
  解法: 3層採点パイプライン
&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%2Fjchvx0o869its1ppgai8.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%2Fjchvx0o869its1ppgai8.png" alt="3層品質採点パイプラインのフロー図: Whisper一致率→ルーブリック採点→オーナー耳チェック→最終採用" width="799" height="222"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;採用したのは3層構造だ。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;① Whisper一致率（客観層）&lt;/strong&gt; — mlx-whisper（large-v3-turbo）で各トラックを文字起こしし、実使用歌詞と単語列一致率を計算する。数字は英単語に正規化して比較（"1,2,3"表記による偽低スコアを補正済み）。この層が本文の核だ。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;② 独立ルーブリック採点（別コンテキスト）&lt;/strong&gt; — 英語の正しさ25/設計原則遵守25/教育効果25/歌いやすさ25の4軸で、別コンテキストの採点官が各100点採点。平均94.1点。客観層が漏らす「教育的な意味でいい曲か」を補う。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;③ オーナー耳チェック（最終）&lt;/strong&gt; — 数字で表せない「歌い心地」「子どもに届くか」を最終判定。特に文字の歌唱（A-B-C）はWhisperが誤転写しやすく一致率が下振れるため、ここが補正になる。&lt;/p&gt;

&lt;h3&gt;
  
  
  実装①: mlx-whisperローカル文字起こし
&lt;/h3&gt;

&lt;p&gt;まずは文字起こしの実装だ。完璧にローカルで完結させたい。&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 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;&lt;strong&gt;uvx arm64罠にハマった&lt;/strong&gt;。このMacの既定python3はanaconda（x86/Rosetta）で、uvxが失敗する。Python実行環境のアーキテクチャ不一致だ。&lt;code&gt;--python cpython-3.12-macos-aarch64-none&lt;/code&gt;でarm64明示で解決した。罠は「何も言わずに失敗する」点だ。エラーメッセージも明確でないため、x86環境が選ばれていることに気づくのに時間がかかる。&lt;/p&gt;

&lt;p&gt;モデルはlarge-v3-turbo（Apache 2.0）をローカルに配置。完全ローカル・課金ゼロ・APIキー不要。16音源の転写には約2分（M1 Max 64GB）。&lt;/p&gt;

&lt;h3&gt;
  
  
  実装②: 独立ルーブリック採点
&lt;/h3&gt;

&lt;p&gt;客観層だけでは不十分だ。歌詞が完璧に聞こえても、教育効果が薄ければ意味がない。そこで独立ルーブリックを導入した。&lt;/p&gt;

&lt;p&gt;基準は4軸25点ずつ：英語の正しさ（文法・表現の自然さ）、設計原則遵守（LMNOP問題回避・音節数）、教育効果（子どもが覚えやすいか）、歌いやすさ（BPM・キー・リズム）。&lt;/p&gt;

&lt;p&gt;別コンテキストで採点することで、「作り手のバイアス」を排除する。平均94.1点は「教育的に優れた曲セット」であることを示している。ただし数字が高いから最終採用というわけではない。&lt;/p&gt;

&lt;h2&gt;
  
  
  実測結果: 32テイクの分布
&lt;/h2&gt;

&lt;p&gt;Daily Routineソングブック（全8曲×2バリアント＝16音源×2版v1/v2＝32テイク）の実測結果だ。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;採用8本の平均一致率: 83.9%&lt;/strong&gt;&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="n"&gt;python3&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vals=[75.3, 79.4, 97.1, 76.7, 94.4, 73.3, 75.2, 100.0]; print(sum(vals)/len(vals))&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="c1"&gt;# 出力: 83.925 → 83.9%
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;平均83.9%だが、分布は広い。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;最高100.0%&lt;/strong&gt; — 08_Introduce_Yourself v2a。完璧な聞き取り。Whisperが歌詞を完全に認識。穴あきBridge実装済みで教育的にも優秀。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;最低0.0%&lt;/strong&gt; — 07_Seasons v1b。不良テイク。Whisperが"Thank you"を幻聴し続ける状態。ボーカル不明瞭。これは「不良テイク・不採用」として即除外。&lt;/p&gt;

&lt;p&gt;分布の両端を引用することが重要だ。平均83.9%だけ見ると「実用的なレベル」に見えるが、0.0%〜100.0%のギャップが存在する。この分散がAI音楽生成の現実だ。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;v1/v2の裁定原則&lt;/strong&gt;。歌詞修正版（v2）を再生成しても、明瞭度が落ちる場合がある。&lt;/p&gt;

&lt;p&gt;例: 01_ABC_Song v1a=75.3%だが、v2a=53.2%に低下。ルーブリック採点で指摘された「TPR動作語ゼロ→clap追加」を反映したv2は、歌詞は良くなったが明瞭度が大幅減だ。seed運だから。clap追加の益が明瞭度低下を上回らない。&lt;/p&gt;

&lt;p&gt;結論: &lt;strong&gt;旧版を捨てず曲ごとに最良テイクを採用する&lt;/strong&gt;のが正解。歌詞修正（v2）とseed運は独立だ。v2で明瞭度が落ちる場合、v1を維持する。&lt;/p&gt;

&lt;p&gt;最終採用セットは8本（採用テイクを曲名のみで配置）。&lt;/p&gt;

&lt;h2&gt;
  
  
  曲採用一致率裁定理由
&lt;/h2&gt;

&lt;p&gt;01 ABCv1a75.3%v2は明瞭度大幅減。clap追加の益＜明瞭度&lt;br&gt;
 02 Numbersv2a79.4%歌詞修正済みかつ明瞭度も最高&lt;br&gt;
 03 Hellov1b97.1%全曲最高の明瞭度・97点&lt;br&gt;
 04 Weatherv2b76.7%明瞭度同等で不自然英語を解消&lt;br&gt;
 05 Days&amp;amp;Monthsv1b94.4%明瞭度94.4を優先&lt;br&gt;
 06 What Timev1a73.3%98点曲。"sweet"軽微＜明瞭度&lt;br&gt;
 07 Seasonsv2a75.2%歌詞4件修正済みかつ明瞭度も向上&lt;br&gt;
 08 Introducev2a100.0%一致率100%＝完璧な聞き取り・穴あきBridge実装済み&lt;/p&gt;

&lt;p&gt;教訓: 品質採点の3つのポイント&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;① 1リクエスト2バリアント生成＝seed運の保険&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ACE-Step 1.5は1リクエストで自動的に2曲生成する（seed違い＝A/B試聴に使える）。これが今回、2本の不良テイク（07_Seasons v1b=0.0%、01_ABC_Song v1b=16.7%）を無償で回避した。生成コストは変わらないが、品質の安定性が向上する。seed運に頼らない構造だ。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;② 旧版を捨てず曲ごとに最良テイクを採用する&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ABC曲の例が示す通り、v2で歌詞修正しても明瞭度が落ちる場合がある。「最新版が最良」という前提は危険だ。曲ごとにWhisper一致率×ルーブリック×耳チェックで最良を採用する。文字の歌唱（A-B-C）はWhisperが誤転写しやすく一致率が下振れるため、最終判定は耳だ。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;③ 文字の歌唱はWhisperが誤転写しやすい&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A-B-Cのような文字の歌唱は、Whisperが文字認識の誤りをする。一致率が低くても耳では聞き取れることがある。逆に、内容のある歌詞は一致率が高い傾向にある。客観指標を鵜呑みにせず、耳での最終判定が必要だ。&lt;/p&gt;

&lt;p&gt;品質採点は「品質を可視化し、再生成の意思決定を助ける」ためのものだ。数字に振り回されず、数字と耳の両方で判定する。これがAI音楽生成の実用レベルへの道だ。&lt;/p&gt;

</description>
      <category>localai</category>
      <category>musicgeneration</category>
      <category>qualityassurance</category>
      <category>mlx</category>
    </item>
    <item>
      <title>Ollama 0.32.0の「ローカルAIエージェント」を本番機で試せなかった話 — 共有GPUインフラのバージョンを上げない判断と、リリースノートと現物のドリフト</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Thu, 30 Jul 2026 11:45:03 +0000</pubDate>
      <link>https://dev.to/bigkijimon/ollama-0320norokaruaieziento-woben-fan-ji-deshi-senakatutahua-gong-you-gpuinhuranobaziyonwoshang-genaipan-duan-to-ririsunototoxian-wu-nodorihuto-37gb</link>
      <guid>https://dev.to/bigkijimon/ollama-0320norokaruaieziento-woben-fan-ji-deshi-senakatutahua-gong-you-gpuinhuranobaziyonwoshang-genaipan-duan-to-ririsunototoxian-wu-nodorihuto-37gb</guid>
      <description>&lt;p&gt;「&lt;code&gt;ollama&lt;/code&gt;にエージェント機能が入った」というリリースノートを見て、自分のMacで&lt;code&gt;ollama agent&lt;/code&gt;を叩いたら&lt;code&gt;unknown command "agent"&lt;/code&gt;で撃たれた——という人向けの記事です。結論から言うと、うちのM1 Max 64GB本番機は今も Ollama 0.30.8 のままで、&lt;code&gt;agent&lt;/code&gt; サブコマンドは存在しません。そしてそれは「更新し忘れた」のではなく、意図して上げていません。理由を実測とうちで実際に起きた事故から書きます。&lt;/p&gt;

&lt;h2&gt;
  
  
  本番機の実測 — バージョンは0.30.8、&lt;code&gt;agent&lt;/code&gt;は無い
&lt;/h2&gt;

&lt;p&gt;まず自分の手元で叩いた結果です。&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="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ollama &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="go"&gt;ollama version is 0.30.8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--help&lt;/code&gt;のコマンド一覧にも &lt;code&gt;agent&lt;/code&gt; は無く、&lt;code&gt;serve / create / show / run / stop / pull / push / list / ps / cp / rm / launch&lt;/code&gt; までしかありません。実際に叩くとこうなります。&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="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ollama agent
&lt;span class="go"&gt;Error: unknown command "agent" for "ollama"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;リリースノートで謳われている機能が、自分の手元のバイナリには物理的に存在しない状態です。&lt;/p&gt;

&lt;h2&gt;
  
  
  リリースノートは何を約束しているか
&lt;/h2&gt;

&lt;p&gt;Ollama公式のリリースノート本体はこの記事では開いていません（うちのブログ生成パイプラインは無人運転区間で外部Webを一切読まない設計にしています。理由は後述の「自律実行」の話と地続きです）。社内の別便が事前に検証・要約した一文だけを引用します。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;v0.32.0（2026-07-11）で、&lt;code&gt;ollama&lt;/code&gt;を実行するとエージェントが起動し、コードを書いたりWeb検索をしたり実作業を委任できるようになった。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;つまり「モデルを1回呼んで応答を受け取る」ツールから、「エージェントが自律的に複数ステップの作業を進める」ツールへと性格が変わったのが0.32系、という理解です。&lt;/p&gt;

&lt;h2&gt;
  
  
  Homebrewを見ても本番機のバージョンは分からない
&lt;/h2&gt;

&lt;p&gt;「じゃあHomebrewで確認すればいいのでは」と思って叩いたのがこれです。&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="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;brew info ollama
&lt;span class="gp"&gt;==&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ollama: stable 0.32.1 &lt;span class="o"&gt;(&lt;/span&gt;bottled&lt;span class="o"&gt;)&lt;/span&gt;, HEAD
&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;Not installed
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Homebrewのカタログ上のstable版はすでに0.32.1まで進んでいます。しかし「Not installed」——つまりこのMacはOllamaをHomebrewで入れていません。実体は&lt;code&gt;/Applications/Ollama.app&lt;/code&gt;（Electronベースのメニューバーアプリ）で、&lt;code&gt;/usr/local/bin/ollama&lt;/code&gt;はそのアプリ内バイナリへのシンボリックリンクです。&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="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;defaults &lt;span class="nb"&gt;read&lt;/span&gt; /Applications/Ollama.app/Contents/Info.plist CFBundleShortVersionString
&lt;span class="go"&gt;0.30.8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;アプリ本体のディレクトリ日時を見ると、最後に置き換わったのは2026-06-12。それ以降アップグレードしていません。「brewでバージョン確認」は、インストール経路が違うツールには効かないという、地味だが実際に踏む罠でした。&lt;/p&gt;

&lt;h2&gt;
  
  
  なぜ上げない判断をしたか（1）— 同じアプリの設定DBで7モデルが消えた前例
&lt;/h2&gt;

&lt;p&gt;この判断は「新機能が怖いから様子見」という抽象的な慎重論ではありません。うちには2026-07-14に、同じOllama.appのアップデート絡みの作業で実際に事故った記録があります。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ollama list&lt;/code&gt;が突然1モデルしか返さなくなり、ディスク上のmanifestファイルは無傷なのに他の6モデルが見えなくなりました。真因はOllama.appの設定DB（&lt;code&gt;~/Library/Application Support/Ollama/db.sqlite&lt;/code&gt;）の&lt;code&gt;settings.models&lt;/code&gt;カラムが、誤ってモデル階層の1サブフォルダを指すようになっていたことでした。厄介だったのは、この&lt;strong&gt;アプリ内設定が&lt;code&gt;launchctl setenv&lt;/code&gt;や起動時plistの&lt;code&gt;OLLAMA_MODELS&lt;/code&gt;環境変数より優先される&lt;/strong&gt;という点です。環境変数を直しても直らず、最終的に&lt;code&gt;db.sqlite&lt;/code&gt;を直接&lt;code&gt;UPDATE&lt;/code&gt;文で書き換えて復旧しました。&lt;/p&gt;

&lt;p&gt;つまり「バージョンやアプリの状態が変わると、設定の扱われ方そのものが変わって既存の環境を壊す」ケースが、このツール・このMacでは既に実物として起きています。0.32系は挙動が「応答を返すだけ」から「自律的に複数ステップ進める」へ変わる更新です。設定スキーマや内部状態の扱いが変わっていないという保証はどこにもありません。&lt;/p&gt;

&lt;h2&gt;
  
  
  なぜ上げない判断をしたか（2）— このMacは検証機ではなく共有本番機
&lt;/h2&gt;

&lt;p&gt;もう1つの実測です。今この記事を書いている最中も、同じMacでこれが動いています。&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="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ps aux | &lt;span class="nb"&gt;grep &lt;/span&gt;ollama
&lt;span class="go"&gt;yuma  93913 ... llama-server --model .../qwen3-coder-next ... --port 55433 -np 1 --flash-attn on ...
yuma  1088  ... ollama serve
yuma  1076  ... Ollama hidden
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;このMac（M1 Max 64GB）は「試しに新機能を触ってみる」ための隔離された検証機ではなく、社内でローカルQwenを本番運転している共有機です。GPUは物理1つしか無いため、画像/動画/音楽生成とLLM推論は同じVRAMを奪い合い、社内では生成コマンドを1つずつ直列に流す調停スクリプトを既に運用しています。長時間の動画・音楽生成中はOllama側が凍結され続けることも社内で把握済みです。&lt;/p&gt;

&lt;p&gt;ここに「エージェントが自律的にWeb検索やコード実行を何ステップも積む」機能が乗ると、外側の直列調停スクリプトからは「1回の生成コマンドがどれだけの時間・どれだけの内部ステップを勝手に消費するか」が見えなくなります。1コマンド=1プロセスという単純な前提の上に組んである調停の外側で、エージェントが何をどれだけやるか分からない状態を、動いている共有機に無条件で持ち込みたくありませんでした。&lt;/p&gt;

&lt;h2&gt;
  
  
  上げる/上げないをどう判断するか
&lt;/h2&gt;

&lt;p&gt;今回の判断から一般化できる、共有GPU機でツールをアップグレードするかどうかのチェックリストです。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;まず自分の手元で叩く。&lt;/strong&gt;リリースノートの主張とインストール経路（Homebrew/公式アプリ/手動ビルド）は別物。&lt;code&gt;--version&lt;/code&gt;と&lt;code&gt;--help&lt;/code&gt;を実際に叩いて現物を確認する。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;過去にそのツールで事故った記録があるか探す。&lt;/strong&gt;設定ファイル・内部DB・環境変数の優先順位が変わるアップデートは、既存の壊れ方を再演する可能性がある。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;その機体が検証機か本番機かを確認する。&lt;/strong&gt;&lt;code&gt;ps aux&lt;/code&gt;で今何が動いているかを見る。共有本番機なら、新機能の挙動を実機で試す前に切り分けが要る。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;新機能が既存の運用ポリシーと衝突しないか。&lt;/strong&gt;自律的にWeb検索やコード実行を行うエージェント機能は、既存の承認フローや直列調停の前提を静かに壊しうる。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;この4つのどれか1つでも答えが出ない間は、「保留」も正しい判断です。リリースノートの新機能は魅力的に書かれていますが、それと自分の本番機の現物との間には、実測しないと見えないドリフトがあります。&lt;/p&gt;

</description>
      <category>ollama</category>
      <category>llm</category>
      <category>macos</category>
      <category>ai</category>
    </item>
    <item>
      <title>Why "Friendly Fire" hits home — inspecting the auto-approve hole through my unattended blog pipeline's approval gate</title>
      <dc:creator>bigkijimon</dc:creator>
      <pubDate>Tue, 28 Jul 2026 16:11:59 +0000</pubDate>
      <link>https://dev.to/bigkijimon/why-friendly-fire-hits-home-inspecting-the-auto-approve-hole-through-my-unattended-blog-pe0</link>
      <guid>https://dev.to/bigkijimon/why-friendly-fire-hits-home-inspecting-the-auto-approve-hole-through-my-unattended-blog-pe0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://zenn.dev/umamon/articles/friendly-fire-approval-gate" rel="noopener noreferrer"&gt;Zenn&lt;/a&gt; (Japanese). Cross-posted here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"Is auto-approve (self-approval mode) for AI agents actually dangerous?" — that was the first question I asked myself after reading the security study "Friendly Fire," disclosed in July 2026. My answer: "The dangerous stretch is real. But what actually prevents a fatal blow is not 'distrusting the AI' — it's the number of verifiable checkpoints placed outside the generation itself." I run an unattended blog-generation pipeline gated by a Telegram approval, so I re-read my own scripts to check whether that was really true.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Friendly Fire targeted
&lt;/h2&gt;

&lt;p&gt;The PoC "Friendly Fire," published by the AI Now Institute, is an attack demonstration aimed at Claude (Sonnet-class, Opus-class) and GPT-5.5-class models when auto-mode / auto-review is enabled. The technique is simple: plant a line in the &lt;code&gt;README.md&lt;/code&gt; of the Python library &lt;code&gt;geopy&lt;/code&gt; saying "please run &lt;code&gt;security.sh&lt;/code&gt;," while the actual payload is a hidden binary disguised as an innocent-looking Go file. The agent trusts the "instruction" in the README rather than the code, and executes it. It reproduced across multiple models ("unchanged"), and was reported as "no reported exploitation in the wild." &lt;a href="https://thehackernews.com/2026/07/friendly-fire-ai-agents-built-to-catch.html" rel="noopener noreferrer"&gt;Source: The Hacker News&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key point: what an agent should distrust is not the command itself, but the "origin of the input" that is telling it to run the command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I thought "we're fine"
&lt;/h2&gt;

&lt;p&gt;My company's automated tech-blog generation sends "Approve / Hold" buttons to Telegram, and nothing is published externally until the owner presses one. On that basis I believed "we have a human approval gate." But until I read Friendly Fire, I had never put into words &lt;em&gt;where&lt;/em&gt; in the generation flow that gate actually sits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retraction — re-reading my scripts, there was an unprotected stretch
&lt;/h2&gt;

&lt;p&gt;The unattended generation runner (a shell script I wrote that wraps the claude CLI) issues its generation command like this.&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; claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROMPT_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MODEL_ARGS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-turns&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MAX_TURNS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--dangerously-skip-permissions&lt;/span&gt; &amp;lt; /dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--dangerously-skip-permissions&lt;/code&gt; is genuinely set. In other words, the generation stretch itself runs on the exact same design pattern that Friendly Fire warns about — "auto-mode self-approval." There are mechanisms that bound it, but none of them are human approval.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;MAX_TURNS&lt;/code&gt;: default is 20 (the upper bound on the agent loop). But the &lt;code&gt;tech&lt;/code&gt; / &lt;code&gt;hs&lt;/code&gt; runs have already been raised to 60 — a comment in the script still records how it crashed with &lt;code&gt;Error: Reached max turns (20)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TIMEOUT_MIN=45&lt;/code&gt;: on overrun, the whole process tree is force-killed. The last-resort insurance against hangs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mkdir&lt;/code&gt; lock: makes concurrent multi-launch physically impossible (as long as the lock directory already exists, no new run can start).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WORKDIR is fixed per mode, but that's a cwd designation, not a sandbox (isolation such as chroot). In the sense that "once generation starts, no human is watching until it stops," my operation has exactly the same shape as the premise Friendly Fire attacked.&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%2Fiic4ihl7sc2n5g09x1it.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%2Fiic4ihl7sc2n5g09x1it.png" alt="A timeline diagram of the 4 steps of the unattended blog-generation pipeline (generate -&gt; QA gate -&gt; Telegram approval -&gt; publish), showing whether human approval is present at each step. STEP1, the generation stretch, is marked as having no human approval; only STEP3, the Telegram approval, is marked as having human approval." width="800" height="320"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;There is no human approval in the generation stretch (STEP1). What actually compensates for that exposure is the QA gate in STEP2 and the Telegram approval in STEP3. Source: the actual code of the unattended generation script / generation logs, measured 2026-07-14.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The design I thought I had&lt;/strong&gt;: "There's an approval button right before publishing, so it's safe."&lt;br&gt;
&lt;strong&gt;The design I actually had&lt;/strong&gt;: "The generation stretch is unprotected. Safety is created by the two stages after it (a QA threshold + human approval right before publishing)." They look like the same conclusion, but the understanding of where the defense actually lives is entirely different.&lt;/p&gt;
&lt;h2&gt;
  
  
  So what is actually serving as the defense
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;I choose the origin of the input myself&lt;/strong&gt;: the source material for generation is a list of topics I curated myself, not a region that anyone can write to like Friendly Fire's README.md. "Contaminated input" as an attack surface structurally does not exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The generation stretch reads nothing external&lt;/strong&gt;: external research and fact-checking are separated into a different run (weekly; a human reads the results) that never enters the inside of the approval gate. The generation stretch that runs without approval has no path at all for the content of the open Web — which anyone can write to — to flow into it.
—— To be honest, &lt;strong&gt;I added this separation while writing this very article&lt;/strong&gt;. Before that, the design was "the generation stretch fetches, exactly once, the URLs written in the material list" — meaning &lt;em&gt;the very same shape of hole that Friendly Fire exploited was open in my own pipeline too&lt;/em&gt;. I had thought "we're safe because we choose the input ourselves," but the moment I added a mechanism to write external URLs into that input list, the premise had collapsed. Even a single fetch is still a hole.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QA gate + approval = a verifiable checkpoint after generation, before publishing&lt;/strong&gt;: as a concrete example, one generation log carries the record &lt;code&gt;QA未達(93&amp;lt;95)＝この記事は公開便に流さない（差し戻し対象）&lt;/code&gt; (QA below threshold (93&amp;lt;95) = do not send this article to the publish run; sent back for revision). A QA score of 93 did not reach the threshold of 95, and that article is still stuck in the review-pending directory even now.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All three are not about "whether to trust how the AI behaves" — they are about "gates that live outside the generation and can be confirmed in the logs."&lt;/p&gt;
&lt;h2&gt;
  
  
  The accident that actually happened that same week was far more mundane
&lt;/h2&gt;

&lt;p&gt;While I was worrying about a malicious attack like Friendly Fire, what was actually stopping the pipeline was something else. Counting the last 6 records in the generation log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ grep -oE "rc=[0-9]+" headless_tech.log | tail -6 | sort | uniq -c
   1 rc=0
   5 rc=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;5 out of 6 had failed. The error message was this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;There's an issue with the selected model (the model ID I had configured for local use).
It may not exist or you may not have access to it. Run --model to pick a different model.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cause was recorded in a comment in a config file. Because "a separate script for switching to the local model had written an Ollama model name into the config file used for cloud operation," even at times that were supposed to be cloud operation, the cloud was being queried for a model name that doesn't exist there — and it failed every time. Not a sophisticated attack of the kind the security PoC warns about, but a mundane tug-of-war in which another tool rewrote a config file — that is what caused silent failures over 3 days, 5 out of 6 times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;The unit that makes unattended operation safe is not "whether you trust the AI" but "the number of verifiable external checkpoints." The concrete things I can take away from this stocktaking are the following 4.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Leave rc (exit code) and the QA score in the generation log every time — you can't fix what you can't count.&lt;/li&gt;
&lt;li&gt;Place a threshold gate after generation, before publishing (here, QA 95 points).&lt;/li&gt;
&lt;li&gt;Consolidate human approval into a single spot right before publishing. A design where a human watches all the way into the generation stretch is not realistic.&lt;/li&gt;
&lt;li&gt;A mechanism to detect unintended rewrites of config files — this time there &lt;strong&gt;was none&lt;/strong&gt;. I'm leaving that here, as-is, as the next task.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let me state the retraction one more time. "Unattended = dangerous" is not it. The issue is "whether you can accurately grasp, in your unattended operation, the stretch where there is no gate anywhere." Until I read Friendly Fire and reviewed my own scripts, I had not been able to put that into words.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>automation</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
