はじめに
子供の小学校は自治体のWebサイトに給食献立表のPDFが毎月アップロードされるので、Amazon Echoに特定日付の献立を聞いたら、それを答えてくれるようにします。
Alexa-hostedならAWSアカウントなくてもAmazon Developerのアカウント登録したら作ることができ、スキル専用のAWS機能(Lambda, S3, DynamoDBなど)を使うことができます。
AWSのアカウント持ってないし(いつか無料枠を使いたい時のために)まだ作りたくないので、作らずに開発します。
また、Amazon Echoに紐づけているAmazonアカウントをDeveloperアカウントにしたら、スキルを公開までしなくても自分のEchoではテスト開発中状態で使うことができます。個人利用なら一旦ここゴールで良いかなと。
最終的なシステム構成
いずれも無料枠の範疇です。
- Google Apps Scriptで、日勤で各月の献立PDFをGemini APIを使ってスクレイピングして、Googleスプレッドシートの各月のシートに格納する
- Google Apps Scriptで、各月のシート内容をJSONで返すAPI
- Alexa-hosted スキルで、問い合わせられた日に対するJSONを発話する
スキル作成
alexa developer consoleで「スキルの作成」を押す
- スキル名: (任意の文字列)
- プライマリロケールを選択: 日本語
- エクスペリエンスのタイプ: その他
- モデル: カスタム
- ホスティングサービス: Alexa-hosted (Python)
- ホスト地域: 米国東部(バージニア北部) (アジアないので適当に選択)
- テンプレート: スクラッチで作成
ビルド
- Skill Invocation Name(スキル呼び出し名): (任意の文字列)
- Intents: テンプレートのHelloWorldIntentを書き換えるか新規作成するかで、AMAZON.DATE形式のスロット{date}を入力させます。Sample Utterancesはこんな感じ
- {date}
- {date} を教えて
- {date} の給食は
- {date} の献立は
- {date} のメニューは
で、「Build Skill」を押すと、Alexaスキルが呼びかけを受け取る部分は完成です。
コードエディタ
作成したIntentに対応するHandlerクラスを実装します。
コードほとんどAIに書いてもらったので、概念だけ書きます。
- {date} スロットから日付を取得orなければ今日を対象にする
- 対象月の献立表JSONを取得(後述)
- 対象日をkeyとする項目があればそれを発話、なければない旨を発話させる
- おまけでEcho Showなら画面にも表示する
スクレイピング
後述としていた、献立表JSONを取得する部分です。
大きな流れとしてはこう
- 自治体Webサイトの当該ページHTMLから献立表PDFへのリンクを取得
- PDFから日付と献立をどうにかして取り出して、以下のようなJSONを作る
{
"1": "米飯、牛乳、鶏肉のバーベキューソースかけ、野菜のおかか和え、こどもの日スープ、柏餅",
"7": "コッペパン、牛乳、大豆とマカロニのトマト煮、キャベツとベーコンのソテー"
...
}
AIと壁打ちして、最初はローカルのPythonでBeautifulSoup4, pdfplumberを使ってそれっぽく取得できることを確認。
その後Alexa-hosted (Python)のLambdaにコードを移行したのですが、スクレイピングがAlexaスキルのタイムアウト時間(8秒)に処理が間に合わず、取得に失敗してしまいました。
ということで、別でスクレイピングしておいたものを取得だけする戦法に切り替えます。
定番手法としては、Alexa-hostedではなく自前AWSにホスティングであれば、スキルとは別のLambdaで自前S3にJSON保存を定期実行しておき、スキルではそのS3を見に行くというのが考えられます。
しかし、表題のとおりAWSアカウントなしで行くため、Google Apps Scriptに救いを求めました。
Google Apps Script
Google Drive APIによるOCR
Googleスプレッドシートから「拡張機能」→「Apps Script」を開いてスクリプトを作成。
これもAI壁打ちコードですが、こんな感じでGoogle Drive APIを使って各月のPDFをOCRしてGoogleドキュメントに変換して、その全文から日と献立をスプレッドシートの各月のシートに書き出す処理を作ります。
「サービス」からDrive APIを追加しておく必要があります。
const pdfBlob = UrlFetchApp.fetch(pdfUrl).getBlob();
const docFile = Drive.Files.create(fileMetadata, pdfBlob, { ocr: true });
const doc = DocumentApp.openById(docFile.id);
const fullText = doc.getBody().getText();
完成して動作確認を済ませたら、「トリガー」を追加して、毎日深夜に実行されるようにしておくことで、自治体Webサイトの更新を自動で拾ってきます。
課題
OCRの精度の問題で、料理名がうまく区切られないことがありました。
また、食材まで返させようと思ったら、順番がバラバラになったり、どの料理にどの食材が使われているかの対応がとれない感じでした。
| 献立表PDF | OCR後のGoogleドキュメント | テキスト取得結果(一部) |
|---|---|---|
![]() |
![]() |
米飯、牛乳、鶏肉のオーブン、焼ききんぴら、ごぼう、みそ汁 |
解決策: Gemini API
結局ここもAIに助けてもらいます ![]()
リクエスト数、トークン数(gemini-3.1-flash-liteで4K/1PDFぐらいでした)ともに無料枠で全然収まります。
Google AI StudioでAPIキーを発行し、それを使ってGemini APIをリクエストします。
プロンプトこんな感じで。
const prompt = `
添付の給食献立表PDFを解析し、以下の形式のJSONで出力してください。
[
["11", "米飯、牛乳、鶏肉のオーブン焼き、きんぴらごぼう、みそ汁", "こめ、ぎゅうにゅう、とりももにく、にんにく、しお、こしょう、オリーブオイル、ごぼう、ぶたももにく、にんじん、つきこんにゃく、こめあぶら、酒、さとう、こいくちしょうゆ、みりん、ごまあぶら、いりごま(白)、あぶらあげ、じゃがいも、たまねぎ、にんじん、あおねぎ、みそ、にぼし、だしこんぶ"],
]
注意点:
- 日付順に、全ての日のぶんを出力してください。
- キャベツとベーコンのソ
テー
のような献立名は、右端で折り返されているので、「キャベツとベーコンのソテー」と結合してください。セル内の行間の空白を重視して分割してください。
- ☆、★は削除してください。例:「☆牛乳」→「牛乳」
- 食品名の「さけ」は「酒」、「サケ」は「鮭」に変換してください。
- 余計な説明文は省き、JSONのみを返してください。
`;
返ってきたJSONを、先述のシートに書き出します。
APIを作成
この表をAlexaスキルが取り出せるように、同じApps ScriptにdoGet関数を作成します。
シート名sheetをパラメータとして、内容を先述のJSONの形で返させます。
作成できたら「デプロイ」
- 種類: ウェブアプリ
- 次のユーザーとして実行: 自分
- アクセスできるユーザー: 全員
出てきたURL https://script.google.com/macros/s/????????/exec に ?sheet=シート名 をくっつけてWebブラウザのURL欄に入力してJSONが返ってきたら動作確認OKです。
Alexa-hosted (Python)のLambdaからこのAPI URLを呼び出すようにしたら、取得できます。
なお、毎回APIにアクセスしていると応答に時間かかったり使用制限に引っかかったりするので、各月の取得結果をAlexa-hostedのS3領域に保存して、JSONがS3にある同月2回目以降の呼び出しではそっちを参照するようにします。
別解: 半自動(一部手動)
月1回の手間と割り切れば、ローカルでスクレイピングしたJSONを手動でAlexa-hostedのS3領域に直接アップロードするのも、手ではあります ![]()
動作確認
「コードエディタ」でデプロイして、「テスト」タブで「スキルテストが有効になっているステージ」を「非公開」から「開発中」に変更したら、同タブで動作確認ができます。
この時点で、アカウントに紐づけられているEchoからも呼びかけて動作させることができます。
定型アクションの設定
「{スキル呼び出し名} {日付}」のような呼び出しが直感的ではないので、頻出するであろう「今日の給食」「明日の給食」については、Alexaアプリの定型アクションで、カスタムアクション「{スキル呼び出し名} 今日」「{スキル呼び出し名} 明日」が実行されるようにすることで、自然な問いかけができます。


