2024年1月25日木曜日

Google Gemini Pro Visionを呼び出して写真の動物を説明してもらうアプリを作る

スマホで撮影した写真に写っている動物について、Google Gemini Pro Visionを呼び出して説明してもらう簡単なアプリを作成してみました。

Google Gemini Pro Visionを呼び出すために、以前の記事「Google Geminiを呼び出すAPEXアプリケーションを作る」で紹介しているパッケージUTL_GOOGLE_GEMINI_APIを使っています。Google AI Studioから取得したAPIキーWeb資格証明として使っています。Google Geminiの扱いについてはすでにパッケージに実装済みであるため、本記事の内容には含まれていません。

本記事では主にSPA(Single Page Application)のAPEXアプリケーションの作り方を紹介します。

作成したAPEXアプリケーションは以下のように動作します。


以下よりAPEXアプリケーションの作り方を紹介します。

最初にAPEXのアプリからアップロードされる写真データを受け取って、Gemini Pro Visionを呼び出すORDSのRESTサービスを作成します。

モジュールテンプレートを作成し、以下のコードをソースとしたPOSTメソッドのハンドラを作成します。今回の記事ではモジュール名Google Gemini Pro Visionモジュール・ベース・パス/gemini-pro-vision/テンプレートanimalとしています。完全なURLはあらかじめ記録しておきます。後で、APEXアプリケーションの置換文字列G_REQUEST_URL置換値として設定します。

declare
l_response clob;
l_candidates json_array_t;
l_prompt_feedback json_object_t;
l_role varchar2(8);
l_prompt clob;
begin
l_prompt := 'この画像に写っている動物の名前と生息地および生態を教えてください。';
utl_google_gemini_api.generate_content(
p_text => l_prompt
,p_image => :body
,p_mimetype => :content_type
,p_candidates => l_candidates
,p_prompt_feedback => l_prompt_feedback
,p_response => l_response
,p_credential_static_id => 'GOOGLE_GEMINI_API_KEY'
,p_transfer_timeout => 60
);
:status := 200;
htp.p(utl_google_gemini_api.get_first_text(
p_candidates => l_candidates
,p_role => l_role
));
exception
when others then
htp.p(l_response);
:status := 400;
end;


ORDSのRESTサービスはAPEXのセッションで保護します。

ロールとしてRESTful ServicesモジュールとしてGoogle Gemini Pro Visionを割り当てた権限を作成します。


以上でRESTサービスの準備はできました。

アプリケーション作成ウィザードを起動します。作成するアプリケーションの名前この動物は何?としています。Google Gemini Pro Visionを呼び出す際にプロンプトとして「この画像に写っている動物の名前と生息地および生態を教えてください。」を与えていますが、プロンプトはどのように変更してもよいので、プロンプトに合わせてアプリの名前を変えても良いでしょう。

スマホにインストールして使用することを想定しているため、機能プログレッシブWebアプリケーションのインストールチェックします。


アプリケーションが作成されます。すべての機能はホーム・ページに実装します。

アプリケーション定義置換を開き、置換文字列G_REQUEST_URL置換値として、ORDSのRESTサービスの完全なURLを設定します。APEXアプリケーションとORDSのRESTサービスは同じワークスペースに実装されていることが前提なので(そうでないとAPEXセッションで認証できない)、プロトコルやホストを除いて設定します。


ページ・デザイナホーム・ページを開きます。

ブレッドクラムはいらないので削除します。


写真を保持するページ・アイテムを画面の中央に配置するため、静的コンテンツのリージョンを作成します。タイトルRequestとします。

外観テンプレートとしてBlank with Attributes (No Grid)を選択し、CSSクラスとしてu-flex u-justify-content-centerを設定します。このCSSクラスはAPEXのUniversal Themeにより定義されています。説明は、Universal ThemeのLayout Modifiersのリファレンスを参照してください。


Gemini Pro Visionの応答を表示するページ・アイテムも画面中央に配置するため、同じ設定で静的コンテンツのリージョンを作成します。タイトルResponseとします。


ボタンSUBMITを作成します。

ラベル問い合わせるとします。外観ホットオンにし、CSSクラスにはbutton-submitを設定します。このCSSクラスは後ほど定義し、問い合わせボタンを画面下部に固定します。

動作アクション動的アクションで定義を選択します。SPAのアプリを作成するときは、ほとんどの処理を動的アクションとJavaScriptで実装することになります。


リージョンResponseの下に、Google Gemini Pro Visionの応答を保持するページ・アイテムP1_RESPONSEを作成します。タイプリッチ・テキスト・エディタを選びます。

テンプレートhiddenを選択します。テンプレート・オプションLeft MarginRight MarginLargeを指定します。セッション・ステートデータ型CLOBを選択し、ストレージリクエストごと(メモリーのみ)を選択します。ページが送信されなければ、セッション・ステートにページ・アイテムの値が保存される機会はありません。そのためストレージセッションごと(永続)を選んでも動作は変わりません。

読取り専用タイプ常時を指定します。


リージョンRequestの下に送信する写真を保持するページ・アイテムP1_IMAGEを作成します。タイプイメージ・アップロードを選択します。

表示表示形式としてアイコン・ドロップ・ゾーンを選択します。アイコンとして写真がプレビューされます。プレビュー・サイズ特大にします。キャプチャに使用メイン・カメラを選択します。アプリがスマホで実行されているときは、ファイル選択のダイアログが開く代わりに、背面カメラを起動するようにします。

ストレージファイルをパージするタイミングリクエストの終わりタイプとして表APEX_APPLICATION_TEMP_FILESを選択します。ただし、ページの送信は行わないため、イメージが表APEX_APPLICATION_TEMP_FILESに保存されることはありません。

サイズ変更最大ファイル・サイズ1000を入力します。トリミングトリミングの許可オンにし、アスペクト比として1:1(正方形)を選択します。

トリミングオンにしていると写真のデータはPNG形式に変換され最大ファイル・サイズに収まるように解像度を落としてサイズを縮小します(JPEGがPNGに変換されるのは現行のAPEXのバージョンの不具合かも知れず、今後のバージョンで動作が変わる可能性もあります)。Google Gemini Pro Visionにリクエストを送信する際に、イメージのサイズおよび送信するリクエストのサイズが制限を超えないようにしています。

外観テンプレートとしてHiddenセッション・ステートストレージとしてリクエストごと(メモリーのみ)を選択します。


ページ・アイテムP1_IMAGEに動的アクションを作成し、値が変更されたときにページ・アイテムP1_RESPONSEクリアします。

動的アクションのタイミング変更です。


TRUEアクションクリアを選択し、影響を受ける要素として選択タイプアイテムアイテムとしてP1_RESPONSEを指定します。


TRUEアクションを追加します。写真が変更されたときに、ボタンSUBMIT表示します。

アクション表示影響を受ける要素選択タイプボタンボタンとしてSUBMITを選択します。初期化時に実行オフにします。


ボタンSUBMITをクリックしたときに実行される動的アクションを作成します。

タイミングイベントクリックです。


TRUEアクションとしてJavaScriptコードの実行を選択します。設定コードに以下を記述します。

// スピナーを表示し、画面操作をブロックする。
let spinner = apex.widget.waitPopup();
// P1_IMAGEに設定されているファイルを取り出す。
let file = document.getElementById("P1_IMAGE").files[0];
// Gemini Pro VisionをORDSのREST API経由で呼び出す。
const req = new XMLHttpRequest();
req.open("POST", "&G_REQUEST_URL.", true);
// 現行のAPEXは画像のクロッピングを行うと、フォーマットがPNGに変わる。
// この動作は将来のバージョンで変更される可能性がある。
req.setRequestHeader('content-type','image/png');
// APEXのセッションでORDS APIを認証するためにApex-Sessionヘッダーを設定している。
const authHeader = apex.env.APP_ID.concat(',',apex.env.APP_SESSION);
req.setRequestHeader('Apex-Session', authHeader);
req.onload = (event) => {
// console.log(req.response);
// 影響を受ける要素にREST APIのレスポンスを設定する。
let elem = this.affectedElements.toArray()[0];
apex.item(elem).setValue(req.response);
// スピナーの停止および画面操作のブロックを解除する
spinner.remove();
};
// 要求の送信。
req.send(file);
影響を受ける要素選択タイプアイテムアイテムとしてP1_RESPONSEを選択します。Gemini Pro Visionのレスポンスを表示するページ・アイテムを指定しています。


TRUEアクションを追加します。写真を送信した後は、ボタンSUBMIT非表示にします。

アクション非表示影響を受ける要素選択タイプボタンボタンとしてSUBMITを選択します。ページが初めて表示されたときに非表示とするため、初期化時に実行オンにします。


以上でアプリケーションの動作の部分は実装できました。


フッター部分を非表示にするのと、ボタンを修飾するCSSを、ページ・プロパティCSSインラインに記述します。

#t_Footer {
display: none;
}
.button-submit {
font-size: 2em;
width: 10em;
height: 3em;
position: fixed;
bottom: 5%;
left: 50%;
transform: translateX(-50%);
}
view raw ask-amimal.css hosted with ❤ by GitHub

ページ・プロパティ外観ページ・テンプレートMinimal (No Navigation)に切り替えます。また、テンプレート・オプションDeferred Page Renderingオンにします。この設定により、ページ・ロード時に一瞬ボタンSUBMITが表示される現象を抑制します。


アプリケーションに移り、テーマ・ローラを開いて見かけをカスタマイズします。

プライマリ・アクセント色ボタンの枠線の角の丸め20pxホットの色をに変更しています。このあたりはお好みで設定されるとよいでしょう。

変更したテーマを保存します。


以上でアプリケーションは完成です。実行すると記事の先頭のGIF動画のように動作します。

写真のデータは一旦APEXにアップロードされ、その後、Geminiに渡されます。そのため、データはブラウザからORDSとオラクル・データベースを通ります。単にGeminiを呼び出すだけであれば、ブラウザから直接APIを呼び出せば、ORDSからデータベースへのアクティブな接続が不要になります。通信するデータ量が多いためCPU負荷は低くてもセッションは長時間アクティブなままになります。データベースへの負荷を減らすには、写真はオブジェクト・ストレージにアップロードして、ファイルの位置をAPEXに渡すといった実装も考慮する必要があります。

今回作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/ask-animal.zip

Oracle APEXのアプリケーション作成の参考になれば幸いです。