2023年11月2日木曜日

熊の出没を報告するアプリを作成する

熊の出没を報告するAPEXアプリケーションを試作してみました。入力の手間を少なくすることを、第一の目標にしています。以下の機能を実装しています。
  1. ブラウザから取得できる現在位置の座標から住所を取得する。
  2. 写真のアップロードができる。
  3. 状況は音声で入力する。
  4. OpenAI WhisperのAPIを呼び出して、音声から文字起こしをする。
  5. OpenAI ChatGPTのAPIを呼び出して、状況を説明した文章からファセット検索に使う単語のJSON配列を生成する。
1の現在位置から住所を求めるために、こちらの記事「国土交通省のデータを使って簡易的な逆ジオコーディングを実装する」の実装を使っています。また、4の音声のレコーディングには、こちらの記事「OpenAI Whisperを使った文字起こしアプリの作成(5) - ブラウザからの音声入力」の実装を使っています。OpenAIのAPIを呼び出すために使用するWeb資格証明の作成方法については、こちらの記事「OpenAIのChatGPTのAPIを呼び出すAPEXアプリを作る」で紹介しています。

OpenAIのWhisperとChatGPTのAPI呼び出しは、アカウント作成時に与えられる$40のクレジットを使い切る(もしくは期限切れになる)と、使用量に応じて課金されます。

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


以下より、アプリケーションの作成手順を紹介します。

最初に、クイックSQLの以下のモデルを元に、出没情報を保存する表を作成します。表BEAR2_SIGHTINGSには出没情報、表BEAR2_PHOTOSには、出没情報に紐づく写真、表BEAR2_VOICESには、音声による報告が保存されます。

SQLの生成SQLスクリプトを保存レビューおよび実行を順次実行します。



SQLスクリプトの編集画面が開くので、そのまま実行します。確認画面が開くので、即時実行をクリックします。その後、生成されたDDLが実行されて上記の3つの表が作成されます。


この画面からはアプリケーションの作成は行いません。

アプリケーション作成ウィザードを起動し、空のアプリケーションを作成します。アプリケーションの名前熊出没情報とします。スマートフォンにインストールできるように、機能プログレッシブWebアプリケーションのインストールチェックを入れます。

アプリケーションの作成をクリックします。


アプリケーションが作成されます。

報告した内容を表示するページを作成します。ページの作成をクリックし、ページ作成ウィザードを起動します。


クラシック・レポートを選択します。


ページ番号2名前報告内容とします。データ・ソース表/ビューの名前としてBEAR2_SIGHTINGSを指定します。このページは、出没情報の送信後に遷移する画面なので、このページを直接開くことはありません。そのため、ナビゲーションブレッドクラムの使用ナビゲーションの使用ともにオフにします。


ページが作成されます。

このページは登録された1件の出没情報だけを表示します。表BEAR2_SIGHTINGSで表示対象とする行の主キーの値を保持するページ・アイテムを作成します。

識別名前P2_IDタイプ非表示とします。


クラシック・レポートのリージョン報告内容を選択し、ソースWHERE句に以下を記述し、表示する対象を1つに絞ります。

id = :P2_ID


プロパティ・エディタ属性タブを開き、外観テンプレートValue Attribute Pair - Columnに変更します。横並びだった列を縦方向に並べます。


出没情報に紐づけられた写真を表示するために、カード・リージョンを作成します。

識別タイトル写真タイプカードを選択します。ソース表名としてBEAR2_PHOTOSを指定し、WHERE句に以下を記述します。

sighting_id = :P2_ID


プロパティ・エディタ属性タブを開きます。

今回は写真が表示される最低限の変更のみを実施します。メディアソースとしてBLOB列を選び、BLOB列としてPHOTOを選択します。その後にカード主キー列1IDを指定します。

変更を保存します。


報告内容を表示するページは、以上で完成です。

熊出没情報を入力するフォームのページを作成します。

メニューよりページを実行し、ページ作成ウィザードを開始します。


フォームを選択します。


ページ番号3名前熊出没報告とします。データ・ソース表/ビューの名前としてBEAR2_SIGHTINGSを指定します。このページも直接開くことはない(現在位置の座標を受け取るため、つねにホーム・ページからリダイレクトされます)ため、ナビゲーションブレッドクラムの使用ナビゲーションの使用ともにオフとします。

へ進みます。


主キー列1としてID (Number)を選択します。ブランチ・ページ送信時にここにブランチは、先ほど作成した報告内容を表示するページを遷移先とするため2を指定します。取り消してページに移動2としますが、取消ボタンは削除するため、取り消し処理が行われることはありません。

ページの作成をクリックします。


ページが作成されます。

ページ・プロパティ識別別名reportに変更します。ホーム・ページからリダイレクトする宛先として、この別名を使用します。外観ページ・テンプレートとしてMinimal(No Navigation)を選択し、不要な装飾を除きます。保存されていない変更の警告オフにします。


セキュリティページ・アクセス保護を、引数にチェックサムが必要から制限なしに変更します。ホーム・ページからリダイレクトする際に、チェックサムの計算を省略しているためです。


このページが日本時間で動作するように、レンダリング前/ヘッダーの前にプロセスを作成し、タイムゾーンを設定します。

識別名前日本時間を強制タイプコードの実行を選択します。ソースPL/SQLコードとして以下を記述し、タイムゾーンAsia/Tokyoに設定します。

apex_util.set_session_time_zone('Asia/Tokyo');


ページ・アイテムP3_VOICE_IDタイプ非表示に変更し、設定保護された値オフにします。この値は、音声ファイルのアップロードを行った後に、JavaScriptのコードから設定されます。


ページ・アイテムP3_REPORT_DATEを選択します。

ラベル出没日時とします。設定時間の表示オン外観書式マスクYYYY-MM-DD HH24:MI:SSデフォルトタイプとしてを選択し、PL/SQL式に以下を記述します。

to_char(current_timestamp, 'YYYY-MM-DD HH24:MI:SS')

出没日時の初期値として現在の時刻を設定しています。


ページ・アイテムP3_LATITUDEP3_LONGITUDEを選択します。これらの座標値は地図上をクリックして設定します。ユーザーが入力することはないので、タイプ非表示にします。JavaScriptから値を設定するため、保護された値オフにします。セッション・ステートストレージとしてセッションごと(永続)に変更します。


ページ・アイテムP3_ADDRESSを選択します。このページ・アイテムは逆ジオコーディングを行った住所を、選択リストにします。

タイプ選択リストに変更し、ラベル出没した場所とします。LOVタイプSQL問合せSQL問合せとして以下を記述します。

追加値の表示オフNULL値の表示オフです。カスケードLOV親アイテムP3_LATITUDEP3_LONGITUDEを設定し、これらの値が変更されたときに、選択リストがその座標に近い住所に更新されるようにします。


ページ・アイテムP3_REPORT_TEXTP3_ATTRIBUTESP3_CREATEDP3_CREATED_BYP3_UPDATEDP3_UPDATED_BYは、APEXのプロセスやトリガーによりサーバー側で設定されます。ユーザーが扱うことはありません。

これらのページ・アイテムをすべて選択し、構成ビルド・オプションを使ってコメント・アウトします。


フォームのリージョン熊出没注意を選択します。

外観テンプレートBlank with Attributesに変更し、見かけをシンプルにします。


プロパティ・エディタ属性タブを開きます。

編集実行可能な操作から行の更新行の削除チェックを外します。この後、ボタンSAVE、DELETE、CANCELを削除しますが、ボタンを削除してもJavaScriptコンソールからコードを実行すると編集や削除を呼び出すことができます。この設定により、そのようなリスクからデータを守ることができます。


ボタンCANCELDELETESAVEを選択し、コンテキスト・メニューを表示させて削除を実行します。


ボタンCREATEを選択し、ページ・アイテムP3_UPDATED_BYの下に配置します。

識別ラベル報告に変更し、動作アクション動的アクションで定義に変更します。


外観テンプレート・オプションを開き、詳細WidthStretchSpacing TopLargeに変更します。


地図を表示するリージョンをRegion Bodyに作成します。

Region Body上でコンテキスト・メニューを表示し、リージョンの作成を実行します。

作成したリージョンをページ・アイテムP3_ADDRESSの下に配置します。識別タイトル地図タイプマップにします。外観テンプレートBlank with Attributes詳細静的IDとしてmapを設定します。


レイヤーを選択します。

識別名前現在位置とします。ソースタイプSQL問合せに変更し、SQL問合せとして以下を記述します。
select
    to_number(:P3_LATITUDE) as lat
    ,to_number(:P3_LONGITUDE) as lon
from dual
送信するページ・アイテムとしてP3_LATITUDEP3_LONGITUDEを指定します。

列のマッピングジオメトリ列のデータ型として経度/緯度を選択し、経度列LON緯度列LATを選択します。


マップ・リージョン地図に動的アクションを作成します。

識別名前地図上をクリックとします。タイミングイベントとしてマップがクリックされました[マップ]を選択します。


TRUEアクションJavaScriptコードの実行に変更します。設定コードとして以下を記述します。
apex.items.P3_LATITUDE.setValue(this.data.lat);
apex.items.P3_LONGITUDE.setValue(this.data.lng);
apex.region("map").refresh();

リージョン地図を選択し、プロパティ・エディタ属性タブを開きます。

マップ高さ240に変更します。初期位置およびズームタイプSQL問合せに変更し、SQL問合せとして以下を記述します。ホーム・ページから熊出没報告へ遷移する際にページ・アイテムP3_LATITUDEとP3_LONGITUDEに現在位置の座標値が渡されます。そのため、地図が初期化される際の初期位置現在位置ズーム・レベル12になります。
select
    to_number(:P3_LATITUDE) as lat
    ,to_number(:P3_LONGITUDE) as lon
    ,12 zoom
from dual
ジオメトリ列のデータ型として経度/緯度を選択します。初期経度列LON初期緯度列LAT初期ズーム・レベルZOOMとします。

レイヤーは1つだけなので、凡例表示オフにします。


写真を選択するページ・アイテムを作成します。作成したページ・アイテムはリージョン地図の下に配置します。

識別名前P3_PHOTOSタイプとしてファイル参照...を選択します。ラベル写真とします。

設定表示形式Native File Browseを選択し、モバイル・デバイスでの最適な表示形式を優先します。記憶域タイプTable APEX_APPLICATION_TEMP_FILESファイルをパージするタイミングとしてEnd of Requestを選択します。アップロードした写真ファイルは表BEAR2_PHOTOSに保存するため、一時表に残しておく必要はありません。複数ファイルの許可オンにし、複数のファイルを一度にアップロード可能にします。ファイル・タイプとしてimage/*を指定し、アップロード対象をイメージに限定します。最大ファイル・サイズ10000KB(=10MB)とします。

セッション・ステートストレージリスエストごと(メモリーのみ)を設定します。


音声を録音するためのプレーヤーを配置します。

ページ・アイテムP3_PHOTOS上でコンテキスト・メニューを表示させ、下にリージョンを作成を実行します。


作成したリージョンの識別タイトル録音とします。タイプ静的コンテンツソースのHTMLコードとして以下を記述します。

<audio id="player" controls></audio>

外観テンプレートとしてBlank with Attributesを選択します。


リージョン録音上でコンテキスト・メニューを表示させ、ボタンの作成を実行します。


作成したボタンのボタン名STARTラベル録音開始とします。

レイアウト位置Previous動作アクションとして動的アクションで定義を指定し、詳細カスタム属性として以下を記述します。

data-action="#action$start-audio-recording"


同様の手順でもう一つボタンを作成します。ボタン名STOPラベル録音終了とします。

レイアウト位置Next動作アクションとして動的アクションで定義を指定し、詳細カスタム属性として以下を記述します。

data-action="#action$stop-audio-recording"


ページ・プロパティJavaScriptファンクションおよびグローバル変数の宣言に以下を記述します。残念なことに、このコードではiOS上のSafariでは録音が機能しません。AudioContextを使うとiOSのSafariでもWeb Audio APIが使えるとの情報もありますが、本題から外れるので今回は対応していません。



音声データのアップロードを受け付けて、表BEAR2_VOICESに保存するRESTサービスを作成します。

SQLワークショップからRESTfulサービスを開きます。


モジュールとしてuploadを作成し、そのモジュールにテンプレートとしてvoiceを作成します。voiceのPOSTハンドラとして、ソースに以下のPL/SQLコードを記述します。送信されたファイルを受け付けて表BEAR2_VOICESに保存します。音声ファイルを保存した行の主キーの値を呼び出し元に返します。この返却されたIDは、ページ・アイテムP3_VOICE_IDに設定されます。

本来であればApex-Sessionヘッダーなどを使ってRESTサービスを保護する必要がありますが、今回の手順では省略しています。保護の方法については、こちらの記事を参照してください。

完全なURLのホスト名以下のパスは、APEXから音声ファイルのアップロードを行う際にURLとして指定します。メモを取っておいてください。


APEXのページ・デザイナに戻ります。

ボタンCREATE動的アクションを作成します。

識別名前録音を保存してから送信とします。タイミングイベントはボタンのデフォルトであるクリックになります。


TRUEアクションJavaScriptコードの実行に変更し、設定コードに以下を記述します。最初に録音データをアップロードし、その後にページの送信を行います。コード中のrequest.openに与えるURLは環境に合わせて変更します。


左ペインでプロセス・ビューを開き、アップロードされた写真ファイルを保存するプロセスを作成します。

新規にプロセスを作成し、プロセスプロセス・フォーム熊出没報告の下に配置します。識別名前写真を保存タイプコードの実行です。ソースPL/SQLコードに以下を記述します。

サーバー側の条件ボタン押下時CREATEを指定します。


アップロードされた音声ファイルを、OpenAIのWhisperを呼び出して文字起こしを行い、その文字列からファセット検索に使用する単語を抽出するために、ChatGPTのAPIを呼び出します。ChatGPTのモデルとしてgpt-3.5-turboを指定しています。プロンプトをもっとチューニングすると、より適切な結果が得られると思います。

以下のコードを実行し、パッケージUTL_BEAR_OPENAIを作成します。SQLワークショップSQLスクリプトを使って実行できます。


上記のパッケージに含まれるプロシージャを呼び出すプロセスを作成します。

新規にプロセスを作成します。作成したプロセスは写真を保存の下に配置します。

識別名前WhisperとChatGPT呼び出しタイプコードを実行です。ソースPL/SQLコードに以下を記述します。

サーバー側の条件ボタン押下時としてCREATEを設定します。


プロセスプロセス・フォーム熊出没報告を選択します。成功メッセージに「目撃報告が送信されました。」を設定し、デフォルトのメッセージを変更します。


ブランチページに移動2を選択し、動作ターゲットを開きます。


アイテムの設定として、名前P2_ID&P3_ID.を追加します。今回作成された出没情報のIDの値が&P3_ID.なので、その値がページ・アイテムP2_IDに渡されます。結果として、今回作成された出没情報が遷移先のページで表示されます。


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

ページ・プロパティ外観ページ・テンプレートMinimal(No Navigation)に変更します。ホーム・ページはアプリケーションにサインインした直後に開きますが、現在地の座標を取得した後すぐにページ番号3の熊出没報告のページに移動します。そのため、ナビゲーションは不要です。


ブレッドクラム熊出没情報の上でコンテキスト・メニューを表示させ、削除を実行します。ホーム・ページにはデフォルトでブレッドクラムが作成されますが、今回の用途ではブレッドクラムは不要です。


ページ・プロパティJavaScriptページ・ロード時に実行に、現在の位置の座標を取得した後、ページ番号3へリダイレクトするコードを記述します。コード中のワークスペース名やアプリケーション名は作成したアプリケーションに合わせて変更が必要です。



アプリケーション定義に含まれるアプリケーション別名英語名にしておくと、上記の設定が容易になります。


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

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

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