概要

本方详细介绍了如何在Xamarin From中如何实现离线语音识别合生,并提供解决方案与部分代码。这里我们选用 sherpa 开源项目部署语音识别、合成等模型。离线语音识别库有whisper、kaldi、pocketshpinx等,在了解这些库的时候,发现了所谓“下一代Kaldi”的sherpa。从文档和模型名称看,它是一个很新的离线语音识别库,支持中英双语识别,文件和实时语音识别。sherpa是一个基于下一代 Kaldi 和 onnxruntime 的开源项目,专注于语音识别、文本转语音、说话人识别和语音活动检测(VAD)等功能。该项目支持在没有互联网连接的情况下本地运行,适用于嵌入式系统、Android、iOS、Raspberry Pi、RISC-V 和 x86_64 服务器等多种平台。支持流式语音处理。

他有 ncnn、onnx 等平台的子项目:
https://github.com/k2-fsa/sherpa-onnx
https://github.com/k2-fsa/sherpa-ncnn

包含的功能如下:

功能 描述
实时语音识别 (Streaming Speech Recognition) 在语音输入的同时进行处理和识别,适用于需要即时反馈的场景,如会议和语音助手。
非实时语音识别 (Non-Streaming Speech Recognition) 在录制完毕后进行处理,适合需要高准确率的场景,如音频转写和文档生成。
文本转语音 (Text-to-Speech, TTS) 将文本内容转换为自然语音输出,广泛应用于语音助手和导航系统。
说话人分离 (Speaker Diarization) 识别和区分音频流中的不同说话人,常用于会议记录和多说话人对话分析。
说话人识别 (Speaker Identification) 确认说话者的身份,分析声纹特征并与数据库进行比对。
说话人验证 (Speaker Verification) 要求说话者提供声纹以确认身份,常用于安全性较高的场合,如银行系统。
口语语言识别 (Spoken Language Identification) 识别语音中使用的语言,帮助系统在多语言环境中自动切换语言。
音频标记 (Audio Tagging) 为音频内容添加标签,便于分类和搜索,常用于音频库管理和内容推荐。
语音活动检测 (Voice Activity Detection, VAD) 检测音频流中是否存在语音活动,提升语音识别准确性并节省带宽和处理资源。
关键词检测 (Keyword Spotting) 识别特定关键词或短语,常用于智能助手和语音控制设备,允许用户通过语音命令与设备交互。

官方参考文档:
https://k2-fsa.github.io/sherpa/onnx/index.html

技术细节

步骤一、在Visual Studio中导入nuget包

找到项目 引用 右击引用 管理NuGet程序包
安装 org.k2fsa.sherpa.onnx.runtime.osx-x64
下载 org.k2fsa.sherpa.onnx.runtime.osx-x64
安装 Microsoft.ML.OnnxRuntime
安装 Microsoft.ML.OnnxRuntime**

步骤二、下载官方模型文件与SDK引入到项目

语音识别模型下载与引用
本文使用的是sherpa-onnx-streaming-zipformer-ctc-multi-zh-hans-2023-12-13模型,当然官方还提供了不少其他模型,各位小伙伴根据自己的需求下载
官方模型列表地址:官方模型
本文模型下载地址:sherpa-onnx-streaming-zipformer-ctc-multi-zh-hans-2023-12-13
在这里插入图片描述
模型解压后得到如下目录结构
在这里插入图片描述
复制此目录到项目目录Assets目录下:如果没有请Assets 在此目录下创建Assets文件夹
因为下来文件名sherpa-onnx-streaming-zipformer-ctc-multi-zh-hans-2023-12-13太长了所以我把名字修改成了sherpaonnxSpeechToText
在这里插入图片描述在这里插入图片描述
并修改所有文件属性为AndroidAsset
在这里插入图片描述

语音合成模型下载与引用
本文使用的是sherpa-onnx-vits-zh-ll模型,当然官方还提供了不少其他模型,各位小伙伴根据自己的需求下载
官方模型列表地址:官方模型
本文模型下载地址:sherpa-onnx-vits-zh-l
在这里插入图片描述
解压后文件目录
在这里插入图片描述
和上面一样复制此目录到Accets目录下 并修改所有目录文件属性为AndroidAsset
此项目中sherpa-onnx-vits-zh-ll改名成sherpaonnxvitszhll
在这里插入图片描述
引用so文件
官方模型列表地址:官方模型
这里选择的是v1.10.20java8的版本
在这里插入图片描述
解压后目录
在这里插入图片描述
在这里插入图片描述
在项目中创建libs目录 按照解压后的文件把so分别放到项目对应的目录中
在这里插入图片描述
设置所有so文件属性AndroidNativeLibrary
在这里插入图片描述
导入jar包
和模型一起下载的还有一个jar包 我们需要把此jar包引用到项目中,犹豫vs项目不支持直接引用jar包,所以我们要创建一个jar项目,再通过引用jar项目来引用jar包
在这里插入图片描述
引用此jar包项目
在这里插入图片描述
上面准备工作做好了现在我们就可以写代码运行了
这里是我的代码结构
在这里插入图片描述
ArsTools功能是把Assets资源中的模型导入到app内部目录(直接访问外部目录可能导致系统找不到模型文件)
ArsTools代码

using Android.App;
using Android.Content;
using System;
using System.IO;
using Android.Content.Res;
using Android.Content;
using Android.Media;
using Xamarin.Essentials;
using static Android.Provider.MediaStore;

namespace Your.Namespace
{

    public class ArsTools
    {
        private static readonly string TAG = typeof(Tools).FullName;

        // 设置 context
        private static Context _context;

        public static void SetContext(Context context)
        {
            _context = context;
        }

        // 递归复制文件
        public static void CopyAsset(string assetPath, string root)
        {
            AssetManager assetManager = _context.Assets;
            string[] files = null;

            try
            {
                // 获取指定目录下的所有文件和目录
                files = assetManager.List(assetPath);
            }
            catch (IOException e)
            {
                Console.WriteLine(e.Message);
            }

            if (files != null)
            {
                foreach (var filename in files)
                {
                    string assetFilePath = Path.Combine(assetPath, filename); // 资源文件的完整路径
                    string destFilePath = Path.Combine(root, assetPath) + "/" + filename; // 目标文件的完整路径

                    try
                    {
                        if (!File.Exists(Path.Combine(root, assetPath)))
                        {
                            // 如果是目录,则创建目录并递归复制
                            Directory.CreateDirectory(Path.Combine(root, assetPath));
                        }
                        // 判断是否为目录
                        if (assetManager.List(assetFilePath)?.Length > 0)
                        {
                            if (!File.Exists(destFilePath))
                            {
                                // 如果是目录,则创建目录并递归复制
                                Directory.CreateDirectory(Path.GetDirectoryName(destFilePath));
                            }
                            CopyAsset(assetFilePath, root);
                        }
                        else
                        {
                            if (!File.Exists(destFilePath))
                            {
                                using (System.IO.Stream input = assetManager.Open(assetFilePath))
                                using (System.IO.Stream output = File.Create(destFilePath))
                                {
                                    // 复制文件
                                    input.CopyTo(output);
                                }
                            }
                        }

                        Console.WriteLine($"文件复制:{destFilePath},成功:{File.Exists(destFilePath)}");
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine(e.Message);
                    }
                }
            }
        }
        //}



        // 播放 wav 文件
        //public static async void PlayWav(string wavPath)
        //{
        //    try
        //    {
        //        await Audio.PlayAsync(wavPath);
        //    }
        //    catch (Exception e)
        //    {
        //        Console.WriteLine(e.Message);
        //    }
        //}

        // 获取 app 存储路径
        public static string GetAppStoragePath()
        {
            return FileSystem.AppDataDirectory;
        }
    }

}

业务代码
ArsSpeechHelper

using AGVS.HIMS.Androids.Business.util;
using AGVS.HIMS.Forms.AppServer;
using Android.Content;
using Com.K2fsa.Sherpa.Onnx;
using NPOI.POIFS.Properties;
using System;
using System.Diagnostics;
using System.IO;

namespace Your.Namespace
{
    public class ArsSpeechHelper
    {
        Context ArsContext;
        public ArsSpeechHelper(Context context)
        {
            ArsContext = context;
        }

        public void SpeechToText()
        {
            try
            {
                //https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-ctc-multi-zh-hans-2023-12-13.tar.bz2
                //数据库名称
                var sqliteFilename = "sherpaonnxSpeechToText";

                ArsTools.SetContext(ArsContext);
                ArsTools.CopyAsset(sqliteFilename, Tools.GetAppStoragePath());
                string model = ArsTools.GetAppStoragePath() + "/sherpaonnxSpeechToText/ctc-epoch-20-avg-1-chunk-16-left-128.onnx";
                string tokens = ArsTools.GetAppStoragePath() + "/sherpaonnxSpeechToText/tokens.txt";
                string waveFilename = ArsTools.GetAppStoragePath() + "/sherpaonnxSpeechToText/test_wavs/DEV_T0000000000.wav";
                
                WaveReader reader = new WaveReader(waveFilename);
                OnlineZipformer2CtcModelConfig ctc = OnlineZipformer2CtcModelConfig.InvokeBuilder()
                    .SetModel(model).Build();
                OnlineModelConfig modelConfig = OnlineModelConfig.InvokeBuilder()
                                .SetZipformer2Ctc(ctc)
                                .SetTokens(tokens)
                                .SetNumThreads(1)
                                .SetDebug(true)
                                .Build();
                OnlineRecognizerConfig config = OnlineRecognizerConfig.InvokeBuilder()
                                .SetOnlineModelConfig(modelConfig)
                                .SetDecodingMethod("greedy_search")
                                .Build();
                OnlineRecognizer recognizer = new OnlineRecognizer(config);
                OnlineStream stream = recognizer.CreateStream();
                stream.AcceptWaveform(reader.GetSamples(), reader.SampleRate);
                float[] tailPaddings = new float[(int)(0.3 * reader.SampleRate)];
                stream.AcceptWaveform(tailPaddings, reader.SampleRate);
                while (recognizer.IsReady(stream))
                {
                    recognizer.Decode(stream);
                }
                string text = recognizer.GetResult(stream).Text;
                //System.out.printf("filename:%s\nresult:%s\n", waveFilename, text);
                stream.Release();
                recognizer.Release();

            }
            catch (Exception ex) { }
        }

        public void TextToSpeech(string text = "你好!")
        {
            try
            {
                //测试
                text = "在语音输入的同时进行处理和识别,适用于需要即时反馈的场景,如会议和语音助手。在录制完毕后进行处理,适合需要高准确率的场景,如音频转写和文档生成。";
                // please visit
                //  https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-melo-tts-zh_en.tar.bz2
                // to download model files
                //读取数据库文件
                string documentsPath = (string)Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDownloads);
                //数据库名称
                var sqliteFilename = "sherpaonnxvitszhll";

                ArsTools.SetContext(ArsContext);
                ArsTools.CopyAsset(sqliteFilename, Tools.GetAppStoragePath());

                string model = ArsTools.GetAppStoragePath() + "/sherpaonnxvitszhll/model.onnx";

                string tokens = ArsTools.GetAppStoragePath() + "/sherpaonnxvitszhll/tokens.txt";
                string lexicon = ArsTools.GetAppStoragePath() + "/sherpaonnxvitszhll/lexicon.txt";
                string dictDir = ArsTools.GetAppStoragePath() + "/sherpaonnxvitszhll/dict";
                string ruleFsts =
                         ArsTools.GetAppStoragePath() + "/sherpaonnxvitszhll/phone.fst," +
                         ArsTools.GetAppStoragePath() + "/sherpaonnxvitszhll/date.fst," +
                         ArsTools.GetAppStoragePath() + "/sherpaonnxvitszhll/number.fst";

                OfflineTtsVitsModelConfig vitsModelConfig = OfflineTtsVitsModelConfig.InvokeBuilder()
                                .SetModel(model)
                                .SetTokens(tokens)
                                .SetLexicon(lexicon)
                                .SetDictDir(dictDir)
                                .Build();
                OfflineTtsModelConfig modelConfig = OfflineTtsModelConfig.InvokeBuilder()
                                .SetVits(vitsModelConfig)
                                .SetNumThreads(1)
                                .SetDebug(true)
                                .Build();
                OfflineTtsConfig config = OfflineTtsConfig.InvokeBuilder()
                    .SetModel(modelConfig)
                    .Build();
                OfflineTts tts = new OfflineTts(config);
                int sid = 100;
                float speed = 1.0f;
                // 使用 Stopwatch 记录处理时间
                Stopwatch stopwatch = new Stopwatch();
                stopwatch.Start();
                GeneratedAudio audio = tts.Generate(text, sid, speed);
                stopwatch.Stop();
                float timeElapsedSeconds = stopwatch.ElapsedMilliseconds / 1000.0f;
                float audioDuration = audio.GetSamples().Length / (float)audio.SampleRate;
                float real_time_factor = timeElapsedSeconds / audioDuration;
                string waveFilename = Path.Combine(documentsPath, "tts-vits-zh.wav");
                audio.Save(waveFilename);
                //System.out.printf("-- elapsed : %.3f seconds\n", timeElapsedSeconds);
                //System.out.printf("-- audio duration: %.3f seconds\n", timeElapsedSeconds);
                //System.out.printf("-- real-time factor (RTF): %.3f\n", real_time_factor);
                //System.out.printf("-- text: %s\n", text);
                //System.out.printf("-- Saved to %s\n", waveFilename);
                tts.Release();

            }
            catch (Exception ex)
            {

            }
        }
    }
}

这样我们就可以在MainActivity.cs中使用啦

 [Activity(Label = "测试", Icon = "@drawable/app2", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
 public class MainActivity : FormsAppCompatActivity
 {
  protected override async void OnCreate(Bundle savedInstanceState)
 {
     try
     {
         //设置主题
         this.SetTheme(Resource.Style.MainTheme);
         TabLayoutResource = Resource.Layout.Tabbar;
         ToolbarResource = Resource.Layout.Toolbar;
         //全屏界面
         var uiOpts = SystemUiFlags.LayoutStable
          | SystemUiFlags.LayoutHideNavigation
          | SystemUiFlags.LayoutFullscreen
          | SystemUiFlags.Fullscreen
          | SystemUiFlags.HideNavigation
          | SystemUiFlags.Immersive;
         Window.DecorView.SystemUiVisibility = (StatusBarVisibility)uiOpts;
         base.OnCreate(savedInstanceState);
         //调用接口
         ArsSpeechHelper arsSpeechHelper = new ArsSpeechHelper(BaseContext);
         arsSpeechHelper.TextToSpeech("");
         arsSpeechHelper.SpeechToText();
     }
  	 catch (Exception ex)
  	 {
  	 }
  	 finally
  	 {
      	 try
      	 {
          	 //初始化容器
          	 LoadApplication(new Bootstrapper(IoC.Get<SimpleContainer>()));
      	 }
      catch (System.Exception ex)
      {
      }
  }
 }

//启动程序 语音生成 与 语音合成 也都达到了我想要的结果(亲测有效)
当前还有一些不完美的地方
后期打算把这段ArsSpeechHelper逻辑放到Services中通过后台服务来执行。

GitHub示例代码地址

android 项目示例代码:
ars语音识别示例项目
tts语音合成示例项目

更多推荐