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

使用 Azure Cognitive Services 构建通用翻译器,第一部分:在浏览器中录制语音

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2022 年 3 月 10 日

CPOL

8分钟阅读

viewsIcon

4677

如何为通用翻译器构建前端 Web 应用程序

《星际迷航》的粉丝对通用翻译器的概念并不陌生。勇敢的英雄们使用这些虚构的设备与银河系中的各种物种进行交流,将一种口语无缝地翻译成另一种。无需掌握外星语学学位。

得益于人工智能 (AI) 的进步,开发人员现在可以构建通用翻译器。这些应用程序可以翻译数十种,甚至数百种(基于地球的)语言,帮助旅行者和商务联系人与世界几乎任何人进行交流。

开发人员可以使用许多有用的 Azure 服务将翻译功能构建到他们的应用程序中,例如 TranslatorSpeech to TextText to Speech

请跟随本系列三部分教程,构建一个简单的通用翻译器 Web 应用程序。该应用程序将使用 Azure 服务来转录用户基于浏览器的录音,将文本翻译成另一种语言,然后将该文本转换成语音。最终的工具展示了如何轻松地将全面的翻译服务集成到您的应用程序中。

我们将从本系列的第一部分开始,使用 React 编写 Web 前端,然后将代码托管为 Azure 静态 Web 应用程序。

必备组件

要跟进本教程,如果您还没有安装 Node.js,则需要 安装 Node.js,以便在本地构建和测试应用程序。

要随时查看此应用程序的完整源代码,请访问 GitHub

构建 React 模板应用程序

我们的 Web 应用程序基于 标准的 React 模板。通过运行以下命令创建此模板:

npx create-react-app voice-recording

该命令将在 voice-recording 目录中创建一个新的 Web 应用程序。使用以下命令进入目录:

cd voice-recording

我们的应用程序将使用 Material UI 库。使用以下命令将其添加到项目中:

npm install --save @mui/material @emotion/react @emotion/styled @mui/icons-material

我们的应用程序将把录制语音样本、转录、翻译、然后将文本转换为语音的步骤呈现为一个向导,用户可以通过点击进行操作。因此,请安装另一个依赖项以公开向导界面,命令如下:

npm install --save react-step-wizard

最后一个依赖项是 react-media-recorder,它提供了一种便捷的方式来通过 Web 浏览器与麦克风进行交互。使用此命令安装依赖项:

npm install –save react-media-recorder

现在框架已就位,可以创建应用程序了。下一步是构建向导界面。

扩展应用程序入口点

React 模板创建了一个 App() 函数,它充当应用程序的入口点。

由于多个步骤在用户逐步进行向导时会消耗收集到的数据,因此请在顶层 App() 函数中定义所有应用程序状态。下面的代码展示了 状态钩子,用于捕获表示向导将收集的各种输入的变量:

function App() {
 
    const [mediaBlob, setMediaBlob] = React.useState(null);
    const [transcribedText, setTranscribedText] = React.useState("");
    const [translatedText, setTranslatedText] = React.useState("");
    const [sourceLanguage, setSourceLanguage] = React.useState("en-US");
    const [targetLanguage, setTargetLanguage] = React.useState("de-DE");
    const [config, setConfig] = React.useState(null);

我们的 Web 应用程序必须与托管在不同位置的 API 进行通信。例如,本地开发将针对托管在 localhost:7071 上的 API,而生产构建将访问与 Web 应用程序具有相同主机名的 API。因此,我们加载一个 JSON 文件并将结果捕获在 config 状态变量中:

    useEffect(() => {
        fetch('config.json', {
            method: 'GET'
        })
            .then(response => response.json())
            .then(data => setConfig(data))
            .catch(() => window.alert("Failed to load settings"))
    }, []);

接下来,我们将开始构建 HTML 界面。一个 AppContext 元素包装了交互式元素。此元素将前面定义的所有状态变量暴露给每个子元素,而无需在每个子元素的属性中重复包含它们:

    return (
        <div className="App">
            <h1>Universal Translator</h1>
            <AppContext.Provider value={{
                mediaBlob,
                setMediaBlob,
                sourceLanguage,
                setSourceLanguage,
                targetLanguage,
                setTargetLanguage,
                transcribedText,
                setTranscribedText,
                translatedText,
                setTranslatedText,
                config
            }}>

如果 config 文件已加载,应用程序将显示 StepWizard 以及构成每个步骤的子元素,如下所示:

        {config != null && <StepWizard>
            <Record/>
            <Transcribe/>
            <Translate/>
            <Speak/>
        </StepWizard>}

如果 config 文件仍在加载中,应用程序将显示一个简单的加载消息:

                {config == null &&
                    <h2>Loading Configuration</h2>}
            </AppContext.Provider>
        </div>
    );
}

然后,代码导出 App 函数:

export default App;

通过浏览器录制语音

向导的第一步允许用户通过浏览器录制语音。Record 组件负责此步骤。

我们首先定义 Media Recorder API 使用的用于指示录音状态的常量值:

const readyToRecordStates = ["stopped", "idle"];
const recordingStates = ["recording"];
const recordedStates = ["stopped"]; 

Record 函数接收父 StepWizard 传递的标准参数:

export const Record = (params) => {

然后,应用程序通过 useContext 访问共享上下文:

const appContext = React.useContext(AppContext);

handleNext 函数将向导推进到下一步。它接受媒体录制器创建的 Blob URL,将 URL 保存在共享上下文中,并调用父 StepWizard 元素在 params 中传递的 nextStep 函数:

    function handleNext(mediaBlobUrl) {
        return () => {
            appContext.setMediaBlob(mediaBlobUrl);
            params.nextStep();
        }
    }

record 步骤创建了一个 ReactMediaRecorder 元素。render 属性接受一个函数,该函数构建录音捕获用户界面 (UI),并提供各种参数来控制录音过程和查询录音状态:

    return (
        <div>
            <ReactMediaRecorder
                audio={true}
                video={false}
                render={({status, startRecording, stopRecording, mediaBlobUrl}) => (

然后,我们使用 MUI 网格布局来显示开始和停止录音的按钮:

                    <Grid
                        container
                        spacing={2}
                        rowGap={2}
                        justifyContent="center"
                        alignItems="center">
                        <Grid item xs={12}>
                            <h3>Record your message</h3>
                        </Grid>
 
                        <Grid item md={3} xs={0}/>
                        <Grid item md={3} xs={12}>
                            <Button
                                variant="contained"
                                className={"fullWidth"}
                                onClick={startRecording}
                                disabled={readyToRecordStates.indexOf(status) === -1}>
                                Start Recording
                            </Button>
                        </Grid>
                        <Grid item md={3} xs={12}>
                            <Button
                                variant="contained"
                                className={"fullWidth"}
                                onClick={stopRecording}
                                disabled={recordingStates.indexOf(status) === -1}>
                                Stop Recording
                            </Button>
                        </Grid>
                        <Grid item md={3} xs={0}/>

然后,我们将添加一个音频播放元素。这允许用户重放他们的新录音:

                        <Grid item xs={12}>
                            <audio src={mediaBlobUrl} controls/>
                        </Grid>

然后,应用程序会显示一个按钮,用于将向导推进到下一步:

                        <Grid item md={3} xs={0}/>
                        <Grid item md={3} xs={12}>
                            <Button
                                variant="contained"
                                className={"fullWidth"}
                                onClick={handleNext(mediaBlobUrl)}
                                disabled={recordedStates.indexOf(status) === -1}>
                                Transcribe >
                            </Button>
                        </Grid>
                    </Grid>)}
            />
        </div>
    )
}

下图显示了结果。

转录录音

第二步是将录制的音频转录成文本。Transcribe 组件负责此操作。与之前一样,应用程序通过调用 useContext 来公开通用上下文:

export const Transcribe = (params) => {
const appContext = React.useContext(AppContext);

此组件在名为 processing 的变量中跟踪后端 API 调用的状态。在本教程中,我们将在稍后将其作为 Azure 函数创建:

const [processing, setProcessing] = useState(false);

下一步显示一个选择列表,其中包含录音中捕获的语言列表。分配给 handleOutputLanguageChange 的函数响应选择更改:

const handleOutputLanguageChange = (event) => {
appContext.setTranscribedText("");
appContext.setSourceLanguage(event.target.value);
};

分配给 transcribeText 的函数将音频录音传递给后端 API,并在响应中接收转录的文本。

后端 API 公开一个名为 /transcribe 的端点,该端点在 POST 请求正文中接受二进制音频数据,并在名为 language 的查询参数中定义语言:

    const transcribeText = async () => {
        setProcessing(true);
        appContext.setTranscribedText(null);
 
        const audioBlob = await fetch(appContext.mediaBlob).then(r => r.blob());
 
        fetch(appContext.config.translateService + "/transcribe?language=" + 
                                                    appContext.sourceLanguage, {
            method: 'POST',
            body: audioBlob,
            headers: {"Content-Type": "application/octet-stream"}
        })
            .then(response => response.text())
            .then(data => appContext.setTranscribedText(data))
            .catch(() => window.alert("Failed to transcribe message"))
            .finally(() => setProcessing(false))
    }

接下来,我们使用网格布局构建步骤 UI:

    return <div>
        <Grid
            container
            spacing={2}
            rowGap={2}
            justifyContent="center"
            alignItems="center">
            <Grid item xs={12}>
                <h3>Transcribe your message</h3>
            </Grid>
            <Grid item md={3} xs={0}/>
            <Grid item md={6} xs={12}>

此列表展示了 Azure 支持的语言 的一小部分样本。

                <FormControl fullWidth>
                    <InputLabel id="outputLanguage-label">Recorded Language</InputLabel>
                    <Select
                        labelId="outputLanguage-label"
                        value={appContext.sourceLanguage}
                        label="Recorded Language"
                        onChange={handleOutputLanguageChange}
                    >
                        <MenuItem value={"en-US"}>English</MenuItem>
                        <MenuItem value={"de-DE"}>German</MenuItem>
                        <MenuItem value={"ja-JP"}>Japanese</MenuItem>
                    </Select>
                </FormControl>
            </Grid>
            <Grid item md={3} xs={0}/>

转录按钮启动后端 API 调用:

            <Grid item md={4} xs={0}/>
            <Grid item md={4} xs={12}>
                <Button
                    variant="contained"
                    className={"fullWidth"}
                    onClick={transcribeText}
                    disabled={processing}>
                    Transcribe
                </Button>
            </Grid>
            <Grid item md={4} xs={0}/>

下方生成的文本将显示在文本框中:

            <Grid item md={3} xs={0}/>
            <Grid item md={6} xs={12}>
                <TextField
                    rows={10}
                    multiline={true}
                    fullWidth={true}
                    disabled={true}
                    value={appContext.transcribedText}
                />
            </Grid>
            <Grid item md={3} xs={0}/>

然后,我们想添加后退和前进按钮,以帮助用户导航向导。

            <Grid item md={3} xs={0}/>
            <Grid item md={3} xs={12}>
                <Button
                    variant="contained"
                    className={"fullWidth"}
                    onClick={() => {
                        appContext.setTranscribedText("");
                        params.previousStep();
                    }}
                    disabled={processing}>
                    < Record
                </Button>
            </Grid>
            <Grid item md={3} xs={12}>
                <Button
                    variant="contained"
                    className={"fullWidth"}
                    onClick={() => params.nextStep()}
                    disabled={!appContext.transcribedText || processing}>
                    Translate >
                </Button>
            </Grid>
            <Grid item md={3} xs={0}/>
        </Grid>
    </div>;
}

下图显示了转录步骤。

翻译消息

第三步是将消息翻译成新语言。Translate 组件定义了此步骤。与所有步骤一样,它通过 useContext 访问全局状态:

export const Translate = (params) => {
    const appContext = React.useContext(AppContext); 

我们使用 processing 变量来跟踪后端 API 调用的状态:

    const [processing, setProcessing] = useState(false);
 
    const handleOutputLanguageChange = (event) => {
        appContext.setTranslatedText("");
        appContext.setTargetLanguage(event.target.value);
    }; 

分配给 translateText 的函数调用 /translate 端点上的后端 API,并将响应捕获在全局上下文中:

    const translateText = async () => {
        setProcessing(true);
 
        fetch(appContext.config.translateService + "/translate?sourceLanguage=" +  
        appContext.sourceLanguage + "&targetLanguage=" + appContext.targetLanguage, {
            method: 'POST',
            body: appContext.transcribedText,
            headers: {"Content-Type": "text/plain"}
        })
            .then(response => response.json())
            .then(data => appContext.setTranslatedText(data[0].translations[0].text))
            .catch(() => window.alert("Failed to translate message"))
            .finally(() => setProcessing(false))
    } 

到目前为止,UI 应该看起来很熟悉了。与所有步骤一样,基于网格的布局显示各个元素。在这种情况下,它是一个显示要翻译文本的语言的选择列表、一个启动翻译的按钮、一个显示翻译结果的文本框,以及用于在向导中前后导航的按钮:

    return <div>
        <Grid
            container
            spacing={2}
            rowGap={2}
            justifyContent="center"
            alignItems="center">
            <Grid item xs={12}>
                <h3>Translate your message</h3>
            </Grid>
            <Grid item md={3} xs={0}/>
            <Grid md={6} xs={12}>
                <FormControl fullWidth>
                    <InputLabel id="outputLanguage-label">Translated Language</InputLabel>
                    <Select
                        labelId="outputLanguage-label"
                        value={appContext.targetLanguage}
                        label="Translated Language"
                        onChange={handleOutputLanguageChange}
                    >
                        <MenuItem value={"en-US"}>English</MenuItem>
                        <MenuItem value={"de-DE"}>German</MenuItem>
                        <MenuItem value={"ja-JP"}>Japanese</MenuItem>
                    </Select>
                </FormControl>
            </Grid>
            <Grid item md={3} xs={0}/>
            <Grid item md={4} xs={0}/>
            <Grid item md={4} xs={12}>
                <Button
                    variant="contained"
                    className={"fullWidth"}
                    onClick={translateText}>
                    Translate
                </Button>
            </Grid>
            <Grid item md={4} xs={0}/>
            <Grid item md={3} xs={0}/>
            <Grid item md={6} xs={12}>
                <TextField
                    rows={10}
                    multiline={true}
                    fullWidth={true}
                    disabled={true}
                    value={appContext.translatedText}
                />
            </Grid>
            <Grid item md={3} xs={0}/>
            <Grid item md={3} xs={0}/>
            <Grid item md={3} xs={12}>
                <Button
                    variant="contained"
                    className={"fullWidth"}
                    onClick={() => {
                        appContext.setTranslatedText("");
                        params.previousStep();
                    }}
                    disabled={processing}>
                    < Transcribe
                </Button>
            </Grid>
            <Grid item md={3} xs={12}>
                <Button
                    variant="contained"
                    className={"fullWidth"}
                    onClick={() => params.nextStep()}
                    disabled={!appContext.translatedText || processing}>
                    Speak >
                </Button>
            </Grid>
            <Grid item md={3} xs={0}/>
        </Grid>
    </div>;
} 

将文本转换为语音

最后一步是将翻译的文本转换回语音。Speak 组件实现了这一点:

export const Speak = (params) => {
    const appContext = React.useContext(AppContext); 

两个状态变量分别捕获后端 API 调用的状态和 API 返回的音频文件的 Blob 位置:

    const [processing, setProcessing] = useState(false);
    const [audioBlob, setAudioBlob] = useState(null); 

分配给 convertText 的函数调用后端 API,传递翻译的文本,并保存返回的音频文件:

    const convertText = async () => {
        setProcessing(true);
        setAudioBlob(null);
 
        fetch(appContext.config.translateService + "/speak?targetLanguage=" + 
                                                    appContext.targetLanguage, {
            method: 'POST',
            body: appContext.translatedText,
            headers: {"Content-Type": "text/plain"}
        })
            .then(response => response.blob())
            .then(blob => {
                const objectURL = URL.createObjectURL(blob);
                setAudioBlob(objectURL);
            })
            .catch(() => window.alert("Failed to convert message to speech"))
            .finally(() => {
                setProcessing(false);
            })
    }

同时,UI 又是另一个基于网格的布局。它定义了一个用于启动文本处理的按钮、一个用于在浏览器中播放生成的音频文件的音频播放器元素,以及一个用于在向导中后退的按钮:

    return <div>
        <Grid
            container
            spacing={2}
            rowGap={2}
            justifyContent="center"
            alignItems="center">
            <Grid item xs={12}>
                <h3>Convert your message to speech</h3>
            </Grid>
            <Grid item md={4} xs={0}/>
            <Grid item md={4} xs={12}>
                <Button
                    variant="contained"
                    className={"fullWidth"}
                    disabled={processing}
                    onClick={convertText}>
                    Speak
                </Button>
            </Grid>
            <Grid item md={4} xs={0}/>
            <Grid item md={4} xs={0}/>
            <Grid item md={4} xs={12}>
                {audioBlob &&
                    <audio controls="controls" src={audioBlob} type="audio/wav"/>}
            </Grid>
            <Grid item md={4} xs={0}/>
            <Grid item md={3} xs={0}/>
            <Grid item md={3} xs={12}>
                <Button
                    variant="contained"
                    className={"fullWidth"}
                    onClick={() => {
                        setAudioBlob(null);
                        params.previousStep()
                    }}
                    disabled={processing}>
                    < Translate
                </Button>
            </Grid>
            <Grid item md={6} xs={0}/>
        </Grid>
    </div>;
}

创建配置文件

如前所述,保存在 public/config.json 中的 JSON 配置文件定义了后端 API 的 URL。此文件包含一个名为 translateService 的属性,该属性定义了 API URL。

此值是 Azure Functions 项目的本地开发实例默认公开的 URL。本文系列的 下一部分 将创建此项目。

{
  "translateService": "https://:7071/api"
}

构建和发布 Web 应用程序

React 应用程序纯粹是前端代码,因此非常适合作为 Azure 静态 Web 应用程序 进行托管。 Microsoft 的文档 提供了有关创建新的静态 Web 应用程序的详细说明。

与 Microsoft 教程唯一的区别是,我们的应用程序将使用 GitHub Actions 来部署代码,而不是让 Web 应用程序从 Git 中消耗更改。要使用 GitHub Actions,请将 **Source** 设置为 **Other**。

创建 Web 应用程序后,记下部署令牌。我们希望通过点击 **Manage deployment token** 按钮来显示此令牌:

要从 GitHub 发布 Web 应用程序,请将以下工作流文件添加到 .github/workflows/node.yaml

name: Node.js Build
'on':
  workflow_dispatch: {}
  push: {}
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
        with:
          fetch-depth: '0'
      - uses: actions/setup-node@v2
        with:
          node-version: lts/* 

此代码会更新 config 文件,以反映发布到 Azure 时的后端 API URL。正如我们将在本系列下一篇文章中演示的那样,后端 API 通过 自带函数 功能链接到静态 Web 应用程序。此功能可确保后端 API 和前端 Web 应用程序共享相同的域名。因此,前端代码可以通过相对路径 /api 访问后端 API:

      - name: Update config.json
        run: |-
          echo "`jq '.translateService="/api"' public/config.json`" > public/config.json

Azure 提供了一个名为 Azure/static-web-apps-deploy@v1 的 GitHub Action 来构建和部署 Web 应用程序。注意 azure_static_web_apps_api_token 属性,它是一个 secret,包含静态 Web 应用程序公开的部署令牌:

      - name: Build And Deploy
        id: builddeploy
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          action: 'upload'
          app_location: '/'
          api_location: '' 

有了这个工作流,GitHub 会在每次提交到 Git 存储库时构建和发布 Web 应用程序,从而创建一个持续集成和持续部署 (CI/CD) 管道。

Web 应用程序允许用户在没有后端 API 的情况下录制和播放语音。但是,第一次调用后端 API 将会失败,因为它尚不存在。本教程系列的下一部分将创建初始的后端 API 端点。

后续步骤

本教程构建了 Web 应用程序,该应用程序提供了一个向导式流程,用于在浏览器中录制语音、转录、翻译,然后将结果转换回语音。它还通过 GitHub Actions 建立了一个 CI/CD 管道,将代码作为 Azure 静态 Web 应用程序进行部署。

结果是一个完整的 Web 应用程序,但它缺少完成向导所需的后端 API。请继续阅读本系列三部分教程的 第二部分,以构建初始后端 API 并将其发布为 Azure 函数应用。

要了解有关 Speech 服务的更多信息并浏览 API 参考,请查看我们的 Speech 服务文档

© . All rights reserved.