使用 Azure Speech Service 进行语音识别

Download Sample下载示例

作为基于云的 API,Azure 语音服务具备以下功能:

  • 语音转文本功能会将音频文件或流转录为文本。
  • 文本转语音功能会将输入文本转换为类似人工合成的语音。
  • 语音翻译功能为语音转文本和语音转语音启用实时多语言翻译。
  • 语音助理功能可以为应用程序创建类似人类的对话界面。

本文介绍如何使用 Azure 语音服务在示例 Xamarin.Forms 应用程序中实现语音转文本。 以下屏幕截图显示了 iOS 和 Android 上的示例应用程序:

Screenshots of the sample application on iOS and Android

创建 Azure 语音服务资源

Azure 语音服务是 Azure 认知服务的一部分,它为图像识别、语音识别和翻译以及 Bing 搜索等任务提供基于云的 API。 有关详细信息,请参阅“什么是 Azure 认知服务?”。

该示例项目需要在 Azure 门户中创建 Azure 认知服务资源。 可以为单个服务(例如语音服务)创建认知服务资源,也可以将其创建为多服务资源。 语音服务资源的创建步骤如下:

  1. 登录到 Azure 门户
  2. 创建多服务或单服务资源。
  3. 获取资源的 API 密钥和区域信息。
  4. 更新示例 Constants.cs 文件。

有关创建资源的分步指南,请参阅“创建认知服务资源”。

注意

如果还没有 Azure 订阅,可以在开始前创建一个免费帐户。 拥有帐户后,可以在免费层创建单一服务资源来试用该服务。

使用语音服务配置应用

创建认知服务资源后,可以使用 Azure 资源中的区域和 API 密钥更新 Constants.cs 文件:

public static class Constants
{
    public static string CognitiveServicesApiKey = "YOUR_KEY_GOES_HERE";
    public static string CognitiveServicesRegion = "westus";
}

安装 NuGet 语音服务包

示例应用程序使用 Microsoft.CognitiveServices.Speech NuGet 包连接到 Azure 语音服务。 在共享项目和每个平台项目中安装此 NuGet 包。

创建 IMicrophoneService 接口

每个平台都需要访问麦克风的权限。 示例项目在共享项目中提供了一个 IMicrophoneService 接口,并使用 Xamarin.FormsDependencyService 获取该接口的平台实现。

public interface IMicrophoneService
{
    Task<bool> GetPermissionAsync();
    void OnRequestPermissionResult(bool isGranted);
}

创建页面布局

示例项目在 MainPage.xaml 文件中定义基本页面布局。 关键布局元素是 Button(用于启动听录过程)、Label(包含听录文本)以及 ActivityIndicator(用于显示听录正在进行中):

<ContentPage ...>
    <StackLayout>
        <Frame ...>
            <ScrollView x:Name="scroll"
                        ...>
                <Label x:Name="transcribedText"
                       ... />
            </ScrollView>
        </Frame>

        <ActivityIndicator x:Name="transcribingIndicator"
                           IsRunning="False" />
        <Button x:Name="transcribeButton"
                ...
                Clicked="TranscribeClicked"/>
    </StackLayout>
</ContentPage>

实现语音服务

MainPage.xaml.cs 代码隐藏文件包含从 Azure 语音服务发送音频和接收听录文本的所有逻辑。

MainPage 构造函数从 DependencyService 获取 IMicrophoneService 接口的实例:

public partial class MainPage : ContentPage
{
    SpeechRecognizer recognizer;
    IMicrophoneService micService;
    bool isTranscribing = false;

    public MainPage()
    {
        InitializeComponent();

        micService = DependencyService.Resolve<IMicrophoneService>();
    }

    // ...
}

点击 transcribeButton 实例时,系统将调用 TranscribeClicked 方法:

async void TranscribeClicked(object sender, EventArgs e)
{
    bool isMicEnabled = await micService.GetPermissionAsync();

    // EARLY OUT: make sure mic is accessible
    if (!isMicEnabled)
    {
        UpdateTranscription("Please grant access to the microphone!");
        return;
    }

    // initialize speech recognizer 
    if (recognizer == null)
    {
        var config = SpeechConfig.FromSubscription(Constants.CognitiveServicesApiKey, Constants.CognitiveServicesRegion);
        recognizer = new SpeechRecognizer(config);
        recognizer.Recognized += (obj, args) =>
        {
            UpdateTranscription(args.Result.Text);
        };
    }

    // if already transcribing, stop speech recognizer
    if (isTranscribing)
    {
        try
        {
            await recognizer.StopContinuousRecognitionAsync();
        }
        catch(Exception ex)
        {
            UpdateTranscription(ex.Message);
        }
        isTranscribing = false;
    }

    // if not transcribing, start speech recognizer
    else
    {
        Device.BeginInvokeOnMainThread(() =>
        {
            InsertDateTimeRecord();
        });
        try
        {
            await recognizer.StartContinuousRecognitionAsync();
        }
        catch(Exception ex)
        {
            UpdateTranscription(ex.Message);
        }
        isTranscribing = true;
    }
    UpdateDisplayState();
}

TranscribeClicked 方法执行以下操作:

  1. 检查应用程序是否可以访问麦克风,如果不能访问则提前退出。
  2. 创建 SpeechRecognizer 类的实例(前提是此实例尚不存在)。
  3. 如果连续听录正在进行中,则停止。
  4. 插入时间戳并启动连续听录(如果尚未进行)。
  5. 通知应用程序根据新的应用程序状态更新其外观。

MainPage 类方法的其余部分是用于显示应用程序状态的帮助程序:

void UpdateTranscription(string newText)
{
    Device.BeginInvokeOnMainThread(() =>
    {
        if (!string.IsNullOrWhiteSpace(newText))
        {
            transcribedText.Text += $"{newText}\n";
        }
    });
}

void InsertDateTimeRecord()
{
    var msg = $"=================\n{DateTime.Now.ToString()}\n=================";
    UpdateTranscription(msg);
}

void UpdateDisplayState()
{
    Device.BeginInvokeOnMainThread(() =>
    {
        if (isTranscribing)
        {
            transcribeButton.Text = "Stop";
            transcribeButton.BackgroundColor = Color.Red;
            transcribingIndicator.IsRunning = true;
        }
        else
        {
            transcribeButton.Text = "Transcribe";
            transcribeButton.BackgroundColor = Color.Green;
            transcribingIndicator.IsRunning = false;
        }
    });
}

UpdateTranscription 方法将提供的 newTextstring 写入名为 transcribedTextLabel 元素。 该方法强制在 UI 线程上执行此更新,以便可以从任何上下文进行调用,而不会造成异常。 InsertDateTimeRecord 将当前日期和时间写入 transcribedText 实例以标记新听录的开始。 最后,UpdateDisplayState 方法更新 ButtonActivityIndicator 元素以反映听录是否正在进行。

创建平台麦克风服务

应用程序必须具有麦克风访问权限才能收集语音数据。 IMicrophoneService 接口必须在每个平台上实现并注册到 DependencyService 中,应用程序才能正常运行。

Android

该示例项目为 Android 定义了一个名为 AndroidMicrophoneServiceIMicrophoneService 实现:

[assembly: Dependency(typeof(AndroidMicrophoneService))]
namespace CognitiveSpeechService.Droid.Services
{
    public class AndroidMicrophoneService : IMicrophoneService
    {
        public const int RecordAudioPermissionCode = 1;
        private TaskCompletionSource<bool> tcsPermissions;
        string[] permissions = new string[] { Manifest.Permission.RecordAudio };

        public Task<bool> GetPermissionAsync()
        {
            tcsPermissions = new TaskCompletionSource<bool>();

            if ((int)Build.VERSION.SdkInt < 23)
            {
                tcsPermissions.TrySetResult(true);
            }
            else
            {
                var currentActivity = MainActivity.Instance;
                if (ActivityCompat.CheckSelfPermission(currentActivity, Manifest.Permission.RecordAudio) != (int)Permission.Granted)
                {
                    RequestMicPermissions();
                }
                else
                {
                    tcsPermissions.TrySetResult(true);
                }

            }

            return tcsPermissions.Task;
        }

        public void OnRequestPermissionResult(bool isGranted)
        {
            tcsPermissions.TrySetResult(isGranted);
        }

        void RequestMicPermissions()
        {
            if (ActivityCompat.ShouldShowRequestPermissionRationale(MainActivity.Instance, Manifest.Permission.RecordAudio))
            {
                Snackbar.Make(MainActivity.Instance.FindViewById(Android.Resource.Id.Content),
                        "Microphone permissions are required for speech transcription!",
                        Snackbar.LengthIndefinite)
                        .SetAction("Ok", v =>
                        {
                            ((Activity)MainActivity.Instance).RequestPermissions(permissions, RecordAudioPermissionCode);
                        })
                        .Show();
            }
            else
            {
                ActivityCompat.RequestPermissions((Activity)MainActivity.Instance, permissions, RecordAudioPermissionCode);
            }
        }
    }
}

AndroidMicrophoneService 具有以下功能:

  1. Dependency 属性用于向 DependencyService 注册该类。
  2. GetPermissionAsync 方法根据 Android SDK 版本检查是否需要权限,如果尚未授予权限,则调用 RequestMicPermissions
  3. RequestMicPermissions 方法会在需要理由时使用 Snackbar 类向用户请求权限,否则会直接请求录音权限。
  4. 用户响应权限请求后,系统就会调用 OnRequestPermissionResult 方法并返回 bool 结果。

已将 MainActivity 类定制为在权限请求完成时更新 AndroidMicrophoneService 实例:

public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
    IMicrophoneService micService;
    internal static MainActivity Instance { get; private set; }
    
    protected override void OnCreate(Bundle savedInstanceState)
    {
        Instance = this;
        // ...
        micService = DependencyService.Resolve<IMicrophoneService>();
    }
    public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
    {
        // ...
        switch(requestCode)
        {
            case AndroidMicrophoneService.RecordAudioPermissionCode:
                if (grantResults[0] == Permission.Granted)
                {
                    micService.OnRequestPermissionResult(true);
                }
                else
                {
                    micService.OnRequestPermissionResult(false);
                }
                break;
        }
    }
}

MainActivity 类定义了一个名为 Instance 的静态引用,AndroidMicrophoneService 对象在请求权限时需要该引用。 当用户批准或拒绝权限请求时,它会重写 OnRequestPermissionsResult 方法来更新 AndroidMicrophoneService 对象。

最后,Android 应用程序必须包含在 AndroidManifest.xml 文件中录制音频权限:

<manifest ...>
    ...
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
</manifest>

iOS

该示例项目为 iOS 定义了一个名为 iOSMicrophoneServiceIMicrophoneService 实现:

[assembly: Dependency(typeof(iOSMicrophoneService))]
namespace CognitiveSpeechService.iOS.Services
{
    public class iOSMicrophoneService : IMicrophoneService
    {
        TaskCompletionSource<bool> tcsPermissions;

        public Task<bool> GetPermissionAsync()
        {
            tcsPermissions = new TaskCompletionSource<bool>();
            RequestMicPermission();
            return tcsPermissions.Task;
        }

        public void OnRequestPermissionResult(bool isGranted)
        {
            tcsPermissions.TrySetResult(isGranted);
        }

        void RequestMicPermission()
        {
            var session = AVAudioSession.SharedInstance();
            session.RequestRecordPermission((granted) =>
            {
                tcsPermissions.TrySetResult(granted);
            });
        }
    }
}

iOSMicrophoneService 具有以下功能:

  1. Dependency 属性用于向 DependencyService 注册该类。
  2. GetPermissionAsync 方法调用 RequestMicPermissions 来请求设备用户的权限。
  3. RequestMicPermissions 方法使用共享的 AVAudioSession 实例来请求录音权限。
  4. OnRequestPermissionResult 方法使用提供的 bool 值更新 TaskCompletionSource 实例。

最后,iOS 应用程序的 Info.plist 必须包含一条消息,告诉用户应用程序请求访问麦克风的原因。 编辑 Info.plist 文件以在 <dict> 元素中加入以下标签:

<plist>
    <dict>
        ...
        <key>NSMicrophoneUsageDescription</key>
        <string>Voice transcription requires microphone access</string>
    </dict>
</plist>

UWP

示例项目为 UWP 定义了一个名为 UWPMicrophoneServiceIMicrophoneService 实现:

[assembly: Dependency(typeof(UWPMicrophoneService))]
namespace CognitiveSpeechService.UWP.Services
{
    public class UWPMicrophoneService : IMicrophoneService
    {
        public async Task<bool> GetPermissionAsync()
        {
            bool isMicAvailable = true;
            try
            {
                var mediaCapture = new MediaCapture();
                var settings = new MediaCaptureInitializationSettings();
                settings.StreamingCaptureMode = StreamingCaptureMode.Audio;
                await mediaCapture.InitializeAsync(settings);
            }
            catch(Exception ex)
            {
                isMicAvailable = false;
            }

            if(!isMicAvailable)
            {
                await Windows.System.Launcher.LaunchUriAsync(new Uri("ms-settings:privacy-microphone"));
            }

            return isMicAvailable;
        }

        public void OnRequestPermissionResult(bool isGranted)
        {
            // intentionally does nothing
        }
    }
}

UWPMicrophoneService 具有以下功能:

  1. Dependency 属性用于向 DependencyService 注册该类。
  2. GetPermissionAsync 方法会尝试初始化 MediaCapture 实例。 如果尝试失败,它将启动用户请求以启用麦克风。
  3. OnRequestPermissionResult 方法的存在是为了满足该接口,但 UWP 实现不需要该方法。

最后,UWP 的 Package.appxmanifest 必须表明应用程序会使用麦克风。 双击 Package.appxmanifest 文件,然后在 Visual Studio 2019 的“功能”选项卡上选择“麦克风”选项:

Screenshot of the manifest in Visual Studio 2019

测试应用程序

运行应用并单击“转录”按钮。 应用应请求麦克风访问权限并启动听录过程。 ActivityIndicator 将呈现动画,显示听录处于活动状态。 当你说话时,应用程序会将音频数据流式传输到 Azure 语音服务资源,该资源将使用转录文本作出响应。 系统会在收到转录文本时,在 Label 元素中显示该转录文本。

注意

Android 模拟器无法加载和初始化语音服务库。 对于 Android 平台,建议在物理设备上进行测试。