Initial Commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e18f1785366987e4886fb11f30d70040
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cde9306012742314db5ada2fe0d9bff2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
+128
@@ -0,0 +1,128 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 701ca4114422ead418e08b1c0bc26348
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 11
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
applyGammaDecoding: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Android
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: WebGL
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spritePackingTag:
|
||||
pSDRemoveMatte: 0
|
||||
pSDShowRemoveMatteOption: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
+128
@@ -0,0 +1,128 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a3ec408155652024f94168a419eabdcc
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 11
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
applyGammaDecoding: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Android
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: WebGL
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spritePackingTag:
|
||||
pSDRemoveMatte: 0
|
||||
pSDShowRemoveMatteOption: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
+128
@@ -0,0 +1,128 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3387a752287aa7845892fd1c1ca0eb8f
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 11
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
applyGammaDecoding: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Android
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: WebGL
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spritePackingTag:
|
||||
pSDRemoveMatte: 0
|
||||
pSDShowRemoveMatteOption: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
+128
@@ -0,0 +1,128 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 45b126b5c88878b42840b8dd157251b6
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 11
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
applyGammaDecoding: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Android
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: WebGL
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spritePackingTag:
|
||||
pSDRemoveMatte: 0
|
||||
pSDShowRemoveMatteOption: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,40 @@
|
||||
# Text-to-Speech Overview
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
Voice SDK’s Text-to-Speech (TTS) feature uses a Wit.ai based service to provide audio files for text strings. It’s handled by a single TTSService prefab for all TTS settings and you can use a simple TTSSpeaker script for each scene location in which a TTS clip can be played.
|
||||
|
||||
To keep TTS working smoothly, Voice SDK handles the caching of TTS files during runtime (or when otherwise needed). If streaming TTS audio files is not an option for your application, however, you can set it to use preloaded static TTS files prior to building your app.
|
||||
|
||||
<b>Note:</b> With this initial release of TTS, only a few Wit.ai TTS voice settings are adjustable, but the package will be updated as more voices become available.
|
||||
|
||||
|
||||
## Setup
|
||||
|
||||
Use the following steps to set up TTS for your app once the plugin has been imported:
|
||||
1. Open the scene you want to use TTS within.
|
||||
2. Generate a new Wit Configuration using the Wit Configurations menu within the Oculus > Voice SDK > Voice HUB. Ensure it refreshes successfully and displays all voices available for your configuration.
|
||||
2. Navigate to Assets > Create > Voice SDK > TTS > Add Default TTS Setup
|
||||
3. In your scene heirarchy navigate inside the newly generated TTS GameObject to select the TTSWitService gameobject and adjust the inspector to fit your needs:
|
||||
3a. Use TTSWit > Request Settings > Configuration to select the Wit Configuration asset generated in step 2.
|
||||
3b. Once your configuration is setup, go to 'Preset Voice Settings' and setup any voices that might be shared by multiple TTSSpeakers.
|
||||
For more information, see TTS Voice Customization: https://developer.oculus.com/documentation/unity/voice-sdk-tts-voice-customization
|
||||

|
||||
3c. Under TTS Runtime Cache (Script), adjust the settings to indicate how often clips will be automatically uploaded from memory.
|
||||
For more information, see TTS Cache Options: https://developer.oculus.com/documentation/unity/voice-sdk-tts-cache-options
|
||||
3d. If needed, adjust your disk cache directory location and name in the tree under TTS Disk Cache (Script).
|
||||
For more information, see TTS Cache Options: https://developer.oculus.com/documentation/unity/voice-sdk-tts-cache-options/
|
||||

|
||||
4. Move & duplicate the TTSSpeaker to all the locations in your app where you would like TTS to be played.
|
||||
5. Modify each TTSSpeaker via the Inspector to fit your needs:
|
||||
5a. Under Voice Settings, select the Voice Preset for the specific speaker or select Custom to apply speaker specific settings.
|
||||
5b. Adjust the AudioSource in order to add the TTSSpeaker to a custom audio group or set audio from 2D to 3D.
|
||||
6. Via a script use the following TTSSpeaker methods to load and play text.
|
||||
6a. Use the TTSSpeaker script’s Speak(textToSpeak : string) method to request and say specified text on load.
|
||||
6b. Use the TTSSpeaker script’s SpeakQueued(textToSpeak : string) method to request and say specified text.
|
||||
6c. Send a custom TTSSpeakerClipEvents into any Speak/SpeakQueued method for request specific text load & playback event callbacks.
|
||||
6d. Use the TTSSpeaker script’s Stop() method to immediately stop all loading & playing TTS clips.
|
||||
6e. Use TTSSpeaker's Stop(textToSpeak : string) to immediately stop loading & playing of a specific text string.
|
||||
|
||||
<b>Note:</b> Check out TTS/Samples/Scenes/TTSSample.unity for an example of TTS implementation. Ensure that you set your WitConfiguration to the sample prior to running.
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0a78e70d61637eb498ee552bab9479ab
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,19 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 98bfdca232b84c3db2527e4b5c9d3b9d, type: 3}
|
||||
m_Name: TTS Overview Readme
|
||||
m_EditorClassIdentifier:
|
||||
_displayName: Overview
|
||||
_prefix: Documentation/TTS/
|
||||
_context: {fileID: 11400000, guid: f2e797de12b196c40ad409b92d6394fc, type: 2}
|
||||
_markdownFile: {fileID: 4900000, guid: 0a78e70d61637eb498ee552bab9479ab, type: 3}
|
||||
_priority: 200
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c745949bbb55f7b4dba6eeac61f4a7bd
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5d4d513ffe662a4db92f38c8468357e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 68d2ecf49ba62e0408b8f86acb9b103c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 748ff8d1d0d4814429b86058a56a3ee4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e581f7e133ba2048995048327a1ef85
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 466ea4f9ad1762f41a5effc0e1985b21
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b1da9fac71952343b124f3771afe034
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "Meta.WitAi.TTS.Editor",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"GUID:4504b1a6e0fdcc3498c30b266e4a63bf",
|
||||
"GUID:fa958eb9f0171754fb207d563a15ddfa",
|
||||
"GUID:8bbcefc153e1f1b4a98680670797dd16",
|
||||
"GUID:1c28d8b71ced07540b7c271537363cc6",
|
||||
"GUID:5c61c7ae4b0c6f94299e51352f802670"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e31cc0253d8f956459091c800a16d68d
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 716af1288c1a89d44950d38c999fe096
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Meta.WitAi.TTS.Preload
|
||||
{
|
||||
[Serializable]
|
||||
public class TTSPreloadPhraseData
|
||||
{
|
||||
/// <summary>
|
||||
/// ID used to identify this phrase
|
||||
/// </summary>
|
||||
public string clipID;
|
||||
/// <summary>
|
||||
/// Actual phrase to be spoken
|
||||
/// </summary>
|
||||
public string textToSpeak;
|
||||
|
||||
/// <summary>
|
||||
/// Meta data for whether clip is downloaded or not
|
||||
/// </summary>
|
||||
public bool downloaded;
|
||||
/// <summary>
|
||||
/// Meta data for clip download progress
|
||||
/// </summary>
|
||||
public float downloadProgress;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class TTSPreloadVoiceData
|
||||
{
|
||||
/// <summary>
|
||||
/// Specific preset voice settings id to be used with TTSService
|
||||
/// </summary>
|
||||
public string presetVoiceID;
|
||||
/// <summary>
|
||||
/// All data corresponding to text to speak
|
||||
/// </summary>
|
||||
public TTSPreloadPhraseData[] phrases;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class TTSPreloadData
|
||||
{
|
||||
public TTSPreloadVoiceData[] voices;
|
||||
}
|
||||
|
||||
public class TTSPreloadSettings : ScriptableObject
|
||||
{
|
||||
[SerializeField] public TTSPreloadData data = new TTSPreloadData();
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 80be64a601ff65e41b0a8036ba872084
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+481
@@ -0,0 +1,481 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Meta.WitAi.TTS.Data;
|
||||
using Meta.WitAi.Utilities;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Meta.WitAi.TTS.Preload
|
||||
{
|
||||
[CustomEditor(typeof(TTSPreloadSettings), true)]
|
||||
public class TTSPreloadSettingsInspector : UnityEditor.Editor
|
||||
{
|
||||
// TTS Settings
|
||||
public TTSPreloadSettings Settings { get; private set; }
|
||||
|
||||
// TTS Service
|
||||
public TTSService TtsService { get; private set; }
|
||||
private List<string> _ttsVoiceIDs;
|
||||
|
||||
// Layout items
|
||||
public const float ACTION_BTN_INDENT = 15f;
|
||||
public virtual Texture2D HeaderIcon => WitTexts.HeaderIcon;
|
||||
public virtual string HeaderUrl => WitTexts.GetAppURL(string.Empty, WitTexts.WitAppEndpointType.Settings);
|
||||
public virtual string DocsUrl => WitTexts.Texts.WitDocsUrl;
|
||||
|
||||
// Layout
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
// Get settings
|
||||
if (Settings != target)
|
||||
{
|
||||
Settings = target as TTSPreloadSettings;
|
||||
}
|
||||
|
||||
// Draw header
|
||||
WitEditorUI.LayoutHeaderButton(HeaderIcon, HeaderUrl, DocsUrl);
|
||||
GUILayout.Space(WitStyles.HeaderPaddingBottom);
|
||||
|
||||
// Layout actions
|
||||
LayoutPreloadActions();
|
||||
// Layout data
|
||||
LayoutPreloadData();
|
||||
}
|
||||
// Layout Preload Data
|
||||
protected virtual void LayoutPreloadActions()
|
||||
{
|
||||
// Layout preload actions
|
||||
EditorGUILayout.Space();
|
||||
WitEditorUI.LayoutSubheaderLabel("TTS Preload Actions");
|
||||
|
||||
// Indent
|
||||
EditorGUI.indentLevel++;
|
||||
EditorGUILayout.Space();
|
||||
|
||||
// Hide when playing
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
WitEditorUI.LayoutErrorLabel("TTS preload actions cannot be performed at runtime.");
|
||||
EditorGUI.indentLevel--;
|
||||
return;
|
||||
}
|
||||
|
||||
// Get TTS Service if needed
|
||||
TtsService = EditorGUILayout.ObjectField("TTS Service", TtsService, typeof(TTSService), true) as TTSService;
|
||||
if (TtsService == null)
|
||||
{
|
||||
TtsService = GameObjectSearchUtility.FindSceneObject<TTSService>(true);
|
||||
if (TtsService == null)
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
WitEditorUI.LayoutErrorLabel("You must add a TTS Service to the loaded scene in order perform TTS actions.");
|
||||
EditorGUI.indentLevel--;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (_ttsVoiceIDs == null)
|
||||
{
|
||||
_ttsVoiceIDs = GetVoiceIDs(TtsService);
|
||||
}
|
||||
|
||||
// Begin buttons
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
|
||||
// Import JSON
|
||||
GUILayout.Space(ACTION_BTN_INDENT * EditorGUI.indentLevel);
|
||||
if (WitEditorUI.LayoutTextButton("Refresh Data"))
|
||||
{
|
||||
RefreshData();
|
||||
}
|
||||
GUILayout.Space(ACTION_BTN_INDENT);
|
||||
if (WitEditorUI.LayoutTextButton("Import JSON"))
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
if (TTSPreloadUtility.ImportData(Settings))
|
||||
{
|
||||
RefreshData();
|
||||
}
|
||||
}
|
||||
GUILayout.Space(ACTION_BTN_INDENT);
|
||||
if (WitEditorUI.LayoutTextButton("Import AutoLoader Data"))
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
if (TTSPreloadUtility.ImportPhrases(Settings))
|
||||
{
|
||||
RefreshData();
|
||||
}
|
||||
}
|
||||
// Clear disk cache
|
||||
GUI.enabled = TtsService != null;
|
||||
EditorGUILayout.Space();
|
||||
Color col = GUI.color;
|
||||
GUI.color = Color.red;
|
||||
if (WitEditorUI.LayoutTextButton("Delete Cache"))
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
TTSPreloadUtility.DeleteData(TtsService);
|
||||
RefreshData();
|
||||
}
|
||||
// Preload disk cache
|
||||
GUILayout.Space(ACTION_BTN_INDENT);
|
||||
GUI.color = Color.green;
|
||||
if (WitEditorUI.LayoutTextButton("Preload Cache"))
|
||||
{
|
||||
DownloadClips();
|
||||
}
|
||||
GUI.color = col;
|
||||
GUI.enabled = true;
|
||||
|
||||
// End buttons
|
||||
EditorGUILayout.EndHorizontal();
|
||||
EditorGUILayout.Space();
|
||||
|
||||
// Indent
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
// Refresh
|
||||
private void RefreshData()
|
||||
{
|
||||
TTSPreloadUtility.RefreshPreloadData(TtsService, Settings.data, (p) =>
|
||||
{
|
||||
EditorUtility.DisplayProgressBar("TTS Preload Utility", "Refreshing Data", p);
|
||||
}, (d, l) =>
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
EditorUtility.SetDirty(Settings);
|
||||
Debug.Log($"TTS Preload Utility - Refresh Complete{l}");
|
||||
});
|
||||
}
|
||||
// Download
|
||||
private void DownloadClips()
|
||||
{
|
||||
TTSPreloadUtility.PreloadData(TtsService, Settings.data, (p) =>
|
||||
{
|
||||
EditorUtility.DisplayProgressBar("TTS Preload Utility", "Downloading Clips", p);
|
||||
}, (d, l) =>
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
EditorUtility.SetDirty(Settings);
|
||||
AssetDatabase.Refresh();
|
||||
Debug.Log($"TTS Preload Utility - Preload Complete{l}");
|
||||
});
|
||||
}
|
||||
// Layout Preload Data
|
||||
protected virtual void LayoutPreloadData()
|
||||
{
|
||||
// For updates
|
||||
bool updated = false;
|
||||
|
||||
// Layout preload items
|
||||
GUILayout.Space(WitStyles.WindowPaddingBottom);
|
||||
GUILayout.BeginHorizontal();
|
||||
WitEditorUI.LayoutSubheaderLabel("TTS Preload Data");
|
||||
if (WitEditorUI.LayoutTextButton("Add Voice"))
|
||||
{
|
||||
AddVoice();
|
||||
updated = true;
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
EditorGUILayout.Space();
|
||||
|
||||
// Indent
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
// Generate
|
||||
if (Settings.data == null)
|
||||
{
|
||||
Settings.data = new TTSPreloadData();
|
||||
}
|
||||
if (Settings.data.voices == null)
|
||||
{
|
||||
Settings.data.voices = new TTSPreloadVoiceData[] {new TTSPreloadVoiceData()};
|
||||
}
|
||||
|
||||
// Begin scroll
|
||||
for (int v = 0; v < Settings.data.voices.Length; v++)
|
||||
{
|
||||
if (!LayoutVoiceData(Settings.data, v, ref updated))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Set dirty
|
||||
if (updated)
|
||||
{
|
||||
EditorUtility.SetDirty(Settings);
|
||||
}
|
||||
|
||||
// Indent
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
// Layout
|
||||
private bool LayoutVoiceData(TTSPreloadData preloadData, int voiceIndex, ref bool updated)
|
||||
{
|
||||
// Indent
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
// Get data
|
||||
TTSPreloadVoiceData voiceData = preloadData.voices[voiceIndex];
|
||||
string voiceID = voiceData.presetVoiceID;
|
||||
if (string.IsNullOrEmpty(voiceID))
|
||||
{
|
||||
voiceID = "No Voice Selected";
|
||||
}
|
||||
voiceID = $"{(voiceIndex+1)} - {voiceID}";
|
||||
|
||||
// Foldout
|
||||
GUILayout.BeginHorizontal();
|
||||
bool show = WitEditorUI.LayoutFoldout(new GUIContent(voiceID), voiceData);
|
||||
if (!show)
|
||||
{
|
||||
GUILayout.EndHorizontal();
|
||||
EditorGUI.indentLevel--;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Delete
|
||||
if (WitEditorUI.LayoutTextButton("Delete Voice"))
|
||||
{
|
||||
DeleteVoice(voiceIndex);
|
||||
GUILayout.EndHorizontal();
|
||||
EditorGUI.indentLevel--;
|
||||
updated = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Begin Voice Data
|
||||
GUILayout.EndHorizontal();
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
// Voice Text Field
|
||||
if (TtsService == null || _ttsVoiceIDs == null || _ttsVoiceIDs.Count == 0)
|
||||
{
|
||||
WitEditorUI.LayoutTextField(new GUIContent("Voice ID"), ref voiceData.presetVoiceID, ref updated);
|
||||
}
|
||||
// Voice Preset Select
|
||||
else
|
||||
{
|
||||
int presetIndex = _ttsVoiceIDs.IndexOf(voiceData.presetVoiceID);
|
||||
bool presetUpdated = false;
|
||||
WitEditorUI.LayoutPopup("Voice ID", _ttsVoiceIDs.ToArray(), ref presetIndex, ref presetUpdated);
|
||||
if (presetUpdated)
|
||||
{
|
||||
voiceData.presetVoiceID = _ttsVoiceIDs[presetIndex];
|
||||
string l = string.Empty;
|
||||
TTSPreloadUtility.RefreshVoiceData(TtsService, voiceData, null, ref l);
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure phrases exist
|
||||
if (voiceData.phrases == null)
|
||||
{
|
||||
voiceData.phrases = new TTSPreloadPhraseData[] { };
|
||||
}
|
||||
|
||||
// Phrase Foldout
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
bool isLayout = WitEditorUI.LayoutFoldout(new GUIContent($"Phrases ({voiceData.phrases.Length})"),
|
||||
voiceData.phrases);
|
||||
if (WitEditorUI.LayoutTextButton("Add Phrase"))
|
||||
{
|
||||
TTSPreloadPhraseData lastPhrase = voiceData.phrases.Length == 0 ? null : voiceData.phrases[voiceData.phrases.Length - 1];
|
||||
voiceData.phrases = AddArrayItem<TTSPreloadPhraseData>(voiceData.phrases, new TTSPreloadPhraseData()
|
||||
{
|
||||
textToSpeak = lastPhrase?.textToSpeak,
|
||||
clipID = lastPhrase?.clipID
|
||||
});
|
||||
GUILayout.EndHorizontal();
|
||||
EditorGUI.indentLevel--;
|
||||
updated = true;
|
||||
return false;
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
if (isLayout)
|
||||
{
|
||||
for (int p = 0; p < voiceData.phrases.Length; p++)
|
||||
{
|
||||
if (!LayoutPhraseData(voiceData, p, ref updated))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// End Voice Data
|
||||
EditorGUILayout.Space();
|
||||
EditorGUI.indentLevel--;
|
||||
EditorGUI.indentLevel--;
|
||||
return true;
|
||||
}
|
||||
// Layout phrase data
|
||||
private bool LayoutPhraseData(TTSPreloadVoiceData voiceData, int phraseIndex, ref bool updated)
|
||||
{
|
||||
// Begin Phrase
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
// Get data
|
||||
TTSPreloadPhraseData phraseData = voiceData.phrases[phraseIndex];
|
||||
string title = $"{(phraseIndex+1)} - {phraseData.textToSpeak}";
|
||||
|
||||
// Foldout
|
||||
GUILayout.BeginHorizontal();
|
||||
bool show = WitEditorUI.LayoutFoldout(new GUIContent(title), phraseData);
|
||||
if (!show)
|
||||
{
|
||||
GUILayout.EndHorizontal();
|
||||
EditorGUI.indentLevel--;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Delete
|
||||
if (WitEditorUI.LayoutTextButton("Delete Phrase"))
|
||||
{
|
||||
voiceData.phrases = DeleteArrayItem<TTSPreloadPhraseData>(voiceData.phrases, phraseIndex);
|
||||
GUILayout.EndHorizontal();
|
||||
EditorGUI.indentLevel--;
|
||||
updated = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Begin phrase Data
|
||||
GUILayout.EndHorizontal();
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
// Phrase
|
||||
bool phraseChange = false;
|
||||
WitEditorUI.LayoutTextField(new GUIContent("Phrase"), ref phraseData.textToSpeak, ref phraseChange);
|
||||
if (phraseChange)
|
||||
{
|
||||
TTSPreloadUtility.RefreshPhraseData(TtsService, new TTSDiskCacheSettings()
|
||||
{
|
||||
DiskCacheLocation = TTSDiskCacheLocation.Preload
|
||||
}, TtsService?.GetPresetVoiceSettings(voiceData.presetVoiceID), phraseData);
|
||||
updated = true;
|
||||
}
|
||||
|
||||
// Clip
|
||||
string clipID = phraseData.clipID;
|
||||
WitEditorUI.LayoutTextField(new GUIContent("Clip ID"), ref clipID, ref phraseChange);
|
||||
|
||||
// State
|
||||
Color col = GUI.color;
|
||||
Color stateColor = Color.green;
|
||||
string stateValue = "Downloaded";
|
||||
if (!phraseData.downloaded)
|
||||
{
|
||||
if (phraseData.downloadProgress <= 0f)
|
||||
{
|
||||
stateColor = Color.red;
|
||||
stateValue = "Missing";
|
||||
}
|
||||
else
|
||||
{
|
||||
stateColor = Color.yellow;
|
||||
stateValue = $"Downloading {(phraseData.downloadProgress * 100f):00.0}%";
|
||||
}
|
||||
}
|
||||
GUI.color = stateColor;
|
||||
WitEditorUI.LayoutKeyLabel("State", stateValue);
|
||||
GUI.color = col;
|
||||
|
||||
// End Phrase
|
||||
EditorGUILayout.Space();
|
||||
EditorGUI.indentLevel--;
|
||||
EditorGUI.indentLevel--;
|
||||
return true;
|
||||
}
|
||||
// Add
|
||||
private T[] AddArrayItem<T>(T[] array, T item) => EditArray<T>(array, (l) => l.Add(item));
|
||||
// Delete
|
||||
private T[] DeleteArrayItem<T>(T[] array, int index) => EditArray<T>(array, (l) => l.RemoveAt(index));
|
||||
// Edit array
|
||||
private T[] EditArray<T>(T[] array, Action<List<T>> edit)
|
||||
{
|
||||
// Generate list
|
||||
List<T> list = new List<T>();
|
||||
|
||||
// Add array to list
|
||||
if (array != null)
|
||||
{
|
||||
list.AddRange(array);
|
||||
}
|
||||
|
||||
// Call edit action
|
||||
edit(list);
|
||||
|
||||
// Set to array
|
||||
T[] result = list.ToArray();
|
||||
|
||||
// Refresh foldout value
|
||||
WitEditorUI.SetFoldoutValue(result, WitEditorUI.GetFoldoutValue(array));
|
||||
|
||||
// Return array
|
||||
return result;
|
||||
}
|
||||
//
|
||||
private void AddVoice()
|
||||
{
|
||||
List<TTSPreloadVoiceData> voices = new List<TTSPreloadVoiceData>();
|
||||
if (Settings?.data?.voices != null)
|
||||
{
|
||||
voices.AddRange(Settings.data.voices);
|
||||
}
|
||||
voices.Add(new TTSPreloadVoiceData()
|
||||
{
|
||||
presetVoiceID = _ttsVoiceIDs == null || _ttsVoiceIDs.Count == 0 ? "" : _ttsVoiceIDs[0],
|
||||
phrases = new TTSPreloadPhraseData[] { new TTSPreloadPhraseData() }
|
||||
});
|
||||
Settings.data.voices = voices.ToArray();
|
||||
}
|
||||
// Delete voice
|
||||
private void DeleteVoice(int index)
|
||||
{
|
||||
// Invalid
|
||||
if (Settings?.data?.voices == null || index < 0 || index >= Settings.data.voices.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Cancelled
|
||||
if (!EditorUtility.DisplayDialog("Delete Voice?",
|
||||
$"Are you sure you would like to remove voice data:\n#{(index + 1)} - {Settings.data.voices[index].presetVoiceID}?",
|
||||
"Okay", "Cancel"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove
|
||||
List<TTSPreloadVoiceData> voices = new List<TTSPreloadVoiceData>(Settings.data.voices);
|
||||
voices.RemoveAt(index);
|
||||
Settings.data.voices = voices.ToArray();
|
||||
}
|
||||
// Get voice ids
|
||||
private List<string> GetVoiceIDs(TTSService service)
|
||||
{
|
||||
List<string> results = new List<string>();
|
||||
if (service != null)
|
||||
{
|
||||
foreach (var voiceSetting in service.GetAllPresetVoiceSettings())
|
||||
{
|
||||
if (voiceSetting != null && !string.IsNullOrEmpty(voiceSetting.SettingsId) &&
|
||||
!results.Contains(voiceSetting.SettingsId))
|
||||
{
|
||||
results.Add(voiceSetting.SettingsId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af9c44712a1f27646a9538dbb053e961
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+632
@@ -0,0 +1,632 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Meta.WitAi.TTS.Data;
|
||||
using Meta.WitAi.Data.Configuration;
|
||||
using Meta.WitAi.Json;
|
||||
using Meta.WitAi.TTS.Utilities;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace Meta.WitAi.TTS.Preload
|
||||
{
|
||||
public static class TTSPreloadUtility
|
||||
{
|
||||
#region MANAGEMENT
|
||||
/// <summary>
|
||||
/// Create a new preload settings asset by prompting a save location
|
||||
/// </summary>
|
||||
public static TTSPreloadSettings CreatePreloadSettings()
|
||||
{
|
||||
string savePath = WitConfigurationUtility.GetFileSaveDirectory("Save TTS Preload Settings", "TTSPreloadSettings", "asset");
|
||||
return CreatePreloadSettings(savePath);
|
||||
}
|
||||
/// <summary>
|
||||
/// Create a new preload settings asset at specified location
|
||||
/// </summary>
|
||||
public static TTSPreloadSettings CreatePreloadSettings(string savePath)
|
||||
{
|
||||
// Ignore if empty
|
||||
if (string.IsNullOrEmpty(savePath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get asset path
|
||||
string assetPath = savePath.Replace("\\", "/");
|
||||
if (!assetPath.StartsWith(Application.dataPath))
|
||||
{
|
||||
VLog.E(
|
||||
$"TTS Preload Utility - Cannot Create Setting Outside of Assets Directory\nPath: {assetPath}");
|
||||
return null;
|
||||
}
|
||||
assetPath = assetPath.Replace(Application.dataPath, "Assets");
|
||||
|
||||
// Generate & save
|
||||
TTSPreloadSettings settings = ScriptableObject.CreateInstance<TTSPreloadSettings>();
|
||||
AssetDatabase.CreateAsset(settings, assetPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
// Reload & return
|
||||
return AssetDatabase.LoadAssetAtPath<TTSPreloadSettings>(assetPath);
|
||||
}
|
||||
/// <summary>
|
||||
/// Find all preload settings currently in the Assets directory
|
||||
/// </summary>
|
||||
public static TTSPreloadSettings[] GetPreloadSettings()
|
||||
{
|
||||
List<TTSPreloadSettings> results = new List<TTSPreloadSettings>();
|
||||
string[] guids = AssetDatabase.FindAssets("t:TTSPreloadSettings");
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
TTSPreloadSettings settings = AssetDatabase.LoadAssetAtPath<TTSPreloadSettings>(path);
|
||||
results.Add(settings);
|
||||
}
|
||||
return results.ToArray();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region ITERATE
|
||||
// Performer
|
||||
public static CoroutineUtility.CoroutinePerformer _performer;
|
||||
//
|
||||
public delegate IEnumerator TTSPreloadIterateDelegate(TTSService service, TTSDiskCacheSettings cacheSettings, TTSVoiceSettings voiceSettings, TTSPreloadPhraseData phraseData, Action<float> onProgress, Action<string> onComplete);
|
||||
// Iterating
|
||||
public static bool IsIterating()
|
||||
{
|
||||
return _performer != null;
|
||||
}
|
||||
// Perform a check of all data
|
||||
private static bool CheckIterateData(TTSService service, TTSPreloadData preloadData, TTSPreloadIterateDelegate onIterate, Action<float> onProgress, Action<string> onComplete)
|
||||
{
|
||||
// No service
|
||||
if (service == null)
|
||||
{
|
||||
onProgress?.Invoke(1f);
|
||||
onComplete?.Invoke("\nNo TTSService found in current scene");
|
||||
return false;
|
||||
}
|
||||
// No preload data
|
||||
if (preloadData == null)
|
||||
{
|
||||
onProgress?.Invoke(1f);
|
||||
onComplete?.Invoke("\nTTS Preload Data Not Found");
|
||||
return false;
|
||||
}
|
||||
// No preload data
|
||||
if (preloadData.voices == null)
|
||||
{
|
||||
onProgress?.Invoke(1f);
|
||||
onComplete?.Invoke("\nTTS Preload Data Voices Not Found");
|
||||
return false;
|
||||
}
|
||||
// Ignore if running
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
onProgress?.Invoke(1f);
|
||||
onComplete?.Invoke("Cannot preload while running");
|
||||
return false;
|
||||
}
|
||||
// Ignore if running
|
||||
if (onIterate == null)
|
||||
{
|
||||
onProgress?.Invoke(1f);
|
||||
onComplete?.Invoke("Code recompiled mid update");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Iterate phrases
|
||||
private static void IteratePhrases(TTSService service, TTSPreloadData preloadData, TTSPreloadIterateDelegate onIterate, Action<float> onProgress, Action<string> onComplete)
|
||||
{
|
||||
// Skip if check fails
|
||||
if (!CheckIterateData(service, preloadData, onIterate, onProgress, onComplete))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Unload previous coroutine performer
|
||||
if (_performer != null)
|
||||
{
|
||||
_performer.gameObject.DestroySafely();
|
||||
_performer = null;
|
||||
}
|
||||
|
||||
// Run new coroutine
|
||||
_performer = CoroutineUtility.StartCoroutine(PerformIteratePhrases(service, preloadData, onIterate, onProgress, onComplete));
|
||||
}
|
||||
// Perform iterate
|
||||
private static IEnumerator PerformIteratePhrases(TTSService service, TTSPreloadData preloadData, TTSPreloadIterateDelegate onIterate, Action<float> onProgress, Action<string> onComplete)
|
||||
{
|
||||
// Get cache settings
|
||||
TTSDiskCacheSettings cacheSettings = new TTSDiskCacheSettings()
|
||||
{
|
||||
DiskCacheLocation = TTSDiskCacheLocation.Preload
|
||||
};
|
||||
|
||||
// Get total phrases
|
||||
int phraseTotal = 0;
|
||||
foreach (var voice in preloadData.voices)
|
||||
{
|
||||
if (voice.phrases == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
foreach (var phrase in voice.phrases)
|
||||
{
|
||||
phraseTotal++;
|
||||
}
|
||||
}
|
||||
|
||||
// Begin
|
||||
onProgress?.Invoke(0f);
|
||||
|
||||
// Iterate
|
||||
int phraseCount = 0;
|
||||
float phraseInc = 1f / (float)phraseTotal;
|
||||
string log = string.Empty;
|
||||
for (int v = 0; v < preloadData.voices.Length; v++)
|
||||
{
|
||||
// Get voice data
|
||||
TTSPreloadVoiceData voiceData = preloadData.voices[v];
|
||||
if (voiceData.phrases == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get voice
|
||||
TTSVoiceSettings voiceSettings = service.GetPresetVoiceSettings(voiceData.presetVoiceID);
|
||||
if (voiceSettings == null)
|
||||
{
|
||||
log += "\n-Missing Voice Setting: " + voiceData.presetVoiceID;
|
||||
phraseCount += voiceData.phrases.Length;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Iterate phrases
|
||||
for (int p = 0; p < voiceData.phrases.Length; p++)
|
||||
{
|
||||
// Iterate progress
|
||||
float progress = (float) phraseCount / (float) phraseTotal;
|
||||
onProgress?.Invoke(progress);
|
||||
phraseCount++;
|
||||
|
||||
// Iterate Load
|
||||
yield return onIterate(service, cacheSettings, voiceSettings, voiceData.phrases[p],
|
||||
(p2) => onProgress?.Invoke(progress + p2 * phraseInc), (l) => log += l);
|
||||
|
||||
// Skip if check fails
|
||||
if (!CheckIterateData(service, preloadData, onIterate, onProgress, onComplete))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Complete
|
||||
onProgress?.Invoke(1f);
|
||||
onComplete?.Invoke(log);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region PRELOAD
|
||||
// Can preload data
|
||||
public static bool CanPreloadData()
|
||||
{
|
||||
return TTSService.Instance != null;
|
||||
}
|
||||
// Preload from data
|
||||
public static void PreloadData(TTSService service, TTSPreloadData preloadData, Action<float> onProgress, Action<TTSPreloadData, string> onComplete)
|
||||
{
|
||||
IteratePhrases(service, preloadData, PreloadPhraseData, onProgress, (l) => onComplete?.Invoke(preloadData, l));
|
||||
}
|
||||
// Preload voice text
|
||||
private static IEnumerator PreloadPhraseData(TTSService service, TTSDiskCacheSettings cacheSettings, TTSVoiceSettings voiceSettings, TTSPreloadPhraseData phraseData, Action<float> onProgress, Action<string> onComplete)
|
||||
{
|
||||
// Begin running
|
||||
bool running = true;
|
||||
|
||||
// Download
|
||||
string log = string.Empty;
|
||||
service.DownloadToDiskCache(phraseData.textToSpeak, string.Empty, voiceSettings, cacheSettings, delegate(TTSClipData data, string path, string error)
|
||||
{
|
||||
// Set phrase data
|
||||
phraseData.clipID = data.clipID;
|
||||
phraseData.downloaded = string.IsNullOrEmpty(error);
|
||||
// Failed
|
||||
if (!phraseData.downloaded)
|
||||
{
|
||||
log += $"\n-{voiceSettings.SettingsId} Preload Failed: {phraseData.textToSpeak}";
|
||||
}
|
||||
// Next
|
||||
running = false;
|
||||
});
|
||||
|
||||
// Wait for running to complete
|
||||
while (running)
|
||||
{
|
||||
//Debug.Log($"Preload Wait: {voiceSettings.SettingsId} - {phraseData.textToSpeak}");
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Invoke
|
||||
onComplete?.Invoke(log);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region REFRESH
|
||||
// Refresh
|
||||
public static void RefreshPreloadData(TTSService service, TTSPreloadData preloadData, Action<float> onProgress, Action<TTSPreloadData, string> onComplete)
|
||||
{
|
||||
IteratePhrases(service, preloadData, RefreshPhraseData, onProgress, (l) => onComplete?.Invoke(preloadData, l));
|
||||
}
|
||||
// Refresh
|
||||
private static IEnumerator RefreshPhraseData(TTSService service, TTSDiskCacheSettings cacheSettings, TTSVoiceSettings voiceSettings, TTSPreloadPhraseData phraseData, Action<float> onProgress, Action<string> onComplete)
|
||||
{
|
||||
RefreshPhraseData(service, cacheSettings, voiceSettings, phraseData);
|
||||
yield return null;
|
||||
onComplete?.Invoke(string.Empty);
|
||||
}
|
||||
// Refresh phrase data
|
||||
public static void RefreshVoiceData(TTSService service, TTSPreloadVoiceData voiceData, TTSDiskCacheSettings cacheSettings, ref string log)
|
||||
{
|
||||
// Get voice settings
|
||||
if (service == null)
|
||||
{
|
||||
log += "\n-No TTS service found";
|
||||
return;
|
||||
}
|
||||
// No voice data
|
||||
if (voiceData == null)
|
||||
{
|
||||
log += "\n-No voice data provided";
|
||||
return;
|
||||
}
|
||||
// Get voice
|
||||
TTSVoiceSettings voiceSettings = service.GetPresetVoiceSettings(voiceData.presetVoiceID);
|
||||
if (voiceSettings == null)
|
||||
{
|
||||
log += "\n-Missing Voice Setting: " + voiceData.presetVoiceID;
|
||||
return;
|
||||
}
|
||||
// Generate
|
||||
if (cacheSettings == null)
|
||||
{
|
||||
cacheSettings = new TTSDiskCacheSettings()
|
||||
{
|
||||
DiskCacheLocation = TTSDiskCacheLocation.Preload
|
||||
};
|
||||
}
|
||||
|
||||
// Iterate phrases
|
||||
for (int p = 0; p < voiceData.phrases.Length; p++)
|
||||
{
|
||||
RefreshPhraseData(service, cacheSettings, voiceSettings, voiceData.phrases[p]);
|
||||
}
|
||||
}
|
||||
// Refresh phrase data
|
||||
public static void RefreshPhraseData(TTSService service, TTSDiskCacheSettings cacheSettings, TTSVoiceSettings voiceSettings, TTSPreloadPhraseData phraseData)
|
||||
{
|
||||
// Get voice settings
|
||||
if (service == null || voiceSettings == null || string.IsNullOrEmpty(phraseData.textToSpeak))
|
||||
{
|
||||
phraseData.clipID = string.Empty;
|
||||
phraseData.downloaded = false;
|
||||
phraseData.downloadProgress = 0f;
|
||||
return;
|
||||
}
|
||||
if (cacheSettings == null)
|
||||
{
|
||||
cacheSettings = new TTSDiskCacheSettings()
|
||||
{
|
||||
DiskCacheLocation = TTSDiskCacheLocation.Preload
|
||||
};
|
||||
}
|
||||
|
||||
// Get phrase data
|
||||
phraseData.clipID = service.GetClipID(phraseData.textToSpeak, voiceSettings);
|
||||
|
||||
// Check if file exists
|
||||
string path = service.GetDiskCachePath(phraseData.textToSpeak, phraseData.clipID, voiceSettings, cacheSettings);
|
||||
phraseData.downloaded = File.Exists(path);
|
||||
phraseData.downloadProgress = phraseData.downloaded ? 1f : 0f;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region DELETE
|
||||
// Clear all clips in a tts preload file
|
||||
public static void DeleteData(TTSService service)
|
||||
{
|
||||
// Get test file path
|
||||
string path = service.GetDiskCachePath(string.Empty, "TEST", null, new TTSDiskCacheSettings()
|
||||
{
|
||||
DiskCacheLocation = TTSDiskCacheLocation.Preload
|
||||
});
|
||||
// Get directory
|
||||
string directory = new FileInfo(path).DirectoryName;
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Ask
|
||||
if (!EditorUtility.DisplayDialog("Delete Preload Cache",
|
||||
$"Are you sure you would like to delete the TTS Preload directory at:\n{directory}?", "Okay", "Cancel"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete recursively
|
||||
Directory.Delete(directory, true);
|
||||
// Delete meta
|
||||
string meta = directory + ".meta";
|
||||
if (File.Exists(meta))
|
||||
{
|
||||
File.Delete(meta);
|
||||
}
|
||||
// Refresh assets
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region IMPORT
|
||||
/// <summary>
|
||||
/// Prompt user for a json file to be imported into an existing TTSPreloadSettings asset
|
||||
/// </summary>
|
||||
public static bool ImportData(TTSPreloadSettings preloadSettings)
|
||||
{
|
||||
// Select a file
|
||||
string textFilePath = EditorUtility.OpenFilePanel("Select TTS Preload Json", Application.dataPath, "json");
|
||||
if (string.IsNullOrEmpty(textFilePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// Import with selected file path
|
||||
return ImportData(preloadSettings, textFilePath);
|
||||
}
|
||||
/// <summary>
|
||||
/// Imported json data into an existing TTSPreloadSettings asset
|
||||
/// </summary>
|
||||
public static bool ImportData(TTSPreloadSettings preloadSettings, string textFilePath)
|
||||
{
|
||||
// Check for file
|
||||
if (!File.Exists(textFilePath))
|
||||
{
|
||||
VLog.E($"TTS Preload Utility - Preload file does not exist\nPath: {textFilePath}");
|
||||
return false;
|
||||
}
|
||||
// Load file
|
||||
string textFileContents = File.ReadAllText(textFilePath);
|
||||
if (string.IsNullOrEmpty(textFileContents))
|
||||
{
|
||||
VLog.E($"TTS Preload Utility - Preload file load failed\nPath: {textFilePath}");
|
||||
return false;
|
||||
}
|
||||
// Parse file
|
||||
WitResponseNode node = WitResponseNode.Parse(textFileContents);
|
||||
if (node == null)
|
||||
{
|
||||
VLog.E($"TTS Preload Utility - Preload file parse failed\nPath: {textFilePath}");
|
||||
return false;
|
||||
}
|
||||
// Iterate children for texts
|
||||
WitResponseClass data = node.AsObject;
|
||||
Dictionary<string, List<string>> textsByVoice = new Dictionary<string, List<string>>();
|
||||
foreach (var voiceName in data.ChildNodeNames)
|
||||
{
|
||||
// Get texts list
|
||||
List<string> texts;
|
||||
if (textsByVoice.ContainsKey(voiceName))
|
||||
{
|
||||
texts = textsByVoice[voiceName];
|
||||
}
|
||||
else
|
||||
{
|
||||
texts = new List<string>();
|
||||
}
|
||||
|
||||
// Add text phrases
|
||||
string[] voicePhrases = data[voiceName].AsStringArray;
|
||||
if (voicePhrases != null)
|
||||
{
|
||||
foreach (var phrase in voicePhrases)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(phrase) && !texts.Contains(phrase))
|
||||
{
|
||||
texts.Add(phrase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply
|
||||
textsByVoice[voiceName] = texts;
|
||||
}
|
||||
// Import
|
||||
return ImportData(preloadSettings, textsByVoice);
|
||||
}
|
||||
/// <summary>
|
||||
/// Find all ITTSPhraseProviders loaded in scenes & generate
|
||||
/// data file to import all phrases associated with the files.
|
||||
/// </summary>
|
||||
public static bool ImportPhrases(TTSPreloadSettings preloadSettings)
|
||||
{
|
||||
// Find phrase providers in all scenes
|
||||
List<ITTSPhraseProvider> phraseProviders = new List<ITTSPhraseProvider>();
|
||||
for (int s = 0; s < SceneManager.sceneCount; s++)
|
||||
{
|
||||
Scene scene = SceneManager.GetSceneAt(s);
|
||||
foreach (var root in scene.GetRootGameObjects())
|
||||
{
|
||||
ITTSPhraseProvider[] found = root.GetComponentsInChildren<ITTSPhraseProvider>(true);
|
||||
if (found != null)
|
||||
{
|
||||
phraseProviders.AddRange(found);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Get all phrases by voice id
|
||||
Dictionary<string, List<string>> textsByVoice = new Dictionary<string, List<string>>();
|
||||
foreach (var phraseProvider in phraseProviders)
|
||||
{
|
||||
// Ignore if no voices are found
|
||||
List<string> voiceIds = phraseProvider.GetVoiceIds();
|
||||
if (voiceIds == null || voiceIds.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Iterate voice ids
|
||||
foreach (var voiceId in voiceIds)
|
||||
{
|
||||
// Ignore empty voice id
|
||||
if (string.IsNullOrEmpty(voiceId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore if phrases are null
|
||||
List<string> phrases = phraseProvider.GetVoicePhrases(voiceId);
|
||||
if (phrases == null || phrases.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get phrase list
|
||||
List<string> voicePhrases;
|
||||
if (textsByVoice.ContainsKey(voiceId))
|
||||
{
|
||||
voicePhrases = textsByVoice[voiceId];
|
||||
}
|
||||
else
|
||||
{
|
||||
voicePhrases = new List<string>();
|
||||
}
|
||||
|
||||
// Append unique phrases
|
||||
foreach (var phrase in phrases)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(phrase) && !voicePhrases.Contains(phrase))
|
||||
{
|
||||
voicePhrases.Add(phrase);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply phrase list
|
||||
textsByVoice[voiceId] = voicePhrases;
|
||||
}
|
||||
}
|
||||
// Import with data
|
||||
return ImportData(preloadSettings, textsByVoice);
|
||||
}
|
||||
/// <summary>
|
||||
/// Imported dictionary data into an existing TTSPreloadSettings asset
|
||||
/// </summary>
|
||||
public static bool ImportData(TTSPreloadSettings preloadSettings, Dictionary<string, List<string>> textsByVoice)
|
||||
{
|
||||
// Import
|
||||
if (preloadSettings == null)
|
||||
{
|
||||
VLog.E("TTS Preload Utility - Import Failed - Null Preload Settings");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whether or not changed
|
||||
bool changed = false;
|
||||
|
||||
// Generate if needed
|
||||
if (preloadSettings.data == null)
|
||||
{
|
||||
preloadSettings.data = new TTSPreloadData();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// Begin voice list
|
||||
List<TTSPreloadVoiceData> voices = new List<TTSPreloadVoiceData>();
|
||||
if (preloadSettings.data.voices != null)
|
||||
{
|
||||
voices.AddRange(preloadSettings.data.voices);
|
||||
}
|
||||
|
||||
// Iterate voice names
|
||||
foreach (var voiceName in textsByVoice.Keys)
|
||||
{
|
||||
// Get voice index if possible
|
||||
int voiceIndex = voices.FindIndex((v) => string.Equals(v.presetVoiceID, voiceName));
|
||||
|
||||
// Generate voice
|
||||
TTSPreloadVoiceData voice;
|
||||
if (voiceIndex == -1)
|
||||
{
|
||||
voice = new TTSPreloadVoiceData();
|
||||
voice.presetVoiceID = voiceName;
|
||||
voiceIndex = voices.Count;
|
||||
voices.Add(voice);
|
||||
}
|
||||
// Use existing
|
||||
else
|
||||
{
|
||||
voice = voices[voiceIndex];
|
||||
}
|
||||
|
||||
// Get texts & phrases for current voice
|
||||
List<string> texts = new List<string>();
|
||||
List<TTSPreloadPhraseData> phrases = new List<TTSPreloadPhraseData>();
|
||||
if (voice.phrases != null)
|
||||
{
|
||||
foreach (var phrase in voice.phrases)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(phrase.textToSpeak) && !texts.Contains(phrase.textToSpeak))
|
||||
{
|
||||
texts.Add(phrase.textToSpeak);
|
||||
phrases.Add(phrase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get data
|
||||
List<string> newTexts = textsByVoice[voiceName];
|
||||
if (newTexts != null)
|
||||
{
|
||||
foreach (var newText in newTexts)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(newText) && !texts.Contains(newText))
|
||||
{
|
||||
changed = true;
|
||||
texts.Add(newText);
|
||||
phrases.Add(new TTSPreloadPhraseData()
|
||||
{
|
||||
textToSpeak = newText
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply voice
|
||||
voice.phrases = phrases.ToArray();
|
||||
voices[voiceIndex] = voice;
|
||||
}
|
||||
|
||||
// Apply data
|
||||
if (changed)
|
||||
{
|
||||
preloadSettings.data.voices = voices.ToArray();
|
||||
EditorUtility.SetDirty(preloadSettings);
|
||||
}
|
||||
|
||||
// Return changed
|
||||
return changed;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6428a97eb23d27b48bff4ecaa464004e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Meta.WitAi.Data.Configuration;
|
||||
using Meta.WitAi.TTS.Integrations;
|
||||
using Meta.WitAi.TTS.Utilities;
|
||||
using Meta.WitAi.Data.Info;
|
||||
using Meta.WitAi.Lib;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Meta.WitAi.TTS
|
||||
{
|
||||
public static class TTSEditorUtilities
|
||||
{
|
||||
// Default TTS Setup
|
||||
public static Transform CreateDefaultSetup()
|
||||
{
|
||||
// Generate parent
|
||||
Transform parent = GenerateGameObject("TTS").transform;
|
||||
|
||||
// Add TTS Service
|
||||
TTSService service = CreateService(parent);
|
||||
|
||||
// Add TTS Speaker
|
||||
CreateSpeaker(parent, service);
|
||||
|
||||
// Select parent
|
||||
Selection.activeObject = parent.gameObject;
|
||||
return parent;
|
||||
}
|
||||
|
||||
// Default TTS Service
|
||||
public static TTSService CreateService(Transform parent = null, bool ignoreErrors = false)
|
||||
{
|
||||
// Get parent
|
||||
if (parent == null)
|
||||
{
|
||||
Transform selected = Selection.activeTransform;
|
||||
if (selected != null && selected.gameObject.scene.rootCount > 0)
|
||||
{
|
||||
parent = Selection.activeTransform;
|
||||
}
|
||||
}
|
||||
// Ignore if found
|
||||
TTSService instance = GameObject.FindObjectOfType<TTSService>();
|
||||
if (instance != null)
|
||||
{
|
||||
// Log
|
||||
if (!ignoreErrors)
|
||||
{
|
||||
VLog.W($"TTS Service - A TTSService is already in scene\nGameObject: {instance.gameObject.name}");
|
||||
}
|
||||
|
||||
// Move into parent
|
||||
if (parent != null)
|
||||
{
|
||||
instance.transform.SetParent(parent, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate TTSWit
|
||||
else
|
||||
{
|
||||
instance = CreateWitService(parent);
|
||||
}
|
||||
|
||||
// Select & return instance
|
||||
Selection.activeObject = instance.gameObject;
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Default TTS Service
|
||||
private static TTSWit CreateWitService(Transform parent = null)
|
||||
{
|
||||
// Generate new TTSWit & add caches
|
||||
TTSWit ttsWit = GenerateGameObject("TTSWitService", parent).AddComponent<TTSWit>();
|
||||
ttsWit.gameObject.AddComponent<TTSRuntimeCache>();
|
||||
ttsWit.gameObject.AddComponent<TTSDiskCache>();
|
||||
VLog.D($"TTS Service - Instantiated Service {ttsWit.gameObject.name}");
|
||||
|
||||
// Refresh configuration
|
||||
WitConfiguration configuration = SetupConfiguration(ttsWit);
|
||||
if (configuration != null)
|
||||
{
|
||||
RefreshAvailableVoices(ttsWit);
|
||||
}
|
||||
|
||||
// Log
|
||||
return ttsWit;
|
||||
}
|
||||
|
||||
// Wit configuration
|
||||
private static WitConfiguration SetupConfiguration(TTSService instance)
|
||||
{
|
||||
// Ignore non-tts wit
|
||||
if (instance.GetType() != typeof(TTSWit))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
// Already setup
|
||||
TTSWit ttsWit = instance as TTSWit;
|
||||
if (ttsWit.RequestSettings.configuration != null)
|
||||
{
|
||||
return ttsWit.RequestSettings.configuration;
|
||||
}
|
||||
|
||||
// Refresh configuration list
|
||||
if (WitConfigurationUtility.WitConfigs == null)
|
||||
{
|
||||
WitConfigurationUtility.ReloadConfigurationData();
|
||||
}
|
||||
|
||||
// Assign first wit configuration found
|
||||
if (WitConfigurationUtility.WitConfigs != null && WitConfigurationUtility.WitConfigs.Length > 0)
|
||||
{
|
||||
ttsWit.RequestSettings.configuration = WitConfigurationUtility.WitConfigs[0];
|
||||
VLog.D($"TTS Service - Assigned Wit Configuration {ttsWit.RequestSettings.configuration.name}");
|
||||
}
|
||||
|
||||
// Warning
|
||||
if (ttsWit.RequestSettings.configuration == null)
|
||||
{
|
||||
VLog.W($"TTS Service - Please create and assign a WitConfiguration to TTSWit");
|
||||
}
|
||||
|
||||
// Return configuration
|
||||
return ttsWit.RequestSettings.configuration;
|
||||
}
|
||||
|
||||
// Refresh available voices
|
||||
internal static void RefreshAvailableVoices(TTSWit ttsWit, Action<WitAppInfo> onUpdateComplete = null)
|
||||
{
|
||||
// Fail without configuration
|
||||
if (ttsWit == null)
|
||||
{
|
||||
VLog.W($"TTS Service - Cannot refresh voices without TTS Wit Service");
|
||||
return;
|
||||
}
|
||||
IWitRequestConfiguration configuration = ttsWit.RequestSettings.configuration;
|
||||
if (configuration == null)
|
||||
{
|
||||
VLog.W($"TTS Service - Cannot refresh voices without TTS Wit Configuration");
|
||||
return;
|
||||
}
|
||||
|
||||
// Update app info
|
||||
WitAppInfoUtility.Update(ttsWit.RequestSettings.configuration, (newInfo, r) =>
|
||||
{
|
||||
configuration.SetApplicationInfo(newInfo);
|
||||
UpdatePresets(ttsWit, newInfo);
|
||||
onUpdateComplete?.Invoke(newInfo);
|
||||
});
|
||||
}
|
||||
|
||||
// Add all presets
|
||||
private static void UpdatePresets(TTSWit ttsWit, WitAppInfo appInfo)
|
||||
{
|
||||
// Cannot update presets without voices
|
||||
if (appInfo.voices == null || appInfo.voices.Length == 0)
|
||||
{
|
||||
VLog.W("TTS Refresh failed to find voices");
|
||||
return;
|
||||
}
|
||||
|
||||
// Add all voices to preset voice list
|
||||
if (ttsWit.PresetWitVoiceSettings == null || ttsWit.PresetWitVoiceSettings.Length == 0)
|
||||
{
|
||||
AddPresetsForInfo(ttsWit, appInfo.voices);
|
||||
}
|
||||
|
||||
// Refresh speakers
|
||||
RefreshEmptySpeakers(ttsWit);
|
||||
}
|
||||
|
||||
// Adds a list of voice infos
|
||||
internal static void AddPresetsForInfo(TTSWit ttsWit, WitVoiceInfo[] voiceInfos)
|
||||
{
|
||||
// Ignore without infos
|
||||
if (voiceInfos == null || voiceInfos.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Add all voices to preset voice list
|
||||
List<TTSWitVoiceSettings> voices = new List<TTSWitVoiceSettings>();
|
||||
if (ttsWit.PresetWitVoiceSettings != null)
|
||||
{
|
||||
voices.AddRange(ttsWit.PresetWitVoiceSettings);
|
||||
}
|
||||
foreach (var voiceData in voiceInfos)
|
||||
{
|
||||
voices.Add(GetDefaultVoiceSetting(voiceData));
|
||||
}
|
||||
ttsWit.SetVoiceSettings(voices.ToArray());
|
||||
}
|
||||
|
||||
// Adds a preset for a specific voice
|
||||
internal static void AddPresetForInfo(TTSWit ttsWit, WitVoiceInfo voiceData)
|
||||
{
|
||||
List<TTSWitVoiceSettings> voices = new List<TTSWitVoiceSettings>();
|
||||
if (ttsWit.PresetWitVoiceSettings != null)
|
||||
{
|
||||
voices.AddRange(ttsWit.PresetWitVoiceSettings);
|
||||
}
|
||||
voices.Add(GetDefaultVoiceSetting(voiceData));
|
||||
ttsWit.SetVoiceSettings(voices.ToArray());
|
||||
}
|
||||
|
||||
// Get default voice settings
|
||||
private static TTSWitVoiceSettings GetDefaultVoiceSetting(WitVoiceInfo voiceData)
|
||||
{
|
||||
TTSWitVoiceSettings result = new TTSWitVoiceSettings()
|
||||
{
|
||||
SettingsId = voiceData.name.ToUpper(),
|
||||
voice = voiceData.name
|
||||
};
|
||||
// Use first style provided
|
||||
if (voiceData.styles != null && voiceData.styles.Length > 0)
|
||||
{
|
||||
result.style = voiceData.styles[0];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// Set all blank IDs to default voice id
|
||||
private static void RefreshEmptySpeakers(TTSService service)
|
||||
{
|
||||
string defaultVoiceID = service.VoiceProvider.VoiceDefaultSettings?.SettingsId;
|
||||
foreach (var speaker in GameObject.FindObjectsOfType<TTSSpeaker>())
|
||||
{
|
||||
if (string.IsNullOrEmpty(speaker.presetVoiceID))
|
||||
{
|
||||
speaker.presetVoiceID = defaultVoiceID;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default TTS Speaker
|
||||
public static TTSSpeaker CreateSpeaker(Transform parent = null, TTSService service = null)
|
||||
{
|
||||
// Get parent
|
||||
if (parent == null)
|
||||
{
|
||||
Transform selected = Selection.activeTransform;
|
||||
if (selected != null && selected.gameObject.scene.rootCount > 0)
|
||||
{
|
||||
parent = Selection.activeTransform;
|
||||
}
|
||||
}
|
||||
// Generate service if possible
|
||||
if (service == null)
|
||||
{
|
||||
service = CreateService(parent);
|
||||
}
|
||||
|
||||
// TTS Speaker
|
||||
string goName = typeof(TTSSpeaker).Name;
|
||||
TTSSpeaker speaker = GenerateGameObject(goName, parent).AddComponent<TTSSpeaker>();
|
||||
speaker.presetVoiceID = string.Empty;
|
||||
|
||||
// Audio Source
|
||||
AudioSource audio = GenerateGameObject($"{goName}Audio", speaker.transform).AddComponent<AudioSource>();
|
||||
audio.playOnAwake = false;
|
||||
audio.loop = false;
|
||||
audio.spatialBlend = 0f; // Default to 2D
|
||||
|
||||
// Return speaker
|
||||
VLog.D($"TTS Service - Instantiated Speaker {speaker.gameObject.name}");
|
||||
Selection.activeObject = speaker.gameObject;
|
||||
return speaker;
|
||||
}
|
||||
|
||||
// Generate with specified name
|
||||
private static GameObject GenerateGameObject(string name, Transform parent = null)
|
||||
{
|
||||
Transform result = new GameObject(name).transform;
|
||||
result.SetParent(parent);
|
||||
result.localPosition = Vector3.zero;
|
||||
result.localRotation = Quaternion.identity;
|
||||
result.localScale = Vector3.one;
|
||||
return result.gameObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c590c0f8426e4194db6efc14c68db75c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using Meta.WitAi.TTS.Data;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Meta.WitAi.TTS
|
||||
{
|
||||
[CustomEditor(typeof(TTSService), true)]
|
||||
public class TTSServiceInspector : UnityEditor.Editor
|
||||
{
|
||||
// Service
|
||||
private TTSService _service;
|
||||
// Dropdown
|
||||
private bool _clipFoldout = false;
|
||||
// Maximum text for abbreviated
|
||||
private const int MAX_DISPLAY_TEXT = 20;
|
||||
|
||||
// GUI
|
||||
public sealed override void OnInspectorGUI()
|
||||
{
|
||||
// Display default ui
|
||||
OnEditTimeGUI();
|
||||
OnPlaytimeGUI();
|
||||
}
|
||||
|
||||
protected virtual void OnEditTimeGUI()
|
||||
{
|
||||
base.OnInspectorGUI();
|
||||
}
|
||||
|
||||
protected virtual void OnPlaytimeGUI()
|
||||
{
|
||||
// Ignore if in editor
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get service
|
||||
if (!_service)
|
||||
{
|
||||
_service = target as TTSService;
|
||||
}
|
||||
|
||||
// Add spaces
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.LabelField("Runtime Clip Cache", EditorStyles.boldLabel);
|
||||
|
||||
// No clips
|
||||
TTSClipData[] clips = _service.GetAllRuntimeCachedClips();
|
||||
if (clips == null || clips.Length == 0)
|
||||
{
|
||||
WitEditorUI.LayoutErrorLabel("No clips found");
|
||||
return;
|
||||
}
|
||||
// Has clips
|
||||
_clipFoldout = WitEditorUI.LayoutFoldout(new GUIContent($"Clips: {clips.Length}"), _clipFoldout);
|
||||
if (_clipFoldout)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
// Iterate clips
|
||||
foreach (TTSClipData clip in clips)
|
||||
{
|
||||
// Get display name
|
||||
string displayName = clip.textToSpeak;
|
||||
// Crop if too long
|
||||
if (displayName.Length > MAX_DISPLAY_TEXT)
|
||||
{
|
||||
displayName = displayName.Substring(0, MAX_DISPLAY_TEXT);
|
||||
}
|
||||
// Add voice setting id
|
||||
if (clip.voiceSettings != null)
|
||||
{
|
||||
displayName = $"{clip.voiceSettings.SettingsId} - {displayName}";
|
||||
}
|
||||
// Foldout if desired
|
||||
bool foldout = WitEditorUI.LayoutFoldout(new GUIContent(displayName), clip);
|
||||
if (foldout)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
DrawClipGUI(clip);
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
}
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
}
|
||||
|
||||
// Clip data
|
||||
public static void DrawClipGUI(TTSClipData clip)
|
||||
{
|
||||
// Generation Settings
|
||||
WitEditorUI.LayoutKeyLabel("Text", clip.textToSpeak);
|
||||
EditorGUILayout.TextField("Clip ID", clip.clipID);
|
||||
EditorGUILayout.ObjectField("Clip", clip.clip, typeof(AudioClip), true);
|
||||
|
||||
// Loaded
|
||||
TTSClipLoadState loadState = clip.loadState;
|
||||
if (loadState != TTSClipLoadState.Preparing)
|
||||
{
|
||||
WitEditorUI.LayoutKeyLabel("Load State", loadState.ToString());
|
||||
}
|
||||
// Loading with progress
|
||||
else
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
int loadProgress = Mathf.FloorToInt(clip.loadProgress * 100f);
|
||||
WitEditorUI.LayoutKeyLabel("Load State", $"{loadState} ({loadProgress}%)");
|
||||
GUILayout.HorizontalSlider(loadProgress, 0, 100);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
// Additional Settings
|
||||
WitEditorUI.LayoutKeyObjectLabels("Voice Settings", clip.voiceSettings);
|
||||
WitEditorUI.LayoutKeyObjectLabels("Cache Settings", clip.diskCacheSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a7b031cd5a557e14fa08e59b869a2e78
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Meta.WitAi.TTS.Utilities;
|
||||
using Meta.WitAi.TTS.Data;
|
||||
|
||||
namespace Meta.WitAi.TTS
|
||||
{
|
||||
[CustomEditor(typeof(TTSSpeaker), true)]
|
||||
public class TTSSpeakerInspector : Editor
|
||||
{
|
||||
// Speaker
|
||||
private TTSSpeaker _speaker;
|
||||
private SerializedProperty _presetVoiceProperty;
|
||||
private SerializedProperty _customVoiceProperty;
|
||||
|
||||
// Voices
|
||||
private int _voiceIndex = -1;
|
||||
private string[] _voicePresetIds = null;
|
||||
|
||||
// Voice text
|
||||
private const string UI_PRESET_VOICE_KEY = "Voice Preset";
|
||||
private const string UI_CUSTOM_VOICE_KEY = "Custom Voice";
|
||||
private const string UI_CUSTOM_KEY = "CUSTOM";
|
||||
|
||||
//
|
||||
void OnEnable()
|
||||
{
|
||||
_speaker = target as TTSSpeaker;
|
||||
_presetVoiceProperty = serializedObject.FindProperty("presetVoiceID");
|
||||
_customVoiceProperty = serializedObject.FindProperty("customWitVoiceSettings");
|
||||
}
|
||||
|
||||
// GUI
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
// Display default ui
|
||||
base.OnInspectorGUI();
|
||||
|
||||
// Check voices
|
||||
TTSService tts = _speaker.TTSService;
|
||||
TTSVoiceSettings[] settings = tts?.GetAllPresetVoiceSettings();
|
||||
if (_voicePresetIds == null
|
||||
|| (settings != null && _voicePresetIds.Length != settings.Length + 1)
|
||||
|| (_voiceIndex >= 0 && _voiceIndex < _voicePresetIds.Length - 1 && !string.Equals(_speaker.presetVoiceID, _voicePresetIds[_voiceIndex])))
|
||||
{
|
||||
RefreshVoices(settings);
|
||||
}
|
||||
|
||||
// No preset voices found, assume custom
|
||||
if (_voicePresetIds == null || _voicePresetIds.Length == 0)
|
||||
{
|
||||
GUI.enabled = false;
|
||||
EditorGUILayout.TextField(UI_PRESET_VOICE_KEY, UI_CUSTOM_KEY);
|
||||
GUI.enabled = true;
|
||||
}
|
||||
// Voice dropdown
|
||||
else
|
||||
{
|
||||
bool updated = false;
|
||||
WitEditorUI.LayoutPopup(UI_PRESET_VOICE_KEY, _voicePresetIds, ref _voiceIndex, ref updated);
|
||||
if (updated)
|
||||
{
|
||||
if (_voiceIndex >= 0 && _voiceIndex < _voicePresetIds.Length - 1)
|
||||
{
|
||||
_presetVoiceProperty.stringValue = _voicePresetIds[_voiceIndex];
|
||||
}
|
||||
else
|
||||
{
|
||||
_presetVoiceProperty.stringValue = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add custom layout
|
||||
if (_voicePresetIds == null || _voiceIndex < 0 || _voiceIndex >= _voicePresetIds.Length - 1)
|
||||
{
|
||||
EditorGUILayout.PropertyField(_customVoiceProperty, new GUIContent(UI_CUSTOM_VOICE_KEY));
|
||||
}
|
||||
|
||||
// Apply all modified properties
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
|
||||
// Layout TTS clip queue
|
||||
LayoutClipQueue();
|
||||
}
|
||||
|
||||
// Refresh voices
|
||||
private void RefreshVoices(TTSVoiceSettings[] settings)
|
||||
{
|
||||
// Reset voice data
|
||||
_voiceIndex = -1;
|
||||
_voicePresetIds = null;
|
||||
if (settings == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all ids
|
||||
List<string> presetIds = settings.Select(s => s.SettingsId).ToList();
|
||||
// Get voice index
|
||||
_voiceIndex = presetIds.IndexOf(_speaker.presetVoiceID);
|
||||
if (_voiceIndex == -1)
|
||||
{
|
||||
_voiceIndex = presetIds.Count;
|
||||
}
|
||||
// Add custom key
|
||||
presetIds.Add(UI_CUSTOM_KEY);
|
||||
// Apply preset ids
|
||||
_voicePresetIds = presetIds.ToArray();
|
||||
}
|
||||
|
||||
// Layout clip queue
|
||||
private const string UI_CLIP_HEADER_TEXT = "Clip Queue";
|
||||
private const string UI_CLIP_SPEAKER_TEXT = "Speaker Clip:";
|
||||
private const string UI_CLIP_QUEUE_TEXT = "Loading Clips:";
|
||||
private bool _speakerFoldout = false;
|
||||
private bool _queueFoldout = false;
|
||||
private void LayoutClipQueue()
|
||||
{
|
||||
// Ignore unless playing
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Add header
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.LabelField(UI_CLIP_HEADER_TEXT, EditorStyles.boldLabel);
|
||||
|
||||
// Speaker Foldout
|
||||
_speakerFoldout = EditorGUILayout.Foldout(_speakerFoldout, UI_CLIP_SPEAKER_TEXT);
|
||||
if (_speakerFoldout)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
if (!_speaker.IsSpeaking)
|
||||
{
|
||||
EditorGUILayout.LabelField("None");
|
||||
}
|
||||
else
|
||||
{
|
||||
TTSServiceInspector.DrawClipGUI(_speaker.SpeakingClip);
|
||||
}
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
// Queue Foldout
|
||||
List<TTSClipData> queuedClips = _speaker.QueuedClips;
|
||||
_queueFoldout = EditorGUILayout.Foldout(_queueFoldout, $"{UI_CLIP_QUEUE_TEXT} {(queuedClips == null ? 0 : queuedClips.Count)}");
|
||||
if (_queueFoldout)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
if (queuedClips == null || queuedClips.Count == 0)
|
||||
{
|
||||
EditorGUILayout.LabelField("None");
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < queuedClips.Count; i++)
|
||||
{
|
||||
TTSClipData clipData = queuedClips[i];
|
||||
bool oldFoldout = WitEditorUI.GetFoldoutValue(clipData);
|
||||
bool newFoldout = EditorGUILayout.Foldout(oldFoldout, $"Clip[{i}]");
|
||||
if (oldFoldout != newFoldout)
|
||||
{
|
||||
WitEditorUI.SetFoldoutValue(clipData, newFoldout);
|
||||
}
|
||||
if (newFoldout)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
TTSServiceInspector.DrawClipGUI(clipData);
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
}
|
||||
}
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3835212f72d4d5149bed0f07915f204e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Meta.WitAi.TTS.Integrations;
|
||||
|
||||
namespace Meta.WitAi.TTS
|
||||
{
|
||||
[CustomEditor(typeof(TTSWit), true)]
|
||||
public class TTSWitInspector : TTSServiceInspector
|
||||
{
|
||||
private int selectedBaseVoice;
|
||||
|
||||
protected override void OnEditTimeGUI()
|
||||
{
|
||||
base.OnEditTimeGUI();
|
||||
|
||||
var ttsWit = (TTSWit)target;
|
||||
var config = ttsWit.RequestSettings.configuration;
|
||||
if (!config) return;
|
||||
|
||||
// Get app info for voices
|
||||
var appInfo = config.GetApplicationInfo();
|
||||
if (null != appInfo.voices && appInfo.voices.Length > 0)
|
||||
{
|
||||
// Get all voice names from wit
|
||||
string[] voiceNames = appInfo.voices.Select(v => v.name).ToArray();
|
||||
|
||||
// Add a selected preset
|
||||
GUILayout.BeginHorizontal();
|
||||
selectedBaseVoice = EditorGUILayout.Popup(selectedBaseVoice, voiceNames);
|
||||
GUI.enabled = selectedBaseVoice >= 0 && selectedBaseVoice < appInfo.voices.Length;
|
||||
if (WitEditorUI.LayoutTextButton("Add Preset"))
|
||||
{
|
||||
TTSEditorUtilities.AddPresetForInfo(ttsWit, appInfo.voices[selectedBaseVoice]);
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
// Add all unused presets
|
||||
GUI.enabled = true;
|
||||
if (GUILayout.Button("Add Unused Voices as Presets"))
|
||||
{
|
||||
// Get used voices
|
||||
List<string> usedVoiceNames = ttsWit.PresetWitVoiceSettings.Select(v => v.voice).ToList();
|
||||
|
||||
// Get unused voices
|
||||
var unusedVoices = appInfo.voices.Where(v => !usedVoiceNames.Contains(v.name)).ToArray();
|
||||
|
||||
// Add all unused presets
|
||||
TTSEditorUtilities.AddPresetsForInfo(ttsWit, unusedVoices);
|
||||
}
|
||||
}
|
||||
// Log warning
|
||||
else
|
||||
{
|
||||
GUILayout.Label("There are currently no base presets available. Click refresh to check for updates.", EditorStyles.helpBox);
|
||||
}
|
||||
|
||||
// Refresh button
|
||||
if (GUILayout.Button("Refresh Presets"))
|
||||
{
|
||||
TTSEditorUtilities.RefreshAvailableVoices(ttsWit, info =>
|
||||
{
|
||||
Repaint();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f6696f09b6647cda45870d07c064cab
|
||||
timeCreated: 1681499481
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 69fad6077e2eeef4a812dc81d313fc2a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+322
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEditor;
|
||||
using Meta.WitAi.TTS.Integrations;
|
||||
using Meta.WitAi.Windows;
|
||||
using Meta.WitAi.Data.Info;
|
||||
using Meta.WitAi.Lib;
|
||||
using Meta.WitAi.Data.Configuration;
|
||||
using Meta.WitAi.TTS.Data;
|
||||
using Meta.WitAi.TTS.Utilities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Meta.WitAi.TTS.Voices
|
||||
{
|
||||
[CustomPropertyDrawer(typeof( TTSWitVoiceSettings))]
|
||||
public class TTSWitVoiceSettingsDrawer : PropertyDrawer
|
||||
{
|
||||
// Constants for var layout
|
||||
private const float VAR_HEIGHT = 20f;
|
||||
private const float VAR_MARGIN = 4f;
|
||||
private const float FIELD_HEIGHT = 60f;
|
||||
|
||||
// Constants for var lookup
|
||||
private const string VAR_SETTINGS = "SettingsId";
|
||||
private const string VAR_VOICE = "voice";
|
||||
private const string VAR_STYLE = "style";
|
||||
|
||||
// Voice data
|
||||
private TTSService _ttsService;
|
||||
private bool _onSpeaker;
|
||||
private IWitRequestConfiguration _configuration;
|
||||
private bool _configUpdating;
|
||||
private WitVoiceInfo[] _voices;
|
||||
private string[] _voiceNames;
|
||||
|
||||
// Subfields
|
||||
private static readonly FieldInfo[] _fields = FieldGUI.GetFields(typeof( TTSWitVoiceSettings));
|
||||
|
||||
// Determine height
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
{
|
||||
// Get service
|
||||
if (!_ttsService)
|
||||
{
|
||||
GetService(property, out _ttsService, out _onSpeaker);
|
||||
}
|
||||
// Property
|
||||
if (!property.isExpanded)
|
||||
{
|
||||
return VAR_HEIGHT;
|
||||
}
|
||||
// Add each field + 1 for dropdown
|
||||
int total = _fields.Length + 1;
|
||||
// Remove 3 on speaker
|
||||
if (_onSpeaker)
|
||||
{
|
||||
total -= 3;
|
||||
}
|
||||
// Add 2 for voice properties
|
||||
int voiceIndex = GetVoiceIndex(property);
|
||||
if (voiceIndex != -1)
|
||||
{
|
||||
total += 2;
|
||||
}
|
||||
// Get height
|
||||
float height = total * VAR_HEIGHT + Mathf.Max(0, total - 1) * VAR_MARGIN;
|
||||
// Add fields
|
||||
if (!_onSpeaker)
|
||||
{
|
||||
height += 2 * (FIELD_HEIGHT - VAR_HEIGHT);
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
// Handles gui layout
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
// Get service if needed
|
||||
if (!_ttsService)
|
||||
{
|
||||
GetService(property, out _ttsService, out _onSpeaker);
|
||||
}
|
||||
|
||||
// On gui
|
||||
float y = position.y;
|
||||
string voiceName = property.FindPropertyRelative(VAR_SETTINGS).stringValue;
|
||||
if (string.IsNullOrEmpty(voiceName))
|
||||
{
|
||||
voiceName = property.displayName;
|
||||
}
|
||||
property.isExpanded =
|
||||
EditorGUI.Foldout(new Rect(position.x, y, position.width, VAR_HEIGHT), property.isExpanded, voiceName);
|
||||
if (!property.isExpanded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
y += VAR_HEIGHT + VAR_MARGIN;
|
||||
|
||||
// Increment
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
// Refresh voices if needed
|
||||
RefreshVoices(_ttsService, property);
|
||||
// Get voice index
|
||||
int voiceIndex = GetVoiceIndex(property);
|
||||
|
||||
// Iterate subfields
|
||||
for (int s = 0; s < _fields.Length; s++)
|
||||
{
|
||||
// Get subfield
|
||||
FieldInfo subfield = _fields[s];
|
||||
|
||||
// Ignore base fields on speaker
|
||||
if (_onSpeaker && subfield.DeclaringType == typeof(TTSVoiceSettings))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get subfield property
|
||||
SerializedProperty subfieldProperty = property.FindPropertyRelative(subfield.Name);
|
||||
Rect subfieldRect = new Rect(position.x, y, position.width, VAR_HEIGHT);
|
||||
if (string.Equals(subfield.Name, VAR_VOICE) && voiceIndex != -1)
|
||||
{
|
||||
int newVoiceIndex = EditorGUI.Popup(subfieldRect, subfieldProperty.displayName, voiceIndex,
|
||||
_voiceNames);
|
||||
newVoiceIndex = Mathf.Clamp(newVoiceIndex, 0, _voiceNames.Length);
|
||||
if (voiceIndex != newVoiceIndex)
|
||||
{
|
||||
voiceIndex = newVoiceIndex;
|
||||
subfieldProperty.stringValue = _voiceNames[voiceIndex];
|
||||
GUI.FocusControl(null);
|
||||
}
|
||||
y += VAR_HEIGHT + VAR_MARGIN;
|
||||
continue;
|
||||
}
|
||||
if (string.Equals(subfield.Name, VAR_STYLE) && voiceIndex >= 0 && voiceIndex < _voices.Length)
|
||||
{
|
||||
// Get voice data
|
||||
WitVoiceInfo voiceInfo = _voices[voiceIndex];
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
// Locale layout
|
||||
EditorGUI.LabelField(subfieldRect, "Locale", voiceInfo.locale);
|
||||
y += VAR_HEIGHT + VAR_MARGIN;
|
||||
|
||||
// Gender layout
|
||||
subfieldRect = new Rect(position.x, y, position.width, VAR_HEIGHT);
|
||||
EditorGUI.LabelField(subfieldRect, "Gender", voiceInfo.gender);
|
||||
y += VAR_HEIGHT + VAR_MARGIN;
|
||||
|
||||
// Style layout/select
|
||||
subfieldRect = new Rect(position.x, y, position.width, VAR_HEIGHT);
|
||||
if (voiceInfo.styles != null && voiceInfo.styles.Length > 0)
|
||||
{
|
||||
// Get style index
|
||||
string style = subfieldProperty.stringValue;
|
||||
int styleIndex = new List<string>(voiceInfo.styles).IndexOf(style);
|
||||
|
||||
// Show style select
|
||||
int newStyleIndex = EditorGUI.Popup(subfieldRect, subfieldProperty.displayName, styleIndex,
|
||||
voiceInfo.styles);
|
||||
newStyleIndex = Mathf.Clamp(newStyleIndex, 0, voiceInfo.styles.Length);
|
||||
if (styleIndex != newStyleIndex)
|
||||
{
|
||||
// Apply style
|
||||
styleIndex = newStyleIndex;
|
||||
subfieldProperty.stringValue = voiceInfo.styles[styleIndex];
|
||||
GUI.FocusControl(null);
|
||||
}
|
||||
|
||||
// Move down
|
||||
y += VAR_HEIGHT + VAR_MARGIN;
|
||||
EditorGUI.indentLevel--;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Undent
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
//
|
||||
TextAreaAttribute area = subfield.GetCustomAttribute<TextAreaAttribute>();
|
||||
if (area != null)
|
||||
{
|
||||
subfieldRect.height = FIELD_HEIGHT;
|
||||
y += FIELD_HEIGHT - VAR_HEIGHT;
|
||||
}
|
||||
|
||||
// Default layout
|
||||
EditorGUI.PropertyField(subfieldRect, subfieldProperty, new GUIContent(subfieldProperty.displayName));
|
||||
|
||||
// Clamp in between range
|
||||
RangeAttribute range = subfield.GetCustomAttribute<RangeAttribute>();
|
||||
if (range != null)
|
||||
{
|
||||
int newValue = Mathf.Clamp(subfieldProperty.intValue, (int)range.min, (int)range.max);
|
||||
if (subfieldProperty.intValue != newValue)
|
||||
{
|
||||
subfieldProperty.intValue = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Increment
|
||||
y += VAR_HEIGHT + VAR_MARGIN;
|
||||
}
|
||||
|
||||
// Undent
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
// Get service & if on a speaker
|
||||
private void GetService(SerializedProperty property, out TTSService ttsService, out bool onSpeaker)
|
||||
{
|
||||
// Get tts wit if possible
|
||||
object targetObject = property.serializedObject.targetObject;
|
||||
if (targetObject != null)
|
||||
{
|
||||
// Currently on TTSService
|
||||
if (targetObject is TTSService service)
|
||||
{
|
||||
ttsService = service;
|
||||
onSpeaker = false;
|
||||
return;
|
||||
}
|
||||
// Currently on TTSSpeaker
|
||||
if (targetObject is TTSSpeaker speaker)
|
||||
{
|
||||
ttsService = speaker.TTSService;
|
||||
onSpeaker = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Speaker not found
|
||||
ttsService = null;
|
||||
onSpeaker = false;
|
||||
}
|
||||
// Refresh voices
|
||||
private void RefreshVoices(TTSService ttsService, SerializedProperty property)
|
||||
{
|
||||
// Ensure service exists
|
||||
if (!(ttsService is TTSWit))
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Get configuration
|
||||
IWitRequestConfiguration configuration = (ttsService as TTSWit).RequestSettings.configuration;
|
||||
// Set configuration
|
||||
if (_configuration != configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_voices = null;
|
||||
_voiceNames = null;
|
||||
_configUpdating = false;
|
||||
}
|
||||
// Ignore if null
|
||||
if (configuration == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Ignore if already set up
|
||||
if (_voices != null && _voiceNames != null && !_configUpdating)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Get voices
|
||||
_voices = configuration.GetApplicationInfo().voices;
|
||||
_voiceNames = _voices?.Select(voice => voice.name).ToArray();
|
||||
|
||||
// Voices found!
|
||||
if (_voices != null && _voices.Length > 0)
|
||||
{
|
||||
_configUpdating = false;
|
||||
}
|
||||
// Configuration needs voices, perform update
|
||||
else if (!_configUpdating)
|
||||
{
|
||||
// Perform update if possible
|
||||
if (_configuration is WitConfiguration witConfig && !witConfig.IsUpdatingData())
|
||||
{
|
||||
witConfig.RefreshAppInfo();
|
||||
}
|
||||
// Now updating
|
||||
_configUpdating = true;
|
||||
}
|
||||
}
|
||||
// Get voice index
|
||||
private int GetVoiceIndex(SerializedProperty property)
|
||||
{
|
||||
SerializedProperty voiceProperty = property.FindPropertyRelative(VAR_VOICE);
|
||||
string voiceID = voiceProperty.stringValue;
|
||||
int voiceIndex = -1;
|
||||
List<string> voiceNames = new List<string>();
|
||||
if (_voiceNames != null)
|
||||
{
|
||||
voiceNames.AddRange(_voiceNames);
|
||||
}
|
||||
if (voiceNames.Count > 0)
|
||||
{
|
||||
if (string.IsNullOrEmpty(voiceID))
|
||||
{
|
||||
voiceIndex = 0;
|
||||
voiceID = voiceNames[0];
|
||||
voiceProperty.stringValue = voiceID;
|
||||
GUI.FocusControl(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
voiceIndex = voiceNames.IndexOf(voiceID);
|
||||
}
|
||||
}
|
||||
return voiceIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0bf9132926065a54da619451f5258d3e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8c945bb3fb3322b4eb73aba670cad74a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f4d30ee47fb6e7d4a888e9ee4b707391
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Meta.Voice.Audio;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Meta.WitAi.TTS.Data
|
||||
{
|
||||
// Various request load states
|
||||
public enum TTSClipLoadState
|
||||
{
|
||||
Unloaded,
|
||||
Preparing,
|
||||
Loaded,
|
||||
Error
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class TTSClipData
|
||||
{
|
||||
// Text to be spoken
|
||||
public string textToSpeak;
|
||||
// Unique identifier
|
||||
public string clipID;
|
||||
// Audio type
|
||||
public AudioType audioType;
|
||||
// Voice settings for request
|
||||
public TTSVoiceSettings voiceSettings;
|
||||
// Cache settings for request
|
||||
public TTSDiskCacheSettings diskCacheSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Unique request id used for tracking & logging
|
||||
/// </summary>
|
||||
public string queryRequestId => _queryRequestId;
|
||||
private string _queryRequestId = Guid.NewGuid().ToString();
|
||||
// Whether service should stream audio or just provide all at once
|
||||
public bool queryStream;
|
||||
// Request data
|
||||
public Dictionary<string, string> queryParameters;
|
||||
|
||||
// Clip stream
|
||||
public IAudioClipStream clipStream
|
||||
{
|
||||
get => _clipStream;
|
||||
set
|
||||
{
|
||||
// Unload previous clip stream
|
||||
IAudioClipStream v = value;
|
||||
if (_clipStream != null && _clipStream != v)
|
||||
{
|
||||
clipStream.OnStreamReady = null;
|
||||
clipStream.OnStreamUpdated = null;
|
||||
clipStream.OnStreamComplete = null;
|
||||
_clipStream.Unload();
|
||||
}
|
||||
// Apply new clip stream
|
||||
_clipStream = v;
|
||||
}
|
||||
}
|
||||
private IAudioClipStream _clipStream;
|
||||
public AudioClip clip
|
||||
{
|
||||
get
|
||||
{
|
||||
if (clipStream is IAudioClipProvider uacs)
|
||||
{
|
||||
return uacs.Clip;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// Clip load state
|
||||
[NonSerialized] public TTSClipLoadState loadState;
|
||||
// Clip load progress
|
||||
[NonSerialized] public float loadProgress;
|
||||
|
||||
// On clip state change
|
||||
public Action<TTSClipData, TTSClipLoadState> onStateChange;
|
||||
|
||||
/// <summary>
|
||||
/// A callback when clip stream is ready
|
||||
/// Returns an error if there was an issue
|
||||
/// </summary>
|
||||
public Action<string> onPlaybackReady;
|
||||
/// <summary>
|
||||
/// A callback when clip has downloaded successfully
|
||||
/// Returns an error if there was an issue
|
||||
/// </summary>
|
||||
public Action<string> onDownloadComplete;
|
||||
|
||||
/// <summary>
|
||||
/// Compare clips if possible
|
||||
/// </summary>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is TTSClipData other)
|
||||
{
|
||||
return Equals(other);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/// <summary>
|
||||
/// Compare clip ids
|
||||
/// </summary>
|
||||
public bool Equals(TTSClipData other)
|
||||
{
|
||||
return HasClipId(other?.clipID);
|
||||
}
|
||||
/// <summary>
|
||||
/// Compare clip ids
|
||||
/// </summary>
|
||||
public bool HasClipId(string clipId)
|
||||
{
|
||||
return string.Equals(clipID, clipId, StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
/// <summary>
|
||||
/// Get hash code
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hash = 17;
|
||||
hash = hash * 31 + clipID.GetHashCode();
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef626b8cea4f59646a5076430a0e14aa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+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