Mate-Engine/Assets/LLMUnity/Editor/LLMEditor.cs
shinyflvre 39a7b41d7d 1.4.0 Pre Release
**Changelog 1.4.0 – THE LOCAL AI Update**

**Disclaimer:**
The AI runs *entirely locally* and is designed to be environmentally and ethically responsible.
- It does **not** harm nature or exploit any artists.
- It has only **1.5 billion parameters**, making it a **very small model** by modern standards.
- It consumes as much GPU energy as watching a **1440p 60fps YouTube video**.
- It was **not trained on artist data**; it uses personal input and public Wikipedia datasets.
- It has its own personality, and I ensured it adheres to **ethical standards**.

---

### New Features

- Added **Qwen 2.5 1.5B LLM** to MateEngine!
  - Press **Middle Mouse Button** while hovering over the pet to open the chat window.
  - Runs fully locally on your GPU — no internet connection required.
  - Only uses GPU while processing prompts.
  - Note: This is a **lightweight model** and won’t handle complex tasks or coding. It’s just a small, fun feature!

- Added **Resize Button** – Quickly adjust app size for 1080p, 1440p, and 4K monitors.

- Added **VRM Information Panel** – Displays polygon count, VRM version, and bone status:
  - **Perfect** – Properly exported model.
  - **Failure** – Model has export issues; animations may break.

- Added **Options Submenu**.
- Added **Volume Sliders** – Independently control pet, effects, and menu volume.
- Added **Graphics Settings**.
- Added **Dance Trails** – Glowing effects on the pet’s hands during dance. Toggle on/off as desired.
- Improved **Music Source Selector** – You can now whitelist or add custom audio sources for dancing.
- Added **MateEngine Version Info** – View the current app version from the menu.
- Menu can now be opened with **M** key or **Right-Click**.
- Added full support for **VRM 1.x** and **VRM 0.x** models.
- Introduced **VRM Validator Tool** in Unity Editor – Great for content creators.

---

### Improvements & Fixes

- Major **Performance Optimizations**.
- Fixed a **GC Error** that could appear after 6–12 hours of continuous use.
- Improved **Hand Tracking** – Reduced stuttering in some animations.
- Added **Discord Rich Presence** integration.
- Added **Glowing Halo** for Steam users as exclusive DLC!
2025-04-08 19:16:19 +02:00

491 lines
20 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
namespace LLMUnity
{
[CustomEditor(typeof(LLM), true)]
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<string> modelNames;
static List<string> modelOptions;
static List<string> modelLicenses;
static List<string> modelURLs;
string elementFocus = "";
bool showCustomURL = false;
string customURL = "";
bool customURLLora = false;
bool customURLFocus = false;
bool expandedView = false;
public void AddSecuritySettings(SerializedObject llmScriptSO, LLM llmScript)
{
void AddSSLLoad(string type, Callback<string> setterCallback)
{
if (GUILayout.Button("Load SSL " + type, GUILayout.Width(buttonWidth)))
{
EditorApplication.delayCall += () =>
{
string path = EditorUtility.OpenFilePanel("Select a SSL " + type + " file", "", "");
if (!string.IsNullOrEmpty(path)) setterCallback(path);
};
}
}
void AddSSLInfo(string propertyName, string type, Callback<string> setterCallback)
{
string path = llmScriptSO.FindProperty(propertyName).stringValue;
if (path != "")
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("SSL " + type + " path", path);
if (GUILayout.Button(trashIcon, GUILayout.Height(actionColumnWidth), GUILayout.Width(actionColumnWidth))) setterCallback("");
EditorGUILayout.EndHorizontal();
}
}
EditorGUILayout.LabelField("Server Security Settings", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(llmScriptSO.FindProperty("APIKey"));
if (llmScriptSO.FindProperty("advancedOptions").boolValue)
{
EditorGUILayout.BeginHorizontal();
AddSSLLoad("certificate", llmScript.SetSSLCert);
AddSSLLoad("key", llmScript.SetSSLKey);
EditorGUILayout.EndHorizontal();
AddSSLInfo("SSLCertPath", "certificate", llmScript.SetSSLCert);
AddSSLInfo("SSLKeyPath", "key", llmScript.SetSSLKey);
}
Space();
}
public void AddModelLoadersSettings(SerializedObject llmScriptSO, LLM llmScript)
{
EditorGUILayout.LabelField("Model Settings", EditorStyles.boldLabel);
AddModelLoaders(llmScriptSO, llmScript);
AddModelSettings(llmScriptSO);
}
public void AddModelLoaders(SerializedObject llmScriptSO, LLM llmScript)
{
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 override void AddModelSettings(SerializedObject llmScriptSO)
{
List<Type> attributeClasses = new List<Type> { typeof(ModelAttribute) };
if (llmScriptSO.FindProperty("advancedOptions").boolValue)
{
attributeClasses.Add(typeof(ModelAdvancedAttribute));
if (LLMUnitySetup.FullLlamaLib) attributeClasses.Add(typeof(ModelExtrasAttribute));
}
ShowPropertiesOfClass("", llmScriptSO, attributeClasses, false);
Space();
}
void ShowProgress(float progress, string progressText)
{
if (progress != 1) EditorGUI.ProgressBar(EditorGUILayout.GetControlRect(), progress, progressText);
}
static void ResetModelOptions()
{
List<string> existingOptions = new List<string>();
foreach (ModelEntry entry in LLMManager.modelEntries) existingOptions.Add(entry.url);
modelOptions = new List<string>(){"Download model", "Custom URL"};
modelNames = new List<string>(){null, null};
modelURLs = new List<string>(){null, null};
modelLicenses = new List<string>(){null, null};
foreach (var entry in LLMUnitySetup.modelOptions)
{
string category = entry.Key;
foreach ((string name, string url, string license) in entry.Value)
{
if (url != null && existingOptions.Contains(url)) continue;
modelOptions.Add(category + "/" + name);
modelNames.Add(name);
modelURLs.Add(url);
modelLicenses.Add(license);
}
}
}
float[] GetColumnWidths(bool expandedView)
{
List<float> widths = new List<float>(){actionColumnWidth, nameColumnWidth, templateColumnWidth};
if (expandedView) widths.AddRange(new List<float>(){textColumnWidth, textColumnWidth});
widths.AddRange(new List<float>(){includeInBuildColumnWidth, actionColumnWidth});
return widths.ToArray();
}
List<Rect> CreateColumnRects(Rect rect)
{
float[] widths = GetColumnWidths(expandedView);
float offsetX = rect.x;
float offsetY = rect.y + (rect.height - EditorGUIUtility.singleLineHeight) / 2;
List<Rect> rects = new List<Rect>();
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.AddLora(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))
{
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();
if (customURLFocus)
{
customURLFocus = false;
elementFocus = "customURLFocus";
}
}
if (exit || submit)
{
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) LLMUnitySetup.LogWarning($"The {modelNames[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, modelNames[modelIndex]);
SetModelIfNone(filename, false);
UpdateModels(true);
}
if (GUILayout.Button("Load model", GUILayout.Width(buttonWidth)))
{
EditorApplication.delayCall += () =>
{
string path = EditorUtility.OpenFilePanelWithFilters("Select a gguf model file", "", new string[] { "Model Files", "gguf" });
if (!string.IsNullOrEmpty(path))
{
string filename = LLMManager.LoadModel(path, true);
SetModelIfNone(filename, false);
UpdateModels();
}
};
}
EditorGUILayout.EndHorizontal();
if (llmScript.advancedOptions)
{
EditorGUILayout.BeginHorizontal();
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 gguf lora file", "", new string[] { "Model Files", "gguf" });
if (!string.IsNullOrEmpty(path))
{
string filename = LLMManager.LoadLora(path, true);
SetModelIfNone(filename, true);
UpdateModels();
}
};
}
EditorGUILayout.EndHorizontal();
}
}
async Task AddLoadButtons()
{
if (showCustomURL) await createCustomURLField();
else await createButtons();
}
void OnEnable()
{
LLM llmScript = (LLM)target;
ResetModelOptions();
trashIcon = new GUIContent(Resources.Load<Texture2D>("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)
{
drawElementCallback = (rect, index, isActive, isFocused) =>
{
if (index >= LLMManager.modelEntries.Count) return;
ModelEntry entry = LLMManager.modelEntries[index];
List<Rect> 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.loraManager.Contains(entry.filename);
bool newSelected = EditorGUI.Toggle(selectRect, isSelected);
if (newSelected && !isSelected) llmScript.AddLora(entry.filename);
else if (!newSelected && isSelected) llmScript.RemoveLora(entry.filename);
}
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))
{
if (isSelected)
{
if (!entry.lora) llmScript.SetModel("");
else llmScript.RemoveLora(entry.filename);
}
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<Rect> 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();
}
}
private void CopyToClipboard(string text)
{
TextEditor te = new TextEditor {text = text};
te.SelectAll();
te.Copy();
}
public void AddExtrasToggle()
{
if (ToggleButton("Use extras", LLMUnitySetup.FullLlamaLib)) LLMUnitySetup.SetFullLlamaLib(!LLMUnitySetup.FullLlamaLib);
}
public override void AddOptionsToggles(SerializedObject llmScriptSO)
{
AddDebugModeToggle();
EditorGUILayout.BeginHorizontal();
AddAdvancedOptionsToggle(llmScriptSO);
AddExtrasToggle();
EditorGUILayout.EndHorizontal();
Space();
}
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(LLMManager.modelProgress, "Model Downloading");
ShowProgress(LLMManager.loraProgress, "LoRA Downloading");
GUI.enabled = LLMUnitySetup.libraryProgress == 1 && LLMManager.modelProgress == 1 && LLMManager.loraProgress == 1;
AddOptionsToggles(llmScriptSO);
AddSetupSettings(llmScriptSO);
if (llmScriptSO.FindProperty("remote").boolValue) AddSecuritySettings(llmScriptSO, llmScript);
AddModelLoadersSettings(llmScriptSO, llmScript);
AddChatSettings(llmScriptSO);
OnInspectorGUIEnd(llmScriptSO);
}
}
}