Initial Commit

This commit is contained in:
2025-07-04 20:33:06 +03:00
commit 8f09347ae0
9219 changed files with 2447903 additions and 0 deletions
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4b1da9fac71952343b124f3771afe034
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,22 @@
{
"name": "Meta.WitAi.TTS.Editor",
"rootNamespace": "",
"references": [
"GUID:4504b1a6e0fdcc3498c30b266e4a63bf",
"GUID:fa958eb9f0171754fb207d563a15ddfa",
"GUID:8bbcefc153e1f1b4a98680670797dd16",
"GUID:1c28d8b71ced07540b7c271537363cc6",
"GUID:5c61c7ae4b0c6f94299e51352f802670"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: e31cc0253d8f956459091c800a16d68d
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 716af1288c1a89d44950d38c999fe096
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,59 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System;
using UnityEngine;
namespace Meta.WitAi.TTS.Preload
{
[Serializable]
public class TTSPreloadPhraseData
{
/// <summary>
/// ID used to identify this phrase
/// </summary>
public string clipID;
/// <summary>
/// Actual phrase to be spoken
/// </summary>
public string textToSpeak;
/// <summary>
/// Meta data for whether clip is downloaded or not
/// </summary>
public bool downloaded;
/// <summary>
/// Meta data for clip download progress
/// </summary>
public float downloadProgress;
}
[Serializable]
public class TTSPreloadVoiceData
{
/// <summary>
/// Specific preset voice settings id to be used with TTSService
/// </summary>
public string presetVoiceID;
/// <summary>
/// All data corresponding to text to speak
/// </summary>
public TTSPreloadPhraseData[] phrases;
}
[Serializable]
public class TTSPreloadData
{
public TTSPreloadVoiceData[] voices;
}
public class TTSPreloadSettings : ScriptableObject
{
[SerializeField] public TTSPreloadData data = new TTSPreloadData();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 80be64a601ff65e41b0a8036ba872084
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,481 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System;
using System.Collections.Generic;
using Meta.WitAi.TTS.Data;
using Meta.WitAi.Utilities;
using UnityEditor;
using UnityEngine;
namespace Meta.WitAi.TTS.Preload
{
[CustomEditor(typeof(TTSPreloadSettings), true)]
public class TTSPreloadSettingsInspector : UnityEditor.Editor
{
// TTS Settings
public TTSPreloadSettings Settings { get; private set; }
// TTS Service
public TTSService TtsService { get; private set; }
private List<string> _ttsVoiceIDs;
// Layout items
public const float ACTION_BTN_INDENT = 15f;
public virtual Texture2D HeaderIcon => WitTexts.HeaderIcon;
public virtual string HeaderUrl => WitTexts.GetAppURL(string.Empty, WitTexts.WitAppEndpointType.Settings);
public virtual string DocsUrl => WitTexts.Texts.WitDocsUrl;
// Layout
public override void OnInspectorGUI()
{
// Get settings
if (Settings != target)
{
Settings = target as TTSPreloadSettings;
}
// Draw header
WitEditorUI.LayoutHeaderButton(HeaderIcon, HeaderUrl, DocsUrl);
GUILayout.Space(WitStyles.HeaderPaddingBottom);
// Layout actions
LayoutPreloadActions();
// Layout data
LayoutPreloadData();
}
// Layout Preload Data
protected virtual void LayoutPreloadActions()
{
// Layout preload actions
EditorGUILayout.Space();
WitEditorUI.LayoutSubheaderLabel("TTS Preload Actions");
// Indent
EditorGUI.indentLevel++;
EditorGUILayout.Space();
// Hide when playing
if (Application.isPlaying)
{
EditorUtility.ClearProgressBar();
WitEditorUI.LayoutErrorLabel("TTS preload actions cannot be performed at runtime.");
EditorGUI.indentLevel--;
return;
}
// Get TTS Service if needed
TtsService = EditorGUILayout.ObjectField("TTS Service", TtsService, typeof(TTSService), true) as TTSService;
if (TtsService == null)
{
TtsService = GameObjectSearchUtility.FindSceneObject<TTSService>(true);
if (TtsService == null)
{
EditorUtility.ClearProgressBar();
WitEditorUI.LayoutErrorLabel("You must add a TTS Service to the loaded scene in order perform TTS actions.");
EditorGUI.indentLevel--;
return;
}
}
if (_ttsVoiceIDs == null)
{
_ttsVoiceIDs = GetVoiceIDs(TtsService);
}
// Begin buttons
EditorGUILayout.Space();
EditorGUILayout.BeginHorizontal();
// Import JSON
GUILayout.Space(ACTION_BTN_INDENT * EditorGUI.indentLevel);
if (WitEditorUI.LayoutTextButton("Refresh Data"))
{
RefreshData();
}
GUILayout.Space(ACTION_BTN_INDENT);
if (WitEditorUI.LayoutTextButton("Import JSON"))
{
EditorUtility.ClearProgressBar();
if (TTSPreloadUtility.ImportData(Settings))
{
RefreshData();
}
}
GUILayout.Space(ACTION_BTN_INDENT);
if (WitEditorUI.LayoutTextButton("Import AutoLoader Data"))
{
EditorUtility.ClearProgressBar();
if (TTSPreloadUtility.ImportPhrases(Settings))
{
RefreshData();
}
}
// Clear disk cache
GUI.enabled = TtsService != null;
EditorGUILayout.Space();
Color col = GUI.color;
GUI.color = Color.red;
if (WitEditorUI.LayoutTextButton("Delete Cache"))
{
EditorUtility.ClearProgressBar();
TTSPreloadUtility.DeleteData(TtsService);
RefreshData();
}
// Preload disk cache
GUILayout.Space(ACTION_BTN_INDENT);
GUI.color = Color.green;
if (WitEditorUI.LayoutTextButton("Preload Cache"))
{
DownloadClips();
}
GUI.color = col;
GUI.enabled = true;
// End buttons
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
// Indent
EditorGUI.indentLevel--;
}
// Refresh
private void RefreshData()
{
TTSPreloadUtility.RefreshPreloadData(TtsService, Settings.data, (p) =>
{
EditorUtility.DisplayProgressBar("TTS Preload Utility", "Refreshing Data", p);
}, (d, l) =>
{
EditorUtility.ClearProgressBar();
EditorUtility.SetDirty(Settings);
Debug.Log($"TTS Preload Utility - Refresh Complete{l}");
});
}
// Download
private void DownloadClips()
{
TTSPreloadUtility.PreloadData(TtsService, Settings.data, (p) =>
{
EditorUtility.DisplayProgressBar("TTS Preload Utility", "Downloading Clips", p);
}, (d, l) =>
{
EditorUtility.ClearProgressBar();
EditorUtility.SetDirty(Settings);
AssetDatabase.Refresh();
Debug.Log($"TTS Preload Utility - Preload Complete{l}");
});
}
// Layout Preload Data
protected virtual void LayoutPreloadData()
{
// For updates
bool updated = false;
// Layout preload items
GUILayout.Space(WitStyles.WindowPaddingBottom);
GUILayout.BeginHorizontal();
WitEditorUI.LayoutSubheaderLabel("TTS Preload Data");
if (WitEditorUI.LayoutTextButton("Add Voice"))
{
AddVoice();
updated = true;
}
GUILayout.EndHorizontal();
EditorGUILayout.Space();
// Indent
EditorGUI.indentLevel++;
// Generate
if (Settings.data == null)
{
Settings.data = new TTSPreloadData();
}
if (Settings.data.voices == null)
{
Settings.data.voices = new TTSPreloadVoiceData[] {new TTSPreloadVoiceData()};
}
// Begin scroll
for (int v = 0; v < Settings.data.voices.Length; v++)
{
if (!LayoutVoiceData(Settings.data, v, ref updated))
{
break;
}
}
// Set dirty
if (updated)
{
EditorUtility.SetDirty(Settings);
}
// Indent
EditorGUI.indentLevel--;
}
// Layout
private bool LayoutVoiceData(TTSPreloadData preloadData, int voiceIndex, ref bool updated)
{
// Indent
EditorGUI.indentLevel++;
// Get data
TTSPreloadVoiceData voiceData = preloadData.voices[voiceIndex];
string voiceID = voiceData.presetVoiceID;
if (string.IsNullOrEmpty(voiceID))
{
voiceID = "No Voice Selected";
}
voiceID = $"{(voiceIndex+1)} - {voiceID}";
// Foldout
GUILayout.BeginHorizontal();
bool show = WitEditorUI.LayoutFoldout(new GUIContent(voiceID), voiceData);
if (!show)
{
GUILayout.EndHorizontal();
EditorGUI.indentLevel--;
return true;
}
// Delete
if (WitEditorUI.LayoutTextButton("Delete Voice"))
{
DeleteVoice(voiceIndex);
GUILayout.EndHorizontal();
EditorGUI.indentLevel--;
updated = true;
return false;
}
// Begin Voice Data
GUILayout.EndHorizontal();
EditorGUI.indentLevel++;
// Voice Text Field
if (TtsService == null || _ttsVoiceIDs == null || _ttsVoiceIDs.Count == 0)
{
WitEditorUI.LayoutTextField(new GUIContent("Voice ID"), ref voiceData.presetVoiceID, ref updated);
}
// Voice Preset Select
else
{
int presetIndex = _ttsVoiceIDs.IndexOf(voiceData.presetVoiceID);
bool presetUpdated = false;
WitEditorUI.LayoutPopup("Voice ID", _ttsVoiceIDs.ToArray(), ref presetIndex, ref presetUpdated);
if (presetUpdated)
{
voiceData.presetVoiceID = _ttsVoiceIDs[presetIndex];
string l = string.Empty;
TTSPreloadUtility.RefreshVoiceData(TtsService, voiceData, null, ref l);
updated = true;
}
}
// Ensure phrases exist
if (voiceData.phrases == null)
{
voiceData.phrases = new TTSPreloadPhraseData[] { };
}
// Phrase Foldout
EditorGUILayout.BeginHorizontal();
bool isLayout = WitEditorUI.LayoutFoldout(new GUIContent($"Phrases ({voiceData.phrases.Length})"),
voiceData.phrases);
if (WitEditorUI.LayoutTextButton("Add Phrase"))
{
TTSPreloadPhraseData lastPhrase = voiceData.phrases.Length == 0 ? null : voiceData.phrases[voiceData.phrases.Length - 1];
voiceData.phrases = AddArrayItem<TTSPreloadPhraseData>(voiceData.phrases, new TTSPreloadPhraseData()
{
textToSpeak = lastPhrase?.textToSpeak,
clipID = lastPhrase?.clipID
});
GUILayout.EndHorizontal();
EditorGUI.indentLevel--;
updated = true;
return false;
}
EditorGUILayout.EndHorizontal();
if (isLayout)
{
for (int p = 0; p < voiceData.phrases.Length; p++)
{
if (!LayoutPhraseData(voiceData, p, ref updated))
{
break;
}
}
}
// End Voice Data
EditorGUILayout.Space();
EditorGUI.indentLevel--;
EditorGUI.indentLevel--;
return true;
}
// Layout phrase data
private bool LayoutPhraseData(TTSPreloadVoiceData voiceData, int phraseIndex, ref bool updated)
{
// Begin Phrase
EditorGUI.indentLevel++;
// Get data
TTSPreloadPhraseData phraseData = voiceData.phrases[phraseIndex];
string title = $"{(phraseIndex+1)} - {phraseData.textToSpeak}";
// Foldout
GUILayout.BeginHorizontal();
bool show = WitEditorUI.LayoutFoldout(new GUIContent(title), phraseData);
if (!show)
{
GUILayout.EndHorizontal();
EditorGUI.indentLevel--;
return true;
}
// Delete
if (WitEditorUI.LayoutTextButton("Delete Phrase"))
{
voiceData.phrases = DeleteArrayItem<TTSPreloadPhraseData>(voiceData.phrases, phraseIndex);
GUILayout.EndHorizontal();
EditorGUI.indentLevel--;
updated = true;
return false;
}
// Begin phrase Data
GUILayout.EndHorizontal();
EditorGUI.indentLevel++;
// Phrase
bool phraseChange = false;
WitEditorUI.LayoutTextField(new GUIContent("Phrase"), ref phraseData.textToSpeak, ref phraseChange);
if (phraseChange)
{
TTSPreloadUtility.RefreshPhraseData(TtsService, new TTSDiskCacheSettings()
{
DiskCacheLocation = TTSDiskCacheLocation.Preload
}, TtsService?.GetPresetVoiceSettings(voiceData.presetVoiceID), phraseData);
updated = true;
}
// Clip
string clipID = phraseData.clipID;
WitEditorUI.LayoutTextField(new GUIContent("Clip ID"), ref clipID, ref phraseChange);
// State
Color col = GUI.color;
Color stateColor = Color.green;
string stateValue = "Downloaded";
if (!phraseData.downloaded)
{
if (phraseData.downloadProgress <= 0f)
{
stateColor = Color.red;
stateValue = "Missing";
}
else
{
stateColor = Color.yellow;
stateValue = $"Downloading {(phraseData.downloadProgress * 100f):00.0}%";
}
}
GUI.color = stateColor;
WitEditorUI.LayoutKeyLabel("State", stateValue);
GUI.color = col;
// End Phrase
EditorGUILayout.Space();
EditorGUI.indentLevel--;
EditorGUI.indentLevel--;
return true;
}
// Add
private T[] AddArrayItem<T>(T[] array, T item) => EditArray<T>(array, (l) => l.Add(item));
// Delete
private T[] DeleteArrayItem<T>(T[] array, int index) => EditArray<T>(array, (l) => l.RemoveAt(index));
// Edit array
private T[] EditArray<T>(T[] array, Action<List<T>> edit)
{
// Generate list
List<T> list = new List<T>();
// Add array to list
if (array != null)
{
list.AddRange(array);
}
// Call edit action
edit(list);
// Set to array
T[] result = list.ToArray();
// Refresh foldout value
WitEditorUI.SetFoldoutValue(result, WitEditorUI.GetFoldoutValue(array));
// Return array
return result;
}
//
private void AddVoice()
{
List<TTSPreloadVoiceData> voices = new List<TTSPreloadVoiceData>();
if (Settings?.data?.voices != null)
{
voices.AddRange(Settings.data.voices);
}
voices.Add(new TTSPreloadVoiceData()
{
presetVoiceID = _ttsVoiceIDs == null || _ttsVoiceIDs.Count == 0 ? "" : _ttsVoiceIDs[0],
phrases = new TTSPreloadPhraseData[] { new TTSPreloadPhraseData() }
});
Settings.data.voices = voices.ToArray();
}
// Delete voice
private void DeleteVoice(int index)
{
// Invalid
if (Settings?.data?.voices == null || index < 0 || index >= Settings.data.voices.Length)
{
return;
}
// Cancelled
if (!EditorUtility.DisplayDialog("Delete Voice?",
$"Are you sure you would like to remove voice data:\n#{(index + 1)} - {Settings.data.voices[index].presetVoiceID}?",
"Okay", "Cancel"))
{
return;
}
// Remove
List<TTSPreloadVoiceData> voices = new List<TTSPreloadVoiceData>(Settings.data.voices);
voices.RemoveAt(index);
Settings.data.voices = voices.ToArray();
}
// Get voice ids
private List<string> GetVoiceIDs(TTSService service)
{
List<string> results = new List<string>();
if (service != null)
{
foreach (var voiceSetting in service.GetAllPresetVoiceSettings())
{
if (voiceSetting != null && !string.IsNullOrEmpty(voiceSetting.SettingsId) &&
!results.Contains(voiceSetting.SettingsId))
{
results.Add(voiceSetting.SettingsId);
}
}
}
return results;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: af9c44712a1f27646a9538dbb053e961
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,632 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System;
using System.Collections;
using System.IO;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using Meta.WitAi.TTS.Data;
using Meta.WitAi.Data.Configuration;
using Meta.WitAi.Json;
using Meta.WitAi.TTS.Utilities;
using UnityEngine.SceneManagement;
namespace Meta.WitAi.TTS.Preload
{
public static class TTSPreloadUtility
{
#region MANAGEMENT
/// <summary>
/// Create a new preload settings asset by prompting a save location
/// </summary>
public static TTSPreloadSettings CreatePreloadSettings()
{
string savePath = WitConfigurationUtility.GetFileSaveDirectory("Save TTS Preload Settings", "TTSPreloadSettings", "asset");
return CreatePreloadSettings(savePath);
}
/// <summary>
/// Create a new preload settings asset at specified location
/// </summary>
public static TTSPreloadSettings CreatePreloadSettings(string savePath)
{
// Ignore if empty
if (string.IsNullOrEmpty(savePath))
{
return null;
}
// Get asset path
string assetPath = savePath.Replace("\\", "/");
if (!assetPath.StartsWith(Application.dataPath))
{
VLog.E(
$"TTS Preload Utility - Cannot Create Setting Outside of Assets Directory\nPath: {assetPath}");
return null;
}
assetPath = assetPath.Replace(Application.dataPath, "Assets");
// Generate & save
TTSPreloadSettings settings = ScriptableObject.CreateInstance<TTSPreloadSettings>();
AssetDatabase.CreateAsset(settings, assetPath);
AssetDatabase.SaveAssets();
// Reload & return
return AssetDatabase.LoadAssetAtPath<TTSPreloadSettings>(assetPath);
}
/// <summary>
/// Find all preload settings currently in the Assets directory
/// </summary>
public static TTSPreloadSettings[] GetPreloadSettings()
{
List<TTSPreloadSettings> results = new List<TTSPreloadSettings>();
string[] guids = AssetDatabase.FindAssets("t:TTSPreloadSettings");
foreach (var guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
TTSPreloadSettings settings = AssetDatabase.LoadAssetAtPath<TTSPreloadSettings>(path);
results.Add(settings);
}
return results.ToArray();
}
#endregion
#region ITERATE
// Performer
public static CoroutineUtility.CoroutinePerformer _performer;
//
public delegate IEnumerator TTSPreloadIterateDelegate(TTSService service, TTSDiskCacheSettings cacheSettings, TTSVoiceSettings voiceSettings, TTSPreloadPhraseData phraseData, Action<float> onProgress, Action<string> onComplete);
// Iterating
public static bool IsIterating()
{
return _performer != null;
}
// Perform a check of all data
private static bool CheckIterateData(TTSService service, TTSPreloadData preloadData, TTSPreloadIterateDelegate onIterate, Action<float> onProgress, Action<string> onComplete)
{
// No service
if (service == null)
{
onProgress?.Invoke(1f);
onComplete?.Invoke("\nNo TTSService found in current scene");
return false;
}
// No preload data
if (preloadData == null)
{
onProgress?.Invoke(1f);
onComplete?.Invoke("\nTTS Preload Data Not Found");
return false;
}
// No preload data
if (preloadData.voices == null)
{
onProgress?.Invoke(1f);
onComplete?.Invoke("\nTTS Preload Data Voices Not Found");
return false;
}
// Ignore if running
if (Application.isPlaying)
{
onProgress?.Invoke(1f);
onComplete?.Invoke("Cannot preload while running");
return false;
}
// Ignore if running
if (onIterate == null)
{
onProgress?.Invoke(1f);
onComplete?.Invoke("Code recompiled mid update");
return false;
}
return true;
}
// Iterate phrases
private static void IteratePhrases(TTSService service, TTSPreloadData preloadData, TTSPreloadIterateDelegate onIterate, Action<float> onProgress, Action<string> onComplete)
{
// Skip if check fails
if (!CheckIterateData(service, preloadData, onIterate, onProgress, onComplete))
{
return;
}
// Unload previous coroutine performer
if (_performer != null)
{
_performer.gameObject.DestroySafely();
_performer = null;
}
// Run new coroutine
_performer = CoroutineUtility.StartCoroutine(PerformIteratePhrases(service, preloadData, onIterate, onProgress, onComplete));
}
// Perform iterate
private static IEnumerator PerformIteratePhrases(TTSService service, TTSPreloadData preloadData, TTSPreloadIterateDelegate onIterate, Action<float> onProgress, Action<string> onComplete)
{
// Get cache settings
TTSDiskCacheSettings cacheSettings = new TTSDiskCacheSettings()
{
DiskCacheLocation = TTSDiskCacheLocation.Preload
};
// Get total phrases
int phraseTotal = 0;
foreach (var voice in preloadData.voices)
{
if (voice.phrases == null)
{
continue;
}
foreach (var phrase in voice.phrases)
{
phraseTotal++;
}
}
// Begin
onProgress?.Invoke(0f);
// Iterate
int phraseCount = 0;
float phraseInc = 1f / (float)phraseTotal;
string log = string.Empty;
for (int v = 0; v < preloadData.voices.Length; v++)
{
// Get voice data
TTSPreloadVoiceData voiceData = preloadData.voices[v];
if (voiceData.phrases == null)
{
continue;
}
// Get voice
TTSVoiceSettings voiceSettings = service.GetPresetVoiceSettings(voiceData.presetVoiceID);
if (voiceSettings == null)
{
log += "\n-Missing Voice Setting: " + voiceData.presetVoiceID;
phraseCount += voiceData.phrases.Length;
continue;
}
// Iterate phrases
for (int p = 0; p < voiceData.phrases.Length; p++)
{
// Iterate progress
float progress = (float) phraseCount / (float) phraseTotal;
onProgress?.Invoke(progress);
phraseCount++;
// Iterate Load
yield return onIterate(service, cacheSettings, voiceSettings, voiceData.phrases[p],
(p2) => onProgress?.Invoke(progress + p2 * phraseInc), (l) => log += l);
// Skip if check fails
if (!CheckIterateData(service, preloadData, onIterate, onProgress, onComplete))
{
yield break;
}
}
}
// Complete
onProgress?.Invoke(1f);
onComplete?.Invoke(log);
}
#endregion
#region PRELOAD
// Can preload data
public static bool CanPreloadData()
{
return TTSService.Instance != null;
}
// Preload from data
public static void PreloadData(TTSService service, TTSPreloadData preloadData, Action<float> onProgress, Action<TTSPreloadData, string> onComplete)
{
IteratePhrases(service, preloadData, PreloadPhraseData, onProgress, (l) => onComplete?.Invoke(preloadData, l));
}
// Preload voice text
private static IEnumerator PreloadPhraseData(TTSService service, TTSDiskCacheSettings cacheSettings, TTSVoiceSettings voiceSettings, TTSPreloadPhraseData phraseData, Action<float> onProgress, Action<string> onComplete)
{
// Begin running
bool running = true;
// Download
string log = string.Empty;
service.DownloadToDiskCache(phraseData.textToSpeak, string.Empty, voiceSettings, cacheSettings, delegate(TTSClipData data, string path, string error)
{
// Set phrase data
phraseData.clipID = data.clipID;
phraseData.downloaded = string.IsNullOrEmpty(error);
// Failed
if (!phraseData.downloaded)
{
log += $"\n-{voiceSettings.SettingsId} Preload Failed: {phraseData.textToSpeak}";
}
// Next
running = false;
});
// Wait for running to complete
while (running)
{
//Debug.Log($"Preload Wait: {voiceSettings.SettingsId} - {phraseData.textToSpeak}");
yield return null;
}
// Invoke
onComplete?.Invoke(log);
}
#endregion
#region REFRESH
// Refresh
public static void RefreshPreloadData(TTSService service, TTSPreloadData preloadData, Action<float> onProgress, Action<TTSPreloadData, string> onComplete)
{
IteratePhrases(service, preloadData, RefreshPhraseData, onProgress, (l) => onComplete?.Invoke(preloadData, l));
}
// Refresh
private static IEnumerator RefreshPhraseData(TTSService service, TTSDiskCacheSettings cacheSettings, TTSVoiceSettings voiceSettings, TTSPreloadPhraseData phraseData, Action<float> onProgress, Action<string> onComplete)
{
RefreshPhraseData(service, cacheSettings, voiceSettings, phraseData);
yield return null;
onComplete?.Invoke(string.Empty);
}
// Refresh phrase data
public static void RefreshVoiceData(TTSService service, TTSPreloadVoiceData voiceData, TTSDiskCacheSettings cacheSettings, ref string log)
{
// Get voice settings
if (service == null)
{
log += "\n-No TTS service found";
return;
}
// No voice data
if (voiceData == null)
{
log += "\n-No voice data provided";
return;
}
// Get voice
TTSVoiceSettings voiceSettings = service.GetPresetVoiceSettings(voiceData.presetVoiceID);
if (voiceSettings == null)
{
log += "\n-Missing Voice Setting: " + voiceData.presetVoiceID;
return;
}
// Generate
if (cacheSettings == null)
{
cacheSettings = new TTSDiskCacheSettings()
{
DiskCacheLocation = TTSDiskCacheLocation.Preload
};
}
// Iterate phrases
for (int p = 0; p < voiceData.phrases.Length; p++)
{
RefreshPhraseData(service, cacheSettings, voiceSettings, voiceData.phrases[p]);
}
}
// Refresh phrase data
public static void RefreshPhraseData(TTSService service, TTSDiskCacheSettings cacheSettings, TTSVoiceSettings voiceSettings, TTSPreloadPhraseData phraseData)
{
// Get voice settings
if (service == null || voiceSettings == null || string.IsNullOrEmpty(phraseData.textToSpeak))
{
phraseData.clipID = string.Empty;
phraseData.downloaded = false;
phraseData.downloadProgress = 0f;
return;
}
if (cacheSettings == null)
{
cacheSettings = new TTSDiskCacheSettings()
{
DiskCacheLocation = TTSDiskCacheLocation.Preload
};
}
// Get phrase data
phraseData.clipID = service.GetClipID(phraseData.textToSpeak, voiceSettings);
// Check if file exists
string path = service.GetDiskCachePath(phraseData.textToSpeak, phraseData.clipID, voiceSettings, cacheSettings);
phraseData.downloaded = File.Exists(path);
phraseData.downloadProgress = phraseData.downloaded ? 1f : 0f;
}
#endregion
#region DELETE
// Clear all clips in a tts preload file
public static void DeleteData(TTSService service)
{
// Get test file path
string path = service.GetDiskCachePath(string.Empty, "TEST", null, new TTSDiskCacheSettings()
{
DiskCacheLocation = TTSDiskCacheLocation.Preload
});
// Get directory
string directory = new FileInfo(path).DirectoryName;
if (!Directory.Exists(directory))
{
return;
}
// Ask
if (!EditorUtility.DisplayDialog("Delete Preload Cache",
$"Are you sure you would like to delete the TTS Preload directory at:\n{directory}?", "Okay", "Cancel"))
{
return;
}
// Delete recursively
Directory.Delete(directory, true);
// Delete meta
string meta = directory + ".meta";
if (File.Exists(meta))
{
File.Delete(meta);
}
// Refresh assets
AssetDatabase.Refresh();
}
#endregion
#region IMPORT
/// <summary>
/// Prompt user for a json file to be imported into an existing TTSPreloadSettings asset
/// </summary>
public static bool ImportData(TTSPreloadSettings preloadSettings)
{
// Select a file
string textFilePath = EditorUtility.OpenFilePanel("Select TTS Preload Json", Application.dataPath, "json");
if (string.IsNullOrEmpty(textFilePath))
{
return false;
}
// Import with selected file path
return ImportData(preloadSettings, textFilePath);
}
/// <summary>
/// Imported json data into an existing TTSPreloadSettings asset
/// </summary>
public static bool ImportData(TTSPreloadSettings preloadSettings, string textFilePath)
{
// Check for file
if (!File.Exists(textFilePath))
{
VLog.E($"TTS Preload Utility - Preload file does not exist\nPath: {textFilePath}");
return false;
}
// Load file
string textFileContents = File.ReadAllText(textFilePath);
if (string.IsNullOrEmpty(textFileContents))
{
VLog.E($"TTS Preload Utility - Preload file load failed\nPath: {textFilePath}");
return false;
}
// Parse file
WitResponseNode node = WitResponseNode.Parse(textFileContents);
if (node == null)
{
VLog.E($"TTS Preload Utility - Preload file parse failed\nPath: {textFilePath}");
return false;
}
// Iterate children for texts
WitResponseClass data = node.AsObject;
Dictionary<string, List<string>> textsByVoice = new Dictionary<string, List<string>>();
foreach (var voiceName in data.ChildNodeNames)
{
// Get texts list
List<string> texts;
if (textsByVoice.ContainsKey(voiceName))
{
texts = textsByVoice[voiceName];
}
else
{
texts = new List<string>();
}
// Add text phrases
string[] voicePhrases = data[voiceName].AsStringArray;
if (voicePhrases != null)
{
foreach (var phrase in voicePhrases)
{
if (!string.IsNullOrEmpty(phrase) && !texts.Contains(phrase))
{
texts.Add(phrase);
}
}
}
// Apply
textsByVoice[voiceName] = texts;
}
// Import
return ImportData(preloadSettings, textsByVoice);
}
/// <summary>
/// Find all ITTSPhraseProviders loaded in scenes & generate
/// data file to import all phrases associated with the files.
/// </summary>
public static bool ImportPhrases(TTSPreloadSettings preloadSettings)
{
// Find phrase providers in all scenes
List<ITTSPhraseProvider> phraseProviders = new List<ITTSPhraseProvider>();
for (int s = 0; s < SceneManager.sceneCount; s++)
{
Scene scene = SceneManager.GetSceneAt(s);
foreach (var root in scene.GetRootGameObjects())
{
ITTSPhraseProvider[] found = root.GetComponentsInChildren<ITTSPhraseProvider>(true);
if (found != null)
{
phraseProviders.AddRange(found);
}
}
}
// Get all phrases by voice id
Dictionary<string, List<string>> textsByVoice = new Dictionary<string, List<string>>();
foreach (var phraseProvider in phraseProviders)
{
// Ignore if no voices are found
List<string> voiceIds = phraseProvider.GetVoiceIds();
if (voiceIds == null || voiceIds.Count == 0)
{
continue;
}
// Iterate voice ids
foreach (var voiceId in voiceIds)
{
// Ignore empty voice id
if (string.IsNullOrEmpty(voiceId))
{
continue;
}
// Ignore if phrases are null
List<string> phrases = phraseProvider.GetVoicePhrases(voiceId);
if (phrases == null || phrases.Count == 0)
{
continue;
}
// Get phrase list
List<string> voicePhrases;
if (textsByVoice.ContainsKey(voiceId))
{
voicePhrases = textsByVoice[voiceId];
}
else
{
voicePhrases = new List<string>();
}
// Append unique phrases
foreach (var phrase in phrases)
{
if (!string.IsNullOrEmpty(phrase) && !voicePhrases.Contains(phrase))
{
voicePhrases.Add(phrase);
}
}
// Apply phrase list
textsByVoice[voiceId] = voicePhrases;
}
}
// Import with data
return ImportData(preloadSettings, textsByVoice);
}
/// <summary>
/// Imported dictionary data into an existing TTSPreloadSettings asset
/// </summary>
public static bool ImportData(TTSPreloadSettings preloadSettings, Dictionary<string, List<string>> textsByVoice)
{
// Import
if (preloadSettings == null)
{
VLog.E("TTS Preload Utility - Import Failed - Null Preload Settings");
return false;
}
// Whether or not changed
bool changed = false;
// Generate if needed
if (preloadSettings.data == null)
{
preloadSettings.data = new TTSPreloadData();
changed = true;
}
// Begin voice list
List<TTSPreloadVoiceData> voices = new List<TTSPreloadVoiceData>();
if (preloadSettings.data.voices != null)
{
voices.AddRange(preloadSettings.data.voices);
}
// Iterate voice names
foreach (var voiceName in textsByVoice.Keys)
{
// Get voice index if possible
int voiceIndex = voices.FindIndex((v) => string.Equals(v.presetVoiceID, voiceName));
// Generate voice
TTSPreloadVoiceData voice;
if (voiceIndex == -1)
{
voice = new TTSPreloadVoiceData();
voice.presetVoiceID = voiceName;
voiceIndex = voices.Count;
voices.Add(voice);
}
// Use existing
else
{
voice = voices[voiceIndex];
}
// Get texts & phrases for current voice
List<string> texts = new List<string>();
List<TTSPreloadPhraseData> phrases = new List<TTSPreloadPhraseData>();
if (voice.phrases != null)
{
foreach (var phrase in voice.phrases)
{
if (!string.IsNullOrEmpty(phrase.textToSpeak) && !texts.Contains(phrase.textToSpeak))
{
texts.Add(phrase.textToSpeak);
phrases.Add(phrase);
}
}
}
// Get data
List<string> newTexts = textsByVoice[voiceName];
if (newTexts != null)
{
foreach (var newText in newTexts)
{
if (!string.IsNullOrEmpty(newText) && !texts.Contains(newText))
{
changed = true;
texts.Add(newText);
phrases.Add(new TTSPreloadPhraseData()
{
textToSpeak = newText
});
}
}
}
// Apply voice
voice.phrases = phrases.ToArray();
voices[voiceIndex] = voice;
}
// Apply data
if (changed)
{
preloadSettings.data.voices = voices.ToArray();
EditorUtility.SetDirty(preloadSettings);
}
// Return changed
return changed;
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6428a97eb23d27b48bff4ecaa464004e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,292 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using Meta.WitAi.Data.Configuration;
using Meta.WitAi.TTS.Integrations;
using Meta.WitAi.TTS.Utilities;
using Meta.WitAi.Data.Info;
using Meta.WitAi.Lib;
using UnityEditor;
using UnityEngine;
namespace Meta.WitAi.TTS
{
public static class TTSEditorUtilities
{
// Default TTS Setup
public static Transform CreateDefaultSetup()
{
// Generate parent
Transform parent = GenerateGameObject("TTS").transform;
// Add TTS Service
TTSService service = CreateService(parent);
// Add TTS Speaker
CreateSpeaker(parent, service);
// Select parent
Selection.activeObject = parent.gameObject;
return parent;
}
// Default TTS Service
public static TTSService CreateService(Transform parent = null, bool ignoreErrors = false)
{
// Get parent
if (parent == null)
{
Transform selected = Selection.activeTransform;
if (selected != null && selected.gameObject.scene.rootCount > 0)
{
parent = Selection.activeTransform;
}
}
// Ignore if found
TTSService instance = GameObject.FindObjectOfType<TTSService>();
if (instance != null)
{
// Log
if (!ignoreErrors)
{
VLog.W($"TTS Service - A TTSService is already in scene\nGameObject: {instance.gameObject.name}");
}
// Move into parent
if (parent != null)
{
instance.transform.SetParent(parent, true);
}
}
// Generate TTSWit
else
{
instance = CreateWitService(parent);
}
// Select & return instance
Selection.activeObject = instance.gameObject;
return instance;
}
// Default TTS Service
private static TTSWit CreateWitService(Transform parent = null)
{
// Generate new TTSWit & add caches
TTSWit ttsWit = GenerateGameObject("TTSWitService", parent).AddComponent<TTSWit>();
ttsWit.gameObject.AddComponent<TTSRuntimeCache>();
ttsWit.gameObject.AddComponent<TTSDiskCache>();
VLog.D($"TTS Service - Instantiated Service {ttsWit.gameObject.name}");
// Refresh configuration
WitConfiguration configuration = SetupConfiguration(ttsWit);
if (configuration != null)
{
RefreshAvailableVoices(ttsWit);
}
// Log
return ttsWit;
}
// Wit configuration
private static WitConfiguration SetupConfiguration(TTSService instance)
{
// Ignore non-tts wit
if (instance.GetType() != typeof(TTSWit))
{
return null;
}
// Already setup
TTSWit ttsWit = instance as TTSWit;
if (ttsWit.RequestSettings.configuration != null)
{
return ttsWit.RequestSettings.configuration;
}
// Refresh configuration list
if (WitConfigurationUtility.WitConfigs == null)
{
WitConfigurationUtility.ReloadConfigurationData();
}
// Assign first wit configuration found
if (WitConfigurationUtility.WitConfigs != null && WitConfigurationUtility.WitConfigs.Length > 0)
{
ttsWit.RequestSettings.configuration = WitConfigurationUtility.WitConfigs[0];
VLog.D($"TTS Service - Assigned Wit Configuration {ttsWit.RequestSettings.configuration.name}");
}
// Warning
if (ttsWit.RequestSettings.configuration == null)
{
VLog.W($"TTS Service - Please create and assign a WitConfiguration to TTSWit");
}
// Return configuration
return ttsWit.RequestSettings.configuration;
}
// Refresh available voices
internal static void RefreshAvailableVoices(TTSWit ttsWit, Action<WitAppInfo> onUpdateComplete = null)
{
// Fail without configuration
if (ttsWit == null)
{
VLog.W($"TTS Service - Cannot refresh voices without TTS Wit Service");
return;
}
IWitRequestConfiguration configuration = ttsWit.RequestSettings.configuration;
if (configuration == null)
{
VLog.W($"TTS Service - Cannot refresh voices without TTS Wit Configuration");
return;
}
// Update app info
WitAppInfoUtility.Update(ttsWit.RequestSettings.configuration, (newInfo, r) =>
{
configuration.SetApplicationInfo(newInfo);
UpdatePresets(ttsWit, newInfo);
onUpdateComplete?.Invoke(newInfo);
});
}
// Add all presets
private static void UpdatePresets(TTSWit ttsWit, WitAppInfo appInfo)
{
// Cannot update presets without voices
if (appInfo.voices == null || appInfo.voices.Length == 0)
{
VLog.W("TTS Refresh failed to find voices");
return;
}
// Add all voices to preset voice list
if (ttsWit.PresetWitVoiceSettings == null || ttsWit.PresetWitVoiceSettings.Length == 0)
{
AddPresetsForInfo(ttsWit, appInfo.voices);
}
// Refresh speakers
RefreshEmptySpeakers(ttsWit);
}
// Adds a list of voice infos
internal static void AddPresetsForInfo(TTSWit ttsWit, WitVoiceInfo[] voiceInfos)
{
// Ignore without infos
if (voiceInfos == null || voiceInfos.Length == 0)
{
return;
}
// Add all voices to preset voice list
List<TTSWitVoiceSettings> voices = new List<TTSWitVoiceSettings>();
if (ttsWit.PresetWitVoiceSettings != null)
{
voices.AddRange(ttsWit.PresetWitVoiceSettings);
}
foreach (var voiceData in voiceInfos)
{
voices.Add(GetDefaultVoiceSetting(voiceData));
}
ttsWit.SetVoiceSettings(voices.ToArray());
}
// Adds a preset for a specific voice
internal static void AddPresetForInfo(TTSWit ttsWit, WitVoiceInfo voiceData)
{
List<TTSWitVoiceSettings> voices = new List<TTSWitVoiceSettings>();
if (ttsWit.PresetWitVoiceSettings != null)
{
voices.AddRange(ttsWit.PresetWitVoiceSettings);
}
voices.Add(GetDefaultVoiceSetting(voiceData));
ttsWit.SetVoiceSettings(voices.ToArray());
}
// Get default voice settings
private static TTSWitVoiceSettings GetDefaultVoiceSetting(WitVoiceInfo voiceData)
{
TTSWitVoiceSettings result = new TTSWitVoiceSettings()
{
SettingsId = voiceData.name.ToUpper(),
voice = voiceData.name
};
// Use first style provided
if (voiceData.styles != null && voiceData.styles.Length > 0)
{
result.style = voiceData.styles[0];
}
return result;
}
// Set all blank IDs to default voice id
private static void RefreshEmptySpeakers(TTSService service)
{
string defaultVoiceID = service.VoiceProvider.VoiceDefaultSettings?.SettingsId;
foreach (var speaker in GameObject.FindObjectsOfType<TTSSpeaker>())
{
if (string.IsNullOrEmpty(speaker.presetVoiceID))
{
speaker.presetVoiceID = defaultVoiceID;
}
}
}
// Default TTS Speaker
public static TTSSpeaker CreateSpeaker(Transform parent = null, TTSService service = null)
{
// Get parent
if (parent == null)
{
Transform selected = Selection.activeTransform;
if (selected != null && selected.gameObject.scene.rootCount > 0)
{
parent = Selection.activeTransform;
}
}
// Generate service if possible
if (service == null)
{
service = CreateService(parent);
}
// TTS Speaker
string goName = typeof(TTSSpeaker).Name;
TTSSpeaker speaker = GenerateGameObject(goName, parent).AddComponent<TTSSpeaker>();
speaker.presetVoiceID = string.Empty;
// Audio Source
AudioSource audio = GenerateGameObject($"{goName}Audio", speaker.transform).AddComponent<AudioSource>();
audio.playOnAwake = false;
audio.loop = false;
audio.spatialBlend = 0f; // Default to 2D
// Return speaker
VLog.D($"TTS Service - Instantiated Speaker {speaker.gameObject.name}");
Selection.activeObject = speaker.gameObject;
return speaker;
}
// Generate with specified name
private static GameObject GenerateGameObject(string name, Transform parent = null)
{
Transform result = new GameObject(name).transform;
result.SetParent(parent);
result.localPosition = Vector3.zero;
result.localRotation = Quaternion.identity;
result.localScale = Vector3.one;
return result.gameObject;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c590c0f8426e4194db6efc14c68db75c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,127 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System;
using Meta.WitAi.TTS.Data;
using UnityEditor;
using UnityEngine;
namespace Meta.WitAi.TTS
{
[CustomEditor(typeof(TTSService), true)]
public class TTSServiceInspector : UnityEditor.Editor
{
// Service
private TTSService _service;
// Dropdown
private bool _clipFoldout = false;
// Maximum text for abbreviated
private const int MAX_DISPLAY_TEXT = 20;
// GUI
public sealed override void OnInspectorGUI()
{
// Display default ui
OnEditTimeGUI();
OnPlaytimeGUI();
}
protected virtual void OnEditTimeGUI()
{
base.OnInspectorGUI();
}
protected virtual void OnPlaytimeGUI()
{
// Ignore if in editor
if (!Application.isPlaying)
{
return;
}
// Get service
if (!_service)
{
_service = target as TTSService;
}
// Add spaces
EditorGUILayout.Space();
EditorGUILayout.Space();
EditorGUILayout.LabelField("Runtime Clip Cache", EditorStyles.boldLabel);
// No clips
TTSClipData[] clips = _service.GetAllRuntimeCachedClips();
if (clips == null || clips.Length == 0)
{
WitEditorUI.LayoutErrorLabel("No clips found");
return;
}
// Has clips
_clipFoldout = WitEditorUI.LayoutFoldout(new GUIContent($"Clips: {clips.Length}"), _clipFoldout);
if (_clipFoldout)
{
EditorGUI.indentLevel++;
// Iterate clips
foreach (TTSClipData clip in clips)
{
// Get display name
string displayName = clip.textToSpeak;
// Crop if too long
if (displayName.Length > MAX_DISPLAY_TEXT)
{
displayName = displayName.Substring(0, MAX_DISPLAY_TEXT);
}
// Add voice setting id
if (clip.voiceSettings != null)
{
displayName = $"{clip.voiceSettings.SettingsId} - {displayName}";
}
// Foldout if desired
bool foldout = WitEditorUI.LayoutFoldout(new GUIContent(displayName), clip);
if (foldout)
{
EditorGUI.indentLevel++;
DrawClipGUI(clip);
EditorGUI.indentLevel--;
}
}
EditorGUI.indentLevel--;
}
}
// Clip data
public static void DrawClipGUI(TTSClipData clip)
{
// Generation Settings
WitEditorUI.LayoutKeyLabel("Text", clip.textToSpeak);
EditorGUILayout.TextField("Clip ID", clip.clipID);
EditorGUILayout.ObjectField("Clip", clip.clip, typeof(AudioClip), true);
// Loaded
TTSClipLoadState loadState = clip.loadState;
if (loadState != TTSClipLoadState.Preparing)
{
WitEditorUI.LayoutKeyLabel("Load State", loadState.ToString());
}
// Loading with progress
else
{
EditorGUILayout.BeginHorizontal();
int loadProgress = Mathf.FloorToInt(clip.loadProgress * 100f);
WitEditorUI.LayoutKeyLabel("Load State", $"{loadState} ({loadProgress}%)");
GUILayout.HorizontalSlider(loadProgress, 0, 100);
EditorGUILayout.EndHorizontal();
}
// Additional Settings
WitEditorUI.LayoutKeyObjectLabels("Voice Settings", clip.voiceSettings);
WitEditorUI.LayoutKeyObjectLabels("Cache Settings", clip.diskCacheSettings);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a7b031cd5a557e14fa08e59b869a2e78
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,189 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System.Linq;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using Meta.WitAi.TTS.Utilities;
using Meta.WitAi.TTS.Data;
namespace Meta.WitAi.TTS
{
[CustomEditor(typeof(TTSSpeaker), true)]
public class TTSSpeakerInspector : Editor
{
// Speaker
private TTSSpeaker _speaker;
private SerializedProperty _presetVoiceProperty;
private SerializedProperty _customVoiceProperty;
// Voices
private int _voiceIndex = -1;
private string[] _voicePresetIds = null;
// Voice text
private const string UI_PRESET_VOICE_KEY = "Voice Preset";
private const string UI_CUSTOM_VOICE_KEY = "Custom Voice";
private const string UI_CUSTOM_KEY = "CUSTOM";
//
void OnEnable()
{
_speaker = target as TTSSpeaker;
_presetVoiceProperty = serializedObject.FindProperty("presetVoiceID");
_customVoiceProperty = serializedObject.FindProperty("customWitVoiceSettings");
}
// GUI
public override void OnInspectorGUI()
{
// Display default ui
base.OnInspectorGUI();
// Check voices
TTSService tts = _speaker.TTSService;
TTSVoiceSettings[] settings = tts?.GetAllPresetVoiceSettings();
if (_voicePresetIds == null
|| (settings != null && _voicePresetIds.Length != settings.Length + 1)
|| (_voiceIndex >= 0 && _voiceIndex < _voicePresetIds.Length - 1 && !string.Equals(_speaker.presetVoiceID, _voicePresetIds[_voiceIndex])))
{
RefreshVoices(settings);
}
// No preset voices found, assume custom
if (_voicePresetIds == null || _voicePresetIds.Length == 0)
{
GUI.enabled = false;
EditorGUILayout.TextField(UI_PRESET_VOICE_KEY, UI_CUSTOM_KEY);
GUI.enabled = true;
}
// Voice dropdown
else
{
bool updated = false;
WitEditorUI.LayoutPopup(UI_PRESET_VOICE_KEY, _voicePresetIds, ref _voiceIndex, ref updated);
if (updated)
{
if (_voiceIndex >= 0 && _voiceIndex < _voicePresetIds.Length - 1)
{
_presetVoiceProperty.stringValue = _voicePresetIds[_voiceIndex];
}
else
{
_presetVoiceProperty.stringValue = null;
}
}
}
// Add custom layout
if (_voicePresetIds == null || _voiceIndex < 0 || _voiceIndex >= _voicePresetIds.Length - 1)
{
EditorGUILayout.PropertyField(_customVoiceProperty, new GUIContent(UI_CUSTOM_VOICE_KEY));
}
// Apply all modified properties
serializedObject.ApplyModifiedProperties();
// Layout TTS clip queue
LayoutClipQueue();
}
// Refresh voices
private void RefreshVoices(TTSVoiceSettings[] settings)
{
// Reset voice data
_voiceIndex = -1;
_voicePresetIds = null;
if (settings == null)
{
return;
}
// Get all ids
List<string> presetIds = settings.Select(s => s.SettingsId).ToList();
// Get voice index
_voiceIndex = presetIds.IndexOf(_speaker.presetVoiceID);
if (_voiceIndex == -1)
{
_voiceIndex = presetIds.Count;
}
// Add custom key
presetIds.Add(UI_CUSTOM_KEY);
// Apply preset ids
_voicePresetIds = presetIds.ToArray();
}
// Layout clip queue
private const string UI_CLIP_HEADER_TEXT = "Clip Queue";
private const string UI_CLIP_SPEAKER_TEXT = "Speaker Clip:";
private const string UI_CLIP_QUEUE_TEXT = "Loading Clips:";
private bool _speakerFoldout = false;
private bool _queueFoldout = false;
private void LayoutClipQueue()
{
// Ignore unless playing
if (!Application.isPlaying)
{
return;
}
// Add header
EditorGUILayout.Space();
EditorGUILayout.LabelField(UI_CLIP_HEADER_TEXT, EditorStyles.boldLabel);
// Speaker Foldout
_speakerFoldout = EditorGUILayout.Foldout(_speakerFoldout, UI_CLIP_SPEAKER_TEXT);
if (_speakerFoldout)
{
EditorGUI.indentLevel++;
if (!_speaker.IsSpeaking)
{
EditorGUILayout.LabelField("None");
}
else
{
TTSServiceInspector.DrawClipGUI(_speaker.SpeakingClip);
}
EditorGUI.indentLevel--;
}
// Queue Foldout
List<TTSClipData> queuedClips = _speaker.QueuedClips;
_queueFoldout = EditorGUILayout.Foldout(_queueFoldout, $"{UI_CLIP_QUEUE_TEXT} {(queuedClips == null ? 0 : queuedClips.Count)}");
if (_queueFoldout)
{
EditorGUI.indentLevel++;
if (queuedClips == null || queuedClips.Count == 0)
{
EditorGUILayout.LabelField("None");
}
else
{
for (int i = 0; i < queuedClips.Count; i++)
{
TTSClipData clipData = queuedClips[i];
bool oldFoldout = WitEditorUI.GetFoldoutValue(clipData);
bool newFoldout = EditorGUILayout.Foldout(oldFoldout, $"Clip[{i}]");
if (oldFoldout != newFoldout)
{
WitEditorUI.SetFoldoutValue(clipData, newFoldout);
}
if (newFoldout)
{
EditorGUI.indentLevel++;
TTSServiceInspector.DrawClipGUI(clipData);
EditorGUI.indentLevel--;
}
}
}
EditorGUI.indentLevel--;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3835212f72d4d5149bed0f07915f204e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,77 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System.Linq;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using Meta.WitAi.TTS.Integrations;
namespace Meta.WitAi.TTS
{
[CustomEditor(typeof(TTSWit), true)]
public class TTSWitInspector : TTSServiceInspector
{
private int selectedBaseVoice;
protected override void OnEditTimeGUI()
{
base.OnEditTimeGUI();
var ttsWit = (TTSWit)target;
var config = ttsWit.RequestSettings.configuration;
if (!config) return;
// Get app info for voices
var appInfo = config.GetApplicationInfo();
if (null != appInfo.voices && appInfo.voices.Length > 0)
{
// Get all voice names from wit
string[] voiceNames = appInfo.voices.Select(v => v.name).ToArray();
// Add a selected preset
GUILayout.BeginHorizontal();
selectedBaseVoice = EditorGUILayout.Popup(selectedBaseVoice, voiceNames);
GUI.enabled = selectedBaseVoice >= 0 && selectedBaseVoice < appInfo.voices.Length;
if (WitEditorUI.LayoutTextButton("Add Preset"))
{
TTSEditorUtilities.AddPresetForInfo(ttsWit, appInfo.voices[selectedBaseVoice]);
}
GUILayout.EndHorizontal();
// Add all unused presets
GUI.enabled = true;
if (GUILayout.Button("Add Unused Voices as Presets"))
{
// Get used voices
List<string> usedVoiceNames = ttsWit.PresetWitVoiceSettings.Select(v => v.voice).ToList();
// Get unused voices
var unusedVoices = appInfo.voices.Where(v => !usedVoiceNames.Contains(v.name)).ToArray();
// Add all unused presets
TTSEditorUtilities.AddPresetsForInfo(ttsWit, unusedVoices);
}
}
// Log warning
else
{
GUILayout.Label("There are currently no base presets available. Click refresh to check for updates.", EditorStyles.helpBox);
}
// Refresh button
if (GUILayout.Button("Refresh Presets"))
{
TTSEditorUtilities.RefreshAvailableVoices(ttsWit, info =>
{
Repaint();
});
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2f6696f09b6647cda45870d07c064cab
timeCreated: 1681499481
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 69fad6077e2eeef4a812dc81d313fc2a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,322 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using Meta.WitAi.TTS.Integrations;
using Meta.WitAi.Windows;
using Meta.WitAi.Data.Info;
using Meta.WitAi.Lib;
using Meta.WitAi.Data.Configuration;
using Meta.WitAi.TTS.Data;
using Meta.WitAi.TTS.Utilities;
using UnityEngine;
namespace Meta.WitAi.TTS.Voices
{
[CustomPropertyDrawer(typeof( TTSWitVoiceSettings))]
public class TTSWitVoiceSettingsDrawer : PropertyDrawer
{
// Constants for var layout
private const float VAR_HEIGHT = 20f;
private const float VAR_MARGIN = 4f;
private const float FIELD_HEIGHT = 60f;
// Constants for var lookup
private const string VAR_SETTINGS = "SettingsId";
private const string VAR_VOICE = "voice";
private const string VAR_STYLE = "style";
// Voice data
private TTSService _ttsService;
private bool _onSpeaker;
private IWitRequestConfiguration _configuration;
private bool _configUpdating;
private WitVoiceInfo[] _voices;
private string[] _voiceNames;
// Subfields
private static readonly FieldInfo[] _fields = FieldGUI.GetFields(typeof( TTSWitVoiceSettings));
// Determine height
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
// Get service
if (!_ttsService)
{
GetService(property, out _ttsService, out _onSpeaker);
}
// Property
if (!property.isExpanded)
{
return VAR_HEIGHT;
}
// Add each field + 1 for dropdown
int total = _fields.Length + 1;
// Remove 3 on speaker
if (_onSpeaker)
{
total -= 3;
}
// Add 2 for voice properties
int voiceIndex = GetVoiceIndex(property);
if (voiceIndex != -1)
{
total += 2;
}
// Get height
float height = total * VAR_HEIGHT + Mathf.Max(0, total - 1) * VAR_MARGIN;
// Add fields
if (!_onSpeaker)
{
height += 2 * (FIELD_HEIGHT - VAR_HEIGHT);
}
return height;
}
// Handles gui layout
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
// Get service if needed
if (!_ttsService)
{
GetService(property, out _ttsService, out _onSpeaker);
}
// On gui
float y = position.y;
string voiceName = property.FindPropertyRelative(VAR_SETTINGS).stringValue;
if (string.IsNullOrEmpty(voiceName))
{
voiceName = property.displayName;
}
property.isExpanded =
EditorGUI.Foldout(new Rect(position.x, y, position.width, VAR_HEIGHT), property.isExpanded, voiceName);
if (!property.isExpanded)
{
return;
}
y += VAR_HEIGHT + VAR_MARGIN;
// Increment
EditorGUI.indentLevel++;
// Refresh voices if needed
RefreshVoices(_ttsService, property);
// Get voice index
int voiceIndex = GetVoiceIndex(property);
// Iterate subfields
for (int s = 0; s < _fields.Length; s++)
{
// Get subfield
FieldInfo subfield = _fields[s];
// Ignore base fields on speaker
if (_onSpeaker && subfield.DeclaringType == typeof(TTSVoiceSettings))
{
continue;
}
// Get subfield property
SerializedProperty subfieldProperty = property.FindPropertyRelative(subfield.Name);
Rect subfieldRect = new Rect(position.x, y, position.width, VAR_HEIGHT);
if (string.Equals(subfield.Name, VAR_VOICE) && voiceIndex != -1)
{
int newVoiceIndex = EditorGUI.Popup(subfieldRect, subfieldProperty.displayName, voiceIndex,
_voiceNames);
newVoiceIndex = Mathf.Clamp(newVoiceIndex, 0, _voiceNames.Length);
if (voiceIndex != newVoiceIndex)
{
voiceIndex = newVoiceIndex;
subfieldProperty.stringValue = _voiceNames[voiceIndex];
GUI.FocusControl(null);
}
y += VAR_HEIGHT + VAR_MARGIN;
continue;
}
if (string.Equals(subfield.Name, VAR_STYLE) && voiceIndex >= 0 && voiceIndex < _voices.Length)
{
// Get voice data
WitVoiceInfo voiceInfo = _voices[voiceIndex];
EditorGUI.indentLevel++;
// Locale layout
EditorGUI.LabelField(subfieldRect, "Locale", voiceInfo.locale);
y += VAR_HEIGHT + VAR_MARGIN;
// Gender layout
subfieldRect = new Rect(position.x, y, position.width, VAR_HEIGHT);
EditorGUI.LabelField(subfieldRect, "Gender", voiceInfo.gender);
y += VAR_HEIGHT + VAR_MARGIN;
// Style layout/select
subfieldRect = new Rect(position.x, y, position.width, VAR_HEIGHT);
if (voiceInfo.styles != null && voiceInfo.styles.Length > 0)
{
// Get style index
string style = subfieldProperty.stringValue;
int styleIndex = new List<string>(voiceInfo.styles).IndexOf(style);
// Show style select
int newStyleIndex = EditorGUI.Popup(subfieldRect, subfieldProperty.displayName, styleIndex,
voiceInfo.styles);
newStyleIndex = Mathf.Clamp(newStyleIndex, 0, voiceInfo.styles.Length);
if (styleIndex != newStyleIndex)
{
// Apply style
styleIndex = newStyleIndex;
subfieldProperty.stringValue = voiceInfo.styles[styleIndex];
GUI.FocusControl(null);
}
// Move down
y += VAR_HEIGHT + VAR_MARGIN;
EditorGUI.indentLevel--;
continue;
}
// Undent
EditorGUI.indentLevel--;
}
//
TextAreaAttribute area = subfield.GetCustomAttribute<TextAreaAttribute>();
if (area != null)
{
subfieldRect.height = FIELD_HEIGHT;
y += FIELD_HEIGHT - VAR_HEIGHT;
}
// Default layout
EditorGUI.PropertyField(subfieldRect, subfieldProperty, new GUIContent(subfieldProperty.displayName));
// Clamp in between range
RangeAttribute range = subfield.GetCustomAttribute<RangeAttribute>();
if (range != null)
{
int newValue = Mathf.Clamp(subfieldProperty.intValue, (int)range.min, (int)range.max);
if (subfieldProperty.intValue != newValue)
{
subfieldProperty.intValue = newValue;
}
}
// Increment
y += VAR_HEIGHT + VAR_MARGIN;
}
// Undent
EditorGUI.indentLevel--;
}
// Get service & if on a speaker
private void GetService(SerializedProperty property, out TTSService ttsService, out bool onSpeaker)
{
// Get tts wit if possible
object targetObject = property.serializedObject.targetObject;
if (targetObject != null)
{
// Currently on TTSService
if (targetObject is TTSService service)
{
ttsService = service;
onSpeaker = false;
return;
}
// Currently on TTSSpeaker
if (targetObject is TTSSpeaker speaker)
{
ttsService = speaker.TTSService;
onSpeaker = true;
return;
}
}
// Speaker not found
ttsService = null;
onSpeaker = false;
}
// Refresh voices
private void RefreshVoices(TTSService ttsService, SerializedProperty property)
{
// Ensure service exists
if (!(ttsService is TTSWit))
{
return;
}
// Get configuration
IWitRequestConfiguration configuration = (ttsService as TTSWit).RequestSettings.configuration;
// Set configuration
if (_configuration != configuration)
{
_configuration = configuration;
_voices = null;
_voiceNames = null;
_configUpdating = false;
}
// Ignore if null
if (configuration == null)
{
return;
}
// Ignore if already set up
if (_voices != null && _voiceNames != null && !_configUpdating)
{
return;
}
// Get voices
_voices = configuration.GetApplicationInfo().voices;
_voiceNames = _voices?.Select(voice => voice.name).ToArray();
// Voices found!
if (_voices != null && _voices.Length > 0)
{
_configUpdating = false;
}
// Configuration needs voices, perform update
else if (!_configUpdating)
{
// Perform update if possible
if (_configuration is WitConfiguration witConfig && !witConfig.IsUpdatingData())
{
witConfig.RefreshAppInfo();
}
// Now updating
_configUpdating = true;
}
}
// Get voice index
private int GetVoiceIndex(SerializedProperty property)
{
SerializedProperty voiceProperty = property.FindPropertyRelative(VAR_VOICE);
string voiceID = voiceProperty.stringValue;
int voiceIndex = -1;
List<string> voiceNames = new List<string>();
if (_voiceNames != null)
{
voiceNames.AddRange(_voiceNames);
}
if (voiceNames.Count > 0)
{
if (string.IsNullOrEmpty(voiceID))
{
voiceIndex = 0;
voiceID = voiceNames[0];
voiceProperty.stringValue = voiceID;
GUI.FocusControl(null);
}
else
{
voiceIndex = voiceNames.IndexOf(voiceID);
}
}
return voiceIndex;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0bf9132926065a54da619451f5258d3e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8c945bb3fb3322b4eb73aba670cad74a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f4d30ee47fb6e7d4a888e9ee4b707391
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,135 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System;
using System.Collections.Generic;
using Meta.Voice.Audio;
using UnityEngine;
namespace Meta.WitAi.TTS.Data
{
// Various request load states
public enum TTSClipLoadState
{
Unloaded,
Preparing,
Loaded,
Error
}
[Serializable]
public class TTSClipData
{
// Text to be spoken
public string textToSpeak;
// Unique identifier
public string clipID;
// Audio type
public AudioType audioType;
// Voice settings for request
public TTSVoiceSettings voiceSettings;
// Cache settings for request
public TTSDiskCacheSettings diskCacheSettings;
/// <summary>
/// Unique request id used for tracking & logging
/// </summary>
public string queryRequestId => _queryRequestId;
private string _queryRequestId = Guid.NewGuid().ToString();
// Whether service should stream audio or just provide all at once
public bool queryStream;
// Request data
public Dictionary<string, string> queryParameters;
// Clip stream
public IAudioClipStream clipStream
{
get => _clipStream;
set
{
// Unload previous clip stream
IAudioClipStream v = value;
if (_clipStream != null && _clipStream != v)
{
clipStream.OnStreamReady = null;
clipStream.OnStreamUpdated = null;
clipStream.OnStreamComplete = null;
_clipStream.Unload();
}
// Apply new clip stream
_clipStream = v;
}
}
private IAudioClipStream _clipStream;
public AudioClip clip
{
get
{
if (clipStream is IAudioClipProvider uacs)
{
return uacs.Clip;
}
return null;
}
}
// Clip load state
[NonSerialized] public TTSClipLoadState loadState;
// Clip load progress
[NonSerialized] public float loadProgress;
// On clip state change
public Action<TTSClipData, TTSClipLoadState> onStateChange;
/// <summary>
/// A callback when clip stream is ready
/// Returns an error if there was an issue
/// </summary>
public Action<string> onPlaybackReady;
/// <summary>
/// A callback when clip has downloaded successfully
/// Returns an error if there was an issue
/// </summary>
public Action<string> onDownloadComplete;
/// <summary>
/// Compare clips if possible
/// </summary>
public override bool Equals(object obj)
{
if (obj is TTSClipData other)
{
return Equals(other);
}
return false;
}
/// <summary>
/// Compare clip ids
/// </summary>
public bool Equals(TTSClipData other)
{
return HasClipId(other?.clipID);
}
/// <summary>
/// Compare clip ids
/// </summary>
public bool HasClipId(string clipId)
{
return string.Equals(clipID, clipId, StringComparison.CurrentCultureIgnoreCase);
}
/// <summary>
/// Get hash code
/// </summary>
/// <returns></returns>
public override int GetHashCode()
{
var hash = 17;
hash = hash * 31 + clipID.GetHashCode();
return hash;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ef626b8cea4f59646a5076430a0e14aa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,51 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System;
namespace Meta.WitAi.TTS.Data
{
// TTS Cache disk location
public enum TTSDiskCacheLocation
{
/// <summary>
/// Does not cache
/// </summary>
Stream,
/// <summary>
/// Stores files in editor only & loads files from internal project location (Application.streamingAssetsPath)
/// </summary>
Preload,
/// <summary>
/// Stores files at persistent location (Application.persistentDataPath)
/// </summary>
Persistent,
/// <summary>
/// Stores files at temporary cache location (Application.temporaryCachePath)
/// </summary>
Temporary
}
[Serializable]
public class TTSDiskCacheSettings
{
/// <summary>
/// Where the TTS clip should be cached
/// </summary>
public TTSDiskCacheLocation DiskCacheLocation = TTSDiskCacheLocation.Stream;
/// <summary>
/// Where the TTS clip should streamed from cache
/// </summary>
public bool StreamFromDisk = false;
/// <summary>
/// Length of a streamed clip buffer in seconds
/// </summary>
public float StreamBufferLength = 5f;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a4d1170a24dd77d49bf3cd610dd1c9a5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,28 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using UnityEngine;
using UnityEngine.Serialization;
namespace Meta.WitAi.TTS.Data
{
public abstract class TTSVoiceSettings
{
[Tooltip("A unique id used for linking these voice settings to a TTS Speaker")]
[FormerlySerializedAs("settingsID")]
public string SettingsId;
[Tooltip("Text that is added to the front of any TTS request using this voice setting")]
[TextArea]
public string PrependedText;
[TextArea]
[Tooltip("Text that is added to the end of any TTS request using this voice setting")]
public string AppendedText;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5dbbd0a6d06807d4f8a3190785a267f4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5a7b8f23689f0b94dbe6a9aae4811de6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,40 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System;
using Meta.WitAi.TTS.Data;
using UnityEngine;
using UnityEngine.Events;
namespace Meta.WitAi.TTS.Events
{
[Serializable]
public class TTSClipDownloadEvent : UnityEvent<TTSClipData, string>
{
}
[Serializable]
public class TTSClipDownloadErrorEvent : UnityEvent<TTSClipData, string, string>
{
}
[Serializable]
public class TTSDownloadEvents
{
[Tooltip("Called when a audio clip download begins")]
public TTSClipDownloadEvent OnDownloadBegin = new TTSClipDownloadEvent();
[Tooltip("Called when a audio clip is downloaded successfully")]
public TTSClipDownloadEvent OnDownloadSuccess = new TTSClipDownloadEvent();
[Tooltip("Called when a audio clip downloaded has been cancelled")]
public TTSClipDownloadEvent OnDownloadCancel = new TTSClipDownloadEvent();
[Tooltip("Called when a audio clip downloaded has failed")]
public TTSClipDownloadErrorEvent OnDownloadError = new TTSClipDownloadErrorEvent();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a8c6f2c6a5fdba344b75e8f613c5dc09
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,38 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System;
using UnityEngine;
namespace Meta.WitAi.TTS.Events
{
[Serializable]
public class TTSServiceEvents
{
[Tooltip("Called when a audio clip has been added to the runtime cache")]
public TTSClipEvent OnClipCreated = new TTSClipEvent();
[Tooltip("Called when a audio clip has been removed from the runtime cache")]
public TTSClipEvent OnClipUnloaded = new TTSClipEvent();
/// <summary>
/// All events related to web requests
/// </summary>
public TTSWebRequestEvents WebRequest = new TTSWebRequestEvents();
/// <summary>
/// All events related to streaming from web or disk
/// </summary>
public TTSStreamEvents Stream = new TTSStreamEvents();
/// <summary>
/// All events related to downloading from the web
/// </summary>
public TTSDownloadEvents Download = new TTSDownloadEvents();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a41b87319719e004da4ad59b6a70358d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,117 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System;
using Meta.WitAi.Speech;
using UnityEngine;
using UnityEngine.Events;
using Meta.WitAi.TTS.Data;
namespace Meta.WitAi.TTS.Utilities
{
/// <summary>
/// A unity event that returns a TTSSpeaker & TTSClipData
/// for a specific speaker playback request.
/// </summary>
[Serializable]
public class TTSSpeakerClipEvent : UnityEvent<TTSSpeaker, TTSClipData> { }
/// <summary>
/// A unity event that returns a TTSSpeaker, TTSClipData & text
/// for a specific speaker playback request
/// </summary>
[Serializable]
public class TTSSpeakerClipMessageEvent : UnityEvent<TTSSpeaker, TTSClipData, string> { }
/// <summary>
/// A collection of events used for speaker tts playback.
/// </summary>
[Serializable]
public class TTSSpeakerClipEvents : VoiceSpeechEvents
{
[Header("Speaker Lifecycle Events")]
[SerializeField] [Tooltip("Initial callback as soon as the audio clip speak request is generated")]
private TTSSpeakerClipEvent _onInit = new TTSSpeakerClipEvent();
/// <summary>
/// Initial callback as soon as the audio clip speak request is generated
/// </summary>
public TTSSpeakerClipEvent OnInit => _onInit;
[SerializeField] [Tooltip("Final call for a 'Speak' request that is called following a load failure, load abort, playback cancellation or playback completion")]
private TTSSpeakerClipEvent _onComplete = new TTSSpeakerClipEvent();
/// <summary>
/// Final call for a 'Speak' request that is called following a load failure,
/// load abort, playback cancellation or playback completion
/// </summary>
public TTSSpeakerClipEvent OnComplete => _onComplete;
[Header("Speaker Loading Events")]
[SerializeField] [Tooltip("Called when TTS audio clip load begins")]
private TTSSpeakerClipEvent _onLoadBegin = new TTSSpeakerClipEvent();
/// <summary>
/// Called when TTS audio clip load begins
/// </summary>
public TTSSpeakerClipEvent OnLoadBegin => _onLoadBegin;
[SerializeField] [Tooltip("Called when TTS audio clip load is cancelled")]
private TTSSpeakerClipEvent _onLoadAbort = new TTSSpeakerClipEvent();
/// <summary>
/// Called when TTS audio clip load is cancelled
/// </summary>
public TTSSpeakerClipEvent OnLoadAbort => _onLoadAbort;
[SerializeField] [Tooltip("Called when TTS audio clip load fails due to a network or load error")]
private TTSSpeakerClipMessageEvent _onLoadFailed = new TTSSpeakerClipMessageEvent();
/// <summary>
/// Called when TTS audio clip load fails due to a network or load error
/// </summary>
public TTSSpeakerClipMessageEvent OnLoadFailed => _onLoadFailed;
[SerializeField] [Tooltip("Called when TTS audio clip load successfully")]
private TTSSpeakerClipEvent _onLoadSuccess = new TTSSpeakerClipEvent();
/// <summary>
/// Called when TTS audio clip load successfully
/// </summary>
public TTSSpeakerClipEvent OnLoadSuccess => _onLoadSuccess;
[Header("Speaker Playback Events")]
[SerializeField] [Tooltip("Called when TTS audio clip playback is ready")]
private TTSSpeakerClipEvent _onPlaybackReady = new TTSSpeakerClipEvent();
/// <summary>
/// Called when TTS audio clip playback is ready
/// </summary>
public TTSSpeakerClipEvent OnPlaybackReady => _onPlaybackReady;
[SerializeField] [Tooltip("Called when TTS audio clip playback has begun")]
private TTSSpeakerClipEvent _onPlaybackStart = new TTSSpeakerClipEvent();
/// <summary>
/// Called when TTS audio clip playback has begun
/// </summary>
public TTSSpeakerClipEvent OnPlaybackStart => _onPlaybackStart;
[SerializeField] [Tooltip("Called when TTS audio clip playback been cancelled")]
private TTSSpeakerClipMessageEvent _onPlaybackCancelled = new TTSSpeakerClipMessageEvent();
/// <summary>
/// Called when TTS audio clip playback been cancelled
/// </summary>
public TTSSpeakerClipMessageEvent OnPlaybackCancelled => _onPlaybackCancelled;
[SerializeField] [Tooltip("Called when TTS audio clip is updated during streamed playback")]
private TTSSpeakerClipEvent _onPlaybackClipUpdated = new TTSSpeakerClipEvent();
/// <summary>
/// Called when TTS audio clip is updated during streamed playback
/// </summary>
public TTSSpeakerClipEvent OnPlaybackClipUpdated => _onPlaybackClipUpdated;
[SerializeField] [Tooltip("Called when TTS audio clip playback completed successfully")]
private TTSSpeakerClipEvent _onPlaybackComplete = new TTSSpeakerClipEvent();
/// <summary>
/// Called when TTS audio clip playback completed successfully
/// </summary>
public TTSSpeakerClipEvent OnPlaybackComplete => _onPlaybackComplete;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2c303e406a42b8b4d9853bfd74e434ba
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,70 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System;
using Meta.WitAi.Speech;
using UnityEngine;
using UnityEngine.Events;
using Meta.WitAi.TTS.Data;
using UnityEngine.Serialization;
namespace Meta.WitAi.TTS.Utilities
{
[Serializable]
public class TTSSpeakerEvent : UnityEvent<TTSSpeaker, string> { }
[Serializable]
public class TTSSpeakerClipDataEvent : UnityEvent<TTSClipData> { }
[Serializable]
public class TTSSpeakerEvents : TTSSpeakerClipEvents
{
[Header("Queue Events")]
[Tooltip("Called when a tts request is added to an empty queue")]
[SerializeField] [FormerlySerializedAs("OnPlaybackQueueBegin")]
private UnityEvent _onPlaybackQueueBegin = new UnityEvent();
public UnityEvent OnPlaybackQueueBegin => _onPlaybackQueueBegin;
[Tooltip("Called the final request is removed from a queue")]
[SerializeField] [FormerlySerializedAs("OnPlaybackQueueComplete")]
private UnityEvent _onPlaybackQueueComplete = new UnityEvent();
public UnityEvent OnPlaybackQueueComplete => _onPlaybackQueueComplete;
[Header("Deprecated Events")]
[Obsolete("Use 'OnLoadBegin' event")]
public TTSSpeakerClipDataEvent OnClipDataQueued;
[Obsolete("Use 'OnLoadBegin' event")]
public TTSSpeakerEvent OnClipLoadBegin;
[Obsolete("Use 'OnLoadBegin' event")]
public TTSSpeakerClipDataEvent OnClipDataLoadBegin;
[Obsolete("Use 'OnLoadAbort' event")]
public TTSSpeakerEvent OnClipLoadAbort;
[Obsolete("Use 'OnLoadAbort' event")]
public TTSSpeakerClipDataEvent OnClipDataLoadAbort;
[Obsolete("Use 'OnLoadFailed' event")]
public TTSSpeakerEvent OnClipLoadFailed;
[Obsolete("Use 'OnLoadFailed' event")]
public TTSSpeakerClipDataEvent OnClipDataLoadFailed;
[Obsolete("Use 'OnLoadSuccess' event")]
public TTSSpeakerEvent OnClipLoadSuccess;
[Obsolete("Use 'OnLoadSuccess' event")]
public TTSSpeakerClipDataEvent OnClipDataLoadSuccess;
[Obsolete("Use 'OnPlaybackReady' event")]
public TTSSpeakerClipDataEvent OnClipDataPlaybackReady;
[Obsolete("Use 'OnPlaybackStart' event")]
public TTSSpeakerEvent OnStartSpeaking;
[Obsolete("Use 'OnPlaybackStart' event")]
public TTSSpeakerClipDataEvent OnClipDataPlaybackStart;
[Obsolete("Use 'OnPlaybackCancelled' event")]
public TTSSpeakerEvent OnCancelledSpeaking;
[Obsolete("Use 'OnPlaybackCancelled' event")]
public TTSSpeakerClipDataEvent OnClipDataPlaybackCancelled;
[Obsolete("Use 'OnPlaybackComplete' event")]
public TTSSpeakerEvent OnFinishedSpeaking;
[Obsolete("Use 'OnPlaybackComplete' event")]
public TTSSpeakerClipDataEvent OnClipDataPlaybackFinished;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f8f392c4b8438cd4e8a5318177de7a23
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,46 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System;
using Meta.WitAi.TTS.Data;
using UnityEngine;
using UnityEngine.Events;
namespace Meta.WitAi.TTS.Events
{
[Serializable]
public class TTSClipEvent : UnityEvent<TTSClipData>
{
}
[Serializable]
public class TTSClipErrorEvent : UnityEvent<TTSClipData, string>
{
}
[Serializable]
public class TTSStreamEvents
{
[Tooltip("Called when a audio clip stream begins")]
public TTSClipEvent OnStreamBegin = new TTSClipEvent();
[Tooltip("Called when a audio clip is ready for playback")]
public TTSClipEvent OnStreamReady = new TTSClipEvent();
[Tooltip("Called if/when an audio clip is adjusted")]
public TTSClipEvent OnStreamClipUpdate = new TTSClipEvent();
[Tooltip("Called when a audio clip is completely loaded")]
public TTSClipEvent OnStreamComplete = new TTSClipEvent();
[Tooltip("Called when a audio clip stream has been cancelled")]
public TTSClipEvent OnStreamCancel = new TTSClipEvent();
[Tooltip("Called when a audio clip stream has failed")]
public TTSClipErrorEvent OnStreamError = new TTSClipErrorEvent();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cc1209a088b657247b1f0c645ae7ee93
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,40 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System;
using Meta.WitAi.TTS.Data;
using UnityEngine;
using UnityEngine.Events;
namespace Meta.WitAi.TTS.Events
{
/// <summary>
/// Events related to web requests
/// </summary>
[Serializable]
public class TTSWebRequestEvents
{
[Tooltip("Called when a web request begins transmission")]
public TTSClipEvent OnRequestBegin = new TTSClipEvent();
[Tooltip("Called when a web request is cancelled")]
public TTSClipEvent OnRequestCancel = new TTSClipEvent();
[Tooltip("Called when a web request fails")]
public TTSClipErrorEvent OnRequestError = new TTSClipErrorEvent();
[Tooltip("Called when a web request receives first data")]
public TTSClipEvent OnRequestFirstResponse = new TTSClipEvent();
[Tooltip("Called when a web request is ready for playback")]
public TTSClipEvent OnRequestReady = new TTSClipEvent();
[Tooltip("Called when a web request is completed via success, cancellation or failure")]
public TTSClipEvent OnRequestComplete = new TTSClipEvent();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 59d937d620dd3ea45abde93fe056c240
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,17 @@
{
"name": "Meta.WitAi.TTS",
"rootNamespace": "",
"references": [
"GUID:1c28d8b71ced07540b7c271537363cc6",
"GUID:4504b1a6e0fdcc3498c30b266e4a63bf"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 8bbcefc153e1f1b4a98680670797dd16
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e8e61f36a843a8e4f92bb0985d1267d3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,224 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System;
using System.IO;
using System.Collections.Generic;
using UnityEngine;
using Meta.WitAi.TTS.Data;
using Meta.WitAi.TTS.Events;
using Meta.WitAi.TTS.Interfaces;
using Meta.WitAi.Utilities;
using Meta.WitAi.Requests;
using Meta.Voice.Audio;
namespace Meta.WitAi.TTS.Integrations
{
public class TTSDiskCache : MonoBehaviour, ITTSDiskCacheHandler
{
[Header("Disk Cache Settings")]
/// <summary>
/// The relative path from the DiskCacheLocation in TTSDiskCacheSettings
/// </summary>
[SerializeField] private string _diskPath = "TTS/";
public string DiskPath => _diskPath;
/// <summary>
/// The cache default settings
/// </summary>
[SerializeField] private TTSDiskCacheSettings _defaultSettings = new TTSDiskCacheSettings();
public TTSDiskCacheSettings DiskCacheDefaultSettings => _defaultSettings;
/// <summary>
/// The cache streaming events
/// </summary>
[SerializeField] private TTSStreamEvents _events = new TTSStreamEvents();
public TTSStreamEvents DiskStreamEvents
{
get => _events;
set { _events = value; }
}
// All currently performing stream requests
private Dictionary<string, VRequest> _streamRequests = new Dictionary<string, VRequest>();
// Cancel all requests
protected virtual void OnDestroy()
{
Dictionary<string, VRequest> requests = _streamRequests;
_streamRequests.Clear();
foreach (var request in requests.Values)
{
request.Cancel();
}
}
/// <summary>
/// Builds full cache path
/// </summary>
/// <param name="clipData"></param>
/// <returns></returns>
public string GetDiskCachePath(TTSClipData clipData)
{
// Disabled
if (!ShouldCacheToDisk(clipData))
{
return string.Empty;
}
// Get directory path
TTSDiskCacheLocation location = clipData.diskCacheSettings.DiskCacheLocation;
string directory = string.Empty;
switch (location)
{
case TTSDiskCacheLocation.Persistent:
directory = Application.persistentDataPath;
break;
case TTSDiskCacheLocation.Temporary:
directory = Application.temporaryCachePath;
break;
case TTSDiskCacheLocation.Preload:
directory = Application.streamingAssetsPath;
break;
}
if (string.IsNullOrEmpty(directory))
{
return string.Empty;
}
// Add tts cache path & clean
directory = Path.Combine(directory, DiskPath);
// Generate tts directory if possible
if (location != TTSDiskCacheLocation.Preload || !Application.isPlaying)
{
if (!IOUtility.CreateDirectory(directory, true))
{
VLog.E($"Failed to create tts directory\nPath: {directory}\nLocation: {location}");
return string.Empty;
}
}
// Return clip path
return Path.Combine(directory, clipData.clipID + "." + WitTTSVRequest.GetAudioExtension(clipData.audioType));
}
/// <summary>
/// Determine if should cache to disk or not
/// </summary>
/// <param name="clipData">All clip data</param>
/// <returns>Returns true if should cache to disk</returns>
public bool ShouldCacheToDisk(TTSClipData clipData)
{
return clipData != null && clipData.diskCacheSettings.DiskCacheLocation != TTSDiskCacheLocation.Stream && !string.IsNullOrEmpty(clipData.clipID);
}
/// <summary>
/// Determines if file is cached on disk
/// </summary>
/// <param name="clipData">Request data</param>
/// <returns>True if file is on disk</returns>
public void CheckCachedToDisk(TTSClipData clipData, Action<TTSClipData, bool> onCheckComplete)
{
// Get path
string cachePath = GetDiskCachePath(clipData);
if (string.IsNullOrEmpty(cachePath))
{
onCheckComplete?.Invoke(clipData, false);
return;
}
// Check if file exists
VRequest request = new VRequest();
bool canPerform = request.RequestFileExists(cachePath, (success, error) =>
{
// Remove
if (_streamRequests.ContainsKey(clipData.clipID))
{
_streamRequests.Remove(clipData.clipID);
}
// Complete
onCheckComplete(clipData, success);
});
if (canPerform)
{
_streamRequests[clipData.clipID] = request;
}
}
/// <summary>
/// Performs async load request
/// </summary>
public void StreamFromDiskCache(TTSClipData clipData)
{
// Invoke begin
DiskStreamEvents?.OnStreamBegin?.Invoke(clipData);
// Get file path
string filePath = GetDiskCachePath(clipData);
// Load clip async
VRequest request = new VRequest((progress) => clipData.loadProgress = progress);
bool canPerform = request.RequestAudioStream(clipData.clipStream, new Uri(request.CleanUrl(filePath)),
(clipStream, error) =>
{
clipData.clipStream = clipStream;
OnStreamComplete(clipData, error);
}, clipData.audioType, clipData.diskCacheSettings.StreamFromDisk);
if (canPerform)
{
_streamRequests[clipData.clipID] = request;
}
}
/// <summary>
/// Cancels unity request
/// </summary>
public void CancelDiskCacheStream(TTSClipData clipData)
{
// Ignore if not currently streaming
if (!_streamRequests.ContainsKey(clipData.clipID))
{
return;
}
// Get request
VRequest request = _streamRequests[clipData.clipID];
_streamRequests.Remove(clipData.clipID);
// Cancel immediately
request?.Cancel();
request = null;
// Call cancel
DiskStreamEvents?.OnStreamCancel?.Invoke(clipData);
}
// On stream completion
protected virtual void OnStreamComplete(TTSClipData clipData, string error)
{
// Ignore if not currently streaming
if (!_streamRequests.ContainsKey(clipData.clipID))
{
return;
}
// Remove from list
_streamRequests.Remove(clipData.clipID);
// Error
if (!string.IsNullOrEmpty(error))
{
DiskStreamEvents?.OnStreamError?.Invoke(clipData, error);
}
// Success
else
{
DiskStreamEvents?.OnStreamReady?.Invoke(clipData);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b0ffdd015bcb8ea41bb96f19a723bf7d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,208 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Serialization;
using Meta.WitAi.TTS.Data;
using Meta.WitAi.TTS.Interfaces;
using Meta.WitAi.TTS.Events;
namespace Meta.WitAi.TTS.Integrations
{
// A simple LRU Cache
public class TTSRuntimeCache : MonoBehaviour, ITTSRuntimeCacheHandler
{
/// <summary>
/// Whether or not to unload clip data after the clip capacity is hit
/// </summary>
[Header("Runtime Cache Settings")]
[Tooltip("Whether or not to unload clip data after the clip capacity is hit")]
[FormerlySerializedAs("_clipLimit")]
public bool ClipLimit = true;
/// <summary>
/// The maximum clips allowed in the runtime cache
/// </summary>
[Tooltip("The maximum clips allowed in the runtime cache")]
[FormerlySerializedAs("_clipCapacity")]
[Min(1)] public int ClipCapacity = 20;
/// <summary>
/// Whether or not to unload clip data after the ram capacity is hit
/// </summary>
[Tooltip("Whether or not to unload clip data after the ram capacity is hit")]
[FormerlySerializedAs("_ramLimit")]
public bool RamLimit = true;
/// <summary>
/// The maximum amount of RAM allowed in the runtime cache. In KBs
/// </summary>
[Tooltip("The maximum amount of RAM allowed in the runtime cache. In KBs")]
[FormerlySerializedAs("_ramCapacity")]
[Min(1)] public int RamCapacity = 32768;
/// <summary>
/// On clip added callback
/// </summary>
public TTSClipEvent OnClipAdded { get; set; } = new TTSClipEvent();
/// <summary>
/// On clip removed callback
/// </summary>
public TTSClipEvent OnClipRemoved { get; set; } = new TTSClipEvent();
// Clips & their ids
private Dictionary<string, TTSClipData> _clips = new Dictionary<string, TTSClipData>();
private List<string> _clipOrder = new List<string>();
/// <summary>
/// Simple getter for all clips
/// </summary>
public TTSClipData[] GetClips() => _clips.Values.ToArray();
// Remove all
protected virtual void OnDestroy()
{
_clips.Clear();
_clipOrder.Clear();
}
/// <summary>
/// Getter for a clip that also moves clip to the back of the queue
/// </summary>
public TTSClipData GetClip(string clipID)
{
// Id not found
if (!_clips.ContainsKey(clipID))
{
return null;
}
// Sort to end
int clipIndex = _clipOrder.IndexOf(clipID);
_clipOrder.RemoveAt(clipIndex);
_clipOrder.Add(clipID);
// Return clip
return _clips[clipID];
}
/// <summary>
/// Add clip to cache and ensure it is most recently referenced
/// </summary>
/// <param name="clipData"></param>
public bool AddClip(TTSClipData clipData)
{
// Do not add null
if (clipData == null)
{
return false;
}
// Remove from order
bool wasAdded = true;
int clipIndex = _clipOrder.IndexOf(clipData.clipID);
if (clipIndex != -1)
{
wasAdded = false;
_clipOrder.RemoveAt(clipIndex);
}
// Add clip
_clips[clipData.clipID] = clipData;
// Add to end of order
_clipOrder.Add(clipData.clipID);
// Evict least recently used clips
while (IsCacheFull() && _clipOrder.Count > 0)
{
// Remove clip
RemoveClip(_clipOrder[0]);
}
// Call add delegate even if removed
if (wasAdded && _clips.Keys.Count > 0)
{
OnClipAdded?.Invoke(clipData);
}
// True if successfully added
return _clips.Keys.Count > 0;
}
/// <summary>
/// Remove clip from cache immediately
/// </summary>
/// <param name="clipID"></param>
public void RemoveClip(string clipID)
{
// Id not found
if (!_clips.ContainsKey(clipID))
{
return;
}
// Remove from dictionary
TTSClipData clipData = _clips[clipID];
_clips.Remove(clipID);
// Remove from order list
int clipIndex = _clipOrder.IndexOf(clipID);
_clipOrder.RemoveAt(clipIndex);
// Call remove delegate
OnClipRemoved?.Invoke(clipData);
}
/// <summary>
/// Check if cache is full
/// </summary>
protected bool IsCacheFull()
{
// Capacity full
if (ClipLimit && _clipOrder.Count > ClipCapacity)
{
return true;
}
// Ram full
if (RamLimit && GetCacheDiskSize() > RamCapacity)
{
return true;
}
// Free
return false;
}
/// <summary>
/// Get RAM size of cache in KBs
/// </summary>
/// <returns>Returns size in KBs rounded up</returns>
public int GetCacheDiskSize()
{
long total = 0;
foreach (var key in _clips.Keys)
{
total += GetClipBytes(_clips[key].clipStream.Channels, _clips[key].clipStream.TotalSamples);
}
return (int)(total / (long)1024) + 1;
}
// Return bytes occupied by clip
public static long GetClipBytes(AudioClip clip)
{
if (clip != null)
{
return GetClipBytes(clip.channels, clip.samples);
}
return 0;
}
// Return bytes occupied by clip
public static long GetClipBytes(int channels, int samples)
{
return channels * samples * 2;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1d60dcb6d02034b4b96284db469db5e3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,498 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Meta.Voice.Audio;
using UnityEngine;
using UnityEngine.Serialization;
using Meta.WitAi.Interfaces;
using Meta.WitAi.Data.Configuration;
using Meta.WitAi.Json;
using Meta.WitAi.TTS.Data;
using Meta.WitAi.TTS.Events;
using Meta.WitAi.TTS.Interfaces;
using Meta.WitAi.Requests;
namespace Meta.WitAi.TTS.Integrations
{
[Serializable]
public class TTSWitVoiceSettings : TTSVoiceSettings
{
/// <summary>
/// Default voice name used if no voice is provided
/// </summary>
public const string DEFAULT_VOICE = "Charlie";
/// <summary>
/// Default style used if no style is provided
/// </summary>
public const string DEFAULT_STYLE = "default";
/// <summary>
/// Unique voice name
/// </summary>
public string voice = DEFAULT_VOICE;
/// <summary>
/// Voice style (ex. formal, fast)
/// </summary>
public string style = DEFAULT_STYLE;
/// <summary>
/// Text-to-speech speed percentage
/// </summary>
[Range(50, 200)]
public int speed = 100;
/// <summary>
/// Text-to-speech audio pitch percentage
/// </summary>
[Range(25, 200)]
public int pitch = 100;
/// <summary>
/// Checks if request can be decoded for TTS data
/// Example Data:
/// {
/// "q": "Text to be spoken"
/// "voice": "Charlie
/// }
/// </summary>
/// <param name="responseNode">The deserialized json data class</param>
/// <returns>True if request can be decoded</returns>
public static bool CanDecode(WitResponseNode responseNode)
{
return responseNode != null && responseNode.AsObject.HasChild(WitConstants.ENDPOINT_TTS_PARAM) && responseNode.AsObject.HasChild("voice");
}
}
[Serializable]
public struct TTSWitRequestSettings
{
public WitConfiguration configuration;
public TTSWitAudioType audioType;
public bool audioStream;
}
public class TTSWit : TTSService, ITTSVoiceProvider, ITTSWebHandler, IWitConfigurationProvider
{
#region TTSService
// Voice provider
public override ITTSVoiceProvider VoiceProvider => this;
// Request handler
public override ITTSWebHandler WebHandler => this;
// Runtime cache handler
public override ITTSRuntimeCacheHandler RuntimeCacheHandler
{
get
{
if (_runtimeCache == null)
{
_runtimeCache = gameObject.GetComponent<ITTSRuntimeCacheHandler>();
}
return _runtimeCache;
}
}
private ITTSRuntimeCacheHandler _runtimeCache;
// Cache handler
public override ITTSDiskCacheHandler DiskCacheHandler
{
get
{
if (_diskCache == null)
{
_diskCache = gameObject.GetComponent<ITTSDiskCacheHandler>();
}
return _diskCache;
}
}
private ITTSDiskCacheHandler _diskCache;
// Web request events
public TTSWebRequestEvents WebRequestEvents => Events.WebRequest;
// Configuration provider
public WitConfiguration Configuration => RequestSettings.configuration;
// Use wit tts vrequest type
protected override AudioType GetAudioType()
{
return WitTTSVRequest.GetAudioType(RequestSettings.audioType);
}
// Get tts request prior to transmission
private WitTTSVRequest GetTtsRequest(TTSClipData clipData)
{
// Apply audio type
clipData.audioType = GetAudioType();
clipData.queryStream = RequestSettings.audioStream;
// Return request
return new WitTTSVRequest(RequestSettings.configuration, clipData.queryRequestId,
clipData.textToSpeak, clipData.queryParameters,
RequestSettings.audioType, clipData.queryStream,
(progress) => OnRequestProgressUpdated(clipData, progress),
() => OnRequestFirstResponse(clipData));
}
// Download progress callbacks
private void OnRequestProgressUpdated(TTSClipData clipData, float newProgress)
{
if (clipData != null)
{
clipData.loadProgress = newProgress;
}
}
// Progress callbacks
private void OnRequestFirstResponse(TTSClipData clipData)
{
if (clipData != null)
{
WebRequestEvents?.OnRequestFirstResponse?.Invoke(clipData);
}
}
#endregion
#region ITTSWebHandler Streams
// Request settings
[Header("Web Request Settings")]
[FormerlySerializedAs("_settings")]
public TTSWitRequestSettings RequestSettings = new TTSWitRequestSettings
{
audioType = TTSWitAudioType.PCM,
audioStream = true,
};
// Use settings web stream events
public TTSStreamEvents WebStreamEvents { get; set; } = new TTSStreamEvents();
// Requests bly clip id
private Dictionary<string, VRequest> _webStreams = new Dictionary<string, VRequest>();
// Whether TTSService is valid
public override string GetInvalidError()
{
string invalidError = base.GetInvalidError();
if (!string.IsNullOrEmpty(invalidError))
{
return invalidError;
}
if (RequestSettings.configuration == null)
{
return "No WitConfiguration Set";
}
if (string.IsNullOrEmpty(RequestSettings.configuration.GetClientAccessToken()))
{
return "No WitConfiguration Client Token";
}
return string.Empty;
}
// Ensures text can be sent to wit web service
public string IsTextValid(string textToSpeak) => string.IsNullOrEmpty(textToSpeak) ? WitConstants.ENDPOINT_TTS_NO_TEXT : string.Empty;
/// <summary>
/// Method for performing a web load request
/// </summary>
/// <param name="clipData">Clip request data</param>
/// <param name="onStreamSetupComplete">Stream setup complete: returns clip and error if applicable</param>
public void RequestStreamFromWeb(TTSClipData clipData)
{
// Stream begin
WebStreamEvents?.OnStreamBegin?.Invoke(clipData);
// Check if valid
string validError = IsRequestValid(clipData, RequestSettings.configuration);
if (!string.IsNullOrEmpty(validError))
{
WebStreamEvents?.OnStreamError?.Invoke(clipData, validError);
return;
}
// Ignore if already performing
if (_webStreams.ContainsKey(clipData.clipID))
{
CancelWebStream(clipData);
}
// Begin request
WebRequestEvents?.OnRequestBegin?.Invoke(clipData);
// Whether to stream
bool stream = Application.isPlaying && RequestSettings.audioStream;
// Request tts
WitTTSVRequest request = GetTtsRequest(clipData);
request.RequestStream(clipData.clipStream,
(clipStream, error) =>
{
// Apply
_webStreams.Remove(clipData.clipID);
// Set new clip stream
clipData.clipStream = clipStream;
// Unloaded
if (clipData.loadState == TTSClipLoadState.Unloaded)
{
error = WitConstants.CANCEL_ERROR;
clipStream?.Unload();
}
// Error
if (!string.IsNullOrEmpty(error))
{
if (string.Equals(error, WitConstants.CANCEL_ERROR, StringComparison.CurrentCultureIgnoreCase))
{
WebStreamEvents?.OnStreamCancel?.Invoke(clipData);
WebRequestEvents?.OnRequestCancel?.Invoke(clipData);
}
else
{
WebStreamEvents?.OnStreamError?.Invoke(clipData, error);
WebRequestEvents?.OnRequestError?.Invoke(clipData, error);
}
}
// Success
else
{
WebStreamEvents?.OnStreamReady?.Invoke(clipData);
WebRequestEvents?.OnRequestReady?.Invoke(clipData);
if (!RequestSettings.audioStream || !WitTTSVRequest.CanStreamAudio(RequestSettings.audioType))
{
WebStreamEvents?.OnStreamComplete?.Invoke(clipData);
WebRequestEvents?.OnRequestComplete?.Invoke(clipData);
}
}
});
_webStreams[clipData.clipID] = request;
}
/// <summary>
/// Cancel web stream
/// </summary>
/// <param name="clipID">Unique clip id</param>
public bool CancelWebStream(TTSClipData clipData)
{
// Ignore without
if (!_webStreams.ContainsKey(clipData.clipID))
{
return false;
}
// Get request
VRequest request = _webStreams[clipData.clipID];
_webStreams.Remove(clipData.clipID);
// Destroy immediately
request?.Cancel();
request = null;
// Call delegate
WebStreamEvents?.OnStreamCancel?.Invoke(clipData);
WebRequestEvents?.OnRequestCancel?.Invoke(clipData);
// Success
return true;
}
#endregion
#region ITTSWebHandler Downloads
// Use settings web download events
public TTSDownloadEvents WebDownloadEvents { get; set; } = new TTSDownloadEvents();
// Requests by clip id
private Dictionary<string, WitVRequest> _webDownloads = new Dictionary<string, WitVRequest>();
/// <summary>
/// Method for performing a web load request
/// </summary>
/// <param name="clipData">Clip request data</param>
/// <param name="downloadPath">Path to save clip</param>
public void RequestDownloadFromWeb(TTSClipData clipData, string downloadPath)
{
// Begin
WebDownloadEvents?.OnDownloadBegin?.Invoke(clipData, downloadPath);
// Ensure valid
string validError = IsRequestValid(clipData, RequestSettings.configuration);
if (!string.IsNullOrEmpty(validError))
{
WebDownloadEvents?.OnDownloadError?.Invoke(clipData, downloadPath, validError);
return;
}
// Abort if already performing
if (_webDownloads.ContainsKey(clipData.clipID))
{
CancelWebDownload(clipData, downloadPath);
}
// Begin request
WebRequestEvents?.OnRequestBegin?.Invoke(clipData);
// Request tts
WitTTSVRequest request = GetTtsRequest(clipData);
request.RequestDownload(downloadPath,
(success, error) =>
{
_webDownloads.Remove(clipData.clipID);
if (string.IsNullOrEmpty(error))
{
WebDownloadEvents?.OnDownloadSuccess?.Invoke(clipData, downloadPath);
WebRequestEvents?.OnRequestReady?.Invoke(clipData);
}
else
{
WebDownloadEvents?.OnDownloadError?.Invoke(clipData, downloadPath, error);
WebRequestEvents?.OnRequestError?.Invoke(clipData, error);
}
WebRequestEvents?.OnRequestComplete?.Invoke(clipData);
});
_webDownloads[clipData.clipID] = request;
}
/// <summary>
/// Method for cancelling a running load request
/// </summary>
/// <param name="clipData">Clip request data</param>
public bool CancelWebDownload(TTSClipData clipData, string downloadPath)
{
// Ignore if not performing
if (!_webDownloads.ContainsKey(clipData.clipID))
{
return false;
}
// Get request
WitVRequest request = _webDownloads[clipData.clipID];
_webDownloads.Remove(clipData.clipID);
// Destroy immediately
request?.Cancel();
request = null;
// Download cancelled
WebDownloadEvents?.OnDownloadCancel?.Invoke(clipData, downloadPath);
WebRequestEvents?.OnRequestCancel?.Invoke(clipData);
// Success
return true;
}
#endregion
#region ITTSVoiceProvider
// Preset voice settings
[Header("Voice Settings")]
#if UNITY_2021_3_2 || UNITY_2021_3_3 || UNITY_2021_3_4 || UNITY_2021_3_5
[NonReorderable]
#endif
[SerializeField] private TTSWitVoiceSettings[] _presetVoiceSettings;
public TTSWitVoiceSettings[] PresetWitVoiceSettings => _presetVoiceSettings;
// Cast to voice array
public TTSVoiceSettings[] PresetVoiceSettings
{
get
{
if (_presetVoiceSettings == null)
{
_presetVoiceSettings = new TTSWitVoiceSettings[] { };
}
return _presetVoiceSettings;
}
}
// Default voice setting uses the first voice in the list
public TTSVoiceSettings VoiceDefaultSettings => PresetVoiceSettings == null || PresetVoiceSettings.Length == 0 ? null : PresetVoiceSettings[0];
#if UNITY_EDITOR
// Apply settings
public void SetVoiceSettings(TTSWitVoiceSettings[] newVoiceSettings)
{
_presetVoiceSettings = newVoiceSettings;
}
#endif
// Convert voice settings into dictionary to be used with web requests
private const string VOICE_KEY = "voice";
private const string STYLE_KEY = "style";
public Dictionary<string, string> EncodeVoiceSettings(TTSVoiceSettings voiceSettings)
{
Dictionary<string, string> parameters = new Dictionary<string, string>();
if (voiceSettings != null)
{
foreach (FieldInfo field in GetVoiceSettingsFields(voiceSettings))
{
// Ensure field value exists
object fieldVal = field.GetValue(voiceSettings);
if (fieldVal != null)
{
// Clamp in between range
RangeAttribute range = field.GetCustomAttribute<RangeAttribute>();
if (range != null && field.FieldType == typeof(int))
{
int oldFloat = (int) fieldVal;
int newFloat = Mathf.Clamp(oldFloat, (int)range.min, (int)range.max);
if (oldFloat != newFloat)
{
fieldVal = newFloat;
}
}
// Apply
parameters[field.Name] = fieldVal.ToString();
}
}
// Set default if no voice is provided
if (!parameters.ContainsKey(VOICE_KEY) || string.IsNullOrEmpty(parameters[VOICE_KEY]))
{
parameters[VOICE_KEY] = TTSWitVoiceSettings.DEFAULT_VOICE;
}
// Set default if no style is given
if (!parameters.ContainsKey(STYLE_KEY) || string.IsNullOrEmpty(parameters[STYLE_KEY]))
{
parameters[STYLE_KEY] = TTSWitVoiceSettings.DEFAULT_STYLE;
}
}
return parameters;
}
// Obtain all fields for a specific voice settings
private static readonly Dictionary<Type, FieldInfo[]> _settingsFields = new Dictionary<Type, FieldInfo[]>();
private static FieldInfo[] GetVoiceSettingsFields(TTSVoiceSettings voiceSettings)
{
// Return fields if already found
Type settingsType = voiceSettings.GetType();
if (_settingsFields.ContainsKey(settingsType))
{
return _settingsFields[settingsType];
}
// Get public/instance fields
FieldInfo[] fields = settingsType.GetFields(BindingFlags.Public | BindingFlags.Instance);
// Remove fields from TTSVoiceSettings
Type baseType = typeof(TTSVoiceSettings);
fields = fields.ToList().FindAll((field) => field.DeclaringType != baseType).ToArray();
// Apply & return
_settingsFields[settingsType] = fields;
return fields;
}
// Returns an error if request is not valid
private string IsRequestValid(TTSClipData clipData, WitConfiguration configuration)
{
// Invalid tts
string invalidError = GetInvalidError();
if (!string.IsNullOrEmpty(invalidError))
{
return invalidError;
}
// Invalid clip
if (clipData == null)
{
return "No clip data provided";
}
// Success
return string.Empty;
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a6b3124b830442d45b9f357ff99b152f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0e55113cde3e75a48a7c733b0c8e8a3e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,60 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System;
using Meta.WitAi.TTS.Data;
using Meta.WitAi.TTS.Events;
namespace Meta.WitAi.TTS.Interfaces
{
public interface ITTSDiskCacheHandler
{
/// <summary>
/// All events for streaming from the disk cache
/// </summary>
TTSStreamEvents DiskStreamEvents { get; set; }
/// <summary>
/// The default cache settings
/// </summary>
TTSDiskCacheSettings DiskCacheDefaultSettings { get; }
/// <summary>
/// A method for obtaining the path to a specific cache clip
/// </summary>
/// <param name="clipData">Clip request data</param>
/// <returns>Returns the clip's cache path</returns>
string GetDiskCachePath(TTSClipData clipData);
/// <summary>
/// Whether or not the clip data should be cached on disk
/// </summary>
/// <param name="clipData">Clip request data</param>
/// <returns>Returns true if should cache</returns>
bool ShouldCacheToDisk(TTSClipData clipData);
/// <summary>
/// Performs a check to determine if a file is cached to disk or not
/// </summary>
/// <param name="clipData">Clip request data</param>
/// <returns>Returns true if currently on disk (Except for Android Streaming Assets)</returns>
void CheckCachedToDisk(TTSClipData clipData, Action<TTSClipData, bool> onCheckComplete);
/// <summary>
/// Method for streaming from disk cache
/// </summary>
/// <param name="clipData">Clip request data</param>
void StreamFromDiskCache(TTSClipData clipData);
/// <summary>
/// Method for cancelling a running cache load request
/// </summary>
/// <param name="clipData">Clip request data</param>
void CancelDiskCacheStream(TTSClipData clipData);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4a1c9ff5df097b741b2059f3e92081c2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,45 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using Meta.WitAi.TTS.Data;
using Meta.WitAi.TTS.Events;
namespace Meta.WitAi.TTS.Interfaces
{
public interface ITTSRuntimeCacheHandler
{
/// <summary>
/// Callback for clips being added to the runtime cache
/// </summary>
TTSClipEvent OnClipAdded { get; set; }
/// <summary>
/// Callback for clips being removed from the runtime cache
/// </summary>
TTSClipEvent OnClipRemoved { get; set; }
/// <summary>
/// Method for obtaining all cached clips
/// </summary>
TTSClipData[] GetClips();
/// <summary>
/// Method for obtaining a specific cached clip
/// </summary>
TTSClipData GetClip(string clipID);
/// <summary>
/// Method for adding a clip to the cache
/// </summary>
/// <param name="clipData"></param>
/// <returns></returns>
bool AddClip(TTSClipData clipData);
/// <summary>
/// Method for removing a clip from the cache
/// </summary>
void RemoveClip(string clipID);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 476c71d5ecb266a42960ce92aae00508
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,34 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System.Collections.Generic;
using Meta.WitAi.TTS.Data;
namespace Meta.WitAi.TTS.Interfaces
{
public interface ITTSVoiceProvider
{
/// <summary>
/// Returns preset voice data if no voice data is selected.
/// Useful for menu ai, etc.
/// </summary>
TTSVoiceSettings VoiceDefaultSettings { get; }
/// <summary>
/// Returns all preset voice settings
/// </summary>
TTSVoiceSettings[] PresetVoiceSettings { get; }
/// <summary>
/// Encode voice data to be transmitted
/// </summary>
/// <param name="voiceSettings">The voice settings class</param>
/// <returns>Returns a dictionary with all variables</returns>
Dictionary<string, string> EncodeVoiceSettings(TTSVoiceSettings voiceSettings);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e7bd457b1d9cef444b011a63b950a90d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,64 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using Meta.WitAi.TTS.Data;
using Meta.WitAi.TTS.Events;
namespace Meta.WitAi.TTS.Interfaces
{
public interface ITTSWebHandler
{
/// <summary>
/// Any web request performs this event
/// </summary>
TTSWebRequestEvents WebRequestEvents { get; }
/// <summary>
/// Streaming events
/// </summary>
TTSStreamEvents WebStreamEvents { get; }
/// <summary>
/// Method for determining if text to speak is valid
/// </summary>
/// <param name="textToSpeak">Text to be spoken by TTS</param>
/// <returns>Invalid error</returns>
string IsTextValid(string textToSpeak);
/// <summary>
/// Method for performing a web load request
/// </summary>
/// <param name="clipData">Clip request data</param>
void RequestStreamFromWeb(TTSClipData clipData);
/// <summary>
/// Cancel web stream
/// </summary>
/// <param name="clipID">Clip unique identifier</param>
bool CancelWebStream(TTSClipData clipData);
/// <summary>
/// Download events
/// </summary>
TTSDownloadEvents WebDownloadEvents { get; }
/// <summary>
/// Method for performing a web load request
/// </summary>
/// <param name="clipData">Clip request data</param>
/// <param name="downloadPath">Path to save clip</param>
void RequestDownloadFromWeb(TTSClipData clipData, string downloadPath);
/// <summary>
/// Cancel web download
/// </summary>
/// <param name="clipID">Clip unique identifier</param>
/// <param name="downloadPath">Path to save clip</param>
bool CancelWebDownload(TTSClipData clipData, string downloadPath);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 210f082ad442e48479f3f7ee7eaeafed
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,34 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System.Collections.Generic;
using Meta.WitAi.TTS.Utilities;
namespace Meta.WitAi.TTS.Interfaces
{
public interface ISpeakerTextPreprocessor
{
/// <summary>
/// Called before prefix/postfix modifications are applied to the input string
/// </summary>
/// <param name="speaker">The speaker that will be used to speak the resulting text</param>
/// <param name="phrases">The current phrase list that will be used for speech. Can be added to or removed as needed.</param>
void OnPreprocessTTS(TTSSpeaker speaker, List<string> phrases);
}
public interface ISpeakerTextPostprocessor
{
/// <summary>
/// Called after prefix/postfix modifications are applied to the input string
/// </summary>
/// <param name="speaker">The speaker that will be used to speak the resulting text</param>
/// <param name="phrases">The current phrase list that will be used for speech. Can be added to or removed as needed.</param>
void OnPostprocessTTS(TTSSpeaker speaker, List<string> phrases);
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b39a041597ab4bd9b33e797f74fb2125
timeCreated: 1671215442
@@ -0,0 +1,969 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System;
using System.Collections;
using System.Text;
using System.Security.Cryptography;
using System.Collections.Generic;
using System.Linq;
using Meta.Voice.Audio;
using Meta.WitAi.Requests;
using UnityEngine;
using Meta.WitAi.TTS.Data;
using Meta.WitAi.TTS.Events;
using Meta.WitAi.TTS.Interfaces;
namespace Meta.WitAi.TTS
{
public abstract class TTSService : MonoBehaviour
{
#region SETUP
// Accessor
public static TTSService Instance
{
get
{
if (_instance == null)
{
// Get all services
TTSService[] services = Resources.FindObjectsOfTypeAll<TTSService>();
if (services != null)
{
// Set as first instance that isn't a prefab
_instance = Array.Find(services, (o) => o.gameObject.scene.rootCount != 0);
}
}
return _instance;
}
}
private static TTSService _instance;
/// <summary>
/// Audio system to be used for streaming & playback
/// </summary>
public IAudioSystem AudioSystem
{
get
{
if (_audioSystem == null && Application.isPlaying)
{
// Search on TTS Service
_audioSystem = gameObject.GetComponent<IAudioSystem>();
if (_audioSystem == null)
{
// Add default unity audio system if not found
_audioSystem = gameObject.AddComponent<UnityAudioSystem>();
}
}
return _audioSystem;
}
set => _audioSystem = value;
}
private IAudioSystem _audioSystem;
// Handles TTS runtime cache
public abstract ITTSRuntimeCacheHandler RuntimeCacheHandler { get; }
// Handles TTS cache requests
public abstract ITTSDiskCacheHandler DiskCacheHandler { get; }
// Handles TTS web requests
public abstract ITTSWebHandler WebHandler { get; }
// Handles TTS voice presets
public abstract ITTSVoiceProvider VoiceProvider { get; }
/// <summary>
/// Static event called whenever any TTSService.Awake is called
/// </summary>
public static event Action<TTSService> OnServiceStart;
/// <summary>
/// Static event called whenever any TTSService.OnDestroy is called
/// </summary>
public static event Action<TTSService> OnServiceDestroy;
/// <summary>
/// Returns error if invalid
/// </summary>
public virtual string GetInvalidError()
{
if (WebHandler == null)
{
return "Web Handler Missing";
}
if (VoiceProvider == null)
{
return "Voice Provider Missing";
}
return string.Empty;
}
// Handles TTS events
public TTSServiceEvents Events => _events;
[Header("Event Settings")]
[SerializeField] private TTSServiceEvents _events = new TTSServiceEvents();
// Set instance
protected virtual void Awake()
{
_instance = this;
_delegates = false;
}
// Call event
protected virtual void Start()
{
OnServiceStart?.Invoke(this);
}
// Log if invalid
protected virtual void OnEnable()
{
string validError = GetInvalidError();
if (!string.IsNullOrEmpty(validError))
{
VLog.W(validError);
}
}
// Remove delegates
protected virtual void OnDisable()
{
RemoveDelegates();
}
// Add delegates
private bool _delegates = false;
protected virtual void AddDelegates()
{
// Ignore if already added
if (_delegates)
{
return;
}
_delegates = true;
if (RuntimeCacheHandler != null)
{
RuntimeCacheHandler.OnClipAdded.AddListener(OnRuntimeClipAdded);
RuntimeCacheHandler.OnClipRemoved.AddListener(OnRuntimeClipRemoved);
}
if (DiskCacheHandler != null)
{
DiskCacheHandler.DiskStreamEvents.OnStreamBegin.AddListener(OnDiskStreamBegin);
DiskCacheHandler.DiskStreamEvents.OnStreamCancel.AddListener(OnDiskStreamCancel);
DiskCacheHandler.DiskStreamEvents.OnStreamReady.AddListener(OnDiskStreamReady);
DiskCacheHandler.DiskStreamEvents.OnStreamError.AddListener(OnDiskStreamError);
}
if (WebHandler != null)
{
WebHandler.WebStreamEvents.OnStreamBegin.AddListener(OnWebStreamBegin);
WebHandler.WebStreamEvents.OnStreamCancel.AddListener(OnWebStreamCancel);
WebHandler.WebStreamEvents.OnStreamReady.AddListener(OnWebStreamReady);
WebHandler.WebStreamEvents.OnStreamError.AddListener(OnWebStreamError);
WebHandler.WebDownloadEvents.OnDownloadBegin.AddListener(OnWebDownloadBegin);
WebHandler.WebDownloadEvents.OnDownloadCancel.AddListener(OnWebDownloadCancel);
WebHandler.WebDownloadEvents.OnDownloadSuccess.AddListener(OnWebDownloadSuccess);
WebHandler.WebDownloadEvents.OnDownloadError.AddListener(OnWebDownloadError);
}
}
// Remove delegates
protected virtual void RemoveDelegates()
{
// Ignore if not yet added
if (!_delegates)
{
return;
}
_delegates = false;
if (RuntimeCacheHandler != null)
{
RuntimeCacheHandler.OnClipAdded.RemoveListener(OnRuntimeClipAdded);
RuntimeCacheHandler.OnClipRemoved.RemoveListener(OnRuntimeClipRemoved);
}
if (DiskCacheHandler != null)
{
DiskCacheHandler.DiskStreamEvents.OnStreamBegin.RemoveListener(OnDiskStreamBegin);
DiskCacheHandler.DiskStreamEvents.OnStreamCancel.RemoveListener(OnDiskStreamCancel);
DiskCacheHandler.DiskStreamEvents.OnStreamReady.RemoveListener(OnDiskStreamReady);
DiskCacheHandler.DiskStreamEvents.OnStreamError.RemoveListener(OnDiskStreamError);
}
if (WebHandler != null)
{
WebHandler.WebStreamEvents.OnStreamBegin.RemoveListener(OnWebStreamBegin);
WebHandler.WebStreamEvents.OnStreamCancel.RemoveListener(OnWebStreamCancel);
WebHandler.WebStreamEvents.OnStreamReady.RemoveListener(OnWebStreamReady);
WebHandler.WebStreamEvents.OnStreamError.RemoveListener(OnWebStreamError);
WebHandler.WebDownloadEvents.OnDownloadBegin.RemoveListener(OnWebDownloadBegin);
WebHandler.WebDownloadEvents.OnDownloadCancel.RemoveListener(OnWebDownloadCancel);
WebHandler.WebDownloadEvents.OnDownloadSuccess.RemoveListener(OnWebDownloadSuccess);
WebHandler.WebDownloadEvents.OnDownloadError.RemoveListener(OnWebDownloadError);
}
}
// Remove instance
protected virtual void OnDestroy()
{
if (_instance == this)
{
_instance = null;
}
UnloadAll();
OnServiceDestroy?.Invoke(this);
}
/// <summary>
/// Get clip log data
/// </summary>
protected virtual string GetClipLog(string logMessage, TTSClipData clipData)
{
StringBuilder builder = new StringBuilder();
builder.AppendLine(logMessage);
if (clipData != null)
{
builder.AppendLine($"Voice: {(clipData.voiceSettings == null ? "Default" : clipData.voiceSettings.SettingsId)}");
builder.AppendLine($"Text: {clipData.textToSpeak}");
builder.AppendLine($"ID: {clipData.clipID}");
TTSDiskCacheLocation cacheLocation = TTSDiskCacheLocation.Stream;
if (DiskCacheHandler != null)
{
TTSDiskCacheSettings settings = clipData.diskCacheSettings;
if (settings == null)
{
settings = DiskCacheHandler.DiskCacheDefaultSettings;
}
if (settings != null)
{
cacheLocation = settings.DiskCacheLocation;
}
}
builder.AppendLine($"Cache: {cacheLocation}");
builder.AppendLine($"Type: {clipData.audioType}");
if (clipData.clipStream != null)
{
builder.AppendLine($"Length: {clipData.clipStream.Length:0.00} seconds");
}
}
return builder.ToString();
}
#endregion
#region HELPERS
// Frequently used keys
private const string CLIP_ID_DELIM = "|";
private readonly SHA256 CLIP_HASH = SHA256.Create();
/// <summary>
/// Gets the text to be spoken after applying all relevant voice settings.
/// </summary>
/// <param name="textToSpeak">Text to be spoken by a particular voice</param>
/// <param name="voiceSettings">Voice settings to be used</param>
/// <returns>Returns a the final text to be spoken</returns>
public string GetFinalText(string textToSpeak, TTSVoiceSettings voiceSettings)
{
StringBuilder result = new StringBuilder();
AppendFinalText(result, textToSpeak, voiceSettings);
return result.ToString();
}
// Finalize text using a string builder
protected virtual void AppendFinalText(StringBuilder builder, string textToSpeak, TTSVoiceSettings voiceSettings)
{
if (!string.IsNullOrEmpty(voiceSettings?.PrependedText))
{
builder.Append(voiceSettings.PrependedText);
}
builder.Append(textToSpeak);
if (!string.IsNullOrEmpty(voiceSettings?.AppendedText))
{
builder.Append(voiceSettings.AppendedText);
}
}
/// <summary>
/// Obtain unique id for clip data
/// </summary>
public virtual string GetClipID(string textToSpeak, TTSVoiceSettings voiceSettings)
{
// Get a text string for a unique id
StringBuilder uniqueId = new StringBuilder();
// Add all data items
if (VoiceProvider != null)
{
Dictionary<string, string> data = VoiceProvider.EncodeVoiceSettings(voiceSettings);
foreach (var key in data.Keys)
{
string keyClean = data[key].Replace(CLIP_ID_DELIM, "");
uniqueId.Append(keyClean);
uniqueId.Append(CLIP_ID_DELIM);
}
}
// Finally, add unique id
AppendFinalText(uniqueId, textToSpeak, voiceSettings);
// Return id
return GetSha256Hash(CLIP_HASH, uniqueId.ToString().ToLower());
}
private string GetSha256Hash(SHA256 shaHash, string input)
{
// Convert the input string to a byte array and compute the hash.
byte[] data = shaHash.ComputeHash(Encoding.UTF8.GetBytes(input));
// Create a new Stringbuilder to collect the bytes
// and create a string.
StringBuilder sBuilder = new StringBuilder();
// Loop through each byte of the hashed data
// and format each one as a hexadecimal string.
for (int i = 0; i < data.Length; i++)
{
sBuilder.Append(data[i].ToString("x2"));
}
// Return the hexadecimal string.
return sBuilder.ToString();
}
/// <summary>
/// Creates new clip data or returns existing cached clip
/// </summary>
/// <param name="textToSpeak">Text to speak</param>
/// <param name="clipID">Unique clip id</param>
/// <param name="voiceSettings">Voice settings</param>
/// <param name="diskCacheSettings">Disk Cache settings</param>
/// <returns>Clip data structure</returns>
protected virtual TTSClipData CreateClipData(string textToSpeak, string clipID, TTSVoiceSettings voiceSettings,
TTSDiskCacheSettings diskCacheSettings)
{
// Use default voice settings if none are set
if (voiceSettings == null && VoiceProvider != null)
{
voiceSettings = VoiceProvider.VoiceDefaultSettings;
}
// Use default disk cache settings if none are set
if (diskCacheSettings == null && DiskCacheHandler != null)
{
diskCacheSettings = DiskCacheHandler.DiskCacheDefaultSettings;
}
// Determine clip id if empty
if (string.IsNullOrEmpty(clipID))
{
clipID = GetClipID(textToSpeak, voiceSettings);
}
// Get clip from runtime cache if applicable
TTSClipData clipData = GetRuntimeCachedClip(clipID);
if (clipData != null)
{
return clipData;
}
// Generate new clip data
clipData = new TTSClipData()
{
clipID = clipID,
audioType = GetAudioType(),
textToSpeak = GetFinalText(textToSpeak, voiceSettings),
voiceSettings = voiceSettings,
diskCacheSettings = diskCacheSettings,
loadState = TTSClipLoadState.Unloaded,
loadProgress = 0f,
queryParameters = VoiceProvider?.EncodeVoiceSettings(voiceSettings),
queryStream = false,
clipStream = CreateClipStream()
};
// Return generated clip
return clipData;
}
// Generate a new audio clip stream
protected virtual IAudioClipStream CreateClipStream()
{
// Default
if (AudioSystem == null)
{
return new UnityAudioClipStream(WitConstants.ENDPOINT_TTS_CHANNELS, WitConstants.ENDPOINT_TTS_SAMPLE_RATE, 0.1f);
}
// Get audio clip via audio system
return AudioSystem.GetAudioClipStream(WitConstants.ENDPOINT_TTS_CHANNELS,
WitConstants.ENDPOINT_TTS_SAMPLE_RATE);
}
// Get audio type
protected virtual AudioType GetAudioType()
{
return AudioType.WAV;
}
// Set clip state
protected virtual void SetClipLoadState(TTSClipData clipData, TTSClipLoadState loadState)
{
clipData.loadState = loadState;
clipData.onStateChange?.Invoke(clipData, clipData.loadState);
}
#endregion
#region LOAD
// TTS Request options
public TTSClipData Load(string textToSpeak, Action<TTSClipData, string> onStreamReady = null) => Load(textToSpeak, null, null, null, onStreamReady);
public TTSClipData Load(string textToSpeak, string presetVoiceId, Action<TTSClipData, string> onStreamReady = null) => Load(textToSpeak, null, GetPresetVoiceSettings(presetVoiceId), null, onStreamReady);
public TTSClipData Load(string textToSpeak, string presetVoiceId, TTSDiskCacheSettings diskCacheSettings, Action<TTSClipData, string> onStreamReady = null) => Load(textToSpeak, null, GetPresetVoiceSettings(presetVoiceId), diskCacheSettings, onStreamReady);
public TTSClipData Load(string textToSpeak, TTSVoiceSettings voiceSettings, TTSDiskCacheSettings diskCacheSettings, Action<TTSClipData, string> onStreamReady = null) => Load(textToSpeak, null, voiceSettings, diskCacheSettings, onStreamReady);
/// <summary>
/// Perform a request for a TTS audio clip
/// </summary>
/// <param name="textToSpeak">Text to be spoken in clip</param>
/// <param name="clipID">Unique clip id</param>
/// <param name="voiceSettings">Custom voice settings</param>
/// <param name="diskCacheSettings">Custom cache settings</param>
/// <returns>Generated TTS clip data</returns>
public virtual TTSClipData Load(string textToSpeak, string clipID, TTSVoiceSettings voiceSettings,
TTSDiskCacheSettings diskCacheSettings, Action<TTSClipData, string> onStreamReady)
{
// Add delegates if needed
AddDelegates();
// Get clip data
TTSClipData clipData = CreateClipData(textToSpeak, clipID, voiceSettings, diskCacheSettings);
if (clipData == null)
{
VLog.E("No clip provided");
onStreamReady?.Invoke(clipData, "No clip provided");
return null;
}
if (string.IsNullOrEmpty(clipData.textToSpeak))
{
clipData.loadState = TTSClipLoadState.Loaded;
}
// From Runtime Cache
if (clipData.loadState != TTSClipLoadState.Unloaded)
{
// Add callback
if (onStreamReady != null)
{
// Call once ready
if (clipData.loadState == TTSClipLoadState.Preparing)
{
clipData.onPlaybackReady += (e) => onStreamReady(clipData, e);
}
// Call after return
else
{
CoroutineUtility.StartCoroutine(CallAfterAMoment(() => onStreamReady(clipData,
clipData.loadState == TTSClipLoadState.Loaded ? string.Empty : "Error")));
}
}
// Return clip
return clipData;
}
// Add to runtime cache if possible
if (RuntimeCacheHandler != null)
{
if (!RuntimeCacheHandler.AddClip(clipData))
{
// Add callback
if (onStreamReady != null)
{
// Call once ready
if (clipData.loadState == TTSClipLoadState.Preparing)
{
clipData.onPlaybackReady += (e) => onStreamReady(clipData, e);
}
// Call after return
else
{
CoroutineUtility.StartCoroutine(CallAfterAMoment(() => onStreamReady(clipData,
clipData.loadState == TTSClipLoadState.Loaded ? string.Empty : "Error")));
}
}
// Return clip
return clipData;
}
}
// Load begin
else
{
OnLoadBegin(clipData);
}
// Add on ready delegate
clipData.onPlaybackReady += (error) => onStreamReady?.Invoke(clipData, error);
// Wait a moment and load
CoroutineUtility.StartCoroutine(CallAfterAMoment(() =>
{
// Check for invalid text
string invalidError = WebHandler.IsTextValid(clipData.textToSpeak);
if (!string.IsNullOrEmpty(invalidError))
{
OnWebStreamError(clipData, invalidError);
return;
}
// If should cache to disk, attempt to do so
if (ShouldCacheToDisk(clipData))
{
// Download was canceled before starting
if (clipData.loadState != TTSClipLoadState.Preparing)
{
string downloadPath = DiskCacheHandler.GetDiskCachePath(clipData);
OnWebDownloadBegin(clipData, downloadPath);
OnWebDownloadCancel(clipData, downloadPath);
OnWebStreamBegin(clipData);
OnWebStreamCancel(clipData);
return;
}
// Download
DownloadToDiskCache(clipData, (clipData2, downloadPath, error) =>
{
// Download was canceled before starting
if (string.Equals(error, WitConstants.CANCEL_ERROR))
{
OnWebStreamBegin(clipData);
OnWebStreamCancel(clipData);
return;
}
// Not in cache & cannot download
if (string.Equals(error, WitConstants.ERROR_TTS_CACHE_DOWNLOAD))
{
WebHandler?.RequestStreamFromWeb(clipData);
return;
}
// Download failed, throw error
if (!string.IsNullOrEmpty(error))
{
OnWebStreamBegin(clipData);
OnWebStreamError(clipData, error);
return;
}
// Stream from Cache
DiskCacheHandler?.StreamFromDiskCache(clipData);
});
}
// Simply stream from the web
else
{
// Stream was canceled before starting
if (clipData.loadState != TTSClipLoadState.Preparing)
{
OnWebStreamBegin(clipData);
OnWebStreamCancel(clipData);
return;
}
// Stream
WebHandler?.RequestStreamFromWeb(clipData);
}
}));
// Return data
return clipData;
}
// Wait a moment
private IEnumerator CallAfterAMoment(Action call)
{
if (Application.isPlaying)
{
yield return new WaitForEndOfFrame();
}
else
{
yield return null;
}
call();
}
// Load begin
private void OnLoadBegin(TTSClipData clipData)
{
// Now preparing
SetClipLoadState(clipData, TTSClipLoadState.Preparing);
// Begin load
VLog.I(GetClipLog("Load Clip", clipData));
Events?.OnClipCreated?.Invoke(clipData);
}
// Handle begin of disk cache streaming
private void OnDiskStreamBegin(TTSClipData clipData) => OnStreamBegin(clipData, true);
private void OnWebStreamBegin(TTSClipData clipData) => OnStreamBegin(clipData, false);
private void OnStreamBegin(TTSClipData clipData, bool fromDisk)
{
// Set delegates for clip stream update/completion
if (clipData.clipStream != null)
{
clipData.clipStream.OnStreamUpdated = (cs) => OnStreamUpdated(clipData, cs, fromDisk);
clipData.clipStream.OnStreamComplete = (cs) => OnStreamComplete(clipData, cs, fromDisk);
}
// Callback delegate
VLog.I(GetClipLog($"{(fromDisk ? "Disk" : "Web")} Stream Begin", clipData));
Events?.Stream?.OnStreamBegin?.Invoke(clipData);
}
// Handle cancel of disk cache streaming
private void OnDiskStreamCancel(TTSClipData clipData) => OnStreamCancel(clipData, true);
private void OnWebStreamCancel(TTSClipData clipData) => OnStreamCancel(clipData, false);
private void OnStreamCancel(TTSClipData clipData, bool fromDisk)
{
// Handled as an error
SetClipLoadState(clipData, TTSClipLoadState.Error);
// Invoke
clipData.onPlaybackReady?.Invoke(WitConstants.CANCEL_ERROR);
clipData.onPlaybackReady = null;
// Callback delegate
VLog.I(GetClipLog($"{(fromDisk ? "Disk" : "Web")} Stream Canceled", clipData));
Events?.Stream?.OnStreamCancel?.Invoke(clipData);
// Unload clip
Unload(clipData);
}
// Handle disk cache streaming error
private void OnDiskStreamError(TTSClipData clipData, string error) => OnStreamError(clipData, error, true);
private void OnWebStreamError(TTSClipData clipData, string error) => OnStreamError(clipData, error, false);
private void OnStreamError(TTSClipData clipData, string error, bool fromDisk)
{
// Cancelled
if (error.Equals(WitConstants.CANCEL_ERROR))
{
OnStreamCancel(clipData, fromDisk);
return;
}
// Error
SetClipLoadState(clipData, TTSClipLoadState.Error);
// Invoke playback is ready
clipData.onPlaybackReady?.Invoke(error);
clipData.onPlaybackReady = null;
// Stream error
VLog.E(GetClipLog($"{(fromDisk ? "Disk" : "Web")} Stream Error\nError: {error}", clipData));
Events?.Stream?.OnStreamError?.Invoke(clipData, error);
// Unload clip
Unload(clipData);
}
// Handle successful completion of disk cache streaming
private void OnDiskStreamReady(TTSClipData clipData) => OnStreamReady(clipData, true);
private void OnWebStreamReady(TTSClipData clipData) => OnStreamReady(clipData, false);
private void OnStreamReady(TTSClipData clipData, bool fromDisk)
{
// Refresh cache for file size
if (RuntimeCacheHandler != null)
{
// Stop forcing an unload if runtime cache update fails
RuntimeCacheHandler.OnClipRemoved.RemoveListener(OnRuntimeClipRemoved);
bool failed = !RuntimeCacheHandler.AddClip(clipData);
RuntimeCacheHandler.OnClipRemoved.AddListener(OnRuntimeClipRemoved);
// Handle fail directly
if (failed)
{
OnStreamError(clipData, "Removed from runtime cache due to file size", fromDisk);
OnRuntimeClipRemoved(clipData);
return;
}
}
// Set delegates again since the stream may have changed during setup
if (clipData.clipStream != null)
{
clipData.clipStream.OnStreamUpdated = (cs) => OnStreamUpdated(clipData, cs, fromDisk);
clipData.clipStream.OnStreamComplete = (cs) => OnStreamComplete(clipData, cs, fromDisk);
}
// Set clip stream state
SetClipLoadState(clipData, TTSClipLoadState.Loaded);
VLog.I(GetClipLog($"{(fromDisk ? "Disk" : "Web")} Stream Ready", clipData));
// Invoke playback is ready
clipData.onPlaybackReady?.Invoke(string.Empty);
clipData.onPlaybackReady = null;
// Callback delegate
Events?.Stream?.OnStreamReady?.Invoke(clipData);
}
// Stream clip update
private void OnStreamUpdated(TTSClipData clipData, IAudioClipStream clipStream, bool fromDisk)
{
// Ignore invalid
if (clipStream == null || clipData == null || clipStream != clipData.clipStream)
{
return;
}
// Log & call event
VLog.I(GetClipLog($"{(fromDisk ? "Disk" : "Web")} Stream Updated", clipData));
Events?.Stream?.OnStreamClipUpdate?.Invoke(clipData);
}
// Stream complete
private void OnStreamComplete(TTSClipData clipData, IAudioClipStream clipStream, bool fromDisk)
{
// Ignore invalid
if (clipStream == null || clipData == null || clipStream != clipData.clipStream)
{
return;
}
// Log & call event
VLog.I(GetClipLog($"{(fromDisk ? "Disk" : "Web")} Stream Complete", clipData));
Events?.Stream?.OnStreamComplete?.Invoke(clipData);
// Web request completion
if (!fromDisk)
{
Events?.WebRequest?.OnRequestComplete.Invoke(clipData);
}
}
#endregion
#region UNLOAD
/// <summary>
/// Unload all audio clips from the runtime cache
/// </summary>
public void UnloadAll()
{
// Failed
TTSClipData[] clips = RuntimeCacheHandler?.GetClips();
if (clips == null)
{
return;
}
// Copy array
HashSet<TTSClipData> remaining = new HashSet<TTSClipData>(clips);
// Unload all clips
foreach (var clip in remaining)
{
Unload(clip);
}
}
/// <summary>
/// Force a runtime cache unload
/// </summary>
public void Unload(TTSClipData clipData)
{
if (RuntimeCacheHandler != null)
{
RuntimeCacheHandler.RemoveClip(clipData.clipID);
}
else
{
OnUnloadBegin(clipData);
}
}
/// <summary>
/// Perform clip unload
/// </summary>
/// <param name="clipID"></param>
protected virtual void OnUnloadBegin(TTSClipData clipData)
{
// Abort if currently preparing
if (clipData.loadState == TTSClipLoadState.Preparing)
{
// Cancel web stream
WebHandler?.CancelWebStream(clipData);
// Cancel web download to cache
WebHandler?.CancelWebDownload(clipData, GetDiskCachePath(clipData.textToSpeak, clipData.clipID, clipData.voiceSettings, clipData.diskCacheSettings));
// Cancel disk cache stream
DiskCacheHandler?.CancelDiskCacheStream(clipData);
}
// Unloads clip stream
clipData.clipStream = null;
// Clip is now unloaded
SetClipLoadState(clipData, TTSClipLoadState.Unloaded);
// Unload
VLog.I(GetClipLog($"Unload Clip", clipData));
Events?.OnClipUnloaded?.Invoke(clipData);
}
#endregion
#region RUNTIME CACHE
/// <summary>
/// Obtain a clip from the runtime cache, if applicable
/// </summary>
public TTSClipData GetRuntimeCachedClip(string clipID) => RuntimeCacheHandler?.GetClip(clipID);
/// <summary>
/// Obtain all clips from the runtime cache, if applicable
/// </summary>
public TTSClipData[] GetAllRuntimeCachedClips() => RuntimeCacheHandler?.GetClips();
/// <summary>
/// Called when runtime cache adds a clip
/// </summary>
/// <param name="clipData"></param>
protected virtual void OnRuntimeClipAdded(TTSClipData clipData) => OnLoadBegin(clipData);
/// <summary>
/// Called when runtime cache unloads a clip
/// </summary>
/// <param name="clipData">Clip to be unloaded</param>
protected virtual void OnRuntimeClipRemoved(TTSClipData clipData) => OnUnloadBegin(clipData);
#endregion
#region DISK CACHE
/// <summary>
/// Whether a specific clip should be cached
/// </summary>
/// <param name="clipData">Clip data</param>
/// <returns>True if should be cached</returns>
public bool ShouldCacheToDisk(TTSClipData clipData) =>
DiskCacheHandler != null && DiskCacheHandler.ShouldCacheToDisk(clipData);
/// <summary>
/// Get disk cache
/// </summary>
/// <param name="textToSpeak">Text to be spoken in clip</param>
/// <param name="clipID">Unique clip id</param>
/// <param name="voiceSettings">Custom voice settings</param>
/// <param name="diskCacheSettings">Custom disk cache settings</param>
/// <returns></returns>
public string GetDiskCachePath(string textToSpeak, string clipID, TTSVoiceSettings voiceSettings,
TTSDiskCacheSettings diskCacheSettings) =>
DiskCacheHandler?.GetDiskCachePath(CreateClipData(textToSpeak, clipID, voiceSettings, diskCacheSettings));
// Download options
public TTSClipData DownloadToDiskCache(string textToSpeak,
Action<TTSClipData, string, string> onDownloadComplete = null) =>
DownloadToDiskCache(textToSpeak, null, null, null, onDownloadComplete);
public TTSClipData DownloadToDiskCache(string textToSpeak, string presetVoiceId,
Action<TTSClipData, string, string> onDownloadComplete = null) => DownloadToDiskCache(textToSpeak, null,
GetPresetVoiceSettings(presetVoiceId), null, onDownloadComplete);
public TTSClipData DownloadToDiskCache(string textToSpeak, string presetVoiceId,
TTSDiskCacheSettings diskCacheSettings, Action<TTSClipData, string, string> onDownloadComplete = null) =>
DownloadToDiskCache(textToSpeak, null, GetPresetVoiceSettings(presetVoiceId), diskCacheSettings,
onDownloadComplete);
public TTSClipData DownloadToDiskCache(string textToSpeak, TTSVoiceSettings voiceSettings,
TTSDiskCacheSettings diskCacheSettings, Action<TTSClipData, string, string> onDownloadComplete = null) =>
DownloadToDiskCache(textToSpeak, null, voiceSettings, diskCacheSettings, onDownloadComplete);
/// <summary>
/// Perform a download for a TTS audio clip
/// </summary>
/// <param name="textToSpeak">Text to be spoken in clip</param>
/// <param name="clipID">Unique clip id</param>
/// <param name="voiceSettings">Custom voice settings</param>
/// <param name="diskCacheSettings">Custom disk cache settings</param>
/// <param name="onDownloadComplete">Callback when file has finished downloading</param>
/// <returns>Generated TTS clip data</returns>
public TTSClipData DownloadToDiskCache(string textToSpeak, string clipID, TTSVoiceSettings voiceSettings,
TTSDiskCacheSettings diskCacheSettings, Action<TTSClipData, string, string> onDownloadComplete = null)
{
TTSClipData clipData = CreateClipData(textToSpeak, clipID, voiceSettings, diskCacheSettings);
DownloadToDiskCache(clipData, onDownloadComplete);
return clipData;
}
// Performs download to disk cache
protected virtual void DownloadToDiskCache(TTSClipData clipData, Action<TTSClipData, string, string> onDownloadComplete)
{
// Add delegates if needed
AddDelegates();
// Check if cached to disk & log
string downloadPath = DiskCacheHandler.GetDiskCachePath(clipData);
DiskCacheHandler.CheckCachedToDisk(clipData, (clip, found) =>
{
// Cache checked
VLog.I(GetClipLog($"Disk Cache {(found ? "Found" : "Missing")}\nPath: {downloadPath}", clipData));
// Already downloaded, return successful
if (found)
{
onDownloadComplete?.Invoke(clipData, downloadPath, string.Empty);
return;
}
// Preload selected but not in disk cache, return an error
if (Application.isPlaying && clipData.diskCacheSettings.DiskCacheLocation == TTSDiskCacheLocation.Preload)
{
onDownloadComplete?.Invoke(clipData, downloadPath, WitConstants.ERROR_TTS_CACHE_DOWNLOAD);
return;
}
// Add download completion callback
clipData.onDownloadComplete += (error) => onDownloadComplete?.Invoke(clipData, downloadPath, error);
// Download to cache
WebHandler.RequestDownloadFromWeb(clipData, downloadPath);
});
}
// On web download begin
private void OnWebDownloadBegin(TTSClipData clipData, string downloadPath)
{
VLog.I(GetClipLog($"Download Clip - Begin\nPath: {downloadPath}", clipData));
Events?.Download?.OnDownloadBegin?.Invoke(clipData, downloadPath);
}
// On web download complete
private void OnWebDownloadSuccess(TTSClipData clipData, string downloadPath)
{
// Invoke clip callback & clear
clipData.onDownloadComplete?.Invoke(string.Empty);
clipData.onDownloadComplete = null;
// Log
VLog.I(GetClipLog($"Download Clip - Success\nPath: {downloadPath}", clipData));
Events?.Download?.OnDownloadSuccess?.Invoke(clipData, downloadPath);
}
// On web download complete
private void OnWebDownloadCancel(TTSClipData clipData, string downloadPath)
{
// Invoke clip callback & clear
clipData.onDownloadComplete?.Invoke(WitConstants.CANCEL_ERROR);
clipData.onDownloadComplete = null;
// Log
VLog.I(GetClipLog($"Download Clip - Canceled\nPath: {downloadPath}", clipData));
Events?.Download?.OnDownloadCancel?.Invoke(clipData, downloadPath);
}
// On web download complete
private void OnWebDownloadError(TTSClipData clipData, string downloadPath, string error)
{
// Cancelled
if (error.Equals(WitConstants.CANCEL_ERROR))
{
OnWebDownloadCancel(clipData, downloadPath);
return;
}
// Invoke clip callback & clear
clipData.onDownloadComplete?.Invoke(error);
clipData.onDownloadComplete = null;
// Log
VLog.E(GetClipLog($"Download Clip - Failed\nPath: {downloadPath}\nError: {error}", clipData));
Events?.Download?.OnDownloadError?.Invoke(clipData, downloadPath, error);
}
#endregion
#region VOICES
/// <summary>
/// Return all preset voice settings
/// </summary>
/// <returns></returns>
public TTSVoiceSettings[] GetAllPresetVoiceSettings() => VoiceProvider?.PresetVoiceSettings;
/// <summary>
/// Return preset voice settings for a specific id
/// </summary>
/// <param name="presetVoiceId"></param>
/// <returns></returns>
public TTSVoiceSettings GetPresetVoiceSettings(string presetVoiceId)
{
if (VoiceProvider == null || VoiceProvider.PresetVoiceSettings == null)
{
return null;
}
return Array.Find(VoiceProvider.PresetVoiceSettings, (v) => string.Equals(v.SettingsId, presetVoiceId, StringComparison.CurrentCultureIgnoreCase));
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f18e4991da1755f4cbb87c883f205186
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 211556d9cf1bb5d4dadd0c632ab9457f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b15403450229c3a4b8455a61d6143a6d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,195 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System.Collections.Generic;
using UnityEngine;
using Meta.WitAi.TTS.Data;
namespace Meta.WitAi.TTS.Utilities
{
public interface ITTSPhraseProvider
{
/// <summary>
/// The supported voice ids
/// </summary>
List<string> GetVoiceIds();
/// <summary>
/// Get specific phrases per voice
/// </summary>
List<string> GetVoicePhrases(string voiceId);
}
[RequireComponent(typeof(TTSSpeaker))]
public class TTSSpeakerAutoLoader : MonoBehaviour, ITTSPhraseProvider
{
/// <summary>
/// TTSSpeaker to be used
/// </summary>
public TTSSpeaker Speaker;
/// <summary>
/// Text file with phrases separated by line
/// </summary>
public TextAsset PhraseFile;
/// <summary>
/// All phrases to be loaded
/// </summary>
public string[] Phrases => _phrases;
[SerializeField] private string[] _phrases;
/// <summary>
/// Whether LoadClips has to be called explicitly.
/// If false, it is called on start
/// </summary>
public bool LoadManually = false;
// Generated clips
public TTSClipData[] Clips => _clips;
private TTSClipData[] _clips;
// Done loading
public bool IsLoaded => _clipsLoading == 0;
private int _clipsLoading = 0;
// Load on start if not manual
protected virtual void Start()
{
if (!LoadManually)
{
LoadClips();
}
}
// Load all phrase clips
public virtual void LoadClips()
{
// Done
if (_clips != null)
{
VLog.W("Cannot autoload clips twice.");
return;
}
// Set phrase list
_phrases = GetAllPhrases().ToArray();
// Load all clips
List<TTSClipData> list = new List<TTSClipData>();
foreach (var phrase in _phrases)
{
_clipsLoading++;
TTSClipData clip = TTSService.Instance.Load(phrase, Speaker.presetVoiceID, null, OnClipReady);
list.Add(clip);
}
_clips = list.ToArray();
}
// Return all phrases
public virtual List<string> GetAllPhrases()
{
// Ensure speaker exists
SetupSpeaker();
// Get all phrases unformatted
List<string> unformattedPhrases = new List<string>();
// Add phrases split from phrase file
AddUniquePhrases(unformattedPhrases, PhraseFile?.text.Split('\n'));
// Add phrases serialized in phrase array
AddUniquePhrases(unformattedPhrases, Phrases);
// Iterate old phrases
List<string> phrases = new List<string>();
for (int i = 0; i < unformattedPhrases.Count; i++)
{
// Format phrases
List<string> newPhrases = Speaker.GetFinalText(unformattedPhrases[i]);
// Add to final list
if (newPhrases != null && newPhrases.Count > 0)
{
phrases.AddRange(newPhrases);
}
}
// Return array
return phrases;
}
// Add unique, non-null phrases
private void AddUniquePhrases(List<string> list, string[] newPhrases)
{
if (newPhrases != null)
{
foreach (var phrase in newPhrases)
{
if (!string.IsNullOrEmpty(phrase) && !list.Contains(phrase))
{
list.Add(phrase);
}
}
}
}
// Setup speaker
protected virtual void SetupSpeaker()
{
if (!Speaker)
{
Speaker = gameObject.GetComponent<TTSSpeaker>();
if (!Speaker)
{
Speaker = gameObject.AddComponent<TTSSpeaker>();
}
}
}
// Clip ready callback
protected virtual void OnClipReady(TTSClipData clipData, string error)
{
_clipsLoading--;
}
// Unload phrases
protected virtual void OnDestroy()
{
UnloadClips();
}
// Unload all clips
protected virtual void UnloadClips()
{
if (_clips == null)
{
return;
}
foreach (var clip in _clips)
{
TTSService.Instance?.Unload(clip);
}
_clips = null;
_phrases = null;
}
#region ITTSVoicePhraseProvider
/// <summary>
/// Returns the supported voice ids (Only this speaker)
/// </summary>
public virtual List<string> GetVoiceIds()
{
SetupSpeaker();
string voiceId = Speaker?.VoiceSettings.SettingsId;
if (string.IsNullOrEmpty(voiceId))
{
return null;
}
List<string> results = new List<string>();
results.Add(voiceId);
return results;
}
/// <summary>
/// Returns the supported phrases per voice
/// </summary>
public virtual List<string> GetVoicePhrases(string voiceId)
{
return GetAllPhrases();
}
#endregion ITTSVoicePhraseProvider
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 25d484b58054b064db727b9fe66aed60
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,135 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System.Collections.Generic;
using Meta.WitAi.TTS.Interfaces;
using System.Text;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.Serialization;
namespace Meta.WitAi.TTS.Utilities
{
public class TTSSpeechSplitter : MonoBehaviour, ISpeakerTextPreprocessor
{
[Tooltip("If text-to-speech phrase is greater than this length, it will be split.")]
[Range(10, 250)] [FormerlySerializedAs("maxTextLength")]
public int MaxTextLength = 250;
// Regex for cleaning out SAML
private Regex _cleaner = new Regex(@"\s\s+|</?s>|</?p>", RegexOptions.Compiled | RegexOptions.Multiline);
// Regex for splitting
private Regex _sentenceSplitter = new Regex(@"(?<=[.?,;!]\s+|<p>|<s>)", RegexOptions.Compiled);
private Regex _wordSplitter = new Regex(@"(?=\s+)", RegexOptions.Compiled);
/// <summary>
/// Split each phrase larger than min text length into multiple phrases
/// </summary>
/// <param name="speaker">The speaker that will be used to speak the resulting text</param>
/// <param name="phrases">The current phrase list that will be used for speech. Can be added to or removed as needed.</param>
public void OnPreprocessTTS(TTSSpeaker speaker, List<string> phrases)
{
// To be used
StringBuilder message = new StringBuilder();
// Split if possible
int index = 0;
while (index < phrases.Count)
{
// Cleanup phrase
var text = _cleaner.Replace(phrases[index], " ");
// If under/equal to max add cleaned phrase directly
if (text.Length <= MaxTextLength)
{
phrases[index] = text;
index++;
continue;
}
// Remove previous phrase from list
phrases.RemoveAt(index);
// Split text into sentences & iterate
var sentences = _sentenceSplitter.Split(text);
for (int s = 0; s < sentences.Length; s++)
{
// Ignore if empty
var sentence = sentences[s];
if (sentence.Length == 0)
{
continue;
}
// If building message would be too long, finalize previous message
if (message.Length > 0 && message.Length + sentence.Length > MaxTextLength)
{
phrases.Insert(index, message.ToString().Trim());
message.Clear();
index++;
}
// If sentence fits, append to message
if (sentence.Length <= MaxTextLength)
{
message.Append(sentence);
continue;
}
// Sentence is longer than max length, split further
var words = _wordSplitter.Split(sentence);
for (int w = 0; w < words.Length; w++)
{
// Ignore if empty
string word = words[w];
if (word.Length == 0)
{
continue;
}
// If building message would be too long, finalize previous message
if (message.Length > 0 && message.Length + word.Length > MaxTextLength)
{
phrases.Insert(index, message.ToString().Trim());
message.Clear();
index++;
}
// Trim start for new message
if (message.Length == 0)
{
word = word.TrimStart();
}
// If word fits, append to message
if (word.Length <= MaxTextLength)
{
message.Append(word);
continue;
}
// Word is longer than max length: truncate, warn & add truncated word to tts
message.Append(word.Substring(0, MaxTextLength));
VLog.W($"Word is longer than MaxTextLength & will be truncated\nWord: {word}\nTruncated: {message}\nFrom Length: {word.Length}\nTo Length: {MaxTextLength}");
phrases.Insert(index, message.ToString());
message.Clear();
index++;
}
}
// Add remaining message
if (message.Length > 0)
{
phrases.Insert(index, message.ToString().Trim());
message.Clear();
index++;
}
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d2accdd3f9f0a354b84b364eae500512
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: