65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Azure Cognitive Services 构建通用翻译器,第二部分:制作语音转文本 Java 应用

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2022 年 3 月 11 日

CPOL

7分钟阅读

viewsIcon

5172

如何构建初始后端 API 并将其作为 Azure 函数应用发布

本系列三篇文章的第一部分构建了一个通用翻译器的前端 Web 应用。它提供了一个向导,可以在浏览器中录制语音、转录语音、翻译语音,然后将结果文本转换回音频。

但是,前端 Web 应用不包含处理音频或文本所需的逻辑。由 Java 编写的 Azure Function App 后端 API 负责处理此逻辑。

本教程将构建 API 的第一部分,公开 /transcribe 端点,用于将音频文件转换为文本。

必备组件

本教程需要Azure Functions 运行时版本 4GStreamer来转换 WebM 音频文件,以便 Azure API 进行处理。此外,我们的后端应用程序需要安装Java 11 JDK

此应用程序的完整源代码可在GitHub上找到,后端应用程序可作为 Docker 镜像:mcasperson/translator

创建语音服务

Azure 语音服务提供语音转文本的功能。后端 API 将作为前端 Web 应用和 Azure 语音服务之间的代理。

Microsoft 的文档提供了有关在 Azure 中创建语音服务的说明。

创建服务后,记下密钥。与应用程序进行交互时需要此密钥。

引导后端应用程序

Microsoft 文档提供了有关为本教程创建示例项目的说明。

要创建示例应用程序,请运行以下命令。请注意,它使用的是 Java 11 而不是 Microsoft 文档指定的 Java 8。

mvn archetype:generate -DarchetypeGroupId=com.microsoft.azure 
    -DarchetypeArtifactId=azure-functions-archetype -DjavaVersion=11 –Ddocker

添加 Maven 依赖项

我们需要为应用程序的 pom.xml 文件添加几个额外的依赖项。这些依赖项添加了语音服务 SDK、HTTP 客户端以及一些用于处理文件和文本的通用实用程序。

<dependency>
<groupId>com.microsoft.cognitiveservices.speech</groupId>
<artifactId>client-sdk</artifactId>
<version>1.19.0</version>
</dependency>

<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.3</version>
</dependency>

<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
       <artifactId>commons-text</artifactId>
<version>1.9</version>
</dependency>

处理压缩音频文件

转录音频文件的第一个挑战是 Azure API 原生只支持 WAV 文件。然而,浏览器录制的音频文件几乎肯定会是 WebM 等压缩格式。

幸运的是,Azure SDK 允许使用 GStreamer 库转换压缩音频文件。这就是为什么 GStreamer 是我们后端应用程序的先决条件之一。

要使用压缩音频文件,我们扩展 PullAudioInputStreamCallback 类,提供一个读取器,该读取器无需任何额外处理即可消耗压缩音频文件的字节数组。我们使用 ByteArrayReader 类来完成此操作。

package com.matthewcasperson.azuretranslate.readers;

import com.microsoft.cognitiveservices.speech.audio.PullAudioInputStreamCallback;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

public class ByteArrayReader extends PullAudioInputStreamCallback {

  private InputStream inputStream;

  public ByteArrayReader(final byte[] data) {
    inputStream = new ByteArrayInputStream(data);
  }

  @Override
  public int read(final byte[] bytes) {
    try {
      return inputStream.read(bytes, 0, bytes.length);
    } catch (final IOException e) {
      e.printStackTrace();
    }
    return 0;
  }

  @Override
  public void close() {
    try {
      inputStream.close();
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}

转录音频文件

TranscribeService 类包含与 Azure 语音服务交互的逻辑。

package com.matthewcasperson.azuretranslate.services;

import com.matthewcasperson.azuretranslate.readers.ByteArrayReader;
import com.microsoft.cognitiveservices.speech.SpeechRecognitionResult;
import com.microsoft.cognitiveservices.speech.SpeechRecognizer;
import com.microsoft.cognitiveservices.speech.audio.AudioConfig;
import com.microsoft.cognitiveservices.speech.audio.AudioStreamContainerFormat;
import com.microsoft.cognitiveservices.speech.audio.AudioStreamFormat;
import com.microsoft.cognitiveservices.speech.audio.PullAudioInputStream;
import com.microsoft.cognitiveservices.speech.translation.SpeechTranslationConfig;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

public class TranscribeService {

此类包含一个名为 transcribe 的方法,该方法接收上传的音频文件(作为字节数组)和音频的语言(作为 string)。

  public String transcribe(final byte[] file, final String language)
      throws IOException, ExecutionException, InterruptedException {

代码使用源自环境变量的语音服务密钥和区域创建 SpeechTranslationConfig 对象。

    try {
      try (SpeechTranslationConfig config = SpeechTranslationConfig.fromSubscription(
          System.getenv("SPEECH_KEY"),
          System.getenv("SPEECH_REGION"))) {

然后,代码定义音频文件中包含的语言。

        config.setSpeechRecognitionLanguage(language);

PullAudioInputStream 表示要处理的音频流。我们将前面创建的读取器传递给它,该读取器作为上传的包含音频文件的字节数组的简单包装器。代码将压缩格式设置为 ANY,允许 GStreamer 库确定浏览器上传的是何种音频文件格式,并将其转换为正确的格式。

        final PullAudioInputStream pullAudio = PullAudioInputStream.create(
            new ByteArrayReader(file),
            AudioStreamFormat.getCompressedFormat(AudioStreamContainerFormat.ANY));

AudioConfig 表示音频输入配置。在这种情况下,它是从流中读取音频。

final AudioConfig audioConfig = AudioConfig.fromStreamInput(pullAudio);

SpeechRecognizer 类提供了对语音识别服务的访问。

final SpeechRecognizer reco = new SpeechRecognizer(config, audioConfig);

然后,应用程序请求将音频文件转换为文本。返回的文本如下所示:

        final Future<SpeechRecognitionResult> task = reco.recognizeOnceAsync();
        final SpeechRecognitionResult result = task.get();
        return result.getText();
      }

代码然后记录并重新抛出异常。

    } catch (final Exception ex) {
      System.out.println(ex);
      throw ex;
    }
  }
}

公开 Azure Function HTTP 触发器

与 Azure Function HTTP 触发器关联的方法将 TranscribeService 类公开为 HTTP 端点。

Function 类包含项目的所有触发器。

package com.matthewcasperson.azuretranslate;
import com.matthewcasperson.azuretranslate.services.TranscribeService;
import com.microsoft.azure.functions.ExecutionContext;
import com.microsoft.azure.functions.HttpMethod;
import com.microsoft.azure.functions.HttpRequestMessage;
import com.microsoft.azure.functions.HttpResponseMessage;
import com.microsoft.azure.functions.HttpStatus;
import com.microsoft.azure.functions.annotation.AuthorizationLevel;
import com.microsoft.azure.functions.annotation.FunctionName;
import com.microsoft.azure.functions.annotation.HttpTrigger;
import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
/**
* Azure Functions with HTTP Trigger.
*/
public class Function {

接下来,我们定义 TranscribeService 类的静态实例。

private static final TranscribeService TRANSCRIBE_SERVICE = new TranscribeService();

代码使用 @FunctionName 注释 transcribe 方法,以在 URL /api/transcribe 上公开该函数,响应 HTTP POST 请求,并将字节数组作为方法正文。

请注意,当应用程序调用此端点时,必须将 Content-Type 标头设置为 application/octet-stream,以确保 Azure Functions 平台正确地将请求正文序列化为字节数组。

    /**
     * Must use Content-Type: application/octet-stream
     * https://github.com.cnpmjs.org/microsoft/azure-maven-plugins/issues/1351
     */
    @FunctionName("transcribe")
    public HttpResponseMessage transcribe(
            @HttpTrigger(
                name = "req",
                methods = {HttpMethod.POST},
                authLevel = AuthorizationLevel.ANONYMOUS)
                HttpRequestMessage<Optional<byte[]>> request,
            final ExecutionContext context) {

该函数期望请求正文包含音频文件,因此我们需要验证输入以确保用户提供了数据。

        if (request.getBody().isEmpty()) {
            return request.createResponseBuilder(HttpStatus.BAD_REQUEST)
                .body("The audio file must be in the body of the post.")
                .build();
        }

应用程序将音频文件和语言传递给转录服务。结果文本返回给调用者。

        try {
            final String text = TRANSCRIBE_SERVICE.transcribe(
                request.getBody().get(),
                request.getQueryParameters().get("language"));
 
            return request.createResponseBuilder(HttpStatus.OK).body(text).build();

任何异常都将导致调用者收到 500 响应代码。

        } catch (final IOException | ExecutionException | InterruptedException ex) {
            return request.createResponseBuilder(HttpStatus.INTERNAL_SERVER_ERROR)
                .body("There was an error transcribing the audio file.")
                .build();
        }
    }
 }

在本地测试 API

为了启用跨域资源共享(CORS),这允许 Web 应用程序在不同主机或端口上联系 API,我们将以下 JSON 复制到 local.settings.json 文件中。

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "java"
  },
  "Host": {
    "CORS": "*"
  }
}

语音服务密钥和区域必须作为环境变量公开。因此,我们在 PowerShell 中运行以下命令:

$env:SPEECH_KEY="your key goes here"
$env:SPEECH_REGION="your region goes here"

或者,我们也可以在 Bash 中使用等效命令:

export SPEECH_KEY="your key goes here"
export SPEECH_REGION="your region goes here"

现在,要构建后端,请运行以下命令:

mvn clean package

然后,我们运行此命令在本地运行函数:

mvn azure-functions:run

然后,使用以下命令启动上一篇文章中介绍的前端 Web 应用程序:

npm start

要测试该应用程序,请打开 https://:3000 并录制一些语音。然后,单击“Translate >”按钮进入向导的下一步。选择音频的语言,然后单击“Transcribe”按钮。

在后台,Web 应用程序调用后端 API,后端 API 调用 Azure 语音服务来转录音频文件的文本。然后,应用程序在文本框中打印返回的结果。

部署后端应用

从先决条件部分注意到,该应用程序需要 GStreamer。此工具使应用程序能够将浏览器保存的压缩音频转换为 Azure 语音服务可接受的格式。

将应用程序与外部依赖项(如 GStreamer)捆绑的最简单方法是将它们打包在 Docker 镜像中。

创建示例应用程序时,本教程还创建了一个示例 Dockerfile。因此,向 Dockerfile 添加一个新的 RUN 命令来安装 GStreamer 库。完整的 Dockerfile 如下所示:

ARG JAVA_VERSION=11
# This image additionally contains function core tools – useful when using custom extensions
#FROM mcr.microsoft.com/azure-functions/java:3.0-java$JAVA_VERSION-core-tools AS installer-env
FROM mcr.microsoft.com/azure-functions/java:3.0-java$JAVA_VERSION-build AS installer-env
 
COPY . /src/java-function-app
RUN cd /src/java-function-app && \
    mkdir -p /home/site/wwwroot && \
    mvn clean package && \
    cd ./target/azure-functions/ && \
    cd $(ls -d */|head -n 1) && \
    cp -a . /home/site/wwwroot
 
# This image is ssh enabled
FROM mcr.microsoft.com/azure-functions/java:3.0-java$JAVA_VERSION-appservice
# This image isn't ssh enabled
#FROM mcr.microsoft.com/azure-functions/java:3.0-java$JAVA_VERSION
 
RUN apt update && \
    apt install -y libgstreamer1.0-0 \
    gstreamer1.0-plugins-base \
    gstreamer1.0-plugins-good \
    gstreamer1.0-plugins-bad \
    gstreamer1.0-plugins-ugly && \
    rm -rf /var/lib/apt/lists/*
 
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
    AzureFunctionsJobHost__Logging__Console__IsEnabled=true
 
COPY --from=installer-env ["/home/site/wwwroot", "/home/site/wwwroot"]

我们使用以下命令构建此 Docker 镜像,将 dockerhubuser 替换为Docker Hub用户名。

docker build . -t dockerhubuser/translator

然后,我们使用以下命令将生成的镜像推送到 Docker Hub:

docker push dockerhubuser/translator

Microsoft 文档提供了有关将自定义 Linux Docker 镜像作为 Azure Function 部署的说明。

除了 Microsoft 文档中列出的应用设置外,还要设置 SPEECH_KEYSPEECH_REGION 值。将 yourresourcegroupyourappnameyourspeechkeyyourspeechregion 替换为语音服务实例的相应值。

az functionapp config appsettings set --name yourappname --resource-group yourresourcegroup 
--settings "SPEECH_KEY=yourspeechkey"

az functionapp config appsettings set --name yourappname --resource-group yourresourcegroup 
--settings "SPEECH_REGION=yourspeechregion"

部署后,Azure Function 将在其自己的域名上可用,例如 https://yourappname.azurewebsites.net。此 URL 是可接受的,但最好通过与静态 Web 应用相同的宿主名称公开该函数应用。此方法将允许静态 Web 应用使用相对 URL 与函数进行交互,而不是硬编码外部宿主名称。

使用静态 Web 应用的自带函数功能来实现此 URL 功能。

自带函数

首先,在 Azure 中打开静态网站资源,然后在左侧菜单中选择“Functions”链接。

静态 Web 应用不公开任何函数。通过将 Azure/static-web-apps-deploy@v1 GitHub Action 步骤中的 api_location 属性设置为空字符串来强制执行此操作。

相反,我们将一个外部函数链接到静态 Web 应用。单击“Link to a Function app”链接,选择 translator 函数,然后单击“Link”按钮。

外部函数随后链接到静态 Web 应用,并以相同的宿主名称公开。

在将函数链接到静态应用后,现在打开静态应用的公共 URL,录制音频文件并进行转录。Web 应用在相对 URL /app 上联系后端,现在由于“自带函数”功能而可用。

后续步骤

本教程在前一篇文章的基础上,公开了一个 Azure Function App,实现了使用 Azure 语音服务转录音频文件的功能。

由于依赖于外部库,函数应用被打包成 Docker 镜像。这种方法可以轻松地分发带有必需库的代码。

最后,使用“自带函数”功能将 Azure Function App 链接到静态 Web 应用,以确保 Web 应用和函数应用共享相同的宿主名称。

要完成通用翻译器,还有一些工作要做。该应用程序必须将转录的文本翻译成新语言,然后将翻译后的文本转换回语音。请继续阅读本系列的第三篇也是最后一篇文章来实现此逻辑。

要了解有关 Microsoft Cognitive Services Speech SDK 的更多信息并查看其示例,请参阅Microsoft Cognitive Services Speech SDK Samples

© . All rights reserved.