Skip to content

Commit 7d9b2dc

Browse files
Big changes
- Add automatic agent and manager creation in scene - All required fields in editor windows highlight red if empty - Add more validation in editor scripts - Fix logging
1 parent eb5429e commit 7d9b2dc

File tree

10 files changed

+264
-34
lines changed

10 files changed

+264
-34
lines changed

unity/Editor/BaseAgentController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
using UnityEngine.Networking;
1313
using Whisper.Utils;
1414
using UnityNeuroSpeech.Runtime;
15-
using LogUtils = UnityNeuroSpeech.Utils.LogUtils;
1615
using UnityNeuroSpeech.Utils;
1716
using UnityNeuroSpeech.Shared;
17+
using LogUtils = UnityNeuroSpeech.Utils.LogUtils;
1818

1919
namespace UnityNeuroSpeech.Editor
2020
{

unity/Editor/CreateAgentTemplate.cs

Lines changed: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,95 @@
11
#if UNITY_EDITOR
2-
using UnityNeuroSpeech.Runtime;
3-
using UnityNeuroSpeech.Utils;
2+
using System;
43
using System.IO;
4+
using System.Linq;
55
using UnityEditor;
66
using UnityEngine;
7+
using UnityEngine.UI;
8+
using UnityNeuroSpeech.Runtime;
9+
using UnityNeuroSpeech.Utils;
10+
using Whisper;
11+
using Whisper.Utils;
12+
using LogUtils = UnityNeuroSpeech.Utils.LogUtils;
713

814
namespace UnityNeuroSpeech.Editor
915
{
1016
internal sealed class CreateAgentTemplate : EditorWindow
1117
{
1218
private string _modelName = "qwen3:1.7b", _agentName = "Alex", _systemPrompt = "Your answer must be fewer than 50 words";
1319

20+
private Button _enableMicButton;
21+
private Sprite _enableMicSprite, _disableMicSprite;
22+
private AudioSource _responseAudioSource;
23+
1424
// Commented out to avoid conflicts with the generated CreateAgent.cs file.
1525
// [MenuItem("UnityNeuroSpeech/Create Agent")]
1626
// public static void ShowWindow() => GetWindow<CreateAgent>("CreateAgent");
1727

1828
private void OnGUI()
1929
{
20-
EditorGUILayout.LabelField("Agent parametrs", EditorStyles.boldLabel);
30+
EditorGUILayout.LabelField("Agent parameters", EditorStyles.boldLabel);
31+
32+
if (string.IsNullOrEmpty(_modelName)) GUI.backgroundColor = Color.red;
2133

2234
_modelName = EditorGUILayout.TextField("Model name", _modelName);
35+
36+
GUI.backgroundColor = Color.white;
37+
38+
if (string.IsNullOrEmpty(_agentName)) GUI.backgroundColor = Color.red;
39+
2340
_agentName = EditorGUILayout.TextField("Agent name", _agentName);
2441

42+
GUI.backgroundColor = Color.white;
43+
2544
_systemPrompt = EditorGUILayout.TextField("System prompt", _systemPrompt);
2645

46+
EditorGUILayout.Space(10);
47+
48+
EditorGUILayout.LabelField("Agent component values", EditorStyles.boldLabel);
49+
50+
if (_enableMicButton == null) GUI.backgroundColor = Color.red;
51+
52+
_enableMicButton = (Button)EditorGUILayout.ObjectField(new GUIContent("Microphone enable Button"), _enableMicButton, typeof(Button), true);
53+
54+
GUI.backgroundColor = Color.white;
55+
56+
if (_enableMicSprite == null) GUI.backgroundColor = Color.red;
57+
58+
_enableMicSprite = (Sprite)EditorGUILayout.ObjectField(new GUIContent("Enabled microphone Sprite"), _enableMicSprite, typeof(Sprite), true);
59+
60+
GUI.backgroundColor = Color.white;
61+
62+
if (_disableMicSprite == null) GUI.backgroundColor = Color.red;
63+
64+
_disableMicSprite = (Sprite)EditorGUILayout.ObjectField(new GUIContent("Disabled microphone Sprite"), _disableMicSprite, typeof(Sprite), true);
65+
66+
GUI.backgroundColor = Color.white;
67+
68+
if (_responseAudioSource == null) GUI.backgroundColor = Color.red;
69+
70+
_responseAudioSource = (AudioSource)EditorGUILayout.ObjectField(new GUIContent("Response AudioSource"), _responseAudioSource, typeof(AudioSource), true);
71+
72+
GUI.backgroundColor = Color.white;
73+
2774
if (GUILayout.Button("Generate Agent"))
2875
{
29-
// The foundation. Without this, nothing works.
30-
if (string.IsNullOrEmpty(_modelName) || string.IsNullOrEmpty(_modelName))
76+
if (string.IsNullOrEmpty(_modelName) || string.IsNullOrEmpty(_agentName))
77+
{
78+
LogUtils.LogError("[UnityNeuroSpeech] \"Model name\" and \"Agent name\" can't be empty!");
79+
return;
80+
}
81+
82+
if(_enableMicButton == null || _disableMicSprite == null || _enableMicSprite == null || _responseAudioSource == null)
3183
{
32-
LogUtils.LogError("[UnityNeuroSpeech] \"Model name\" and \"Agent name\" must not be empty!");
84+
LogUtils.LogError("[UnityNeuroSpeech] All values in \"Agent component values\" can't be empty!");
3385
return;
3486
}
3587

36-
SafeExecutionUtils.SafeExecute("CreateAgentSettings", CreateAgentSettings);
88+
SafeExecutionUtils.SafeExecute("CreateAgentSettings", CreateAgentSettings);
3789
SafeExecutionUtils.SafeExecute("CreateAgentController", CreateAgentController);
3890
}
91+
92+
if (GUILayout.Button("Create agent in scene")) SafeExecutionUtils.SafeExecute("CreateAgentInScene", CreateAgentInScene);
3993
}
4094

4195
private void CreateAgentSettings()
@@ -85,6 +139,46 @@ private void CreateAgentController()
85139
AssetDatabase.SaveAssets();
86140
AssetDatabase.Refresh();
87141
}
142+
143+
private void CreateAgentInScene()
144+
{
145+
// Find GameObject in scene
146+
var unsManager = GameObject.Find("UnityNeuroSpeechManager");
147+
148+
if (unsManager == null)
149+
{
150+
LogUtils.LogError("[UnityNeuroSpeech] Create UnityNeuroSpeechManager in your scene!");
151+
return;
152+
}
153+
154+
// Create new GameObject in scene
155+
var agentObj = new GameObject($"{_agentName}Agent");
156+
157+
// Find generated agent controller
158+
var agentControllerType = AppDomain.CurrentDomain.GetAssemblies().SelectMany(asm => asm.GetTypes()).FirstOrDefault(t => t.Name == $"{_agentName}Controller");
159+
160+
if (agentControllerType == null)
161+
{
162+
LogUtils.LogError($"[UnityNeuroSpeech] No {_agentName}Controlller was found!");
163+
return;
164+
}
165+
166+
// Add component with this type
167+
var agentController = agentObj.AddComponent(agentControllerType);
168+
169+
// Find generated agent settings
170+
var settingsPath = $"Assets/UnityNeuroSpeech/Runtime/GeneratedAgents/Agent_{_agentName}.asset";
171+
var generatedAgentSettings = AssetDatabase.LoadAssetAtPath<AgentSettings>(settingsPath);
172+
173+
// Change values in component
174+
agentControllerType.GetField("agentSettings")?.SetValue(agentController, generatedAgentSettings);
175+
agentController.ChangePrivateField("_whisper", unsManager.GetComponent<WhisperManager>());
176+
agentController.ChangePrivateField("_microphoneRecord", unsManager.GetComponent<MicrophoneRecord>());
177+
agentController.ChangePrivateField("_enableMicButton", _enableMicButton);
178+
agentController.ChangePrivateField("_enableMicSprite", _enableMicSprite);
179+
agentController.ChangePrivateField("_disableMicSprite", _disableMicSprite);
180+
agentController.ChangePrivateField("_responseAudioSource", _responseAudioSource);
181+
}
88182
}
89183
}
90184
#endif

unity/Editor/CreateSettings.cs

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -74,33 +74,49 @@ private void OnGUI()
7474

7575
_isFrameworkInAnotherFolder = EditorGUILayout.Toggle(new GUIContent("Not in Assets folder", "If framework isn't in Assets directory, turn it on"), _isFrameworkInAnotherFolder);
7676

77-
if (!_isFrameworkInAnotherFolder) GUI.enabled = false;
77+
if (!_isFrameworkInAnotherFolder)
78+
{
79+
GUI.enabled = false;
80+
GUI.backgroundColor = Color.white;
81+
}
82+
83+
if (_isFrameworkInAnotherFolder && string.IsNullOrEmpty(_anotherFolderName)) GUI.backgroundColor = Color.red;
7884

7985
_anotherFolderName = EditorGUILayout.TextField(new GUIContent("Directory name", "For example, if you throw this framework in Assets\\MyImports\\Frameworks, then write \"MyImports/Frameworks\""), _anotherFolderName);
8086

87+
GUI.backgroundColor = Color.white;
88+
8189
if (!_isFrameworkInAnotherFolder) GUI.enabled = true;
8290

8391
EditorGUILayout.Space(10);
8492

8593
// Agents
8694
EditorGUILayout.LabelField("Agents", EditorStyles.boldLabel);
8795

96+
if (_emotions.Count == 0) GUI.backgroundColor = Color.red;
97+
8898
_emotionsReorderableList.DoLayoutList();
8999

100+
GUI.backgroundColor = Color.white;
101+
102+
if (_requestTimeout == 0) GUI.backgroundColor = Color.red;
103+
90104
_requestTimeout = EditorGUILayout.IntField(new GUIContent("Request timeout(secs)", "Timeout for requests to local TTS Python sever"), _requestTimeout);
91105

106+
GUI.backgroundColor = Color.white;
107+
92108
EditorGUILayout.Space(10);
93109

94110
// Python
95111
EditorGUILayout.LabelField("Python", EditorStyles.boldLabel);
96112

97113
_enablePythonDebug = EditorGUILayout.Toggle(new GUIContent("Enable Python debug", "If framework isn't in Assets directory, turn it on"), _enablePythonDebug);
98114

99-
if (!_enablePythonDebug) GUI.enabled = false;
115+
if (string.IsNullOrEmpty(_absolutePathToMainPy)) GUI.backgroundColor = Color.red;
100116

101117
_absolutePathToMainPy = EditorGUILayout.TextField(new GUIContent("Absolute path to main.py"), _absolutePathToMainPy);
102118

103-
if (!_enablePythonDebug) GUI.enabled = true;
119+
GUI.backgroundColor = Color.white;
104120

105121
EditorGUILayout.Space(10);
106122

@@ -122,13 +138,43 @@ private void OnGUI()
122138
_ => LogLevel.All
123139
};
124140

141+
if (_emotions.Count == 0 || _emotions.Contains(string.Empty))
142+
{
143+
LogUtils.LogError("[UnityNeuroSpeech] You need to add at least one emotion!");
144+
return;
145+
}
146+
147+
if (_requestTimeout == 0)
148+
{
149+
LogUtils.LogError("[UnityNeuroSpeech] \"Request timeout\" field can't be empty!");
150+
return;
151+
}
152+
153+
if (string.IsNullOrEmpty(_absolutePathToMainPy))
154+
{
155+
LogUtils.LogError("[UnityNeuroSpeech] You need to write absolute path to main.py!");
156+
return;
157+
}
158+
159+
if (!File.Exists(_absolutePathToMainPy))
160+
{
161+
LogUtils.LogError("[UnityNeuroSpeech] main.py doesn't exist in this path!");
162+
return;
163+
}
164+
125165
string createAgentScriptContent, createAgentScriptPath;
126166

127167
if (_isFrameworkInAnotherFolder)
128168
{
129169
// If the framework is placed in another folder, we'll have to change paths in multiple places.
130170

131-
// First, remove the old CreateAgent.cs and copy a template version.
171+
if (string.IsNullOrEmpty(_anotherFolderName))
172+
{
173+
LogUtils.LogError("[UnityNeuroSpeech] If you enabled \"Not in Assets folder\", you need to write \"Directory name\"?");
174+
return;
175+
}
176+
177+
// Remove the old CreateAgent.cs and copy a template version.
132178
// Later, we’ll replace parts of it to fit the custom folder structure.
133179
AssetDatabase.DeleteAsset($"Assets/{_anotherFolderName}/UnityNeuroSpeech/Editor/CreateAgent.cs");
134180
AssetDatabase.CopyAsset($"Assets/{_anotherFolderName}/UnityNeuroSpeech/Editor/CreateAgentTemplate.cs", $"Assets/{_anotherFolderName}/UnityNeuroSpeech/Editor/CreateAgent.cs");
@@ -155,18 +201,12 @@ private void OnGUI()
155201
createAgentScriptContent = File.ReadAllText(createAgentScriptPath);
156202
}
157203

158-
if (_emotions.Count == 0)
159-
{
160-
LogUtils.LogError("[UnityNeuroSpeech] You need to add at least one emotion!");
161-
return;
162-
}
163-
164204
// Flatten all emotions into a single comma-separated string.
165205
var emotionsString = "";
166206
foreach (var em in _emotions) emotionsString += $"<{em}>, ";
167207

168208
// Replace the system prompt to explicitly instruct the model to use only these emotions.
169-
// (Note: some smaller models might still mess up, even with strict prompts.)
209+
// (Note: some smaller models might still mess up, even with strict prompts)
170210
createAgentScriptContent = createAgentScriptContent.Replace("For example: <angry>, <happy>, <sad>, etc.", $"You can only use these emotions: {emotionsString}. WRITE THEM ONLY LIKE I SAID.");
171211

172212
// Turn the template into a real editor window script.
@@ -179,17 +219,19 @@ private void OnGUI()
179219

180220
// Changing debug in Python
181221
string mainPyContent;
222+
182223
mainPyContent = File.ReadAllText(_absolutePathToMainPy);
183224

184225
if (_enablePythonDebug)
185226
{
186227
// If debug already enabled
187-
if (mainPyContent.Contains("# warnings.simplefilter(action='ignore', category=FutureWarning)")) return;
188-
189-
mainPyContent = mainPyContent.Replace("warnings.simplefilter(action='ignore', category=FutureWarning", "# warnings.simplefilter(action='ignore', category=FutureWarning)");
190-
mainPyContent = mainPyContent.Replace("sys.stdout = open(os.devnull, 'w')", "# sys.stdout = open(os.devnull, 'w')");
191-
mainPyContent = mainPyContent.Replace("logging.disable(logging.CRITICAL)", "# logging.disable(logging.CRITICAL)");
192-
mainPyContent = mainPyContent.Replace("# print(f\"Python executable(for gebug): {sys.executable}\")", "print(f\"Python executable(for gebug): {sys.executable}\")");
228+
if (!mainPyContent.Contains("# warnings.simplefilter(action='ignore', category=FutureWarning)"))
229+
{
230+
mainPyContent = mainPyContent.Replace("warnings.simplefilter(action='ignore', category=FutureWarning", "# warnings.simplefilter(action='ignore', category=FutureWarning)");
231+
mainPyContent = mainPyContent.Replace("sys.stdout = open(os.devnull, 'w')", "# sys.stdout = open(os.devnull, 'w')");
232+
mainPyContent = mainPyContent.Replace("logging.disable(logging.CRITICAL)", "# logging.disable(logging.CRITICAL)");
233+
mainPyContent = mainPyContent.Replace("# print(f\"Python executable(for gebug): {sys.executable}\")", "print(f\"Python executable(for gebug): {sys.executable}\")");
234+
}
193235
}
194236
else
195237
{
@@ -198,13 +240,17 @@ private void OnGUI()
198240
mainPyContent = mainPyContent.Replace("# logging.disable(logging.CRITICAL)", "logging.disable(logging.CRITICAL)");
199241
mainPyContent = mainPyContent.Replace("print(f\"Python executable(for gebug): {sys.executable}\")", "# print(f\"Python executable(for gebug): {sys.executable}\")");
200242
}
243+
201244
File.WriteAllText(_absolutePathToMainPy, mainPyContent);
202245

203246
// Save the settings into a JSON file(unreadable code moment)
204247
var data = new JsonData(LogUtils.logLevel, string.IsNullOrEmpty(_customOllamaURI) ? "http://localhost:11434" : _customOllamaURI, string.IsNullOrEmpty(_customTTSURI) ? "http://localhost:7777" : _customTTSURI, _requestTimeout);
205248

206249
var json = JsonUtility.ToJson(data, true);
207-
File.WriteAllText("Assets/Resources/Settings/UnityNeuroSpeechSettings.json", json);
250+
251+
var dir = Application.dataPath + "/Resources/Settings";
252+
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
253+
File.WriteAllText(Path.Combine(dir, "UnityNeuroSpeechSettings.json"), json);
208254

209255
AssetDatabase.SaveAssets();
210256
AssetDatabase.Refresh();

unity/Editor/LoadSettings.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
#if UNITY_EDITOR
23
using UnityNeuroSpeech.Utils;
34
using UnityEditor;
@@ -19,9 +20,10 @@ private static void LoadFrameworkSettings()
1920
// Attempts to access the JSON settings file
2021
try
2122
{
22-
dataText = Resources.Load<TextAsset>("Settings/UnityNeuroSpeechSettings").text;
23+
dataText = Resources.Load<TextAsset>("Settings/UnityNeuroSpeechSettings").text;
2324
}
24-
catch{
25+
catch
26+
{
2527
LogUtils.LogMessage("[UnityNeuroSpeech] No settings file was found.");
2628
return;
2729
}
@@ -40,4 +42,4 @@ private static void LoadFrameworkSettings()
4042

4143
}
4244
}
43-
#endif
45+
#endif

0 commit comments

Comments
 (0)