2026年 01月 03日

『JavaScriptから現実世界に干渉する7の方法: ブラウザでハードウェアをコントロールする技術』という本を書いた

[JavaScriptから現実世界に干渉する7の方法: ブラウザでハードウェアをコントロールする技術]

JavaScriptから現実世界に干渉する7の方法: ブラウザでハードウェアをコントロールする技術という本を書いてみた。前々からこのテーマでまとめた一冊というのを作りたかったので勢いで書いた。サンプルコードもほぼ各章ごとに、そこそこすぐ試せる形で用意したりして工夫してみた。

JSから直接ハードウェアをコントロールするのが好きなのと (特に WebSerial/WebUSB)、もともと WebAudio が好きだったのもあるので、これらを包括的に「現実的に干渉する」というまとめにすることにしてみた。そうなると「ディスプレイ」(光)」も無視できないので、光→振動→音→(HID→MIDI)→Serial→USB とより低レベルになるような構成にしてみた。

直前にいろいろ一気に書いたけど、だんだんやりかたをアップデートした結果、これが一番図とサンプルコードに気合が入っている。

1月3日17時〜7日の17時までは無料に設定してみたので眺めてみてください。

2026年 01月 04日

日記システムPerlからgolangに書き換え

この日記システムを golang で書きなおした。元日から4日ぐらいかけた。なんかおかしかったら教えてください。

前々から日記システムをgolangにしたいなという気持ちを抱きつつxatena-goを書いたりしていた。しかし実際全体を移行するという気がなかなか起きなかった。というのも Hatena記法だけじゃなくてMarkdownだったり素のHTMLだったり、tDiary記法だったりをそのままDBに入れていたり、類似エントリ機能とか、類似画像機能とか、それらを実行するジョブキューとか、なんか細けえ最適化 (linkヘッダとか) が入ってたりとか……

最初のとっかかりが重要、ということで、既存のDBそのままで閲覧側だけまず作り (表示するだけだから特にフォーマッタとかは関係ない)、一旦安心しつつ書きこみ系を実装していった。大きな変更は主に Claude Code で計画させて割と自動で実装してもらい、Claude Code の Limit がきたら、細かいレビューしたり、小さい変更は Gemini CLI で補完した。

無駄に元のスキーマで DATE や DATETIME をつかっていたせいで (SQLite ではTEXTと一緒)、これらを go-sqlite3 が「いい感じ」に time.Time に変換するので、若干ややこしいクエリに書きかえたり、最小限のマイグレーション (created_at などを TZ 付きのフルのISO 8601に) をかけた。

サーバサイドで主につかったもの

  • sqlc backtick のエスケープに対応してない?のが罠なぐらい?
  • echo 特に気になるところない。HideBanner = true ぐらい

管理画面

自分しか使わない管理画面をリッチに作る気がさらさら起きなかったので、今までは1つの「編集」画面しかなかった。十分といえば十分だけど予約投稿機能が実はあるので、それを管理するのが面倒なのと、ジョブキューの見える化をしたかったので、新しく作った。

編集画面は、元々は Polymer による Web Components 実装だった。なので、最初は Lit で Web Components を書き直したけど、どうしても HTML in JS が許せないので、やっぱやめて使ったことない Svelte にしてみた。

Svelteも最初は Web Components モード (SvelteはSPAを念頭にしてるが、そういうモードがある) として使ってたけど最終的には管理画面全体で SPA として再構築した。

といってもほとんどこれらの「あっちこっち」の書き換えは Gemini CLI でやったので特にまだ Svelte の特徴を感じてはいない……

Gemini CLI はなんか Lit は Google が推してるから安心ですよみたいなこと言ってきたので「PolymerやAngularJSのこと考えると Google が推してるからといって安心ということはない」と言ったら Svelte がこの手の小さいやつだと最適とか本当に適当なことをいう。

PSS (Proportional Set Size) = 実質的な消費量

smem というツールを使うと実質的な消費量が簡単に見れるらしい。知らんかった。preforkモデルだと copy on writeを考慮した実質的なメモリ消費量が top や ps だと簡単にわからないので便利だ〜

smem -P "/srv/www/backend.psgi" -t -k
  PID User     Command                         Swap      USS      PSS      RSS 
2438110 cho45    /srv/www/backend.psgi (mast        0     6.0M    10.9M    50.0M 
2438111 cho45    /srv/www/backend.psgi (work        0     6.8M    10.9M    49.4M 
2438114 cho45    /srv/www/backend.psgi (work        0     7.5M    11.2M    49.1M 
2438205 cho45    /usr/bin/python3 /usr/bin/s        0    11.0M    11.3M    13.7M 
2438118 cho45    /srv/www/backend.psgi (work        0     7.8M    11.4M    49.4M 
2438119 cho45    /srv/www/backend.psgi (work        0     8.5M    12.2M    50.2M 
2438120 cho45    /srv/www/backend.psgi (work        0     8.7M    12.3M    49.6M 
2438117 cho45    /srv/www/backend.psgi (work        0     8.7M    12.3M    49.7M 
2438112 cho45    /srv/www/backend.psgi (work        0     8.9M    12.4M    49.7M 
2438113 cho45    /srv/www/backend.psgi (work        0     8.8M    12.6M    50.5M 
2438115 cho45    /srv/www/backend.psgi (work        0     9.3M    13.0M    51.1M 
2438116 cho45    /srv/www/backend.psgi (work        0    17.9M    21.6M    57.8M 
-------------------------------------------------------------------------------
   12 1                                           0   109.8M   152.0M   570.1M 

PSSの合計が重要で、この場合152MB使っている。

  PID User     Command                         Swap      USS      PSS      RSS 
2498750 cho45    /usr/bin/python3 /usr/bin/s        0     9.8M    10.1M    12.2M 
2498688 cho45    /srv/www/lowreal.net/Hanran        0    17.8M    17.9M    19.6M 
-------------------------------------------------------------------------------
    2 1                                           0    27.6M    28.0M    31.8M 

移行後はそもそも1プロセスにした。やたらメモリ使用量減ってくれた。

bye daemontools

日記のバックエンド系のコンポーネントだけ daemontools を使い続けていたけど、移行したことによって完全に無になった。

あんまりアクセスログとか見てなくて気付かなかったけど、GPTBot/1.3 が 5req/sec ぐらいでずっとアクセスしてきていて謎い。そんなにずっと辿るほどのエントリ数はないだろ……

2026年 01月 05日

システムあたらしくしたのでどんどん使っていくぞ〜

ポケモンZAはスタッフロールみるところまでやりました。

Chemr に mcp を実装してみて、はじめて自動的に役に立った。具体的には Gemini CLI が golang の MarshalXML でクソみたいな推測でモノを言うから検索しろと言ったらちゃんと最速で必要なドキュメントに辿りついた。「CDATA にはするには独自実装するしかない」と言い張っていた (そんなことはなく構造体タグにオプション書くだけ)。

というか別に Google Search がちゃんとしてればこんなMCPいらないはずなんだよな。誰だか知らねークソエントリをトップに出して公式サイトの順位を下げる(あるいは一切表示しない)というクソサーチエンジンが悪い。

LLM に我々(誰)が必要な図を描かせる場合、今のところは「matplotlib で描け」と指示するのが良さそう。特に↑のように2つのグラフの関係が正確であってほしいときは強力。

こういうのとかも: https://github.com/cho45/Hanrangon/blob/main/docs/frontend_arch_diagram.png

ただデフォルトのままよりは、ちゃんと Noto Sans JP を入れて使ってもらったほうが数段上に見える。

Mermaid で描かせてたけど、それほど表現力が高くないので最初からコードで書かせたほうがいい。まず人間が伝えるためのプロンプトとして Mermaid はいいかもだけど、自然言語でも伝わるしなあ

1月1日にインターネットが数度不通になってやきもきした。

Gemini CLI が急に無応答になったと思いきや、ネット全般に繋がらない。今までよくあった DS-Lite の NAT 枯渇のような症状ではなく、IPv6 も全滅。一応 VDSL モデムとかルーターの再起動をしてみたけど、関係なし。まぁずっと安定してるしLEDも正常だった。

ということで、これはまたかつての固定回線異常を彷彿とさせるなと新年早々に気分が重く。

問合せ方法とかを調べていたら自動的に復旧したので良かった、と思いきやまた数分切断とかとなって嫌な感じだった。結局 NTT 側の障害だった (結構この障害情報も遅れて表示されるので、当日の夜にようやく原因がわかった)

とりあえず2日〜4日は大丈夫だった。

Makefile で複数のプロセスを起動させたい(けど同時に終了してほしい)とき以下のようなイディオムがあるのを知った。foreman とかないと面倒だと思ってたわ

run-with-fe-dev:
        trap 'kill 0' EXIT; \
        (cd admin-frontend && npm run dev) & \
        HANRANGON_FE_DEV=true go run -tags "$(GO_TAGS)" .

Makefile というかシェルの機能だけど、EXIT をトラップして kill 0 (プロセスグループにシグナルを送る = 起動したシェル以下のプロセス全部が死ぬ)

node あるなら concurrently が一番楽なのかな? 名前が長いよな…… あとは高級なやつだと mprocsがある? ほかにもあるんだろうか

2026年 01月 06日

content-visibility とか content とかレンダリングまわりの最適化専用のCSSプロパティが増えていて知らなかった。

まぁなんかやっぱりサーバサイド再実装すると Cache-Control 忘れたりとかシンタックスハイライトの CSS が一部ロードされてないとか、微妙に気付きにくのがいろいろある……

年末年始2週間ぐらいずっとギターサボった。モチベあがらんな~ フェードアウトせず一応練習再開したのはえらい……

おうちでフラフラ持ち歩く用に使っている M1 MacBook Air も、買ったのが2023年11月と丸2年経過してしまった。ヘビーなことしないので、これで別に困ってはいなくて必要十分なんだけど、昨今の事情もあるしちょっと心配だなあ。

メインPCは2022年11月に更新したけど DDR4。DDR5 で組む機会を逃してしまった。今のところ別に困ってはいないので壊れたりしなければしばらくは……

メインスマフォの Pixel 7 は2023年8月に買っていて (たぶんセールだったんだと思う。62000円ぐらいだった)、バッテリーの持ちが悪くなってきた以外には不満がないので、買い替えるほどでもないんだよなあ。というか Pixel 10 も Pixel 9 もそもそも高いよ (256GB で14万)、Pixel 9a はカメラがちょっと心配だしなあ。Pixel 7 は下取りに出しても1万ぐらいにしかならないので、買い替えるとしても予備機として持ってたほうがよさそう。

Pixel 7 のサポート期限は 米国の Google ストアでのデバイス販売開始日から 5 年間らしい。Pixel 7 は2022年10 月発売なので、2027年10月までは大丈夫。もうすぐやんけという感じはする

2026年 01月 07日

CSS 2026

せっかくバックエンドシステムも変わったことだしと思ってCSSをアてなおした。何年ぶりかわからん (調べたら約10年一緒だった)

以下のように gemini-3-flash-preview に指示して、何度かやりなおしたら割といい感じだったので、さらに指示を加えたり手動で数字いじってなんとかした。

この日記を以下の条件で CSS を書きなおして新しいデザイン(見た目)を適用するとしたらどうしますか。やってみて
- 落ち着いたトーン 
- 「読みやすさ」最重視  
-「コード」は情報密度を高く
- 「写真」はできる限り大きく 
- ブロックの大きさに px 基本的に指定しないこと 
- フルにレスポンシブ (モバイルも同一ページです) 

中央揃えだとなんかモダンに感じる。

コードブロックと写真の幅を画面いっぱいにしたりするのは後からの指示。

HTML の変更はしないつもりだったけど、結局微調整した。

ヘッダのアイコン

ヘッダにアイコンも追加してみた。Nano Banana Pro で何度かイメージを伝えつつ気に入るのをいろいろ出させてブラッシュアップしたあと、Inkscape でトレースして、「パスの簡略化」で点の数を削減した。

なお gemini-3-pro-preview に同様にことをやらせたら、完全なるゴミができたため捨てました。遅いし

JS と WebComponents

CSS とは直接関係ないけどJSまわりも若干モダンにした。ものすごく古い書き方をしていたり、loading="lazy" がない時代の画像のプレースホルダーコードが残ったりしていた。なんか大抵のことはブラウザがやってくれる時代になっていた……

見出しの改行位置を調整するのを独自でやっていたけど、完全にやめて budou-xのWebComponents を導入した。

アーカイブあさり

  • 2014年1月 はこれはこれでよかったな
  • 2015年2月 は今も使ってるタイトルに近い。けどウェブフォントだったようだ
  • 2016年10月 は前のCSSに近い。ここからはほぼマイナーアプデしかしてないはず

バックエンドかえてからメタデータ(ogpとかもろもろ)がちゃんと入っておらず、はてブのタイトルとかがおかしくなっていた。なおしたつもり。

結構いろいろなおしまくっている

  • sitemap.xml / feed.xml は encoding/xml で出すように
    • html/template だと XML の PI が出せなかったり、そもそもXML用じゃないので問題がある。text/template は論外
  • 関連エントリを 2-gram かつより効率良い実装に書き換え。
    • エントリ数が1万弱なので、そんなに困ってはいないのだけどやりたくて

DB の DATE 型にしてあるカラムを TEXT 型にする

go-sqlite3 だと DATE を time.Time にしてくれちゃうが、それだと困るという。DBをそのままで持ってくる前提で、元々 CAST(date AS TEXT) as date とかで頑張っていたけど、これが sqlc と非常に相性が悪い。CAST するとそのクエリ専用の型が作られてしまう。そして共通の型に詰め替えるという手間 (コピー) が発生する。

sqlc の overrides とかでなんとかならないかと思ったが、go-sqlite3 の時点で time.Time になっちゃうのと、特にドライバオプションで切り替えることもできなそうなので、諦めて TEXT にすることにした。

ただ SQLite はカラムの型変更が ALTER TABLE でできない。知らなかった。

ということでめんどいことに……

BEGIN TRANSACTION;

-- 1. Create new table with TEXT type for date
CREATE TABLE entries_new (
    id INTEGER PRIMARY KEY,
    ...
);

-- 2. Copy data from old table
INSERT INTO entries_new (id, title, body, formatted_body, path, format, date, created_at, modified_at, publish_at, status)
SELECT id, title, body, formatted_body, path, format, CAST(date AS TEXT), created_at, modified_at, publish_at, status
FROM entries;

-- 3. Drop old table and rename new table
DROP TABLE entries;
ALTER TABLE entries_new RENAME TO entries;

-- 4. Re-create indexes
CREATE INDEX index_date ON entries (date, path);
...

COMMIT;
2026年 01月 08日

pprof 入れてバックエンドのプロファイルとって最適化した。プロファイラの解釈はLLMにやらせると眠くてもできて便利。しかしLLMに「推測するな計測せよ」「推測するなドキュメントを見よ」を徹底させることができない。すぐ「たぶんあれだ!」って検討違いに走り出して「ちがった〜」ってなってる。自分みたい。腹立つ。

html/template を便利に使いすぎるとやたらリフレクションがコールツリーに出てくるようになる。変なことするなら、渡すデータ構造変えたほうがいいというの一理あると思いつつ、そこまでじゃないんだよな、ということで複数回リフレクション読んでたところを、単に funcMap で一気にやる1個のリフレクションでいけるようにしたりとかした。

ここ数日夜遅くまでこのブログをいじっているので死ぬほど眠い

表側にも検索機能つけようか (管理画面には単純なLIKE検索がつけてある)と試行錯誤してみてたけどやっぱりあんまりよくない気がしている。Google が見捨てた(ひっかからない)ような古くて恥かしいやつが検索によって浮かびあがってくると辛い。元々表側に直接的な検索つけてない最大の理由だしなあ。

なんかいい方法があればなあ。といっても時間で足切りするぐらいしか思いついてないけど

I'm feeling lucky 的な検索しかできないとかだと良いかもしれない。

「あのとき見たあのエントリ検索したい」というぐらいの価値があるエントリならググればたぶんヒットするだろうし、独自にサイト内検索を実装するなら、本来の検索とは別の意味が必要なんじゃないか。

といっても最近は Google も個人サイトを超冷遇していて、site:lowreal.net してもひっかからないことが増えてしまったのだよなあ。だから検索は検索としてほしい。でも自分用の検索は別に管理画面で十分。寝ろ

ogp の画像が自分の写真で、Slack に流れきたのを見てゾっとしたので作りかえた

HTMLで1200x630指定して作って「ノードのスクリーンショットをキャプチャ」をするのが最速っぽい。

headless chrome でスクショとるのとか試してみたけど、なんか変な帯ができたりして意図したビューポートサイズにならないことに1時間ぐらいハマったので諦めた。

スクロールハイジャッキングがないことの安心感

モバイルでウェブサイト見る上での重要な要素になってしまったな。広告があるサイトだと、変なところをタッチしないための慎重なスクロールを要求される。

スクロールを邪魔してこないという信頼が価値になってしまったか

h2o ssl-offload: zerocopy

デフォルトだと fusion が有効にならなかったが明示的に指定したらいけた。

cmake -DWITH_BUNDLED_SSL=on  -DWITH_FUSION=ON -DCMAKE_C_FLAGS="-march=native" .

ゼロコピー系の設定をいれてみた。ただ効果測定できるほどの情報がない

proxy.zerocopy: ALWAYS  
ssl-offload: zerocopy   

AVIF

Lightroom Classic から AVIF が書けるようになってたことに気付いた。試してみる。画像サイズ取得するコードがたぶん動かんなこれ……

caniuse でも Baseline 2024らしい。

16Mピクセルで書き出したとき、この画像だとファイルサイズが JPEG 80% で 6MB ぐらい。AVIF だと 1.5MB ぐらい。

なんと、画像サイズちゃんととれてた。やったー

2026年 01月 09日

OGP画像の動的生成

統一OGP画像を作ったところで、そういえば最近のサービスはどこもOGP画像をダイナミック生成させていて、リッチでちょっと羨しいなと思っていたことに気付く。こんな日記には実装・運用コスト的に、見合う価値があるわけないのだけど、バックエンド置き換えの機会ということでやってみることにした。

使ってるのは golang 標準の image と golang.org/x/image/font/opentype

RGBAで処理しつつ、最終的に16色のパレットに割り当ててサイズを減らしている。一応ファイルキャッシュをしたいが、あまり容量の余裕のあるサーバではないのでできる限りのことをする。

以下のような感じで最適化した。パレット化の効果はすさまじい。

  1. 初期実装 (RGBA / BestCompression なし): 66KB
  2. BestCompression 適用後: 64KB
  3. 256色パレット化 (RGBA → Paletted): 25KB
  4. 128色パレット化: 23KB
  5. 64色パレット化: 21KB
  6. 32色パレット化: 19KB
  7. 16色パレット化: 15KB


あと速度的にも手元で 1600req/sec ぐらい (静的ファイル 590000req/secに対し) 出るようにまで調整してある (pprof の結果を Gemini CLI に投げつけると割とよくやってくれた)

ggはダメでした

gg で Noto Sans JP を読むと交差部分が正しく描画されなくてだめだった。Gemini CLI が「OGP作るならこれ!」っていうから、とりあえず採用してみたけどかなしい。あきらかにだいぶ古いライブラリっぽいしなあ。

結果的には images で必要十分かつパフォーマンスも満足なのでよかった。

タイトル長いエントリのとき

短くフェードアウトするようにしてみた。最初は「…」で省略してたけどイマイチだったのでこの形に

パフォーマンスチューニング

pprof でひたすら潰していった。html/template のリフレクションを丁寧に潰すのが地味に効く (funcMap 呼ぶのが本当に遅いみたい)

一通りやったので `hey -z 5s -c 20` (20並列 5秒) でそれぞれのエンドポイントのスループットを測った結果出してみた。マシン性能で数字が変わっちゃうので、静的ファイルに対しての係数も出してみた。

フィードは encoding/xml で、思ったよりすごく早くてびっくり。個別ページとトップページが html/template で、レンダリング後のキャッシュは入れてないので毎回出している。

   基準値: 静的ファイル配信 (/images/github.svg) = 0.3ms (コスト係数 1.0)
 
 
   ┌──────────────────────┬────────────────────────┬───────────────────┬───────────────────────┐
   │ ページ種別           │ スループット (req/sec) │ 平均処理時間 (ms) │ コスト係数 (静的=1.0) │
   ├──────────────────────┼────────────────────────┼───────────────────┼───────────────────────┤
   │ 静的ファイル         │ 59,240                 │ 0.3               │ 1.0                   │
   │ OGP (キャッシュあり) │ 48,695                 │ 0.4               │ 1.3                   │
   │ フィード             │ 7,143                  │ 2.8               │ 9.3                   │
   │ 個別ページ           │ 4,958                  │ 4.0               │ 13.3                  │
   │ トップページ         │ 2,305                  │ 8.7               │ 29.0                  │
   │ OGP (キャッシュなし) │ 1,644                  │ 12.1              │ 40.3                  │
   └──────────────────────┴────────────────────────┴───────────────────┴───────────────────────┘
 
 
   指標の解説
    * 平均処理時間 (ms): hey で計測された平均レイテンシです。
    * コスト係数: (各ページの平均処理時間) / (静的ファイルの平均処理時間) で算出。
        * 例: トップページ(8.7ms)は静的配信(0.3ms)の 29倍 の時間を要しており、そのマシンの性能によらず「静的ファイル29個分の重さがある」という実装の相対的な複雑さを示します。


実際はサーバのネットワークがボトルネックになる。

h3のサーバが不安定なのでGSOをオフにしてみる

VPS 上の h2o サーバの h3 接続が不安定に。手元から curl しても、https://http3check.net/ を使っても繋がったり繋がらなかったり不定。

結局 mtu いじったりなんやかんやいじっているうちになおってしまった。設定は結局元のままだしわけわからん。

GSO が怪しいのでオフにしてみることに。Gemini と壁打ちしたところ、最終的に矛盾がないのは「virtio_net ドライバの TXリングバッファ(送信キュー)がスタックしていた」説らしいですが。

$ sudo ethtool -k ens3 | grep -i segmentation  
tcp-segmentation-offload: off
        tx-tcp-segmentation: off
        tx-tcp-ecn-segmentation: off
        tx-tcp-mangleid-segmentation: off
        tx-tcp6-segmentation: off
generic-segmentation-offload: on
tx-fcoe-segmentation: off [fixed]
tx-gre-segmentation: off [fixed]
tx-gre-csum-segmentation: off [fixed]
tx-ipxip4-segmentation: off [fixed]
tx-ipxip6-segmentation: off [fixed]
tx-udp_tnl-segmentation: off [fixed]
tx-udp_tnl-csum-segmentation: off [fixed]
tx-sctp-segmentation: off [fixed]
tx-esp-segmentation: off [fixed]
tx-udp-segmentation: off [fixed]

$ sudo ethtool -K ens3 gso off

$ sudo ethtool -k ens3 | grep -i segmentation  
tcp-segmentation-offload: off
        tx-tcp-segmentation: off
        tx-tcp-ecn-segmentation: off
        tx-tcp-mangleid-segmentation: off
        tx-tcp6-segmentation: off
generic-segmentation-offload: off
tx-fcoe-segmentation: off [fixed]
tx-gre-segmentation: off [fixed]
tx-gre-csum-segmentation: off [fixed]
tx-ipxip4-segmentation: off [fixed]
tx-ipxip6-segmentation: off [fixed]
tx-udp_tnl-segmentation: off [fixed]
tx-udp_tnl-csum-segmentation: off [fixed]
tx-sctp-segmentation: off [fixed]
tx-esp-segmentation: off [fixed]
tx-udp-segmentation: off [fixed]

Chrome の is_broken=true

Chrome は一回 h3 がうまくいかなくなるとブラックリスト的なものに入れるようで、しばらく h2 でしかアクセスしてくれなくなる。クリア方法がわからん。

chrome://net-export/ して HTTP_STREAM_JOB_CONTROLLER で検索すると is_broken=true が見える。

24497: HTTP_STREAM_JOB_CONTROLLER
https://lowreal.net/
Start Time: 2026-01-09 08:50:46.370

t=83062 [st= 0] +HTTP_STREAM_JOB_CONTROLLER  [dt=50]
                 --> allowed_bad_certs = []
                 --> is_preconnect = true
                 --> privacy_mode = "disabled"
                 --> url = "https://lowreal.net/"
t=83062 [st= 0]   +PROXY_RESOLUTION_SERVICE  [dt=0]
t=83062 [st= 0]      PROXY_RESOLUTION_SERVICE_RESOLVED_PROXY_LIST
                     --> proxy_info = "DIRECT"
t=83062 [st= 0]   -PROXY_RESOLUTION_SERVICE
t=83062 [st= 0]    HTTP_STREAM_JOB_CONTROLLER_PROXY_SERVER_RESOLVED
                   --> proxy_chain = "[direct://]"
t=83062 [st= 0]    HTTP_STREAM_JOB_CONTROLLER_ALT_SVC_FOUND
                   --> alt_svc = "quic lowreal.net:443, expires 2026-01-09 09:00:11"
                   --> is_broken = true
t=83112 [st=50] -HTTP_STREAM_JOB_CONTROLLER

GUI からの操作だとどうあがいてもクリアできないっぽい。待つか、Profile フォルダの "Network Persistent State" の JSON を削除するしかないっぽい。

$  pwd
/Users/cho45/Library/Application Support/Google/Chrome/Default

$ cat "Network Persistent State" | jq '.net.http_server_properties.broken_alternative_services[] | select(.host == "lowreal.net")'
{
  "anonymization": [
    "...",
    false,
    0
  ],
  "broken_count": 3,
  "host": "lowreal.net",
  "port": 443,
  "protocol_str": "quic"
}
{
  "anonymization": [
    "...",
    false,
    0
  ],
  "broken_count": 8,
  "broken_until": "1767933207",
  "host": "lowreal.net",
  "port": 443,
  "protocol_str": "quic"
}

一応バックアップとってからクリア。

$ cp "Network Persistent State" "Network Persistent State.bak"
$ cat "Network Persistent State.bak" | jq '.net.http_server_properties.broken_alternative_services |= map(select(.host != "lowreal.net"))' > "Network Persistent State"

エントリの要約 (OGPとかメタデータにつかう) summary と、最初の画像 image を DB のカラムとして入れることにした。今まで毎回 HTML パーサーでパースさせていたので割と無駄だった。

でさらに summary を LLM で生成できたらいいなあという目論見で、Open AI 準拠のAPI (さくらのAI Engine) でいくつかモデルやプロンプトを試してみたけど、いまいち質が安定せず、一旦お蔵入り。

Web Share API

Web Share API というのがあるんだなあ、ということで付けてみた。ファイルもこれで共有できるみたい。知らなかった。

Firefox は未対応。あとmacOSの共有はあんまり意味ないので、SNSの共有ボタンをまるごと代替できるものではないようだ。

customElements.define('web-share-button', class extends HTMLElement {
	connectedCallback() {
		const title = this.getAttribute('title');
		const url = this.getAttribute('url');

		if (!navigator.share) {
			this.style.display = 'none';
			return;
		}

		// Web Share API 対応: ボタンを表示
		const button = document.createElement('button');
		button.className = 'share-button';
		button.textContent = '共有';
		button.addEventListener('click', async () => {
			try {
				await navigator.share({ title, url });
			} catch (err) {
				if (err.name !== 'AbortError') {
					console.error('共有エラー:', err);
				}
			}
		});
		this.appendChild(button);
	}
});


https://github.com/cho45/ticker-generator でも対応させてみた。

2026年 01月 10日

配信画像どうするか悩んでる

既存のをまずは最適化した。トータルで JPEG 以外も含めて 5.9GB ある。

$ jpegoptim -p --all-progressive --total *.jpg
...
Average compression (2467 files): 4.79% (292586k)
jpegoptim -p --all-progressive --total *.jpg  980.72s user 45.35s system 98% cpu 17:24.23 total

約 292MB削減。ロスレス最適化だけで結構減ったなあ。

全体を AVIF 化してしまうのをやったほうがいい気もする。

まず新規アップロードについて JPEG でアップロードしたら自動で AVIF に変換かけることにした。ただ、これが非常に重かったのでいろいろ妥協した以下のようにした。

avifenc --jobs 3 --speed 8 --yuv 420 -q 60

Lightroom からは AVIF で出すので、写真を JPEG でアップロードするのはあんまりこだわらないときに限られるのでいいだろう……

あと PNG に関しても oxipng か optipng が入っていたら自動で通すようにした。スクショとか上げることが多いのでこれは効くはず

全JPEGのAVIF化で問題になるのは、あとはOGP画像。OGP画像は今まではエントリの最初の img の src をそのままブチこむということをしてたけど、これができなくなる。ウーン。まあ諦めて普通のOGP画像を出せばいいか。大して共有されるわけではないから

Ubuntu 20.04 に avifenc を入れる

README.md に書いてある通りだけど、ext/ 以下で libaom をビルドしてそれを static link するのが良いようだ

git clone -b v1.3.0 --depth 1 https://github.com/AOMediaCodec/libavif.git
cd libavif
cd ext
sh aom.cmd
cd ..
cmake -DAVIF_BUILD_APPS=ON -DBUILD_SHARED_LIBS=OFF -DAVIF_CODEC_AOM=LOCAL -DAVIF_LIBYUV=LOCAL . 

v1.3.0 より新しいと要求する cmake があたらしすぎてダメだった

Ubuntu 20 から 24 に do-release-upgrade

LLM にリリースノートと ps aux の結果とかを食わせながら注意点を適当にあらいだしてやった。こういうときペアでやってる感じがあってだいぶ気が楽だ。

そこそこすんなりいったのでよかった。

h2o

openssl のアップグレードがあるので h2o を一時的に static link に。前回アップグレード時もビルドしなおしになってハマった記憶

h2o 用の openssl を配置しとく

 wget https://www.openssl.org/source/openssl-3.0.12.tar.gz
 tar xzvf openssl-3.0.12.tar.gz 
 cd openssl*
./config --prefix=/opt/h2o-ssl no-shared
make
sudo make install

ビルドしなおし

$ git clean -fdx
$ export PKG_CONFIG_PATH=/opt/h2o-ssl/lib64/pkgconfig  
$ cmake -DOPENSSL_ROOT_DIR=/opt/h2o-ssl -DOPENSSL_USE_STATIC_LIBS=TRUE  -DWITH_FUSION=ON -DCMAKE_C_FLAGS="-march=native" .
$ make
$ make install

$ ./h2o -v
h2o version 2.3.0-DEV@afe696c72
OpenSSL: OpenSSL 3.0.12 24 Oct 2023
mruby: YES
capabilities: YES
fusion: YES
ssl-zerocopy: YES
ktls: YES
key-exchanges: secp521r1, secp384r1, x25519, secp256r1

$ ldd ./h2o
        linux-vdso.so.1 (0x00007ffef3ffb000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f411d0e2000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f411d0dc000)
        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f411cf8d000)
        libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f411cf71000)
        libcap.so.2 => /lib/x86_64-linux-gnu/libcap.so.2 (0x00007f411cf68000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f411cd76000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f411d951000)

バックアップ

念のため

sudo tar czvf /data/vps_config_backup_20260110_v2.tar.gz \
  /home \         
  /etc \
  /usr/local \
  /var/lib \
  /var/spool/cron \
  /root \
  /opt \
  /srv \
  /var/www \
  --exclude=/var/lib/docker \
  --exclude="/home/*/tmp" \
  --exclude="/home/*/swp"

sudo do-release-upgrade

20 → 22 → 24 とあげていった。普通に起動してくれてよかった

rabbitmq

mqtt server としてしか使ってない割に do-release-upgrade のたびに障壁になってたので、使うのをやめた。

mochi-mqtt/server + すこしの実装で、自分の要求には完全な代替を Gemini が書いてくれたので、それをsystemdでデーモン化した。

mackerel-agent

インストールスクリプト流しなおした

certbot

動かなくなっていたので snap 版をつかうようにした。

 sudo apt install snapd
 sudo snap install core; sudo snap refresh core
 sudo snap install --classic certbot
 sudo ln -s /snap/bin/certbot /usr/bin/certbot

systemd と Unix domain socket

2021年にh2oをsystemd管理にしてから、UNIXドメインソケットでの接続 (unix:/tmp/backend とかにしてた) がうまくいかず、ずっと TCP を無駄に通してた。

これがいまさらながら解決した。つまり /tmp は systemd が PrivateTmp で隔離しているので共有できないというだけだった。

なので以下のように RuntimeDirectory の設定を入れる。

RuntimeDirectory=hanrangon
RuntimeDirectoryMode=0755

これで /run/hanrangon が systemd によって作られるので、ここの下に /run/hanrangon/socket として UNIXドメインソケットを作るといいらしい。

フロント(h2o)からは http://[unix:/run/hanrangon/socket] を指定する。

2026年 01月 11日

matplotlibのフォントキャッシュ削除

以下をコピーして pbpaste | python する

import matplotlib
import shutil
import os

# キャッシュディレクトリの場所を取得
cache_dir = matplotlib.get_cachedir()
print(f"キャッシュディレクトリ: {cache_dir}")

# キャッシュディレクトリ内の fontlist 関連ファイルを削除
for file in os.listdir(cache_dir):
    if file.startswith("fontlist") and file.endswith(".json"):
        path = os.path.join(cache_dir, file)
        print(f"削除中: {path}")
        os.remove(path)

print("削除完了。Jupyter Notebookやスクリプトを再起動してください。")

まちがえました。この日記には削除機能がないんです

管理画面のヘッダをローカルと色変えるようにしました

<!DOCTYPE html>

類似画像検索をOKLCH色空間で再実装

類似画像検索まわりをだいぶいじった。Perl の実装では Libpuzzle を適当に使ったやつだったが、独自実装に変えてみた。↑ の画像は管理画面

3次元のヒストグラム

「類似」をどうするかをこの日記においては色の傾向が似ている (形は無視する) としてみる。意図としては「雰囲気が似ている」画像を出すというのを目標にするもの。

つまりヒストグラムの比較で類似性を判断する。

人間の色認知は3次元の色空間で表現されるので、この3次元空間をそれぞれの次元で均等に区切り (バケツに比喩される)、各ピクセルをそれぞれ、その空間(バケツ)に放りこむ。

使う色空間を OKLCH に

RGB の3次元でもヒストグラムは作れるけど、距離が知覚と一定ではない問題があるので、ビットごとに知覚的な空間との間に乖離がある。ちょうどいい色空間として、人間の知覚的に均等な距離を持つ OKLCH 空間があるのでこれを使ってみることにした。

こうすることで知覚とバケツのサイズを一致させ、1ビットの価値を均等にできる。

ヒストグラムのエンコーディング (64bitのビットマスク化)

集計したヒストグラムをそのまま保存するとデータ量が大きいため、各バケツのピクセル数(頻度)を保存するのではなく、「その色が画像の中に一定以上(例えば面積の3%以上)存在するかどうか」という 0 か 1 かのフラグに変換して保存する。

これを 64個のバケツすべてで行うと、画像は 64ビットの1つの整数(シグネチャ) として表現できる。これが画像の「色の指紋」となる。

OKLCH を使うと書いたけど、L(明るさ)、C(彩度)、H(色相)を、それぞれビット位置として L:2bit (4段階) C:1bit (2段階) H:3bit (8方向) と配分した。Lab ではなく LCH を使うことで H に多くのビットを割くということができる。

これでバケツごとに1bit(有無)に情報圧縮される。バケツは「こんな感じの色」という単位なので、「こんな感じの色が含まれている」という集合の状態になる。同時にこれは画像全体のサイズによって正規化されている。

類似度の計算(Jaccard 係数)

最終的に画像同士のシグネチャ、実体としては色の集合のビットマスクを比べる際は Jaccard(ジャカード)係数 を使う。これは「共通して持っている色の数」を「両方の画像のどちらかに存在する色の総数」で割ったものであり、パレット(色の品揃え)がどれだけ重なっているかとして評価できる。

ただこれを全画像に毎回やるのは大変なので SQL で検索可能にするための工夫をする。

検索の高速化 (ngram)

似ているビットマスクを検索する手段として、ビットをいくつかに分割してngramにして保存しておくという方法が使われる。別に任意のビットマスクでよく使わる方法で特別な方法ではない。

ここでは1ビットごとにずらした12bitをそのパターンのオフセット位置を共に、52個のワードとして保存する。つまり1つの画像は64bitのシグネチャにされたあと、この64bitをスライディングウィンドウで12bitずつ切り出して保存し、これにインデックスを貼る。

そして類似画像の検索時は、このインデックスを利用して、部分マッチするものを先に足切りしてとってくる。SQL レベルで「同じ明るさ・色相の場所に、同じ色の塊を持っている画像」をピンポイントで引き抜くことができる。

空間充填曲線(Z-order)による ngram の高密度化

さらに、この検索用の12ビットの窓にも、より多くの「関連する色」を詰め込むため、Z-order (Morton order) を採用してビットのアドレスを決定する。

これにより、1次元のビット列上でも3次元的な色の近接性が保たれ、1つの ngram が「色空間上の意味のある局所的なボリューム」を指紋として表現できるようになる

具体的には図の上ように、単にLCHをそれぞれ L=2bit, C=1bit, H=3bit としてビットを構築すしたままだと、ところどころ色が離れた場所にジャンプしてしまう。これを下のようにビットインターリーブ(z-order化)することで、12bitの情報を均等にできる。

2026年 01月 12日

golang で AVIF 対応 image.Decode

標準ないし準標準 (golang.org/x/image/webp みたいな) にはないので面倒っぽそうだなあとなんとなく思ってたけど意外にも簡単に対応できた。

CGo を許容するかで今のところ2択っぽい

  1. _ "github.com/vegidio/avif-go" CGo
  2. _ "github.com/gen2brain/avif" libavif を WASM にコンパイルし、wazero で実行

どっちもブランクインポートするだけでつかえる。ベンチ的には CGo のほうが10倍早い

こういう検証とベンチをむちゃくちゃサクっとやれるのすごいいいよなあ。Agentic Coding。脳の負荷が位置が変わっているのを感じる。

検証

vegidio/avif-go

# register_vegidio_test.go
package main

import (
	"bytes"
	"image"
	"os"
	"testing"

	_ "github.com/vegidio/avif-go"
)

func TestRegisterVegidio(t *testing.T) {
	data, err := os.ReadFile("../../static/fixtures/sample.avif")
	if err != nil {
		t.Fatal(err)
	}

	img, format, err := image.Decode(bytes.NewReader(data))
	if err != nil {
		t.Fatalf("Vegidio registration failed: %v", err)
	}

	t.Logf("Successfully decoded %dx%d %s image using vegidio", img.Bounds().Dx(), img.Bounds().Dy(), format)
	if format != "avif" {
		t.Errorf("Expected format avif, got %s", format)
	}
}
go test -v register_vegidio_test.go   
=== RUN   TestRegisterVegidio
    register_vegidio_test.go:23: Successfully decoded 2886x2164 avif image using vegidio
--- PASS: TestRegisterVegidio (0.08s)
PASS
ok      command-line-arguments  0.313s

github.com/gen2brain/avif

# register_gen2brain_test.go
package main

import (
	"bytes"
	"image"
	"os"
	"testing"

	_ "github.com/gen2brain/avif"
)

func TestRegisterGen2brain(t *testing.T) {
	data, err := os.ReadFile("../../static/fixtures/sample.avif")
	if err != nil {
		t.Fatal(err)
	}

	img, format, err := image.Decode(bytes.NewReader(data))
	if err != nil {
		t.Fatalf("Gen2brain registration failed: %v", err)
	}

	t.Logf("Successfully decoded %dx%d %s image using gen2brain", img.Bounds().Dx(), img.Bounds().Dy(), format)
	if format != "avif" {
		t.Errorf("Expected format avif, got %s", format)
	}
}
go test -v register_gen2brain_test.go 
=== RUN   TestRegisterGen2brain
    register_gen2brain_test.go:23: Successfully decoded 2886x2164 avif image using gen2brain
--- PASS: TestRegisterGen2brain (0.73s)
PASS
ok      command-line-arguments  1.119s

Cloudflare R2を使ってみる

容量的には 10GB までは無料で、そのあと従量課金になってもそれほど高額ではなさそう。

ただクラスB操作(参照系)がコントロールしにくいのでちょっと怖い。CDN経由のキャッシュミスだけが問題なのでファイル数が十分に少なければ恐るることはなさそう。現状ではこのサイトの全GETリクエスト(ほとんどクローラーだけど)がR2に飛んだとしても無料枠に収まる。

まず新規アップロードを R2 にするようにしてある。様子を見つつ過去分もアップロードするつもりではあるけど、その前に JPEG ファイルを全ファイル AVIF にするということをしたいのでまだやれてない。

もともと Cloudflare Registrar 契約してるので使いはじめに抵抗がない感じ。

CDNエッジでジオシティーズ的なやつを作ってみる

半年以上前に Cloudflare Workers上で Honoフレームワーク使いつつ懐しい構成のサイトを作ってみたやつ、日記に書いてなかった。

中身がシーディーエンヌのエッジのワーカーで動いてるけど見てくれがジオシティーズだったら面白いなと思って、当時は Github Copilot 使いながら作った記憶。

いわゆる6hotサイトみたいのは現状の Worker の無料枠で全然問題なかろうという気がするし、そういう意味では現代のジオシティーズなんじゃと思ったのだった。

Durable Objects まわり仕様が変化していくのでエーアイに書かせて困った思い出がある。作りっぱなし

AVIFすごすぎるな

過去のをちまちま変換してるけど、1/10 になってかつ見た目がまったく変わらないのも多い。JPEGはやめよう!

リンク切れした画像の一部を復活させた

過去画像の AVIF 化にともなって、Picasa → Google Photos → セルフホストと画像移行する過程で、うまくファイル名一致で移行できずにリンク切れになっていたファイルを、Lightroom Classic から再現像することである程度は復元した。

エントリの中にはあるがファイルシステムにないものを抽出して、

2026/01/12 17:38:39 AVIF変換を検証中...
2026/01/12 17:38:39 [欠落] エントリID:19390 (2012/04/20/1) 画像: IMG_0191-2048.jpg (AVIF:なし)
2026/01/12 17:38:39 [欠落] エントリID:19392 (2012/04/21/1) 画像: IMG_0123-2048.jpg (AVIF:なし)
2026/01/12 17:38:39 [欠落] エントリID:19393 (2012/04/22/1) 画像: IMG_0170-2048.jpg (AVIF:なし)
...
|<

この情報をGeminiに投げつけながらワンライナーでファイル名の一部を抽出してもらい、Lightroom の検索窓に投げんだ。あきらかに無関係なファイル以外をクイックコレクションに追加してすべて書き出た。

ただ、カメラのファイル名はあんまり一意ではない(買い替えのタイミングとかでリセットされている)。なので exiftool で撮影日時を取得し、↑ のログの日付と、因果律が崩れない範囲でファイル名をマッチングさせるコードを書いてもらった (なぜか Ruby で書いてくれた)

そのうえで何度か漏れたファイルを出力した。1件だけどうしても見つからないファイルがあったけどもういいかなという感じ。

過去画像を R2 に移行した

AVIF変換おわらせたあとガッと移行させた。

これで写真の表示が早くなるといいな。なんだかんだ Google Photos から移行してきてからは表示の遅さゆえに写真アップロードするモチベが落ちていたなと思う。

事前処理

もともとのファイル郡と拡張子ごとのサイズ

$ find . -type f -printf "%s %f\n" | awk '{ext=$NF; if(ext ~ /\./) {sub(/.*\./,"",ext); ext=tolower(ext)} else {ext="no_ext"}; sum[ext]+=$1} END {for(e in sum) print sum[e], e}' | sort -nr | numfmt --to=iec --field=1
5.5G jpg
298M png
60M webp
56M gif
1.5M avif
1.1M jpeg
219K ds_store

oxipng と avif への変換後

$ find . -type f -printf "%s %f\n" | awk '{ext=$NF; if(ext ~ /\./) {sub(/.*\./,"",ext); ext=tolower(ext)} else {ext="no_ext"}; sum[ext]+=$1} END {for(e in sum) print sum[e], e}' | sort -nr | numfmt --to=iec --field=1
3.3G avif
450M jpg
240M png
60M webp
56M gif

この450MBの jpg については参照されてないものっぽいのでGCすることに。

2026年 01月 13日


2011年1月2日撮影 / この猫は去年この世を去りました

Claude Code Pro の Weekly Limit が普通にキツくて、あんまり使えてない。Claude は計画の部分とかツールの使いかたとかがどんどん良くなってて、計画だけ決まればあとは自動運転でも割と良いことが多い (おかしいことしてるときもあるけど)。

そしてここしばらくは Google AI Pro を契約して gemini-3-flash-preview を Gemini CLI でかなり使っている。基本的に gemini-3-flash-preview ならほぼずっと使い続けられる。gemini-3-pro-preview は遅いしリミットくるし、それほど頭よく感じないしでほぼ使ってない。

Gemini CLI と Claude Code はツールとしての完成度がぜんぜん違うので、使いかたも違う。

Gemili CLI はとにかく編集するという機能が弱くて、だいぶつらい。

  • 巨大な replace をしようとして失敗する
  • replace でコメントを消しちゃう
  • replace で大量の改行を入れこむ
  • うまくいかなくて here doc でやろうとしてまた失敗する
  • write_file で全部書き戻そうとしてぶっこわす
  • 小さく変更しようとすると前後の文脈の重要な部分を省略して消しちゃう

とにかく編集という基本機能に問題があるので基本的に一切 accept editing してないで変更は全チェックしている。ここだけ我慢できれば普通に使えてる。


あと特定ケースでピンポイントにQwen3-Coder-480B-A35B-Instructを Kilo 経由で使ってみてる。Kilo よくできてるなあという気持ち

このサイトのドメインにDKIMも設定した。あと無駄に spf についてたアドレスを外して google のリレーだけ許可するようにかえた。

自分しか見ないけど管理画面を充実させつつある。今気付いたけどオレンジの枠なのは Cloudflare カラーなのか?

あとプレビュー機能もつけた。


Cloudflare のキャッシュも明示的な設定を入れた。R2の配信ドメインはクエリパラメータを無視するとか、年キャッシュをデフォルトにするとか

短文には関連エントリー出さないみたいな最適化を入れたい。1エントリが仰々しいと、短かい日記が書きにくいみたいなところある。どうしたもんか

短文エントリ (タグない・画像ない・140文字未満) にはリストで関連エントリとシェアボタンを出さなくした。

2026年 01月 14日

トップページの長さを若干アダプティブにした

この日記は「日記」なので、日付単位で出している。新しい日付順 (date DESC) かつ、同一日付内では上が古い時系列 (time ASC)。そして日付単位では全てのエントリを出す。

トップページにおいては10日分を一気に表示するようにしてたけど、これだとすごく活発に日記を書いてるときは長大なページになってしまう。

これを若干ややこしい処理にかえた。10日分のエントリをすべてひいてくるが、エントリ数が10以下になるまで日付を減らしていく、というものに。

来週健康診断で気が重い

「公開を遅延」する機能はもともとあったけど、予約投稿もどうせなら欲しいなと思って実装してみた。しかし思ったよりも大変だった。

考えかたとして「予約投稿」で指定した日付に新規投稿されるだけだから、そんなに難しくないのだが、再編集を考慮しはじめると一気に複雑性が増す。

# エントリのステータスとパス(URL)決定ロジック

本プロジェクトにおけるエントリのステータス管理は、「URL(パス)がいつ確定するか」という一点において区別されます。

## 1. コンセプト:URLの確定タイミング

| ステータス | 役割 | URLの確定タイミング |
| :--- | :--- | :--- |
| **`public`** | 即時公開 | **保存時**に確定する。 |
| **`scheduled`** | 公開を遅延 | **保存時**に確定する(公開まで非表示なだけ)。 |
| **`reserved`** | 予約投稿 | **公開当日**の最新番号で確定する(未来の新着として扱うため)。 |

*   **`scheduled`** は「URLを今日の日付で固定したまま、公開だけを未来(30日後など)にしたい」場合に使用します。`public` と同様の扱いです。
*   **`reserved`** は「公開されるその日の新着記事として予約したい」場合に使用します。公開されるまで URL は未確定です。

## 2. URLの不変原則

一度確定した URL (`YYYY/MM/DD/N`) は、**原則として変更されません**。
本文の修正、`public``scheduled` の切り替え、あるいは一旦 `draft`(下書き)に戻して再公開した場合でも、最初期に確定した URL が維持されます。

### 唯一の例外:`reserved` による「未来への移動」
公開済みの記事を `reserved` に変更して未来の日付を指定した場合のみ、URL が変更(再採番)されます。これは、その記事を未来の「新着記事」として扱い直すための特別な操作です。

## 3. 状態遷移の基本ルール

| 行先ステータス | パス (`path`) の扱い |
| :--- | :--- |
| **`draft`** | 既存パスがあれば維持。なければ空。 |
| **`public`** | 既存パスがあれば維持。なければ**保存時**に採番。 |
| **`scheduled`** | 既存パスがあれば維持。なければ**保存時**に採番。 |
| **`reserved`** | 既存パスがあれば維持。ただし無視し、**公開時**にその日の最新番号で採番。 |

## 4. `date` との関係

`path``YYYY/MM/DD/N` の場合、 `date``YYYY-MM-DD` となる

テストも全遷移を一応テストさせている。これに加えて不安なシナリオのテストもある…… golang だと fmt が自動でかかるので空白いれてテーブルテストを成形するのが難しい。しょうがないので文字数をあわせてるけど、LLMはこの手の操作が死ぬほど下手なのがつらい。もっと良い方法あるのだろうか……

	tests := []struct {
		ST        Transition
		SavedPath string
		PubAt     time.Time
		WantStat  string
		WantPath  string
		Name      string
	}{
		//    Transition        SavedPath  PubAt    WantStat  WantPath
		// --- New Entry ---
		{FromTo(NONE_, DRAFT), _____, ZERO_, DRAFT, EMPT_, "New -> Draft"},
		{FromTo(NONE_, PUBLI), _____, ZERO_, PUBLI, GENT_, "New -> Public"},
		{FromTo(NONE_, PUBLI), _____, FUTUR, PUBLI, GENT_, "New -> Public (Future Ignored)"},
		{FromTo(NONE_, SCHED), _____, FUTUR, SCHED, GENT_, "New -> Scheduled"},
		{FromTo(NONE_, RESER), _____, FUTUR, RESER, EMPT_, "New -> Reserved"},

		// --- From Draft ---
		{FromTo(DRAFT, DRAFT), _____, ZERO_, DRAFT, EMPT_, "Draft -> Draft"},
		{FromTo(DRAFT, PUBLI), _____, ZERO_, PUBLI, GENT_, "Draft -> Public"},
		{FromTo(DRAFT, SCHED), _____, FUTUR, SCHED, GENT_, "Draft -> Scheduled"},
		{FromTo(DRAFT, SCHED), _____, FUTUR, SCHED, TODAY + "/1", "Draft -> Scheduled (Path is Today, not Future)"},
		{FromTo(DRAFT, RESER), _____, FUTUR, RESER, EMPT_, "Draft -> Reserved"},

		// --- From Public (URL Immutability) ---
		{FromTo(PUBLI, DRAFT), P2025, ZERO_, DRAFT, KEEP_, "Public -> Draft (Keep URL)"},
		{FromTo(PUBLI, PUBLI), P2025, ZERO_, PUBLI, KEEP_, "Public -> Public (Keep URL)"},
		{FromTo(PUBLI, SCHED), P2025, FUTUR, SCHED, KEEP_, "Public -> Scheduled (Keep URL)"},
		{FromTo(PUBLI, RESER), P2025, FUTUR, RESER, KEEP_, "Public -> Reserved (Keep URL until publish)"},

		// --- From Scheduled (URL Immutability) ---
		{FromTo(SCHED, DRAFT), P2025, ZERO_, DRAFT, KEEP_, "Scheduled -> Draft (Keep URL)"},
		{FromTo(SCHED, PUBLI), P2025, ZERO_, PUBLI, KEEP_, "Scheduled -> Public (Keep URL)"},
		{FromTo(SCHED, SCHED), P2025, FUTUR, SCHED, KEEP_, "Scheduled -> Scheduled (Keep URL)"},
		{FromTo(SCHED, RESER), P2025, FUTUR, RESER, KEEP_, "Scheduled -> Reserved (Keep URL until publish)"},

		// --- From Reserved ---
		{FromTo(RESER, DRAFT), _____, ZERO_, DRAFT, EMPT_, "Reserved -> Draft"},
		{FromTo(RESER, PUBLI), _____, ZERO_, PUBLI, GENT_, "Reserved -> Public"},
		{FromTo(RESER, SCHED), _____, FUTUR, SCHED, GENT_, "Reserved -> Scheduled"},
		{FromTo(RESER, RESER), _____, FUTUR, RESER, EMPT_, "Reserved -> Reserved"},

		// --- From Draft with Path (Previously Published) ---
		{FromTo(DRAFT, DRAFT), P2025, ZERO_, DRAFT, KEEP_, "Draft(P) -> Draft (Keep URL)"},
		{FromTo(DRAFT, PUBLI), P2025, ZERO_, PUBLI, KEEP_, "Draft(P) -> Public (Keep URL)"},
		{FromTo(DRAFT, SCHED), P2025, FUTUR, SCHED, KEEP_, "Draft(P) -> Scheduled (Keep URL)"},
		{FromTo(DRAFT, RESER), P2025, FUTUR, RESER, KEEP_, "Draft(P) -> Reserved (Keep URL until publish)"},
	}

日記の編集画面に過去エントリのリンクを一瞬で貼れる機能をつけた

よく昔の日記を検索してはりつけるので、さっさと作ればよかった。

Ctrl-L で起動して、Ctrl-N Ctrl-P で選択し、よくわからんかったら一度 Shift-Enter で別窓で開き、確定なら Enter で挿入できる。すごい便利

スクリーンレコードしたやつを video 要素でそのまま貼る。

↑試しにスクリーンレコード(OBS)したものを以下のように AV1 + webm にして、video 要素ではりつけてみた。107KB

こういうことするとき anigif を貼り付けてたけど、もうそんなことしないでもよい時代になっているか?

$ ffmpeg -ss 1 -i "/Users/cho45/Movies/2026-01-14 23-01-13.mkv" \
	-t "5" \
	-vf "scale=1280:-2" \
	-an \
	-c:v libsvtav1 \
	-crf 45 \
	-preset 4 \
	-svtav1-params tune=0:enable-overlays=1 \
	-pix_fmt yuv420p \
	"./foo.webm"

Gemini はレートリミットはなんかちょっと小賢しい。細かい単位(数十秒程度)でリミットをかけて「AIの思考時間」を装っている感じ。

2026年 01月 15日

ブラウザが www.lowreal.net を lowreal.net として表示するのが気持ち悪すぎるんだよなあ。何なのそれと思う。おれは悪くないです。ブラウザが100%悪い。

プログラミング言語はコミュニケーションのためのもの

プログラムろくに書いたことない人はプログラミング言語をシンプルに誤解してて、機械語と区別がついていないのだろうと思う。

プログラミング言語はたまたま最終的に機械語に変換できるだけで、前提は人間同士(自分自身も含む)のコミュニケーション言語であるという理解がない。形式言語という形で意図を正確に伝えられるのがプログラミング言語であって、そうすると、たまたま実行可能にしやすいだけ。

LLMとのやりとりで自然言語を通してプログラミング言語を生成するのは、この意図が正確に反映されたかを、人間がレビューする必要性があるからにほかならない。

「ちゃんとできたか」を確認するのは最終的にはその要求をしている主体だ。一方で、人間は要求・要件を自然言語で100%正確に漏れなく曖昧さなく書くことはできない。なぜなら自然言語だから。やってみたいことを書き出したら矛盾することすらある。

プログラミングは、単なる実装(機械への命令)というより、曖昧な人間の意思を形式言語でミラーして具現化する行為であって、これはLLMを通じても別に変わらない。

とにかく本質的には人間は自分が欲しいものを、そもそも自分自身でも正確に理解していないところにある。「AIが無限に強くなれば言わなくてもやってくれる」という期待は、自分は自分が欲しいものを正確に理解しているという根本的な錯覚に基いている。

たとえAIが神になっても祈る側の願いが不定な以上は結果は定まらず、祈りと矛盾しないが意図しない結果がうまれる。この手の話は神話の時代からあり、上記の通り人間の埋め込みバグなので解決することはない。

だからこそ一度コードという形で曖昧さをなくし、テスト可能にし、自分の要求がなんだったのかを矛盾なく記述する必要がある。

ScreenToGif

Windows でスクリーンレコード気軽にとるなら ScreenToGif がよさげ?

名前からしてデフォルトでは anigif 出力なんだけど、WebM 形式にするとエンコーダーを選べて、普通に AVIF 出力ができる。録音はそもそもされないみたい。


macOS だと何がいいんだろうな。Kap なのかな。今度使ってみる。

Kap 非常にシンプルで完璧に使い勝手良いが、デフォルトだと WebM + VP9 か AV1 + MP4 で、パラメータもカスタマイズできないみたい。まぁ VP9 でもいいけども

簡易並列テストランナー

複数のテストを並列実行でマルチカラムで表示するやつをGeminiに書いてもらった。依存なし180行 で必要十分なのができて満足

人間が実行したときは途中経過を見せて、tty 経由じゃなければ最後まで無言でやってFAILしたものだけログをすべて出すというようにしてる。(Agentが実行するとき対策)

もうちょっと綺麗に(108行)なった。

2026年 01月 16日

Windows の Bitwarden アプリ、アップデートのたびにタスクバーのピン止めをやりなおしになる。めんどい

日記を書いたあとの後処理の node.js のプロセスを30分ぐらいは起動しっぱなしにするようにした。

Perlのときは別のデーモンとして常駐させていたんだけど、常駐させるまでもない(書いた直後しか必要ない)ので、毎回起動させるようにかえてた。

しかしそうなると今度は連続して編集するときに起動のオーバーヘッドが気になるので、若干ややこしいプロセス管理を入れた感じ。

やってることは node 側のプロセスにタイムアウト時間を渡し、アイドル時間が一定期間になったら死んでもらうという割とシンプルなことだけど、終了処理とかとの兼ね合いで結構複雑なコードになってしまった。

2026年 01月 17日

2010年1月3日撮影

関連エントリー (画像)

Bluesky は AVIF アップロードできる (表示はJPEGに変換される)、Twitter はアップロードがそもそもできない (アップロードの最終でハネられる)

友達がいなくても新しい言語は学べる 10年前のおれいいこと書いてて感心しちゃった…… LLMあるから今はもっと友達いらんね…… でもそれも我々(誰)が少なからずわからなかったことは公開される形で記録しつつ解決して、検索できるようにしたおかげじゃない?

もう日記の実装はやることなくなってきた。R2にあって日記にないファイルをリストする(GC)とかもつくったし。なんかほかにあったかなあ

OpenCode もちょっと使いはじめてる。なんか特に登録とかしなくても GLM 4.7 とか使えて謎なんだが…… そして GLM 4.7 普通に使えるし

2026年 01月 18日

日記のジョブキューに依存を入れれるような(フルページキャッシュを念頭に)改修をしてみたけど、ちょっと実装するだけでも複雑だ……

いろんなLLMつかってやらせてみたがどれもこれも非同期なテストが書けなくてつらい。

2026年 01月 19日

むちゃくちゃ気が重い

Chromeの開発者ツールでそのプロトコルが選ばれた理由が表示されることに気付いた

ずーーーと「なんでh3にならんのかな〜」ということをやってたけど、この開発者ツールのプロトコルの部分にホバーすると理由が出るということにようやく気付いた。ただ出ないこともある

HTTPS レコードで勝った場合

`Chrome` used a `HTTP/3` connection due to the `DNS record` indicating `HTTP/3` support, which won a race against establishing a connection using a different `HTTP` version.

HTTPSレコードちゃんと使ってるんだ。

ただレースして勝ったから使っていると書いてある。従来のTCP接続も同時に投げているようだ。

おそらく初回接続だけで、次からは Alt-Svc 優先になると思われる。(シークレットウィンドウで初回アクセスするとこれになる)

Alt-Svc ヘッダで勝った場合

`Chrome` used a `HTTP/3` connection induced by an `Alt-Svc` header without racing against establishing a connection using a different `HTTP` version.

2回目移行の接続。こっちはレースしないで決め打ちで繋ぐんだなあ

microdata → JSON-LD

JSON-LD 全然好きではないが、microdata でやっていた構造情報を JSON-LD にのせかえてみた。どっちにしろたいした情報はないが……

microdata だと display: none な要素を使ってメタデータを埋めこむ必要がしばしばあって、これがDOMの構成要素を増やす要因になっていて気になっていた。

最近のChromeだとDOM要素多いとパフォーマンス悪化するぞと脅してくる。

HTMLとしては非常にシンプルになって気持ちよいけど、やっぱ microdata のほうが好きだなあ。

2026年 01月 20日

ギターの練習さぼりまくり。モチベが著しく低い。たまに弾くと楽しい。そしてサボってから弾くと指が痛い。

モチベとは関係ないがボリュームPOTの調子が悪く、何度か分解解体してバネ圧高めたり接点を#10000ぐらいでやすったり、接点復活剤かけたりしてる。一時的にはよくなるんだけどなあ。買い替えるのがいいんだろうなあ。

しかしサウンドハウスで500K Aカーブを買おうと思うと980~1300円とびっくりする値段 (と思ったけど380円ぐらいのもあるな)。ブランドモノなのか? 秋月だと60円。 どうするかなあ。元々ついてたのは ALPS のやつ。

jQuery 20周年……???

skills とか subcommands とかいまいち整備する気にならんのだよなあ。

と一回書くとやる気になるはず

このサイト開くと、便利情報として右上に IPv4 か IPv6 か表示するようにしてあるんだけど、ここに今繋いでる HTTP のバージョンも出すようにしてみた。

/.ip としてシンプルな情報を出すエンドポイントが前からあるのでここに足しただけ

      "/.ip":
        header.set: "Cache-Control: private"
        mruby.handler: |
          lambda do |env|
            ipv6 = env["REMOTE_ADDR"].include?(":")
            proto = {"HTTP/2" => "h2", "HTTP/3" => "h3"}.fetch(env["SERVER_PROTOCOL"], "")
            [200, {'content-type' => 'text/plain'}, [ "#{ipv6 ? 'IPv6' : 'IPv4'} #{proto}" ]]
          end


直接関係ないけど、h2o からproxy.reverse.url のバックエンドに直接こういう変数を入れたカスタムしたヘッダを送るのはあんまり簡単でなさそう? http_request 使うぐらい?

うーなんか静的ファイルの配信がおかしい。途中で切れたりする。リロードすると再現したりしなかったり。おかしすぎる

うーん

proxy.zerocopy: ALWAYS

をコメントアウトしたらいったん再現しなくなった。

もうバックエンドの最適化でやることがないので、無用な複雑性だとは思いつつもページ全体のキャッシュ処理をするようにした。

ファイルシステムだと競合処理がどうしても不安なので、SQLite バックエンドではあるけど、これでも10倍ぐらいはやい…… (314req/sec → 3039req/sec) ただし静的ファイルは 12930req/sec

ログイン(自分)の状態だと常にキャッシュ無視なので壊れてても気付けない。非同期ジョブが全部終わった段階で関連キャッシュをクリアしている

2026年 01月 21日

オッドタクシー面白かった。

z.ai に一瞬金払ってみようかなと思ったがメインクレカそのまま入れたくないな、ということから楽天プリペ発行してみたり(サブスク不可だった)、Revolut 入れてみたりまでして結局やってない

39歳

2006年から開発のバイトはじめてるから20年ぐらい仕事としてプログラム書いたことになるっぽい。あんまり誕生日に日記書いてないので、単に遡ると20代終了までにいきついてしまった。そして内容的にはほとんど何も変わってない。ただじわじわ不健康になっていっているだけ。

働き方を変えないといけない気がしてる。子どもの進学タイミングがマイルストーンになりそうと言い訳しつつゴールポストを動かしてしまいそう、40歳か、43歳か、45歳か。45歳だと子どもが成人年齢になってしまう。

LLMでコード書けるのは楽しすぎる。手が早いだけの人の価値はふっとんでしまったなと思う。自分は自分のことをレゴブロック積み上げて最速で形を作ってるだけという自覚があるので、コーディングエージェントは直接的に競合する相手のはずだけど、なんか全然そこに危機感はない。さっさともっと賢くなって完全に仕事奪えやという気持ちがある。

つまり逆に現状のコーディングエージェントでは全然性能が足りない。「すごい、こんなにはやくできたぞ! スゲー」というのと「は? こんなこともできねーのかよ。ゴミだな」というのを毎日繰替えしてる。ただコーディングエージェントなしにはもう戻れない。

2026年 01月 23日

健康診断2026

健康診断、保険組合が変わり、去年まで行っていたところと違うので早めに家を出たが、予約時刻の15分前から案内だった。はやく着きすぎた。

スマホで案内されるシステムでなんかすごいが、別にファイル置いていくシステムでも良さそうな気がする。いや、違うな、数字通りじゃなくあっちこっち行かされるので、待ち行列が最小になるように個別案内されている?

スタッフの人の手際がすごい。2倍速ぐらいで動いてしゃべる。最適化されとる。


とにかく胃カメラが毎回気が重い。鎮静剤ありの経口で予約。そして今回は最後に胃カメラというパターンではなく普通にほかの検査の途中だった。

事前に「当院の鎮静剤は優しめで、眠ったりしません」といわててビビっていた。実際、完全に眠ることは全くなく、普通に寝不足の深夜とか、泥酔一歩手前ぐらい。

喉通過のときは普通にえづいてしまった。ただ先生もゆっくり動かしてくれたのと、看護師さんが背中さすってリラックスさせてくれていたのが地味に良かった…… おかげで、通過してからもずっとえづいてしょうがないということはなかった。

胃の下あたりから終わりまでは目をあけてモニタ見てた。結果は綺麗。鎮静剤優しめだけど、もっとひどい人に比べれば、それほどえづいてなかったと説明される。というかそもそも内視鏡が結構細かった気がする。喉通過のとき飲み込んでくださいとも言われなかった。あと先生も丁寧だった。

そういえばなんか肺活量の検査なかったな。



検診後にびっくりドンキー。扉メニューないの普通に不便だな…… 白玉デザートもなくなって悲しい。

誕生日よりも健康診断がおわったことのほうがめでたいし気分上がるわ。抑圧と解放の例だ……

2026年 01月 25日

機械に機械学習のモデルを設計してもらう(何もわからない)

主に Gemini を使って、LLM主体でモデルの設計と学習をなるべくさせてみるということに数日とりくんでた。

自分が知らないことをLLMにやらせてみようというのを時々やっている。

課題

モールス符号の音響受信をさせる。7年前にKerasを使って頑張って似たようなことをやった覚えがある。その情報は渡さずに「今こういうのやるならどういうモデルがいいかな」という相談からはじめた。

Gemini いわく「 CTC損失を用いたStreaming Conformer (CNN + Transformer)」がいいのではということだった。よくわからないが全く理解しないままこの方向ですすんでみることにする。(賢いあなたは、わからないことがあったらその時点で必ず解消しましょう)。

リアルタイム音声認識ということでこれを提案してきたのだろう……

できたもの

https://cho45.github.io/morse-decoder-2026/demo.html

一応計画の段階で「最終的にブラウザでリアルタイムにストリーム処理できること」は入れてたので、いろいろ問題はありつつも動くものはできた。

pytorch でモデルを作り、最終的に onnx に変換して onnx web で動いている (wasm)。WebGPU でも動きはするけど、なんかちゃんとやってないのでパフォーマンスがでない。

「SNRいくつまでデコードができる」みたいなことを言いたいが、SNRの定義がなんともいえない (信号のある瞬間の振幅基準) ので難しい。このSNRで-15dBぐらいまでギリデコードができる。もうちょっと頑張ってほしいけど……

モデル構造

こんな感じ。CNN + Transformer のことを Conformerというらしい。まんなかの4層がそれ。ただのモールス復号のモデルにしてはデカいはずだけど、もっと少ない状態だとまったくうまくいかなかった。

入力はスペクトログラムを特定ピーク中心で切り出したものになっている。つまりベースバンド波形ではなく画像として見てる。別途ピーク検出と切り出しを行ってからこのモデルに入れる想定。位相が不要だから画像として扱うほうがいいっぽい?

しかし疑問点

モデルの内容には、ほぼ口を出さずにやれるところまでやったけど、そもそもいくつか疑問があるので、今更訊いた。

なぜ入力にメルスペクトログラム?

(意図: 特定周波数付近を切り出して渡しているので、通常のスペクトログラムで十分だし実用上は楽なのでは?)

A:「メルスペクトログラムは、一般的な音声認識(ASR)モデルの標準的な入力形式であり、情報の圧縮とノイズ耐性に優れているため採用されています。狭帯域にクリップする場合、メル尺度による圧縮の恩恵は薄れるため、本アプリにおいては線形スペクトログラムの方が解像度を維持でき、より合理的である可能性が高いです。」

「リアルタイム音声認識」にひっぱられてあんまり考えずに決めてるな……

出力がCTCなのはなぜ?

Q: 「今回のモールス学習の場合、かなり厳密にラベルがつけられるはずですよね。そして実際そうしているし Signal Head として出力もしている。それでもなおCTCを使う理由は?」

A:「Signal Head による物理的な信号認識を補助としつつ、CTC を使うことで「文字」としての文脈や区切りを柔軟に扱い、デコード時の頑健性を高めるためです。」

Q:「今回の場合ラベルの柔軟性というよりは出力の簡略化の側面がつよい?」

A:「はい、その通りです。Signal Headのように「全フレームを分類」するのではなく、CTCを使うことで「重複除去とblank削除」という単純なルールだけで、複雑な後処理なしに最終的なテキストを直接得られるという、出力の簡略化と利便性の側面が非常に強いです。」

フーン……

出力に CTC 以外に Signal Logits と Boundary Logits がある。これらはCTC出力からだけの学習で一切上手くいかなかったため、苦肉の策として追加した出力になっている。(ここは自分の指示……)

  • Signal Logits: モールスの「短点」「長点」「符号間空白」「文字間空白」「単語間空白」「ブランク」を予想させる -- モールスの形状を強制的に学習させる
  • Boundary Logits: モールスにおいて、符号が確定する瞬間に1になる。つまり文字符号の後の空白1ユニットの直後に発火する -- CTCが発火すべきポイントを間接的に学習させる意図 --- これそのままCTCへの入力に使ったら楽や~んと思ったけど、それだとCTCがチートして、Boundary Logits が高かったら適当に文字出すようになってしまって一敗

これが正しいことかはわからないが、Gemini 的には学習を進めるうえで有効ですとはいっていた。まぁ結果としてうまくはいったけど、これのせいでモデルサイズが大きくなっている面がある気がする。

得られたもの

やはり自分で正解知らないことをボーっとやらせるのは非常に難しい。結局急がば回れになることが多い…… わかってるんだよ。わかってるけどわからないまま作りたいんだ!

機械学習の修正タスク投げると、いくら事前に指示をしていてもテストを書かないし実行してくれない。最終的には「平均的なエンジニアはテストを書かず本体のコードだけに集中している。そこから学んでいるので自分もそういう傾向にあるのだ」という開きなおりをされた。

家計決算報告スライド

hakobe さんが「NotebookLM に家計情報置いてスライド作らせると面白いよ」と言うのでやってみたらかなり面白かった。役立つというよりは笑える。

知らんうちに肩書がついていたり、よく決算資料で見るようなウォーターフォール図(という言葉もしらなかった)だったり、アクションプランだったりをいろいろ勝手に作ってくれる。

フォントが変なので画像生成は使ってるんだろうけど、グラフの正確さが画像生成のそれではないように思える。どうやってるだろうなあ。

2026年 01月 26日

盆栽 疑似音声信号発生器

https://github.com/cho45/pseudo-audio-signal

定期的にチマチマいじって盆栽している。あんまり意味ないけど複数サンプルレートに対応させたり、E2Eテストを簡単に追加したり……

ずっと以下のような八車文乃プロンプトにしてたけど、LLMにターミナルの結果をバッを貼りつけるときにまぁ不便 (「何か文字化けしているようです」とかいわれる)のでそろそろ止め時。供養します

[cho45@Anna] ~
 | q ド _ リ|$ <
2026年 01月 27日

NotebookLM ぜんぜん活用できてない。日常の中にない。

WebAudio のRTT測るツール作ろうかと思ったけどM系列ですぐ測れるやつがすでにあった https://gilpanal.github.io/weblatencytest/ 72ms だった (https://github.com/gilpanal/weblatencytest )

2026年 01月 28日

WebAudio-Signal-Generator

むか〜し書いた WebAudio-Signal-Generatorをモダンにした。AngularJS 1.2 + Bootstrap 3 だったのを Vue3 + Vanilla CSS に。というかそもそも ScriptProcessorNode 使っててマジで古かった。

ブラウンノイズ こんな感じで設定をURLに反映するようにして、リンクを貼れるように。

まぁもうこんなの欲しいとおもったときにLLMに書かせりゃいいという気はするけど