目录

Unity-GPT-SoVITS接入处理GPTAPI的SSE响应流

Unity–GPT-SoVITS接入、处理GPTAPI的SSE响应流

GPT-SoVITS

GPT-SoVITS- v2(v3也可以,两者对模型文件具有兼容)

https://i-blog.csdnimg.cn/direct/c58af17601404eed89a6a4efd2a7721e.png

点击后 会进入新的游览器网页

https://i-blog.csdnimg.cn/direct/66d5cd09caa24d8d91d1bff3dfa5aa18.png


看了一圈,发现主要问题集中在模型的训练很需要CPU,也就是模型的制作上,问题很多,如果有现有的模型,直接引用,使用“推理”即可

就比如用

这个up主练出来的模型

自己要做的其实就很少了

https://i-blog.csdnimg.cn/direct/1210b368abf14f32b1e90a2240fafd55.png

导入之后重启webui.bat

再开启TTS推理WebUI,就进入了UI界面,自己点击交互设置,然后产出结果可以直接用

https://i-blog.csdnimg.cn/direct/51142e53c9da4ef1b7bca26e4540636c.png

但这是SoVITS的界面交互,想在unity里使用,必须要用提供的API(不同于网络API的本地API,不需要联网,可以直接处理)

这个up的项目现在也在维护,进项目里了解是可以的,但是在里面的使用过程总是有一些报错,想解决比较麻烦

但是还是可以了解重要的区域是哪一块,哪里要改

而且up对功能的拆分也很不错,LLM类和TTS类分开,大概率是用了继承,然后下面的多个模型都可以使用,在各自里写网络传递请求的方法


https://i-blog.csdnimg.cn/direct/02456e69a75446a6825a2fdb40271f3e.png

ViTS的Api调用,是和webui分开的一种更偏向接口功能切割,整块提供的,所以只需要api.bat文件打开,就可以连接api了

https://i-blog.csdnimg.cn/direct/588a1bbf083948179884034aea61ef0e.png

对于这个http

用这个代替才能进fastapi

https://i-blog.csdnimg.cn/direct/7f3ab6fc639540dbb64413d486d9f915.png

然后就进入了api的模块,,这里有参数设置然后自动生成请求格式的

这里的路径是可以填绝对路径的

https://i-blog.csdnimg.cn/direct/a030e1e0bd134f039e29b91249ca2ec1.png

prompt都是传入的材料的

之后的text则是需要生成的文本、语言(再后面的可以调整也可以不动)

https://i-blog.csdnimg.cn/direct/fc1cf26931a54a65bc2b6beb5dd07a06.png

api的格式,,在这生成的是一模一样的

URL 传输时,会对 特殊字符 进行 百分号编码(Percent-Encoding) ,比如:

  • \ (反斜杠) → %5C
  • : (冒号) → %3A
  • 日文、中文等非 ASCII 字符 会用 UTF-8 编码 转成 %E7%等形式
  • 空格%20

所以会出现乱码一样的情况,但是信息没有变

把这一段直接粘贴到游览器,可以直接得到生成 的音频文件,

(换行清晰格式
localhost:9880/
?refer_wav_path=D%3A%5CBaiduNetdiskDownload%5CUnity_Related%5CIroha%5C参考音频%5C测试参考音频%5C001.私としては奉仕部が協力してくれるのが一番%E3%80%80面倒がないんです.wav
&prompt_text=私としては奉仕部が協力してくれるのが一番%E3%80%80面倒がないんです
&prompt_language=日文
&text=コネクタの使い方すら知らないなんてバカなの?
&text_language=日文
&top_k=15
&top_p=1
&temperature=1
&speed=1

TITS里的切分,是为了生成的时候不跳字吞字而切分的

但是默认的基础api里是不切分的,很容易出现问题

那问题来了,我要怎么整合到unity里用,通过这个网站上的这种格式简化肯定不现实

下面是api的注释内容,

基于 HTTP 请求的语音合成 API ,支持 GETPOST 请求,并返回音频流或 JSON 结果。

在 Unity 里调用它,需要构建 HTTP 请求 并处理 返回的音频数据

简单测试的,用GET 请求,参数放在 URL 里 (可见),要传的数据量少

URL 可直接访问 ,可以用浏览器打开测试

POST 请求,参数放在请求体里(Body) ,不会出现在 URL,不能用游览器打开

适用于复杂请求 (大数据/JSON)浏览器不会缓存,数据不会暴露在 URL

支持 JSON、文件上传等复杂数据

https://i-blog.csdnimg.cn/direct/5eb1b15c11794659a14fcf4d7b5b925d.png

直接按照api注释里的来不行

https://i-blog.csdnimg.cn/direct/9d3ea70ed2584303a2820ce812172190.png

在unity中要使用http,在这里改成allow,,默认是为了安全不允许使用的

https://i-blog.csdnimg.cn/direct/a2d0b5d5127548fcb2a2fdb5e8223e2a.png

改了之后,就可以收到并和api产生交互了

本来以为这些填进去的东西一次只能一填,但并不是这样的 https://i-blog.csdnimg.cn/direct/8108e56be7f64e48ab787c284211e111.png

这个网站 是api文档,,对传输的输入字符串 的格式规则的介绍

并没有什么很复杂的地方

要做的是在unity的C#脚本里调用传入这段字符串的时候,这段字符串是符合格式的,是可以丢给UnityWebRequest去做访问请求,并且拿到返回的数据的

https://i-blog.csdnimg.cn/direct/3bcb53f1094e456caa2456d29b448a82.png

也就是说,在APi文档介绍下,我复制这种格式,然后把需要的部分变成变量,其实就已经可以做到APi较好的交互了

现在unity里已经可以发出声音了,,不过这个效果的确是一言难尽,

原来那些B站上发视频这么像的,都是自己一句一句挑最好的表现出来,,

那确实很肝,


处理GPTAPI的SSE响应流

功能是串通了,可以做到输入文本并且朗读出来了

https://i-blog.csdnimg.cn/direct/8027893f03dd4642bff5e8a33314db7e.png

但是没有使用流,  等待文本彻底发回—-将文本整个进行TTS,有较长的等待周期,(对于DeepSeek这点更加严重,GPT回复的相对更快,两者的API文档差距并没有很多,接下来还是以GPT的API调用来develop)

为了改进,所以先去了解一下gpt的api调用里,是怎么规定实现流的传输的

想让GPT一边生成文本,TTS立刻 解析并且读出来

对于这个的解决方案就是多种的

比如GPT每生成一段文本到逗号和句号的位置,TTS就拆开了一段,并且解析成需要播放的资源

GPT生成文本的速度一般快于TTS转–读的速度,那是否需要一个queue来装载后续的每个文件,然后一直播放?这样会不会出现语句衔接不自然的问题?(但现在主要是解决等待问题,这一方面可以不考虑)

先是得到SSE的响应,这在OpenAI的API请求上有变化

下面是第一次成功用cmd接收了SSE响应的格式

curl -N -X POST "https://api.openai.com/v1/chat/completions" ^
  -x http://127.0.0.1:7890 ^
  -H "Authorization: Bearer sk-你的API密钥" ^
  -H "Content-Type: application/json" ^
  -H "Accept: text/event-stream" ^
  -H "Cache-Control: no-cache" ^
  -d "{ \"model\": \"gpt-4o\", \"messages\": [ { \"role\": \"user\", \"content\": \"和我讲讲陶喆是谁\" } ], \"stream\": true }"

这里坑最多的还是curl的测试,对语法的忽视,而这一点AI同样经常忽视

换行符,在很多地方粘贴为了美观会自动加上(或者自动删除没有内容的连续空格),这就让curl真的没有用都浑然不知,,比如我粘贴到了CSDN上,然后再复制,就不能在cmd窗口运行了

所以很有必要了解cmd的书写规则


cmdEnter 解析为命令的结束 换行必须用 ^( ^ 后面不能有多余的空格 )

路径中有空格,必须 用双引号包裹( -d 里JSON 里的空格不影响

https://i-blog.csdnimg.cn/direct/05637a5802dd40289a36a8973968a5f5.png

curl 允许多个空格,但少部分情况下会出现问题,

出现More?的时候才是说明你是想为了美观用 ^换行了,本质上还是一串黏在一起的

在之后的测试里(注意这里^后面的空格是为了分开,在cmd里绝对不能加)

-X POST  并不必要特意声明post请求类型

-N 也不必要特意声明是(逐行读取流式数据,对于curl)

-x http://127.0.0.1:7890 ^  至关重要 这段对代理的引用,在cmd里curl发送请求给服务器(应该是cmd自己的问题,curl总是不按照代理的来走,手动set Proxy代理也出问题)

-H “Accept: text/event-stream” ^    这个也并不重要,可以去掉

-H “Cache-Control: no-cache” ^    这句也不重要(大部分应该都是openai的api网站已经配置好了)

最后保留的的确只有官方文档里的了curl请求格式了(注意一下换行符和空格)

——-curl可以访问之后,是想c#如何可以接收这种持久输出的格式–

可以接受响应了(apikey的粘贴复制容易出现一些莫名其妙的误差,会提示unauthorized就说明响应是没问题的,只是apikey有格式的问题,去了解一下key传过来的复制粘贴会不会额外动里面的数据)

https://i-blog.csdnimg.cn/direct/fde130734ec149289a848eab3aa6daff.png

在请求根据官方api格式发了之后,

 目前接受响应的办法,但是这些都是一次性发送到unity的,没有做到逐条更新
 var response = await httpClient.PostAsync(apiUrl, new StringContent(jsondata, Encoding.UTF8, "application/json"));
        Debug.Log(response.StatusCode);
        Debug.Log(response.Content.ReadAsStringAsync().Result);
        using (var stream = await response.Content.ReadAsStreamAsync())
        using (var reader = new StreamReader(stream, Encoding.UTF8))
        {
            string line;
            while ((line = await reader.ReadLineAsync()) != null)
            {
                if (line.StartsWith("data:"))
                {
                    string data = line.Substring(5);
                    // 在这里处理接收到的数据
                    Debug.Log(data);
                }
            }
        }

完结,

的确是因为httpclient本身的方法里没有可以设置接受请求头就返回的方法

要换成SendAsync(HttpRequestMessage requst)

必须 自己构建这个Message的内部消息,让httpclient帮忙发送

https://i-blog.csdnimg.cn/direct/6030e74e31354b6d8ba26a208b86f9cc.png

整个完整的函数,在各个类功能正常运行的情况下逻辑一定是没问题的(如果有问题就检查自己的httpclient要不要额外设置代理Proxy,还有就是apikey的复制粘贴可能自动修改内容的问题)

 async Task GetChatStream(string userInput)
    {
        // 请求的 JSON 数据
        var requestData = new
        {
            model = "gpt-4",
            messages = new[]
            {
                new { role = "system",content = CharacterSetting },
                new { role = "user", content = userInput }

            },
            stream = true
        };
        string jsondata = JsonConvert.SerializeObject(requestData);
        // Print the complete request in a format that can be used directly with curl
        //string curlCommand = $"curl -X POST \"{apiUrl}\" -H \"Authorization: Bearer {apiKey}\" -H \"Content-Type: application/json\" -d '{jsondata}'";
        Debug.Log("jsondataBody_Ready");

        var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, apiUrl)
        {
            Content = new StringContent(jsondata, Encoding.UTF8, "application/json")
        };
        httpRequestMessage.Headers.TryAddWithoutValidation("Authorization", $"Bearer {apiKey}");
        //*****************************************
        var response = await httpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead);
        Debug.Log("response.StatusCode:" + response.StatusCode);
        //Debug.Log(response.Content.ReadAsStringAsync().Result);
        using (var stream = await response.Content.ReadAsStreamAsync())
        {
          //  Debug.Log("stream");
            using (var reader = new StreamReader(stream, Encoding.UTF8))
            {
               // Debug.Log("reader");
                string line;
                while ((line = await reader.ReadLineAsync()) != null)
                {
                  //  Debug.Log("line");
                    if (line.StartsWith("data:"))
                    {
                        string chunkdata = line.Substring(5);
                        // 在这里处理接收到的数据
                        var openAIChunk = JsonConvert.DeserializeObject<OpenAIChunk>(chunkdata);
                        Debug.Log(openAIChunk.Choices[0].Delta.Content);
                        text.text += openAIChunk.Choices[0].Delta.Content;
                    }
                }
            }
        }
    }

优化整理

需要的功能:玩家的对话,AI记得聊天历史

项目启动的时候自动打开SoVITS,可以的话不要显示控制台

目前暂时定这两个需求

之后的打算:把这个2d的交互场景设置到3D场景里,在3D的场景里实现这个2D窗口的所有功能


目前先做一个Scroll view,可以滚动查看聊天记录

https://i-blog.csdnimg.cn/direct/ee91e89cda4942cfb9035f1cf4faf826.gif

配置好了,接下来接入SoVITS,还有角色动嘴

//关于SoVITS的API打开,想了一下还是不管窗口显隐的问题了,还有打开会需要10秒左右的时间,不能产生空判断,这一段的过程比较重复,是一些使用信息的积累,可以做但是还是先管更重要的东西

—所以接下来都假设在SoVITS已经打开好的条件下(正常来说游戏的进入动画就可以把这些给做了,我在这做了也不知道实际如何)

Process类也不用了,直接到队列读取那一块了

https://i-blog.csdnimg.cn/direct/229effc987924d53a193cf44a56c7f7d.png

https://i-blog.csdnimg.cn/direct/67d15d9ec81940c4b854f480be8fc9c6.png

这里把对话内容的 实时更新Text组件 和  拆分句子给SoVITS去转换成audioClip文件播放  分开

而且返回的响应一般是一个字的居多,也就不考虑一次来的响应里有多个字符,标点在前面了

https://i-blog.csdnimg.cn/direct/65c98b2f833d412484c15ccf59eac93f.png

很好,看的到播放的队列正常更新了,Clip队列也一直在运作

https://i-blog.csdnimg.cn/direct/2b5806384e5444a2887af4705dde1cc0.png

现在就差口型的绑定了,这一部分就直接绑定audioSource就可以了

https://i-blog.csdnimg.cn/direct/43e8044e2645402281bd2b60ca60b6c5.png

就会根据音量调整嘴巴的闭合

https://i-blog.csdnimg.cn/direct/36739be5a5494550822a3c7b3f40d550.png

存在嘴巴没有跟着张开的情况,这里的模式调整为override,就可以避免视线 追踪 对2D模型的动画状态一直在调整 https://i-blog.csdnimg.cn/direct/229a6b9d94554976b32538565a26dd47.png

这里是调整声音大小的标准,让嘴巴在正确的区间里开闭