UnityからAIを使用する際、OpenAI APIを使うケースが現状最も多く見られます。
が、今回はあえてClaudeを使って画像認識を実装した話を書きます!
なぜなら、OpenAI API で「画像の人物の表情を分析して」と聞くと、頑なに拒否されたためです。。
しかしClaudeでは出来ました!
似たようなケースは他にもありそうなので、複数のAIを組み込んでおくと幅が広がりそうですね。
まずはコードを貼っちゃいます
リクエストおよびレスポンスデータを定義するためのクラス群
using System.Collections.Generic; using System; using Newtonsoft.Json; [Serializable] public class ClaudeMessage { public string role; public List<Content> content; } [Serializable] public class Content { public string type; [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string text; // Textを送る際に使用 [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public Source source; // Imageを送る際に使用 } [Serializable] public class Source { public string type = "base64"; public string media_type; public string data; } [Serializable] public class ClaudeRequest { public string model; public string system; public List<ClaudeMessage> messages; public int max_tokens; } [Serializable] public class ClaudeResponse { public string id; public ResponseContent[] content; public Usage usage; } [Serializable] public class ResponseContent { public string text; } [Serializable] public class Usage { public int input_tokens; public int output_tokens; }
APIリクエストを管理し、テキストと画像データをAPIに送信するクラス
using System; using Cysharp.Threading.Tasks; using UnityEngine.Networking; using UnityEngine; using System.Collections.Generic; using Newtonsoft.Json; using System.Threading; public class ClaudeApiConnector : MonoBehaviour { [SerializeField] private string _apiKey; [SerializeField] private int _maxTokens = 1024; private readonly string _apiUrl = "https://api.anthropic.com/v1/messages"; private readonly string _modelName = "claude-3-5-sonnet-20240620"; private List<ClaudeMessage> _messageHistory = new List<ClaudeMessage>(); /// <summary> /// Claude APIを呼び出す。TextとTexture2Dのどちらか、または両方を引数で渡して使用。 /// </summary> /// <returns></returns> public async UniTask<string> CallApi(string prompt, string text = null, Texture2D texture2D = null, CancellationToken token = default) { if (text == null && texture2D == null) { Debug.LogError("Text and Texture2D are null.", gameObject); return "unknown"; } ClaudeMessage[] messages = CreateMessages(text, texture2D); _messageHistory.AddRange(messages); // 入力の履歴を保持 return await ExecuteApiRequest(prompt, token); } private ClaudeMessage[] CreateMessages(string text = null, Texture2D texture2D = null) { List<Content> contents = new List<Content>(); if (texture2D != null) { Content imageContent = new Content { type = "image", // type を設定 source = new Source { type = "base64", media_type = "image/png", data = Convert.ToBase64String(texture2D.EncodeToPNG()) } }; contents.Add(imageContent); Debug.Log("Added images to messages to Claude API.", gameObject); } if (!string.IsNullOrEmpty(text)) { Content textContent = new Content { type = "text", text = text }; contents.Add(textContent); Debug.Log("Added Text to messages to Claude API : " + text, gameObject); } ClaudeMessage[] messages = new ClaudeMessage[] { new ClaudeMessage { role = "user", content = contents } }; return messages; } private async UniTask<string> ExecuteApiRequest(string prompt, CancellationToken token = default) { Debug.Log("Execute a request to Claude API.", gameObject); ClaudeRequest chatBody = new ClaudeRequest { system = prompt, model = _modelName, messages = _messageHistory, max_tokens = _maxTokens }; string body = JsonConvert.SerializeObject(chatBody); byte[] jsonToSend = new System.Text.UTF8Encoding().GetBytes(body); using UnityWebRequest request = new UnityWebRequest(_apiUrl, UnityWebRequest.kHttpVerbPOST); request.uploadHandler = new UploadHandlerRaw(jsonToSend); request.downloadHandler = new DownloadHandlerBuffer(); request.SetRequestHeader("x-api-key", _apiKey); request.SetRequestHeader("anthropic-version", "2023-06-01"); request.SetRequestHeader("Content-Type", "application/json"); await request.SendWebRequest().ToUniTask(cancellationToken: token); if (request.result != UnityWebRequest.Result.Success) { Debug.LogError("API request failed : " + request.error); return null; } string response = request.downloadHandler.text; ClaudeResponse responseJson = JsonConvert.DeserializeObject<ClaudeResponse>(response); string output = responseJson.content[0].text; Debug.Log("Claude API response : " + output, gameObject); return output; } #if UNITY_EDITOR [UnityEditor.CustomEditor(typeof(ClaudeApiConnector))] private class ClaudeApiConnectorTest : UnityEditor.Editor { private const string PromptKey = "ClaudeApiPrompt"; private const string TextKey = "ClaudeApiText"; [SerializeField] private string _prompt; [SerializeField] private string _text; [SerializeField] private Texture2D _texture; public override void OnInspectorGUI() { base.OnInspectorGUI(); UnityEditor.EditorGUILayout.Space(10); UnityEditor.EditorGUILayout.LabelField("--- Debug ---"); _prompt = UnityEditor.EditorPrefs.GetString(PromptKey, _prompt); _text = UnityEditor.EditorPrefs.GetString(TextKey, _text); _prompt = UnityEditor.EditorGUILayout.TextField("Prompt", _prompt); _text = UnityEditor.EditorGUILayout.TextField("Text", _text); _texture = (Texture2D)UnityEditor.EditorGUILayout.ObjectField("Texture2D", _texture, typeof(Texture2D), allowSceneObjects: false); UnityEditor.EditorPrefs.SetString(PromptKey, _prompt); UnityEditor.EditorPrefs.SetString(TextKey, _text); if (target is not ClaudeApiConnector system) return; if (GUILayout.Button("Call Claude API")) { system.CallApi(_prompt, text: _text, texture2D: _texture).Forget(); _text = null; _texture = null; } } } #endif }
使用方法
UniTask と Newtonsoft.Json を事前にUnityプロジェクトにインポートしておきます。
シーン上に空のGameObjectを作成し、そこにClaudeApiConnectorをアタッチし、そのインスペクターを設定していきます。
・API Key と プロンプトを入力。
・「Text」または「Texture2D」または「Text・Texture2D の両方」を設定。
※ 画像はpng形式で、Import Settings の Read/Write にチェックを入れた状態にしておく。
・シーン再生中に、インスペクターのボタン「Call Claude API」を押すと実行できます。
実装のメモ
・ClaudeApiConnector の CreateMessages にて、media_type = “image/png”としていますが、これを “image/jpeg” にすれば jpeg の画像を送れます。
・画像は Base64 にエンコードして送っています。
[Serializable] public class Content { public string type; [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string text; // Textを送る際に使用 [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public Source source; // Imageを送る際に使用 }
・↑ここの定義で[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
とすることで、Claudeへ送るコンテンツの Text か Image を省略した場合はJSONシリアライズ時に省略されるようにしました。これにより、Input内容が柔軟に変更できる仕様になっています。
おわりに
ゲーム画面のスクショを送ってみたり、Webカメラのスクショを送ってみたり、色々実装できそうですね!
今度は Germini Multimodal Live API を調べているので、形になったらまた書こうと思います。