SpeechSynthesizer 无法获取所有已安装的语音 3

Tag*_*gor 10 .net c# text-to-speech windows-10 visual-studio-2017

我使用区域和语言下的“添加语言”添加了许多声音。这些显示在“语音”中的“文本转语音”下。(我使用的是Windows 10)

我想在我的应用程序中将这些SpeechSynthesizerSystem.Speech.Synthesis.

当在我的应用程序中列出可用的语音时,仅显示少数实际可用的语音:

static void Main()
{
    SpeechSynthesizer speech = new SpeechSynthesizer();

    ReadOnlyCollection<InstalledVoice> voices = speech.GetInstalledVoices();
    if (File.Exists("available_voices.txt"))
    {
        File.WriteAllText("available_voices.txt", string.Empty);
    }
    using (StreamWriter sw = File.AppendText("available_voices.txt"))
    {
        foreach (InstalledVoice voice in voices)
        {                 
            sw.WriteLine(voice.VoiceInfo.Name);                           
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

查看available_voices.txt仅列出这些声音:

Microsoft David Desktop
Microsoft Hazel Desktop
Microsoft Zira Desktop
Microsoft Irina Desktop
Run Code Online (Sandbox Code Playgroud)

但是在设置中的“文本转语音”下还有更多内容,例如Microsoft GeorgeMicrosoft Mark

这里接受的答案: SpeechSynthesizer 没有获取所有已安装的声音 建议将平台更改为 x86。我尝试过,但没有看到任何变化。

这个答案: SpeechSynthesizer 没有获取所有已安装的声音 2 建议使用 .NET v4.5,因为 .NET v4.5 中存在错误System.Speech.Synthesis。我的目标是 .NET Framework 4.5,但我仍然只能检索 4 个声音。

我链接的问题中的答案都没有帮助我解决我的问题,所以我再次询问。任何帮助表示赞赏。

mor*_*cer 11

最初的问题提出三年后,API 似乎包含相同的问题,所以这里有一个更“深入”的答案。

TL;博士; 代码示例 - 在底部

语音列表的问题是 Microsoft Speech API 的一个奇怪的设计 - Windows 中有两组语音注册在注册表的不同位置 - 一组位于 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\ Speech \Voices,另一组 - 位于 HKEY_LOCAL_MACHINE\软件\微软\ Speech_OneCore \声音。

问题是 SpeechSynthesizer 的(或更具体地说 - VoiceSynthesis的)初始化例程固定在第一个例程上,同时我们通常需要两者的组合。

因此,实际上有两种方法可以克服这种行为。

选项 1(在其他答案中提到的选项):操作注册表以从 Speech_OneCore 注册表中物理复制语音定义记录,使它们对 SpeechSynthesizer 可见。在这里您有很多选择:手动注册表操作、PowerShell 脚本、基于代码等。

选项 2(我在项目中使用的选项):使用反射将其他声音放入内部 VoiceSyntesis 的_installedVoices字段中,有效模拟 Microsoft 在其代码中所做的操作。

好消息是 Speech API 源代码现已开放,因此我们不必在黑暗中摸索试图了解我们需要做什么。

这是原始代码片段:

using (ObjectTokenCategory category = ObjectTokenCategory.Create(SAPICategories.Voices))
{
    if (category != null)
    {
        // Build a list with all the voicesInfo
        foreach (ObjectToken voiceToken in category.FindMatchingTokens(null, null))
        {
            if (voiceToken != null && voiceToken.Attributes != null)
            {
                voices.Add(new InstalledVoice(voiceSynthesizer, new VoiceInfo(voiceToken)));
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我们只需要用另一个注册表项路径替换SAPICategories.Voices常量,然后重复整个过程。

坏消息是,这里使用的所有需要​​的类、方法和字段都是内部的,因此我们必须广泛使用反射来实例化类、调用方法和获取/设置字段。

请在下面找到我的实现示例 - 您调用合成器上的 InjectOneCoreVoices 扩展方法,它就会完成工作。请注意,如果出现问题,它会抛出异常,因此不要忘记正确的 try/catch 环境。


public static class SpeechApiReflectionHelper
{
    private const string PROP_VOICE_SYNTHESIZER = "VoiceSynthesizer";
    private const string FIELD_INSTALLED_VOICES = "_installedVoices";

    private const string ONE_CORE_VOICES_REGISTRY = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices";

    private static readonly Type ObjectTokenCategoryType = typeof(SpeechSynthesizer).Assembly
        .GetType("System.Speech.Internal.ObjectTokens.ObjectTokenCategory")!;

    private static readonly Type VoiceInfoType = typeof(SpeechSynthesizer).Assembly
        .GetType("System.Speech.Synthesis.VoiceInfo")!; 
    
    private static readonly Type InstalledVoiceType = typeof(SpeechSynthesizer).Assembly
        .GetType("System.Speech.Synthesis.InstalledVoice")!;


    public static void InjectOneCoreVoices(this SpeechSynthesizer synthesizer)
    {
        var voiceSynthesizer = GetProperty(synthesizer, PROP_VOICE_SYNTHESIZER);
        if (voiceSynthesizer == null) throw new NotSupportedException($"Property not found: {PROP_VOICE_SYNTHESIZER}");

        var installedVoices = GetField(voiceSynthesizer, FIELD_INSTALLED_VOICES) as IList;
        if (installedVoices == null)
            throw new NotSupportedException($"Field not found or null: {FIELD_INSTALLED_VOICES}");

        if (ObjectTokenCategoryType
                .GetMethod("Create", BindingFlags.Static | BindingFlags.NonPublic)?
                .Invoke(null, new object?[] {ONE_CORE_VOICES_REGISTRY}) is not IDisposable otc)
            throw new NotSupportedException($"Failed to call Create on {ObjectTokenCategoryType} instance");

        using (otc)
        {
            if (ObjectTokenCategoryType
                    .GetMethod("FindMatchingTokens", BindingFlags.Instance | BindingFlags.NonPublic)?
                    .Invoke(otc, new object?[] {null, null}) is not IList tokens)
                throw new NotSupportedException($"Failed to list matching tokens");

            foreach (var token in tokens)
            {
                if (token == null || GetProperty(token, "Attributes") == null) continue;
                
                var voiceInfo =
                    typeof(SpeechSynthesizer).Assembly
                        .CreateInstance(VoiceInfoType.FullName!, true,
                            BindingFlags.Instance | BindingFlags.NonPublic, null,
                            new object[] {token}, null, null);

                if (voiceInfo == null)
                    throw new NotSupportedException($"Failed to instantiate {VoiceInfoType}");
                
                var installedVoice =
                    typeof(SpeechSynthesizer).Assembly
                        .CreateInstance(InstalledVoiceType.FullName!, true,
                            BindingFlags.Instance | BindingFlags.NonPublic, null,
                            new object[] {voiceSynthesizer, voiceInfo}, null, null);
                
                if (installedVoice == null) 
                    throw new NotSupportedException($"Failed to instantiate {InstalledVoiceType}");
                
                installedVoices.Add(installedVoice);
            }
        }
    }

    private static object? GetProperty(object target, string propName)
    {
        return target.GetType().GetProperty(propName, BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(target);
    }

    private static object? GetField(object target, string propName)
    {
        return target.GetType().GetField(propName, BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(target);
    }
}
Run Code Online (Sandbox Code Playgroud)


Ida*_*man 6

在尝试了所有已发布的解决方案后,我通过编辑注册表解决了这个问题:
Computer\HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Speech_OneCore\Voices\Tokens\MSTTS_V110_heIL_AsafMSTTS_V110_heIL_Asaf我想在.NET中使用的语音的注册表文件夹在哪里,但没有出现在GetInstalledVoices())复制到看起来相同但相反的注册表地址其Speech_OneCore只是Speech

从技术上讲,为了复制注册表文件夹,我导出了原始文件夹,然后编辑 .reg 文件以更改Speech OneCoreSpeech,然后应用新的 .reg 文件。


Tag*_*gor 2

我通过安装其他来源的语音并获取 Microsoft Speech Platform - Runtime(版本 11)解决了这个问题

可用的语音可以在微软网站上找到(单击红色下载按钮,应该会列出语音)