🗓️

[Raycast] 選択テキストをGoogleカレンダーの予定に変換する拡張機能を作ってみた (OpenAI API)

2024/11/30に公開

Mac用のランチャーアプリ Raycast の拡張機能を自作しました。
https://www.raycast.com/izm51/ai-text-to-calendar

SNSやウェブサイト上で見つけたイベント情報を忘れないようにしたいですが、手動でカレンダーに登録するのは手間がかかってしまい、結局保存しないまま忘れてしまうことはありませんか?この拡張機能を使えば、テキストで記載されたイベント情報を簡単にGoogleカレンダーに登録することができます!

今回作成したコードはこちらです。
https://github.com/raycast/extensions/pull/15605/files

作ったもの

今回開発したのは、SNSやウェブサイト上で見つけたイベント情報をGoogleカレンダーに簡単に登録するためのRaycastの拡張機能です。日時や場所が記載されたテキストを選択して拡張機能を実行すると、OpenAIのAPIによって予定情報を抽出します。そして、表示されるGoogleカレンダーへの登録画面から予定を登録することができます。

① イベント情報のテキストを選択する

② Raycastを開き、今回作成した拡張機能 "AI Text to Calendar" を実行する

③ ブラウザにGoogleカレンダーへの登録画面が表示されるので、内容を確認して保存する

(※ この拡張機能を使うためには、利用者各自がOpenAIのAPIキーを取得して、Raycastの設定画面でキーを登録する必要があります。)

作成ログ

プロジェクトフォルダの作成

Raycastの拡張機能は、TypeScriptで実装することができます。

まずはプロジェクトフォルダを作成します。
プロジェクトフォルダは、Raycastの Create Extension コマンドで作成することができます。

このとき、いくつかのサンプルコードがテンプレートとして用意されています。今回は Run Script のテンプレートを使用して作り始めてみました。

Create Extension (Cmd+Enter) を実行すると、指定したディレクトリにプロジェクトが作成されます。そのディレクトリで下記のコマンドを実行すると、リアルタイムでコードをビルドしてRaycastに反映されるようになり、動作を試すことができます。

npm install && npm run dev

実装①:選択中のテキストを利用する

まずは、選択しているテキストを拡張機能側で利用できるようにしました。以下のようにすると、選択中のテキストを取得し、HUDで表示することができました。

import { getSelectedText, showHUD } from "@raycast/api";

export default async function main() {
  const selectedText = await getSelectedText();
  await showHUD(selectedText);
}

ドキュメント: https://developers.raycast.com/api-reference/environment#getselectedtext

実装②:OpenAIのAPIへの問い合わせ

取得したテキストから予定の情報を抽出します。テキストの解析にはOpenAIのAPIを使いました。

OpenAIのAPIへのリクエストには openai のライブラリを使用します。

import { getPreferenceValues } from "@raycast/api";
import OpenAI from "openai";

function openAiRequest(text: string, language: string) {
  const openaiKey = getPreferenceValues<Preferences>().openAiApiKey;
  const openai = new OpenAI({ apiKey: openaiKey });
  const systemMessage = "{後述するため省略}"
  const response = await openai.chat.completions.create({
    model: "gpt-4o-mini",
    response_format: { type: "json_object" },
    messages: [
      { role: "system", content: systemMessage },
      { role: "user", content: text },
    ],
    temperature: 0.2,
    max_tokens: 256,
  });

  return response.choices[0].message.content;
}

ここで、OpenAIのAPIキー openaiKey の保存方法に迷いました。
そこで、OpenAIを使用している拡張機能のソースコードを参考にしました。
https://github.com/raycast/extensions/blob/b5ee7a0e3ad9096489dc966110cb4736118bc607/extensions/openai-gpt/package.json#L19-L32

上記のソースコードを参考に、package.jsonpreferences の必須の設定項目として指定しておくと、初回の起動時に自動で入力画面が表示されるようになりました。

(初回以降も、RaycastのExtension設定画面で値を変更することができます。)

OpenAIを利用するときのプロンプトはこのようにしています。
Googleカレンダーに登録するためのURLに直接変換することも試しましたが、意図しない形式で返ってくることが多くプロンプトの作成が難しかったです。なので、予定情報をJSON形式で返却してもらうことにしています。

const systemMessage = `\
ユーザーから与えられた文章から、日程情報を抽出してください。
出力は次のようなJSON形式としてください。

{
  title: string, // 予定のタイトル
  start_date: YYYYMMDD, // 開始日
  start_time: hhmmss, // 開始時刻
  end_date: YYYYMMDD, // 終了日
  end_time: hhmmss, // 終了時刻
  details: string, // 100文字以内で要約。URLは文字数に関わらず優先的に残す
  location: string // 開催場所
}

注意:
* ${language}で出力してください
* 出力にはJSON形式以外の内容を含まないでください
* 主催者の名称が分かればtitleに含めてください
* locationは位置が特定しやすいようにしてください
* 終了日時が不明な場合は開始日時から2時間後にしてください\
`

(※ 実際の実装では、英語に直して記述しています)

実装③:Googleカレンダーへの登録URLに変換する

OpenAIによってJSON形式に変換できた予定情報を、Googleカレンダーへの登録URLへと変換します。

この関数では、予定のタイトル、開始・終了日時、詳細、場所といった各フィールドをGoogleカレンダーのURLパラメータにマッピングします。生成されたURLをブラウザで開くことで、ユーザーはワンクリックでイベントをGoogleカレンダーに追加することができます。

function toURL(json: CalendarEvent) {
  const url = `https://calendar.google.com/calendar/render?action=TEMPLATE&text=${json.title}&dates=${json.start_date}T${json.start_time}/${json.end_date}T${json.end_time}&details=${json.details}&location=${json.location}&trp=false`;
  return url;
}

Raycastのopenメソッドにurlを渡すと、ブラウザで開くことができます。

await open(url);

ドキュメント: https://developers.raycast.com/api-reference/utilities#open

ストアに公開

作成した拡張機能を誰でも利用できるように、Raycastのストアに公開してみます。

公開方法はこちらのドキュメントに記載されています。
https://developers.raycast.com/basics/prepare-an-extension-for-store
https://developers.raycast.com/basics/publish-an-extension

プロジェクトフォルダで下記のコマンドを実行すると、自動でRaycastの公式リポジトリをフォークし、今回の実装が反映されたPRが作成されます。

npm run publish

今回作成できたPRはこちらです。Raycastチームによるレビューで承認されると、Storeに公開することができるようです。
https://github.com/raycast/extensions/pull/15605

ちなみに今回自分が公開しようとしたタイミング("@raycast/api": "^1.86.1")では、publishコマンドでエラーが出てしまいました。

npm run publish

> publish
> npx @raycast/api@latest publish

ready  - validate package.json file
ready  - validate package-lock.json
ready  - validate other lock files
ready  - validate extension icons
ready  - run ESLint
ready  - run Prettier 3.4.1
ready  - getting fork
error  - preparing clone
    Error: failed running git Command failed: git reset --hard upstream/main
    fatal: ambiguous argument 'upstream/main': unknown revision or path not in the working tree.
    Use '--' to separate paths from revisions, like this:
    'git <command> [<revision>...] -- [<file>...]'


    fatal: ambiguous argument 'upstream/main': unknown revision or path not in the working tree.
    Use '--' to separate paths from revisions, like this:
    'git <command> [<revision>...] -- [<file>...]'

issueのコメントを参考に、古いバージョンのライブラリを使用してpublishコマンドを実行すると成功しました。

npx @raycast/api@1.85.2 publish

https://github.com/raycast/extensions/issues/15509

終わりに

今回はOpenAIのAPIをつかって、Raycastの拡張機能を作成してみました。

ちなみに、1回あたりの使用トークン量は詳しくは未確認ですが、何度か利用していても$0.01未満のコストで収まっていました。

カレンダーへの登録が手軽にできるようになり、とても便利です。
今回の作成レポートが少しでもどなたかの参考になれば嬉しいです。

Discussion