System.Speech.Synthesis 在 2012 R2 上挂起 CPU

System.Speech.Synthesis hangs with high CPU on 2012 R2

我有一个 asp.net MVC 应用程序,它有一个控制器动作,将字符串作为输入并发送合成语音的响应 wav 文件。这是一个简化的例子:

    public async Task<ActionResult> Speak(string text)
    {
        Task<FileContentResult> task = Task.Run(() =>
        {
            using (var synth = new System.Speech.Synthesis.SpeechSynthesizer())
            using (var stream = new MemoryStream())
            {
                synth.SetOutputToWaveStream(stream);
                synth.Speak(text);
                var bytes = stream.GetBuffer();
                return File(bytes, "audio/x-wav");
            }
        });
        return await task;
    }

该应用程序(尤其是此操作方法)在 2008 R2 服务器、2012(非 R2)服务器和我的 8.1 开发 PC 上的服务器环境中 运行 正常。它在标准 Azure 2012 R2 虚拟机上也 运行 没问题。但是,当我将它部署到三台 2012 R2 服务器(它最终的永久宿主​​)时,操作方法永远不会产生 HTTP 响应——IIS 工作进程无限期地最大化 CPU 核心之一。事件查看器中没有任何内容,在使用 Procmon 观看服务器时也没有任何提示。我已经通过远程调试附加到进程,synth.Speak(text) 从来没有 returns。当执行 synth.Speak(text) 调用时,我立即在服务器的任务管理器中看到失控的 w3wp.exe 进程。

我的第一个想法是相信某些进程通常会干扰服务器上的语音合成,但 Windows 讲述人可以正常工作,像这样的简单控制台应用程序也可以正常工作:

static void Main(string[] args)
{
    var synth = new System.Speech.Synthesis.SpeechSynthesizer();
    synth.Speak("hello");
}

所以显然我不能一般地责怪服务器的语音合成。所以也许我的代码有问题,或者 IIS 配置有什么奇怪的地方?我怎样才能让这个控制器动作在这些服务器上正常工作?

这是测试操作方法的简单方法(只需要为路由获取正确的 url 值):

<div>
    <input type="text" id="txt" autofocus />
    <button type="button" id="btn">Speak</button>
</div>

<script>
    document.getElementById('btn').addEventListener('click', function () {
        var text = document.getElementById('txt').value;
        var url = window.location.href + '/speak?text=' + encodeURIComponent(text);
        var audio = document.createElement('audio');
        var canPlayWavFileInAudioElement = audio.canPlayType('audio/wav'); 
        var bgSound = document.createElement('bgsound');
        bgSound.src = url;
        var canPlayBgSoundElement = bgSound.getAttribute('src');

        if (canPlayWavFileInAudioElement) {
            // probably Firefox and Chrome
            audio.setAttribute('src', url);
            audio.setAttribute('autoplay', '');
            document.getElementsByTagName('body')[0].appendChild(audio);
        } else if (canPlayBgSoundElement) {
            // internet explorer
            document.getElementsByTagName('body')[0].appendChild(bgSound);
        } else {
            alert('This browser probably can\'t play a wav file');
        }
    });
</script>

我认为问题出在 return 类型上。 IIS Express 让您摆脱困境,但 IIS 不是:

Task<FileContentResult>

所以如果你尝试:

public async Task<FileContentResult> Speak(string text)
{
    Task<FileContentResult> task = Task.Run(() =>
    {
        using (var synth = new System.Speech.Synthesis.SpeechSynthesizer())
        using (var stream = new MemoryStream())
        {
            synth.SetOutputToWaveStream(stream);
            synth.Speak(text);
            var bytes = stream.GetBuffer();
            return File(bytes, "audio/x-wav");
        }
    });
    return await task;
}

我打赌您还需要在 IIS 中添加 audio/wav MIME 类型。

这只是我的想法,还没有经过测试,但您可以这样做:

public ActionResult Speak(string text)
{
var speech = new SpeechSynthesizer();
speech.Speak(text);

byte[] bytes;
using (var stream = new MemoryStream())
{
    speech.SetOutputToWaveStream(stream);
    bytes = stream.ToArray();
}
return File(bytes, "audio/x-wav");
}

我之前在服务器 2012R2 上有过这种经历(不是合成器 api 授予的,但同样的问题)。我通过在所有任务中使用 "await task.ConfigureAwait(false)" 来修复它。看看是否适合你。

祝你好运。

this blog 您可以找到类似问题的解决方案 - 在全新 Windows 8.1 安装中使用 SpeechSynthesizer 时出现异常。这种情况下的问题是 CurrentUserLexicon 用户的权限条目错误(由 SpeechSynthesizer 使用。要解决此问题,此博客 post 建议从 Software\Microsoft\Speech\CurrentUserLexicon 注册表项中删除权限条目 "ALL APPLICATION PACKAGES"。

我发现我可以在其他服务器(包括 Azure VM)上重现该问题,因此我排除了我们的特定环境出现问题的可能性。

此外,我发现如果我 运行 应用程序池的身份是服务器上的管理员 并且之前已经登录,我可以让代码在 2012 R2 上正常工作进入服务器。在排除权限问题的漫长过程之后,我决定一定是登录过程中发生的某些事情使 TTS API 调用能够正常工作。 (不管它是什么,我都无法通过 procmon 跟踪找到它)。所以幸运的是,ApplicationPoolIdentity 可以通过在 IIS 中为应用程序池打开 "Advanced Settings" 并将 Load User Profile 设置为 True.

来应用类似的登录魔法

运行应用程序池的身份还需要读取 HKU\.Default\Software\Microsoft\Speech 的权限,可以通过使用本地服务器作为位置和 IIS APPPOOL\.Net v4.5 将其 g运行 发送到 ApplicationPoolIdentity用户名(其中 .Net v4.5 是应用程序池的名称)。

一旦对注册表项的读取权限被 g运行ted,并且应用程序池被配置为加载用户配置文件,以上代码就可以正常工作。在来自 MSDN ISO 的 Azure VM 和 vanilla 2012 R2 上进行了测试。