在本教程中,我们将结合 Vue.js、three.js 和 LUIS(认知服务) 创建一个语音控制的 Web 可视化。
但首先,简要介绍一下背景
为什么我们需要使用语音识别? 这种技术可以解决什么问题?
前段时间,我在芝加哥乘坐公交车。 司机没有看到我,就把门关在了我的手腕上。 当他开始开车时,我听到手腕发出“砰”的一声,其他乘客开始喊叫,他才最终停了下来,但在我手腕上的几条肌腱已经被撕裂了。
我原本应该休息一段时间,但当时博物馆员工都是合同制,没有真正的医疗保险。 我本来赚的就不多,所以休息根本不是选项。 我强忍着疼痛继续工作。 最后,我的手腕状况开始恶化。 甚至刷牙都会非常疼痛。 语音转文字当时还不是像现在这样无处不在的技术,最好的工具是 Dragon。 效果还不错,但学习起来很痛苦,而且我仍然需要经常用手,因为它经常出错。 那是十年前的事了,我相信这些技术自那以后已经有了很大的改进。 我的手腕也在这段时间里有了很大的改善。
整个经历让我对语音控制技术产生了浓厚的兴趣。 如果我们可以通过说话来控制 Web 行为,我们能做些什么呢? 我决定使用 LUIS 进行实验,这是一种基于机器学习的服务,可以利用自定义模型构建自然语言,并且可以不断改进。 我们可以在应用程序、机器人和 物联网 设备中使用它。 通过这种方式,我们可以创建一个对任何声音做出响应的可视化效果,并且它可以通过学习不断改进自身。

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

设置 LUIS
我们将获得 Azure 的免费试用帐户,然后 转到门户网站。 我们将选择认知服务。
在选择 **新建 → AI/机器学习** 后,我们将选择“语言理解”(或 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.js、 sine-waves 和 axios。 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,
},
intent
和 intensity
将分别存储 App、强度和意图。 score
将存储我们的置信度(这是一个 0 到 100 的分数,衡量模型认为它可以对输入进行排名的程度)。
对于 uiState
,我们有三种不同的状态
idle
– 等待用户输入listening
– 听到用户输入fetching
– 从 API 获取用户数据
zoom
和 counter
都将用于更新数据可视化效果。
现在,在 操作 中,我们将 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 }) => {
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) => {
state.uiState = status
},
setIntent: (state, status) => {
state.intent = status
},
这一切都很简单。 我们传递了状态,以便我们可以为每次出现更新它,除了 Intensity,它会根据情况递增或递减计数器。 我们将在下一节中使用该计数器来更新可视化效果。
.then(({ data }) => {
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 => {
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()
来启动所有操作。
- 再次,如果您想使用代码,请访问 仓库
- 您可以通过获取 免费 Azure 帐户 来创建自己的模型
- 您还需要查看 LUIS(认知服务)
看到您可以为网页创建哪些无需任何手动操作即可控制的东西,真是很有趣。这带来了很多机会!
感谢您分享这段代码片段
太棒了!网站上线了吗?可以分享链接吗?
我认为我找到了一个可用的链接
https://sdras.github.io/three-vue-pattern/
FYI,它似乎在 Firefox 中不起作用(
TypeError: oe is not a constructor
)。