├── .gitattributes ├── Editor.meta ├── Editor ├── Scripts.meta └── Scripts │ ├── GemmaManagerEditor.cs │ ├── GemmaManagerEditor.cs.meta │ ├── GemmaModelSetup.cs │ └── GemmaModelSetup.cs.meta ├── LICENSE ├── Plugins.meta ├── Plugins ├── x64.meta └── x64 │ ├── gemma-debug.dll │ ├── gemma-debug.dll.meta │ ├── gemma-debug.pdb │ ├── gemma-debug.pdb.meta │ ├── gemma.dll │ └── gemma.dll.meta ├── README.md ├── Runtime.meta ├── Runtime ├── GemmaCpp.asmdef ├── GemmaCpp.asmdef.meta ├── Scripts.meta └── Scripts │ ├── GemmaInterop.cs │ ├── GemmaInterop.cs.meta │ ├── GemmaManager.cs │ ├── GemmaManager.cs.meta │ ├── GemmaManagerSettings.cs │ ├── GemmaManagerSettings.cs.meta │ ├── GemmaModelType.cs │ ├── GemmaModelType.cs.meta │ ├── GemmaModelUtils.cs │ └── GemmaModelUtils.cs.meta ├── docs └── CONTRIBUTING.md ├── package.json └── package.json.meta /.gitattributes: -------------------------------------------------------------------------------- 1 | *.dll filter=lfs diff=lfs merge=lfs -text 2 | *.pdb filter=lfs diff=lfs merge=lfs -text 3 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 38810ec31c986cd4cacb922d0423bf95 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Scripts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f8bf10c595cf6414ca21042e4d8a91b5 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Scripts/GemmaManagerEditor.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | using UnityEditor; 17 | using UnityEngine; 18 | 19 | namespace GemmaCpp.Editor 20 | { 21 | [CustomEditor(typeof(GemmaManager))] 22 | public class GemmaManagerEditor : UnityEditor.Editor 23 | { 24 | public override void OnInspectorGUI() 25 | { 26 | var manager = (GemmaManager)target; 27 | 28 | EditorGUILayout.Space(); 29 | if (GUILayout.Button("Create New Settings Asset")) 30 | { 31 | var settings = ScriptableObject.CreateInstance(); 32 | var path = EditorUtility.SaveFilePanelInProject( 33 | "Save Gemma Settings", 34 | "GemmaSettings", 35 | "asset", 36 | "Please enter a file name to save the Gemma settings to" 37 | ); 38 | 39 | if (!string.IsNullOrEmpty(path)) 40 | { 41 | AssetDatabase.CreateAsset(settings, path); 42 | AssetDatabase.SaveAssets(); 43 | } 44 | } 45 | 46 | DrawDefaultInspector(); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /Editor/Scripts/GemmaManagerEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 82ae0dbfa4ed38b488a724a95377fb61 -------------------------------------------------------------------------------- /Editor/Scripts/GemmaModelSetup.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | using UnityEditor; 17 | using UnityEngine; 18 | using System.IO; 19 | using System.Linq; 20 | 21 | namespace GemmaCpp.Editor 22 | { 23 | public static class GemmaModelSetup 24 | { 25 | [MenuItem("Gemma/Validate Model Setup")] 26 | public static void ValidateModelSetup() 27 | { 28 | // Ensure StreamingAssets exists 29 | var streamingAssetsPath = Application.streamingAssetsPath; 30 | if (!Directory.Exists(streamingAssetsPath)) 31 | { 32 | Directory.CreateDirectory(streamingAssetsPath); 33 | Debug.Log("Created StreamingAssets directory"); 34 | } 35 | 36 | // Check for model folders 37 | var modelFolders = Directory.GetDirectories(streamingAssetsPath) 38 | .Where(d => Path.GetFileName(d).StartsWith("gemma-")); 39 | 40 | if (!modelFolders.Any()) 41 | { 42 | Debug.LogWarning( 43 | "No Gemma model folders found in StreamingAssets.\n" + 44 | "Please download a Gemma model and place it in:\n" + 45 | $"{streamingAssetsPath}/YOUR_MODEL_NAME_HERE" 46 | ); 47 | return; 48 | } 49 | 50 | foreach (var modelPath in modelFolders) 51 | { 52 | ValidateModelFolder(modelPath); 53 | } 54 | } 55 | 56 | // @fixme unfortunately models on model hubs don't always match the files below so... what to do? 57 | private static void ValidateModelFolder(string modelPath) 58 | { 59 | var modelName = Path.GetFileName(modelPath); 60 | Debug.Log($"Checking model: {modelName}"); 61 | 62 | // Note: We no longer enforce specific filenames 63 | // Instead, we check that at least one tokenizer and weight file exists 64 | bool hasTokenizer = Directory.GetFiles(modelPath, "*.spm").Length > 0; 65 | bool hasWeights = Directory.GetFiles(modelPath, "*.sbs").Length > 0; 66 | 67 | if (!hasTokenizer) 68 | { 69 | Debug.LogError($"No tokenizer file found in {modelName}"); 70 | } 71 | if (!hasWeights) 72 | { 73 | Debug.LogError($"No weights file found in {modelName}"); 74 | } 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /Editor/Scripts/GemmaModelSetup.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a1949e78e381c8e49953a25cb0a1f8eb -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /Plugins.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cd0a8aa2009d23f418138bb23aafcad7 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Plugins/x64.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1ecf7c68b95acc64c90a8c2c2a4c8676 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Plugins/x64/gemma-debug.dll: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:72910c9280c231c1d7a0392fe872ca532c10a7c04906fd4ab99cf47cacc3f82e 3 | size 46015488 4 | -------------------------------------------------------------------------------- /Plugins/x64/gemma-debug.dll.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7f86aeece489ddf4c95d106e99d69f40 3 | PluginImporter: 4 | externalObjects: {} 5 | serializedVersion: 3 6 | iconMap: {} 7 | executionOrder: {} 8 | defineConstraints: [] 9 | isPreloaded: 0 10 | isOverridable: 1 11 | isExplicitlyReferenced: 0 12 | validateReferences: 1 13 | platformData: 14 | Android: 15 | enabled: 1 16 | settings: 17 | AndroidLibraryDependee: UnityLibrary 18 | AndroidSharedLibraryType: Executable 19 | CPU: ARMv7 20 | Any: 21 | enabled: 1 22 | settings: 23 | Exclude Android: 0 24 | Exclude Editor: 0 25 | Exclude Linux64: 0 26 | Exclude OSXUniversal: 0 27 | Exclude WebGL: 0 28 | Exclude Win: 0 29 | Exclude Win64: 0 30 | Exclude iOS: 0 31 | Editor: 32 | enabled: 1 33 | settings: 34 | CPU: x86_64 35 | DefaultValueInitialized: true 36 | OS: AnyOS 37 | Linux64: 38 | enabled: 1 39 | settings: 40 | CPU: x86_64 41 | OSXUniversal: 42 | enabled: 1 43 | settings: 44 | CPU: x86_64 45 | WebGL: 46 | enabled: 1 47 | settings: {} 48 | Win: 49 | enabled: 1 50 | settings: 51 | CPU: None 52 | Win64: 53 | enabled: 1 54 | settings: 55 | CPU: x86_64 56 | iOS: 57 | enabled: 1 58 | settings: 59 | AddToEmbeddedBinaries: false 60 | CPU: AnyCPU 61 | CompileFlags: 62 | FrameworkDependencies: 63 | userData: 64 | assetBundleName: 65 | assetBundleVariant: 66 | -------------------------------------------------------------------------------- /Plugins/x64/gemma-debug.pdb: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4dbbe13a587445bb50e58c25a0bd5ba8e98496ee33a4bc12c988f699a1958c33 3 | size 84443136 4 | -------------------------------------------------------------------------------- /Plugins/x64/gemma-debug.pdb.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2fab3ae2cb1c3aa4fa8caeeca27bb6a1 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Plugins/x64/gemma.dll: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:6525c0793f8d1c92027532419d299d875a5ecfe204dc5022e1c5465cd8d8aa5a 3 | size 14075392 4 | -------------------------------------------------------------------------------- /Plugins/x64/gemma.dll.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4184def121bd8a547aedf63af6c2820a 3 | PluginImporter: 4 | externalObjects: {} 5 | serializedVersion: 3 6 | iconMap: {} 7 | executionOrder: {} 8 | defineConstraints: [] 9 | isPreloaded: 1 10 | isOverridable: 1 11 | isExplicitlyReferenced: 0 12 | validateReferences: 1 13 | platformData: 14 | Android: 15 | enabled: 1 16 | settings: 17 | AndroidLibraryDependee: UnityLibrary 18 | AndroidSharedLibraryType: Executable 19 | CPU: ARMv7 20 | Any: 21 | enabled: 1 22 | settings: 23 | Exclude Android: 0 24 | Exclude Editor: 0 25 | Exclude Linux64: 0 26 | Exclude OSXUniversal: 0 27 | Exclude WebGL: 0 28 | Exclude Win: 0 29 | Exclude Win64: 0 30 | Exclude iOS: 0 31 | Editor: 32 | enabled: 1 33 | settings: 34 | CPU: x86_64 35 | DefaultValueInitialized: true 36 | OS: AnyOS 37 | Linux64: 38 | enabled: 1 39 | settings: 40 | CPU: x86_64 41 | OSXUniversal: 42 | enabled: 1 43 | settings: 44 | CPU: x86_64 45 | WebGL: 46 | enabled: 1 47 | settings: {} 48 | Win: 49 | enabled: 1 50 | settings: 51 | CPU: None 52 | Win64: 53 | enabled: 1 54 | settings: 55 | CPU: x86_64 56 | iOS: 57 | enabled: 1 58 | settings: 59 | AddToEmbeddedBinaries: false 60 | CPU: AnyCPU 61 | CompileFlags: 62 | FrameworkDependencies: 63 | userData: 64 | assetBundleName: 65 | assetBundleVariant: 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gemma.cpp for Unity 2 | 3 | This plugin provides C# bindings and a high-level Unity integration for the [Gemma.cpp](https://github.com/google/gemma.cpp) library, allowing you to run Google's Gemma models directly within your Unity projects. 4 | 5 | This is not an officially supported Google product. 6 | 7 | ## Dependencies 8 | 9 | This plugin depends upon the following Unity Package Manager (UPM) package: 10 | 11 | - **UniTask** >= 2.5.10 ([link](https://github.com/Cysharp/UniTask.git))\ 12 | This is used by `GemmaManager` to handle asynchronous operations and ensure callbacks (like `onTokenReceived`) can safely interact with the Unity API from background threads. 13 | 14 | Note that if this package is added to a Unity project using UPM, Unity should automatically fetch UniTask based on the dependency information. 15 | 16 | ## Setup & Usage 17 | 18 | 1. **Add Package:** Add this repository's URL to the Unity Package Manager in your Unity project. 19 | 2. **Create Settings:** Create a `GemmaManagerSettings` ScriptableObject asset (Assets -> Create -> GemmaCpp -> Gemma Manager Settings). Configure it with the paths to your downloaded Gemma model weights, tokenizer file, and desired model parameters (Model Type, Weight Format, Temperature). 20 | 3. **Add Component:** Add the `GemmaManager` component to a GameObject in your scene. 21 | 4. **Assign Settings:** Drag your created `GemmaManagerSettings` asset onto the `Settings` field of the `GemmaManager` component in the Inspector. 22 | 5. **Use in Script:** Get a reference to the `GemmaManager` component in your scripts and call its methods (see API Reference below). 23 | 24 | ## Recommended Usage Patterns 25 | 26 | ### Setting up a `Gemma` instance 27 | A `Gemma` instance is created by passing in the information that would ordinarily be passed to **Gemma.cpp** on the command line. 28 | 29 | ```csharp 30 | Gemma gemma = new Gemma( 31 | "tokenizer.spm", // tokenizer path 32 | "gemma3-4b", // model flag 33 | "4b-it-sfp.sbs", // weights file path 34 | "sfp", // weights format, usually "sfp" 35 | 384 // maximum number of tokens generated per turn 36 | ); 37 | gemma.SetMultiturn(true); 38 | gemma.SetTemperature(1.0f); 39 | gemma.EnableLogging(true); 40 | ``` 41 | 42 | ### Managing Multiple NPC Conversations 43 | 44 | **Note:** This pattern relies on multiturn conversation history. Ensure that `gemma.SetMultiturn(true)` has been called. 45 | 46 | Managing conversations with multiple Non-Player Characters (NPCs) can be achieved by using Gemma.cpp's **conversation context** feature. 47 | 48 | In brief, Gemma.cpp keeps track of a number of `ConversationData` structs that each contain a KV cache and an absolute position (i.e. the point in the cache up to which tokens have been generated). 49 | 50 | ```cpp 51 | // gemma.cpp - gemma/bindings/context.h 52 | struct ConversationData { 53 | std::unique_ptr kv_cache; 54 | size_t abs_pos = 0; 55 | 56 | ConversationData(const ModelConfig& model_config, size_t prefill_tbatch_size); 57 | }; 58 | ``` 59 | 60 | In this way, each `ConversationData` represents a conversation that the user is having with Gemma, with strict boundaries - what happens in conversation A, does not affect conversation B. By using the various conversation-related methods in Gemma.cpp, it is possible to switch between conversations appropriately, allowing one Gemma model instance to power multiple NPCs. 61 | 62 | Each conversation is stored in a key-value store, where the key is a string, and the value is a `ConversationData` struct. The base conversation is `"default"`. 63 | 64 | #### Example usage 65 | 66 | The user wants to talk to an NPC, "Elara Smith", with the unique string identifier `npc_elara`. 67 | 68 | ```csharp 69 | // create conversation if it doesn't yet exist 70 | bool conversation_existed = gemma.HasConversation("npc_elara"); 71 | if (!conversation_existed) { 72 | gemma.CreateConversation("npc_elara"); 73 | } 74 | 75 | // switch to the conversation 76 | gemma.SwitchConversation("npc_elara"); 77 | 78 | // make sure that multiturn is on 79 | gemma.SetMultiturn(true); 80 | 81 | // prewarm the conversation by sending the NPC's biography as a turn 82 | if (!conversation_existed) { 83 | var elara_bio = "Your name is Elara Smith, a wise old elf living in the Whispering Woods. You are knowledgeable about ancient runes but wary of strangers."; 84 | gemma.Generate(elara_bio, 256); // we won't display the inference result, so keep maxLength short 85 | } 86 | 87 | // ready to talk to the NPC! 88 | ``` 89 | 90 | As Gemma is set up to operate in multiturn mode, when talking to the model the user should only send `the current user input` as the prompt (e.g., `"Can you tell me about the Rune of Binding?"`). It is not necessary to prepend the conversation history; the model automatically uses the history stored within the active conversation context's KV cache. 91 | 92 | It may be useful to perform prewarming well in advance of interacting with an NPC. We provide the `Prewarm()` method that takes a `Dictionary` object as a parameter for this purpose. 93 | 94 | ### `GemmaManager`, a Unity `MonoBehaviour` that wraps `Gemma` 95 | 96 | `GemmaManager` is a convenience `MonoBehaviour` class that provides a high-level interface for interacting with Gemma within Unity. 97 | 98 | After the model has been loaded in `Start()`, use `GeneateResponseAsync()` to perform inference. This method takes a callback as one of its parameters, which uses **UniTask** for thread-safe use of Unity engine APIs (e.g. for updating text boxes and the like). 99 | 100 | ## API Reference - `GemmaManager` 101 | 102 | The `GemmaManager` MonoBehaviour provides a high-level interface for interacting with the Gemma model within Unity. 103 | 104 | **Configuration (Inspector)** 105 | 106 | * **Settings:** (Required) Assign a `GemmaManagerSettings` ScriptableObject containing model paths and parameters. 107 | * **Verbose Logging:** Enable detailed logging from both the C# wrapper and the underlying native library. 108 | 109 | **Methods** 110 | * **`async UniTask Prewarm(Dictionary conversations, PrewarmStatusCallback statusCallback = null)`** 111 | * Asynchronously prewarms specified conversation contexts before they are actively used. 112 | * `conversations`: A dictionary where keys represent the unique names of the conversations to prewarm, and values represent the initial prompt to send to each respective conversation. 113 | * `statusCallback`: (Optional) A delegate of type `GemmaManager.PrewarmStatusCallback(string conversationName, PrewarmState state)` that can be provided to receive status updates during the prewarming process. The callback receives the name of the conversation being processed and its subsequent `PrewarmState` (e.g., Pending, InProgress, Done, Skipped, Failed). 114 | * For each entry, it ensures the conversation exists (creates if not), switches to it, and generates an initial response using the provided prompt. This helps reduce latency on the first interaction. 115 | 116 | * **`async UniTask GenerateResponseAsync(string prompt, Gemma.TokenCallback onTokenReceived = null)`** 117 | * Generates a text response based on the input `prompt`. 118 | * Runs the generation on a background thread. 119 | * `onTokenReceived`: Optional callback delegate (`bool TokenCallback(string token)`) that receives generated tokens one by one. Callbacks are executed on the main thread via UniTask, allowing safe interaction with Unity APIs (e.g., updating UI elements). Return `true` from the callback to continue generation, `false` to stop early. 120 | * Returns the complete generated response string. 121 | 122 | * **`async UniTask GenerateMultimodalResponseAsync(string prompt, RenderTexture renderTexture, Gemma.TokenCallback onTokenReceived = null)`** 123 | * Generates a text response based on the input `prompt` and an image provided as a `RenderTexture`. 124 | * The `RenderTexture` is automatically converted to the required format on the main thread. 125 | * Runs the generation on a background thread. 126 | * `onTokenReceived`: Optional callback, same behavior as in `GenerateResponseAsync`. This is the appropriate place to update UI based on incoming tokens. 127 | * Returns the complete generated response string. 128 | 129 | * **`void ResetCurrentConversation()`** 130 | * Resets the history of the current conversation context in the Gemma model. 131 | 132 | * **`bool CreateConversation(string conversationName)`** 133 | * Creates a new, named conversation context. Returns `true` on success. 134 | 135 | * **`bool SwitchConversation(string conversationName)`** 136 | * Switches the active context to a previously created conversation. Returns `true` if the conversation exists and was switched to. 137 | 138 | * **`bool DeleteConversation(string conversationName)`** 139 | * Deletes a named conversation context. Returns `true` on success. 140 | 141 | * **`bool HasConversation(string conversationName)`** 142 | * Checks if a conversation with the given name exists. Returns `true` if it exists. 143 | 144 | * **`string GetCurrentConversation()`** 145 | * Gets the name of the currently active conversation context. 146 | * Returns the name as a string. Returns null or empty if no conversation is active or an error occurs. 147 | 148 | ### `GemmaManagerSettings` 149 | 150 | This ScriptableObject holds the configuration for the Gemma model used by `GemmaManager`. 151 | 152 | * **Tokenizer Path:** Filesystem path to the Gemma tokenizer file (`tokenizer.spm`). 153 | * **Weights Path:** Filesystem path to the Gemma model weights file (e.g., `.sbs` file). 154 | * **Model Flag:** The model type string (e.g., "gemma3-4b"). 155 | * **Weight Format:** The format of the weights file (e.g., `sfp`). 156 | * **Temperature:** Sampling temperature for generation (e.g., 0.9). 157 | 158 | --- 159 | 160 | ## Low-Level API - `GemmaInterop.cs` (`GemmaCpp.Gemma`) 161 | 162 | This class provides direct C# bindings to the native `gemma.dll` functions. It is used internally by `GemmaManager` but can be used directly for more advanced scenarios. 163 | 164 | **Constructor** 165 | 166 | * **`Gemma(string tokenizerPath, string modelType, string weightsPath, string weightType, int maxLength = 8192)`** 167 | * Creates and initializes a native Gemma context. Throws `GemmaException` on failure. 168 | 169 | **Methods** 170 | 171 | * **`string Generate(string prompt, int maxLength = 4096)`** 172 | * Generates text based on the input `prompt`, to a maximum length of `maxLength`. 173 | * **`string Generate(string prompt, TokenCallback callback, int maxLength = 4096)`** 174 | * Generates text based on the input `prompt`, to a maximum length of `maxLength`. The `TokenCallback` (if provided) is executed directly on the generation thread. Return `true` to continue, `false` to stop. 175 | * **`string GenerateMultimodal(string prompt, float[] imageData, int imageWidth, int imageHeight, int maxLength = 4096)`** 176 | * Generates text based on a text input `prompt`, with an image input as well, to a maximum length of `maxLength`. `imageData` must be a flat array of RGB float values (0.0-1.0). 177 | * **`string GenerateMultimodal(string prompt, float[] imageData, int imageWidth, int imageHeight, TokenCallback callback, int maxLength = 4096)`** 178 | * Generates text with image input. The `TokenCallback` (if provided) is executed directly on the generation thread. Return `true` to continue, `false` to stop. 179 | * **`int CountTokens(string text)`** 180 | * Counts the number of tokens in the given text according to the loaded tokenizer. 181 | * **`void SetMultiturn(bool enable)`** 182 | * Enables or disables multiturn conversation history. 183 | * **`void SetTemperature(float temperature)`** 184 | * Sets the sampling temperature. 185 | * **`void SetTopK(int topK)`** 186 | * Sets the top-K sampling value. 187 | * **`void SetDeterministic(bool deterministic)`** 188 | * Enables or disables deterministic sampling (uses seed 0). 189 | * **`void ResetConversation()`** 190 | * Resets the history of the currently active conversation context. 191 | * **`bool CreateConversation(string conversationName)`** 192 | * Creates a named conversation context. 193 | * **`bool SwitchConversation(string conversationName)`** 194 | * Switches the active context. 195 | * **`bool DeleteConversation(string conversationName)`** 196 | * Deletes a named conversation context. 197 | * **`bool HasConversation(string conversationName)`** 198 | * Checks if a named conversation exists. 199 | * **`string GetCurrentConversation()`** 200 | * Gets the history of the currently active conversation context as a string. Returns `null` on error. 201 | * **`void EnableLogging(bool enable = true)`** 202 | * Enables or disables log messages from the native library via a callback to `Debug.WriteLine`. 203 | * **`void Dispose()`** 204 | * Releases the native Gemma context and associated resources. Must be called when finished. Implements `IDisposable`. 205 | 206 | **Exceptions** 207 | 208 | * **`GemmaException`**: Thrown for errors during Gemma operations (e.g., initialization failure, generation failure). 209 | * **`DllNotFoundException`**: Thrown if the `gemma.dll` (or platform equivalent) cannot be loaded. 210 | * **`ObjectDisposedException`**: Thrown if methods are called after `Dispose()` has been called. 211 | -------------------------------------------------------------------------------- /Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a636b72106a965349bf5789298875279 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/GemmaCpp.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GemmaCpp", 3 | "rootNamespace": "Google.Gemma.Cpp", 4 | "references": [ 5 | "UniTask" 6 | ], 7 | "includePlatforms": [], 8 | "excludePlatforms": [], 9 | "allowUnsafeCode": true, 10 | "overrideReferences": false, 11 | "precompiledReferences": [], 12 | "autoReferenced": true, 13 | "defineConstraints": [], 14 | "versionDefines": [], 15 | "noEngineReferences": false 16 | } -------------------------------------------------------------------------------- /Runtime/GemmaCpp.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8cfc57c05ec5b104ea766df5536e378d 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime/Scripts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 87005464a5ab9184b80270844b120ef7 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/Scripts/GemmaInterop.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | using System; 17 | using System.Diagnostics; 18 | using System.Runtime.InteropServices; 19 | using System.Text; 20 | namespace GemmaCpp 21 | { 22 | public class GemmaException : Exception 23 | { 24 | public GemmaException(string message) : base(message) { } 25 | } 26 | 27 | public class Gemma : IDisposable 28 | { 29 | private IntPtr _context; 30 | private bool _disposed; 31 | 32 | // Optional: Allow setting DLL path 33 | public static string DllPath { get; set; } = "gemma.dll"; 34 | 35 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] 36 | private static extern IntPtr LoadLibrary(string lpFileName); 37 | 38 | static Gemma() 39 | { 40 | // Load DLL from specified path 41 | if (LoadLibrary(DllPath) == IntPtr.Zero) 42 | { 43 | throw new DllNotFoundException($"Failed to load {DllPath}. Error: {Marshal.GetLastWin32Error()}"); 44 | } 45 | } 46 | 47 | [DllImport("gemma", CallingConvention = CallingConvention.Cdecl)] 48 | private static extern IntPtr GemmaCreate( 49 | [MarshalAs(UnmanagedType.LPUTF8Str)] string tokenizerPath, 50 | [MarshalAs(UnmanagedType.LPUTF8Str)] string weightsPath, 51 | int maxGeneratedTokens); 52 | 53 | [DllImport("gemma", CallingConvention = CallingConvention.Cdecl)] 54 | private static extern void GemmaDestroy(IntPtr context); 55 | 56 | // Delegate type for token callbacks 57 | public delegate bool TokenCallback(string token); 58 | 59 | // Keep delegate alive for duration of calls 60 | private GCHandle _callbackHandle; 61 | 62 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 63 | private delegate bool GemmaTokenCallback( 64 | [MarshalAs(UnmanagedType.LPUTF8Str)] string text, 65 | IntPtr userData); 66 | 67 | [DllImport("gemma", CallingConvention = CallingConvention.Cdecl)] 68 | private static extern int GemmaGenerate( 69 | IntPtr context, 70 | [MarshalAs(UnmanagedType.LPUTF8Str)] string prompt, 71 | [Out] byte[] output, 72 | int maxOutputChars, 73 | GemmaTokenCallback callback, 74 | IntPtr userData); 75 | 76 | [DllImport("gemma", CallingConvention = CallingConvention.Cdecl)] 77 | private static extern int GemmaGenerateMultimodal( 78 | IntPtr context, 79 | [MarshalAs(UnmanagedType.LPUTF8Str)] string prompt, 80 | IntPtr image_data, // Renamed param to match C API 81 | int image_width, // Added dimension 82 | int image_height, // Added dimension 83 | [MarshalAs(UnmanagedType.LPUTF8Str)] StringBuilder output, // Output should be StringBuilder for multimodal 84 | int maxOutputChars, 85 | GemmaTokenCallback callback, 86 | IntPtr userData); 87 | 88 | [DllImport("gemma", CallingConvention = CallingConvention.Cdecl)] 89 | private static extern int GemmaCountTokens( 90 | IntPtr context, 91 | [MarshalAs(UnmanagedType.LPUTF8Str)] string text); 92 | 93 | // Configuration function imports 94 | [DllImport("gemma", CallingConvention = CallingConvention.Cdecl)] 95 | private static extern void GemmaSetMaxGeneratedTokens(IntPtr context, int value); 96 | 97 | [DllImport("gemma", CallingConvention = CallingConvention.Cdecl)] 98 | private static extern void GemmaSetMultiturn(IntPtr context, int value); 99 | 100 | [DllImport("gemma", CallingConvention = CallingConvention.Cdecl)] 101 | private static extern void GemmaSetTemperature(IntPtr context, float value); 102 | 103 | [DllImport("gemma", CallingConvention = CallingConvention.Cdecl)] 104 | private static extern void GemmaSetTopK(IntPtr context, int value); 105 | 106 | [DllImport("gemma", CallingConvention = CallingConvention.Cdecl)] 107 | private static extern void GemmaSetDeterministic(IntPtr context, int value); 108 | 109 | [DllImport("gemma", CallingConvention = CallingConvention.Cdecl)] 110 | private static extern void GemmaSetPrefillTbatchSize(IntPtr context, int value); 111 | 112 | [DllImport("gemma", CallingConvention = CallingConvention.Cdecl, EntryPoint = "GemmaResetConversation")] 113 | private static extern void GemmaResetConversation(IntPtr context); 114 | 115 | // Conversation management function imports 116 | [DllImport("gemma", CallingConvention = CallingConvention.Cdecl, EntryPoint = "GemmaCreateConversation")] 117 | private static extern int GemmaCreateConversation( 118 | IntPtr context, 119 | [MarshalAs(UnmanagedType.LPUTF8Str)] string conversationName); 120 | 121 | [DllImport("gemma", CallingConvention = CallingConvention.Cdecl, EntryPoint = "GemmaSwitchConversation")] 122 | private static extern int GemmaSwitchConversation( 123 | IntPtr context, 124 | [MarshalAs(UnmanagedType.LPUTF8Str)] string conversationName); 125 | 126 | [DllImport("gemma", CallingConvention = CallingConvention.Cdecl, EntryPoint = "GemmaDeleteConversation")] 127 | private static extern int GemmaDeleteConversation( 128 | IntPtr context, 129 | [MarshalAs(UnmanagedType.LPUTF8Str)] string conversationName); 130 | 131 | [DllImport("gemma", CallingConvention = CallingConvention.Cdecl, EntryPoint = "GemmaHasConversation")] 132 | private static extern int GemmaHasConversation( 133 | IntPtr context, 134 | [MarshalAs(UnmanagedType.LPUTF8Str)] string conversationName); 135 | 136 | [DllImport("gemma", CallingConvention = CallingConvention.Cdecl, EntryPoint = "GemmaGetCurrentConversation")] 137 | [return: MarshalAs(UnmanagedType.LPUTF8Str)] // Marshal the const char* return value as a string 138 | private static extern string GemmaGetCurrentConversation(IntPtr context); 139 | 140 | [DllImport("gemma", CallingConvention = CallingConvention.Cdecl, EntryPoint = "GemmaSaveConversation")] 141 | private static extern void GemmaSaveConversation(IntPtr context); 142 | 143 | // Native callback delegate type 144 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 145 | private delegate void GemmaLogCallback( 146 | [MarshalAs(UnmanagedType.LPUTF8Str)] string message, 147 | IntPtr userData); 148 | 149 | [DllImport("gemma", CallingConvention = CallingConvention.Cdecl)] 150 | private static extern void GemmaSetLogCallback( 151 | IntPtr context, 152 | GemmaLogCallback callback, 153 | IntPtr userData); 154 | 155 | private GCHandle _logCallbackHandle; 156 | private bool _loggingEnabled = false; 157 | 158 | public Gemma(string tokenizerPath, string weightsPath, int maxGeneratedTokens = 8192) 159 | { 160 | _context = GemmaCreate(tokenizerPath, weightsPath, maxGeneratedTokens); 161 | if (_context == IntPtr.Zero) 162 | { 163 | throw new GemmaException("Failed to create Gemma context"); 164 | } 165 | } 166 | 167 | // Enable debug logging 168 | public void EnableLogging(bool enable = true) 169 | { 170 | if (enable && !_loggingEnabled) 171 | { 172 | GemmaLogCallback logCallback = (message, _) => 173 | { 174 | Debug.WriteLine($"Gemma: {message}"); 175 | }; 176 | _logCallbackHandle = GCHandle.Alloc(logCallback); 177 | GemmaSetLogCallback(_context, logCallback, IntPtr.Zero); 178 | _loggingEnabled = true; 179 | } 180 | else if (!enable && _loggingEnabled) 181 | { 182 | if (_logCallbackHandle.IsAllocated) 183 | _logCallbackHandle.Free(); 184 | GemmaSetLogCallback(_context, null, IntPtr.Zero); 185 | _loggingEnabled = false; 186 | } 187 | } 188 | 189 | // Configuration methods 190 | public void SetMultiturn(bool enable) 191 | { 192 | if (_disposed) 193 | throw new ObjectDisposedException(nameof(Gemma)); 194 | 195 | if (_context == IntPtr.Zero) 196 | throw new GemmaException("Gemma context is invalid"); 197 | 198 | GemmaSetMultiturn(_context, enable ? 1 : 0); 199 | Debug.WriteLine($"Gemma: Set multiturn to {(enable ? "enabled" : "disabled")}"); 200 | } 201 | 202 | public void SetTemperature(float temperature) 203 | { 204 | if (_disposed) 205 | throw new ObjectDisposedException(nameof(Gemma)); 206 | 207 | if (_context == IntPtr.Zero) 208 | throw new GemmaException("Gemma context is invalid"); 209 | 210 | GemmaSetTemperature(_context, temperature); 211 | Debug.WriteLine($"Gemma: Set temperature to {temperature}"); 212 | } 213 | 214 | public void SetTopK(int topK) 215 | { 216 | if (_disposed) 217 | throw new ObjectDisposedException(nameof(Gemma)); 218 | 219 | if (_context == IntPtr.Zero) 220 | throw new GemmaException("Gemma context is invalid"); 221 | 222 | GemmaSetTopK(_context, topK); 223 | Debug.WriteLine($"Gemma: Set topK to {topK}"); 224 | } 225 | 226 | public void SetDeterministic(bool deterministic) 227 | { 228 | if (_disposed) 229 | throw new ObjectDisposedException(nameof(Gemma)); 230 | 231 | if (_context == IntPtr.Zero) 232 | throw new GemmaException("Gemma context is invalid"); 233 | 234 | GemmaSetDeterministic(_context, deterministic ? 1 : 0); 235 | Debug.WriteLine($"Gemma: Set deterministic to {(deterministic ? "true" : "false")}"); 236 | } 237 | 238 | // Renamed public method 239 | public void ResetConversation() 240 | { 241 | if (_disposed) 242 | throw new ObjectDisposedException(nameof(Gemma)); 243 | 244 | if (_context == IntPtr.Zero) 245 | throw new GemmaException("Gemma context is invalid"); 246 | 247 | GemmaResetConversation(_context); // Call P/Invoke method 248 | Debug.WriteLine("Gemma: Reset active conversation"); 249 | } 250 | 251 | // Conversation management methods 252 | public bool CreateConversation(string conversationName) 253 | { 254 | if (_disposed) 255 | throw new ObjectDisposedException(nameof(Gemma)); 256 | 257 | if (_context == IntPtr.Zero) 258 | throw new GemmaException("Gemma context is invalid"); 259 | 260 | bool result = GemmaCreateConversation(_context, conversationName) != 0; // Call P/Invoke method 261 | Debug.WriteLine($"Gemma: Create conversation '{conversationName}' - {(result ? "succeeded" : "failed")}"); 262 | return result; 263 | } 264 | 265 | public bool SwitchConversation(string conversationName) 266 | { 267 | if (_disposed) 268 | throw new ObjectDisposedException(nameof(Gemma)); 269 | 270 | if (_context == IntPtr.Zero) 271 | throw new GemmaException("Gemma context is invalid"); 272 | 273 | bool result = GemmaSwitchConversation(_context, conversationName) != 0; // Call P/Invoke method 274 | Debug.WriteLine($"Gemma: Switch to conversation '{conversationName}' - {(result ? "succeeded" : "failed")}"); 275 | return result; 276 | } 277 | 278 | public bool DeleteConversation(string conversationName) 279 | { 280 | if (_disposed) 281 | throw new ObjectDisposedException(nameof(Gemma)); 282 | 283 | if (_context == IntPtr.Zero) 284 | throw new GemmaException("Gemma context is invalid"); 285 | 286 | bool result = GemmaDeleteConversation(_context, conversationName) != 0; // Call P/Invoke method 287 | Debug.WriteLine($"Gemma: Delete conversation '{conversationName}' - {(result ? "succeeded" : "failed")}"); 288 | return result; 289 | } 290 | 291 | public bool HasConversation(string conversationName) 292 | { 293 | if (_disposed) 294 | throw new ObjectDisposedException(nameof(Gemma)); 295 | 296 | if (_context == IntPtr.Zero) 297 | throw new GemmaException("Gemma context is invalid"); 298 | 299 | bool result = GemmaHasConversation(_context, conversationName) != 0; // Call P/Invoke method 300 | Debug.WriteLine($"Gemma: Has conversation '{conversationName}' - {result}"); 301 | return result; 302 | } 303 | 304 | public string GetCurrentConversation() 305 | { 306 | if (_disposed) 307 | throw new ObjectDisposedException(nameof(Gemma)); 308 | 309 | if (_context == IntPtr.Zero) 310 | throw new GemmaException("Gemma context is invalid"); 311 | 312 | string currentConversation = GemmaGetCurrentConversation(_context); // Call P/Invoke method 313 | Debug.WriteLine($"Gemma: Current conversation is '{currentConversation}'"); 314 | return currentConversation; 315 | } 316 | 317 | public void SaveConversation() 318 | { 319 | if (_disposed) 320 | throw new ObjectDisposedException(nameof(Gemma)); 321 | 322 | if (_context == IntPtr.Zero) 323 | throw new GemmaException("Gemma context is invalid"); 324 | 325 | GemmaSaveConversation(_context); 326 | Debug.WriteLine($"Gemma: Saved current conversation ('{GetCurrentConversation()}') to prewarmed cache."); 327 | } 328 | 329 | public int CountTokens(string prompt) 330 | { 331 | if (_disposed) 332 | throw new ObjectDisposedException(nameof(Gemma)); 333 | 334 | if (_context == IntPtr.Zero) 335 | throw new GemmaException("Gemma context is invalid"); 336 | int count = GemmaCountTokens(_context, prompt); 337 | return count; 338 | } 339 | 340 | public string Generate(string prompt, int maxOutputChars = 4096) 341 | { 342 | return Generate(prompt, null, maxOutputChars); 343 | } 344 | 345 | public string Generate(string prompt, TokenCallback callback, int maxOutputChars = 4096) 346 | { 347 | if (_disposed) 348 | throw new ObjectDisposedException(nameof(Gemma)); 349 | 350 | if (_context == IntPtr.Zero) 351 | throw new GemmaException("Gemma context is invalid"); 352 | 353 | var outputBuffer = new byte[maxOutputChars * 4]; // Allow for worst case UTF-8 size 354 | GemmaTokenCallback nativeCallback = null; 355 | 356 | // Track token count for debugging 357 | int tokenCount = 0; 358 | 359 | if (callback != null) 360 | { 361 | nativeCallback = (text, _) => 362 | { 363 | tokenCount++; 364 | // Log token for debugging 365 | Debug.WriteLine($"Token {tokenCount}: '{text}'"); 366 | 367 | // Pass token to user callback 368 | return callback(text); 369 | }; 370 | _callbackHandle = GCHandle.Alloc(nativeCallback); 371 | } 372 | 373 | try 374 | { 375 | int length = GemmaGenerate(_context, prompt, outputBuffer, maxOutputChars, 376 | nativeCallback, IntPtr.Zero); 377 | 378 | if (length < 0) 379 | throw new GemmaException("Generation failed"); 380 | 381 | Debug.WriteLine($"Generation complete: {tokenCount} tokens processed, result length: {length}"); 382 | 383 | // Convert the byte buffer to a string using UTF-8 encoding 384 | string result = Encoding.UTF8.GetString(outputBuffer, 0, length); 385 | return result; 386 | } 387 | finally 388 | { 389 | if (_callbackHandle.IsAllocated) 390 | _callbackHandle.Free(); 391 | } 392 | } 393 | 394 | public string GenerateMultimodal(string prompt, float[] imageData, int imageWidth, int imageHeight, int maxOutputChars = 4096) 395 | { 396 | // Pass width and height to the overloaded method 397 | return GenerateMultimodal(prompt, imageData, imageWidth, imageHeight, null, maxOutputChars); 398 | } 399 | 400 | public string GenerateMultimodal(string prompt, float[] imageData, int imageWidth, int imageHeight, TokenCallback callback, int maxOutputChars = 4096) 401 | { 402 | if (_disposed) 403 | throw new ObjectDisposedException(nameof(Gemma)); 404 | 405 | if (_context == IntPtr.Zero) 406 | throw new GemmaException("Gemma context is invalid"); 407 | 408 | if (imageData == null || imageData.Length == 0) 409 | throw new ArgumentException("Image data cannot be null or empty", nameof(imageData)); 410 | 411 | if (imageWidth <= 0 || imageHeight <= 0) 412 | throw new ArgumentException("Image dimensions must be positive"); 413 | 414 | if (imageData.Length < imageWidth * imageHeight * 3) 415 | throw new ArgumentException("Image data array is too small for the specified dimensions"); 416 | 417 | var output = new StringBuilder(maxOutputChars); 418 | GemmaTokenCallback nativeCallback = null; 419 | 420 | if (callback != null) 421 | { 422 | nativeCallback = (text, _) => callback(text); 423 | _callbackHandle = GCHandle.Alloc(nativeCallback); 424 | } 425 | 426 | // Pin the image data so it doesn't move during the native call 427 | GCHandle imageHandle = GCHandle.Alloc(imageData, GCHandleType.Pinned); 428 | 429 | try 430 | { 431 | IntPtr imagePtr = imageHandle.AddrOfPinnedObject(); 432 | 433 | // Pass image dimensions to the native call 434 | int length = GemmaGenerateMultimodal(_context, prompt, imagePtr, imageWidth, imageHeight, output, maxOutputChars, 435 | nativeCallback, IntPtr.Zero); 436 | 437 | if (length < 0) 438 | throw new GemmaException("Multimodal generation failed"); 439 | 440 | return output.ToString(); 441 | } 442 | finally 443 | { 444 | imageHandle.Free(); 445 | 446 | if (_callbackHandle.IsAllocated) 447 | _callbackHandle.Free(); 448 | } 449 | } 450 | 451 | public void Dispose() 452 | { 453 | if (!_disposed) 454 | { 455 | if (_context != IntPtr.Zero) 456 | { 457 | GemmaDestroy(_context); 458 | _context = IntPtr.Zero; 459 | } 460 | if (_logCallbackHandle.IsAllocated) 461 | _logCallbackHandle.Free(); 462 | _disposed = true; 463 | } 464 | } 465 | 466 | ~Gemma() 467 | { 468 | Dispose(); 469 | } 470 | } 471 | } 472 | -------------------------------------------------------------------------------- /Runtime/Scripts/GemmaInterop.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2e76a0bf6f4f28a459c242be11fb97d2 -------------------------------------------------------------------------------- /Runtime/Scripts/GemmaManager.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | using UnityEngine; 17 | using System; 18 | using System.Collections.Generic; // Added for Dictionary 19 | using System.Text; // Added for StringBuilder 20 | using Cysharp.Threading.Tasks; 21 | 22 | namespace GemmaCpp 23 | { 24 | public class GemmaManager : MonoBehaviour 25 | { 26 | [SerializeField] 27 | public GemmaManagerSettings settings; 28 | 29 | [SerializeField] 30 | private bool verboseLogging = false; 31 | 32 | private Gemma gemma; 33 | private bool isInitialized; 34 | 35 | public bool Initialized => isInitialized; 36 | 37 | private void Start() 38 | { 39 | Debug.Log("GemmaManager: Starting initialization"); 40 | InitializeGemma(); 41 | } 42 | 43 | private void OnDestroy() 44 | { 45 | if (gemma != null) 46 | { 47 | gemma.Dispose(); 48 | gemma = null; 49 | isInitialized = false; 50 | Debug.Log("GemmaManager: Resources cleaned up"); 51 | } 52 | } 53 | 54 | private void InitializeGemma() 55 | { 56 | try 57 | { 58 | Debug.Log($"GemmaManager: Initializing with tokenizer: {settings.TokenizerPath}, weights: {settings.WeightsPath}"); 59 | Debug.Log($"GemmaManager: Using max tokens: {settings.MaxGeneratedTokens}"); 60 | 61 | gemma = new Gemma( 62 | settings.TokenizerPath, 63 | settings.WeightsPath, 64 | settings.MaxGeneratedTokens 65 | ); 66 | gemma.EnableLogging(verboseLogging); 67 | isInitialized = true; 68 | 69 | verboseLogging = true; 70 | 71 | // Apply settings and logging after successful initialization 72 | if (isInitialized) 73 | { 74 | try 75 | { 76 | // Enable Multiturn mode by default for Manager usage 77 | gemma.SetMultiturn(true); 78 | Debug.Log("GemmaManager: Multiturn mode enabled by default."); 79 | 80 | gemma.SetTemperature(settings.Temperature); 81 | 82 | if (verboseLogging) 83 | { 84 | Debug.Log($"GemmaManager: Applied settings - Multiturn: Enabled, Temperature: {settings.Temperature}, Native Logging: Enabled"); 85 | } 86 | else 87 | { 88 | Debug.Log($"GemmaManager: Applied settings - Multiturn: Enabled, Temperature: {settings.Temperature}"); 89 | } 90 | } 91 | catch (Exception settingsEx) 92 | { 93 | Debug.LogWarning($"GemmaManager: Failed to apply some settings - {settingsEx.Message}"); 94 | // Continue initialization even if settings fail to apply 95 | } 96 | } 97 | 98 | Debug.Log("GemmaManager: Initialized successfully"); 99 | } 100 | catch (Exception e) 101 | { 102 | Debug.LogError($"GemmaManager: Error initializing - {e.Message}\n{e.StackTrace}"); 103 | } 104 | } 105 | public enum PrewarmState 106 | { 107 | NotApplicable, 108 | Pending, 109 | InProgress, 110 | Done 111 | } 112 | public delegate void PrewarmStatusCallback(string conversation, PrewarmState state); 113 | 114 | public async UniTask Prewarm(Dictionary conversations, PrewarmStatusCallback callback = null) 115 | { 116 | 117 | // Using Time.time for elapsed game time, formatted to 3 decimal places (F3) 118 | string timestamp = $"[{Time.time:F3}]"; 119 | Debug.Log($"GemmaManager::Prewarm(): {timestamp} Prewarm sequence started. Waiting for GemmaManager initialization..."); 120 | 121 | while (!isInitialized) 122 | { 123 | timestamp = $"[{Time.time:F3}]"; 124 | Debug.Log($"GemmaManager::Prewarm(): {timestamp} Waiting for GemmaManager to initialize..."); 125 | await UniTask.Delay(TimeSpan.FromSeconds(1)); // Wait 1 second 126 | } 127 | 128 | timestamp = $"[{Time.time:F3}]"; 129 | Debug.Log($"GemmaManager::Prewarm(): {timestamp} GemmaManager initialized. Starting conversation prewarming."); 130 | 131 | int count = 0; 132 | foreach (var kvp in conversations) 133 | { 134 | string conversationName = kvp.Key; 135 | string initialPrompt = kvp.Value; // Use the value from the dictionary as the prompt 136 | 137 | timestamp = $"[{Time.time:F3}]"; 138 | if (string.IsNullOrEmpty(conversationName)) 139 | { 140 | Debug.LogWarning($"GemmaManager::Prewarm(): {timestamp} Skipping conversation at index {count} due to missing conversation name."); 141 | count++; 142 | continue; 143 | } 144 | 145 | // Use 'this' to access GemmaManager methods 146 | if (!this.HasConversation(conversationName)) 147 | { 148 | Debug.Log($"GemmaManager::Prewarm(): {timestamp} Prewarming conversation: {conversationName}"); 149 | try 150 | { 151 | this.CreateConversation(conversationName); 152 | Debug.Log($"GemmaManager::Prewarm(): {timestamp} Successfully created conversation: {conversationName}"); 153 | 154 | // Switch to the new conversation and generate initial response 155 | this.SwitchConversation(conversationName); 156 | if (!string.IsNullOrEmpty(initialPrompt)) 157 | { 158 | Debug.Log($"GemmaManager::Prewarm(): {timestamp} Generating initial response for {conversationName}..."); 159 | if (callback != null) 160 | { 161 | callback(conversationName, PrewarmState.InProgress); 162 | } 163 | await UniTask.RunOnThreadPool(() => { 164 | gemma.Generate(initialPrompt, 64); 165 | gemma.SaveConversation(); 166 | //GenerateResponseAsync(initialPrompt); 167 | }); 168 | if (callback != null) 169 | { 170 | callback(conversationName, PrewarmState.Done); 171 | } 172 | Debug.Log($"GemmaManager::Prewarm(): {timestamp} Initial response generated for {conversationName}."); 173 | } 174 | else 175 | { 176 | Debug.Log($"GemmaManager::Prewarm(): {timestamp} No initial prompt provided for {conversationName}, skipping generation."); 177 | } 178 | } 179 | catch (Exception ex) 180 | { 181 | Debug.LogError($"GemmaManager::Prewarm(): {timestamp} Failed to create or prewarm conversation {conversationName}: {ex.Message}"); 182 | } 183 | } 184 | else 185 | { 186 | Debug.Log($"GemmaManager::Prewarm(): {timestamp} Conversation already exists: {conversationName}"); 187 | } 188 | count++; 189 | } 190 | 191 | timestamp = $"[{Time.time:F3}]"; 192 | Debug.Log($"GemmaManager::Prewarm(): {timestamp} Prewarm finished processing {count} conversations."); 193 | 194 | SwitchConversation("default"); 195 | } 196 | 197 | public async UniTask GenerateResponseAsync(string prompt, Gemma.TokenCallback onTokenReceived = null) 198 | { 199 | if (!isInitialized) 200 | { 201 | Debug.LogError("GemmaManager: Cannot generate response - not initialized"); 202 | throw new InvalidOperationException("Gemma is not initialized"); 203 | } 204 | 205 | if (verboseLogging) 206 | { 207 | Debug.Log($"GemmaManager: Generating response for prompt: \"{TruncateForLogging(prompt)}\""); 208 | } 209 | 210 | // Create a callback that uses UniTask's PlayerLoopTiming to run on main thread 211 | Gemma.TokenCallback wrappedCallback = null; 212 | if (onTokenReceived != null) 213 | { 214 | wrappedCallback = (token) => 215 | { 216 | bool result = false; 217 | UniTask.Post(() => 218 | { 219 | result = onTokenReceived(token); 220 | /* 221 | if (verboseLogging) 222 | { 223 | Debug.Log($"GemmaManager: Token received: \"{TruncateForLogging(token)}\""); 224 | } 225 | */ 226 | }, PlayerLoopTiming.Update); 227 | return true; 228 | }; 229 | } 230 | 231 | // Run the generation on a background thread 232 | Debug.Log("GemmaManager: Starting text generation on background thread"); 233 | try 234 | { 235 | string result = await UniTask.RunOnThreadPool(() => 236 | { 237 | try 238 | { 239 | Debug.Log(prompt); 240 | return gemma.Generate(prompt, wrappedCallback, settings.MaxGeneratedTokens); 241 | } 242 | catch (Exception e) 243 | { 244 | Debug.LogError($"GemmaManager: Generation failed - {e.Message}\n{e.StackTrace}"); 245 | throw new Exception($"Failed to generate response: {e.Message}", e); 246 | } 247 | }); 248 | 249 | if (verboseLogging) 250 | { 251 | Debug.Log($"GemmaManager: Generation complete, result length: {result.Length} chars"); 252 | } 253 | else 254 | { 255 | Debug.Log("GemmaManager: Text generation complete"); 256 | } 257 | 258 | return result; 259 | } 260 | catch (Exception e) 261 | { 262 | Debug.LogError($"GemmaManager: Exception during generation - {e.Message}\n{e.StackTrace}"); 263 | throw; 264 | } 265 | } 266 | 267 | /// 268 | /// Generate a response from Gemma using both text and image input 269 | /// 270 | /// The text prompt to send to Gemma 271 | /// The RenderTexture containing the image to process 272 | /// Optional callback to receive tokens as they're generated 273 | /// The generated response 274 | public async UniTask GenerateMultimodalResponseAsync(string prompt, RenderTexture renderTexture, Gemma.TokenCallback onTokenReceived = null) 275 | { 276 | if (!isInitialized) 277 | { 278 | Debug.LogError("GemmaManager: Cannot generate multimodal response - not initialized"); 279 | throw new InvalidOperationException("Gemma is not initialized"); 280 | } 281 | 282 | if (renderTexture == null) 283 | { 284 | Debug.LogError("GemmaManager: RenderTexture is null"); 285 | throw new ArgumentNullException(nameof(renderTexture), "RenderTexture cannot be null"); 286 | } 287 | 288 | Debug.Log($"GemmaManager: Starting multimodal generation with image {renderTexture.width}x{renderTexture.height}"); 289 | if (verboseLogging) 290 | { 291 | Debug.Log($"GemmaManager: Multimodal prompt: \"{TruncateForLogging(prompt)}\""); 292 | } 293 | 294 | // Create a callback that uses UniTask's PlayerLoopTiming to run on main thread 295 | Gemma.TokenCallback wrappedCallback = null; 296 | if (onTokenReceived != null) 297 | { 298 | wrappedCallback = (token) => 299 | { 300 | bool result = false; 301 | UniTask.Post(() => 302 | { 303 | result = onTokenReceived(token); 304 | if (verboseLogging) 305 | { 306 | Debug.Log($"GemmaManager: Multimodal token received: \"{TruncateForLogging(token)}\""); 307 | } 308 | }, PlayerLoopTiming.Update); 309 | return true; 310 | }; 311 | } 312 | 313 | try 314 | { 315 | // Convert RenderTexture to float array on the main thread 316 | Debug.Log("GemmaManager: Converting RenderTexture to float array"); 317 | float[] imageData = await ConvertRenderTextureToFloatArrayAsync(renderTexture); 318 | Debug.Log($"GemmaManager: Converted image to float array, size: {imageData.Length} elements"); 319 | 320 | // Run the generation on a background thread 321 | Debug.Log("GemmaManager: Starting multimodal generation on background thread"); 322 | string result = await UniTask.RunOnThreadPool(() => 323 | { 324 | try 325 | { 326 | return gemma.GenerateMultimodal(prompt, imageData, renderTexture.width, renderTexture.height, wrappedCallback, settings.MaxGeneratedTokens); 327 | } 328 | catch (Exception e) 329 | { 330 | Debug.LogError($"GemmaManager: Multimodal generation failed - {e.Message}\n{e.StackTrace}"); 331 | throw new Exception($"Failed to generate multimodal response: {e.Message}", e); 332 | } 333 | }); 334 | 335 | if (verboseLogging) 336 | { 337 | Debug.Log($"GemmaManager: Multimodal generation complete, result length: {result.Length} chars"); 338 | } 339 | else 340 | { 341 | Debug.Log("GemmaManager: Multimodal generation complete"); 342 | } 343 | 344 | return result; 345 | } 346 | catch (Exception e) 347 | { 348 | Debug.LogError($"GemmaManager: Exception during multimodal generation - {e.Message}\n{e.StackTrace}"); 349 | throw; 350 | } 351 | } 352 | 353 | /// 354 | /// Converts a RenderTexture to a float array of RGB values in the range [0,1] 355 | /// 356 | /// The RenderTexture to convert 357 | /// Float array of RGB values 358 | private async UniTask ConvertRenderTextureToFloatArrayAsync(RenderTexture renderTexture) 359 | { 360 | // This needs to run on the main thread because it accesses Unity objects 361 | await UniTask.SwitchToMainThread(); 362 | Debug.Log("GemmaManager: Starting RenderTexture conversion on main thread"); 363 | 364 | int width = renderTexture.width; 365 | int height = renderTexture.height; 366 | Debug.Log($"GemmaManager: Processing image of size {width}x{height}"); 367 | 368 | // Create a temporary texture to read the pixels 369 | Texture2D texture = new Texture2D(width, height, TextureFormat.RGB24, false); 370 | Debug.Log("GemmaManager: Created temporary Texture2D"); 371 | 372 | // Remember the current active render texture 373 | RenderTexture previousActive = RenderTexture.active; 374 | 375 | try 376 | { 377 | // Set the render texture as active and read the pixels 378 | RenderTexture.active = renderTexture; 379 | texture.ReadPixels(new Rect(0, 0, width, height), 0, 0); 380 | texture.Apply(); 381 | Debug.Log("GemmaManager: Read pixels from RenderTexture"); 382 | 383 | // Get the raw pixel data 384 | Color32[] pixels = texture.GetPixels32(); 385 | Debug.Log($"GemmaManager: Got {pixels.Length} pixels from texture"); 386 | 387 | // Convert to float array in the format expected by Gemma (RGB values in [0,1]) 388 | float[] imageData = new float[pixels.Length * 3]; 389 | for (int i = 0; i < pixels.Length; i++) 390 | { 391 | imageData[i * 3 + 0] = pixels[i].r / 255.0f; // R 392 | imageData[i * 3 + 1] = pixels[i].g / 255.0f; // G 393 | imageData[i * 3 + 2] = pixels[i].b / 255.0f; // B 394 | } 395 | 396 | if (verboseLogging) 397 | { 398 | // Log some sample pixel values to verify conversion 399 | Debug.Log($"GemmaManager: Sample pixel values (first 3 pixels):"); 400 | for (int i = 0; i < Math.Min(3, pixels.Length); i++) 401 | { 402 | Debug.Log($" Pixel {i}: R={imageData[i * 3 + 0]:F2}, G={imageData[i * 3 + 1]:F2}, B={imageData[i * 3 + 2]:F2}"); 403 | } 404 | } 405 | 406 | Debug.Log($"GemmaManager: Converted to float array with {imageData.Length} elements"); 407 | return imageData; 408 | } 409 | catch (Exception e) 410 | { 411 | Debug.LogError($"GemmaManager: Error converting RenderTexture - {e.Message}\n{e.StackTrace}"); 412 | throw; 413 | } 414 | finally 415 | { 416 | // Restore the previous active render texture 417 | RenderTexture.active = previousActive; 418 | Debug.Log("GemmaManager: Restored previous RenderTexture.active"); 419 | 420 | // Clean up the temporary texture 421 | Destroy(texture); 422 | Debug.Log("GemmaManager: Destroyed temporary texture"); 423 | } 424 | } 425 | 426 | #region Conversation Management 427 | 428 | /// 429 | /// Enables / disables multiturn. 430 | /// 431 | public void SetMultiturn(bool enable) 432 | { 433 | gemma.SetMultiturn(enable); 434 | } 435 | 436 | /// 437 | /// Resets the current conversation context in the Gemma model. 438 | /// 439 | public void ResetCurrentConversation() 440 | { 441 | if (!isInitialized) 442 | { 443 | Debug.LogError("GemmaManager: Cannot reset conversation - not initialized"); 444 | throw new InvalidOperationException("Gemma is not initialized"); 445 | } 446 | 447 | try 448 | { 449 | gemma.ResetConversation(); // Call the method from GemmaInterop 450 | Debug.Log("GemmaManager: Current conversation reset requested."); 451 | } 452 | catch (Exception e) 453 | { 454 | Debug.LogError($"GemmaManager: Error resetting conversation - {e.Message}\n{e.StackTrace}"); 455 | throw; // Re-throw or handle as appropriate 456 | } 457 | } 458 | 459 | /// 460 | /// Creates a new named conversation context. 461 | /// 462 | /// The unique name for the new conversation. 463 | /// True if the conversation was created successfully, false otherwise. 464 | public bool CreateConversation(string conversationName) 465 | { 466 | if (!isInitialized) 467 | { 468 | Debug.LogError("GemmaManager: Cannot create conversation - not initialized"); 469 | throw new InvalidOperationException("Gemma is not initialized"); 470 | } 471 | if (string.IsNullOrEmpty(conversationName)) 472 | { 473 | Debug.LogError("GemmaManager: Conversation name cannot be null or empty."); 474 | return false; 475 | } 476 | 477 | try 478 | { 479 | bool result = gemma.CreateConversation(conversationName); 480 | if (verboseLogging) Debug.Log($"GemmaManager: Create conversation '{conversationName}' result: {result}"); 481 | return result; 482 | } 483 | catch (Exception e) 484 | { 485 | Debug.LogError($"GemmaManager: Error creating conversation '{conversationName}' - {e.Message}\n{e.StackTrace}"); 486 | throw; 487 | } 488 | } 489 | 490 | /// 491 | /// Switches the active conversation context to the specified name. 492 | /// 493 | /// The name of the conversation to switch to. 494 | /// True if the switch was successful, false otherwise (e.g., conversation doesn't exist). 495 | public bool SwitchConversation(string conversationName) 496 | { 497 | if (!isInitialized) 498 | { 499 | Debug.LogError("GemmaManager: Cannot switch conversation - not initialized"); 500 | throw new InvalidOperationException("Gemma is not initialized"); 501 | } 502 | if (string.IsNullOrEmpty(conversationName)) 503 | { 504 | Debug.LogError("GemmaManager: Conversation name cannot be null or empty."); 505 | return false; 506 | } 507 | 508 | try 509 | { 510 | bool result = gemma.SwitchConversation(conversationName); 511 | if (verboseLogging) Debug.Log($"GemmaManager: Switch to conversation '{conversationName}' result: {result}"); 512 | return result; 513 | } 514 | catch (Exception e) 515 | { 516 | Debug.LogError($"GemmaManager: Error switching to conversation '{conversationName}' - {e.Message}\n{e.StackTrace}"); 517 | throw; 518 | } 519 | } 520 | 521 | /// 522 | /// Deletes a named conversation context. 523 | /// 524 | /// The name of the conversation to delete. 525 | /// True if deletion was successful, false otherwise. 526 | public bool DeleteConversation(string conversationName) 527 | { 528 | if (!isInitialized) 529 | { 530 | Debug.LogError("GemmaManager: Cannot delete conversation - not initialized"); 531 | throw new InvalidOperationException("Gemma is not initialized"); 532 | } 533 | if (string.IsNullOrEmpty(conversationName)) 534 | { 535 | Debug.LogError("GemmaManager: Conversation name cannot be null or empty."); 536 | return false; 537 | } 538 | 539 | try 540 | { 541 | bool result = gemma.DeleteConversation(conversationName); 542 | if (verboseLogging) Debug.Log($"GemmaManager: Delete conversation '{conversationName}' result: {result}"); 543 | return result; 544 | } 545 | catch (Exception e) 546 | { 547 | Debug.LogError($"GemmaManager: Error deleting conversation '{conversationName}' - {e.Message}\n{e.StackTrace}"); 548 | throw; 549 | } 550 | } 551 | 552 | /// 553 | /// Checks if a conversation with the specified name exists. 554 | /// 555 | /// The name of the conversation to check. 556 | /// True if the conversation exists, false otherwise. 557 | public bool HasConversation(string conversationName) 558 | { 559 | if (!isInitialized) 560 | { 561 | Debug.LogError("GemmaManager: Cannot check conversation - not initialized"); 562 | throw new InvalidOperationException("Gemma is not initialized"); 563 | } 564 | if (string.IsNullOrEmpty(conversationName)) 565 | { 566 | Debug.LogError("GemmaManager: Conversation name cannot be null or empty."); 567 | return false; 568 | } 569 | 570 | try 571 | { 572 | bool result = gemma.HasConversation(conversationName); 573 | if (verboseLogging) Debug.Log($"GemmaManager: Has conversation '{conversationName}' result: {result}"); 574 | return result; 575 | } 576 | catch (Exception e) 577 | { 578 | Debug.LogError($"GemmaManager: Error checking for conversation '{conversationName}' - {e.Message}\n{e.StackTrace}"); 579 | throw; 580 | } 581 | } 582 | 583 | /// 584 | /// Gets the name of the currently active conversation. 585 | /// 586 | /// The name of the active conversation, or null/empty if none is active or an error occurs. 587 | public string GetCurrentConversation() 588 | { 589 | if (!isInitialized) 590 | { 591 | Debug.LogError("GemmaManager: Cannot get current conversation - not initialized"); 592 | throw new InvalidOperationException("Gemma is not initialized"); 593 | } 594 | 595 | try 596 | { 597 | string conversationName = gemma.GetCurrentConversation(); // Assuming GemmaInterop.Gemma has this method 598 | if (verboseLogging) Debug.Log($"GemmaManager: Current conversation is '{conversationName}'"); 599 | return conversationName; 600 | } 601 | catch (Exception e) 602 | { 603 | Debug.LogError($"GemmaManager: Error getting current conversation - {e.Message}\n{e.StackTrace}"); 604 | throw; // Re-throw or handle as appropriate 605 | } 606 | } 607 | 608 | #endregion 609 | 610 | /// 611 | /// Truncates a string for logging purposes to avoid flooding the console 612 | /// 613 | private string TruncateForLogging(string text, int maxLength = 50) 614 | { 615 | if (string.IsNullOrEmpty(text)) 616 | return string.Empty; 617 | 618 | if (text.Length <= maxLength) 619 | return text; 620 | 621 | return text.Substring(0, maxLength) + "..."; 622 | } 623 | } 624 | } 625 | -------------------------------------------------------------------------------- /Runtime/Scripts/GemmaManager.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b9ee03520f6527f40928298a9c13269b -------------------------------------------------------------------------------- /Runtime/Scripts/GemmaManagerSettings.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | using UnityEngine; 17 | using System.IO; 18 | 19 | namespace GemmaCpp 20 | { 21 | public enum GemmaWeightFormat 22 | { 23 | f32, // 32-bit floating point 24 | bf16, // Brain floating point (16-bit) 25 | sfp // Structured floating point 26 | } 27 | 28 | [CreateAssetMenu(fileName = "GemmaSettings", menuName = "Gemma/Settings")] 29 | public class GemmaManagerSettings : ScriptableObject 30 | { 31 | [Header("Model Configuration")] 32 | [SerializeField] private GemmaModelType modelType = GemmaModelType.Gemma2B; 33 | [SerializeField] private GemmaWeightFormat weightFormat = GemmaWeightFormat.sfp; 34 | 35 | [Header("Model Files")] 36 | [SerializeField, Tooltip("Folder name in StreamingAssets containing the model files")] 37 | private string modelFolder = "gemma-3.0-4b"; 38 | 39 | [SerializeField, Tooltip("Name of the tokenizer file (e.g., tokenizer.model)")] 40 | private string tokenizerFileName = "tokenizer.spm"; 41 | 42 | [SerializeField, Tooltip("Name of the weights file")] 43 | private string weightsFileName = "4b-it-sfp.sbs"; 44 | [SerializeField, Tooltip("Model type string")] 45 | private string modelFlag = "gemma3-4b"; 46 | 47 | [Header("Generation Settings")] 48 | [SerializeField, Tooltip("Maximum number of tokens to generate per turn")] 49 | private int maxGeneratedTokens = 384; 50 | 51 | [SerializeField, Range(0f, 1f), Tooltip("Temperature for text generation (higher = more random)")] 52 | private float temperature = 0.7f; 53 | 54 | [SerializeField, Range(0f, 1f), Tooltip("Top-p sampling (nucleus sampling) threshold")] 55 | private float topP = 0.9f; 56 | 57 | // Properties 58 | public string ModelFlag => modelFlag; 59 | public GemmaWeightFormat WeightFormat => weightFormat; 60 | public string ModelPath => Path.Combine(Application.streamingAssetsPath, modelFolder); 61 | public string TokenizerPath => Path.Combine(ModelPath, tokenizerFileName); 62 | public string WeightsPath => Path.Combine(ModelPath, weightsFileName); 63 | public int MaxGeneratedTokens => maxGeneratedTokens; 64 | public float Temperature => temperature; 65 | public float TopP => topP; 66 | 67 | private void OnValidate() 68 | { 69 | // Validate paths 70 | #if UNITY_EDITOR 71 | if (!string.IsNullOrEmpty(ModelPath)) 72 | { 73 | if (!Directory.Exists(ModelPath)) 74 | { 75 | Debug.LogWarning($"Model folder not found: {ModelPath}"); 76 | } 77 | else 78 | { 79 | if (!File.Exists(TokenizerPath)) 80 | { 81 | Debug.LogWarning($"Tokenizer file not found: {TokenizerPath}"); 82 | } 83 | if (!File.Exists(WeightsPath)) 84 | { 85 | Debug.LogWarning($"Weights file not found: {WeightsPath}"); 86 | } 87 | } 88 | } 89 | #endif 90 | 91 | maxGeneratedTokens = Mathf.Max(1, maxGeneratedTokens); 92 | temperature = Mathf.Clamp(temperature, 0f, 1f); 93 | topP = Mathf.Clamp(topP, 0f, 1f); 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /Runtime/Scripts/GemmaManagerSettings.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a9bcc1c371a69d747aed70ff036787c4 -------------------------------------------------------------------------------- /Runtime/Scripts/GemmaModelType.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | namespace GemmaCpp 17 | { 18 | public enum GemmaModelType 19 | { 20 | Unknown, 21 | Gemma2B, 22 | Gemma7B, 23 | Gemma2_9B, 24 | Gemma2_27B, 25 | Griffin2B, 26 | GemmaTiny, 27 | Gemma2_2B, 28 | Gemma3_4B, 29 | Gemma3_1B, 30 | Gemma3_12B, 31 | Gemma3_27B, 32 | PaliGemma224, 33 | PaliGemma448, 34 | PaliGemma2_3B_224, 35 | PaliGemma2_3B_448, 36 | PaliGemma2_10B_224, 37 | PaliGemma2_10B_448 38 | } 39 | } -------------------------------------------------------------------------------- /Runtime/Scripts/GemmaModelType.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c0c22a46fef302f4ebba530d06f4a009 -------------------------------------------------------------------------------- /Runtime/Scripts/GemmaModelUtils.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // SPDX-License-Identifier: Apache-2.0 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | using System.Collections.Generic; 17 | 18 | namespace GemmaCpp 19 | { 20 | public static class GemmaModelUtils 21 | { 22 | // Model flag strings that map to model types 23 | private static readonly string[] ModelFlags = new[] 24 | { 25 | "2b-pt", "2b-it", // Gemma 2B 26 | "7b-pt", "7b-it", // Gemma 7B 27 | "gr2b-pt", "gr2b-it", // RecurrentGemma 28 | "tiny", // Gemma Tiny 29 | "gemma2-2b-pt", "gemma2-2b-it", // Gemma2 2B 30 | "9b-pt", "9b-it", // Gemma2 9B 31 | "27b-pt", "27b-it", // Gemma2 27B 32 | "gemma3-4b", // Gemma3 4B 33 | "gemma3-1b", // Gemma3 1B 34 | "gemma3-12b", // Gemma3 12B 35 | "gemma3-27b", // Gemma3 27B 36 | "paligemma-224", // PaliGemma 224 37 | "paligemma-448", // PaliGemma 448 38 | "paligemma2-3b-224", // PaliGemma2 3B 224 39 | "paligemma2-3b-448", // PaliGemma2 3B 448 40 | "paligemma2-10b-224", // PaliGemma2 10B 224 41 | "paligemma2-10b-448", // PaliGemma2 10B 448 42 | }; 43 | 44 | // Corresponding model types for each flag 45 | private static readonly GemmaModelType[] ModelTypes = new[] 46 | { 47 | GemmaModelType.Gemma2B, GemmaModelType.Gemma2B, // Gemma 2B 48 | GemmaModelType.Gemma7B, GemmaModelType.Gemma7B, // Gemma 7B 49 | GemmaModelType.Griffin2B, GemmaModelType.Griffin2B, // RecurrentGemma 50 | GemmaModelType.GemmaTiny, // Gemma Tiny 51 | GemmaModelType.Gemma2_2B, GemmaModelType.Gemma2_2B, // Gemma2 2B 52 | GemmaModelType.Gemma2_9B, GemmaModelType.Gemma2_9B, // Gemma2 9B 53 | GemmaModelType.Gemma2_27B, GemmaModelType.Gemma2_27B, // Gemma2 27B 54 | GemmaModelType.Gemma3_4B, // Gemma3 4B 55 | GemmaModelType.Gemma3_1B, // Gemma3 1B 56 | GemmaModelType.Gemma3_12B, // Gemma3 12B 57 | GemmaModelType.Gemma3_27B, 58 | GemmaModelType.PaliGemma224, // PaliGemma 224 59 | GemmaModelType.PaliGemma2_3B_224, // PaliGemma2 3B 224 60 | GemmaModelType.PaliGemma2_3B_448, // PaliGemma2 3B 448 61 | GemmaModelType.PaliGemma2_10B_224, // PaliGemma2 10B 224 62 | GemmaModelType.PaliGemma2_10B_448, // PaliGemma2 10B 448 63 | }; 64 | 65 | // Cache the mapping for faster lookups 66 | private static readonly Dictionary FlagToModelType; 67 | private static readonly Dictionary> ModelTypeToFlags; 68 | 69 | static GemmaModelUtils() 70 | { 71 | FlagToModelType = new Dictionary(); 72 | ModelTypeToFlags = new Dictionary> 73 | { 74 | }; 75 | 76 | // Build the mappings 77 | for (int i = 0; i < ModelFlags.Length; i++) 78 | { 79 | FlagToModelType[ModelFlags[i]] = ModelTypes[i]; 80 | 81 | if (!ModelTypeToFlags.ContainsKey(ModelTypes[i])) 82 | { 83 | ModelTypeToFlags[ModelTypes[i]] = new List(); 84 | } 85 | ModelTypeToFlags[ModelTypes[i]].Add(ModelFlags[i]); 86 | } 87 | } 88 | 89 | public static GemmaModelType GetModelTypeFromFlag(string flag) 90 | { 91 | return FlagToModelType.TryGetValue(flag, out var modelType) 92 | ? modelType 93 | : GemmaModelType.Unknown; 94 | } 95 | 96 | public static List GetFlagsForModelType(GemmaModelType modelType) 97 | { 98 | return ModelTypeToFlags.TryGetValue(modelType, out var flags) 99 | ? flags 100 | : new List(); 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /Runtime/Scripts/GemmaModelUtils.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 507b8d7329569b14ea5a47f72a19119e -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google/conduct/). 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.google.gemma.cpp", 3 | "version": "1.0.5", 4 | "displayName": "Gemma.cpp for Unity", 5 | "description": "Unity integration for Gemma.cpp", 6 | "unity": "2022.3", 7 | "dependencies": { 8 | "com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask#2.5.10" 9 | }, 10 | "keywords": [ 11 | "gemma", 12 | "llm", 13 | "ai" 14 | ], 15 | "author": { 16 | "name": "Google", 17 | "url": "https://github.com/google/gemma-cpp-unity-plugin" 18 | } 19 | } -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 14dc3d3a5cbf4f6448e201a0e3da85eb 3 | PackageManifestImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------