Voice-Controlled Web Visualizations mit Vue.js und Machine Learning

Avatar of Sarah Drasner
Sarah Drasner am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

In diesem Tutorial kombinieren wir Vue.js, three.js und LUIS (Cognitive Services), um eine sprachgesteuerte Web-Visualisierung zu erstellen.

Aber zuerst ein wenig Kontext

Warum sollten wir Sprach Erkennung nutzen? Welches Problem könnte etwas wie das lösen?

Vor einiger Zeit stieg ich in Chicago in einen Bus. Der Busfahrer sah mich nicht und schloss die Tür auf mein Handgelenk. Als er losfuhr, hörte ich ein Knacken in meinem Handgelenk und er hielt irgendwann an, als die anderen Fahrgäste schrien, aber nicht bevor er ein paar Sehnen in meinem Arm riss.

Ich hätte mir frei nehmen sollen, aber typisch für Museumsangestellte zu dieser Zeit war ich auf Vertrag und hatte keine richtige Krankenversicherung. Ich verdiente sowieso nicht viel, also war eine Auszeit für mich einfach keine Option. Ich arbeitete durch den Schmerz. Und die Gesundheit meines Handgelenks begann sich zu verschlechtern. Es wurde sehr schmerzhaft, sogar Zähne zu putzen. Voice-to-Text war damals nicht die allgegenwärtige Technologie, die sie heute ist, und das beste Werkzeug, das damals verfügbar war, war Dragon. Es funktionierte einigermaßen, war aber ziemlich frustrierend zu lernen, und ich musste immer noch ziemlich oft meine Hände benutzen, weil es oft Fehler machte. Das war vor 10 Jahren, also bin ich sicher, dass sich diese spezielle Technologie seitdem erheblich verbessert hat. Mein Handgelenk hat sich in dieser Zeit auch erheblich verbessert.

Die gesamte Erfahrung weckte mein starkes Interesse an sprachgesteuerten Technologien. Was können wir tun, wenn wir das Verhalten des Webs zu unseren Gunsten steuern können, nur durch Sprechen? Als Experiment beschloss ich, LUIS zu verwenden, einen maschinellen Lernservice, um natürliche Sprache durch die Verwendung benutzerdefinierter Modelle aufzubauen, die sich kontinuierlich verbessern können. Wir können dies für Apps, Bots und IoT-Geräte verwenden. Auf diese Weise können wir eine Visualisierung erstellen, die auf jede Stimme reagiert – und sie kann sich selbst verbessern, indem sie unterwegs lernt.

GitHub-Repo

Live-Demo

preview of three-vue-pattern with different moods

Hier ist ein Überblick über das, was wir bauen

birds-eye view of LUIS demo

LUIS einrichten

Wir besorgen uns ein kostenloses Testkonto für Azure und gehen dann zum Portal. Wir wählen Cognitive Services aus.

Nachdem wir auf Neu → KI/Maschinelles Lernen geklickt haben, wählen wir „Language Understanding“ (oder LUIS) aus.

new cognitive services

Dann wählen wir unseren Namen und unsere Ressourcengruppe aus.

create new luis

Wir holen unsere Schlüssel vom nächsten Bildschirm ab und gehen dann zum LUIS Dashboard.

Es macht eigentlich wirklich Spaß, diese Maschinen zu trainieren! Wir richten eine neue Anwendung ein und erstellen einige Intents, die Ergebnisse, die wir basierend auf einer gegebenen Bedingung auslösen möchten. Hier ist das Beispiel aus dieser Demo.

Sie werden vielleicht bemerken, dass wir hier ein Namensschema haben. Das tun wir, um die Intents leichter kategorisieren zu können. Wir werden zuerst die Emotion ermitteln und dann auf die Intensität hören, daher sind die anfänglichen Intents mit entweder App (diese werden hauptsächlich in der App.vue Komponente verwendet) oder Intensity präfixiert.

Wenn wir uns jeden einzelnen Intent ansehen, sehen wir, wie das Modell trainiert wird. Wir haben einige ähnliche Phrasen, die ungefähr dasselbe bedeuten.

Sie sehen, wir haben viele Synonyme zum Trainieren, aber auch den „Train“-Button oben, wenn wir bereit sind, das Modell zu trainieren. Wir klicken auf diesen Button, erhalten eine Erfolgsmeldung und sind dann bereit zu veröffentlichen. 😀

Vue einrichten

Wir erstellen eine ziemlich Standard-Vue.js-Anwendung über die Vue CLI. Zuerst führen wir aus

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>

Dies startet einen Server für uns und zeigt einen typischen Vue-Willkommensbildschirm an. Wir werden auch einige Abhängigkeiten zu unserer Anwendung hinzufügen: three.js, sine-waves und axios. three.js hilft uns bei der Erstellung der WebGL-Visualisierung. sine-waves gibt uns eine schöne Canvas-Abstraktion für den Loader. axios bietet uns einen sehr guten HTTP-Client, damit wir Aufrufe an LUIS zur Analyse tätigen können.

yarn add three sine-waves axios

Unser Vuex-Store einrichten

Nachdem wir ein funktionierendes Modell haben, holen wir es mit axios und bringen es in unseren Vuex-Store. Dann können wir die Informationen an alle verschiedenen Komponenten verteilen.

Im state speichern wir, was wir brauchen werden.

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

intent und intensity speichern die App- bzw. Intensitäts- und Intents. Der score speichert unsere Konfidenz (einen Wert von 0 bis 100, der angibt, wie gut das Modell die Eingabe einordnen kann).

Für uiState haben wir drei verschiedene Zustände:

  • idle – wartet auf Benutzereingaben
  • listening – hört auf Benutzereingaben
  • fetching – holt Benutzerdaten von der API

Sowohl zoom als auch counter verwenden wir, um die Datenvisualisierung zu aktualisieren.

Nun stellen wir in den Actions den uiState (in einer Mutation) auf fetching ein und rufen die API mit axios unter Verwendung der generierten Schlüssel auf, die wir bei der Einrichtung von LUIS erhalten haben.

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'
   }
 })

Sobald wir das getan haben, können wir den am besten bewerteten Intent abrufen und ihn in unserem state speichern.

Wir müssen auch einige Mutationen erstellen, die wir zur Änderung des Zustands verwenden können. Wir werden diese in unseren Actions verwenden. In der kommenden Vue 3.0 wird dies vereinfacht, da Mutationen entfernt werden.

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
},

Das ist alles ziemlich unkompliziert. Wir übergeben den Zustand, damit wir ihn für jedes Vorkommen aktualisieren können – mit Ausnahme von Intensity, die den Zähler entsprechend erhöht und verringert. Diesen Zähler verwenden wir im nächsten Abschnitt, um die Visualisierung zu aktualisieren.

.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)
})

In dieser Action committen wir die gerade besprochenen Mutationen oder loggen einen Fehler, wenn etwas schiefgeht.

Die Logik funktioniert so, dass der Benutzer die erste Aufnahme macht, um zu sagen, wie er sich fühlt. Er drückt eine Taste, um alles zu starten. Die Visualisierung erscheint und zu diesem Zeitpunkt hört die App kontinuierlich darauf, dass der Benutzer weniger oder mehr sagt, um die zurückgegebene Visualisierung zu steuern. Lassen Sie uns den Rest der App einrichten.

Die App einrichten

In App.vue zeigen wir je nach dem, ob wir unsere Stimmung bereits angegeben haben, zwei verschiedene Komponenten in der Mitte der Seite an.

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

Beide zeigen dem Betrachter Informationen sowie eine SineWaves-Komponente an, während die Benutzeroberfläche im Lauschen-Zustand ist.

Die Basis der Anwendung ist dort, wo die Visualisierung angezeigt wird. Sie wird mit unterschiedlichen Props angezeigt, je nach Stimmung. Hier sind zwei Beispiele:

<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>

Die Datenvisualisierung einrichten

Ich wollte mit Kaleidoskop-ähnlichen Bildern für die Visualisierung arbeiten und habe nach einiger Suche dieses Repository gefunden. Die Funktionsweise ist, dass sich eine Form im Raum dreht und dies das Bild zerlegt und Teile davon wie ein Kaleidoskop anzeigt. Das klingt vielleicht fantastisch, denn (juhu!) die Arbeit ist getan, oder?

Leider nicht.

Es waren eine Reihe von größeren Änderungen nötig, um dies zum Laufen zu bringen, und es erwies sich sogar als ein gewaltiges Unterfangen, auch wenn der endgültige visuelle Ausdruck dem Original ähnelt.

  • Da wir die Visualisierung abbauen müssten, wenn wir sie ändern wollten, musste ich den bestehenden Code in bufferArrays umwandeln, die für diesen Zweck performanter sind.
  • Der ursprüngliche Code war ein einziger großer Block, also habe ich einige der Funktionen in kleinere Methoden auf der Komponente aufgeteilt, um das Lesen und Warten zu erleichtern.
  • Da wir Dinge dynamisch aktualisieren möchten, musste ich einige Elemente als Daten in der Komponente speichern und schließlich als Props, die sie vom Elternteil empfangen würde. Ich habe auch einige nette Standardwerte eingefügt (excited ist, wie alle Standardwerte aussehen).
  • Wir verwenden den Zähler aus dem Vuex-Status, um den Abstand der Kamera relativ zum Objekt zu aktualisieren, damit wir weniger oder mehr davon sehen und es dadurch komplexer und weniger komplex wird.

Um das Aussehen entsprechend den Konfigurationen zu ändern, erstellen wir einige Props.

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

Diese verwenden wir, wenn wir die Formen erstellen.

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)
},

Wie bereits erwähnt, ist dies jetzt in eine eigene Methode ausgelagert. Wir erstellen auch eine weitere Methode, die die Animation startet, die auch jedes Mal neu startet, wenn sie aktualisiert wird. Die Animation verwendet 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)
},

Wir erstellen eine berechnete Eigenschaft namens shapeZoom, die den Zoom aus dem Store zurückgibt. Wenn Sie sich erinnern, wird dieser aktualisiert, wenn die Stimme des Benutzers die Intensität ändert.

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

Wir können dann einen Watcher verwenden, um zu sehen, ob sich der Zoom-Level ändert, die Animation abzubrechen, die Formen neu zu erstellen und die Animation neu zu starten.

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

Im Datenbereich speichern wir auch einige Dinge, die wir für die Instanziierung der three.js-Szene benötigen – insbesondere die Gewährleistung, dass die Kamera exakt zentriert ist.

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
   ),

Es gibt noch mehr in dieser Demo, wenn Sie das Repository erkunden oder es selbst mit Ihren eigenen Parametern einrichten möchten. Die init-Methode macht, was Sie denken: Sie initialisiert die gesamte Visualisierung. Ich habe viele der wichtigsten Teile auskommentiert, wenn Sie den Quellcode einsehen. Es gibt auch eine weitere Methode, die die Geometrie aktualisiert und – Sie haben es erraten – updateGeometry genannt wird. Sie werden dort auch viele Variablen bemerken. Das liegt daran, dass es üblich ist, Variablen in dieser Art von Visualisierung wiederzuverwenden. Wir starten alles, indem wir this.init() im mounted()-Lifecycle-Hook aufrufen.

Es ist ziemlich unterhaltsam zu sehen, wie weit man mit Dingen für das Web kommen kann, die keine Handbewegungen zur Steuerung benötigen. Das eröffnet viele Möglichkeiten!