Initial Commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f4d30ee47fb6e7d4a888e9ee4b707391
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Meta.Voice.Audio;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Meta.WitAi.TTS.Data
|
||||
{
|
||||
// Various request load states
|
||||
public enum TTSClipLoadState
|
||||
{
|
||||
Unloaded,
|
||||
Preparing,
|
||||
Loaded,
|
||||
Error
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class TTSClipData
|
||||
{
|
||||
// Text to be spoken
|
||||
public string textToSpeak;
|
||||
// Unique identifier
|
||||
public string clipID;
|
||||
// Audio type
|
||||
public AudioType audioType;
|
||||
// Voice settings for request
|
||||
public TTSVoiceSettings voiceSettings;
|
||||
// Cache settings for request
|
||||
public TTSDiskCacheSettings diskCacheSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Unique request id used for tracking & logging
|
||||
/// </summary>
|
||||
public string queryRequestId => _queryRequestId;
|
||||
private string _queryRequestId = Guid.NewGuid().ToString();
|
||||
// Whether service should stream audio or just provide all at once
|
||||
public bool queryStream;
|
||||
// Request data
|
||||
public Dictionary<string, string> queryParameters;
|
||||
|
||||
// Clip stream
|
||||
public IAudioClipStream clipStream
|
||||
{
|
||||
get => _clipStream;
|
||||
set
|
||||
{
|
||||
// Unload previous clip stream
|
||||
IAudioClipStream v = value;
|
||||
if (_clipStream != null && _clipStream != v)
|
||||
{
|
||||
clipStream.OnStreamReady = null;
|
||||
clipStream.OnStreamUpdated = null;
|
||||
clipStream.OnStreamComplete = null;
|
||||
_clipStream.Unload();
|
||||
}
|
||||
// Apply new clip stream
|
||||
_clipStream = v;
|
||||
}
|
||||
}
|
||||
private IAudioClipStream _clipStream;
|
||||
public AudioClip clip
|
||||
{
|
||||
get
|
||||
{
|
||||
if (clipStream is IAudioClipProvider uacs)
|
||||
{
|
||||
return uacs.Clip;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// Clip load state
|
||||
[NonSerialized] public TTSClipLoadState loadState;
|
||||
// Clip load progress
|
||||
[NonSerialized] public float loadProgress;
|
||||
|
||||
// On clip state change
|
||||
public Action<TTSClipData, TTSClipLoadState> onStateChange;
|
||||
|
||||
/// <summary>
|
||||
/// A callback when clip stream is ready
|
||||
/// Returns an error if there was an issue
|
||||
/// </summary>
|
||||
public Action<string> onPlaybackReady;
|
||||
/// <summary>
|
||||
/// A callback when clip has downloaded successfully
|
||||
/// Returns an error if there was an issue
|
||||
/// </summary>
|
||||
public Action<string> onDownloadComplete;
|
||||
|
||||
/// <summary>
|
||||
/// Compare clips if possible
|
||||
/// </summary>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is TTSClipData other)
|
||||
{
|
||||
return Equals(other);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/// <summary>
|
||||
/// Compare clip ids
|
||||
/// </summary>
|
||||
public bool Equals(TTSClipData other)
|
||||
{
|
||||
return HasClipId(other?.clipID);
|
||||
}
|
||||
/// <summary>
|
||||
/// Compare clip ids
|
||||
/// </summary>
|
||||
public bool HasClipId(string clipId)
|
||||
{
|
||||
return string.Equals(clipID, clipId, StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
/// <summary>
|
||||
/// Get hash code
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hash = 17;
|
||||
hash = hash * 31 + clipID.GetHashCode();
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef626b8cea4f59646a5076430a0e14aa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+51
@@ -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;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4d1170a24dd77d49bf3cd610dd1c9a5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace Meta.WitAi.TTS.Data
|
||||
{
|
||||
public abstract class TTSVoiceSettings
|
||||
{
|
||||
[Tooltip("A unique id used for linking these voice settings to a TTS Speaker")]
|
||||
[FormerlySerializedAs("settingsID")]
|
||||
public string SettingsId;
|
||||
|
||||
[Tooltip("Text that is added to the front of any TTS request using this voice setting")]
|
||||
[TextArea]
|
||||
public string PrependedText;
|
||||
|
||||
[TextArea]
|
||||
[Tooltip("Text that is added to the end of any TTS request using this voice setting")]
|
||||
public string AppendedText;
|
||||
}
|
||||
}
|
||||
+11
@@ -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:
|
||||
+40
@@ -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();
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a8c6f2c6a5fdba344b75e8f613c5dc09
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Meta.WitAi.TTS.Events
|
||||
{
|
||||
[Serializable]
|
||||
public class TTSServiceEvents
|
||||
{
|
||||
[Tooltip("Called when a audio clip has been added to the runtime cache")]
|
||||
public TTSClipEvent OnClipCreated = new TTSClipEvent();
|
||||
|
||||
[Tooltip("Called when a audio clip has been removed from the runtime cache")]
|
||||
public TTSClipEvent OnClipUnloaded = new TTSClipEvent();
|
||||
|
||||
/// <summary>
|
||||
/// All events related to web requests
|
||||
/// </summary>
|
||||
public TTSWebRequestEvents WebRequest = new TTSWebRequestEvents();
|
||||
|
||||
/// <summary>
|
||||
/// All events related to streaming from web or disk
|
||||
/// </summary>
|
||||
public TTSStreamEvents Stream = new TTSStreamEvents();
|
||||
|
||||
/// <summary>
|
||||
/// All events related to downloading from the web
|
||||
/// </summary>
|
||||
public TTSDownloadEvents Download = new TTSDownloadEvents();
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a41b87319719e004da4ad59b6a70358d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using Meta.WitAi.Speech;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using Meta.WitAi.TTS.Data;
|
||||
|
||||
namespace Meta.WitAi.TTS.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// A unity event that returns a TTSSpeaker & TTSClipData
|
||||
/// for a specific speaker playback request.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class TTSSpeakerClipEvent : UnityEvent<TTSSpeaker, TTSClipData> { }
|
||||
/// <summary>
|
||||
/// A unity event that returns a TTSSpeaker, TTSClipData & text
|
||||
/// for a specific speaker playback request
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class TTSSpeakerClipMessageEvent : UnityEvent<TTSSpeaker, TTSClipData, string> { }
|
||||
|
||||
/// <summary>
|
||||
/// A collection of events used for speaker tts playback.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class TTSSpeakerClipEvents : VoiceSpeechEvents
|
||||
{
|
||||
[Header("Speaker Lifecycle Events")]
|
||||
[SerializeField] [Tooltip("Initial callback as soon as the audio clip speak request is generated")]
|
||||
private TTSSpeakerClipEvent _onInit = new TTSSpeakerClipEvent();
|
||||
/// <summary>
|
||||
/// Initial callback as soon as the audio clip speak request is generated
|
||||
/// </summary>
|
||||
public TTSSpeakerClipEvent OnInit => _onInit;
|
||||
|
||||
[SerializeField] [Tooltip("Final call for a 'Speak' request that is called following a load failure, load abort, playback cancellation or playback completion")]
|
||||
private TTSSpeakerClipEvent _onComplete = new TTSSpeakerClipEvent();
|
||||
/// <summary>
|
||||
/// Final call for a 'Speak' request that is called following a load failure,
|
||||
/// load abort, playback cancellation or playback completion
|
||||
/// </summary>
|
||||
public TTSSpeakerClipEvent OnComplete => _onComplete;
|
||||
|
||||
[Header("Speaker Loading Events")]
|
||||
[SerializeField] [Tooltip("Called when TTS audio clip load begins")]
|
||||
private TTSSpeakerClipEvent _onLoadBegin = new TTSSpeakerClipEvent();
|
||||
/// <summary>
|
||||
/// Called when TTS audio clip load begins
|
||||
/// </summary>
|
||||
public TTSSpeakerClipEvent OnLoadBegin => _onLoadBegin;
|
||||
|
||||
[SerializeField] [Tooltip("Called when TTS audio clip load is cancelled")]
|
||||
private TTSSpeakerClipEvent _onLoadAbort = new TTSSpeakerClipEvent();
|
||||
/// <summary>
|
||||
/// Called when TTS audio clip load is cancelled
|
||||
/// </summary>
|
||||
public TTSSpeakerClipEvent OnLoadAbort => _onLoadAbort;
|
||||
|
||||
[SerializeField] [Tooltip("Called when TTS audio clip load fails due to a network or load error")]
|
||||
private TTSSpeakerClipMessageEvent _onLoadFailed = new TTSSpeakerClipMessageEvent();
|
||||
/// <summary>
|
||||
/// Called when TTS audio clip load fails due to a network or load error
|
||||
/// </summary>
|
||||
public TTSSpeakerClipMessageEvent OnLoadFailed => _onLoadFailed;
|
||||
|
||||
[SerializeField] [Tooltip("Called when TTS audio clip load successfully")]
|
||||
private TTSSpeakerClipEvent _onLoadSuccess = new TTSSpeakerClipEvent();
|
||||
/// <summary>
|
||||
/// Called when TTS audio clip load successfully
|
||||
/// </summary>
|
||||
public TTSSpeakerClipEvent OnLoadSuccess => _onLoadSuccess;
|
||||
|
||||
[Header("Speaker Playback Events")]
|
||||
[SerializeField] [Tooltip("Called when TTS audio clip playback is ready")]
|
||||
private TTSSpeakerClipEvent _onPlaybackReady = new TTSSpeakerClipEvent();
|
||||
/// <summary>
|
||||
/// Called when TTS audio clip playback is ready
|
||||
/// </summary>
|
||||
public TTSSpeakerClipEvent OnPlaybackReady => _onPlaybackReady;
|
||||
|
||||
[SerializeField] [Tooltip("Called when TTS audio clip playback has begun")]
|
||||
private TTSSpeakerClipEvent _onPlaybackStart = new TTSSpeakerClipEvent();
|
||||
/// <summary>
|
||||
/// Called when TTS audio clip playback has begun
|
||||
/// </summary>
|
||||
public TTSSpeakerClipEvent OnPlaybackStart => _onPlaybackStart;
|
||||
|
||||
[SerializeField] [Tooltip("Called when TTS audio clip playback been cancelled")]
|
||||
private TTSSpeakerClipMessageEvent _onPlaybackCancelled = new TTSSpeakerClipMessageEvent();
|
||||
/// <summary>
|
||||
/// Called when TTS audio clip playback been cancelled
|
||||
/// </summary>
|
||||
public TTSSpeakerClipMessageEvent OnPlaybackCancelled => _onPlaybackCancelled;
|
||||
|
||||
[SerializeField] [Tooltip("Called when TTS audio clip is updated during streamed playback")]
|
||||
private TTSSpeakerClipEvent _onPlaybackClipUpdated = new TTSSpeakerClipEvent();
|
||||
/// <summary>
|
||||
/// Called when TTS audio clip is updated during streamed playback
|
||||
/// </summary>
|
||||
public TTSSpeakerClipEvent OnPlaybackClipUpdated => _onPlaybackClipUpdated;
|
||||
|
||||
[SerializeField] [Tooltip("Called when TTS audio clip playback completed successfully")]
|
||||
private TTSSpeakerClipEvent _onPlaybackComplete = new TTSSpeakerClipEvent();
|
||||
/// <summary>
|
||||
/// Called when TTS audio clip playback completed successfully
|
||||
/// </summary>
|
||||
public TTSSpeakerClipEvent OnPlaybackComplete => _onPlaybackComplete;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2c303e406a42b8b4d9853bfd74e434ba
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using Meta.WitAi.Speech;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using Meta.WitAi.TTS.Data;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace Meta.WitAi.TTS.Utilities
|
||||
{
|
||||
[Serializable]
|
||||
public class TTSSpeakerEvent : UnityEvent<TTSSpeaker, string> { }
|
||||
[Serializable]
|
||||
public class TTSSpeakerClipDataEvent : UnityEvent<TTSClipData> { }
|
||||
[Serializable]
|
||||
public class TTSSpeakerEvents : TTSSpeakerClipEvents
|
||||
{
|
||||
[Header("Queue Events")]
|
||||
[Tooltip("Called when a tts request is added to an empty queue")]
|
||||
[SerializeField] [FormerlySerializedAs("OnPlaybackQueueBegin")]
|
||||
private UnityEvent _onPlaybackQueueBegin = new UnityEvent();
|
||||
public UnityEvent OnPlaybackQueueBegin => _onPlaybackQueueBegin;
|
||||
|
||||
[Tooltip("Called the final request is removed from a queue")]
|
||||
[SerializeField] [FormerlySerializedAs("OnPlaybackQueueComplete")]
|
||||
private UnityEvent _onPlaybackQueueComplete = new UnityEvent();
|
||||
public UnityEvent OnPlaybackQueueComplete => _onPlaybackQueueComplete;
|
||||
|
||||
[Header("Deprecated Events")]
|
||||
[Obsolete("Use 'OnLoadBegin' event")]
|
||||
public TTSSpeakerClipDataEvent OnClipDataQueued;
|
||||
[Obsolete("Use 'OnLoadBegin' event")]
|
||||
public TTSSpeakerEvent OnClipLoadBegin;
|
||||
[Obsolete("Use 'OnLoadBegin' event")]
|
||||
public TTSSpeakerClipDataEvent OnClipDataLoadBegin;
|
||||
[Obsolete("Use 'OnLoadAbort' event")]
|
||||
public TTSSpeakerEvent OnClipLoadAbort;
|
||||
[Obsolete("Use 'OnLoadAbort' event")]
|
||||
public TTSSpeakerClipDataEvent OnClipDataLoadAbort;
|
||||
[Obsolete("Use 'OnLoadFailed' event")]
|
||||
public TTSSpeakerEvent OnClipLoadFailed;
|
||||
[Obsolete("Use 'OnLoadFailed' event")]
|
||||
public TTSSpeakerClipDataEvent OnClipDataLoadFailed;
|
||||
[Obsolete("Use 'OnLoadSuccess' event")]
|
||||
public TTSSpeakerEvent OnClipLoadSuccess;
|
||||
[Obsolete("Use 'OnLoadSuccess' event")]
|
||||
public TTSSpeakerClipDataEvent OnClipDataLoadSuccess;
|
||||
[Obsolete("Use 'OnPlaybackReady' event")]
|
||||
public TTSSpeakerClipDataEvent OnClipDataPlaybackReady;
|
||||
[Obsolete("Use 'OnPlaybackStart' event")]
|
||||
public TTSSpeakerEvent OnStartSpeaking;
|
||||
[Obsolete("Use 'OnPlaybackStart' event")]
|
||||
public TTSSpeakerClipDataEvent OnClipDataPlaybackStart;
|
||||
[Obsolete("Use 'OnPlaybackCancelled' event")]
|
||||
public TTSSpeakerEvent OnCancelledSpeaking;
|
||||
[Obsolete("Use 'OnPlaybackCancelled' event")]
|
||||
public TTSSpeakerClipDataEvent OnClipDataPlaybackCancelled;
|
||||
[Obsolete("Use 'OnPlaybackComplete' event")]
|
||||
public TTSSpeakerEvent OnFinishedSpeaking;
|
||||
[Obsolete("Use 'OnPlaybackComplete' event")]
|
||||
public TTSSpeakerClipDataEvent OnClipDataPlaybackFinished;
|
||||
}
|
||||
}
|
||||
+11
@@ -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();
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cc1209a088b657247b1f0c645ae7ee93
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using Meta.WitAi.TTS.Data;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace Meta.WitAi.TTS.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Events related to web requests
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class TTSWebRequestEvents
|
||||
{
|
||||
[Tooltip("Called when a web request begins transmission")]
|
||||
public TTSClipEvent OnRequestBegin = new TTSClipEvent();
|
||||
|
||||
[Tooltip("Called when a web request is cancelled")]
|
||||
public TTSClipEvent OnRequestCancel = new TTSClipEvent();
|
||||
|
||||
[Tooltip("Called when a web request fails")]
|
||||
public TTSClipErrorEvent OnRequestError = new TTSClipErrorEvent();
|
||||
|
||||
[Tooltip("Called when a web request receives first data")]
|
||||
public TTSClipEvent OnRequestFirstResponse = new TTSClipEvent();
|
||||
|
||||
[Tooltip("Called when a web request is ready for playback")]
|
||||
public TTSClipEvent OnRequestReady = new TTSClipEvent();
|
||||
|
||||
[Tooltip("Called when a web request is completed via success, cancellation or failure")]
|
||||
public TTSClipEvent OnRequestComplete = new TTSClipEvent();
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 59d937d620dd3ea45abde93fe056c240
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Meta.WitAi.TTS",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"GUID:1c28d8b71ced07540b7c271537363cc6",
|
||||
"GUID:4504b1a6e0fdcc3498c30b266e4a63bf"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
+7
@@ -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:
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Meta.WitAi.TTS.Data;
|
||||
using Meta.WitAi.TTS.Events;
|
||||
using Meta.WitAi.TTS.Interfaces;
|
||||
using Meta.WitAi.Utilities;
|
||||
using Meta.WitAi.Requests;
|
||||
using Meta.Voice.Audio;
|
||||
|
||||
namespace Meta.WitAi.TTS.Integrations
|
||||
{
|
||||
public class TTSDiskCache : MonoBehaviour, ITTSDiskCacheHandler
|
||||
{
|
||||
[Header("Disk Cache Settings")]
|
||||
/// <summary>
|
||||
/// The relative path from the DiskCacheLocation in TTSDiskCacheSettings
|
||||
/// </summary>
|
||||
[SerializeField] private string _diskPath = "TTS/";
|
||||
public string DiskPath => _diskPath;
|
||||
|
||||
/// <summary>
|
||||
/// The cache default settings
|
||||
/// </summary>
|
||||
[SerializeField] private TTSDiskCacheSettings _defaultSettings = new TTSDiskCacheSettings();
|
||||
public TTSDiskCacheSettings DiskCacheDefaultSettings => _defaultSettings;
|
||||
|
||||
/// <summary>
|
||||
/// The cache streaming events
|
||||
/// </summary>
|
||||
[SerializeField] private TTSStreamEvents _events = new TTSStreamEvents();
|
||||
public TTSStreamEvents DiskStreamEvents
|
||||
{
|
||||
get => _events;
|
||||
set { _events = value; }
|
||||
}
|
||||
|
||||
// All currently performing stream requests
|
||||
private Dictionary<string, VRequest> _streamRequests = new Dictionary<string, VRequest>();
|
||||
|
||||
// Cancel all requests
|
||||
protected virtual void OnDestroy()
|
||||
{
|
||||
Dictionary<string, VRequest> requests = _streamRequests;
|
||||
_streamRequests.Clear();
|
||||
foreach (var request in requests.Values)
|
||||
{
|
||||
request.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds full cache path
|
||||
/// </summary>
|
||||
/// <param name="clipData"></param>
|
||||
/// <returns></returns>
|
||||
public string GetDiskCachePath(TTSClipData clipData)
|
||||
{
|
||||
// Disabled
|
||||
if (!ShouldCacheToDisk(clipData))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Get directory path
|
||||
TTSDiskCacheLocation location = clipData.diskCacheSettings.DiskCacheLocation;
|
||||
string directory = string.Empty;
|
||||
switch (location)
|
||||
{
|
||||
case TTSDiskCacheLocation.Persistent:
|
||||
directory = Application.persistentDataPath;
|
||||
break;
|
||||
case TTSDiskCacheLocation.Temporary:
|
||||
directory = Application.temporaryCachePath;
|
||||
break;
|
||||
case TTSDiskCacheLocation.Preload:
|
||||
directory = Application.streamingAssetsPath;
|
||||
break;
|
||||
}
|
||||
if (string.IsNullOrEmpty(directory))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Add tts cache path & clean
|
||||
directory = Path.Combine(directory, DiskPath);
|
||||
|
||||
// Generate tts directory if possible
|
||||
if (location != TTSDiskCacheLocation.Preload || !Application.isPlaying)
|
||||
{
|
||||
if (!IOUtility.CreateDirectory(directory, true))
|
||||
{
|
||||
VLog.E($"Failed to create tts directory\nPath: {directory}\nLocation: {location}");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
// Return clip path
|
||||
return Path.Combine(directory, clipData.clipID + "." + WitTTSVRequest.GetAudioExtension(clipData.audioType));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine if should cache to disk or not
|
||||
/// </summary>
|
||||
/// <param name="clipData">All clip data</param>
|
||||
/// <returns>Returns true if should cache to disk</returns>
|
||||
public bool ShouldCacheToDisk(TTSClipData clipData)
|
||||
{
|
||||
return clipData != null && clipData.diskCacheSettings.DiskCacheLocation != TTSDiskCacheLocation.Stream && !string.IsNullOrEmpty(clipData.clipID);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if file is cached on disk
|
||||
/// </summary>
|
||||
/// <param name="clipData">Request data</param>
|
||||
/// <returns>True if file is on disk</returns>
|
||||
public void CheckCachedToDisk(TTSClipData clipData, Action<TTSClipData, bool> onCheckComplete)
|
||||
{
|
||||
// Get path
|
||||
string cachePath = GetDiskCachePath(clipData);
|
||||
if (string.IsNullOrEmpty(cachePath))
|
||||
{
|
||||
onCheckComplete?.Invoke(clipData, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
VRequest request = new VRequest();
|
||||
bool canPerform = request.RequestFileExists(cachePath, (success, error) =>
|
||||
{
|
||||
// Remove
|
||||
if (_streamRequests.ContainsKey(clipData.clipID))
|
||||
{
|
||||
_streamRequests.Remove(clipData.clipID);
|
||||
}
|
||||
// Complete
|
||||
onCheckComplete(clipData, success);
|
||||
});
|
||||
if (canPerform)
|
||||
{
|
||||
_streamRequests[clipData.clipID] = request;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs async load request
|
||||
/// </summary>
|
||||
public void StreamFromDiskCache(TTSClipData clipData)
|
||||
{
|
||||
// Invoke begin
|
||||
DiskStreamEvents?.OnStreamBegin?.Invoke(clipData);
|
||||
|
||||
// Get file path
|
||||
string filePath = GetDiskCachePath(clipData);
|
||||
|
||||
// Load clip async
|
||||
VRequest request = new VRequest((progress) => clipData.loadProgress = progress);
|
||||
bool canPerform = request.RequestAudioStream(clipData.clipStream, new Uri(request.CleanUrl(filePath)),
|
||||
(clipStream, error) =>
|
||||
{
|
||||
clipData.clipStream = clipStream;
|
||||
OnStreamComplete(clipData, error);
|
||||
}, clipData.audioType, clipData.diskCacheSettings.StreamFromDisk);
|
||||
if (canPerform)
|
||||
{
|
||||
_streamRequests[clipData.clipID] = request;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Cancels unity request
|
||||
/// </summary>
|
||||
public void CancelDiskCacheStream(TTSClipData clipData)
|
||||
{
|
||||
// Ignore if not currently streaming
|
||||
if (!_streamRequests.ContainsKey(clipData.clipID))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get request
|
||||
VRequest request = _streamRequests[clipData.clipID];
|
||||
_streamRequests.Remove(clipData.clipID);
|
||||
|
||||
// Cancel immediately
|
||||
request?.Cancel();
|
||||
request = null;
|
||||
|
||||
// Call cancel
|
||||
DiskStreamEvents?.OnStreamCancel?.Invoke(clipData);
|
||||
}
|
||||
// On stream completion
|
||||
protected virtual void OnStreamComplete(TTSClipData clipData, string error)
|
||||
{
|
||||
// Ignore if not currently streaming
|
||||
if (!_streamRequests.ContainsKey(clipData.clipID))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from list
|
||||
_streamRequests.Remove(clipData.clipID);
|
||||
|
||||
// Error
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
DiskStreamEvents?.OnStreamError?.Invoke(clipData, error);
|
||||
}
|
||||
// Success
|
||||
else
|
||||
{
|
||||
DiskStreamEvents?.OnStreamReady?.Invoke(clipData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b0ffdd015bcb8ea41bb96f19a723bf7d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
using Meta.WitAi.TTS.Data;
|
||||
using Meta.WitAi.TTS.Interfaces;
|
||||
using Meta.WitAi.TTS.Events;
|
||||
|
||||
namespace Meta.WitAi.TTS.Integrations
|
||||
{
|
||||
// A simple LRU Cache
|
||||
public class TTSRuntimeCache : MonoBehaviour, ITTSRuntimeCacheHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not to unload clip data after the clip capacity is hit
|
||||
/// </summary>
|
||||
[Header("Runtime Cache Settings")]
|
||||
[Tooltip("Whether or not to unload clip data after the clip capacity is hit")]
|
||||
[FormerlySerializedAs("_clipLimit")]
|
||||
public bool ClipLimit = true;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum clips allowed in the runtime cache
|
||||
/// </summary>
|
||||
[Tooltip("The maximum clips allowed in the runtime cache")]
|
||||
[FormerlySerializedAs("_clipCapacity")]
|
||||
[Min(1)] public int ClipCapacity = 20;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to unload clip data after the ram capacity is hit
|
||||
/// </summary>
|
||||
[Tooltip("Whether or not to unload clip data after the ram capacity is hit")]
|
||||
[FormerlySerializedAs("_ramLimit")]
|
||||
public bool RamLimit = true;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum amount of RAM allowed in the runtime cache. In KBs
|
||||
/// </summary>
|
||||
[Tooltip("The maximum amount of RAM allowed in the runtime cache. In KBs")]
|
||||
[FormerlySerializedAs("_ramCapacity")]
|
||||
[Min(1)] public int RamCapacity = 32768;
|
||||
|
||||
/// <summary>
|
||||
/// On clip added callback
|
||||
/// </summary>
|
||||
public TTSClipEvent OnClipAdded { get; set; } = new TTSClipEvent();
|
||||
/// <summary>
|
||||
/// On clip removed callback
|
||||
/// </summary>
|
||||
public TTSClipEvent OnClipRemoved { get; set; } = new TTSClipEvent();
|
||||
|
||||
// Clips & their ids
|
||||
private Dictionary<string, TTSClipData> _clips = new Dictionary<string, TTSClipData>();
|
||||
private List<string> _clipOrder = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Simple getter for all clips
|
||||
/// </summary>
|
||||
public TTSClipData[] GetClips() => _clips.Values.ToArray();
|
||||
|
||||
// Remove all
|
||||
protected virtual void OnDestroy()
|
||||
{
|
||||
_clips.Clear();
|
||||
_clipOrder.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Getter for a clip that also moves clip to the back of the queue
|
||||
/// </summary>
|
||||
public TTSClipData GetClip(string clipID)
|
||||
{
|
||||
// Id not found
|
||||
if (!_clips.ContainsKey(clipID))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sort to end
|
||||
int clipIndex = _clipOrder.IndexOf(clipID);
|
||||
_clipOrder.RemoveAt(clipIndex);
|
||||
_clipOrder.Add(clipID);
|
||||
|
||||
// Return clip
|
||||
return _clips[clipID];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add clip to cache and ensure it is most recently referenced
|
||||
/// </summary>
|
||||
/// <param name="clipData"></param>
|
||||
public bool AddClip(TTSClipData clipData)
|
||||
{
|
||||
// Do not add null
|
||||
if (clipData == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// Remove from order
|
||||
bool wasAdded = true;
|
||||
int clipIndex = _clipOrder.IndexOf(clipData.clipID);
|
||||
if (clipIndex != -1)
|
||||
{
|
||||
wasAdded = false;
|
||||
_clipOrder.RemoveAt(clipIndex);
|
||||
}
|
||||
|
||||
// Add clip
|
||||
_clips[clipData.clipID] = clipData;
|
||||
// Add to end of order
|
||||
_clipOrder.Add(clipData.clipID);
|
||||
|
||||
// Evict least recently used clips
|
||||
while (IsCacheFull() && _clipOrder.Count > 0)
|
||||
{
|
||||
// Remove clip
|
||||
RemoveClip(_clipOrder[0]);
|
||||
}
|
||||
|
||||
// Call add delegate even if removed
|
||||
if (wasAdded && _clips.Keys.Count > 0)
|
||||
{
|
||||
OnClipAdded?.Invoke(clipData);
|
||||
}
|
||||
|
||||
// True if successfully added
|
||||
return _clips.Keys.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove clip from cache immediately
|
||||
/// </summary>
|
||||
/// <param name="clipID"></param>
|
||||
public void RemoveClip(string clipID)
|
||||
{
|
||||
// Id not found
|
||||
if (!_clips.ContainsKey(clipID))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from dictionary
|
||||
TTSClipData clipData = _clips[clipID];
|
||||
_clips.Remove(clipID);
|
||||
|
||||
// Remove from order list
|
||||
int clipIndex = _clipOrder.IndexOf(clipID);
|
||||
_clipOrder.RemoveAt(clipIndex);
|
||||
|
||||
// Call remove delegate
|
||||
OnClipRemoved?.Invoke(clipData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if cache is full
|
||||
/// </summary>
|
||||
protected bool IsCacheFull()
|
||||
{
|
||||
// Capacity full
|
||||
if (ClipLimit && _clipOrder.Count > ClipCapacity)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// Ram full
|
||||
if (RamLimit && GetCacheDiskSize() > RamCapacity)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// Free
|
||||
return false;
|
||||
}
|
||||
/// <summary>
|
||||
/// Get RAM size of cache in KBs
|
||||
/// </summary>
|
||||
/// <returns>Returns size in KBs rounded up</returns>
|
||||
public int GetCacheDiskSize()
|
||||
{
|
||||
long total = 0;
|
||||
foreach (var key in _clips.Keys)
|
||||
{
|
||||
total += GetClipBytes(_clips[key].clipStream.Channels, _clips[key].clipStream.TotalSamples);
|
||||
}
|
||||
return (int)(total / (long)1024) + 1;
|
||||
}
|
||||
// Return bytes occupied by clip
|
||||
public static long GetClipBytes(AudioClip clip)
|
||||
{
|
||||
if (clip != null)
|
||||
{
|
||||
return GetClipBytes(clip.channels, clip.samples);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
// Return bytes occupied by clip
|
||||
public static long GetClipBytes(int channels, int samples)
|
||||
{
|
||||
return channels * samples * 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1d60dcb6d02034b4b96284db469db5e3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,498 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Meta.Voice.Audio;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
using Meta.WitAi.Interfaces;
|
||||
using Meta.WitAi.Data.Configuration;
|
||||
using Meta.WitAi.Json;
|
||||
using Meta.WitAi.TTS.Data;
|
||||
using Meta.WitAi.TTS.Events;
|
||||
using Meta.WitAi.TTS.Interfaces;
|
||||
using Meta.WitAi.Requests;
|
||||
|
||||
namespace Meta.WitAi.TTS.Integrations
|
||||
{
|
||||
[Serializable]
|
||||
public class TTSWitVoiceSettings : TTSVoiceSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Default voice name used if no voice is provided
|
||||
/// </summary>
|
||||
public const string DEFAULT_VOICE = "Charlie";
|
||||
/// <summary>
|
||||
/// Default style used if no style is provided
|
||||
/// </summary>
|
||||
public const string DEFAULT_STYLE = "default";
|
||||
|
||||
/// <summary>
|
||||
/// Unique voice name
|
||||
/// </summary>
|
||||
public string voice = DEFAULT_VOICE;
|
||||
/// <summary>
|
||||
/// Voice style (ex. formal, fast)
|
||||
/// </summary>
|
||||
public string style = DEFAULT_STYLE;
|
||||
/// <summary>
|
||||
/// Text-to-speech speed percentage
|
||||
/// </summary>
|
||||
[Range(50, 200)]
|
||||
public int speed = 100;
|
||||
/// <summary>
|
||||
/// Text-to-speech audio pitch percentage
|
||||
/// </summary>
|
||||
[Range(25, 200)]
|
||||
public int pitch = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if request can be decoded for TTS data
|
||||
/// Example Data:
|
||||
/// {
|
||||
/// "q": "Text to be spoken"
|
||||
/// "voice": "Charlie
|
||||
/// }
|
||||
/// </summary>
|
||||
/// <param name="responseNode">The deserialized json data class</param>
|
||||
/// <returns>True if request can be decoded</returns>
|
||||
public static bool CanDecode(WitResponseNode responseNode)
|
||||
{
|
||||
return responseNode != null && responseNode.AsObject.HasChild(WitConstants.ENDPOINT_TTS_PARAM) && responseNode.AsObject.HasChild("voice");
|
||||
}
|
||||
}
|
||||
[Serializable]
|
||||
public struct TTSWitRequestSettings
|
||||
{
|
||||
public WitConfiguration configuration;
|
||||
public TTSWitAudioType audioType;
|
||||
public bool audioStream;
|
||||
}
|
||||
|
||||
public class TTSWit : TTSService, ITTSVoiceProvider, ITTSWebHandler, IWitConfigurationProvider
|
||||
{
|
||||
#region TTSService
|
||||
// Voice provider
|
||||
public override ITTSVoiceProvider VoiceProvider => this;
|
||||
// Request handler
|
||||
public override ITTSWebHandler WebHandler => this;
|
||||
// Runtime cache handler
|
||||
public override ITTSRuntimeCacheHandler RuntimeCacheHandler
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_runtimeCache == null)
|
||||
{
|
||||
_runtimeCache = gameObject.GetComponent<ITTSRuntimeCacheHandler>();
|
||||
}
|
||||
return _runtimeCache;
|
||||
}
|
||||
}
|
||||
private ITTSRuntimeCacheHandler _runtimeCache;
|
||||
// Cache handler
|
||||
public override ITTSDiskCacheHandler DiskCacheHandler
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_diskCache == null)
|
||||
{
|
||||
_diskCache = gameObject.GetComponent<ITTSDiskCacheHandler>();
|
||||
}
|
||||
return _diskCache;
|
||||
}
|
||||
}
|
||||
private ITTSDiskCacheHandler _diskCache;
|
||||
|
||||
// Web request events
|
||||
public TTSWebRequestEvents WebRequestEvents => Events.WebRequest;
|
||||
// Configuration provider
|
||||
public WitConfiguration Configuration => RequestSettings.configuration;
|
||||
|
||||
// Use wit tts vrequest type
|
||||
protected override AudioType GetAudioType()
|
||||
{
|
||||
return WitTTSVRequest.GetAudioType(RequestSettings.audioType);
|
||||
}
|
||||
// Get tts request prior to transmission
|
||||
private WitTTSVRequest GetTtsRequest(TTSClipData clipData)
|
||||
{
|
||||
// Apply audio type
|
||||
clipData.audioType = GetAudioType();
|
||||
clipData.queryStream = RequestSettings.audioStream;
|
||||
|
||||
// Return request
|
||||
return new WitTTSVRequest(RequestSettings.configuration, clipData.queryRequestId,
|
||||
clipData.textToSpeak, clipData.queryParameters,
|
||||
RequestSettings.audioType, clipData.queryStream,
|
||||
(progress) => OnRequestProgressUpdated(clipData, progress),
|
||||
() => OnRequestFirstResponse(clipData));
|
||||
}
|
||||
|
||||
// Download progress callbacks
|
||||
private void OnRequestProgressUpdated(TTSClipData clipData, float newProgress)
|
||||
{
|
||||
if (clipData != null)
|
||||
{
|
||||
clipData.loadProgress = newProgress;
|
||||
}
|
||||
}
|
||||
|
||||
// Progress callbacks
|
||||
private void OnRequestFirstResponse(TTSClipData clipData)
|
||||
{
|
||||
if (clipData != null)
|
||||
{
|
||||
WebRequestEvents?.OnRequestFirstResponse?.Invoke(clipData);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region ITTSWebHandler Streams
|
||||
// Request settings
|
||||
[Header("Web Request Settings")]
|
||||
[FormerlySerializedAs("_settings")]
|
||||
public TTSWitRequestSettings RequestSettings = new TTSWitRequestSettings
|
||||
{
|
||||
audioType = TTSWitAudioType.PCM,
|
||||
audioStream = true,
|
||||
};
|
||||
|
||||
// Use settings web stream events
|
||||
public TTSStreamEvents WebStreamEvents { get; set; } = new TTSStreamEvents();
|
||||
|
||||
// Requests bly clip id
|
||||
private Dictionary<string, VRequest> _webStreams = new Dictionary<string, VRequest>();
|
||||
|
||||
// Whether TTSService is valid
|
||||
public override string GetInvalidError()
|
||||
{
|
||||
string invalidError = base.GetInvalidError();
|
||||
if (!string.IsNullOrEmpty(invalidError))
|
||||
{
|
||||
return invalidError;
|
||||
}
|
||||
if (RequestSettings.configuration == null)
|
||||
{
|
||||
return "No WitConfiguration Set";
|
||||
}
|
||||
if (string.IsNullOrEmpty(RequestSettings.configuration.GetClientAccessToken()))
|
||||
{
|
||||
return "No WitConfiguration Client Token";
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
// Ensures text can be sent to wit web service
|
||||
public string IsTextValid(string textToSpeak) => string.IsNullOrEmpty(textToSpeak) ? WitConstants.ENDPOINT_TTS_NO_TEXT : string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Method for performing a web load request
|
||||
/// </summary>
|
||||
/// <param name="clipData">Clip request data</param>
|
||||
/// <param name="onStreamSetupComplete">Stream setup complete: returns clip and error if applicable</param>
|
||||
public void RequestStreamFromWeb(TTSClipData clipData)
|
||||
{
|
||||
// Stream begin
|
||||
WebStreamEvents?.OnStreamBegin?.Invoke(clipData);
|
||||
|
||||
// Check if valid
|
||||
string validError = IsRequestValid(clipData, RequestSettings.configuration);
|
||||
if (!string.IsNullOrEmpty(validError))
|
||||
{
|
||||
WebStreamEvents?.OnStreamError?.Invoke(clipData, validError);
|
||||
return;
|
||||
}
|
||||
// Ignore if already performing
|
||||
if (_webStreams.ContainsKey(clipData.clipID))
|
||||
{
|
||||
CancelWebStream(clipData);
|
||||
}
|
||||
|
||||
// Begin request
|
||||
WebRequestEvents?.OnRequestBegin?.Invoke(clipData);
|
||||
|
||||
// Whether to stream
|
||||
bool stream = Application.isPlaying && RequestSettings.audioStream;
|
||||
|
||||
// Request tts
|
||||
WitTTSVRequest request = GetTtsRequest(clipData);
|
||||
request.RequestStream(clipData.clipStream,
|
||||
(clipStream, error) =>
|
||||
{
|
||||
// Apply
|
||||
_webStreams.Remove(clipData.clipID);
|
||||
|
||||
// Set new clip stream
|
||||
clipData.clipStream = clipStream;
|
||||
|
||||
// Unloaded
|
||||
if (clipData.loadState == TTSClipLoadState.Unloaded)
|
||||
{
|
||||
error = WitConstants.CANCEL_ERROR;
|
||||
clipStream?.Unload();
|
||||
}
|
||||
|
||||
// Error
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
if (string.Equals(error, WitConstants.CANCEL_ERROR, StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
WebStreamEvents?.OnStreamCancel?.Invoke(clipData);
|
||||
WebRequestEvents?.OnRequestCancel?.Invoke(clipData);
|
||||
}
|
||||
else
|
||||
{
|
||||
WebStreamEvents?.OnStreamError?.Invoke(clipData, error);
|
||||
WebRequestEvents?.OnRequestError?.Invoke(clipData, error);
|
||||
}
|
||||
}
|
||||
// Success
|
||||
else
|
||||
{
|
||||
WebStreamEvents?.OnStreamReady?.Invoke(clipData);
|
||||
WebRequestEvents?.OnRequestReady?.Invoke(clipData);
|
||||
if (!RequestSettings.audioStream || !WitTTSVRequest.CanStreamAudio(RequestSettings.audioType))
|
||||
{
|
||||
WebStreamEvents?.OnStreamComplete?.Invoke(clipData);
|
||||
WebRequestEvents?.OnRequestComplete?.Invoke(clipData);
|
||||
}
|
||||
}
|
||||
});
|
||||
_webStreams[clipData.clipID] = request;
|
||||
}
|
||||
/// <summary>
|
||||
/// Cancel web stream
|
||||
/// </summary>
|
||||
/// <param name="clipID">Unique clip id</param>
|
||||
public bool CancelWebStream(TTSClipData clipData)
|
||||
{
|
||||
// Ignore without
|
||||
if (!_webStreams.ContainsKey(clipData.clipID))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get request
|
||||
VRequest request = _webStreams[clipData.clipID];
|
||||
_webStreams.Remove(clipData.clipID);
|
||||
|
||||
// Destroy immediately
|
||||
request?.Cancel();
|
||||
request = null;
|
||||
|
||||
// Call delegate
|
||||
WebStreamEvents?.OnStreamCancel?.Invoke(clipData);
|
||||
WebRequestEvents?.OnRequestCancel?.Invoke(clipData);
|
||||
|
||||
// Success
|
||||
return true;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region ITTSWebHandler Downloads
|
||||
// Use settings web download events
|
||||
public TTSDownloadEvents WebDownloadEvents { get; set; } = new TTSDownloadEvents();
|
||||
|
||||
// Requests by clip id
|
||||
private Dictionary<string, WitVRequest> _webDownloads = new Dictionary<string, WitVRequest>();
|
||||
|
||||
/// <summary>
|
||||
/// Method for performing a web load request
|
||||
/// </summary>
|
||||
/// <param name="clipData">Clip request data</param>
|
||||
/// <param name="downloadPath">Path to save clip</param>
|
||||
public void RequestDownloadFromWeb(TTSClipData clipData, string downloadPath)
|
||||
{
|
||||
// Begin
|
||||
WebDownloadEvents?.OnDownloadBegin?.Invoke(clipData, downloadPath);
|
||||
|
||||
// Ensure valid
|
||||
string validError = IsRequestValid(clipData, RequestSettings.configuration);
|
||||
if (!string.IsNullOrEmpty(validError))
|
||||
{
|
||||
WebDownloadEvents?.OnDownloadError?.Invoke(clipData, downloadPath, validError);
|
||||
return;
|
||||
}
|
||||
// Abort if already performing
|
||||
if (_webDownloads.ContainsKey(clipData.clipID))
|
||||
{
|
||||
CancelWebDownload(clipData, downloadPath);
|
||||
}
|
||||
|
||||
// Begin request
|
||||
WebRequestEvents?.OnRequestBegin?.Invoke(clipData);
|
||||
|
||||
// Request tts
|
||||
WitTTSVRequest request = GetTtsRequest(clipData);
|
||||
request.RequestDownload(downloadPath,
|
||||
(success, error) =>
|
||||
{
|
||||
_webDownloads.Remove(clipData.clipID);
|
||||
if (string.IsNullOrEmpty(error))
|
||||
{
|
||||
WebDownloadEvents?.OnDownloadSuccess?.Invoke(clipData, downloadPath);
|
||||
WebRequestEvents?.OnRequestReady?.Invoke(clipData);
|
||||
}
|
||||
else
|
||||
{
|
||||
WebDownloadEvents?.OnDownloadError?.Invoke(clipData, downloadPath, error);
|
||||
WebRequestEvents?.OnRequestError?.Invoke(clipData, error);
|
||||
}
|
||||
WebRequestEvents?.OnRequestComplete?.Invoke(clipData);
|
||||
});
|
||||
_webDownloads[clipData.clipID] = request;
|
||||
}
|
||||
/// <summary>
|
||||
/// Method for cancelling a running load request
|
||||
/// </summary>
|
||||
/// <param name="clipData">Clip request data</param>
|
||||
public bool CancelWebDownload(TTSClipData clipData, string downloadPath)
|
||||
{
|
||||
// Ignore if not performing
|
||||
if (!_webDownloads.ContainsKey(clipData.clipID))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get request
|
||||
WitVRequest request = _webDownloads[clipData.clipID];
|
||||
_webDownloads.Remove(clipData.clipID);
|
||||
|
||||
// Destroy immediately
|
||||
request?.Cancel();
|
||||
request = null;
|
||||
|
||||
// Download cancelled
|
||||
WebDownloadEvents?.OnDownloadCancel?.Invoke(clipData, downloadPath);
|
||||
WebRequestEvents?.OnRequestCancel?.Invoke(clipData);
|
||||
|
||||
// Success
|
||||
return true;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region ITTSVoiceProvider
|
||||
// Preset voice settings
|
||||
[Header("Voice Settings")]
|
||||
#if UNITY_2021_3_2 || UNITY_2021_3_3 || UNITY_2021_3_4 || UNITY_2021_3_5
|
||||
[NonReorderable]
|
||||
#endif
|
||||
[SerializeField] private TTSWitVoiceSettings[] _presetVoiceSettings;
|
||||
public TTSWitVoiceSettings[] PresetWitVoiceSettings => _presetVoiceSettings;
|
||||
|
||||
// Cast to voice array
|
||||
public TTSVoiceSettings[] PresetVoiceSettings
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_presetVoiceSettings == null)
|
||||
{
|
||||
_presetVoiceSettings = new TTSWitVoiceSettings[] { };
|
||||
}
|
||||
return _presetVoiceSettings;
|
||||
}
|
||||
}
|
||||
// Default voice setting uses the first voice in the list
|
||||
public TTSVoiceSettings VoiceDefaultSettings => PresetVoiceSettings == null || PresetVoiceSettings.Length == 0 ? null : PresetVoiceSettings[0];
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// Apply settings
|
||||
public void SetVoiceSettings(TTSWitVoiceSettings[] newVoiceSettings)
|
||||
{
|
||||
_presetVoiceSettings = newVoiceSettings;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Convert voice settings into dictionary to be used with web requests
|
||||
private const string VOICE_KEY = "voice";
|
||||
private const string STYLE_KEY = "style";
|
||||
public Dictionary<string, string> EncodeVoiceSettings(TTSVoiceSettings voiceSettings)
|
||||
{
|
||||
Dictionary<string, string> parameters = new Dictionary<string, string>();
|
||||
if (voiceSettings != null)
|
||||
{
|
||||
foreach (FieldInfo field in GetVoiceSettingsFields(voiceSettings))
|
||||
{
|
||||
// Ensure field value exists
|
||||
object fieldVal = field.GetValue(voiceSettings);
|
||||
if (fieldVal != null)
|
||||
{
|
||||
// Clamp in between range
|
||||
RangeAttribute range = field.GetCustomAttribute<RangeAttribute>();
|
||||
if (range != null && field.FieldType == typeof(int))
|
||||
{
|
||||
int oldFloat = (int) fieldVal;
|
||||
int newFloat = Mathf.Clamp(oldFloat, (int)range.min, (int)range.max);
|
||||
if (oldFloat != newFloat)
|
||||
{
|
||||
fieldVal = newFloat;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply
|
||||
parameters[field.Name] = fieldVal.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
// Set default if no voice is provided
|
||||
if (!parameters.ContainsKey(VOICE_KEY) || string.IsNullOrEmpty(parameters[VOICE_KEY]))
|
||||
{
|
||||
parameters[VOICE_KEY] = TTSWitVoiceSettings.DEFAULT_VOICE;
|
||||
}
|
||||
// Set default if no style is given
|
||||
if (!parameters.ContainsKey(STYLE_KEY) || string.IsNullOrEmpty(parameters[STYLE_KEY]))
|
||||
{
|
||||
parameters[STYLE_KEY] = TTSWitVoiceSettings.DEFAULT_STYLE;
|
||||
}
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
// Obtain all fields for a specific voice settings
|
||||
private static readonly Dictionary<Type, FieldInfo[]> _settingsFields = new Dictionary<Type, FieldInfo[]>();
|
||||
private static FieldInfo[] GetVoiceSettingsFields(TTSVoiceSettings voiceSettings)
|
||||
{
|
||||
// Return fields if already found
|
||||
Type settingsType = voiceSettings.GetType();
|
||||
if (_settingsFields.ContainsKey(settingsType))
|
||||
{
|
||||
return _settingsFields[settingsType];
|
||||
}
|
||||
|
||||
// Get public/instance fields
|
||||
FieldInfo[] fields = settingsType.GetFields(BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
// Remove fields from TTSVoiceSettings
|
||||
Type baseType = typeof(TTSVoiceSettings);
|
||||
fields = fields.ToList().FindAll((field) => field.DeclaringType != baseType).ToArray();
|
||||
|
||||
// Apply & return
|
||||
_settingsFields[settingsType] = fields;
|
||||
return fields;
|
||||
}
|
||||
// Returns an error if request is not valid
|
||||
private string IsRequestValid(TTSClipData clipData, WitConfiguration configuration)
|
||||
{
|
||||
// Invalid tts
|
||||
string invalidError = GetInvalidError();
|
||||
if (!string.IsNullOrEmpty(invalidError))
|
||||
{
|
||||
return invalidError;
|
||||
}
|
||||
// Invalid clip
|
||||
if (clipData == null)
|
||||
{
|
||||
return "No clip data provided";
|
||||
}
|
||||
// Success
|
||||
return string.Empty;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+11
@@ -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:
|
||||
+60
@@ -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);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4a1c9ff5df097b741b2059f3e92081c2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+45
@@ -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);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 476c71d5ecb266a42960ce92aae00508
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+34
@@ -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);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e7bd457b1d9cef444b011a63b950a90d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using Meta.WitAi.TTS.Data;
|
||||
using Meta.WitAi.TTS.Events;
|
||||
|
||||
namespace Meta.WitAi.TTS.Interfaces
|
||||
{
|
||||
public interface ITTSWebHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Any web request performs this event
|
||||
/// </summary>
|
||||
TTSWebRequestEvents WebRequestEvents { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Streaming events
|
||||
/// </summary>
|
||||
TTSStreamEvents WebStreamEvents { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Method for determining if text to speak is valid
|
||||
/// </summary>
|
||||
/// <param name="textToSpeak">Text to be spoken by TTS</param>
|
||||
/// <returns>Invalid error</returns>
|
||||
string IsTextValid(string textToSpeak);
|
||||
|
||||
/// <summary>
|
||||
/// Method for performing a web load request
|
||||
/// </summary>
|
||||
/// <param name="clipData">Clip request data</param>
|
||||
void RequestStreamFromWeb(TTSClipData clipData);
|
||||
|
||||
/// <summary>
|
||||
/// Cancel web stream
|
||||
/// </summary>
|
||||
/// <param name="clipID">Clip unique identifier</param>
|
||||
bool CancelWebStream(TTSClipData clipData);
|
||||
|
||||
/// <summary>
|
||||
/// Download events
|
||||
/// </summary>
|
||||
TTSDownloadEvents WebDownloadEvents { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Method for performing a web load request
|
||||
/// </summary>
|
||||
/// <param name="clipData">Clip request data</param>
|
||||
/// <param name="downloadPath">Path to save clip</param>
|
||||
void RequestDownloadFromWeb(TTSClipData clipData, string downloadPath);
|
||||
|
||||
/// <summary>
|
||||
/// Cancel web download
|
||||
/// </summary>
|
||||
/// <param name="clipID">Clip unique identifier</param>
|
||||
/// <param name="downloadPath">Path to save clip</param>
|
||||
bool CancelWebDownload(TTSClipData clipData, string downloadPath);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 210f082ad442e48479f3f7ee7eaeafed
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+34
@@ -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);
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b39a041597ab4bd9b33e797f74fb2125
|
||||
timeCreated: 1671215442
|
||||
@@ -0,0 +1,969 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Text;
|
||||
using System.Security.Cryptography;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Meta.Voice.Audio;
|
||||
using Meta.WitAi.Requests;
|
||||
using UnityEngine;
|
||||
using Meta.WitAi.TTS.Data;
|
||||
using Meta.WitAi.TTS.Events;
|
||||
using Meta.WitAi.TTS.Interfaces;
|
||||
|
||||
namespace Meta.WitAi.TTS
|
||||
{
|
||||
public abstract class TTSService : MonoBehaviour
|
||||
{
|
||||
#region SETUP
|
||||
// Accessor
|
||||
public static TTSService Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
// Get all services
|
||||
TTSService[] services = Resources.FindObjectsOfTypeAll<TTSService>();
|
||||
if (services != null)
|
||||
{
|
||||
// Set as first instance that isn't a prefab
|
||||
_instance = Array.Find(services, (o) => o.gameObject.scene.rootCount != 0);
|
||||
}
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
private static TTSService _instance;
|
||||
|
||||
/// <summary>
|
||||
/// Audio system to be used for streaming & playback
|
||||
/// </summary>
|
||||
public IAudioSystem AudioSystem
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_audioSystem == null && Application.isPlaying)
|
||||
{
|
||||
// Search on TTS Service
|
||||
_audioSystem = gameObject.GetComponent<IAudioSystem>();
|
||||
if (_audioSystem == null)
|
||||
{
|
||||
// Add default unity audio system if not found
|
||||
_audioSystem = gameObject.AddComponent<UnityAudioSystem>();
|
||||
}
|
||||
}
|
||||
return _audioSystem;
|
||||
}
|
||||
set => _audioSystem = value;
|
||||
}
|
||||
private IAudioSystem _audioSystem;
|
||||
|
||||
// Handles TTS runtime cache
|
||||
public abstract ITTSRuntimeCacheHandler RuntimeCacheHandler { get; }
|
||||
// Handles TTS cache requests
|
||||
public abstract ITTSDiskCacheHandler DiskCacheHandler { get; }
|
||||
// Handles TTS web requests
|
||||
public abstract ITTSWebHandler WebHandler { get; }
|
||||
// Handles TTS voice presets
|
||||
public abstract ITTSVoiceProvider VoiceProvider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Static event called whenever any TTSService.Awake is called
|
||||
/// </summary>
|
||||
public static event Action<TTSService> OnServiceStart;
|
||||
/// <summary>
|
||||
/// Static event called whenever any TTSService.OnDestroy is called
|
||||
/// </summary>
|
||||
public static event Action<TTSService> OnServiceDestroy;
|
||||
|
||||
/// <summary>
|
||||
/// Returns error if invalid
|
||||
/// </summary>
|
||||
public virtual string GetInvalidError()
|
||||
{
|
||||
if (WebHandler == null)
|
||||
{
|
||||
return "Web Handler Missing";
|
||||
}
|
||||
if (VoiceProvider == null)
|
||||
{
|
||||
return "Voice Provider Missing";
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Handles TTS events
|
||||
public TTSServiceEvents Events => _events;
|
||||
[Header("Event Settings")]
|
||||
[SerializeField] private TTSServiceEvents _events = new TTSServiceEvents();
|
||||
|
||||
// Set instance
|
||||
protected virtual void Awake()
|
||||
{
|
||||
_instance = this;
|
||||
_delegates = false;
|
||||
}
|
||||
// Call event
|
||||
protected virtual void Start()
|
||||
{
|
||||
OnServiceStart?.Invoke(this);
|
||||
}
|
||||
// Log if invalid
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
string validError = GetInvalidError();
|
||||
if (!string.IsNullOrEmpty(validError))
|
||||
{
|
||||
VLog.W(validError);
|
||||
}
|
||||
}
|
||||
// Remove delegates
|
||||
protected virtual void OnDisable()
|
||||
{
|
||||
RemoveDelegates();
|
||||
}
|
||||
// Add delegates
|
||||
private bool _delegates = false;
|
||||
protected virtual void AddDelegates()
|
||||
{
|
||||
// Ignore if already added
|
||||
if (_delegates)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_delegates = true;
|
||||
|
||||
if (RuntimeCacheHandler != null)
|
||||
{
|
||||
RuntimeCacheHandler.OnClipAdded.AddListener(OnRuntimeClipAdded);
|
||||
RuntimeCacheHandler.OnClipRemoved.AddListener(OnRuntimeClipRemoved);
|
||||
}
|
||||
if (DiskCacheHandler != null)
|
||||
{
|
||||
DiskCacheHandler.DiskStreamEvents.OnStreamBegin.AddListener(OnDiskStreamBegin);
|
||||
DiskCacheHandler.DiskStreamEvents.OnStreamCancel.AddListener(OnDiskStreamCancel);
|
||||
DiskCacheHandler.DiskStreamEvents.OnStreamReady.AddListener(OnDiskStreamReady);
|
||||
DiskCacheHandler.DiskStreamEvents.OnStreamError.AddListener(OnDiskStreamError);
|
||||
}
|
||||
if (WebHandler != null)
|
||||
{
|
||||
WebHandler.WebStreamEvents.OnStreamBegin.AddListener(OnWebStreamBegin);
|
||||
WebHandler.WebStreamEvents.OnStreamCancel.AddListener(OnWebStreamCancel);
|
||||
WebHandler.WebStreamEvents.OnStreamReady.AddListener(OnWebStreamReady);
|
||||
WebHandler.WebStreamEvents.OnStreamError.AddListener(OnWebStreamError);
|
||||
WebHandler.WebDownloadEvents.OnDownloadBegin.AddListener(OnWebDownloadBegin);
|
||||
WebHandler.WebDownloadEvents.OnDownloadCancel.AddListener(OnWebDownloadCancel);
|
||||
WebHandler.WebDownloadEvents.OnDownloadSuccess.AddListener(OnWebDownloadSuccess);
|
||||
WebHandler.WebDownloadEvents.OnDownloadError.AddListener(OnWebDownloadError);
|
||||
}
|
||||
}
|
||||
// Remove delegates
|
||||
protected virtual void RemoveDelegates()
|
||||
{
|
||||
// Ignore if not yet added
|
||||
if (!_delegates)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_delegates = false;
|
||||
|
||||
if (RuntimeCacheHandler != null)
|
||||
{
|
||||
RuntimeCacheHandler.OnClipAdded.RemoveListener(OnRuntimeClipAdded);
|
||||
RuntimeCacheHandler.OnClipRemoved.RemoveListener(OnRuntimeClipRemoved);
|
||||
}
|
||||
if (DiskCacheHandler != null)
|
||||
{
|
||||
DiskCacheHandler.DiskStreamEvents.OnStreamBegin.RemoveListener(OnDiskStreamBegin);
|
||||
DiskCacheHandler.DiskStreamEvents.OnStreamCancel.RemoveListener(OnDiskStreamCancel);
|
||||
DiskCacheHandler.DiskStreamEvents.OnStreamReady.RemoveListener(OnDiskStreamReady);
|
||||
DiskCacheHandler.DiskStreamEvents.OnStreamError.RemoveListener(OnDiskStreamError);
|
||||
}
|
||||
if (WebHandler != null)
|
||||
{
|
||||
WebHandler.WebStreamEvents.OnStreamBegin.RemoveListener(OnWebStreamBegin);
|
||||
WebHandler.WebStreamEvents.OnStreamCancel.RemoveListener(OnWebStreamCancel);
|
||||
WebHandler.WebStreamEvents.OnStreamReady.RemoveListener(OnWebStreamReady);
|
||||
WebHandler.WebStreamEvents.OnStreamError.RemoveListener(OnWebStreamError);
|
||||
WebHandler.WebDownloadEvents.OnDownloadBegin.RemoveListener(OnWebDownloadBegin);
|
||||
WebHandler.WebDownloadEvents.OnDownloadCancel.RemoveListener(OnWebDownloadCancel);
|
||||
WebHandler.WebDownloadEvents.OnDownloadSuccess.RemoveListener(OnWebDownloadSuccess);
|
||||
WebHandler.WebDownloadEvents.OnDownloadError.RemoveListener(OnWebDownloadError);
|
||||
}
|
||||
}
|
||||
// Remove instance
|
||||
protected virtual void OnDestroy()
|
||||
{
|
||||
if (_instance == this)
|
||||
{
|
||||
_instance = null;
|
||||
}
|
||||
UnloadAll();
|
||||
OnServiceDestroy?.Invoke(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get clip log data
|
||||
/// </summary>
|
||||
protected virtual string GetClipLog(string logMessage, TTSClipData clipData)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine(logMessage);
|
||||
if (clipData != null)
|
||||
{
|
||||
builder.AppendLine($"Voice: {(clipData.voiceSettings == null ? "Default" : clipData.voiceSettings.SettingsId)}");
|
||||
builder.AppendLine($"Text: {clipData.textToSpeak}");
|
||||
builder.AppendLine($"ID: {clipData.clipID}");
|
||||
TTSDiskCacheLocation cacheLocation = TTSDiskCacheLocation.Stream;
|
||||
if (DiskCacheHandler != null)
|
||||
{
|
||||
TTSDiskCacheSettings settings = clipData.diskCacheSettings;
|
||||
if (settings == null)
|
||||
{
|
||||
settings = DiskCacheHandler.DiskCacheDefaultSettings;
|
||||
}
|
||||
if (settings != null)
|
||||
{
|
||||
cacheLocation = settings.DiskCacheLocation;
|
||||
}
|
||||
}
|
||||
builder.AppendLine($"Cache: {cacheLocation}");
|
||||
builder.AppendLine($"Type: {clipData.audioType}");
|
||||
if (clipData.clipStream != null)
|
||||
{
|
||||
builder.AppendLine($"Length: {clipData.clipStream.Length:0.00} seconds");
|
||||
}
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region HELPERS
|
||||
// Frequently used keys
|
||||
private const string CLIP_ID_DELIM = "|";
|
||||
private readonly SHA256 CLIP_HASH = SHA256.Create();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the text to be spoken after applying all relevant voice settings.
|
||||
/// </summary>
|
||||
/// <param name="textToSpeak">Text to be spoken by a particular voice</param>
|
||||
/// <param name="voiceSettings">Voice settings to be used</param>
|
||||
/// <returns>Returns a the final text to be spoken</returns>
|
||||
public string GetFinalText(string textToSpeak, TTSVoiceSettings voiceSettings)
|
||||
{
|
||||
StringBuilder result = new StringBuilder();
|
||||
AppendFinalText(result, textToSpeak, voiceSettings);
|
||||
return result.ToString();
|
||||
}
|
||||
// Finalize text using a string builder
|
||||
protected virtual void AppendFinalText(StringBuilder builder, string textToSpeak, TTSVoiceSettings voiceSettings)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(voiceSettings?.PrependedText))
|
||||
{
|
||||
builder.Append(voiceSettings.PrependedText);
|
||||
}
|
||||
builder.Append(textToSpeak);
|
||||
if (!string.IsNullOrEmpty(voiceSettings?.AppendedText))
|
||||
{
|
||||
builder.Append(voiceSettings.AppendedText);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtain unique id for clip data
|
||||
/// </summary>
|
||||
public virtual string GetClipID(string textToSpeak, TTSVoiceSettings voiceSettings)
|
||||
{
|
||||
// Get a text string for a unique id
|
||||
StringBuilder uniqueId = new StringBuilder();
|
||||
// Add all data items
|
||||
if (VoiceProvider != null)
|
||||
{
|
||||
Dictionary<string, string> data = VoiceProvider.EncodeVoiceSettings(voiceSettings);
|
||||
foreach (var key in data.Keys)
|
||||
{
|
||||
string keyClean = data[key].Replace(CLIP_ID_DELIM, "");
|
||||
uniqueId.Append(keyClean);
|
||||
uniqueId.Append(CLIP_ID_DELIM);
|
||||
}
|
||||
}
|
||||
// Finally, add unique id
|
||||
AppendFinalText(uniqueId, textToSpeak, voiceSettings);
|
||||
// Return id
|
||||
return GetSha256Hash(CLIP_HASH, uniqueId.ToString().ToLower());
|
||||
}
|
||||
|
||||
private string GetSha256Hash(SHA256 shaHash, string input)
|
||||
{
|
||||
// Convert the input string to a byte array and compute the hash.
|
||||
byte[] data = shaHash.ComputeHash(Encoding.UTF8.GetBytes(input));
|
||||
|
||||
// Create a new Stringbuilder to collect the bytes
|
||||
// and create a string.
|
||||
StringBuilder sBuilder = new StringBuilder();
|
||||
|
||||
// Loop through each byte of the hashed data
|
||||
// and format each one as a hexadecimal string.
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
sBuilder.Append(data[i].ToString("x2"));
|
||||
}
|
||||
|
||||
// Return the hexadecimal string.
|
||||
return sBuilder.ToString();
|
||||
}
|
||||
/// <summary>
|
||||
/// Creates new clip data or returns existing cached clip
|
||||
/// </summary>
|
||||
/// <param name="textToSpeak">Text to speak</param>
|
||||
/// <param name="clipID">Unique clip id</param>
|
||||
/// <param name="voiceSettings">Voice settings</param>
|
||||
/// <param name="diskCacheSettings">Disk Cache settings</param>
|
||||
/// <returns>Clip data structure</returns>
|
||||
protected virtual TTSClipData CreateClipData(string textToSpeak, string clipID, TTSVoiceSettings voiceSettings,
|
||||
TTSDiskCacheSettings diskCacheSettings)
|
||||
{
|
||||
// Use default voice settings if none are set
|
||||
if (voiceSettings == null && VoiceProvider != null)
|
||||
{
|
||||
voiceSettings = VoiceProvider.VoiceDefaultSettings;
|
||||
}
|
||||
// Use default disk cache settings if none are set
|
||||
if (diskCacheSettings == null && DiskCacheHandler != null)
|
||||
{
|
||||
diskCacheSettings = DiskCacheHandler.DiskCacheDefaultSettings;
|
||||
}
|
||||
// Determine clip id if empty
|
||||
if (string.IsNullOrEmpty(clipID))
|
||||
{
|
||||
clipID = GetClipID(textToSpeak, voiceSettings);
|
||||
}
|
||||
|
||||
// Get clip from runtime cache if applicable
|
||||
TTSClipData clipData = GetRuntimeCachedClip(clipID);
|
||||
if (clipData != null)
|
||||
{
|
||||
return clipData;
|
||||
}
|
||||
|
||||
// Generate new clip data
|
||||
clipData = new TTSClipData()
|
||||
{
|
||||
clipID = clipID,
|
||||
audioType = GetAudioType(),
|
||||
textToSpeak = GetFinalText(textToSpeak, voiceSettings),
|
||||
voiceSettings = voiceSettings,
|
||||
diskCacheSettings = diskCacheSettings,
|
||||
loadState = TTSClipLoadState.Unloaded,
|
||||
loadProgress = 0f,
|
||||
queryParameters = VoiceProvider?.EncodeVoiceSettings(voiceSettings),
|
||||
queryStream = false,
|
||||
clipStream = CreateClipStream()
|
||||
};
|
||||
|
||||
// Return generated clip
|
||||
return clipData;
|
||||
}
|
||||
// Generate a new audio clip stream
|
||||
protected virtual IAudioClipStream CreateClipStream()
|
||||
{
|
||||
// Default
|
||||
if (AudioSystem == null)
|
||||
{
|
||||
return new UnityAudioClipStream(WitConstants.ENDPOINT_TTS_CHANNELS, WitConstants.ENDPOINT_TTS_SAMPLE_RATE, 0.1f);
|
||||
}
|
||||
|
||||
// Get audio clip via audio system
|
||||
return AudioSystem.GetAudioClipStream(WitConstants.ENDPOINT_TTS_CHANNELS,
|
||||
WitConstants.ENDPOINT_TTS_SAMPLE_RATE);
|
||||
}
|
||||
// Get audio type
|
||||
protected virtual AudioType GetAudioType()
|
||||
{
|
||||
return AudioType.WAV;
|
||||
}
|
||||
// Set clip state
|
||||
protected virtual void SetClipLoadState(TTSClipData clipData, TTSClipLoadState loadState)
|
||||
{
|
||||
clipData.loadState = loadState;
|
||||
clipData.onStateChange?.Invoke(clipData, clipData.loadState);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region LOAD
|
||||
// TTS Request options
|
||||
public TTSClipData Load(string textToSpeak, Action<TTSClipData, string> onStreamReady = null) => Load(textToSpeak, null, null, null, onStreamReady);
|
||||
public TTSClipData Load(string textToSpeak, string presetVoiceId, Action<TTSClipData, string> onStreamReady = null) => Load(textToSpeak, null, GetPresetVoiceSettings(presetVoiceId), null, onStreamReady);
|
||||
public TTSClipData Load(string textToSpeak, string presetVoiceId, TTSDiskCacheSettings diskCacheSettings, Action<TTSClipData, string> onStreamReady = null) => Load(textToSpeak, null, GetPresetVoiceSettings(presetVoiceId), diskCacheSettings, onStreamReady);
|
||||
public TTSClipData Load(string textToSpeak, TTSVoiceSettings voiceSettings, TTSDiskCacheSettings diskCacheSettings, Action<TTSClipData, string> onStreamReady = null) => Load(textToSpeak, null, voiceSettings, diskCacheSettings, onStreamReady);
|
||||
|
||||
/// <summary>
|
||||
/// Perform a request for a TTS audio clip
|
||||
/// </summary>
|
||||
/// <param name="textToSpeak">Text to be spoken in clip</param>
|
||||
/// <param name="clipID">Unique clip id</param>
|
||||
/// <param name="voiceSettings">Custom voice settings</param>
|
||||
/// <param name="diskCacheSettings">Custom cache settings</param>
|
||||
/// <returns>Generated TTS clip data</returns>
|
||||
public virtual TTSClipData Load(string textToSpeak, string clipID, TTSVoiceSettings voiceSettings,
|
||||
TTSDiskCacheSettings diskCacheSettings, Action<TTSClipData, string> onStreamReady)
|
||||
{
|
||||
// Add delegates if needed
|
||||
AddDelegates();
|
||||
|
||||
// Get clip data
|
||||
TTSClipData clipData = CreateClipData(textToSpeak, clipID, voiceSettings, diskCacheSettings);
|
||||
if (clipData == null)
|
||||
{
|
||||
VLog.E("No clip provided");
|
||||
onStreamReady?.Invoke(clipData, "No clip provided");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(clipData.textToSpeak))
|
||||
{
|
||||
clipData.loadState = TTSClipLoadState.Loaded;
|
||||
}
|
||||
|
||||
// From Runtime Cache
|
||||
if (clipData.loadState != TTSClipLoadState.Unloaded)
|
||||
{
|
||||
// Add callback
|
||||
if (onStreamReady != null)
|
||||
{
|
||||
// Call once ready
|
||||
if (clipData.loadState == TTSClipLoadState.Preparing)
|
||||
{
|
||||
clipData.onPlaybackReady += (e) => onStreamReady(clipData, e);
|
||||
}
|
||||
// Call after return
|
||||
else
|
||||
{
|
||||
CoroutineUtility.StartCoroutine(CallAfterAMoment(() => onStreamReady(clipData,
|
||||
clipData.loadState == TTSClipLoadState.Loaded ? string.Empty : "Error")));
|
||||
}
|
||||
}
|
||||
|
||||
// Return clip
|
||||
return clipData;
|
||||
}
|
||||
|
||||
// Add to runtime cache if possible
|
||||
if (RuntimeCacheHandler != null)
|
||||
{
|
||||
if (!RuntimeCacheHandler.AddClip(clipData))
|
||||
{
|
||||
// Add callback
|
||||
if (onStreamReady != null)
|
||||
{
|
||||
// Call once ready
|
||||
if (clipData.loadState == TTSClipLoadState.Preparing)
|
||||
{
|
||||
clipData.onPlaybackReady += (e) => onStreamReady(clipData, e);
|
||||
}
|
||||
// Call after return
|
||||
else
|
||||
{
|
||||
CoroutineUtility.StartCoroutine(CallAfterAMoment(() => onStreamReady(clipData,
|
||||
clipData.loadState == TTSClipLoadState.Loaded ? string.Empty : "Error")));
|
||||
}
|
||||
}
|
||||
|
||||
// Return clip
|
||||
return clipData;
|
||||
}
|
||||
}
|
||||
// Load begin
|
||||
else
|
||||
{
|
||||
OnLoadBegin(clipData);
|
||||
}
|
||||
|
||||
// Add on ready delegate
|
||||
clipData.onPlaybackReady += (error) => onStreamReady?.Invoke(clipData, error);
|
||||
|
||||
// Wait a moment and load
|
||||
CoroutineUtility.StartCoroutine(CallAfterAMoment(() =>
|
||||
{
|
||||
// Check for invalid text
|
||||
string invalidError = WebHandler.IsTextValid(clipData.textToSpeak);
|
||||
if (!string.IsNullOrEmpty(invalidError))
|
||||
{
|
||||
OnWebStreamError(clipData, invalidError);
|
||||
return;
|
||||
}
|
||||
|
||||
// If should cache to disk, attempt to do so
|
||||
if (ShouldCacheToDisk(clipData))
|
||||
{
|
||||
// Download was canceled before starting
|
||||
if (clipData.loadState != TTSClipLoadState.Preparing)
|
||||
{
|
||||
string downloadPath = DiskCacheHandler.GetDiskCachePath(clipData);
|
||||
OnWebDownloadBegin(clipData, downloadPath);
|
||||
OnWebDownloadCancel(clipData, downloadPath);
|
||||
OnWebStreamBegin(clipData);
|
||||
OnWebStreamCancel(clipData);
|
||||
return;
|
||||
}
|
||||
|
||||
// Download
|
||||
DownloadToDiskCache(clipData, (clipData2, downloadPath, error) =>
|
||||
{
|
||||
// Download was canceled before starting
|
||||
if (string.Equals(error, WitConstants.CANCEL_ERROR))
|
||||
{
|
||||
OnWebStreamBegin(clipData);
|
||||
OnWebStreamCancel(clipData);
|
||||
return;
|
||||
}
|
||||
// Not in cache & cannot download
|
||||
if (string.Equals(error, WitConstants.ERROR_TTS_CACHE_DOWNLOAD))
|
||||
{
|
||||
WebHandler?.RequestStreamFromWeb(clipData);
|
||||
return;
|
||||
}
|
||||
// Download failed, throw error
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
OnWebStreamBegin(clipData);
|
||||
OnWebStreamError(clipData, error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Stream from Cache
|
||||
DiskCacheHandler?.StreamFromDiskCache(clipData);
|
||||
});
|
||||
}
|
||||
// Simply stream from the web
|
||||
else
|
||||
{
|
||||
// Stream was canceled before starting
|
||||
if (clipData.loadState != TTSClipLoadState.Preparing)
|
||||
{
|
||||
OnWebStreamBegin(clipData);
|
||||
OnWebStreamCancel(clipData);
|
||||
return;
|
||||
}
|
||||
|
||||
// Stream
|
||||
WebHandler?.RequestStreamFromWeb(clipData);
|
||||
}
|
||||
}));
|
||||
|
||||
// Return data
|
||||
return clipData;
|
||||
}
|
||||
// Wait a moment
|
||||
private IEnumerator CallAfterAMoment(Action call)
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
yield return new WaitForEndOfFrame();
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
call();
|
||||
}
|
||||
// Load begin
|
||||
private void OnLoadBegin(TTSClipData clipData)
|
||||
{
|
||||
// Now preparing
|
||||
SetClipLoadState(clipData, TTSClipLoadState.Preparing);
|
||||
|
||||
// Begin load
|
||||
VLog.I(GetClipLog("Load Clip", clipData));
|
||||
Events?.OnClipCreated?.Invoke(clipData);
|
||||
}
|
||||
// Handle begin of disk cache streaming
|
||||
private void OnDiskStreamBegin(TTSClipData clipData) => OnStreamBegin(clipData, true);
|
||||
private void OnWebStreamBegin(TTSClipData clipData) => OnStreamBegin(clipData, false);
|
||||
private void OnStreamBegin(TTSClipData clipData, bool fromDisk)
|
||||
{
|
||||
// Set delegates for clip stream update/completion
|
||||
if (clipData.clipStream != null)
|
||||
{
|
||||
clipData.clipStream.OnStreamUpdated = (cs) => OnStreamUpdated(clipData, cs, fromDisk);
|
||||
clipData.clipStream.OnStreamComplete = (cs) => OnStreamComplete(clipData, cs, fromDisk);
|
||||
}
|
||||
|
||||
// Callback delegate
|
||||
VLog.I(GetClipLog($"{(fromDisk ? "Disk" : "Web")} Stream Begin", clipData));
|
||||
Events?.Stream?.OnStreamBegin?.Invoke(clipData);
|
||||
}
|
||||
// Handle cancel of disk cache streaming
|
||||
private void OnDiskStreamCancel(TTSClipData clipData) => OnStreamCancel(clipData, true);
|
||||
private void OnWebStreamCancel(TTSClipData clipData) => OnStreamCancel(clipData, false);
|
||||
private void OnStreamCancel(TTSClipData clipData, bool fromDisk)
|
||||
{
|
||||
// Handled as an error
|
||||
SetClipLoadState(clipData, TTSClipLoadState.Error);
|
||||
|
||||
// Invoke
|
||||
clipData.onPlaybackReady?.Invoke(WitConstants.CANCEL_ERROR);
|
||||
clipData.onPlaybackReady = null;
|
||||
|
||||
// Callback delegate
|
||||
VLog.I(GetClipLog($"{(fromDisk ? "Disk" : "Web")} Stream Canceled", clipData));
|
||||
Events?.Stream?.OnStreamCancel?.Invoke(clipData);
|
||||
|
||||
// Unload clip
|
||||
Unload(clipData);
|
||||
}
|
||||
// Handle disk cache streaming error
|
||||
private void OnDiskStreamError(TTSClipData clipData, string error) => OnStreamError(clipData, error, true);
|
||||
private void OnWebStreamError(TTSClipData clipData, string error) => OnStreamError(clipData, error, false);
|
||||
private void OnStreamError(TTSClipData clipData, string error, bool fromDisk)
|
||||
{
|
||||
// Cancelled
|
||||
if (error.Equals(WitConstants.CANCEL_ERROR))
|
||||
{
|
||||
OnStreamCancel(clipData, fromDisk);
|
||||
return;
|
||||
}
|
||||
|
||||
// Error
|
||||
SetClipLoadState(clipData, TTSClipLoadState.Error);
|
||||
|
||||
// Invoke playback is ready
|
||||
clipData.onPlaybackReady?.Invoke(error);
|
||||
clipData.onPlaybackReady = null;
|
||||
|
||||
// Stream error
|
||||
VLog.E(GetClipLog($"{(fromDisk ? "Disk" : "Web")} Stream Error\nError: {error}", clipData));
|
||||
Events?.Stream?.OnStreamError?.Invoke(clipData, error);
|
||||
|
||||
// Unload clip
|
||||
Unload(clipData);
|
||||
}
|
||||
// Handle successful completion of disk cache streaming
|
||||
private void OnDiskStreamReady(TTSClipData clipData) => OnStreamReady(clipData, true);
|
||||
private void OnWebStreamReady(TTSClipData clipData) => OnStreamReady(clipData, false);
|
||||
private void OnStreamReady(TTSClipData clipData, bool fromDisk)
|
||||
{
|
||||
// Refresh cache for file size
|
||||
if (RuntimeCacheHandler != null)
|
||||
{
|
||||
// Stop forcing an unload if runtime cache update fails
|
||||
RuntimeCacheHandler.OnClipRemoved.RemoveListener(OnRuntimeClipRemoved);
|
||||
bool failed = !RuntimeCacheHandler.AddClip(clipData);
|
||||
RuntimeCacheHandler.OnClipRemoved.AddListener(OnRuntimeClipRemoved);
|
||||
|
||||
// Handle fail directly
|
||||
if (failed)
|
||||
{
|
||||
OnStreamError(clipData, "Removed from runtime cache due to file size", fromDisk);
|
||||
OnRuntimeClipRemoved(clipData);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Set delegates again since the stream may have changed during setup
|
||||
if (clipData.clipStream != null)
|
||||
{
|
||||
clipData.clipStream.OnStreamUpdated = (cs) => OnStreamUpdated(clipData, cs, fromDisk);
|
||||
clipData.clipStream.OnStreamComplete = (cs) => OnStreamComplete(clipData, cs, fromDisk);
|
||||
}
|
||||
|
||||
// Set clip stream state
|
||||
SetClipLoadState(clipData, TTSClipLoadState.Loaded);
|
||||
VLog.I(GetClipLog($"{(fromDisk ? "Disk" : "Web")} Stream Ready", clipData));
|
||||
|
||||
// Invoke playback is ready
|
||||
clipData.onPlaybackReady?.Invoke(string.Empty);
|
||||
clipData.onPlaybackReady = null;
|
||||
|
||||
// Callback delegate
|
||||
Events?.Stream?.OnStreamReady?.Invoke(clipData);
|
||||
}
|
||||
// Stream clip update
|
||||
private void OnStreamUpdated(TTSClipData clipData, IAudioClipStream clipStream, bool fromDisk)
|
||||
{
|
||||
// Ignore invalid
|
||||
if (clipStream == null || clipData == null || clipStream != clipData.clipStream)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Log & call event
|
||||
VLog.I(GetClipLog($"{(fromDisk ? "Disk" : "Web")} Stream Updated", clipData));
|
||||
Events?.Stream?.OnStreamClipUpdate?.Invoke(clipData);
|
||||
}
|
||||
// Stream complete
|
||||
private void OnStreamComplete(TTSClipData clipData, IAudioClipStream clipStream, bool fromDisk)
|
||||
{
|
||||
// Ignore invalid
|
||||
if (clipStream == null || clipData == null || clipStream != clipData.clipStream)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Log & call event
|
||||
VLog.I(GetClipLog($"{(fromDisk ? "Disk" : "Web")} Stream Complete", clipData));
|
||||
Events?.Stream?.OnStreamComplete?.Invoke(clipData);
|
||||
|
||||
// Web request completion
|
||||
if (!fromDisk)
|
||||
{
|
||||
Events?.WebRequest?.OnRequestComplete.Invoke(clipData);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region UNLOAD
|
||||
/// <summary>
|
||||
/// Unload all audio clips from the runtime cache
|
||||
/// </summary>
|
||||
public void UnloadAll()
|
||||
{
|
||||
// Failed
|
||||
TTSClipData[] clips = RuntimeCacheHandler?.GetClips();
|
||||
if (clips == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy array
|
||||
HashSet<TTSClipData> remaining = new HashSet<TTSClipData>(clips);
|
||||
|
||||
// Unload all clips
|
||||
foreach (var clip in remaining)
|
||||
{
|
||||
Unload(clip);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Force a runtime cache unload
|
||||
/// </summary>
|
||||
public void Unload(TTSClipData clipData)
|
||||
{
|
||||
if (RuntimeCacheHandler != null)
|
||||
{
|
||||
RuntimeCacheHandler.RemoveClip(clipData.clipID);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnUnloadBegin(clipData);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Perform clip unload
|
||||
/// </summary>
|
||||
/// <param name="clipID"></param>
|
||||
protected virtual void OnUnloadBegin(TTSClipData clipData)
|
||||
{
|
||||
// Abort if currently preparing
|
||||
if (clipData.loadState == TTSClipLoadState.Preparing)
|
||||
{
|
||||
// Cancel web stream
|
||||
WebHandler?.CancelWebStream(clipData);
|
||||
// Cancel web download to cache
|
||||
WebHandler?.CancelWebDownload(clipData, GetDiskCachePath(clipData.textToSpeak, clipData.clipID, clipData.voiceSettings, clipData.diskCacheSettings));
|
||||
// Cancel disk cache stream
|
||||
DiskCacheHandler?.CancelDiskCacheStream(clipData);
|
||||
}
|
||||
|
||||
// Unloads clip stream
|
||||
clipData.clipStream = null;
|
||||
|
||||
// Clip is now unloaded
|
||||
SetClipLoadState(clipData, TTSClipLoadState.Unloaded);
|
||||
|
||||
// Unload
|
||||
VLog.I(GetClipLog($"Unload Clip", clipData));
|
||||
Events?.OnClipUnloaded?.Invoke(clipData);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region RUNTIME CACHE
|
||||
/// <summary>
|
||||
/// Obtain a clip from the runtime cache, if applicable
|
||||
/// </summary>
|
||||
public TTSClipData GetRuntimeCachedClip(string clipID) => RuntimeCacheHandler?.GetClip(clipID);
|
||||
/// <summary>
|
||||
/// Obtain all clips from the runtime cache, if applicable
|
||||
/// </summary>
|
||||
public TTSClipData[] GetAllRuntimeCachedClips() => RuntimeCacheHandler?.GetClips();
|
||||
|
||||
/// <summary>
|
||||
/// Called when runtime cache adds a clip
|
||||
/// </summary>
|
||||
/// <param name="clipData"></param>
|
||||
protected virtual void OnRuntimeClipAdded(TTSClipData clipData) => OnLoadBegin(clipData);
|
||||
|
||||
/// <summary>
|
||||
/// Called when runtime cache unloads a clip
|
||||
/// </summary>
|
||||
/// <param name="clipData">Clip to be unloaded</param>
|
||||
protected virtual void OnRuntimeClipRemoved(TTSClipData clipData) => OnUnloadBegin(clipData);
|
||||
#endregion
|
||||
|
||||
#region DISK CACHE
|
||||
/// <summary>
|
||||
/// Whether a specific clip should be cached
|
||||
/// </summary>
|
||||
/// <param name="clipData">Clip data</param>
|
||||
/// <returns>True if should be cached</returns>
|
||||
public bool ShouldCacheToDisk(TTSClipData clipData) =>
|
||||
DiskCacheHandler != null && DiskCacheHandler.ShouldCacheToDisk(clipData);
|
||||
|
||||
/// <summary>
|
||||
/// Get disk cache
|
||||
/// </summary>
|
||||
/// <param name="textToSpeak">Text to be spoken in clip</param>
|
||||
/// <param name="clipID">Unique clip id</param>
|
||||
/// <param name="voiceSettings">Custom voice settings</param>
|
||||
/// <param name="diskCacheSettings">Custom disk cache settings</param>
|
||||
/// <returns></returns>
|
||||
public string GetDiskCachePath(string textToSpeak, string clipID, TTSVoiceSettings voiceSettings,
|
||||
TTSDiskCacheSettings diskCacheSettings) =>
|
||||
DiskCacheHandler?.GetDiskCachePath(CreateClipData(textToSpeak, clipID, voiceSettings, diskCacheSettings));
|
||||
|
||||
// Download options
|
||||
public TTSClipData DownloadToDiskCache(string textToSpeak,
|
||||
Action<TTSClipData, string, string> onDownloadComplete = null) =>
|
||||
DownloadToDiskCache(textToSpeak, null, null, null, onDownloadComplete);
|
||||
public TTSClipData DownloadToDiskCache(string textToSpeak, string presetVoiceId,
|
||||
Action<TTSClipData, string, string> onDownloadComplete = null) => DownloadToDiskCache(textToSpeak, null,
|
||||
GetPresetVoiceSettings(presetVoiceId), null, onDownloadComplete);
|
||||
public TTSClipData DownloadToDiskCache(string textToSpeak, string presetVoiceId,
|
||||
TTSDiskCacheSettings diskCacheSettings, Action<TTSClipData, string, string> onDownloadComplete = null) =>
|
||||
DownloadToDiskCache(textToSpeak, null, GetPresetVoiceSettings(presetVoiceId), diskCacheSettings,
|
||||
onDownloadComplete);
|
||||
public TTSClipData DownloadToDiskCache(string textToSpeak, TTSVoiceSettings voiceSettings,
|
||||
TTSDiskCacheSettings diskCacheSettings, Action<TTSClipData, string, string> onDownloadComplete = null) =>
|
||||
DownloadToDiskCache(textToSpeak, null, voiceSettings, diskCacheSettings, onDownloadComplete);
|
||||
|
||||
/// <summary>
|
||||
/// Perform a download for a TTS audio clip
|
||||
/// </summary>
|
||||
/// <param name="textToSpeak">Text to be spoken in clip</param>
|
||||
/// <param name="clipID">Unique clip id</param>
|
||||
/// <param name="voiceSettings">Custom voice settings</param>
|
||||
/// <param name="diskCacheSettings">Custom disk cache settings</param>
|
||||
/// <param name="onDownloadComplete">Callback when file has finished downloading</param>
|
||||
/// <returns>Generated TTS clip data</returns>
|
||||
public TTSClipData DownloadToDiskCache(string textToSpeak, string clipID, TTSVoiceSettings voiceSettings,
|
||||
TTSDiskCacheSettings diskCacheSettings, Action<TTSClipData, string, string> onDownloadComplete = null)
|
||||
{
|
||||
TTSClipData clipData = CreateClipData(textToSpeak, clipID, voiceSettings, diskCacheSettings);
|
||||
DownloadToDiskCache(clipData, onDownloadComplete);
|
||||
return clipData;
|
||||
}
|
||||
|
||||
// Performs download to disk cache
|
||||
protected virtual void DownloadToDiskCache(TTSClipData clipData, Action<TTSClipData, string, string> onDownloadComplete)
|
||||
{
|
||||
// Add delegates if needed
|
||||
AddDelegates();
|
||||
|
||||
// Check if cached to disk & log
|
||||
string downloadPath = DiskCacheHandler.GetDiskCachePath(clipData);
|
||||
DiskCacheHandler.CheckCachedToDisk(clipData, (clip, found) =>
|
||||
{
|
||||
// Cache checked
|
||||
VLog.I(GetClipLog($"Disk Cache {(found ? "Found" : "Missing")}\nPath: {downloadPath}", clipData));
|
||||
|
||||
// Already downloaded, return successful
|
||||
if (found)
|
||||
{
|
||||
onDownloadComplete?.Invoke(clipData, downloadPath, string.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
// Preload selected but not in disk cache, return an error
|
||||
if (Application.isPlaying && clipData.diskCacheSettings.DiskCacheLocation == TTSDiskCacheLocation.Preload)
|
||||
{
|
||||
onDownloadComplete?.Invoke(clipData, downloadPath, WitConstants.ERROR_TTS_CACHE_DOWNLOAD);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add download completion callback
|
||||
clipData.onDownloadComplete += (error) => onDownloadComplete?.Invoke(clipData, downloadPath, error);
|
||||
|
||||
// Download to cache
|
||||
WebHandler.RequestDownloadFromWeb(clipData, downloadPath);
|
||||
});
|
||||
}
|
||||
// On web download begin
|
||||
private void OnWebDownloadBegin(TTSClipData clipData, string downloadPath)
|
||||
{
|
||||
VLog.I(GetClipLog($"Download Clip - Begin\nPath: {downloadPath}", clipData));
|
||||
Events?.Download?.OnDownloadBegin?.Invoke(clipData, downloadPath);
|
||||
}
|
||||
// On web download complete
|
||||
private void OnWebDownloadSuccess(TTSClipData clipData, string downloadPath)
|
||||
{
|
||||
// Invoke clip callback & clear
|
||||
clipData.onDownloadComplete?.Invoke(string.Empty);
|
||||
clipData.onDownloadComplete = null;
|
||||
|
||||
// Log
|
||||
VLog.I(GetClipLog($"Download Clip - Success\nPath: {downloadPath}", clipData));
|
||||
Events?.Download?.OnDownloadSuccess?.Invoke(clipData, downloadPath);
|
||||
}
|
||||
// On web download complete
|
||||
private void OnWebDownloadCancel(TTSClipData clipData, string downloadPath)
|
||||
{
|
||||
// Invoke clip callback & clear
|
||||
clipData.onDownloadComplete?.Invoke(WitConstants.CANCEL_ERROR);
|
||||
clipData.onDownloadComplete = null;
|
||||
|
||||
// Log
|
||||
VLog.I(GetClipLog($"Download Clip - Canceled\nPath: {downloadPath}", clipData));
|
||||
Events?.Download?.OnDownloadCancel?.Invoke(clipData, downloadPath);
|
||||
}
|
||||
// On web download complete
|
||||
private void OnWebDownloadError(TTSClipData clipData, string downloadPath, string error)
|
||||
{
|
||||
// Cancelled
|
||||
if (error.Equals(WitConstants.CANCEL_ERROR))
|
||||
{
|
||||
OnWebDownloadCancel(clipData, downloadPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// Invoke clip callback & clear
|
||||
clipData.onDownloadComplete?.Invoke(error);
|
||||
clipData.onDownloadComplete = null;
|
||||
|
||||
// Log
|
||||
VLog.E(GetClipLog($"Download Clip - Failed\nPath: {downloadPath}\nError: {error}", clipData));
|
||||
Events?.Download?.OnDownloadError?.Invoke(clipData, downloadPath, error);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region VOICES
|
||||
/// <summary>
|
||||
/// Return all preset voice settings
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public TTSVoiceSettings[] GetAllPresetVoiceSettings() => VoiceProvider?.PresetVoiceSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Return preset voice settings for a specific id
|
||||
/// </summary>
|
||||
/// <param name="presetVoiceId"></param>
|
||||
/// <returns></returns>
|
||||
public TTSVoiceSettings GetPresetVoiceSettings(string presetVoiceId)
|
||||
{
|
||||
if (VoiceProvider == null || VoiceProvider.PresetVoiceSettings == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return Array.Find(VoiceProvider.PresetVoiceSettings, (v) => string.Equals(v.SettingsId, presetVoiceId, StringComparison.CurrentCultureIgnoreCase));
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f18e4991da1755f4cbb87c883f205186
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 211556d9cf1bb5d4dadd0c632ab9457f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b15403450229c3a4b8455a61d6143a6d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Meta.WitAi.TTS.Data;
|
||||
|
||||
namespace Meta.WitAi.TTS.Utilities
|
||||
{
|
||||
public interface ITTSPhraseProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// The supported voice ids
|
||||
/// </summary>
|
||||
List<string> GetVoiceIds();
|
||||
|
||||
/// <summary>
|
||||
/// Get specific phrases per voice
|
||||
/// </summary>
|
||||
List<string> GetVoicePhrases(string voiceId);
|
||||
}
|
||||
|
||||
[RequireComponent(typeof(TTSSpeaker))]
|
||||
public class TTSSpeakerAutoLoader : MonoBehaviour, ITTSPhraseProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// TTSSpeaker to be used
|
||||
/// </summary>
|
||||
public TTSSpeaker Speaker;
|
||||
/// <summary>
|
||||
/// Text file with phrases separated by line
|
||||
/// </summary>
|
||||
public TextAsset PhraseFile;
|
||||
/// <summary>
|
||||
/// All phrases to be loaded
|
||||
/// </summary>
|
||||
public string[] Phrases => _phrases;
|
||||
[SerializeField] private string[] _phrases;
|
||||
/// <summary>
|
||||
/// Whether LoadClips has to be called explicitly.
|
||||
/// If false, it is called on start
|
||||
/// </summary>
|
||||
public bool LoadManually = false;
|
||||
|
||||
// Generated clips
|
||||
public TTSClipData[] Clips => _clips;
|
||||
private TTSClipData[] _clips;
|
||||
|
||||
// Done loading
|
||||
public bool IsLoaded => _clipsLoading == 0;
|
||||
private int _clipsLoading = 0;
|
||||
|
||||
// Load on start if not manual
|
||||
protected virtual void Start()
|
||||
{
|
||||
if (!LoadManually)
|
||||
{
|
||||
LoadClips();
|
||||
}
|
||||
}
|
||||
// Load all phrase clips
|
||||
public virtual void LoadClips()
|
||||
{
|
||||
// Done
|
||||
if (_clips != null)
|
||||
{
|
||||
VLog.W("Cannot autoload clips twice.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Set phrase list
|
||||
_phrases = GetAllPhrases().ToArray();
|
||||
|
||||
// Load all clips
|
||||
List<TTSClipData> list = new List<TTSClipData>();
|
||||
foreach (var phrase in _phrases)
|
||||
{
|
||||
_clipsLoading++;
|
||||
TTSClipData clip = TTSService.Instance.Load(phrase, Speaker.presetVoiceID, null, OnClipReady);
|
||||
list.Add(clip);
|
||||
}
|
||||
_clips = list.ToArray();
|
||||
}
|
||||
// Return all phrases
|
||||
public virtual List<string> GetAllPhrases()
|
||||
{
|
||||
// Ensure speaker exists
|
||||
SetupSpeaker();
|
||||
|
||||
// Get all phrases unformatted
|
||||
List<string> unformattedPhrases = new List<string>();
|
||||
// Add phrases split from phrase file
|
||||
AddUniquePhrases(unformattedPhrases, PhraseFile?.text.Split('\n'));
|
||||
// Add phrases serialized in phrase array
|
||||
AddUniquePhrases(unformattedPhrases, Phrases);
|
||||
|
||||
// Iterate old phrases
|
||||
List<string> phrases = new List<string>();
|
||||
for (int i = 0; i < unformattedPhrases.Count; i++)
|
||||
{
|
||||
// Format phrases
|
||||
List<string> newPhrases = Speaker.GetFinalText(unformattedPhrases[i]);
|
||||
// Add to final list
|
||||
if (newPhrases != null && newPhrases.Count > 0)
|
||||
{
|
||||
phrases.AddRange(newPhrases);
|
||||
}
|
||||
}
|
||||
|
||||
// Return array
|
||||
return phrases;
|
||||
}
|
||||
// Add unique, non-null phrases
|
||||
private void AddUniquePhrases(List<string> list, string[] newPhrases)
|
||||
{
|
||||
if (newPhrases != null)
|
||||
{
|
||||
foreach (var phrase in newPhrases)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(phrase) && !list.Contains(phrase))
|
||||
{
|
||||
list.Add(phrase);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Setup speaker
|
||||
protected virtual void SetupSpeaker()
|
||||
{
|
||||
if (!Speaker)
|
||||
{
|
||||
Speaker = gameObject.GetComponent<TTSSpeaker>();
|
||||
if (!Speaker)
|
||||
{
|
||||
Speaker = gameObject.AddComponent<TTSSpeaker>();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clip ready callback
|
||||
protected virtual void OnClipReady(TTSClipData clipData, string error)
|
||||
{
|
||||
_clipsLoading--;
|
||||
}
|
||||
|
||||
// Unload phrases
|
||||
protected virtual void OnDestroy()
|
||||
{
|
||||
UnloadClips();
|
||||
}
|
||||
// Unload all clips
|
||||
protected virtual void UnloadClips()
|
||||
{
|
||||
if (_clips == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (var clip in _clips)
|
||||
{
|
||||
TTSService.Instance?.Unload(clip);
|
||||
}
|
||||
_clips = null;
|
||||
_phrases = null;
|
||||
}
|
||||
|
||||
#region ITTSVoicePhraseProvider
|
||||
/// <summary>
|
||||
/// Returns the supported voice ids (Only this speaker)
|
||||
/// </summary>
|
||||
public virtual List<string> GetVoiceIds()
|
||||
{
|
||||
SetupSpeaker();
|
||||
string voiceId = Speaker?.VoiceSettings.SettingsId;
|
||||
if (string.IsNullOrEmpty(voiceId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
List<string> results = new List<string>();
|
||||
results.Add(voiceId);
|
||||
return results;
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns the supported phrases per voice
|
||||
/// </summary>
|
||||
public virtual List<string> GetVoicePhrases(string voiceId)
|
||||
{
|
||||
return GetAllPhrases();
|
||||
}
|
||||
#endregion ITTSVoicePhraseProvider
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 25d484b58054b064db727b9fe66aed60
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+135
@@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d2accdd3f9f0a354b84b364eae500512
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user