2026年 01月 03日

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

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

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

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

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

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

2025年 12月 31日

XSLTという関数型言語

ブラウザの安全性を高めるための XSLT の削除とかいうことで、つまりブラウザネイティブでの XSLT のサポートは消えるので、ちょっと哀しいですねという気持ち。

一時期(20年前……)は XML を手書きで書いて、XSLT で変換してHTMLにするという日記システムを作って使っていた (さすがにブラウザ直ではなくPHPでやらせてたけど)。そこから20年、クライアントサイド XSLT の本来の目的である、データをレンダリング可能な HTML に変換する機能は、より安全で、より人間工学に基づき、より適切にメンテナンスされた JavaScript API に取って代わられています。とか書いてあるのを見ると、まぁ事実なんだけどただかなしいなという。

XSLT (特に 1.0) は、副作用がなく、変数は不変(一度定義したら変更不可)、ループは再帰で実現するという、割と強めの関数型言語で、自分が初めて触った関数型だったと思う。XSLT のおかげで他の関数型のパラダイムも割とすんなり入った気もする。

Gemini に XSLT で関数型っぽさがわかるサンプルを書かせてみた

出力そのまま。SVG を出力するというサンプルを書いてくれた。しかもアニメーションする。これが素のブラウザで見れるのも今だけです。

XML はただの起点

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="fractal.xsl"?>
<root></root>

XSLT 側では4つの四角を call-template で生成するところから開始して、再帰でフラクタル状に四角をつくっていく。

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://www.w3.org/2000/svg">
  <xsl:output method="xml" indent="yes" media-type="image/svg+xml"/>

  <xsl:template match="/">
    <svg width="100%" height="100%" viewBox="-500 -500 1000 1000" style="background-color: #1a1a1a;">
      <title>XSLT Recursive Fractal</title>
      
      <xsl:call-template name="draw-square">
        <xsl:with-param name="x" select="0"/>
        <xsl:with-param name="y" select="0"/>
        <xsl:with-param name="size" select="300"/>
        <xsl:with-param name="depth" select="5"/> </xsl:call-template>
    </svg>
  </xsl:template>

  <xsl:template name="draw-square">
    <xsl:param name="x"/>
    <xsl:param name="y"/>
    <xsl:param name="size"/>
    <xsl:param name="depth"/>

    <xsl:if test="$depth > 0">
      
      <xsl:variable name="color">
        <xsl:text>hsl(</xsl:text>
        <xsl:value-of select="$depth * 40"/>
        <xsl:text>, 70%, 60%)</xsl:text>
      </xsl:variable>

      <rect 
        x="{$x - $size div 2}" 
        y="{$y - $size div 2}" 
        width="{$size}" 
        height="{$size}" 
        fill="{$color}" 
        opacity="0.9"
        stroke="#fff"
        stroke-width="1"
      >
        <animateTransform 
            attributeName="transform" 
            type="rotate" 
            from="0 {$x} {$y}" 
            to="360 {$x} {$y}" 
            dur="{10 + $depth * 5}s" 
            repeatCount="indefinite" />
      </rect>

      <xsl:variable name="nextSize" select="$size * 0.5"/>
      <xsl:variable name="offset" select="$size * 0.75"/> <xsl:variable name="nextDepth" select="$depth - 1"/>

      <xsl:call-template name="draw-square">
        <xsl:with-param name="x" select="$x"/>
        <xsl:with-param name="y" select="$y - $offset"/>
        <xsl:with-param name="size" select="$nextSize"/>
        <xsl:with-param name="depth" select="$nextDepth"/>
      </xsl:call-template>

      <xsl:call-template name="draw-square">
        <xsl:with-param name="x" select="$x"/>
        <xsl:with-param name="y" select="$y + $offset"/>
        <xsl:with-param name="size" select="$nextSize"/>
        <xsl:with-param name="depth" select="$nextDepth"/>
      </xsl:call-template>

      <xsl:call-template name="draw-square">
        <xsl:with-param name="x" select="$x - $offset"/>
        <xsl:with-param name="y" select="$y"/>
        <xsl:with-param name="size" select="$nextSize"/>
        <xsl:with-param name="depth" select="$nextDepth"/>
      </xsl:call-template>

      <xsl:call-template name="draw-square">
        <xsl:with-param name="x" select="$x + $offset"/>
        <xsl:with-param name="y" select="$y"/>
        <xsl:with-param name="size" select="$nextSize"/>
        <xsl:with-param name="depth" select="$nextDepth"/>
      </xsl:call-template>

    </xsl:if>
  </xsl:template>

</xsl:stylesheet>
2025年 12月 30日

Chemr (Chemrtron) を再実装

Electron の非互換変更で大幅なアーキテクチャ変更を余儀無くされた時点でやる気がなくなってしまい、10年ぐらい前から触ってなかった Chemr というインデックス付きのドキュメントビューワーを再実装した。

なお Chemr はもともと .chm (Compiled HTML Help) を macOS で読むためものだった。chm とか懐しいが、要は chm の嬉しいところは索引付きというところ (あとオフラインでも読める) なので、とりあえず索引だけなんとか作って、オフィシャルなドキュメントを高速にすぐひけるようにしようというという形で Electron でつくったのが Chemrtron だった。

再実装

コード自体はほぼコーディングエージェントにやらせた。

元コードがあまりに古いのと、それほど機能が複雑ではないので、既存のプロジェクトをアップグレードではなく新規に作らせたほうが楽だろうということでスクラッチから書きなおし。ただし旧実装はディレクトリをわけて置いておいて、参考にしろという形でコンテキストを与えながらやった。

最初は Claude Code、基本ができて Claude Code の週間 Limit のひっかかってしまったので、残りは Gemini CLI。

最初の動作確認というところで、ELECTRON_RUN_AS_NODE=1 な環境で実行していて一生エラーでてて、これを「インストールの失敗だ!」「お前の環境が悪い!」と言い張ってて Claude がまじでアホだった。

そしてバカでお茶目な Gemini ちゃんは「✦ 大変申し訳ありません。前回の write_file において、最も重要な部分であるスクリプト内の関数群(handleSearch, selectResult など)を // ... (中略) ... という文字列で置き換えてしまうという、致命的なミスを犯しておりました。 」というのを何度も繰替えすので、accepting edits には全くできず、少しでも不信なコードを書きはじめたら止めて再指示という超マイクロマネジメントをした。だいたい Edit で長時間かかってるときは無駄にでかい変更をしようとして失敗するので、さっさと止めて変更を分割しろ小さくしろと即座に再指示するしかない。

Gemini はデフォルトのシステムプロンプトが本当にクソでイライラするので書きかえて使っている。レビューしてる時に質がゴミなのに「次に進みますか?」って何度も訊いてくるのが本当に我慢ならない。

Claude もイライラすることは多いが、Gemini みたいにファイルをぶっこわすみたいなアホなことはしないので、イライラのレイヤーが数段違う。

そんなこんなで1時間ぐらいは無駄にしたけど、だいたい(3時間+5時間)2日で機能がそろって v2.0.0 として出せた。

インデックスを作る部分も古すぎるので非互換変更を入れ、既存のもののうち重要そうなのは、LLMに再実装させた。ついでに全部のインデクサがちゃんと動くかテストできるようにした。

2025年 12月 27日

本を書く(LLMを使って)

最近、技術書をまとめていくつか出してみた。すべてLLM(Claude/Gemini/ChatGPT全部)を活用してながら書いてみてる。

LLMはまぁそれが主戦場なので文章は結構書いてくれる。けど、本として読んで価値あるものにしようと思うと、メインとなる背景の「考えかた(ないしは思想)」は、とにかく明確に定義する必要がある。つまり企画をしっかりやる重要さをより強く感じた。とにかく何を書きたいか、なぜ書こうと思ったかを自分と対話して明示する必要がある。

企画(設計)だけしっかりしてればLLM活用しても価値があるものが作れる気がしている。LLMに書かせてみて「それが面白いことじゃないんだよな」と否定と修正を繰り返すと、無意識のこだわりの輪郭が浮かびあがってくるように思う。

とはいえ、ニュアンスやトーンの指定は困難すぎるし、ロールを与えててもうまく意図や背景は汲み取ってくれないので、割とアホみたいな定型NG語尾集をつくったりする必要がある。

書きたいこと自体はやりだしてみると自分が思っていたよりあったりして (これら以外でもまだ書いてる)、やってるぶんには結構おもしろい。以下リンクです。(年末年始、順次無料キャンペーンを設定してあります)

アマゾンでの一覧

2025年 12月 08日

パスキー

パスキー設定できるサービスは設定しつつあるけど、いまだに結構なんとなく心理的負荷が高い。

  • 保存先
    • Google Password Manager?
    • Bitwarden?
    • 端末限定?
  • パスキーが失われたら?
    • 各サービスごとのリカバリポリシーを理解してからでないと最悪の事態になる
    • さすがにメアドでどうにかなることが多いが、パスキーとメアドは明示的に紐付かない


特に保存先について、ITよくわからんという人が理解できると思えない…… 基本的に普通の人は Google Password Manager に保存するのが現状でのベストだと信じている。理由は

  • 利用者が多い
  • 端末個別に保存するよりパスキーを失う可能性が低い
  • 現実的にメアドも Gmail なら、「Googleを信用する」としても問題が少ない
  • フィッシング対策としては十分

ただ、複数パスキーが設定可能なら Bitwarden も追加するようにしている。とにかくパスキーはリカバリーが問題で、サービスごとのリカバリ方法をいちいち調べるよりはパスキーを複数作ってロストしないことが最重要と考える。


実際は

1. セキュリティデバイス保存 (耐タンパ性)
2. スマフォ端末保存
3. クラウド保存

でレベルが違うわけだけど、現実的によく問題になっているフィッシングへの対策としては保存先はどれでもよく、利便性重視でよいと思う。

2025年 11月 04日

iGPU を活用して VRAM の節約

RTX 4070 (12GB VRAM) を使ってるけど、Stable Diffusion やら Photoshop や DaVinci Resolve などを使うと非常にカツカツでアプリケーションを終了させたりしてしのいでいる。あとほんの少しVRAMがあれば……と思うことも多々ある。これらのアプリが共有メモリに侵食してくると劇的にパフォーマンスが落ちるので……

ふと iGPU を有効にして微妙に VRAM を使用する Chrome ベースのアプリや Chrome 自体を iGPU に持ってくれば、dGPU から多少の VRAM を節約できるのではないかと考えた。のでやってみることにした。なおメインメモリは余っているものとする。

iGPUを有効にする

これまで何も考えずに iGPU は無効にしていたので、まずは UEFI メニューから iGPU を有効にして再起動。メインディスプレイ以外は iGPU 側に接続しなおした。

そのうえで、アプリケーションごとの設定も、特にパフォーマンスが必要ないものは iGPU を使うように。これは設定 > システム > ディスプレイ > グラフィックスからアプリケーションごとに設定できる。

細かく VRAM 使っているプロセスを指定していく。というかデフォルトを iGPU にできればいい気がするが……

Intel ドライバのアップデート

Chrome がどうもちょくちょくブラックアウトするので、Intel のディスプレイドライバを最新に

https://www.intel.co.jp/content/www/jp/ja/download/864990/intel-11th-14th-gen-processor-graphics-windows.html

Windows における iGPU と dGPU

dwm.exe (デスクトップウィンドウマネージャ) がウィンドウの合成を担当しており、これが各ウィンドウの内容をテクスチャとして合成してディスプレイへ出力している。

あるアプリケーションが iGPU を使ってレンダリングした結果を、dGPU に接続されているディスプレイで表示する場合、dwm が合成する際に PCIe 経由でのデータ転送が発生する(転送のオーバーヘッドがかかる)。

今回のように dGPU の VRAM 負荷を軽減するために iGPU(システムメモリ)にオフロードする場合は、PCIe 転送のオーバーヘッドとのトレードオフがあることに注意が必要。

dGPU の VRAM 負荷を軽減する戦略:

  • アプリ単位でのオフロード
    • GPU 指定機能を使い、軽量なアプリを iGPU に割り当てる
    • dGPU 接続のディスプレイに表示する場合は PCIe 転送のオーバーヘッドが発生
  • マルチディスプレイ構成の活用
    • メインディスプレイを dGPU に接続
    • サブディスプレイを iGPU に接続
      • サブディスプレイ上のアプリを iGPU 指定すれば、転送オーバーヘッドなしでオフロード可能

転送オーバーヘッドは画面領域×更新頻度に比例するため:

  • 大きな画面領域を高頻度で更新するアプリ(フルスクリーンゲーム、動画編集のプレビューなど)は転送コストが大きく、オフロードに不向き
  • 小さい画面や更新頻度が低いアプリ(テキストエディタ、ブラウザの静的ページなど)は転送コストが小さく、オフロードしやすい

具体例:

  • dGPU 優先: 重い3Dゲーム、3DCGレンダリング、大量のテクスチャを使うアプリ、AIモデル推論など
  • iGPU 可: Web ブラウザ、動画再生、コーディング環境、軽量な2Dアプリなど
2025年 10月 18日

Raspberry Pi から HDMI-CEC コントロール

サイネージ HDMI CEC 連携

カレンダーを表示するおうちサイネージ というのを作っていたがモニタの置き場がなくなってしまったので、テレビ画面に映すことにしてみた。

Google TV と Switch でしか使わないので、普段は基本的に Google TV のリモコンで操作している。しかし Raspberry Pi の HDMI を繋いで外部入力をそちらにすると、Google TV のリモコンからの操作 (HDMI CEC信号) をテレビ(SONY BRAVIA)が無視するという困った挙動になった。

試行錯誤してみると Raspberry Pi を HDMI CEC デバイスとして単に参加させる (以下のように)と、他の機器の CEC 信号も無視しなくなるようだった。

# 再生デバイスとして CEC デバイスのネットワークに参加
 cec-ctl --playback

他のデバイスのCEC信号を受けて自動表示

ここまではいいとして、カレンダーアプリはある程度自動的に起動してくれないと見ないため、Google TV のリモコンから電源をオフにすると、自動的に Raspberry Pi のカレンダーを表示する、という挙動にしたくなった。cec-ctl --monitor で HDMI CEC のイベントをストリーム取得できるようなので、これを利用してフックし、他の機器からのSTANDBYがきたら、Raspberry Pi 側に画面を切り替えるという処理を入れた。

 #!/usr/bin/env python3
 import subprocess
 import time
 import re
 import sys
 
 # ====== 設定値 ======
 CEC_ADAPTER_INIT_CMD = ["/usr/bin/cec-ctl", "--playback"]
 CEC_MONITOR_CMD = ["/usr/bin/cec-ctl", "--monitor"]
 TV_ON_CMDS = [
     ["/usr/bin/cec-ctl", "--to", "0", "--image-view-on"],
     ["/usr/bin/cec-ctl", "--to", "0", "--active-source", "phys-addr=4.0.0.0"]
 ]
 DEBUG = True
 # ====================
 
 
 def log(msg):
     """ログ出力(デバッグ時のみ)"""
     if DEBUG:
         print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)
 
 
 def run_cmd(cmd):
     """単発コマンド実行"""
     try:
         subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
         log(f"Executed: {' '.join(cmd)}")
     except subprocess.CalledProcessError as e:
         log(f"Command failed: {cmd} ({e})")
 
 
 def main():
     log("Initializing CEC adapter...")
     run_cmd(CEC_ADAPTER_INIT_CMD)
 
     log("Starting CEC monitor...")
     proc = subprocess.Popen(
         CEC_MONITOR_CMD,
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
         text=True
     )
 
     last_device_standby = 0
 
     try:
         for line in proc.stdout:
             line = line.strip()
             if not line:
                 continue
 
             # 他のデバイスが STANDBY 送信
             if re.search(r"Received from Playback Device", line) and "STANDBY" in line:
                 log("Detected STANDBY from another playback device")
                 time.sleep(3)
                 for cmd in TV_ON_CMDS:
                     run_cmd(cmd)
 
     except KeyboardInterrupt:
         log("Interrupted by user")
     finally:
         proc.terminate()
         log("Monitor stopped")
 
 
 if __name__ == "__main__":
     if sys.version_info < (3, 6):
         print("Python 3.6+ is required", file=sys.stderr)
         sys.exit(1)
     main()

これを systemd にいれておく

sudo vi /etc/systemd/system/cec-init.service
 [Unit]
 Description=Configure HDMI CEC playback mode
 After=graphical.target
 Wants=graphical.target
 
 [Service]
 Type=oneshot
 ExecStart=/home/pi/cec.py
 RemainAfterExit=true
 
 [Install]
 WantedBy=multi-user.target
 sudo systemctl daemon-reload
 sudo systemctl enable cec-init.service
 sudo systemctl start cec-init.service
 
 sudo systemctl status cec-init.service 
 ● cec-init.service - Configure HDMI CEC playback mode
      Loaded: loaded (/etc/systemd/system/cec-init.service; enabled; preset: enabled)
      Active: activating (start) since Sat 2025-10-18 14:59:08 JST; 11min ago
    Main PID: 2820 (python3)
       Tasks: 2 (limit: 754)
         CPU: 371ms
      CGroup: /system.slice/cec-init.service
              ├─2820 python3 /home/pi/cec.py
              └─2823 /usr/bin/cec-ctl --monitor
 
 Oct 18 14:59:08 sinage systemd[1]: Starting cec-init.service - Configure HDMI CEC playback mode...
 Oct 18 14:59:08 sinage cec.py[2820]: [14:59:08] Initializing CEC adapter...
 Oct 18 14:59:09 sinage cec.py[2820]: [14:59:09] Executed: /usr/bin/cec-ctl --playback
 Oct 18 14:59:09 sinage cec.py[2820]: [14:59:09] Starting CEC monitor...
 Oct 18 15:00:42 sinage cec.py[2820]: [15:00:42] Detected STANDBY from another playback device
 Oct 18 15:00:45 sinage cec.py[2820]: [15:00:45] Executed: /usr/bin/cec-ctl --to 0 --image-view-on
 Oct 18 15:00:45 sinage cec.py[2820]: [15:00:45] Executed: /usr/bin/cec-ctl --to 0 --active-source phys-addr=4.0.0.0
 Oct 18 15:08:43 sinage cec.py[2820]: [15:08:43] Detected STANDBY from another playback device
 Oct 18 15:08:46 sinage cec.py[2820]: [15:08:46] Executed: /usr/bin/cec-ctl --to 0 --image-view-on
 Oct 18 15:08:46 sinage cec.py[2820]: [15:08:46] Executed: /usr/bin/cec-ctl --to 0 --active-source phys-addr=4.0.0.0
2025年 10月 16日

NASの10Gbps接続を10GBASE-SR(光ファイバー)に

2021年から使っているNASのHDDの音が気になるようになってしまった。作業部屋を移動したからかもしれないがなんとかすることに。

玄関に追い出す

箱に入れるとかも考えたが、同じ部屋に置いておくのは無理という結論にいたり、玄関に置き場所を確保した。

そのうえで 10Gbps の LAN をどうするかという問題に対処することに。

10Gbps 接続の選択肢

今までは 1m の DAC (ダイレクトアタッチケーブル) を使っていたが、玄関まで配線しようとすると 7m ぐらい必要。あんまり長い DAC よくなさそうだし高いので、この時点で排除して2択

  • AOC (Active Optical Cable)
    • 両端 SFP+ でケーブルは光ファイバ。光コネクタがなくて直結している。DACみたいに使える
    • 若干安い
  • 10GBASE-SR トランシーバ×2 + 光ファイバーケーブル
    • 一番汎用性が高い。光コネクタとかを理解する必要がある

一応 10GBASE-T のトランシーバ×2 + CAT6 LAN ケーブルという選択肢もあるかもだけど、消費電力は高いしコストも高いし信頼性も低いので新規導入では選択肢に入らないと思う。

結論としては 10GBASE-SR にした。

10GBASE-SR と光コネクタと光ファイバー

10GBASE-SR の SR は Short Range の略。MMF(マルチモードファイバー)というコアが比較的太くて機械的精度要求が低い光ファイバーを使い、300mぐらいまでの通信に使えるものらしい。意外とコスト低め。家庭内では十分

SFP+ の場合、光コネクタは LC を使うのがデファクトスタンダードらしい。

光ファイバーは OM3、OM4、OM5 などがマルチモードファイバーの規格名のようだ。数字が大きいほど性能が良いが、OM3でも十分なので一番安いのを選べばよい。つまり LC/LC OM3 となっている光ファイバーを買えばいい。

光ファイバーケーブルの接続

送受信で別々のファイバーを使うので、2本必要。duplex と書かれたものを買う。両端でペアになるように部品がついている。そしてペアのどっちがどっちのケーブルかわかるようになっている。そしてこれらはクロス接続する必要がある。

ただ今回買った光ファイバーは最初からクロス接続になるように配線されていた。買ってそのまま挿せばいいだけだった。

問題なくおわった

何もハマることなくすんなり接続できた。SFP+ はベンダーロックとかがあったりするので不安要素が多いのだけど、今回は大丈夫だった。

光ファイバーは最小曲げ半径が 7.5mm らしく意外を曲げられるようだ? しかしなんとなく銅線よりも取り回しに気をつかってしまう。

これで電気的に絶縁されて接続されてると思うと気分が良い

2025年 09月 04日

月1万円の支出削減は資産400万円に相当する

経済的独立 (FIREのFIの部分)の文脈では「月1万円の支出削減は資産400万円を増やすのと同じ効果」があるといえる。

これは いわゆる「25倍ルール」 ないし 「33倍ルール」 から考えられる。

25倍ルール・33倍ルールとは

25倍ルールは、年間支出の 25倍の資産 があれば、資産の4%を毎年取り崩しても一生暮らせるという目安。「4%ルール」に基づくもの。

より安全に見積もりたい場合、資産の3%しか取り崩さないとすると、必要資産は 年間支出の33倍。国内だと税制やらなんやらで3%で計算しろとかなんとか。

たとえば年間400万円の支出 (税金や社会保険料なども含む) がかかるとすると

  • 25倍ルール → 1億円(400万 × 25)
  • 33倍ルール → 1.32億円(400万 × 33)

で無収入でも破綻しなくなる (破綻するかどうかは実際は確率の問題だが)

月1万円削減が資産400万円に相当する、とは

月1万円の支出を減らすと

  • 月1万円の支出削減 → 年間で12万円の削減
    • 1万円 × 12ヶ月 = 12万円
  • 25倍ルールを適用すると、必要な資産は:
    • 12万円 × 25 = 300万円
  • 33倍ルールを適用すると:
    • 12万円 × 33 = 396万円(約400万円)

月額1万円を得るのために400万円の資産が必要、ということは、逆に見れば「月1万円の支出を減らすと、300〜400万円の資産を持っているのと同じ効果」といえる。

なぜこの考え方が重要なのか?

資産を300〜400万円増やすのは自明に大変だが、月1万円の削減なら比較的簡単である。見方を変え、月あたりの1万円には資産換算で400万円という価値があると感じることができれば浪費により強いブレーキをかけることができる。

逆に月1万円の収入には400万円の資産と同等の効果があるともいえる。キャッシュフローの重要さが身に染みる。

2025年 08月 30日

人間のファイナンシャルプランナーよりLLMのほうがマシだよね

ファイナンシャルプランナーって信用できないじゃないですか。信用できるかどうか判断するためには相手と同等以上の金融知識が必要になる。人間には利害関係があり、不動産屋と金融屋は特に倫理観がないので、利害関係がない分、たとえハルシネーションがあったとしてもLLMのほうがマシだと考える。

ということでMoneyForward ME の家計簿データをLLMに診断してもらうということを考える。

MoneyForward ME の家計簿データを一括でダウンロードするブックマークレット

https://gist.github.com/cho45/e0ed3e408f87ab9a5c29e2d612db82ab

過去1年分の取引履歴CSVデータを結合したものと、資産推移を以下のような形式で一括でダウンロードする。transactions 以下は構造化されていないCSVがそのまま入っている。

これ自体も Claude Code に指示しつつ書いたのでほぼ手は入れてない。

### JSON構造

```json
{
  "exportDate": "2024-08-29",
  "period": {
    "from": "2023-09",
    "to": "2024-08"
  },
  "monthlyAssets": [...],
  "transactions": {...}
}
```

### フィールド詳細

#### `exportDate`
- データを取得した日付(YYYY-MM-DD形式)

#### `period`
- `from`: 取得期間の開始月(YYYY-MM形式)
- `to`: 取得期間の終了月(YYYY-MM形式)

#### `monthlyAssets`
月末時点の資産データの配列。各要素の構造:
```json
{
  "date": "2024-08-31",
  "assets": {
    "現金・預金": 1000000,
    "投資信託": 500000,
    "株式": 300000,
    "その他": 50000
  }
}
```

#### `transactions`
取引履歴データ:
```json
{
  "format": "csv",
  "encoding": "utf-8",
  "successCount": 12,
  "failedMonths": [],
  "rowCount": 365,
  "data": "\"計算対象\",\"日付\",\"内容\",\"金額(円)\",\"保有金融機関\",\"大項目\",\"中項目\",\"メモ\",\"振替\",\"ID\"\n\"1\",\"2024/08/25\",\"給与 ABC株式会社\",\"300000\",\"△△銀行\",\"収入\",\"給与\",\"\",\"0\",\"abc123def456ghi789\"\n..."
}
```

- `successCount`: 正常に取得できた月数
- `failedMonths`: 取得に失敗した月のリスト
- `rowCount`: 取引データの行数(ヘッダー除く)
- `data`: 実際のCSVデータ(Shift-JISからUTF-8に変換済み、ダブルクォート付き)

### CSVデータフォーマット詳細

`transactions.data` に含まれるCSVは以下の構造です:

#### ヘッダー行
```
"計算対象","日付","内容","金額(円)","保有金融機関","大項目","中項目","メモ","振替","ID"
```

#### データ行の例
```
"1","2024/08/29","コンビニA店","-500","△△銀行","食費","食料品","","0","abc123def456"
"1","2024/08/28","給与 ABC株式会社","250000","△△銀行","収入","給与","","0","def456ghi789"
"1","2024/08/27","電力会社","-8500","○○カード","水道・光熱費","電気代","","0","ghi789jkl012"
"0","2024/08/26","証券会社 積立","30000","△△銀行","未分類","未分類","","1","jkl012mno345"
```

#### 各列の詳細

1. **計算対象**: 家計簿の計算に含めるかの判定(1=計算対象、0=計算対象外)
2. **日付**: YYYY/MM/DD形式の取引日
3. **内容**: 取引の説明文(店舗名、サービス名など)
4. **金額(円)**: 取引金額(収入は正数、支出は負数)
5. **保有金融機関**: 連携している金融機関名
6. **大項目**: MoneyForwardの大分類カテゴリ
   - 例:収入、食費、住宅、水道・光熱費、通信費、交通費、税・社会保障、日用品、健康・医療、教養・教育、趣味・娯楽、衣服・美容、その他、未分類
7. **中項目**: 大項目の下位分類
   - 例:給与、食料品、家賃・地代、電気代、ガス・灯油代、インターネット、電車、所得税・住民税など
8. **メモ**: ユーザーが手動で入力したメモ(空白の場合が多い)
9. **振替**: 振替取引の場合は1、通常取引は0
10. **ID**: MoneyForward内部のユニークな取引ID

LLMへ指示をするプロンプト

家庭状況によって必要な診断が異なるので、ここに関しては↑のフォーマット情報を利用してプロンプトを生成するコードをLLMに書かせるのがよいと思う。

例えば最終的なプロンプトが以下のようになるようなコード自体をLLMに書かせる。

あなたは家計の浪費防止に特化したファイナンシャルアドバイザーです。以下のデータを基に「無駄な支出の特定」と「浪費防止策」を重点的に分析してください。

【家庭状況】
- 世帯主: 38歳
- 家族構成: 配偶者(38歳)・子供1人 (11歳)
- 住居: 首都圏、賃貸マンション(家賃月xx万円)
- 投資方針: 積極的
- 交通: 電車中心

妻と自分とで完全に財布が別で、厳密ではないが支出種別ごとに支払い者が違います。
食費や服飾費が妻、それ以外は自分という分類です。

<以下フォーマットされた分析データ>
【資産推移分析】
...

【月別収支推移】
...

【カテゴリ別年間支出】
...

【異常値・変動分析】
...

**統計的外れ値(大口支出上位10件)**
...

**カテゴリ別支出分布統計**
...

【浪費防止分析を実施してください】
1. **異常値に基づく浪費特定**: 統計的外れ値や前月比異常値から無駄な大口支出を特定。重複パターンから習慣的な無駄を抽出
2. **変動性からのリスク分析**: 月別変動係数の高いカテゴリから予算コントロールが困難な項目を特定し、安定化策を提案
...

これを ChatGPT なり Gemini なり Claude にコピペする。データからわからない前提はしっかり書いておかないと適切にならない。