スクリーンショットをmarkdownに変換してクリップボードに貼り付けるAIツールを作る(あるいは、Apple Script事始め)
はじめに
Google、OpenAIの12月の発表で目立ったのがRealtimeでした。いま見ているディスプレイに対して即時回答する、AIと一緒にゲームする、視覚の代わりにする...
これらがこのタイミングで実現した主たる要因は、LLMの応答速度の飛躍的向上でしょう。
筆者も速度が原因で眠らせていたアイデアがいくつかあるので、今回はその一つ、スクリーンショットをmarkdownとしてコピペする機能をApple Scriptで作ってみました。
なお、この記事は下記のScrapの再構成です。試行錯誤の過程を辿りたい場合は参考にしてください。
🦂 本記事のスコープ
- Apple ScriptとGeminiを使い、スクリーンショットの内容をmarkdownにして、クリップボードに貼り付けるアプリを作成します。
- したがって、Windowsは対象外です。
- 筆者はApple Scriptの作成は今回が初めて。粗い部分があります。
🌳 環境
- MacBook Air M3
- MacOS:Sonoma 14.6
⚾️ 完成イメージ
たとえば、落合博満の成績テーブルをスクショすると...
以下のmarkdownがクリップボードに貼り付けされます。
年度 | 球団 | 試合 | 打席 | 打数 | 得点 | 安打 | 二塁打 | 三塁打 | 本塁打 | 塁打 | 打点 | 盗塁 | 盗塁死 | 犠打 | 犠飛 | 四球 | 敬遠 | 死球 | 三振 | 併殺打 | 打率 | 出塁率 | 長打率 | OPS |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1979 | 36 | 69 | 64 | 7 | 15 | 3 | 1 | 2 | 26 | 7 | 1 | 0 | 0 | 0 | 4 | 0 | 1 | 12 | 2 | .234 | .290 | .406 | .696 | |
1980 | 57 | 188 | 166 | 28 | 47 | 7 | 0 | 15 | 99 | 32 | 1 | 0 | 2 | 2 | 17 | 0 | 1 | 23 | 5 | .283 | .349 | .596 | .946 | |
1981 | 127 | 502 | 423 | 69 | 138 | 19 | 3 | 33 | 262 | 90 | 6 | 3 | 1 | 4 | 68 | 1 | 6 | 55 | 17 | .326 | .423 | .619 | 1.043 | |
1982 | ロッテ | 128 | 552 | 462 | 86 | 150 | 32 | 1 | 32 | 280 | 99 | 8 | 2 | 0 | 4 | 81 | 6 | 5 | 58 | 11 | .325 | .428 | .606 | 1.034 |
1983 | 119 | 497 | 428 | 79 | 142 | 22 | 1 | 25 | 241 | 75 | 6 | 5 | 0 | 3 | 64 | 5 | 2 | 52 | 14 | .332 | .419 | .563 | .982 | |
1984 | 129 | 562 | 456 | 89 | 143 | 17 | 3 | 33 | 265 | 94 | 8 | 1 | 0 | 4 | 98 | 8 | 4 | 83 | 14 | .314 | .436 | .581 | 1.017 | |
1985 | 130 | 568 | 460 | 118 | 169 | 24 | 1 | 52 | 351 | 146 | 5 | 1 | 0 | 4 | 101 | 26 | 3 | 40 | 16 | .367 | .481 | .763 | 1.244 | |
1986 | 123 | 522 | 417 | 98 | 150 | 11 | 0 | 50 | 311 | 116 | 5 | 1 | 0 | 1 | 101 | 19 | 3 | 59 | 15 | .360 | .487 [注12] | .746 | 1.232 | |
1987 | 125 | 519 | 432 | 83 | 143 | 33 | 0 | 28 | 260 | 85 | 1 | 4 | 0 | 4 | 81 | 10 | 2 | 51 | 10 | .331 | .435 | .602 | 1.037 | |
1988 | 130 | 557 | 450 | 82 | 132 | 31 | 1 | 32 | 261 | 95 | 3 | 4 | 0 | 6 | 98 | 13 | 3 | 70 | 11 | .293 | .418 | .580 | .998 | |
1989 | 130 | 559 | 476 | 78 | 153 | 23 | 1 | 40 | 298 | 116 | 4 | 3 | 1 | 6 | 75 | 7 | 1 | 69 | 11 | .321 | .410 | .626 | 1.036 | |
1990 | 中日 | 131 | 570 | 458 | 93 | 133 | 19 | 1 | 34 | 256 | 102 | 3 | 3 | 0 | 8 | 100 | 17 | 4 | 87 | 7 | .290 | .416 | .559 | .975 |
1991 | 112 | 478 | 374 | 80 | 127 | 17 | 0 | 37 | 255 | 91 | 4 | 2 | 0 | 5 | 95 | 16 | 4 | 55 | 9 | .340 | .473 | .682 | 1.155 | |
1992 | 116 | 481 | 384 | 58 | 112 | 22 | 1 | 22 | 202 | 71 | 2 | 3 | 0 | 6 | 88 | 8 | 3 | 74 | 12 | .292 | .422 | .526 | .948 | |
1993 | 119 | 504 | 396 | 64 | 113 | 19 | 0 | 17 | 183 | 65 | 1 | 2 | 0 | 8 | 96 | 14 | 4 | 69 | 13 | .285 | .423 | .462 | .885 | |
1994 | 129 | 540 | 447 | 53 | 125 | 19 | 0 | 15 | 189 | 68 | 0 | 0 | 0 | 6 | 81 | 4 | 6 | 56 | 13 | .280 | .393 | .423 | .815 | |
1995 | 巨人 | 117 | 483 | 399 | 64 | 124 | 15 | 1 | 17 | 192 | 65 | 1 | 0 | 0 | 8 | 73 | 2 | 3 | 87 | 17 | .311 | .414 | .481 | .895 |
1996 | 106 | 448 | 376 | 60 | 113 | 18 | 0 | 21 | 194 | 86 | 3 | 0 | 0 | 2 | 67 | 3 | 3 | 53 | 11 | .301 | .408 | .516 | .924 | |
1997 | 113 | 466 | 397 | 35 | 104 | 14 | 0 | 3 | 127 | 43 | 3 | 0 | 0 | 5 | 61 | 1 | 3 | 60 | 16 | .262 | .361 | .320 | .680 | |
1998 | 日本ハム | 59 | 192 | 162 | 11 | 38 | 6 | 0 | 2 | 50 | 18 | 0 | 1 | 0 | 2 | 26 | 0 | 2 | 22 | 12 | .235 | .344 | .309 | .652 |
通算:20年 | 2236 | 9257 | 7627 | 1335 | 2371 | 371 | 15 | 510 | 4302 | 1564 | 65 | 35 | 4 | 88 | 1475 | 160 | 63 | 1135 | 236 | .311 | .423 | .564 | .987 |
全体のフロー構成図は以下のとおりです。GeminiAPIのところは、画像解析ができる任意のAIで構いません。
🔧 準備: Google AI StudioからAPIキーを取得
どのLLMプロバイダーを使うかは自由ですが、リクエスト制限に達しない簡単なユースケースでは、Google AI StudioのGeminiが最適解と思います。無料で、画像認識も速度もトップクラスです。
ただし、無料の分、諸々の制限付きなので、目的に合わせて選択してください。
手順は簡単ですので、説明は省略します。
🦦 バックエンドAPIを作る
Apple Scriptで細かいことをしようとするとややこしいので、Gemini APIの前に中間のバックエンドAPIを立てます。このバックエンドの役割は以下です。
- Base64をApple Scriptから受け取る
- Gemini APIにリクエスト
- 回答をparse
- Apple Scriptに文字列で返す
今回のアプリで変更が起きやすいのはGemini APIへのリクエストなので、その部分をバックエンドに切り離しておくと、クライアントサイドのApple Scriptの修正をしなくて済みます。
回答を文字列で返すのもApple Scriptの負荷を減らすためですが、デメリットとして、エラー処理がうまくできません。本記事ではエラーメッセージもそのままクリップボードに貼り付けされる仕様です。[1]
ScrapではGoogle Apps Scriptを使って作りましたが、この記事では、Hono.js on Cloudflare Workersでサクッと作ります。
🔥 Hono.jsの作成
まずプロジェクトを作成します。
pnpm create hono@latest screenshot-to-markdown
続いてメインコードを追加。
import { Hono } from 'hono'
import { bearerAuth } from 'hono/bearer-auth'
import { logger } from 'hono/logger'
import { secureHeaders } from 'hono/secure-headers'
import { convertToMarkdown } from './convertToMarkdown'
import { HTTPException } from 'hono/http-exception'
interface Secrets {
Bindings: {
BEARER_AUTH: string,
GEMINI_API_KEY: string
}
}
const app = new Hono<Secrets>()
app.use(logger())
app.use(secureHeaders())
app.use(bearerAuth({
verifyToken: async (token, c) => {
if (!c.env.BEARER_AUTH) {
console.error('BEARER_AUTH is not set')
return false
}
return token === c.env.BEARER_AUTH
},
}))
app.post('/', async (c) => {
if (c.env.GEMINI_API_KEY === undefined) {
throw new Error('GEMINI_API_KEY is not set')
}
const { image: base64 } = await c.req.json()
const markdown = await convertToMarkdown(base64, c.env.GEMINI_API_KEY)
return c.text(markdown)
})
app.onError((err, c) => {
if (err instanceof HTTPException) {
switch (err.status) {
case 400:
return c.text("Bad Request", 400)
case 401:
return c.text("Unauthorized", 401)
default:
return c.text("Internal Server Error", 500)
}
}
console.error(err)
return c.text("Internal Server Error", 500)
})
export default app
以下がGemini APIへのアクセス。ササっと作るためライブラリは使わず、戻り値もキャストしてしまっています。
モデルには最新のgemini-2.0-flash-exp
を使っています。日本語OCRもかなりの精度です。
export async function convertToMarkdown(base64: string, apiKey: string): Promise<string> {
const SYSTEM_PROMPT = `あなたはスクリーンショットをmarkdownに変換するアシスタントです。以下に注意して変換してください。
- 画像内の文字を読み取ってmarkdownに変換してください。その際、必要に応じてmarkdown tableやlist、boldなどの記法を使い、なるべく画像内の文字を忠実に再現するように努めてください。
- 特にテーブルやリストでうまく表現できる場合は積極的に使ってください。
- 画像に文字がない場合は、空文字("")を返してください。
- 回答形式は、変換したmarkdownのみとしてください。前置きや解説は不要です。`
const endpoint = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=${apiKey}`;
const payload = {
"contents": {
"role": "USER",
"parts": [
{
"inlineData": {
"data": base64,
"mimeType": "image/png"
}
},
{
"text": "スクリーンショットをmarkdownに文字起こししてください。"
}
]
},
"system_instruction": {
"parts": [
{
"text": SYSTEM_PROMPT,
}
]
},
"generation_config": {
"temperature": 0,
}
};
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Failed to fetch: ${response.statusText}`);
}
const json = await response.json() as {
candidates: {
content: {
parts: {
text: string;
}[];
};
}[];
};
const output = (json.candidates && json.candidates.length > 0 &&
json.candidates[0].content && json.candidates[0].content.parts &&
json.candidates[0].content.parts.length > 0)
? json.candidates[0].content.parts[0].text
: "";
console.log(output);
return output;
}
Secretの追加
- BEARER_AUTH
- GEMINI_API_KEY
をシークレットとして使っています。wranglerを使って以下で登録できます。
$npx wrangler secret put BEARER_AUTH
$npx wrangler secret put GEMINI_API_KEY
デプロイ。
$pnpm run deploy
これで完成です。
🍎 Apple Scriptの作成
🤖 Automatorの起動
今回、Apple Scriptを任意のショートカットキーで実行できるように、Automator経由で作成します。
「アプリケーション」フォルダ
クイックアクションから作成。
検索窓から「AppScriptを実行」を選択。
この後、ここにApple Scriptを貼り付けます。
🍏 Apple Script
Apple Scriptは以下のようになります。
最初の変数でバックエンドAPIのURL、Tokenをセットしてください。
try
-- 変数設定
set {tempFolder, tempPath, apiURL, token} to {¬
(do shell script "echo $TMPDIR"), ¬
((do shell script "echo $TMPDIR") & "temp_screenshot.png"), ¬
"<バックエンドAPIのURL>", ¬
"<登録したSecret Token>"}
-- スクリーンショット取得(選択範囲指定モード)
do shell script "screencapture -i " & quoted form of tempPath
-- スクリーンショットが取得されたか確認
set fileExists to do shell script "[ -f " & quoted form of tempPath & " ] && echo 'yes' || echo 'no'"
if fileExists is "no" then
error "スクリーンショットの取得に失敗しました。選択範囲の指定をキャンセルした可能性があります。"
end if
-- 画像をリサイズ(Base64のデータ量削減のため)
do shell script "sips -Z 600 " & quoted form of tempPath
-- 画像をbase64エンコード
set base64Data to do shell script "base64 < " & quoted form of tempPath
-- JSONデータの作成
set jsonData to "{\"image\":\"" & base64Data & "\"}"
-- curlでPOSTリクエストを実行
set curlCommand to "curl -L -H \"Content-Type: application/json\" -H \"Authorization: Bearer " & token & "\" -d " & quoted form of jsonData & " " & apiURL
-- リクエスト実行と結果取得
set response to ""
try
set response to do shell script curlCommand
-- レスポンスが空でないかチェック
if response is "" then
error "APIからのレスポンスが空です。"
end if
-- レスポンスをクリップボードにコピー
set the clipboard to response
-- 成功通知
display notification "Markdownをクリップボードにコピーしました。" with title "変換成功" sound name "Glass"
on error errorMessage
error "APIリクエストに失敗しました。: " & errorMessage
end try
-- 一時ファイルの削除
do shell script "rm -f " & quoted form of tempPath
on error errorMessage
-- エラー通知
display notification "エラーが発生しました: " & errorMessage with title "エラー" sound name "Basso"
end try
完成したらAutomatorに貼り付け、「⌘+S」で保存してください。
🍰 ショートカットの割り当て
Spotlightなどで「設定 > キーボードショートカット」を開き、「サービス > 一般」に作ったクイックアクションがあるのでキーボードを割り当てられます。
画面収録の許可
スクリプトエディタおよびスクショを撮るアプリに画面収録の許可が必要です。
「プライバシーとセキュリティ」から許可します。
🧪 テスト
これで完成です。テストしてみましょう。
割り当てたショートカットキーでMacBook Air M3のスペック表をコピペしてみます。
結果は下記でした。
M3 | M3 | M3 |
8コアCPU | 8コアCPU | 8コアCPU |
10コアGPU | 10コアGPU | 10コアGPU |
16GBユニファイドメモリ | 16GBユニファイドメモリ | 24GBユニファイドメモリ |
256GB SSDストレージ¹ | 512GB SSDストレージ¹ | 512GB SSDストレージ¹ |
16コアNeural Engine | 16コアNeural Engine | 16コアNeural Engine |
True Tone搭載15.3インチLiquid Retinaディスプレイ¹ | True Tone搭載15.3インチLiquid Retinaディスプレイ¹ | True Tone搭載15.3インチLiquid Retinaディスプレイ¹ |
1080p FaceTime HDカメラ | 1080p FaceTime HDカメラ | 1080p FaceTime HDカメラ |
MagSafe 3充電ポート | MagSafe 3充電ポート | MagSafe 3充電ポート |
Thunderbolt/USB 4ポート×2 | Thunderbolt/USB 4ポート×2 | Thunderbolt/USB 4ポート×2 |
最大2台の外部ディスプレイに対応(ノートブックを閉じた状態) | 最大2台の外部ディスプレイに対応(ノートブックを閉じた状態) | 最大2台の外部ディスプレイに対応(ノートブックを閉じた状態) |
Touch ID搭載Magic Keyboard | Touch ID搭載Magic Keyboard | Touch ID搭載Magic Keyboard |
感圧タッチトラックパッド | 感圧タッチトラックパッド | 感圧タッチトラックパッド |
デュアルUSB-Cポート搭載35Wコンパクト電源アダプタ | デュアルUSB-Cポート搭載35Wコンパクト電源アダプタ | デュアルUSB-Cポート搭載35Wコンパクト電源アダプタ |
Apple Intelligenceのために設計⁴ | Apple Intelligenceのために設計⁴ | Apple Intelligenceのために設計⁴ |
良いですね。
コピペできないApple Musicの曲目も、文字起こしできます。
ちなみにこれはラトル/ベルリンフィル
Symphony No. 7 in E Minor
作曲者: グスタフ・マーラー
1. I. Langsam (Adagio) - Allegro risoluto, ma non troppo + 21:33
2. II. Nachtmusik I. Allegro moderato 14:48
3. III. Scherzo. Schattenhaft - Trio 10:21
4. IV. Nachtmusik II. Andante amoroso 12:05
5. V. Rondo-Finale. Tempo I (Allegro ordinario) - Tempo II (Allegro moderato ma energico) 17:21
テーブルにしてもらえるともっと良かったのですが、バックエンドのプロンプトの調整でなんとかなるでしょう。
注意点として、画面収録を許可していないアプリのスクショだと空のレスポンスが返ってきます。この辺りのきめ細かいエラー処理をしたい場合は、バックエンドAPIの戻り値をerror
などの項目を持つJSONにしてあげる必要があります。
🍙 結び
少し前までは、日本語のOCRの精度の問題とレスポンス速度がネックになっていたのですが、gemini-2.0-flash-exp
だともう気にならないレベルです。近い将来、ローカルLLMが搭載されれば、もっとやりやすくなるでしょう。
構成もバックエンドAPIを置いてあげることで、Apple Scriptの部分は最小限で済み、好きな言語、ホスティングサービスで開発できました。
Apple Scriptは全くの初心者でしたが、LLMを使えば個人用では十分な精度で返してくれました。ぜひお試しください。
🧀 おまけ: ダイアログを使って任意のプロンプトを送る
もう少し汎用にしたい場合、ダイアログを使ってユーザーの入力も受け付けられます。
ファイルサイズの都合でガビガビですが、アルバムのジャケットをスクショし、「何が写っていますか?」と入力しています。
結果は、以下の通りでした。
アンドラーシュ・シフによるJ.S.バッハの「ゴルトベルク変奏曲」のアルバムカバーです。カバーは、赤い背景に青い線で三角形が描かれています。
実装は難しくなく、
display dialog "AIへの指示を入力してください:" default answer ""
set userInput to text returned of the result
-- JSONデータの作成
set jsonData to "{\"image\":\"" & base64Data & "\", \"prompt\":\"" & userInput & "\"}"
とダイアログを出し、JSONに追加、バックエンドで受け取ってGeminiに渡すだけです。
-
Apple Script側でJSONをparseしたい場合、jqやPython、Rubyなどを使って行います。jqはinstallが必要、Python, Rubyはデフォルトで入っていますが、懸念要素が増えます。 ↩︎
Discussion