Skip to content

Commit

Permalink
Move XMLSchemaSet and CultureInfo properties out of the `VoiceDet…
Browse files Browse the repository at this point in the history
…ails` type to prevent parsing errors with Value.FromReflection().
  • Loading branch information
Tkael committed Jun 29, 2024
1 parent 2b60d38 commit a0de662
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 116 deletions.
2 changes: 1 addition & 1 deletion SpeechResponder/CustomFunctions/GetState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public class GetState : ICustomFunction
}
if ( @value is string s )
{
{
return s;
}
Expand Down
2 changes: 1 addition & 1 deletion SpeechResponder/CustomFunctions/VoiceDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class VoiceDetails : ICustomFunction
public string name => "VoiceDetails";
public FunctionCategory Category => FunctionCategory.Details;
public string description => Properties.CustomFunctions_Untranslated.VoiceDetails;
public Type ReturnType => typeof( VoiceDetails );
public Type ReturnType => typeof( EddiSpeechService.VoiceDetails );
public IFunction function => Function.CreateNativeMinMax( ( runtime, values, writer ) =>
{
if (values.Count == 0)
Expand Down
108 changes: 106 additions & 2 deletions SpeechService/SpeechPreparation/SpeechFormatter.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;
using Utilities;

namespace EddiSpeechService.SpeechPreparation
Expand Down Expand Up @@ -38,7 +42,7 @@ public static string TrimSpeech(string s)

internal static void PrepareSpeech(VoiceDetails voice, ref string speech, out bool useSSML)
{
var lexicons = voice.GetLexicons();
var lexicons = GetLexicons(voice);
if (speech.Contains("<") || lexicons.Any())
{
// Keep XML version at 1.0. Version 1.1 is not recommended for general use. https://en.wikipedia.org/wiki/XML#Versions
Expand Down Expand Up @@ -227,5 +231,105 @@ public static string DisableIPA(string speech)
speech = Regex.Replace(speech, @"<\/phoneme>", string.Empty);
return speech;
}

#region Lexicons

private static HashSet<string> GetLexicons ( VoiceDetails voice )
{
var result = new HashSet<string>();
HashSet<string> GetLexiconsFromDirectory ( string directory, bool createIfMissing = false )
{
// When multiple lexicons are referenced, their precedence goes from lower to higher with document order.
// Precedence means that a token is first looked up in the lexicon with highest precedence.
// Only if not found in that lexicon, the next lexicon is searched and so on until a first match or until all lexicons have been used for lookup. (https://www.w3.org/TR/2004/REC-speech-synthesis-20040907/#S3.1.4).

if ( string.IsNullOrEmpty( directory ) || string.IsNullOrEmpty( voice.culturecode ) )
{ return null; }
DirectoryInfo dir = new DirectoryInfo(directory);
if ( dir.Exists )
{
// Find two letter language code lexicons (these will have lower precedence than any full language code lexicons)
foreach ( var file in dir.GetFiles( "*.pls", SearchOption.AllDirectories )
.Where( f => $"{f.Name.ToLowerInvariant()}" == $"{voice.cultureTwoLetterISOLanguageName.ToLowerInvariant()}.pls" ) )
{
CheckAndAdd( file );
}
// Find full language code lexicons
foreach ( var file in dir.GetFiles( "*.pls", SearchOption.AllDirectories )
.Where( f => $"{f.Name.ToLowerInvariant()}" == $"{voice.cultureIetfLanguageTag.ToLowerInvariant()}.pls" ) )
{
CheckAndAdd( file );
}
}
else if ( createIfMissing )
{
dir.Create();
}
return result;
}

void CheckAndAdd ( FileInfo file )
{
if ( IsValidXML( file.FullName, out _ ) )
{
result.Add( file.FullName );
}
else
{
file.MoveTo( $"{file.FullName}.malformed" );
}
}

// When multiple lexicons are referenced, their precedence goes from lower to higher with document order (https://www.w3.org/TR/2004/REC-speech-synthesis-20040907/#S3.1.4)

// Add lexicons from our installation directory
result.UnionWith( GetLexiconsFromDirectory( new FileInfo( System.Reflection.Assembly.GetExecutingAssembly().Location ).DirectoryName + @"\lexicons" ) );

// Add lexicons from our user configuration (allowing these to overwrite any prior lexeme values)
result.UnionWith( GetLexiconsFromDirectory( Constants.DATA_DIR + @"\lexicons" ) );

return result;
}

private static bool IsValidXML ( string filename, out XDocument xml )
{
// Check whether the file is valid .xml (.pls is an xml-based format)
xml = null;
try
{
// Try to load the file as xml
xml = XDocument.Load( filename );

// Validate the lexicon xml against the schema
xml.Validate( SpeechService.Instance.lexiconSchemas, ( o, e ) =>
{
if ( e.Severity == XmlSeverityType.Warning || e.Severity == XmlSeverityType.Error )
{
throw new XmlSchemaValidationException( e.Message, e.Exception );
}
} );
var reader = xml.CreateReader();
var lastNodeName = string.Empty;
while ( reader.Read() )
{
if ( reader.HasValue &&
reader.NodeType is XmlNodeType.Text &&
lastNodeName == "phoneme" &&
!IPA.IsValid( reader.Value ) )
{
throw new ArgumentException( $"Invalid phoneme found in lexicon file: {reader.Value}" );
}
lastNodeName = reader.Name;
}
return true;
}
catch ( Exception ex )
{
Logging.Warn( $"Could not load lexicon file '{filename}', please review.", ex );
return false;
}
}

#endregion
}
}
124 changes: 12 additions & 112 deletions SpeechService/SpeechService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;
using Utilities;

Expand Down Expand Up @@ -57,7 +55,7 @@ public SpeechServiceConfiguration Configuration
.Select(v => v.name)
.ToList();

private readonly XmlSchemaSet lexiconSchemas = new XmlSchemaSet();
internal readonly XmlSchemaSet lexiconSchemas = new XmlSchemaSet();

private static readonly object activeAudioLock = new object();
private static readonly object activeSpeechLock = new object();
Expand Down Expand Up @@ -787,128 +785,30 @@ public class VoiceDetails : IEquatable<VoiceDetails>
public string synthType { get; }

[Utilities.PublicAPI]
public string cultureinvariantname => Culture.EnglishName;
public string cultureinvariantname { get; }

[Utilities.PublicAPI]
public string culturename => Culture.NativeName;

public CultureInfo Culture { get; }
public string culturename { get; }

public bool hideVoice { get; set; }

internal string cultureTwoLetterISOLanguageName;
internal string cultureIetfLanguageTag;

internal VoiceDetails( string displayName, string gender, CultureInfo Culture, string synthType, XmlSchemaSet lexiconSchemas )
{
this.name = displayName;
this.gender = gender;
this.Culture = Culture;
this.cultureinvariantname = Culture.EnglishName;
this.culturename = Culture.NativeName;
this.cultureTwoLetterISOLanguageName = Culture.TwoLetterISOLanguageName;
this.cultureIetfLanguageTag = Culture.IetfLanguageTag;
this.synthType = synthType;

culturecode = BestGuessCulture();
this.lexiconSchemas = lexiconSchemas;
}

#region Lexicons

private XmlSchemaSet lexiconSchemas;

public HashSet<string> GetLexicons()
{
var result = new HashSet<string>();
HashSet<string> GetLexiconsFromDirectory(string directory, bool createIfMissing = false)
{
// When multiple lexicons are referenced, their precedence goes from lower to higher with document order.
// Precedence means that a token is first looked up in the lexicon with highest precedence.
// Only if not found in that lexicon, the next lexicon is searched and so on until a first match or until all lexicons have been used for lookup. (https://www.w3.org/TR/2004/REC-speech-synthesis-20040907/#S3.1.4).

if (string.IsNullOrEmpty(directory) || string.IsNullOrEmpty(culturecode)) { return null; }
DirectoryInfo dir = new DirectoryInfo(directory);
if (dir.Exists)
{
// Find two letter language code lexicons (these will have lower precedence than any full language code lexicons)
foreach (var file in dir.GetFiles("*.pls", SearchOption.AllDirectories)
.Where(f => $"{f.Name.ToLowerInvariant()}" == $"{Culture.TwoLetterISOLanguageName.ToLowerInvariant()}.pls"))
{
CheckAndAdd(file);
}
// Find full language code lexicons
foreach (var file in dir.GetFiles("*.pls", SearchOption.AllDirectories)
.Where(f => $"{f.Name.ToLowerInvariant()}" == $"{Culture.IetfLanguageTag.ToLowerInvariant()}.pls"))
{
CheckAndAdd(file);
}
}
else if (createIfMissing)
{
dir.Create();
}
return result;
}

void CheckAndAdd(FileInfo file)
{
if (IsValidXML(file.FullName, out _))
{
result.Add(file.FullName);
}
else
{
file.MoveTo($"{file.FullName}.malformed");
}
}

// When multiple lexicons are referenced, their precedence goes from lower to higher with document order (https://www.w3.org/TR/2004/REC-speech-synthesis-20040907/#S3.1.4)

// Add lexicons from our installation directory
result.UnionWith(GetLexiconsFromDirectory(new FileInfo(System.Reflection.Assembly.GetExecutingAssembly().Location).DirectoryName + @"\lexicons"));

// Add lexicons from our user configuration (allowing these to overwrite any prior lexeme values)
result.UnionWith(GetLexiconsFromDirectory(Constants.DATA_DIR + @"\lexicons"));

return result;
}

private bool IsValidXML(string filename, out XDocument xml)
{
// Check whether the file is valid .xml (.pls is an xml-based format)
xml = null;
try
{
// Try to load the file as xml
xml = XDocument.Load(filename);

// Validate the lexicon xml against the schema
xml.Validate(lexiconSchemas, ( o, e ) =>
{
if ( e.Severity == XmlSeverityType.Warning || e.Severity == XmlSeverityType.Error )
{
throw new XmlSchemaValidationException( e.Message, e.Exception );
}
} );
var reader = xml.CreateReader();
var lastNodeName = string.Empty;
while ( reader.Read() )
{
if ( reader.HasValue &&
reader.NodeType is XmlNodeType.Text &&
lastNodeName == "phoneme" &&
!IPA.IsValid( reader.Value ) )
{
throw new ArgumentException( $"Invalid phoneme found in lexicon file: {reader.Value}" );
}
lastNodeName = reader.Name;
}
return true;
}
catch (Exception ex)
{
Logging.Warn($"Could not load lexicon file '{filename}', please review.", ex);
return false;
}
culturecode = BestGuessCulture(Culture);
}

#endregion

private string BestGuessCulture()
private string BestGuessCulture(CultureInfo Culture)
{
string guess;
if (name.Contains("CereVoice"))
Expand Down

0 comments on commit a0de662

Please sign in to comment.