init
This commit is contained in:
8
Assets/Oculus/Voice/Lib/Wit.ai/Features/TTS/Samples.meta
Normal file
8
Assets/Oculus/Voice/Lib/Wit.ai/Features/TTS/Samples.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5d4d513ffe662a4db92f38c8468357e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Meta.WitAi.TTS.Samples",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"GUID:8bbcefc153e1f1b4a98680670797dd16",
|
||||
"GUID:1c28d8b71ced07540b7c271537363cc6"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e67ffd8913b227943835c952869d26fb
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 68d2ecf49ba62e0408b8f86acb9b103c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,269 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &3550832292187419422
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 3550832292187419420}
|
||||
- component: {fileID: 3550832292187419423}
|
||||
- component: {fileID: 1295848615748463565}
|
||||
m_Layer: 0
|
||||
m_Name: TTSSpeaker
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &3550832292187419420
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3550832292187419422}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_Children:
|
||||
- {fileID: 3550832292895390900}
|
||||
m_Father: {fileID: 0}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &3550832292187419423
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3550832292187419422}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: b15403450229c3a4b8455a61d6143a6d, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
presetVoiceID: CHARLIE
|
||||
AudioSource: {fileID: 3550832292895390901}
|
||||
_cloneAudioSource: 0
|
||||
prependedText:
|
||||
appendedText:
|
||||
_events:
|
||||
OnTextPlaybackStart:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnTextPlaybackCancelled:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnTextPlaybackFinished:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnAudioClipPlaybackReady:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnAudioClipPlaybackStart:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnAudioClipPlaybackCancelled:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnAudioClipPlaybackFinished:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnStartSpeaking:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnFinishedSpeaking:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnCancelledSpeaking:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnClipLoadBegin:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnClipLoadFailed:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnClipLoadSuccess:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnClipLoadAbort:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnClipDataQueued:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnClipDataLoadBegin:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnClipDataLoadFailed:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnClipDataLoadSuccess:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnClipDataLoadAbort:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnClipDataPlaybackReady:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnClipDataPlaybackStart:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnClipDataPlaybackFinished:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnClipDataPlaybackCancelled:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnPlaybackQueueBegin:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnPlaybackQueueComplete:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
_ttsService: {fileID: 0}
|
||||
--- !u!114 &1295848615748463565
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3550832292187419422}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 81f17bb00ee9f68428d962165826f2fd, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_maxTextLength: 250
|
||||
--- !u!1 &3550832292895390903
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 3550832292895390900}
|
||||
- component: {fileID: 3550832292895390901}
|
||||
m_Layer: 0
|
||||
m_Name: Voice
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &3550832292895390900
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3550832292895390903}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 2, y: 1.665, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_Children: []
|
||||
m_Father: {fileID: 3550832292187419420}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!82 &3550832292895390901
|
||||
AudioSource:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3550832292895390903}
|
||||
m_Enabled: 1
|
||||
serializedVersion: 4
|
||||
OutputAudioMixerGroup: {fileID: 0}
|
||||
m_audioClip: {fileID: 0}
|
||||
m_PlayOnAwake: 0
|
||||
m_Volume: 1
|
||||
m_Pitch: 1
|
||||
Loop: 0
|
||||
Mute: 0
|
||||
Spatialize: 0
|
||||
SpatializePostEffects: 0
|
||||
Priority: 128
|
||||
DopplerLevel: 1
|
||||
MinDistance: 1
|
||||
MaxDistance: 500
|
||||
Pan2D: 0
|
||||
rolloffMode: 0
|
||||
BypassEffects: 0
|
||||
BypassListenerEffects: 0
|
||||
BypassReverbZones: 0
|
||||
rolloffCustomCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
panLevelCustomCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
spreadCustomCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
reverbZoneMixCustomCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0.33333334
|
||||
outWeight: 0.33333334
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 45fe3f4c7b67f4d4c83f7c1d3c8f1a50
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e264fcb4f1c2dc248bacd7a1f80da833
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,175 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &4852061571279439017
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 4852061571279439020}
|
||||
- component: {fileID: 4852061571279439023}
|
||||
- component: {fileID: 3627599131962939471}
|
||||
- component: {fileID: 4852061571279439022}
|
||||
m_Layer: 0
|
||||
m_Name: TTSWit
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &4852061571279439020
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4852061571279439017}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &4852061571279439023
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4852061571279439017}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a6b3124b830442d45b9f357ff99b152f, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_events:
|
||||
OnClipCreated:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnClipUnloaded:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
Stream:
|
||||
OnStreamBegin:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnStreamReady:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnStreamClipUpdate:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnStreamComplete:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnStreamCancel:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnStreamError:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
Download:
|
||||
OnDownloadBegin:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnDownloadSuccess:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnDownloadCancel:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnDownloadError:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
RequestSettings:
|
||||
configuration: {fileID: 11400000, guid: 8ca721df475c92e4ca9167b74de4b2f6, type: 2}
|
||||
audioType: 0
|
||||
audioStream: 1
|
||||
audioStreamReadyDuration: 0.1
|
||||
audioStreamChunkLength: 5
|
||||
audioStreamPreloadCount: 3
|
||||
_presetVoiceSettings:
|
||||
- settingsID: CHARLIE
|
||||
voice: Charlie
|
||||
style: default
|
||||
speed: 100
|
||||
pitch: 100
|
||||
gain: 50
|
||||
- settingsID: REBECCA
|
||||
voice: Rebecca
|
||||
style: default
|
||||
speed: 100
|
||||
pitch: 100
|
||||
gain: 50
|
||||
- settingsID: COOPER
|
||||
voice: Cooper
|
||||
style: default
|
||||
speed: 100
|
||||
pitch: 100
|
||||
gain: 50
|
||||
- settingsID: VAMPIRE
|
||||
voice: Vampire
|
||||
style: default
|
||||
speed: 100
|
||||
pitch: 100
|
||||
gain: 50
|
||||
- settingsID: PROSPECTOR
|
||||
voice: Prospector
|
||||
style: default
|
||||
speed: 100
|
||||
pitch: 100
|
||||
gain: 50
|
||||
--- !u!114 &3627599131962939471
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4852061571279439017}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 1d60dcb6d02034b4b96284db469db5e3, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
ClipLimit: 1
|
||||
ClipCapacity: 100
|
||||
RamLimit: 0
|
||||
RamCapacity: 20000
|
||||
--- !u!114 &4852061571279439022
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4852061571279439017}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: b0ffdd015bcb8ea41bb96f19a723bf7d, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_diskPath: TTS/
|
||||
_defaultSettings:
|
||||
DiskCacheLocation: 0
|
||||
_events:
|
||||
OnStreamBegin:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnStreamReady:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnStreamClipUpdate:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnStreamComplete:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnStreamCancel:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
OnStreamError:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a89561c2ba096ad4dbf37bbb423d6f3c
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 748ff8d1d0d4814429b86058a56a3ee4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 79647fe399cbd69458a2728d01b2f739
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e581f7e133ba2048995048327a1ef85
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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.Integrations;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Meta.WitAi.TTS.Samples
|
||||
{
|
||||
public class TTSCacheToggle : MonoBehaviour
|
||||
{
|
||||
// UI references
|
||||
[SerializeField] private TTSDiskCache _diskCache;
|
||||
[SerializeField] private Text _cacheLabel;
|
||||
[SerializeField] private Button _button;
|
||||
|
||||
// Current disk cache location
|
||||
private TTSDiskCacheLocation _cacheLocation = (TTSDiskCacheLocation) (-1);
|
||||
|
||||
// Add listeners
|
||||
private void OnEnable()
|
||||
{
|
||||
// Obtain disk cache if possible
|
||||
if (_diskCache == null)
|
||||
{
|
||||
_diskCache = GameObject.FindObjectOfType<TTSDiskCache>();
|
||||
}
|
||||
// Reset location text
|
||||
RefreshLocation();
|
||||
_button.onClick.AddListener(ToggleCache);
|
||||
}
|
||||
// Current disk cache location
|
||||
private TTSDiskCacheLocation GetCurrentCacheLocation() => _diskCache == null ? TTSDiskCacheLocation.Stream : _diskCache.DiskCacheDefaultSettings.DiskCacheLocation;
|
||||
// Check for changes
|
||||
private void Update()
|
||||
{
|
||||
if (_cacheLocation != GetCurrentCacheLocation())
|
||||
{
|
||||
RefreshLocation();
|
||||
}
|
||||
}
|
||||
// Refresh location & button text
|
||||
private void RefreshLocation()
|
||||
{
|
||||
_cacheLocation = GetCurrentCacheLocation();
|
||||
_cacheLabel.text = $"Disk Cache: {_cacheLocation}";
|
||||
}
|
||||
// Remove listeners
|
||||
private void OnDisable()
|
||||
{
|
||||
_button.onClick.RemoveListener(ToggleCache);
|
||||
}
|
||||
// Toggle cache
|
||||
public void ToggleCache()
|
||||
{
|
||||
// Toggle to next option
|
||||
TTSDiskCacheLocation cacheLocation = GetCurrentCacheLocation();
|
||||
switch (cacheLocation)
|
||||
{
|
||||
case TTSDiskCacheLocation.Stream:
|
||||
cacheLocation = TTSDiskCacheLocation.Temporary;
|
||||
break;
|
||||
case TTSDiskCacheLocation.Temporary:
|
||||
cacheLocation = TTSDiskCacheLocation.Persistent;
|
||||
break;
|
||||
case TTSDiskCacheLocation.Persistent:
|
||||
cacheLocation = TTSDiskCacheLocation.Preload;
|
||||
break;
|
||||
default:
|
||||
cacheLocation = TTSDiskCacheLocation.Stream;
|
||||
break;
|
||||
}
|
||||
|
||||
// Set next option
|
||||
_diskCache.DiskCacheDefaultSettings.DiskCacheLocation = cacheLocation;
|
||||
// Clear runtime cache
|
||||
TTSService.Instance.UnloadAll();
|
||||
|
||||
// Refresh location
|
||||
RefreshLocation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e7aad95c862d94f4f912114f5fd46959
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.UI;
|
||||
|
||||
namespace Meta.WitAi.TTS.Samples
|
||||
{
|
||||
public class TTSErrorText : MonoBehaviour
|
||||
{
|
||||
// Label
|
||||
[SerializeField] private Text _errorLabel;
|
||||
// Current error response
|
||||
private string _error = string.Empty;
|
||||
|
||||
// Add listeners
|
||||
private void Update()
|
||||
{
|
||||
if (TTSService.Instance != null)
|
||||
{
|
||||
string invalidError = TTSService.Instance.GetInvalidError();
|
||||
if (!string.Equals(invalidError, _error))
|
||||
{
|
||||
_error = invalidError;
|
||||
if (string.IsNullOrEmpty(_error))
|
||||
{
|
||||
_errorLabel.text = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
_errorLabel.text = $"TTS Service Error: {_error}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1f9580bff99ab114d85ccce9a75b067b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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;
|
||||
using UnityEngine.UI;
|
||||
using Meta.WitAi.TTS.Utilities;
|
||||
|
||||
namespace Meta.WitAi.TTS.Samples
|
||||
{
|
||||
public class TTSSpeakerInput : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Text _title;
|
||||
[SerializeField] private InputField _input;
|
||||
[SerializeField] private TTSSpeaker _speaker;
|
||||
[SerializeField] private Button _stopButton;
|
||||
[SerializeField] private Button _speakButton;
|
||||
[SerializeField] private Button _speakQueuedButton;
|
||||
|
||||
[SerializeField] private string _dateId = "[DATE]";
|
||||
[SerializeField] private string[] _queuedText;
|
||||
|
||||
// States
|
||||
private bool _loading;
|
||||
private bool _speaking;
|
||||
|
||||
// Add delegates
|
||||
private void OnEnable()
|
||||
{
|
||||
RefreshButtons();
|
||||
_stopButton.onClick.AddListener(StopClick);
|
||||
_speakButton.onClick.AddListener(SpeakClick);
|
||||
_speakQueuedButton.onClick.AddListener(SpeakQueuedClick);
|
||||
}
|
||||
// Stop click
|
||||
private void StopClick() => _speaker.Stop();
|
||||
// Speak phrase click
|
||||
private void SpeakClick() => _speaker.Speak(FormatText(_input.text));
|
||||
// Speak queued phrase click
|
||||
private void SpeakQueuedClick()
|
||||
{
|
||||
foreach (var text in _queuedText)
|
||||
{
|
||||
_speaker.SpeakQueued(FormatText(text));
|
||||
}
|
||||
_speaker.SpeakQueued(FormatText(_input.text));
|
||||
}
|
||||
// Format text with current datetime
|
||||
private string FormatText(string text)
|
||||
{
|
||||
string result = text;
|
||||
if (result.Contains(_dateId))
|
||||
{
|
||||
DateTime now = DateTime.Now;
|
||||
string dateString = $"{now.ToLongDateString()} at {now.ToShortTimeString()}";
|
||||
result = text.Replace(_dateId, dateString);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// Remove delegates
|
||||
private void OnDisable()
|
||||
{
|
||||
_stopButton.onClick.RemoveListener(StopClick);
|
||||
_speakButton.onClick.RemoveListener(SpeakClick);
|
||||
_speakQueuedButton.onClick.RemoveListener(SpeakQueuedClick);
|
||||
}
|
||||
|
||||
// Preset text fields
|
||||
private void Update()
|
||||
{
|
||||
// On preset voice id update
|
||||
if (!string.Equals(_title.text, _speaker.presetVoiceID))
|
||||
{
|
||||
_title.text = _speaker.presetVoiceID;
|
||||
_input.placeholder.GetComponent<Text>().text = $"Write something to say in {_speaker.presetVoiceID}'s voice";
|
||||
}
|
||||
// On state changes
|
||||
if (_loading != _speaker.IsLoading)
|
||||
{
|
||||
_loading = _speaker.IsLoading;
|
||||
RefreshButtons();
|
||||
}
|
||||
if (_speaking != _speaker.IsSpeaking)
|
||||
{
|
||||
_speaking = _speaker.IsSpeaking;
|
||||
RefreshButtons();
|
||||
}
|
||||
}
|
||||
// Refresh interactable based on states
|
||||
private void RefreshButtons()
|
||||
{
|
||||
_stopButton.interactable = _loading || _speaking;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a57b9d9fb2760434e859220f24690a1e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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.Text;
|
||||
using Meta.WitAi.TTS.Data;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using Meta.WitAi.TTS.Utilities;
|
||||
|
||||
namespace Meta.WitAi.TTS.Samples
|
||||
{
|
||||
public class TTSStatusLabel : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private TTSSpeaker _speaker;
|
||||
[SerializeField] private Text _label;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
RefreshLabel();
|
||||
_speaker.Events.OnClipDataLoadBegin.AddListener(OnClipRefresh);
|
||||
_speaker.Events.OnClipDataLoadAbort.AddListener(OnClipRefresh);
|
||||
_speaker.Events.OnClipDataLoadFailed.AddListener(OnClipRefresh);
|
||||
_speaker.Events.OnClipDataLoadSuccess.AddListener(OnClipRefresh);
|
||||
_speaker.Events.OnClipDataQueued.AddListener(OnClipRefresh);
|
||||
_speaker.Events.OnClipDataPlaybackReady.AddListener(OnClipRefresh);
|
||||
_speaker.Events.OnClipDataPlaybackStart.AddListener(OnClipRefresh);
|
||||
_speaker.Events.OnClipDataPlaybackFinished.AddListener(OnClipRefresh);
|
||||
_speaker.Events.OnClipDataPlaybackCancelled.AddListener(OnClipRefresh);
|
||||
}
|
||||
private void OnClipRefresh(TTSClipData clipData)
|
||||
{
|
||||
RefreshLabel();
|
||||
}
|
||||
private void OnDisable()
|
||||
{
|
||||
_speaker.Events.OnClipDataQueued.RemoveListener(OnClipRefresh);
|
||||
_speaker.Events.OnClipDataLoadBegin.RemoveListener(OnClipRefresh);
|
||||
_speaker.Events.OnClipDataLoadAbort.RemoveListener(OnClipRefresh);
|
||||
_speaker.Events.OnClipDataLoadFailed.RemoveListener(OnClipRefresh);
|
||||
_speaker.Events.OnClipDataLoadSuccess.RemoveListener(OnClipRefresh);
|
||||
_speaker.Events.OnClipDataPlaybackReady.RemoveListener(OnClipRefresh);
|
||||
_speaker.Events.OnClipDataPlaybackStart.RemoveListener(OnClipRefresh);
|
||||
_speaker.Events.OnClipDataPlaybackFinished.RemoveListener(OnClipRefresh);
|
||||
_speaker.Events.OnClipDataPlaybackCancelled.RemoveListener(OnClipRefresh);
|
||||
}
|
||||
|
||||
private void RefreshLabel()
|
||||
{
|
||||
StringBuilder status = new StringBuilder();
|
||||
if (_speaker.SpeakingClip != null)
|
||||
{
|
||||
status.AppendLine($"Speaking: {_speaker.IsSpeaking}");
|
||||
}
|
||||
int index = 0;
|
||||
foreach (var clip in _speaker.QueuedClips)
|
||||
{
|
||||
status.Insert(0, $"Queue[{index}]: {clip.loadState.ToString()}\n");
|
||||
index++;
|
||||
}
|
||||
if (status.Length > 0)
|
||||
{
|
||||
status.Remove(status.Length - 1, 1);
|
||||
}
|
||||
_label.text = status.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 443d56f5ec5b41c4aa647a2350247db1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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.UI;
|
||||
using Meta.WitAi.TTS.Integrations;
|
||||
|
||||
namespace Meta.WitAi.TTS.Samples
|
||||
{
|
||||
public class TTSStreamToggle : MonoBehaviour
|
||||
{
|
||||
// UI references
|
||||
[SerializeField] private TTSWit _service;
|
||||
[SerializeField] private Text _label;
|
||||
[SerializeField] private Button _button;
|
||||
|
||||
// Current stream
|
||||
private bool _streamEnabled = true;
|
||||
|
||||
// Add listeners
|
||||
private void OnEnable()
|
||||
{
|
||||
// Obtain disk cache if possible
|
||||
if (_service == null)
|
||||
{
|
||||
_service = GameObject.FindObjectOfType<TTSWit>();
|
||||
}
|
||||
// Log for missing service
|
||||
if (_service == null)
|
||||
{
|
||||
VLog.E("TTS Stream Toggle - Cannot work without a TTSWit reference");
|
||||
}
|
||||
// Reset
|
||||
RefreshStreaming();
|
||||
_button.onClick.AddListener(ToggleStreaming);
|
||||
}
|
||||
// Remove listeners
|
||||
private void OnDisable()
|
||||
{
|
||||
_button.onClick.RemoveListener(ToggleStreaming);
|
||||
}
|
||||
|
||||
// Refresh location & button text
|
||||
private void RefreshStreaming()
|
||||
{
|
||||
_streamEnabled = GetStreaming();
|
||||
_label.text = $"Streaming: {(_streamEnabled ? "ON" : "OFF")}";
|
||||
}
|
||||
// Toggle streaming
|
||||
public void ToggleStreaming()
|
||||
{
|
||||
SetStreaming(!_streamEnabled);
|
||||
RefreshStreaming();
|
||||
}
|
||||
|
||||
// Get streaming option from service
|
||||
private bool GetStreaming()
|
||||
{
|
||||
return _service && _service.RequestSettings.audioStream;
|
||||
}
|
||||
// Set streaming option to
|
||||
private void SetStreaming(bool toStreaming)
|
||||
{
|
||||
if (_service != null)
|
||||
{
|
||||
_service.RequestSettings.audioStream = toStreaming;
|
||||
}
|
||||
}
|
||||
// Update if changed externally
|
||||
private void Update()
|
||||
{
|
||||
if (_streamEnabled != GetStreaming())
|
||||
{
|
||||
RefreshStreaming();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 62fa707a35782fb4dafd0da34ea548a1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,11 @@
|
||||
Welcome to Voice SDK Text-To-Speech!
|
||||
The following are phrases to be auto-loaded.
|
||||
Hi, how are you?
|
||||
I am great! Thank you for asking!
|
||||
Would you mind repeating that?
|
||||
I actually cannot hear you right now.
|
||||
Hmmm
|
||||
Well that is interesting!
|
||||
My favorite color is green.
|
||||
My favorite color is purple.
|
||||
Sentence one begins with a few words and then ends with an ellipsis... Does sentence two start with two spaces and end with a question mark? Sentence three also starts with two spaces, has a comma; has a semicolon and ends with two exclamation points!! A sentence with a semicolon; may be used as well.
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c94bdcb6d297dfe41a49608ed6309ab9
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Oculus/Voice/Lib/Wit.ai/Features/TTS/Scripts.meta
Normal file
8
Assets/Oculus/Voice/Lib/Wit.ai/Features/TTS/Scripts.meta
Normal file
@@ -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.Editor.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,483 @@
|
||||
/*
|
||||
* 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.Data.Configuration;
|
||||
using Meta.WitAi.TTS.Data;
|
||||
using Meta.WitAi.TTS.Editor.Preload;
|
||||
using Meta.WitAi.Utilities;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Meta.WitAi.TTS.Editor
|
||||
{
|
||||
[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,633 @@
|
||||
/*
|
||||
* 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;
|
||||
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.Editor.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
|
||||
string[] voiceIds = phraseProvider.GetVoiceIds();
|
||||
if (voiceIds == null || voiceIds.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Iterate voice ids
|
||||
foreach (var voiceId in voiceIds)
|
||||
{
|
||||
// Ignore empty voice id
|
||||
if (string.IsNullOrEmpty(voiceId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore if phrases are null
|
||||
string[] phrases = phraseProvider.GetVoicePhrases(voiceId);
|
||||
if (phrases == null || phrases.Length == 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,262 @@
|
||||
/*
|
||||
* 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.Data.Configuration;
|
||||
using Meta.WitAi.TTS.Data;
|
||||
using Meta.WitAi.TTS.Editor.Voices;
|
||||
using Meta.WitAi.TTS.Integrations;
|
||||
using Meta.WitAi.TTS.Utilities;
|
||||
using Meta.WitAi;
|
||||
using Meta.WitAi.Data.Info;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Meta.WitAi.TTS.Editor
|
||||
{
|
||||
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
|
||||
private static void RefreshAvailableVoices(TTSWit ttsWit)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Get application info
|
||||
WitAppInfo appInfo = configuration.GetApplicationInfo();
|
||||
if (appInfo.voices == null || appInfo.voices.Length == 0)
|
||||
{
|
||||
VLog.W($"TTS Service - No voices found");
|
||||
if (ttsWit.PresetVoiceSettings == null || ttsWit.PresetVoiceSettings.Length == 0)
|
||||
{
|
||||
WitVoiceInfo voiceInfo = new WitVoiceInfo()
|
||||
{
|
||||
name = TTSWitVoiceSettings.DEFAULT_VOICE,
|
||||
};
|
||||
TTSWitVoiceSettings placeholder = GetDefaultVoiceSetting(voiceInfo);
|
||||
ttsWit.SetVoiceSettings(new TTSWitVoiceSettings[] { placeholder });
|
||||
}
|
||||
}
|
||||
// Reset list of voices
|
||||
else
|
||||
{
|
||||
WitVoiceInfo[] voices = appInfo.voices;
|
||||
TTSWitVoiceSettings[] newSettings = new TTSWitVoiceSettings[voices.Length];
|
||||
for (int i = 0; i < voices.Length; i++)
|
||||
{
|
||||
newSettings[i] = GetDefaultVoiceSetting(voices[i]);
|
||||
}
|
||||
ttsWit.SetVoiceSettings(newSettings);
|
||||
VLog.D($"TTS Service - Successfully applied {voices.Length} voices to {ttsWit.gameObject.name}");
|
||||
}
|
||||
|
||||
// Refresh
|
||||
RefreshEmptySpeakers(ttsWit);
|
||||
}
|
||||
|
||||
// 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) || string.Equals(speaker.presetVoiceID, TTSVoiceSettings.DEFAULT_ID))
|
||||
{
|
||||
speaker.presetVoiceID = defaultVoiceID;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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
|
||||
speaker.AudioSource = audio;
|
||||
|
||||
// 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,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.TTS.Data;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Meta.WitAi.TTS.Editor
|
||||
{
|
||||
[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 override void OnInspectorGUI()
|
||||
{
|
||||
// Display default ui
|
||||
base.OnInspectorGUI();
|
||||
|
||||
// Get service
|
||||
if (_service == null)
|
||||
{
|
||||
_service = target as TTSService;
|
||||
}
|
||||
|
||||
// Ignore if in editor
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 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,171 @@
|
||||
/*
|
||||
* 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 UnityEditor;
|
||||
using UnityEngine;
|
||||
using Meta.WitAi.TTS.Utilities;
|
||||
using Meta.WitAi.TTS.Data;
|
||||
|
||||
namespace Meta.WitAi.TTS.Editor
|
||||
{
|
||||
[CustomEditor(typeof(TTSSpeaker), true)]
|
||||
public class TTSSpeakerInspector : UnityEditor.Editor
|
||||
{
|
||||
// Speaker
|
||||
private TTSSpeaker _speaker;
|
||||
|
||||
// Voices
|
||||
private int _voiceIndex = -1;
|
||||
private string[] _voices = null;
|
||||
|
||||
// Voice text
|
||||
private const string UI_VOICE_HEADER = "Voice Settings";
|
||||
private const string UI_VOICE_KEY = "Voice Preset";
|
||||
|
||||
// GUI
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
// Get speaker
|
||||
if (_speaker == null)
|
||||
{
|
||||
_speaker = target as TTSSpeaker;
|
||||
}
|
||||
// Get voices
|
||||
if (_voices == null || (_voiceIndex >= 0 && _voiceIndex < _voices.Length && !string.Equals(_speaker.presetVoiceID, _voices[_voiceIndex])))
|
||||
{
|
||||
RefreshVoices();
|
||||
}
|
||||
|
||||
// Voice select
|
||||
EditorGUILayout.LabelField(UI_VOICE_HEADER, EditorStyles.boldLabel);
|
||||
// No voices found
|
||||
if (_voices == null || _voices.Length == 0)
|
||||
{
|
||||
EditorGUILayout.TextField(UI_VOICE_KEY, _speaker.presetVoiceID);
|
||||
}
|
||||
// Voice dropdown
|
||||
else
|
||||
{
|
||||
bool updated = false;
|
||||
WitEditorUI.LayoutPopup(UI_VOICE_KEY, _voices, ref _voiceIndex, ref updated);
|
||||
if (updated)
|
||||
{
|
||||
string newVoiceID = _voiceIndex >= 0 && _voiceIndex < _voices.Length
|
||||
? _voices[_voiceIndex]
|
||||
: string.Empty;
|
||||
_speaker.presetVoiceID = newVoiceID;
|
||||
EditorUtility.SetDirty(_speaker);
|
||||
}
|
||||
}
|
||||
|
||||
// Display default ui
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.Space();
|
||||
base.OnInspectorGUI();
|
||||
|
||||
// Layout TTS clip queue
|
||||
LayoutClipQueue();
|
||||
}
|
||||
|
||||
// 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
|
||||
TTSClipData[] QueuedClips = _speaker.QueuedClips;
|
||||
_queueFoldout = EditorGUILayout.Foldout(_queueFoldout, $"{UI_CLIP_QUEUE_TEXT} {(QueuedClips == null ? 0 : QueuedClips.Length)}");
|
||||
if (_queueFoldout)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
if (QueuedClips == null || QueuedClips.Length == 0)
|
||||
{
|
||||
EditorGUILayout.LabelField("None");
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < QueuedClips.Length; 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--;
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh voices
|
||||
private void RefreshVoices()
|
||||
{
|
||||
// Reset voice data
|
||||
_voiceIndex = -1;
|
||||
_voices = null;
|
||||
|
||||
// Get settings
|
||||
TTSService tts = TTSService.Instance;
|
||||
TTSVoiceSettings[] settings = tts?.GetAllPresetVoiceSettings();
|
||||
if (settings == null)
|
||||
{
|
||||
VLog.E("No Preset Voice Settings Found!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply all settings
|
||||
_voices = new string[settings.Length];
|
||||
for (int i = 0; i < settings.Length; i++)
|
||||
{
|
||||
_voices[i] = settings[i].settingsID;
|
||||
if (string.Equals(_speaker.presetVoiceID, _voices[i], StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
_voiceIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3835212f72d4d5149bed0f07915f204e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 69fad6077e2eeef4a812dc81d313fc2a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* 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 Oculus.Interaction.Deprecated
|
||||
{
|
||||
[Obsolete("Replaced by Meta.WitAi.Data.Lib.WitVoiceInfo")]
|
||||
public class TTSWitVoiceData { }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b485971ba702574fa0f255d17bbc46f
|
||||
MonoImporter:
|
||||
labels: ["oculus_interaction_deprecated"]
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* 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 UnityEngine;
|
||||
|
||||
namespace Meta.WitAi.TTS.Editor.Voices
|
||||
{
|
||||
[CustomPropertyDrawer(typeof( TTSWitVoiceSettings))]
|
||||
public class TTSWitVoiceSettingsDrawer : PropertyDrawer
|
||||
{
|
||||
// Constants for var layout
|
||||
private const float VAR_HEIGHT = 20f;
|
||||
private const float VAR_MARGIN = 4f;
|
||||
|
||||
// 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 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)
|
||||
{
|
||||
// Property
|
||||
if (!property.isExpanded)
|
||||
{
|
||||
return VAR_HEIGHT;
|
||||
}
|
||||
// Add each
|
||||
int total = _fields.Length + 1;
|
||||
int voiceIndex = GetVoiceIndex(property);
|
||||
if (voiceIndex != -1)
|
||||
{
|
||||
total += 2;
|
||||
}
|
||||
return total * VAR_HEIGHT + Mathf.Max(0, total - 1) * VAR_MARGIN;
|
||||
}
|
||||
|
||||
// Handles gui layout
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
// On gui
|
||||
float y = position.y;
|
||||
string voiceName = property.FindPropertyRelative(VAR_SETTINGS).stringValue;
|
||||
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(property);
|
||||
// Get voice index
|
||||
int voiceIndex = GetVoiceIndex(property);
|
||||
|
||||
// Iterate subfields
|
||||
for (int s = 0; s < _fields.Length; s++)
|
||||
{
|
||||
FieldInfo subfield = _fields[s];
|
||||
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--;
|
||||
}
|
||||
|
||||
// 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--;
|
||||
}
|
||||
// Refresh voices
|
||||
private void RefreshVoices(SerializedProperty property)
|
||||
{
|
||||
// Get tts wit if possible
|
||||
object targetObject = property.serializedObject.targetObject;
|
||||
if (targetObject == null || targetObject.GetType() != typeof(TTSWit))
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Get configuration
|
||||
TTSWit wit = property.serializedObject.targetObject as TTSWit;
|
||||
IWitRequestConfiguration configuration = wit.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,15 @@
|
||||
/*
|
||||
* 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 Oculus.Interaction.Deprecated
|
||||
{
|
||||
[Obsolete("Handled by Meta.WitAi.WitAppInfoUtility")]
|
||||
public class TTSWitVoiceUtility { }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca11f6ef3c756c64dbbf6d74fdd4c954
|
||||
MonoImporter:
|
||||
labels: ["oculus_interaction_deprecated"]
|
||||
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,98 @@
|
||||
/*
|
||||
* 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 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;
|
||||
|
||||
// Request data
|
||||
public Dictionary<string, string> queryParameters;
|
||||
|
||||
// Clip
|
||||
[NonSerialized] public AudioClip clip;
|
||||
// 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,21 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Meta.WitAi.TTS.Data
|
||||
{
|
||||
public abstract class TTSVoiceSettings
|
||||
{
|
||||
// Used for initial value
|
||||
public const string DEFAULT_ID = "Default Voice";
|
||||
|
||||
/// <summary>
|
||||
/// The unique voice settings id
|
||||
/// </summary>
|
||||
public string settingsID = DEFAULT_ID;
|
||||
}
|
||||
}
|
||||
@@ -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,29 @@
|
||||
/*
|
||||
* 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();
|
||||
|
||||
// Streaming events
|
||||
public TTSStreamEvents Stream = new TTSStreamEvents();
|
||||
|
||||
// Download events
|
||||
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,66 @@
|
||||
/*
|
||||
* 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
|
||||
{
|
||||
[Serializable]
|
||||
public class TTSSpeakerEvent : UnityEvent<TTSSpeaker, string> { }
|
||||
[Serializable]
|
||||
public class TTSSpeakerClipDataEvent : UnityEvent<TTSClipData> { }
|
||||
[Serializable]
|
||||
public class TTSSpeakerEvents : VoiceSpeechEvents
|
||||
{
|
||||
[Header("Speaker Events")]
|
||||
[Tooltip("Called when a speaking begins")]
|
||||
public TTSSpeakerEvent OnStartSpeaking;
|
||||
[Tooltip("Called when a speaking finishes")]
|
||||
public TTSSpeakerEvent OnFinishedSpeaking;
|
||||
[Tooltip("Called when a speaking is cancelled")]
|
||||
public TTSSpeakerEvent OnCancelledSpeaking;
|
||||
[Tooltip("Called when TTS audio clip load begins")]
|
||||
public TTSSpeakerEvent OnClipLoadBegin;
|
||||
[Tooltip("Called when TTS audio clip load fails")]
|
||||
public TTSSpeakerEvent OnClipLoadFailed;
|
||||
[Tooltip("Called when TTS audio clip load successfully")]
|
||||
public TTSSpeakerEvent OnClipLoadSuccess;
|
||||
[Tooltip("Called when TTS audio clip load is cancelled")]
|
||||
public TTSSpeakerEvent OnClipLoadAbort;
|
||||
|
||||
[Header("TTSClip Data Events")]
|
||||
[Tooltip("Called when a new clip is added to the playback queue")]
|
||||
public TTSSpeakerClipDataEvent OnClipDataQueued;
|
||||
[Tooltip("Called when TTS audio clip load begins")]
|
||||
public TTSSpeakerClipDataEvent OnClipDataLoadBegin;
|
||||
[Tooltip("Called when TTS audio clip load fails")]
|
||||
public TTSSpeakerClipDataEvent OnClipDataLoadFailed;
|
||||
[Tooltip("Called when TTS audio clip load successfully")]
|
||||
public TTSSpeakerClipDataEvent OnClipDataLoadSuccess;
|
||||
[Tooltip("Called when TTS audio clip load is cancelled")]
|
||||
public TTSSpeakerClipDataEvent OnClipDataLoadAbort;
|
||||
[Tooltip("Called when a clip is ready for playback")]
|
||||
public TTSSpeakerClipDataEvent OnClipDataPlaybackReady;
|
||||
[Tooltip("Called when a clip playback has begun")]
|
||||
public TTSSpeakerClipDataEvent OnClipDataPlaybackStart;
|
||||
[Tooltip("Called when a clip playback has completed successfully")]
|
||||
public TTSSpeakerClipDataEvent OnClipDataPlaybackFinished;
|
||||
[Tooltip("Called when a clip playback has been cancelled")]
|
||||
public TTSSpeakerClipDataEvent OnClipDataPlaybackCancelled;
|
||||
|
||||
[Header("Queue Events")]
|
||||
[Tooltip("Called when a tts request is added to an empty queue")]
|
||||
public UnityEvent OnPlaybackQueueBegin;
|
||||
[Tooltip("Called the final request is removed from a queue")]
|
||||
public UnityEvent OnPlaybackQueueComplete;
|
||||
}
|
||||
}
|
||||
@@ -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,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;
|
||||
|
||||
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();
|
||||
bool canPerform = request.RequestAudioClip(new Uri(request.CleanUrl(filePath)), (clip, error) =>
|
||||
{
|
||||
// Apply clip
|
||||
clipData.clip = clip;
|
||||
// Call on complete
|
||||
OnStreamComplete(clipData, error);
|
||||
}, clipData.audioType, clipData.diskCacheSettings.StreamFromDisk, 0.01f, clipData.diskCacheSettings.StreamBufferLength, (progress) => clipData.loadProgress = progress);
|
||||
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,203 @@
|
||||
/*
|
||||
* 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].clip);
|
||||
}
|
||||
return (int)(total / (long)1024) + 1;
|
||||
}
|
||||
// Return bytes occupied by clip
|
||||
public static long GetClipBytes(AudioClip clip)
|
||||
{
|
||||
if (clip == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return ((clip.samples * clip.channels) * 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,505 @@
|
||||
/*
|
||||
* 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.Reflection;
|
||||
using UnityEngine;
|
||||
using Meta.WitAi.Interfaces;
|
||||
using Meta.WitAi.Data.Configuration;
|
||||
using Meta.WitAi.TTS.Data;
|
||||
using Meta.WitAi.TTS.Events;
|
||||
using Meta.WitAi.TTS.Interfaces;
|
||||
using Meta.WitAi.Requests;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace Meta.WitAi.TTS.Integrations
|
||||
{
|
||||
[Serializable]
|
||||
public class TTSWitVoiceSettings : TTSVoiceSettings
|
||||
{
|
||||
// Default values
|
||||
public const string DEFAULT_VOICE = "Charlie";
|
||||
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;
|
||||
[Range(50, 200)]
|
||||
public int speed = 100;
|
||||
[Range(25, 200)]
|
||||
public int pitch = 100;
|
||||
}
|
||||
[Serializable]
|
||||
public struct TTSWitRequestSettings
|
||||
{
|
||||
public WitConfiguration configuration;
|
||||
public TTSWitAudioType audioType;
|
||||
public bool audioStream;
|
||||
[Tooltip("Amount of clip length in seconds that must be received before stream is considered ready.")]
|
||||
public float audioStreamReadyDuration;
|
||||
[Tooltip("Total samples to be used to generate clip. A new clip will be generated every time this chunk size is surpassed.")]
|
||||
public float audioStreamChunkLength;
|
||||
[Tooltip("Amount of placeholder stream clips to be generated on service generation.")]
|
||||
public int audioStreamPreloadCount;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Configuration provider
|
||||
public WitConfiguration Configuration => RequestSettings.configuration;
|
||||
|
||||
// Use wit tts vrequest type
|
||||
protected override AudioType GetAudioType()
|
||||
{
|
||||
return WitTTSVRequest.GetAudioType(RequestSettings.audioType);
|
||||
}
|
||||
// Preload stream cache
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
PreloadStreamCache();
|
||||
}
|
||||
// Add delegates
|
||||
protected override void OnEnable()
|
||||
{
|
||||
base.OnEnable();
|
||||
AudioStreamHandler.OnClipUpdated += OnStreamClipUpdated;
|
||||
AudioStreamHandler.OnStreamComplete += OnStreamClipComplete;
|
||||
}
|
||||
// Remove delegates
|
||||
protected override void OnDisable()
|
||||
{
|
||||
base.OnDisable();
|
||||
AudioStreamHandler.OnClipUpdated -= OnStreamClipUpdated;
|
||||
AudioStreamHandler.OnStreamComplete -= OnStreamClipComplete;
|
||||
}
|
||||
// Destroy stream cache
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
base.OnDestroy();
|
||||
UnloadStreamCache();
|
||||
}
|
||||
|
||||
// Clip stream updated
|
||||
private void OnStreamClipUpdated(AudioClip oldClip, AudioClip newClip)
|
||||
{
|
||||
TTSClipData[] clips = GetAllRuntimeCachedClips();
|
||||
if (clips == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (var clipData in clips)
|
||||
{
|
||||
if (oldClip == clipData.clip)
|
||||
{
|
||||
clipData.clip = newClip;
|
||||
WebStreamEvents?.OnStreamClipUpdate?.Invoke(clipData);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clip stream complete
|
||||
private void OnStreamClipComplete(AudioClip clip)
|
||||
{
|
||||
TTSClipData[] clips = GetAllRuntimeCachedClips();
|
||||
if (clips == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (var clipData in clips)
|
||||
{
|
||||
if (clip == clipData.clip)
|
||||
{
|
||||
WebStreamEvents?.OnStreamComplete?.Invoke(clipData);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region AudioStream Cache
|
||||
// Simple check for cache
|
||||
private bool _wasCached = false;
|
||||
// Preload the stream cache
|
||||
private void PreloadStreamCache()
|
||||
{
|
||||
// Ignore
|
||||
if (!RequestSettings.audioStream || RequestSettings.audioStreamPreloadCount <= 0 || _wasCached)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Total samples to preload
|
||||
int totalSamples = Mathf.CeilToInt(RequestSettings.audioStreamChunkLength *
|
||||
WitConstants.ENDPOINT_TTS_CHANNELS *
|
||||
WitConstants.ENDPOINT_TTS_SAMPLE_RATE);
|
||||
|
||||
// Preload specified amount of clips
|
||||
_wasCached = true;
|
||||
AudioStreamHandler.PreloadCachedClips(RequestSettings.audioStreamPreloadCount, totalSamples, WitConstants.ENDPOINT_TTS_CHANNELS, WitConstants.ENDPOINT_TTS_SAMPLE_RATE);
|
||||
}
|
||||
// Preload the stream cache
|
||||
private void UnloadStreamCache()
|
||||
{
|
||||
// Ignore if was not cached
|
||||
if (!_wasCached)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Destroy all cached clips
|
||||
AudioStreamHandler.DestroyCachedClips();
|
||||
_wasCached = false;
|
||||
}
|
||||
#endregion AudioStream Cache
|
||||
|
||||
#region ITTSWebHandler Streams
|
||||
// Request settings
|
||||
[Header("Web Request Settings")]
|
||||
[FormerlySerializedAs("_settings")]
|
||||
public TTSWitRequestSettings RequestSettings = new TTSWitRequestSettings
|
||||
{
|
||||
audioType = TTSWitAudioType.PCM,
|
||||
audioStream = true,
|
||||
audioStreamReadyDuration = 0.1f, // .1 seconds received before starting playback
|
||||
audioStreamChunkLength = 5f, // 5 seconds per clip generation
|
||||
audioStreamPreloadCount = 3 // 3 clips preloaded to be streamed at once
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Whether to stream
|
||||
bool stream = Application.isPlaying && RequestSettings.audioStream;
|
||||
|
||||
// Request tts
|
||||
WitTTSVRequest request = new WitTTSVRequest(RequestSettings.configuration);
|
||||
request.RequestStream(clipData.textToSpeak, RequestSettings.audioType, stream, RequestSettings.audioStreamReadyDuration, RequestSettings.audioStreamChunkLength, clipData.queryParameters,
|
||||
(clip, error) =>
|
||||
{
|
||||
// Apply
|
||||
_webStreams.Remove(clipData.clipID);
|
||||
clipData.clip = clip;
|
||||
// Unloaded
|
||||
if (clipData.loadState == TTSClipLoadState.Unloaded)
|
||||
{
|
||||
error = WitConstants.CANCEL_ERROR;
|
||||
clip.DestroySafely();
|
||||
clip = null;
|
||||
}
|
||||
// Error
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
if (string.Equals(error, WitConstants.CANCEL_ERROR, StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
WebStreamEvents?.OnStreamCancel?.Invoke(clipData);
|
||||
}
|
||||
else
|
||||
{
|
||||
WebStreamEvents?.OnStreamError?.Invoke(clipData, error);
|
||||
}
|
||||
}
|
||||
// Success
|
||||
else
|
||||
{
|
||||
clipData.clip.name = clipData.clipID;
|
||||
WebStreamEvents?.OnStreamReady?.Invoke(clipData);
|
||||
if (!stream)
|
||||
{
|
||||
WebStreamEvents?.OnStreamComplete?.Invoke(clipData);
|
||||
}
|
||||
}
|
||||
},
|
||||
(progress) => clipData.loadProgress = progress);
|
||||
_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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Request tts
|
||||
WitTTSVRequest request = new WitTTSVRequest(RequestSettings.configuration);
|
||||
request.RequestDownload(downloadPath, clipData.textToSpeak, RequestSettings.audioType, clipData.queryParameters,
|
||||
(success, error) =>
|
||||
{
|
||||
_webDownloads.Remove(clipData.clipID);
|
||||
if (string.IsNullOrEmpty(error))
|
||||
{
|
||||
WebDownloadEvents?.OnDownloadSuccess?.Invoke(clipData, downloadPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
WebDownloadEvents?.OnDownloadError?.Invoke(clipData, downloadPath, error);
|
||||
}
|
||||
},
|
||||
(progress) => clipData.loadProgress = progress);
|
||||
_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);
|
||||
|
||||
// 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.Length == 0)
|
||||
{
|
||||
_presetVoiceSettings = new TTSWitVoiceSettings[] { new TTSWitVoiceSettings() };
|
||||
}
|
||||
return _presetVoiceSettings;
|
||||
}
|
||||
}
|
||||
// Default voice setting uses the first voice in the list
|
||||
public TTSVoiceSettings VoiceDefaultSettings => 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 SETTINGS_KEY = "settingsID";
|
||||
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 voiceSettings.GetType().GetFields())
|
||||
{
|
||||
if (!field.IsStatic && !string.Equals(field.Name, SETTINGS_KEY, StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
// Get field value
|
||||
object fieldVal = field.GetValue(voiceSettings);
|
||||
|
||||
// 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;
|
||||
}
|
||||
// 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,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 Meta.WitAi.TTS.Data;
|
||||
using Meta.WitAi.TTS.Events;
|
||||
|
||||
namespace Meta.WitAi.TTS.Interfaces
|
||||
{
|
||||
public interface ITTSWebHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Streaming events
|
||||
/// </summary>
|
||||
TTSStreamEvents WebStreamEvents { get; set; }
|
||||
|
||||
/// <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; set; }
|
||||
|
||||
/// <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,848 @@
|
||||
/*
|
||||
* 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 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;
|
||||
|
||||
// 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>
|
||||
/// 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()
|
||||
{
|
||||
// Set instance
|
||||
_instance = this;
|
||||
_delegates = false;
|
||||
}
|
||||
// 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.WebStreamEvents.OnStreamClipUpdate.AddListener(OnStreamClipUpdated);
|
||||
WebHandler.WebStreamEvents.OnStreamComplete.AddListener(OnWebStreamComplete);
|
||||
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.WebStreamEvents.OnStreamClipUpdate.RemoveListener(OnStreamClipUpdated);
|
||||
WebHandler.WebStreamEvents.OnStreamComplete.RemoveListener(OnWebStreamComplete);
|
||||
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()
|
||||
{
|
||||
// Remove instance
|
||||
if (_instance == this)
|
||||
{
|
||||
_instance = null;
|
||||
}
|
||||
// Abort & unload all
|
||||
UnloadAll();
|
||||
}
|
||||
|
||||
/// <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}");
|
||||
builder.AppendLine($"Length: {(clipData.clip == null ? "NULL" : clipData.clip.length.ToString("0.000") + "secs")}");
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region HELPERS
|
||||
/// <summary>
|
||||
/// Obtain unique id for clip data
|
||||
/// </summary>
|
||||
private const string CLIP_ID_DELIM = "|";
|
||||
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].ToLower().Replace(CLIP_ID_DELIM, "");
|
||||
uniqueID.Append(keyClean);
|
||||
uniqueID.Append(CLIP_ID_DELIM);
|
||||
}
|
||||
}
|
||||
// Finally, add unique id
|
||||
uniqueID.Append(textToSpeak.ToLower());
|
||||
// Return id
|
||||
return GetSha256Hash(CLIP_HASH, uniqueID.ToString());
|
||||
}
|
||||
private readonly SHA256 CLIP_HASH = SHA256.Create();
|
||||
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 = textToSpeak,
|
||||
voiceSettings = voiceSettings,
|
||||
diskCacheSettings = diskCacheSettings,
|
||||
loadState = TTSClipLoadState.Unloaded,
|
||||
loadProgress = 0f,
|
||||
queryParameters = VoiceProvider?.EncodeVoiceSettings(voiceSettings)
|
||||
};
|
||||
|
||||
// Return generated clip
|
||||
return clipData;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Success
|
||||
if (string.IsNullOrEmpty(error))
|
||||
{
|
||||
DiskCacheHandler?.StreamFromDiskCache(clipData);
|
||||
}
|
||||
// Failed
|
||||
else
|
||||
{
|
||||
WebHandler?.RequestStreamFromWeb(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.D(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)
|
||||
{
|
||||
// Callback delegate
|
||||
VLog.D(GetClipLog($"{(fromDisk ? "Disk" : "Web")} Stream Begin", clipData));
|
||||
Events?.Stream?.OnStreamBegin?.Invoke(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;
|
||||
}
|
||||
}
|
||||
|
||||
// Now loaded
|
||||
SetClipLoadState(clipData, TTSClipLoadState.Loaded);
|
||||
VLog.D(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);
|
||||
}
|
||||
// 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.D(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);
|
||||
}
|
||||
// Web stream complete
|
||||
private void OnStreamClipUpdated(TTSClipData clipData)
|
||||
{
|
||||
VLog.D(GetClipLog($"Stream Clip Updated", clipData));
|
||||
Events?.Stream?.OnStreamClipUpdate?.Invoke(clipData);
|
||||
}
|
||||
// Web stream complete
|
||||
private void OnWebStreamComplete(TTSClipData clipData)
|
||||
{
|
||||
VLog.D(GetClipLog($"Web Stream Complete", clipData));
|
||||
Events?.Stream?.OnStreamComplete?.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>
|
||||
private 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);
|
||||
}
|
||||
// Destroy clip
|
||||
if (clipData.clip != null)
|
||||
{
|
||||
clipData.clip.DestroySafely();
|
||||
clipData.clip = null;
|
||||
}
|
||||
|
||||
// Clip is now unloaded
|
||||
SetClipLoadState(clipData, TTSClipLoadState.Unloaded);
|
||||
|
||||
// Unload
|
||||
VLog.D(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.D(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, "File is not Preloaded");
|
||||
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.D(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.D(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.D(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,837 @@
|
||||
/*
|
||||
* 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.Collections.Generic;
|
||||
using Meta.WitAi.Speech;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
using Meta.WitAi.TTS.Data;
|
||||
using Meta.WitAi.TTS.Integrations;
|
||||
using Meta.WitAi.TTS.Interfaces;
|
||||
|
||||
namespace Meta.WitAi.TTS.Utilities
|
||||
{
|
||||
public class TTSSpeaker : MonoBehaviour, ISpeechEventProvider
|
||||
{
|
||||
#region LIFECYCLE
|
||||
// Preset voice id
|
||||
[HideInInspector] [SerializeField] public string presetVoiceID;
|
||||
public TTSVoiceSettings VoiceSettings => TTSService.GetPresetVoiceSettings(presetVoiceID);
|
||||
// Audio source
|
||||
[SerializeField] [FormerlySerializedAs("_source")]
|
||||
public AudioSource AudioSource;
|
||||
[Tooltip("Duplicates audio source reference on awake instead of using it directly.")]
|
||||
[SerializeField] private bool _cloneAudioSource = false;
|
||||
public bool CloneAudioSource => _cloneAudioSource;
|
||||
|
||||
[Tooltip("Text that is added to the front of any Speech() request")]
|
||||
[TextArea]
|
||||
[SerializeField] private string prependedText;
|
||||
[TextArea]
|
||||
[Tooltip("Text that is added to the end of any Speech() text")]
|
||||
[SerializeField] private string appendedText;
|
||||
|
||||
// Events
|
||||
[SerializeField] private TTSSpeakerEvents _events;
|
||||
public TTSSpeakerEvents Events => _events;
|
||||
public VoiceSpeechEvents SpeechEvents => _events;
|
||||
|
||||
// Current clip to be played
|
||||
public TTSClipData SpeakingClip { get; private set; }
|
||||
// Whether currently speaking or not
|
||||
public bool IsSpeaking => SpeakingClip != null;
|
||||
|
||||
// Loading clip queue
|
||||
public TTSClipData[] QueuedClips => _queuedClips.ToArray();
|
||||
// Full clip data list
|
||||
private Queue<TTSClipData> _queuedClips = new Queue<TTSClipData>();
|
||||
// Whether currently loading or not
|
||||
public bool IsLoading => _queuedClips.Count > 0;
|
||||
|
||||
// Current tts service
|
||||
[SerializeField] private TTSService _ttsService;
|
||||
public TTSService TTSService
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_ttsService)
|
||||
{
|
||||
_ttsService = GetComponent<TTSService>();
|
||||
if (!_ttsService)
|
||||
{
|
||||
_ttsService = TTSService.Instance;
|
||||
}
|
||||
}
|
||||
return _ttsService;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if queued
|
||||
private bool _hasQueue = false;
|
||||
private bool _willHaveQueue = false;
|
||||
|
||||
// Text processors
|
||||
private ISpeakerTextPreprocessor[] _textPreprocessors;
|
||||
private ISpeakerTextPostprocessor[] _textPostprocessors;
|
||||
|
||||
public static TTSSpeaker speaker;
|
||||
// Automatically generate source if needed
|
||||
protected virtual void Awake()
|
||||
{
|
||||
speaker = this;
|
||||
// Find base audio source if possible
|
||||
if (AudioSource == null)
|
||||
{
|
||||
AudioSource = gameObject.GetComponentInChildren<AudioSource>();
|
||||
}
|
||||
|
||||
// Duplicate audio source
|
||||
if (CloneAudioSource)
|
||||
{
|
||||
// Create new audio source
|
||||
AudioSource instance = new GameObject($"{gameObject.name}_AudioOneShot").AddComponent<AudioSource>();
|
||||
instance.PreloadCopyData();
|
||||
|
||||
// Move into this transform & default to 3D audio
|
||||
if (AudioSource == null)
|
||||
{
|
||||
instance.transform.SetParent(transform, false);
|
||||
instance.spread = 1f;
|
||||
}
|
||||
|
||||
// Move into audio source & copy source values
|
||||
else
|
||||
{
|
||||
instance.transform.SetParent(AudioSource.transform, false);
|
||||
instance.Copy(AudioSource);
|
||||
}
|
||||
|
||||
// Reset instance's transform
|
||||
instance.transform.localPosition = Vector3.zero;
|
||||
instance.transform.localRotation = Quaternion.identity;
|
||||
instance.transform.localScale = Vector3.one;
|
||||
|
||||
// Apply
|
||||
AudioSource = instance;
|
||||
}
|
||||
|
||||
// Setup audio source settings
|
||||
AudioSource.playOnAwake = false;
|
||||
|
||||
// Get text processors
|
||||
RefreshProcessors();
|
||||
}
|
||||
// Refresh processors
|
||||
protected virtual void RefreshProcessors()
|
||||
{
|
||||
// Get preprocessors
|
||||
if (_textPreprocessors == null)
|
||||
{
|
||||
_textPreprocessors = GetComponents<ISpeakerTextPreprocessor>();
|
||||
}
|
||||
// Get postprocessors
|
||||
if (_textPostprocessors == null)
|
||||
{
|
||||
_textPostprocessors = GetComponents<ISpeakerTextPostprocessor>();
|
||||
}
|
||||
// Fix prepend text to ensure it has a space
|
||||
if (!string.IsNullOrEmpty(prependedText) && prependedText.Length > 0 && !prependedText.EndsWith(" "))
|
||||
{
|
||||
prependedText = prependedText + " ";
|
||||
}
|
||||
// Fix append text to ensure it is spaced correctly
|
||||
if (!string.IsNullOrEmpty(appendedText) && appendedText.Length > 0 && !appendedText.StartsWith(" "))
|
||||
{
|
||||
appendedText = " " + appendedText;
|
||||
}
|
||||
}
|
||||
// Stop
|
||||
protected virtual void OnDestroy()
|
||||
{
|
||||
Stop();
|
||||
_queuedClips = null;
|
||||
SpeakingClip = null;
|
||||
}
|
||||
// Add listener for clip unload
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
if (!TTSService)
|
||||
{
|
||||
return;
|
||||
}
|
||||
TTSService.Events.OnClipUnloaded.AddListener(OnClipUnload);
|
||||
TTSService.Events.Stream.OnStreamClipUpdate.AddListener(OnClipUpdated);
|
||||
}
|
||||
// Stop speaking & remove listener
|
||||
protected virtual void OnDisable()
|
||||
{
|
||||
Stop();
|
||||
if (!TTSService)
|
||||
{
|
||||
return;
|
||||
}
|
||||
TTSService.Events.OnClipUnloaded.RemoveListener(OnClipUnload);
|
||||
TTSService.Events.Stream.OnStreamClipUpdate.RemoveListener(OnClipUpdated);
|
||||
}
|
||||
// Clip unloaded externally
|
||||
protected virtual void OnClipUnload(TTSClipData clipData)
|
||||
{
|
||||
// Cancel load
|
||||
if (QueueContainsClip(clipData))
|
||||
{
|
||||
// Remove all references of the clip
|
||||
RemoveLoadingClip(clipData, true);
|
||||
// Cancel
|
||||
OnLoadCancelled(clipData);
|
||||
return;
|
||||
}
|
||||
// Cancel playback
|
||||
if (clipData.Equals(SpeakingClip))
|
||||
{
|
||||
StopSpeaking();
|
||||
}
|
||||
}
|
||||
// Clip stream complete
|
||||
protected virtual void OnClipUpdated(TTSClipData clipData)
|
||||
{
|
||||
// Ignore if not speaking clip
|
||||
if (!clipData.Equals(SpeakingClip) || AudioSource == null || !AudioSource.isPlaying)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop previous clip playback
|
||||
int elapsedSamples = AudioSource.timeSamples;
|
||||
AudioSource.Stop();
|
||||
|
||||
// Apply new clip
|
||||
SpeakingClip = clipData;
|
||||
AudioSource.clip = SpeakingClip.clip;
|
||||
AudioSource.timeSamples = elapsedSamples;
|
||||
AudioSource.Play();
|
||||
}
|
||||
// Check queue
|
||||
private bool QueueContainsClip(TTSClipData clipData)
|
||||
{
|
||||
if (_queuedClips != null)
|
||||
{
|
||||
foreach (var clip in _queuedClips)
|
||||
{
|
||||
if (clip.Equals(clipData))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Refresh queue
|
||||
private void RefreshQueued()
|
||||
{
|
||||
bool newHasQueueStatus = IsLoading || IsSpeaking || _willHaveQueue;
|
||||
if (_hasQueue != newHasQueueStatus)
|
||||
{
|
||||
_hasQueue = newHasQueueStatus;
|
||||
if (_hasQueue)
|
||||
{
|
||||
Events?.OnPlaybackQueueBegin?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
Events?.OnPlaybackQueueComplete?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region TEXT
|
||||
/// <summary>
|
||||
/// Gets final text following prepending/appending & any special formatting
|
||||
/// </summary>
|
||||
/// <param name="textToSpeak">The base text to be spoken</param>
|
||||
/// <returns>Returns an array of split texts to be spoken</returns>
|
||||
public virtual string[] GetFinalText(string textToSpeak)
|
||||
{
|
||||
// Get processors
|
||||
RefreshProcessors();
|
||||
|
||||
// Get results
|
||||
List<string> phrases = new List<string>();
|
||||
phrases.Add(textToSpeak);
|
||||
|
||||
// Pre-processor
|
||||
if (_textPreprocessors != null)
|
||||
{
|
||||
foreach (var preprocessor in _textPreprocessors)
|
||||
{
|
||||
preprocessor.OnPreprocessTTS(this, phrases);
|
||||
}
|
||||
}
|
||||
|
||||
// Add prepend & appended text to each item
|
||||
for (int i = 0; i < phrases.Count; i++)
|
||||
{
|
||||
string phrase = phrases[i];
|
||||
phrase = $"{prependedText}{phrase}{appendedText}".Trim();
|
||||
phrases[i] = phrase;
|
||||
}
|
||||
|
||||
// Post-processors
|
||||
if (_textPostprocessors != null)
|
||||
{
|
||||
foreach (var postprocessor in _textPostprocessors)
|
||||
{
|
||||
postprocessor.OnPostprocessTTS(this, phrases);
|
||||
}
|
||||
}
|
||||
|
||||
// Return all text items
|
||||
return phrases.ToArray();
|
||||
}
|
||||
/// <summary>
|
||||
/// Obtain final text list from format & text list
|
||||
/// </summary>
|
||||
/// <param name="format">The format to be used</param>
|
||||
/// <param name="textsToSpeak">The array of strings to be inserted into the format</param>
|
||||
/// <returns>Returns a list of formatted texts</returns>
|
||||
public virtual string[] GetFinalTextFormatted(string format, params string[] textsToSpeak)
|
||||
{
|
||||
return GetFinalText(GetFormattedText(format, textsToSpeak));
|
||||
}
|
||||
/// <summary>
|
||||
/// Formats text using an initial format string parameter and additional text items to
|
||||
/// be inserted into the format
|
||||
/// </summary>
|
||||
/// <param name="format">The format to be used</param>
|
||||
/// <param name="textsToSpeak">The array of strings to be inserted into the format</param>
|
||||
/// <returns>A formatted text string</returns>
|
||||
public string GetFormattedText(string format, params string[] textsToSpeak)
|
||||
{
|
||||
if (textsToSpeak != null && !string.IsNullOrEmpty(format))
|
||||
{
|
||||
object[] objects = new object[textsToSpeak.Length];
|
||||
textsToSpeak.CopyTo(objects, 0);
|
||||
return string.Format(format, objects);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region REQUESTS
|
||||
/// <summary>
|
||||
/// Load a tts clip using the specified text & cache settings.
|
||||
/// Plays clip immediately upon load & will cancel all previously loading/spoken phrases.
|
||||
/// </summary>
|
||||
/// <param name="textToSpeak">The text to be spoken</param>
|
||||
/// <param name="diskCacheSettings">Specific tts load caching settings</param>
|
||||
public void Speak(string textToSpeak, TTSDiskCacheSettings diskCacheSettings) => Speak(textToSpeak, diskCacheSettings, false);
|
||||
public void Speak(string textToSpeak) => Speak(textToSpeak, null);
|
||||
/// <summary>
|
||||
/// Load a tts clip using the specified text & cache settings.
|
||||
/// Adds clip to speak queue and will speak once previously spoken phrases are complete
|
||||
/// </summary>
|
||||
/// <param name="textToSpeak">The text to be spoken</param>
|
||||
/// <param name="diskCacheSettings">Specific tts load caching settings</param>
|
||||
public void SpeakQueued(string textToSpeak, TTSDiskCacheSettings diskCacheSettings) => Speak(textToSpeak, diskCacheSettings, true);
|
||||
public void SpeakQueued(string textToSpeak) => SpeakQueued(textToSpeak, null);
|
||||
|
||||
/// <summary>
|
||||
/// Loads a formated phrase to be spoken
|
||||
/// Adds clip to speak queue and will speak once previously spoken phrases are complete
|
||||
/// </summary>
|
||||
/// <param name="format">Format string to be filled in with texts</param>
|
||||
public void SpeakFormat(string format, params string[] textsToSpeak) =>
|
||||
Speak(GetFormattedText(format, textsToSpeak), null, false);
|
||||
/// <summary>
|
||||
/// Loads a formated phrase to be spoken
|
||||
/// Adds clip to speak queue and will speak once previously spoken phrases are complete
|
||||
/// </summary>
|
||||
/// <param name="format">Format string to be filled in with texts</param>
|
||||
public void SpeakFormatQueued(string format, params string[] textsToSpeak) =>
|
||||
Speak(GetFormattedText(format, textsToSpeak), null, true);
|
||||
|
||||
/// <summary>
|
||||
/// Speak and wait for load/playback completion
|
||||
/// </summary>
|
||||
/// <param name="textToSpeak">The text to be spoken</param>
|
||||
/// <param name="diskCacheSettings">Specific tts load caching settings</param>
|
||||
public IEnumerator SpeakAsync(string textToSpeak, TTSDiskCacheSettings diskCacheSettings)
|
||||
{
|
||||
_willHaveQueue = true;
|
||||
Stop();
|
||||
_willHaveQueue = false;
|
||||
yield return SpeakQueuedAsync(new string[] {textToSpeak}, diskCacheSettings);
|
||||
}
|
||||
public IEnumerator SpeakAsync(string textToSpeak)
|
||||
{
|
||||
yield return SpeakAsync(textToSpeak, null);
|
||||
}
|
||||
/// <summary>
|
||||
/// Speak and wait for load/playback completion
|
||||
/// </summary>
|
||||
/// <param name="textToSpeak">The text to be spoken</param>
|
||||
/// <param name="diskCacheSettings">Specific tts load caching settings</param>
|
||||
public IEnumerator SpeakQueuedAsync(string[] textsToSpeak, TTSDiskCacheSettings diskCacheSettings)
|
||||
{
|
||||
// Speak each queued
|
||||
foreach (var textToSpeak in textsToSpeak)
|
||||
{
|
||||
SpeakQueued(textToSpeak, diskCacheSettings);
|
||||
}
|
||||
// Wait while loading/speaking
|
||||
yield return new WaitWhile(() => IsLoading || IsSpeaking);
|
||||
}
|
||||
public IEnumerator SpeakQueuedAsync(string[] textsToSpeak)
|
||||
{
|
||||
yield return SpeakQueuedAsync(textsToSpeak, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a tts clip & handles playback
|
||||
/// </summary>
|
||||
/// <param name="textToSpeak">The text to be spoken</param>
|
||||
/// <param name="diskCacheSettings">Specific tts load caching settings</param>
|
||||
/// <param name="addToQueue">Whether or not this phrase should be enqueued into the speak queue</param>
|
||||
private void Speak(string textToSpeak, TTSDiskCacheSettings diskCacheSettings, bool addToQueue)
|
||||
{
|
||||
// Ensure voice settings exist
|
||||
TTSVoiceSettings voiceSettings = VoiceSettings;
|
||||
if (voiceSettings == null)
|
||||
{
|
||||
VLog.E($"No voice found with preset id: {presetVoiceID}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get final text phrases to be spoken
|
||||
string[] phrases = GetFinalText(textToSpeak);
|
||||
if (phrases == null || phrases.Length == 0)
|
||||
{
|
||||
VLog.W($"All phrases removed\nSource Phrase: {textToSpeak}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel previous loading queue
|
||||
if (!addToQueue)
|
||||
{
|
||||
_willHaveQueue = true;
|
||||
StopLoading();
|
||||
_willHaveQueue = false;
|
||||
}
|
||||
|
||||
// Iterate voices
|
||||
foreach (var phrase in phrases)
|
||||
{
|
||||
// Handle load
|
||||
HandleLoad(phrase, voiceSettings, diskCacheSettings, addToQueue);
|
||||
|
||||
// Add additional to queue
|
||||
if (!addToQueue)
|
||||
{
|
||||
addToQueue = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Stop loading all items in the queue
|
||||
public virtual void StopLoading()
|
||||
{
|
||||
// Ignore if not loading
|
||||
if (!IsLoading)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel each clip from loading
|
||||
while (_queuedClips.Count > 0)
|
||||
{
|
||||
OnLoadCancelled(_queuedClips.Dequeue());
|
||||
}
|
||||
|
||||
// Refresh in queue check
|
||||
RefreshQueued();
|
||||
}
|
||||
// Stop playback if possible
|
||||
public virtual void StopSpeaking()
|
||||
{
|
||||
// Cannot stop speaking when not currently speaking
|
||||
if (!IsSpeaking)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel playback
|
||||
HandlePlaybackComplete(true);
|
||||
}
|
||||
// Stops loading & speaking immediately
|
||||
public virtual void Stop()
|
||||
{
|
||||
StopLoading();
|
||||
StopSpeaking();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region LOAD
|
||||
// Handles speaking depending on the state of the specified audio
|
||||
private void HandleLoad(string textToSpeak, TTSVoiceSettings voiceSettings,
|
||||
TTSDiskCacheSettings diskCacheSettings, bool addToQueue)
|
||||
{
|
||||
// Perform load request (Always waits a frame to ensure callbacks occur first)
|
||||
DateTime startTime = DateTime.Now;
|
||||
string clipId = TTSService.GetClipID(textToSpeak, voiceSettings);
|
||||
TTSClipData clipData = TTSService.Load(textToSpeak, clipId, voiceSettings, diskCacheSettings,
|
||||
(clipData2, error) => HandleLoadComplete(clipData2, error, addToQueue, startTime));
|
||||
|
||||
// Ignore without clip
|
||||
if (clipData == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Enqueue
|
||||
_queuedClips.Enqueue(clipData);
|
||||
RefreshQueued();
|
||||
|
||||
// Load begin
|
||||
OnLoadBegin(clipData);
|
||||
}
|
||||
// Load begin
|
||||
protected virtual void OnLoadBegin(TTSClipData clipData)
|
||||
{
|
||||
VLog.D($"Load Begin\nText: {clipData?.textToSpeak}");
|
||||
Events?.OnClipDataLoadBegin?.Invoke(clipData);
|
||||
Events?.OnClipLoadBegin?.Invoke(this, clipData?.textToSpeak);
|
||||
Events?.OnClipDataQueued?.Invoke(clipData);
|
||||
}
|
||||
// Load complete
|
||||
private void HandleLoadComplete(TTSClipData clipData, string error, bool addToQueue, DateTime startTime)
|
||||
{
|
||||
// Invalid clip, ignore
|
||||
if (!QueueContainsClip(clipData))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for other errors
|
||||
if (string.IsNullOrEmpty(error))
|
||||
{
|
||||
if (clipData.clip == null)
|
||||
{
|
||||
error = "No clip returned";
|
||||
}
|
||||
else if (clipData.loadState == TTSClipLoadState.Error)
|
||||
{
|
||||
error = "Error";
|
||||
}
|
||||
else if (clipData.loadState == TTSClipLoadState.Unloaded)
|
||||
{
|
||||
error = WitConstants.CANCEL_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
// Load failed
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
// Remove clip
|
||||
RemoveLoadingClip(clipData, false);
|
||||
|
||||
// Cancelled
|
||||
if (string.Equals(WitConstants.CANCEL_ERROR, error))
|
||||
{
|
||||
OnLoadCancelled(clipData);
|
||||
}
|
||||
// Failed
|
||||
else
|
||||
{
|
||||
OnLoadFailed(clipData, error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Load success event
|
||||
double loadDuration = (DateTime.Now - startTime).TotalMilliseconds;
|
||||
OnLoadSuccess(clipData, loadDuration);
|
||||
|
||||
// Stop speaking except for this clip
|
||||
if (!addToQueue)
|
||||
{
|
||||
StopSpeaking();
|
||||
}
|
||||
|
||||
// Playback ready
|
||||
HandlePlaybackReady(clipData);
|
||||
}
|
||||
// Remove first instance or all instances of clip
|
||||
private void RemoveLoadingClip(TTSClipData clipData, bool allInstances)
|
||||
{
|
||||
// If first & does not need all, dequeue clip
|
||||
if (!allInstances && _queuedClips.Peek().Equals(clipData))
|
||||
{
|
||||
_queuedClips.Dequeue();
|
||||
RefreshQueued();
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise create discard queue
|
||||
Queue<TTSClipData> discard = _queuedClips;
|
||||
_queuedClips = new Queue<TTSClipData>();
|
||||
|
||||
// Iterate all items
|
||||
bool found = false;
|
||||
while (discard.Count > 0)
|
||||
{
|
||||
// Dequeue from discard
|
||||
TTSClipData check = discard.Dequeue();
|
||||
|
||||
// Matching clip
|
||||
if (check.Equals(clipData))
|
||||
{
|
||||
// First
|
||||
if (!found)
|
||||
{
|
||||
found = true;
|
||||
}
|
||||
// Enqueue Duplicate
|
||||
else if (!allInstances)
|
||||
{
|
||||
_queuedClips.Enqueue(check);
|
||||
}
|
||||
}
|
||||
// Enqueue if check matches & not equal
|
||||
else if (check != null)
|
||||
{
|
||||
_queuedClips.Enqueue(check);
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh in queue check
|
||||
RefreshQueued();
|
||||
}
|
||||
// Load cancelled
|
||||
protected virtual void OnLoadCancelled(TTSClipData clipData)
|
||||
{
|
||||
VLog.D($"Load Cancelled\nText: {clipData?.textToSpeak}");
|
||||
Events?.OnClipDataLoadAbort?.Invoke(clipData);
|
||||
Events?.OnClipLoadAbort?.Invoke(this, clipData?.textToSpeak);
|
||||
}
|
||||
// Load failed
|
||||
protected virtual void OnLoadFailed(TTSClipData clipData, string error)
|
||||
{
|
||||
VLog.E($"Load Failed\nText: {clipData?.textToSpeak}");
|
||||
Events?.OnClipDataLoadFailed?.Invoke(clipData);
|
||||
Events?.OnClipLoadFailed?.Invoke(this, clipData?.textToSpeak);
|
||||
}
|
||||
// Load success
|
||||
protected virtual void OnLoadSuccess(TTSClipData clipData, double loadDuration)
|
||||
{
|
||||
VLog.D($"Load Success\nText: {clipData?.textToSpeak}\nDuration: {loadDuration:0.00}ms");
|
||||
Events?.OnClipDataLoadSuccess?.Invoke(clipData);
|
||||
Events?.OnClipLoadSuccess?.Invoke(this, clipData?.textToSpeak);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region READY
|
||||
// Playback ready
|
||||
private void HandlePlaybackReady(TTSClipData clipData)
|
||||
{
|
||||
// Invalid clip, ignore
|
||||
if (!QueueContainsClip(clipData))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Callback delegate
|
||||
OnPlaybackReady(clipData);
|
||||
|
||||
// Attempt to play next in queue
|
||||
RefreshPlayback();
|
||||
}
|
||||
// Ready
|
||||
protected virtual void OnPlaybackReady(TTSClipData clipData)
|
||||
{
|
||||
VLog.D($"Playback Ready\nText: {clipData.textToSpeak}");
|
||||
Events?.OnAudioClipPlaybackReady?.Invoke(clipData.clip);
|
||||
Events?.OnClipDataPlaybackReady?.Invoke(clipData);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region PLAYBACK
|
||||
// Wait for playback completion
|
||||
private Coroutine _waitForCompletion;
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes playback queue to play next available clip if possible
|
||||
/// </summary>
|
||||
private void RefreshPlayback()
|
||||
{
|
||||
// Ignore if currently playing or nothing in uque
|
||||
if (SpeakingClip != null || _queuedClips.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Peek next clip
|
||||
TTSClipData clipData = _queuedClips.Peek();
|
||||
if (clipData == null)
|
||||
{
|
||||
HandlePlaybackFailure(null, "TTSClipData no longer exists");
|
||||
return;
|
||||
}
|
||||
// Still preparing
|
||||
if (clipData.loadState == TTSClipLoadState.Preparing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (clipData.loadState != TTSClipLoadState.Loaded)
|
||||
{
|
||||
HandlePlaybackFailure(clipData, $"TTSClipData is {clipData.loadState}");
|
||||
return;
|
||||
}
|
||||
// No audio source
|
||||
if (AudioSource == null)
|
||||
{
|
||||
HandlePlaybackFailure(clipData, "AudioSource not found");
|
||||
return;
|
||||
}
|
||||
// Somehow clip unloaded
|
||||
if (clipData.clip == null)
|
||||
{
|
||||
HandlePlaybackFailure(clipData, "AudioClip no longer exists");
|
||||
return;
|
||||
}
|
||||
|
||||
// Dequeue & apply
|
||||
SpeakingClip = _queuedClips.Dequeue();
|
||||
|
||||
// Started speaking
|
||||
AudioSource.clip = SpeakingClip.clip;
|
||||
AudioSource.timeSamples = 0;
|
||||
AudioSource.Play();
|
||||
|
||||
// Callback events
|
||||
OnPlaybackBegin(SpeakingClip);
|
||||
|
||||
// Wait for completion
|
||||
if (_waitForCompletion != null)
|
||||
{
|
||||
StopCoroutine(_waitForCompletion);
|
||||
_waitForCompletion = null;
|
||||
}
|
||||
_waitForCompletion = StartCoroutine(WaitForPlaybackComplete());
|
||||
}
|
||||
// Handles failure
|
||||
private void HandlePlaybackFailure(TTSClipData clipData, string error)
|
||||
{
|
||||
// Perform load completion
|
||||
HandleLoadComplete(clipData, error, false, default(DateTime));
|
||||
|
||||
// Try to play next
|
||||
RefreshPlayback();
|
||||
}
|
||||
// Playback begin
|
||||
protected virtual void OnPlaybackBegin(TTSClipData clipData)
|
||||
{
|
||||
VLog.D($"Playback Begin\nText: {clipData.textToSpeak}");
|
||||
Events?.OnStartSpeaking?.Invoke(this, clipData.textToSpeak);
|
||||
Events?.OnTextPlaybackStart?.Invoke(clipData.textToSpeak);
|
||||
Events?.OnAudioClipPlaybackStart?.Invoke(clipData.clip);
|
||||
Events?.OnClipDataPlaybackStart?.Invoke(clipData);
|
||||
}
|
||||
// Wait for clip completion
|
||||
private IEnumerator WaitForPlaybackComplete()
|
||||
{
|
||||
// Use delta time to wait for completion
|
||||
float elapsedTime = 0f;
|
||||
while (!IsPlaybackComplete(elapsedTime))
|
||||
{
|
||||
yield return new WaitForEndOfFrame();
|
||||
elapsedTime += Time.deltaTime;
|
||||
}
|
||||
|
||||
// Playback completed
|
||||
HandlePlaybackComplete(false);
|
||||
}
|
||||
// Check for playback completion
|
||||
protected virtual bool IsPlaybackComplete(float elapsedTime)
|
||||
{
|
||||
return SpeakingClip == null || SpeakingClip.clip == null || elapsedTime >= SpeakingClip.clip.length || (AudioSource != null && !AudioSource.isPlaying);
|
||||
}
|
||||
// Completed playback
|
||||
protected virtual void HandlePlaybackComplete(bool stopped)
|
||||
{
|
||||
// Old clip
|
||||
TTSClipData lastClipData = SpeakingClip;
|
||||
|
||||
// Clear speaking clip
|
||||
SpeakingClip = null;
|
||||
|
||||
// Stop playback handler
|
||||
if (_waitForCompletion != null)
|
||||
{
|
||||
StopCoroutine(_waitForCompletion);
|
||||
_waitForCompletion = null;
|
||||
}
|
||||
|
||||
// Stop audio source playback
|
||||
if (AudioSource != null && AudioSource.isPlaying)
|
||||
{
|
||||
AudioSource.Stop();
|
||||
}
|
||||
|
||||
// Stopped
|
||||
if (stopped)
|
||||
{
|
||||
OnPlaybackCancelled(lastClipData, "Playback Stopped");
|
||||
}
|
||||
// No clip found
|
||||
else if (lastClipData == null)
|
||||
{
|
||||
OnPlaybackCancelled(null, "TTSClipData no longer exists");
|
||||
}
|
||||
// Clip unloaded
|
||||
else if (lastClipData.loadState == TTSClipLoadState.Unloaded)
|
||||
{
|
||||
OnPlaybackCancelled(lastClipData, "TTSClipData was unloaded");
|
||||
}
|
||||
// Clip destroyed
|
||||
else if (lastClipData.clip == null)
|
||||
{
|
||||
OnPlaybackCancelled(lastClipData, "AudioClip no longer exists");
|
||||
}
|
||||
// Success
|
||||
else
|
||||
{
|
||||
OnPlaybackComplete(lastClipData);
|
||||
}
|
||||
|
||||
// Refresh in queue check
|
||||
RefreshQueued();
|
||||
|
||||
// Attempt to play next in queue
|
||||
RefreshPlayback();
|
||||
}
|
||||
// Playback cancelled
|
||||
protected virtual void OnPlaybackCancelled(TTSClipData clipData, string reason)
|
||||
{
|
||||
VLog.D($"Playback Cancelled\nText: {clipData?.textToSpeak}\nReason: {reason}");
|
||||
Events?.OnCancelledSpeaking?.Invoke(this, clipData?.textToSpeak);
|
||||
Events?.OnTextPlaybackCancelled?.Invoke(clipData?.textToSpeak);
|
||||
Events?.OnAudioClipPlaybackCancelled?.Invoke(clipData?.clip);
|
||||
Events?.OnClipDataPlaybackCancelled?.Invoke(clipData);
|
||||
}
|
||||
// Playback success
|
||||
protected virtual void OnPlaybackComplete(TTSClipData clipData)
|
||||
{
|
||||
VLog.D($"Playback Finished\nText: {clipData?.textToSpeak}");
|
||||
Events?.OnFinishedSpeaking?.Invoke(this, clipData?.textToSpeak);
|
||||
Events?.OnTextPlaybackFinished?.Invoke(clipData?.textToSpeak);
|
||||
Events?.OnAudioClipPlaybackFinished?.Invoke(clipData?.clip);
|
||||
Events?.OnClipDataPlaybackFinished?.Invoke(clipData);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b15403450229c3a4b8455a61d6143a6d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* 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>
|
||||
string[] GetVoiceIds();
|
||||
|
||||
/// <summary>
|
||||
/// Get specific phrases per voice
|
||||
/// </summary>
|
||||
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();
|
||||
|
||||
// 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 string[] GetAllPhrases()
|
||||
{
|
||||
// Ensure speaker exists
|
||||
SetupSpeaker();
|
||||
|
||||
// Get all phrases
|
||||
List<string> phrases = new List<string>();
|
||||
|
||||
// Add phrases split from phrase file
|
||||
AddUniquePhrases(phrases, PhraseFile?.text.Split('\n'));
|
||||
// Add phrases serialized in phrase array
|
||||
AddUniquePhrases(phrases, Phrases);
|
||||
|
||||
// Get final text
|
||||
string[] oldPhrases = phrases.ToArray();
|
||||
phrases.Clear();
|
||||
for (int i = 0; i < oldPhrases.Length; i++)
|
||||
{
|
||||
string[] newPhrases = Speaker.GetFinalText(oldPhrases[i]);
|
||||
if (newPhrases != null && newPhrases.Length > 0)
|
||||
{
|
||||
phrases.AddRange(newPhrases);
|
||||
}
|
||||
}
|
||||
|
||||
// Return array
|
||||
return phrases.ToArray();
|
||||
}
|
||||
// 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 string[] GetVoiceIds()
|
||||
{
|
||||
SetupSpeaker();
|
||||
string voiceId = Speaker?.presetVoiceID;
|
||||
if (string.IsNullOrEmpty(voiceId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return new string[] {voiceId};
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns the supported phrases per voice
|
||||
/// </summary>
|
||||
public virtual 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: 81f17bb00ee9f68428d962165826f2fd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* 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 Oculus.Interaction.Deprecated
|
||||
{
|
||||
[Obsolete("Replaced by Meta.WitAi.RequestUtility")]
|
||||
public class VoiceUnityRequest { }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 81e67c2eb3d16d04bbc5300763f1bea1
|
||||
MonoImporter:
|
||||
labels: ["oculus_interaction_deprecated"]
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* 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 Oculus.Interaction.Deprecated
|
||||
{
|
||||
[Obsolete("Replaced by Meta.WitAi.WitRequestUtility")]
|
||||
public class WitUnityRequest { }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3010b240dac799a4faf046323c12e522
|
||||
MonoImporter:
|
||||
labels: ["oculus_interaction_deprecated"]
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user