はじまり:面倒な名刺管理をAIで効率化する
きっかけはシンプルでした。
「名刺の管理、面倒くさい…」「スキャンまでしたけど、後処理が…」。
そこで、画像認識に強い LLM(大規模言語モデル) を活用して、名刺データを半自動でデジタル化するCLIツールの開発を始めました。
最初のゴールは2つです。
- 名刺画像からLLMで情報を抽出する
- その結果をObsidianで扱いやすいMarkdownに保存する
こんな感じで動く、クールなツール(当社比)になりました。
CLIでLLMを操作できるllm
コマンドとコアに、シェルスクリプトで作成しました。
Step 1: LLMの対話をスクリプト化
まずは、llm
コマンドを使って名刺画像を入力し、テキスト化するところから。コマンドをインストールしてAPIキーを設定します。
検討の結果、gemini-2.5-flash-lite
モデルを採用し、Markdown形式で必要な情報を素早く出力できるようになりました。
llm \
-m gemini-2.5-flash-lite \
-a ./test_img.jpg \
"あなたは添付された名刺画像から情報を正確に読み取り、Obsidianで管理するためのMarkdownノートを生成する専門家です。以下の「出力フォーマット」と「厳守事項」に完璧に従って、純粋なMarkdownノートを1つだけ生成してください。
### 出力フォーマット
---
type: business-card
date: $(date +'%Y-%m-%d')
company: <会社名を記述>
name: <氏名を記述>
head: <nameの頭文字。日本語名はひらがな1文字、英語名は英大文字1文字>
---
## 概要
(氏名、会社名、役職などを1〜2行で簡潔に要約)
## 連絡先
- **Email**: <メールアドレスを記述>
- **Phone**: <電話番号を+81形式で記述>
- **Mobile**: <携帯電話番号を+81形式で記述>
- **Address**: <住所を記述>
- **Web/SNS**: <ウェブサイトやSNSのURLを記述>
## 接点・メモ
- **役職**: <部署名と役職を記述>
- **備考**: <その他、名刺から読み取れる特記事項を記述>
---
### 厳守事項
1. **純粋なMarkdownのみ出力**: **絶対に、解説、前置き、後書き、コードブロック(\`\`\`)で囲まないでください。**
2. **テンプレート厳守**: 上記「出力フォーマット」の構造(YAMLのキーと順序、本文の見出し)を絶対に変更しないでください。
3. **欠損情報の明記**: 読み取れない項目は、値の部分に **『不明』** と正確に記述してください。
4. **曖昧な情報の表現**: 確信が持てない、または複数の候補がある場合は **(候補: ...)** や **(推定)** を使って表現してください。
5. **固定値のルール**: 'type'は'business-card'に必ず固定してください。"
- このllmコマンドを核にしてブラッシュアップしていきます
- gemini-2.5-flash-liteで、個人で使う名刺管理ならfree tierで間に合いそうです
この手順を自動化するため、処理をまとめたシェルスクリプトbc2md.sh
を作成。複数の名刺画像を引数で一括処理できるようにし、実用性がぐっと高まりました。(最終版コードは後述)
Step 2: 実用的なツールへ進化
核となるllmコマンドはできたので、日常運用に耐えるための改善を追加していきます。
ファイル名の自動生成
固定ファイル名では上書きされてしまうため、会社名と氏名を抽出して「日付_会社名_氏名.md」というルールで保存する仕組みに変更。ひとめで誰の名刺かわかるようになりました。
名刺画像の埋め込み
テキストだけでなく画像そのものもObsidianで見たいですよね。そこで既存のアップロード用Pythonスクリプトと連携し、次の機能を追加しました。
-
ImageMagick
で縦横比を判定し、Obsidian用に最適な表示サイズを自動設定 -
basename
でファイル名を整え、Markdownの見た目をきれいに保持
Step 3: nnn
と連携してワークフロー完成
次の課題は「ターミナルを開いてコマンドを打つ」手間。理想は普段使っているファイルマネージャから直接実行できることです。
そこで、CLIファイルマネージャnnn
用のプラグインobscard.sh
を作成しました。
- 複数選択(
$NNN_SEL
)と単一選択($1
)の両対応 -
file
コマンドで画像ファイルかを事前チェックし、誤操作を防止
これで「nnn
でファイルを選ぶ → キー2発で処理実行」という理想的なフローが実現しました。
obscard(最終版)のコードを見る
#!/usr/bin/env bash
set -euo pipefail
# ▼▼▼ 設定項目 ▼▼▼
# あなたが作成した名刺管理スクリプトのフルパスをここに指定してください
BC2MD_SCRIPT="/path/to/bc2md.sh"
# ▲▲▲ 設定はここまで ▲▲▲
# nnnの選択ファイルリストを取得
sel=${NNN_SEL:-${XDG_CONFIG_HOME:-$HOME/.config}/nnn/.selection}
targets=()
# 複数ファイルが選択されている場合
if [ -s "$sel" ]; then
# 選択ファイルリストからファイルパスを読み込み、targets配列に格納
while IFS= read -r -d '' i || [ -n "$i" ]; do
targets+=( "$i" )
done < "$sel"
# ファイルが選択されていない場合
else
# 最初の引数(カーソル下のファイル)を処理対象にする
# 引数が存在し、かつそれが通常のファイルであることを確認
if [ -f "$1" ]; then
targets=("$1")
else
echo "Error: The file under the cursor in nnn is invalid or no file was specified." >&2
read -r -n1
echo
exit 1
fi
fi
# チェック処理
for target_file in "${targets[@]}"; do
if ! file "$target_file" | grep -qE 'image|bitmap'; then
echo "Error: The selected files include non-image items." >&2
echo " -> $(basename "$target_file")" >&2
read -r -n1
echo
exit 1
fi
done
# --- メイン処理 ---
# 選択されたすべてのファイルを引数として名刺管理スクリプトを実行
# "${targets[@]}" により、ファイルパスが正しくクォートされて渡される
echo "--- Launching business card management script ---"
echo "Generating Markdown file from image data..."
"$BC2MD_SCRIPT" "${targets[@]}"
echo "--- Business card management script completed ---"
# nnnの選択をクリアする (obslinkforgeと同じ)
if [ -s "$sel" ] && [ -p "$NNN_PIPE" ]; then
printf "-" > "$NNN_PIPE"
fi
# 終了前に待機
echo
printf "Processing completed. Press any key to return to nnn..."
read -r -n1
echo
exit 0
Step 4: 安定性と仕上げ
最後に、ツールを壊れにくくする仕上げを行いました。
-
エラーハンドリング:
set -e
で止まる問題を|| true
で回避 - 環境固定: 特定のPython実行パスを明示し、依存関係の不具合を解消
- プロンプト改善: 「株式会社」などの法人格削除をプロンプトに任せ、スクリプトをシンプル化
bc2md.sh(最終版)のコードを見る
#!/usr/bin/env bash
set -euo pipefail
# ▼▼▼ 設定項目 ▼▼▼
PYTHON_EXEC="/opt/homebrew/bin/python3.9"
UPLOADER_SCRIPT="/path/to/myscript.py"
# ▲▲▲ 設定ここまで ▲▲▲
if [ "$#" -lt 1 ]; then
echo "usage: $0 image1.jpg [image2.jpg ...]" >&2
exit 1
fi
TMP_OUT="card_info.tmp.md"
attach_opts=()
for img in "$@"; do
attach_opts+=(-a "$img")
done
TODAY="$(date +'%Y-%m-%d')"
read -r -d '' PROMPT <<EOF || true
あなたは添付された名刺画像から情報を正確に読み取り、Obsidianで管理するためのMarkdownノートを生成する専門家です。以下の「出力フォーマット」と「厳守事項」に完璧に従って、純粋なMarkdownノートを1つだけ生成してください。
### 出力フォーマット
---
type: business-card
date: ${TODAY}
company: <会社名を記述。株式会社や合同会社などの法人格は含めない。できるだけ日本語で>
name: <氏名を記述。姓と名のあいだは半角スペースを空ける。できるだけ日本語で>
head: <nameの頭文字。日本語名は名字のひらがな1文字、英語名は英大文字1文字>
---
## 概要
(氏名、会社名、役職などを1〜2行で簡潔に要約)
## 連絡先
- **Email**: <メールアドレスを記述>
- **Phone**: <電話番号を+81形式で記述>
- **Mobile**: <携帯電話番号を+81形式で記述>
- **Address**: <住所を記述>
- **Web/SNS**: <ウェブサイトやSNSのURLを記述>
## 接点・メモ
- **役職**: <部署名と役職を記述>
- **備考**: <その他、名刺から読み取れる特記事項を記述>
---
### 厳守事項
1. 純粋なMarkdownのみ出力: 絶対に、解説、前置き、後書き、コードブロック(\`\`\`)で囲まない。
2. テンプレート厳守: 上記「出力フォーマット」の構造(YAMLのキーと順序、本文の見出し)を絶対に変更しない。
3. 欠損情報の明記: 読み取れない項目は、値の部分に『不明』と正確に記述する。
4. 日本語が使える場合は日本語を用いる(氏名、会社名、役職名など全般的に)。
5. 曖昧な情報の表現: 確信が持てない、または複数候補がある場合は (候補: ...) や (推定) を使って表現する。
6. 固定値のルール: type は必ず business-card に固定する。
EOF
echo "Extracting business card info via LLM..."
llm \
-m gemini-2.5-flash-lite \
"${attach_opts[@]}" \
"$PROMPT" \
> "$TMP_OUT" || true
awk -v d="$TODAY" 'BEGIN{done=0}
{
if (!done && /^date:[[:space:]]/) { print "date: " d; done=1 }
else print
}' "$TMP_OUT" > "${TMP_OUT}.tmp" && mv "${TMP_OUT}.tmp" "$TMP_OUT"
# 先に画像をアップロードして一時ファイルに追記
echo "Uploading images and embedding links..."
echo "" >> "$TMP_OUT" # 追記先を $FINAL_OUT から $TMP_OUT に変更
for img in "$@"; do
echo "Uploading: ${img}"
image_url=$("$PYTHON_EXEC" "$UPLOADER_SCRIPT" "$img")
if [ -n "$image_url" ]; then
# ----------------------------------------------------------------
# 画像の縦横を判別してリンク形式を切り替える
# ImageMagickのidentifyコマンドで画像の「幅 高さ」を取得
dimensions=$(identify -format "%w %h" "$img")
read -r width height <<< "$dimensions" # 取得した値をwidthとheight変数に分割
# 高さが幅より大きい場合(縦長画像)
if [ "$height" -gt "$width" ]; then
# 縦長用のMarkdownリンクを追記
echo "" >> "$TMP_OUT"
else
# 横長または正方形用のMarkdownリンクを追記
echo "" >> "$TMP_OUT"
fi
# 修正ここまで
# ----------------------------------------------------------------
echo " -> Link appended."
else
echo " -> Failed to retrieve URL."
fi
done
# ファイル名を生成してリネーム
COMPANY_NAME=$(grep '^company:' "$TMP_OUT" | sed 's/company: //')
PERSON_NAME=$(grep '^name:' "$TMP_OUT" | sed 's/name: //')
COMPANY_NAME_SAFE=$(echo "$COMPANY_NAME" | tr -d '[:space:]')
PERSON_NAME_SAFE=$(echo "$PERSON_NAME" | tr -d '[:space:]')
DATE_FOR_FILENAME=$(date +'%y%m%d')
FINAL_OUT="${DATE_FOR_FILENAME}_${COMPANY_NAME_SAFE}_${PERSON_NAME_SAFE}.md"
mv "$TMP_OUT" "$FINAL_OUT"
echo "Business card note created: $FINAL_OUT"
リンク集
- Obsidian公式ページ: https://obsidian.md/
環境
以下のバージョンで実施しました。
- Obsidian v1.9.10
- macOS Sequoia 15.5
- MacBook Pro (14-inch, 2024)
まとめ
アウトプットされたmdファイルを、Obsidianの特定フォルダに入れたら、PCでもスマホでも見られる名刺データベースに。Basesを使おうかと思ってたんですが、とりあえずは要らなさそう。
「面倒な名刺入力をどうにかしたい」という小さな発想から始まった試みは、実務に耐えうる、堅牢で便利なワークフローに進化しそうになってきました。