
Article
家庭内の写真・動画をローカルAIで検索できるようにする GX10 Gallery開発記
GX10を使って、家庭内の写真と動画をローカルAIで検索できるようにするメディアライブラリを作り始めた。
名前は仮にGX10 Galleryと呼んでいる。やっていることは、外付けストレージに入っている写真と動画を読み込み、EXIF、撮影日時、GPS由来の大まかな場所、動画のキーフレーム、音声認識、顔識別、Qwenによる説明文をまとめてインデックス化することだ。Web UIから検索したり、イベント単位で見返したり、自動で短いVlog風の動画を作ったりできるようにしたい。
最初から完成形が見えていたわけではない。きっかけはもっと素朴で、家族の写真と動画が大量にあるのに、いざ探そうとすると見つからない、という問題だった。
写真アプリのライブラリ、カメラのRAW、スマホ動画、ミラーレスの動画、動画編集用の素材。イベント単位では頭の中にあるのに、実際のファイルは複数のフォルダに分かれている。撮影日で探せることもあるが、「水族館イベントで子どもがショーを見ている動画」「旅行イベントで家族メンバーが歩いている場面」「キャンプイベントで料理をしている写真」みたいな探し方は難しい。
そこで、写真と動画を一度全部テキスト化して、検索できるようにしようと思った。
クラウドAIでやると怖い量
この手の処理は、いまならクラウドAI APIでもできる。
写真を1枚ずつ画像モデルに投げる。動画からフレームを切り出して投げる。音声をSTTにかける。説明文をembeddingにする。人物らしきものを整理する。やろうと思えばできる。
ただ、家庭内のメディアは量が多い。しかも、一回処理して終わりではない。
実装中は何度もやり直す。プロンプトを直したら再処理したい。GPS由来の場所名を追加したら、Qwenの説明文にも反映したい。人物ラベルを付けたら、「幼い子ども」ではなく「人物Aが写っている」と説明を更新したい。動画生成の品質を上げるために、クリップ候補をもう一度評価したい。
こうなると、クラウドAPIの従量課金はかなり怖い。画像、動画、音声、embeddingを大量に回し、さらに試行錯誤で再実行する。個人の写真や動画を外に出す抵抗もある。
GX10を使う理由はここにある。ローカルで回せば、コストは主に電気代と時間になる。個人メディアを外部APIに投げずに済む。そして失敗しても、もう一度回せる。
これはローカルAIのかなり実用的な使い道だと思う。
全体構成
構成は次のように分けた。
外付けストレージはソースとして読むだけにした。インデックス、キャッシュ、Markdown、生成動画はMac側の別の場所に書く。元の写真や動画を壊さないためだ。
実装言語はGoにした。理由はかなり実務的で、CLI、HTTP API、SQLite、ファイル走査、ffmpegの呼び出しを一つのバイナリにまとめやすいから。凝ったフレームワークを使うより、ローカルツールとして堅く動くことを優先した。
DBはSQLite。大量データだから最初は少し迷ったが、単一ユーザーのローカルメディアライブラリで、読み書きも基本的にはこのプロセスが握る。検索にはFTSを使える。まずはSQLiteで十分と判断した。
GX10側は、Qwenによる画像・動画フレーム理解、STT、embedding、顔検出を担当する。Mac側から見ると、必要なタイミングでGX10にHTTPで問い合わせるだけだ。重い推論をGX10に逃がし、ファイルI/Oや動画レンダリングはMac側で進める。この分担がかなり扱いやすい。
イベント単位で扱う
写真と動画は、完全に同じ構造で保存されているわけではない。
写真は写真用のフォルダ、動画は動画用のフォルダに分かれている。日付が同じでもイベント名が少し違うことがある。旅行のように、写真側は大きなセッションで、動画側は日付ごとのフォルダになっていることもある。
最初は、写真と動画を撮影日時で細かくマッチングすることも考えた。ただ、今回の目的はまず「イベントとして見返せること」なので、イベント単位で扱うことにした。
イベントには、写真rootと動画rootを持たせる。両方あるイベントもあれば、写真だけ、動画だけのイベントもある。今後、同日や前後日の似たフォルダを候補として出して、マッチ漏れを補正する余地はあるが、まずはイベント単位で十分だ。
ここで大事なのは、イベント名をそのまま公開情報にしないことだ。実装内部では元データと対応付ける必要があるが、記事や公開用の図では水族館イベント、旅行イベント、キャンプイベント、公園イベントのような丸めた名前で扱う。
インデックスの中身
各メディアごとに、次のような情報を持つ。
metadata:
ファイル種別
撮影日時
サイズ
解像度
動画の長さ
カメラ情報
location:
EXIF GPS
逆ジオコーディングした大まかな場所名
vision:
写真サムネイルの説明
動画キーフレームの説明
audio:
動画の短い音声区間から起こしたSTT結果
people:
顔検出
顔クラスタ
匿名化した人物ラベル
search:
SQLite FTS
embedding
写真はサムネイルをQwenに見せて説明文を作る。
動画は全フレームを見るわけではない。キーフレームを数枚切り出し、短い音声区間をSTTにかけ、メタデータと合わせて説明文を作る。全フレームを見ると品質は上がるかもしれないが、処理時間が現実的ではない。まずは「探せる」ことを優先している。
人物ラベルも同じで、公開記事では実名や生のPeopleラベルは出さない。内部では検索に使えるようにするが、外向けには人物A、人物B、家族メンバー、子どもくらいに丸める。これは単なる表記の問題ではなく、このプロジェクト全体の前提だ。家庭内メディアを扱うなら、検索しやすさと公開時の安全性を分けて考えないと危ない。
Web UIを作る
CLIだけでもインデックスは作れるが、メディアライブラリとして使うにはWeb UIが必要だった。
Web UIではイベント一覧を見られる。イベントを開くと、写真と動画のカードが並ぶ。動画はその場でプレビューできる。検索結果にもサムネイルや動画プレビューを出す。
検索は、単にファイル名を探すのではなく、Qwenが作った説明文、STT、場所名、人物ラベル、タグをまとめて見る。たとえば「水族館 ショー」「夜 風船」「キャンプ 料理」のような検索ができる。
この体験ができるだけで、普通のフォルダ管理とはかなり違う。
さらに、Web UIはただの閲覧画面ではなく、後続処理の入口にもなる。People画面で人物ラベルを確定する。Storyboardでイベント動画の候補を見る。Renderキューで動画生成の状態を見る。処理途中のイベントは、必要なコンテキスト更新が終わるまでレンダリングを止める。
このあたりは、写真管理アプリというより、ローカルAIの処理基盤に近い。
ローカルAI専用機としてのGX10
GX10側では、Qwen、embedding、STT、顔検出をOpenAI互換に近いAPIとして立てている。
実装側から見ると、MacはGX10にHTTPで問い合わせるだけだ。Qwenに画像を渡す。STTに音声を渡す。embeddingを取る。顔検出にサムネイルを渡す。
この分離は扱いやすい。Macは普段の作業マシンでもあるので、重いAI推論をGX10に逃がせる。一方で、ファイルI/Oや動画レンダリングはMac側でやった方が楽だ。
そして何より、試行錯誤しやすい。
実際、プロンプトやロジックは何度も変えた。写真の説明に余計な編集素材っぽい文言が入ることがあり、それを取り除いた。横倒しや天地逆転の動画クリップをQwenで落とすようにした。人物ラベルや場所名が後から入ったら、Qwen説明文だけを再実行する仕組みも入れた。
この「失敗して直して再処理する」流れは、ローカルAIでないと心理的にやりにくい。クラウドAPIでも技術的にはできるが、個人データの扱いと再実行コストを毎回考えることになる。GX10をローカルAI専用機として置くと、この試行錯誤の摩擦がかなり下がる。
現時点の到達点
現時点では、次のところまで動いている。
- 写真と動画のイベント単位スキャン
- EXIF、撮影日時、GPS情報の取得
- GPSから大まかな場所名への変換
- Qwenによる写真説明、動画キーフレーム説明
- 動画音声のSTT
- SQLite FTSとembeddingによる検索
- Web UIでのイベント一覧、検索、プレビュー
- 顔検出、人物クラスタ、匿名化した人物ラベル
- 人物ラベルや場所名が変わった時のQwen再説明
- イベント動画のドラフト作成とレンダリング
- HLSによるWeb再生
まだ完成ではない。写真と動画のイベントマッチングは改善余地がある。動画生成も、プロの編集のようなテンポやストーリーにはまだ届かない。人物識別も、ラベル付けの運用をもう少し良くしたい。
それでも、家庭内メディアを「AIで検索できるライブラリ」にする土台としてはかなり見えてきた。
GX10を買ってローカルLLMを動かすだけだと、「速い」「遅い」「このモデルは動く」という話で終わりがちだ。でも、個人の大量データを扱うと、ローカルAIの価値はかなり分かりやすくなる。外に出したくない。何度も処理し直したい。検索できるようにしたい。この条件が揃うと、ローカルで回せること自体が機能になる。
次回は、実際にどうやって写真と動画をテキスト化し、人物ラベルや場所名まで検索に入れているのかを書く。


