Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

能否提高离线讲述人自然语音的响应速度 #1

Open
anmi2004 opened this issue May 12, 2024 · 5 comments
Open

能否提高离线讲述人自然语音的响应速度 #1

anmi2004 opened this issue May 12, 2024 · 5 comments

Comments

@anmi2004
Copy link

开发者您好,我使用的是nvda屏幕阅读器。在使用自然语音朗读时,有点儿不太跟手,也就是说当我按下按键到它发出声音有短暂的延迟,这个延迟在打字之类的场景的时候,影响最大。不知道能否进行优化,再次提升一些响应速度。

@gexgd0419
Copy link
Owner

我用程序测试了一下,几种不同情况下,从按下键盘按键到读屏软件输出音频的延迟。

其中,NVDA 版本为 2024.1,系统为 Win 11 23H2,所有语音都调整到了最大速度。

测试方法是输入A到Z的字母,然后取每次延迟的平均值。

延迟的计算方法是,使用程序监测键盘和回录系统声音,记录键盘按下的时间点、音频数据变为非零的时间点(即输出音频延迟)、音频数据大于0.0001的时间点(即音频可听见延迟)。

读屏软件 语音 输出音频延迟 音频可听见延迟
NVDA eSpeak NG 17毫秒 68毫秒
NVDA Huihui (OneCore) 19毫秒 90毫秒
NVDA Huihui (SAPI) 87毫秒 154毫秒
NVDA Xiaoxiao (本引擎) 124毫秒 163毫秒
讲述人 Huihui (OneCore) 21毫秒 66毫秒
讲述人 Huihui (SAPI) 66毫秒 118毫秒
讲述人 Xiaoxiao (内置功能) 58毫秒 110毫秒
讲述人 Xiaoxiao (本引擎) 113毫秒 152毫秒

看来这个引擎的确是延迟最大的。

我又测试了直接作为 SAPI 客户端调用 Speak 函数的延迟。

步骤 与上一步骤间的延迟
客户端发起调用 -
引擎接收到调用 47毫秒
引擎接收到自然语音数据 9毫秒
输出音频 27毫秒
音频可听见 37毫秒
总延迟 122毫秒

其中,从客户端调用请求 SAPI 合成语音,到引擎接收到来自客户端程序的请求,SAPI 5 系统自己就带来了四十多毫秒的延迟。对这一部分的延迟,我可能是没什么办法的。

而如果去除这四十多毫秒的延迟,延迟的水平就和讲述人内置的自然语音功能差不多了。同时从数据也可以看出,SAPI 版的 Huihui 语音也比 OneCore 版的 Huihui 多出五十毫秒左右的延迟。

(提示:Huihui Desktop 是 SAPI 语音,不带 Desktop 的 Huihui 是 OneCore 语音)

@cary-rowen
Copy link

OneCore 的延迟还是可以的。
不了解本项目的实现机制,是否有办法适配 oneCore 接口呢?
另 NVDA log 也可以看到一些延迟数据,但 @gexgd0419 这个似乎更有参考意义。

@gexgd0419
Copy link
Owner

首先要注意一点,即使是离线语音,自然语音也比普通语音的延迟更高。因为自然语音看起来是使用了 AI 模型运算的,使用了 onnxruntime 组件,也会有一定的 CPU 占用。可以把“讲述人 Xiaoxiao (内置功能)”的延迟作为基准,如果讲述人内置的自然语音功能的延迟可以接受的话,或许还有优化空间。

OneCore 接口的问题在于,微软并没有提供“如何编写第三方 OneCore 语音”的文档,反而在 OneCore 语音的 SpeechSynthesizer 文档里指出 OneCore 语音必须有微软的签名。

Only Microsoft-signed voices installed on the system can be used to generate speech.

单看这一点的话,支持 OneCore 似乎是没戏了,因为微软不可能放行一个“破解”了自然语音的项目。

但是我发现,至少在桌面版的 Windows 上, OneCore 语音架构是和 SAPI5 的架构非常相似的。

SAPI5 语音注册表:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech\Voices
OneCore 语音注册表:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices

并且里面的结构也是相似的。

你甚至可以把 OneCore 的注册表复制到 SAPI5 的注册表里面,让只支持 SAPI5 语音的程序能使用 OneCore 语音。(只是一部分,某些 OneCore 语音这样用会不正常,例如 Kangkang 会“变性”成女声)但是反过来操作,把 SAPI5 的注册表复制到 OneCore 的注册表里面,却是行不通的。除了 Chromium 系软件可以识别外,大多数 UWP 应用都是没法使用注册表里的 SAPI5 语音的。

研究了一下发现,OneCore 语音有一套“注册表隔离”系统,也就是每个应用有属于自己的 Speech_OneCore 注册表(大概是为了做 UWP 的应用隔离,毕竟 OneCore 语音一开始就是给移动应用和 UWP 用的)。因此,把语音注册表数据放在 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices 里面是没有用的。

又研究了一阵发现,这些应用自己的 Speech_OneCore 注册表的初始数据来源,是存放在 C:\Windows\System32\Speech_OneCore\common 位置的 tokens.xml 文件。这里面以 XML 的格式写着所有 Speech_OneCore 所需的注册表数据,而每个应用自己的 Speech_OneCore 注册表的初始数据就是从这里来的。这意味着,如果我想把自己的语音引擎加入之后每个应用的 Speech_OneCore 注册表,我需要修改这个文件,将注册表数据加入 XML。不过,修改这个文件需要 TrustedInstaller 权限。我有些担心,更新系统的话,会不会把这个文件重写了。

除此之外,还需要在 HKEY_LOCAL_MACHINE\Software\Classes\Interface 中添加 {14056589-E16C-11D2-BB90-00C04F8EE6C0} 子键,默认值为 ISpObjectToken,之后给它添加子键 ProxyStubClsid32,默认值为 {00020424-0000-0000-C000-000000000046}

经过一番折腾(包括为文件配置合适的权限让 UWP 也能访问等),我居然真的能够让该引擎的语音在只支持 OneCore 语音的 UWP 阅读器中加载了!看来微软的“必须要求微软签名”的说法,貌似也不是那么准确。

所以,就算是微软没有提供官方文档,也是有可能对接 OneCore 接口的。

只不过会有一些问题。

  • 这个方法需要更改系统文件,并且是更改通常无权改动的文件。后续的系统更新也可能不知道什么时候就把语音覆盖没了,毕竟这不是官方提供的方法。
  • 如果某程序同时支持 SAPI5 语音和 OneCore 语音,它会加载两遍一样的自然语音列表。另外,Edge 浏览器自己就支持 Edge 语音,也需要避免再加载一遍 Edge 语音列表。

@cary-rowen
Copy link

感谢如此细致的研究,还提供了宝贵的研究成果。

是的,即使是离线语音依然有瓶颈。但如果本项目能提高到讲述人的延迟水平也是非常值得欣喜的,视障朋友对 TTS 的延迟太敏感了。

你甚至可以把 OneCore 的注册表复制到 SAPI5 的注册表里面,让只支持 SAPI5 语音的程序能使用 OneCore 语音。

没错,让我想到了一个项目,或许你也会感兴趣:https://github.com/Mahmood-Taghavi/SAPI_Unifier

(只是一部分,某些 OneCore 语音这样用会不正常,例如 Kangkang 会“变性”成女声)。

我确实记得用以上工具注册后‘康康’会出问题,但当时整理了一个注册表,导入就正常了,然而导致问题的原因/具体细节完全不记得。相关注册表如下:

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\SPEECH\Voices\Tokens\MSTTS_V110_zhCN_KangkangM]
@="Microsoft Kangkang - Chinese (Simplified, PRC)"
"804"="Microsoft Kangkang - Chinese (Simplified, PRC)"
"CLSID"="{179F3D56-1B0B-42B2-A962-59B7EF59FE1B}"
"LangDataPath"=hex(2):43,00,3a,00,5c,00,57,00,69,00,6e,00,64,00,6f,00,77,00,73,\
  00,5c,00,53,00,70,00,65,00,65,00,63,00,68,00,5f,00,4f,00,6e,00,65,00,43,00,\
  6f,00,72,00,65,00,5c,00,45,00,6e,00,67,00,69,00,6e,00,65,00,73,00,5c,00,54,\
  00,54,00,53,00,5c,00,7a,00,68,00,2d,00,43,00,4e,00,5c,00,4d,00,53,00,54,00,\
  54,00,53,00,4c,00,6f,00,63,00,7a,00,68,00,43,00,4e,00,2e,00,64,00,61,00,74,\
  00,00,00
"VoicePath"=hex(2):43,00,3a,00,5c,00,57,00,69,00,6e,00,64,00,6f,00,77,00,73,00,\
  5c,00,53,00,70,00,65,00,65,00,63,00,68,00,5f,00,4f,00,6e,00,65,00,43,00,6f,\
  00,72,00,65,00,5c,00,45,00,6e,00,67,00,69,00,6e,00,65,00,73,00,5c,00,54,00,\
  54,00,53,00,5c,00,7a,00,68,00,2d,00,43,00,4e,00,5c,00,4d,00,32,00,30,00,35,\
  00,32,00,4b,00,61,00,6e,00,67,00,6b,00,61,00,6e,00,67,00,00,00

[HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\SPEECH\Voices\Tokens\MSTTS_V110_zhCN_KangkangM\Attributes]
"Age"="Adult"
"DataVersion"="11.0.2013.1022"
"Gender"="Male"
"Language"="804"
"Name"="Microsoft Kangkang"
"SayAsSupport"="spell=NativeSupported; cardinal=GlobalSupported; ordinal=NativeSupported; date=GlobalSupported; time=GlobalSupported; telephone=NativeSupported; computer=NativeSupported; address=NativeSupported; percentage=NativeSupported; currency=NativeSupported; message=NativeSupported; url=NativeSupported; alphanumeric=NativeSupported"
"SharedPronunciation"=""
"Vendor"="Microsoft"
"Version"="11.0"

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\SPEECH\Voices\Tokens\MSTTS_V110_zhCN_KangkangM]
@="Microsoft Kangkang - Chinese (Simplified, PRC)"
"804"="Microsoft Kangkang - Chinese (Simplified, PRC)"
"CLSID"="{179F3D56-1B0B-42B2-A962-59B7EF59FE1B}"
"LangDataPath"=hex(2):43,00,3a,00,5c,00,57,00,69,00,6e,00,64,00,6f,00,77,00,73,\
  00,5c,00,53,00,70,00,65,00,65,00,63,00,68,00,5f,00,4f,00,6e,00,65,00,43,00,\
  6f,00,72,00,65,00,5c,00,45,00,6e,00,67,00,69,00,6e,00,65,00,73,00,5c,00,54,\
  00,54,00,53,00,5c,00,7a,00,68,00,2d,00,43,00,4e,00,5c,00,4d,00,53,00,54,00,\
  54,00,53,00,4c,00,6f,00,63,00,7a,00,68,00,43,00,4e,00,2e,00,64,00,61,00,74,\
  00,00,00
"VoicePath"=hex(2):43,00,3a,00,5c,00,57,00,69,00,6e,00,64,00,6f,00,77,00,73,00,\
  5c,00,53,00,70,00,65,00,65,00,63,00,68,00,5f,00,4f,00,6e,00,65,00,43,00,6f,\
  00,72,00,65,00,5c,00,45,00,6e,00,67,00,69,00,6e,00,65,00,73,00,5c,00,54,00,\
  54,00,53,00,5c,00,7a,00,68,00,2d,00,43,00,4e,00,5c,00,4d,00,32,00,30,00,35,\
  00,32,00,4b,00,61,00,6e,00,67,00,6b,00,61,00,6e,00,67,00,00,00

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\SPEECH\Voices\Tokens\MSTTS_V110_zhCN_KangkangM\Attributes]
"Age"="Adult"
"DataVersion"="11.0.2013.1022"
"Gender"="Male"
"Language"="804"
"Name"="Microsoft Kangkang"
"SayAsSupport"="spell=NativeSupported; cardinal=GlobalSupported; ordinal=NativeSupported; date=GlobalSupported; time=GlobalSupported; telephone=NativeSupported; computer=NativeSupported; address=NativeSupported; percentage=NativeSupported; currency=NativeSupported; message=NativeSupported; url=NativeSupported; alphanumeric=NativeSupported"
"SharedPronunciation"=""
"Vendor"="Microsoft"
"Version"="11.0"

@gexgd0419
Copy link
Owner

如果想要减小读屏延迟,最好的方法可能是使用直接与读屏软件对接的插件。NVDA 实际上已经有了微软离线自然语音的插件,名叫 NeuralVoices,而且里面自带所有离线语音包。目前还没测试过它的延迟水平如何。

#18 里也有人建议我出一个 NVDA 插件,因为 NVDA 不支持 SAPI5 语音的自动语言切换,并且这个 NeuralVoices 也不支持自动语言切换。我暂时没有出 NVDA 插件的计划,不过给 NVDA 提了 PR 修了 SAPI5 的语言切换问题。SAPI5 更主要的是通用性,不只限于读屏软件,任何需要 TTS 的程序都可以使用。

这个项目本身只是一个将 SAPI5 对接到 Azure Speech SDK 的适配器。使用离线语音的关键,在于得到的密钥。Azure Speech SDK 支持多种平台(安卓/Linux/macOS/Windows)和编程语言(C#/C++/Java),因此,只要有了语音模型文件和密钥,就可以按照官方文档的示例,在各种场景使用离线语音了。注意 Azure Speech SDK 要使用 1.38.0 或之前的版本,因为从 1.40 版本开始,密钥验证方法改变了。

没错,让我想到了一个项目,或许你也会感兴趣:https://github.com/Mahmood-Taghavi/SAPI_Unifier

看起来原理也是把注册表复制到 SAPI5 的注册表位置。这是不是意味着,这几个看起来不同的语音系统,底层实际上是相同的?(Azure Speech SDK 除外,否则就不需要单独的程序适配了)

我去看了一下 SAPI5 和 OneCore 的 Voices 注册表,发现系统自带语音,无论是 SAPI5 还是 OneCore,CLSID 都是相同的 {179F3D56-1B0B-42B2-A962-59B7EF59FE1B}。也就是说,系统自带的 SAPI5 和 OneCore 语音,底层使用了相同的 COM 组件。

我把这个 SAPI5 语音引擎程序稍加修改就能“适配”OneCore,或许也是这个原因。可惜的是这个没法用在 NVDA 上,因为 NVDA 对于 OneCore 语音的检查很严格,如果语音注册表不存在,或者不包含 LangDataPathVoicePath,就会直接被 NVDA 判定为无效语音而剔出语音列表。这是因为前面提到的“注册表隔离”机制在删除 OneCore 语音后不会更新注册表,导致残留无效的 OneCore 语音项目,于是 NVDA 对于 OneCore 语音会做有效性判断。但是这个引擎的语音列表都是虚拟的、动态生成的,既不存在实际的语音注册表,也没有 LangDataPathVoicePath,于是被 NVDA 移出列表了。

另外,貌似 OneCore 语音用到了一些未公开的 COM 接口。尽管这个 SAPI5 引擎已经尽可能地把所有的 SAPI5 指令转换成了对应的 SSML(以发送到 Speech SDK),但依然有一些 SSML 特性因为无法翻译成 SAPI5 指令而丢失,例如 <pitch>contour 属性等。然而,即使使用 SAPI5 版本的自带语音,例如 Huihui,也是可以支持这种 SSML 的。这些语音貌似是使用了一个支持 SSML 特性的新 COM 接口,而 SAPI5 系统会优先尝试语音的新接口以完整传达 SSML 指令,不支持的话再回落至旧接口,并将 SSML 翻译成 SAPI5 指令。可惜的是这种新接口(ITtsEngineSsml)没有任何公开文档和可查阅的资料。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants