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: e18f1785366987e4886fb11f30d70040
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: cde9306012742314db5ada2fe0d9bff2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

@@ -0,0 +1,128 @@
fileFormatVersion: 2
guid: 701ca4114422ead418e08b1c0bc26348
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 11
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
applyGammaDecoding: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

@@ -0,0 +1,128 @@
fileFormatVersion: 2
guid: a3ec408155652024f94168a419eabdcc
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 11
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
applyGammaDecoding: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

@@ -0,0 +1,128 @@
fileFormatVersion: 2
guid: 3387a752287aa7845892fd1c1ca0eb8f
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 11
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
applyGammaDecoding: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

@@ -0,0 +1,128 @@
fileFormatVersion: 2
guid: 45b126b5c88878b42840b8dd157251b6
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 11
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
applyGammaDecoding: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,40 @@
# Text-to-Speech Overview
## Overview
Voice SDKs Text-to-Speech (TTS) feature uses a Wit.ai based service to provide audio files for text strings. Its handled by a single TTSService prefab for all TTS settings and you can use a simple TTSSpeaker script for each scene location in which a TTS clip can be played.
To keep TTS working smoothly, Voice SDK handles the caching of TTS files during runtime (or when otherwise needed). If streaming TTS audio files is not an option for your application, however, you can set it to use preloaded static TTS files prior to building your app.
<b>Note:</b> With this initial release of TTS, only a few Wit.ai TTS voice settings are adjustable, but the package will be updated as more voices become available.
## Setup
Use the following steps to set up TTS for your app once the plugin has been imported:
1. Open the scene you want to use TTS within.
2. Generate a new Wit Configuration using the Wit Configurations menu within the Oculus > Voice SDK > Voice HUB. Ensure it refreshes successfully and displays all voices available for your configuration.
2. Navigate to Assets > Create > Voice SDK > TTS > Add Default TTS Setup
3. In your scene heirarchy navigate inside the newly generated TTS GameObject to select the TTSWitService gameobject and adjust the inspector to fit your needs:
3a. Use TTSWit > Request Settings > Configuration to select the Wit Configuration asset generated in step 2.
3b. Once your configuration is setup, go to 'Preset Voice Settings' and setup any voices that might be shared by multiple TTSSpeakers.
For more information, see TTS Voice Customization: https://developer.oculus.com/documentation/unity/voice-sdk-tts-voice-customization
![image](Images/tts_service_settings.png)
3c. Under TTS Runtime Cache (Script), adjust the settings to indicate how often clips will be automatically uploaded from memory.
For more information, see TTS Cache Options: https://developer.oculus.com/documentation/unity/voice-sdk-tts-cache-options
3d. If needed, adjust your disk cache directory location and name in the tree under TTS Disk Cache (Script).
For more information, see TTS Cache Options: https://developer.oculus.com/documentation/unity/voice-sdk-tts-cache-options/
![image](Images/tts_service_cachesettings.png)
4. Move & duplicate the TTSSpeaker to all the locations in your app where you would like TTS to be played.
5. Modify each TTSSpeaker via the Inspector to fit your needs:
5a. Under Voice Settings, select the Voice Preset for the specific speaker or select Custom to apply speaker specific settings.
5b. Adjust the AudioSource in order to add the TTSSpeaker to a custom audio group or set audio from 2D to 3D.
6. Via a script use the following TTSSpeaker methods to load and play text.
6a. Use the TTSSpeaker scripts Speak(textToSpeak : string) method to request and say specified text on load.
6b. Use the TTSSpeaker scripts SpeakQueued(textToSpeak : string) method to request and say specified text.
6c. Send a custom TTSSpeakerClipEvents into any Speak/SpeakQueued method for request specific text load & playback event callbacks.
6d. Use the TTSSpeaker scripts Stop() method to immediately stop all loading & playing TTS clips.
6e. Use TTSSpeaker's Stop(textToSpeak : string) to immediately stop loading & playing of a specific text string.
<b>Note:</b> Check out TTS/Samples/Scenes/TTSSample.unity for an example of TTS implementation. Ensure that you set your WitConfiguration to the sample prior to running.
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 0a78e70d61637eb498ee552bab9479ab
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,19 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 98bfdca232b84c3db2527e4b5c9d3b9d, type: 3}
m_Name: TTS Overview Readme
m_EditorClassIdentifier:
_displayName: Overview
_prefix: Documentation/TTS/
_context: {fileID: 11400000, guid: f2e797de12b196c40ad409b92d6394fc, type: 2}
_markdownFile: {fileID: 4900000, guid: 0a78e70d61637eb498ee552bab9479ab, type: 3}
_priority: 200
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c745949bbb55f7b4dba6eeac61f4a7bd
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d5d4d513ffe662a4db92f38c8468357e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 68d2ecf49ba62e0408b8f86acb9b103c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 748ff8d1d0d4814429b86058a56a3ee4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5e581f7e133ba2048995048327a1ef85
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 466ea4f9ad1762f41a5effc0e1985b21
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -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: