HN 日本語サマリー

← 一覧へ戻る
AI・機械学習

WebLLM:ブラウザ内での高性能LLM推論エンジン

WebLLM: high-performance in-browser LLM inference engine (github.com)

130 pointsby saikatsg21 コメント

要約

WebLLMは、WebGPUによるハードウェアアクセラレーションを利用して、ブラウザ内で直接大規模言語モデル(LLM)の推論を実行できる高性能エンジンです。サーバーサポートなしで動作し、OpenAI APIとの完全な互換性を持ち、ストリーミングやJSONモードなどの機能も利用可能です。これにより、プライバシーを保護しつつGPUアクセラレーションを活用したAIアシスタントの開発が容易になります。

全文翻訳

WebLLM

[画像: NPM Package] [画像: "WebLLM Chat Deployed"] [画像: Join Discord] [画像: Related Repository: WebLLM Chat] [画像: Related Repository: MLC LLM]

高性能ブラウザ内LLM推論エンジン。

ドキュメント | ブログ記事 | 論文 | 例

概要

WebLLMは、ハードウェアアクセラレーションを備えた言語モデル推論をウェブブラウザに直接もたらす、高性能なブラウザ内LLM推論エンジンです。すべてがサーバーサポートなしでブラウザ内で実行され、WebGPUによって高速化されます。

WebLLMはOpenAI APIと完全に互換性があります。つまり、ストリーミング、JSONモード、関数呼び出し(開発中)などの機能を含め、同じOpenAI APIを任意のオープンソースモデルでローカルに使用できます。

これにより、誰もがAIアシスタントを構築するための多くの楽しい機会が生まれ、GPUアクセラレーションを楽しみながらプライバシーを確保できます。

以下の例に従って、WebLLMをベースのnpmパッケージとして使用し、その上に独自のWebアプリケーションを構築できます。このプロジェクトは、ハードウェア環境全体でのLLMのユニバーサルデプロイメントを可能にするMLC LLMのコンパニオンプロジェクトです。

主な機能

  • ブラウザ内推論: WebLLMは、WebGPUを活用してハードウェアアクセラレーションを行う高性能なブラウザ内言語モデル推論エンジンであり、サーバーサイド処理なしで強力なLLM操作をウェブブラウザ内で直接実行できます。

  • 完全なOpenAI API互換性: ストリーミング、JSONモード、ロジットレベル制御、シードなどの機能を使用して、WebLLMとアプリをシームレスに統合します。

  • 構造化JSON生成: WebLLMは、最適なパフォーマンスのためにモデルライブラリのWebAssembly部分に実装された、最先端のJSONモード構造化生成をサポートします。カスタムJSONスキーマでJSON出力を生成する際は、HuggingFaceのWebLLM JSON Playgroundを試してみてください。

  • 豊富なモデルサポート: WebLLMは、Llama 3、Phi 3、Gemma、Mistral、Qwen(通義千問)など、さまざまなモデルをネイティブにサポートしており、多様なAIタスクに対応できます。サポートされているモデルの完全なリストについては、MLC Modelsを確認してください。

  • カスタムモデル統合: MLC形式のカスタムモデルを簡単に統合およびデプロイでき、モデルデプロイメントの柔軟性を高め、特定のニーズやシナリオに合わせてWebLLMを適応させることができます。

  • プラグアンドプレイ統合: NPMやYarnなどのパッケージマネージャーを使用するか、CDNを介して直接、WebLLMをプロジェクトに簡単に統合できます。包括的な例と、UIコンポーネントとの接続のためのモジュラー設計が用意されています。

  • ストリーミングとリアルタイムインタラクション: ストリーミングチャット補完をサポートし、リアルタイムの出力生成を可能にし、チャットボットや仮想アシスタントのようなインタラクティブなアプリケーションを強化します。

  • Web WorkerとService Workerのサポート: 別個のワーカー スレッドまたはサービスワーカーに計算をオフロードすることで、UIパフォーマンスを最適化し、モデルのライフサイクルを効率的に管理します。

  • Chrome拡張機能のサポート: WebLLMを使用してカスタムChrome拡張機能を通じてWebブラウザの機能を拡張できます。基本的な拡張機能と高度な拡張機能の両方を構築するための例が利用可能です。

内蔵モデル

利用可能なモデルの完全なリストはMLC Modelsで確認してください。WebLLMはこれらの利用可能なモデルの一部をサポートしており、リストはprebuiltAppConfig.model_listでアクセスできます。

現在サポートされている主なモデルファミリーは以下の通りです。

  • Llama: Llama 3, Llama 2, Hermes-2-Pro-Llama-3
  • Phi: Phi 3, Phi 2, Phi 1.5
  • Gemma: Gemma-2B
  • Mistral: Mistral-7B-v0.3, Hermes-2-Pro-Mistral-7B, NeuralHermes-2.5-Mistral-7B, OpenHermes-2.5-Mistral-7B
  • Qwen (通義千問): Qwen2 0.5B, 1.5B, 7B

さらに多くのモデルが必要な場合は、Issueを開いて新しいモデルをリクエストするか、カスタムモデルで独自のモデルをWebLLMでコンパイルして使用する方法を確認してください。

例で始める

このシンプルなチャットボットの例を通じて、WebLLMを使用して大規模言語モデルをアプリケーションに統合し、チャット補完を生成する方法を学びましょう。

[画像: Example Chatbot on JSFiddle] [画像: Example Chatbot on Codepen]

より大きく複雑なプロジェクトの高度な例については、WebLLM Chatを参照してください。

その他のユースケースの例は、examplesフォルダで利用可能です。

開始方法

WebLLMは、ブラウザでチャットボットにアクセスするためのミニマルでモジュラーなインターフェースを提供します。このパッケージは、任意のUIコンポーネントにフックできるようにモジュラー方式で設計されています。

インストール
パッケージマネージャー
# npm
npm install @mlc-ai/web-llm
# yarn
yarn add @mlc-ai/web-llm
# または pnpm
pnpm install @mlc-ai/web-llm

その後、コードでモジュールをインポートします。

// すべてをインポート
import * as webllm from "@mlc-ai/web-llm";
// 必要なものだけをインポート
import { CreateMLCEngine } from "@mlc-ai/web-llm";
CDN配信

jsdelivr.comのおかげで、WebLLMはURLから直接インポートでき、jsfiddle.net、Codepen.io、Scribblerなどのクラウド開発プラットフォームでそのまま動作します。

import * as webllm from "https://esm.run/@mlc-ai/web-llm";

動的にインポートすることもできます。

const webllm = await import("https://esm.run/@mlc-ai/web-llm");
MLCEngineの作成

WebLLMのほとんどの操作は、MLCEngineインターフェースを通じて呼び出されます。MLCEngineインスタンスを作成し、CreateMLCEngine()ファクトリ関数を呼び出してモデルをロードできます。

(モデルのロードにはダウンロードが必要であり、キャッシュされていない最初の実行ではかなりの時間がかかる場合があります。この非同期呼び出しは適切に処理する必要があります。)

import { CreateMLCEngine } from "@mlc-ai/web-llm";

// モデルロードの進捗を更新するためのコールバック関数
const initProgressCallback = (initProgress) => {
  console.log(initProgress);
};
const selectedModel = "Llama-3.1-8B-Instruct-q4f32_1-MLC";

const engine = await CreateMLCEngine(
  selectedModel,
  { initProgressCallback: initProgressCallback }, // engineConfig
);

内部的には、このファクトリ関数は、最初にエンジンインスタンスを作成する(同期)ステップと、次にモデルをロードする(非同期)ステップを実行します。アプリケーションでこれらを別々に行うこともできます。

import { MLCEngine } from "@mlc-ai/web-llm";

// これは同期呼び出しで、すぐに返されます
const engine = new MLCEngine({
  initProgressCallback: initProgressCallback,
});

// これは非同期呼び出しで、完了に時間がかかる場合があります
await engine.reload(selectedModel);
キャッシュバックエンドポリシー

WebLLMは、AppConfig.cacheBackendを介して4つのキャッシュバックエンドをサポートしています。

例:

import { CreateMLCEngine, prebuiltAppConfig } from "@mlc-ai/web-llm";

const appConfig = { ...prebuiltAppConfig, cacheBackend: "cross-origin" };
const engine = await CreateMLCEngine("Llama-3.1-8B-Instruct-q4f32_1-MLC", {
  appConfig,
});

注:

  • OPFSをサポートしない環境で"opfs"が選択された場合、キャッシュ操作はOPFS可用性エラーで失敗します。
  • "opfs"を使用する場合、appConfig.opfsAccessModeを"auto"に設定すると、サポートされている場所でOPFS同期アクセスハンドルを使用できます。または"sync"に設定すると同期アクセスハンドルが必要になります。デフォルトは"async"です。
  • "cross-origin"バックエンドは、互換性のあるブラウザ拡張機能のインストールと有効化が必要です。
  • クロスオリジンバックエンドは現在、プログラムによるテンソルキャッシュの削除をサポートしていません。クリアは拡張機能によって管理されます。
チャット補完

エンジンを正常に初期化した後、engine.chat.completionsインターフェースを通じてOpenAIスタイルのチャットAPIを使用してチャット補完を呼び出すことができます。パラメータとその説明の完全なリストについては、以下のセクションとOpenAI APIリファレンスを参照してください。

(注: modelパラメータはサポートされておらず、無視されます。代わりに、Create MLCEngineに示されているようにCreateMLCEngine(model)またはengine.reload(model)を呼び出してください。)

const messages = [
  { role: "system", content: "You are a helpful AI assistant." },
  { role: "user", content: "Hello!" },
];

const reply = await engine.chat.completions.create({
  messages,
});
console.log(reply.choices[0].message);
console.log(reply.usage);
ストリーミング

WebLLMは、チャット補完生成のストリーミングもサポートしています。これを使用するには、engine.chat.completions.create呼び出しにstream: trueを渡すだけです。

const messages = [
  { role: "system", content: "You are a helpful AI assistant." },
  { role: "user", content: "Hello!" },
];

// ChunksはAsyncGeneratorオブジェクトです
const chunks = await engine.chat.completions.create({
  messages,
  temperature: 1,
  stream: true, // <-- ストリーミングを有効にする
  stream_options: { include_usage: true },
});

let reply = "";
for await (const chunk of chunks) {
  reply += chunk.choices[0]?.delta.content || "";
  console.log(reply);
  if (chunk.usage) {
    console.log(chunk.usage); // 最後のチャンクのみがusageを持つ
  }
}

const fullReply = await engine.getMessage();
console.log(fullReply);

高度な使用法

ワーカーの使用

アプリケーションのパフォーマンスを最適化するために、重い計算をワーカー スクリプトに配置できます。これを行うには、次の手順を実行する必要があります。

  1. フロントエンドと通信しながらリクエストを処理するハンドラーをワーカー スレッドに作成します。
  2. メインアプリケーションでWorker Engineを作成します。これは、内部的にワーカー スレッドのハンドラーにメッセージを送信します。

さまざまな種類のワーカーの詳細な実装については、以下のセクションを参照してください。

専用WebWorker

WebLLMにはWebWorker用のAPIサポートが付属しているため、生成プロセスを別のワーカー スレッドにフックして、ワーカー スレッドでの計算がUIを妨げないようにすることができます。

フロントエンドと通信しながらリクエストを処理するハンドラーをワーカー スレッドに作成します。

// worker.ts
import { WebWorkerMLCEngineHandler } from "@mlc-ai/web-llm";

// ワーカー スレッドに常駐するハンドラー
const handler = new WebWorkerMLCEngineHandler();
self.onmessage = (msg: MessageEvent) => {
  handler.onmessage(msg);
};

メインロジックでは、MLCEngineInterfaceを実装するWebWorkerMLCEngineを作成します。残りのロジックは同じままです。

// main.ts
import { CreateWebWorkerMLCEngine } from "@mlc-ai/web-llm";

async function main() {
  // ここでMLCEngineの代わりにWebWorkerMLCEngineを使用します
  const engine = await CreateWebWorkerMLCEngine(
    new Worker(new URL("./worker.ts", import.meta.url), {
      type: "module",
    }),
    selectedModel,
    { initProgressCallback }, // engineConfig
  );

  // 他のすべては同じままです
}
Service Workerの使用

WebLLMにはServiceWorker用のAPIサポートが付属しているため、生成プロセスをサービスワーカーにフックして、ページを訪問するたびにモデルを再ロードするのを回避し、アプリケーションのオフラインエクスペリエンスを最適化できます。

(注: Service Workerのライフサイクルはブラウザによって管理され、Webアプリに通知せずにいつでも終了される可能性があります。ServiceWorkerMLCEngineは定期的にハートビートイベントを送信してサービスワーカー スレッドをアクティブに保とうとしますが、アプリケーションにも適切なエラー処理を含める必要があります。詳細については、ServiceWorkerMLCEngineのkeepAliveMsとmissedHeatbeatを確認してください。)

フロントエンドと通信しながらリクエストを処理するハンドラーをワーカー スレッドに作成します。メッセージリスナーが初期スクリプト評価中に登録されるように、ワーカー スクリプトのトップレベルでハンドラーをインスタンス化します。activateまたはmessageリスナーからインスタンス化しないでください。ブラウザは、activateイベントを再度ディスパッチせずに、既にアクティブなワーカーを再起動できます。

// sw.ts
import { ServiceWorkerMLCEngineHandler } from "@mlc-ai/web-llm";

new ServiceWorkerMLCEngineHandler();
console.log("Service Worker is ready");

次に、メインロジックでサービスワーカーを登録し、CreateServiceWorkerMLCEngine関数を使用してエンジンを作成します。残りのロジックは同じままです。

// main.ts
import {
  MLCEngineInterface,
  CreateServiceWorkerMLCEngine,
} from "@mlc-ai/web-llm";

if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register(
    new URL("sw.ts", import.meta.url), // worker script
    { type: "module" },
  );
}

const engine: MLCEngineInterface = await CreateServiceWorkerMLCEngine(
  selectedModel,
  { initProgressCallback }, // engineConfig
);

サービスワーカーでWebLLMを実行する方法の完全な例は、examples/service-workerで見つけることができます。

Chrome拡張機能

examples/chrome-extensionおよびexamples/chrome-extension-webgpu-service-workerで、WebLLMを使用したChrome拡張機能の構築例も見つけることができます。後者はサービスワーカーを活用しているため、拡張機能はバックグラウンドで永続的です。さらに、WebLLMを活用したChrome拡張機能の別の完全なプロジェクトであるWebLLM Assistantをこちらで探索できます。

完全なOpenAI互換性

WebLLMはOpenAI APIと完全に互換性があるように設計されています。したがって、シンプルなチャットボットの構築に加えて、WebLLMで次の機能も利用できます。

  • ストリーミング: 出力をリアルタイムでAsyncGeneratorの形式でチャンクとして返します。
  • JSONモード: 出力がJSON形式であることを効率的に保証します。OpenAIリファレンスを参照してください。
  • 再現可能なシード: シードを使用して、seedフィールドで再現可能な出力を保証します。
  • 関数呼び出し(開発中): toolsとtool_choiceフィールドを使用した関数呼び出し(予備的なサポートあり)。または、toolsやtool_choiceなしの手動関数呼び出し(最大の柔軟性を維持)。

整合性検証

WebLLMは、SRI (Subresource Integrity)ハッシュを使用したモデルアーティファクトのオプションの整合性検証をサポートしています。ModelRecordにintegrityフィールドが設定されている場合、WebLLMはロード前にダウンロードされた設定、WASM、およびトークナイザーファイルを指定されたハッシュに対して検証します。

import { CreateMLCEngine } from "@mlc-ai/web-llm";

const appConfig = {
  model_list: [
    {
      model: "https://huggingface.co/mlc-ai/Llama-3.2-1B-Instruct-q4f16_1-MLC",
      model_id: "Llama-3.2-1B-Instruct-q4f16_1-MLC",
      model_lib:
        "https://raw.githubusercontent.com/user/model-libs/main/model.wasm",
      integrity: {
        config: "sha256-<base64-hash-of-mlc-chat-config.json>",
        model_lib: "sha256-<base64-hash-of-wasm-file>",
        tokenizer: {
          "tokenizer.json": "sha256-<base64-hash-of-tokenizer.json>",
        },
        onFailure: "error", // "error" (default) throws IntegrityError, "warn" logs and continues
      },
    },
  ],
};

const engine = await CreateMLCEngine("Llama-3.2-1B-Instruct-q4f16_1-MLC", {
  appConfig,
});

ハッシュは次のように生成できます。

# SHA-256
openssl dgst -sha256 -binary <file> | openssl base64 -A | sed 's/^/sha256-/'
# SHA-384
openssl dgst -sha384 -binary <file> | openssl base64 -A | sed 's/^/sha384-/'
# SHA-512
openssl dgst -sha512 -binary <file> | openssl base64 -A | sed 's/^/sha512-/'

opensslコマンドはUnixライクなシェル(macOS/Linux)を必要とします。Windowsでは、Git BashまたはWSLを介してopensslを実行してください。

ハッシュが一致しない場合、IntegrityErrorがスローされます(またはonFailure: "warn"の場合は警告がログに記録されます)。integrityのすべてのフィールドはオプションです。指定されたアーティファクトのみが検証されます。integrityフィールドが完全に省略されている場合、WebLLMは以前とまったく同じように動作します(検証なし)。

完全な動作デモについては、integrity-verification exampleを参照してください。

カスタムモデル

WebLLMはMLC LLMのコンパニオンプロジェクトとして機能し、MLC形式のカスタムモデルをサポートします。モデルアーティファクトを再利用し、MLC LLMのフローを構築します。独自のモデルをコンパイルしてWebLLMで使用するには、新しいモデルウェイトとライブラリをWebLLMにコンパイルしてデプロイする方法について、MLC LLMドキュメントを参照してください。

ここでは、ハイレベルなアイデアを説明します。WebLLMパッケージには、新しいモデルとウェイトバリアントを有効にする2つの要素があります。

  • model: モデルアーティファクト(ウェイトやメタデータなど)へのURLが含まれます。
  • model_lib: モデル計算を高速化する実行可能ファイルを含むWebAssemblyライブラリ(つまりwasmファイル)へのURLです。

これらは両方ともWebLLMでカスタマイズ可能です。

import { CreateMLCEngine } from "@mlc-ai/web-llm";

async main() {
  const appConfig = {
    "model_list": [
      {
        "model": "/url/to/my/llama",
        "model_id": "MyLlama-3b-v1-q4f32_0",
        "model_lib": "/url/to/myllama3b.wasm",
      }
    ],
  };
  // デフォルトをオーバーライド
  const chatOpts = {
    "repetition_penalty": 1.01
  };

  // 事前ビルドされたモデルをロード
  // チャットオプションのオーバーライドとアプリ設定付き
  // 内部的には、myLlamaUrlからモデルをロードします
  // ブラウザキャッシュにキャッシュします
  // チャットは、 myLlamaUrlのモデルと互換性があると仮定して、"/url/to/myllama3b.wasm"からモデルライブラリもロードします。
  const engine = await CreateMLCEngine(
    "MyLlama-3b-v1-q4f32_0",
    { appConfig }, // engineConfig
    chatOpts,
  );
}

多くの場合、新しいモデル(例: NeuralHermes-MistralはMistralのモデルライブラリを再利用できます)を必ずしも提供するのではなく、モデルウェイトバリアントのみを提供したい場合があります。モデルライブラリが異なるモデルバリアントによって共有される方法の例については、webllm.prebuiltAppConfigを参照してください。

ソースからWebLLMパッケージをビルドする

注: WebLLMパッケージを変更したい場合を除き、ソースからビルドする必要はありません。npmを使用するには、Get Startedまたは任意の例に従ってください。

ソースからビルドするには、次を実行します。

npm install
npm run build

次に、例でコード変更の効果をテストするには、examples/get-started/package.json内で、"@mlc-ai/web-llm": "^0.2.85"を"@mlc-ai/web-llm": ../..に変更します。

その後、次を実行します。

cd examples/get-started
npm install
npm start

場合によっては、npmに新しい変更を認識させるために、file:../..と../..を切り替える必要があることに注意してください。最悪の場合、次を実行できます。

cd examples/get-started
rm -rf node_modules dist package-lock.json .parcel-cache
npm install
npm start
TVMjsをソースからビルドする必要がある場合

WebLLMのランタイムは、主にTVMjsに依存しています: https://github.com/apache/tvm/tree/main/web

npmパッケージとしても利用可能ですが: https://www.npmjs.com/package/@mlc-ai/web-runtime、必要に応じて以下の手順に従ってソースからビルドできます。

  1. emscriptenをインストールします。これは、C/C++ソースコードをWebAssemblyにコンパイルするLLVMベースのコンパイラです。

    • 最新のemsdkをインストールするには、インストール手順に従ってください。
    • source path/to/emsdk_env.shを実行してemccがPATHから到達可能になり、emccコマンドが機能するようにします。

    ターミナルでemccを試すことで、正常なインストールを確認できます。

    注: 最近、最新のemccバージョンを使用すると実行時に問題が発生する可能性があることがわかりました。現在のところ、回避策として./emsdk install latestの代わりに./emsdk install 3.1.56を使用してください。エラーは次のようなものになる可能性があります。

    Init error, LinkError: WebAssembly.instantiate(): Import #6 module="wasi_snapshot_preview1"
    function="proc_exit": function import requires a callable
    
  2. ./package.jsonで、"@mlc-ai/web-runtime": "0.18.0-dev2",を"@mlc-ai/web-runtime": "file:./tvm_home/web",に変更します。

  3. 必要な環境を設定する

    Webビルドに必要な依存関係をすべて準備します。

    ./scripts/prep_deps.sh
    

    このステップで、環境に$TVM_SOURCE_DIRが定義されていない場合、次の行を実行してtvmjs依存関係をビルドします。

    git clone https://github.com/mlc-ai/relax 3rdparty/tvm-unity --recursive
    

    これはmlc-ai/relaxの現在のHEADをクローンしますが、常に正しいブランチまたはコミットをクローンするとは限りません。ソースから特定のnpmバージョンをビルドするには、バージョンバンプPRを参照してください。これは、現在のWebLLMバージョンがどのブランチ(例: mlc-ai/relaxまたはapache/tvm)とコミットに依存しているかを示しています。たとえば、バージョン0.2.52は、そのバージョンバンプPR #521によると、mlc-ai/relaxのHEADではなく、apache/tvmの次のコミットhttps://github.com/apache/tvm/commit/e6476847753c80e054719ac47bc2091c888418b6をチェックアウトしてビルドされています。

    さらに、--recursiveは必要であり、重要です。そうしないと、fatal error: 'dlpack/dlpack.h' file not foundのようなエラーが発生する可能性があります。

  4. WebLLMパッケージをビルドする

    npm run build
    
  5. サブパッケージの一部を検証する

    その後、examplesのサブフォルダに移動して、サブパッケージの一部を検証できます。バンドルにはParcelv2を使用しています。Parcelは親ディレクトリの変更を追跡するのが得意ではありませんが、WebLLMパッケージに変更を加えた場合は、サブフォルダのpackage.jsonを編集して保存すると、Parcelが再ビルドをトリガーします。

リンク

  • デモアプリ: WebLLM Chat
  • ネイティブランタイムでLLMを実行したい場合は、MLC-LLMを確認してください。
  • Web Stable Diffusionも興味があるかもしれません。

謝辞

このプロジェクトは、CMU Catalyst、UW SAMPL、SJTU、OctoML、およびMLCコミュニティのメンバーによって開始されました。オープンソースMLコミュニティの開発とサポートを継続していきたいと考えています。

このプロジェクトは、私たちが立っているオープンソースエコシステムの肩のおかげでのみ実現可能です。Apache TVMコミュニティとTVM Unityプロジェクトの開発者に感謝します。オープンソースMLコミュニティのメンバーがこれらのモデルを公開しました。PyTorchとHugging Faceコミュニティがこれらのモデルをアクセス可能にしました。Vicuna、SentencePiece、LLaMA、Alpacaのチームに感謝します。また、WebAssembly、Emscripten、WebGPUコミュニティにも感謝します。最後に、DawnとWebGPUの開発者に感謝します。

引用

このプロジェクトが役立つと思われる場合は、以下を引用してください。

@misc{ruan2026webllmhighperformanceinbrowserllm,
      title={WebLLM: A High-Performance In-Browser LLM Inference Engine},
      author={Charlie F. Ruan and Yucheng Qin and Akaash R. Parthasarathy and Xun Zhou and Ruihang Lai and Hongyi Jin and Yixin Dong and Bohan Hou and Meng-Shiun Yu and Yiyan Zhai and Sudeep Agarwal and Hangrui Cao and Siyuan Feng and Tianqi Chen},
      year={2026},
      eprint={2412.15803},
      archivePrefix={arXiv},
      primaryClass={cs.LG},
      url={https://arxiv.org/abs/2412.15803},
}

貢献者

[画像: contributors]

⬆ トップへ ⬆