Unity+Qwen2.5 Omni 源码 | 多模态端到端交互的AI二次元小姐姐,文本/语音/图片/视频全模态交互实测与交互示例

一、前言

    前段时候阿里开源了千问Omni多模态模型,根据官方介绍以及效果演示视频可以了解到,这个模型是多模态模型,可以支持多种不同的输入形式,包括了文本、文本+音频、文本+图片以及文本+视频的多模态输入。 并且模型输出形式不局限于文本了,同时输出语音合成结果,省去了再去集成其他语音模型的流程,并且模型反馈的速度非常的快,整体的体验效果相当出色。从阿里云百炼官方平台查看了关于Omni模型的文档,那目前阿里官方是通过了商业版以及开源两个可用版本,开源版虽然只有7B的参数量,但是实际上,这个模型是多模态模型,本地部署要跑起来所需要的显存要求的比较大,据其他网友提供的资料,大致需要70GB的显存容量,才能够跑起来全部的功能。 对普通用户来说,使用官方提供的商业版API接口,就能够获得稳定高效的接口服务,这是比较推荐的方式,具体的接口加入如下所示:

二、Omni接口规范

        本节内容将根据阿里官方文档,说明Qwen Omni模型的接口规范。

        1、基本报文

        根据官方接口文档的说明,常规的文本输入接口是兼容OpenAI API的,其中,发送的报文结果与openAI的接口报文格式一样,如下所示:

curl -X POST https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions \ -H "Authorization: Bearer $DASHSCOPE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "qwen-omni-turbo", "messages": [ { "role": "user", "content": "你是谁?" } ], "stream":true, "stream_options":{ "include_usage":true }, "modalities":["text","audio"], "audio":{"voice":"Cherry","format":"wav"} }'

    这里我们注意一下,stream选项,默认是true,即表示采用流式输出。

        modalities配置里,默认值是["text","audio"],表示模型返回的数据,包含文本以及音频数据,如果不需要音频,只保留["text"]就可以了。

        audio配置,定义了返回的音频的音色,以及音频格式。根据官方文档内容,输出音频的音色与文件格式(只支持设定为"wav")通过audio参数来配置,如:audio={"voice": "Cherry", "format": "wav"},其中商业版模型voice参数可选值为:["Cherry", "Serena", "Ethan", "Chelsie"],开源版模型voice参数可选值为:["Ethan", "Chelsie"]

        2、图片/音频/视频报文

        这里我们不使用SDK,因此发送的报文中,需要把多媒体资源转换为base64编码格式,并拼接到报文中,进行传输。

        1)文本+图片的报文格式

curl -X POST https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions \ -H "Authorization: Bearer $DASHSCOPE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "qwen-omni-turbo", "messages": [ { "role": "user", "content": [ { "type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64_image}"} }, { "type": "text", "text": "图中描绘的是什么景象?" } ] } ], "stream":true, "stream_options":{ "include_usage":true }, "modalities":["text","audio"], "audio":{"voice":"Cherry","format":"wav"} }'

     其中,{base64_image}修改为你上传的图片的base64编码。

        2)文本+音频的报文格式

curl -X POST https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions \ -H "Authorization: Bearer $DASHSCOPE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "qwen-omni-turbo", "messages": [ { "role": "user", "content": [ { "type": "input_audio", "input_audio": { "data": f"data:;base64,{base64_audio}", "format": "mp3" } }, { "type": "text", "text": "这段音频在说什么" } ] } ], "stream":true, "stream_options":{ "include_usage":true }, "modalities":["text","audio"], "audio":{"voice":"Cherry","format":"wav"} }'

    其中,{base64_audio}修改为需要输入的音频文件的base64编码,需要注意的是,音频的格式需要为mp3.

        3)文本+视频的报文格式

        输入视频的报文支持两种方式,①传送视频文件数据;②传输图片序列数据。

        以下是传输视频文件数据的报文格式:

curl -X POST https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions \ -H "Authorization: Bearer $DASHSCOPE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "qwen-omni-turbo", "messages": [ { "role": "user", "content": [ { "type": "video_url", "video_url": {"url": f"data:;base64,{base64_video}"}, }, { "type": "text", "text": "视频的内容是什么" } ] } ], "stream":true, "stream_options": { "include_usage": true }, "modalities":["text","audio"], "audio":{"voice":"Cherry","format":"wav"} }'

        其中,{base64_video}修改为你需要导入的视频文件的base64编码数据。        

        传输图片序列的报文格式:

curl -X POST https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions \ -H "Authorization: Bearer $DASHSCOPE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "qwen-omni-turbo", "messages": [ { "role": "user", "content": [ { "type": "video", "video": [ f"data:image/jpeg;base64,{base64_image_1}", f"data:image/jpeg;base64,{base64_image_2}", f"data:image/jpeg;base64,{base64_image_3}", f"data:image/jpeg;base64,{base64_image_4}", ], }, { "type": "text", "text": "描述这个视频的具体过程" } ] } ], "stream": true, "stream_options": { "include_usage": true }, "modalities": ["text", "audio"], "audio": { "voice": "Cherry", "format": "wav" } }'

        其中,video的列表应存储为图片序列的base编码,{base64_image_1}对用各个图片的base64编码数据。

        3、返回的报文结果

        使用音频模式时,模型只能返回流式数据,返回的报文结构如下所示:

data: {"choices": [{"finish_reason":null, "delta": {"audio": {"transcript":"我是"}}, "index":0,"logprobs":null}], "object":"chat.completion.chunk", "usage":null,"created":1747031484, "system_fingerprint":null, "model":"qwen-omni-turbo", "id":"chatcmpl-92c43996-1c01-90c0-86a6-197f7e00ec5a" }

        我们需要逐条处理返回的结果,获取结果数据后进行解析与处理。需要注意,最后一条返回的数据结构为:data: [DONE] 我们可以根据解析结果是否包含[DONE] 来判断是否处理完成。

三、Unity端的代码实现

        本例是使用Unity实现的二次元小姐姐交互项目,通过集成Omni模型的接口,实现纯文本、文本+实时语音互动、文本+本地音频文件、文本+本地图片以及文本+本地视频文件的AI互动交流效果的代码实现,项目演示效果可以查看文末的视频。

        以下是针对不同类型媒体资源的关键代码实现:

        1、纯文本输入

/// <summary> /// 只发送文本 /// </summary> /// <param name="_postWord"></param> /// <param name="_callback"></param> /// <returns></returns> public IEnumerator OnTxtRequest(string _postWord, System.Action<string> _callback) { stopwatch.Restart(); using (UnityWebRequest request = new UnityWebRequest(url, "POST")) { // 初始化请求头和数据 var _sendWord = new SendData(); _sendWord.role = "user"; TextContentData _textContent= new TextContentData(); _textContent.text = _postWord; _sendWord.content.Add(_textContent); PostTextData _postData = new PostTextData(); _postData.model = m_ModelName; _postData.messages.Add(_sendWord); _postData.stream = true; _postData.modalities.Add("text"); _postData.modalities.Add("audio"); _postData.audio.voice = m_VoiceType.ToString();//音色 string _jsonText = JsonConvert.SerializeObject(_postData); //string _jsonText = JsonUtility.ToJson(_postData); byte[] data = Encoding.UTF8.GetBytes(_jsonText); request.uploadHandler = new UploadHandlerRaw(data); request.downloadHandler = new DownloadHandlerBuffer(); request.SetRequestHeader("Content-Type", "application/json"); request.SetRequestHeader("Authorization", $"Bearer {api_key}"); // 异步发送请求 request.SendWebRequest(); int bytesReceived = 0; // 实时处理流数据 while (!request.isDone) { // 获取最新接收的字节数 int newBytes = request.downloadHandler.data != null ? request.downloadHandler.data.Length : 0; if (newBytes > bytesReceived) { // 提取新增数据并转换 byte[] newData = new byte[newBytes - bytesReceived]; Array.Copy(request.downloadHandler.data, bytesReceived, newData, 0, newData.Length); string chunk = Encoding.UTF8.GetString(newData); // 处理数据块 ProcessChunk(chunk, _callback); bytesReceived = newBytes; } yield return null; } // 处理剩余数据 if (request.downloadHandler.data != null && bytesReceived < request.downloadHandler.data.Length) { byte[] remainingData = new byte[request.downloadHandler.data.Length - bytesReceived]; Array.Copy(request.downloadHandler.data, bytesReceived, remainingData, 0, remainingData.Length); ProcessChunk(Encoding.UTF8.GetString(remainingData), _callback); } // 错误处理 if (request.result != UnityWebRequest.Result.Success) { Debug.LogError($"Error: {request.error}"); } } stopwatch.Stop(); Debug.Log($"Total time: {stopwatch.Elapsed.TotalSeconds}s"); }

  2、文本+音频输入

/// <summary> /// 发送音频以及文本 /// </summary> /// <param name="_postWord"></param> /// <param name="_base64"></param> /// <param name="_callback"></param> /// <returns></returns> public IEnumerator OnVoiceAndTextRequest(string _postWord, string _base64,System.Action<string> _callback) { stopwatch.Restart(); using (UnityWebRequest request = new UnityWebRequest(url, "POST")) { if (_postWord == "") { _postWord = "请根据语音内容进行回答"; } // 初始化请求头和数据 var _sendWord = new SendData(); _sendWord.role = "user"; VoiceContentData _voiceContent=new VoiceContentData(); _sendWord.content.Add(_voiceContent); _voiceContent.input_audio.data += _base64; //添加文本 TextContentData _textContent=new TextContentData(); _textContent.text = _postWord; _sendWord.content.Add(_textContent); PostTextData _postData = new PostTextData(); _postData.model = m_ModelName; _postData.messages.Add(_sendWord); _postData.stream = true; _postData.modalities.Add("text"); _postData.modalities.Add("audio"); _postData.audio.voice = m_VoiceType.ToString();//音色 string _jsonText = JsonConvert.SerializeObject( _postData ); //string _jsonText = JsonUtility.ToJson(_postData); byte[] data = Encoding.UTF8.GetBytes(_jsonText); request.uploadHandler = new UploadHandlerRaw(data); request.downloadHandler = new DownloadHandlerBuffer(); request.SetRequestHeader("Content-Type", "application/json"); request.SetRequestHeader("Authorization", $"Bearer {api_key}"); // 异步发送请求 request.SendWebRequest(); int bytesReceived = 0; // 实时处理流数据 while (!request.isDone) { // 获取最新接收的字节数 int newBytes = request.downloadHandler.data != null ? request.downloadHandler.data.Length : 0; if (newBytes > bytesReceived) { // 提取新增数据并转换 byte[] newData = new byte[newBytes - bytesReceived]; Array.Copy(request.downloadHandler.data, bytesReceived, newData, 0, newData.Length); string chunk = Encoding.UTF8.GetString(newData); // 处理数据块 ProcessChunk(chunk, _callback); bytesReceived = newBytes; } yield return null; } // 处理剩余数据 if (request.downloadHandler.data != null && bytesReceived < request.downloadHandler.data.Length) { byte[] remainingData = new byte[request.downloadHandler.data.Length - bytesReceived]; Array.Copy(request.downloadHandler.data, bytesReceived, remainingData, 0, remainingData.Length); ProcessChunk(Encoding.UTF8.GetString(remainingData), _callback); } // 错误处理 if (request.result != UnityWebRequest.Result.Success) { Debug.LogError($"Error: {request.error}"); } } stopwatch.Stop(); Debug.Log($"Total time: {stopwatch.Elapsed.TotalSeconds}s"); }

 3、文本+图片输入

/// <summary> /// 发送图片与文本 /// </summary> /// <param name="_postWord"></param> /// <param name="_img_base64"></param> /// <param name="_callback"></param> /// <returns></returns> public IEnumerator OnImageAndTextRequest(string _postWord,string _img_base64, System.Action<string> _callback) { stopwatch.Restart(); using (UnityWebRequest request = new UnityWebRequest(url, "POST")) { // 初始化请求头和数据 var _sendWord = new SendData(); _sendWord.role = "user"; ImageContentData _imgContent = new ImageContentData(); _sendWord.content.Add(_imgContent); _imgContent.image_url.url += _img_base64; //添加文本 TextContentData _textContent = new TextContentData(); _textContent.text = _postWord; _sendWord.content.Add(_textContent); PostTextData _postData = new PostTextData(); _postData.model = m_ModelName; _postData.messages.Add(_sendWord); _postData.stream = true; _postData.modalities.Add("text"); _postData.modalities.Add("audio"); _postData.audio.voice = m_VoiceType.ToString();//音色 string _jsonText = JsonConvert.SerializeObject(_postData); //string _jsonText = JsonUtility.ToJson(_postData); byte[] data = Encoding.UTF8.GetBytes(_jsonText); request.uploadHandler = new UploadHandlerRaw(data); request.downloadHandler = new DownloadHandlerBuffer(); request.SetRequestHeader("Content-Type", "application/json"); request.SetRequestHeader("Authorization", $"Bearer {api_key}"); // 异步发送请求 request.SendWebRequest(); int bytesReceived = 0; // 实时处理流数据 while (!request.isDone) { // 获取最新接收的字节数 int newBytes = request.downloadHandler.data != null ? request.downloadHandler.data.Length : 0; if (newBytes > bytesReceived) { // 提取新增数据并转换 byte[] newData = new byte[newBytes - bytesReceived]; Array.Copy(request.downloadHandler.data, bytesReceived, newData, 0, newData.Length); string chunk = Encoding.UTF8.GetString(newData); // 处理数据块 ProcessChunk(chunk, _callback); bytesReceived = newBytes; } yield return null; } // 处理剩余数据 if (request.downloadHandler.data != null && bytesReceived < request.downloadHandler.data.Length) { byte[] remainingData = new byte[request.downloadHandler.data.Length - bytesReceived]; Array.Copy(request.downloadHandler.data, bytesReceived, remainingData, 0, remainingData.Length); ProcessChunk(Encoding.UTF8.GetString(remainingData), _callback); } // 错误处理 if (request.result != UnityWebRequest.Result.Success) { Debug.LogError($"Error: {request.error}"); } } stopwatch.Stop(); Debug.Log($"Total time: {stopwatch.Elapsed.TotalSeconds}s"); }

 4、文本+视频输入

/// <summary> /// 发送视频与文本 /// </summary> /// <param name="_postWord"></param> /// <param name="_video_base64"></param> /// <param name="_callback"></param> /// <returns></returns> public IEnumerator OnVideoAndTextRequest(string _postWord, string _video_base64, System.Action<string> _callback) { stopwatch.Restart(); using (UnityWebRequest request = new UnityWebRequest(url, "POST")) { // 初始化请求头和数据 var _sendWord = new SendData(); _sendWord.role = "user"; VideoContentData _videoContent = new VideoContentData(); _sendWord.content.Add(_videoContent); _videoContent.video_url.url += _video_base64; //添加文本 TextContentData _textContent = new TextContentData(); _textContent.text = _postWord; _sendWord.content.Add(_textContent); PostTextData _postData = new PostTextData(); _postData.model = m_ModelName; _postData.messages.Add(_sendWord); _postData.stream = true; _postData.modalities.Add("text"); _postData.modalities.Add("audio"); _postData.audio.voice = m_VoiceType.ToString();//音色 string _jsonText = JsonConvert.SerializeObject(_postData); //string _jsonText = JsonUtility.ToJson(_postData); byte[] data = Encoding.UTF8.GetBytes(_jsonText); request.uploadHandler = new UploadHandlerRaw(data); request.downloadHandler = new DownloadHandlerBuffer(); request.SetRequestHeader("Content-Type", "application/json"); request.SetRequestHeader("Authorization", $"Bearer {api_key}"); // 异步发送请求 request.SendWebRequest(); int bytesReceived = 0; // 实时处理流数据 while (!request.isDone) { // 获取最新接收的字节数 int newBytes = request.downloadHandler.data != null ? request.downloadHandler.data.Length : 0; if (newBytes > bytesReceived) { // 提取新增数据并转换 byte[] newData = new byte[newBytes - bytesReceived]; Array.Copy(request.downloadHandler.data, bytesReceived, newData, 0, newData.Length); string chunk = Encoding.UTF8.GetString(newData); // 处理数据块 ProcessChunk(chunk, _callback); bytesReceived = newBytes; } yield return null; } // 处理剩余数据 if (request.downloadHandler.data != null && bytesReceived < request.downloadHandler.data.Length) { byte[] remainingData = new byte[request.downloadHandler.data.Length - bytesReceived]; Array.Copy(request.downloadHandler.data, bytesReceived, remainingData, 0, remainingData.Length); ProcessChunk(Encoding.UTF8.GetString(remainingData), _callback); } // 错误处理 if (request.result != UnityWebRequest.Result.Success) { Debug.LogError($"Error: {request.error}"); } } stopwatch.Stop(); Debug.Log($"Total time: {stopwatch.Elapsed.TotalSeconds}s"); }

5、文本+图片序列输入

/// <summary> /// 发送图片序列 /// </summary> /// <param name="_postWord"></param> /// <param name="_img_base64"></param> /// <param name="_callback"></param> /// <returns></returns> public IEnumerator OnImageFrameAndTextRequest(string _postWord, List<string> _img_base64, System.Action<string> _callback) { stopwatch.Restart(); using (UnityWebRequest request = new UnityWebRequest(url, "POST")) { // 初始化请求头和数据 var _sendWord = new SendData(); _sendWord.role = "user"; ImageFrameContentData _imageFrameContent = new ImageFrameContentData(); _sendWord.content.Add(_imageFrameContent); foreach(var item in _img_base64) { string _val = "data:image/jpeg;base64," + item; _imageFrameContent.video.Add(_val); } //添加文本 TextContentData _textContent = new TextContentData(); _textContent.text = _postWord; _sendWord.content.Add(_textContent); PostTextData _postData = new PostTextData(); _postData.model = m_ModelName; _postData.messages.Add(_sendWord); _postData.stream = true; _postData.modalities.Add("text"); _postData.modalities.Add("audio"); _postData.audio.voice = m_VoiceType.ToString();//音色 string _jsonText = JsonConvert.SerializeObject(_postData); //string _jsonText = JsonUtility.ToJson(_postData); byte[] data = Encoding.UTF8.GetBytes(_jsonText); request.uploadHandler = new UploadHandlerRaw(data); request.downloadHandler = new DownloadHandlerBuffer(); request.SetRequestHeader("Content-Type", "application/json"); request.SetRequestHeader("Authorization", $"Bearer {api_key}"); // 异步发送请求 request.SendWebRequest(); int bytesReceived = 0; // 实时处理流数据 while (!request.isDone) { // 获取最新接收的字节数 int newBytes = request.downloadHandler.data != null ? request.downloadHandler.data.Length : 0; if (newBytes > bytesReceived) { // 提取新增数据并转换 byte[] newData = new byte[newBytes - bytesReceived]; Array.Copy(request.downloadHandler.data, bytesReceived, newData, 0, newData.Length); string chunk = Encoding.UTF8.GetString(newData); // 处理数据块 ProcessChunk(chunk, _callback); bytesReceived = newBytes; } yield return null; } // 处理剩余数据 if (request.downloadHandler.data != null && bytesReceived < request.downloadHandler.data.Length) { byte[] remainingData = new byte[request.downloadHandler.data.Length - bytesReceived]; Array.Copy(request.downloadHandler.data, bytesReceived, remainingData, 0, remainingData.Length); ProcessChunk(Encoding.UTF8.GetString(remainingData), _callback); } // 错误处理 if (request.result != UnityWebRequest.Result.Success) { Debug.LogError($"Error: {request.error}"); } } stopwatch.Stop(); Debug.Log($"Total time: {stopwatch.Elapsed.TotalSeconds}s"); }

       6、报文结构定义类

// 以下为数据结构定义 [Serializable] public class SendData { public string role; public List<ContentData> content=new List<ContentData>(); } [Serializable] public class PostTextData { public string model; public List<SendData> messages = new List<SendData>(); public bool stream = true; public List<string> modalities = new List<string>(); public AudioSet audio= new AudioSet(); } [Serializable] public class ContentData { public string type = ""; } /// <summary> /// 文本类型报文 /// </summary> [Serializable] public class TextContentData : ContentData { public TextContentData() { type = "text"; } public string text = ""; } /// <summary> /// 文本+音频 /// </summary> [Serializable] public class VoiceContentData: ContentData { public VoiceContentData() { type = "input_audio"; } public AudioInput input_audio=new AudioInput(); } [Serializable] public class AudioInput { public string data = "data:;base64,";//,后添加base64音频编码 public string format = "wav";//wav,mp3 } /// <summary> /// 文本+图片 /// </summary> [Serializable] public class ImageContentData : ContentData { public ImageContentData() { type = "image_url"; } public ImageInput image_url = new ImageInput(); } public class ImageInput { public string url = "data:image/png;base64,";//,后添加base64图片编码 } /// <summary> /// 文本+视频 /// </summary> [Serializable] public class VideoContentData : ContentData { public VideoContentData() { type = "video_url"; } public VideoInput video_url = new VideoInput(); } [Serializable] public class VideoInput { public string url = "data:;base64,";//,后添加base64视频编码 } /// <summary> /// 文本+图片序列 /// </summary> [Serializable] public class ImageFrameContentData : ContentData { public ImageFrameContentData() { type = "video"; } public List<string> video = new List<string>();// "data:image/jpeg;base64,";//,后添加base64图片编码 } [Serializable] public class MessageBack { public List<Choice> choices = new List<Choice>(); } [Serializable] public class Choice { public Delta delta=new Delta(); } [Serializable] public class Delta { public string role= string.Empty; public string content= string.Empty; public Audio audio=new Audio(); } [Serializable] public class AudioSet { public string voice = "Cherry";//Cherry、Serena、Ethan、Chelsie public string format = "wav"; } [Serializable] public class Audio { public string transcript=""; public string data=""; } /// <summary> /// 音色 /// </summary> public enum VoiceType { Cherry, Serena, Ethan, Chelsie }

四、结束语  

        本文针对最新的阿里开源的Qwen Omni2.5 - 7B多模态模型的接口规范进行介绍,并提供了在Unity端与Omni官方接口对接的代码实现示例,本文包含的代码为接口对接的核心代码,多模态端到端交互效果可以查阅以下视频。

演示效果

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容