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





5.00/5 (1投票)
如何构建初始后端 API 并将其作为 Azure 函数应用发布
本系列三篇文章的第一部分构建了一个通用翻译器的前端 Web 应用。它提供了一个向导,可以在浏览器中录制语音、转录语音、翻译语音,然后将结果文本转换回音频。
但是,前端 Web 应用不包含处理音频或文本所需的逻辑。由 Java 编写的 Azure Function App 后端 API 负责处理此逻辑。
本教程将构建 API 的第一部分,公开 /transcribe
端点,用于将音频文件转换为文本。
必备组件
本教程需要Azure Functions 运行时版本 4和GStreamer来转换 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_KEY
和 SPEECH_REGION
值。将 yourresourcegroup
、yourappname
、yourspeechkey
和 yourspeechregion
替换为语音服务实例的相应值。
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。