Process Reward Models (PRMs): AIの推論プロセスを評価する新しいアプローチ

## はじめに

大規模言語モデル(LLM)の性能向上において、単に「正しい答え」を出すだけでなく、「正しい推論プロセス」を経て答えを導き出すことの重要性が高まっています。Process Reward Models (PRMs)は、この課題に対する革新的なアプローチとして注目を集めています。

本記事では、PRMsの基本概念から実用的な応用まで、分かりやすく解説します。

## Process Reward Models (PRMs) とは?

PRMsは、AIモデルが問題を解く**プロセスの各ステップ**を評価する報酬モデルです。最終的な答えの正誤だけでなく、そこに至るまでの推論の各段階が正しいかどうかを判定します。

### 具体例で理解する

数学の問題を例に考えてみましょう:

**問題**: 「りんごが12個あります。3人で等しく分けると、1人あたり何個になりますか?」

**AIの推論プロセス**:
1. ステップ1: 「12個のりんごを3人で分けるので、割り算を使う」 ✓
2. ステップ2: 「12 ÷ 3 = 4」 ✓
3. ステップ3: 「答えは4個」 ✓

PRMsは、**各ステップごとに**正しいかどうかをスコアリングします。

## ORM vs PRM: 何が違うのか?

従来のOutcome Reward Models (ORM)と比較すると、その違いが明確になります。

### ORM (Outcome Reward Model)
– **評価対象**: 最終的な答えのみ
– **スコア**: 答えが正しいか間違っているか
– **問題点**: 途中で間違った推論をしても、偶然正解にたどり着けば高評価

“`
問題: 2 + 3 × 4 = ?

間違った推論:
1. 2 + 3 = 5 ← 間違い(順序を無視)
2. 5 × 4 = 20 ← 間違い(順序を無視)
3. 答え: 14 ← 偶然正解

ORMの評価: 高スコア(答えが正しいため)
“`

### PRM (Process Reward Model)
– **評価対象**: 推論の各ステップ
– **スコア**: 各ステップの正しさを個別に評価
– **利点**: 正しい推論プロセスを学習できる

“`
正しい推論:
1. 掛け算を先に計算: 3 × 4 = 12 ← 正しい ✓
2. 足し算を実行: 2 + 12 = 14 ← 正しい ✓
3. 答え: 14 ← 正しい ✓

PRMの評価: 各ステップが高スコア
“`

## PRMsの主な利点

### 1. エラーの早期発見
推論の途中で誤りがあった場合、そのステップで低いスコアが付けられるため、問題箇所を特定しやすくなります。

### 2. 信頼性の向上
正しいプロセスを経た答えは、偶然の正解よりも信頼できます。

### 3. 説明可能性の向上
どのステップで高/低評価を受けたかが分かるため、AIの判断プロセスがより透明になります。

### 4. 複雑な問題への対応
多段階の推論が必要な問題(数学、プログラミング、論理的推論など)で特に効果的です。

## PRMsの応用分野

### 数学問題の解決
OpenAIの研究では、PRMsを使うことで数学問題の正答率が大幅に向上することが示されています。

“`python
# 疑似コード例: PRMsを使った推論
def solve_with_prm(problem):
steps = generate_reasoning_steps(problem)

for step in steps:
score = prm.evaluate_step(step, context)
if score < threshold: # スコアが低いステップを修正 step = regenerate_step(step, context) return final_answer ``` ### コード生成 プログラミングコードの生成においても、各行や各関数が適切かどうかを評価できます。 ### 論理的推論 複雑な論理問題や推論タスクにおいて、推論チェーンの各リンクを検証できます。 ## PRMsの訓練方法 PRMsの訓練には、主に以下のアプローチが使われます: ### 1. 人間によるアノテーション 人間が推論の各ステップを評価し、そのデータで訓練します。 ``` ステップ: "2と3を先に足す" 人間の評価: ❌ (演算順序が間違っている) ステップ: "3と4を先に掛ける" 人間の評価: ✅ (正しい) ``` ### 2. 自動検証 数学問題などでは、各ステップの計算結果を自動的に検証できます。 ### 3. Outcome Supervisionとの組み合わせ 最終的な答えの正誤情報も活用しながら、プロセスを評価します。 ## 実装のポイント PRMsを実装する際の重要なポイント: ### ステップの粒度 推論を適切な粒度でステップに分割することが重要です。 - 細かすぎる: 訓練データが大量に必要 - 粗すぎる: プロセスの評価が不十分 ### 評価の一貫性 人間のアノテーターが異なる評価をしないよう、明確なガイドラインが必要です。 ### スケーラビリティ 大規模なデータセットで訓練するため、効率的な実装が求められます。 ## 最新の研究動向 ### OpenAIの研究成果 OpenAIは「Let's Verify Step by Step」という論文で、PRMsがORMsよりも優れた性能を示すことを発表しました。特に数学問題において、PRMsは以下の結果を達成しています: - MATH datasetでの正答率向上 - より信頼性の高い推論プロセス - ベストオブN sampling での性能向上 ### 今後の展望 - **マルチモーダルPRMs**: テキストだけでなく、画像や図表を含む推論の評価 - **自己改善**: PRMsを使ってモデル自身が推論を改善 - **効率化**: より少ないアノテーションデータでの訓練方法 ## まとめ Process Reward Models (PRMs)は、AIの推論品質を根本的に向上させる技術です。主なポイントをまとめます: - **プロセス重視**: 答えだけでなく、推論の各ステップを評価 - **信頼性向上**: 正しいプロセスを経た答えはより信頼できる - **応用範囲**: 数学、コード生成、論理的推論など幅広い分野で有効 - **今後の発展**: さらなる性能向上と応用拡大が期待される PRMsは、より信頼できる、説明可能なAIシステムの構築に向けた重要な一歩と言えるでしょう。 ## 参考文献 - OpenAI: "Let's Verify Step by Step" - [Process Reward Modelsに関する最新の研究論文] - [RLHFとPRMsの関係に関する文献]

Geminiのコンテキストキャッシュ

Geminiコンテキストキャッシュは、繰り返し使用する入力トークンを保存して再利用できる機能 Google AIです。GoogleがGemini APIで提供している機能で、コストと遅延の削減を目的としています。

## 主な特徴
2つのタイプ
### 暗黙的キャッシュ(Implicit caching): 自動的に有効化され、キャッシュヒットが発生すると自動的にコスト削減が提供される Google機能で、2025年5月から利用可能になりました
### 明示的キャッシュ(Explicit caching): 開発者が手動でキャッシュを作成・管理する方法で、より細かい制御が可能

コスト削減効果
Gemini 2.5以降のモデルでは、キャッシュされたトークンに対して通常の入力トークンコストの10%のみを支払う Google Cloud仕組みです。

使用例
– 長い動画やドキュメントに対して複数の質問をする場合
– 大量のシステムプロンプトを繰り返し使用する場合
– チャットボットで製品情報や詳細なペルソナ設定を維持する場合
– 大規模なコードベースを分析する場合

制限事項
– デフォルトの有効期限(TTL)は1時間 Google AI
– モデルごとに最小トークン数の要件がある
– キャッシュされたコンテンツはプロンプトのプレフィックスとして機能

この機能により、同じ大量のコンテキスト情報を何度も送信する必要がなくなり、APIの使用がより効率的で経済的になります。

$ pip3 install google-generativeai

import google.generativeai as genai
import os
from dotenv import load_dotenv
load_dotenv()

# 設定
genai.configure(api_key=os.environ.get("GEMINI_API_KEY"))

# モデルを直接初期化(キャッシュなし)
model = genai.GenerativeModel('gemini-2.5-flash')

# 長い文書(毎回送信される)
long_document = """
製品名: スマートウォッチ Pro X

特徴:
- 心拍数モニタリング機能
- GPS内蔵
- 防水性能(50m)
- バッテリー持続時間: 最大7日間
"""

print("📝 通常の方法(キャッシュなし)でGeminiを使用\n")

# 質問1
print("❓ 質問1: バッテリー持続時間は?")
response1 = model.generate_content(
    f"{long_document}\n\nバッテリーは何日持ちますか?"
)
print(f"💬 回答: {response1.text}\n")

# 質問2(毎回long_documentを送信する必要がある)
print("❓ 質問2: 防水性能は?")
response2 = model.generate_content(
    f"{long_document}\n\nこの製品は防水ですか?"
)
print(f"💬 回答: {response2.text}\n")

print("="*60)
print("💡 キャッシュとの違い:")
print("  - キャッシュなし: 毎回long_documentを送信(コスト高)")
print("  - キャッシュあり: 1度だけ送信、以降は再利用(コスト削減)")
print("\n⚠️  無料枠ではキャッシュに厳しい制限があるため、")
print("    実用的にはこちらの方法を使う必要があるかもしれません。")

$ python3 gemini_without_cache.py
📝 通常の方法(キャッシュなし)でGeminiを使用

❓ 質問1: バッテリー持続時間は?
WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
E0000 00:00:1769337671.150316 345042 alts_credentials.cc:93] ALTS creds ignored. Not running on GCP and untrusted ALTS is not enabled.
💬 回答: バッテリーは**最大7日間**持ちます。

❓ 質問2: 防水性能は?
💬 回答: はい、この製品は防水です。

特徴に「防水性能(50m)」と記載されています。

============================================================
💡 キャッシュとの違い:
– キャッシュなし: 毎回long_documentを送信(コスト高)
– キャッシュあり: 1度だけ送信、以降は再利用(コスト削減)

⚠️ 無料枠ではキャッシュに厳しい制限があるため、
実用的にはこちらの方法を使う必要があるかもしれません。

import os
import google.generativeai as genai
from google.generativeai import caching
import datetime
import os

from dotenv import load_dotenv
load_dotenv()

# 設定
genai.configure(api_key=os.environ.get("GEMINI_API_KEY"))

# 1. キャッシュ作成(長い文書を1回だけ保存)
cache = caching.CachedContent.create(
    model="gemini-1.5-flash",  # バージョン番号なし
    contents=["これは長い文書です。" * 1000],  # 繰り返し使うコンテンツ
    ttl=datetime.timedelta(minutes=30),
)

# 2. キャッシュを使ってモデル初期化
model = genai.GenerativeModel.from_cached_content(cache)

# 3. 質問(キャッシュが自動で使われる)
response = model.generate_content("この文書を要約してください")
print(response.text)

# 4. 別の質問(同じキャッシュを再利用 = コスト削減!)
response2 = model.generate_content("文書のキーワードを3つ教えてください")
print(response2.text)

# 5. 削除
cache.delete()

Gemini APIのコンテキストキャッシュ(Context Caching)は、原則として「有料(従量課金制)プラン」のみの機能となっています。

Figma AIを使ってみる

Figma Makeで使うと…

Lovableみたいに自然文でUI Designを作ってくれる

### 生成された後の便利な操作
生成されたデザインをベースに、以下のAI機能を組み合わせできる

– 指示を追加して微調整(Iterate): 左下の「Ask for changes」という入力欄に、「背景をもっと暗くして」や「ロゴを左上に配置して」と入力すると、今のデザインを維持したまま修正

– プロトタイプを自動で貼る: 「新規登録」などのボタンが配置されているので、別の画面も生成した後にAIメニューから「Make prototype」を実行すれば、ボタンの遷移設定も自動で行える

– レイヤー名の整理: AIが生成した直後はレイヤー名が整っていますが、自分で編集を加えた後はAIメニューの「Rename layers」を使って、常に整理された状態を保つのがおすすめ

Figma Makeによるモックアップ

今のプロの現場では、「AIに80点のワイヤーフレームを30秒で出させ、残りの20点を人間が手書きや微調整で100点に磨き上げる」という使い方がされている

なるほど~

[Design] ニールセンの10のヒューリスティックステスト

チェック項目を作成して、WFの段階できんちんと1枚ずつ確認していく

システム状態の視認性
L システムが今何をしているか、ユーザーに常にフィードバックを返すこと(例:アップロード中のプログレスバー)。
システムと実世界の一致
L 専門用語ではなく、ユーザーが使い慣れた言葉や概念を使うこと(例:削除ボタンにゴミ箱のアイコンを使う)。
ユーザーの制御権と自由
L 間違えて操作したときに、すぐにやり直したりキャンセルしたりできる「非常口」を用意すること。
一貫性と標準
L 同じ製品内や業界の標準で、言葉や操作の一貫性を保つこと(例:リンクは青色にする、など)。
エラーの防止
L エラーメッセージを出す前に、そもそもエラーが起きないような設計にすること(例:削除前に確認ダイアログを出す)。
想起(記憶)よりも再認
L ユーザーに何かを覚えさせなくても、見れば操作がわかるようにすること。
柔軟性と効率性
L 初心者には分かりやすく、上級者にはショートカットなどの効率的な手段を提供すること。
美学的で最小限のデザイン
L 不要な情報は、本当に必要な情報の邪魔をしないように削ぎ落とすこと。
ユーザーによるエラーの認識・診断・回復のサポート
L エラーが起きた際、何が問題でどう解決すればいいかを平易な言葉で伝えること。
ヘルプとドキュメント

[TTS] Mel-spectrogram出力 → vocoder変換 のサンプル

「波形への復元」には、数学的な近似アルゴリズムである Griffin-Lim法 を使う

import librosa
import numpy as np
import soundfile as sf

# 1. アップロードされたファイルの読み込み
# path は実行環境に合わせて調整してください(通常はカレントディレクトリ)
file_path = 'speech.mp3' 
y, sr = librosa.load(file_path, sr=None) # sr=None で元のサンプリングレートを維持

# 2. Mel-spectrogram への変換
S = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=128)
S_dB = librosa.power_to_db(S, ref=np.max)

# 3. 復元 (Griffin-Lim)
S_inv = librosa.db_to_power(S_dB)
y_inv = librosa.feature.inverse.mel_to_audio(S_inv, sr=sr)

# 4. 保存
sf.write('output_speech.wav', y_inv, sr)
print(f"'{file_path}' の処理が完了し、'output_speech.wav' として保存しました。")


output

Mel-spectrogramとは?: 音の強さを「時間」と「周波数」の2軸で表したものですが、周波数軸を人間の耳の特性に近いメル尺度に変換したものです。

Vocoder(ボコーダー)の役割: スペクトログラムから音声を復元する際、元の波形が持っていた「位相(Phase)」の情報が失われています。このサンプルで使った mel_to_audio(Griffin-Lim)は、その位相を推測して補完する簡易的なボコーダーの役割を果たしています。

より高品質にするには: 実用的なAI(TTSや声質変換)では、Griffin-Limの代わりに HiFi-GAN や BigVGAN といったディープラーニングベースのボコーダーを使って、より肉声に近い高品質な復元を行います。

位相推論に限界があるということですね。

instagram api

$ install instaloader

import instaloader

# インスタンスの作成
L = instaloader.Instaloader()

# 取得したいユーザーのIDを指定
target_profile = "instagram"  # 例としてInstagram公式アカウント

try:
    # プロフィール情報の読み込み
    profile = instaloader.Profile.from_username(L.context, target_profile)

    print(f"ユーザー名: {profile.username}")
    print(f"フォロワー数: {profile.followers}")
    print(f"自己紹介: {profile.biography}")

    # 最新の投稿を3件表示
    print("\n--- 最新の投稿 ---")
    for post in profile.get_posts():
        print(f"投稿日: {post.date}")
        print(f"キャプション: {post.caption[:30]}...") # 最初の30文字だけ
        print(f"URL: {post.url}")
        
        # 3件取得したらストップ
        if post.owner_username == target_profile:
            break 

except Exception as e:
    print(f"エラーが発生しました: {e}")

$ python3 app.py
ユーザー名: instagram
フォロワー数: 698490388
自己紹介: Discover what’s new on Instagram 🔎✨

— 最新の投稿 —
投稿日: 2025-12-26 21:05:21
キャプション: the definition of “never let t…
URL: https://scontent-nrt1-2.cdninstagram.com/v/t51.2885-15/606469868_18689042716001321_9120174716535860606_n.jpg?stp=dst-jpg_e15_fr_p1080x1080_tt6&_nc_ht=scontent-nrt1-2.cdninstagram.com&_nc_cat=1&_nc_oc=Q6cZ2QFZnredhbdVl_SvagHdS1_L6FOS6xwQKfiSolX7tuMyxIYTOVN7Um-LzzQ_jBneDHI&_nc_ohc=VXPBA7g5rlQQ7kNvwFrzZcc&_nc_gid=s6qDPQ_v2Qy2aL1kVrz0Wg&edm=AOQ1c0wBAAAA&ccb=7-5&oh=00_AfmMpzoxHZbJe-l-Inrc-cUqHsm3Nfz13kckIgLq0ttU4A&oe=69584AA3&_nc_sid=8b3546

instaloader は、ブラウザでインスタを見る時と同じような仕組みで情報を読み取っているため、**「公開アカウント」かつ「数件の取得」**であれば、面倒なAPIキーの手続きなしでサクッと動く

「仕事で使いたい」「公式にアプリをリリースしたい」となった場合は、Meta社が提供する**「Instagram Graph API」**を使う必要がある

import instaloader

L = instaloader.Instaloader()

# 検索したいハッシュタグ(#は不要)
hashtag_name = "猫"

try:
    # ハッシュタグオブジェクトの作成
    hashtag = instaloader.Hashtag.from_name(L.context, hashtag_name)

    print(f"#{hashtag_name} の投稿を取得中...")

    # 最新の投稿を5件ループで回す
    for count, post in enumerate(hashtag.get_posts(), 1):
        print(f"\n[{count}件目]")
        print(f"投稿者: {post.owner_username}")
        print(f"いいね数: {post.likes}")
        print(f"キャプション: {post.caption[:50]}...") # 50文字まで
        print(f"URL: {post.url}")

        # 負荷軽減のため5件でストップ
        if count >= 5:
            break

except Exception as e:
    print(f"エラーが発生しました: {e}")

$ python3 cat.py
JSON Query to api/v1/tags/web_info/: 403 Forbidden – “fail” status, message “login_required” when accessing https://i.instagram.com/api/v1/tags/web_info/?__a=1&__d=dis&tag_name=%E7%8C%AB [retrying; skip with ^C]
JSON Query to api/v1/tags/web_info/: 403 Forbidden – “fail” status, message “login_required” when accessing https://i.instagram.com/api/v1/tags/web_info/?__a=1&__d=dis&tag_name=%E7%8C%AB [retrying; skip with ^C]
エラーが発生しました: JSON Query to api/v1/tags/web_info/: 403 Forbidden – “fail” status, message “login_required” when accessing https://i.instagram.com/api/v1/tags/web_info/?__a=1&__d=dis&tag_name=%E7%8C%AB

タグ検索の場合は、ログインが必要となる。
L.login(“自分のユーザー名”, “パスワード”)

本格的にやりたい場合はInstagram Graph API

その他

import instaloader

L = instaloader.Instaloader()

# --- 修正ポイント:パスワードを使わずブラウザのクッキーを使う ---
# Chromeがインストールされている場合、以下の1行でセッションを読み込めます
# 注意: 初回実行時にOSからブラウザデータへのアクセス許可を求められることがあります
USER = ""
try:
    L.interactive_login(USER) # または L.load_session_from_instaloader(USER)
    # 最も簡単な方法:
    # ブラウザのクッキーをインポートするヘルパー関数(instaloader公式推奨)
    # ※ pip install browser-cookie3 が必要な場合があります
    # 以下の1行でブラウザのログイン状態を引き継ぎます
    L.load_session_from_instaloader(USER) 
except:
    # ブラウザからセッションを取得するスクリプトを別途実行するか
    # 以下のコマンドをターミナルで実行してセッションファイルを作ってください
    # instaloader -l YOUR_USER_NAME
    print("セッションの読み込みに失敗しました。")

# 実行
hashtag_name = "猫"
hashtag = instaloader.Hashtag.from_name(L.context, hashtag_name)

for count, post in enumerate(hashtag.get_posts(), 1):
    print(f"[{count}] {post.owner_username}: {post.caption[:20]}...")
    if count >= 3: break

Open AI apiの TextToImage

import os
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()

# クライアント作成
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

# 2. 画像生成のリクエスト
response = client.images.generate(
    model="dall-e-3",
    prompt="サイバーパンクな都市を歩く柴犬",
    n=1,
    size="1024x1024"
)

# 3. 生成された画像のURLを表示
print(response.data[0].url)

geminiも同じようなことができる

import os
from google import genai
from google.genai import types
from PIL import Image
from io import BytesIO

# 1. クライアントの初期化(APIキーを入力)
# client = genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))

# 2. 画像生成のリクエスト(Imagen 3モデルを使用)
response = client.models.generate_images(
    model='imagen-4.0-generate-001', 
    prompt='サイバーパンクな都市を歩く柴犬',
    config=types.GenerateImagesConfig(
        number_of_images=1,
    )
)

# 3. 生成された画像を表示・保存
for generated_image in response.generated_images:
    # バイトデータから画像を開く
    image = Image.open(BytesIO(generated_image.image.image_bytes))
    image.show() # 標準のフォトビューアーで開く
    # image.save("result.png") # ファイルとして保存する場合

[App] Rorkでアプリを作成する

### プロンプト
シンプルな「日英翻訳アプリ」を作成してください。
このアプリは、外部の翻訳APIは使用せず、内部に持たせた簡単な辞書データで翻訳処理を代替します。

### 1. 辞書データ(必須)
以下のデータを持ったオブジェクトまたは配列をコンポーネント内に定義してください。

* **日本語:** ‘こんにちは’ → **英語:** ‘Hello’
* **日本語:** ‘ありがとう’ → **英語:** ‘Thank you’
* **日本語:** ‘さようなら’ → **英語:** ‘Goodbye’
* **日本語:** ‘はい’ → **英語:** ‘Yes’
* **日本語:** ‘いいえ’ → **英語:** ‘No’

### 2. UI/機能要件(必須)
1つの画面(App.jsのみで完結)に以下の要素を配置してください。

* **入力エリア:** ユーザーが日本語の単語を入力できる `` コンポーネント。
* **翻訳ボタン:** 「翻訳する」というテキストのボタン。
* **結果表示:** 翻訳結果(英語)を表示する `` コンポーネント。初期値は「翻訳結果がここに表示されます」としてください。

### 3. 処理ロジック
1. ユーザーが入力エリアに日本語を入力し、「翻訳する」ボタンを押します。
2. 入力された日本語が、辞書データ内に存在するかチェックします。
3. 存在する場合、対応する英語を結果表示エリアに表示します。
4. 辞書データに存在しない単語が入力された場合、「辞書に単語がありません」というメッセージを表示してください。

ここまで来たか… oh my goodness

Three.jsで3Dアバターの表示

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Three.js glTF アバターデモ (最終サイズ調整版)</title>
    <style>
        body { margin: 0; overflow: hidden; background-color: #f0f0f0; }
        canvas { display: block; }
    </style>
    
    <script src="https://unpkg.com/three@0.137.0/build/three.min.js"></script>
    <script src="https://unpkg.com/three@0.137.0/examples/js/loaders/GLTFLoader.js"></script>
    <script src="https://unpkg.com/three@0.137.0/examples/js/controls/OrbitControls.js"></script>
</head>
<body>
    <script>
        // ==========================================================
        // 1. シーン、カメラ、レンダラーのセットアップ
        // ==========================================================
        const scene = new THREE.Scene();
        // カメラのクリッピング範囲を広げる (near=0.001, far=10000)
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.001, 10000); 
        const renderer = new THREE.WebGLRenderer({ antialias: true });
        
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.outputEncoding = THREE.sRGBEncoding; 
        renderer.toneMapping = THREE.ACESFilmicToneMapping;
        renderer.setClearColor(0xcccccc, 1); 
        document.body.appendChild(renderer.domElement);

        // ==========================================================
        // 2. ライトのセットアップ
        // ==========================================================
        const ambientLight = new THREE.AmbientLight(0xffffff, 2.0); 
        scene.add(ambientLight);

        const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
        directionalLight.position.set(5, 10, 5);
        scene.add(directionalLight);

        const pointLight = new THREE.PointLight(0xffffff, 3.0);
        pointLight.position.set(0, 3, 0); 
        scene.add(pointLight);

        // ==========================================================
        // 3. GLTFLoaderとアニメーションミキサーのセットアップ
        // ==========================================================
        const loader = new THREE.GLTFLoader(); 
        let mixer; 
        const clock = new THREE.Clock(); 

        // ==========================================================
        // 4. モデルの読み込み
        // ==========================================================
        const MODEL_PATH = './ellen_joe_oncampus/scene.gltf';

        loader.load(
            MODEL_PATH,
            function (gltf) {
                const model = gltf.scene;
                scene.add(model);

                // 【最終修正点】モデルを 50倍に拡大し、適切なサイズで表示
                model.scale.set(50, 50, 50); 
                
                // モデルの位置を強制的に原点 (0, 0, 0) に設定
                model.position.set(0, 0, 0); 
                
                // アニメーションデータの処理
                if (gltf.animations && gltf.animations.length > 0) {
                    mixer = new THREE.AnimationMixer(model);
                    const action = mixer.clipAction(gltf.animations[0]);
                    action.play();
                } else {
                    console.log('モデルにアニメーションデータが見つかりませんでした。静止画として表示します。');
                }
            },
            function (xhr) {
                console.log('モデル読み込み中: ' + (xhr.loaded / xhr.total * 100).toFixed(2) + '%');
            },
            function (error) {
                console.error('モデルの読み込み中にエラーが発生しました。', error);
            }
        );

        // ==========================================================
        // 5. カメラとコントロールの設定
        // ==========================================================
        // 【最終修正点】50倍スケールに合わせてカメラを遠ざけ、全体が見えるように調整
        camera.position.set(0, 5, 10); 

        const controls = new THREE.OrbitControls(camera, renderer.domElement); 
        // 【最終修正点】注視点をモデルの中心 (Y=5m) に設定
        controls.target.set(0, 5, 0); 
        controls.update();

        // ==========================================================
        // 6. アニメーションループ (毎フレームの更新処理)
        // ==========================================================
        function animate() {
            requestAnimationFrame(animate);

            const delta = clock.getDelta();

            if (mixer) {
                mixer.update(delta);
            }

            controls.update();
            renderer.render(scene, camera);
        }

        animate();

        // 画面サイズ変更時の対応
        window.addEventListener('resize', () => {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        });

    </script>
</body>
</html>