diff --git a/.github/LLMCharacter_GameObject.png b/.github/LLMCharacter_GameObject.png index 59ead911..1cba47a4 100644 Binary files a/.github/LLMCharacter_GameObject.png and b/.github/LLMCharacter_GameObject.png differ diff --git a/.github/LLM_GameObject.png b/.github/LLM_GameObject.png index 696c7086..91391e8a 100644 Binary files a/.github/LLM_GameObject.png and b/.github/LLM_GameObject.png differ diff --git a/.github/LLM_manager.png b/.github/LLM_manager.png new file mode 100644 index 00000000..5dbcb829 Binary files /dev/null and b/.github/LLM_manager.png differ diff --git a/.github/LLM_manager_expanded.png b/.github/LLM_manager_expanded.png new file mode 100644 index 00000000..1b6bde23 Binary files /dev/null and b/.github/LLM_manager_expanded.png differ diff --git a/.github/android.png b/.github/android.png new file mode 100644 index 00000000..f41c388f Binary files /dev/null and b/.github/android.png differ diff --git a/.github/doxygen/Doxyfile b/.github/doxygen/Doxyfile index f0f3fa3a..fc99709d 100644 --- a/.github/doxygen/Doxyfile +++ b/.github/doxygen/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = "LLM for Unity" # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = v2.0.3 +PROJECT_NUMBER = v2.1.0 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/CHANGELOG.md b/CHANGELOG.md index d45dc1d2..f10a04b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## v2.1.0 +#### ๐Ÿš€ Features + +- Android deployment (PR: #194) +- Allow to download models on startup with resumable download functionality (PR: #196) +- LLM model manager (PR: #196) +- Add Llama 3 7B and Qwen2 0.5B models (PR: #198) +- Start LLM always asynchronously (PR: #199) +- Add contributing guidelines (PR: #201) + ## v2.0.3 #### ๐Ÿš€ Features diff --git a/CHANGELOG.release.md b/CHANGELOG.release.md index f692fe7f..34a829c4 100644 --- a/CHANGELOG.release.md +++ b/CHANGELOG.release.md @@ -1,15 +1,8 @@ ### ๐Ÿš€ Features -- Add LLM selector in Inspector mode (PR: #182) -- Allow to save chat history at custom path (PR: #179) -- Use asynchronous startup by default (PR: #186) -- Assign LLM if not set according to the scene and hierarchy (PR: #187) -- Allow to set log level (PR: #189) -- Allow to add callback functions for error messages (PR: #190) -- Allow to set a LLM base prompt for all LLMCharacter objects (PR: #192) - -### ๐Ÿ› Fixes - -- set higher priority for mac build with Accelerate than without (PR: #180) -- Fix duplicate bos warning - +- Android deployment (PR: #194) +- Allow to download models on startup with resumable download functionality (PR: #196) +- LLM model manager (PR: #196) +- Add Llama 3 7B and Qwen2 0.5B models (PR: #198) +- Start LLM always asynchronously (PR: #199) +- Add contributing guidelines (PR: #201) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..c0a62ae6 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting a project team member. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md.meta b/CODE_OF_CONDUCT.md.meta new file mode 100644 index 00000000..927b2541 --- /dev/null +++ b/CODE_OF_CONDUCT.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: df335783a8d8a4e53bbe626c147d010f +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..33fa04c1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,133 @@ +# Contributing to LLMUnity + +:+1: :tada: :heart: Thanks for your interest! :heart: :tada: :+1: + +The following is a set of guidelines for contributing to [LLMUnity](https://github.com/undreamai/LLMUnity). These are just guidelines, not rules. Use your best judgment, and +feel free to propose changes to this document in a pull request. + +#### Table Of Contents + +[How Can I Contribute?](#how-can-i-contribute) + * [Code of Conduct](#code-of-conduct) + * [Set up your dev environment](#set-up-your-dev-environment) + * [Reporting Bugs](#reporting-bugs) + * [Suggesting Enhancements](#suggesting-enhancements) + * [Good First Issue](#good-first-issue) + * [Issue and Pull Request Labels](#issue-and-pull-request-labels) + + +## How Can I Contribute? + +### Code of Conduct + +This project adheres to the Contributor Covenant [code of conduct](CODE_OF_CONDUCT.md). +By participating, you are expected to uphold this code. + +### Set up your dev environment + + +1. Fork the repo. +2. Clone your forked repo into a Unity project's `Assets`. +3. Create a symbolic link to `Samples~`, for example with: + ```bash + cd Assets && ln -s ./LLMUnity/Samples~ ./Samples + ``` +4. Add the package to your projects libraries `Packages/manifest.json`: + ```json + "ai.undream.llm": "file:path/to/project/Assets/LLMUnity", + ``` +5. Create a topic branch from where you want to base your work. +Name your branch prefixed with an appropriate [label](https://github.com/undreamai/LLMUnity/labels), following the naming convention `enhancement/*`, `bug/*`, `documentation/*`, etc. Make commits of logical units. +6. Set up pre-commit hooks with `sh ./.github/setup.sh` + + +### Reporting Bugs + +This section guides you through submitting a bug report for LLMUnity. +Following these guidelines helps maintainers and the community understand your +report :pencil:, reproduce the behavior :computer:, and find related +reports :mag_right:. + +Before creating bug reports, please check [this section](#before-submitting-a-bug-report) +as you might find out that you don't need to create one. When you are creating +a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report) as it helps us resolve issues faster. + +#### Before Submitting A Bug Report + +**Perform a [cursory search](https://github.com/undreamai/LLMUnity/labels/bug)** +to see if the problem has already been reported. If it does exist, add a +[reaction](https://help.github.com/articles/about-discussions-in-issues-and-pull-requests/#reacting-to-ideas-in-issues-and-pull-requests) +to the issue to indicate this is also an issue for you, and add a +comment to the existing issue if there is extra information you can contribute. + +#### How Do I Submit A (Good) Bug Report? + +Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/). + +Simply create an issue on the [LLMUnity issue tracker](https://github.com/undreamai/LLMUnity/issues), choose the appropriate provided issue template and fill it out. + +The information we are interested in includes: + + - details about your environment - which build, which operating system + - details about reproducing the issue - what steps to take, what happens, how + often it happens + - other relevant information - log files, screenshots, etc. + +### Suggesting Enhancements + +This section guides you through submitting an enhancement suggestion for +LLMUnity, including completely new features and minor improvements to +existing functionality. Following these guidelines helps maintainers and the +community understand your suggestion :pencil: and find related suggestions +:mag_right:. + +Before creating enhancement suggestions, please check [this section](#before-submitting-an-enhancement-suggestion) +as you might find out that you don't need to create one. When you are creating +an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-a-good-enhancement-suggestion). + +#### Before Submitting An Enhancement Suggestion + +**Perform a [cursory search](https://github.com/undreamai/LLMUnity/labels/enhancement)** +to see if the enhancement has already been suggested. If it has, add a +:thumbsup: to indicate your interest in it, or comment if there is additional +information you would like to add. + +#### How Do I Submit A (Good) Enhancement Suggestion? + +Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). + +Simply create an issue on the [LLMUnity issue tracker](https://github.com/undreamai/LLMUnity/issues), choose the appropriate provided issue template and fill it out and provide the following information: + +* **Use a clear and descriptive title** for the issue to identify the + suggestion. +* **Provide a step-by-step description of the suggested enhancement** in as + much detail as possible. This additional context helps the maintainers to + understand the enhancement from your perspective +* **Explain why this enhancement would be useful** to LLMUnity users. +* **Include screenshots and animated GIFs** if relevant to help you demonstrate + the steps or point out the part of LLMUnity which the suggestion is + related to. You can use [this tool](http://www.cockos.com/licecap/) to record + GIFs on macOS and Windows. +* **List some other applications where this enhancement exists, if applicable.** + +### Good First Issue + +We'll identify enhancements or bugs that can be categorized as tasks that: + + - have low impact, or have a known workaround + - should be fixed + - have a narrow scope and/or easy reproduction steps + - can be worked on independent of other tasks + +These issues will be labelled as [`good-first-issue`](https://github.com/undreamai/LLMUnity/labels/good%20first%20issue) +in the repository. If you are interested in contributing to the project, please +comment on the issue to let the maintainers (and community) know you are +interested in picking this up. + +### Issue and Pull Request Labels + +See [this page](https://github.com/undreamai/LLMUnity/labels) for the list of the labels we use to help us track and manage issues and pull requests. + + + + diff --git a/CONTRIBUTING.md.meta b/CONTRIBUTING.md.meta new file mode 100644 index 00000000..a5b77518 --- /dev/null +++ b/CONTRIBUTING.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 0cf827c3ba62e4d598d999b5d39345b5 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/LLMBuildProcessor.cs b/Editor/LLMBuildProcessor.cs index d0362ead..23165cbf 100644 --- a/Editor/LLMBuildProcessor.cs +++ b/Editor/LLMBuildProcessor.cs @@ -2,135 +2,58 @@ using UnityEditor.Build; using UnityEditor.Build.Reporting; using UnityEngine; -using System.IO; -using System.Collections.Generic; -using System; namespace LLMUnity { public class LLMBuildProcessor : IPreprocessBuildWithReport, IPostprocessBuildWithReport { public int callbackOrder => 0; - static string tempDir = Path.Combine(Application.temporaryCachePath, "LLMBuildProcessor", Path.GetFileName(LLMUnitySetup.libraryPath)); - static string foldersMovedCache = Path.Combine(tempDir, "moved.json"); - [InitializeOnLoadMethod] - private static void InitializeOnLoad() - { - if (!Directory.Exists(tempDir)) Directory.CreateDirectory(tempDir); - else ResetLibraryPlatforms(); - } - - // CALLED BEFORE THE BUILD + // called before the build public void OnPreprocessBuild(BuildReport report) { - // Start listening for errors when build starts Application.logMessageReceived += OnBuildError; - List platforms = GetLibraryPlatformsToHide(report.summary.platform); - HideLibraryPlatforms(platforms); - } - - // CALLED DURING BUILD TO CHECK FOR ERRORS - private void OnBuildError(string condition, string stacktrace, LogType type) - { - if (type == LogType.Error) - { - // FAILED TO BUILD, STOP LISTENING FOR ERRORS - BuildCompleted(); - } - } - - // CALLED AFTER THE BUILD - public void OnPostprocessBuild(BuildReport report) - { - BuildCompleted(); - } - - public void BuildCompleted() - { - Application.logMessageReceived -= OnBuildError; - ResetLibraryPlatforms(); - } - - static List GetLibraryPlatformsToHide(BuildTarget platform) - { - List platforms = new List(){ "windows", "macos", "linux" }; - switch (platform) + string platform = null; + switch (report.summary.platform) { case BuildTarget.StandaloneWindows: case BuildTarget.StandaloneWindows64: - platforms.Remove("windows"); + platform = "windows"; break; case BuildTarget.StandaloneLinux64: - platforms.Remove("linux"); + platform = "linux"; break; case BuildTarget.StandaloneOSX: - platforms.Remove("macos"); + platform = "macos"; + break; + case BuildTarget.Android: + platform = "android"; + break; + case BuildTarget.iOS: + platform = "ios"; break; } - return platforms; + LLMBuilder.HideLibraryPlatforms(platform); + LLMBuilder.BuildModels(); + AssetDatabase.Refresh(); } - static bool MovePath(string source, string target, List foldersMoved = null) + // called during build to check for errors + private void OnBuildError(string condition, string stacktrace, LogType type) { - bool moved = false; - if (File.Exists(source)) - { - File.Move(source, target); - moved = true; - } - else if (Directory.Exists(source)) - { - Directory.Move(source, target); - moved = true; - } - if (moved && foldersMoved != null) foldersMoved.Add(new FoldersMovedPair {source = source, target = target}); - return moved; + if (type == LogType.Error) BuildCompleted(); } - static void HideLibraryPlatforms(List platforms) + // called after the build + public void OnPostprocessBuild(BuildReport report) { - List foldersMoved = new List(); - foreach (string dirname in Directory.GetDirectories(LLMUnitySetup.libraryPath)) - { - foreach (string platform in platforms) - { - if (Path.GetFileName(dirname).StartsWith(platform)) - { - string movePath = Path.Combine(tempDir, Path.GetFileName(dirname)); - MovePath(dirname + ".meta", movePath + ".meta", foldersMoved); - MovePath(dirname, movePath, foldersMoved); - File.WriteAllText(foldersMovedCache, JsonUtility.ToJson(new FoldersMovedWrapper { foldersMoved = foldersMoved })); - } - } - } - if (foldersMoved.Count > 0) AssetDatabase.Refresh(); + BuildCompleted(); } - static void ResetLibraryPlatforms() + public void BuildCompleted() { - if (!File.Exists(foldersMovedCache)) return; - List foldersMoved = JsonUtility.FromJson(File.ReadAllText(foldersMovedCache)).foldersMoved; - if (foldersMoved == null) return; - - bool refresh = false; - foreach (var pair in foldersMoved) refresh |= MovePath(pair.target, pair.source); - if (refresh) AssetDatabase.Refresh(); - - File.Delete(foldersMovedCache); + Application.logMessageReceived -= OnBuildError; + LLMBuilder.Reset(); } } - - [Serializable] - public struct FoldersMovedPair - { - public string source; - public string target; - } - - [Serializable] - public class FoldersMovedWrapper - { - public List foldersMoved; - } } diff --git a/Editor/LLMEditor.cs b/Editor/LLMEditor.cs index 39e30dc9..bebaf578 100644 --- a/Editor/LLMEditor.cs +++ b/Editor/LLMEditor.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using UnityEditor; +using UnityEditorInternal; using UnityEngine; namespace LLMUnity @@ -9,6 +11,24 @@ namespace LLMUnity [CustomEditor(typeof(LLM))] public class LLMEditor : PropertyEditor { + private ReorderableList modelList; + static float nameColumnWidth = 150f; + static float templateColumnWidth = 150f; + static float textColumnWidth = 150f; + static float includeInBuildColumnWidth = 30f; + static float actionColumnWidth = 20f; + static int elementPadding = 10; + static GUIContent trashIcon; + static List modelOptions; + static List modelLicenses; + static List modelURLs; + string elementFocus = ""; + bool showCustomURL = false; + string customURL = ""; + bool customURLLora = false; + bool customURLFocus = false; + bool expandedView = false; + protected override Type[] GetPropertyTypes() { return new Type[] { typeof(LLM) }; @@ -18,24 +38,177 @@ public void AddModelLoadersSettings(SerializedObject llmScriptSO, LLM llmScript) { EditorGUILayout.LabelField("Model Settings", EditorStyles.boldLabel); AddModelLoaders(llmScriptSO, llmScript); - AddModelAddonLoaders(llmScriptSO, llmScript); AddModelSettings(llmScriptSO); } public void AddModelLoaders(SerializedObject llmScriptSO, LLM llmScript) { - EditorGUILayout.BeginHorizontal(); + if (LLMManager.modelEntries.Count > 0) + { + float[] widths = GetColumnWidths(expandedView); + float listWidth = 2 * ReorderableList.Defaults.padding; + foreach (float width in widths) listWidth += width + (listWidth == 0 ? 0 : elementPadding); + EditorGUILayout.BeginHorizontal(GUILayout.Width(listWidth + actionColumnWidth)); + + EditorGUILayout.BeginVertical(GUILayout.Width(listWidth)); + modelList.DoLayoutList(); + EditorGUILayout.EndVertical(); + + Rect expandedRect = GUILayoutUtility.GetRect(actionColumnWidth, modelList.elementHeight + ReorderableList.Defaults.padding); + expandedRect.y += modelList.GetHeight() - modelList.elementHeight - ReorderableList.Defaults.padding; + if (GUI.Button(expandedRect, expandedView ? "ยซ" : "ยป")) + { + expandedView = !expandedView; + Repaint(); + } + EditorGUILayout.EndHorizontal(); + } + _ = AddLoadButtons(); + bool downloadOnStart = EditorGUILayout.Toggle("Download on Start", LLMManager.downloadOnStart); + if (downloadOnStart != LLMManager.downloadOnStart) LLMManager.SetDownloadOnStart(downloadOnStart); + } + + public void AddModelSettings(SerializedObject llmScriptSO) + { + List attributeClasses = new List { typeof(ModelAttribute) }; + if (llmScriptSO.FindProperty("advancedOptions").boolValue) + { + attributeClasses.Add(typeof(ModelAdvancedAttribute)); + } + ShowPropertiesOfClass("", llmScriptSO, attributeClasses, false); + Space(); + } + + void ShowProgress(float progress, string progressText) + { + if (progress != 1) EditorGUI.ProgressBar(EditorGUILayout.GetControlRect(), progress, progressText); + } + + static void ResetModelOptions() + { + List existingOptions = new List(); + foreach (ModelEntry entry in LLMManager.modelEntries) existingOptions.Add(entry.url); + modelOptions = new List(){"Download model", "Custom URL"}; + modelURLs = new List(){null, null}; + modelLicenses = new List(){null, null}; + foreach ((string name, string url, string license) in LLMUnitySetup.modelOptions) + { + if (url != null && existingOptions.Contains(url)) continue; + modelOptions.Add(name); + modelURLs.Add(url); + modelLicenses.Add(license); + } + } + + float[] GetColumnWidths(bool expandedView) + { + List widths = new List(){actionColumnWidth, nameColumnWidth, templateColumnWidth}; + if (expandedView) widths.AddRange(new List(){textColumnWidth, textColumnWidth}); + widths.AddRange(new List(){includeInBuildColumnWidth, actionColumnWidth}); + return widths.ToArray(); + } - string[] options = new string[LLMUnitySetup.modelOptions.Length]; - for (int i = 0; i < LLMUnitySetup.modelOptions.Length; i++) + List CreateColumnRects(Rect rect) + { + float[] widths = GetColumnWidths(expandedView); + float offsetX = rect.x; + float offsetY = rect.y + (rect.height - EditorGUIUtility.singleLineHeight) / 2; + List rects = new List(); + foreach (float width in widths) + { + rects.Add(new Rect(offsetX, offsetY, width, EditorGUIUtility.singleLineHeight)); + offsetX += width + elementPadding; + } + return rects; + } + + void UpdateModels(bool resetOptions = false) + { + if (resetOptions) ResetModelOptions(); + Repaint(); + } + + void showCustomURLField(bool lora) + { + customURL = ""; + customURLLora = lora; + showCustomURL = true; + customURLFocus = true; + Repaint(); + } + + void SetModelIfNone(string filename, bool lora) + { + LLM llmScript = (LLM)target; + int num = LLMManager.Num(lora); + if (!lora && llmScript.model == "" && num == 1) llmScript.SetModel(filename); + if (lora && llmScript.lora == "" && num == 1) llmScript.SetLora(filename); + } + + async Task createCustomURLField() + { + bool submit = false; + bool exit = false; + Event e = Event.current; + if (e.type == EventType.KeyDown && (e.keyCode == KeyCode.Return || e.keyCode == KeyCode.KeypadEnter)) + { + submit = true; + e.Use(); + } + else if (e.type == EventType.KeyDown && (e.keyCode == KeyCode.Escape)) { - options[i] = LLMUnitySetup.modelOptions[i].Item1; + exit = true; + e.Use(); } + else + { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Enter URL", GUILayout.Width(100)); + GUI.SetNextControlName("customURLFocus"); + customURL = EditorGUILayout.TextField(customURL, GUILayout.Width(buttonWidth)); + submit = GUILayout.Button("Submit", GUILayout.Width(buttonWidth / 2)); + exit = GUILayout.Button("Back", GUILayout.Width(buttonWidth / 2)); + EditorGUILayout.EndHorizontal(); - int newIndex = EditorGUILayout.Popup("Model", llmScript.SelectedModel, options); - if (newIndex != llmScript.SelectedModel) + if (customURLFocus) + { + customURLFocus = false; + elementFocus = "customURLFocus"; + } + } + + if (exit || submit) { - LLMUnitySetup.DownloadModel(llmScript, newIndex); + showCustomURL = false; + elementFocus = "dummy"; + Repaint(); + if (submit && customURL != "") + { + string filename = await LLMManager.Download(customURL, customURLLora, true); + SetModelIfNone(filename, customURLLora); + UpdateModels(true); + } + } + } + + async Task createButtons() + { + LLM llmScript = (LLM)target; + EditorGUILayout.BeginHorizontal(); + + GUIStyle centeredPopupStyle = new GUIStyle(EditorStyles.popup); + centeredPopupStyle.alignment = TextAnchor.MiddleCenter; + int modelIndex = EditorGUILayout.Popup(0, modelOptions.ToArray(), centeredPopupStyle, GUILayout.Width(buttonWidth)); + if (modelIndex == 1) + { + showCustomURLField(false); + } + else if (modelIndex > 1) + { + if (modelLicenses[modelIndex] != null) Debug.LogWarning($"The {modelOptions[modelIndex]} model is released under the following license: {modelLicenses[modelIndex]}. By using this model, you agree to the terms of the license."); + string filename = await LLMManager.DownloadModel(modelURLs[modelIndex], true, modelOptions[modelIndex]); + SetModelIfNone(filename, false); + UpdateModels(true); } if (GUILayout.Button("Load model", GUILayout.Width(buttonWidth))) @@ -45,37 +218,31 @@ public void AddModelLoaders(SerializedObject llmScriptSO, LLM llmScript) string path = EditorUtility.OpenFilePanelWithFilters("Select a gguf model file", "", new string[] { "Model Files", "gguf" }); if (!string.IsNullOrEmpty(path)) { - llmScript.SelectedModel = 0; - llmScript.SetModel(path); + string filename = LLMManager.LoadModel(path, true); + SetModelIfNone(filename, false); + UpdateModels(); } }; } EditorGUILayout.EndHorizontal(); - string[] templateOptions = ChatTemplate.templatesDescription.Keys.ToList().ToArray(); - int index = Array.IndexOf(ChatTemplate.templatesDescription.Values.ToList().ToArray(), llmScript.chatTemplate); - newIndex = EditorGUILayout.Popup("Chat Template", index, templateOptions); - if (newIndex != index) - { - llmScript.SetTemplate(ChatTemplate.templatesDescription[templateOptions[newIndex]]); - } - } - - public void AddModelAddonLoaders(SerializedObject llmScriptSO, LLM llmScript, bool layout = true) - { - if (llmScriptSO.FindProperty("advancedOptions").boolValue) + if (llmScript.advancedOptions) { EditorGUILayout.BeginHorizontal(); - GUILayout.Label("Lora", GUILayout.Width(EditorGUIUtility.labelWidth)); - - if (GUILayout.Button("Load lora", GUILayout.Width(buttonWidth))) + if (GUILayout.Button("Download LoRA", GUILayout.Width(buttonWidth))) + { + showCustomURLField(true); + } + if (GUILayout.Button("Load LoRA", GUILayout.Width(buttonWidth))) { EditorApplication.delayCall += () => { string path = EditorUtility.OpenFilePanelWithFilters("Select a bin lora file", "", new string[] { "Model Files", "bin" }); if (!string.IsNullOrEmpty(path)) { - llmScript.SetLora(path); + string filename = LLMManager.LoadLora(path, true); + SetModelIfNone(filename, true); + UpdateModels(); } }; } @@ -83,38 +250,173 @@ public void AddModelAddonLoaders(SerializedObject llmScriptSO, LLM llmScript, bo } } - public void AddModelSettings(SerializedObject llmScriptSO) + async Task AddLoadButtons() { - List attributeClasses = new List { typeof(ModelAttribute) }; - if (llmScriptSO.FindProperty("advancedOptions").boolValue) + if (showCustomURL) await createCustomURLField(); + else await createButtons(); + } + + void OnEnable() + { + LLM llmScript = (LLM)target; + ResetModelOptions(); + trashIcon = new GUIContent(Resources.Load("llmunity_trash_icon"), "Delete Model"); + Texture2D loraLineTexture = new Texture2D(1, 1); + loraLineTexture.SetPixel(0, 0, Color.black); + loraLineTexture.Apply(); + + modelList = new ReorderableList(LLMManager.modelEntries, typeof(ModelEntry), false, true, false, false) { - attributeClasses.Add(typeof(ModelAdvancedAttribute)); + drawElementCallback = (rect, index, isActive, isFocused) => + { + if (index >= LLMManager.modelEntries.Count) return; + ModelEntry entry = LLMManager.modelEntries[index]; + + List rects = CreateColumnRects(rect); + int col = 0; + Rect selectRect = rects[col++]; + Rect nameRect = rects[col++]; + Rect templateRect = rects[col++]; + Rect urlRect = new Rect(); + Rect pathRect = new Rect(); + if (expandedView) + { + urlRect = rects[col++]; + pathRect = rects[col++]; + } + Rect includeInBuildRect = rects[col++]; + Rect actionRect = rects[col++]; + + bool hasPath = entry.path != null && entry.path != ""; + bool hasURL = entry.url != null && entry.url != ""; + + bool isSelected = false; + if (!entry.lora) + { + isSelected = llmScript.model == entry.filename; + bool newSelected = EditorGUI.Toggle(selectRect, isSelected, EditorStyles.radioButton); + if (newSelected && !isSelected) llmScript.SetModel(entry.filename); + } + else + { + isSelected = llmScript.lora == entry.filename; + bool newSelected = EditorGUI.Toggle(selectRect, isSelected, EditorStyles.radioButton); + if (newSelected && !isSelected) llmScript.SetLora(entry.filename); + else if (!newSelected && isSelected) llmScript.SetLora(""); + } + + DrawCopyableLabel(nameRect, entry.label, entry.filename); + + if (!entry.lora) + { + string[] templateDescriptions = ChatTemplate.templatesDescription.Keys.ToList().ToArray(); + string[] templates = ChatTemplate.templatesDescription.Values.ToList().ToArray(); + int templateIndex = Array.IndexOf(templates, entry.chatTemplate); + int newTemplateIndex = EditorGUI.Popup(templateRect, templateIndex, templateDescriptions); + if (newTemplateIndex != templateIndex) + { + LLMManager.SetTemplate(entry.filename, templates[newTemplateIndex]); + UpdateModels(); + } + } + + if (expandedView) + { + if (hasURL) + { + DrawCopyableLabel(urlRect, entry.url); + } + else + { + string newURL = EditorGUI.TextField(urlRect, entry.url); + if (newURL != entry.url) + { + LLMManager.SetURL(entry, newURL); + UpdateModels(); + } + } + DrawCopyableLabel(pathRect, entry.path); + } + + bool includeInBuild = EditorGUI.ToggleLeft(includeInBuildRect, "", entry.includeInBuild); + if (includeInBuild != entry.includeInBuild) + { + LLMManager.SetIncludeInBuild(entry, includeInBuild); + UpdateModels(); + } + + if (GUI.Button(actionRect, trashIcon)) + { + LLMManager.Remove(entry); + UpdateModels(true); + } + + if (!entry.lora && index < LLMManager.modelEntries.Count - 1 && LLMManager.modelEntries[index + 1].lora) + { + GUI.DrawTexture(new Rect(rect.x - ReorderableList.Defaults.padding, rect.yMax, rect.width + ReorderableList.Defaults.padding * 2, 1), loraLineTexture); + } + }, + drawHeaderCallback = (rect) => + { + List rects = CreateColumnRects(rect); + int col = 0; + EditorGUI.LabelField(rects[col++], ""); + EditorGUI.LabelField(rects[col++], "Model"); + EditorGUI.LabelField(rects[col++], "Chat template"); + if (expandedView) + { + EditorGUI.LabelField(rects[col++], "URL"); + EditorGUI.LabelField(rects[col++], "Path"); + } + EditorGUI.LabelField(rects[col++], "Build"); + EditorGUI.LabelField(rects[col++], ""); + }, + drawFooterCallback = {}, + footerHeight = 0, + }; + } + + private void DrawCopyableLabel(Rect rect, string label, string text = "") + { + if (text == "") text = label; + EditorGUI.LabelField(rect, label); + if (Event.current.type == EventType.ContextClick && rect.Contains(Event.current.mousePosition)) + { + GenericMenu menu = new GenericMenu(); + menu.AddItem(new GUIContent("Copy"), false, () => CopyToClipboard(text)); + menu.ShowAsContext(); + Event.current.Use(); } - ShowPropertiesOfClass("", llmScriptSO, attributeClasses, false); - Space(); } - void ShowProgress(float progress, string progressText) + private void CopyToClipboard(string text) { - if (progress != 1) EditorGUI.ProgressBar(EditorGUILayout.GetControlRect(), progress, progressText); + TextEditor te = new TextEditor {text = text}; + te.SelectAll(); + te.Copy(); } public override void OnInspectorGUI() { + if (elementFocus != "") + { + EditorGUI.FocusTextInControl(elementFocus); + elementFocus = ""; + } + LLM llmScript = (LLM)target; SerializedObject llmScriptSO = new SerializedObject(llmScript); OnInspectorGUIStart(llmScriptSO); ShowProgress(LLMUnitySetup.libraryProgress, "Setup Library"); - ShowProgress(llmScript.modelProgress, "Model Downloading"); - ShowProgress(llmScript.modelCopyProgress, "Model Copying"); + ShowProgress(LLMManager.modelProgress, "Model Downloading"); + ShowProgress(LLMManager.loraProgress, "LoRA Downloading"); + GUI.enabled = LLMUnitySetup.libraryProgress == 1 && LLMManager.modelProgress == 1 && LLMManager.loraProgress == 1; - GUI.enabled = LLMUnitySetup.libraryProgress == 1 && llmScript.modelProgress == 1 && llmScript.modelCopyProgress == 1; AddOptionsToggles(llmScriptSO); AddSetupSettings(llmScriptSO); AddModelLoadersSettings(llmScriptSO, llmScript); - GUI.enabled = true; AddChatSettings(llmScriptSO); OnInspectorGUIEnd(llmScriptSO); diff --git a/Editor/PropertyEditor.cs b/Editor/PropertyEditor.cs index 7229804a..87a40938 100644 --- a/Editor/PropertyEditor.cs +++ b/Editor/PropertyEditor.cs @@ -8,7 +8,7 @@ namespace LLMUnity { public class PropertyEditor : Editor { - protected int buttonWidth = 150; + public static int buttonWidth = 150; public void AddScript(SerializedObject llmScriptSO) { @@ -94,10 +94,23 @@ public List GetPropertiesOfClass(SerializedObject so, List attributeClasses, bool addSpace = true) + public void ShowPropertiesOfClass(string title, SerializedObject so, List attributeClasses, bool addSpace = true, List excludeAttributeClasses = null) { // display a property if it belongs to a certain class and/or has a specific attribute class List properties = GetPropertiesOfClass(so, attributeClasses); + if (excludeAttributeClasses != null) + { + List excludeProperties = GetPropertiesOfClass(so, excludeAttributeClasses); + List removeProperties = new List(); + foreach (SerializedProperty excprop in excludeProperties) + { + foreach (SerializedProperty prop in properties) + { + if (prop.displayName == excprop.displayName) removeProperties.Add(prop); + } + } + foreach (SerializedProperty prop in removeProperties) properties.Remove(prop); + } if (properties.Count == 0) return; if (title != "") EditorGUILayout.LabelField(title, EditorStyles.boldLabel); foreach (SerializedProperty prop in properties) @@ -141,7 +154,7 @@ public Attribute GetPropertyAttribute(SerializedProperty prop, Type attributeCla { foreach (Attribute attr in fieldInfo.GetCustomAttributes(attributeClass, true)) { - if (attr.GetType() == attributeClass) + if (attributeClass.IsAssignableFrom(attr.GetType())) return attr; } } diff --git a/README.md b/README.md index 16bf1921..3fcdc83f 100644 --- a/README.md +++ b/README.md @@ -28,13 +28,13 @@ LLM for Unity is built on top of the awesome [llama.cpp](https://github.com/gger Setup  โ€ข  How to use  โ€ข  Examples  โ€ข  -Use your own model  โ€ข  +LLM model management  โ€ข  Options  โ€ข  License ## At a glance -- ๐Ÿ’ป Cross-platform! Windows, Linux and macOS +- ๐Ÿ’ป Cross-platform! Windows, Linux, macOS and Android - ๐Ÿ  Runs locally without internet access. No data ever leaves the game! - โšก Blazing fast inference on CPU and GPU (Nvidia, AMD, Apple Metal) - ๐Ÿค— Supports all major LLM models @@ -47,7 +47,7 @@ LLM for Unity is built on top of the awesome [llama.cpp](https://github.com/gger ## How to help - [โญ Star](https://github.com/undreamai/LLMUnity) the repo, leave us a [review](https://assetstore.unity.com/packages/slug/273604) and spread the word about the project! - Join us at [Discord](https://discord.gg/RwXKQb6zdv) and say hi! -- Submit feature requests or bugs as issues or even submit a PR and become a collaborator +- [Contribute](CONTRIBUTING.md) by submitting feature requests or bugs as issues or even submiting a PR and become a collaborator! ## Games using LLM for Unity - [Verbal Verdict](https://store.steampowered.com/app/2778780/Verbal_Verdict/) @@ -55,6 +55,7 @@ LLM for Unity is built on top of the awesome [llama.cpp](https://github.com/gger - [Nameless Souls of the Void](https://unicorninteractive.itch.io/nameless-souls-of-the-void) - [Murder in Aisle 4](https://roadedlich.itch.io/murder-in-aisle-4) - [Finicky Food Delivery AI](https://helixngc7293.itch.io/finicky-food-delivery-ai) +- [AI Emotional Girlfriend](https://whynames.itch.io/aiemotionalgirlfriend) ## Setup _Method 1: Install using the asset store_ @@ -73,12 +74,12 @@ _Method 2: Install using the GitHub repo:_ First you will setup the LLM for your game ๐ŸŽ: - Create an empty GameObject.
In the GameObject Inspector click `Add Component` and select the LLM script. -- Download one of the default models with the `Download Model` button (~GBs).
Or load your own .gguf model with the `Load model` button (see [Use your own model](#use-your-own-model)). +- Download one of the default models with the `Download Model` button (~GBs).
Or load your own .gguf model with the `Load model` button (see [LLM model management](#llm-model-management)). Then you can setup each of your characters as follows ๐Ÿ™‹โ€โ™€๏ธ: - Create an empty GameObject for the character.
In the GameObject Inspector click `Add Component` and select the LLMCharacter script. -- Select the LLM constructed above in the `LLM` field. -- Define the role of your AI in the `Prompt`. You can also define the name of the AI (`AI Name`) and the player (`Player Name`). +- Define the role of your AI in the `Prompt`. You can define the name of the AI (`AI Name`) and the player (`Player Name`). +- (Optional) Select the LLM constructed above in the `LLM` field if you have more than one LLM GameObjects. You can also adjust the LLM and character settings according to your preference (see [Options](#options)). @@ -131,7 +132,17 @@ That's all โœจ!

You can also: +
+Build a mobile app on Android + +To build an Android app you need to specify the `IL2CPP` scripting backend and the `ARM64` as the target architecture in the player settings.
+These settings can be accessed from the `Edit > Project Settings` menu within the `Player > Other Settings` section.
+ + +It is also a good idea to enable the `Download on Build` option in the LLM GameObject to download the model on launch in order to keep the app size small. + +
Save / Load your chat history @@ -156,7 +167,7 @@ where filename the filename or relative path of your choice. ``` c# void WarmupCompleted(){ // do something when the warmup is complete - Debug.Log("The AI is warm"); + Debug.Log("The AI is nice and ready"); } void Game(){ @@ -227,15 +238,20 @@ public class MyScript : MonoBehaviour async void Start() { - // disable gameObject so that Awake is not called immediately + // disable gameObject so that theAwake is not called immediately gameObject.SetActive(false); // Add an LLM object llm = gameObject.AddComponent(); - // set the model with a path relative to StreamingAssets folder - await llm.SetModel("Phi-3-mini-4k-instruct-q4.gguf"); - // you can also set a lora in a similar fashion - // await llm.SetLora("my-lora.bin"); + // set the model using the filename of the model. + // The model needs to be added to the LLM model manager (see LLM model management) by loading or downloading it. + // Otherwise the model file can be copied directly inside the StreamingAssets folder. + llm.SetModel("Phi-3-mini-4k-instruct-q4.gguf"); + // optional: you can also set a lora in a similar fashion + llm.SetLora("my-lora.bin"); + // optional: you can set the chat template of the model if it is not correctly identified + // You can find a list of chat templates in the ChatTemplate.templates.Keys + llm.SetTemplate("phi-3"); // optional: set number of threads llm.numThreads = -1; // optional: enable GPU by setting the number of model layers to offload to it @@ -250,12 +266,14 @@ public class MyScript : MonoBehaviour // set the AI and player name llmCharacter.AIName = "AI"; llmCharacter.playerName = "Human"; - // optional: set streaming to false to get the complete result in one go + // optional: set streaming to false to get the complete result in one go // llmCharacter.stream = true; - // optional: set a save path + // optional: set a save path // llmCharacter.save = "AICharacter1"; - // optional: set a grammar - // llmCharacter.SetGrammar("json.gbnf"); + // optional: enable the save cache to avoid recomputation when loading a save file (requires ~100 MB) + // llmCharacter.saveCache = true; + // optional: set a grammar + // await llmCharacter.SetGrammar("json.gbnf"); // re-enable gameObject gameObject.SetActive(true); @@ -267,10 +285,10 @@ public class MyScript : MonoBehaviour
Use a remote server -You can also use a remote server that does the processing and implement Characters that interact with it. To do that: +You can use a remote server to carry out the processing and implement characters that interact with it. To do that: - Create a project with a GameObject using the `LLM` script as described above. Enable the `Remote` option and optionally configure the port. - Create a second project with the game characters using the `LLMCharacter` script as described above. - Enable the `Remote` option and configure the host and port with the IP address (starting with "http://") and port of the server. + Enable the `Remote` option and configure the host with the IP address (starting with "http://") and port of the server.
@@ -283,6 +301,7 @@ The [Samples~](Samples~) folder contains several examples of interaction ๐Ÿค–: - [MultipleCharacters](Samples~/MultipleCharacters): Demonstrates a simple interaction using multiple AI characters - [KnowledgeBaseGame](Samples~/KnowledgeBaseGame): Simple detective game using a knowledge base to provide information to the LLM based on [google/mysteryofthreebots](https://github.com/google/mysteryofthreebots) - [ChatBot](Samples~/ChatBot): Demonstrates interaction between a player and a AI with a UI similar to a messaging app (see image below) +- [AndroidDemo](Samples~/AndroidDemo): Example Android app with an initial screen with model download progress @@ -291,16 +310,32 @@ To install a sample: - Select the `LLM for Unity` Package. From the `Samples` Tab, click `Import` next to the sample you want to install. The samples can be run with the `Scene.unity` scene they contain inside their folder.
-In the scene, select the `LLM` GameObject and click the `Download Model` button to download the default model.
-You can also load your own model in .gguf format with the `Load model` button (see [Use your own model](#use-your-own-model)).
+In the scene, select the `LLM` GameObject and click the `Download Model` button to download a default model or `Load model` to load your own model (see [LLM model management](#llm-model-management)).
Save the scene, run and enjoy! -## Use your own model -LLM for Unity uses the [Mistral 7B Instruct](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.2), [OpenHermes 2.5](https://huggingface.co/teknium/OpenHermes-2.5-Mistral-7B) or [Microsoft Phi-3](https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-gguf) model by default, quantised with the Q4 method.
- -Alternative models can be downloaded from [HuggingFace](https://huggingface.co/models?library=gguf&sort=downloads).
-The required model format is .gguf as defined by the llama.cpp.
-HuggingFace models can be converted to gguf with this [online converter](https://huggingface.co/spaces/ggml-org/gguf-my-repo).
+## LLM model management +LLM for Unity implements a model manager that allows to load or download LLMs and ship them directly in your game.
+The model manager can be found as part of the LLM GameObject:
+ + +You can download models with the `Download model` button.
+LLM for Unity includes different state of the art models built-in for different model sizes, quantised with the Q4_K_M method.
+Alternative models can be downloaded from [HuggingFace](https://huggingface.co/models?library=gguf&sort=downloads) in the .gguf format.
+You can download a model locally and load it with the `Load model` button, or copy the URL in the `Download model > Custom URL` field to directly download it.
+If a HuggingFace model does not provide a gguf file, it can be converted to gguf with this [online converter](https://huggingface.co/spaces/ggml-org/gguf-my-repo).
+ +The chat template used for constructing the prompts is determined automatically from the model (if a relevant entry exists) or the model name.
+If incorrecly identified, you can select another template from the chat template dropdown.
+
+Models added in the model manager are copied to the game during the building process.
+You can omit a model from being built in by deselecting the "Build" checkbox.
+To remove the model (but not delete it from disk) you can click the bin button.
+The the path and URL (if downloaded) of each added model is diplayed in the expanded view of the model manager access with the `>>` button:
+ + +You can create lighter builds by selecting the `Download on Build` option.
+Using this option the models will be downloaded the first time the game starts instead of copied in the build.
+If you have loaded a model locally you need to set its URL through the expanded view, otherwise it will be copied in the build.
โ• Before using any model make sure you **check their license** โ• @@ -335,14 +370,17 @@ If the user's GPU is not supported, the LLM will fall back to the CPU #### ๐Ÿค— Model Settings - `Download model` click to download one of the default models - `Load model` click to load your own model in .gguf format -- `Model` the path of the model being used (relative to the Assets/StreamingAssets folder) --
Chat Template the chat template to use for constructing the prompts The chat template is determined automatically by the chat template of the model (if it exists) or the model name.
The "chatml" template works with most of the models.
+- `Download on Start` enable to downloaded the LLM models the first time the game starts. Alternatively the LLM models wil be copied directly in the build + -
Advanced options + - `Download lora` click to download a LoRA model in .bin format - `Load lora` click to load a LoRA model in .bin format - - `Lora` the path of the LoRA being used (relative to the Assets/StreamingAssets folder) -
Context Size size of the prompt context (0 = context size of the model) This is the number of tokens the model can take as input when generating responses. Higher values use more RAM or VRAM (if using GPU).
- `Batch Size` batch size for prompt processing (default: 512) + - `Model` the path of the model being used (relative to the Assets/StreamingAssets folder) + - `Chat Template` the chat template being used for the LLM + - `Lora` the path of the LoRA being used (relative to the Assets/StreamingAssets folder)
diff --git a/Resources.meta b/Resources.meta new file mode 100644 index 00000000..cd69ccb5 --- /dev/null +++ b/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 688bae55bf18bd75dbc7fee333923c15 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Resources/llmunity_trash_icon.png b/Resources/llmunity_trash_icon.png new file mode 100644 index 00000000..7457cc94 Binary files /dev/null and b/Resources/llmunity_trash_icon.png differ diff --git a/Resources/llmunity_trash_icon.png.meta b/Resources/llmunity_trash_icon.png.meta new file mode 100644 index 00000000..9b334b18 --- /dev/null +++ b/Resources/llmunity_trash_icon.png.meta @@ -0,0 +1,140 @@ +fileFormatVersion: 2 +guid: 0e04eced3ed2d120e84e7c10c8b32ddc +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/LLM.cs b/Runtime/LLM.cs index 74e51e33..2f4c57cf 100644 --- a/Runtime/LLM.cs +++ b/Runtime/LLM.cs @@ -45,35 +45,8 @@ public class LLM : MonoBehaviour [LLM] public bool debug = false; /// number of prompts that can happen in parallel (-1 = number of LLMCharacter objects) [LLMAdvanced] public int parallelPrompts = -1; - /// allows to start the server asynchronously. - /// This is useful to not block Unity while the server is initialised. - /// For example it can be used as follows: - /// \code - /// void Start(){ - /// StartCoroutine(Loading()); - /// ... - /// } - /// - /// IEnumerator Loading() - /// { - /// // show loading screen - /// while (!llm.started) - /// { - /// yield return null; - /// } - /// Debug.Log("Server is ready"); - /// } - /// \endcode - /// - [LLMAdvanced] public bool asynchronousStartup = true; /// select to not destroy the LLM GameObject when loading a new Scene. [LLMAdvanced] public bool dontDestroyOnLoad = true; - /// the path of the model being used (relative to the Assets/StreamingAssets folder). - /// Models with .gguf format are allowed. - [Model] public string model = ""; - /// the path of the LORA model being used (relative to the Assets/StreamingAssets folder). - /// Models with .bin format are allowed. - [ModelAdvanced] public string lora = ""; /// Size of the prompt context (0 = context size of the model). /// This is the number of tokens the model can take as input when generating responses. [ModelAdvanced] public int contextSize = 0; @@ -81,19 +54,25 @@ public class LLM : MonoBehaviour [ModelAdvanced] public int batchSize = 512; /// a base prompt to use as a base for all LLMCharacter objects [TextArea(5, 10), ChatAdvanced] public string basePrompt = ""; - /// Boolean set to true if the server has started and is ready to receive requests, false otherwise. public bool started { get; protected set; } = false; /// Boolean set to true if the server has failed to start. public bool failed { get; protected set; } = false; + /// Boolean set to true if the models were not downloaded successfully. + public static bool modelSetupFailed { get; protected set; } = false; + /// Boolean set to true if the server has started and is ready to receive requests, false otherwise. + public static bool modelSetupComplete { get; protected set; } = false; - /// \cond HIDE - public int SelectedModel = 0; - [HideInInspector] public float modelProgress = 1; - [HideInInspector] public float modelCopyProgress = 1; - [HideInInspector] public bool modelHide = true; + /// the LLM model to use. + /// Models with .gguf format are allowed. + [ModelAdvanced] public string model = ""; + /// Chat template used for the model + [ModelAdvanced] public string chatTemplate = ChatTemplate.DefaultTemplate; + /// the path of the LORA model being used (relative to the Assets/StreamingAssets folder). + /// Models with .bin format are allowed. + [ModelAdvanced] public string lora = ""; - public string chatTemplate = ChatTemplate.DefaultTemplate; + /// \cond HIDE IntPtr LLMObject = IntPtr.Zero; List clients = new List(); @@ -101,38 +80,100 @@ public class LLM : MonoBehaviour StreamWrapper logStreamWrapper = null; Thread llmThread = null; List streamWrappers = new List(); + public LLMManager llmManager = new LLMManager(); - public void SetModelProgress(float progress) + /// \endcond + + public LLM() { - modelProgress = progress; + LLMManager.Register(this); } - /// \endcond - - async Task CopyAsset(string path) + /// + /// The Unity Awake function that starts the LLM server. + /// The server can be started asynchronously if the asynchronousStartup option is set. + /// + public async void Awake() { -#if UNITY_EDITOR - if (!EditorApplication.isPlaying) + if (!enabled) return; +#if !UNITY_EDITOR + modelSetupFailed = !await LLMManager.Setup(); +#endif + modelSetupComplete = true; + if (modelSetupFailed) { - modelCopyProgress = 0; - path = await LLMUnitySetup.AddAsset(path, LLMUnitySetup.GetAssetPath()); - modelCopyProgress = 1; + failed = true; + return; } -#endif + string arguments = GetLlamaccpArguments(); + if (arguments == null) + { + failed = true; + return; + } + await Task.Run(() => StartLLMServer(arguments)); + if (dontDestroyOnLoad) DontDestroyOnLoad(transform.root.gameObject); + if (basePrompt != "") await SetBasePrompt(basePrompt); + } + + public async Task WaitUntilReady() + { + while (!started) await Task.Yield(); + } + + public static async Task WaitUntilModelSetup(Callback downloadProgressCallback = null) + { + if (downloadProgressCallback != null) LLMManager.downloadProgressCallbacks.Add(downloadProgressCallback); + while (!modelSetupComplete) await Task.Yield(); + return !modelSetupFailed; + } + + public string GetModelLoraPath(string path) + { + string assetPath = LLMManager.GetAssetPath(path); + if (!string.IsNullOrEmpty(assetPath)) return assetPath; return path; } + public string SetModelLoraPath(string path, bool lora) + { + if (string.IsNullOrEmpty(path)) return path; + ModelEntry modelEntry = LLMManager.Get(path); + if (modelEntry != null) return modelEntry.filename; + + string modelType = lora ? "Lora" : "Model"; + string assetPath = LLMUnitySetup.GetAssetPath(path); + if (!File.Exists(assetPath)) + { + LLMUnitySetup.LogError($"The {modelType} file {path} was not found."); + return path; + } + + if (!LLMUnitySetup.IsSubPath(assetPath, LLMUnitySetup.GetAssetPath())) + { + string errorMessage = $"The {modelType} file {path} was loaded locally. If you want to include it in the build:"; + errorMessage += $"\n-Copy the {modelType} inside the StreamingAssets folder and use its relative path or"; + errorMessage += $"\n-Load the {modelType} with the LLMManager: `string filename=LLMManager.Load{modelType}(path); llm.Set{modelType}(filename)`"; + LLMUnitySetup.LogWarning(errorMessage); + } + return assetPath; + } + /// /// Allows to set the model used by the LLM. /// The model provided is copied to the Assets/StreamingAssets folder that allows it to also work in the build. /// Models supported are in .gguf format. /// /// path to model to use (.gguf format) - public async Task SetModel(string path) + public void SetModel(string path) { - // set the model and enable the model editor properties - model = await CopyAsset(path); - SetTemplate(ChatTemplate.FromGGUF(LLMUnitySetup.GetAssetPath(model))); + model = SetModelLoraPath(path, false); + if (!string.IsNullOrEmpty(model)) + { + ModelEntry modelEntry = LLMManager.Get(model); + string template = modelEntry != null ? modelEntry.chatTemplate : ChatTemplate.FromGGUF(GetModelLoraPath(model)); + SetTemplate(template); + } #if UNITY_EDITOR if (!EditorApplication.isPlaying) EditorUtility.SetDirty(this); #endif @@ -144,9 +185,9 @@ public async Task SetModel(string path) /// Models supported are in .bin format. /// /// path to LORA model to use (.bin format) - public async Task SetLora(string path) + public void SetLora(string path) { - lora = await CopyAsset(path); + lora = SetModelLoraPath(path, true); #if UNITY_EDITOR if (!EditorApplication.isPlaying) EditorUtility.SetDirty(this); #endif @@ -156,10 +197,13 @@ public async Task SetLora(string path) /// Set the chat template for the LLM. /// /// the chat template to use. The available templates can be found in the ChatTemplate.templates.Keys array - public void SetTemplate(string templateName) + public void SetTemplate(string templateName, bool setDirty = true) { chatTemplate = templateName; - llmlib?.LLM_SetTemplate(LLMObject, chatTemplate); + if (started) llmlib?.LLM_SetTemplate(LLMObject, chatTemplate); +#if UNITY_EDITOR + if (setDirty && !EditorApplication.isPlaying) EditorUtility.SetDirty(this); +#endif } /// @@ -179,7 +223,7 @@ protected virtual string GetLlamaccpArguments() LLMUnitySetup.LogError("No model file provided!"); return null; } - string modelPath = LLMUnitySetup.GetAssetPath(model); + string modelPath = GetModelLoraPath(model); if (!File.Exists(modelPath)) { LLMUnitySetup.LogError($"File {modelPath} not found!"); @@ -188,7 +232,7 @@ protected virtual string GetLlamaccpArguments() string loraPath = ""; if (lora != "") { - loraPath = LLMUnitySetup.GetAssetPath(lora); + loraPath = GetModelLoraPath(lora); if (!File.Exists(loraPath)) { LLMUnitySetup.LogError($"File {loraPath} not found!"); @@ -196,28 +240,18 @@ protected virtual string GetLlamaccpArguments() } } + int numThreadsToUse = numThreads; + if (Application.platform == RuntimePlatform.Android && numThreads <= 0) numThreadsToUse = LLMUnitySetup.AndroidGetNumBigCores(); + int slots = GetNumClients(); string arguments = $"-m \"{modelPath}\" -c {contextSize} -b {batchSize} --log-disable -np {slots}"; if (remote) arguments += $" --port {port} --host 0.0.0.0"; - if (numThreads > 0) arguments += $" -t {numThreads}"; + if (numThreadsToUse > 0) arguments += $" -t {numThreadsToUse}"; if (loraPath != "") arguments += $" --lora \"{loraPath}\""; arguments += $" -ngl {numGPULayers}"; return arguments; } - /// - /// The Unity Awake function that starts the LLM server. - /// The server can be started asynchronously if the asynchronousStartup option is set. - /// - public async void Awake() - { - if (!enabled) return; - if (asynchronousStartup) await Task.Run(() => StartLLMServer()); - else StartLLMServer(); - if (dontDestroyOnLoad) DontDestroyOnLoad(transform.root.gameObject); - if (basePrompt != "") await SetBasePrompt(basePrompt); - } - private void SetupLogging() { logStreamWrapper = ConstructStreamWrapper(LLMUnitySetup.LogWarning, true); @@ -231,12 +265,10 @@ private void StopLogging() DestroyStreamWrapper(logStreamWrapper); } - private void StartLLMServer() + private void StartLLMServer(string arguments) { started = false; failed = false; - string arguments = GetLlamaccpArguments(); - if (arguments == null) return; bool useGPU = numGPULayers > 0; LLMUnitySetup.Log($"Server command: {arguments}"); @@ -282,7 +314,7 @@ private void InitServer(string arguments) if (debug) SetupLogging(); LLMObject = llmlib.LLM_Construct(arguments); if (remote) llmlib.LLM_StartServer(LLMObject); - SetTemplate(chatTemplate); + SetTemplate(chatTemplate, false); CheckLLMStatus(false); } @@ -499,6 +531,7 @@ public void Destroy() public void OnDestroy() { Destroy(); + LLMManager.Unregister(this); } } } diff --git a/Runtime/LLMBuilder.cs b/Runtime/LLMBuilder.cs new file mode 100644 index 00000000..a3cf0070 --- /dev/null +++ b/Runtime/LLMBuilder.cs @@ -0,0 +1,153 @@ +using UnityEditor; +using UnityEngine; +using System.IO; +using System.Collections.Generic; + +#if UNITY_EDITOR +namespace LLMUnity +{ + public class LLMBuilder + { + static List movedPairs = new List(); + public static string BuildTempDir = Path.Combine(Application.temporaryCachePath, "LLMUnityBuild"); + static string movedCache = Path.Combine(BuildTempDir, "moved.json"); + + [InitializeOnLoadMethod] + private static void InitializeOnLoad() + { + Reset(); + } + + public static void CopyPath(string source, string target) + { + if (File.Exists(source)) + { + File.Copy(source, target, true); + } + else if (Directory.Exists(source)) + { + Directory.CreateDirectory(target); + List filesAndDirs = new List(); + filesAndDirs.AddRange(Directory.GetFiles(source)); + filesAndDirs.AddRange(Directory.GetDirectories(source)); + foreach (string path in filesAndDirs) + { + CopyPath(path, Path.Combine(target, Path.GetFileName(path))); + } + } + } + + public static void MovePath(string source, string target) + { + CopyPath(source, target); + DeletePath(source); + } + + public static bool DeletePath(string path) + { + if (!LLMUnitySetup.IsSubPath(path, LLMUnitySetup.GetAssetPath()) && !LLMUnitySetup.IsSubPath(path, BuildTempDir)) + { + LLMUnitySetup.LogError($"Safeguard: {path} will not be deleted because it may not be safe"); + return false; + } + if (File.Exists(path)) File.Delete(path); + else if (Directory.Exists(path)) Directory.Delete(path, true); + return true; + } + + static void AddMovedPair(string source, string target) + { + movedPairs.Add(new StringPair {source = source, target = target}); + File.WriteAllText(movedCache, JsonUtility.ToJson(new ListStringPair { pairs = movedPairs }, true)); + } + + static void AddTargetPair(string target) + { + AddMovedPair("", target); + } + + static bool MoveAction(string source, string target, bool addEntry = true) + { + ActionCallback moveCallback; + if (File.Exists(source)) moveCallback = File.Move; + else if (Directory.Exists(source)) moveCallback = MovePath; + else return false; + + if (addEntry) AddMovedPair(source, target); + moveCallback(source, target); + return true; + } + + static bool CopyAction(string source, string target, bool addEntry = true) + { + ActionCallback copyCallback; + if (File.Exists(source)) copyCallback = File.Copy; + else if (Directory.Exists(source)) copyCallback = CopyPath; + else return false; + + if (addEntry) AddTargetPair(target); + copyCallback(source, target); + return true; + } + + static void CopyActionAddMeta(string source, string target) + { + CopyAction(source, target); + AddTargetPair(target + ".meta"); + } + + static void AddActionAddMeta(string target) + { + AddTargetPair(target); + AddTargetPair(target + ".meta"); + } + + public static void HideLibraryPlatforms(string platform) + { + List platforms = new List(){ "windows", "macos", "linux", "android", "ios" }; + platforms.Remove(platform); + foreach (string source in Directory.GetDirectories(LLMUnitySetup.libraryPath)) + { + foreach (string platformPrefix in platforms) + { + if (Path.GetFileName(source).StartsWith(platformPrefix)) + { + string target = Path.Combine(BuildTempDir, Path.GetFileName(source)); + MoveAction(source, target); + MoveAction(source + ".meta", target + ".meta"); + } + } + } + } + + public static void BuildModels() + { + LLMManager.Build(CopyActionAddMeta); + if (File.Exists(LLMUnitySetup.LLMManagerPath)) AddActionAddMeta(LLMUnitySetup.LLMManagerPath); + } + + public static void Build(string platform) + { + Directory.CreateDirectory(BuildTempDir); + HideLibraryPlatforms(platform); + BuildModels(); + } + + public static void Reset() + { + if (!File.Exists(movedCache)) return; + List movedPairs = JsonUtility.FromJson(File.ReadAllText(movedCache)).pairs; + if (movedPairs == null) return; + + bool refresh = false; + foreach (var pair in movedPairs) + { + if (pair.source == "") refresh |= DeletePath(pair.target); + else refresh |= MoveAction(pair.target, pair.source, false); + } + if (refresh) AssetDatabase.Refresh(); + DeletePath(movedCache); + } + } +} +#endif diff --git a/Runtime/LLMBuilder.cs.meta b/Runtime/LLMBuilder.cs.meta new file mode 100644 index 00000000..14615c3e --- /dev/null +++ b/Runtime/LLMBuilder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e52304e7914527ae0801d752670d7bec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/LLMCharacter.cs b/Runtime/LLMCharacter.cs index 8d36ead9..9f37721a 100644 --- a/Runtime/LLMCharacter.cs +++ b/Runtime/LLMCharacter.cs @@ -322,8 +322,9 @@ public async Task LoadTemplate() public async void SetGrammar(string path) { #if UNITY_EDITOR - if (!EditorApplication.isPlaying) path = await LLMUnitySetup.AddAsset(path, LLMUnitySetup.GetAssetPath()); + if (!EditorApplication.isPlaying) path = LLMUnitySetup.AddAsset(path); #endif + await LLMUnitySetup.AndroidExtractAsset(path, true); grammar = path; InitGrammar(); } diff --git a/Runtime/LLMChatTemplates.cs b/Runtime/LLMChatTemplates.cs index 39f7fa9e..f6aa22cc 100644 --- a/Runtime/LLMChatTemplates.cs +++ b/Runtime/LLMChatTemplates.cs @@ -207,7 +207,7 @@ public class ChatMLTemplate : ChatTemplate { public override string GetName() { return "chatml"; } public override string GetDescription() { return "chatml (most widely used)"; } - public override string[] GetNameMatches() { return new string[] {"chatml", "hermes"}; } + public override string[] GetNameMatches() { return new string[] {"chatml", "hermes", "qwen"}; } public override string[] GetChatTemplateMatches() { return new string[] {"{% for message in messages %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}"}; } protected override string SystemPrefix() { return "<|im_start|>system\n"; } diff --git a/Runtime/LLMLib.cs b/Runtime/LLMLib.cs index 45fc4260..c8d49ccd 100644 --- a/Runtime/LLMLib.cs +++ b/Runtime/LLMLib.cs @@ -104,6 +104,8 @@ public static IntPtr LoadLibrary(string libraryName) handle = Linux.dlopen(libraryName); else if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.OSXPlayer) handle = Mac.dlopen(libraryName); + else if (Application.platform == RuntimePlatform.Android) + handle = Android.dlopen(libraryName); else throw new PlatformNotSupportedException($"Current platform is unknown, unable to load library '{libraryName}'."); @@ -122,6 +124,8 @@ public static IntPtr GetSymbol(IntPtr library, string symbolName) handle = Linux.dlsym(library, symbolName); else if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.OSXPlayer) handle = Mac.dlsym(library, symbolName); + else if (Application.platform == RuntimePlatform.Android) + handle = Android.dlsym(library, symbolName); else throw new PlatformNotSupportedException($"Current platform is unknown, unable to load symbol '{symbolName}' from library {library}."); @@ -139,6 +143,8 @@ public static void FreeLibrary(IntPtr library) Linux.dlclose(library); else if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.OSXPlayer) Mac.dlclose(library); + else if (Application.platform == RuntimePlatform.Android) + Android.dlclose(library); else throw new PlatformNotSupportedException($"Current platform is unknown, unable to close library '{library}'."); } @@ -231,6 +237,22 @@ private static class Win32 [DllImport(SystemLibrary, SetLastError = true, CharSet = CharSet.Ansi)] public static extern void FreeLibrary(IntPtr hModule); } + + private static class Android + { + public static IntPtr dlopen(string path) => dlopen(path, 1); + // LoadLibrary for Android + [DllImport("__Internal")] + public static extern IntPtr dlopen(string filename, int flags); + + // GetSymbol for Android + [DllImport("__Internal")] + public static extern IntPtr dlsym(IntPtr handle, string symbol); + + // FreeLibrary for Android + [DllImport("__Internal")] + public static extern int dlclose(IntPtr handle); + } } public class LLMLib @@ -334,6 +356,10 @@ public static List PossibleArchitectures(bool gpu = false) architectures.Add("x64-no_acc"); } } + else if (Application.platform == RuntimePlatform.Android) + { + architectures.Add("android"); + } else { string error = "Unknown OS"; @@ -376,6 +402,10 @@ public static string GetArchitecturePath(string arch) { filename = $"macos-{arch}/libundreamai_macos-{arch}.dylib"; } + else if (Application.platform == RuntimePlatform.Android) + { + return "libundreamai_android.so"; + } else { string error = "Unknown OS"; diff --git a/Runtime/LLMManager.cs b/Runtime/LLMManager.cs new file mode 100644 index 00000000..ff1101d8 --- /dev/null +++ b/Runtime/LLMManager.cs @@ -0,0 +1,438 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; + +namespace LLMUnity +{ + [Serializable] + public class ModelEntry + { + public string label; + public string filename; + public string path; + public bool lora; + public string chatTemplate; + public string url; + public bool includeInBuild; + + + public ModelEntry(string path, bool lora = false, string label = null, string url = null) + { + filename = Path.GetFileName(path); + this.label = label == null ? filename : label; + this.lora = lora; + this.path = Path.GetFullPath(path).Replace('\\', '/'); + chatTemplate = lora ? null : ChatTemplate.FromGGUF(this.path); + this.url = url; + includeInBuild = true; + } + + public ModelEntry OnlyRequiredFields() + { + ModelEntry entry = (ModelEntry)MemberwiseClone(); + entry.label = null; + entry.path = entry.filename; + return entry; + } + } + + [Serializable] + public class LLMManagerStore + { + public bool downloadOnStart; + public List modelEntries; + } + + [DefaultExecutionOrder(-2)] + public class LLMManager + { + public static bool downloadOnStart = false; + public static List modelEntries = new List(); + static List llms = new List(); + + public static float downloadProgress = 1; + public static List> downloadProgressCallbacks = new List>(); + static Task SetupTask; + static readonly object lockObject = new object(); + static long totalSize; + static long currFileSize; + static long completedSize; + + public static void SetDownloadProgress(float progress) + { + downloadProgress = (completedSize + progress * currFileSize) / totalSize; + foreach (Callback downloadProgressCallback in downloadProgressCallbacks) downloadProgressCallback?.Invoke(downloadProgress); + } + + public static Task Setup() + { + lock (lockObject) + { + if (SetupTask == null) SetupTask = SetupOnce(); + } + return SetupTask; + } + + public static async Task SetupOnce() + { + await LLMUnitySetup.AndroidExtractAsset(LLMUnitySetup.LLMManagerPath, true); + LoadFromDisk(); + + List downloads = new List(); + foreach (ModelEntry modelEntry in modelEntries) + { + string target = LLMUnitySetup.GetAssetPath(modelEntry.filename); + if (File.Exists(target)) continue; + + if (!downloadOnStart || string.IsNullOrEmpty(modelEntry.url)) + { + await LLMUnitySetup.AndroidExtractFile(modelEntry.filename); + if (!File.Exists(target)) LLMUnitySetup.LogError($"Model {modelEntry.filename} could not be found!"); + } + else + { + downloads.Add(new StringPair {source = modelEntry.url, target = target}); + } + } + if (downloads.Count == 0) return true; + + try + { + downloadProgress = 0; + totalSize = 0; + completedSize = 0; + + ResumingWebClient client = new ResumingWebClient(); + Dictionary fileSizes = new Dictionary(); + foreach (StringPair pair in downloads) + { + long size = client.GetURLFileSize(pair.source); + fileSizes[pair.source] = size; + totalSize += size; + } + + foreach (StringPair pair in downloads) + { + currFileSize = fileSizes[pair.source]; + await LLMUnitySetup.DownloadFile(pair.source, pair.target, false, null, SetDownloadProgress); + await LLMUnitySetup.AndroidExtractFile(Path.GetFileName(pair.target)); + completedSize += currFileSize; + } + + completedSize = totalSize; + SetDownloadProgress(0); + } + catch (Exception ex) + { + LLMUnitySetup.LogError($"Error downloading the models: {ex.Message}"); + return false; + } + return true; + } + + public static void SetTemplate(string filename, string chatTemplate) + { + SetTemplate(Get(filename), chatTemplate); + } + + public static void SetTemplate(ModelEntry entry, string chatTemplate) + { + if (entry == null) return; + entry.chatTemplate = chatTemplate; + foreach (LLM llm in llms) + { + if (llm != null && llm.model == entry.filename) llm.SetTemplate(chatTemplate); + } +#if UNITY_EDITOR + Save(); +#endif + } + + public static ModelEntry Get(string path) + { + string filename = Path.GetFileName(path); + string fullPath = Path.GetFullPath(path).Replace('\\', '/'); + foreach (ModelEntry entry in modelEntries) + { + if (entry.filename == filename || entry.path == fullPath) return entry; + } + return null; + } + + public static string GetAssetPath(string filename) + { + ModelEntry entry = Get(filename); + if (entry == null) return ""; +#if UNITY_EDITOR + return entry.path; +#else + return LLMUnitySetup.GetAssetPath(entry.filename); +#endif + } + + public static int Num(bool lora) + { + int num = 0; + foreach (ModelEntry entry in modelEntries) + { + if (entry.lora == lora) num++; + } + return num; + } + + public static int NumModels() + { + return Num(false); + } + + public static int NumLoras() + { + return Num(true); + } + + public static void Register(LLM llm) + { + llms.Add(llm); + } + + public static void Unregister(LLM llm) + { + llms.Remove(llm); + } + + public static void LoadFromDisk() + { + if (!File.Exists(LLMUnitySetup.LLMManagerPath)) return; + LLMManagerStore store = JsonUtility.FromJson(File.ReadAllText(LLMUnitySetup.LLMManagerPath)); + downloadOnStart = store.downloadOnStart; + modelEntries = store.modelEntries; + } + +#if UNITY_EDITOR + static string LLMManagerPref = "LLMManager"; + + [HideInInspector] public static float modelProgress = 1; + [HideInInspector] public static float loraProgress = 1; + + [InitializeOnLoadMethod] + static void InitializeOnLoad() + { + Load(); + } + + public static string AddEntry(ModelEntry entry) + { + int indexToInsert = modelEntries.Count; + if (!entry.lora) + { + if (modelEntries.Count > 0 && modelEntries[0].lora) indexToInsert = 0; + else + { + for (int i = modelEntries.Count - 1; i >= 0; i--) + { + if (!modelEntries[i].lora) + { + indexToInsert = i + 1; + break; + } + } + } + } + modelEntries.Insert(indexToInsert, entry); + Save(); + return entry.filename; + } + + public static string AddEntry(string path, bool lora = false, string label = null, string url = null) + { + return AddEntry(new ModelEntry(path, lora, label, url)); + } + + public static async Task Download(string url, bool lora = false, bool log = false, string label = null) + { + foreach (ModelEntry entry in modelEntries) + { + if (entry.url == url) + { + if (log) LLMUnitySetup.Log($"Found existing entry for {url}"); + return entry.filename; + } + } + + string modelName = Path.GetFileName(url).Split("?")[0]; + ModelEntry entryPath = Get(modelName); + if (entryPath != null) + { + if (log) LLMUnitySetup.Log($"Found existing entry for {modelName}"); + return entryPath.filename; + } + + string modelPath = Path.Combine(LLMUnitySetup.modelDownloadPath, modelName); + float preModelProgress = modelProgress; + float preLoraProgress = loraProgress; + try + { + if (!lora) + { + modelProgress = 0; + await LLMUnitySetup.DownloadFile(url, modelPath, false, null, SetModelProgress); + } + else + { + loraProgress = 0; + await LLMUnitySetup.DownloadFile(url, modelPath, false, null, SetLoraProgress); + } + } + catch (Exception ex) + { + modelProgress = preModelProgress; + loraProgress = preLoraProgress; + LLMUnitySetup.LogError($"Error downloading the model from URL '{url}': " + ex.Message); + return null; + } + return AddEntry(modelPath, lora, label, url); + } + + public static string Load(string path, bool lora = false, bool log = false, string label = null) + { + ModelEntry entry = Get(path); + if (entry != null) + { + if (log) LLMUnitySetup.Log($"Found existing entry for {entry.filename}"); + return entry.filename; + } + return AddEntry(path, lora, label); + } + + public static async Task DownloadModel(string url, bool log = false, string label = null) + { + return await Download(url, false, log, label); + } + + public static async Task DownloadLora(string url, bool log = false, string label = null) + { + return await Download(url, true, log, label); + } + + public static string LoadModel(string path, bool log = false, string label = null) + { + return Load(path, false, log, label); + } + + public static string LoadLora(string path, bool log = false, string label = null) + { + return Load(path, true, log, label); + } + + public static void SetURL(string filename, string url) + { + SetURL(Get(filename), url); + } + + public static void SetURL(ModelEntry entry, string url) + { + if (entry == null) return; + entry.url = url; + Save(); + } + + public static void SetIncludeInBuild(string filename, bool includeInBuild) + { + SetIncludeInBuild(Get(filename), includeInBuild); + } + + public static void SetIncludeInBuild(ModelEntry entry, bool includeInBuild) + { + if (entry == null) return; + entry.includeInBuild = includeInBuild; + Save(); + } + + public static void SetDownloadOnStart(bool value) + { + downloadOnStart = value; + if (downloadOnStart) + { + bool warn = false; + foreach (ModelEntry entry in modelEntries) + { + if (entry.url == null || entry.url == "") warn = true; + } + if (warn) LLMUnitySetup.LogWarning("Some models do not have a URL and will be copied in the build. To resolve this fill in the URL field in the expanded view of the LLM Model list."); + } + Save(); + } + + public static void Remove(string filename) + { + Remove(Get(filename)); + } + + public static void Remove(ModelEntry entry) + { + if (entry == null) return; + modelEntries.Remove(entry); + Save(); + foreach (LLM llm in llms) + { + if (!entry.lora && llm.model == entry.filename) llm.model = ""; + else if (entry.lora && llm.lora == entry.filename) llm.lora = ""; + } + } + + public static void SetModelProgress(float progress) + { + modelProgress = progress; + } + + public static void SetLoraProgress(float progress) + { + loraProgress = progress; + } + + public static void Save() + { + string json = JsonUtility.ToJson(new LLMManagerStore { modelEntries = modelEntries, downloadOnStart = downloadOnStart }, true); + PlayerPrefs.SetString(LLMManagerPref, json); + PlayerPrefs.Save(); + } + + public static void Load() + { + string pref = PlayerPrefs.GetString(LLMManagerPref); + if (pref == null || pref == "") return; + LLMManagerStore store = JsonUtility.FromJson(pref); + downloadOnStart = store.downloadOnStart; + modelEntries = store.modelEntries; + } + + public static void SaveToDisk() + { + List modelEntriesBuild = new List(); + foreach (ModelEntry modelEntry in modelEntries) + { + if (!modelEntry.includeInBuild) continue; + modelEntriesBuild.Add(modelEntry.OnlyRequiredFields()); + } + string json = JsonUtility.ToJson(new LLMManagerStore { modelEntries = modelEntriesBuild, downloadOnStart = downloadOnStart }, true); + File.WriteAllText(LLMUnitySetup.LLMManagerPath, json); + } + + public static void Build(ActionCallback copyCallback) + { + SaveToDisk(); + + foreach (ModelEntry modelEntry in modelEntries) + { + string target = LLMUnitySetup.GetAssetPath(modelEntry.filename); + if (!modelEntry.includeInBuild || File.Exists(target)) continue; + if (!downloadOnStart || string.IsNullOrEmpty(modelEntry.url)) copyCallback(modelEntry.path, target); + } + } + +#endif + } +} diff --git a/Runtime/LLMManager.cs.meta b/Runtime/LLMManager.cs.meta new file mode 100644 index 00000000..ea565c07 --- /dev/null +++ b/Runtime/LLMManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 936a5c66e859e31489f7ab1b78acb987 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/LLMUnitySetup.cs b/Runtime/LLMUnitySetup.cs index 74022dbc..58c6b677 100644 --- a/Runtime/LLMUnitySetup.cs +++ b/Runtime/LLMUnitySetup.cs @@ -4,10 +4,10 @@ using System.IO; using UnityEngine; using System.Threading.Tasks; -using System.Net; using System; using System.IO.Compression; using System.Collections.Generic; +using UnityEngine.Networking; /// @defgroup llm LLM /// @defgroup template Chat Templates @@ -44,6 +44,8 @@ public class LocalRemoteAttribute : PropertyAttribute {} public class RemoteAttribute : PropertyAttribute {} public class LocalAttribute : PropertyAttribute {} public class ModelAttribute : PropertyAttribute {} + public class ModelDownloadAttribute : ModelAttribute {} + public class ModelDownloadAdvancedAttribute : ModelAdvancedAttribute {} public class ModelAdvancedAttribute : PropertyAttribute {} public class ChatAttribute : PropertyAttribute {} public class ChatAdvancedAttribute : PropertyAttribute {} @@ -58,6 +60,20 @@ public NotImplementedException() : base("The method needs to be implemented by s public delegate void Callback(T message); public delegate Task TaskCallback(T message); public delegate T2 ContentCallback(T message); + public delegate void ActionCallback(string source, string target); + + [Serializable] + public struct StringPair + { + public string source; + public string target; + } + + [Serializable] + public class ListStringPair + { + public List pairs; + } /// \endcond /// @ingroup utils @@ -68,21 +84,28 @@ public class LLMUnitySetup { // DON'T CHANGE! the version is autocompleted with a GitHub action /// LLM for Unity version - public static string Version = "v2.0.3"; + public static string Version = "v2.1.0"; /// LlamaLib version - public static string LlamaLibVersion = "v1.1.5"; + public static string LlamaLibVersion = "v1.1.6"; /// LlamaLib url public static string LlamaLibURL = $"https://github.com/undreamai/LlamaLib/releases/download/{LlamaLibVersion}/undreamai-{LlamaLibVersion}-llamacpp.zip"; /// LlamaLib path public static string libraryPath = GetAssetPath(Path.GetFileName(LlamaLibURL).Replace(".zip", "")); + /// LLMnity store path + public static string LLMUnityStore = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "LLMUnity"); + /// Model download path + public static string modelDownloadPath = Path.Combine(LLMUnityStore, "models"); + /// Path of file with build information for runtime + public static string LLMManagerPath = GetAssetPath("LLMManager.json"); /// Default models for download - [HideInInspector] public static readonly (string, string)[] modelOptions = new(string, string)[] + [HideInInspector] public static readonly (string, string, string)[] modelOptions = new(string, string, string)[] { - ("Download model", null), - ("Mistral 7B Instruct v0.2 (medium, best overall)", "https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/mistral-7b-instruct-v0.2.Q4_K_M.gguf?download=true"), - ("OpenHermes 2.5 7B (medium, best for conversation)", "https://huggingface.co/TheBloke/OpenHermes-2.5-Mistral-7B-GGUF/resolve/main/openhermes-2.5-mistral-7b.Q4_K_M.gguf?download=true"), - ("Phi 3 (small, great)", "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-gguf/resolve/main/Phi-3-mini-4k-instruct-q4.gguf?download=true"), + ("Llama 3 7B (medium, best overall)", "https://huggingface.co/lmstudio-community/Meta-Llama-3-8B-Instruct-GGUF/resolve/main/Meta-Llama-3-8B-Instruct-Q4_K_M.gguf?download=true", "https://huggingface.co/meta-llama/Meta-Llama-3-8B/blob/main/LICENSE"), + ("Mistral 7B Instruct v0.2 (medium, great overall)", "https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/mistral-7b-instruct-v0.2.Q4_K_M.gguf?download=true", null), + ("OpenHermes 2.5 7B (medium, good for conversation)", "https://huggingface.co/TheBloke/OpenHermes-2.5-Mistral-7B-GGUF/resolve/main/openhermes-2.5-mistral-7b.Q4_K_M.gguf?download=true", null), + ("Phi 3 (small, great small model)", "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-gguf/resolve/main/Phi-3-mini-4k-instruct-q4.gguf?download=true", null), + ("Qwen 2 0.5B (tiny, useful for mobile)", "https://huggingface.co/Qwen/Qwen2-0.5B-Instruct-GGUF/resolve/main/qwen2-0_5b-instruct-q4_k_m.gguf?download=true", null), }; /// Add callback function to call for error logs @@ -113,6 +136,8 @@ public enum DebugModeType } [LLMUnity] public static DebugModeType DebugMode = DebugModeType.All; static List> errorCallbacks = new List>(); + static readonly object lockObject = new object(); + static Dictionary androidExtractTasks = new Dictionary(); public static void Log(string message) { @@ -150,7 +175,8 @@ public static void SetDebugMode(DebugModeType newDebugMode) public static string GetAssetPath(string relPath = "") { // Path to store llm server binaries and models - return Path.Combine(Application.streamingAssetsPath, relPath).Replace('\\', '/'); + string assetsDir = Application.platform == RuntimePlatform.Android ? Application.persistentDataPath : Application.streamingAssetsPath; + return Path.Combine(assetsDir, relPath).Replace('\\', '/'); } #if UNITY_EDITOR @@ -160,103 +186,126 @@ static async Task InitializeOnLoad() await DownloadLibrary(); LoadDebugMode(); } + #else [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] void InitializeOnLoad() { LoadDebugMode(); } + #endif -#if UNITY_EDITOR - [HideInInspector] public static float libraryProgress = 1; + static Dictionary downloadClients = new Dictionary(); - public class DownloadStatus + public static void CancelDownload(string savePath) { - Callback progresscallback; - - public DownloadStatus(Callback progresscallback = null) - { - this.progresscallback = progresscallback; - } - - public void DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e) - { - progresscallback?.Invoke(e.ProgressPercentage / 100.0f); - } + if (!downloadClients.ContainsKey(savePath)) return; + downloadClients[savePath].CancelDownloadAsync(); + downloadClients.Remove(savePath); } public static async Task DownloadFile( string fileUrl, string savePath, bool overwrite = false, - TaskCallback callback = null, Callback progresscallback = null, - bool async = true + Callback callback = null, Callback progressCallback = null ) { - // download a file to the specified path if (File.Exists(savePath) && !overwrite) { Log($"File already exists at: {savePath}"); } else { - Log($"Downloading {fileUrl}..."); + Log($"Downloading {fileUrl} to {savePath}..."); string tmpPath = Path.Combine(Application.temporaryCachePath, Path.GetFileName(savePath)); - WebClient client = new WebClient(); - DownloadStatus downloadStatus = new DownloadStatus(progresscallback); - client.DownloadProgressChanged += downloadStatus.DownloadProgressChanged; - if (async) - { - await client.DownloadFileTaskAsync(fileUrl, tmpPath); - } - else - { - client.DownloadFile(fileUrl, tmpPath); - } - + ResumingWebClient client = new ResumingWebClient(); + downloadClients[savePath] = client; + await client.DownloadFileTaskAsyncResume(new Uri(fileUrl), tmpPath, !overwrite, progressCallback); + downloadClients.Remove(savePath); +#if UNITY_EDITOR AssetDatabase.StartAssetEditing(); +#endif Directory.CreateDirectory(Path.GetDirectoryName(savePath)); File.Move(tmpPath, savePath); +#if UNITY_EDITOR AssetDatabase.StopAssetEditing(); +#endif Log($"Download complete!"); } - progresscallback?.Invoke(1f); - if (callback != null) await callback.Invoke(savePath); + progressCallback?.Invoke(1f); + callback?.Invoke(savePath); } - public static async Task AddAsset(string assetPath, string basePath) + public static async Task AndroidExtractFile(string assetName, bool overwrite = false, bool log = true, int chunkSize = 1024*1024) { - if (!File.Exists(assetPath)) + Task extractionTask; + lock (lockObject) { - LogError($"{assetPath} does not exist!"); - return null; + if (!androidExtractTasks.TryGetValue(assetName, out extractionTask)) + { + extractionTask = AndroidExtractFileOnce(assetName, overwrite, log, chunkSize); + androidExtractTasks[assetName] = extractionTask; + } + } + await extractionTask; + } + + public static async Task AndroidExtractFileOnce(string assetName, bool overwrite = false, bool log = true, int chunkSize = 1024*1024) + { + string source = "jar:file://" + Application.dataPath + "!/assets/" + assetName; + string target = GetAssetPath(assetName); + if (!overwrite && File.Exists(target)) + { + if (log) Log($"File {target} already exists"); + return; } - // add an asset to the basePath directory if it is not already there and return the relative path - string basePathSlash = basePath.Replace('\\', '/'); - string fullPath = Path.GetFullPath(assetPath).Replace('\\', '/'); - Directory.CreateDirectory(basePathSlash); - if (!fullPath.StartsWith(basePathSlash)) + + Log($"Extracting {source} to {target}"); + + // UnityWebRequest to read the file from StreamingAssets + UnityWebRequest www = UnityWebRequest.Get(source); + // Send the request and await its completion + var operation = www.SendWebRequest(); + + while (!operation.isDone) await Task.Delay(1); + if (www.result != UnityWebRequest.Result.Success) { - // if the asset is not in the assets dir copy it over - fullPath = Path.Combine(basePathSlash, Path.GetFileName(assetPath)); - Log($"copying {assetPath} to {fullPath}"); - AssetDatabase.StartAssetEditing(); - await Task.Run(() => + LogError("Failed to load file from StreamingAssets: " + www.error); + } + else + { + byte[] buffer = new byte[chunkSize]; + using (Stream responseStream = new MemoryStream(www.downloadHandler.data)) + using (FileStream fileStream = new FileStream(target, FileMode.Create, FileAccess.Write)) { - foreach (string filename in new string[] {fullPath, fullPath + ".meta"}) + int bytesRead; + while ((bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length)) > 0) { - if (File.Exists(fullPath)) - File.Delete(fullPath); + await fileStream.WriteAsync(buffer, 0, bytesRead); } - File.Copy(assetPath, fullPath); - }); - AssetDatabase.StopAssetEditing(); - Log("copying complete!"); + } } - return fullPath.Substring(basePathSlash.Length + 1); } + public static async Task AndroidExtractAsset(string path, bool overwrite = false) + { + if (Application.platform != RuntimePlatform.Android) return; + await AndroidExtractFile(Path.GetFileName(path), overwrite); + } + + public static bool IsSubPath(string childPath, string parentPath) + { + string fullParentPath = Path.GetFullPath(parentPath).Replace('\\', '/'); + string fullChildPath = Path.GetFullPath(childPath).Replace('\\', '/'); + return fullChildPath.StartsWith(fullParentPath, StringComparison.OrdinalIgnoreCase); + } + +#if UNITY_EDITOR + + [HideInInspector] public static float libraryProgress = 1; + private static async Task DownloadLibrary() { if (libraryProgress < 1) return; @@ -265,7 +314,24 @@ private static async Task DownloadLibrary() if (!Directory.Exists(libraryPath)) { await DownloadFile(LlamaLibURL, libZip, true, null, SetLibraryProgress); + AssetDatabase.StartAssetEditing(); ZipFile.ExtractToDirectory(libZip, libraryPath); + string androidDir = Path.Combine(libraryPath, "android"); + if (Directory.Exists(androidDir)) + { + string androidPluginDir = Path.Combine(Application.dataPath, "Plugins", "Android"); + Directory.CreateDirectory(androidPluginDir); + Directory.Move(androidDir, Path.Combine(androidPluginDir, Path.GetFileName(libraryPath))); + } + foreach (string librarySubPath in Directory.GetDirectories(libraryPath)) + { + if (Path.GetFileName(librarySubPath).StartsWith("android")) + { + string pluginPath = Path.Combine(Application.dataPath, "Plugins", "Android", Path.GetFileName(librarySubPath)); + Directory.Move(librarySubPath, pluginPath); + } + } + AssetDatabase.StopAssetEditing(); File.Delete(libZip); } libraryProgress = 1; @@ -276,19 +342,183 @@ private static void SetLibraryProgress(float progress) libraryProgress = progress; } - public static void DownloadModel(LLM llm, int optionIndex) + public static string AddAsset(string assetPath) { - // download default model and disable model editor properties until the model is set - llm.SelectedModel = optionIndex; - string modelUrl = modelOptions[optionIndex].Item2; - if (modelUrl == null) return; - llm.modelProgress = 0; - string modelName = Path.GetFileName(modelUrl).Split("?")[0]; - string modelPath = GetAssetPath(modelName); - Task downloadTask = DownloadFile(modelUrl, modelPath, false, llm.SetModel, llm.SetModelProgress); + if (!File.Exists(assetPath)) + { + LogError($"{assetPath} does not exist!"); + return null; + } + string filename = Path.GetFileName(assetPath); + string fullPath = GetAssetPath(filename); + AssetDatabase.StartAssetEditing(); + foreach (string path in new string[] {fullPath, fullPath + ".meta"}) + { + if (File.Exists(path)) File.Delete(path); + } + File.Copy(assetPath, fullPath); + AssetDatabase.StopAssetEditing(); + return filename; } #endif /// \endcond + public static int GetMaxFreqKHz(int cpuId) + { + string[] paths = new string[] + { + $"/sys/devices/system/cpu/cpufreq/stats/cpu{cpuId}/time_in_state", + $"/sys/devices/system/cpu/cpu{cpuId}/cpufreq/stats/time_in_state", + $"/sys/devices/system/cpu/cpu{cpuId}/cpufreq/cpuinfo_max_freq" + }; + + foreach (var path in paths) + { + if (!File.Exists(path)) continue; + + int maxFreqKHz = 0; + using (StreamReader sr = new StreamReader(path)) + { + string line; + while ((line = sr.ReadLine()) != null) + { + string[] parts = line.Split(' '); + if (parts.Length > 0 && int.TryParse(parts[0], out int freqKHz)) + { + if (freqKHz > maxFreqKHz) + { + maxFreqKHz = freqKHz; + } + } + } + } + if (maxFreqKHz != 0) return maxFreqKHz; + } + return -1; + } + + public static bool IsSmtCpu(int cpuId) + { + string[] paths = new string[] + { + $"/sys/devices/system/cpu/cpu{cpuId}/topology/core_cpus_list", + $"/sys/devices/system/cpu/cpu{cpuId}/topology/thread_siblings_list" + }; + + foreach (var path in paths) + { + if (!File.Exists(path)) continue; + using (StreamReader sr = new StreamReader(path)) + { + string line; + while ((line = sr.ReadLine()) != null) + { + if (line.Contains(",") || line.Contains("-")) + { + return true; + } + } + } + } + return false; + } + + /// + /// Calculates the number of big cores in Android similarly to ncnn (https://github.com/Tencent/ncnn) + /// + /// + public static int AndroidGetNumBigCores() + { + int maxFreqKHzMin = int.MaxValue; + int maxFreqKHzMax = 0; + List cpuMaxFreqKHz = new List(); + List cpuIsSmtCpu = new List(); + + try + { + string cpuPath = "/sys/devices/system/cpu/"; + int coreIndex; + if (Directory.Exists(cpuPath)) + { + foreach (string cpuDir in Directory.GetDirectories(cpuPath)) + { + string dirName = Path.GetFileName(cpuDir); + if (!dirName.StartsWith("cpu")) continue; + if (!int.TryParse(dirName.Substring(3), out coreIndex)) continue; + + int maxFreqKHz = GetMaxFreqKHz(coreIndex); + cpuMaxFreqKHz.Add(maxFreqKHz); + if (maxFreqKHz > maxFreqKHzMax) maxFreqKHzMax = maxFreqKHz; + if (maxFreqKHz < maxFreqKHzMin) maxFreqKHzMin = maxFreqKHz; + cpuIsSmtCpu.Add(IsSmtCpu(coreIndex)); + } + } + } + catch (Exception e) + { + LogError(e.Message); + } + + int numBigCores = 0; + int numCores = SystemInfo.processorCount; + int maxFreqKHzMedium = (maxFreqKHzMin + maxFreqKHzMax) / 2; + if (maxFreqKHzMedium == maxFreqKHzMax) numBigCores = numCores; + else + { + for (int i = 0; i < cpuMaxFreqKHz.Count; i++) + { + if (cpuIsSmtCpu[i] || cpuMaxFreqKHz[i] >= maxFreqKHzMedium) numBigCores++; + } + } + + if (numBigCores == 0) numBigCores = SystemInfo.processorCount / 2; + else numBigCores = Math.Min(numBigCores, SystemInfo.processorCount); + + return numBigCores; + } + + /// + /// Calculates the number of big cores in Android similarly to Unity (https://docs.unity3d.com/2022.3/Documentation/Manual/android-thread-configuration.html) + /// + /// + public static int AndroidGetNumBigCoresCapacity() + { + List capacities = new List(); + int minCapacity = int.MaxValue; + try + { + string cpuPath = "/sys/devices/system/cpu/"; + int coreIndex; + if (Directory.Exists(cpuPath)) + { + foreach (string cpuDir in Directory.GetDirectories(cpuPath)) + { + string dirName = Path.GetFileName(cpuDir); + if (!dirName.StartsWith("cpu")) continue; + if (!int.TryParse(dirName.Substring(3), out coreIndex)) continue; + + string capacityPath = Path.Combine(cpuDir, "cpu_capacity"); + if (!File.Exists(capacityPath)) break; + + int capacity = int.Parse(File.ReadAllText(capacityPath).Trim()); + capacities.Add(capacity); + if (minCapacity > capacity) minCapacity = capacity; + } + } + } + catch (Exception e) + { + LogError(e.Message); + } + + int numBigCores = 0; + foreach (int capacity in capacities) + { + if (capacity >= 2 * minCapacity) numBigCores++; + } + + if (numBigCores == 0 || numBigCores > SystemInfo.processorCount) numBigCores = SystemInfo.processorCount; + return numBigCores; + } } } diff --git a/Runtime/ResumingWebClient.cs b/Runtime/ResumingWebClient.cs new file mode 100644 index 00000000..b282caa2 --- /dev/null +++ b/Runtime/ResumingWebClient.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace LLMUnity +{ + public class ResumingWebClient : WebClient + { + private const int timeoutMs = 30 * 1000; + private SynchronizationContext _context; + private const int DefaultDownloadBufferLength = 65536; + List requests = new List(); + + public ResumingWebClient() + { + _context = SynchronizationContext.Current ?? new SynchronizationContext(); + } + + public long GetURLFileSize(string address) + { + return GetURLFileSize(new Uri(address)); + } + + public long GetURLFileSize(Uri address) + { + WebRequest request = GetWebRequest(address); + request.Method = "HEAD"; + WebResponse response = request.GetResponse(); + return response.ContentLength; + } + + public Task DownloadFileTaskAsyncResume(Uri address, string fileName, bool resume = false, Callback progressCallback = null) + { + var tcs = new TaskCompletionSource(address); + FileStream fs = null; + long bytesToSkip = 0; + + try + { + FileMode filemode = FileMode.Create; + if (resume) + { + var fileInfo = new FileInfo(fileName); + if (fileInfo.Exists) bytesToSkip = fileInfo.Length; + } + + WebRequest request = GetWebRequest(address); + if (request is HttpWebRequest webRequest && bytesToSkip > 0) + { + long remoteFileSize = GetURLFileSize(address); + if (bytesToSkip >= remoteFileSize) + { + LLMUnitySetup.Log($"File is already fully downloaded: {fileName}"); + tcs.TrySetResult(true); + return tcs.Task; + } + + filemode = FileMode.Append; + LLMUnitySetup.Log($"File exists at {fileName}, skipping {bytesToSkip} bytes"); + webRequest.AddRange(bytesToSkip); + webRequest.ReadWriteTimeout = timeoutMs; + } + + fs = new FileStream(fileName, filemode, FileAccess.Write); + DownloadBitsAsync(request, fs, bytesToSkip, progressCallback, tcs); + } + catch (Exception e) + { + fs?.Close(); + tcs.TrySetException(e); + } + + return tcs.Task; + } + + public void CancelDownloadAsync() + { + LLMUnitySetup.Log("Cancellation requested, aborting download."); + foreach (WebRequest request in requests) AbortRequest(request); + requests.Clear(); + } + + public void AbortRequest(WebRequest request) + { + try + { + request?.Abort(); + } + catch (Exception e) + { + LLMUnitySetup.LogError($"Error aborting request: {e.Message}"); + } + } + + private async void DownloadBitsAsync(WebRequest request, Stream writeStream, long bytesToSkip = 0, Callback progressCallback = null, TaskCompletionSource tcs = null) + { + try + { + requests.Add(request); + WebResponse response = await request.GetResponseAsync().ConfigureAwait(false); + + long contentLength = response.ContentLength; + byte[] copyBuffer = new byte[contentLength == -1 || contentLength > DefaultDownloadBufferLength ? DefaultDownloadBufferLength : contentLength]; + + long TotalBytesToReceive = Math.Max(contentLength, 0) + bytesToSkip; + long BytesReceived = bytesToSkip; + + using (writeStream) + using (Stream readStream = response.GetResponseStream()) + { + if (readStream != null) + { + while (true) + { + int bytesRead = await readStream.ReadAsync(new Memory(copyBuffer)).ConfigureAwait(false); + if (bytesRead == 0) + { + break; + } + + BytesReceived += bytesRead; + if (BytesReceived != TotalBytesToReceive) + { + PostProgressChanged(progressCallback, BytesReceived, TotalBytesToReceive); + } + + await writeStream.WriteAsync(new ReadOnlyMemory(copyBuffer, 0, bytesRead)).ConfigureAwait(false); + } + } + + if (TotalBytesToReceive < 0) + { + TotalBytesToReceive = BytesReceived; + } + PostProgressChanged(progressCallback, BytesReceived, TotalBytesToReceive); + } + tcs.TrySetResult(true); + } + catch (Exception e) + { + tcs.TrySetException(e); + LLMUnitySetup.LogError(e.Message); + AbortRequest(request); + tcs.TrySetResult(false); + } + finally + { + writeStream?.Close(); + requests.Remove(request); + } + } + + private void PostProgressChanged(Callback progressCallback, long BytesReceived, long TotalBytesToReceive) + { + if (progressCallback != null && BytesReceived > 0) + { + float progressPercentage = TotalBytesToReceive < 0 ? 0 : TotalBytesToReceive == 0 ? 1 : (float)BytesReceived / TotalBytesToReceive; + _context.Post(_ => progressCallback?.Invoke(progressPercentage), null); + } + } + } +} diff --git a/Runtime/ResumingWebClient.cs.meta b/Runtime/ResumingWebClient.cs.meta new file mode 100644 index 00000000..369fc67e --- /dev/null +++ b/Runtime/ResumingWebClient.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 00e47c7cca64b8c57ba4b7b6b2c2c5b8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/AndroidDemo.meta b/Samples~/AndroidDemo.meta new file mode 100644 index 00000000..9b3afc06 --- /dev/null +++ b/Samples~/AndroidDemo.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4259a324b4c9af10a95ced582b171297 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/AndroidDemo/AndroidDemo.cs b/Samples~/AndroidDemo/AndroidDemo.cs new file mode 100644 index 00000000..92e52b7f --- /dev/null +++ b/Samples~/AndroidDemo/AndroidDemo.cs @@ -0,0 +1,106 @@ +using UnityEngine; +using LLMUnity; +using UnityEngine.UI; +using System.Threading.Tasks; + +namespace LLMUnitySamples +{ + public class AndroidDemo : MonoBehaviour + { + public LLMCharacter llmCharacter; + + public GameObject ChatPanel; + public InputField playerText; + public Text AIText; + public GameObject ErrorText; + + public GameObject DownloadPanel; + public Scrollbar progressBar; + public Text progressText; + + async void Start() + { + playerText.onSubmit.AddListener(onInputFieldSubmit); + playerText.interactable = false; + await DownloadThenWarmup(); + } + + async Task DownloadThenWarmup() + { + ChatPanel.SetActive(false); + DownloadPanel.SetActive(true); + bool downloadOK = await LLM.WaitUntilModelSetup(SetProgress); + if (!downloadOK) + { + ErrorText.SetActive(true); + } + else + { + DownloadPanel.SetActive(false); + ChatPanel.SetActive(true); + await WarmUp(); + } + } + + async Task WarmUp() + { + AIText.text += $"Warming up the model..."; + await llmCharacter.Warmup(); + AIText.text = ""; + AIReplyComplete(); + } + + void SetProgress(float progress) + { + progressText.text = ((int)(progress * 100)).ToString() + "%"; + progressBar.size = progress; + } + + void onInputFieldSubmit(string message) + { + playerText.interactable = false; + AIText.text = "..."; + _ = llmCharacter.Chat(message, SetAIText, AIReplyComplete); + } + + public void SetAIText(string text) + { + AIText.text = text; + } + + public void AIReplyComplete() + { + playerText.interactable = true; + playerText.Select(); + playerText.text = ""; + } + + public void CancelRequests() + { + llmCharacter.CancelRequests(); + AIReplyComplete(); + } + + public void ExitGame() + { + Debug.Log("Exit button clicked"); + Application.Quit(); + } + + bool onValidateWarning = true; + bool onValidateInfo = true; + void OnValidate() + { + if (onValidateWarning && !llmCharacter.remote && llmCharacter.llm != null && llmCharacter.llm.model == "") + { + Debug.LogWarning($"Please select a model in the {llmCharacter.llm.gameObject.name} GameObject!"); + onValidateWarning = false; + } + if (onValidateInfo) + { + Debug.Log($"Select 'Download On Start' in the {llmCharacter.llm.gameObject.name} GameObject to download the models when the app starts."); + onValidateInfo = false; + } + } + } +} diff --git a/Samples~/AndroidDemo/AndroidDemo.cs.meta b/Samples~/AndroidDemo/AndroidDemo.cs.meta new file mode 100644 index 00000000..3409ae47 --- /dev/null +++ b/Samples~/AndroidDemo/AndroidDemo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 708542fec6999ea3ebd7c69404932bb3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/AndroidDemo/Scene.unity b/Samples~/AndroidDemo/Scene.unity new file mode 100644 index 00000000..d7a58ae7 --- /dev/null +++ b/Samples~/AndroidDemo/Scene.unity @@ -0,0 +1,2336 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0.44657832, g: 0.49641222, b: 0.57481664, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &107963744 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 107963746} + - component: {fileID: 107963747} + m_Layer: 0 + m_Name: AndroidDemo + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &107963746 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 107963744} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &107963747 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 107963744} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 708542fec6999ea3ebd7c69404932bb3, type: 3} + m_Name: + m_EditorClassIdentifier: + llmCharacter: {fileID: 498662973} + ChatPanel: {fileID: 1084608230} + playerText: {fileID: 1966107897} + AIText: {fileID: 887085510} + ErrorText: {fileID: 1688602496} + DownloadPanel: {fileID: 332743750} + progressBar: {fileID: 332743752} + progressText: {fileID: 381203299} +--- !u!1 &158550913 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 158550917} + - component: {fileID: 158550916} + - component: {fileID: 158550915} + - component: {fileID: 158550914} + m_Layer: 5 + m_Name: Canvas + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &158550914 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 158550913} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!114 &158550915 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 158550913} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3} + m_Name: + m_EditorClassIdentifier: + m_UiScaleMode: 1 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 720, y: 720} + m_ScreenMatchMode: 0 + m_MatchWidthOrHeight: 0.5 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 + m_PresetInfoIsWorld: 0 +--- !u!223 &158550916 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 158550913} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 0 + m_Camera: {fileID: 0} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_VertexColorAlwaysGammaSpace: 0 + m_AdditionalShaderChannelsFlag: 0 + m_UpdateRectTransformForStandalone: 0 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!224 &158550917 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 158550913} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0, y: 0, z: 0} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 268891590} + - {fileID: 332743751} + - {fileID: 1084608231} + - {fileID: 1308766010} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 0} +--- !u!1 &268891589 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 268891590} + - component: {fileID: 268891592} + - component: {fileID: 268891591} + m_Layer: 5 + m_Name: Panel + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &268891590 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 268891589} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1.01, y: 1.01, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 158550917} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &268891591 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 268891589} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &268891592 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 268891589} + m_CullTransparentMesh: 1 +--- !u!1 &332743750 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 332743751} + - component: {fileID: 332743754} + - component: {fileID: 332743753} + - component: {fileID: 332743752} + m_Layer: 5 + m_Name: DownloadPanel + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!224 &332743751 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 332743750} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 2047786417} + - {fileID: 1688602497} + - {fileID: 1705264488} + - {fileID: 381203298} + m_Father: {fileID: 158550917} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: -34.8} + m_SizeDelta: {x: 160, y: 18} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &332743752 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 332743750} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2a4db7a114972834c8e4117be1d82ba3, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 0.31764707, g: 0.6431373, b: 0.31764707, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 659217392} + m_HandleRect: {fileID: 659217391} + m_Direction: 0 + m_Value: 0 + m_Size: 0 + m_NumberOfSteps: 0 + m_OnValueChanged: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &332743753 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 332743750} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.5207577, g: 0.5207577, b: 0.5207577, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &332743754 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 332743750} + m_CullTransparentMesh: 1 +--- !u!1 &381203297 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 381203298} + - component: {fileID: 381203300} + - component: {fileID: 381203299} + m_Layer: 5 + m_Name: Progress + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &381203298 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 381203297} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 332743751} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 5, y: 0} + m_SizeDelta: {x: 79.6491, y: 19.2105} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &381203299 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 381203297} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 1 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 0% +--- !u!222 &381203300 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 381203297} + m_CullTransparentMesh: 1 +--- !u!1 &433287079 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 433287080} + - component: {fileID: 433287082} + - component: {fileID: 433287081} + m_Layer: 5 + m_Name: Text (Legacy) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &433287080 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 433287079} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 724531320} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &433287081 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 433287079} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 16 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 'Stop + +' +--- !u!222 &433287082 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 433287079} + m_CullTransparentMesh: 1 +--- !u!1 &498662970 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 498662972} + - component: {fileID: 498662973} + m_Layer: 0 + m_Name: LLMCharacter + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &498662972 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 498662970} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 1024.2354, y: 495.014, z: -3.4752133} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &498662973 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 498662970} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3f6c87a428fd5d0be9bbc686bdc8c3c2, type: 3} + m_Name: + m_EditorClassIdentifier: + advancedOptions: 0 + remote: 0 + llm: {fileID: 1047848254} + host: localhost + port: 13333 + save: + saveCache: 0 + debugPrompt: 0 + stream: 1 + grammar: + cachePrompt: 1 + seed: 0 + numPredict: 256 + temperature: 0.2 + topK: 40 + topP: 0.9 + minP: 0.05 + repeatPenalty: 1.1 + presencePenalty: 0 + frequencyPenalty: 0 + tfsZ: 1 + typicalP: 1 + repeatLastN: 64 + penalizeNl: 1 + penaltyPrompt: + mirostat: 0 + mirostatTau: 5 + mirostatEta: 0.1 + nProbs: 0 + ignoreEos: 0 + nKeep: -1 + stop: [] + playerName: user + AIName: assistant + prompt: A chat between a curious human and an artificial intelligence assistant. + The assistant gives helpful, detailed, and polite answers to the human's questions. + setNKeepToPrompt: 1 + chat: [] + grammarString: +--- !u!1 &659217390 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 659217391} + - component: {fileID: 659217393} + - component: {fileID: 659217392} + m_Layer: 5 + m_Name: Handle + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &659217391 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 659217390} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1705264488} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &659217392 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 659217390} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &659217393 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 659217390} + m_CullTransparentMesh: 1 +--- !u!1 &724531319 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 724531320} + - component: {fileID: 724531323} + - component: {fileID: 724531322} + - component: {fileID: 724531321} + m_Layer: 5 + m_Name: StopButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &724531320 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 724531319} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 433287080} + m_Father: {fileID: 1084608231} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 10.624025, y: -80.006996} + m_SizeDelta: {x: 107.0426, y: 30} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &724531321 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 724531319} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 724531322} + m_OnClick: + m_PersistentCalls: + m_Calls: + - m_Target: {fileID: 107963747} + m_TargetAssemblyTypeName: LLMUnitySamples.AndroidDemo, Assembly-CSharp + m_MethodName: CancelRequests + m_Mode: 1 + m_Arguments: + m_ObjectArgument: {fileID: 0} + m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine + m_IntArgument: 0 + m_FloatArgument: 0 + m_StringArgument: + m_BoolArgument: 0 + m_CallState: 0 +--- !u!114 &724531322 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 724531319} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.31764707, g: 0.6431373, b: 0.31764707, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &724531323 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 724531319} + m_CullTransparentMesh: 1 +--- !u!1 &726528676 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 726528679} + - component: {fileID: 726528678} + - component: {fileID: 726528677} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &726528677 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 726528676} + m_Enabled: 1 +--- !u!20 &726528678 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 726528676} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 0 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &726528679 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 726528676} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &856480601 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 856480602} + - component: {fileID: 856480604} + - component: {fileID: 856480603} + m_Layer: 5 + m_Name: Player title + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &856480602 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 856480601} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1084608231} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 10.624025, y: 62.99302} + m_SizeDelta: {x: 160, y: 30} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &856480603 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 856480601} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 20 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 2 + m_MaxSize: 40 + m_Alignment: 1 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Player +--- !u!222 &856480604 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 856480601} + m_CullTransparentMesh: 1 +--- !u!1 &887085508 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 887085509} + - component: {fileID: 887085511} + - component: {fileID: 887085510} + - component: {fileID: 887085512} + m_Layer: 5 + m_Name: AIText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &887085509 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 887085508} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 2091685447} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -20, y: -20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &887085510 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 887085508} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 16 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: +--- !u!222 &887085511 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 887085508} + m_CullTransparentMesh: 1 +--- !u!210 &887085512 +SortingGroup: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 887085508} + m_Enabled: 1 + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 1 + m_SortAtRoot: 0 +--- !u!1 &909474451 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 909474453} + - component: {fileID: 909474452} + m_Layer: 0 + m_Name: Directional Light + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!108 &909474452 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 909474451} + m_Enabled: 1 + serializedVersion: 10 + m_Type: 1 + m_Shape: 0 + m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1} + m_Intensity: 1 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.80208 + m_CookieSize: 10 + m_Shadows: + m_Type: 2 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 1 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_CullingMatrixOverride: + e00: 1 + e01: 0 + e02: 0 + e03: 0 + e10: 0 + e11: 1 + e12: 0 + e13: 0 + e20: 0 + e21: 0 + e22: 1 + e23: 0 + e30: 0 + e31: 0 + e32: 0 + e33: 1 + m_UseCullingMatrixOverride: 0 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingLayerMask: 1 + m_Lightmapping: 4 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 6570 + m_UseColorTemperature: 0 + m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} + m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 + m_ShadowRadius: 0 + m_ShadowAngle: 0 +--- !u!4 &909474453 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 909474451} + serializedVersion: 2 + m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} + m_LocalPosition: {x: 0, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} +--- !u!1 &1047848253 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1047848255} + - component: {fileID: 1047848254} + m_Layer: 0 + m_Name: LLM + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1047848254 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1047848253} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a50e3140c3ecaaf1c848dbf141cc2074, type: 3} + m_Name: + m_EditorClassIdentifier: + advancedOptions: 0 + remote: 0 + port: 13333 + numThreads: -1 + numGPULayers: 0 + debug: 0 + parallelPrompts: -1 + dontDestroyOnLoad: 1 + contextSize: 0 + batchSize: 512 + basePrompt: + model: + chatTemplate: chatml + lora: +--- !u!4 &1047848255 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1047848253} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1059033619 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1059033620} + - component: {fileID: 1059033622} + - component: {fileID: 1059033621} + m_Layer: 5 + m_Name: Placeholder + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1059033620 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1059033619} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1966107896} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: -0.5} + m_SizeDelta: {x: -20, y: -13} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1059033621 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1059033619} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 16 + m_FontStyle: 2 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Enter text... +--- !u!222 &1059033622 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1059033619} + m_CullTransparentMesh: 1 +--- !u!1 &1084608230 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1084608231} + m_Layer: 5 + m_Name: ChatPanel + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1084608231 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1084608230} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1966107896} + - {fileID: 2091685447} + - {fileID: 856480602} + - {fileID: 1342801405} + - {fileID: 724531320} + m_Father: {fileID: 158550917} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 87.00699} + m_SizeDelta: {x: 400, y: 100} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1171567417 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1171567418} + - component: {fileID: 1171567420} + - component: {fileID: 1171567419} + m_Layer: 5 + m_Name: Text (Legacy) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1171567418 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1171567417} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1308766010} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 2} + m_SizeDelta: {x: 25, y: 25} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1171567419 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1171567417} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 20 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 2 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: "\xD7" +--- !u!222 &1171567420 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1171567417} + m_CullTransparentMesh: 1 +--- !u!1 &1308766009 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1308766010} + - component: {fileID: 1308766013} + - component: {fileID: 1308766012} + - component: {fileID: 1308766011} + m_Layer: 5 + m_Name: ExitButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1308766010 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1308766009} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1171567418} + m_Father: {fileID: 158550917} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 35, y: 35} + m_Pivot: {x: 1, y: 1} +--- !u!114 &1308766011 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1308766009} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 1308766012} + m_OnClick: + m_PersistentCalls: + m_Calls: + - m_Target: {fileID: 107963747} + m_TargetAssemblyTypeName: LLMUnitySamples.AndroidDemo, Assembly-CSharp + m_MethodName: ExitGame + m_Mode: 1 + m_Arguments: + m_ObjectArgument: {fileID: 0} + m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine + m_IntArgument: 0 + m_FloatArgument: 0 + m_StringArgument: + m_BoolArgument: 0 + m_CallState: 1 +--- !u!114 &1308766012 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1308766009} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.24722636, g: 0.24722636, b: 0.24722636, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10913, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1308766013 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1308766009} + m_CullTransparentMesh: 1 +--- !u!1 &1342801404 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1342801405} + - component: {fileID: 1342801407} + - component: {fileID: 1342801406} + m_Layer: 5 + m_Name: AI title + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1342801405 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1342801404} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1084608231} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 5.6240253, y: -133.00699} + m_SizeDelta: {x: 160, y: 29.6652} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1342801406 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1342801404} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 20 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 2 + m_MaxSize: 40 + m_Alignment: 1 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: AI +--- !u!222 &1342801407 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1342801404} + m_CullTransparentMesh: 1 +--- !u!1 &1609985808 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1609985809} + - component: {fileID: 1609985811} + - component: {fileID: 1609985810} + m_Layer: 5 + m_Name: Text (Legacy) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1609985809 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1609985808} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1966107896} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: -0.5} + m_SizeDelta: {x: -20, y: -13} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1609985810 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1609985808} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 16 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 0 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: +--- !u!222 &1609985811 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1609985808} + m_CullTransparentMesh: 1 +--- !u!1 &1688602496 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1688602497} + - component: {fileID: 1688602499} + - component: {fileID: 1688602498} + m_Layer: 5 + m_Name: NetworkError + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!224 &1688602497 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1688602496} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 332743751} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -0.000025749, y: -28.3} + m_SizeDelta: {x: 160, y: 30} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1688602498 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1688602496} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.8983897, g: 0, b: 0, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 16 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 1 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Network error occured +--- !u!222 &1688602499 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1688602496} + m_CullTransparentMesh: 1 +--- !u!1 &1705264487 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1705264488} + m_Layer: 5 + m_Name: Sliding Area + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1705264488 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1705264487} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 659217391} + m_Father: {fileID: 332743751} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: -0.0000038146973} + m_SizeDelta: {x: 0, y: -20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1966107895 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1966107896} + - component: {fileID: 1966107899} + - component: {fileID: 1966107898} + - component: {fileID: 1966107897} + m_Layer: 5 + m_Name: PlayerInput + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1966107896 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1966107895} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1059033620} + - {fileID: 1609985809} + m_Father: {fileID: 1084608231} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: -0.000022888184} + m_SizeDelta: {x: 400, y: 100} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1966107897 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1966107895} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d199490a83bb2b844b9695cbf13b01ef, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 1966107898} + m_TextComponent: {fileID: 1609985810} + m_Placeholder: {fileID: 1059033621} + m_ContentType: 0 + m_InputType: 0 + m_AsteriskChar: 42 + m_KeyboardType: 0 + m_LineType: 1 + m_HideMobileInput: 0 + m_CharacterValidation: 0 + m_CharacterLimit: 0 + m_OnSubmit: + m_PersistentCalls: + m_Calls: [] + m_OnDidEndEdit: + m_PersistentCalls: + m_Calls: [] + m_OnValueChanged: + m_PersistentCalls: + m_Calls: [] + m_CaretColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_CustomCaretColor: 0 + m_SelectionColor: {r: 0.65882355, g: 0.80784315, b: 1, a: 0.7529412} + m_Text: + m_CaretBlinkRate: 0.85 + m_CaretWidth: 1 + m_ReadOnly: 0 + m_ShouldActivateOnSelect: 1 +--- !u!114 &1966107898 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1966107895} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.31764707, g: 0.6431373, b: 0.31764707, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10911, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1966107899 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1966107895} + m_CullTransparentMesh: 1 +--- !u!1 &2015159264 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2015159267} + - component: {fileID: 2015159266} + - component: {fileID: 2015159265} + m_Layer: 0 + m_Name: EventSystem + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &2015159265 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2015159264} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3} + m_Name: + m_EditorClassIdentifier: + m_SendPointerHoverToParent: 1 + m_HorizontalAxis: Horizontal + m_VerticalAxis: Vertical + m_SubmitButton: Submit + m_CancelButton: Cancel + m_InputActionsPerSecond: 10 + m_RepeatDelay: 0.5 + m_ForceModuleActive: 0 +--- !u!114 &2015159266 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2015159264} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_FirstSelected: {fileID: 0} + m_sendNavigationEvents: 1 + m_DragThreshold: 10 +--- !u!4 &2015159267 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2015159264} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &2047786416 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2047786417} + - component: {fileID: 2047786419} + - component: {fileID: 2047786418} + m_Layer: 5 + m_Name: Title + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2047786417 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2047786416} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 332743751} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -0.000025749, y: 15.999993} + m_SizeDelta: {x: 160, y: 30} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &2047786418 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2047786416} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 16 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 1 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Downloading model... +--- !u!222 &2047786419 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2047786416} + m_CullTransparentMesh: 1 +--- !u!1 &2091685446 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2091685447} + - component: {fileID: 2091685449} + - component: {fileID: 2091685448} + m_Layer: 5 + m_Name: AIImage + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2091685447 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2091685446} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 887085509} + m_Father: {fileID: 1084608231} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: -197.7609} + m_SizeDelta: {x: 400, y: 100} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &2091685448 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2091685446} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.53662825, g: 0.53662825, b: 0.9029823, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &2091685449 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2091685446} + m_CullTransparentMesh: 1 +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 726528679} + - {fileID: 909474453} + - {fileID: 158550917} + - {fileID: 2015159267} + - {fileID: 1047848255} + - {fileID: 498662972} + - {fileID: 107963746} diff --git a/Samples~/AndroidDemo/Scene.unity.meta b/Samples~/AndroidDemo/Scene.unity.meta new file mode 100644 index 00000000..124260ce --- /dev/null +++ b/Samples~/AndroidDemo/Scene.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 15abb96f71f7fc08db7606aa88332b00 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/ChatBot/ChatBot.cs b/Samples~/ChatBot/ChatBot.cs index 0aeea759..3a2b1d77 100644 --- a/Samples~/ChatBot/ChatBot.cs +++ b/Samples~/ChatBot/ChatBot.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using LLMUnity; +using UnityEngine.UI; namespace LLMUnitySamples { @@ -18,6 +19,7 @@ public class ChatBot : MonoBehaviour public float textPadding = 10f; public float bubbleSpacing = 10f; public Sprite sprite; + public Button stopButton; private InputBubble inputBubble; private List chatBubbles = new List(); @@ -51,6 +53,7 @@ void Start() inputBubble.AddSubmitListener(onInputFieldSubmit); inputBubble.AddValueChangedListener(onValueChanged); inputBubble.setInteractable(false); + stopButton.gameObject.SetActive(true); _ = llmCharacter.Warmup(WarmUpCallback); } diff --git a/Samples~/ChatBot/Scene.unity b/Samples~/ChatBot/Scene.unity index 07764b61..bea05e22 100644 --- a/Samples~/ChatBot/Scene.unity +++ b/Samples~/ChatBot/Scene.unity @@ -182,6 +182,7 @@ MonoBehaviour: textPadding: 10 bubbleSpacing: 10 sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0} + stopButton: {fileID: 600245850} --- !u!1 &507661347 GameObject: m_ObjectHideFlags: 0 @@ -350,7 +351,7 @@ GameObject: m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 - m_IsActive: 1 + m_IsActive: 0 --- !u!224 &600245849 RectTransform: m_ObjectHideFlags: 0 @@ -663,17 +664,13 @@ MonoBehaviour: numGPULayers: 0 debug: 0 parallelPrompts: -1 - asynchronousStartup: 1 dontDestroyOnLoad: 1 - model: - lora: contextSize: 0 batchSize: 512 - SelectedModel: 0 - modelProgress: 1 - modelCopyProgress: 1 - modelHide: 1 + basePrompt: + model: chatTemplate: chatml + lora: --- !u!1 &1051131186 GameObject: m_ObjectHideFlags: 0 diff --git a/Samples~/KnowledgeBaseGame/KnowledgeBase.asmdef b/Samples~/KnowledgeBaseGame/KnowledgeBase.asmdef new file mode 100644 index 00000000..e8831c36 --- /dev/null +++ b/Samples~/KnowledgeBaseGame/KnowledgeBase.asmdef @@ -0,0 +1,28 @@ +{ + "name": "KnowledgeBase", + "rootNamespace": "", + "references": [ + "undream.llmunity.Runtime", + "undream.RAGSearchUnity.Runtime", + "Unity.Sentis", + "HuggingFace.SharpTransformers", + "Cloud.Unum.USearch" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [ + "RAGSEARCHUNITY" + ], + "versionDefines": [ + { + "name": "ai.undream.ragsearchunity", + "expression": "1.0.0", + "define": "RAGSEARCHUNITY" + } + ], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Samples~/KnowledgeBaseGame/KnowledgeBase.asmdef.meta b/Samples~/KnowledgeBaseGame/KnowledgeBase.asmdef.meta new file mode 100644 index 00000000..35a5d9b7 --- /dev/null +++ b/Samples~/KnowledgeBaseGame/KnowledgeBase.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 42f2ee663135398278c988534b3ae0b3 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/KnowledgeBaseGame/KnowledgeBaseGame.cs b/Samples~/KnowledgeBaseGame/KnowledgeBaseGame.cs index eda4ea21..e6f3bc29 100644 --- a/Samples~/KnowledgeBaseGame/KnowledgeBaseGame.cs +++ b/Samples~/KnowledgeBaseGame/KnowledgeBaseGame.cs @@ -188,6 +188,7 @@ public class KnowledgeBaseGameUI : MonoBehaviour { public Dropdown CharacterSelect; public InputField PlayerText; + public Text SetupText; public Text AIText; public TextAsset ButlerText; @@ -215,6 +216,11 @@ public class KnowledgeBaseGameUI : MonoBehaviour public Dropdown Answer2; public Dropdown Answer3; + void Awake() + { + if (SetupText != null) SetupText.gameObject.SetActive(false); + } + protected void Start() { AddListeners(); diff --git a/Samples~/KnowledgeBaseGame/Scene.unity b/Samples~/KnowledgeBaseGame/Scene.unity index 5f01ed7a..ff10abb9 100644 --- a/Samples~/KnowledgeBaseGame/Scene.unity +++ b/Samples~/KnowledgeBaseGame/Scene.unity @@ -1025,6 +1025,7 @@ MonoBehaviour: m_EditorClassIdentifier: CharacterSelect: {fileID: 1821864875} PlayerText: {fileID: 283994679} + SetupText: {fileID: 2024418435} AIText: {fileID: 681685488} ButlerText: {fileID: 4900000, guid: c2c64001c142dfdbab3c68bbf78335ed, type: 3} MaidText: {fileID: 4900000, guid: 70839d4b753f9ff66a48a0d931db2629, type: 3} @@ -2856,6 +2857,7 @@ RectTransform: - {fileID: 964460962} - {fileID: 1723006217} - {fileID: 1468962045} + - {fileID: 2024418434} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} @@ -7236,6 +7238,87 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2019606177} m_CullTransparentMesh: 1 +--- !u!1 &2024418433 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2024418434} + - component: {fileID: 2024418436} + - component: {fileID: 2024418435} + m_Layer: 5 + m_Name: SetupText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2024418434 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2024418433} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 898411689} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -65.892715, y: -447.78424} + m_SizeDelta: {x: 1154.2146, y: 70} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &2024418435 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2024418433} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 0, b: 0, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 30 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 2 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 'The sample requires the RAGSearchUnity asset from: + + https://github.com/undreamai/RAGSearchUnity' +--- !u!222 &2024418436 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2024418433} + m_CullTransparentMesh: 1 --- !u!1 &2038337339 GameObject: m_ObjectHideFlags: 0 @@ -7458,18 +7541,13 @@ MonoBehaviour: numGPULayers: 0 debug: 0 parallelPrompts: -1 - asynchronousStartup: 1 dontDestroyOnLoad: 1 - model: - lora: contextSize: 0 batchSize: 512 basePrompt: - SelectedModel: 0 - modelProgress: 1 - modelCopyProgress: 1 - modelHide: 1 + model: chatTemplate: chatml + lora: --- !u!4 &2142407557 Transform: m_ObjectHideFlags: 0 diff --git a/Samples~/MultipleCharacters/Scene.unity b/Samples~/MultipleCharacters/Scene.unity index 999f08a1..75117583 100644 --- a/Samples~/MultipleCharacters/Scene.unity +++ b/Samples~/MultipleCharacters/Scene.unity @@ -1520,17 +1520,13 @@ MonoBehaviour: numGPULayers: 0 debug: 0 parallelPrompts: -1 - asynchronousStartup: 1 dontDestroyOnLoad: 1 - model: - lora: contextSize: 0 batchSize: 512 - SelectedModel: 0 - modelProgress: 1 - modelCopyProgress: 1 - modelHide: 1 + basePrompt: + model: chatTemplate: chatml + lora: --- !u!4 &1047848255 Transform: m_ObjectHideFlags: 0 diff --git a/Samples~/SimpleInteraction/Scene.unity b/Samples~/SimpleInteraction/Scene.unity index 9e41c779..b4e5e246 100644 --- a/Samples~/SimpleInteraction/Scene.unity +++ b/Samples~/SimpleInteraction/Scene.unity @@ -1043,17 +1043,13 @@ MonoBehaviour: numGPULayers: 0 debug: 0 parallelPrompts: -1 - asynchronousStartup: 1 dontDestroyOnLoad: 1 - model: - lora: contextSize: 0 batchSize: 512 - SelectedModel: 0 - modelProgress: 1 - modelCopyProgress: 1 - modelHide: 1 + basePrompt: + model: chatTemplate: chatml + lora: --- !u!4 &1047848255 Transform: m_ObjectHideFlags: 0 diff --git a/Samples~/SimpleInteraction/TestDownload.cs b/Samples~/SimpleInteraction/TestDownload.cs new file mode 100644 index 00000000..585814d1 --- /dev/null +++ b/Samples~/SimpleInteraction/TestDownload.cs @@ -0,0 +1,59 @@ +using UnityEngine; +using LLMUnity; +using UnityEngine.UI; +using System.Threading.Tasks; +using System.IO; + +namespace LLMUnitySamples +{ + public class TestDownload : MonoBehaviour + { + public InputField playerText; + public Scrollbar progressBar; + public Text progressText; + public Toggle overwriteToggle; + + void Start() + { + playerText.onSubmit.AddListener(onInputFieldSubmit); + } + + void SetProgress(float progress) + { + // Debug.Log(progress); + progressText.text = ((int)(progress * 100)).ToString() + "%"; + progressBar.size = progress; + } + + string path; + void onInputFieldSubmit(string message) + { + string url = message.Trim(); + path = "/tmp/" + Path.GetFileName(url).Split("?")[0]; + playerText.interactable = false; + Debug.Log(overwriteToggle.isOn); + _ = LLMUnitySetup.DownloadFile( + url, path, overwriteToggle.isOn, + CompleteCallback, SetProgress + ); + } + + public void CompleteCallback(string path) + { + Complete(); + } + + public void Complete() + { + playerText.interactable = true; + playerText.Select(); + playerText.text = ""; + } + + public void CancelRequests() + { + LLMUnitySetup.CancelDownload(path); + Complete(); + } + } +} diff --git a/Samples~/SimpleInteraction/TestDownload.cs.meta b/Samples~/SimpleInteraction/TestDownload.cs.meta new file mode 100644 index 00000000..5c4986c8 --- /dev/null +++ b/Samples~/SimpleInteraction/TestDownload.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ca77155fbdc403fffad6c19155d4d894 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/TestLLM.cs b/Tests/Runtime/TestLLM.cs index 60947fcc..b5afc880 100644 --- a/Tests/Runtime/TestLLM.cs +++ b/Tests/Runtime/TestLLM.cs @@ -6,38 +6,52 @@ using System; using System.Collections; using UnityEngine.TestTools; +using System.IO; namespace LLMUnityTests { public class TestLLM { - GameObject gameObject; - LLM llm; - LLMCharacter llmCharacter; + protected GameObject gameObject; + protected LLM llm; + protected LLMCharacter llmCharacter; + protected static string modelUrl = "https://huggingface.co/afrideva/smol_llama-220M-openhermes-GGUF/resolve/main/smol_llama-220m-openhermes.q4_k_m.gguf?download=true"; + protected static string filename = Path.GetFileName(modelUrl).Split("?")[0]; Exception error = null; string prompt = "Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request."; public TestLLM() { + LLMUnitySetup.SetDebugMode(LLMUnitySetup.DebugModeType.All); Task task = Init(); task.Wait(); } - public async Task Init() + public virtual async Task Init() { gameObject = new GameObject(); gameObject.SetActive(false); + await SetLLM(); + SetLLMCharacter(); + gameObject.SetActive(true); + } + + public async Task EmptyTask() + { + await Task.Delay(1); + } + public virtual async Task SetLLM() + { llm = gameObject.AddComponent(); - string modelUrl = "https://huggingface.co/afrideva/smol_llama-220M-openhermes-GGUF/resolve/main/smol_llama-220m-openhermes.q4_k_m.gguf?download=true"; - string modelPath = "LLMUnityTests/smol_llama-220m-openhermes.q4_k_m.gguf"; - string fullModelPath = LLMUnitySetup.GetAssetPath(modelPath); - _ = LLMUnitySetup.DownloadFile(modelUrl, fullModelPath, false, null, null, false); - await llm.SetModel(fullModelPath); + string filename = await LLMManager.DownloadModel(modelUrl); + llm.SetModel(filename); llm.parallelPrompts = 1; llm.SetTemplate("alpaca"); - llm.asynchronousStartup = false; + } + public virtual void SetLLMCharacter() + { llmCharacter = gameObject.AddComponent(); llmCharacter.llm = llm; llmCharacter.playerName = "Instruction"; @@ -47,11 +61,9 @@ public async Task Init() llmCharacter.seed = 0; llmCharacter.stream = false; llmCharacter.numPredict = 20; - - gameObject.SetActive(true); } - public async Task RunTests() + public virtual async Task RunTests() { error = null; try @@ -92,6 +104,7 @@ public IEnumerator RunTestsWait() Debug.LogError(error.ToString()); throw (error); } + OnDestroy(); } public void TestInitParameters(int nkeep, int chats) @@ -127,5 +140,58 @@ public void TestPostChat(int num) { Assert.That(llmCharacter.chat.Count == num); } + + public virtual void OnDestroy() + { + LLMManager.Remove(filename); + } + } + + public class TestLLM_LLMManager_Load : TestLLM + { + public override Task SetLLM() + { + llm = gameObject.AddComponent(); + string sourcePath = Path.Combine(LLMUnitySetup.modelDownloadPath, filename); + filename = LLMManager.LoadModel(sourcePath); + llm.SetModel(filename); + llm.parallelPrompts = 1; + llm.SetTemplate("alpaca"); + return Task.CompletedTask; + } + } + + public class TestLLM_StreamingAssets_Load : TestLLM + { + public override Task SetLLM() + { + llm = gameObject.AddComponent(); + string sourcePath = Path.Combine(LLMUnitySetup.modelDownloadPath, filename); + string targetPath = LLMUnitySetup.GetAssetPath(filename); + if (!File.Exists(targetPath)) File.Copy(sourcePath, targetPath); + llm.SetModel(filename); + llm.parallelPrompts = 1; + llm.SetTemplate("alpaca"); + return Task.CompletedTask; + } + + public override void OnDestroy() + { + string targetPath = LLMUnitySetup.GetAssetPath(filename); + if (!File.Exists(targetPath)) File.Delete(targetPath); + } + } + + public class TestLLM_SetModel_Warning : TestLLM + { + public override Task SetLLM() + { + llm = gameObject.AddComponent(); + string sourcePath = Path.Combine(LLMUnitySetup.modelDownloadPath, filename); + llm.SetModel(sourcePath); + llm.parallelPrompts = 1; + llm.SetTemplate("alpaca"); + return Task.CompletedTask; + } } } diff --git a/Third Party Notices.md b/Third Party Notices.md index 0c482440..ca86d120 100644 --- a/Third Party Notices.md +++ b/Third Party Notices.md @@ -26,6 +26,22 @@ License: [link](https://github.com/Mozilla-Ocho/llamafile/blob/main/LICENSE) The following models can be downloaded with LLMUnity: +### meta-llama/Meta-Llama-3-8B-Instruct + +Developer: Meta
+Origin: [link](https://huggingface.co/meta-llama/Meta-Llama-3-8B-Instruct)
+License Type: "llama3"
+License: [link](https://huggingface.co/meta-llama/Meta-Llama-3-8B/blob/main/LICENSE) + +##### modified by: lmstudio-community/Meta-Llama-3-8B-Instruct-GGUF + +Developer: LM Studio
+Origin: [link](https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF)
+License Type: "llama3"
+License: [link](https://huggingface.co/meta-llama/Meta-Llama-3-8B/blob/main/LICENSE) + +
+ ### mistralai/Mistral-7B-Instruct-v0.2 Developer: Mistral AI
@@ -65,6 +81,15 @@ Origin: [link](https://huggingface.co/TheBloke/OpenHermes-2.5-Mistral-7B-GGUF) License: [link](https://huggingface.co/TheBloke/OpenHermes-2.5-Mistral-7B-GGUF) +
+ +### Qwen/Qwen2-0.5B-Instruct-GGUF + +Developer: Qwen
+Origin: [link](https://huggingface.co/Qwen/Qwen2-0.5B-Instruct-GGUF)
+License Type: "Apache 2.0"
+License: [link](https://huggingface.co/Qwen/Qwen2-0.5B-Instruct-GGUF/blob/main/LICENSE) + --- ## Testing diff --git a/VERSION b/VERSION index f256be60..1defe531 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v2.0.3 +v2.1.0 diff --git a/package.json b/package.json index bb83f63a..4f00e89d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ai.undream.llm", - "version": "2.0.3", + "version": "2.1.0", "displayName": "LLM for Unity", "description": "LLM for Unity allows to run and distribute Large Language Models (LLMs) in the Unity engine.", "unity": "2022.3", @@ -44,6 +44,11 @@ "displayName": "ChatBot", "description": "Interaction between a player and a AI with a UI similar to a messaging app", "path": "Samples~/ChatBot" + }, + { + "displayName": "AndroidDemo", + "description": "Example Android app with an initial screen with model download progress", + "path": "Samples~/AndroidDemo" } ], "author": {