Instructions to use Sky-Kim/moonshine-tiny-ko-unity with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- unity-sentis
How to use Sky-Kim/moonshine-tiny-ko-unity with unity-sentis:
string modelName = "[Your model name here].sentis"; Model model = ModelLoader.Load(Application.streamingAssetsPath + "/" + modelName); IWorker engine = WorkerFactory.CreateWorker(BackendType.GPUCompute, model); // Please see provided C# file for more details
- Notebooks
- Google Colab
- Kaggle
Download Assets/Scripts/ASRRuntimeController.cs from Sky-Kim/moonshine-tiny-ko-unity: direct link, hf CLI and curl.
- Browser
- Download file 12.7 kB
-
https://huggingface.co/Sky-Kim/moonshine-tiny-ko-unity/resolve/main/Assets/Scripts/ASRRuntimeController.cs
- Command line
-
hf download hf://Sky-Kim/moonshine-tiny-ko-unity/Assets/Scripts/ASRRuntimeController.cs
-
curl -L -o ASRRuntimeController.cs https://huggingface.co/Sky-Kim/moonshine-tiny-ko-unity/resolve/main/Assets/Scripts/ASRRuntimeController.cs
12.7 kB
| using System; | |
| using System.Collections; | |
| using System.Text; | |
| using UnityEngine; | |
| using UnityEngine.UI; | |
| public class ASRRuntimeController : MonoBehaviour | |
| { | |
| [] | |
| [] private ASRManager m_ASRManager; | |
| [] private GameObject m_TextBlockPrefab; | |
| [] private Transform m_ContentRoot; | |
| [] private ScrollRect m_ResultScrollRect; | |
| [] private GameObject m_StartButton; | |
| [] private Dropdown m_BackendDropdown; | |
| private Font m_WebGlKoreanFont; | |
| private bool _missingBindingWarningLogged; | |
| private bool _hasPendingBackendSelection; | |
| private bool _startButtonDismissed; | |
| private ASRManager.InferenceBackend _pendingBackendSelection = ASRManager.InferenceBackend.GPUCompute; | |
| private Coroutine _scrollCoroutine; | |
| private GameObject _activeLineItem; | |
| private Text _activeLineText; | |
| private readonly StringBuilder _activeLineBuffer = new StringBuilder(256); | |
| private const string WebGlKoreanFontResourcePath = "NanumGothic-Regular"; | |
| private const string BackendOptionGpuCompute = "GPUCompute"; | |
| private const string BackendOptionCpu = "CPU"; | |
| private const bool STOP_AND_PROCESS_CURRENT_SEGMENT = true; | |
| private const int MAX_RESULT_ITEMS = 100; | |
| private void Awake() | |
| { | |
| if (m_ASRManager == null) | |
| m_ASRManager = FindFirstObjectByType<ASRManager>(); | |
| if (m_ResultScrollRect == null && m_ContentRoot != null) | |
| m_ResultScrollRect = m_ContentRoot.GetComponentInParent<ScrollRect>(); | |
| ApplyWebGlKoreanFontIfNeeded(); | |
| InitializeBackendDropdown(); | |
| if (m_StartButton != null) | |
| m_StartButton.SetActive(false); | |
| } | |
| private void OnEnable() | |
| { | |
| if (m_ASRManager == null) | |
| return; | |
| m_ASRManager.OnStateChanged += OnStateChanged; | |
| m_ASRManager.OnSpeechTextReceived += OnSpeechTextReceived; | |
| } | |
| private void OnDisable() | |
| { | |
| if (_scrollCoroutine != null) | |
| { | |
| StopCoroutine(_scrollCoroutine); | |
| _scrollCoroutine = null; | |
| } | |
| if (m_ASRManager == null) | |
| return; | |
| m_ASRManager.OnStateChanged -= OnStateChanged; | |
| m_ASRManager.OnSpeechTextReceived -= OnSpeechTextReceived; | |
| } | |
| private void Start() | |
| { | |
| ApplyWebGlKoreanFontToExistingItems(); | |
| TryApplyPendingBackendSelection(); | |
| } | |
| public void ToggleListeningFromButton() | |
| { | |
| if (m_ASRManager == null) | |
| return; | |
| if (!_startButtonDismissed) | |
| { | |
| _startButtonDismissed = true; | |
| if (m_StartButton != null) | |
| m_StartButton.SetActive(false); | |
| } | |
| if (m_ASRManager.currentState == ASRManager.State.Listening || | |
| m_ASRManager.currentState == ASRManager.State.Speaking) | |
| { | |
| m_ASRManager.StopListening(STOP_AND_PROCESS_CURRENT_SEGMENT); | |
| return; | |
| } | |
| m_ASRManager.Listen(); | |
| } | |
| private void OnStateChanged(ASRManager.State state) | |
| { | |
| if (m_StartButton != null) | |
| { | |
| var isReady = !_startButtonDismissed && | |
| (state == ASRManager.State.Ready || | |
| state == ASRManager.State.Listening || | |
| state == ASRManager.State.Speaking || | |
| state == ASRManager.State.STTProcessing); | |
| m_StartButton.SetActive(isReady); | |
| } | |
| TryApplyPendingBackendSelection(); | |
| } | |
| private void OnSpeechTextReceived(string text) | |
| { | |
| if (string.IsNullOrWhiteSpace(text)) | |
| return; | |
| AppendStreamingText(text); | |
| } | |
| private bool TryCreateResultItem(string text, out GameObject item, out Text resultText) | |
| { | |
| item = null; | |
| resultText = null; | |
| if (m_TextBlockPrefab == null || m_ContentRoot == null) | |
| { | |
| if (!_missingBindingWarningLogged) | |
| { | |
| Debug.LogWarning("[ASRRuntimeController] Assign TextBlock prefab and Content transform."); | |
| _missingBindingWarningLogged = true; | |
| } | |
| return false; | |
| } | |
| item = Instantiate(m_TextBlockPrefab, m_ContentRoot); | |
| item.transform.SetAsLastSibling(); | |
| resultText = item.GetComponentInChildren<Text>(true); | |
| if (resultText == null) | |
| { | |
| Debug.LogWarning("[ASRRuntimeController] Text component not found in TextBlock prefab."); | |
| Destroy(item); | |
| item = null; | |
| return false; | |
| } | |
| ApplyWebGlKoreanFontToText(resultText); | |
| resultText.text = text; | |
| resultText.horizontalOverflow = HorizontalWrapMode.Overflow; | |
| resultText.resizeTextForBestFit = false; | |
| TrimOldItemsIfNeeded(); | |
| RequestScrollToBottom(); | |
| return true; | |
| } | |
| private void AppendStreamingText(string chunk) | |
| { | |
| EnsureActiveLine(); | |
| if (_activeLineText == null) | |
| return; | |
| for (var i = 0; i < chunk.Length; i++) | |
| { | |
| var ch = chunk[i]; | |
| if (ch == '\r') | |
| continue; | |
| if (ch == '\n') | |
| { | |
| MoveToNextLine(); | |
| continue; | |
| } | |
| _activeLineBuffer.Append(ch); | |
| if (WouldOverflowLine(_activeLineText, _activeLineBuffer) && _activeLineBuffer.Length > 1) | |
| { | |
| _activeLineBuffer.Length -= 1; | |
| ApplyActiveLineBuffer(); | |
| MoveToNextLine(); | |
| if (char.IsWhiteSpace(ch)) | |
| continue; | |
| _activeLineBuffer.Append(ch); | |
| } | |
| ApplyActiveLineBuffer(); | |
| } | |
| RequestScrollToBottom(); | |
| } | |
| private void EnsureActiveLine() | |
| { | |
| if (_activeLineText != null) | |
| return; | |
| if (!TryCreateResultItem(string.Empty, out _activeLineItem, out _activeLineText)) | |
| return; | |
| _activeLineBuffer.Clear(); | |
| SetLineStyle(_activeLineText, isActive: true); | |
| } | |
| private void MoveToNextLine() | |
| { | |
| if (_activeLineText != null) | |
| SetLineStyle(_activeLineText, isActive: false); | |
| _activeLineItem = null; | |
| _activeLineText = null; | |
| _activeLineBuffer.Clear(); | |
| EnsureActiveLine(); | |
| } | |
| private void ApplyActiveLineBuffer() | |
| { | |
| if (_activeLineText == null) | |
| return; | |
| _activeLineText.text = _activeLineBuffer.ToString(); | |
| } | |
| private bool WouldOverflowLine(Text textComponent, StringBuilder candidateBuffer) | |
| { | |
| if (textComponent == null || candidateBuffer == null) | |
| return false; | |
| var maxWidth = ResolveLineWidth(textComponent); | |
| if (maxWidth <= 0f) | |
| return false; | |
| var settings = textComponent.GetGenerationSettings(new Vector2(maxWidth, float.MaxValue)); | |
| var preferredWidth = | |
| textComponent.cachedTextGeneratorForLayout.GetPreferredWidth(candidateBuffer.ToString(), settings) / | |
| textComponent.pixelsPerUnit; | |
| return preferredWidth > maxWidth + 0.5f; | |
| } | |
| private float ResolveLineWidth(Text textComponent) | |
| { | |
| var textWidth = textComponent.rectTransform.rect.width; | |
| if (textWidth > 0f) | |
| return textWidth; | |
| if (textComponent.transform.parent is RectTransform parentRect && parentRect.rect.width > 0f) | |
| return parentRect.rect.width; | |
| if (m_ContentRoot is RectTransform contentRect && contentRect.rect.width > 0f) | |
| return contentRect.rect.width; | |
| return 0f; | |
| } | |
| private static void SetLineStyle(Text lineText, bool isActive) | |
| { | |
| if (lineText == null) | |
| return; | |
| lineText.fontStyle = isActive ? FontStyle.Bold : FontStyle.Normal; | |
| } | |
| private void RequestScrollToBottom() | |
| { | |
| if (_scrollCoroutine != null) | |
| return; | |
| _scrollCoroutine = StartCoroutine(ScrollToBottomCoroutine()); | |
| } | |
| private IEnumerator ScrollToBottomCoroutine() | |
| { | |
| yield return new WaitForEndOfFrame(); | |
| _scrollCoroutine = null; | |
| if (m_ResultScrollRect == null && m_ContentRoot != null) | |
| m_ResultScrollRect = m_ContentRoot.GetComponentInParent<ScrollRect>(); | |
| if (m_ResultScrollRect == null) | |
| yield break; | |
| if (m_ResultScrollRect.content != null) | |
| LayoutRebuilder.ForceRebuildLayoutImmediate(m_ResultScrollRect.content); | |
| Canvas.ForceUpdateCanvases(); | |
| m_ResultScrollRect.verticalNormalizedPosition = 0f; | |
| } | |
| private void TrimOldItemsIfNeeded() | |
| { | |
| if (m_ContentRoot == null) | |
| return; | |
| var excessCount = m_ContentRoot.childCount - MAX_RESULT_ITEMS; | |
| for (var i = 0; i < excessCount; i++) | |
| { | |
| var oldest = m_ContentRoot.GetChild(0); | |
| oldest.SetParent(null); | |
| Destroy(oldest.gameObject); | |
| } | |
| } | |
| private void ApplyWebGlKoreanFontIfNeeded() | |
| { | |
| if (m_WebGlKoreanFont == null) | |
| m_WebGlKoreanFont = Resources.Load<Font>(WebGlKoreanFontResourcePath); | |
| if (m_WebGlKoreanFont == null) | |
| Debug.LogWarning($"[ASRRuntimeController] WebGL Korean font not found at Resources/{WebGlKoreanFontResourcePath}."); | |
| } | |
| private void ApplyWebGlKoreanFontToExistingItems() | |
| { | |
| if (m_ContentRoot == null) | |
| return; | |
| for (var i = 0; i < m_ContentRoot.childCount; i++) | |
| { | |
| var child = m_ContentRoot.GetChild(i); | |
| var text = child.GetComponentInChildren<Text>(true); | |
| ApplyWebGlKoreanFontToText(text); | |
| } | |
| } | |
| private void ApplyWebGlKoreanFontToText(Text textComponent) | |
| { | |
| if (textComponent == null || m_WebGlKoreanFont == null) | |
| return; | |
| textComponent.font = m_WebGlKoreanFont; | |
| } | |
| private void InitializeBackendDropdown() | |
| { | |
| if (m_BackendDropdown == null) | |
| return; | |
| m_BackendDropdown.onValueChanged.RemoveListener(OnBackendDropdownValueChanged); | |
| m_BackendDropdown.onValueChanged.AddListener(OnBackendDropdownValueChanged); | |
| var defaultBackend = ASRManager.InferenceBackend.GPUCompute; | |
| var defaultIndex = GetDropdownIndexForBackend(defaultBackend); | |
| if (defaultIndex >= 0) | |
| m_BackendDropdown.SetValueWithoutNotify(defaultIndex); | |
| _pendingBackendSelection = defaultBackend; | |
| _hasPendingBackendSelection = true; | |
| } | |
| private void OnBackendDropdownValueChanged(int selectedIndex) | |
| { | |
| _startButtonDismissed = false; | |
| _pendingBackendSelection = GetBackendFromDropdown(selectedIndex); | |
| _hasPendingBackendSelection = true; | |
| TryApplyPendingBackendSelection(); | |
| } | |
| private void TryApplyPendingBackendSelection() | |
| { | |
| if (!_hasPendingBackendSelection || m_ASRManager == null) | |
| return; | |
| if (!m_ASRManager.TrySetInferenceBackend(_pendingBackendSelection)) | |
| return; | |
| _hasPendingBackendSelection = false; | |
| } | |
| private ASRManager.InferenceBackend GetBackendFromDropdown(int selectedIndex) | |
| { | |
| if (m_BackendDropdown == null || m_BackendDropdown.options == null || m_BackendDropdown.options.Count == 0) | |
| return ASRManager.InferenceBackend.CPU; | |
| var clamped = Mathf.Clamp(selectedIndex, 0, m_BackendDropdown.options.Count - 1); | |
| var option = m_BackendDropdown.options[clamped].text; | |
| if (IsOptionMatch(option, BackendOptionGpuCompute)) | |
| return ASRManager.InferenceBackend.GPUCompute; | |
| return ASRManager.InferenceBackend.CPU; | |
| } | |
| private int GetDropdownIndexForBackend(ASRManager.InferenceBackend backend) | |
| { | |
| if (m_BackendDropdown == null || m_BackendDropdown.options == null) | |
| return -1; | |
| var expected = backend == ASRManager.InferenceBackend.GPUCompute | |
| ? BackendOptionGpuCompute | |
| : BackendOptionCpu; | |
| for (var i = 0; i < m_BackendDropdown.options.Count; i++) | |
| { | |
| if (IsOptionMatch(m_BackendDropdown.options[i].text, expected)) | |
| return i; | |
| } | |
| return -1; | |
| } | |
| private static bool IsOptionMatch(string actual, string expected) | |
| { | |
| if (string.IsNullOrWhiteSpace(actual) || string.IsNullOrWhiteSpace(expected)) | |
| return false; | |
| return string.Equals(actual, expected, StringComparison.OrdinalIgnoreCase) || | |
| actual.IndexOf(expected, StringComparison.OrdinalIgnoreCase) >= 0; | |
| } | |
| } | |