使用 Vue.js 和机器学习构建语音控制的 Web 可视化

Avatar of Sarah Drasner
Sarah Drasner

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 $200 免费信用额度!

在本教程中,我们将结合 Vue.js、three.js 和 LUIS(认知服务) 创建一个语音控制的 Web 可视化。

但首先,简要介绍一下背景

为什么我们需要使用语音识别? 这种技术可以解决什么问题?

前段时间,我在芝加哥乘坐公交车。 司机没有看到我,就把门关在了我的手腕上。 当他开始开车时,我听到手腕发出“砰”的一声,其他乘客开始喊叫,他才最终停了下来,但在我手腕上的几条肌腱已经被撕裂了。

我原本应该休息一段时间,但当时博物馆员工都是合同制,没有真正的医疗保险。 我本来赚的就不多,所以休息根本不是选项。 我强忍着疼痛继续工作。 最后,我的手腕状况开始恶化。 甚至刷牙都会非常疼痛。 语音转文字当时还不是像现在这样无处不在的技术,最好的工具是 Dragon。 效果还不错,但学习起来很痛苦,而且我仍然需要经常用手,因为它经常出错。 那是十年前的事了,我相信这些技术自那以后已经有了很大的改进。 我的手腕也在这段时间里有了很大的改善。

整个经历让我对语音控制技术产生了浓厚的兴趣。 如果我们可以通过说话来控制 Web 行为,我们能做些什么呢? 我决定使用 LUIS 进行实验,这是一种基于机器学习的服务,可以利用自定义模型构建自然语言,并且可以不断改进。 我们可以在应用程序、机器人和 物联网 设备中使用它。 通过这种方式,我们可以创建一个对任何声音做出响应的可视化效果,并且它可以通过学习不断改进自身。

GitHub 仓库

实时演示

preview of three-vue-pattern with different moods

以下是我们要构建内容的概览

birds-eye view of LUIS demo

设置 LUIS

我们将获得 Azure 的免费试用帐户,然后 转到门户网站。 我们将选择认知服务。

在选择 **新建 → AI/机器学习** 后,我们将选择“语言理解”(或 LUIS)。

new cognitive services

然后我们将选择我们的名称和资源组。

create new luis

我们将从下一个屏幕中收集密钥,然后转到 LUIS 仪表盘

训练这些机器实际上很有趣! 我们将设置一个新的应用程序并创建一些意图,这些意图是基于给定条件想要触发的结果。 以下是此演示中的示例

您可能会注意到这里有一个命名方案。 我们这样做是为了更轻松地对意图进行分类。 我们将首先确定情绪,然后监听强度,因此初始意图以 App(这些主要在 App.vue 组件中使用)或 Intensity 为前缀。

如果我们深入研究每个特定意图,我们会看到模型是如何训练的。 我们有一些意思大致相同的类似短语

您可以看到我们有很多用于训练的同义词,但我们也有顶部的“训练”按钮,当我们准备好开始训练模型时可以使用。 我们点击该按钮,收到成功通知,然后就可以发布了。 😀

设置 Vue

我们将通过 Vue CLI 创建一个非常标准的 Vue.js 应用程序。 首先,我们运行

vue create three-vue-pattern
# then select Manually...

Vue CLI v3.0.0

? Please pick a preset:
  default (babel, eslint)
❯ Manually select features

# Then select the PWA feature and the other ones with the spacebar
? Please pick a preset: Manually select features
? Check the features needed for your project:
  ◉ Babel
  ◯ TypeScript
  ◯ Progressive Web App (PWA) Support
  ◯ Router
  ◉ Vuex
  ◉ CSS Pre-processors
  ◉ Linter / Formatter
  ◯ Unit Testing
  ◯ E2E Testing

? Pick a linter / formatter config:
  ESLint with error prevention only
  ESLint + Airbnb config
❯ ESLint + Standard config
  ESLint + Prettier

? Pick additional lint features: (Press <space> to select, a to toggle all, i to invert selection)
❯ ◉ Lint on save
  ◯ Lint and fix on commit

Successfully created project three-vue-pattern.
Get started with the following commands:

$ cd three-vue-pattern
$ yarn serve</space>

这将为我们启动一个服务器,并提供一个典型的 Vue 欢迎屏幕。 我们还将在应用程序中添加一些依赖项: three.jssine-wavesaxios。 three.js 将帮助我们创建 WebGL 可视化效果。 sine-waves 为我们提供了加载程序的不错的画布抽象。 axios 将为我们提供一个非常不错的 HTTP 客户端,以便我们可以调用 LUIS 进行分析。

yarn add three sine-waves axios

设置我们的 Vuex 商店

现在我们有了可用的模型,让我们使用 axios 获取它,并将其导入我们的 Vuex 商店。 然后,我们可以将信息传播到所有不同的组件。

state 中,我们将存储我们需要的内容

state: {
   intent: 'None',
   intensity: 'None',
   score: 0,
   uiState: 'idle',
   zoom: 3,
   counter: 0,
 },

intentintensity 将分别存储 App、强度和意图。 score 将存储我们的置信度(这是一个 0 到 100 的分数,衡量模型认为它可以对输入进行排名的程度)。

对于 uiState,我们有三种不同的状态

  • idle – 等待用户输入
  • listening – 听到用户输入
  • fetching – 从 API 获取用户数据

zoomcounter 都将用于更新数据可视化效果。

现在,在 操作 中,我们将 uiState(在 mutation 中)设置为 fetching,并将使用 axios 使用设置 LUIS 时收到的生成密钥调用 API。

getUnderstanding({ commit }, utterance) {
 commit('setUiState', 'fetching')
 const url = `https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/4aba2274-c5df-4b0d-8ff7-57658254d042`

 https: axios({
   method: 'get',
   url,
   params: {
     verbose: true,
     timezoneOffset: 0,
     q: utterance
   },
   headers: {
     'Content-Type': 'application/json',
     'Ocp-Apim-Subscription-Key': ‘XXXXXXXXXXXXXXXXXXX'
   }
 })

然后,一旦完成,我们就可以获取排名最高的意图,并将其存储在我们的 state 中。

我们还需要创建一些 mutation,这些 mutation 可以用于更改状态。 我们将在操作中使用这些 mutation。 在即将发布的 Vue 3.0 中,这将得到简化,因为 mutation 将被移除。

newIntent: (state, { intent, score }) =&gt; {
 if (intent.includes('Intensity')) {
   state.intensity = intent
   if (intent.includes('More')) {
     state.counter++
   } else if (intent.includes('Less')) {
     state.counter--
   }
 } else {
   state.intent = intent
 }
 state.score = score
},
setUiState: (state, status) =&gt; {
 state.uiState = status
},
setIntent: (state, status) =&gt; {
 state.intent = status
},

这一切都很简单。 我们传递了状态,以便我们可以为每次出现更新它,除了 Intensity,它会根据情况递增或递减计数器。 我们将在下一节中使用该计数器来更新可视化效果。

.then(({ data }) =&gt; {
 console.log('axios result', data)
 if (altMaps.hasOwnProperty(data.query)) {
   commit('newIntent', {
     intent: altMaps[data.query],
     score: 1
   })
 } else {
   commit('newIntent', data.topScoringIntent)
 }
 commit('setUiState', 'idle')
 commit('setZoom')
})
.catch(err =&gt; {
 console.error('axios error', err)
})

在此操作中,我们将提交我们刚刚介绍的 mutation,或者如果出现错误,则记录错误。

该逻辑的工作方式是,用户将进行初始录制来说出他们的感受。 他们将点击一个按钮来启动一切。 可视化效果将出现,此时,应用程序将持续监听用户说出“更少”或“更多”,以控制返回的可视化效果。 让我们设置应用程序的其余部分。

设置应用程序

App.vue 中,我们将根据是否已指定情绪,显示页面中间的两个不同组件。

<app-recordintent v-if="intent === 'None'">
<app-recordintensity v-if="intent !== 'None'" :emotion="intent"></app-recordintensity></app-recordintent>

这两者都将向观看者显示信息,以及当 UI 处于监听状态时的 SineWaves 组件。

应用程序的基础是可视化效果将显示的位置。 它将使用不同的道具根据情绪显示。 以下列举两个例子

<app-base v-if="intent === 'Excited'" :t-config.a="1" :t-config.b="200">
<app-base v-if="intent === 'Nervous'" :t-config.a="1" :color="0xff0000" :wireframe="true" :rainbow="false" :emissive="true"></app-base></app-base>

设置数据可视化

我希望使用万花筒般的图像作为可视化效果,在搜索了一番后,我 找到了这个仓库。 它的工作原理是,一个形状在空间中旋转,这将把图像分解并显示其各个部分,就像万花筒一样。 现在,这听起来可能很棒,因为(耶!)工作已经完成,对吧?

不幸的是,并非如此。

为了使它正常工作,需要进行大量的更改,实际上,这是一项巨大的工程,即使最终的视觉表现看起来与最初的类似。

  • 由于我们需要在决定更改可视化效果时拆除它,我不得不将现有代码转换为使用 bufferArrays,这对于此目的而言性能更高。
  • 原始代码是一个很大的块,所以我将一些函数拆分成组件中的较小方法,以便更容易阅读和维护。
  • 因为我们想要动态更新内容,我不得不将一些项目存储为组件中的数据,并最终存储为它将从父级接收的道具。 我还包括了一些不错的默认值(excited 是所有默认值的外观)。
  • 我们使用来自 Vuex 状态的计数器来更新相机放置位置相对于物体的距离,这样我们就可以看到更多或更少的物体,从而使其变得更复杂或更简单。

为了根据配置改变外观,我们将创建一些道具

props: {
 numAxes: {
   type: Number,
   default: 12,
   required: false
 },
 ...
 tConfig: {
   default() {
     return {
       a: 2,
       b: 3,
       c: 100,
       d: 3
     }
   },
   required: false
 }
},

我们在创建形状时会使用它们

createShapes() {
 this.bufferCamera.position.z = this.shapeZoom

 if (this.torusKnot !== null) {
   this.torusKnot.material.dispose()
   this.torusKnot.geometry.dispose()
   this.bufferScene.remove(this.torusKnot)
 }

 var shape = new THREE.TorusKnotGeometry(
     this.tConfig.a,
     this.tConfig.b,
     this.tConfig.c,
     this.tConfig.d
   ),
   material
 ...
 this.torusKnot = new THREE.Mesh(shape, material)
 this.torusKnot.material.needsUpdate = true

 this.bufferScene.add(this.torusKnot)
},

正如我们之前提到的,现在已将其拆分为单独的方法。我们还将创建另一个启动动画的方法,该方法也会在每次更新时重新启动。动画使用 requestAnimationFrame

animate() {
 this.storeRAF = requestAnimationFrame(this.animate)

 this.bufferScene.rotation.x += 0.01
 this.bufferScene.rotation.y += 0.02

 this.renderer.render(
   this.bufferScene,
   this.bufferCamera,
   this.bufferTexture
 )
 this.renderer.render(this.scene, this.camera)
},

我们将创建一个名为 shapeZoom 的计算属性,它将返回存储中的缩放比例。如您所知,这将随着用户声音变化强度而更新。

computed: {
 shapeZoom() {
   return this.$store.state.zoom
 }
},

然后,我们可以使用观察者来查看缩放级别是否更改,并取消动画、重新创建形状并重新启动动画。

watch: {
 shapeZoom() {
   this.createShapes()
   cancelAnimationFrame(this.storeRAF)
   this.animate()
 }
},

在数据中,我们还存储了一些在实例化 three.js 场景时需要的东西,最值得注意的是确保相机完全居中。

data() {
 return {
   bufferScene: new THREE.Scene(),
   bufferCamera: new THREE.PerspectiveCamera(75, 800 / 800, 0.1, 1000),
   bufferTexture: new THREE.WebGLRenderTarget(800, 800, {
     minFilter: THREE.LinearMipMapLinearFilter,
     magFilter: THREE.LinearFilter,
     antialias: true
   }),
   camera: new THREE.OrthographicCamera(
     window.innerWidth / -2,
     window.innerWidth / 2,
     window.innerHeight / 2,
     window.innerHeight / -2,
     0.1,
     1000
   ),

此演示还有更多内容,如果您想探索仓库或使用自己的参数自行设置,可以查看。init 方法的作用正如您所想:它初始化整个可视化。如果您查看源代码,我会对许多关键部分进行注释。还有一个更新几何体的方法,它被称为 - 您猜对了 - updateGeometry。您可能还会注意到其中有很多变量。这是因为在这种可视化中,重复使用变量是很常见的。我们通过在 mounted() 生命周期钩子中调用 this.init() 来启动所有操作。

看到您可以为网页创建哪些无需任何手动操作即可控制的东西,真是很有趣。这带来了很多机会!