3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Unity】XREAL One Pro + XREAL Eye のカメラ映像を Gemini API で解析する

Last updated at Posted at 2025-11-30

この記事は Iwaken Lab. Advent Calendar 2025 1日目の記事です。

【Unity】XREAL One Pro + XREAL Eye のカメラ映像を Gemini API で解析する

こんにちは、XR エンジニアのイワケンです。

GoogleからAndroid XRが発表され、ARグラスとAIの距離がどんどん縮まっています。そんな中、「ARグラスで見ているものをAIに解析させたい」と思っている方も多いのではないでしょうか。

この記事では、Unity を使って XREAL One Pro + XREAL Eye でカメラ映像をキャプチャし、Gemini API で解析するまでの実装を紹介します。

※ XREAL One でも同じコードで動作します。

この開発は、TokyoNodeXRハッカソンの作品「eX Attendant」で活用しました。

image.png

前提知識:XREAL Eye について

XREAL Eye とは

XREAL One / One Pro 単体にはカメラが搭載されていません。XREAL Eye というアクセサリを装着することで、RGBカメラが使えるようになります。

「XREAL Air 2 Ultra にはカメラがあるのでは?」と思われるかもしれませんが、あちらはモノクロのトラッキング用センサーのため、カラー映像の取得には使用できません。

開発時の制約について

開発者アプリでは 3DoF までの対応となります(通常使用時は6DoF)。

開発環境

この記事の実装は、以下の環境で動作確認しています。

  • Unity 2022.3.62f2
  • XREAL SDK 3.0.0
  • UniTask 2.5.0以上

動作確認には XREAL Beam Pro を使用しています。Android スマートフォンでも DP Alt Mode 対応であれば動作しますが、とりあえず試す場合は、Beam Pro での開発がおすすめです。

XREAL SDK の導入

  1. XREAL Developer Download から com.xreal.xr.tar.gz をダウンロード
  2. Unity で「Window」→「Package Manager」を開く
  3. 左上の「+」→「Add package from tarball...」を選択
  4. ダウンロードした com.xreal.xr.tar.gz を選択してインポート
  5. 「Edit」→「Project Settings」→「XR Plug-in Management」で XREAL を有効化

詳細は 公式ドキュメント を参照してください。

Gemini API キーの取得

Google AI Studio にアクセスし、「Get API key」→「Create API key」で取得できます。無料枠で1日約1,500リクエスト使用できるため、開発用途には十分です。

UniTask のインストール

この記事のサンプルコードでは、非同期処理を行うために UniTask を使用しています。

  1. Unity で「Window」→「Package Manager」を開く
  2. 左上の「+」→「Add package from git URL...」を選択
  3. 以下のURLを入力して「Add」をクリック
https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask

実装

やりたいことはシンプルで、カメラで撮った画像を Gemini に送信して結果を表示するだけです。

ただし、ここで重要な注意点があります。

プレビュー用の XREALRGBCameraTexture から直接キャプチャすると真っ黒な画像になります。
キャプチャには XREALPhotoCapture を使用してください。この点は公式ドキュメントに明記されておらず、実装時にかなり時間を取られました。

以下、実際のコードを紹介します。

1. カメラの初期化

using System;
using System.Text;
using Unity.XR.XREAL;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Networking;
using TMPro;
using Cysharp.Threading.Tasks;

/// <summary>
/// XREAL Eye のカメラ映像を Google Gemini API で解析するクラス
/// 
/// 【注意】本コードは動作確認・学習用のサンプルです。
/// 本番運用では以下の強化を推奨します:
/// - 例外処理の追加(ネットワークエラー、タイムアウトなど)
/// - 画像サイズの制限チェック(Base64文字列長の制限)
/// - リトライロジックの実装
/// - JSONシリアライザの使用(Newtonsoft.Json など)
/// </summary>
public class XREALGemini : MonoBehaviour
{
    [Header("UI References")]
    [SerializeField] RawImage previewImage;
    [SerializeField] RawImage capturedImage;
    [SerializeField] TextMeshProUGUI resultText;
    
    [Header("Gemini Settings")]
    [SerializeField] string apiKey = "YOUR_GEMINI_API_KEY_HERE";
    [SerializeField] string analysisPrompt = "この画像に何が映っていますか?日本語で答えてください。";
    
    // Gemini API 設定(定数化して将来の変更に対応しやすく)
    private const string GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models/";
    private const string GEMINI_MODEL = "gemini-2.5-flash";
    private const string GEMINI_ACTION = ":generateContent";
    
    // Gemini API URL(APIキーをクエリパラメータで渡す)
    private string ApiUrl => $"{GEMINI_BASE_URL}{GEMINI_MODEL}{GEMINI_ACTION}?key={apiKey}";
    
    private XREALRGBCameraTexture m_RGBCameraTexture;
    
    // キャプチャした画像を保持
    private string capturedBase64Image;
    private Texture2D capturedTexture;
    
    void Start()
    {
        InitializeXREALCamera();
    }
    
    void InitializeXREALCamera()
    {
        // XREAL Eye の RGB カメラテクスチャを作成
        m_RGBCameraTexture = XREALRGBCameraTexture.CreateSingleton();
        
        // カメラキャプチャを開始
        m_RGBCameraTexture.StartCapture();
        
        Debug.Log("XREAL Eye Camera initialized");
    }
}

2. プレビュー表示

カメラはYUVフォーマットで出力されるため、専用のシェーダーが必要になります。

void Update()
{
    // カメラプレビューを更新
    UpdateCameraPreview();
}

void UpdateCameraPreview()
{
    // YUV フォーマットのテクスチャを取得
    var yuvTextures = m_RGBCameraTexture.GetYUVFormatTextures();
    
    if (yuvTextures[0] != null)
    {
        // プレビュー画像に YUV テクスチャを設定
        previewImage.texture = yuvTextures[0];
        previewImage.material.SetTexture("_UTex", yuvTextures[1]);
        previewImage.material.SetTexture("_VTex", yuvTextures[2]);
    }
}

3. 画像キャプチャの準備

ここが重要なポイントです。プレビュー用とキャプチャ用で異なるAPIを使用します

using System.Linq;

private XREALPhotoCapture photoCapture;
private Resolution cameraResolution;

// XREALPhotoCapture を初期化
async UniTask InitializePhotoCaptureAsync()
{
    var tcs = new UniTaskCompletionSource();
    
    XREALPhotoCapture.CreateAsync(false, (captureObject) =>
    {
        photoCapture = captureObject;
        
        // サポートされている最高解像度を取得
        cameraResolution = XREALPhotoCapture.SupportedResolutions
            .OrderByDescending(res => res.width * res.height)
            .First();
        
        Debug.Log($"PhotoCapture initialized: {cameraResolution.width}x{cameraResolution.height}");
        tcs.TrySetResult();
    });
    
    await tcs.Task;
}

4. 撮影処理

// キャプチャボタンから呼び出される公開メソッド
public async void Capture()
{
    await CaptureAsync();
}

// 非同期でカメラ映像をキャプチャ
public async UniTask CaptureAsync()
{
    // PhotoCapture が未初期化なら初期化
    if (photoCapture == null)
    {
        await InitializePhotoCaptureAsync();
    }
    
    // PhotoMode を開始
    await StartPhotoModeAsync();
    
    // 写真をキャプチャ
    await TakePhotoAsync();
    
    // PhotoMode を停止
    await StopPhotoModeAsync();
}

// PhotoMode を開始
async UniTask StartPhotoModeAsync()
{
    var tcs = new UniTaskCompletionSource();
    
    var cameraParams = new CameraParameters
    {
        cameraType = Unity.XR.XREAL.CameraType.RGB,
        hologramOpacity = 0.0f,
        frameRate = 30,
        cameraResolutionWidth = cameraResolution.width,
        cameraResolutionHeight = cameraResolution.height,
        pixelFormat = CapturePixelFormat.PNG,
        blendMode = BlendMode.CameraOnly,
        captureSide = CaptureSide.Single
    };
    
    photoCapture.StartPhotoModeAsync(cameraParams, (result) =>
    {
        Debug.Log("PhotoMode started");
        tcs.TrySetResult();
    }, true);
    
    await tcs.Task;
}

// 写真をキャプチャ
async UniTask TakePhotoAsync()
{
    var tcs = new UniTaskCompletionSource();
    
    photoCapture.TakePhotoAsync((result, photoCaptureFrame) =>
    {
        // PNG形式のバイト配列を直接取得
        byte[] imageBytes = photoCaptureFrame.TextureData;
        
        // Base64 形式に変換
        capturedBase64Image = Convert.ToBase64String(imageBytes);
        
        // Texture2D に変換して表示
        var texture = new Texture2D(cameraResolution.width, cameraResolution.height);
        photoCaptureFrame.UploadImageDataToTexture(texture);
        
        capturedTexture = texture;
        capturedImage.texture = texture;
        
        Debug.Log($"Photo captured: {imageBytes.Length / 1024}KB");
        tcs.TrySetResult();
    });
    
    await tcs.Task;
}

// PhotoMode を停止
async UniTask StopPhotoModeAsync()
{
    var tcs = new UniTaskCompletionSource();
    
    photoCapture.StopPhotoModeAsync((result) =>
    {
        Debug.Log("PhotoMode stopped");
        tcs.TrySetResult();
    });
    
    await tcs.Task;
}

5. 解析処理

// 解析ボタンから呼び出される公開メソッド
public async void Analyze()
{
    if (string.IsNullOrEmpty(capturedBase64Image))
    {
        Debug.LogError("No captured image to analyze. Please capture an image first.");
        return;
    }
    
    await AnalyzeAsync();
}

// 非同期でキャプチャした画像を解析
public async UniTask AnalyzeAsync()
{
    await SendToGeminiAsync(capturedBase64Image);
}

6. 撮影と解析を連続実行

// ボタンから呼び出される公開メソッド
public async void CaptureAndAnalyze()
{
    await CaptureAndAnalyzeAsync();
}

// 非同期でキャプチャと解析を順番に実行
public async UniTask CaptureAndAnalyzeAsync()
{
    // まずキャプチャを実行
    await CaptureAsync();
    
    // キャプチャが成功したら解析を実行
    if (!string.IsNullOrEmpty(capturedBase64Image))
    {
        await SendToGeminiAsync(capturedBase64Image);
    }
    else
    {
        Debug.LogError("Failed to capture image");
    }
}

7. Gemini API に送信

// 非同期で Gemini API に画像を送信
async UniTask SendToGeminiAsync(string base64Image)
{
    // 注意: 手書きJSONは簡便だが、プロンプト中に " が含まれると壊れる可能性があります
    // 実運用では Newtonsoft.Json などのシリアライザ使用を推奨
    
    // Gemini API 用の JSON データを構築
    string jsonData = $@"{{
        ""contents"": [
            {{
                ""parts"": [
                    {{
                        ""text"": ""{EscapeJsonString(analysisPrompt)}""
                    }},
                    {{
                        ""inline_data"": {{
                            ""mime_type"": ""image/png"",
                            ""data"": ""{base64Image}""
                        }}
                    }}
                ]
            }}
        ]
    }}";
    
    Debug.Log("Sending to Gemini API...");
    
    // UnityWebRequest を作成
    using (UnityWebRequest request = new UnityWebRequest(ApiUrl, "POST"))
    {
        byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonData);
        request.uploadHandler = new UploadHandlerRaw(bodyRaw);
        request.downloadHandler = new DownloadHandlerBuffer();
        request.SetRequestHeader("Content-Type", "application/json");
        // Gemini は Authorization ヘッダーではなく、クエリパラメータで API キーを渡す
        
        // リクエスト送信(UniTask で await)
        await request.SendWebRequest();
        
        if (request.result == UnityWebRequest.Result.Success)
        {
            string response = request.downloadHandler.text;
            ParseGeminiResponse(response);
        }
        else
        {
            Debug.LogError($"Error: {request.error}");
            Debug.LogError($"Response: {request.downloadHandler.text}");
        }
    }
}

// JSON文字列内の特殊文字をエスケープ
string EscapeJsonString(string str)
{
    return str
        .Replace("\\", "\\\\")
        .Replace("\"", "\\\"")
        .Replace("\n", "\\n")
        .Replace("\r", "\\r")
        .Replace("\t", "\\t");
}

Gemini API は ?key= でAPIキーを渡す点が OpenAI と異なります。

8. レスポンス解析

using TMPro;

[Header("UI References")]
[SerializeField] TextMeshProUGUI resultText;

void ParseGeminiResponse(string jsonResponse)
{
    try
    {
        var response = JsonUtility.FromJson<GeminiResponse>(jsonResponse);
        
        if (response.candidates != null && response.candidates.Length > 0)
        {
            var candidate = response.candidates[0];
            if (candidate.content != null && candidate.content.parts != null && candidate.content.parts.Length > 0)
            {
                string result = candidate.content.parts[0].text;
                
                if (resultText != null)
                {
                    resultText.text = result;
                }
                
                Debug.Log($"Gemini Response: {result}");
            }
        }
    }
    catch (Exception e)
    {
        Debug.LogError($"Failed to parse response: {e.Message}");
        Debug.LogError($"Response JSON: {jsonResponse}");
        if (resultText != null)
        {
            resultText.text = $"Failed to parse response: {e.Message}";
        }
    }
}

// Gemini レスポンス用のデータクラス
[Serializable]
public class GeminiResponse
{
    public GeminiCandidate[] candidates;
}

[Serializable]
public class GeminiCandidate
{
    public GeminiContent content;
}

[Serializable]
public class GeminiContent
{
    public GeminiPart[] parts;
}

[Serializable]
public class GeminiPart
{
    public string text;
}

9. クリーンアップ

void OnDestroy()
{
    // XREAL Eye のカメラ停止
    if (m_RGBCameraTexture != null && m_RGBCameraTexture.IsCapturing)
    {
        m_RGBCameraTexture.StopCapture();
        Debug.Log("XREAL Eye Camera stopped");
    }
    
    // PhotoCapture のリソースを解放
    if (photoCapture != null)
    {
        photoCapture.Dispose();
        photoCapture = null;
        Debug.Log("PhotoCapture disposed");
    }
}

実装時の注意点

1. プレビューとキャプチャは別API

用途 使用するAPI
プレビュー XREALRGBCameraTexture
キャプチャ XREALPhotoCapture

プレビューから直接キャプチャしようとすると真っ黒な画像になります。この点が最もハマりやすいポイントです。

2. 開発者アプリは3DoFまで

通常使用では6DoFですが、開発者アプリでは3DoFまでの対応となります。公式ドキュメントに明記されていない点なので注意してください。

3. APIキーの管理

サンプルでは直書きしていますが、本番では ScriptableObject や環境変数で管理してください。

本番運用時の注意点

  • プライバシー:カメラ映像を外部APIに送信するため、ユーザーへの説明と同意取得が必須です
  • パーミッション:AndroidManifest に CAMERAINTERNET を追加してください
  • エラー処理:サンプルでは省略しているため、本番では適切なエラーハンドリングを追加してください

まとめ

この記事では、XREAL Eye + Gemini API で「見ているものをAIに解析させる」実装を紹介しました。

実装時の最大のポイントは「プレビューとキャプチャで別のAPIを使用する」という点です。ここさえ押さえておけば、スムーズに実装できると思います。

ARグラス × AI の組み合わせは、これから様々な可能性が広がっていく分野だと思います。ぜひこの記事を参考に、実装を試してみていただければと思います。

完全なスクリプト

以下に、今回紹介した実装の完全なスクリプトを掲載します。サンプルのため、エラー処理は最低限となっています。

using System;
using System.Linq;
using System.Text;
using Unity.XR.XREAL;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
using TMPro;
using Cysharp.Threading.Tasks;

/// <summary>
/// XREAL Eye のカメラ映像を Google Gemini API で解析するクラス(UniTask + XREALPhotoCapture 版)
/// 
/// 【注意】本コードは動作確認・学習用のサンプルです。
/// 本番運用では以下の強化を推奨します:
/// - 例外処理の追加(ネットワークエラー、タイムアウトなど)
/// - 画像サイズの制限チェック(Base64文字列長の制限)
/// - リトライロジックの実装
/// - JSONシリアライザの使用(Newtonsoft.Json など)
/// </summary>
public class XREALGemini : MonoBehaviour
{
    [Header("UI References")]
    [SerializeField] private RawImage previewImage;
    [SerializeField] private RawImage capturedImage;
    [SerializeField] private TextMeshProUGUI resultText;
    
    [Header("Gemini Settings")]
    [SerializeField] private string apiKey = "YOUR_GEMINI_API_KEY_HERE";
    [SerializeField] private string analysisPrompt = "この画像に何が映っていますか?日本語で答えてください。";
    
    // Gemini API 設定(定数化して将来の変更に対応しやすく)
    private const string GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models/";
    private const string GEMINI_MODEL = "gemini-2.5-flash";
    private const string GEMINI_ACTION = ":generateContent";
    
    // Gemini API URL(APIキーをクエリパラメータで渡す)
    private string ApiUrl => $"{GEMINI_BASE_URL}{GEMINI_MODEL}{GEMINI_ACTION}?key={apiKey}";
    
    private XREALRGBCameraTexture m_RGBCameraTexture;
    private XREALPhotoCapture photoCapture;
    private Resolution cameraResolution;
    
    // キャプチャした画像を保持
    private string capturedBase64Image;
    private Texture2D capturedTexture;
    
    void Start()
    {
        InitializeXREALCamera();
    }
    
    void Update()
    {
        // カメラプレビューを更新
        UpdateCameraPreview();
    }
    
    void OnDestroy()
    {
        // XREAL Eye のカメラ停止
        if (m_RGBCameraTexture != null && m_RGBCameraTexture.IsCapturing)
        {
            m_RGBCameraTexture.StopCapture();
            Debug.Log("XREAL Eye Camera stopped");
        }
        
        // PhotoCapture のリソースを解放
        if (photoCapture != null)
        {
            photoCapture.Dispose();
            photoCapture = null;
            Debug.Log("PhotoCapture disposed");
        }
    }
    
    /// <summary>
    /// XREAL Eye のカメラを初期化
    /// </summary>
    void InitializeXREALCamera()
    {
        // XREAL Eye の RGB カメラテクスチャを作成
        m_RGBCameraTexture = XREALRGBCameraTexture.CreateSingleton();
        
        // カメラキャプチャを開始
        m_RGBCameraTexture.StartCapture();
        
        Debug.Log("XREAL Eye Camera initialized");
    }
    
    /// <summary>
    /// カメラプレビューを更新(YUV フォーマット)
    /// </summary>
    void UpdateCameraPreview()
    {
        // YUV フォーマットのテクスチャを取得
        var yuvTextures = m_RGBCameraTexture.GetYUVFormatTextures();
        
        if (yuvTextures[0] != null)
        {
            // プレビュー画像に YUV テクスチャを設定
            previewImage.texture = yuvTextures[0];
            previewImage.material.SetTexture("_UTex", yuvTextures[1]);
            previewImage.material.SetTexture("_VTex", yuvTextures[2]);
        }
    }
    
    /// <summary>
    /// XREALPhotoCapture を初期化
    /// </summary>
    async UniTask InitializePhotoCaptureAsync()
    {
        var tcs = new UniTaskCompletionSource();
        
        XREALPhotoCapture.CreateAsync(false, (captureObject) =>
        {
            photoCapture = captureObject;
            
            // サポートされている最高解像度を取得
            cameraResolution = XREALPhotoCapture.SupportedResolutions
                .OrderByDescending(res => res.width * res.height)
                .First();
            
            Debug.Log($"PhotoCapture initialized: {cameraResolution.width}x{cameraResolution.height}");
            tcs.TrySetResult();
        });
        
        await tcs.Task;
    }
    
    /// <summary>
    /// カメラ映像をキャプチャ(公開メソッド:Button などから呼び出し可能)
    /// </summary>
    public async void Capture()
    {
        await CaptureAsync();
    }
    
    /// <summary>
    /// カメラ映像をキャプチャして Texture2D と Base64 形式で保存
    /// </summary>
    public async UniTask CaptureAsync()
    {
        // PhotoCapture が未初期化なら初期化
        if (photoCapture == null)
        {
            await InitializePhotoCaptureAsync();
        }
        
        // PhotoMode を開始
        await StartPhotoModeAsync();
        
        // 写真をキャプチャ
        await TakePhotoAsync();
        
        // PhotoMode を停止
        await StopPhotoModeAsync();
    }
    
    /// <summary>
    /// PhotoMode を開始
    /// </summary>
    async UniTask StartPhotoModeAsync()
    {
        var tcs = new UniTaskCompletionSource();
        
        var cameraParams = new CameraParameters
        {
            cameraType = Unity.XR.XREAL.CameraType.RGB,
            hologramOpacity = 0.0f,
            frameRate = 30,
            cameraResolutionWidth = cameraResolution.width,
            cameraResolutionHeight = cameraResolution.height,
            pixelFormat = CapturePixelFormat.PNG,
            blendMode = BlendMode.CameraOnly,
            captureSide = CaptureSide.Single
        };
        
        photoCapture.StartPhotoModeAsync(cameraParams, (result) =>
        {
            Debug.Log("PhotoMode started");
            tcs.TrySetResult();
        }, true);
        
        await tcs.Task;
    }
    
    /// <summary>
    /// 写真をキャプチャ
    /// </summary>
    async UniTask TakePhotoAsync()
    {
        var tcs = new UniTaskCompletionSource();
        
        photoCapture.TakePhotoAsync((result, photoCaptureFrame) =>
        {
            // PNG形式のバイト配列を直接取得
            byte[] imageBytes = photoCaptureFrame.TextureData;
            
            // Base64 形式に変換
            capturedBase64Image = Convert.ToBase64String(imageBytes);
            
            // Texture2D に変換して表示
            var texture = new Texture2D(cameraResolution.width, cameraResolution.height);
            photoCaptureFrame.UploadImageDataToTexture(texture);
            
            capturedTexture = texture;
            capturedImage.texture = texture;
            
            Debug.Log($"Photo captured: {imageBytes.Length / 1024}KB");
            tcs.TrySetResult();
        });
        
        await tcs.Task;
    }
    
    /// <summary>
    /// PhotoMode を停止
    /// </summary>
    async UniTask StopPhotoModeAsync()
    {
        var tcs = new UniTaskCompletionSource();
        
        photoCapture.StopPhotoModeAsync((result) =>
        {
            Debug.Log("PhotoMode stopped");
            tcs.TrySetResult();
        });
        
        await tcs.Task;
    }
    
    /// <summary>
    /// キャプチャした画像を AI で解析(公開メソッド:Button などから呼び出し可能)
    /// </summary>
    public async void Analyze()
    {
        if (string.IsNullOrEmpty(capturedBase64Image))
        {
            Debug.LogError("No captured image to analyze. Please capture an image first.");
            return;
        }
        
        await AnalyzeAsync();
    }
    
    /// <summary>
    /// キャプチャした画像を Gemini API で解析
    /// </summary>
    public async UniTask AnalyzeAsync()
    {
        await SendToGeminiAsync(capturedBase64Image);
    }
    
    /// <summary>
    /// カメラ映像をキャプチャして AI で解析(公開メソッド:Button などから呼び出し可能)
    /// </summary>
    public async void CaptureAndAnalyze()
    {
        await CaptureAndAnalyzeAsync();
    }
    
    /// <summary>
    /// カメラ映像をキャプチャして、完了後に Gemini API で解析
    /// </summary>
    public async UniTask CaptureAndAnalyzeAsync()
    {
        // まずキャプチャを実行
        await CaptureAsync();
        
        // キャプチャが成功したら解析を実行
        if (!string.IsNullOrEmpty(capturedBase64Image))
        {
            await SendToGeminiAsync(capturedBase64Image);
        }
        else
        {
            Debug.LogError("Failed to capture image");
        }
    }
    
    /// <summary>
    /// Gemini API に画像を送信して解析
    /// </summary>
    async UniTask SendToGeminiAsync(string base64Image)
    {
        // 注意: 手書きJSONは簡便だが、プロンプト中に " が含まれると壊れる可能性があります
        // 実運用では Newtonsoft.Json などのシリアライザ使用を推奨
        
        // Gemini API 用の JSON データを構築
        string jsonData = $@"{{
            ""contents"": [
                {{
                    ""parts"": [
                        {{
                            ""text"": ""{EscapeJsonString(analysisPrompt)}""
                        }},
                        {{
                            ""inline_data"": {{
                                ""mime_type"": ""image/png"",
                                ""data"": ""{base64Image}""
                            }}
                        }}
                    ]
                }}
            ]
        }}";
        
        Debug.Log("Sending to Gemini API...");
        
        // UnityWebRequest を作成
        using (UnityWebRequest request = new UnityWebRequest(ApiUrl, "POST"))
        {
            byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonData);
            request.uploadHandler = new UploadHandlerRaw(bodyRaw);
            request.downloadHandler = new DownloadHandlerBuffer();
            request.SetRequestHeader("Content-Type", "application/json");
            // Gemini は Authorization ヘッダーではなく、クエリパラメータで API キーを渡す
            
            // リクエスト送信(UniTask で await)
            await request.SendWebRequest();
            
            if (request.result == UnityWebRequest.Result.Success)
            {
                string response = request.downloadHandler.text;
                ParseGeminiResponse(response);
            }
            else
            {
                Debug.LogError($"Error: {request.error}");
                Debug.LogError($"Response: {request.downloadHandler.text}");
            }
        }
    }
    
    /// <summary>
    /// JSON文字列内の特殊文字をエスケープ
    /// </summary>
    string EscapeJsonString(string str)
    {
        return str
            .Replace("\\", "\\\\")
            .Replace("\"", "\\\"")
            .Replace("\n", "\\n")
            .Replace("\r", "\\r")
            .Replace("\t", "\\t");
    }
    
    /// <summary>
    /// Gemini からのレスポンスを解析
    /// </summary>
    void ParseGeminiResponse(string jsonResponse)
    {
        try
        {
            var response = JsonUtility.FromJson<GeminiResponse>(jsonResponse);
            
            if (response.candidates != null && response.candidates.Length > 0)
            {
                var candidate = response.candidates[0];
                if (candidate.content != null && candidate.content.parts != null && candidate.content.parts.Length > 0)
                {
                    string result = candidate.content.parts[0].text;
                    
                    if (resultText != null)
                    {
                        resultText.text = result;
                    }
                    
                    Debug.Log($"Gemini Response: {result}");
                }
            }
        }
        catch (Exception e)
        {
            Debug.LogError($"Failed to parse response: {e.Message}");
            Debug.LogError($"Response JSON: {jsonResponse}");
            if (resultText != null)
            {
                resultText.text = $"Failed to parse response: {e.Message}";
            }
        }
    }
}

// Gemini レスポンス用のデータクラス
[Serializable]
public class GeminiResponse
{
    public GeminiCandidate[] candidates;
}

[Serializable]
public class GeminiCandidate
{
    public GeminiContent content;
}

[Serializable]
public class GeminiContent
{
    public GeminiPart[] parts;
}

[Serializable]
public class GeminiPart
{
    public string text;
}

使い方

  1. スクリプトをGameObjectにアタッチ
  2. InspectorでUI要素とAPIキーを設定
  3. ボタンに CaptureAndAnalyze() を設定

AndroidManifestに CAMERAINTERNET パーミッションを忘れずに。

参考

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?