让KokoroSharp用上V1.1-zh的模型

<0x00> 前言

项目要有TTS,然后就找到KokoroSharp这个包
项目中的模型对英文支持很好,但中文总有种好想成为人类的感觉
然后接着寻找,发现有新的v1.1-zh的模型
(KokoroSharp默认使用的是v1.0的模型)
但由于对中文的针对训练导致其他语言输出有一定劣化,所以还不是正式版
然后就开始折腾能不能让这个项目跑起v1.1-zh的模型

前排提示:流水账文章,而且问题也没解决
结果是成功让v1.1-zh跑起来了,但中文输出还是很差
所以只是做一个过程记录
我认为可能是原本模型从torch转换到onnx导致的问题

<0x01> 过程

尝试直接运行

如果直接加载v1.1-zh-onnx的模型,那确实是能加载的
但如果这样运行,会有下面的错误

using KokoroSharp;
using KokoroSharp.Core;

KokoroTTS tts = KokoroTTS.LoadModel("kokoro-v1.1-zh.onnx");
KokoroVoice heartVoice = KokoroVoiceManager.GetVoice("af_heart");
while (true)
{
    tts.Speak(Console.ReadLine(), heartVoice);
}

输入任何文字回车后

Unhandled exception. Microsoft.ML.OnnxRuntime.OnnxRuntimeException: [ErrorCode:InvalidArgument] Input name: 'tokens' is not in the metadata
   at Microsoft.ML.OnnxRuntime.InferenceSession.LookupInputMetadata(String nodeName)
   at Microsoft.ML.OnnxRuntime.InferenceSession.LookupUtf8Names[T](IReadOnlyCollection`1 values, NameExtractor`1 nameExtractor, MetadataLookup metaLookup)
   at Microsoft.ML.OnnxRuntime.InferenceSession.Run(IReadOnlyCollection`1 inputs, IReadOnlyCollection`1 outputNames, RunOptions options)
   at Microsoft.ML.OnnxRuntime.InferenceSession.Run(IReadOnlyCollection`1 inputs, IReadOnlyCollection`1 outputNames)
   at Microsoft.ML.OnnxRuntime.InferenceSession.Run(IReadOnlyCollection`1 inputs)
   at KokoroSharp.Core.KokoroModel.Infer(Int32[] tokens, Single[,,] voiceStyle, Single speed) in E:\GitHub\KokoroSharp\KokoroSharp\Core\KokoroModel.cs:line 58
   at KokoroSharp.Core.KokoroJob.Progress(KokoroModel model) in E:\GitHub\KokoroSharp\KokoroSharp\Core\KokoroJob.cs:line 28
   at KokoroSharp.Core.KokoroEngine.<.ctor>b__2_0() in E:\GitHub\KokoroSharp\KokoroSharp\Core\KokoroEngine.cs:line 20

会说在元数据中找不到tokens

修改入口名

运行一个模型其实跟调用一个函数差不多
首先要确定的是这个模型的输入参数有什么

知道了区别后,就可以尝试修改入口名了
这里使用python修改这个入口名

import onnx

def ModEntryName(onnxModel: onnx.ModelProto, oldName: str, newName: str) -> onnx.ModelProto:
	# 查找输入节点并修改
    for input_tensor in onnxModel.graph.input:
        if input_tensor.name == oldName:
            input_tensor.name = newName
    # 遍历修改后面的每个节点名
    for node in onnxModel.graph.node:
        for i, name in enumerate(node.input):
            if name == oldName:
                node.input[i] = newName
    return onnxModel

model_path = "kokoro-v1.1-zh"
model = onnx.load(model_path + ".onnx")
ModEntryName(model, "input_ids", "tokens")
onnx.save(model, model_path + "-mod.onnx")

修改入口参数类型

像上面改好后,再次运行
还是有报错,如下

Unhandled exception. Microsoft.ML.OnnxRuntime.OnnxRuntimeException: [ErrorCode:InvalidArgument] Tensor element data type discovered: Float metadata expected: Int32
   at Microsoft.ML.OnnxRuntime.ManagedTypeProjection.CreateTensorProjection(NamedOnnxValue node, NodeMetadata elementMeta)
   at Microsoft.ML.OnnxRuntime.ManagedTypeProjection.CreateProjection(NamedOnnxValue namedOnnxValue, NodeMetadata metadata)
   at Microsoft.ML.OnnxRuntime.NamedOnnxValue.InputToOrtValueHandle(NodeMetadata metadata, IDisposable& memoryOwner)
   at Microsoft.ML.OnnxRuntime.InferenceSession.ExtractOrtValueHandleForInput(NamedOnnxValue input, NodeMetadata metadata, IDisposable& memOwner)
   at Microsoft.ML.OnnxRuntime.InferenceSession.GetOrtValuesHandles(IReadOnlyCollection`1 values, MetadataLookup metaLookup, OrtValueHandleExtractor ortValueExtractor, DisposableArray`1& disposer)
   at Microsoft.ML.OnnxRuntime.InferenceSession.Run(IReadOnlyCollection`1 inputs, IReadOnlyCollection`1 outputNames, RunOptions options)
   at Microsoft.ML.OnnxRuntime.InferenceSession.Run(IReadOnlyCollection`1 inputs, IReadOnlyCollection`1 outputNames)
   at Microsoft.ML.OnnxRuntime.InferenceSession.Run(IReadOnlyCollection`1 inputs)
   at KokoroSharp.Core.KokoroModel.Infer(Int32[] tokens, Single[,,] voiceStyle, Single speed) in E:\GitHub\KokoroSharp\KokoroSharp\Core\KokoroModel.cs:line 58
   at KokoroSharp.Core.KokoroJob.Progress(KokoroModel model) in E:\GitHub\KokoroSharp\KokoroSharp\Core\KokoroJob.cs:line 28
   at KokoroSharp.Core.KokoroEngine.<.ctor>b__2_0() in E:\GitHub\KokoroSharp\KokoroSharp\Core\KokoroEngine.cs:line 20

参数错误说是
这时候需要通过下面的代码来检查模型的参数类型

using Microsoft.ML.OnnxRuntime;
// 原本使用的v1.0模型
using var kokoroV1 = new InferenceSession("kokoro.onnx");
// 现在打算使用的v1.1-zh模型
using var kokoroV1_1 = new InferenceSession("kokoro-v1.1-zh-mod.onnx");

Console.WriteLine("Kokoro v1.0");
foreach (var kvp in kokoroV1.InputMetadata)
{
    var name = kvp.Key;
    var meta = kvp.Value;

    Console.WriteLine($"name: {name}");
    Console.WriteLine($"type: {meta.ElementType}");
}
Console.WriteLine();
Console.WriteLine("Kokoro v1.1-zh");
foreach (var kvp in kokoroV1_1.InputMetadata)
{
    var name = kvp.Key;
    var meta = kvp.Value;

    Console.WriteLine($"name: {name}");
    Console.WriteLine($"type: {meta.ElementType}");
}

输出如下

Kokoro v1.0
name: tokens
type: System.Int64
name: style
type: System.Single
name: speed
type: System.Single

Kokoro v1.1-zh
name: tokens
type: System.Int64
name: style
type: System.Single
name: speed
type: System.Int32

可以看到speed参数类型不同,需要修改下
这里用python改入口参数的类型

import onnx
from onnx import helper, TensorProto

def ModEntryType(onnxModel: onnx.ModelProto, entryName: str, targetType) -> onnx.ModelProto:
    graph = onnxModel.graph
    for i, input_tensor in enumerate(graph.input):
        if input_tensor.name == entryName:
            # 获取原始维度
            shape = [
                dim.dim_value if dim.HasField("dim_value") else -1
                for dim in input_tensor.type.tensor_type.shape.dim
            ]
            new_input = helper.make_tensor_value_info(
                name=input_tensor.name, elem_type=targetType, shape=shape
            )

            # 替换旧输入
            graph.input.remove(input_tensor)
            graph.input.insert(i, new_input)
            onnx.checker.check_model(onnxModel)
    return onnxModel
    
model_path = "kokoro-v1.1-zh"
model = onnx.load(model_path + ".onnx")
ModEntryType(model, "tokens", TensorProto.FLOAT)
onnx.save(model, model_path + "-mod.onnx")

然后替换模型文件

获取新voice

现在模型是可以跑了,但输出的语音还是英文
参考v1.0的文档,可以知道示例代码中的af_heart表示一种语音
所以我们要找到中文的语音叫什么
而又参考v1.1-zh的python示例,可以知道示例中传入的是zf_001

但直接这样传入会报这样的错误

Unhandled exception. System.InvalidOperationException: Sequence contains no matching element
   at System.Linq.ThrowHelper.ThrowNoMatchException()
   at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate)
   at KokoroSharp.KokoroVoiceManager.GetVoice(String name) in E:\GitHub\KokoroSharp\KokoroSharp\HighLevel\KokoroVoiceManager.cs:line 33
   at Program.<Main>$(String[] args) in E:\Code\CSharp\RiderTest\Program.cs:line 34

就是说找不到zf_001这个元素

通过反编译,可以知道KokoroSharp是通过检索./voices文件夹来确定的
这个文件夹下面都是*.npy类型的文件
比如说之前的af_heart,就有af_heart.npy文件
zf_001的哪里找呢

在v1.1-zh-onnx的huggingface上,还有voice文件夹
里面就是这些voice文件了,但都是bin格式的,还需要做一个转换
代码如下

import numpy as np

bin_path = "zf_001.bin"
data = np.fromfile(bin_path, dtype=np.float32)
data = data.reshape((510, 1, 256))
np.save("zf_001.npy", data)

然后把输出文件添加到运行目录中的./voices中就可以了
(也就是在项目中添加一个构建时复制的文件)

现在再运行下面的代码

using KokoroSharp;
using KokoroSharp.Core;

// 经过参数名称与类型修改的模型文件
KokoroTTS tts = KokoroTTS.LoadModel("kokoro-v1.1-zh-mod.onnx");
KokoroVoice testVoice = KokoroVoiceManager.GetVoice("zf_001");
while (true)
{
    tts.Speak(Console.ReadLine(), testVoice);
}

总算是能跑了,但效果还是不大能满意

<0x02> 其他尝试

修改Tokenizer

我后面发现新的模型的tokenizer.json有变动
v1.0版本的config.jsonv1.1-zh版本的config.json
于是我就想把这些应用上
但KokoroSharp的Tokenizer将这些硬编码到代码中
所以我只能把代码clone下来修改
修改后只能说差别不大

尝试pull request中的修改

参考这个pull request修改了代码
也是差别不大

<0x03> 没办法

最后实在没办法,找到用onnx跑这个模型的python代码
跑了一下,一听,您猜怎么着
跑出来效果跟我用C#跑的差不多烂
所以我觉得是torch转换到onnx导致效果没那么好
比起之前的话更像是不会中文的人说中文,也算有点效果

Licensed under CC BY-NC-SA 4.0