2025年5月22日木曜日

APEXアプリのページ上でGoogle AI EdgeのMediaPipe LLM Inference APIを呼び出す

GoogleがGoogle AI Edgeのソリューションとして提供しているMediaPipe LLM Inference APIを、Oracle APEXのアプリケーションのページ上で呼び出してみます。

以下のサンプル・アプリケーションに含まれるindex.jsを、Oracle APEXで動くように改変ししました。

MediaPipe LLM Inference task for web
https://github.com/google-ai-edge/mediapipe-samples/tree/main/examples/llm_inference/js

実行環境はmacOSのChromeです。作業としては、ウェブ用LLM推論ガイドのクイックスタートを参照しています。

元々は最近発表されたGemma 3nを動かすつもりだったのですが、Gemma 3nは現時点では.taskファイルのみ公開されているようです。ウェブ用LLM推論ガイドモデル変換に.taskの対応プラットフォームとしてAndroid、iOSはありますが、ウェブがありません。そのため、クイックスタートのとおりにgemma2-2b-it-gpu-int8を使用しました。

サンプル・アプリケーションのindex.htmlは、APEXのアプリケーションのページとして作り直しています。

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


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

APEX向けに書き直したindex.jsは以下になります。

/*
* 元のファイルは以下です。
* https://github.com/google-ai-edge/mediapipe-samples/blob/main/examples/llm_inference/js/index.js
*
* Oracle APEXのページ上で実行するために、input、outputの要素をページ・アイテムに変更し、データの取得や設定方法も
* ページ・アイテムに合わせて変更しています。
*/
// Copyright 2024 The MediaPipe Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ---------------------------------------------------------------------------------------- //
import {FilesetResolver, LlmInference} from 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-genai';
/*
* 要素からページ・アイテムに変更。
const input = document.getElementById('input');
const output = document.getElementById('output');
*/
const submit = document.getElementById('submit');
const modelFileName = '/i/models/gemma2-2b-it-gpu-int8.bin'; /* Update the file name */
// const modelFileName = '/i/models/gemma-3n-E2B-it-int4.task';
/**
* Display newly generated partial results to the output text box.
*/
function displayPartialResults(partialResults, complete) {
// output.textContent += partialResults;
// ページ・アイテムに出力を設定する。
let output = apex.item("P1_OUTPUT").getValue() + partialResults;
apex.item("P1_OUTPUT").setValue(output);
if (complete) {
if (!output) {
// output.textContent = 'Result is empty';
apex.item("P1_OUTPUT").setValue('Result is empty');
}
submit.disabled = false;
}
}
/**
* Main function to run LLM Inference.
*/
async function runDemo() {
const genaiFileset = await FilesetResolver.forGenAiTasks(
'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-genai/wasm');
let llmInference;
submit.onclick = () => {
// output.textContent = '';
const input = apex.item("P1_INPUT").getValue();
apex.item("P1_OUTPUT").setValue('');
submit.disabled = true;
llmInference.generateResponse(input, displayPartialResults);
};
submit.value = 'Loading the model...'
LlmInference
.createFromOptions(genaiFileset, {
baseOptions: {
modelAssetPath: modelFileName
},
/* LLMに与えるパラメータを設定 */
maxTokens: 4096, // The maximum number of tokens (input tokens + output
// // tokens) the model handles.
randomSeed: 1, // The random seed used during text generation.
topK: 1, // The number of tokens the model considers at each step of
// // generation. Limits predictions to the top k most-probable
// // tokens. Setting randomSeed is required for this to make
// // effects.
temperature:
1.0, // The amount of randomness introduced during generation.
// // Setting randomSeed is required for this to make effects.
})
.then(llm => {
llmInference = llm;
submit.disabled = false;
submit.value = 'Get Response'
})
.catch((err) => {
// エラーの発生要因を出力。
console.error('Failed to initialize the task:', err);
alert('Failed to initialize the task.');
});
}
runDemo();
view raw index.js hosted with ❤ by GitHub
このファイルを静的アプリケーション・ファイルとしてアップロードします。作成した静的アプリケーション・ファイル参照をメモします。


静的アプリケーション・ファイルindex.jsをページに読み込むために、ホーム・ページのページ・プロパティJavaScriptファイルURLに以下を記述します。moduleを指定します。

[module]#APP_FILES#index#MIN#.js


LLMに送信する文字列は、ページ・アイテムP1_INPUTに入力します。タイプテキスト領域を選択します。セッション・ステートデータ型CLOBストレージリクエストごと(メモリーのみ)を設定します。


ボタンSUBMITを作成します。ボタンをクリックしたときに実行されるイベントハンドラは、index.js内に記述されています。APEXアプリとしては何も処理をしないので、動作アクションとして動的アクションで定義を選択します。また、index.js内でこのボタンをsubmitというIDで見つけているので、詳細静的IDsubmitを設定します。


LLMの出力はページ・アイテムP1_OUTPUTに表示します。LLMの出力フォーマットはMarkdownなので、ページ・アイテムのタイプとしてMarkdownエディタを選択します。セッション・ステートデータ型CLOBストレージリクエストごと(メモリーのみ)を設定します。


Oracle APEXでMediaPipe LLM Inference APIを呼び出すサンプルは以上で完成です。

モデル・ファイルのgemma2-2b-it-gpu-int8.binは、kaggle.comからダウンロードしました。


LLMとしては小さなモデルとはいえ、数GBはあります。Oracle APEXの静的アプリケーション・ファイルとして保存するのも避けたかったので、ORDSから直接ファイルとしてダウンロードできる場所に配置しました。

(CDNではなく)ORDSがOracle APEXのイメージ・ファイルを提供するように構成している場合、以下のようにstandalone.static.pathが設定されています。(global/settings.xml内)

<entry key="standalone.static.path">/opt/oracle/apex/images</entry>

このパスの下はURLから見ると/i/以下にあるファイルとして参照されます。そのため、(上記の例では/opt/oracle/apex/images以下に)ディレクトリmodelsを作成し、その下にgemma2-2b-it-gpu-int8.binを置きました。

index.js内ではモデル・ファイルを以下のように指定しています。
const modelFileName = '/i/models/gemma2-2b-it-gpu-int8.bin'; /* Update the file name */
CORSのエラーが発生しないよう、APEXが動作しているホストと同じホストからモデル・ファイルをダウンロードする必要があるようです。また、ダウンロード元がlocalhost以外の場合、https経由でダウンロードする必要があるとの記載もありました。今回の作業ではAPEXはlocalhostでアクセスしているため、両方の条件が必須かどうかは確認できていません。

今回使用したモデルはWebGPUを使用します。Chromeのchrome://flagsを開いて、WebGPUに関する以下の2つの設定を有効にしています。

#enable-unsafe-webgpu、#enable-webgpu-developer-features


今回の記事は以上になります。

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

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