在 Web 上构建您自己的 AI 驱动的虚拟助手

2022-09-22  本文已影响0人  程序员DS

使用 Typescript 和 React 构建由 Houndify API 提供支持的无障碍 Alexa 克隆

文章来源:https://cs310.hashnode.dev/build-your-own-ai-powered-virtual-assistant-on-the-web-part1

配置

我们将使用create-react-appTypescript 制作这个项目。

npx create-react-app my-assistant --template typescript

完成后,让我们安装开始所需的库。

npm i houndify jotai react-feather

这将为我们提供以下信息:

我们还将使用 Sass 为我们的组件设置样式,并使用npm i -D sass.

创建服务器

我们首先需要一个服务器来验证对 Houndify API 的请求。Houndify SDK 提供了一个HoundifyExpress对象。这将附加到 Express 服务器并在其中添加我们需要的路由server.js。添加以下代码来设置服务器:

const express = require("express");
const path = require("path");
const houndifyExpress = require("houndify").HoundifyExpress;
const app = express();
require("dotenv").config({ path: "./.env" });

const PORT = process.env.PORT || 8080;

app.use(express.static(path.join(__dirname, "build")));

app.get("/", function (req, res) {
    res.sendFile(path.join(__dirname, "build", "index.html"));
});

app.get(
    "/houndifyAuth",
    houndifyExpress.createAuthenticationHandler({
        clientId: process.env.HOUNDIFY_CLIENT_ID,
        clientKey: process.env.HOUNDIFY_CLIENT_KEY,
    })
);

app.listen(PORT, () => console.log(`Listening on port ${PORT}`));

请注意,它还提供目录中的index.html文件build。因此,如果我们要部署此应用程序,我们将首先运行npm run build以生成静态文件。然后运行node server.js以启动服务器以从那里访问应用程序。

另外,请注意我们正在使用的环境变量,在 中配置.env,我们尚未添加。我们也没有客户端 ID 和客户端密钥。所以让我们开始使用 Houndify API。

使用 API 仪表板

在Houndify免费创建一个开发者帐户并创建一个新客户端。一旦你在那里,输入应用程序的名称和类型,如下所示:

现在您应该会看到一个页面,询问您要为应用程序启用哪些域。我们将使用以下十个域(全部来自第一页):

如果您看一下,其中一些域需要客户端集成。客户端必须应用额外的逻辑来集成功能。我们以后可以随时添加其他域,但现在我们不需要它们。

单击显示概述和 API 密钥的按钮:

一旦你在那里,复制客户端 ID 和客户端密钥并将它们添加到以下.env文件中:

HOUNDIFY_CLIENT_ID={YOUR_CLIENT_ID}
HOUNDIFY_CLIENT_KEY={YOUR_CLIENT_SECRET}

现在我们准备开始构建前端。

创建语音请求

首先,打开src目录并删除以下我们不需要的文件:

然后,定义一个函数来初始化对 houndify API 的语音请求。它获取有关音频流的信息,以及捕获各种事件的处理程序。

// lib/initVoiceRequest.ts
import { RequestHandlers } from "./types";

export default function initVoiceRequest(
    recorder: any,
    conversationState: object,
    handlers: RequestHandlers
) {
    // @ts-ignore (2339)
    const voiceRequest = new window.Houndify.VoiceRequest({
        //Your Houndify Client ID
        clientId: "{YOUR_CLIENT_ID}",

        authURL: "/houndifyAuth",

        //REQUEST INFO JSON
        //See https://houndify.com/reference/RequestInfo
        requestInfo: {
            UserID: "test_user",
            //See https://www.latlong.net/ for your own coordinates
            Latitude: 37.388309,
            Longitude: -121.973968,
        },

        //Pass the current ConversationState stored from previous queries
        //See https://www.houndify.com/docs#conversation-state
        conversationState,

        //Sample rate of input audio
        sampleRate: recorder.sampleRate,

        //Enable Voice Activity Detection
        //Default: true
        enableVAD: true,

        //Partial transcript, response and error handlers
        onTranscriptionUpdate: handlers.onTranscriptionUpdate,
        onResponse: function (response: any, info: any) {
            recorder.stop();
            handlers.onResponse(response, info);
        },
        onError: function (err: any, info: any) {
            recorder.stop();
            handlers.onError(err, info);
        },
    });

    return voiceRequest;
}

让我们分解这段代码:

语音输入显示

VoiceInput.ts现在在目录中创建一个名为的src文件并添加以下代码:

import { useAtom } from "jotai";
import { useRef } from "react";
import { Mic, MicOff } from "react-feather";
import { recorderAtom, recordingAtom } from "./store";
import styles from "./VoiceInput.module.scss";

interface VoiceInputProps {
    transcription: string;
}

export default function VoiceInput({ transcription }: VoiceInputProps) {
    const [recorder] = useAtom(recorderAtom);
    const [recording] = useAtom(recordingAtom);

    const onClickMic = () => {
        if (recorder && recorder.isRecording()) {
            recorder.stop();
            return;
        }

        recorder.start();
    };

    const Icon = recording ? MicOff : Mic;

    return (
        <div className={styles.inputContainer}>
            <button
                type="button"
                title={`${recording ? "Stop" : "Start"} voice query`}
                onClick={onClickMic}
            >
                <Icon size={64} color="#343434" />
            </button>
            <div>
                <div className={styles.transcript}>{transcription}</div>
            </div>
        </div>
    );
}

该组件执行以下操作:

我们现在可以使用以下代码添加样式表:

$backgroundColor: #e8e1d3;
$complimentColor: #efe8e7;

.inputContainer {
    display: flex;
    flex-direction: column;
    align-items: center;
    width: 100%;

    button {
        border-radius: 50%;
        padding: 20px;
        background: transparent;
        border: 3px solid black;

        &:hover {
            cursor: pointer;
            background: $complimentColor;
        }
    }
}

.transcript {
    background-color: lighten($complimentColor, 5%);
    border-radius: 10px;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 30px;
    margin-top: 20px;
    flex: 1;
    min-width: 33.3vw;
    padding: 5px 10px;
}

我们可以atomssrc/lib/store.ts文件中定义我们的:

import { atom } from "jotai";

export const recorderAtom = atom<any>(null);
export const recordingAtom = atom(false);

构建 App 组件

将 的内容替换为src/App.tsx以下代码:

import { useCallback, useEffect, useRef, useState } from "react";
import styles from "./App.module.scss";
import initVoiceRequest from "./lib/initVoiceRequest";
import VoiceInput from "./VoiceInput";
import { useAtom } from "jotai";
import { recorderAtom, recordingAtom } from "./lib/store";

function App() {
    // Keep hold of the state
    const conversationState = useRef<any>(null);

    // Holds what the user is currently saying
    const [transcription, setTranscription] = useState("");

    // Any errors from the voice request will be stored here
    const [error, setError] = useState("");

    const [recorder, setRecorder] = useAtom(recorderAtom);
    const [recording, _setRecording] = useAtom(recordingAtom);

    const setRecording = (value: boolean) => {
        ...
        _setRecording(value);
    };
    ...

    return (
        <div className={styles.root}>
            <h1 className={styles.h1}>Assist310</h1>
            <VoiceInput transcription={transcription} />
            {error && <div className={styles.errorContainer}>{error}</div>}
        </div>
    );
}

export default App;

首先,定义我们的语音请求处理函数:

const onResponse = useCallback((response: any, info: any) => {
    if (response.AllResults && response.AllResults.length) {
        const result = response.AllResults[0];
        conversationState.current = result.ConversationState;
        handleResult(result);
        setTranscription("");
    }
}, []);

const onTranscriptionUpdate = useCallback((transcript: any) => {
    setTranscription(transcript.PartialTranscript);
}, []);

const onError = useCallback((error: any, info: any) => {
    setError(JSON.stringify(error));
}, []);

const handleResult = (result: any) => {
    // We'll add more here later
};

您可以在此处查看服务器响应格式。

我们现在可以创建一个安装效果,它将初始化 AudioRecorder 对象。然后,它将其事件绑定到一个初始化的 VoiceRequest 对象:

useEffect(() => {
    // @ts-ignore (2339)
    const audioRecorder = new window.Houndify.AudioRecorder();
    setRecorder(audioRecorder);

    let voiceRequest: any;

    audioRecorder.on("start", () => {
        setRecording(true);
        voiceRequest = initVoiceRequest(
            audioRecorder,
            conversationState.current,
            {
                onResponse,
                onTranscriptionUpdate,
                onError,
            }
        );
    });

    audioRecorder.on("data", (data: any) => {
        voiceRequest.write(data);
    });

    audioRecorder.on("end", () => {
        voiceRequest.end();
        setRecording(false);
    });

    audioRecorder.on("error", () => {
        voiceRequest.abort();
        setRecording(false);
    });
}, []);

第一次预览

首先,将我们的全局样式添加到src/index.css

body {
    margin: 0;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
        "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
        "Helvetica Neue", sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    width: 100%;
    height: 100%;
    position: fixed;
}

#root {
    width: 100%;
    height: 100%;
}

这使得我们的主要 div 元素占据了整个页面并将所有内容保存在一个视图中。

为了让我们的服务器create-react-app知道我们的其他服务器,我们需要在proxy我们的package.json文件中添加一个:

{
    ...
    "proxy": "http://localhost:8080",
    ...
}

现在在项目目录中打开两个终端窗口。node server.js在一个窗口和另一个窗口中运行npm start

如果一切正常,端口 8080 上的服务器应该记录 8080,而开发服务器应该记录自己的端口。在浏览器中打开后者,您应该会看到以下内容:

如果您尝试单击按钮并说出命令,客户端会将请求发送到服务器。同时,它显示部分转录。但之后不会有其他事情发生。因此,让我们向应用程序添加输出。

将文本添加到语音

我们希望显示来自服务器的书面响应或口头响应。让我们使用语音响应并使用Web Speech API实现 TTS,以便应用程序可以“说出”响应。

设置它就像首先将以下内容添加到App.tsx文件顶部一样简单:

const speech = new SpeechSynthesisUtterance();

// Set to your language code
speech.lang = "en";

const say = (text: string) => {
    speech.text = text;
    window.speechSynthesis.speak(speech);
};

然后将此代码添加到我们的handleResult函数中:

const handleResult = (result: any) => {
    // We'll add more here later
    say(result["SpokenResponseLong"]);
};

这就是一切!

音频反馈

但是现在,我们还可以在用户按下主按钮时添加一些听觉反馈。我们可以使用 Howler.js 库播放音频文件,我们可以使用以下行安装它:

npm i howler

然后使用函数创建一个文件lib/playSound.ts来播放任何音频源:

import { Howler, Howl, HowlOptions } from "howler";

export default function playSound(
    src: string,
    options?: Omit<HowlOptions, "src">
) {
    Howler.stop();
    new Howl({
        src,
        ...options,
    }).play();
}

现在在文件顶部导入我们需要的所有内容,App.tsx如下所示:

import { Howl } from "howler";

import startSound from "./audio/start.wav";
import stopSound from "./audio/stop.wav";
import playSound from "./lib/playSound";

const sources = {
    start: startSound,
    stop: stopSound,
};

您可以在此处下载麦克风提示音。将它们添加到src/audio具有各自名称的文件夹中:startstop.

然后将以下行添加到setRecording函数中:

playSound(sources[value ? "start" : "stop"]);

现在打开浏览器。您应该听到播放的音频和口头响应。

奖励 - 可视化音频输入

我们可以使用 Wave.js 库来可视化来自用户麦克风的音频,并使用以下行安装它:

npm i https://github.com/WoolDoughnut310/Wave.js

确保从我的仓库而不是 NPM 安装它。这是由于我对其进行了细微的更改以使其与我们的 AudioRecorder 对象一起使用。我已经提出了拉取请求,但我仍在等待图书馆作者的回复。

打开src/VoiceInput.tsx并添加以下导入语句:

import { Wave } from "@foobar404/wave";

然后将以下代码添加到onClickMic我们之前创建的函数的末尾:

recorder.on("start", () => {
    if (canvasEl.current) {
        let wave = new Wave(
            {
                source: recorder.source as MediaElementAudioSourceNode,
                context: recorder.audioCtx as AudioContext,
            },
            canvasEl.current
        );
        wave.addAnimation(
            new wave.animations.Lines({
                top: true,
            })
        );
    }
});

现在运行代码,我们应该与开始显示的演示相提并论。

最后的笔记

如果您有任何问题,请随时发表评论。如果您喜欢这个基于网络的 Jarvis 克隆,请务必与朋友分享。下一次,我们将添加额外的功能,例如音乐播放和歌曲识别。和以前一样,我将代码留在了我的GitHub 上,所以请尽情享受吧。

上一篇下一篇

猜你喜欢

热点阅读